diff --git a/.github/workflows/build_bin.yml b/.github/workflows/build_bin.yml index ac1fa562..2f1781ed 100644 --- a/.github/workflows/build_bin.yml +++ b/.github/workflows/build_bin.yml @@ -87,7 +87,7 @@ jobs: --config $env:CONFIG_WIN ` --parallel - - name: Copy shaders + - name: Copy shaders and translations shell: pwsh run: | $output = "build/bin/$env:CONFIG_WIN" @@ -103,6 +103,17 @@ jobs: -Recurse ` -Force + New-Item ` + -ItemType Directory ` + -Force ` + -Path "$output/lang" | Out-Null + + Copy-Item ` + -Path "lang/*" ` + -Destination "$output/lang" ` + -Recurse ` + -Force + - name: Upload Windows build uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: @@ -238,10 +249,11 @@ jobs: "$PYTHON_HOME/bin/python" -c "from PIL import Image; print('Pillow:', Image.__version__)" - - name: Copy shaders + - name: Copy shaders and translations run: | - mkdir -p build/bin/shaders + mkdir -p build/bin/shaders build/bin/lang cp -R shaders/. build/bin/shaders/ + cp -R lang/. build/bin/lang/ - name: Upload Linux build uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 @@ -251,3 +263,80 @@ jobs: build/bin/ if-no-files-found: error retention-days: 14 + + + # ============================================================ + # Public download: release assets need no GitHub account + # ============================================================ + + publish: + name: Publish public download + needs: [windows, linux] + if: github.event_name != 'pull_request' + runs-on: ubuntu-24.04 + permissions: + contents: write + concurrency: + group: publish-${{ github.ref }} + cancel-in-progress: false + + steps: + - name: Download Windows build + uses: actions/download-artifact@v4 + with: + name: maszyna-windows-x64-${{ env.CONFIG_WIN }} + path: dist/maszyna-windows-x64 + + - name: Download Linux build + uses: actions/download-artifact@v4 + with: + name: maszyna-linux-x64-${{ env.CONFIG_LINUX }} + path: dist/maszyna-linux-x64 + + - name: Pack + run: | + cd dist + + find maszyna-linux-x64 -type f -exec sh -c 'file -b "$1" | grep -q "^ELF" && chmod +x "$1"' _ {} \; + if [ -d maszyna-linux-x64/linuxpython64/bin ]; then + chmod +x maszyna-linux-x64/linuxpython64/bin/* + fi + + (cd maszyna-windows-x64 && zip -qr9 ../maszyna-windows-x64.zip .) + tar -czf maszyna-linux-x64.tar.gz -C maszyna-linux-x64 . + ls -la + + - name: Publish rolling release of the branch + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + BRANCH: ${{ github.ref_name }} + SHA: ${{ github.sha }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + slug="$(printf '%s' "$BRANCH" | tr '/ ' '--' | tr -cd '[:alnum:]._-')" + tag="build-$slug" + base="${{ github.server_url }}/${{ github.repository }}/releases/download/$tag" + + notes="$(printf '%s\n' \ + "Automatic build of branch \`$BRANCH\`, commit $SHA, replaced by every push to the branch." \ + "" \ + "Download without a GitHub account:" \ + "- Windows: $base/maszyna-windows-x64.zip" \ + "- Linux: $base/maszyna-linux-x64.tar.gz" \ + "" \ + "Unpack over a MaSzyna installation (the game data isn't included). Build log: $RUN_URL")" + + gh release delete "$tag" --yes --cleanup-tag || true + gh release create "$tag" \ + dist/maszyna-windows-x64.zip \ + dist/maszyna-linux-x64.tar.gz \ + --target "$SHA" \ + --prerelease \ + --latest=false \ + --title "Build: $BRANCH" \ + --notes "$notes" + + echo "### Public download" >> "$GITHUB_STEP_SUMMARY" + echo "- Windows: $base/maszyna-windows-x64.zip" >> "$GITHUB_STEP_SUMMARY" + echo "- Linux: $base/maszyna-linux-x64.tar.gz" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/build_cleanup.yml b/.github/workflows/build_cleanup.yml new file mode 100644 index 00000000..8b20be0d --- /dev/null +++ b/.github/workflows/build_cleanup.yml @@ -0,0 +1,21 @@ +name: Remove public build of a deleted branch + +on: + delete: + +permissions: + contents: write + +jobs: + cleanup: + if: github.event.ref_type == 'branch' + runs-on: ubuntu-24.04 + steps: + - name: Delete the rolling release of the branch + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + BRANCH: ${{ github.event.ref }} + run: | + slug="$(printf '%s' "$BRANCH" | tr '/ ' '--' | tr -cd '[:alnum:]._-')" + gh release delete "build-$slug" --yes --cleanup-tag || true diff --git a/CMakeLists.txt b/CMakeLists.txt index 56cf1bce..62b41aa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,7 +77,7 @@ option(WITH_ZMQ "Compile with cppzmq" OFF) option(WITH_CRASHPAD "Compile with crashpad" OFF) option(WITH_DISCORD_RPC "Compile with DISCORD RICH PRESENCE" ON) option(USE_LTO "Use link-time optimization" OFF) -option(WITH_BETTER_RENDERER "Experimental multi-backend renderer based on NVRHI" ON) +option(WITH_BETTER_RENDERER "Experimental multi-backend renderer based on NVRHI" OFF) option(GENERATE_PDB "Generate executable with program debugging symbols" ON) option(ENABLE_MCC "Enable multicore compilation" ON) option(WITHDUMPGEN "Enable generating DMP files on crash" ON) @@ -95,6 +95,9 @@ set(SOURCES "model/Texture.cpp" "utilities/Timer.cpp" "world/Track.cpp" +"world/Road.cpp" +"world/Sweep.cpp" +"world/RoadPoint.cpp" "world/Traction.cpp" "world/TractionPower.cpp" "vehicle/Train.cpp" @@ -108,6 +111,8 @@ set(SOURCES "vehicle/DynObj.cpp" "EU07.cpp" "export_e3d_standalone.cpp" +"export_btf_standalone.cpp" +"nodebank_previews_standalone.cpp" "world/Event.cpp" "world/EvLaunch.cpp" "utilities/Float3d.cpp" @@ -167,15 +172,50 @@ set(SOURCES "application/driveruipanels.cpp" "input/editorkeyboardinput.cpp" "editor/editorSettings.cpp" +"editor/editorModelSets.cpp" +"editor/editorIncludeInfo.cpp" "editor/editorTerrain.cpp" +"editor/editorTrack.cpp" +"editor/editorRoad.cpp" +"editor/editorAlignment.cpp" +"editor/editorSpeed.cpp" +"editor/editorProfile.cpp" +"editor/editorGauge.cpp" +"editor/editorInfra.cpp" "editor/editorTerrainStreamer.cpp" +"editor/editorOrthophoto.cpp" +"editor/editorPreviews.cpp" +"editor/editorIcons.cpp" "application/editormode.cpp" +"application/editormodetrack.cpp" +"application/editormodetrackux.cpp" +"application/editormoderoad.cpp" +"application/editormodeprofile.cpp" +"application/editormodetextures.cpp" +"application/editormodegauge.cpp" +"application/editormodeinfra.cpp" +"application/editormodespeed.cpp" +"application/editormodejoints.cpp" +"application/editormodeparallel.cpp" +"application/editormodelineside.cpp" +"application/editormodevehicle.cpp" +"application/editormodeselection.cpp" +"application/editormodeviewport.cpp" +"application/editormodesignal.cpp" +"application/editormodesweep.cpp" +"application/editormodeturntable.cpp" +"application/editormodeselftest.cpp" "input/editormouseinput.cpp" "application/editoruilayer.cpp" "application/editoruipanels.cpp" "application/scenarioloadermode.cpp" "application/scenarioloaderuilayer.cpp" "scene/scenenodegroups.cpp" +"scene/scenelayers.cpp" +"scene/scenelayerssave.cpp" +"scene/sceneterrain.cpp" +"scene/heightmapterrain.cpp" +"scene/scenemodelentries.cpp" "simulation/simulationenvironment.cpp" "simulation/simulationstateserializer.cpp" "rendering/precipitation.cpp" @@ -205,6 +245,7 @@ set(SOURCES "${DEPS_DIR}/glad/src/glad.c" "${DEPS_DIR}/dds-ktx/src/dds-ktx.c" +"${DEPS_DIR}/zstd/zstd.c" "gl/shader.cpp" "gl/vao.cpp" @@ -223,6 +264,7 @@ set(SOURCES "imgui/imgui_demo.cpp" "imgui/imgui_draw.cpp" "imgui/imgui_widgets.cpp" +"imgui/imgui_tables.cpp" "imgui/imgui_impl_glfw.cpp" "imgui/ImGuizmo.cpp" @@ -235,6 +277,7 @@ set(SOURCES "launcher/keymapper.cpp" "launcher/textures_scanner.cpp" "launcher/scenery_scanner.cpp" +"launcher/scenery_wizard.cpp" "vr/vr_interface.cpp" ) @@ -293,6 +336,7 @@ if (WITH_OPENGL_MODERN) "rendering/opengl33skydome.cpp" "rendering/opengl33precipitation.cpp" "rendering/opengl33particles.cpp" + "rendering/opengl33terrain.cpp" "rendering/opengl33renderer.cpp") set(DEFINITIONS ${DEFINITIONS} "WITH_OPENGL_MODERN") endif() @@ -343,6 +387,14 @@ endif() configure_file("${CMAKE_SOURCE_DIR}/version_info.h.in" "${CMAKE_BINARY_DIR}/geninc/version_info.h") +# the icons of the editor are built into the executable: the png is written out as the bytes of an array, see editor/editorIcons.cpp +set(EDITOR_ICONS "${CMAKE_SOURCE_DIR}/editor/icons/editor_icons.png") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${EDITOR_ICONS}") +file(READ "${EDITOR_ICONS}" EDITOR_ICONS_HEX HEX) +string(REGEX REPLACE "(................................)" "\\1\n" EDITOR_ICONS_HEX "${EDITOR_ICONS_HEX}") +string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," EDITOR_ICONS_HEX "${EDITOR_ICONS_HEX}") +file(CONFIGURE OUTPUT "${CMAKE_BINARY_DIR}/geninc/editor_icons_png.inc" CONTENT "${EDITOR_ICONS_HEX}\n") + set (PREFIX "") set (SUFFIX "") @@ -412,6 +464,7 @@ target_include_directories(${PROJECT_NAME} PRIVATE "gl" "${DEPS_DIR}/glad/include" "${DEPS_DIR}/dds-ktx/include" + "${DEPS_DIR}/zstd" "${CMAKE_BINARY_DIR}/geninc" "${GLOBAL_INCLUDE_DIR}") @@ -449,8 +502,8 @@ if(NOT ${CMAKE_VERSION} VERSION_LESS "3.16.0") if (USE_PCH) target_precompile_headers(${PROJECT_NAME} PRIVATE "$<$:${CMAKE_CURRENT_SOURCE_DIR}/stdafx.h>") - set_source_files_properties("/glad/src/glad.c" "ref/dds-ktx/src/dds-ktx.c" "stb/stb_image.c" - "imgui/imgui.cpp" "imgui/imgui_demo.cpp" "imgui/imgui_draw.cpp" "imgui/imgui_widgets.cpp" "imgui/imgui_impl_glfw.cpp" "imgui/ImGuizmo.cpp" + set_source_files_properties("/glad/src/glad.c" "ref/dds-ktx/src/dds-ktx.c" "ref/zstd/zstd.c" "stb/stb_image.c" + "imgui/imgui.cpp" "imgui/imgui_demo.cpp" "imgui/imgui_draw.cpp" "imgui/imgui_widgets.cpp" "imgui/imgui_tables.cpp" "imgui/imgui_impl_glfw.cpp" "imgui/ImGuizmo.cpp" PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE) endif() endif() @@ -563,6 +616,19 @@ if (WIN32) target_link_libraries(${PROJECT_NAME} ws2_32) endif() +# http client of the editor's geoportal orthophoto layer: WinHTTP on Windows, libcurl elsewhere when available +if (WIN32) + target_link_libraries(${PROJECT_NAME} winhttp) +else() + find_package(CURL QUIET) + if (CURL_FOUND) + target_link_libraries(${PROJECT_NAME} CURL::libcurl) + target_compile_definitions(${PROJECT_NAME} PRIVATE EU07_WITH_CURL) + else() + message(STATUS "libcurl not found: the editor's orthophoto layer will only show cached tiles") + endif() +endif() + if (WITH_ZMQ) find_package(cppzmq REQUIRED) target_include_directories(${PROJECT_NAME} PRIVATE ${cppzmq_INCLUDE_DIR}) @@ -592,6 +658,13 @@ if (WITH_BETTER_RENDERER) target_link_libraries(${PROJECT_NAME} "betterRenderer") endif () +# yaml-cpp, for the descriptions of scenery templates edited in the scenery editor. +# the experimental renderer brings in the same copy on its own, when enabled +if (NOT TARGET yaml-cpp) + add_subdirectory("betterRenderer/thirdparty/yaml-cpp" "${CMAKE_CURRENT_BINARY_DIR}/yaml-cpp" EXCLUDE_FROM_ALL) +endif () +target_link_libraries(${PROJECT_NAME} yaml-cpp) + # Enable compiling with debug symbols if(GENERATE_PDB) @@ -625,4 +698,4 @@ if(ENABLE_AVX2) else() target_compile_options(${PROJECT_NAME} PRIVATE -mavx2) endif() -endif() \ No newline at end of file +endif() \ No newline at end of file diff --git a/EU07.cpp b/EU07.cpp index e1be8584..e0361829 100644 --- a/EU07.cpp +++ b/EU07.cpp @@ -33,6 +33,8 @@ Stele, firleju, szociu, hunter, ZiomalCl, OLI_EU and others #endif void export_e3d_standalone(std::string in, std::string out, int flags, bool dynamic); +int export_btf_standalone(std::string in, std::string out); +int generate_nodebank_previews(int Argc, char *Argv[]); #include #include @@ -98,6 +100,36 @@ int main(int argc, char *argv[]) int dynamic = std::stoi(std::string(argv[5])); export_e3d_standalone(in, out, flags, dynamic); } + // standalone generation of preview images of the node bank models: -generate-nodebank-previews [options] + else if (argc >= 2 && std::string(argv[1]) == "-generate-nodebank-previews") + { + auto const result{generate_nodebank_previews(argc, argv)}; + fflush(stdout); + fflush(stderr); + std::_Exit(result); + } + // standalone conversion of text terrain file to binary format: -btf -s [-o ] + else if (argc >= 4 && std::string(argv[1]) == "-btf") + { + std::string in; + std::string out; + for (int i = 2; i + 1 < argc; i += 2) + { + std::string const option(argv[i]); + if (option == "-s") + { + in = argv[i + 1]; + } + else if (option == "-o") + { + out = argv[i + 1]; + } + } + auto const result{export_btf_standalone(in, out)}; + fflush(stdout); + fflush(stderr); + std::_Exit(result); + } else { try diff --git a/application/application.cpp b/application/application.cpp index 7bdb37b1..8afd50db 100644 --- a/application/application.cpp +++ b/application/application.cpp @@ -74,9 +74,15 @@ extern WNDPROC BaseWindowProc; void focus_callback(GLFWwindow *window, int focus) { + ui_layer::focus_callback(focus); Application.on_focus_change(focus != 0); } +void cursor_enter_callback(GLFWwindow *window, int entered) +{ + ui_layer::cursor_enter_callback(entered); +} + void framebuffer_resize_callback(GLFWwindow *, int w, int h) { Global.fb_size = glm::ivec2(w, h); @@ -90,6 +96,7 @@ void window_resize_callback(GLFWwindow *, int w, int h) void cursor_pos_callback(GLFWwindow *window, double x, double y) { + ui_layer::cursor_pos_callback(x, y); Global.cursor_pos = glm::ivec2(x, y); Application.on_cursor_pos(x, y); } @@ -1126,6 +1133,8 @@ int eu07_application::init_settings(int Argc, char *Argv[]) } // process command line arguments + bool sceneryfromcommandline{false}; + bool editwithoutscenery{false}; for (int i = 1; i < Argc; ++i) { @@ -1136,6 +1145,7 @@ int eu07_application::init_settings(int Argc, char *Argv[]) if (i + 1 < Argc) { Global.SceneryFile = ToLower(Argv[++i]); + sceneryfromcommandline = true; } } else if (token == "-v") @@ -1145,6 +1155,19 @@ int eu07_application::init_settings(int Argc, char *Argv[]) Global.local_start_vehicle = ToLower(Argv[++i]); } } + else if (token == "-edit") + { + // edit session: the scenery is loaded without player vehicle, straight into the editor + Global.editor_session = true; + Global.local_start_vehicle = "ghostview"; + if (i + 1 < Argc && Argv[i + 1][0] != '-') + { + Global.SceneryFile = ToLower(Argv[++i]); + sceneryfromcommandline = true; + } + else + editwithoutscenery = true; + } else if (token == "-seed") { // a number is the seed; anything else is a word that stands for one. said in the @@ -1159,10 +1182,13 @@ int eu07_application::init_settings(int Argc, char *Argv[]) { std::cout << "usage: " << std::string(Argv[0]) << " [-s sceneryfilepath]" << " [-v vehiclename]" + << " [-edit [sceneryfilepath]]" << " [-seed number|word]" << std::endl; return -1; } } + if (editwithoutscenery && false == sceneryfromcommandline) + Global.SceneryFile.clear(); return 0; } @@ -1332,6 +1358,7 @@ void eu07_application::init_callbacks() glfwSetScrollCallback(window, scroll_callback); glfwSetCharCallback(window, char_callback); glfwSetWindowFocusCallback(window, focus_callback); + glfwSetCursorEnterCallback(window, cursor_enter_callback); } int eu07_application::init_ogl() diff --git a/application/drivermode.cpp b/application/drivermode.cpp index b5b78634..b55d09c2 100644 --- a/application/drivermode.cpp +++ b/application/drivermode.cpp @@ -255,6 +255,19 @@ bool driver_mode::update() // variable step simulation time routines + if (false == Global.editor_enter_vehicle.empty()) + { + auto const name{std::move(Global.editor_enter_vehicle)}; + Global.editor_enter_vehicle.clear(); + if (auto *dynamic{simulation::Vehicles.find(name)}) + { + if (false == FreeFlyModeFlag) + InOutKey(); + m_relay.post(user_command::entervehicle, 0.0, simulation::Train ? simulation::Train->id() : 0, GLFW_PRESS, 0, dynamic->GetPosition(), &dynamic->name()); + change_train = dynamic->name(); + } + } + if (!change_train.empty()) { TTrain *train = simulation::Trains.find(change_train); @@ -412,6 +425,14 @@ bool driver_mode::update() simulation::is_ready = simulation::is_ready || (simulation::Train != nullptr && simulation::Train->is_cab_initialized) || Global.local_start_vehicle == "ghostview"; + if (m_editorpending) + { + // edit session starts in the editor. the switch is made after the initial update on purpose, so the + // simulation state is set up the same way as when the editor is entered by hand right after the load + m_editorpending = false; + Application.push_mode(eu07_application::mode::editor); + } + return true; } @@ -458,6 +479,8 @@ void driver_mode::enter() Timer::ResetTimers(); set_picking(!Global.captureonstart); + + m_editorpending = Global.editor_session; } // maintenance method, called when the mode is deactivated @@ -681,7 +704,7 @@ void driver_mode::update_camera(double const Deltatime) auto const lr{m_input.keyboard.key(GLFW_KEY_LEFT) != GLFW_RELEASE}; // Camera.Yaw powinno być wyzerowane, aby po powrocie patrzeć do przodu - Camera.Pos = controlled->GetPosition() + simulation::Train->MirrorPosition(lr); // pozycja lusterka + Camera.Pos = controlled->GetPosition() + simulation::Train->MirrorPosition(lr); // pozycja lusterka // NOSONAR Camera.Angle.y = 0; // odchylenie na bok od Camera.LookAt if (simulation::Train->Occupied()->CabOccupied == 0) { diff --git a/application/drivermode.h b/application/drivermode.h index 66ff6936..e0406043 100644 --- a/application/drivermode.h +++ b/application/drivermode.h @@ -116,4 +116,5 @@ private: int iPause { 0 }; // wykrywanie zmian w zapauzowaniu command_relay m_relay; std::string change_train; // train name awaiting entering + bool m_editorpending { false }; // scenery was opened for editing, switch to the editor after the initial update }; diff --git a/application/driveruilayer.cpp b/application/driveruilayer.cpp index 0b8463f8..569ade33 100644 --- a/application/driveruilayer.cpp +++ b/application/driveruilayer.cpp @@ -252,7 +252,7 @@ void driver_ui::render_() ImGui::SetNextWindowSize(ImVec2(-1, -1)); if (ImGui::BeginPopupModal(popupheader, nullptr, 0)) { - if (ImGui::Button(STR_C("Resume"), ImVec2(150, 0)) || ImGui::IsKeyReleased(ImGui::GetKeyIndex(ImGuiKey_Escape))) + if (ImGui::Button(STR_C("Resume"), ImVec2(150, 0)) || ImGui::IsKeyReleased(ImGuiKey_Escape)) { m_relay.post(user_command::pausetoggle, 0.0, 0.0, GLFW_RELEASE, 0); } diff --git a/application/editormode.cpp b/application/editormode.cpp index 94b1424d..32f991c5 100644 --- a/application/editormode.cpp +++ b/application/editormode.cpp @@ -8,12 +8,31 @@ http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" +#include +#include +#include +#include #include "application/editormode.h" +#include "scene/scenenodegroups.h" #include "application/editoruilayer.h" +#ifndef _WIN32 +#include +#include +extern char **environ; +#endif +#include "application/editorprojection.h" #include "application/application.h" #include "editor/editorSettings.hpp" +#include "editor/editorModelSets.hpp" +#include "editor/editorIncludeInfo.hpp" +#include "editor/editorGroundMesh.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIcons.hpp" +#include "utilities/translation.h" #include "utilities/Globals.h" +#include "utilities/glmHelpers.h" #include "simulation/simulation.h" #include "simulation/simulationtime.h" #include "simulation/simulationenvironment.h" @@ -22,18 +41,23 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "model/AnimModel.h" #include "model/Model3d.h" +#include "world/Track.h" #include "utilities/Float3d.h" #include "scene/scene.h" +#include "scene/scenelayers.h" +#include "scene/sceneterrain.h" #include "utilities/parser.h" #include "utilities/utilities.h" #include "imgui/imgui.h" +#include "imgui/imgui_internal.h" #include "imgui/ImGuizmo.h" #include "utilities/Logs.h" #include #include #include +#include #include #include #include @@ -164,19 +188,6 @@ namespace return std::abs(sum) * 0.5; } - // short readable name of a node template (model file and texture), like the node bank shows it - std::string template_label(std::string const &Template) - { - cParser tokenizer(Template); - tokenizer.getTokens(9, false); // node, ranges, name, type, position, rotation - auto model{tokenizer.getToken(false)}; - auto texture{tokenizer.getToken(false)}; - replace_slashes(model); - erase_extension(model); - replace_slashes(texture); - return texture == "none" || texture.empty() ? model : model + " (" + texture + ")"; - } - // world triangles bucketed on a uniform XZ grid, for many ground height queries over one area class triangle_grid { @@ -203,8 +214,8 @@ namespace } } - // highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one - bool height_at(double const X, double const Z, double const Ceiling, double &OutY) const + // highest surface at (X,Z) that isn't above Ceiling. returns: true if there is one, and which triangle it is in Index if asked + bool height_at(double const X, double const Z, double const Ceiling, double &OutY, std::uint32_t *Index = nullptr) const { bool found = false; for (auto const idx : m_cells[static_cast(row(Z)) * m_columns + column(X)]) @@ -215,6 +226,8 @@ namespace { OutY = y; found = true; + if (Index != nullptr) + *Index = idx; } } return found; @@ -223,11 +236,11 @@ namespace private: int column(double const X) const { - return std::clamp(static_cast(std::floor((X - m_min.x) / m_cellsize)), 0, m_columns - 1); + return static_cast(std::clamp(std::floor((X - m_min.x) / m_cellsize), 0.0, static_cast(m_columns - 1))); } int row(double const Z) const { - return std::clamp(static_cast(std::floor((Z - m_min.y) / m_cellsize)), 0, m_rows - 1); + return static_cast(std::clamp(std::floor((Z - m_min.y) / m_cellsize), 0.0, static_cast(m_rows - 1))); } std::vector m_triangles; @@ -238,10 +251,77 @@ namespace int m_rows{1}; }; + // meshes of the shapes and the models asked about as ground. the ground is asked about a tile at a time, by the orthophoto + // and by the road tools, and without these every question meant going through every triangle of every terrain model in reach + struct ground_record + { + ground_mesh mesh; + // what the mesh was made of, to tell when it has to be made again + std::size_t vertices{0}; + std::size_t indices{0}; + glm::dvec3 origin{0.0}; + TModel3d const *model{nullptr}; + glm::dmat4 matrix{0.0}; + std::uint64_t used{0}; // number of the last question it served + }; + std::unordered_map ground_shapes; // by the geometry of the shape + std::unordered_map ground_models; + std::uint64_t ground_question{0}; + std::size_t ground_held{0}; // triangles in the meshes + std::size_t constexpr ground_limit = 8000000; // triangles kept; some 400 MB with the grids + std::size_t constexpr ground_small = 256; // triangles; anything smaller is quicker to go through than to look up + + // lets go of the meshes left alone the longest, once there's too much of them + void trim_ground_meshes() + { + if (ground_held <= ground_limit) + return; + std::vector ages; + for (auto const &record : ground_shapes) + ages.emplace_back(record.second.used); + for (auto const &record : ground_models) + ages.emplace_back(record.second.used); + std::sort(ages.begin(), ages.end()); + // the older half goes, except for what the current question needed + auto const threshold = std::min(ages[ages.size() / 2], ground_question - 1); + for (auto record = ground_shapes.begin(); record != ground_shapes.end();) + { + if (record->second.used <= threshold) + { + ground_held -= record->second.mesh.size(); + record = ground_shapes.erase(record); + } + else + ++record; + } + for (auto record = ground_models.begin(); record != ground_models.end();) + { + if (record->second.used <= threshold) + { + ground_held -= record->second.mesh.size(); + record = ground_models.erase(record); + } + else + ++record; + } + } + + // forgets the meshes of the shapes, whose geometry can be changed in place (terrain being sculpted) + void forget_ground_shapes() + { + for (auto const &record : ground_shapes) + ground_held -= record.second.mesh.size(); + ground_shapes.clear(); + } + // appends triangles of a shape node which overlap specified XZ rectangle - void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out) + void gather_shape_triangles(scene::shape_node const &Shape, glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out, bool const Skiproads = false) { auto const &data = Shape.data(); + if (editor_orthophoto::owns(data.geometry)) + return; // the orthophoto laid over the ground isn't ground itself + if (Skiproads && owned_shapes::owns(data.geometry)) + return; // neither are the roads, for the ones which ask where to put a road if (data.area.radius >= 0.0f) { double const r = data.area.radius; @@ -266,6 +346,36 @@ namespace return; auto const &verts = GfxRenderer->Vertices(data.geometry); auto const &indices = GfxRenderer->Indices(data.geometry); + auto const count = (indices.empty() ? verts.size() : indices.size()) / 3; + if (count >= ground_small) + { + // a large shape is sorted once, and asked for what's in the rectangle from then on + auto &record = ground_shapes[(static_cast(data.geometry.bank) << 32) | data.geometry.chunk]; + if (record.mesh.size() == 0 || record.vertices != verts.size() || record.indices != indices.size() || record.origin != data.origin) + { + std::vector triangles; + triangles.reserve(count); + if (false == indices.empty()) + { + for (std::size_t i = 0; i + 2 < indices.size(); i += 3) + triangles.push_back({verts[indices[i]].position, verts[indices[i + 1]].position, verts[indices[i + 2]].position}); + } + else + { + for (std::size_t i = 0; i + 2 < verts.size(); i += 3) + triangles.push_back({verts[i].position, verts[i + 1].position, verts[i + 2].position}); + } + ground_held -= record.mesh.size(); + record.mesh.assign(std::move(triangles), data.origin); + ground_held += record.mesh.size(); + record.vertices = verts.size(); + record.indices = indices.size(); + record.origin = data.origin; + } + record.used = ground_question; + record.mesh.collect(Min, Max, Out); + return; + } auto const to_world = [&](gfx::basic_vertex const &v) { return data.origin + glm::dvec3(v.position); }; if (false == indices.empty()) { @@ -279,6 +389,99 @@ namespace } } + // ground triangles overlapping an XZ rectangle (Min/Max are x,z). shapes hold the (legacy) terrain, + // model instances at least ModelRadius large (if ModelsAsGround) the terrain tiles + // Heightmap: the heightmap terrain is given as triangles too (for the ones which can ask the terrain for heights, it's a waste) + void gather_ground_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const ModelsAsGround, float const ModelRadius, std::vector &Out, bool const Skiproads = false, + bool const Heightmap = true) + { + auto const started = std::chrono::steady_clock::now(); + ++ground_question; + glm::dvec2 const center = (Min + Max) * 0.5; + float const radius = static_cast(glm::length(Max - Min) * 0.5); + auto const sections = simulation::Region->sections(glm::dvec3(center.x, 0.0, center.y), radius); // copy, the result is a scratchpad + for (auto *section : sections) + { + for (auto const &shape : section->m_shapes) + gather_shape_triangles(shape, Min, Max, Out, Skiproads); + for (auto &cell : section->m_cells) + { + double const r = cell.m_area.radius; + if (cell.m_area.center.x + r < Min.x || cell.m_area.center.x - r > Max.x || cell.m_area.center.z + r < Min.y || cell.m_area.center.z - r > Max.y) + continue; + for (auto const &shape : cell.m_shapesopaque) + gather_shape_triangles(shape, Min, Max, Out); + if (false == ModelsAsGround) + continue; + for (auto *instance : cell.m_instancesopaque) + { + if (instance == nullptr || instance->Model() == nullptr || instance->radius() < ModelRadius) + continue; + // going through the triangles of a model which doesn't reach the rectangle is all cost and no gain + double const reach = instance->radius(); + if (instance->location().x + reach < Min.x || instance->location().x - reach > Max.x || instance->location().z + reach < Min.y || instance->location().z - reach > Max.y) + continue; + // the model is gone through once, and asked for what's in the rectangle from then on, for as long as it stays put + auto const matrix = instance_matrix(*instance); + auto &record = ground_models[instance]; + if (record.model != instance->Model() || record.matrix != matrix) + { + std::vector modeltriangles; + gather_submodel_triangles(instance->Model()->Root, matrix, modeltriangles); + glm::dvec3 const origin = instance->location(); + std::vector triangles; + triangles.reserve(modeltriangles.size()); + for (auto const &t : modeltriangles) + triangles.push_back({glm::vec3(t[0] - origin), glm::vec3(t[1] - origin), glm::vec3(t[2] - origin)}); + ground_held -= record.mesh.size(); + record.mesh.assign(std::move(triangles), origin); + ground_held += record.mesh.size(); + // a model which isn't loaded yet has nothing to show for itself, and is looked at again the next time + record.model = (record.mesh.size() > 0 ? instance->Model() : nullptr); + record.matrix = matrix; + } + record.used = ground_question; + record.mesh.collect(Min, Max, Out); + } + } + } + // the heightmap terrain answers for itself, with its full grid whatever is drawn of it + if (Heightmap) + EditorTerrain.gather_triangles(Min, Max, Out); + trim_ground_meshes(); + // going through the scenery is the one thing here which can take long enough to be felt; when it does it's worth knowing + auto const elapsed = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + if (elapsed > 250.0) + WriteLog("editor: gathering the ground took " + std::to_string(static_cast(elapsed)) + " ms, for " + std::to_string(Out.size()) + " triangles; " + std::to_string(ground_held) + " are kept for the next time"); + } + + // ground kept for the road tools, which ask about the same places frame after frame. it's gathered a tile at a time: + // gathering is what takes time, so a place asked about for the first time costs a single tile a frame instead of a stall + struct ground_tile + { + std::unique_ptr grid; + double gathered{0.0}; // when, on the clock of the user interface + int used{0}; // frame it was last asked for + }; + double constexpr ground_tile_size = 256.0; + double constexpr ground_tile_lifetime = 30.0; // older tiles are gathered again when asked for, to catch up with the scenery + std::size_t constexpr ground_tile_limit = 96; + std::map, ground_tile> ground_tiles; + int ground_tile_frame{-1}; // frame the last tile was gathered in without being told to + + // scenery file name without path and extension + std::string scenery_stem() + { + std::string scenery = Global.SceneryFile; + auto const slash = scenery.find_last_of("/\\"); + if (slash != std::string::npos) + scenery = scenery.substr(slash + 1); + auto const dot = scenery.find_last_of('.'); + if (dot != std::string::npos) + scenery = scenery.substr(0, dot); + return scenery.empty() ? "default" : scenery; + } + } bool editor_mode::editormode_input::init() @@ -293,8 +496,129 @@ void editor_mode::editormode_input::poll() editor_mode::editor_mode() { m_userinterface = std::make_shared(); + // the area fill settings live in the node bank window, in the tab of the fill mode + ui()->set_fill_options([this]() { render_area_fill(); }); + ui()->set_gizmo_options([this]() { render_gizmo_options(); }); + ui()->set_toolbar_options([this]() { render_toolbar(); }); + ui()->set_workspace_bar([this]() { render_workspaces(); }); + ui()->set_track_options([this]() { render_track_tool_options(); }, [this]() { render_track_selection(); }); + ui()->set_workspace_options(editor_ui::workspace::roads, [this]() { render_road_tool_options(); }, [this]() { render_road_selection(); }); + ui()->set_workspace_options(editor_ui::workspace::terrain, [this]() { render_terrain_tool_options(); }); + ui()->set_workspace_bank(editor_ui::workspace::terrain, [this]() { render_texture_browser(); }, STR("Textures")); + // a node picked in the scene window is selected as if clicked in the view; a double click flies the camera to it + ui()->set_scene_select([this](scene::basic_node *Node, bool const Focus) { + if (Focus) + start_focus(Node); + std::string reason; + if (Node->from_template() || false == scene::Layers.editable(Node, &reason)) + { + ui()->set_status("\"" + (Node->name().empty() ? std::string{"(unnamed node)"} : Node->name()) + "\" can't be edited: " + reason, true); + return; + } + m_track_window_open = false; + ui()->set_mode(nodebank_panel::MODIFY); + m_node = Node; + ui()->set_node(m_node); + }); + ui()->set_array_options([this]() { render_array(); }); + ui()->set_bend_options([this]() { render_bend(); }); + ui()->set_file_actions([this]() { save(); }, [this]() { export_scenery(); }); + ui()->set_new_scenery([this]() { m_newscenery_asked = true; }); + ui()->set_open_scenery([this]() { m_openscenery_asked = true; }); + ui()->set_edit_menu([this]() { render_edit_menu(); }); + ui()->set_menu_options([this]() { + render_object_menu(); + render_view_menu(); + render_map_menu(); + render_help_menu(); + }); + m_roadtool.settings.normalize(); + road_select(nullptr); + // the orthophoto is fitted onto the same ground as the area fill uses, terrain tile models included + // the roads built in the editor aren't ground: the imagery goes under them + m_orthophoto.ground_source([](glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Heightmap, std::vector &Out) { + gather_ground_triangles(Min, Max, true, 50.0f, Out, true, Heightmap); + }); + m_orthophoto.height_source([](double const X, double const Z, double &Height) { return EditorTerrain.height_at(X, Z, Height); }); + editor_track::observe(this); } +editor_mode::~editor_mode() +{ + editor_track::observe(nullptr); +} + +std::vector editor_mode::ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found) +{ + std::vector heights; + heights.reserve(Points.size()); + if (Found != nullptr) + Found->assign(Points.size(), 0); + for (auto const &point : Points) + heights.push_back(point.y); + if (Points.empty() || simulation::Region == nullptr) + return heights; + + double const now = ImGui::GetTime(); + int const frame = ImGui::GetFrameCount(); + // surfaces much higher than the point are something it's under, rather than the ground + double constexpr height_tolerance = 50.0; + auto const terrains = active_terrains(); + for (std::size_t i = 0; i < Points.size(); ++i) + { + auto const &p = Points[i]; + std::pair const key{static_cast(std::floor(p.x / ground_tile_size)), static_cast(std::floor(p.z / ground_tile_size))}; + auto &tile = ground_tiles[key]; + bool const missing = (tile.grid == nullptr); + bool const stale = missing || now < tile.gathered || now - tile.gathered > ground_tile_lifetime; + // told to wait the caller gets every tile it needs, otherwise what's there, with one tile gathered each frame + if (stale && (Fresh ? missing : ground_tile_frame != frame)) + { + glm::dvec2 const tilemin{key.first * ground_tile_size, key.second * ground_tile_size}; + glm::dvec2 const tilemax{tilemin + glm::dvec2{ground_tile_size}}; + std::vector triangles; + // the same ground as the area fill uses, terrain tile models included + gather_ground_triangles(tilemin, tilemax, true, 50.0f, triangles, true); + tile.grid = std::make_unique(std::move(triangles), tilemin, tilemax); + tile.gathered = now; + if (false == Fresh) + ground_tile_frame = frame; + } + tile.used = frame; + + double const ceiling = p.y + height_tolerance; + double y = 0.0; + bool found = (tile.grid != nullptr) && tile.grid->height_at(p.x, p.z, ceiling, y); + for (editor_terrain *terrain : terrains) + { + if (!terrain->contains(p.x, p.z)) + continue; + double const h = terrain->height_at(p.x, p.z); + if (h <= ceiling && (!found || h > y)) + { + y = h; + found = true; + } + } + if (found) + heights[i] = y; + if (Found != nullptr) + (*Found)[i] = found ? 1 : 0; + } + // the tiles left alone the longest go once there are too many + while (ground_tiles.size() > ground_tile_limit) + { + auto oldest = ground_tiles.begin(); + for (auto tile = ground_tiles.begin(); tile != ground_tiles.end(); ++tile) + if (tile->second.used < oldest->second.used) + oldest = tile; + if (oldest->second.used == frame) + break; + ground_tiles.erase(oldest); + } + return heights; +} + editor_ui *editor_mode::ui() const { return static_cast(m_userinterface.get()); @@ -303,6 +627,7 @@ editor_ui *editor_mode::ui() const bool editor_mode::init() { EditorSettings.load(); + EditorModelSets.load(); Camera.Init({0, 15, 0}, {glm::radians(-30.0), glm::radians(180.0), 0}, nullptr); return m_input.init(); } @@ -362,7 +687,7 @@ void editor_mode::start_focus(scene::basic_node *node, double duration) m_focus_duration = duration; } -void editor_mode::snap_to_ground(scene::basic_node *node) +void editor_mode::snap_to_ground(scene::basic_node *node, bool const Alone, bool const Joined) { if (!node || !simulation::Region) return; @@ -371,89 +696,25 @@ void editor_mode::snap_to_ground(scene::basic_node *node) if (!simulation::Region->point_inside(origin)) return; - // small tolerance so a node already resting on a surface still snaps cleanly to it - double const epsilon = 0.05; - double bestY = -std::numeric_limits::max(); - bool found = false; - - // record the highest surface that is at or below the node's current position at its (x,z) - auto consider_triangle = [&](glm::dvec3 const &a, glm::dvec3 const &b, glm::dvec3 const &c) { - double y; - if (triangle_height_at(a, b, c, origin.x, origin.z, y) && y <= origin.y + epsilon && y > bestY) - { - bestY = y; - found = true; - } - }; - - auto consider_shapes = [&](std::vector const &shapes) { - for (auto const &shape : shapes) - { - // quick reject: skip shapes whose bounding circle doesn't cover our (x,z) column - auto const &sdata = shape.data(); - double const sdx = origin.x - sdata.area.center.x; - double const sdz = origin.z - sdata.area.center.z; - if (sdx * sdx + sdz * sdz > static_cast(sdata.area.radius) * sdata.area.radius) - continue; - - auto const &verts = sdata.vertices; - for (std::size_t i = 0; i + 2 < verts.size(); i += 3) - consider_triangle(verts[i].position, verts[i + 1].position, verts[i + 2].position); - } - }; - - scene::basic_section &sec = simulation::Region->section(origin); - // section level holds the large opaque geometry, including legacy terrain - consider_shapes(sec.m_shapes); - - // scan a 3x3 neighbourhood of cells for smaller geometry and other model instances below us - for (int dz = -1; dz <= 1; ++dz) - for (int dx = -1; dx <= 1; ++dx) - { - scene::basic_cell &cell = sec.cell(origin, glm::ivec2(dx, dz)); - consider_shapes(cell.m_shapesopaque); - consider_shapes(cell.m_shapestranslucent); - - // other instances are approximated by their bounding sphere, so a node can rest on top of them - for (auto *inst : cell.m_instancesopaque) - { - if (!inst || inst == node) - continue; - glm::dvec3 const ic = inst->location(); - double const r = static_cast(inst->radius()); - double const idx = origin.x - ic.x, idz = origin.z - ic.z; - double const horiz2 = idx * idx + idz * idz; - if (horiz2 < r * r) - { - double const ytop = ic.y + std::sqrt(r * r - horiz2); - if (ytop <= origin.y + epsilon && ytop > bestY) - { - bestY = ytop; - found = true; - } - } - } - } - - // editable terrain patches keep their heightmap on the CPU, so query them directly - for (editor_terrain *terrain : active_terrains()) - { - if (!terrain->contains(origin.x, origin.z)) - continue; - double const y = terrain->height_at(origin.x, origin.z); - if (y <= origin.y + epsilon && y > bestY) - { - bestY = y; - found = true; - } - } - - if (!found) + // the same ground the models are put on when they're inserted: the terrain shapes, the terrain tile models and the terrain + // patches of the editor, under the node, or over it when it went under the ground. the shapes alone and the spheres of the + // models around, which were asked about before, missed the ground of the sceneries made of terrain tile models + glm::dvec3 const target = placement_on_ground(origin); + if (std::abs(target.y - origin.y) < 1e-4) return; push_snapshot(node, EditorSnapshot::Action::Move); - glm::dvec3 target = origin; - target.y = bestY; + if (false == m_history.empty()) + m_history.back().joined = Joined; + if (auto *model = dynamic_cast(node); Alone && model != nullptr && node->group() > 1) + { + // the model of a group goes down by itself, the rest of the group stays + simulation::Region->erase(model); + model->location(target); + simulation::Region->insert(model); + model->mark_dirty(); + return; + } m_editor.translate(node, target, true); // true == apply the computed Y (free vertical move) } @@ -473,26 +734,24 @@ void editor_mode::handle_brush_mouse_hold(int Action, int Button) if (!mouseHold || !viewport_click()) return; - const std::string *src = ui()->get_active_node_template(); - if (!src) - return; - - std::string name = "editor_"; - // spacing is measured in world space: the mouse offset is camera-relative and the camera may move mid-stroke glm::dvec3 const newPos = Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); if (m_brush_has_last && glm::distance(newPos, oldPos) < ui()->getSpacing()) return; - TAnimModel *cloned = simulation::State.create_model(*src, name, newPos); + // picked only when a placement is due, so a random pick from a set isn't drawn every tick + const std::string *src = ui()->get_active_node_template(); + if (!src || src->starts_with(editor_includes::directive_mark)) // scenery templates are placed one at a time + return; + + // NOTE: no name for what the brush puts in, as with thousands of nodes the names take their share of the time + // the scenery needs to load. a node which needs one, to be referred to by an event, gets it in the node properties + TAnimModel *cloned = simulation::State.create_model(*src, std::string{}, placement_on_ground(newPos)); oldPos = newPos; m_brush_has_last = true; if (!cloned) return; - std::string new_name = "editor_" + cloned->uuid.to_string(); - - cloned->m_name = new_name; apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value); std::string as_text; @@ -556,6 +815,7 @@ void editor_mode::push_snapshot(scene::basic_node *node, EditorSnapshot::Action snap.node_name = node->name(); snap.position = node->location(); snap.uuid = node->uuid; + snap.layer = node->layer(); if (auto *model = dynamic_cast(node)) { @@ -600,6 +860,11 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) { if (s.node_ptr == node) s.node_ptr = nullptr; + for (auto © : s.copies) + { + if (copy.model == node) + copy.model = nullptr; + } } for (auto &s : g_redo) @@ -610,9 +875,81 @@ void editor_mode::nullify_history_pointers(scene::basic_node *node) // deleted nodes also drop out of the "undo last fill" set m_fill_last.erase(std::remove(m_fill_last.begin(), m_fill_last.end(), node), m_fill_last.end()); + // and out of the selection and their group + m_selection.erase(std::remove(m_selection.begin(), m_selection.end(), node), m_selection.end()); + scene::Groups.remove(node); + + // an array no longer follows its settings once the model it was made of is gone + if (m_array.base == node) + m_array.base = nullptr; +} + +void editor_mode::terrain_history_step() +{ + if (false == m_streamer.active()) + return; + if (m_streamer.recording()) + { + if (terrain_brush() && mouseHold && false == m_history_busy) + return; // the stroke goes on + auto change{m_streamer.end_change()}; + if (false == change.empty()) + { + trim_history(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Terrain; + snap.node_name = STR("terrain"); + if (false == change.chunks.empty()) + { + auto const centre{heightmap::chunk_corner(change.chunks.begin()->first) + glm::dvec2{heightmap::chunk_size * 0.5}}; + snap.position = {centre.x, 0.0, centre.y}; + } + snap.terrain = std::make_shared(std::move(change)); + m_history.push_back(std::move(snap)); + g_redo.clear(); + } + } + m_streamer.begin_change(); +} + +void editor_mode::restore_terrain(EditorSnapshot &Snapshot) +{ + if (Snapshot.terrain == nullptr) + return; + auto opposite{m_streamer.restore(*Snapshot.terrain)}; + Snapshot.terrain = std::make_shared(std::move(opposite)); + forget_ground(); + m_water_selected = -1; + m_water_material_of = -1; } void editor_mode::undo_last() +{ + // a change of the terrain made a moment ago is a step of its own before it's taken back + m_history_busy = true; + terrain_history_step(); + m_history_busy = false; + // the changes made together are taken back together, and their models are the selection again + std::vector touched; + while (false == m_history.empty()) + { + auto const joined{m_history.back().joined}; + undo_one(); + if (m_node != nullptr && std::find(touched.begin(), touched.end(), m_node) == touched.end()) + touched.push_back(m_node); + if (false == joined) + break; + } + if (touched.size() > 1) + { + // the one with the gizmo was recorded first, so it's taken back last + m_node = touched.back(); + m_selection.assign(touched.begin(), touched.end() - 1); + ui()->set_node(m_node); + } +} + +void editor_mode::undo_one() { if (m_history.empty()) return; @@ -620,6 +957,62 @@ void editor_mode::undo_last() EditorSnapshot snap = m_history.back(); m_history.pop_back(); + if (snap.action == EditorSnapshot::Action::Terrain) + { + restore_terrain(snap); + g_redo.push_back(std::move(snap)); + return; + } + + if (snap.action == EditorSnapshot::Action::Bend) + { + restore_bend(snap, true); + g_redo.push_back(std::move(snap)); + return; + } + + if (false == snap.sweeps.empty() || false == snap.sweeps_toggled.empty()) + { + restore_sweeps(snap); + g_redo.push_back(std::move(snap)); + return; + } + + if (false == snap.instances.empty() || false == snap.directives.empty()) + { + restore_includes(snap); + if (snap.action != EditorSnapshot::Action::TrackEdit) + { + g_redo.push_back(std::move(snap)); + return; + } + } + + if (snap.instance != 0) + { + restore_include(snap); + g_redo.push_back(std::move(snap)); + return; + } + + if (snap.action == EditorSnapshot::Action::TrackEdit) + { + restore_track_snapshot(snap, g_redo, true); + return; + } + + if (snap.action == EditorSnapshot::Action::RoadEdit) + { + restore_road_snapshot(snap, g_redo, true); + return; + } + + if (snap.action == EditorSnapshot::Action::Array) + { + restore_array_snapshot(std::move(snap), g_redo, true); + return; + } + if (snap.action == EditorSnapshot::Action::Delete) { // undo delete -> recreate model @@ -629,16 +1022,22 @@ void editor_mode::undo_last() redoSnap.serialized = snap.serialized; redoSnap.position = snap.position; redoSnap.node_ptr = nullptr; + redoSnap.uuid = snap.uuid; + redoSnap.layer = snap.layer; + redoSnap.joined = snap.joined; g_redo.push_back(std::move(redoSnap)); TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position); if (created) { + scene::Layers.move(created, snap.layer); // back to the layer it was deleted from, rather than the active one created->location(snap.position); created->Angles(snap.rotation); m_node = created; m_node->uuid = snap.uuid; // restore original UUID for better tracking (not strictly necessary) add_to_hierarchy(created); + // a node without a name can be found for the redo only this way + g_redo.back().node_ptr = created; ui()->set_node(m_node); } return; @@ -650,8 +1049,10 @@ void editor_mode::undo_last() EditorSnapshot current; current.action = snap.action; + current.joined = snap.joined; current.node_name = snap.node_name; current.node_ptr = target; + current.layer = target->layer(); current.position = target->location(); if (auto *model = dynamic_cast(target)) { @@ -692,6 +1093,28 @@ void editor_mode::undo_last() } void editor_mode::redo_last() +{ + // a change of the terrain made since the undo goes to the history, and the redo steps are dropped with it + m_history_busy = true; + terrain_history_step(); + m_history_busy = false; + // the first of the changes made together is on the top, the others follow it + std::vector touched; + do + { + redo_one(); + if (m_node != nullptr && std::find(touched.begin(), touched.end(), m_node) == touched.end()) + touched.push_back(m_node); + } while (false == g_redo.empty() && g_redo.back().joined); + if (touched.size() > 1) + { + m_node = touched.front(); + m_selection.assign(touched.begin() + 1, touched.end()); + ui()->set_node(m_node); + } +} + +void editor_mode::redo_one() { if (g_redo.empty()) return; @@ -699,6 +1122,62 @@ void editor_mode::redo_last() EditorSnapshot snap = g_redo.back(); g_redo.pop_back(); + if (snap.action == EditorSnapshot::Action::Terrain) + { + restore_terrain(snap); + m_history.push_back(std::move(snap)); + return; + } + + if (snap.action == EditorSnapshot::Action::Bend) + { + restore_bend(snap, false); + m_history.push_back(std::move(snap)); + return; + } + + if (false == snap.sweeps.empty() || false == snap.sweeps_toggled.empty()) + { + restore_sweeps(snap); + m_history.push_back(std::move(snap)); + return; + } + + if (false == snap.instances.empty() || false == snap.directives.empty()) + { + restore_includes(snap); + if (snap.action != EditorSnapshot::Action::TrackEdit) + { + m_history.push_back(std::move(snap)); + return; + } + } + + if (snap.instance != 0) + { + restore_include(snap); + m_history.push_back(std::move(snap)); + return; + } + + if (snap.action == EditorSnapshot::Action::TrackEdit) + { + restore_track_snapshot(snap, m_history, false); + return; + } + + if (snap.action == EditorSnapshot::Action::RoadEdit) + { + restore_road_snapshot(snap, m_history, false); + return; + } + + if (snap.action == EditorSnapshot::Action::Array) + { + restore_array_snapshot(std::move(snap), m_history, false); + return; + } + // handle delete redo (re-delete) separately if (snap.action == EditorSnapshot::Action::Delete) { @@ -708,9 +1187,12 @@ void editor_mode::redo_last() hist.serialized = snap.serialized; hist.position = snap.position; hist.uuid = snap.uuid; + hist.layer = snap.layer; + hist.joined = snap.joined; m_history.push_back(std::move(hist)); - scene::basic_node *target = simulation::Instances.find(snap.node_name); + // NOTE: located the way other steps do it, as the name alone doesn't lead to a node which has none + scene::basic_node *target = find_node_by_any(snap.node_ptr, snap.uuid.to_string(), snap.node_name); if (target) { if (auto *model = dynamic_cast(target)) @@ -729,6 +1211,7 @@ void editor_mode::redo_last() EditorSnapshot hist; hist.action = snap.action; + hist.joined = snap.joined; hist.node_name = snap.node_name; hist.node_ptr = target; @@ -749,6 +1232,7 @@ void editor_mode::redo_last() TAnimModel *created = simulation::State.create_model(snap.serialized, snap.node_name, snap.position); if (created) { + scene::Layers.move(created, snap.layer); created->location(snap.position); created->Angles(snap.rotation); created->Scale(snap.scale); @@ -780,10 +1264,22 @@ void editor_mode::redo_last() bool editor_mode::update() { + Global.editor_tracks = (current_work_area() == work_area::tracks); + selftest_step(); + if (m_vehicle.leave) + { + m_vehicle.leave = false; + Application.pop_mode(); + return true; + } + Timer::UpdateTimers(true); simulation::State.update_clocks(); simulation::Environment.update(); + // bindings are matched with the objects by their locations, before anything gets moved + if (false == m_bindings_loaded && simulation::is_ready) + infra_load(); auto const deltarealtime = Timer::GetDeltaRenderTime(); @@ -796,6 +1292,8 @@ bool editor_mode::update() m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT, GLFW_RELEASE); Application.set_cursor(GLFW_CURSOR_NORMAL); } + if (m_ortho_pan && !ImGui::GetIO().MouseDown[2]) + ortho_pan_stop(); // same for the left button (brush / sculpt / click placement). the UI filter in the application // uses io.WantCaptureMouse from the previous frame, so a press on a panel the cursor has just @@ -809,6 +1307,7 @@ bool editor_mode::update() m_dragging = false; m_input.mouse.button(GLFW_MOUSE_BUTTON_LEFT, GLFW_RELEASE); } + terrain_history_step(); // fixed step render time routines (50 Hz) fTime50Hz += deltarealtime; // accumulate even when paused to keep frame reads stable @@ -819,9 +1318,6 @@ bool editor_mode::update() #endif m_userinterface->update(); - // update brush settings visibility depending on panel mode - ui()->toggleBrushSettings(ui()->mode() == nodebank_panel::BRUSH); - if (mouseHold) { // process continuous brush placement @@ -845,6 +1341,37 @@ bool editor_mode::update() // variable step routines update_camera(deltarealtime); + // drop the selection if its layer was hidden or locked in the meantime + if (m_node != nullptr && false == scene::Layers.editable(m_node)) + { + m_node = nullptr; + m_dragging = false; + ui()->set_node(nullptr); + } + if (m_instance != 0 && false == scene::Layers.removable(m_instance)) + { + select_include(0); + } + if (m_include.changed) + { + // a parameter of the selected include was changed in the node properties + m_include.changed = false; + apply_include(); + } + if (false == m_include.active && false == m_gizmo_using) + { + m_include_gesture = false; + } + // models replaced after changes of includes are destroyed once the renderer can't have them among its pick candidates + for (auto &retired : m_retired) + { + if (--retired.second <= 0) + { + simulation::Instances.purge(retired.first); + } + } + m_retired.erase(std::remove_if(m_retired.begin(), m_retired.end(), [](std::pair const &Retired) { return Retired.second <= 0; }), m_retired.end()); + simulation::Region->update_sounds(); audio::renderer.update(Global.iPause ? 0.0 : deltarealtime); @@ -855,49 +1382,98 @@ bool editor_mode::update() // note: the streamer is advanced centrally in the application main loop (so it runs in every // mode), using the published Global.pCamera; nothing to do here - // continuous terrain sculpting while the left mouse button is held in sculpt mode - if (m_terrain_sculpt && mouseHold) - handle_terrain_sculpt(deltarealtime); + // the brush of the terrain works while the left mouse button is held + if (terrain_brush() && mouseHold) + handle_terrain_brush(deltarealtime); + else + m_terrain_stroke = false; + if (m_terrain_open) + draw_terrain_overlay(); - // debounced auto mesh simplification: once sculpting has settled for a short while, simplify - // any chunk that was edited. holding the brush keeps the timer reset so we don't churn mid-stroke. - if (m_terrain_auto_optimize) - { - auto const terrains = active_terrains(); - bool any_dirty = false; - for (editor_terrain *terrain : terrains) - if (terrain->dirty()) - { - any_dirty = true; - break; - } - - if (!any_dirty || (m_terrain_sculpt && mouseHold)) - { - m_terrain_idle = 0.0; // actively editing (or nothing pending): hold off - } - else - { - m_terrain_idle += deltarealtime; - if (m_terrain_idle >= 0.5) // settle time - { - for (editor_terrain *terrain : terrains) - if (terrain->dirty()) - terrain->optimize(m_terrain_simplify_error); - m_terrain_idle = 0.0; - } - } - } + // --- geoportal orthophoto: streamed around the camera, drawn beneath the other overlays --- + m_orthophoto.update(Camera.Pos); + draw_orthophoto(); // --- ImGuizmo: in-viewport transform gizmo for the selected node --- render_gizmo(); + // the tools, the gizmo modes and the axes over the 3d view + render_viewport_overlays(); - // --- area fill: outline overlay and settings, while the mode is active --- - if (ui()->mode() == nodebank_panel::FILL) + render_track_inspector(); + if (selected_track()) + draw_track_overlay(); + if (route_active()) + draw_route_overlay(); + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights) + draw_straights_overlay(); + if (ui()->mode() == nodebank_panel::TRACK) { - draw_area_fill_outline(); - render_area_fill(); + if (m_track_box.active && false == ImGui::GetIO().MouseDown[0]) + finish_track_box(); + update_track_hover(); + point_drag_update(); + if (m_track_tab == track_tab::turntable) + draw_turntable_overlay(); + if (m_track_tab == track_tab::signals) + { + signal_refresh(); + signal_move_update(); + draw_signal_overlay(); + } + draw_parallel_preview(); + if (m_track_tab == track_tab::lineside) + { + draw_hekto_overlay(); + draw_fouling_overlay(); + draw_vehicle_marker(); + } + render_track_context(); + update_build_tools(); + update_track_intent(); + draw_track_hover(); + draw_track_set(); + draw_track_spread(); + draw_build_overlay(); + draw_track_intent(); + speed_step(); + joints_step(); + draw_track_hints(); + draw_profile_overlay(); + draw_infra_overlay(); + draw_speed_overlay(); + draw_joints_overlay(); } + else + { + draw_sweep_overlay(); + m_sweep.open = false; + bend_drag_update(); + draw_bend_overlay(); + } + update_gauge(); + render_gauge_window(); + + // roads: trajectories of the lanes, and the tools of the road window + update_road_tool(); + draw_road_overlay(); + // the roads and the terrain are fields of work, their tools are drawn in the tool options window in place of the edit mode ones + ui()->set_workspace(m_terrain_open ? editor_ui::workspace::terrain : m_roadtool.window ? editor_ui::workspace::roads : editor_ui::workspace::none); + + // --- array: the one made last is kept in line with its settings --- + update_array(); + + // --- area fill: outline overlay while the mode is active (its settings are drawn in the node bank window) --- + if (ui()->mode() == nodebank_panel::FILL) + draw_area_fill_outline(); + draw_ortho_compass(); + + render_new_scenery_popup(); + render_open_scenery_popup(); + render_shortcuts_window(); + render_orthophoto_window(); + render_environment_window(); + draw_selection_overlay(); + render_context_menu(); // --- ImGui: Editor Settings & History windows --- if(m_settings_open) @@ -912,371 +1488,1766 @@ bool editor_mode::update() void editor_mode::render_settings() { - ImGui::Begin("Editor Settings", &m_settings_open, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Begin((std::string(STR_C("Editor Settings")) + "###editorsettings").c_str(), &m_settings_open); - ImGui::TextUnformatted("Camera movement"); - - const char *schemes[] = {"WSAD (new)", "Arrows (legacy)"}; - int current = EditorSettings.movement() == editorSettings::movement_scheme::legacy ? 1 : 0; - if (ImGui::Combo("##movement_scheme", ¤t, schemes, IM_ARRAYSIZE(schemes))) + if (ImGui::BeginTabBar("##editorsettings")) { - EditorSettings.movement(current == 1 ? editorSettings::movement_scheme::legacy - : editorSettings::movement_scheme::wsad); - m_input.keyboard.apply_scheme(); - EditorSettings.save(); + if (ImGui::BeginTabItem(STR_C("General"))) + { + ImGui::TextUnformatted(STR_C("Camera movement")); + + const char *schemes[] = {STR_C("WSAD (new)"), STR_C("Arrows (legacy)")}; + int current = EditorSettings.movement() == editorSettings::movement_scheme::legacy ? 1 : 0; + if (ImGui::Combo("##movement_scheme", ¤t, schemes, IM_ARRAYSIZE(schemes))) + { + EditorSettings.movement(current == 1 ? editorSettings::movement_scheme::legacy + : editorSettings::movement_scheme::wsad); + m_input.keyboard.apply_scheme(); + EditorSettings.save(); + } + + ImGui::Separator(); + ImGui::Checkbox(STR_C("Transform gizmo (ImGuizmo)"), &m_gizmo_enabled); + + ImGui::Separator(); + ImGui::TextUnformatted(STR_C("Colours of the editor")); + const char *palettes[] = {STR_C("Green, as the rest of the simulator"), STR_C("Amber, of the editor")}; + int palette = std::clamp(EditorSettings.palette(), 0, 1); + if (ImGui::Combo("##palette", &palette, palettes, IM_ARRAYSIZE(palettes))) + { + EditorSettings.palette(palette); + EditorSettings.save(); + } + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } - ImGui::Separator(); - ImGui::Checkbox("Transform gizmo (ImGuizmo)", &m_gizmo_enabled); - - render_terrain_ui(); - ImGui::End(); } +namespace +{ + // point spacings offered for the chunks, as the user reads them + char const *spacing_label(float const Spacing) + { + return Spacing < 0.75f ? "0.5 m" : Spacing < 1.5f ? "1 m" : Spacing < 3.f ? "2 m" : "5 m"; + } + + bool spacing_combo(char const *Label, float &Spacing) + { + bool changed = false; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::BeginCombo(Label, spacing_label(Spacing))) + { + for (auto const spacing : heightmap::spacings) + if (ImGui::Selectable(spacing_label(spacing), spacing == Spacing)) + { + Spacing = spacing; + changed = true; + } + ImGui::EndCombo(); + } + return changed; + } + + // grid points without a height (or another value) take the one of the nearest point which has it (by steps along the grid) + template void fill_from_nearest(std::vector &Heights, std::vector &Found, std::size_t const Side) + { + std::deque queue; + for (std::size_t index = 0; index < Found.size(); ++index) + if (Found[index] != 0) + queue.push_back(index); + while (false == queue.empty()) + { + auto const index = queue.front(); + queue.pop_front(); + auto const ix = index % Side, iz = index / Side; + auto const visit = [&](std::size_t const Next) { + if (Found[Next] != 0) + return; + Found[Next] = 1; + Heights[Next] = Heights[index]; + queue.push_back(Next); + }; + if (ix > 0) + visit(index - 1); + if (ix + 1 < Side) + visit(index + 1); + if (iz > 0) + visit(index - Side); + if (iz + 1 < Side) + visit(index + Side); + } + } +} // namespace + void editor_mode::render_terrain_ui() { - ImGui::Separator(); - ImGui::TextUnformatted("Terrain"); - - ImGui::SetNextItemWidth(120.0f); - ImGui::InputInt("Grid cells", &m_terrain_cells); - m_terrain_cells = std::clamp(m_terrain_cells, 1, 512); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Cell size (m)", &m_terrain_cellsize); - if (m_terrain_cellsize < 0.1f) - m_terrain_cellsize = 0.1f; - ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Base height (m)", &m_terrain_baseheight); - ImGui::SetNextItemWidth(200.0f); - ImGui::InputText("Texture (optional)", m_terrain_texture, IM_ARRAYSIZE(m_terrain_texture)); - - if (ImGui::Button("Create flat terrain")) + if (false == m_streamer.active()) { - // centre the new patch horizontally on the camera, flat at the requested base height - glm::dvec3 const center(Camera.Pos.x, static_cast(m_terrain_baseheight), Camera.Pos.z); - auto terrain = std::make_unique(); - if (terrain->create(center, m_terrain_cells, m_terrain_cellsize, std::string(m_terrain_texture))) - { - if (m_terrain_auto_optimize) - terrain->optimize(m_terrain_simplify_error); - m_terrains.push_back(std::move(terrain)); - } - else - WriteLog("Editor: failed to create terrain", logtype::generic); + ImGui::TextWrapped("%s", STR_C("The scenery has no heightmap terrain yet. It's kept in the folder terrain// of the simulator, " + "and written there when the scenery is saved.")); + if (ImGui::Button(STR_C("Make heightmap terrain for this scenery"))) + ensure_terrain(); + return; } + ImGui::TextDisabled(STR_C("Folder: %s"), m_streamer.directory().c_str()); + ImGui::TextDisabled(STR_C("Chunks in memory: %zu%s"), m_streamer.resident(), m_streamer.modified() ? STR_C(", changes not saved yet") : ""); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputInt("Chunks / side", &m_terrain_chunks); - m_terrain_chunks = std::clamp(m_terrain_chunks, 1, 32); + // what the left mouse button does + auto tool = static_cast(m_terrain_tool); + auto const radio = [&](char const *Label, terrain_tool const Tool, char const *Tooltip) { + ImGui::RadioButton(Label, &tool, static_cast(Tool)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", Tooltip); + }; + // the tools are named like the headers below, which would give them the same IDs + ImGui::PushID("terrain_tools"); + // the tools working on a click, then the brushes + radio(STR_C("Select"), terrain_tool::none, STR_C("LMB picks the models, as in the surroundings")); ImGui::SameLine(); - if (ImGui::Button("Create chunked terrain")) - create_chunked_terrain(); - ImGui::TextDisabled("total %d x %d m, %d chunks", - static_cast(m_terrain_chunks * m_terrain_cells * m_terrain_cellsize), - static_cast(m_terrain_chunks * m_terrain_cells * m_terrain_cellsize), - m_terrain_chunks * m_terrain_chunks); + radio(STR_C("Chunks"), terrain_tool::chunks, STR_C("LMB adds a chunk shaped as the ground under it, next to the clicked one; Shift+LMB removes the clicked chunk")); + ImGui::SameLine(); + radio(STR_C("Spacing"), terrain_tool::spacing, STR_C("LMB selects the clicked chunk, Ctrl+LMB adds it to the selection or takes it away;\nthe point spacing of the selected chunks is converted in \"Chunk resolution\" below")); + ImGui::SameLine(); + radio(STR_C("Water"), terrain_tool::water, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back")); + ImGui::SameLine(); + radio(STR_C("Fill"), terrain_tool::fill, STR_C("LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back")); + radio(STR_C("Sculpt"), terrain_tool::sculpt, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it")); + ImGui::SameLine(); + radio(STR_C("Smooth"), terrain_tool::smooth, STR_C("LMB evens the terrain out under the brush")); + ImGui::SameLine(); + radio(STR_C("Level"), terrain_tool::level, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor")); + ImGui::SameLine(); + radio(STR_C("Paint"), terrain_tool::paint, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette")); + ImGui::SameLine(); + radio(STR_C("Restore"), terrain_tool::restore, STR_C("LMB takes back the touch-ups made over the tracks under the brush:\nthe terrain returns to the shape the modifiers (tracks) give it")); + ImGui::PopID(); + m_terrain_tool = static_cast(tool); - if (ImGui::Checkbox("Chunk edit mode (LMB add neighbour / Shift = delete)", &m_chunk_edit)) - if (m_chunk_edit) - m_terrain_sculpt = false; // mutually exclusive with sculpting - ImGui::Text("Grid chunks: %zu", m_grid_chunks.size()); - - ImGui::Separator(); - ImGui::TextUnformatted("Streaming (open world, follows camera)"); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputInt("Stream radius", &m_stream_radius); - m_stream_radius = std::clamp(m_stream_radius, 0, 16); - ImGui::Checkbox("Persist edits to disk (16-bit)", &m_stream_persist); - bool streaming = m_streamer.active(); - if (ImGui::Checkbox("Stream terrain", &streaming)) + if (ImGui::CollapsingHeader(STR_C("Chunks"), ImGuiTreeNodeFlags_DefaultOpen)) { - if (streaming) + if (spacing_combo(STR_C("Point spacing"), m_terrain_spacing)) + m_streamer.default_spacing(m_terrain_spacing); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Spacing of the grid points of the chunks made.\n" + "A chunk is 250 x 250 m; finer grids take more memory and time to draw.")); + ImGui::SetNextItemWidth(120.0f); + ImGui::InputFloat(STR_C("Base height (m)"), &m_terrain_baseheight, 0.0f, 0.0f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Height of the chunks made where there's no ground under them")); + ImGui::SetNextItemWidth(120.0f); + ImGui::InputInt(STR_C("Chunks along a side"), &m_terrain_add_count); + m_terrain_add_count = std::clamp(m_terrain_add_count, 1, 8); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Add around the camera"))) + add_chunks_around(m_terrain_add_count); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Makes the chunks missing around the camera, shaped as the ground under them")); + render_ground_conversion(); + if (dynamic_cast(m_node) != nullptr) { - // per-scenery chunk folder so chunks from different sceneries don't collide - std::string scenery = Global.SceneryFile; - auto const slash = scenery.find_last_of("/\\"); - if (slash != std::string::npos) - scenery = scenery.substr(slash + 1); - auto const dot = scenery.find_last_of('.'); - if (dot != std::string::npos) - scenery = scenery.substr(0, dot); - if (scenery.empty()) - scenery = "default"; - m_streamer.directory("editor_terrain/" + scenery); - - m_streamer.configure(m_terrain_cells, m_terrain_cellsize, m_stream_radius, - m_terrain_baseheight, std::string(m_terrain_texture)); - m_streamer.simplify(m_terrain_auto_optimize, m_terrain_simplify_error); - m_streamer.persist(m_stream_persist); - - // hand the authored grid chunks over to streaming: persist them to disk, then drop the - // in-memory meshes so the streamer owns residency (it loads them back within the radius) - for (auto &entry : m_grid_chunks) - if (entry.second) - m_streamer.save_chunk(entry.first.first, entry.first.second, *entry.second); - for (auto &entry : m_grid_chunks) - if (entry.second) - entry.second->destroy(); - m_grid_chunks.clear(); + if (ImGui::Button(STR_C("Capture the selected model as terrain"))) + capture_terrain(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The chunks under the model take its shape, and the model is removed")); } + } + + if (ImGui::CollapsingHeader(STR_C("Chunk resolution"), m_terrain_tool == terrain_tool::spacing ? ImGuiTreeNodeFlags_DefaultOpen : 0)) + { + // chunks removed since are no longer selected + for (auto it = m_terrain_selection.begin(); it != m_terrain_selection.end();) + it = m_streamer.exists(*it) ? std::next(it) : m_terrain_selection.erase(it); + if (m_terrain_selection.empty()) + ImGui::TextWrapped("%s", STR_C("No chunk selected. With the Spacing tool LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away.")); else { - m_streamer.clear(); // saves modified chunks before dropping them - } - m_streamer.active(streaming); - } - if (m_streamer.active()) - { - // radius / simplify / persist are safe to tweak live; chunk size/base are fixed at toggle - m_streamer.radius(m_stream_radius); - m_streamer.simplify(m_terrain_auto_optimize, m_terrain_simplify_error); - m_streamer.persist(m_stream_persist); - ImGui::Text("Resident chunks: %zu (dir: %s)", m_streamer.resident(), m_streamer.directory().c_str()); - } - - ImGui::Text("Patches: %zu", m_terrains.size()); - - // capture: sample the selected model's geometry into an editable patch and remove the original - if (dynamic_cast(m_node) != nullptr) - { - if (ImGui::Button("Capture selected model as terrain")) - capture_terrain(); - } - else - { - ImGui::TextDisabled("Capture: select a model instance first"); - } - - std::vector const terrains = active_terrains(); - if (!terrains.empty()) - { - ImGui::Separator(); - if (ImGui::Checkbox("Sculpt mode (LMB raise / Shift = lower)", &m_terrain_sculpt)) - if (m_terrain_sculpt) - m_chunk_edit = false; // mutually exclusive with chunk editing - ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Brush radius", &m_terrain_brush_radius); - if (m_terrain_brush_radius < 0.5f) - m_terrain_brush_radius = 0.5f; - ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Brush strength", &m_terrain_brush_strength); - - // one-shot nudge of the most recent manual patch at its centre, handy for a quick test - if (!m_terrains.empty()) - { - auto &terrain = m_terrains.back(); - glm::dvec3 const c = terrain->centre(); - if (ImGui::Button("Raise centre")) - terrain->sculpt(c.x, c.z, m_terrain_brush_radius, m_terrain_brush_strength); + bool coarser = false; + std::size_t points = 0, after = 0; + int const cells = heightmap::cells_for(m_terrain_convert_spacing); + ImGui::BeginChild("##chunkselection", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * std::min(6.0f, static_cast(m_terrain_selection.size()) + 0.5f)), true); + for (auto const &key : m_terrain_selection) + { + auto const centre = heightmap::chunk_corner(key) + glm::dvec2{heightmap::chunk_size * 0.5}; + auto const *chunk = m_streamer.terrain_at(centre.x, centre.y); + if (chunk == nullptr) + { + ImGui::TextDisabled(STR_C("%d,%d: not loaded"), key.first, key.second); + continue; + } + auto const &data = chunk->data(); + coarser |= cells < data.cells; + points += static_cast(data.side()) * data.side(); + after += static_cast(cells + 1) * (cells + 1); + auto const paint = data.paint > 0 ? format(STR_C(", paint every %.2f m"), heightmap::chunk_size / data.paint) : std::string(STR_C(", one material")); + auto const touchups = data.adjust.empty() ? std::string() : std::string(STR_C(", touch-ups")); + ImGui::Text("%d,%d: %s%s%s", key.first, key.second, spacing_label(static_cast(data.spacing())), paint.c_str(), touchups.c_str()); + } + ImGui::EndChild(); + spacing_combo(STR_C("Convert to"), m_terrain_convert_spacing); ImGui::SameLine(); - if (ImGui::Button("Lower centre")) - terrain->sculpt(c.x, c.z, m_terrain_brush_radius, -m_terrain_brush_strength); + if (ImGui::Button(STR_C("Convert"))) + convert_selected_chunks(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Clear the selection"))) + m_terrain_selection.clear(); + ImGui::Checkbox(STR_C("Paint every 0.5 m with 0.5 m spacing"), &m_terrain_convert_paint); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Chunks converted to 0.5 m get their paint samples every 0.5 m as well (every metre otherwise),\n" + "so the materials can be painted as finely as the ground is shaped")); + if (points > 0) + ImGui::TextDisabled(STR_C("Grid points: %zu -> %zu"), points, after); + if (coarser) + ImGui::TextWrapped("%s", STR_C("A coarser grid can't hold the shapes smaller than its spacing: they're averaged out. " + "The edges of the chunks keep their heights, so the neighbours still meet them. Ctrl+Z takes it back.")); } - - ImGui::Separator(); - ImGui::TextUnformatted("Optimize (mesh simplification, all patches)"); - ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Flatness tol (m)", &m_terrain_simplify_error); - if (m_terrain_simplify_error < 0.01f) - m_terrain_simplify_error = 0.01f; - ImGui::Checkbox("Auto-optimize after sculpt", &m_terrain_auto_optimize); - if (ImGui::Button("Optimize all")) - for (editor_terrain *t : terrains) - t->optimize(m_terrain_simplify_error); - ImGui::SameLine(); - if (ImGui::Button("Full-res all")) - for (editor_terrain *t : terrains) - t->unoptimize(); - - std::size_t tris = 0, full = 0; - for (editor_terrain *t : terrains) - { - tris += t->triangles(); - full += t->full_triangles(); - } - ImGui::Text("Triangles: %zu / %zu", tris, full); } + + if (terrain_brush() && ImGui::CollapsingHeader(STR_C("Brush"), ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat(STR_C("Radius (m)"), &m_terrain_brush_radius, 0.25f, 0.5f, 500.0f, "%.1f"); + m_terrain_brush_radius = std::max(0.5f, m_terrain_brush_radius); + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat(STR_C("Strength"), &m_terrain_brush_strength, 0.05f, 0.05f, 50.0f, "%.2f"); + m_terrain_brush_strength = std::max(0.05f, m_terrain_brush_strength); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Sculpting: metres per second at the middle of the brush; smoothing, levelling, painting and restoring: how fast they work")); + if (m_terrain_tool == terrain_tool::sculpt || m_terrain_tool == terrain_tool::smooth || m_terrain_tool == terrain_tool::level) + { + auto touchup = m_streamer.touch_up(); + if (ImGui::Checkbox(STR_C("Touch up over the tracks"), &touchup)) + m_streamer.touch_up(touchup); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("On: the brush shapes the terrain as it's seen, also where the tracks (modifiers) shape it;\n" + "the changes there are kept as touch-ups over the modifiers, and stay when the track changes.\n" + "The Restore tool takes them back.\n" + "Off: the brush shapes only the ground under the modifiers, the tracks keep their formation and slopes.")); + } + if (m_terrain_tool == terrain_tool::level) + { + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat(STR_C("Target height (m)"), &m_terrain_target, 0.05f, -1000.0f, 5000.0f, "%.2f"); + ImGui::SameLine(); + if (ImGui::Button(m_terrain_picking ? STR_C("Click in the view...") : STR_C("Pick"))) + m_terrain_picking = false == m_terrain_picking; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The next click takes the height of whatever is under the cursor: terrain, a track, a model.\nCtrl+LMB does the same at any time")); + char const *modes[]{STR_C("Raise and lower"), STR_C("Only raise"), STR_C("Only lower")}; + ImGui::SetNextItemWidth(160.0f); + ImGui::Combo("##levelmode", &m_terrain_level_mode, modes, IM_ARRAYSIZE(modes)); + ImGui::Checkbox(STR_C("Target where the stroke starts"), &m_terrain_target_from_stroke); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Each stroke takes the height of the terrain under the cursor where it starts as its target,\nand drags it along: the ground stroked over is levelled with that point")); + } + } + + if (ImGui::CollapsingHeader(STR_C("Materials"), m_terrain_tool == terrain_tool::paint ? ImGuiTreeNodeFlags_DefaultOpen : 0)) + { + auto const &layers{m_streamer.manifest().layers}; + m_terrain_layer = std::clamp(m_terrain_layer, 0, static_cast(layers.size()) - 1); + for (std::size_t i = 0; i < layers.size(); ++i) + { + auto label{layers[i].size > 0.f ? format("%zu: %s (%.1f m)", i, layers[i].material.c_str(), layers[i].size) : format("%zu: %s", i, layers[i].material.c_str())}; + if (layers[i].rotation != 0.f) + label += format(STR_C(", turned %.0f deg"), layers[i].rotation); + if (ImGui::Selectable(label.c_str(), static_cast(i) == m_terrain_layer)) + m_terrain_layer = static_cast(i); + // a material dropped on an entry takes its place, on every chunk painted with it + std::string dropped; + if (material_drop(dropped)) + m_streamer.layer_material(static_cast(i), dropped); + } + if (false == layers.empty()) + { + // the values are kept while dragged and given to the palette once the dragging ends; + // read back from the palette every frame, the release would hand it the old value + if (false == m_terrain_layer_size_dragged) + m_terrain_layer_size = layers[m_terrain_layer].size; + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat(STR_C("Repeats every (m)"), &m_terrain_layer_size, 0.1f, 0.0f, 200.0f, "%.1f"); + m_terrain_layer_size_dragged = ImGui::IsItemActive(); + if (ImGui::IsItemDeactivatedAfterEdit()) + m_streamer.layer_size(static_cast(m_terrain_layer), m_terrain_layer_size); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Metres the textures of the chosen material repeat at; 0: as the material gives it, or 8 m")); + if (false == m_terrain_layer_rotation_dragged) + m_terrain_layer_rotation = layers[m_terrain_layer].rotation; + ImGui::SetNextItemWidth(120.0f); + ImGui::SliderFloat(STR_C("Turned by (deg)"), &m_terrain_layer_rotation, -180.0f, 180.0f, "%.0f"); + m_terrain_layer_rotation_dragged = ImGui::IsItemActive(); + if (ImGui::IsItemDeactivatedAfterEdit()) + m_streamer.layer_rotation(static_cast(m_terrain_layer), m_terrain_layer_rotation); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Angle the textures of the chosen material are turned by, on every chunk painted with it.\n" + "Ctrl+click to type it. To have the material turned in one place only, fill it there with an angle of its own.")); + } + ImGui::SetNextItemWidth(160.0f); + ImGui::InputText(STR_C("Material"), m_terrain_material, IM_ARRAYSIZE(m_terrain_material)); + material_drop(m_terrain_material, IM_ARRAYSIZE(m_terrain_material)); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Add to the palette")) && m_terrain_material[0] != '\0') + m_terrain_layer = m_streamer.layer(m_terrain_material, m_terrain_material_size); + ImGui::TextDisabled("%s", STR_C("New chunks are covered with the first material. A chunk holds up to 8 of them. " + "A texture dragged from the browser onto an entry of the palette takes its place.")); + } + + if (ImGui::CollapsingHeader(STR_C("Fill"), m_terrain_tool == terrain_tool::fill ? ImGuiTreeNodeFlags_DefaultOpen : 0)) + { + auto const &layers{m_streamer.manifest().layers}; + if (false == layers.empty()) + { + auto const &chosen{layers[std::clamp(m_terrain_layer, 0, static_cast(layers.size()) - 1)]}; + ImGui::Text(STR_C("Material: %s (chosen in the palette below)"), chosen.material.c_str()); + } + ImGui::Text(STR_C("Outline: %zu points"), m_paint_outline.size()); + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat(STR_C("Edge blur (m)"), &m_paint_feather, 0.1f, 0.0f, 100.0f, m_paint_feather > 0.0f ? "%.1f" : STR_C("sharp")); + m_paint_feather = std::max(0.0f, m_paint_feather); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Metres across the outline the material blends in over, half of them inside the area.\n" + "0: a sharp edge, as sharp as the paint samples go: every metre, or every 0.5 m in chunks converted to 0.5 m with fine paint")); + ImGui::SetNextItemWidth(120.0f); + ImGui::SliderFloat(STR_C("Texture turned by (deg)"), &m_paint_rotation, -180.0f, 180.0f, "%.0f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The area gets the material turned by this angle: an entry of the palette with the material at that angle\n" + "is used, or added. Ctrl+click to type it")); + ImGui::BeginDisabled(m_paint_outline.size() < 3 || layers.empty()); + if (ImGui::Button(STR_C("Fill the area"))) + fill_terrain_area(); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Clear the outline##fill"))) + m_paint_outline.clear(); + } + + if (ImGui::CollapsingHeader(STR_C("Water"), m_terrain_tool == terrain_tool::water ? ImGuiTreeNodeFlags_DefaultOpen : 0)) + { + if (m_terrain_tool == terrain_tool::water) + { + ImGui::Text(STR_C("Outline: %zu points"), m_water_points.size()); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::DragFloat(STR_C("Level (m)"), &m_water_level, 0.01f, -1000.0f, 5000.0f, "%.2f")) + m_water_level_edited = true; + ImGui::SetNextItemWidth(160.0f); + ImGui::InputText(STR_C("Material##water"), m_water_material, IM_ARRAYSIZE(m_water_material)); + material_drop(m_water_material, IM_ARRAYSIZE(m_water_material)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Empty: %s, the water material of the terrain"), m_streamer.manifest().water_material.c_str()); + ImGui::BeginDisabled(m_water_points.size() < 3); + if (ImGui::Button(STR_C("Make the water"))) + { + auto water{m_streamer.water()}; + heightmap::water_body body; + body.name = format("water %zu", water.size() + 1); + body.level = m_water_level; + body.material = m_water_material; + for (auto const &point : m_water_points) + body.outline.emplace_back(point.x, point.z); + water.push_back(std::move(body)); + m_streamer.water(std::move(water)); + m_water_selected = static_cast(m_streamer.water().size()) - 1; + m_water_points.clear(); + m_water_level_edited = false; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Clear the outline"))) + { + m_water_points.clear(); + m_water_level_edited = false; + } + } + auto const &water{m_streamer.water()}; + std::optional removed; + std::optional> dropped; + for (std::size_t i = 0; i < water.size(); ++i) + { + ImGui::PushID(static_cast(i)); + auto const material = water[i].material.empty() ? std::string(STR_C("default material")) : water[i].material; + if (ImGui::Selectable(format("%s, %.2f m, %zu points, %s", water[i].name.c_str(), water[i].level, water[i].outline.size(), material.c_str()).c_str(), static_cast(i) == m_water_selected)) + m_water_selected = static_cast(i); + // a texture dropped on an entry becomes the material of that water + std::string name; + if (material_drop(name)) + dropped = std::make_pair(i, name); + ImGui::PopID(); + } + if (dropped) + { + auto changed{water}; + changed[dropped->first].material = dropped->second; + m_streamer.water(std::move(changed)); + m_water_selected = static_cast(dropped->first); + m_water_material_of = -1; + } + if (m_water_selected >= 0 && m_water_selected < static_cast(water.size())) + { + // the level is changed in the water once the dragging ends + if (false == m_water_level_dragged) + m_water_selected_level = static_cast(water[m_water_selected].level); + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat(STR_C("Level of the chosen (m)"), &m_water_selected_level, 0.01f, -1000.0f, 5000.0f, "%.2f"); + m_water_level_dragged = ImGui::IsItemActive(); + if (ImGui::IsItemDeactivatedAfterEdit()) + { + auto changed{water}; + changed[m_water_selected].level = m_water_selected_level; + m_streamer.water(std::move(changed)); + } + // the material and the repeat of its texture, changed once the field is left (or a texture dropped on it) + if (m_water_material_of != m_water_selected) + { + std::snprintf(m_water_selected_material, sizeof(m_water_selected_material), "%s", water[m_water_selected].material.c_str()); + m_water_selected_size = water[m_water_selected].size; + m_water_material_of = m_water_selected; + } + bool retexture = false; + ImGui::SetNextItemWidth(160.0f); + ImGui::InputText(STR_C("Material of the chosen"), m_water_selected_material, IM_ARRAYSIZE(m_water_selected_material)); + retexture |= ImGui::IsItemDeactivatedAfterEdit(); + retexture |= material_drop(m_water_selected_material, IM_ARRAYSIZE(m_water_selected_material)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("A material or a texture (drag one from the texture browser).\nEmpty: %s, the water material of the terrain"), m_streamer.manifest().water_material.c_str()); + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat(STR_C("Texture repeats every (m)##water"), &m_water_selected_size, 0.1f, 0.0f, 500.0f, "%.1f"); + m_water_selected_size = std::max(0.0f, m_water_selected_size); + retexture |= ImGui::IsItemDeactivatedAfterEdit(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Metres the texture of the water repeats at; 0: as the material gives it, or 8 m")); + if (retexture) + { + auto changed{water}; + changed[m_water_selected].material = m_water_selected_material; + changed[m_water_selected].size = m_water_selected_size; + m_streamer.water(std::move(changed)); + } + if (ImGui::Button(STR_C("Remove the chosen water"))) + removed = static_cast(m_water_selected); + } + if (removed) + { + auto changed{water}; + changed.erase(changed.begin() + *removed); + m_streamer.water(std::move(changed)); + m_water_selected = -1; + m_water_material_of = -1; + } + // the material of the bodies of water which don't name their own + if (false == m_water_default_edited) + std::snprintf(m_water_default_material, sizeof(m_water_default_material), "%s", m_streamer.manifest().water_material.c_str()); + ImGui::SetNextItemWidth(160.0f); + ImGui::InputText(STR_C("Default water material"), m_water_default_material, IM_ARRAYSIZE(m_water_default_material)); + m_water_default_edited = ImGui::IsItemActive(); + bool redefault = ImGui::IsItemDeactivatedAfterEdit(); + redefault |= material_drop(m_water_default_material, IM_ARRAYSIZE(m_water_default_material)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Material of the bodies of water which don't name their own")); + if (redefault) + m_streamer.water_material(m_water_default_material); + } + + if (ImGui::CollapsingHeader(STR_C("Modifiers"))) + { + auto const &modifiers{m_streamer.modifiers()}; + if (modifiers.empty()) + ImGui::TextWrapped("%s", STR_C("None. The vertical profile of the tracks makes them: the terrain follows the formation and the slopes of the route.")); + std::optional removed, baked; + for (auto const &modifier : modifiers) + { + ImGui::PushID(modifier.name.c_str()); + ImGui::TextUnformatted(format("%s, %zu points", modifier.name.c_str(), modifier.points.size()).c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Bake"))) + baked = modifier.name; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The ground keeps the shape the modifier gives it, and the modifier is removed:\n" + "from then on the terrain there is shaped by hand like anywhere else, and no longer follows the track")); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Remove"))) + removed = modifier.name; + ImGui::PopID(); + } + if (auto const touchups = m_streamer.touched_up(); touchups > 0) + ImGui::TextDisabled(STR_C("Touch-ups over the modifiers: %zu points (of the chunks in memory)"), touchups); + if (baked) + { + m_streamer.bake_modifier(*baked); + ground_tiles.clear(); + forget_ground_shapes(); + } + if (removed) + { + m_streamer.remove_modifier(*removed); + ground_tiles.clear(); + forget_ground_shapes(); + } + } + + if (ImGui::CollapsingHeader(STR_C("Drawing and files"))) + { + auto detail{m_streamer.detail()}; + ImGui::SetNextItemWidth(160.0f); + if (ImGui::SliderFloat(STR_C("Detail"), &detail, 30.0f, 400.0f, "%.0f")) + m_streamer.detail(detail); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("How far the full grid of the chunks reaches: metres of distance per metre of the drawn grid. Further away the grid is coarser")); + bool compress{m_streamer.manifest().compress}; + if (ImGui::Checkbox(STR_C("Compress the files (zstd)"), &compress)) + m_streamer.compress(compress); + ImGui::TextDisabled("%s", STR_C("The terrain is written with the scenery (Ctrl+S).")); + } + if (false == m_terrain_status.empty()) + ImGui::TextWrapped("%s", m_terrain_status.c_str()); +} + +void editor_mode::draw_terrain_overlay() +{ + if (false == m_streamer.active()) + return; + screen_projection const projection; + auto *drawlist = ImGui::GetBackgroundDrawList(); + glm::dvec3 const offset = GfxRenderer->Mouse_Position(); + bool const hovering = glm::length(offset) > 1e-3 && false == ImGui::GetIO().WantCaptureMouse; + glm::dvec3 const cursor = Camera.Pos + offset; + auto const ground = [&](double const X, double const Z, double const Fallback) { + double height; + return m_streamer.height_at(X, Z, height) ? height : Fallback; + }; + auto const polyline = [&](std::vector const &Points, ImU32 const Color, bool const Closed, float const Thickness) { + auto const count = Points.size(); + for (std::size_t i = 0; i + (Closed ? 0 : 1) < count; ++i) + { + ImVec2 from, to; + if (projection.project(Points[i], from) && projection.project(Points[(i + 1) % count], to)) + drawlist->AddLine(from, to, Color, Thickness); + } + }; + + if (terrain_brush() && hovering) + { + std::vector circle; + for (int i = 0; i < 64; ++i) + { + auto const angle = i * 6.283185307179586 / 64.0; + auto const x = cursor.x + std::cos(angle) * m_terrain_brush_radius; + auto const z = cursor.z + std::sin(angle) * m_terrain_brush_radius; + circle.emplace_back(x, ground(x, z, cursor.y) + 0.1, z); + } + auto const color = m_terrain_tool == terrain_tool::paint ? IM_COL32(120, 230, 120, 220) + : m_terrain_tool == terrain_tool::smooth ? IM_COL32(120, 200, 255, 220) + : m_terrain_tool == terrain_tool::restore ? IM_COL32(230, 120, 230, 220) + : IM_COL32(255, 180, 80, 220); + polyline(circle, color, true, 2.0f); + if (m_terrain_tool == terrain_tool::level) + { + // the target: the rim of the brush at its height, joined with the ground, and the height by the cursor + auto const target = static_cast(m_terrain_target); + std::vector level; + for (auto const &point : circle) + level.emplace_back(point.x, target, point.z); + ImU32 const levelcolor = IM_COL32(255, 230, 90, 230); + polyline(level, levelcolor, true, 1.5f); + for (std::size_t i = 0; i < circle.size(); i += circle.size() / 8) + { + ImVec2 from, to; + if (projection.project(circle[i], from) && projection.project(level[i], to)) + drawlist->AddLine(from, to, IM_COL32(255, 230, 90, 120), 1.0f); + } + ImVec2 label; + if (projection.project(glm::dvec3{cursor.x, target, cursor.z}, label)) + { + double ground{cursor.y}; + m_streamer.height_at(cursor.x, cursor.z, ground); + auto const text = format("%.2f m (%+.2f)", target, target - ground); + drawlist->AddText(ImVec2(label.x + 12.0f, label.y - 8.0f), levelcolor, text.c_str()); + } + } + } + // outline of a chunk, laid over the ground + auto const chunk_outline = [&](heightmap::chunk_key const &Key, double const Fallback, ImU32 const Color, float const Thickness) { + auto const low = heightmap::chunk_corner(Key); + std::vector square; + auto const edge = [&](glm::dvec2 const &From, glm::dvec2 const &To) { + for (int i = 0; i < 25; ++i) + { + auto const point = From + (To - From) * (i / 25.0); + square.emplace_back(point.x, ground(point.x, point.y, Fallback) + 0.2, point.y); + } + }; + auto const size = heightmap::chunk_size; + edge(low, low + glm::dvec2{size, 0.0}); + edge(low + glm::dvec2{size, 0.0}, low + glm::dvec2{size}); + edge(low + glm::dvec2{size}, low + glm::dvec2{0.0, size}); + edge(low + glm::dvec2{0.0, size}, low); + polyline(square, Color, true, Thickness); + }; + if (m_terrain_tool == terrain_tool::spacing) + for (auto const &key : m_terrain_selection) + chunk_outline(key, cursor.y, IM_COL32(90, 220, 255, 230), 3.0f); + if ((m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing) && hovering) + { + auto const key = heightmap::chunk_at(cursor.x, cursor.z); + auto const exists = m_streamer.exists(key); + chunk_outline(key, cursor.y, exists ? IM_COL32(255, 255, 255, 200) : IM_COL32(255, 230, 90, 200), 2.0f); + ImVec2 label; + if (exists && projection.project(cursor, label)) + { + auto const *chunk = m_streamer.terrain_at(cursor.x, cursor.z); + if (chunk != nullptr) + { + auto const &data = chunk->data(); + auto text = std::string(spacing_label(static_cast(data.spacing()))); + if (m_terrain_tool == terrain_tool::spacing) + { + if (data.paint > 0) + text += format(STR_C(", paint every %.2f m"), heightmap::chunk_size / data.paint); + if (m_terrain_selection.count(key) != 0) + text += STR(" (selected)"); + } + drawlist->AddText(ImVec2(label.x + 14.0f, label.y - 14.0f), IM_COL32(255, 255, 255, 230), text.c_str()); + } + } + } + // the bodies of water, and the one being outlined + auto const &water = m_streamer.water(); + for (std::size_t i = 0; i < water.size(); ++i) + { + std::vector outline; + for (auto const &point : water[i].outline) + outline.emplace_back(point.x, water[i].level, point.y); + polyline(outline, static_cast(i) == m_water_selected ? IM_COL32(120, 200, 255, 255) : IM_COL32(60, 120, 220, 160), true, static_cast(i) == m_water_selected ? 2.5f : 1.5f); + } + if (m_terrain_tool == terrain_tool::fill && false == m_paint_outline.empty()) + { + // laid over the ground, the edges followed every few metres + std::vector outline; + auto const closed = m_paint_outline.size() >= 3; + for (std::size_t i = 0; i + (closed ? 0 : 1) < m_paint_outline.size(); ++i) + { + auto const &from = m_paint_outline[i]; + auto const &to = m_paint_outline[(i + 1) % m_paint_outline.size()]; + auto const steps = std::clamp(static_cast(glm::distance(glm::dvec2{from.x, from.z}, glm::dvec2{to.x, to.z}) / 4.0), 1, 200); + for (int k = 0; k < steps; ++k) + { + auto const point = from + (to - from) * (static_cast(k) / steps); + outline.emplace_back(point.x, ground(point.x, point.z, point.y) + 0.15, point.z); + } + } + if (false == closed) + outline.push_back(m_paint_outline.back() + glm::dvec3{0.0, 0.15, 0.0}); + polyline(outline, IM_COL32(250, 200, 90, 255), false, 2.0f); + for (auto const &point : m_paint_outline) + { + ImVec2 dot; + if (projection.project(point + glm::dvec3{0.0, 0.15, 0.0}, dot)) + drawlist->AddCircleFilled(dot, 4.0f, IM_COL32(250, 200, 90, 255)); + } + } + if (m_terrain_tool == terrain_tool::water && false == m_water_points.empty()) + { + std::vector outline; + for (auto const &point : m_water_points) + outline.emplace_back(point.x, m_water_level, point.z); + polyline(outline, IM_COL32(120, 220, 255, 255), m_water_points.size() >= 3, 2.0f); + for (auto const &point : outline) + { + ImVec2 dot; + if (projection.project(point, dot)) + drawlist->AddCircleFilled(dot, 4.0f, IM_COL32(120, 220, 255, 255)); + } + } +} + +void editor_mode::load_orthophoto_settings() +{ + std::string const scenery = scenery_stem(); + if (scenery == m_orthophoto_scenery) + return; + m_orthophoto_scenery = scenery; + + auto const &stored = EditorSettings.orthophoto(); + editor_orthophoto::config config = m_orthophoto.settings(); + config.radius = stored.radius; + config.height = stored.height; + config.opacity = stored.opacity; + config.year = stored.year; + config.hires = stored.hires; + config.drape = stored.drape; + config.lift = stored.lift; + config.in_scene = stored.in_scene; + config.north = 0.0; + config.east = 0.0; + m_georeference_read = false; + read_georeference(); + if (false == std::as_const(EditorSettings).orthophoto_origin(scenery, config.north, config.east) && m_georeference) + { + config.north = m_georeference_origin.x; + config.east = m_georeference_origin.y; + } + m_orthophoto.settings(config); + m_orthophoto_origin_edit = {config.north, config.east}; +} + +void editor_mode::save_orthophoto_settings() +{ + editor_orthophoto::config const &config = m_orthophoto.settings(); + auto &stored = EditorSettings.orthophoto(); + stored.radius = config.radius; + stored.height = config.height; + stored.opacity = config.opacity; + stored.year = config.year; + stored.hires = config.hires; + stored.drape = config.drape; + stored.lift = config.lift; + stored.in_scene = config.in_scene; + if (config.north != 0.0 || config.east != 0.0) + EditorSettings.orthophoto_origin(m_orthophoto_scenery, config.north, config.east); + EditorSettings.save(); +} + +void editor_mode::render_orthophoto_ui() +{ + load_orthophoto_settings(); + + editor_orthophoto::config config = m_orthophoto.settings(); + bool changed = false; // apply to the layer + bool persist = false; // and store in the editor settings + + bool enabled = m_orthophoto.enabled(); + if (ImGui::Checkbox(STR_C("Show orthophoto (geoportal.gov.pl)"), &enabled)) + m_orthophoto.enabled(enabled); + + ImGui::Separator(); + ImGui::TextUnformatted(STR_C("Scenery origin (0,0,0) in PUWG 1992 / EPSG:2180")); + // geodetic convention, as displayed by geoportal.gov.pl: X grows north, Y grows east. + // applied once editing ends, so half-typed numbers don't trigger downloads + ImGui::SetNextItemWidth(160.0f); + ImGui::InputDouble(STR_C("X (northing, m)"), &m_orthophoto_origin_edit.x, 0.0, 0.0, "%.2f"); + bool const northedited = ImGui::IsItemDeactivatedAfterEdit(); + ImGui::SetNextItemWidth(160.0f); + ImGui::InputDouble(STR_C("Y (easting, m)"), &m_orthophoto_origin_edit.y, 0.0, 0.0, "%.2f"); + bool const eastedited = ImGui::IsItemDeactivatedAfterEdit(); + if (northedited || eastedited) + { + config.north = m_orthophoto_origin_edit.x; + config.east = m_orthophoto_origin_edit.y; + changed = persist = true; + } + if (m_georeference) + { + ImGui::TextDisabled("%s", m_georeference_line.c_str()); + ImGui::TextDisabled(STR_C("in the scenery file: Y (easting) %.0f m, X (northing) %.0f m"), m_georeference_origin.y, m_georeference_origin.x); + if (config.north != m_georeference_origin.x || config.east != m_georeference_origin.y) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Use it"))) + { + config.north = m_orthophoto_origin_edit.x = m_georeference_origin.x; + config.east = m_orthophoto_origin_edit.y = m_georeference_origin.y; + changed = persist = true; + } + } + } + if (config.north == 0.0 && config.east == 0.0) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), STR_C("Enter the origin of this scenery first")); + else + ImGui::TextDisabled(STR_C("Camera at X %.1f Y %.1f"), config.north + Camera.Pos.z, config.east - Camera.Pos.x); + + ImGui::Separator(); + ImGui::SetNextItemWidth(160.0f); + changed |= ImGui::SliderInt(STR_C("Distance (tiles)"), &config.radius, 0, editor_orthophoto::max_radius); + persist |= ImGui::IsItemDeactivatedAfterEdit(); + ImGui::SameLine(); + int const span = static_cast((2 * config.radius + 1) * editor_orthophoto::tile_size); + ImGui::TextDisabled("%d x %d m", span, span); + + // ImGui 1.73 has no public disabled state yet, the internal item flag does the job + auto const begin_disabled = [](bool Disabled) { + if (!Disabled) + return; + ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); + }; + auto const end_disabled = [](bool Disabled) { + if (!Disabled) + return; + ImGui::PopStyleVar(); + ImGui::PopItemFlag(); + }; + + if (ImGui::Checkbox(STR_C("Fit to terrain"), &config.drape)) + changed = persist = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Lays the imagery onto the ground geometry (terrain shapes, terrain tile models, heightmap terrain).\n" + "Places without any ground use the height below.")); + if (config.drape) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Refit"))) + { + // the ground was edited: what's kept of it has to go as well + ground_tiles.clear(); + forget_ground_shapes(); + m_orthophoto.refit(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Samples the ground again, after it was edited")); + } + + begin_disabled(!config.drape); + ImGui::SetNextItemWidth(160.0f); + changed |= ImGui::DragFloat(STR_C("Lift above ground (m)"), &config.lift, 0.01f, 0.0f, 50.0f, "%.2f"); + persist |= ImGui::IsItemDeactivatedAfterEdit(); + end_disabled(!config.drape); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Raises the fitted imagery above the ground. Keep it below the objects you want to see:\n" + "in the scene everything lower than this ends up under the imagery.")); + + begin_disabled(config.drape); + ImGui::SetNextItemWidth(160.0f); + changed |= ImGui::DragFloat(STR_C("Height (m)"), &config.height, 0.1f, -1000.0f, 3000.0f, "%.2f"); + persist |= ImGui::IsItemDeactivatedAfterEdit(); + end_disabled(config.drape); + + if (ImGui::Checkbox(STR_C("Covered by objects and terrain"), &config.in_scene)) + changed = persist = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\n" + "It is lit like the scene. Fully opaque, mouse placement lands on it as on any surface;\n" + "translucent, it doesn't catch the mouse.\n" + "Not available with the Better Renderer, which doesn't pick up geometry added in the editor.")); + + ImGui::SetNextItemWidth(160.0f); + changed |= ImGui::SliderFloat(STR_C("Opacity"), &config.opacity, 0.0f, 1.0f, "%.2f"); + persist |= ImGui::IsItemDeactivatedAfterEdit(); + if (config.in_scene && ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("In the scene the opacity is part of the textures: the tiles are read again from the cache\n" + "shortly after the slider stops, which takes a moment.")); + + // newest imagery, or the newest imagery taken up to the end of the selected year + int const thisyear = static_cast(std::chrono::year_month_day{std::chrono::floor(std::chrono::system_clock::now())}.year()); + std::string const yearlabel = config.year ? std::to_string(config.year) : std::string(STR_C("Newest")); + ImGui::SetNextItemWidth(160.0f); + if (ImGui::BeginCombo("Photo year", yearlabel.c_str())) + { + if (ImGui::Selectable(STR_C("Newest"), config.year == 0)) + { + config.year = 0; + changed = persist = true; + } + for (int year = thisyear; year >= editor_orthophoto::oldest_year; --year) + { + if (ImGui::Selectable(std::to_string(year).c_str(), config.year == year)) + { + config.year = year; + changed = persist = true; + } + } + ImGui::EndCombo(); + } + + if (ImGui::Checkbox(STR_C("4K tiles where available"), &config.hires)) + changed = persist = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("4096 px tiles (~6 cm/px) from the high-resolution orthophoto, for the tiles around the camera.\n" + "Areas it doesn't cover use the standard imagery. Each 4K tile takes ~85 MB of video memory.")); + + if (changed) + m_orthophoto.settings(config); + if (persist) + save_orthophoto_settings(); + + ImGui::Separator(); + auto const stats = m_orthophoto.stats(); + ImGui::Text(STR_C("Tiles: %d shown, %d loading, %d failed"), stats.resident, stats.loading, stats.failed); + if (stats.failed > 0) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Retry"))) + m_orthophoto.retry_failed(); + } + if (!editor_orthophoto::can_download()) + ImGui::TextDisabled(STR_C("This build has no HTTP client, only cached tiles are shown")); + ImGui::TextDisabled(STR_C("Cache: %s"), editor_orthophoto::cache_directory().c_str()); + if (ImGui::Button(STR_C("Clear cache"))) + m_orthophoto.clear_cache(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Deletes downloaded tiles from disk. They are fetched again around the camera.")); + ImGui::TextDisabled(STR_C("Imagery: GUGiK, geoportal.gov.pl")); +} + +void editor_mode::draw_orthophoto() +{ + if (!m_orthophoto.enabled()) + return; + // the camera was just published to the renderer (update_camera), so build the view from it rather than + // from the renderer's previous frame; camera-relative rotation, same projection as the other overlays + glm::dmat4 viewmatrix{1.0}; + Camera.SetMatrix(viewmatrix); + glm::mat4 const view = glm::mat4(glm::mat3(viewmatrix)); + ImGuiIO const &io = ImGui::GetIO(); + float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; + m_orthophoto.draw(editor_mode::projection_matrix(aspect) * view, Camera.Pos, io.DisplaySize.x, io.DisplaySize.y); +} + +void editor_mode::draw_ortho_compass() const +{ + if (false == Global.EditorOrtho) + return; + screen_projection const projection; + ImVec2 north_from, north_to, east_from, east_to; + glm::dvec3 const eye{Camera.Pos}; + if (false == projection.project(eye + glm::dvec3{0.0, 0.0, -80.0}, north_from) || false == projection.project(eye + glm::dvec3{0.0, 0.0, 80.0}, north_to) || + false == projection.project(eye + glm::dvec3{80.0, 0.0, 0.0}, east_from) || false == projection.project(eye + glm::dvec3{-80.0, 0.0, 0.0}, east_to)) + return; + auto const dir = [](ImVec2 const From, ImVec2 const To) { + ImVec2 const d{To.x - From.x, To.y - From.y}; + auto const length{std::sqrt(d.x * d.x + d.y * d.y)}; + return length > 1e-3f ? ImVec2{d.x / length, d.y / length} : ImVec2{0.0f, -1.0f}; + }; + auto const north{dir(north_from, north_to)}; + auto const east{dir(east_from, east_to)}; + ImVec2 const south{-north.x, -north.y}; + ImVec2 const west{-east.x, -east.y}; + + ImGuiIO const &io = ImGui::GetIO(); + float const radius{36.0f}; + float const margin{16.0f}; + ImVec2 const center{io.DisplaySize.x - margin - radius, margin + radius}; + auto *drawlist{ImGui::GetForegroundDrawList()}; + drawlist->AddCircleFilled(center, radius + 14.0f, IM_COL32(0, 0, 0, 180), 48); + drawlist->AddCircle(center, radius - 6.0f, IM_COL32(255, 255, 255, 50), 48, 1.2f); + drawlist->AddCircleFilled(center, 3.0f, IM_COL32(255, 255, 255, 200), 12); + + auto const at = [&](ImVec2 const Unit, float const Length) { return ImVec2{center.x + Unit.x * Length, center.y + Unit.y * Length}; }; + ImVec2 const side{-north.y * 5.0f, north.x * 5.0f}; + drawlist->AddTriangleFilled(at(north, radius - 14.0f), ImVec2{center.x + side.x, center.y + side.y}, ImVec2{center.x - side.x, center.y - side.y}, IM_COL32(230, 70, 55, 255)); + drawlist->AddTriangleFilled(at(south, radius - 18.0f), ImVec2{center.x + side.x, center.y + side.y}, ImVec2{center.x - side.x, center.y - side.y}, IM_COL32(210, 210, 210, 210)); + + auto const letter = [&](char const *Text, ImVec2 const Unit, ImU32 const Color) { + auto const size{ImGui::CalcTextSize(Text)}; + auto const p{at(Unit, radius + 2.0f)}; + drawlist->AddText(ImVec2(p.x - size.x * 0.5f, p.y - size.y * 0.5f), Color, Text); + }; + letter("N", north, IM_COL32(255, 90, 70, 255)); + letter("E", east, IM_COL32(230, 230, 230, 230)); + letter("S", south, IM_COL32(190, 190, 190, 180)); + letter("W", west, IM_COL32(230, 230, 230, 230)); +} + +void editor_mode::forget_ground() +{ + ground_tiles.clear(); + forget_ground_shapes(); } editor_terrain *editor_mode::terrain_at(double X, double Z) { - for (auto &terrain : m_terrains) - if (terrain && terrain->contains(X, Z)) - return terrain.get(); - double const size = chunk_grid_size(); - auto const it = m_grid_chunks.find({static_cast(std::floor(X / size)), static_cast(std::floor(Z / size))}); - if (it != m_grid_chunks.end() && it->second && it->second->contains(X, Z)) - return it->second.get(); return m_streamer.terrain_at(X, Z); } std::vector editor_mode::active_terrains() { std::vector out; - out.reserve(m_terrains.size() + m_grid_chunks.size() + m_streamer.resident()); - for (auto &terrain : m_terrains) - if (terrain) - out.push_back(terrain.get()); - for (auto &entry : m_grid_chunks) - if (entry.second) - out.push_back(entry.second.get()); + out.reserve(m_streamer.resident()); m_streamer.collect(out); return out; } -void editor_mode::add_grid_chunk(int Cx, int Cz) +bool editor_mode::ensure_terrain() { - std::pair const key{Cx, Cz}; - if (m_grid_chunks.count(key)) - return; // already occupied - - double const size = chunk_grid_size(); - int const cells = std::clamp(m_terrain_cells, 1, 256); - glm::dvec3 const center((Cx + 0.5) * size, static_cast(m_terrain_baseheight), (Cz + 0.5) * size); - - auto terrain = std::make_unique(); - if (!terrain->create(center, cells, m_terrain_cellsize, std::string(m_terrain_texture))) - return; - if (m_terrain_auto_optimize) - terrain->optimize(m_terrain_simplify_error); - m_grid_chunks[key] = std::move(terrain); + if (m_streamer.active()) + return true; + auto const name = scenery_stem(); + if (name.empty()) + return false; + m_streamer.open(name); + m_terrain_spacing = m_streamer.manifest().spacing; + return m_streamer.active(); } -void editor_mode::remove_grid_chunk(int Cx, int Cz) +bool editor_mode::add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer) { - auto const it = m_grid_chunks.find({Cx, Cz}); - if (it == m_grid_chunks.end()) - return; - if (it->second) - it->second->destroy(); - m_grid_chunks.erase(it); -} - -void editor_mode::handle_chunk_edit_click(bool DeleteMode) -{ - // world point under the cursor; must land on existing geometry to give a valid depth - glm::dvec3 const world = Camera.Pos + GfxRenderer->Mouse_Position(); - double const size = chunk_grid_size(); - int const cx = static_cast(std::floor(world.x / size)); - int const cz = static_cast(std::floor(world.z / size)); - bool const streaming = m_streamer.active(); - - if (DeleteMode) - { - if (streaming) - m_streamer.remove_chunk(cx, cz); - else - remove_grid_chunk(cx, cz); - return; - } - - // if the clicked cell holds a chunk, target the neighbour nearest the clicked edge (the empty - // side); otherwise fill the clicked cell - bool const occupied = streaming - ? m_streamer.terrain_at(world.x, world.z) != nullptr - : m_grid_chunks.count({cx, cz}) > 0; - int tcx = cx, tcz = cz; - if (occupied) - { - double const lx = world.x - cx * size, lz = world.z - cz * size; - double const dw = lx, de = size - lx, dn = lz, ds = size - lz; - double const nearest = std::min({dw, de, dn, ds}); - if (nearest == dw) - tcx = cx - 1; - else if (nearest == de) - tcx = cx + 1; - else if (nearest == dn) - tcz = cz - 1; - else - tcz = cz + 1; - } - - if (streaming) - m_streamer.add_chunk(tcx, tcz); - else - add_grid_chunk(tcx, tcz); -} - -void editor_mode::create_chunked_terrain() -{ - int const chunks = std::clamp(m_terrain_chunks, 1, 32); - double const size = chunk_grid_size(); - - // snap the field to the global chunk grid (so it aligns with manual/streamed chunks), centred - // on the camera's chunk - int const ccx = static_cast(std::floor(Camera.Pos.x / size)); - int const ccz = static_cast(std::floor(Camera.Pos.z / size)); - int const half = chunks / 2; - - int created = 0; - for (int dz = 0; dz < chunks; ++dz) - for (int dx = 0; dx < chunks; ++dx) + if (m_streamer.exists(Key)) + return false; + auto const low = heightmap::chunk_corner(Key); + auto const high = low + glm::dvec2{heightmap::chunk_size}; + // the ground under the chunk, the edges of the neighbouring chunks included, so the new one meets them + std::vector triangles; + gather_ground_triangles(low - glm::dvec2{1.0}, high + glm::dvec2{1.0}, true, 50.0f, triangles, true); + triangle_grid const ground(std::move(triangles), low - glm::dvec2{1.0}, high + glm::dvec2{1.0}); + auto const cells = heightmap::cells_for(Spacing); + auto const side = static_cast(cells) + 1; + auto const step = heightmap::chunk_size / cells; + std::vector heights(side * side, 0.0); + std::vector found(side * side, 0); + bool any = false; + for (std::size_t iz = 0; iz < side; ++iz) + for (std::size_t ix = 0; ix < side; ++ix) { - int const cx = ccx - half + dx, cz = ccz - half + dz; - if (!m_grid_chunks.count({cx, cz})) + auto const index = iz * side + ix; + if (ground.height_at(low.x + ix * step, low.y + iz * step, std::numeric_limits::max(), heights[index])) + found[index] = 1, any = true; + } + // past the edge of the ground the chunk is carried on level with the nearest point of it + if (any) + fill_from_nearest(heights, found, side); + else + std::fill(heights.begin(), heights.end(), m_terrain_baseheight); + auto const added = m_streamer.add_chunk(Key.first, Key.second, Spacing, [&](double const X, double const Z) { + auto const ix = static_cast(std::clamp(std::lround((X - low.x) / step), 0, cells)); + auto const iz = static_cast(std::clamp(std::lround((Z - low.y) / step), 0, cells)); + return static_cast(heights[iz * side + ix]); + }, Layer); + if (added) + { + ground_tiles.clear(); + forget_ground_shapes(); + } + return added; +} + +void editor_mode::add_chunks_around(int const Count) +{ + if (false == ensure_terrain()) + return; + auto const centre = heightmap::chunk_at(Camera.Pos.x, Camera.Pos.z); + auto const first = -(Count - 1) / 2; + int made = 0; + for (int dz = 0; dz < Count; ++dz) + for (int dx = 0; dx < Count; ++dx) + if (add_chunk_from_ground({centre.first + first + dx, centre.second + first + dz}, m_terrain_spacing)) + ++made; + m_terrain_status = format(STR_C("%d chunks made"), made); +} + +void editor_mode::handle_terrain_click(bool const Shift) +{ + glm::dvec3 const offset = GfxRenderer->Mouse_Position(); + if (glm::length(offset) < 1e-3) + return; // nothing under the cursor (yet) + glm::dvec3 const world = Camera.Pos + offset; + switch (m_terrain_tool) + { + case terrain_tool::chunks: + { + if (false == ensure_terrain()) + return; + auto const key = heightmap::chunk_at(world.x, world.z); + if (Shift) + { + if (m_streamer.exists(key)) { - add_grid_chunk(cx, cz); - ++created; + m_streamer.remove_chunk(key.first, key.second); + ground_tiles.clear(); + forget_ground_shapes(); + m_terrain_status = format(STR_C("Chunk %d,%d removed"), key.first, key.second); + } + return; + } + // a click on a chunk adds the neighbour by the nearest edge, a click outside fills the clicked place + auto target = key; + if (m_streamer.exists(key)) + { + auto const low = heightmap::chunk_corner(key); + double const west = world.x - low.x, east = heightmap::chunk_size - west, south = world.z - low.y, north = heightmap::chunk_size - south; + double const nearest = std::min({west, east, south, north}); + if (nearest == west) + --target.first; + else if (nearest == east) + ++target.first; + else if (nearest == south) + --target.second; + else + ++target.second; + } + m_terrain_status = add_chunk_from_ground(target, m_terrain_spacing) ? format(STR_C("Chunk %d,%d made"), target.first, target.second) : std::string(STR_C("There's a chunk there already")); + break; + } + case terrain_tool::spacing: + { + // a click selects the chunk, Ctrl adds it to the selection or takes it away + auto const key = heightmap::chunk_at(world.x, world.z); + if (false == m_streamer.exists(key)) + { + if (false == Global.ctrlState) + m_terrain_selection.clear(); + return; + } + if (false == Global.ctrlState) + m_terrain_selection = {key}; + else if (m_terrain_selection.erase(key) == 0) + m_terrain_selection.insert(key); + m_terrain_status = format(STR_C("Chunks selected: %zu"), m_terrain_selection.size()); + break; + } + case terrain_tool::fill: + { + if (Shift) + { + if (false == m_paint_outline.empty()) + m_paint_outline.pop_back(); + return; + } + m_paint_outline.push_back(placement_on_ground(world)); + break; + } + case terrain_tool::water: + { + if (Shift) + { + if (false == m_water_points.empty()) + m_water_points.pop_back(); + return; + } + auto const point = placement_on_ground(world); + // the water reaches the lowest point of its shore, unless given by hand + if (false == m_water_level_edited) + m_water_level = m_water_points.empty() ? static_cast(point.y) : std::min(m_water_level, static_cast(point.y)); + m_water_points.push_back(point); + break; + } + default: + break; + } +} + +namespace +{ + // takes the shapes of specified terrain files and materials out of the scene. returns: the count of the shapes + std::size_t drop_terrain_shapes(std::set const &Files, std::set const &Materials) + { + std::size_t dropped = 0; + auto const dropping = [&](scene::shape_node const &Shape) { + auto const &data = Shape.data(); + bool const drop = Files.count(data.terrainfile) != 0 && Materials.count(data.material) != 0; + dropped += drop ? 1 : 0; + return drop; + }; + for (std::size_t i = 0; i < sq(scene::EU07_REGIONSIDESECTIONCOUNT); ++i) + { + auto *section = simulation::Region->get_section(i); + if (section == nullptr) + continue; + section->m_shapes.erase(std::remove_if(section->m_shapes.begin(), section->m_shapes.end(), dropping), section->m_shapes.end()); + for (auto &cell : section->m_cells) + { + cell.m_shapesopaque.erase(std::remove_if(cell.m_shapesopaque.begin(), cell.m_shapesopaque.end(), dropping), cell.m_shapesopaque.end()); + cell.m_shapestranslucent.erase(std::remove_if(cell.m_shapestranslucent.begin(), cell.m_shapestranslucent.end(), dropping), cell.m_shapestranslucent.end()); } } + return dropped; + } +} // namespace - WriteLog("Editor: created chunked terrain with " + std::to_string(created) + " chunks", logtype::generic); +void editor_mode::load_ground_files() +{ + auto &conversion{m_ground_conversion}; + conversion.files.clear(); + conversion.triangles.clear(); + conversion.materials.clear(); + conversion.names.clear(); + conversion.chosen.clear(); + std::map lookup; + auto const &references{scene::terrain_file::references()}; + for (std::size_t index = 0; index < references.size(); ++index) + { + ground_conversion::source source; + source.file = references[index].file; + source.reference = static_cast(index + 1); + source.first = conversion.triangles.size(); + source.good = scene::terrain_file::read_triangles( + references[index], + [&](std::string const &Material, std::array const &Triangle) { + auto found{lookup.find(Material)}; + if (found == lookup.end()) + { + found = lookup.emplace(Material, static_cast(conversion.names.size())).first; + conversion.names.push_back(Material); + } + conversion.triangles.push_back(Triangle); + conversion.materials.push_back(found->second); + }, + &source.message, &source.text); + if (false == source.good) + { + // what was read before the failure doesn't count + conversion.triangles.resize(source.first); + conversion.materials.resize(source.first); + source.chosen = false; + } + source.count = conversion.triangles.size() - source.first; + conversion.files.push_back(std::move(source)); + } + // the terrain files hold the ground, all of it is taken unless told otherwise + for (std::size_t index = 0; index < conversion.names.size(); ++index) + conversion.chosen.insert(index); } -void editor_mode::save_scene_with_terrain() +std::string editor_mode::convert_ground() { - // commit authored terrain so the scenery streams it on load. if not already streaming, hand the - // manual grid chunks over to the streamer (same as toggling Stream terrain on) - if (!m_streamer.active()) + auto &conversion{m_ground_conversion}; + if (simulation::Region == nullptr || conversion.chosen.empty()) + return {}; + if (false == ensure_terrain()) + return STR_C("The heightmap terrain can't be opened"); + double const size = heightmap::chunk_size; + int const cells = heightmap::cells_for(m_terrain_spacing); + double const cellsize = size / cells; + + // the chosen triangles of the chosen files + std::vector triangles; + std::vector materials; + std::set files; + for (auto const &source : conversion.files) { - std::string scenery = Global.SceneryFile; - auto const slash = scenery.find_last_of("/\\"); - if (slash != std::string::npos) - scenery = scenery.substr(slash + 1); - auto const dot = scenery.find_last_of('.'); - if (dot != std::string::npos) - scenery = scenery.substr(0, dot); - if (scenery.empty()) - scenery = "default"; + if (false == source.good || false == source.chosen) + continue; + files.insert(source.reference); + for (auto index = source.first; index < source.first + source.count; ++index) + { + if (conversion.chosen.count(conversion.materials[index]) == 0) + continue; + triangles.push_back(conversion.triangles[index]); + materials.push_back(conversion.materials[index]); + } + } + if (triangles.empty()) + return STR_C("The chosen files and materials have no triangles"); - m_streamer.directory("editor_terrain/" + scenery); - m_streamer.configure(m_terrain_cells, m_terrain_cellsize, m_stream_radius, m_terrain_baseheight, - std::string(m_terrain_texture)); - m_streamer.simplify(m_terrain_auto_optimize, m_terrain_simplify_error); - m_streamer.persist(true); - - for (auto &entry : m_grid_chunks) - if (entry.second) - m_streamer.save_chunk(entry.first.first, entry.first.second, *entry.second); - for (auto &entry : m_grid_chunks) - if (entry.second) - entry.second->destroy(); - m_grid_chunks.clear(); - m_streamer.active(true); + // the triangles reaching each chunk + std::map> buckets; + for (std::uint32_t i = 0; i < triangles.size(); ++i) + { + auto const &t = triangles[i]; + // a triangle which only touches the edge of a chunk gives it nothing but the edge, which the chunk on the other side has + auto const margin{1e-3}; + auto const low{heightmap::chunk_at(std::min({t[0].x, t[1].x, t[2].x}) + margin, std::min({t[0].z, t[1].z, t[2].z}) + margin)}; + auto const high{heightmap::chunk_at(std::max({t[0].x, t[1].x, t[2].x}) - margin, std::max({t[0].z, t[1].z, t[2].z}) - margin)}; + for (int x = low.first; x <= high.first; ++x) + for (int z = low.second; z <= high.second; ++z) + buckets[{x, z}].push_back(i); } - m_streamer.flush(); // save resident edited chunks to disk + // palette entries of the materials, added as they're needed + std::map palette; + auto const layer_of = [&](std::uint16_t const Material) { + auto const found{palette.find(Material)}; + if (found != palette.end()) + return found->second; + auto const layer{m_streamer.layer(conversion.names[Material])}; + palette.emplace(Material, layer); + return layer; + }; - // export scenery; the exported .scm now carries an `editorterrain` directive (streamer is active) - simulation::State.export_as_text(Global.SceneryFile); - WriteLog("Editor: saved scene + terrain", logtype::generic); + std::size_t made = 0, skipped = 0; + std::size_t const side = static_cast(cells) + 1; + auto const samples{heightmap::default_paint_samples}; + std::size_t const paintside = static_cast(samples) + 1; + double const paintstep = size / samples; + for (auto const &bucket : buckets) + { + auto const &key = bucket.first; + if (m_streamer.exists(key)) + { + ++skipped; + continue; + } + auto const low{heightmap::chunk_corner(key)}; + std::vector local; + std::vector localmaterials; + for (auto const i : bucket.second) + { + local.push_back(triangles[i]); + localmaterials.push_back(materials[i]); + } + triangle_grid const ground(std::move(local), low - glm::dvec2{1.0}, low + glm::dvec2{size + 1.0}); + auto const ceiling{std::numeric_limits::max()}; + // heights of the grid points; where there's no ground they're taken from the nearest point which has some + std::vector heights(side * side, 0.0); + std::vector found(side * side, 0); + bool any = false; + for (std::size_t iz = 0; iz < side; ++iz) + for (std::size_t ix = 0; ix < side; ++ix) + { + auto const index = iz * side + ix; + if (ground.height_at(low.x + ix * cellsize, low.y + iz * cellsize, ceiling, heights[index])) + found[index] = 1, any = true; + } + if (false == any) + continue; + fill_from_nearest(heights, found, side); + // the material on top at each paint sample + std::vector paint(paintside * paintside, 0); + std::vector painted(paintside * paintside, 0); + std::map counts; + for (std::size_t iz = 0; iz < paintside; ++iz) + for (std::size_t ix = 0; ix < paintside; ++ix) + { + double height; + std::uint32_t triangle; + if (false == ground.height_at(low.x + ix * paintstep, low.y + iz * paintstep, ceiling, height, &triangle)) + continue; + auto const index = iz * paintside + ix; + paint[index] = localmaterials[triangle]; + painted[index] = 1; + ++counts[paint[index]]; + } + if (counts.empty()) + { + // the points of the grid hit the ground, the samples in between didn't; the nearest triangle covers the chunk + counts[localmaterials.front()] = 1; + std::fill(paint.begin(), paint.end(), localmaterials.front()); + } + else + fill_from_nearest(paint, painted, paintside); + // a chunk holds up to 8 materials, the ones covering the most; the rest is given to the one covering the most + std::vector> ranking; + for (auto const &count : counts) + ranking.emplace_back(count.second, count.first); + std::sort(ranking.begin(), ranking.end(), [](auto const &A, auto const &B) { return A.first > B.first; }); + if (ranking.size() > heightmap::max_chunk_layers) + ranking.resize(heightmap::max_chunk_layers); + + auto data{heightmap::chunk_data::make_flat(key, m_terrain_spacing, 0.f)}; + for (std::size_t index = 0; index < data->heights.size(); ++index) + data->heights[index] = static_cast(heights[index]); + std::map slots; + for (std::size_t slot = 0; slot < ranking.size(); ++slot) + { + data->layers[slot] = layer_of(ranking[slot].second); + slots.emplace(ranking[slot].second, slot); + } + if (ranking.size() > 1) + { + auto const plane{paintside * paintside}; + data->paint = samples; + data->weights.assign(plane * ranking.size(), 0); + for (std::size_t index = 0; index < plane; ++index) + { + auto const slot{slots.find(paint[index])}; + data->weights[(slot != slots.end() ? slot->second : 0) * plane + index] = 255; + } + } + if (m_streamer.add_chunk(std::move(data))) + ++made; + } + forget_ground(); + + // the converted ground goes from the scene, and from the text files on save + std::set handles; + for (auto const material : conversion.chosen) + if (material < conversion.names.size()) + handles.insert(GfxRenderer->Fetch_Material(conversion.names[material])); + auto const dropped = drop_terrain_shapes(files, handles); + std::set layers; + std::size_t binary = 0; + for (auto const &source : conversion.files) + { + if (false == source.good || false == source.chosen) + continue; + if (false == source.text) + { + ++binary; + continue; + } + auto const textfile{source.file + ".txtf"}; + for (std::size_t layer = 1; layer <= scene::Layers.size(); ++layer) + { + auto name{scene::Layers.layer(static_cast(layer)).name}; + replace_slashes(name); + if (name.ends_with(textfile)) + layers.insert(static_cast(layer)); + } + } + auto const erased = scene::Layers.erase_shapes(handles, &layers); + auto summary = format(STR_C("Ground converted: %zu chunks of heightmap terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the terrain files on save"), made, dropped, erased.first); + if (skipped > 0) + summary += format(STR_C(", %zu chunks kept as there was terrain already"), skipped); + if (erased.second > 0) + summary += format(STR_C(", %zu nodes stay as their files can't be rewritten"), erased.second); + if (binary > 0) + summary += format(STR_C(". %zu of the files are binary (.btf) without their text: their triangles come back with the next load, until the files are taken out of the terrain directive"), binary); + WriteLog("Editor: " + summary, logtype::generic); + return summary; } -void editor_mode::handle_terrain_sculpt(double Deltatime) +bool editor_mode::save() +{ + if (scene::Layers.empty()) + { + // without the sources tracked during the load there's no telling where the changes belong + ui()->set_status(STR("The scenery wasn't opened for editing. Start the simulator with: -edit "), true); + return false; + } + + // the heightmap terrain is kept in files of its own, the scenery only needs the directive which loads it + std::vector rootstatements; + if (m_streamer.active()) + { + std::string error; + if (false == m_streamer.save(&error)) + { + ui()->set_status(format(STR_C("The terrain couldn't be saved: %s"), error.c_str()), true); + return false; + } + if (false == scene::Layers.terrain_directive()) + rootstatements.emplace_back("heightmap_terrain " + m_streamer.name() + " endheightmap_terrain"); + } + + if (m_profile.changed) + profile_store(); + infra_store(); + if (false == m_saved_camera.has_value() || glm::distance(m_saved_camera->first, glm::dvec3{Camera.Pos}) > 0.5 || glm::distance(m_saved_camera->second, glm::vec3{Camera.Angle}) > 0.01f) + { + m_saved_camera = std::make_pair(glm::dvec3{Camera.Pos}, glm::vec3{Camera.Angle}); + scene::Layers.mark(scene::layer_handle{1}, "//$c", {format("%.3f %.3f %.3f %.5f %.5f", Camera.Pos.x, Camera.Pos.y, Camera.Pos.z, Camera.Angle.x, Camera.Angle.y)}); + } + auto const terrainadded{false == rootstatements.empty()}; + auto const controls{turntable_controls(rootstatements)}; + std::vector trailing; + starter_trainset(trailing); + auto const result = scene::Layers.save(rootstatements, trailing); + if (result.success && controls) + m_turntable_included = true; + if (result.success && trailing.size() == 3) + { + std::istringstream header{trailing.front()}; + std::string keyword, timetable, track; + double offset{0.0}; + header >> keyword >> timetable >> track >> offset; + if (auto *path{simulation::Paths.find(track)}; path != nullptr && simulation::Vehicles.find("wmb10-6457") == nullptr) + simulation::State.insert_trainset("wmb10-6457", path, offset, trailing[1] + "\n"); + } + if (result.success && terrainadded) + { + scene::Layers.terrain_directive(true); + } + ui()->set_status(result.message, false == result.success); + WriteLog("Editor: " + result.message, logtype::generic); + return result.success; +} + +void editor_mode::toggle_include(scene::instance_handle const Instance) +{ + if (false == scene::Layers.tracked(Instance)) + return; + auto const &included = scene::Layers.instance(Instance); + if (included.dead) + { + // its directive is gone from the scenery file, and the place it was at with it + ui()->set_status("Include of \"" + *included.file + "\" was removed from the saved scenery, this can't be taken back.", true); + return; + } + scene::Layers.removed(Instance, false == included.removed); + ui()->set_status("Include of \"" + *included.file + "\" " + (included.removed ? "removed." : "restored.")); +} + +void editor_mode::restore_include(EditorSnapshot &Snapshot) +{ + if (Snapshot.serialized.empty()) + { + // the include was placed or removed; either is taken back by flipping its removal + toggle_include(Snapshot.instance); + select_include(0); + return; + } + // parameters of the include were changed. the snapshot holds the directive to go back to, and leaves with the current one + auto current = scene::Layers.directive(Snapshot.instance); + if (current.empty() || scene::Layers.instance(Snapshot.instance).dead) + { + ui()->set_status("Include of \"" + Snapshot.node_name + "\" is gone from the scenery, the change can't be taken back.", true); + return; + } + set_include_directive(Snapshot.instance, Snapshot.serialized); + Snapshot.serialized = std::move(current); + select_include(scene::Layers.removable(Snapshot.instance) ? Snapshot.instance : 0); +} + +void editor_mode::restore_includes(EditorSnapshot &Snapshot) +{ + std::size_t gone{0}; + for (auto const instance : Snapshot.instances) + { + if (false == scene::Layers.tracked(instance) || scene::Layers.instance(instance).dead) + { + ++gone; + continue; + } + scene::Layers.removed(instance, false == scene::Layers.instance(instance).removed); + } + for (auto &entry : Snapshot.directives) + { + auto current = scene::Layers.directive(entry.first); + if (current.empty() || scene::Layers.instance(entry.first).dead) + { + ++gone; + continue; + } + set_include_directive(entry.first, entry.second); + entry.second = std::move(current); + } + select_include(0); + ui()->set_status(gone > 0 ? std::to_string(gone) + " of the includes are gone from the saved scenery, they can't be taken back." : std::to_string(Snapshot.instances.size() + Snapshot.directives.size()) + " includes restored.", gone > 0); +} + +void editor_mode::select_include(scene::instance_handle const Instance) +{ + m_instance = (scene::Layers.tracked(Instance) ? Instance : 0); + m_include = include_selection{}; + m_include_gesture = false; + if (m_instance == 0) + { + ui()->set_include(nullptr); + return; + } + auto const &included = scene::Layers.instance(m_instance); + m_include.instance = m_instance; + m_include.target = *included.file; + // NOTE: a template without a usable description is still presented, with its parameters unnamed + std::string error; + editor_includes::load(*included.file, m_include.info, error); + if (false == editor_includes::parse_directive(scene::Layers.directive(m_instance), m_include.target, m_include.values)) + { + m_include.values.clear(); + m_include.issue = "The directive of the include can't be read from the scenery file, its parameters can't be changed."; + } + // with a rotation or a scale in effect the parameters don't translate to a location in the scene in a single way, + // see place_include() + m_include.placement = (included.context.rotation == glm::vec3{0.f} && included.context.scale == glm::vec3{1.f}); + ui()->set_include(&m_include); +} + +void editor_mode::set_include_directive(scene::instance_handle const Instance, std::string const &Directive) +{ + if (false == scene::Layers.modify(Instance, Directive)) + return; + std::vector retired; + simulation::State.rebuild_include(Instance, retired); + // the pick of the renderer is resolved a few frames after it's requested, and can still deliver one of these + for (auto *model : retired) + m_retired.emplace_back(model, 120); +} + +void editor_mode::apply_include() +{ + if (m_instance == 0 || false == m_include.issue.empty()) + return; + for (auto const &value : m_include.values) + { + if (value == "end" || value.find_first_of("\"\r\n") != std::string::npos) + { + // either would cut the directive short + ui()->set_status("A parameter of an include can't be \"end\", or have a quotation mark in it.", true); + select_include(m_instance); // back to the values of the directive + return; + } + } + auto before = scene::Layers.directive(m_instance); + auto const directive = editor_includes::compose_directive(m_include.target, m_include.values); + if (directive == before) + return; + std::string read_before, read_after; + if (false == m_include.info.signal_read.empty()) + { + std::string file; + std::vector previous; + if (editor_includes::parse_directive(before, file, previous)) + { + read_before = editor_includes::substitute(m_include.info.signal_read, previous); + read_after = editor_includes::substitute(m_include.info.signal_read, m_include.values); + } + } + if (false == m_include_gesture) + { + // a single undo step for the whole drag + EditorSnapshot snap; + snap.instance = m_instance; + snap.node_name = *scene::Layers.instance(m_instance).file; + snap.serialized = std::move(before); + if (read_before != read_after) + signal_renamed(read_before, read_after, snap); + m_history.push_back(std::move(snap)); + g_redo.clear(); + m_include_gesture = true; + } + else if (read_before != read_after && false == m_history.empty()) + signal_renamed(read_before, read_after, m_history.back()); + set_include_directive(m_instance, directive); +} + +void editor_mode::place_include(std::string const &File, int const RotationMode, float const FixedRotation) +{ + if (scene::Layers.empty()) + { + // the directive has to go to a scenery file, and without the sources tracked there's no telling which + ui()->set_status("Scenery templates can be placed only in a scenery opened for editing (-edit).", true); + WriteLog("Editor: scenery templates can be placed only in a scenery opened for editing (-edit)", logtype::generic); + return; + } + include_info info; + std::string issue; + auto const parameters = editor_includes::parameter_count(File); + if (false == editor_includes::load(File, info, issue) || false == editor_includes::complete(info, parameters, &issue)) + { + ui()->set_status("Template \"" + File + "\" can't be placed: " + issue, true); + return; + } + + // the directive is added to the file of the active layer, and has to make sense with the placement in effect where it goes + auto const layer = scene::Layers.resolve(scene::Layers.active()); + if (std::string reason; false == scene::Layers.accepts(layer, &reason)) + { + ui()->set_status("Template \"" + File + "\" can't be placed: " + (reason.empty() ? "there's no active layer to put it in" : "the active layer can't take it, " + reason), true); + return; + } + auto const context = scene::Layers.layer(layer).context_insert(); + if (context.rotation != glm::vec3{0.f} || context.scale != glm::vec3{1.f}) + { + // a template can pass its parameters to an origin directive, which isn't affected by these, or straight to + // the nodes, which are. there's no single set of parameter values which puts both at the cursor + ui()->set_status("Template \"" + File + "\" can't be placed: the file of the active layer would receive it with a rotation or a scale in effect. Pick another layer.", true); + return; + } + glm::dvec3 const location = placement_on_ground(Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position())); + std::optional yaw; + if (RotationMode == functions_panel::RANDOM) + yaw = static_cast(LocalRandom(0.0, 360.0)); + else if (RotationMode == functions_panel::FIXED) + yaw = FixedRotation; + + auto const directive = editor_includes::directive(File, info, parameters, context.to_local(location), yaw); + auto const instance = scene::Layers.place(layer, File, directive); + if (instance == 0) + { + ui()->set_status("Template \"" + File + "\" can't be placed in the active layer.", true); + return; + } + // show the models of the template right away. the rest of it comes into play when the saved scenery is loaded + auto const preview = simulation::State.preview_include(directive, context, layer, instance); + + EditorSnapshot snap; + snap.instance = instance; + snap.node_name = File; + m_history.push_back(std::move(snap)); + g_redo.clear(); + select_include(instance); + + ui()->set_status( + "\"" + info.name + "\" placed in layer \"" + scene::Layers.layer(layer).name + "\": " + std::to_string(preview.first) + " model(s) shown" + + (preview.second > 0 ? ", " + std::to_string(preview.second) + " other statement(s) of the template take effect once the scenery is saved and loaded again." : ".")); +} + +void editor_mode::export_scenery() +{ + // hiding a layer clears the visibility flag of its nodes, and the flag is a part of exported node data. + // show the hidden layers for the duration of the export, so the files get the visibility defined in the scenery + auto const hiddenlayers = scene::Layers.hidden(); + for (auto const layer : hiddenlayers) + scene::Layers.visible(layer, true); + + simulation::State.export_as_text(Global.SceneryFile); + + for (auto const layer : hiddenlayers) + scene::Layers.visible(layer, false); +} + +void editor_mode::handle_terrain_brush(double Deltatime) { // world point under the cursor (Mouse_Position is camera-relative, like the brush placement uses) - glm::dvec3 const world = Camera.Pos + GfxRenderer->Mouse_Position(); - // only sculpt when the cursor is actually over terrain (avoids editing on a stale depth read) + glm::dvec3 const offset = GfxRenderer->Mouse_Position(); + if (glm::length(offset) < 1e-3) + return; + glm::dvec3 const world = Camera.Pos + offset; + // only when the cursor is over the terrain (avoids editing on a stale depth read) if (terrain_at(world.x, world.z) == nullptr) return; - double const rate = m_terrain_brush_strength * Deltatime; // metres applied this frame - double const signedrate = Global.shiftState ? -rate : rate; - // apply to every chunk the brush touches; each patch clips to its own bounds, so a stroke - // crossing a chunk boundary edits both and shared-edge vertices stay in sync - for (editor_terrain *terrain : active_terrains()) - terrain->sculpt(world.x, world.z, m_terrain_brush_radius, signedrate); + double const rate = m_terrain_brush_strength * Deltatime; + bool shaped = false; + switch (m_terrain_tool) + { + case terrain_tool::sculpt: + shaped = m_streamer.sculpt(world.x, world.z, m_terrain_brush_radius, Global.shiftState ? -rate : rate); + break; + case terrain_tool::smooth: + shaped = m_streamer.smooth(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.5)); + break; + case terrain_tool::level: + { + if (false == m_terrain_stroke && m_terrain_target_from_stroke) + { + double height; + if (m_streamer.height_at(world.x, world.z, height)) + m_terrain_target = static_cast(height); + } + m_terrain_stroke = true; + shaped = m_streamer.level(world.x, world.z, m_terrain_brush_radius, m_terrain_target, std::min(1.0, rate * 0.5), m_terrain_level_mode); + break; + } + case terrain_tool::paint: + m_streamer.paint(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.25), Global.shiftState ? std::uint16_t{0} : static_cast(m_terrain_layer)); + break; + case terrain_tool::restore: + shaped = m_streamer.restore_stitching(world.x, world.z, m_terrain_brush_radius, std::min(1.0, rate * 0.5)); + break; + default: + break; + } + if (shaped) + { + // what the road tools know about the ground is no longer true + ground_tiles.clear(); + forget_ground_shapes(); + } +} + +void editor_mode::fill_terrain_area() +{ + auto const &layers = m_streamer.manifest().layers; + if (m_paint_outline.size() < 3 || layers.empty()) + return; + auto const chosen = layers[std::clamp(m_terrain_layer, 0, static_cast(layers.size()) - 1)]; + // the material turned by its own angle is another entry of the palette + auto const rotation = static_cast(std::remainder(static_cast(m_paint_rotation), 360.0)); + auto const layer = chosen.rotation == rotation ? static_cast(std::clamp(m_terrain_layer, 0, static_cast(layers.size()) - 1)) + : m_streamer.layer(chosen.material, chosen.size, rotation); + std::vector outline; + for (auto const &point : m_paint_outline) + outline.emplace_back(point.x, point.z); + auto const filled = m_streamer.fill(outline, m_paint_feather, layer); + m_terrain_status = filled ? format(STR_C("Area filled with %s"), chosen.material.c_str()) : std::string(STR_C("Nothing filled: no chunk under the outline, or no room for another material")); + if (filled) + m_paint_outline.clear(); +} + +void editor_mode::convert_selected_chunks() +{ + // with 0.5 m spacing the paint can be as fine, the other spacings keep a sample per metre + auto const paint = m_terrain_convert_paint ? (m_terrain_convert_spacing < 0.75f ? heightmap::default_paint_samples * 2 : heightmap::default_paint_samples) : 0; + std::size_t converted = 0; + for (auto const &key : m_terrain_selection) + if (m_streamer.spacing(key.first, key.second, m_terrain_convert_spacing, paint)) + ++converted; + if (converted > 0) + { + ground_tiles.clear(); + forget_ground_shapes(); + } + m_terrain_status = format(STR_C("Chunks converted to %s: %zu of %zu"), spacing_label(m_terrain_convert_spacing), converted, m_terrain_selection.size()); +} + +bool editor_mode::pick_terrain_target() +{ + glm::dvec3 const offset = GfxRenderer->Mouse_Position(); + if (glm::length(offset) < 1e-3) + return false; // nothing under the cursor + m_terrain_target = static_cast(Camera.Pos.y + offset.y); + m_terrain_status = format(STR_C("Target height: %.2f m"), m_terrain_target); + return true; } void editor_mode::capture_terrain() @@ -1287,6 +3258,8 @@ void editor_mode::capture_terrain() WriteLog("Editor: select a model instance to capture as terrain", logtype::generic); return; } + if (false == ensure_terrain()) + return; std::vector tris; gather_submodel_triangles(model->pModel->Root, instance_matrix(*model), tris); @@ -1297,51 +3270,31 @@ void editor_mode::capture_terrain() } // horizontal bounds of the captured geometry - glm::dvec3 lo(std::numeric_limits::max()); - glm::dvec3 hi(-std::numeric_limits::max()); + glm::dvec2 lo(std::numeric_limits::max()); + glm::dvec2 hi(-std::numeric_limits::max()); for (auto const &t : tris) for (auto const &p : t) { - lo.x = std::min(lo.x, p.x); lo.y = std::min(lo.y, p.y); lo.z = std::min(lo.z, p.z); - hi.x = std::max(hi.x, p.x); hi.y = std::max(hi.y, p.y); hi.z = std::max(hi.z, p.z); + lo = glm::min(lo, glm::dvec2(p.x, p.z)); + hi = glm::max(hi, glm::dvec2(p.x, p.z)); } + triangle_grid const surface(std::move(tris), lo - glm::dvec2(1.0), hi + glm::dvec2(1.0)); - glm::dvec3 const center((lo.x + hi.x) * 0.5, lo.y, (lo.z + hi.z) * 0.5); - double const extent = std::max(hi.x - lo.x, hi.z - lo.z); - int const cells = std::max(1, m_terrain_cells); - float const cellsize = static_cast(std::max(0.1, extent / cells)); - - // sampler: highest captured triangle at (x,z) - auto const sampler = [&tris](double X, double Z, double &OutY) -> bool { - double best = -std::numeric_limits::max(); - bool found = false; - for (auto const &t : tris) - { - double const minx = std::min({t[0].x, t[1].x, t[2].x}); - double const maxx = std::max({t[0].x, t[1].x, t[2].x}); - double const minz = std::min({t[0].z, t[1].z, t[2].z}); - double const maxz = std::max({t[0].z, t[1].z, t[2].z}); - if (X < minx || X > maxx || Z < minz || Z > maxz) - continue; - double y; - if (triangle_height_at(t[0], t[1], t[2], X, Z, y) && (!found || y > best)) - { - best = y; - found = true; - } - } - if (found) - OutY = best; - return found; - }; - - auto terrain = std::make_unique(); - if (!terrain->create(center, cells, cellsize, std::string(m_terrain_texture), sampler)) - { - WriteLog("Editor: terrain capture failed", logtype::generic); - return; - } - m_terrains.push_back(std::move(terrain)); + // the chunks under the model are made where missing, then take its shape wherever it has any + auto const first = heightmap::chunk_at(lo.x, lo.y); + auto const last = heightmap::chunk_at(hi.x, hi.y); + for (int cz = first.second; cz <= last.second; ++cz) + for (int cx = first.first; cx <= last.first; ++cx) + add_chunk_from_ground({cx, cz}, m_terrain_spacing); + m_streamer.reshape(lo, hi, [&](double X, double Z, float &Height) { + double y; + if (false == surface.height_at(X, Z, std::numeric_limits::max(), y)) + return false; + Height = static_cast(y); + return true; + }); + ground_tiles.clear(); + forget_ground_shapes(); // remove the original instance (recorded as a deletion so it can be undone) std::string as_text; @@ -1355,6 +3308,436 @@ void editor_mode::capture_terrain() simulation::State.delete_model(model); } +// the ground under a point the cursor landed on. the orthophoto laid over the ground catches the cursor like any +// other surface, so the point can hang over the ground by as much as the orthophoto is lifted, or lie on the flat sheet +// the orthophoto makes where it's not fitted to the ground. what's put in the scenery is to stand on the ground itself +glm::dvec3 editor_mode::placement_on_ground(glm::dvec3 Location) +{ + double constexpr reach = 2.0; // the ground is asked about this far around the point + double constexpr slack = 0.5; // a surface this far over the point still is what the cursor landed on + double constexpr height_tolerance = 50.0; // with nothing there or lower, how far up the ground is looked for + float constexpr terrain_model_radius = 50.0f; // the same models count as ground as for the area fill and the orthophoto + + glm::dvec2 const bmin{Location.x - reach, Location.z - reach}; + glm::dvec2 const bmax{Location.x + reach, Location.z + reach}; + std::vector triangles; + gather_ground_triangles(bmin, bmax, true, terrain_model_radius, triangles); + triangle_grid const ground(std::move(triangles), bmin, bmax); + auto const terrains = active_terrains(); + for (double const ceiling : {Location.y + slack, Location.y + height_tolerance}) + { + double y = -std::numeric_limits::max(); + bool found = ground.height_at(Location.x, Location.z, ceiling, y); + for (editor_terrain *terrain : terrains) + { + if (!terrain->contains(Location.x, Location.z)) + continue; + double const h = terrain->height_at(Location.x, Location.z); + if (h <= ceiling && (!found || h > y)) + { + y = h; + found = true; + } + } + if (found) + { + Location.y = y; + break; + } + } + // with no ground found the point stays where the cursor landed + return Location; +} + +// puts every instance of the model the selected node shows on the ground under it +void editor_mode::drop_model_instances() +{ + auto *selected = dynamic_cast(m_node); + if (selected == nullptr || selected->Model() == nullptr) + { + ui()->set_status(STR("Select a model in the scene first"), true); + return; + } + if (selected->radius() >= 50.0f) + { + // it'd be put on top of itself + ui()->set_status("\"" + selected->Model()->NameGet() + "\" is large enough to count as ground itself, so there's nothing to put it on", true); + return; + } + std::vector instances; + std::vector points; + int locked = 0; + for (auto *instance : simulation::Instances.sequence()) + { + if (instance == nullptr || instance->Model() != selected->Model()) + continue; + if (instance->from_template() || false == scene::Layers.editable(instance)) + { + // placed by a template, or on a layer which is hidden or locked + ++locked; + continue; + } + instances.push_back(instance); + points.push_back(instance->location()); + } + // the ground the road tools use: terrain shapes, large models and the terrain made in the editor, without the roads and the orthophoto. + // a point with nothing under it comes back with the height it has + auto const heights = ground_heights(points, true); + int moved = 0; + for (std::size_t idx = 0; idx < instances.size(); ++idx) + { + if (std::abs(heights[idx] - points[idx].y) < 0.001) + continue; + auto *instance = instances[idx]; + // one step of the history for each instance + push_snapshot(instance, EditorSnapshot::Action::Move); + auto location = points[idx]; + location.y = heights[idx]; + simulation::Region->erase(instance); + instance->location(location); + simulation::Region->insert(instance); + instance->mark_dirty(); + ++moved; + } + std::string status = std::to_string(moved) + " of " + std::to_string(instances.size()) + " instances of \"" + selected->Model()->NameGet() + "\" put on the ground"; + if (moved > 0) + status += "; Ctrl+Z takes them back one at a time"; + if (locked > 0) + status += ". " + std::to_string(locked) + " left alone: placed by a template, or on a layer which can't be edited"; + ui()->set_status(status); +} + +void editor_mode::read_georeference() +{ + if (m_georeference_read) + return; + m_georeference_read = true; + m_georeference = false; + m_georeference_line.clear(); + // the name of the scenery is kept in lower case, the file may have capitals in its name + std::filesystem::path file{Global.asCurrentSceneryPath + Global.SceneryFile}; + std::error_code error; + if (false == std::filesystem::exists(file, error)) + { + for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error)) + { + auto name{entry.path().filename().string()}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + if (name == Global.SceneryFile) + { + file = entry.path(); + break; + } + } + } + std::ifstream stream(file); + std::string line; + for (int count = 0; count < 200 && std::getline(stream, line); ++count) + { + auto const mark = line.find("//$g"); + if (mark == std::string::npos) + continue; + // Rainsted writes the origin of the scenery as: //$g , in kilometres + std::istringstream words(line.substr(mark + 4)); + std::string system; + double east{0.0}, north{0.0}; + if (false == static_cast(words >> system >> east >> north) || system.find("1992") == std::string::npos) + continue; + auto const scale = (east < 10000.0 && north < 10000.0) ? 1000.0 : 1.0; + m_georeference_origin = {north * scale, east * scale}; + m_georeference_line = line.substr(mark); + if (false == m_georeference_line.empty() && m_georeference_line.back() == '\r') + m_georeference_line.pop_back(); + m_georeference = true; + break; + } +} + +void editor_mode::render_map_menu() +{ + if (false == ImGui::BeginMenu(STR_C("Map"))) + return; + bool enabled = m_orthophoto.enabled(); + if (ImGui::MenuItem(STR_C("Orthophoto"), nullptr, enabled)) + m_orthophoto.enabled(false == enabled); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery")); + ImGui::MenuItem(STR_C("Orthophoto settings..."), nullptr, &m_orthophoto_window); + ImGui::EndMenu(); +} + +void editor_mode::render_orthophoto_window() +{ + if (false == m_orthophoto_window) + return; + ImGui::SetNextWindowSize(ImVec2(430.0f, 0.0f), ImGuiCond_FirstUseEver); + if (ImGui::Begin((std::string(STR_C("Orthophoto")) + "###orthophoto").c_str(), &m_orthophoto_window)) + render_orthophoto_ui(); + ImGui::End(); +} + +void editor_mode::render_view_menu() +{ + if (false == ImGui::BeginMenu(STR_C("View"))) + return; + if (ImGui::MenuItem(STR_C("Top view, orthographic"), "O", Global.EditorOrtho)) + toggle_ortho(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The view straight down without perspective, as on a map; again for the camera as it was")); + ImGui::MenuItem("Show lanes", nullptr, &m_roadtool.lanes); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Draws the lanes of the roads with the ways the vehicles take through the junctions"); + if (ImGui::MenuItem(STR_C("Structure gauge"), nullptr, m_gauge.open)) + m_gauge.open = !m_gauge.open; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)")); + ImGui::Separator(); + ImGui::MenuItem(STR_C("Weather and time"), nullptr, &m_environment_open); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Time of day, day of the year, visibility, clouds and air temperature, as the scenery is seen in the editor")); + ImGui::EndMenu(); +} + +void editor_mode::render_environment_window() +{ + if (false == m_environment_open) + return; + ImGui::SetNextWindowSize(ImVec2(420.0f * Global.ui_scale, 0.0f), ImGuiCond_FirstUseEver); + if (ImGui::Begin((std::string(STR_C("Weather and time")) + "###environment").c_str(), &m_environment_open, ImGuiWindowFlags_NoCollapse)) + { + // what the setdatetime, setweather and settemperature commands do in the driver mode; the editor doesn't take commands + auto const set_date_time = [](int const Yearday, int const Minute) { + simulation::Time.set_time(Yearday, Minute); + auto const weather{Global.Weather}; + simulation::Environment.compute_season(Yearday); + if (weather != Global.Weather) + { + // HACK: force re-calculation of precipitation, as the command does + Global.Overcast = std::clamp(Global.Overcast - 0.0001f, 0.0f, 2.0f); + } + simulation::Environment.update_moon(); + }; + // the names of the period, season and weather end with a colon, being keys of other lookups + auto const name = [](std::string Text) { + if (false == Text.empty() && Text.back() == ':') + Text.pop_back(); + return Text; + }; + auto const &time{simulation::Time.data()}; + auto minute{time.wHour * 60 + time.wMinute}; + auto const clock{format("%02d:%02d (%s)", time.wHour, time.wMinute, name(Global.Period).c_str())}; + if (ImGui::SliderInt((std::string(STR_C("Time of day")) + "###timeofday").c_str(), &minute, 0, 1439, clock.c_str())) + set_date_time(static_cast(std::round(Global.fMoveLight)), std::clamp(minute, 0, 1439)); + auto yearday{Global.fMoveLight}; + auto const day{format("%.0f (%s)", Global.fMoveLight, name(Global.Season).c_str())}; + if (ImGui::SliderFloat((std::string(STR_C("Day of the year")) + "###dayofyear").c_str(), &yearday, 0.0f, 364.0f, day.c_str())) + set_date_time(static_cast(std::round(std::clamp(yearday, 0.0f, 365.0f))), time.wHour * 60 + time.wMinute); + ImGui::Separator(); + auto visibility{std::log(Global.fFogEnd)}; + auto const fog{format("%.0f m", Global.fFogEnd)}; + if (ImGui::SliderFloat((std::string(STR_C("Visibility")) + "###visibility").c_str(), &visibility, std::log(10.0f), std::log(50000.0f), fog.c_str())) + { + Global.fFogEnd = std::clamp(std::exp(visibility), 10.0f, 50000.0f); + simulation::Environment.compute_weather(); + } + auto overcast{Global.Overcast}; + auto const clouds{format("%.2f (%s)", Global.Overcast, name(Global.Weather).c_str())}; + if (ImGui::SliderFloat((std::string(STR_C("Clouds")) + "###overcast").c_str(), &overcast, 0.0f, 2.0f, clouds.c_str())) + { + Global.Overcast = std::clamp(overcast, 0.0f, 2.0f); + simulation::Environment.compute_weather(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Up to 1 the sky clouds over, past it rain or snow falls, by the air temperature")); + auto temperature{Global.AirTemperature}; + auto const air{format("%.1f deg C", Global.AirTemperature)}; + if (ImGui::SliderFloat((std::string(STR_C("Air temperature")) + "###airtemperature").c_str(), &temperature, -35.0f, 40.0f, air.c_str())) + { + Global.AirTemperature = std::clamp(temperature, -35.0f, 40.0f); + simulation::Environment.compute_weather(); + } + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", STR_C("Seen in the editor only; the scenery keeps its own time and weather")); + ImGui::PopStyleColor(); + } + ImGui::End(); +} + +void editor_mode::render_new_scenery_popup() +{ + if (m_newscenery_asked) + { + ImGui::OpenPopup(STR_C("New scenery##restart")); + m_newscenery_asked = false; + } + if (false == ImGui::BeginPopupModal(STR_C("New scenery##restart"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + return; + ImGui::TextUnformatted(STR_C("The editor starts again, with the wizard of a new scenery.")); + auto const editsession{false == scene::Layers.empty()}; + if (false == m_history.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "%s", STR_C("What isn't saved of this scenery is lost.")); + if (editsession && ImGui::Button(STR_C("Save and start the wizard"))) + { + if (save() && false == restart_for_new_scenery()) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + ImGui::CloseCurrentPopup(); + } + if (editsession) + ImGui::SameLine(); + if (ImGui::Button(STR_C("Start the wizard"))) + { + if (false == restart_for_new_scenery()) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); +} + +bool editor_mode::restart_for_new_scenery() +{ + return restart_editor(std::string{}); +} + +bool editor_mode::restart_editor(std::string const &Scenery) +{ +#ifdef _WIN32 + _putenv("EU07_EDITOR_SELFTEST="); + wchar_t path[MAX_PATH]; + if (GetModuleFileNameW(nullptr, path, MAX_PATH) == 0) + return false; + std::wstring command{L"\"" + std::wstring{path} + L"\" -edit"}; + if (false == Scenery.empty()) + command += L" \"" + std::filesystem::path(Scenery).wstring() + L"\""; + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + PROCESS_INFORMATION process{}; + if (false == CreateProcessW(nullptr, command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup, &process)) + return false; + CloseHandle(process.hThread); + CloseHandle(process.hProcess); +#else + unsetenv("EU07_EDITOR_SELFTEST"); + char path[4096]; + auto const size{readlink("/proc/self/exe", path, sizeof(path) - 1)}; + if (size <= 0) + return false; + path[size] = '\0'; + char edit[]{"-edit"}; + std::string scenery{Scenery}; + char *arguments[]{path, edit, scenery.empty() ? nullptr : scenery.data(), nullptr}; + posix_spawnattr_t attributes; + posix_spawnattr_init(&attributes); + posix_spawnattr_setflags(&attributes, POSIX_SPAWN_SETSID); + pid_t child; + auto const result{posix_spawn(&child, path, nullptr, &attributes, arguments, environ)}; + posix_spawnattr_destroy(&attributes); + if (result != 0) + return false; +#endif + WriteLog(Scenery.empty() ? std::string{"Editor: started again for a new scenery"} : "Editor: started again with scenery " + Scenery); + Application.queue_quit(true); + return true; +} + +void editor_mode::render_open_scenery_popup() +{ + auto const lower = [](std::string Text) { + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; + }; + if (m_openscenery_asked) + { + m_openscenery_asked = false; + m_openscenery_list.clear(); + m_openscenery_choice.clear(); + std::error_code error; + for (auto const &entry : std::filesystem::directory_iterator(Global.asCurrentSceneryPath, error)) + { + auto const name{entry.path().filename().string()}; + if (entry.is_regular_file(error) && name.size() > 4 && lower(name.substr(name.size() - 4)) == ".scn" && name.front() != '$') + m_openscenery_list.emplace_back(name); + } + std::sort(m_openscenery_list.begin(), m_openscenery_list.end(), [&](std::string const &A, std::string const &B) { return lower(A) < lower(B); }); + ImGui::OpenPopup(STR_C("Open scenery##restart")); + } + if (false == ImGui::BeginPopupModal(STR_C("Open scenery##restart"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + return; + ImGui::TextUnformatted(STR_C("The editor starts again with the scenery chosen below.")); + ImGui::SetNextItemWidth(360.0f); + if (ImGui::IsWindowAppearing()) + ImGui::SetKeyboardFocusHere(); + ImGui::InputTextWithHint("##opensearch", STR_C("search"), m_openscenery_search, sizeof(m_openscenery_search)); + std::string const text{lower(m_openscenery_search)}; + ImGui::BeginChild("##openlist", ImVec2(360.0f, ImGui::GetTextLineHeightWithSpacing() * 14.0f), true); + for (auto const &name : m_openscenery_list) + { + if (false == text.empty() && lower(name).find(text) == std::string::npos) + continue; + if (ImGui::Selectable(name.c_str(), name == m_openscenery_choice, ImGuiSelectableFlags_AllowDoubleClick)) + { + m_openscenery_choice = name; + if (ImGui::IsMouseDoubleClicked(0) && false == restart_editor(name)) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + } + } + ImGui::EndChild(); + auto const editsession{false == scene::Layers.empty()}; + if (false == m_history.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.3f, 1.0f), "%s", STR_C("What isn't saved of this scenery is lost.")); + auto const chosen{false == m_openscenery_choice.empty()}; + if (chosen && editsession && ImGui::Button(STR_C("Save and open"))) + { + if (save() && false == restart_editor(m_openscenery_choice)) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + ImGui::CloseCurrentPopup(); + } + if (chosen && editsession) + ImGui::SameLine(); + if (chosen && ImGui::Button(STR_C("Open"))) + { + if (false == restart_editor(m_openscenery_choice)) + ui()->set_status(STR_C("The editor couldn't be started again"), true); + ImGui::CloseCurrentPopup(); + } + if (chosen) + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); +} + +void editor_mode::terrain_workspace(bool const Open) +{ + m_terrain_open = Open; + if (false == Open) + m_terrain_tool = terrain_tool::none; + else if (m_streamer.active()) + m_terrain_spacing = m_streamer.manifest().spacing; +} + +void editor_mode::render_terrain_tool_options() +{ + // the orthophoto has a window of its own, see render_orthophoto_window() + render_terrain_ui(); +} + +void editor_mode::render_object_menu() +{ + if (ImGui::BeginMenu(STR_C("Objects"))) + { + auto const *selected = dynamic_cast(m_node); + if (ImGui::MenuItem(STR_C("Put all instances of the selected model on the ground"), nullptr, false, selected != nullptr && selected->Model() != nullptr)) + drop_model_instances(); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", STR_C("Select a model in the scene. Every instance of the same model file is moved straight up or down\n" + "to the ground under it: terrain, large models and terrain made in the editor.")); + ImGui::EndMenu(); + } +} + void editor_mode::add_area_fill_point() { // like the other placement tools, the point comes from the depth buffer under the cursor @@ -1363,7 +3746,8 @@ void editor_mode::add_area_fill_point() if (glm::length(offset) < 1e-3) return; // no ground under the cursor (yet) - m_fill_points.push_back(Camera.Pos + offset); + // the outline gives the fill the height to look for the ground at, and the height of what it finds no ground for + m_fill_points.push_back(placement_on_ground(Camera.Pos + offset)); } void editor_mode::undo_last_area_fill() @@ -1385,7 +3769,7 @@ void editor_mode::undo_last_area_fill() remove_from_hierarchy(model); simulation::State.delete_model(model); } - m_fill_status = "Removed " + std::to_string(created.size()) + " objects of the last fill"; + m_fill_status = format(STR_C("Removed %zu objects of the last fill"), created.size()); } void editor_mode::run_area_fill() @@ -1400,20 +3784,21 @@ void editor_mode::run_area_fill() if (m_fill_points.size() < 3) { - m_fill_status = "Outline needs at least 3 points"; + m_fill_status = STR("Outline needs at least 3 points"); return; } // model package std::vector package; - if (m_fill_package == 0) + if (m_fill_source.kind == model_set_ref::source::manual) package = m_fill_custom; else - package = ui()->nodebank_group_templates(static_cast(m_fill_package - 1)); + for (auto const *entry : ui()->nodebank().set_entries(m_fill_source)) + package.push_back(*entry); package.erase(std::remove_if(package.begin(), package.end(), [](std::string const &Template) { return Template.empty(); }), package.end()); if (package.empty()) { - m_fill_status = "Selected model package is empty"; + m_fill_status = STR("Selected model package is empty"); return; } @@ -1429,7 +3814,7 @@ void editor_mode::run_area_fill() auto const target = static_cast(std::min(std::round(area / 10000.0 * std::max(0.0f, m_fill_density)), max_objects)); if (target == 0) { - m_fill_status = "Density too low for this area, nothing to place"; + m_fill_status = STR("Density too low for this area, nothing to place"); return; } @@ -1470,37 +3855,9 @@ void editor_mode::run_area_fill() points.push_back(p); } - // ground geometry within the area. shapes hold the (legacy) terrain, large instances the terrain tiles + // ground geometry within the area std::vector triangles; - glm::dvec2 const center = (bmin + bmax) * 0.5; - float const radius = static_cast(glm::length(bmax - bmin) * 0.5); - auto const sections = simulation::Region->sections(glm::dvec3(center.x, 0.0, center.y), radius); // copy, the result is a scratchpad - for (auto *section : sections) - { - for (auto const &shape : section->m_shapes) - gather_shape_triangles(shape, bmin, bmax, triangles); - for (auto &cell : section->m_cells) - { - double const r = cell.m_area.radius; - if (cell.m_area.center.x + r < bmin.x || cell.m_area.center.x - r > bmax.x || cell.m_area.center.z + r < bmin.y || cell.m_area.center.z - r > bmax.y) - continue; - for (auto const &shape : cell.m_shapesopaque) - gather_shape_triangles(shape, bmin, bmax, triangles); - if (false == m_fill_models_as_ground) - continue; - for (auto *instance : cell.m_instancesopaque) - { - if (instance == nullptr || instance->Model() == nullptr || instance->radius() < terrain_model_radius) - continue; - std::vector modeltriangles; - gather_submodel_triangles(instance->Model()->Root, instance_matrix(*instance), modeltriangles); - for (auto const &t : modeltriangles) - if (std::max({t[0].x, t[1].x, t[2].x}) >= bmin.x && std::min({t[0].x, t[1].x, t[2].x}) <= bmax.x && std::max({t[0].z, t[1].z, t[2].z}) >= bmin.y && - std::min({t[0].z, t[1].z, t[2].z}) <= bmax.y) - triangles.push_back(t); - } - } - } + gather_ground_triangles(bmin, bmax, m_fill_models_as_ground, terrain_model_radius, triangles); triangle_grid const ground(std::move(triangles), bmin, bmax); auto const terrains = active_terrains(); @@ -1549,10 +3906,10 @@ void editor_mode::run_area_fill() continue; auto const &src = package[package.size() > 1 ? std::min(package.size() - 1, static_cast(LocalRandom(0.0, static_cast(package.size())))) : 0]; - TAnimModel *cloned = simulation::State.create_model(src, "editor_", location); + // NOTE: no names here either, same as with the brush + TAnimModel *cloned = simulation::State.create_model(src, std::string{}, location); if (!cloned) continue; - cloned->m_name = "editor_" + cloned->uuid.to_string(); apply_rotation_for_new_node(cloned, rotation_mode, fixed_rotation_value); if (scalemin != 1.0f || scalemax != 1.0f) cloned->Scale(static_cast(LocalRandom(scalemin, scalemax))); @@ -1562,7 +3919,7 @@ void editor_mode::run_area_fill() // a new fill replaces the "last fill" undo set; the previous objects stay in the scene m_fill_last = std::move(created); - m_fill_status = "Placed " + std::to_string(m_fill_last.size()) + " of " + std::to_string(target) + " objects"; + m_fill_status = format(STR_C("Placed %zu of %zu objects"), m_fill_last.size(), static_cast(target)); if (points.size() < target) m_fill_status += " (limited by min. spacing)"; if (unsupported > 0) @@ -1579,9 +3936,8 @@ void editor_mode::draw_area_fill_outline() const ImGuiIO const &io = ImGui::GetIO(); glm::mat4 const view = GfxRenderer->Camera_View_Matrix(); glm::dvec3 const camerapos = GfxRenderer->Camera_Position(); - float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor); float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; - glm::mat4 const viewprojection = glm::perspective(fovy, aspect, 0.1f, 10000.0f) * view; + glm::mat4 const viewprojection = editor_mode::projection_matrix(aspect) * view; auto const to_clip = [&](glm::dvec3 const &Point) { return viewprojection * glm::vec4(glm::vec3(Point - camerapos), 1.0f); }; auto const to_screen = [&](glm::vec4 const &Clip) { @@ -1589,7 +3945,7 @@ void editor_mode::draw_area_fill_outline() const }; float constexpr nearw = 0.1f; - ImDrawList *drawlist = ImGui::GetBackgroundDrawList(); + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); ImU32 const edgecolor = IM_COL32(255, 200, 40, 230); ImU32 const closingcolor = IM_COL32(255, 200, 40, 110); @@ -1621,140 +3977,773 @@ void editor_mode::draw_area_fill_outline() const void editor_mode::render_area_fill() { - ImGui::Begin("Area fill", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing); - - ImGui::TextUnformatted("LMB: add outline point, Backspace: remove last point"); + ImGui::TextDisabled(STR_C("LMB: add outline point Backspace: remove last point")); double const area = polygon_area_xz(m_fill_points); - ImGui::Text("Points: %zu Area: %.0f m2 (%.2f ha)", m_fill_points.size(), area, area / 10000.0); - if (ImGui::Button("Remove last point") && !m_fill_points.empty()) + ImGui::Text(STR_C("Points: %zu Area: %.0f m2 (%.2f ha)"), m_fill_points.size(), area, area / 10000.0); + if (ImGui::Button(STR_C("Remove last point")) && !m_fill_points.empty()) m_fill_points.pop_back(); ImGui::SameLine(); - if (ImGui::Button("Clear outline")) + if (ImGui::Button(STR_C("Clear outline"))) m_fill_points.clear(); ImGui::Separator(); - // model package: a node bank group, or a custom set assembled from node bank templates - auto const groups = ui()->nodebank_groups(); - m_fill_package = std::clamp(m_fill_package, 0, static_cast(groups.size())); - std::string const current = m_fill_package == 0 ? std::string("Custom set") : groups[m_fill_package - 1]; - ImGui::SetNextItemWidth(250.0f); - if (ImGui::BeginCombo("Model package", current.c_str())) - { - if (ImGui::Selectable("Custom set", m_fill_package == 0)) - m_fill_package = 0; - for (int i = 0; i < static_cast(groups.size()); ++i) - { - auto const label = groups[i] + "##fillgroup" + std::to_string(i); - if (ImGui::Selectable(label.c_str(), m_fill_package == i + 1)) - m_fill_package = i + 1; - } - ImGui::EndCombo(); - } - - if (m_fill_package == 0) - { - ImGui::BeginChild("fill_custom_set", ImVec2(350.0f, 110.0f), true); - for (int i = 0; i < static_cast(m_fill_custom.size()); ++i) - { - auto const label = template_label(m_fill_custom[i]) + "##fillcustom" + std::to_string(i); - if (ImGui::Selectable(label.c_str(), m_fill_custom_idx == i)) - m_fill_custom_idx = i; - } - ImGui::EndChild(); - if (ImGui::Button("Add selected template")) - { - // the node bank selection, not a random pick from the brush set - auto const *src = ui()->get_active_node_template(true); - if (src && !src->empty()) - m_fill_custom.push_back(*src); - } - ImGui::SameLine(); - if (ImGui::Button("Remove") && m_fill_custom_idx >= 0 && m_fill_custom_idx < static_cast(m_fill_custom.size())) - { - m_fill_custom.erase(m_fill_custom.begin() + m_fill_custom_idx); - m_fill_custom_idx = -1; - } - ImGui::SameLine(); - if (ImGui::Button("Clear set")) - { - m_fill_custom.clear(); - m_fill_custom_idx = -1; - } - } + // model package: a hand-assembled list, a user-defined set or a node bank group + auto &bank = ui()->nodebank(); + bank.set_combo(STR_C("Model set"), m_fill_source, STR_C("Custom list")); + if (m_fill_source.kind == model_set_ref::source::manual) + bank.manual_list("fillset", m_fill_custom, m_fill_custom_idx); else - { - ImGui::Text("Templates in group: %zu", ui()->nodebank_group_templates(static_cast(m_fill_package - 1)).size()); - } + ImGui::TextDisabled(STR_C("%zu templates in set"), bank.set_entries(m_fill_source).size()); ImGui::Separator(); ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Density (objects/ha)", &m_fill_density, 10.0f, 100.0f, "%.1f"); + ImGui::InputFloat(STR_C("Density (objects/ha)"), &m_fill_density, 10.0f, 100.0f, "%.1f"); m_fill_density = std::max(0.0f, m_fill_density); ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Min. spacing (m)", &m_fill_min_spacing, 0.5f, 2.0f, "%.1f"); + ImGui::InputFloat(STR_C("Min. spacing (m)"), &m_fill_min_spacing, 0.5f, 2.0f, "%.1f"); m_fill_min_spacing = std::max(0.0f, m_fill_min_spacing); ImGui::SetNextItemWidth(200.0f); ImGui::DragFloatRange2("Scale", &m_fill_scale_min, &m_fill_scale_max, 0.01f, 0.1f, 5.0f, "min %.2f", "max %.2f"); - ImGui::Checkbox("Random rotation (off: Functions panel setting)", &m_fill_random_rotation); - ImGui::Checkbox("Large models count as ground (terrain tiles)", &m_fill_models_as_ground); + ImGui::Checkbox(STR_C("Random rotation"), &m_fill_random_rotation); + if (!m_fill_random_rotation) + ui()->render_rotation_controls(); + ImGui::Checkbox(STR_C("Large models count as ground (terrain tiles)"), &m_fill_models_as_ground); - ImGui::Text("Estimated objects: %.0f", std::round(area / 10000.0 * m_fill_density)); + ImGui::Text(STR_C("Estimated objects: %.0f"), std::round(area / 10000.0 * m_fill_density)); - if (ImGui::Button("Fill area")) + if (ImGui::Button(STR_C("Fill area"))) run_area_fill(); ImGui::SameLine(); - std::string const undolabel = "Undo last fill (" + std::to_string(m_fill_last.size()) + ")"; + std::string const undolabel = format(STR_C("Undo last fill (%zu)"), m_fill_last.size()); if (ImGui::Button(undolabel.c_str()) && !m_fill_last.empty()) undo_last_area_fill(); if (!m_fill_status.empty()) ImGui::TextUnformatted(m_fill_status.c_str()); +} - ImGui::End(); +glm::vec3 editor_mode::array_size(TAnimModel const &Base) +{ + auto *model = Base.Model(); + if (model == nullptr) + return glm::vec3{0.0f}; + if (m_array.sized == model) + return m_array.size; + // the triangles the other tools take for the ground, left where the model has them + std::vector triangles; + gather_submodel_triangles(model->Root, glm::dmat4{1.0}, triangles); + if (triangles.empty()) + return glm::vec3{0.0f}; // not loaded yet, or nothing to it; it's asked about again the next time + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{-std::numeric_limits::max()}; + for (auto const &triangle : triangles) + { + for (auto const &vertex : triangle) + { + low = glm::min(low, vertex); + high = glm::max(high, vertex); + } + } + m_array.sized = model; + m_array.size = glm::vec3{high - low}; + return m_array.size; +} + +glm::dvec3 editor_mode::array_offset(TAnimModel const &Base) +{ + auto const &settings = m_array.settings; + glm::dvec3 offset{0.0}; + if (settings.relative) + offset += glm::dvec3{settings.relative_offset * array_size(Base) * Base.Scale()}; + if (settings.constant) + offset += glm::dvec3{settings.constant_offset}; + return offset; +} + +int editor_mode::array_count(double const Step) const +{ + auto const &settings = m_array.settings; + if (settings.fit == 0) + return std::clamp(settings.count, 1, array_tool::limit); + if (Step < array_tool::minimal_step) + return 1; + // the model, and a copy for each whole offset the length takes + return static_cast(std::min(array_tool::limit, std::floor(std::max(0.0f, settings.length) / Step + 1e-6) + 1.0)); +} + +std::vector> editor_mode::array_placements(TAnimModel &Base) +{ + // a model this large counts as ground itself, and would be put on top of its own copies + float constexpr terrain_model_radius = 50.0f; + + std::vector> places; + auto const &settings = m_array.settings; + auto const offset = array_offset(Base); + auto const count = array_count(glm::length(offset)); + if (count < 2 || glm::length(offset) < array_tool::minimal_step) + return places; + + // the offset goes along the axes of the model, which is turned the way the renderer does it: around y, then x, then z + auto angles = Base.Angles(); + glm::dmat4 turned{1.0}; + turned = glm::rotate(turned, glm::radians(static_cast(angles.y)), glm::dvec3{0.0, 1.0, 0.0}); + turned = glm::rotate(turned, glm::radians(static_cast(angles.x)), glm::dvec3{1.0, 0.0, 0.0}); + turned = glm::rotate(turned, glm::radians(static_cast(angles.z)), glm::dvec3{0.0, 0.0, 1.0}); + auto step = glm::dvec3{turned * glm::dvec4{offset, 0.0}}; + auto location = Base.location(); + places.reserve(count - 1); + for (int idx = 1; idx < count; ++idx) + { + // each copy is turned against the one before it, and leads to the next one the way it faces + location += step; + if (false == simulation::Region->point_inside(location)) + break; // the row ends at the edge of the region, nodes past it are left out of the scene + if (settings.turn != 0.0f) + { + step = glm::rotateY(step, glm::radians(static_cast(settings.turn))); + angles.y = clamp_circular(angles.y + settings.turn, 360.0f); + } + places.emplace_back(location, angles); + } + + if (settings.ground && Base.radius() < terrain_model_radius) + { + // the ground the road tools use. a copy with nothing under it stays at the height the offset gave it + std::vector points; + points.reserve(places.size() + 1); + points.push_back(Base.location()); + for (auto const &place : places) + points.push_back(place.first); + std::vector found; + auto const heights = ground_heights(points, true, &found); + if (found[0]) + { + auto const clearance = points[0].y - heights[0]; + for (std::size_t idx = 0; idx < places.size(); ++idx) + { + if (found[idx + 1]) + places[idx].first.y = heights[idx + 1] + clearance; + } + } + } + return places; +} + +bool editor_mode::array_live() const +{ + // the model the array was made of is still selected, and the array is the last thing done + return m_array.base != nullptr && m_array.base == m_node && false == m_history.empty() && m_history.size() == m_array.step && + m_history.back().action == EditorSnapshot::Action::Array; +} + +void editor_mode::make_array() +{ + auto *base = dynamic_cast(m_node); + if (base == nullptr || false == scene::Layers.editable(base)) + return; + auto const step = glm::length(array_offset(*base)); + if (step < array_tool::minimal_step) + { + m_array.status = STR("The offsets come to nothing, the copies would sit in the model"); + return; + } + if (array_count(step) < 2) + { + m_array.status = (m_array.settings.fit == 0 ? STR("An array of one is the model itself, nothing to make") : STR("The length takes no copy, nothing to make")); + return; + } + + // a single undo step for the whole array, whatever is done to it while it follows the settings + trim_history(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Array; + snap.node_name = base->name(); + snap.position = base->location(); + snap.layer = base->layer(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + m_array.base = base; + m_array.step = m_history.size(); + shape_array(); +} + +void editor_mode::shape_array() +{ + auto *base = m_array.base; + auto &copies = m_history.back().copies; + auto const places = array_placements(*base); + + // the copies there's no place for anymore go + while (copies.size() > places.size()) + { + auto *model = copies.back().model; + copies.pop_back(); + if (model == nullptr) + continue; + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + } + + std::string definition; + for (std::size_t idx = 0; idx < places.size(); ++idx) + { + TAnimModel *model = nullptr; + if (idx < copies.size()) + { + model = copies[idx].model; + m_editor.translate(model, places[idx].first, true); // true: the height is set as well + } + else + { + if (definition.empty()) + base->export_as_text(definition); + // NOTE: no names for the copies, same as with the brush + model = simulation::State.create_model(definition, std::string{}, places[idx].first); + if (model == nullptr) + break; + // the copies go where the model is, rather than to the active layer + scene::Layers.move(model, base->layer()); + EditorSnapshot::array_copy copy; + copy.model = model; + copy.uuid = model->uuid; + copies.push_back(std::move(copy)); + } + model->Angles(places[idx].second); + model->Scale(base->Scale()); + } + + m_array.applied = m_array.settings; + m_array.location = base->location(); + m_array.angles = base->Angles(); + m_array.scale = base->Scale(); + if (copies.size() < places.size()) + m_array.status = format(STR_C("Only %zu of %zu copies could be made"), copies.size(), places.size()); + else if (copies.empty()) + m_array.status = STR("No copies with these settings"); + else + m_array.status = format(STR_C("The model and %zu copies; Ctrl+Z takes the array back"), copies.size()); +} + +void editor_mode::update_array() +{ + if (m_array.base == nullptr) + return; + if (false == array_live()) + { + // once left, the copies are models like any other + m_array.base = nullptr; + return; + } + auto const *base = m_array.base; + if (m_array.settings == m_array.applied && base->location() == m_array.location && base->Angles() == m_array.angles && base->Scale() == m_array.scale) + return; + shape_array(); +} + +void editor_mode::restore_array_snapshot(EditorSnapshot Snapshot, std::vector &Opposite, bool const Undo) +{ + // either way the array is done with following the settings + m_array.base = nullptr; + for (auto © : Snapshot.copies) + { + if (Undo) + { + // NOTE: a copy which was deleted and brought back is another model by now, found by its uuid + auto *model = dynamic_cast(find_node_by_any(copy.model, copy.uuid.to_string(), std::string{})); + copy.model = nullptr; + copy.serialized.clear(); + if (model == nullptr) + continue; // deleted since, and stays that way + // the copy is brought back the way it is now, with whatever was done to it since the array was made + model->export_as_text(copy.serialized); + copy.name = model->name(); + copy.position = model->location(); + copy.rotation = model->Angles(); + copy.scale = model->Scale(); + if (m_node == model) + { + m_node = nullptr; + m_dragging = false; + ui()->set_node(nullptr); + } + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + } + else + { + if (copy.serialized.empty()) + continue; + auto *model = simulation::State.create_model(copy.serialized, copy.name, copy.position); + if (model == nullptr) + continue; + scene::Layers.move(model, Snapshot.layer); + model->Angles(copy.rotation); + model->Scale(copy.scale); + model->uuid = copy.uuid; + add_to_hierarchy(model); + copy.model = model; + } + } + Opposite.push_back(std::move(Snapshot)); +} + +void editor_mode::render_array() +{ + auto &settings = m_array.settings; + auto *selected = dynamic_cast(m_node); + if (selected != nullptr && false == scene::Layers.editable(selected)) + selected = nullptr; + + ImGui::PushID("array"); + ImGui::TextDisabled(STR_C("Copies of the selected model, set out in a row")); + ImGui::RadioButton(STR_C("Fixed count"), &settings.fit, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Fit length"), &settings.fit, 1); + ImGui::SetNextItemWidth(120.0f); + if (settings.fit == 0) + { + ImGui::InputInt(STR_C("Count"), &settings.count); + settings.count = std::clamp(settings.count, 1, array_tool::limit); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Number of the models in the array, the selected one included")); + } + else + { + ImGui::InputFloat(STR_C("Length (m)"), &settings.length, 1.0f, 10.0f, "%.2f"); + settings.length = std::max(0.0f, settings.length); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("As many copies are made as fit in this length, measured along the row")); + } + + ImGui::Checkbox(STR_C("Relative offset"), &settings.relative); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Distance between the copies as a part of the size of the model, along its axes: x, y (up), z.\n" + "1 along an axis puts the copies end to end")); + if (settings.relative) + { + ImGui::SetNextItemWidth(200.0f); + ImGui::DragFloat3(STR_C("x size"), &settings.relative_offset.x, 0.01f, 0.0f, 0.0f, "%.3f"); + } + ImGui::Checkbox(STR_C("Constant offset"), &settings.constant); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Distance between the copies in metres, along the axes of the model: x, y (up), z.\n" + "With both offsets on the copies are set apart by their sum")); + if (settings.constant) + { + ImGui::SetNextItemWidth(200.0f); + ImGui::DragFloat3("m", &settings.constant_offset.x, 0.05f, 0.0f, 0.0f, "%.3f"); + } + ImGui::SetNextItemWidth(120.0f); + ImGui::DragFloat(STR_C("Turn per copy (deg)"), &settings.turn, 0.1f, -180.0f, 180.0f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Each copy is turned by this much around the vertical axis against the one before it,\n" + "and the row bends with them into an arc")); + ImGui::Checkbox(STR_C("Follow the ground"), &settings.ground); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The copies are put as high over the ground as the model is, instead of level with it.\n" + "Not for the models large enough to count as ground themselves")); + + if (selected != nullptr) + { + auto const size = array_size(*selected) * selected->Scale(); + auto const step = glm::length(array_offset(*selected)); + ImGui::TextDisabled(STR_C("Model %.2f x %.2f x %.2f m, %d in the array, %.2f m apart"), size.x, size.y, size.z, array_count(step), step); + } + + if (array_live()) + { + // NOTE: kept away from the place of the other button, a click too many on which would make the array twice + ImGui::TextDisabled(STR_C("The array follows the settings")); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Done"))) + m_array.base = nullptr; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The copies stay the way they are. Selecting or changing anything else does the same")); + } + else if (selected != nullptr) + { + if (ImGui::Button(STR_C("Make array"))) + make_array(); + } + else + { + ImGui::TextDisabled(STR_C("Select a model to make an array of")); + } + if (false == m_array.status.empty()) + ImGui::TextUnformatted(m_array.status.c_str()); + ImGui::PopID(); +} + +void editor_mode::choose_edit_mode(nodebank_panel::edit_mode const Mode) +{ + // an edit mode is the field of work instead of the tracks, roads or terrain + m_track_window_open = false; + m_roadtool.window = false; + terrain_workspace(false); + if (Mode != nodebank_panel::MODIFY) + m_selection.clear(); + ui()->set_mode(Mode); +} + +editor_mode::work_area editor_mode::current_work_area() const +{ + // the terrain is shown over the tracks, see the tool options window + if (m_terrain_open) + return work_area::terrain; + if (m_track_window_open) + return work_area::tracks; + if (m_roadtool.window) + return work_area::roads; + return work_area::surroundings; +} + +void editor_mode::choose_work_area(work_area const Area) +{ + switch (Area) + { + case work_area::surroundings: + // the edit mode used last + choose_edit_mode(ui()->nodebank().mode); + break; + case work_area::tracks: + terrain_workspace(false); + show_track_tab(m_track_tab); + break; + case work_area::roads: + m_track_window_open = false; + terrain_workspace(false); + m_roadtool.window = true; + break; + case work_area::terrain: + m_track_window_open = false; + m_roadtool.window = false; + terrain_workspace(true); + break; + } +} + +void editor_mode::render_workspaces() +{ + struct area + { + char const *label; + work_area which; + editor_icons::icon picture; + char const *tooltip; + }; + area const areas[] = { + {STR_C("Surroundings"), work_area::surroundings, editor_icons::icon::work_surroundings, STR_C("Models around the line: select, insert, brush, area fill, copy to the node bank")}, + {STR_C("Tracks"), work_area::tracks, editor_icons::icon::work_tracks, STR_C("Tracks and switches, signals, objects along the track, checks of the line")}, + {STR_C("Roads"), work_area::roads, editor_icons::icon::work_roads, STR_C("Roads and junctions, level crossings, the points where the traffic comes and goes")}, + {STR_C("Terrain"), work_area::terrain, editor_icons::icon::work_terrain, STR_C("Heightmap terrain: chunks, sculpting, painting, water, orthophoto")}, + }; + char const *const keys[] = {"F2", "F3", "F4", "F5"}; + auto const current{current_work_area()}; + auto const &style{ImGui::GetStyle()}; + auto *list{ImGui::GetWindowDrawList()}; + // bigger and bold, with the font made for it if there's one, or drawn twice a pixel apart + auto *font{ui_layer::font_bold != nullptr ? ui_layer::font_bold : ImGui::GetFont()}; + auto const fontsize{ui_layer::font_bold != nullptr ? ui_layer::font_bold->FontSize : ImGui::GetFontSize() * 1.25f}; + auto const height{ImGui::GetWindowHeight()}; + auto const padding{style.FramePadding.x * 3.0f}; + // the icon of the field of work before its name + auto const iconside{editor_icons::size()}; + auto const spacing{style.ItemInnerSpacing.x * 1.5f}; + render_toolbar_caption(STR_C("Work modes")); + for (auto const &entry : areas) + { + auto const text{font->CalcTextSizeA(fontsize, FLT_MAX, 0.0f, entry.label)}; + ImVec2 const size{text.x + iconside + spacing + padding * 2.0f, height}; + auto const position{ImGui::GetCursorScreenPos()}; + ImGui::PushID(static_cast(entry.which)); + if (ImGui::InvisibleButton("##area", size)) + choose_work_area(entry.which); + ImGui::PopID(); + auto const hovered{ImGui::IsItemHovered()}; + auto const chosen{entry.which == current}; + ImVec2 const end{position.x + size.x, position.y + size.y}; + if (chosen || hovered) + list->AddRectFilled(position, end, ImGui::GetColorU32(chosen ? ImGuiCol_Header : ImGuiCol_HeaderHovered)); + if (chosen) + list->AddRectFilled(ImVec2(position.x, end.y - 3.0f), end, ImGui::GetColorU32(ImGuiCol_CheckMark)); + ImVec2 const textposition{position.x + padding + iconside + spacing, position.y + (height - text.y) * 0.5f}; + auto const color{ImGui::GetColorU32(chosen ? ImGuiCol_Text : ImGuiCol_TextDisabled)}; + editor_icons::draw(list, entry.picture, ImVec2(position.x + padding + iconside * 0.5f, position.y + height * 0.5f), color); + list->AddText(font, fontsize, textposition, color, entry.label); + if (ui_layer::font_bold == nullptr) + list->AddText(font, fontsize, ImVec2(textposition.x + 1.0f, textposition.y), color, entry.label); + if (hovered) + ImGui::SetTooltip("%s [%s]", entry.tooltip, keys[static_cast(entry.which)]); + ImGui::SameLine(0.0f, style.ItemSpacing.x); + } +} + +void editor_mode::render_toolbar() +{ + // the modes of the gizmo and the top view are over the 3d view, see render_viewport_overlays() + render_toolbar_tools(); +} + +void editor_mode::render_edit_menu() +{ + if (false == ImGui::BeginMenu(STR_C("Edit"))) + return; + if (ImGui::MenuItem(STR_C("Undo"), "Ctrl+Z", false, false == m_history.empty())) + undo_last(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Takes the last change back")); + if (ImGui::MenuItem(STR_C("Redo"), "Ctrl+Y", false, false == g_redo.empty())) + redo_last(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Makes the change taken back again")); + ImGui::Separator(); + auto const selected{m_node != nullptr}; + if (ImGui::MenuItem(STR_C("Delete"), STR_C("Del"), false, selected || m_instance != 0)) + delete_selected(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Deletes what's selected: a model, tracks, a road piece or point, a signal, an include of a scenery template")); + auto const models{dynamic_cast(m_node) != nullptr && ui()->mode() != nodebank_panel::TRACK}; + if (ImGui::MenuItem(STR_C("Copy"), "Ctrl+C", false, models)) + copy_selection(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Copies the selected models with their groups")); + if (ImGui::MenuItem(STR_C("Paste"), "Ctrl+V", false, false == m_clipboard.empty())) + paste_clipboard(cursor_ground()); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Puts the copied models at the cursor, around it as they were around the selected one; Ctrl+RMB in the view pastes them where it was clicked")); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Group"), "Ctrl+G", false, models && selected_nodes().size() > 1)) + group_selection(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The selected models move together from now on; a click on one selects them all. Shift+LMB adds a model to the selection")); + // Delete above may have just dropped the node, selected is from before it + if (ImGui::MenuItem(STR_C("Ungroup"), "Ctrl+Shift+G", false, m_node != nullptr && m_node->group() > 1)) + ungroup_selection(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The models of the group move each by itself again")); + if (ImGui::MenuItem(STR_C("Drop to the ground"), STR_C("End"), false, selected)) + drop_selection_to_ground(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Puts each selected model, also each model of their groups, down onto the terrain or the object under it")); + if (ImGui::MenuItem(STR_C("Fly to the selected"), "F", false, selected)) + start_focus(m_node, 0.6); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The camera flies to the selected model and looks at it")); + if (ImGui::MenuItem(STR_C("Find a track by its name"), "Ctrl+F")) + { + terrain_workspace(false); + show_track_tab(m_track_window_open ? m_track_tab : track_tab::path); + m_track_search.focus = true; + } + if (ImGui::MenuItem(STR_C("Bend along the track"), "B", false, ui()->mode() != nodebank_panel::TRACK)) + bend_shortcut(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Bends the selected model, e.g. a platform edge or a fence, along the nearest track; the settings are in the inspector")); + ImGui::EndMenu(); +} + +void editor_mode::render_help_menu() +{ + if (false == ImGui::BeginMenu(STR_C("Help"))) + return; + ImGui::MenuItem(STR_C("Keyboard shortcuts"), "F1", &m_shortcuts_open); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The keys of the editor, for each field of work")); + ImGui::EndMenu(); +} + +void editor_mode::render_shortcuts_window() +{ + if (false == m_shortcuts_open) + return; + struct shortcut + { + char const *keys; + char const *action; + }; + struct group + { + char const *title; + std::vector shortcuts; + }; + static std::vector const groups{ + {"General", + {{"F1", "This list of shortcuts"}, + {"Ctrl+S", "Save the scenery"}, + {"Ctrl+Z / Ctrl+Y", "Undo / redo"}, + {"Ctrl+Space", "Unpin all docks for the most of the view, or pin them back"}, + {"F9", "Log"}, + {"PrtScr", "Screenshot"}, + {"Ctrl+Shift+F11", "Export scenery dump"}, + {"F11", "Leave the editor"}, + {"F10", "Quit"}}}, + {"Fields of work", {{"F2", "Surroundings"}, {"F3", "Tracks"}, {"F4", "Roads"}, {"F5", "Terrain"}}}, + {"Camera", + {{"RMB held, mouse", "Look around"}, + {"RMB held, W S A D", "Fly forwards, back, left, right (arrows in the legacy scheme)"}, + {"RMB held, E / Q", "Fly up / down (Page Up / Page Down in the legacy scheme)"}, + {"F", "Fly to the selected"}, + {"O", "Top view, orthographic"}}}, + {"Gizmo", + {{"Q / W / E", "Translate / rotate / scale"}, {"R", "Local space"}, {"Ctrl held while dragging", "Snap to the step set in the gizmo settings"}}}, + {"Surroundings", + {{"1 2 3 4 5", "Select, insert, brush, area fill, copy to bank"}, + {"LMB", "Select a model, or place one in the insert and brush modes"}, + {"Shift+LMB", "Add a model to the selection, or take it out"}, + {"Ctrl+RMB", "Menu of the selection at the cursor"}, + {"Ctrl+C / Ctrl+V", "Copy the selected models / paste them at the cursor"}, + {"Ctrl+G / Ctrl+Shift+G", "Group the selected models / take their group apart"}, + {"End", "Drop the selected onto the ground, each model of a group by itself"}, + {"Del", "Delete the selected"}, + {"B", "Bend the selected along the track"}, + {"Backspace", "Area fill: take the last point of the outline back"}}}, + {"Tracks", + {{"Esc", "Select tool; cancels what's going on first"}, + {"L / T / U", "Lay track / switch / turntable"}, + {"G / C", "Straight / curve"}, + {"H / B", "Signals / objects along the track"}, + {"P / V / J / I", "Profile / speed / joints / infrastructure"}, + {"Ctrl+F", "Find a track by its name"}, + {"Shift+LMB", "Add a track to the selection, drag: a box"}, + {"RMB on a track", "What can be done with it"}, + {"K", "Split the selected track at the cursor"}, + {"Enter / Backspace", "Laying track: finish / take the last point back"}, + {"Del", "Delete the selected tracks"}}}, + {"Roads", + {{"Esc", "Finish the road being built"}, + {"Ctrl held", "Build: straight ahead"}, + {"Shift+LMB", "Select: add a point, or take it out"}, + {"K", "Split the road at the cursor, or make a point there"}, + {"Del", "Delete the selected"}}}, + {"Terrain", {{"LMB", "Sculpt: raise; smooth: even out; chunks: add one"}, {"Shift+LMB", "Sculpt: lower; chunks: delete one"}}}, + }; + ImGui::SetNextWindowSize(ImVec2(620.0f * Global.ui_scale, 560.0f * Global.ui_scale), ImGuiCond_FirstUseEver); + // it floats over the view, read better on a solid background + ImGui::SetNextWindowBgAlpha(0.97f); + if (ImGui::Begin((std::string(STR_C("Keyboard shortcuts")) + "###shortcuts").c_str(), &m_shortcuts_open, ImGuiWindowFlags_NoCollapse)) + { + for (auto const &entry : groups) + { + ImGui::SeparatorText(STR_C(entry.title)); + if (false == ImGui::BeginTable(entry.title, 2, ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingStretchProp)) + continue; + ImGui::TableSetupColumn("##keys", ImGuiTableColumnFlags_WidthFixed, 190.0f * Global.ui_scale); + ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthStretch); + for (auto const &item : entry.shortcuts) + { + ImGui::TableNextColumn(); + ImGui::TextColored(ImGui::GetStyleColorVec4(ImGuiCol_CheckMark), "%s", STR_C(item.keys)); + ImGui::TableNextColumn(); + ImGui::TextWrapped("%s", STR_C(item.action)); + } + ImGui::EndTable(); + } + } + ImGui::End(); +} + +void editor_mode::render_gizmo_options() +{ + if (ImGui::Button(Global.EditorOrtho ? "3D view (O)" : "Top view, orthographic (O)")) + toggle_ortho(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", Global.EditorOrtho ? STR_C("Wheel zooms. Hold the wheel and drag to pan.") : STR_C("Top-down orthographic view")); + if (Global.EditorOrtho) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(120.0f); + ImGui::SliderFloat(STR_C("Extent (m), wheel / MMB pan"), &Global.EditorOrthoExtent, 5.0f, 5000.0f, "%.0f", ImGuiSliderFlags_Logarithmic); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Wheel zooms. Hold the wheel and drag to pan.")); + } + ImGui::Checkbox(STR_C("Enabled"), &m_gizmo_enabled); + if (!m_gizmo_enabled) + return; + if (terrain_brush() || terrain_clicking()) + { + ImGui::TextDisabled(STR_C("Suspended while editing terrain")); + return; + } + + // lets the user pick the transform mode without keyboard shortcuts + int op = static_cast(m_gizmo_op); + ImGui::RadioButton(STR_C("Translate (Q)"), &op, static_cast(gizmo_operation::translate)); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Rotate (W)"), &op, static_cast(gizmo_operation::rotate)); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Scale (E)"), &op, static_cast(gizmo_operation::scale)); + m_gizmo_op = static_cast(op); + + if (m_gizmo_op != gizmo_operation::scale) // ImGuizmo always scales in local space + ImGui::Checkbox(STR_C("Local space (R)"), &m_gizmo_local); + if (m_gizmo_op == gizmo_operation::translate) + { + ImGui::SetNextItemWidth(120.0f); + ImGui::InputFloat(STR_C("Snap (hold Ctrl)"), &m_gizmo_snap); + if (m_gizmo_snap < 0.0f) + m_gizmo_snap = 0.0f; + } + if (m_instance != 0) + { + auto const located = editor_includes::parameter_with_role(m_include.info, "pos.x") != 0 || editor_includes::parameter_with_role(m_include.info, "pos.y") != 0 || + editor_includes::parameter_with_role(m_include.info, "pos.z") != 0; + ImGui::TextDisabled("%s", false == located ? "Include selected, no position parameters described" : + false == m_include.placement ? STR_C("Include selected, no gizmo under rotate or scale") : + STR_C("Include selected, axes as its template allows")); + } + else if (!m_node) + ImGui::TextDisabled(STR_C("No node selected")); } void editor_mode::render_gizmo() { // the transform gizmo is suppressed while editing terrain, so the brush/chunk tool owns the mouse - if (!m_gizmo_enabled || m_terrain_sculpt || m_chunk_edit) + if (!m_gizmo_enabled || terrain_brush() || terrain_clicking()) { m_gizmo_using = false; return; } - // compact control window: lets the user pick the transform mode without keyboard shortcuts - ImGui::Begin("Gizmo", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing); - int op = static_cast(m_gizmo_op); - ImGui::RadioButton("Translate (Q)", &op, static_cast(gizmo_operation::translate)); - ImGui::SameLine(); - ImGui::RadioButton("Rotate (W)", &op, static_cast(gizmo_operation::rotate)); - ImGui::SameLine(); - ImGui::RadioButton("Scale (E)", &op, static_cast(gizmo_operation::scale)); - m_gizmo_op = static_cast(op); - - if (m_gizmo_op != gizmo_operation::scale) // ImGuizmo always scales in local space - ImGui::Checkbox("Local space (R)", &m_gizmo_local); - if (m_gizmo_op == gizmo_operation::translate) + if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && m_track_set.size() >= 2) { - ImGui::SetNextItemWidth(120.0f); - ImGui::InputFloat("Snap (hold Ctrl)", &m_gizmo_snap); - if (m_gizmo_snap < 0.0f) - m_gizmo_snap = 0.0f; + render_track_set_gizmo(); + return; } - if (!m_node) - ImGui::TextDisabled("No node selected"); - ImGui::End(); - if (!m_node) + if (straights_active()) + { + render_straight_gizmo(); + return; + } + + if (route_active()) + { + render_route_gizmo(); + return; + } + + if (!m_node && m_instance == 0) { m_gizmo_using = false; return; } + if (selected_track()) + { + // in the track modes the whole path is moved only while selecting, or the switch in its mode + if (ui()->mode() != nodebank_panel::TRACK || (m_track_tab == track_tab::path && false == m_point_drag.active) || (m_track_tab == track_tab::turnout && selected_track()->eType == tt_Switch)) + render_track_gizmo(); + else + m_track_gizmo_using = false; + return; + } + ImGuizmo::BeginFrame(); - ImGuizmo::SetOrthographic(false); + ImGuizmo::SetOrthographic(Global.EditorOrtho); ImGuiIO const &io = ImGui::GetIO(); ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); @@ -1767,9 +4756,14 @@ void editor_mode::render_gizmo() // the engine's own projection bakes in reverse-Z (and screen orientation), which ImGuizmo // doesn't expect; rebuild a clean, standard perspective that matches the rendered view. // for the main viewport the engine uses a symmetric frustum with this exact fov/aspect. - float const fovy = glm::radians(Global.FieldOfView / Global.ZoomFactor); float const aspect = io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f; - glm::mat4 const projection = glm::perspective(fovy, aspect, 0.1f, 10000.0f); + glm::mat4 const projection = editor_mode::projection_matrix(aspect); + + if (!m_node) + { + render_include_gizmo(view, projection, camerapos); + return; + } // rotation/scale are only meaningful for instanced models; other node types translate only TAnimModel *model = dynamic_cast(m_node); @@ -1818,7 +4812,8 @@ void editor_mode::render_gizmo() // record a single undo snapshot at the start of the drag if (!m_gizmo_using) { - push_snapshot(m_node, action); + // the models selected along with it and their groups go into the same step of the history + selection_snapshots(action); m_gizmo_using = true; } @@ -1829,7 +4824,9 @@ void editor_mode::render_gizmo() { // apply the rotation delta relative to the model's current orientation glm::vec3 const newangles(newrotation[0], newrotation[1], newrotation[2]); - m_editor.rotate(model, newangles - model->Angles(), 0.0f); + auto const turned{newangles - model->Angles()}; + m_editor.rotate(model, turned, 0.0f); + selection_follow(glm::dvec3(0.0), turned, model->location()); } else if (operation == ImGuizmo::SCALE && model) { @@ -1839,7 +4836,9 @@ void editor_mode::render_gizmo() { glm::dvec3 const newworldpos = camerapos + glm::dvec3(newtranslation[0], newtranslation[1], newtranslation[2]); // pass Snaptoground == true so the gizmo's Y component is applied (free 3D move) + auto const before{m_node->location()}; m_editor.translate(m_node, newworldpos, true); + selection_follow(m_node->location() - before, glm::vec3(0.0f), m_node->location()); } } else @@ -1848,9 +4847,175 @@ void editor_mode::render_gizmo() } } +void editor_mode::render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos) +{ + m_gizmo_using = false; + if (false == m_include.placement || false == m_include.issue.empty()) + return; + + // parameters of the template the gizmo can drive, as indices of their values; -1 where the template has none + static char const *const positionroles[] = {"pos.x", "pos.y", "pos.z"}; + static char const *const rotationroles[] = {"rot.x", "rot.y", "rot.z"}; + auto const numeric_parameter = [this](char const *Role, double &Value) { + auto const index = editor_includes::parameter_with_role(m_include.info, Role) - 1; + if (index < 0 || index >= static_cast(m_include.values.size())) + return -1; + auto const &text = m_include.values[index]; + char *end = nullptr; + auto const number = std::strtod(text.c_str(), &end); + if (text.empty() || *end != '\0') + return -1; + Value = number; + return index; + }; + int position[3], rotation[3]; + double location[3] = {0.0, 0.0, 0.0}, angles[3] = {0.0, 0.0, 0.0}; + auto positions = 0, rotations = 0; + for (auto axis = 0; axis < 3; ++axis) + { + position[axis] = numeric_parameter(positionroles[axis], location[axis]); + rotation[axis] = numeric_parameter(rotationroles[axis], angles[axis]); + positions += (position[axis] >= 0 ? 1 : 0); + rotations += (rotation[axis] >= 0 ? 1 : 0); + } + if (positions == 0) + return; // nothing tells where the include is + + // only the axes the template has parameters for can be manipulated + auto operation = 0; + for (auto axis = 0; axis < 3; ++axis) + { + if (m_gizmo_op == gizmo_operation::translate && position[axis] >= 0) + operation |= (ImGuizmo::TRANSLATE_X << axis); + else if (m_gizmo_op == gizmo_operation::rotate && rotation[axis] >= 0) + operation |= (ImGuizmo::ROTATE_X << axis); + } + if (operation == 0) + return; + auto const translating = (m_gizmo_op == gizmo_operation::translate); + + // the parameters are relative to the origin in effect at the directive + auto const &offset = scene::Layers.instance(m_instance).context.offset; + glm::vec3 const relativepos = glm::vec3(offset + glm::dvec3{location[0], location[1], location[2]} - Camerapos); + float const translation[3] = {relativepos.x, relativepos.y, relativepos.z}; + float const rotationangles[3] = {static_cast(angles[0]), static_cast(angles[1]), static_cast(angles[2])}; + float const scale[3] = {1.0f, 1.0f, 1.0f}; + glm::mat4 matrix(1.0f); + ImGuizmo::RecomposeMatrixFromComponents(translation, rotationangles, scale, glm::value_ptr(matrix)); + + // moving along the axes of the include would need all three of them + ImGuizmo::MODE const mode = (m_gizmo_local && (false == translating || positions == 3)) ? ImGuizmo::LOCAL : ImGuizmo::WORLD; + glm::vec3 const snapvalue(translating ? m_gizmo_snap : 5.0f); + float const *snap = Global.ctrlState && snapvalue.x > 0.0f ? glm::value_ptr(snapvalue) : nullptr; + + ImGuizmo::Manipulate(glm::value_ptr(View), glm::value_ptr(Projection), static_cast(operation), mode, glm::value_ptr(matrix), nullptr, snap); + + if (false == ImGuizmo::IsUsing()) + return; + m_gizmo_using = true; + + float newtranslation[3], newrotation[3], newscale[3]; + ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(matrix), newtranslation, newrotation, newscale); + if (translating) + { + glm::dvec3 const newlocation = Camerapos + glm::dvec3{newtranslation[0], newtranslation[1], newtranslation[2]} - offset; + for (auto axis = 0; axis < 3; ++axis) + { + if (position[axis] >= 0) + m_include.values[position[axis]] = editor_includes::number(newlocation[axis]); + } + } + else + { + if (rotations == 1) + { + // the decomposition spreads a turn of more than a quarter over all three angles; + // with a single axis to turn around the angle is taken straight from the matrix + newrotation[0] = glm::degrees(std::atan2(matrix[1][2], matrix[1][1])); + newrotation[1] = glm::degrees(std::atan2(-matrix[0][2], matrix[0][0])); + newrotation[2] = glm::degrees(std::atan2(matrix[0][1], matrix[0][0])); + } + for (auto axis = 0; axis < 3; ++axis) + { + if (rotation[axis] >= 0) + m_include.values[rotation[axis]] = editor_includes::number(clamp_circular(newrotation[axis])); + } + } + apply_include(); +} + +glm::mat4 editor_mode::projection_matrix(float const Aspect) +{ + if (Global.EditorOrtho) + { + auto const height{Global.EditorOrthoExtent}; + return glm::ortho(-height * Aspect, height * Aspect, -height, height, -10000.0f, 10000.0f); + } + return glm::perspective(glm::radians(Global.FieldOfView / Global.ZoomFactor), Aspect, 0.1f, 10000.0f); +} + +void editor_mode::toggle_ortho() +{ + Global.EditorOrtho = !Global.EditorOrtho; + if (Global.EditorOrtho) + { + m_ortho_pitch = Camera.Angle.x; + Camera.Angle.x = -M_PI_2; + Camera.Angle.z = 0.0; + } + else + { + Camera.Angle.x = m_ortho_pitch; + ortho_pan_stop(); + } +} + +void editor_mode::ortho_pan_to(double const Horizontal, double const Vertical) +{ + auto const &io{ImGui::GetIO()}; + if (io.DisplaySize.y <= 0.0f) + return; + auto const dx{Horizontal - m_ortho_pan_cursor.x}; + auto const dy{Vertical - m_ortho_pan_cursor.y}; + m_ortho_pan_cursor = {Horizontal, Vertical}; + auto const scale{2.0 * static_cast(Global.EditorOrthoExtent) / static_cast(io.DisplaySize.y)}; + auto const world{RotateY(glm::dvec3{dx * scale, 0.0, dy * scale}, static_cast(Camera.Angle.y))}; + Camera.Pos.x -= world.x; + Camera.Pos.z -= world.z; + Camera.Velocity = {}; + m_focus_active = false; + Global.pCamera = Camera; +} + +void editor_mode::ortho_pan_stop() +{ + m_ortho_pan = false; +} + +void editor_mode::on_scroll(double const Xoffset, double const Yoffset) +{ + if (false == Global.EditorOrtho || ImGui::GetIO().WantCaptureMouse) + return; + auto const before{Global.EditorOrthoExtent}; + auto const after{std::clamp(before * static_cast(std::pow(0.85, Yoffset)), 5.0f, 5000.0f)}; + screen_projection const projection; + glm::dvec3 origin, direction; + projection.ray(ImGui::GetIO().MousePos, origin, direction); + auto const scale{static_cast(after) / static_cast(before)}; + Camera.Pos.x = origin.x + (Camera.Pos.x - origin.x) * scale; + Camera.Pos.z = origin.z + (Camera.Pos.z - origin.z) * scale; + Global.EditorOrthoExtent = after; + Global.pCamera = Camera; +} + void editor_mode::update_camera(double const Deltatime) { Camera.Update(); + if (Global.EditorOrtho) + { + Camera.Angle.x = -M_PI_2; + Camera.Angle.z = 0.0; + } // focus animation runs after Camera.Update() so it overrides any residual velocity/rotation; // it smoothly drives both position and orientation toward the framed object @@ -1890,6 +5055,12 @@ void editor_mode::enter() { m_statebackup = {Global.pCamera, FreeFlyModeFlag, Global.ControlPicking}; + if (simulation::Region != nullptr) + { + // the editor works with geometry of the whole scenery, including the parts of binary terrain which weren't needed so far + simulation::Region->load_terrain(); + } + Camera = Global.pCamera; if (!FreeFlyModeFlag) @@ -1907,25 +5078,85 @@ void editor_mode::enter() } } + if (Global.editor_session && false == m_opened_at_track) + { + m_opened_at_track = true; + auto const saved{scene::Layers.empty() ? std::vector{} : scene::Layers.marked(scene::layer_handle{1}, "//$c")}; + if (false == saved.empty()) + { + std::istringstream values(saved.front()); + glm::dvec3 position{0.0}; + glm::vec3 angle{0.0f}; + if (values >> position.x >> position.y >> position.z >> angle.x >> angle.y) + { + Camera.Pos = position; + Camera.Angle = angle; + Camera.m_owner = nullptr; + FreeFlyModeFlag = true; + Global.pCamera = Camera; + m_saved_camera = {position, angle}; + WriteLog("Editor: camera where it was at the last save"); + } + } + for (auto *track : simulation::Paths.sequence()) + { + if (m_saved_camera.has_value() || track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + auto const &path{track->m_paths.front()}; + auto const start{path.points[segment_data::point::start]}; + auto const end{path.points[segment_data::point::end]}; + auto const centre{0.5 * (start + end)}; + glm::dvec3 along{end.x - start.x, 0.0, end.z - start.z}; + along = glm::length(along) > 1e-6 ? glm::normalize(along) : glm::dvec3{0.0, 0.0, 1.0}; + glm::dvec3 const side{-along.z, 0.0, along.x}; + Camera.Pos = centre - along * 30.0 + side * 15.0 + glm::dvec3{0.0, 20.0, 0.0}; + auto const look{glm::normalize(centre - Camera.Pos)}; + Camera.Angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + Camera.m_owner = nullptr; + FreeFlyModeFlag = true; + Global.pCamera = Camera; + WriteLog("Editor: camera at the first track, " + track->name()); + break; + } + } + Global.ControlPicking = true; EditorModeFlag = true; + load_orthophoto_settings(); + Application.set_cursor(GLFW_CURSOR_NORMAL); } void editor_mode::exit() { + Global.editor_tracks = false; EditorModeFlag = false; Global.ControlPicking = m_statebackup.picking; FreeFlyModeFlag = m_statebackup.freefly; Global.pCamera = m_statebackup.camera; + commit_track_drag(true); + m_track_gizmo_using = false; + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_snap = {}; + m_track_point = {}; + m_route = {}; + m_route_gizmo_using = false; + if (Global.EditorOrtho) + toggle_ortho(); + g_redo.clear(); m_history.clear(); m_fill_last.clear(); + m_orthophoto.cancel_pending(); + m_orthophoto.detach_scene(); // the layer is an editor aid, keep it out of the other modes + scene::Layers.show_all(); // likewise for hidden scenery layers, the other modes get the scenery as it was defined // drop selection so a stale/dangling node pointer isn't used on the next editor session m_node = nullptr; + select_include(0); m_gizmo_using = false; ui()->set_node(nullptr); @@ -1937,6 +5168,76 @@ void editor_mode::exit() } } +void editor_mode::delete_model(TAnimModel *model, bool const Joined) +{ + // record deletion for undo (serialize full node) + std::string as_text; + + model->export_as_text(as_text); + std::string debug = "Deleting node: " + as_text + "\nSerialized data:\n"; + push_snapshot(model, EditorSnapshot::Action::Delete, as_text); + if (false == m_history.empty()) + m_history.back().joined = Joined; + WriteLog(debug, logtype::generic); + + // clear history pointers referencing this model before actually deleting it + nullify_history_pointers(model); + remove_from_hierarchy(model); + + if (m_node == model) + { + m_node = nullptr; + m_dragging = false; + ui()->set_node(nullptr); + } + simulation::State.delete_model(model); +} + +void editor_mode::delete_selected() +{ + // what's selected: a road piece or point, a signal, an include of a scenery template, a model, tracks + if (road_delete()) + return; + if (m_instance != 0 && signal_delete(m_instance)) + return; + if (m_instance != 0) + { + // include of a scenery template. what it shows goes out of sight, the directive is erased on save + scene::Layers.removed(m_instance, true); + EditorSnapshot snap; + snap.instance = m_instance; + snap.node_name = *scene::Layers.instance(m_instance).file; + m_history.push_back(std::move(snap)); + g_redo.clear(); + ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" removed, Ctrl+Z brings it back. Its directive is erased when the scenery is saved."); + select_include(0); + return; + } + TAnimModel *model = dynamic_cast(m_node); + if (model) + { + // the models of the selection go together, one step of the history + auto joined{false}; + for (auto *node : selected_nodes()) + { + if (auto *selected = dynamic_cast(node)) + { + delete_model(selected, joined); + joined = true; + } + } + m_selection.clear(); + } + else if (ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::path && m_track_set.size() >= 2) + { + track_set_delete(); + } + else if (selected_track() != nullptr) + { + delete_selected_track(); + } +} + void editor_mode::on_key(int const Key, int const Scancode, int const Action, int const Mods) { #ifndef __unix__ @@ -1946,29 +5247,102 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in #endif bool anyModifier = Mods & (GLFW_MOD_SHIFT | GLFW_MOD_CONTROL | GLFW_MOD_ALT); + if (Action == GLFW_RELEASE && Key >= 0 && Key <= GLFW_KEY_LAST) + { + input::keys[Key] = GLFW_RELEASE; + if (Key == GLFW_KEY_LEFT_SHIFT || Key == GLFW_KEY_RIGHT_SHIFT) + input::key_shift = false; + if (Key == GLFW_KEY_LEFT_CONTROL || Key == GLFW_KEY_RIGHT_CONTROL) + input::key_ctrl = false; + if (Key == GLFW_KEY_LEFT_ALT || Key == GLFW_KEY_RIGHT_ALT) + input::key_alt = false; + } + // first give UI a chance to handle the key if (!anyModifier && m_userinterface->on_key(Key, Action)) return; + if (ui()->mode() == nodebank_panel::TRACK && is_press(Action) && (Mods & (GLFW_MOD_CONTROL | GLFW_MOD_ALT)) == 0 && typed_key(Key)) + return; + // gizmo transform shortcuts (Q/W/E/R) — only when the camera isn't being flown (RMB up). // handled before the camera keyboard step because Q/W/E are also the fly-mode movement keys, // which would otherwise consume them. if (!anyModifier && is_press(Action) && m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) != GLFW_PRESS) { + if (ui()->mode() == nodebank_panel::TRACK && false == m_track_gizmo_using && false == m_straights.dragging && false == m_switch.placing && track_shortcut(Key)) + return; bool handled = true; switch (Key) { + case GLFW_KEY_O: toggle_ortho(); break; case GLFW_KEY_Q: m_gizmo_op = gizmo_operation::translate; break; case GLFW_KEY_W: m_gizmo_op = gizmo_operation::rotate; break; case GLFW_KEY_E: m_gizmo_op = gizmo_operation::scale; break; case GLFW_KEY_R: m_gizmo_local = !m_gizmo_local; break; + case GLFW_KEY_B: handled = ui()->mode() != nodebank_panel::TRACK && bend_shortcut(); break; + case GLFW_KEY_F1: m_shortcuts_open = !m_shortcuts_open; break; + // the fields of work in the order of their row + case GLFW_KEY_F2: choose_work_area(work_area::surroundings); break; + case GLFW_KEY_F3: choose_work_area(work_area::tracks); break; + case GLFW_KEY_F4: choose_work_area(work_area::roads); break; + case GLFW_KEY_F5: choose_work_area(work_area::terrain); break; + // the edit modes in the order of the toolbar; in the track mode the digits typed as a value were taken already + case GLFW_KEY_1: choose_edit_mode(nodebank_panel::MODIFY); break; + case GLFW_KEY_2: choose_edit_mode(nodebank_panel::ADD); break; + case GLFW_KEY_3: choose_edit_mode(nodebank_panel::BRUSH); break; + case GLFW_KEY_4: choose_edit_mode(nodebank_panel::FILL); break; + case GLFW_KEY_5: choose_edit_mode(nodebank_panel::COPY); break; default: handled = false; break; } if (handled) return; } + // all docks unpinned and hidden at the edges, for the most of the view, or pinned back + if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_SPACE) + { + if (is_press(Action)) + ui()->toggle_docks(); + return; + } + + // the selection: copy, paste at the cursor, group and ungroup. ahead of the camera keys, the letters may be bound there + if (Global.ctrlState && (Key == GLFW_KEY_C || Key == GLFW_KEY_V || Key == GLFW_KEY_G) && ui()->mode() != nodebank_panel::TRACK && false == m_roadtool.window) + { + if (is_press(Action)) + { + if (Key == GLFW_KEY_C) + copy_selection(); + else if (Key == GLFW_KEY_V) + paste_clipboard(cursor_ground()); + else if (Global.shiftState) + ungroup_selection(); + else + group_selection(); + } + return; + } + + // save; checked ahead of the camera keys, which have S bound to moving back + if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_S) + { + if (is_press(Action)) + save(); + return; + } + + if (Global.ctrlState && false == Global.shiftState && Key == GLFW_KEY_F && ui()->mode() == nodebank_panel::TRACK) + { + if (is_press(Action)) + { + show_track_tab(m_track_tab); + m_track_search.focus = true; + } + return; + } + // then internal input handling if (m_input.keyboard.key(Key, Action)) return; @@ -1976,7 +5350,9 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in if (Action == GLFW_RELEASE) return; - // shortcuts: undo/redo + // shortcuts: undo/redo. not in the middle of a drag, it would go on from the state which was undone + if (Global.ctrlState && (Key == GLFW_KEY_Z || Key == GLFW_KEY_Y) && is_press(Action) && (m_track_gizmo_using || m_track_set_using || m_straights.dragging || m_extend.active || m_switch.placing || m_point_drag.active || m_handle_drag.active)) + return; if (Global.ctrlState && Key == GLFW_KEY_Z && is_press(Action)) { undo_last(); @@ -2001,7 +5377,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in } else if (Global.ctrlState && Global.shiftState) { - simulation::State.export_as_text(Global.SceneryFile); + export_scenery(); } break; @@ -2013,27 +5389,34 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in break; case GLFW_KEY_DELETE: + if (is_press(Action)) + delete_selected(); + break; + + case GLFW_KEY_ESCAPE: if (is_press(Action)) { - TAnimModel *model = dynamic_cast(m_node); - if (model) + road_cancel(); + } + // the first Esc drops what is going on in the track mode, the next one goes back to the selection + if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK) + { + if (m_lay.active && false == m_lay.points.empty()) + lay_cancel(); + else if (track_busy()) + { + cancel_track_tools(); + m_straights.tool = 0; + } + else if (m_track_tab == track_tab::path && false == m_track_set.empty()) + m_track_set.clear(); + else if (m_track_tab != track_tab::path) + show_track_tab(track_tab::path); + else if (selected_track() != nullptr) { - // record deletion for undo (serialize full node) - std::string as_text; - - model->export_as_text(as_text); - std::string debug = "Deleting node: " + as_text + "\nSerialized data:\n"; - push_snapshot(model, EditorSnapshot::Action::Delete, as_text); - WriteLog(debug, logtype::generic); - - // clear history pointers referencing this model before actually deleting it - nullify_history_pointers(model); - remove_from_hierarchy(model); - m_node = nullptr; - m_dragging = false; + m_track_point = {}; ui()->set_node(nullptr); - simulation::State.delete_model(model); } } break; @@ -2042,14 +5425,32 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in // area fill: remove the last outline point if (is_press(Action) && ui()->mode() == nodebank_panel::FILL && !m_fill_points.empty()) m_fill_points.pop_back(); + if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active && !m_lay.points.empty()) + { + m_lay.points.pop_back(); + if (m_lay.points.empty()) + m_lay.start = {}; + } + break; + + case GLFW_KEY_ENTER: + case GLFW_KEY_KP_ENTER: + if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && m_lay.active) + lay_enter(); + break; + + case GLFW_KEY_K: + if (is_press(Action) && road_split()) + { + break; + } + if (is_press(Action) && ui()->mode() == nodebank_panel::TRACK && selected_track() != nullptr) + split_track_at(*selected_track(), Global.pCamera.Pos + GfxRenderer->Mouse_Position()); break; case GLFW_KEY_F: - if (is_press(Action)) + if (is_press(Action) && m_node) { - if(!m_node) - break; - // start smooth focus camera on selected node start_focus(m_node, 0.6); } @@ -2061,7 +5462,7 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in // Unreal-style "snap to floor": drop the selected node onto the surface below it. // works against triangle geometry (shape_node terrain / opaque shapes); once a proper // editable terrain mesh exists, dropping onto it works without further changes here. - snap_to_ground(m_node); + drop_selection_to_ground(); } break; @@ -2072,6 +5473,8 @@ void editor_mode::on_key(int const Key, int const Scancode, int const Action, in void editor_mode::on_cursor_pos(double const Horizontal, double const Vertical) { + if (m_ortho_pan) + ortho_pan_to(Horizontal, Vertical); // object transforms are handled by the gizmo now; here we only forward the cursor to the // mouse input, which rotates the camera while the right mouse button is held (panning mode) m_input.mouse.position(Horizontal, Vertical); @@ -2086,23 +5489,61 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; } - // in chunk-edit mode the left button adds a neighbouring chunk (Shift = delete the clicked one) - if (m_chunk_edit && Button == GLFW_MOUSE_BUTTON_LEFT) + // the chunk, spacing and water tools of the terrain work on a click (Shift: the other way round) + if (terrain_clicking() && Button == GLFW_MOUSE_BUTTON_LEFT) { if (is_press(Action)) - handle_chunk_edit_click(Global.shiftState); + handle_terrain_click(Global.shiftState); m_input.mouse.button(Button, Action); return; } - // in terrain sculpt mode the left button paints the terrain instead of picking nodes - if (m_terrain_sculpt && Button == GLFW_MOUSE_BUTTON_LEFT) + // with a brush of the terrain the left button works the brush instead of picking nodes + if (terrain_brush() && Button == GLFW_MOUSE_BUTTON_LEFT) { + if (m_terrain_tool == terrain_tool::level && is_press(Action) && (Global.ctrlState || m_terrain_picking)) + { + // a click which picks the target height doesn't level anything + pick_terrain_target(); + m_terrain_picking = false; + mouseHold = false; + m_input.mouse.button(Button, Action); + return; + } mouseHold = is_press(Action); m_input.mouse.button(Button, Action); return; } + // with the road window open the left button works its tools, whichever edit mode is on but the track one + if (m_roadtool.window && Button == GLFW_MOUSE_BUTTON_LEFT && ui()->mode() != nodebank_panel::TRACK) + { + if (is_press(Action)) + { + // NOTE: the pick is resolved a few frames later, by then it's known whether the press was meant for the UI + GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) { + if (viewport_click()) + road_click(); + }); + } + m_input.mouse.button(Button, Action); + return; + } + + if (Button == GLFW_MOUSE_BUTTON_LEFT && is_press(Action) && false == m_bend.picking && ui()->mode() != nodebank_panel::TRACK && (bend_drag_start(Mods) || bend_click())) + { + m_input.mouse.button(Button, Action); + return; + } + + if (m_bend.picking && Button == GLFW_MOUSE_BUTTON_LEFT && ui()->mode() != nodebank_panel::TRACK) + { + if (is_press(Action)) + bend_pick(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); + m_input.mouse.button(Button, Action); + return; + } + if (Button == GLFW_MOUSE_BUTTON_LEFT) { @@ -2126,27 +5567,62 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const return; } + if (mode == nodebank_panel::TRACK) + { + track_press(track_intent_at(Mods)); + m_input.mouse.button(Button, Action); + return; + } + // delegate node picking behaviour depending on current panel mode. // NOTE: the pick is resolved a few frames later, often after the button was already released, // so the callback mustn't depend on the button still being held + // Shift+LMB adds a model to the selection, or takes it out + auto const additive{mode == nodebank_panel::MODIFY && (Mods & GLFW_MOD_SHIFT) != 0}; GfxRenderer->Pick_Node_Callback( - [this, mode, rotation_mode, fixed_rotation_value](scene::basic_node *node) { + [this, mode, rotation_mode, fixed_rotation_value, additive](scene::basic_node *node) { // the press turned out to be meant for the UI if (!viewport_click()) return; + auto *const previous{m_node}; m_node = nullptr; + select_include(0); - // ignore picks that are beyond allowed placement distance - if (node) { + // in a scenery opened for editing selection has its limits. nodes of locked layers are off limits. + // nodes defined by a scenery template can't be rewritten one by one on save, so a click on one + // selects the include of the template as a whole + if (node && mode == nodebank_panel::MODIFY) + { + std::string reason; + if (node->from_template() && scene::Layers.removable(node->m_instance, &reason)) + { + select_include(node->m_instance); + ui()->set_status("Include of \"" + *scene::Layers.instance(m_instance).file + "\" selected. Its parameters are in the node properties, Del removes it from the scenery."); + node = nullptr; + } + else if (node->from_template() || false == scene::Layers.editable(node, &reason)) + { + ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be edited: " + reason, true); + node = nullptr; + } + } + + // models stay within placement distance; tracks in the track workspace have no range cap + if (node && false == (Global.editor_tracks && dynamic_cast(node) != nullptr)) { double const dist = glm::distance(node->location(), glm::dvec3{Global.pCamera.Pos}); if (dist > static_cast(kMaxPlacementDistance)) + { + if (additive) + m_node = previous; return; + } } if (mode == nodebank_panel::MODIFY) { - m_node = node; - ui()->set_node(m_node); + if (additive) + m_node = previous; + select_node(node, additive); } else if (mode == nodebank_panel::COPY) { @@ -2165,9 +5641,16 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const if (!src) return; + if (src->starts_with(editor_includes::directive_mark)) + { + // a scenery template rather than a single node + place_include(src->substr(editor_includes::directive_mark.size()), rotation_mode, fixed_rotation_value); + return; + } + std::string name = "editor_"; glm::dvec3 mouseOffset = clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position()); - TAnimModel *cloned = simulation::State.create_model(*src, name, Camera.Pos + mouseOffset); + TAnimModel *cloned = simulation::State.create_model(*src, name, placement_on_ground(Camera.Pos + mouseOffset)); if (!cloned) return; @@ -2191,16 +5674,68 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const else { if (is_release(Action)) + { mouseHold = false; + finish_track_box(); + point_drag_finish(); + m_handle_drag = {}; + sweep_release(); + if (m_straights.tool_mouse) + finish_straight_gesture(); + if (m_extend.active) + finish_extend(); + if (m_switch.placing) + finish_switch_placement(); + if (m_turntable.placing) + turntable_finish_placement(); + if (m_signal.placing || m_signal.moving) + signal_release(); + } m_dragging = false; } } else if (Button == GLFW_MOUSE_BUTTON_RIGHT) { + // Ctrl+RMB: the menu at the cursor, for the model under it or the selection; RMB alone turns the camera + if ((Mods & GLFW_MOD_CONTROL) != 0 && ui()->mode() != nodebank_panel::TRACK) + { + if (is_press(Action)) + { + m_contextpoint = cursor_ground(); + GfxRenderer->Pick_Node_Callback([this](scene::basic_node *node) { + if (!viewport_click()) + return; + auto const selection{selected_nodes()}; + if (node != nullptr && false == node->from_template() && scene::Layers.editable(node) && std::find(selection.begin(), selection.end(), node) == selection.end()) + select_node(node, false); + m_contextopen = true; + }); + } + return; + } + if (ui()->mode() == nodebank_panel::TRACK) + { + if (is_press(Action)) + track_context_press(); + else if (is_release(Action)) + track_context_release(); + } // game-engine style look: hide & grab the cursor while flying, restore it on release Application.set_cursor(is_press(Action) ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL); } + else if (Button == GLFW_MOUSE_BUTTON_MIDDLE && Global.EditorOrtho) + { + if (is_press(Action)) + { + m_ortho_pan = true; + m_ortho_pan_cursor = m_input.mouse.position(); + m_focus_active = false; + Camera.Velocity = {}; + } + else if (is_release(Action)) + ortho_pan_stop(); + } m_input.mouse.button(Button, Action); } @@ -2208,9 +5743,9 @@ void editor_mode::on_mouse_button(int const Button, int const Action, int const void editor_mode::render_change_history(){ - ImGui::Begin("Editor History", &m_change_history, ImGuiWindowFlags_AlwaysAutoResize); + ImGui::Begin((std::string(STR_C("Editor History")) + "###editorhistory").c_str(), &m_change_history); int maxsize = m_max_history_size; - if (ImGui::InputInt("Max history size", &maxsize)) + if (ImGui::InputInt(STR_C("Max history size"), &maxsize)) { m_max_history_size = std::max(0, maxsize); if ((int)m_history.size() > m_max_history_size && m_max_history_size >= 0) @@ -2225,14 +5760,14 @@ void editor_mode::render_change_history(){ } float dist = kMaxPlacementDistance; - if (ImGui::InputFloat("Max placement distance", &dist)) + if (ImGui::InputFloat(STR_C("Max placement distance"), &dist)) { kMaxPlacementDistance = std::max(0.0f, dist); } ImGui::Separator(); - ImGui::Text("History (newest at end): %zu entries", m_history.size()); + ImGui::Text(STR_C("History (newest at end): %zu entries"), m_history.size()); ImGui::BeginChild("history_list", ImVec2(400, 200), true); for (int i = 0; i < (int)m_history.size(); ++i) { @@ -2243,7 +5778,11 @@ void editor_mode::render_change_history(){ s.action == EditorSnapshot::Action::Delete ? "DEL" : s.action == EditorSnapshot::Action::Move ? "MOV" : s.action == EditorSnapshot::Action::Rotate ? "ROT" : - s.action == EditorSnapshot::Action::Scale ? "SCA" : "OTH", + s.action == EditorSnapshot::Action::Scale ? "SCA" : + s.action == EditorSnapshot::Action::TrackEdit ? "TRK" : + s.action == EditorSnapshot::Action::RoadEdit ? "ROAD" : + s.action == EditorSnapshot::Action::Array ? "ARR" : + s.action == EditorSnapshot::Action::Terrain ? "TER" : "OTH", s.node_name.empty() ? "(noname)" : s.node_name.c_str(), s.position.x, s.position.y, s.position.z); @@ -2253,7 +5792,7 @@ void editor_mode::render_change_history(){ ImGui::EndChild(); ImGui::Separator(); - if (ImGui::Button("Clear History")) + if (ImGui::Button(STR_C("Clear History"))) { m_history.clear(); g_redo.clear(); @@ -2262,7 +5801,7 @@ void editor_mode::render_change_history(){ ImGui::SameLine(); ImGui::SameLine(); - if (ImGui::Button("Undo Selected")) + if (ImGui::Button(STR_C("Undo Selected"))) { if (m_selected_history_idx >= 0 && m_selected_history_idx < (int)m_history.size()) { @@ -2310,3 +5849,34 @@ void editor_mode::set_focus_active(bool isActive) { m_focus_active = isActive; } + +void editor_mode::starter_trainset(std::vector &Trailing) +{ + static std::regex const trainset{R"((^|\n)[ \t]*trainset[ \t])", std::regex::icase}; + for (std::size_t handle = 1; handle <= scene::Layers.size(); ++handle) + { + auto const &layer{scene::Layers.layer(static_cast(handle))}; + if (layer.created) + continue; + std::ifstream input{Global.asCurrentSceneryPath + layer.name, std::ios_base::binary}; + std::string const content{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + if (std::regex_search(content, trainset)) + return; + } + std::error_code error; + if (false == std::filesystem::exists("dynamic/pkp/wmb10_v1/wmb10.mmd", error)) + return; + for (auto const *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Normal || (track->iCategoryFlag & 1) == 0 || track->name().empty() || track->m_paths.empty()) + continue; + auto const length{geometry::bezier{track->m_paths.front()}.plan_length()}; + if (length < 15.0) + continue; + Trailing.push_back("trainset none " + track->name() + " " + to_string(std::min(length, 0.5 * length + 5.0), 2) + " 0"); + Trailing.push_back("node -1 0 wmb10-6457 dynamic pkp/wmb10_v1 wmb10-6457 wmb10 0 headdriver 0 0 enddynamic"); + Trailing.push_back("endtrainset"); + WriteLog("Editor: the scenery has no trainset, a WMB10 is put on " + track->name() + " to drive"); + return; + } +} diff --git a/application/editormode.h b/application/editormode.h index 413b4225..c7dbd391 100644 --- a/application/editormode.h +++ b/application/editormode.h @@ -1,4 +1,4 @@ -/* +/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #pragma once #include "application/applicationmode.h" +#include "application/editoruipanels.h" #include "input/editormouseinput.h" #include "input/editorkeyboardinput.h" #include "vehicle/Camera.h" @@ -17,19 +18,66 @@ http://mozilla.org/MPL/2.0/. #include "scene/scenenode.h" #include "editor/editorTerrain.hpp" #include "editor/editorTerrainStreamer.hpp" +#include "editor/editorOrthophoto.hpp" +#include "editor/editorTrack.hpp" +#include "editor/editorAlignment.hpp" +#include "editor/editorSpeed.hpp" +#include "editor/editorRoad.hpp" +#include "editor/editorProfile.hpp" +#include "editor/editorInfra.hpp" +#include "editor/editorGauge.hpp" +#include "world/Sweep.h" +#include +#include +#include +#include #include +#include #include #include class TAnimModel; +// a tool of a field of work of the editor, a button in the toolbar and over the 3d view +struct editor_tool_button; +class TTrack; +namespace ui +{ +class vehicles_bank; +struct vehicle_desc; +struct skin_set; +} -class editor_mode : public application_mode +class editor_mode : public application_mode, private editor_track::observer { public: + // model entering the structure gauge, at its deepest place + struct gauge_hit + { + std::string model; + glm::dvec3 point{0.0}; + double depth{0.0}; // m + TTrack const *track{nullptr}; // nearest one, may be gone by now + int path{0}; + glm::dvec2 direction{0.0, 1.0}; // of the track there, in the plan + }; + // joint of the paths found wrong by the joints check + struct joint_issue + { + enum class kind { gap, step, kink, cant, grade, open }; + kind type{kind::gap}; + TTrack *track{nullptr}; + editor_track::point_ref point; + TTrack *other{nullptr}; + editor_track::point_ref theirs; + glm::dvec3 position{0.0}; + double value{0.0}; // m, deg, mm, per mille + bool fixed{false}; + }; // constructors editor_mode(); + ~editor_mode() override; // methods // initializes internal data structures of the mode. returns: true on success, false otherwise bool init() override; @@ -43,20 +91,20 @@ class editor_mode : public application_mode void on_key(int Key, int Scancode, int Action, int Mods) override; void on_cursor_pos(double Horizontal, double Vertical) override; void on_mouse_button(int Button, int Action, int Mods) override; - void on_scroll(double const Xoffset, double const Yoffset) override - { - ; - } + void on_scroll(double const Xoffset, double const Yoffset) override; void on_window_resize(int w, int h) override { ; } void on_event_poll() override; bool is_command_processor() const override; + // takes the last change back; changes made together (e.g. to the models of a selection) are taken back together void undo_last(); static bool focus_active(); static void set_focus_active(bool isActive); static TCamera& get_camera() { return Camera; } + static glm::mat4 projection_matrix(float const Aspect); + void toggle_ortho(); static bool change_history() { return m_change_history; } static void set_change_history(bool enabled) { m_change_history = enabled; } static bool settings_open() { return m_settings_open; } @@ -81,9 +129,22 @@ class editor_mode : public application_mode bool picking; }; + struct stored_profile; struct EditorSnapshot { - enum class Action { Move, Rotate, Scale, Add, Delete, Other }; + enum class Action { Move, Rotate, Scale, Add, Delete, TrackEdit, Other, RoadEdit, Array, Bend, Terrain }; + // copy of a model an array made + struct array_copy + { + TAnimModel *model{nullptr}; // nullptr while the copy is out of the scene + UID uuid; + // what it takes to make the copy again, filled in when it's taken out of the scene + std::string serialized; + std::string name; + glm::dvec3 position{0.0}; + glm::vec3 rotation{0.0f}; + glm::vec3 scale{1.0f}; + }; Action action{Action::Other}; std::string node_name; // node identifier (basic_node::name()) @@ -94,7 +155,22 @@ class editor_mode : public application_mode glm::vec3 rotation{0.0f, 0.0f, 0.0f}; glm::vec3 scale{1.0f, 1.0f, 1.0f}; UID uuid; // node UUID for reference, used as fallback lookup for deleted/recreated nodes - + scene::layer_handle layer{null_handle}; // scenery layer of the node, so a recreated node returns to it + scene::instance_handle instance{0}; // include of a scenery template which was placed or removed, instead of a node + std::vector instances; // includes placed or removed together + std::vector> directives; // includes changed together, with the directives to go back to + std::vector> sweeps; // models along curves changed, with the definitions to go back to + std::vector sweeps_toggled; // models along curves made or removed: undo flips whether they're there + std::vector> tracks; + std::vector created; + std::vector removed; + editor_road::record roads; // what a change of the roads comes to + std::vector infra; // objects which followed the paths + bool profiles{false}; // the stored grade line designs changed too, these were the ones before + std::vector profile_library; + std::vector copies; // models an array added to the one it was made of + bool joined{false}; // undone and redone together with the change before it + std::shared_ptr terrain; // the heightmap terrain from before the change (Action::Terrain) }; void push_snapshot(scene::basic_node *node, EditorSnapshot::Action Action = EditorSnapshot::Action::Move, std::string const &Serialized = std::string()); @@ -105,6 +181,15 @@ class editor_mode : public application_mode editor_ui *ui() const; void redo_last(); + // a single step of the history, see undo_last() + void undo_one(); + void redo_one(); + // the changes of the heightmap terrain since the last step become a step of the history: a stroke of the brush, + // or what a frame did; the next change is recorded from now on + void terrain_history_step(); + // puts the terrain back as the step recorded it, the step left with the state it replaced + void restore_terrain(EditorSnapshot &Snapshot); + bool m_history_busy{false}; // undo or redo under way void handle_brush_mouse_hold(int Action, int Button); void apply_rotation_for_new_node(scene::basic_node *node, int rotation_mode, float fixed_rotation_value); // members @@ -124,6 +209,10 @@ class editor_mode : public application_mode double fTime50Hz{0.0}; // bufor czasu dla komunikacji z PoKeys scene::basic_editor m_editor; scene::basic_node *m_node{nullptr}; // currently selected scene node + scene::instance_handle m_instance{0}; // currently selected include of a scenery template; these are handled as a whole + include_selection m_include; // parameters of the selected include, changed with the gizmo and in the node properties + bool m_include_gesture{false}; // a change of the selected include is under way, and has its undo snapshot already + std::vector> m_retired; // models replaced after changes of includes, with the frames left until they're destroyed bool m_takesnapshot{true}; // helper, hints whether snapshot of selected node(s) should be taken before modification bool m_dragging = false; glm::dvec3 oldPos; // world position of the last brush placement @@ -133,6 +222,11 @@ class editor_mode : public application_mode static bool m_change_history; static bool m_settings_open; + double m_ortho_pitch{0.0}; + bool m_ortho_pan{false}; + glm::dvec2 m_ortho_pan_cursor{0.0}; + void ortho_pan_to(double const Horizontal, double const Vertical); + void ortho_pan_stop(); // camera fly-mode (right mouse button held); used to flush motion when it's released command_relay m_camera_relay; bool m_camera_flying{false}; @@ -141,54 +235,282 @@ class editor_mode : public application_mode int m_max_history_size{200}; int m_selected_history_idx{-1}; glm::dvec3 clamp_mouse_offset_to_max(const glm::dvec3 &offset); + // the ground under a point the cursor landed on, for things which are to stand on the ground + glm::dvec3 placement_on_ground(glm::dvec3 Location); + // puts every instance of the model the selected node shows on the ground under it + void drop_model_instances(); + void render_object_menu(); + // undo, redo and what can be done with the selection, with their keys + void render_edit_menu(); + // the list of the keyboard shortcuts, opened from the help menu or by F1 + void render_help_menu(); + void render_shortcuts_window(); + bool m_shortcuts_open{false}; + // deletes what's selected: a road piece or point, a signal, an include of a scenery template, a model, tracks (Del) + void delete_selected(); + void delete_model(TAnimModel *Model, bool const Joined); + + // more models selected at once, in the surroundings: m_node is the one the gizmo is on, the others are in m_selection. + // Shift+LMB adds a model or takes it out; a model of a group stands for the whole group + std::vector m_selection; + std::vector selected_nodes() const; // m_node first + std::vector selection_with_groups() const; // and the other models of their groups + void select_node(scene::basic_node *Node, bool const Additive); + void draw_selection_overlay(); + // the other models of the selection follow m_node moved or turned by the gizmo + void selection_snapshots(EditorSnapshot::Action const Action); + void selection_follow(glm::dvec3 const &Moved, glm::vec3 const &Turned, glm::dvec3 const &Pivot); + // clipboard (Ctrl+C, Ctrl+V): definitions of the models, placed around the cursor as they were around the selected one + struct clipboard_entry + { + std::string definition; + glm::dvec3 offset{0.0}; + glm::vec3 angles{0.0f}; + glm::vec3 scale{1.0f}; + int group{-1}; // models copied with their group are grouped again, each group anew + }; + std::vector m_clipboard; + void copy_selection(); + void paste_clipboard(glm::dvec3 const &Location); + glm::dvec3 cursor_ground(); // the point of the ground under the cursor + // groups (Ctrl+G, Ctrl+Shift+G), written to the scenery as group ... endgroup + void group_selection(); + void ungroup_selection(); + void drop_selection_to_ground(); + // menu at the cursor, Ctrl+RMB + bool m_contextopen{false}; + glm::dvec3 m_contextpoint{0.0}; + void render_context_menu(); // focus camera smoothly on specified node void start_focus(scene::basic_node *node, double duration = 0.6); - // drops the node straight down onto the nearest surface below (terrain or another object) - void snap_to_ground(scene::basic_node *node); + // drops the node straight down onto the nearest surface below (terrain or another object). its group goes along with it, + // unless Alone: then the node goes by itself. Joined: undone together with the change before + void snap_to_ground(scene::basic_node *node, bool const Alone = false, bool const Joined = false); - // editable terrain patches created in the editor + // the heightmap terrain of the scenery (terrain//): chunks, sculpting, painting, water void render_terrain_ui(); - // creates a large terrain as a grid of adjacent chunks (each its own editable patch) - void create_chunked_terrain(); - // manual grid-aligned chunks: add/remove single chunks for fine control - float chunk_grid_size() const { return m_terrain_cells * m_terrain_cellsize; } - void add_grid_chunk(int Cx, int Cz); - void remove_grid_chunk(int Cx, int Cz); - // handles a click in chunk-edit mode (add a neighbour, or Shift = delete the clicked chunk) - void handle_chunk_edit_click(bool DeleteMode); - // commits authored terrain to disk, enables streaming, and exports the scenery (Ctrl+S) - void save_scene_with_terrain(); - // raises/lowers terrain under the cursor while the left mouse button is held in sculpt mode - void handle_terrain_sculpt(double Deltatime); - // returns the terrain patch (if any) whose footprint covers the given world point + // the terrain as a field of work of its own: the terrain tools and the orthophoto in the tool options window + void render_terrain_tool_options(); + // opens or leaves the terrain; leaving it puts the terrain tools away, so the mouse doesn't stay with them + void terrain_workspace(bool const Open); + // opens the heightmap terrain of the scenery for editing, named after the scenery, if the scenery has none yet. returns: true if it's open + bool ensure_terrain(); + // adds chunks around the camera, Count along a side, at the height of the ground under each grid point where there's any + void add_chunks_around(int const Count); + // adds a chunk of the terrain shaped as the ground under it; where there's no ground, carried on level with the nearest point + // of it, or at the base height. returns: false if there's a chunk already + bool add_chunk_from_ground(heightmap::chunk_key const &Key, float const Spacing, std::uint16_t const Layer = 0); + // the tool of the terrain the left mouse button works + enum class terrain_tool + { + none, // picks the models, as in the surroundings + chunks, // adds a chunk, Shift: removes it + sculpt, // raises the ground, Shift: lowers it + smooth, + level, // leads the ground to the target height, Ctrl: picks the target height under the cursor + paint, // paints the chosen material, Shift: the first material of the palette + spacing, // selects chunks (Ctrl: adds or takes away one), whose point spacing is then converted + water, // outlines a body of water + restore, // takes the touch-ups over the modifiers back + fill // outlines an area filled with a material + }; + terrain_tool m_terrain_tool{terrain_tool::none}; + bool terrain_brush() const + { + return m_terrain_tool == terrain_tool::sculpt || m_terrain_tool == terrain_tool::smooth || m_terrain_tool == terrain_tool::level || m_terrain_tool == terrain_tool::paint || + m_terrain_tool == terrain_tool::restore; + } + // converts the point spacing of the selected chunks to m_terrain_convert_spacing + void convert_selected_chunks(); + // takes the height of whatever is under the cursor (terrain, track, model) for the target of the level brush. returns: false if there's nothing + bool pick_terrain_target(); + bool terrain_clicking() const { return m_terrain_tool == terrain_tool::chunks || m_terrain_tool == terrain_tool::spacing || m_terrain_tool == terrain_tool::water || m_terrain_tool == terrain_tool::fill; } + // fills the outlined area with the material of the chosen palette entry, turned by m_paint_rotation + void fill_terrain_area(); + // the brush, the hovered chunk and the outlines of water drawn over the viewport + void draw_terrain_overlay(); + // forgets what's known about the ground, after the terrain changed + void forget_ground(); + // conversion of the ground of the terrain files of the scenery (terrain directive, .txtf/.btf) to heightmap terrain + struct ground_conversion + { + struct source + { + std::string file; // as the terrain directive names it + std::uint16_t reference{0}; // see scene::terrain_file::reference_of() + bool text{false}; // read from the text file, rather than the binary one + bool good{false}; + bool chosen{true}; + std::string message; + std::size_t first{0}, count{0}; // its triangles + }; + bool open{false}; + std::vector files; + std::vector> triangles; + std::vector materials; // of the triangles: index of the name + std::vector names; // of the materials + std::set chosen; // materials to convert + std::string status; + } m_ground_conversion; + // reads the triangles of the terrain files of the scenery for the conversion + void load_ground_files(); + // the folder of the textures as a browser, in the window of the node bank while the terrain is edited: the textures and the + // materials can be dragged onto the fields which take a material + struct texture_browser + { + struct item + { + std::string name; // as the materials are named: relative to the folder of the textures, without the extension + std::string label; // the file, without the folder + bool material{false}; // there's a .mat file of the name + }; + std::string folder; // relative to the folder of the textures, empty or with a trailing slash + std::vector folders; // under the current one + std::vector items; + bool listed{false}; + char filter[64]{}; + float size{96.f}; // of the images, pixels + std::string selected; + std::map thumbnails; // texture shown for the item of the name, 0: none + int loads{0}; // textures read in the frame, a few at most so the browser doesn't stall the editor + } m_textures; + void render_texture_browser(); + // texture of the image of the item of the browser, 0 while there's none + std::uint64_t texture_thumbnail(texture_browser::item const &Item); + // takes a material dragged from the texture browser onto the item drawn last. returns: true if one was dropped, its name in Name + static bool material_drop(std::string &Name); + // the same, for a text field + static bool material_drop(char *Buffer, std::size_t Size); + // replaces the chosen triangles of the chosen terrain files with chunks of heightmap terrain of their shape, painted with + // their materials; the files lose them on save. returns: summary for the user + std::string convert_ground(); + void render_ground_conversion(); + // handles a click of the chunk, spacing and water tools (Shift: the other way round) + void handle_terrain_click(bool const Shift); + // writes the changes to the files of scenery opened for editing (Ctrl+S) + bool save(); + // places include of specified scenery template at the cursor, in the active layer + void place_include(std::string const &File, int RotationMode, float FixedRotation); + // undo and redo of placing or removing an include: brings removed include back, or removes it + void toggle_include(scene::instance_handle Instance); + // makes specified include the selected one, 0 clears the selection + void select_include(scene::instance_handle Instance); + // writes parameter values of the selected include to its directive, and shows the outcome in the scene + void apply_include(); + // replaces directive of specified include, and the models shown for it + void set_include_directive(scene::instance_handle Instance, std::string const &Directive); + // undo and redo of an include snapshot + void restore_include(EditorSnapshot &Snapshot); + // undo and redo of the includes placed, removed or changed together + void restore_includes(EditorSnapshot &Snapshot); + // gizmo for the selected include, limited to what the parameters of its template can express + void render_include_gizmo(glm::mat4 const &View, glm::mat4 const &Projection, glm::dvec3 const &Camerapos); + // exports the scenery in legacy (text) format, with layers hidden in the editor left out of the picture + void export_scenery(); + // works the brush of the terrain under the cursor while the left mouse button is held + void handle_terrain_brush(double Deltatime); + // the chunk of the terrain (if any) covering the given world point editor_terrain *terrain_at(double X, double Z); - // gathers every active terrain patch: manually-created ones plus streamed chunks + // the chunks of the terrain held in memory std::vector active_terrains(); - // samples the selected model instance's geometry into a new editable terrain patch, then removes it + // samples the selected model instance's geometry into the chunks of the terrain it covers, then removes it void capture_terrain(); - std::vector> m_terrains; - // grid-aligned manual chunks, keyed by (cx,cz) on the global chunk grid - std::map, std::unique_ptr> m_grid_chunks; - bool m_terrain_sculpt{false}; // when true, LMB sculpts terrain instead of picking - bool m_chunk_edit{false}; // when true, LMB adds/removes whole chunks - int m_terrain_cells{32}; // grid resolution (quads per side) - int m_terrain_chunks{4}; // chunks per side for a chunked terrain - float m_terrain_cellsize{2.0f}; // metres per quad - float m_terrain_baseheight{0.0f}; // flat starting height + bool m_terrain_open{false}; // the terrain is the field of work, chosen in the toolbar + float m_terrain_spacing{2.0f}; // point spacing of the chunks made and of the spacing tool + float m_terrain_baseheight{0.0f}; // height of chunks made where there's no ground + int m_terrain_add_count{4}; // chunks along a side added around the camera float m_terrain_brush_radius{12.0f}; - float m_terrain_brush_strength{4.0f}; // metres per second while held (one-shot for the buttons) - float m_terrain_simplify_error{0.5f}; // flatness tolerance (m) for mesh simplification - bool m_terrain_auto_optimize{false}; // auto-simplify edited chunks after sculpting settles - double m_terrain_idle{0.0}; // seconds since the last sculpt edit (debounce timer) - char m_terrain_texture[128]{""}; // optional ground texture name + float m_terrain_brush_strength{4.0f}; // metres per second while held; for the paint, how fast it covers + // the level brush + float m_terrain_target{0.0f}; // height the ground is led to + int m_terrain_level_mode{0}; // 0: up and down, 1: only raises, 2: only lowers + bool m_terrain_target_from_stroke{false}; // the target is the height where a stroke starts + bool m_terrain_stroke{false}; // a stroke of the brush is under way + bool m_terrain_picking{false}; // the next click picks the target height + int m_terrain_layer{0}; // entry of the palette painted + char m_terrain_material[128]{"grass"}; // material added to the palette + float m_terrain_material_size{0.0f}; // metres its textures repeat at, 0: given by the material + float m_terrain_layer_size{0.0f}; // repeat of the chosen palette entry, while it's dragged + bool m_terrain_layer_size_dragged{false}; + float m_terrain_layer_rotation{0.0f}; // angle of the chosen palette entry, while it's dragged + bool m_terrain_layer_rotation_dragged{false}; + std::vector m_water_points; // outline of a body of water being drawn + float m_water_level{0.0f}; + bool m_water_level_edited{false}; // the level was given by hand, the points clicked don't change it + char m_water_material[128]{""}; + int m_water_selected{-1}; + char m_water_selected_material[128]{""}; // material of the chosen water, while it's edited + int m_water_material_of{-1}; // the water whose material is in the field above + float m_water_selected_size{0.0f}; // metres its texture repeats at, 0: given by the material + char m_water_default_material[128]{""}; // of the waters which don't name their own, while it's edited + bool m_water_default_edited{false}; + std::set m_terrain_selection; // chunks chosen with the spacing tool + float m_terrain_convert_spacing{1.0f}; // point spacing the selected chunks are converted to + bool m_terrain_convert_paint{true}; // chunks converted to 0.5 m get paint samples every 0.5 m too + std::vector m_paint_outline; // of the area filled with a material + float m_paint_feather{0.0f}; // metres the filled material blends in over across the outline, 0: sharp + float m_paint_rotation{0.0f}; // degrees the textures of the filled material are turned by + float m_water_selected_level{0.0f}; // level of the chosen water, while it's edited + bool m_water_level_dragged{false}; + std::string m_terrain_status; - // streaming terrain that follows the camera (open-world); the editor shares the single - // simulation-level instance so authored terrain also renders in the driver / other modes + // the heightmap terrain, shared with the scenery loader so it shows in every mode terrain_streamer &m_streamer{EditorTerrain}; - int m_stream_radius{2}; - bool m_stream_persist{true}; // save edited chunks to disk and load them back + + // geoportal orthophoto layer drawn under the other viewport overlays + void render_orthophoto_ui(); + void render_map_menu(); + bool m_newscenery_asked{false}; + void render_new_scenery_popup(); + bool restart_for_new_scenery(); + bool restart_editor(std::string const &Scenery); + bool m_openscenery_asked{false}; + bool m_opened_at_track{false}; + bool m_turntable_included{false}; + bool turntable_controls(std::vector &Rootstatements); + void starter_trainset(std::vector &Trailing); + std::optional> m_saved_camera; + char m_openscenery_search[64]{}; + std::vector m_openscenery_list; + std::string m_openscenery_choice; + void render_open_scenery_popup(); + bool m_orthophoto_window{false}; + void render_orthophoto_window(); + // view menu: the top view, the lanes of the roads, the structure gauge, the environment + void render_view_menu(); + // time of day, day of the year, visibility, clouds and air temperature, as the scenery is seen in the editor + void render_environment_window(); + bool m_environment_open{false}; + // over the 3d view: the tools of the field of work along its left edge, the modes of the gizmo at its top, the axes of the + // world and the top view in its top right corner + void render_viewport_overlays(); + void render_viewport_tools(); + // the tools of the field of work, in the order of the toolbar + void work_area_tools(std::vector &Buttons); + // the tools of the field of work in the toolbar, as icons with their names + void render_toolbar_tools(); + // what the row of the toolbar holds, written at its left end: the fields of work, the tools + static void render_toolbar_caption(char const *Text); + void render_viewport_gizmo(); + void render_view_axes(); + // the camera looking along an axis of the world (0: x, 1: y, 2: z) from its positive or negative side + void look_along_axis(int const Axis, bool const Positive); + // origin of the scenery in PUWG 1992 the scenery file gives in its //$g line, if it does + bool m_georeference_read{false}; + bool m_georeference{false}; + glm::dvec2 m_georeference_origin{0.0}; // northing, easting + std::string m_georeference_line; + void read_georeference(); + void draw_orthophoto(); + void draw_ortho_compass() const; + // picks up the stored settings and the origin of the current scenery + void load_orthophoto_settings(); + void save_orthophoto_settings(); + editor_orthophoto m_orthophoto; + std::string m_orthophoto_scenery; // scenery whose origin is loaded + glm::dvec2 m_orthophoto_origin_edit{0.0}; // origin being typed in (northing, easting); applied when editing ends // hierarchy management void add_to_hierarchy(scene::basic_node *node); @@ -211,7 +533,7 @@ class editor_mode : public application_mode std::vector m_fill_last; // instances created by the most recent fill, for "Undo last fill" std::vector m_fill_custom; // user-assembled package of node templates int m_fill_custom_idx{-1}; - int m_fill_package{0}; // 0 = custom set, N = nodebank group N-1 + model_set_ref m_fill_source; // manual = m_fill_custom, otherwise a user set or node bank group float m_fill_density{200.0f}; // objects per hectare float m_fill_min_spacing{2.0f}; // minimal distance between placed objects (m) bool m_fill_random_rotation{true}; // random yaw; otherwise the Functions panel rotation settings apply @@ -220,12 +542,1463 @@ class editor_mode : public application_mode bool m_fill_models_as_ground{true}; // large model instances (terrain tiles) count as ground std::string m_fill_status; // result of the last fill, shown in the panel + // array: copies of the selected model set out in a row, after the array modifier of Blender + struct array_settings + { + int fit{0}; // 0: fixed count, 1: as many as the length takes + int count{2}; // the model itself included + float length{10.0f}; // m + bool relative{true}; + glm::vec3 relative_offset{1.0f, 0.0f, 0.0f}; // of the size of the model, along its axes + bool constant{false}; + glm::vec3 constant_offset{1.0f, 0.0f, 0.0f}; // m, along the axes of the model + float turn{0.0f}; // degrees around the vertical axis, each copy against the one before it + bool ground{false}; // the copies are put as high over the ground as the model is + bool operator==(array_settings const &) const = default; + }; + struct array_tool + { + static constexpr int limit = 1000; // models in an array, so a slip of the finger doesn't freeze the editor + static constexpr double minimal_step = 0.001; // m, copies any closer would sit in one another + array_settings settings; + // the array made last, which follows the settings until something else is selected or changed + TAnimModel *base{nullptr}; // model it was made of, nullptr if there's none + std::size_t step{0}; // size of the undo history with the array at its top + array_settings applied; // what the copies were set out with + glm::dvec3 location{0.0}; // of the model then + glm::vec3 angles{0.0f}; + glm::vec3 scale{1.0f}; + // size of the model asked about last + TModel3d const *sized{nullptr}; + glm::vec3 size{0.0f}; + std::string status; // outcome of the last operation, shown in the panel + }; + array_tool m_array; + void render_array(); + // size of the model of specified instance along its axes, the scale of the instance left out + glm::vec3 array_size(TAnimModel const &Base); + // offset of each copy from the one before it, along the axes of the model + glm::dvec3 array_offset(TAnimModel const &Base); + // number of the models in the array, the one it's made of included. Step: length of the offset + int array_count(double const Step) const; + // location and angles of each copy in turn + std::vector> array_placements(TAnimModel &Base); + // true while the array made last follows the settings + bool array_live() const; + void make_array(); + // brings the copies of the array made last to what the settings ask for + void shape_array(); + void update_array(); + void restore_array_snapshot(EditorSnapshot Snapshot, std::vector &Opposite, bool const Undo); + // ImGuizmo-based transform gizmo for the selected node enum class gizmo_operation { translate, rotate, scale }; void render_gizmo(); + // gizmo settings, drawn in the toolset window + void render_gizmo_options(); + // the tools of the field of work in the toolbar under the menu + void render_toolbar(); + // an edit mode of the node bank as the field of work, from the toolbar or its key (1-5) + void choose_edit_mode(nodebank_panel::edit_mode const Mode); + // fields of work of the editor, in the row above the toolbar: the models around, the tracks, the roads, the terrain + enum class work_area + { + surroundings, + tracks, + roads, + terrain + }; + work_area current_work_area() const; + void choose_work_area(work_area const Area); + void render_workspaces(); bool m_gizmo_enabled{true}; // master switch for the in-viewport gizmo bool m_gizmo_using{false}; // tracks an ongoing drag, so a single undo snapshot is taken per drag bool m_gizmo_local{false}; // manipulate in the object's local space instead of world space gizmo_operation m_gizmo_op{gizmo_operation::translate}; // current transform mode (translate/rotate/scale) float m_gizmo_snap{1.0f}; // translation snap step (metres) applied while Ctrl is held + + TTrack *selected_track() const; + void render_path_ui(); + void draw_track_overlay() const; + editor_track::point_ref track_handle_hit() const; + void select_track(scene::basic_node *Node); + void render_track_gizmo(); + void commit_track_drag(bool const Force); + void cancel_track_tools(); + void push_track_snapshot(std::vector> States, std::vector Created = {}, std::vector Removed = {}); + // false, with the reason, when one of the paths can't be changed, its file not taking the changes back + static bool tracks_editable(std::vector const &Tracks, std::string &Reason); + void trim_history(); + void restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); + editor_track::point_ref m_track_point; + std::vector m_track_drag; + std::vector> m_track_drag_points; + std::vector m_track_joints; // of the neighbours, following the whole track dragged + editor_track::snap_target m_track_snap; + glm::dvec3 m_track_pivot{0.0}; + glm::mat4 m_track_gizmo{1.0f}; + bool m_track_gizmo_using{false}; + bool m_track_dirty{false}; + std::chrono::steady_clock::time_point m_track_last_commit; + editor_track::state m_track_field_before; + std::array, 3> m_track_materials{}; + std::array m_track_material_edited{}; // path whose texture name is being typed + // typed names of the track circuits, the sleeper model and its skin, and the event to add, for the path + TTrack const *m_track_names_for{nullptr}; + std::array m_track_isolated{}; + std::array m_track_sleeper_model{}; + std::array m_track_sleeper_skin{}; + std::array m_track_event{}; + int m_track_event_list{0}; + std::string m_track_events_missing; + void render_path_circuits(TTrack &Track); + std::string m_track_mode_notice; + // structure gauge along the line of the selected path, shown in the 3d view + struct gauge_state + { + bool open{false}; // window + bool enabled{true}; // tunnel along the selected path + std::vector profiles; + int profile{0}; // railway + int road{-1}; + TTrack const *track{nullptr}; // scanned + std::size_t history{0}; + bool pending{false}; // rescan once the edit ends + struct + { + std::vector surface; // triangles of the tunnel + std::vector edges; // lines + std::vector hits; + } line; // along the selected path + struct + { + bool scanned{false}; + std::size_t history{0}; + std::string profile; + std::vector hits; + } map; // whole scenery + struct + { + std::vector surface; + std::vector edges; + } spot; // tunnel around the hit the camera went to + int current{-1}; // hit the camera went to + bool published{false}; // overlay up to date + std::shared_ptr scan; // of the whole scenery, done in slices over the frames + } m_gauge; + void gauge_load(); + // the chosen railway and road outlines + gauge::choice gauge_choice() const; + std::vector const &gauge_hits() const; + void update_gauge(); + void scan_gauge(TTrack &Track); + void scan_gauge_map(); + void step_gauge_map(); + void finish_gauge_map(); + void gauge_publish(); + void gauge_focus(int Index); + void gauge_spot(gauge_hit const &Hit); + void render_gauge_window(); + void render_gauge_hits(); + bool render_gauge_choice(char const *Label, int &Index, bool Road); + bool render_gauge_outline(int Index); + struct route_design + { + TTrack *from{nullptr}; + TTrack *to{nullptr}; + editor_track::chain chain; + std::string error; + std::string status; + alignment::design design; + alignment::result result; + int vertex{-1}; + int grip{-1}; + bool single{true}; // one curve between its straights, instead of the whole line + }; + void render_route_ui(); + bool render_route_parameters(); + bool render_route_vertices(); + bool render_route_vertex(int const Index); + void render_route_result(); + void route_reset(); + void route_bind_ends(); + bool route_from_curve(TTrack &Track); + bool route_from_line(TTrack &Track); + alignment::vertex route_vertex_of(editor_track::curve const &Curve) const; + bool route_load(TTrack &Track) { return m_route.single ? route_from_curve(Track) : route_from_line(Track); } + void route_update() { m_route.result = alignment::compute(m_route.design); } + void route_apply(); + bool route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created); + void route_recommend(alignment::vertex &Vertex) const; + void draw_route_overlay() const; + void render_route_gizmo(); + glm::dvec3 route_vertex_position(int const Vertex) const; + bool route_active() const; + route_design m_route; + + struct straights_state + { + editor_track::straight_tolerance tolerance; + double minimum_length{50.0}; + std::vector found; + int listed{-1}; + TTrack *current_for{nullptr}; + editor_track::straight current; + std::vector neighbours; + std::vector spacings{3.5, 4.0, 4.5, 4.75, 5.0, 5.5, 6.0}; + char filter[32]{}; + int handle{-1}; + bool dragging{false}; + editor_track::straight drag_line; + std::vector set; + std::vector drag_lines; + bool fitting{false}; + editor_track::curve fit_before; + editor_track::curve fit_after; + bool fit_has_before{false}; + bool fit_has_after{false}; + std::vector> drag_states; + glm::dvec3 preview_start{0.0}; + glm::dvec3 preview_end{0.0}; + glm::mat4 gizmo{1.0f}; + glm::dvec3 edit_start{0.0}; + glm::dvec3 edit_end{0.0}; + double edit_length{0.0}; + double edit_azimuth{0.0}; + double distance{4.0}; + double curve_radius{0.0}; + bool auto_transitions{true}; + double transition{0.0}; + int tool{0}; + bool tool_mouse{false}; + std::vector detour; + editor_track::straight detour_line; + glm::dvec3 detour_mouse{0.0}; + bool tool_placed{false}; + bool tool_dragging{false}; + double tool_at{0.0}; + glm::dvec3 tool_point{0.0}; + glm::dvec3 tool_handle{0.0}; + editor_track::straight tool_line; + glm::mat4 tool_gizmo{1.0f}; + std::string status; + }; + void render_straights_ui(); + void draw_straights_overlay() const; + editor_track::straight const ¤t_straight(); + bool straights_active(); + void render_straight_gizmo(); + void straight_apply(editor_track::straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End); + void straights_apply(std::vector const &Lines, std::vector> const &Ends); + bool in_straight_set(editor_track::straight const &Line) const; + bool place_straight_tool(); + void render_straight_tool_gizmo(); + void apply_straight_tool(); + double straight_tool_radius(editor_track::straight const &Line) const; + void straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius); + void straight_refresh(); + bool start_curve_fit(editor_track::straight const &Line, int const Handle); + void update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &End); + void add_detour_point(); + void apply_detour(); + std::vector detour_outline() const; + void find_neighbour_straights(); + // listed spacing of a neighbouring straight the line can snap to, if it is close enough + struct parallel_offer + { + bool nearby{false}; + bool snaps{false}; + double spacing{0.0}; + double distance{0.0}; + double angle{0.0}; // deg between the two straights + double correction{0.0}; // m along the line normal to reach the spacing + editor_track::straight const *neighbour{nullptr}; + }; + parallel_offer straight_parallel_offer(editor_track::straight const &Line, glm::dvec3 const &Offset) const; + glm::dvec3 snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const; + glm::dvec3 snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const; + straights_state m_straights; + + struct curve_sample + { + double station{0.0}; + glm::dvec3 position{0.0}; + glm::dvec2 tangent{0.0, 1.0}; + float roll{0.f}; + float radius{0.f}; + }; + struct switch_tool + { + bool curved{false}; + std::vector frame; + std::vector frame_tracks; + std::vector templates; + bool collected{false}; + int armed{-1}; + int last{0}; // template armed the last time + bool placing{false}; + double along{0.0}; + glm::dvec3 point{0.0}; + glm::dvec3 mouse{0.0}; + editor_track::straight line; + std::string status; + char name[64]{}; + struct drive_family + { + std::string name; + std::array files; + }; + std::vector drives; + bool drives_scanned{false}; + int drive{-1}; + int drive_side{0}; + }; + switch_tool m_switch; + void scan_switch_drives(); + std::string switch_name_for_new() const; + bool place_switch_drive(TTrack &Switch); + bool render_switch_drive_choice(); + int m_turnout_template{0}; + struct turntable_template + { + std::string file; + std::string name; + double length{0.0}; + int angles{0}; + int positions{0}; + int track{0}; + int yaw{0}; + std::vector fixed; + }; + struct turntable_fit + { + editor_track::snap_target end; + double heading{0.0}; + double radius{0.0}; + double length{0.0}; + std::vector pieces; + std::string issue; + bool chosen{true}; + TTrack *replaces{nullptr}; + }; + struct turntable_tool + { + std::vector templates; + bool scanned{false}; + int chosen{-1}; + char name[64]{}; + bool placing{false}; + glm::dvec3 centre{0.0}; + double yaw{0.0}; + glm::vec2 pressed{0.f}; + editor_track::snap_target snap; + TTrack *table{nullptr}; + scene::instance_handle include{0}; + int kind{-1}; + double length{0.0}; + double table_yaw{0.0}; + std::vector exits; + float reach{80.f}; + float radius{150.f}; + float stub{25.f}; + float step{0.f}; + std::vector fits; + int hovered{-1}; + double heading{0.0}; + double exit_length{0.0}; + std::string status; + std::string error; + }; + turntable_tool m_turntable; + void scan_turntables(); + std::string turntable_name_for_new() const; + bool turntable_read(TTrack *Table); + std::vector turntable_includes(TTrack const &Table); + glm::dvec3 turntable_centre() const; + std::vector turntable_lines() const; + double turntable_snap(double const Heading) const; + void turntable_finish_placement(); + void turntable_place(glm::dvec3 const &Centre, double const Yaw); + bool turntable_fit_end(editor_track::snap_target const &End, turntable_fit &Fit) const; + void turntable_find_fits(); + void turntable_build(std::vector const &Fits); + bool turntable_exit_at(glm::dvec3 const &Ground, double &Heading, double &Length) const; + void turntable_store_exits(); + void render_turntable_ui(); + void draw_turntable_overlay() const; + struct extend_tool + { + bool active{false}; + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; + glm::dvec3 mouse{0.0}; + int path{0}; + bool atend{true}; + editor_track::snap_target origin; + editor_track::snap_target snap; + glm::vec2 pressed{0.f}; + }; + extend_tool m_extend; + // lays new track through the clicked points, with no path needed to start from + struct lay_tool + { + bool active{false}; + std::vector points; + editor_track::snap_target start; // free end the track starts from, if any + glm::dvec3 mouse{0.0}; + editor_track::snap_target mouse_snap; + std::vector preview; + std::string preview_error; + double preview_length{0.0}; + double radius{500.0}; + bool transitions{true}; + double piece_length{100.0}; + bool copy_selected{false}; + editor_track::path_style style; + std::string status; + }; + lay_tool m_lay; + void render_lay_ui(); + void render_lay_status(); + void lay_click(); + void lay_finish(editor_track::snap_target const &End); + void lay_cancel(); + void lay_enter(); + bool lay_heading(glm::dvec2 &Heading) const; + std::vector lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const; + alignment::result lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &Design) const; + void render_switch_ui(); + bool start_switch_placement(); + void finish_switch_placement(); + std::vector switch_preview() const; + void insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side); + void switch_placed(std::vector const &Created); + static std::vector sample_chain(editor_track::chain const &Chain); + static curve_sample sample_at(std::vector const &Frame, double const Station); + static double nearest_station(std::vector const &Frame, glm::dvec3 const &Point); + static std::vector curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side); + void insert_curved_switch(int const Direction, int const Side); + bool place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector> &States, std::vector &Created, std::vector &Removed, std::string const &Name = {}); + struct crossover_plan + { + editor_track::straight target; + editor_track::switch_template shape; + double along{0.0}; + int direction{1}; + int side{1}; + segment_data insert; + std::vector paths; + }; + bool plan_crossover(crossover_plan &Plan) const; + bool cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style); + bool insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape); + bool replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape); + std::vector build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style); + bool start_extend(editor_track::snap_target const &End); + void finish_extend(); + std::vector extend_pieces() const; + // the free end of the path as the start of a new track; false if it isn't free or has no direction + static bool free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool); + // the free end new pieces are added at: the selected point if it's one, else the end, else the start + bool chosen_free_end(extend_tool &Tool); + // adds a straight (Side 0) or a curve to the left (1) or the right (-1) at the free end of the selected track + void extend_add(int const Side); + void render_extend_ui(); + bool m_extend_freehand{false}; // dragging the ring of a free end draws a curve through the cursor, not a straight + double m_extend_length{25.0}; + double m_extend_radius{300.0}; + bool extend_snap(editor_track::snap_target &Target) const; + void update_build_tools(); + void draw_build_overlay() const; + // modes of the track editor, one at a time: path is the plain selection + enum class track_tab { straights, route, path, turnout, profile, speed, joints, infra, lay, lineside, turntable, signals }; + struct track_mode + { + char const *label; + char const *key; + track_tab tab; + char const *tooltip; + }; + static std::array const &track_modes(); + // the objects along the track: a tab for each tool + int m_lineside_tab{0}; + void render_lineside_ui(); + void arm_switch(); + bool m_track_window_open{false}; + // the inspector is pinned to the right edge of the screen, the profile strip to the bottom one, both resized from the inner edge + void render_track_inspector(); + // the track tools in the tool options window, and the selected path in the inspector + void render_track_tool_options(); + void render_track_selection(); + void show_track_tab(track_tab const Tab); + bool track_analysis_tab() const { return m_track_tab == track_tab::profile || m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_track_tab == track_tab::infra; } + bool track_shortcut(int const Key); + bool track_busy() const; // something going on in the mode, which Esc drops before it leaves the mode + std::string track_mode_name() const; + void render_turnout_ui(); + struct track_name_edit + { + TTrack const *track{nullptr}; + std::string synced; + char text[128]{}; + editor_track::name_uses uses; + std::string status; + }; + track_name_edit m_track_name; + void render_track_name(TTrack &Track); + void render_path_parameters(TTrack &Track); + void render_straight_ui(); + track_tab m_track_tab{track_tab::path}; + void delete_selected_track(); + void start_straight_gesture(int const Tool); + void finish_straight_gesture(); + void toggle_straight_set(TTrack &Track); + struct key_hint + { + char const *key; + std::string action; + }; + std::vector track_key_hints(bool const All) const; + void render_track_guide(); + std::string track_readout() const; + void draw_track_hints(); + // path under the cursor, outlined before a click so it's known which one the click takes + struct track_hover + { + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 point{0.0}; + double since{0.0}; + }; + track_hover m_hover; + track_hover track_under_cursor() const; + void update_track_hover(); + void draw_track_hover() const; + struct track_intent + { + enum class kind { none, gizmo, select, point, box, straight_set, straight_break, detour, straight_tool, straight_handle, route_vertex, route_grip, switch_at, lay, lay_end, sweep, turntable_place, turntable_exit, turntable_fit, spread, extend_end, signal_place, signal_move }; + kind what{kind::none}; + TTrack *track{nullptr}; + int path{0}; + editor_track::point_ref point; + int index{-1}; + glm::dvec3 position{0.0}; + editor_track::snap_target snap; + std::string action; + }; + track_intent m_intent; + track_intent track_intent_at(int const Mods); + bool track_gesture_on() const; + void update_track_intent(); + void track_press(track_intent const &Intent); + void turntable_press(track_intent const &Intent); + track_intent turntable_intent(glm::dvec3 const &Ground, bool const Shift); + void draw_track_intent() const; + editor_track::snap_target snap_free_end(glm::dvec3 const &Near, glm::vec2 const &Screen, TTrack const *Self, int const Category, std::vector const &Exclude) const; + void draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const; + struct point_drag + { + bool active{false}; + bool moved{false}; + glm::vec2 from{0.f}; + double height{0.0}; + }; + point_drag m_point_drag; + struct handle_drag + { + bool active{false}; + bool moved{false}; + glm::vec2 from{0.f}; + double height{0.0}; + glm::dvec3 offset{0.0}; + glm::dvec3 position{0.0}; + }; + handle_drag m_handle_drag; + void handle_drag_start(glm::dvec3 const &Anchor); + bool handle_drag_step(glm::dvec3 &Position); + void point_drag_start(editor_track::point_ref const &Point); + void point_drag_update(); + void point_drag_finish(); + bool route_hit(int &Vertex, int &Grip) const; + int straight_handle_hit(); + bool switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point); + glm::dvec3 cursor_ground() const; + glm::dvec3 cursor_level(double const Height) const; + std::optional m_cursor_override; + bool sweep_grabs(glm::dvec3 const &Point); + // menu of the path a click of the right mouse button lands on + struct track_context + { + track_hover target; + std::chrono::steady_clock::time_point pressed; + glm::vec3 angle{0.f}; + glm::vec2 mouse{0.f}; + bool armed{false}; + bool pending{false}; + }; + track_context m_track_context; + void track_context_press(); + void track_context_release(); + void render_track_context(); + void split_track_at(TTrack &Track, glm::dvec3 const &Point); + void focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point); + // paths found by their names + struct track_search + { + char text[64]{}; + std::string searched; + std::vector found; + std::size_t total{0}; + bool focus{false}; + }; + track_search m_track_search; + void render_track_search(); + // paths selected together: Shift+LMB adds or takes one out, Shift+drag adds those in the box + std::vector m_track_set; + struct track_box + { + bool active{false}; + glm::vec2 from{0.f}; + glm::vec2 to{0.f}; + TTrack *hovered{nullptr}; + }; + track_box m_track_box; + struct track_set_fields + { + double velocity{100.0}; + char rail[128]{}; + char trackbed[128]{}; + char circuits[256]{}; + int environment{0}; + }; + track_set_fields m_track_set_fields; + glm::mat4 m_track_set_gizmo{1.0f}; + bool m_track_set_using{false}; + glm::dvec3 m_track_set_pivot{0.0}; + std::vector>> m_track_set_joints; + bool in_track_set(TTrack const *Track) const; + void track_set_toggle(TTrack &Track); + void track_set_prune(); + void start_track_box(); + void finish_track_box(); + void draw_track_set() const; + void render_track_set_ui(); + void render_track_set_gizmo(); + void track_set_delete(); + struct track_spread + { + int stop{0}; + bool same_rails{false}; + bool same_bed{false}; + bool same_speed{false}; + float reach{0.f}; + TTrack const *from{nullptr}; + int signature{-1}; + std::vector preview; + double length{0.0}; + }; + mutable track_spread m_spread; + std::vector track_spread_from(TTrack &Start) const; + std::vector const &track_spread_preview(TTrack &Start) const; + void track_spread_apply(TTrack &Start); + void track_set_grow(); + void render_track_spread_ui(); + void draw_track_spread() const; + // changes each path of the set which can be changed; false from Change: the path stays as it was + void track_set_apply(std::function const &Change, bool const Rebuild, char const *What); + // second track alongside the selected path or the line through it, at a spacing, curves included + struct parallel_tool + { + bool open{false}; + bool expand{false}; + double spacing{4.0}; + int side{1}; // 1: to the right, -1: to the left, of the direction of the line + int scope{1}; // 0: the selected path, 1: the line through it, up to the switches + TTrack *built_for{nullptr}; + double built_spacing{0.0}; + int built_side{0}; + int built_scope{-1}; + std::size_t built_history{0}; + TTrack *pair_src{nullptr}; + TTrack *pair_dst{nullptr}; + double measured{0.0}; + std::vector pieces; + std::vector styles; + double length{0.0}; + std::string error; + std::string status; + }; + parallel_tool m_parallel; + TTrack *parallel_source() const; + TTrack *parallel_target() const; + void parallel_update(); + void render_parallel_controls(bool const Scope = true); + void render_parallel_ui(); + void render_parallel_set_ui(); + void parallel_build(); + void parallel_apply(); + bool parallel_preview_visible() const; + void draw_parallel_preview() const; + + // scenery templates standing by the track, placed and checked by the editor + struct template_item + { + std::string file; + glm::dvec3 location{0.0}; // world space + double yaw{0.0}; + std::vector rest; // parameters after the rotation + bool described{false}; + std::string track; + glm::dvec2 tilt{0.0}; // rot.x and rot.z of a described template, degrees + std::map values; + }; + struct standing_template + { + scene::instance_handle instance{0}; + std::string file; // lower case, forward slashes + glm::dvec3 location{0.0}; + double yaw{0.0}; + std::vector values; + }; + // includes of the templates whose file names the test accepts, with the location in p2..p4 and the rotation in p5 + std::vector standing_templates(std::function const &Accept) const; + // places them in the active layer, as one step of the undo. returns: how many were placed + std::size_t place_templates(std::vector const &Items, std::string &Error); + // moves them, as one step of the undo + std::size_t move_templates(std::vector> const &Moves); + // takes them out, as one step of the undo + std::size_t remove_templates(std::vector const &Instances); + + // hectometre posts along the line, at every 100 m of the kilometrage + struct hekto_post + { + int hectometres{0}; + double chainage{0.0}; + glm::dvec3 position{0.0}; + double yaw{0.0}; + int standing{-1}; // post already there with this number + }; + struct hekto_found + { + standing_template post; + int hectometres{0}; + double chainage{0.0}; // where it stands along the line + double expected{0.0}; // where its number belongs + double error{0.0}; // m along the line + double distance{0.0}; // m from where it belongs, in the plan + bool wrong{false}; + bool duplicate{false}; // another post with the same number stands nearer to its place + bool outside{false}; // its number belongs past the ends of the line + }; + struct hekto_tool + { + bool open{false}; + bool expand{false}; + TTrack *track{nullptr}; // the line runs through it + glm::dvec3 point{0.0}; // where the kilometrage is given + double km{0.0}; + bool km_read{false}; // the kilometrage was read from the posts already standing + int growth{1}; // 1: grows along the line, -1: against + int side{1}; // 1: to the right of the growing kilometrage, -1: to the left, 0: alternately + double offset{3.0}; // m from the axis + int style{0}; // nowy, stary, zgnity, mixed + int turn{0}; // degrees added to the rotation + double reach{10000.0}; // m each way + double tolerance{5.0}; // m along the line, for the posts standing + bool dirty{true}; + std::size_t history{0}; + editor_track::route route; + std::vector samples; + double origin{0.0}; // chainage of the point + std::vector posts; + std::vector found; + std::string status; + std::string error; + }; + hekto_tool m_hekto; + void hekto_start(TTrack &Track, glm::dvec3 const &Point); + void hekto_update(); + void hekto_read_kilometrage(); + void hekto_place(); + void hekto_fix(); + void hekto_remove_duplicates(); + void render_hekto_ui(); + void draw_hekto_overlay() const; + + // fouling point markers (W17) where the tracks meeting at a switch are the spacing apart + struct fouling_point + { + TTrack *track{nullptr}; // the switch + glm::dvec3 position{0.0}; + double yaw{0.0}; + int standing{-1}; // marker already there + }; + struct fouling_found + { + standing_template marker; + int point{-1}; // the fouling point it belongs to + double error{0.0}; // m from where it belongs + double height_error{0.0}; // m over where it belongs + bool wrong{false}; + }; + struct fouling_tool + { + bool open{false}; + bool expand{false}; + int scope{0}; // 0: the selected switch, 1: around the camera, 2: the whole scenery + double reach{1500.0}; + double spacing{3.5}; // m between the axes + double tolerance{2.0}; // m, a marker farther than this from its point is wrong + int height_mode{1}; // 0: on the ground, 1: at the height given over the rail heads + double height{0.0}; // m over the rail heads + double height_tolerance{0.03}; // m, a marker standing higher or lower than this is wrong + int model{0}; // w17, w17_new, w17_old, w17_dwuteo + int turn{0}; + bool dirty{true}; + std::size_t history{0}; + TTrack *selected{nullptr}; + std::vector points; + std::vector found; + std::string status; + std::string error; + }; + fouling_tool m_fouling; + void fouling_update(); + void fouling_place(); + void fouling_fix(); + void render_fouling_ui(); + void draw_fouling_overlay() const; + + // a vehicle put on the selected path, to be driven: it isn't written to the scenery files + struct vehicle_tool + { + bool open{false}; + bool expand{false}; + std::shared_ptr bank; + char search[64]{}; + bool controllable_only{true}; + std::shared_ptr vehicle; + std::shared_ptr skin; + int driver{0}; // 0: a driver, to be driven; 1: nobody, parked + TTrack *track{nullptr}; + glm::dvec3 point{0.0}; + std::string placed; + std::string status; + bool leave{false}; // the editor gives way to the driving at the start of the next frame + struct consist_vehicle + { + std::shared_ptr vehicle; + std::shared_ptr skin; + bool turned{false}; + }; + std::vector consist; + int facing{1}; + }; + vehicle_tool m_vehicle; + void vehicle_start(TTrack &Track, glm::dvec3 const &Point); + bool vehicle_place(); + void render_vehicle_ui(); + void draw_vehicle_marker() const; + + struct signal_template + { + std::string file; + std::string name; + std::string description; + std::string kind; + std::string mount; + std::string lamps; + std::string lean; + std::string read; + int parameters{0}; + bool plate{false}; + bool linked{false}; + }; + struct signal_spot + { + TTrack *track{nullptr}; + int path{0}; + glm::dvec3 axis{0.0}; + glm::dvec2 travel{0.0, 1.0}; + int side{1}; + int list{2}; + double offset{0.0}; + glm::dvec3 position{0.0}; + double yaw{0.0}; + }; + struct signal_standing + { + standing_template include; + int kind{-1}; + std::string name; + std::string read; + TTrack *track{nullptr}; + int list{0}; + signal_spot spot; + bool on_track{false}; + }; + struct signal_tool + { + std::vector templates; + int revision{-1}; + int kind{0}; + std::array chosen{-1, -1, -1, -1, -1, -1, -1}; + char filter[64]{}; + char name[64]{"A"}; + char plate[64]{}; + char linked[64]{"none"}; + float height{0.0f}; + bool placing{false}; + signal_spot press; + glm::vec2 pressed{0.f}; + int facing{0}; + std::vector standing; + std::size_t standing_history{static_cast(-1)}; + std::size_t standing_redo{static_cast(-1)}; + double standing_time{-1.0}; + int hover{-1}; + scene::instance_handle selected{0}; + bool moving{false}; + signal_spot moved; + bool moved_valid{false}; + std::string status; + }; + signal_tool m_signal; + void signal_scan(); + signal_template const *signal_armed() const; + signal_template const *signal_template_of(std::string const &File) const; + bool signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const; + void signal_refresh(bool const Force = false); + int signal_hit() const; + void signal_press(track_intent const &Intent); + void signal_release(); + bool signal_place(signal_spot const &Spot, signal_template const &Template); + void signal_move_update(); + void signal_move_finish(); + bool signal_delete(scene::instance_handle const Instance); + void signal_detach_events(std::string const &Read, std::vector> &States); + void signal_renamed(std::string const &Before, std::string const &After, EditorSnapshot &Snapshot); + void signal_select(scene::instance_handle const Instance); + std::string signal_next_name(std::string const &Name) const; + void render_signal_ui(); + void render_signal_palette(); + void render_signal_selected(); + void draw_signal_overlay() const; + std::string signal_intent_label(signal_template const &Template, signal_spot const &Spot) const; + std::string signal_variant(signal_template const &Template, signal_spot const &Spot) const; + + // a model laid along the track: bent to follow it, or repeated along it + struct sweep_tool + { + bool open{false}; + bool expand{false}; + char model[256]{}; + char skin[128]{"none"}; + char search[64]{}; + std::vector models; + bool scanned{false}; + // parameters of a template laid along the track, with their names + std::string parameters_for; + std::vector> parameters; + std::vector parameter_labels; + std::vector parameter_placement; // filled in with zeros: the template is placed in its own space + std::vector> parameter_choices; // variants of the textures a parameter is a part of the name of + int length_mode{2}; // 0: one copy as long as the original, 1: the length given, 2: the whole stretch of the line + double length{200.0}; + sweep_node::state settings; + int axis{0}; // 0: the longer one of the model, 1: z, 2: x + double model_length{0.0}; + std::string measured; // model the length is of + // the curve: the axis of the track + TTrack *curve_for{nullptr}; + std::size_t curve_history{0}; + struct piece + { + TTrack *track{nullptr}; + int path{0}; + bool forward{true}; + }; + std::vector curve_pieces; + std::vector curve; + double curve_length{0.0}; + std::vector outline; // of the curve, for the preview + std::vector stations; + // marking the stretch in the view: press where it starts, the distance from the track sets the offset, drag to the end + bool marking{false}; + bool dragging{false}; + double anchor{0.0}; // station the drag started at + sweep_node *edited{nullptr}; + std::string status; + }; + sweep_tool m_sweep; + // paths the models along curves were laid on, so they follow the changes of the paths + struct sweep_link + { + sweep_node *sweep{nullptr}; + std::size_t piece{0}; + TTrack *track{nullptr}; + int path{0}; + bool reversed{false}; + }; + std::vector m_sweep_links; + void sweep_curve(); + void sweep_outline(); + bool sweep_extend(bool const Atend, glm::dvec3 const &Cursor); + double sweep_station(glm::dvec3 const &Point, double &Lateral) const; + void sweep_scan_models(); + void sweep_measure(); + sweep_node::state sweep_definition() const; + void sweep_create(); + void sweep_apply(); + void sweep_delete(sweep_node &Sweep); + void sweep_edit(sweep_node &Sweep); + std::vector sweeps_near(TTrack const &Track) const; + bool sweep_press(); + void sweep_drag(); + void sweep_release(); + // the same at a point of the world: Anywhere takes the press however far from the track it is + bool sweep_press_at(glm::dvec3 const &Point, bool const Anywhere); + void sweep_drag_at(glm::dvec3 const &Point); + void render_sweep_ui(); + void draw_sweep_overlay() const; + void restore_sweeps(EditorSnapshot &Snapshot); + void sweeps_captured(TTrack const &Track); + void sweeps_committed(std::vector const &Tracks); + void sweeps_split(TTrack &Original, TTrack &Created); + + struct bend_source + { + TAnimModel *model{nullptr}; + scene::instance_handle instance{0}; + std::string file; + std::string skin{"none"}; + std::vector parameters; + glm::dmat4 transform{1.0}; + glm::dvec3 scale{1.0}; + scene::layer_handle layer{null_handle}; + }; + struct bend_tool + { + int axis{0}; + bool turned{false}; + int along_anchor{1}; + int side_anchor{0}; + int height_anchor{0}; + glm::dvec3 point{0.0}; + bool picking{false}; + void const *tracks_for{nullptr}; + glm::dvec3 tracks_at{0.0}; + std::vector> tracks; + int track{0}; + sweep_node *edited{nullptr}; + std::string signature; + bool marker_valid{false}; + glm::dvec3 marker{0.0}; + std::string status; + int dragging{0}; + sweep_node::state drag_state; + glm::dvec3 drag_anchor{0.0}; + double drag_station{0.0}; + std::string drag_label; + sweep_node *hover{nullptr}; + bool over_marker{false}; + std::string label; + std::string details; + }; + bend_tool m_bend; + bool bend_drag_start(int const Mods); + void bend_drag_update(); + sweep_node *sweep_under(glm::dvec3 const &Point) const; + bool bend_click(); + bool bend_shortcut(); + bool bend_source_of_selection(bend_source &Source) const; + void bend_find_tracks(glm::dvec3 const &Point, void const *Key); + bool bend_definition(bend_source const &Source, TTrack &Track, sweep_node::state &State, std::vector &Tracks); + void bend_selection(); + void bend_edit(sweep_node::state const &State); + void bend_reanchor(sweep_node::state &State, int const Side, int const Height, glm::dvec3 const &Point) const; + double bend_fixed(sweep_node const &Sweep, glm::dvec3 const &Point) const; + void bend_stretch(sweep_node const &Sweep, double const Fixed, double const Length, glm::dvec3 const &Point, sweep_node::state &State) const; + void bend_pick(glm::dvec3 const &Point); + glm::dvec3 bend_marker(sweep_node const &Sweep) const; + void restore_bend(EditorSnapshot &Snapshot, bool const Undo); + void render_bend(); + void draw_bend_overlay() const; + + // script of editor actions given in the EU07_EDITOR_SELFTEST file, run a step at a time, for testing without a person + struct selftest + { + bool loaded{false}; + std::vector lines; + std::size_t next{0}; + int wait{0}; + bool quitting{false}; + }; + selftest m_selftest; + void selftest_step(); + // a value typed during a gesture, taken instead of the one the cursor gives + std::string m_typed; + // what the typed value sets in the gesture under way, nullptr when the gesture takes none + char const *typed_meaning() const; + std::vector typed_values() const; + bool typed_key(int const Key); + void typed_extend(); + glm::dvec3 typed_lay(glm::dvec3 const &Mouse) const; + void typed_turn(); + bool m_route_tab{true}; + + // vertical profile (grade line) along a route + struct profile_state + { + bool open{false}; + TTrack *from{nullptr}; + TTrack *to{nullptr}; + TTrack *picked_from{nullptr}; + TTrack *picked_to{nullptr}; + double run_length{3000.0}; + editor_track::route route; + std::vector samples; + std::vector terrain; // NaN where there's no terrain + profile::line line; + profile::context context; + std::vector issues; + double origin{0.0}; // chainage of the start of the route + double departure{0.0}; // largest difference between the grade line and the track + profile::ground_fit fit; + // the heightmap terrain led to the grade line by a modifier: formation under the track, slopes down or up to the ground + struct earthworks + { + bool from_ballast{true}; + double depth{0.7}; // m, formation below the top of the rail + double half_width{3.5}; // m, from the axis of the route to the edge of the formation + double slope{1.5}; // horizontal run of the slopes per metre of height + double reach{60.0}; // m, from the edge of the formation, where the slopes end even if they don't get to the ground + double rounding{1.5}; // m, over which the slopes bend into the formation and into the ground + bool generate{true}; + // chunks of terrain made under the route by the last shaping + std::vector> generated; + // the modifier of the terrain made by the last shaping + std::string modifier; + } earthworks; + double view_from{0.0}; + double view_to{100.0}; + double view_centre{0.0}; + float exaggeration{20.0f}; + bool show_terrain{true}; + bool show_track{true}; + bool show_plan{true}; + int selected{-1}; + int dragging{-1}; + int curve_grip{-1}; + bool panning{false}; + bool changed{false}; + double hover{-1.0}; + // under the cursor in the strip + int hover_point{-1}; + int hover_grip{-1}; + bool hover_line{false}; + int shown{-2}; // point in the side panel + std::string status; + std::string error; + }; + profile_state m_profile; + // grade lines designed in the editor, kept in the scenery files as "//$p" comment lines + struct stored_profile + { + scene::layer_handle layer{null_handle}; + double length{0.0}; + glm::dvec3 start{0.0}, end{0.0}, middle{0.0}; // of the route, world space + double origin{0.0}; + profile::line line; + }; + std::vector m_profiles; + bool m_profiles_loaded{false}; + void profile_library_load(); + void profile_library_mark(std::vector Layers); + int profile_find(bool &Reversed) const; + bool profile_restore(); + void profile_store(); + void profile_forget(); + void profile_open_stored(std::size_t const Index); + void profile_edited(); + void render_profile_body(); + void render_profile_strip(); + void render_profile_toolbar(); + void render_profile_points(); + void render_profile_source(); + bool render_profile_parameters(); + void render_profile_issues(); + void profile_open(TTrack *From, TTrack *To); + void profile_open_run(TTrack &Track); + void profile_take(editor_track::route Route, TTrack *From, TTrack *To); + void profile_resample(); + void profile_recognize(); + void profile_fit_ground(); + static std::pair profile_ballast(TTrack const &Track); + void profile_shape_ground(); + std::pair profile_generate_ground(std::vector> const &Beds, double const Reach); + void profile_restore_ground(); + void render_profile_earthworks(); + void profile_check(); + void profile_apply(); + void profile_fit_view(); + void profile_after_undo(); + struct profile_view; + void render_profile_canvas(float const Height); + void profile_fit_exaggeration(profile_view const &View); + void profile_navigate(profile_view const &View, bool const Hovered); + bool profile_canvas_edit(profile_view const &View, bool const Hovered); + void draw_profile_canvas(profile_view const &View, ImDrawList &Draw) const; + void profile_canvas_tooltip() const; + void render_profile_point(); + void draw_profile_overlay() const; + + // infrastructure along the track: objects bound to the paths follow their changes. bindings are kept in the + // scenery files as "//$b" comment lines + struct infra_candidate + { + infra::binding binding; + double offset{0.0}; // sideways from the axis + std::string reason; + bool chosen{false}; + bool bound{false}; + TTrack *reader{nullptr}; + std::string event; // of that path + std::string cell; + bool loose{false}; + TTrack *nearer{nullptr}; + glm::dvec3 nearer_foot{0.0}; + bool unread{false}; + }; + struct infra_window + { + bool open{false}; + int scope{1}; // 0: the selected path, 1: the line through it, 2: the route of the vertical profile + float reach{1500.0f}; + float corridor{8.0f}; + bool follow{true}; + bool show{true}; + std::vector rules{infra::default_rules()}; + std::vector tracks; // of the scope + std::vector candidates; + int filter{0}; + int read{0}; + int overruled{0}; + int unread{0}; + int elsewhere{0}; + int hovered{-1}; + std::string status; + std::string error; + }; + infra_window m_infra; + std::vector m_bindings; + std::vector> m_bindings_unresolved; // lines kept as they are + bool m_bindings_loaded{false}; + std::vector m_infra_buffer; // states of the objects before the change in progress + bool m_infra_suspended{false}; + void infra_load(); + void infra_store(); + std::vector infra_scope() const; + void infra_recognize(); + void infra_bind_chosen(); + void infra_unbind(std::vector Indices); + infra::binding *infra_find(infra::binding const &Binding); + infra::object_state infra_state_of(infra::binding const &Binding) const; + void infra_restore_state(infra::object_state const &State); + void infra_buffer(infra::binding const &Binding, bool const Refresh); + // editor_track::observer + void track_captured(TTrack const &Track) override; + void tracks_committed(std::vector const &Tracks) override; + void track_retired(TTrack &Track) override; + void track_split(TTrack &Original, TTrack &Created, segment_data const &First) override; + void infra_move(infra::binding &Binding); + void infra_attach(EditorSnapshot &Snapshot); + void render_infra_body(); + + // speed limits of the paths checked against what their geometry allows + struct speed_state + { + bool open{false}; + int scope{0}; // 0: the line through the selected path, 1: the whole scenery + speed_check::options options; + std::vector> queue; + std::size_t next{0}; + std::vector results; + bool show_all{false}; + int limits{0}; // 0: all the paths, 1: only those with a speed limit set, 2: only those without + int selected{-1}; + std::size_t history{0}; // size of the undo history at the check + std::string status; + }; + speed_state m_speed; + void render_speed_body(); + void speed_start(); + void speed_step(); + void speed_focus(int const Index); + // with Branches the limits of the switch branches count too, otherwise they're only warnings + void speed_apply(std::vector const &Indices, bool const Branches); + std::vector speed_listed() const; + void draw_speed_overlay() const; + + // joints of the paths: ends which almost meet, steps, kinks, jumps of the cant and of the grade + struct joints_state + { + bool open{false}; + int scope{0}; // 0: around the camera, 1: the whole scenery + double reach{2000.0}; + double gap{1.0}; // m, free ends closer than this to another end + double step{0.005}; // m + double angle{0.2}; // deg + double cant{5.0}; // mm + double grade{10.0}; // per mille + bool free_ends{false}; + std::vector queue; + std::size_t next{0}; + std::vector issues; + int filter{-1}; + int selected{-1}; + std::size_t history{0}; + std::string status; + }; + joints_state m_joints; + void joints_start(); + void joints_step(); + void joints_check(TTrack &Track); + void joints_focus(int const Index); + bool joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed); + void joints_fix(std::vector const &Indices); + void render_joints_body(); + void draw_joints_overlay() const; + void infra_rebase(); + void render_infra_search(); + void render_infra_candidates(); + void render_infra_bound(); + void draw_infra_overlay() const; + + bool m_route_gizmo_using{false}; + glm::mat4 m_route_gizmo{1.0f}; + float m_track_snap_radius{1.0f}; + bool m_track_align_tangent{true}; + bool m_track_drag_connected{true}; + + // roads: a menu and a window of their own, independent from the edit modes of the toolset + // what a click of the build tool would make + struct road_plan + { + std::vector pieces; // axes of the pieces, in the order they're laid + editor_road::loose_end snap; // loose end of a road or arm of a junction the road is led to + editor_road::branch start; // junction made, or given another arm, for a road led out of the side of a road or out of a junction + bool hasstart{false}; + editor_road::branch end; // the same for a road led into the side of a road or into a junction + bool hasend{false}; + std::string error; // why there's nothing to make + }; + struct road_tool + { + bool window{false}; // the road window is open, the left mouse button belongs to its tools + bool lanes{true}; // lanes of the roads are drawn as trajectories + int tool{0}; // 0: select, 1: build, 2: place + road_node *selected{nullptr}; + junction_node *junction{nullptr}; // selected junction + roadpoint_node *marker{nullptr}; // selected level crossing or traffic point + std::vector points; // selected points where the pieces meet or end + road_node::state settings; // layout shown in the window: of the selected piece, or of the pieces about to be built + bool whole{true}; // layout changes go to every piece of the road the selected one belongs to + bool varied{false}; // the selected piece is shown with lanes of its own for its end + bool chain{false}; // the point to build from is set + glm::dvec3 point{0.0}; // where the next piece starts + glm::dvec2 direction{0.0, 1.0}; // the way it has to leave that point + bool hasdirection{false}; + bool reversed{false}; // the pieces are laid from their ends, to carry on a road from its start + double grade{0.0}; // slope the next piece has to start with, to carry on what it's attached to + bool hasgrade{false}; + // the road being built leaves the side of a road, or a junction between its roads. the point to build from is the centre + // of the junction, which gets made, or gets another arm, along with the first piece + road_node *branchroad{nullptr}; + double branchat{0.0}; // value of the curve parameter of the axis of that road + junction_node *branchjunction{nullptr}; + float offset{0.1f}; // height above the ground the roads are built at + bool follow{true}; // built roads are led over the ground between the clicked points + float crossingspeed{30.f}; // speed limit on the junctions being made + float heightstep{0.1f}; // how much the selected points are raised or lowered at a time + bool ring{false}; // the build tool makes roundabouts + float ringradius{15.f}; // of the middle of the road going around + std::string island{"none"}; // what the middle of a roundabout is covered with + float bankgrade{1.5f}; // metres a bank led to the ground runs for each metre it drops + // level crossings and traffic points + int placekind{0}; // what the place tool puts down: 0: level crossing, 1: spawn point, 2: removal point, 3: pedestrian crossing + roadpoint_node::state placing[4]; // what the next point of each kind is going to be like + float stopmargin{5.f}; // distance from the outermost track of a new crossing to the places the vehicles stop at + roadpoint_node::state target; // the point a click of the place tool would make at the moment + bool hastarget{false}; + glm::dvec3 targetdirection{0.0}; // the way the traffic goes there + std::string targetnote; // what's worth knowing about that point, or why there's none + glm::dvec3 aimed{0.0}; // where the cursor was when the target was worked out, and for what kind of point + int aimedkind{-1}; + char vehicles[4096]{}; // vehicles to add to the set of a spawn point, written the way they're put in a scenery + road_plan plan; // what a click would make at the moment, worked out once a frame for display + glm::dvec3 hover{0.0}; // point of a road or a junction under the cursor a road can be led out of + bool hashover{false}; + glm::dvec3 mouse{0.0}; + std::string status; + char surface[128]{}; + char sides[2][128]{}; + char filter[64]{"asph"}; + char kerbtext[128]{}; + char mediantext[128]{}; + char banktext[128]{}; + char islandtext[128]{"none"}; + }; + void render_road_tool_options(); + // the change of the road tool + void road_choose_tool(int const Tool); + void render_road_selection(); + bool render_road_layout(road_node::state &State); + bool render_road_material(char const *Label, char *Buffer, std::size_t const Size, std::string &Value); + void render_junction_layout(junction_node &Junction); + void junction_apply(junction_node &Junction, junction_node::state const &State); + // leads the banks of the corners of a junction to the ground beside it + void junction_bank_to_ground(junction_node &Junction); + // how far from specified point, and how far down, a slope of the grade set for the banks gets to the ground. + // Outwards: the way the slope leads, seen from above + std::pair bank_reach(glm::dvec3 const &Edge, glm::dvec3 const &Outwards); + void draw_road_overlay() const; + void update_road_tool(); + void road_click(); + void road_select(road_node *Road); + void junction_select(junction_node *Junction); + void road_apply(); + bool road_delete(); + bool road_split(); + void road_cancel(); + // sets the width of the road at the selected points, or the height of each of them + void road_point_width(float const Width); + void road_point_heights(std::vector const &Heights); + bool road_points_apply(std::vector> const &Changes, std::vector> const &Junctions); + // takes the selected points out of their roads + bool road_points_delete(); + // puts a roundabout around specified point + void road_roundabout(glm::dvec3 const &Ground); + // leads the banks of the selected piece, or of the whole road, to the ground beside it + void road_bank_to_ground(); + // level crossings and traffic points + void roadpoint_select(roadpoint_node *Point); + // works out the point a click of the place tool would make. Fresh: even if the cursor is where it was the last time + void roadpoint_aim(bool const Fresh); + void roadpoint_place(); + void roadpoint_apply(roadpoint_node &Point, roadpoint_node::state const &State); + // Point: the point the controls are for, nullptr for the one about to be made + bool render_roadpoint_layout(roadpoint_node::state &State, roadpoint_node const *Point); + // Fresh: the ground which wasn't looked up yet is gathered on the spot, instead of a bit of it each frame + road_plan road_preview(bool const Fresh); + // Firstgrade, Lastgrade: slopes the road has to start and end with, nullptr for none + void road_profile(std::vector &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh); + // height of the ground under each of the points, with the roads not taken for the ground. a point with nothing under it keeps its height + // Found: set for the points which have the ground under them + std::vector ground_heights(std::vector const &Points, bool const Fresh, std::vector *Found = nullptr); + void push_road_snapshot(editor_road::record Record); + void restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo); + road_tool m_roadtool; }; diff --git a/application/editormodegauge.cpp b/application/editormodegauge.cpp new file mode 100644 index 00000000..298724e7 --- /dev/null +++ b/application/editormodegauge.cpp @@ -0,0 +1,831 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGauge.hpp" +#include "editor/editorGeometry.hpp" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/editoroverlay.h" +#include "rendering/renderer.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "world/Sweep.h" +#include "world/Track.h" +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using geometry::plan_of; + +namespace +{ + +char const *const gauge_file{"editor_structure_gauges.txt"}; +// past this distance of the camera from the origin of the overlay it's rebuilt, to keep its float precision +double const gauge_origin_range{2000.0}; +// length of the line shown each way from the selected path, and around a hit +double const gauge_reach{500.0}; +double const gauge_spot_reach{60.0}; +// time spent on the scan of the whole map in a frame +auto const gauge_slice{std::chrono::milliseconds(8)}; +// distance of the camera from the place of a hit, along the track +double const gauge_view_distance{4.0}; + +// sampled line of the tracks with the widening of the gauge along it +struct gauge_line +{ + gauge::profile const *profile{nullptr}; + editor_track::route route; + std::vector samples; + std::vector sections; +}; + +// half of the carriageway of a road at the sample, it narrows linearly to the width at the end +double carriageway(editor_track::route_span const &Span, double const Chainage) +{ + auto const &track{*Span.track}; + auto const end{track.fTrackWidth2 > 0.f ? track.fTrackWidth2 : track.fTrackWidth}; + auto t{Span.to > Span.from ? std::clamp((Chainage - Span.from) / (Span.to - Span.from), 0.0, 1.0) : 0.0}; + if (false == Span.forward) + t = 1.0 - t; + return 0.5 * std::abs(track.fTrackWidth + (end - track.fTrackWidth) * t); +} + +gauge_line make_line(editor_track::route &Route, gauge::profile const &Profile) +{ + gauge_line line; + line.profile = &Profile; + line.samples = editor_track::sample_route(Route, 1.0); + line.route = Route; + bool const road{Profile.kind == gauge::kind::road}; + std::vector chainage, curvature, cant; + for (auto &sample : line.samples) + { + if (false == road) + sample.position.y += gauge::rail_height; + chainage.push_back(sample.chainage); + curvature.push_back(sample.curvature); + cant.push_back(sample.cant); + } + line.sections = gauge::sections(Profile.kind, chainage, curvature, cant); + if (road) + for (std::size_t i = 0; i < line.samples.size(); ++i) + line.sections[i].base = carriageway(line.route.spans[line.samples[i].span], line.samples[i].chainage); + return line; +} + +// outline checked along the path, none on the pavements, which are the roads with the pavement texture +gauge::profile const *outline_of(gauge::choice const &Choice, TTrack const &Track) +{ + auto const *profile{Choice.of(Track.iCategoryFlag)}; + if (profile == nullptr || profile->outline.size() < 2) + return nullptr; + if (profile == Choice.road) + { + auto name{editor_track::material_name(Track.m_material1)}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const C) { return static_cast(std::tolower(C)); }); + if (name.find("chodnik") != std::string::npos) + return nullptr; + } + return profile; +} + +// the line along the route from the path with the outline of its kind, none without the outline or the length +std::optional track_line(gauge::choice const &Choice, TTrack const &Track, editor_track::route &Route) +{ + auto const *profile{outline_of(Choice, Track)}; + if (profile == nullptr) + return std::nullopt; + auto line{make_line(Route, *profile)}; + if (line.samples.size() < 2) + return std::nullopt; + return line; +} + +// lines through all paths of the scenery, each path in one of them at least +std::vector scenery_lines(gauge::choice const &Choice) +{ + std::vector lines; + std::unordered_set visited; + auto const seen{[&](editor_track::route_span const &Span) { return visited.count(Span.track) > 0; }}; + auto const checked{[&](TTrack const *Track) { return Track != nullptr && editor_track::is_supported(*Track) && outline_of(Choice, *Track) != nullptr; }}; + for (auto *track : simulation::Paths.sequence()) + { + if (false == checked(track) || visited.count(track) > 0) + continue; + auto route{editor_track::run_route(*track, 1e7)}; + // the line may run on over the paths already covered, one of them is enough for the widening ahead of a curve + auto &spans{route.spans}; + while (spans.size() > 2 && seen(spans[0]) && seen(spans[1])) + spans.erase(spans.begin()); + while (spans.size() > 2 && seen(spans[spans.size() - 1]) && seen(spans[spans.size() - 2])) + spans.pop_back(); + for (auto const &span : spans) + visited.insert(span.track); + lines.push_back(make_line(route, *outline_of(Choice, *track))); + } + // diverging paths of the switches + for (auto *track : simulation::Paths.sequence()) + if (checked(track) && track->eType == tt_Switch) + { + editor_track::route route; + route.spans.push_back({track, 1, true}); + lines.push_back(make_line(route, *outline_of(Choice, *track))); + } + return lines; +} + +// samples of the lines, found by the position in the plan +class corridor +{ +public: + explicit corridor(std::vector const &Lines) : m_lines{Lines} + { + for (std::uint32_t line = 0; line < Lines.size(); ++line) + for (std::uint32_t i = 0; i < Lines[line].samples.size(); ++i) + { + // with the room for the widening in the sharpest curves on the largest cant + m_margin = std::max(m_margin, gauge::widest(*Lines[line].profile) + Lines[line].sections[i].base + 1.2); + auto const point{plan_of(Lines[line].samples[i].position)}; + m_cells[key(point, cell)].push_back({line, i}); + m_regions.insert(key(point, region)); + } + } + + static glm::dvec2 normal(editor_track::route_sample const &Sample) + { + return {-Sample.direction.y, Sample.direction.x}; + } + + // whether the sphere may reach the gauge of a line sampled in the cells around it + bool close(glm::dvec3 const &Center, double const Radius) const + { + auto const center{plan_of(Center)}; + auto const reach{Radius + m_margin}; + auto const from{index(center - reach, cell)}; + auto const to{index(center + reach, cell)}; + for (auto x = from.x; x <= to.x; ++x) + for (auto y = from.y; y <= to.y; ++y) + if (m_cells.count(key(x, y)) > 0) + return true; + return false; + } + + // whether the sphere may reach the gauge, roughly + bool nearby(glm::dvec3 const &Center, double const Radius) const + { + auto const center{plan_of(Center)}; + auto const reach{Radius + m_margin + region}; + auto const from{index(center - reach, region)}; + auto const to{index(center + reach, region)}; + for (auto x = from.x; x <= to.x; ++x) + for (auto y = from.y; y <= to.y; ++y) + if (m_regions.count(key(x, y)) > 0) + return true; + return false; + } + + struct hit + { + double depth{0.0}; // not positive outside of the gauge + editor_track::route_span const *span{nullptr}; // of the line the point enters + glm::dvec2 direction{0.0}; // of the line there, in the plan + }; + // deepest intrusion of the point into the gauge of the lines nearby + hit intrusion(glm::dvec3 const &Point) const + { + auto const point{plan_of(Point)}; + auto const center{index(point, cell)}; + // nearest sample of every line around + std::vector> nearest; + for (auto x = center.x - 1; x <= center.x + 1; ++x) + for (auto y = center.y - 1; y <= center.y + 1; ++y) + { + auto const found{m_cells.find(key(x, y))}; + if (found == m_cells.end()) + continue; + for (auto const &candidate : found->second) + { + auto const distance{glm::length2(plan_of(sample(candidate).position) - point)}; + if (distance > m_margin * m_margin) + continue; + auto const same{std::find_if(nearest.begin(), nearest.end(), [&](auto const &Other) { return Other.first.line == candidate.line; })}; + if (same == nearest.end()) + nearest.push_back({candidate, distance}); + else if (distance < same->second) + *same = {candidate, distance}; + } + } + hit deepest; + for (auto const &[found, distance] : nearest) + { + auto const &line{m_lines[found.line]}; + auto const &sample{line.samples[found.index]}; + auto const offset{point - plan_of(sample.position)}; + auto const along{glm::dot(offset, sample.direction)}; + if ((found.index == 0 && along < -0.6) || (found.index + 1 == line.samples.size() && along > 0.6)) + continue; + gauge::ring(*line.profile, line.sections[found.index], m_ring); + auto const depth{gauge::intrusion(m_ring, {glm::dot(offset, normal(sample)), Point.y - (sample.position.y + sample.grade * along)})}; + if (depth > deepest.depth) + deepest = {depth, &line.route.spans[sample.span], sample.direction}; + } + return deepest; + } + +private: + static constexpr double cell{8.0}; + static constexpr double region{64.0}; + + struct entry + { + std::uint32_t line; + std::uint32_t index; + }; + + static glm::i64vec2 index(glm::dvec2 const &Point, double const Size) + { + return {static_cast(std::floor(Point.x / Size)), static_cast(std::floor(Point.y / Size))}; + } + static std::uint64_t key(std::int64_t const X, std::int64_t const Y) + { + return (static_cast(X) << 32) ^ static_cast(Y); + } + static std::uint64_t key(glm::dvec2 const &Point, double const Size) + { + auto const cell{index(Point, Size)}; + return key(cell.x, cell.y); + } + editor_track::route_sample const &sample(entry const &Entry) const + { + return m_lines[Entry.line].samples[Entry.index]; + } + + std::vector const &m_lines; + double m_margin{0.0}; + mutable std::vector m_ring; // reused by the checks + std::unordered_map> m_cells; + std::unordered_set m_regions; +}; + +glm::dmat4 placement(TAnimModel const &Instance) +{ + auto const angles{Instance.Angles()}; + auto matrix{glm::translate(glm::dmat4(1.0), Instance.location())}; + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.y)), glm::dvec3(0.0, 1.0, 0.0)); + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.x)), glm::dvec3(1.0, 0.0, 0.0)); + matrix = glm::rotate(matrix, glm::radians(static_cast(angles.z)), glm::dvec3(0.0, 0.0, 1.0)); + return glm::scale(matrix, glm::dvec3(Instance.Scale())); +} + +// calls the visitor with the world corners of the visible triangles of the submodel, its siblings and children +template void visit_triangles(TSubModel *Submodel, glm::dmat4 const &Matrix, Visitor const &Visit) +{ + for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->NextGet()) + { + if (submodel->iVisible == 0) + continue; + auto matrix{Matrix}; + if ((submodel->iFlags & 0xC000) && submodel->GetMatrix() != nullptr) + matrix *= glm::dmat4(glm::make_mat4(submodel->GetMatrix()->readArray())); + if (submodel->eType == GL_TRIANGLES && submodel->m_geometry.handle != null_handle) + { + auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)}; + auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)}; + auto const corner{[&](std::size_t const Index) { return glm::dvec3(matrix * glm::dvec4(glm::dvec3(vertices[Index].position), 1.0)); }}; + auto const count{indices.empty() ? vertices.size() : indices.size()}; + for (std::size_t i = 0; i + 2 < count; i += 3) + { + if (indices.empty()) + Visit(corner(i), corner(i + 1), corner(i + 2)); + else + Visit(corner(indices[i]), corner(indices[i + 1]), corner(indices[i + 2])); + } + } + visit_triangles(submodel->ChildGet(), matrix, Visit); + } +} + +// the triangle enters the gauge when any point of a grid spread over it does; the deepest such point goes to the hit +void scan_triangle(corridor const &Space, glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, editor_mode::gauge_hit &Hit) +{ + auto const middle{(A + B + C) / 3.0}; + if (false == Space.close(middle, std::max({glm::distance(middle, A), glm::distance(middle, B), glm::distance(middle, C)}))) + return; + auto const edge{std::max({glm::distance(A, B), glm::distance(B, C), glm::distance(C, A)})}; + auto const steps{std::clamp(static_cast(std::ceil(edge / 0.5)), 1, 24)}; + for (int u = 0; u <= steps; ++u) + for (int v = 0; u + v <= steps; ++v) + { + auto const point{A + (B - A) * (static_cast(u) / steps) + (C - A) * (static_cast(v) / steps)}; + auto const found{Space.intrusion(point)}; + if (found.depth <= 0.0) + continue; + if (found.depth > Hit.depth) + { + Hit.depth = found.depth; + Hit.point = point; + Hit.track = found.span->track; // NOSONAR + Hit.path = found.span->path; + Hit.direction = found.direction; + } + return; + } +} + +// the deepest place of the model in the gauge, added to the hits when it enters it +void scan_model(corridor const &Space, TAnimModel &Instance, std::vector &Hits) +{ + if (false == Instance.visible() || Instance.Model() == nullptr || Instance.Model()->GetSMRoot() == nullptr) + return; + if (false == Space.nearby(Instance.location(), Instance.radius())) + return; + editor_mode::gauge_hit hit{Instance.name(), Instance.location()}; + visit_triangles(Instance.Model()->GetSMRoot(), placement(Instance), [&](glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C) { scan_triangle(Space, A, B, C, hit); }); + if (hit.depth > 0.0) + Hits.push_back(std::move(hit)); +} + +// the same for the copies of a model laid along a curve +void scan_sweep(corridor const &Space, sweep_node &Sweep, std::vector &Hits) +{ + if (Sweep.m_editorremoved || false == Space.nearby(Sweep.location(), Sweep.radius())) + return; + editor_mode::gauge_hit hit{Sweep.name(), Sweep.location()}; + for (auto const &shape : Sweep.create_shapes()) + { + auto const &vertices{shape.data().vertices}; + for (std::size_t i = 0; i + 2 < vertices.size(); i += 3) + scan_triangle(Space, vertices[i].position, vertices[i + 1].position, vertices[i + 2].position, hit); + } + if (hit.depth > 0.0) + Hits.push_back(std::move(hit)); +} + +void sort_hits(std::vector &Hits) +{ + std::sort(Hits.begin(), Hits.end(), [](auto const &Left, auto const &Right) { return Left.depth > Right.depth; }); +} + +// the models entering the gauge, the deepest first +void scan_models(corridor const &Space, std::vector &Hits) +{ + for (auto *instance : simulation::Instances.sequence()) + if (instance != nullptr) + scan_model(Space, *instance, Hits); + for (auto *sweep : simulation::Sweeps.sequence()) + if (sweep != nullptr) + scan_sweep(Space, *sweep, Hits); + sort_hits(Hits); +} + +// translucent tunnel of the gauge along the line, with the rings every 10 m and the lines along its corners +void build_tunnel(gauge_line const &Line, std::vector &Surface, std::vector &Edges) +{ + std::vector places; + auto const ring{[&](std::size_t const Index) { + gauge::ring(*Line.profile, Line.sections[Index], places); + auto const &sample{Line.samples[Index]}; + auto const normal{corridor::normal(sample)}; + std::vector points; + for (auto const &place : places) + points.push_back(sample.position + glm::dvec3(normal.x * place.lateral, place.height, normal.y * place.lateral)); + return points; + }}; + std::vector previous; + double lastring{-1e9}; + for (std::size_t i = 0; i < Line.samples.size(); i += 2) + { + auto const current{ring(i)}; + if (false == previous.empty()) + for (std::size_t j = 1; j < current.size(); ++j) + { + Surface.insert(Surface.end(), {previous[j - 1], previous[j], current[j], previous[j - 1], current[j], current[j - 1]}); + Edges.insert(Edges.end(), {previous[j], current[j]}); + } + if (Line.samples[i].chainage - lastring >= 10.0 || i + 2 >= Line.samples.size()) + { + lastring = Line.samples[i].chainage; + for (std::size_t j = 1; j < current.size(); ++j) + Edges.insert(Edges.end(), {current[j - 1], current[j]}); + } + previous = current; + } +} + +// table of the points of the outline with the inserting and removing of the points, whether it changed +bool edit_points(std::vector &Points) +{ + bool edited{false}; + bool inserted{false}; + int remove{-1}; + ImGui::Columns(3, STR_C("gaugeoutline"), false); + ImGui::SetColumnWidth(0, 110.0f); + ImGui::SetColumnWidth(1, 110.0f); + ImGui::TextDisabled(STR_C("Half-width")); + ImGui::NextColumn(); + ImGui::TextDisabled(STR_C("Height")); + ImGui::NextColumn(); + ImGui::NextColumn(); + for (int i = 0; i < static_cast(Points.size()); ++i) + { + ImGui::PushID(i); + auto &point{Points[i]}; + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputDouble("##width", &point.half_width, 0.0, 0.0, "%.3f"); + edited |= ImGui::IsItemDeactivatedAfterEdit(); + ImGui::NextColumn(); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputDouble("##height", &point.height, 0.0, 0.0, "%.3f"); + edited |= ImGui::IsItemDeactivatedAfterEdit(); + ImGui::NextColumn(); + if (ImGui::SmallButton("+")) + { + auto const &next{Points[std::min(i + 1, Points.size() - 1)]}; + Points.insert(Points.begin() + i + 1, {0.5 * (point.half_width + next.half_width), 0.5 * (point.height + next.height)}); + inserted = edited = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Insert a point after this one")); + ImGui::SameLine(); + if (Points.size() > 2 && ImGui::SmallButton("x")) + remove = i; + ImGui::NextColumn(); + ImGui::PopID(); + if (inserted) + break; + } + ImGui::Columns(1); + if (remove >= 0) + { + Points.erase(Points.begin() + remove); + edited = true; + } + return edited; +} + +} // namespace + +struct gauge_scan +{ + std::optional railway; + std::optional road; + std::vector lines; + std::optional space; // of the lines + std::size_t next{0}; // instance to check + float progress{0.f}; + std::chrono::steady_clock::time_point started; +}; + +void editor_mode::gauge_load() +{ + if (m_gauge.profiles.empty()) + m_gauge.profiles = gauge::load_profiles(gauge_file); + // the chosen railway and road outlines, the first ones of the kind when there's none + auto const fits{[&](int const Index, bool const Road) { + return Index >= 0 && Index < static_cast(m_gauge.profiles.size()) && (m_gauge.profiles[Index].kind == gauge::kind::road) == Road; + }}; + for (auto [index, road] : {std::pair{&m_gauge.profile, false}, std::pair{&m_gauge.road, true}}) + if (false == fits(*index, road)) + { + *index = -1; + for (int i = 0; i < static_cast(m_gauge.profiles.size()) && *index < 0; ++i) + if (fits(i, road)) + *index = i; + } +} + +gauge::choice editor_mode::gauge_choice() const +{ + auto const pick{[&](int const Index) { return Index >= 0 ? &m_gauge.profiles[Index] : nullptr; }}; + return {pick(m_gauge.profile), pick(m_gauge.road)}; +} + +std::vector const &editor_mode::gauge_hits() const +{ + return m_gauge.map.scanned ? m_gauge.map.hits : m_gauge.line.hits; +} + +void editor_mode::update_gauge() +{ + if (m_gauge.scan) + step_gauge_map(); + if (false == m_gauge.open) + { + if (false == EditorOverlay.batches.empty()) + EditorOverlay.clear(); + m_gauge.published = false; + return; + } + auto *track{m_gauge.enabled ? selected_track() : nullptr}; + // the geometry is rescanned once the edit ends + if (ImGui::IsMouseDown(0) || m_track_gizmo_using || m_dragging) + m_gauge.pending = true; + else if (track == nullptr) + { + if (m_gauge.track != nullptr) + { + m_gauge.line = {}; + m_gauge.track = nullptr; + m_gauge.published = false; + } + } + else if (track != m_gauge.track || m_history.size() != m_gauge.history || m_gauge.pending) + { + m_gauge.track = track; + m_gauge.history = m_history.size(); + m_gauge.pending = false; + scan_gauge(*track); + m_gauge.published = false; + } + if (false == m_gauge.published || glm::distance(Camera.Pos, EditorOverlay.origin) > gauge_origin_range) + gauge_publish(); +} + +void editor_mode::scan_gauge(TTrack &Track) +{ + gauge_load(); + m_gauge.line = {}; + auto route{editor_track::run_route(Track, gauge_reach)}; + auto line{track_line(gauge_choice(), Track, route)}; + if (false == line.has_value()) + return; + std::vector const lines{std::move(*line)}; + corridor const space{lines}; + build_tunnel(lines.front(), m_gauge.line.surface, m_gauge.line.edges); + scan_models(space, m_gauge.line.hits); + if (false == m_gauge.map.scanned) + m_gauge.current = -1; +} + +void editor_mode::scan_gauge_map() +{ + gauge_load(); + auto scan{std::make_shared()}; + scan->started = std::chrono::steady_clock::now(); + auto const chosen{gauge_choice()}; + // copies, the outlines may be edited while it runs + scan->railway = chosen.railway != nullptr ? std::optional{*chosen.railway} : std::nullopt; + scan->road = chosen.road != nullptr ? std::optional{*chosen.road} : std::nullopt; + scan->lines = scenery_lines({scan->railway ? &*scan->railway : nullptr, scan->road ? &*scan->road : nullptr}); + scan->space.emplace(scan->lines); + m_gauge.map = {}; + m_gauge.map.scanned = true; + m_gauge.map.history = m_history.size(); + auto const name{[](gauge::profile const *Profile) { return Profile != nullptr ? Profile->name : std::string{"-"}; }}; + m_gauge.map.profile = name(chosen.railway) + ", " + name(chosen.road); + m_gauge.current = -1; + m_gauge.published = false; + m_gauge.scan = std::move(scan); +} + +void editor_mode::step_gauge_map() +{ + auto &scan{*m_gauge.scan}; + auto const &instances{simulation::Instances.sequence()}; + auto const &sweeps{simulation::Sweeps.sequence()}; + auto const total{instances.size() + sweeps.size()}; + auto const until{std::chrono::steady_clock::now() + gauge_slice}; + while (scan.next < total && std::chrono::steady_clock::now() < until) + { + auto const next{scan.next++}; + if (next < instances.size()) + { + if (auto *instance{instances[next]}) + scan_model(*scan.space, *instance, m_gauge.map.hits); + } + else if (auto *sweep{sweeps[next - instances.size()]}) + scan_sweep(*scan.space, *sweep, m_gauge.map.hits); + } + scan.progress = total == 0 ? 1.f : static_cast(scan.next) / total; + if (scan.next >= total) + finish_gauge_map(); +} + +void editor_mode::finish_gauge_map() +{ + auto const scan{std::move(m_gauge.scan)}; + auto &hits{m_gauge.map.hits}; + // the hit the camera went to stays chosen + auto const chosen{m_gauge.current >= 0 && m_gauge.current < static_cast(hits.size()) ? std::optional{hits[m_gauge.current].point} : std::nullopt}; + sort_hits(hits); + if (chosen) + m_gauge.current = static_cast(std::find_if(hits.begin(), hits.end(), [&](auto const &Hit) { return Hit.point == *chosen; }) - hits.begin()); + WriteLog("Structure gauge (" + m_gauge.map.profile + "): " + std::to_string(hits.size()) + " models enter it along " + std::to_string(scan->lines.size()) + " lines, scanned in " + + std::to_string(std::chrono::duration_cast(std::chrono::steady_clock::now() - scan->started).count()) + " ms"); + if (false == chosen.has_value() && false == hits.empty()) + gauge_focus(0); +} + +void editor_mode::gauge_publish() +{ + m_gauge.published = true; + EditorOverlay.clear(); + EditorOverlay.origin = Camera.Pos; + auto const batch{[](glm::vec4 const &Color, unsigned int const Type, std::vector const &Points) { + gfx::editor_overlay::batch result{Color, Type}; + result.points.reserve(Points.size()); + for (auto const &point : Points) + result.points.emplace_back(point - EditorOverlay.origin); + return result; + }}; + std::vector marker; + auto const &hits{gauge_hits()}; + if (m_gauge.current >= 0 && m_gauge.current < static_cast(hits.size())) + { + auto const point{hits[m_gauge.current].point}; + marker = {point - glm::dvec3(0.0, 3.0, 0.0), point + glm::dvec3(0.0, 3.0, 0.0), point - glm::dvec3(1.0, 0.0, 0.0), point + glm::dvec3(1.0, 0.0, 0.0), + point - glm::dvec3(0.0, 0.0, 1.0), point + glm::dvec3(0.0, 0.0, 1.0)}; + } + EditorOverlay.batches = {batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, m_gauge.line.surface), + batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, m_gauge.line.edges), + batch({1.0f, 0.9f, 0.1f, 1.0f}, GL_LINES, marker), + batch({0.2f, 0.9f, 0.3f, 0.12f}, GL_TRIANGLES, m_gauge.spot.surface), + batch({0.2f, 1.0f, 0.3f, 0.5f}, GL_LINES, m_gauge.spot.edges)}; +} + +void editor_mode::gauge_focus(int const Index) +{ + auto const &hits{gauge_hits()}; + if (hits.empty()) + return; + m_gauge.current = (Index % static_cast(hits.size()) + static_cast(hits.size())) % static_cast(hits.size()); + m_gauge.published = false; + gauge_spot(hits[m_gauge.current]); + auto const &hit{hits[m_gauge.current]}; + // along the track, tangent to the gauge at the height of the place, from the end nearer to the camera + glm::dvec3 look{hit.direction.x, 0.0, hit.direction.y}; + look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); + if (glm::dot(Camera.Pos - hit.point, look) > 0.0) + look = -look; + m_focus_start_pos = Camera.Pos; + m_focus_start_angle = Camera.Angle; + m_focus_target_pos = hit.point - look * gauge_view_distance; + m_focus_target_angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + m_focus_active = true; + m_focus_time = 0.0; + m_focus_duration = 0.6; +} + +void editor_mode::gauge_spot(gauge_hit const &Hit) +{ + m_gauge.spot = {}; + auto const &paths{simulation::Paths.sequence()}; + // the hit outlives the scan, the path may be gone since + if (Hit.track == nullptr || std::find(paths.begin(), paths.end(), Hit.track) == paths.end()) + return; + auto *track{const_cast(Hit.track)}; + editor_track::route route; + if (Hit.path == 0) + route = editor_track::run_route(*track, gauge_spot_reach); + else + route.spans.push_back({track, Hit.path, true}); + if (auto const line{track_line(gauge_choice(), *track, route)}) + build_tunnel(*line, m_gauge.spot.surface, m_gauge.spot.edges); +} + +void editor_mode::render_gauge_window() +{ + if (false == m_gauge.open) + return; + ImGui::SetNextWindowSize(ImVec2(380.0f, 420.0f), ImGuiCond_FirstUseEver); + if (false == ImGui::Begin((std::string(STR_C("Structure gauge")) + "###structuregauge").c_str(), &m_gauge.open)) + { + ImGui::End(); + return; + } + if (ImGui::Checkbox(STR_C("Show along the selected path"), &m_gauge.enabled)) + m_gauge.pending = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Translucent tunnel of the gauge 500 m each way along the line of the selected path.\nWidened in the curves under 250 m and tilted on the cant, from 20 m (inner side)\nand 26 m (outer side) ahead of the curve, as in the PKP PLK standard, volume II.")); + gauge_load(); + bool changed{render_gauge_choice("Railway", m_gauge.profile, false)}; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("GPL: unified structure gauge, widened only in the curves under 250 m.\nG1, G2, GA, GB, GC: limit (GSZ) and nominal (NSZ) installation gauges, widened by 3750/R in all curves.")); + changed |= render_gauge_choice("Road", m_gauge.road, true); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Clearance over the roads, from 0.5 m beyond the edges of the carriageway")); + + if (m_gauge.scan) + { + auto const label{format("Scanning the map %.0f%%, %zu found", m_gauge.scan->progress * 100.f, m_gauge.map.hits.size())}; + ImGui::ProgressBar(m_gauge.scan->progress, ImVec2(-70.0f, 0.0f), label.c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Cancel"))) + finish_gauge_map(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Stops the scan and keeps the models found so far")); + } + else if (ImGui::Button(STR_C("Find the violations on the whole map"))) + scan_gauge_map(); + else if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first.\nRuns in the background, the list fills up as it goes")); + if (m_gauge.map.scanned && false == static_cast(m_gauge.scan)) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Clear"))) + { + m_gauge.map = {}; + m_gauge.current = -1; + m_gauge.published = false; + } + else if (m_gauge.map.history != m_history.size()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the scan (%s)"), m_gauge.map.profile.c_str()); + } + render_gauge_hits(); + changed |= render_gauge_outline(m_gauge.profile); + changed |= render_gauge_outline(m_gauge.road); + if (changed) + m_gauge.pending = true; + ImGui::End(); +} + +void editor_mode::render_gauge_hits() +{ + auto const &hits{gauge_hits()}; + if (false == m_gauge.map.scanned && (false == m_gauge.enabled || m_gauge.track == nullptr)) + return; + if (hits.empty()) + { + ImGui::TextDisabled(m_gauge.map.scanned ? STR_C("No model enters the gauge on the map") : STR_C("The gauge is clear along this line")); + return; + } + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), STR_C("%zu models enter the gauge%s"), hits.size(), m_gauge.map.scanned ? STR_C(" on the map") : ""); + if (ImGui::ArrowButton("##gaugeprevious", ImGuiDir_Left)) + gauge_focus(m_gauge.current - 1); + ImGui::SameLine(); + if (ImGui::ArrowButton("##gaugenext", ImGuiDir_Right)) + gauge_focus(m_gauge.current + 1); + ImGui::SameLine(); + ImGui::Text("%d / %zu", m_gauge.current + 1, hits.size()); + ImGui::BeginChild("gaugehits", ImVec2(0.0f, std::min(160.0f, ImGui::GetTextLineHeightWithSpacing() * hits.size() + 8.0f)), true); + ImGuiListClipper clipper; + clipper.Begin(static_cast(hits.size())); + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) + { + auto const &hit{hits[i]}; + auto const label{(hit.model.empty() ? std::string{"(unnamed)"} : hit.model) + " " + std::to_string(static_cast(std::lround(hit.depth * 100.0))) + " cm##" + std::to_string(i)}; + if (ImGui::Selectable(label.c_str(), m_gauge.current == i)) + gauge_focus(i); + } + ImGui::EndChild(); +} + +bool editor_mode::render_gauge_choice(char const *Label, int &Index, bool const Road) +{ + bool changed{false}; + ImGui::SetNextItemWidth(-60.0f); + if (ImGui::BeginCombo(Label, Index >= 0 ? m_gauge.profiles[Index].name.c_str() : STR_C("none"))) + { + for (int i = 0; i < static_cast(m_gauge.profiles.size()); ++i) + if ((m_gauge.profiles[i].kind == gauge::kind::road) == Road && ImGui::Selectable(m_gauge.profiles[i].name.c_str(), i == Index)) + { + changed = i != Index; + Index = i; + } + ImGui::EndCombo(); + } + return changed; +} + +bool editor_mode::render_gauge_outline(int const Index) +{ + if (Index < 0) + return false; + auto &profile{m_gauge.profiles[Index]}; + if (false == ImGui::TreeNode(&profile, STR_C("Outline of %s (right half)"), profile.name.c_str())) + return false; + ImGui::TextDisabled(profile.kind == gauge::kind::road ? STR_C("Metres from the edge of the carriageway and above the road, bottom up.") + : "PKP PLK standard, volume II; below 1170 mm the limit installation gauge.\nMetres from the track axis and above the rail top, bottom up."); + bool edited{edit_points(profile.outline)}; + if (ImGui::SmallButton(STR_C("Restore the starting outlines"))) + { + m_gauge.profiles = gauge::default_profiles(); + gauge_load(); + edited = true; + } + if (edited) + gauge::save_profiles(gauge_file, m_gauge.profiles); + ImGui::TreePop(); + return edited; +} diff --git a/application/editormodeinfra.cpp b/application/editormodeinfra.cpp new file mode 100644 index 00000000..e805c545 --- /dev/null +++ b/application/editormodeinfra.cpp @@ -0,0 +1,1710 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "editor/editorIncludeInfo.hpp" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "world/Event.h" +#include "world/EvLaunch.h" +#include "world/MemCell.h" +#include "world/Track.h" +#include "world/Traction.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_distance; +using geometry::plan_of; + +std::string const kBindingMark{"//$b"}; + +template +bool contains(std::vector const &Items, Type_ const &Item) +{ + return std::find(Items.begin(), Items.end(), Item) != Items.end(); +} + +ImU32 colour(infra::category const Category, int const Alpha = 255) +{ + switch (Category) + { + case infra::category::signal: return IM_COL32(255, 80, 80, Alpha); + case infra::category::sign: return IM_COL32(255, 220, 60, Alpha); + case infra::category::pole: return IM_COL32(160, 160, 255, Alpha); + case infra::category::catenary: return IM_COL32(80, 200, 255, Alpha); + case infra::category::logic: return IM_COL32(220, 120, 255, Alpha); + case infra::category::other: return IM_COL32(200, 200, 200, Alpha); + } + return IM_COL32(200, 200, 200, Alpha); +} + +// nodes still present in the scene; the editor deletes models outright +class live_nodes +{ + public: + void refresh() + { + std::array const sizes{simulation::Instances.sequence().size(), simulation::Traction.sequence().size(), simulation::Memory.sequence().size(), simulation::Events.launchers().size()}; + if (sizes == m_sizes && m_valid) + return; + m_sizes = sizes; + m_valid = true; + m_nodes.clear(); + for (auto *node : simulation::Instances.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Traction.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Memory.sequence()) + m_nodes.insert(node); + for (auto *node : simulation::Events.launchers()) + m_nodes.insert(node); + } + bool alive(scene::basic_node const *Node) const + { + return Node != nullptr && m_nodes.count(Node) > 0; + } + + private: + std::unordered_set m_nodes; + std::array m_sizes{}; + bool m_valid{false}; +}; +live_nodes Live; + +bool alive(infra::binding const &Binding) +{ + if (Binding.type == infra::kind::include) + { + if (false == scene::Layers.tracked(Binding.include)) + return false; + auto const &included{scene::Layers.instance(Binding.include)}; + return false == included.removed && false == included.dead; + } + return Live.alive(Binding.node); +} + +// placement parameters of an include +struct include_place +{ + std::string file; + std::vector values; + std::array position{-1, -1, -1}; // indices of the values + int yaw{-1}; + glm::dvec3 offset{0.0}; + std::string description; // name and category of the template +}; + +bool numeric(std::string const &Text, double &Value) +{ + char *end{nullptr}; + Value = std::strtod(Text.c_str(), &end); + return false == Text.empty() && *end == '\0'; +} + +bool include_place_of(scene::instance_handle const Instance, include_place &Place) +{ + if (false == scene::Layers.tracked(Instance)) + return false; + auto const &included{scene::Layers.instance(Instance)}; + if (included.removed || included.dead || included.file == nullptr) + return false; + // with a rotation or a scale in effect the parameters don't translate to a location in a single way + if (included.context.rotation != glm::vec3{0.f} || included.context.scale != glm::vec3{1.f}) + return false; + if (false == editor_includes::parse_directive(scene::Layers.directive(Instance), Place.file, Place.values)) + return false; + static std::map descriptions; + auto found{descriptions.find(*included.file)}; + if (found == descriptions.end()) + { + include_info info; + std::string error; + editor_includes::load(*included.file, info, error); + editor_includes::suggest(*included.file, info); + found = descriptions.emplace(*included.file, std::move(info)).first; + } + auto const &info{found->second}; + auto const index = [&](char const *Role) { + auto const result{editor_includes::parameter_with_role(info, Role) - 1}; + double value; + return (result >= 0 && result < static_cast(Place.values.size()) && numeric(Place.values[result], value)) ? result : -1; + }; + Place.position = {index("pos.x"), index("pos.y"), index("pos.z")}; + Place.yaw = index("rot.y"); + Place.offset = included.context.offset; + Place.description = info.name + ' ' + info.category; + return Place.position[0] >= 0 && Place.position[1] >= 0 && Place.position[2] >= 0; +} + +glm::dvec3 include_location(include_place const &Place) +{ + glm::dvec3 result{Place.offset}; + for (int axis = 0; axis < 3; ++axis) + { + double value{0.0}; + numeric(Place.values[Place.position[axis]], value); + result[axis] += value; + } + return result; +} + +// points which define the object, and its rotation around the vertical axis if it has one +bool object_points(infra::binding const &Binding, std::vector &Points, std::optional &Yaw) +{ + Points.clear(); + Yaw.reset(); + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + Points.push_back(model->location()); + Yaw = model->Angles().y; + return true; + } + case infra::kind::include: + { + include_place place; + if (false == include_place_of(Binding.include, place)) + return false; + Points.push_back(include_location(place)); + if (place.yaw >= 0) + { + double value{0.0}; + numeric(place.values[place.yaw], value); + Yaw = value; + } + return true; + } + case infra::kind::traction: + { + auto const *traction{static_cast(Binding.node)}; + Points = {traction->pPoint1, traction->pPoint2, traction->pPoint3, traction->pPoint4}; + return true; + } + case infra::kind::memcell: + case infra::kind::launcher: Points.push_back(Binding.node->location()); return true; + } + return false; +} + +std::string object_key(infra::binding const &Binding) +{ + std::string result; + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + result = model->Model() != nullptr ? model->Model()->NameGet() : std::string{}; + break; + } + case infra::kind::include: result = *scene::Layers.instance(Binding.include).file; break; + case infra::kind::traction: break; + case infra::kind::memcell: + case infra::kind::launcher: result = Binding.node->name(); break; + } + if (result.empty() || result.find_first_of(" \t\r\n") != std::string::npos) + result = "-"; + return result; +} + +std::string object_label(infra::binding const &Binding) +{ + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + auto name{model->Model() != nullptr ? model->Model()->NameGet() : std::string{}}; + if (false == model->name().empty() && model->name() != "none") + name = model->name() + " (" + name + ')'; + return name; + } + case infra::kind::include: return *scene::Layers.instance(Binding.include).file; + case infra::kind::traction: return Binding.node->name().empty() || Binding.node->name() == "none" ? std::string{"traction"} : Binding.node->name(); + case infra::kind::memcell: return "memcell " + Binding.node->name(); + case infra::kind::launcher: return "eventlauncher " + Binding.node->name(); + } + return {}; +} + +void move_model(TAnimModel *Model, glm::dvec3 const &Location, glm::vec3 const &Angles) +{ + if (glm::distance(Model->location(), Location) > 1e-6) + { + simulation::Region->erase(Model); + Model->location(Location); + simulation::Region->insert(Model); + } + Model->Angles(Angles); +} + +void move_traction(TTraction *Traction, std::array const &Points) +{ + if (Traction->pPoint1 == Points[0] && Traction->pPoint2 == Points[1] && Traction->pPoint3 == Points[2] && Traction->pPoint4 == Points[3]) + return; + // the height of the lowest dropper stays the same + auto const minheight{(Traction->pPoint3.y - Traction->pPoint1.y + Traction->pPoint4.y - Traction->pPoint2.y) * 0.5 - Traction->fHeightDifference}; + simulation::Region->erase_and_unregister(Traction); + Traction->pPoint1 = Points[0]; + Traction->pPoint2 = Points[1]; + Traction->pPoint3 = Points[2]; + Traction->pPoint4 = Points[3]; + Traction->fHeightDifference = (Points[2].y - Points[0].y + Points[3].y - Points[1].y) * 0.5 - minheight; + Traction->Init(); + Traction->location(glm::mix(Points[1], Points[0], 0.5)); + Traction->m_area.radius = -1.f; + simulation::Region->insert_and_register(Traction); + Traction->m_geometry = gfx::geometry_handle{}; + auto §ion{simulation::Region->section(Traction->location())}; + if (section.m_geometrycreated) + Traction->create_geometry(section.m_geometrybank); + Traction->mark_dirty(); +} + +void move_memcell(TMemCell *Memcell, glm::dvec3 const &Location) +{ + if (glm::distance(Memcell->location(), Location) <= 1e-6) + return; + simulation::Region->erase(Memcell); + Memcell->location(Location); + simulation::Region->insert(Memcell); +} + +void move_launcher(TEventLauncher *Launcher, glm::dvec3 const &Location) +{ + if (glm::distance(Launcher->location(), Location) <= 1e-6) + return; + // global and radio launchers aren't a part of the region + auto const placed{false == Launcher->IsGlobal() && false == Launcher->IsRadioActivated()}; + if (placed) + simulation::Region->erase(Launcher); + Launcher->location(Location); + if (placed) + simulation::Region->insert(Launcher); + Launcher->mark_dirty(); +} + +// bounding circle of the paths of a track, in the plan +struct track_area +{ + TTrack *track{nullptr}; + glm::dvec3 centre{0.0}; + double radius{0.0}; +}; + +track_area area_of(TTrack &Track) +{ + track_area result; + result.track = &Track; + std::vector points; + for (auto const &path : Track.m_paths) + { + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + points.insert(points.end(), {start, end, start + path.points[segment_data::point::control1], end + path.points[segment_data::point::control2]}); + } + if (points.empty()) + return result; + for (auto const &point : points) + result.centre += point / static_cast(points.size()); + for (auto const &point : points) + result.radius = std::max(result.radius, plan_distance(point, result.centre)); + return result; +} + +// the point lies sideways of the path, not beyond one of its ends +bool beside(infra::station const &Station, glm::dvec3 const &Point, bool const Interior) +{ + if (Interior) + return true; + auto const axis{infra::frame_at(Station)}; + return std::abs(glm::dot(plan_of(Point - axis.point), axis.forward)) <= 0.5; +} + +// nearest point of the tracks, sideways from the axis within the corridor +template +bool nearest_beside(Areas_ const &Areas, glm::dvec3 const &Point, double const Corridor, infra::station &Station) +{ + auto best{-1.0}; + for (auto const &entry : Areas) + { + track_area const &area = entry; + if (plan_distance(Point, area.centre) > area.radius + Corridor) + continue; + infra::station station; + bool interior; + auto const distance{infra::project(*area.track, Point, station, interior)}; + if (distance <= Corridor && (best < 0.0 || distance < best) && beside(station, Point, interior)) + { + best = distance; + Station = station; + } + } + return best >= 0.0; +} + +// paths of the whole scenery by cells of the plan, for the lookups reaching up to kReach from the axis +class track_index +{ + public: + static constexpr double kReach{55.0}; + void add(TTrack &Track) + { + auto const index{m_areas.size()}; + m_areas.push_back(area_of(Track)); + std::set cells; + for (auto const &path : Track.m_paths) + for (auto const &point : infra::outline(path, kCell * 0.5)) + for (auto x = key(point.x - kReach - kCell * 0.25); x <= key(point.x + kReach + kCell * 0.25); ++x) + for (auto z = key(point.z - kReach - kCell * 0.25); z <= key(point.z + kReach + kCell * 0.25); ++z) + cells.insert({x, z}); + for (auto const &cell : cells) + m_cells[cell].push_back(index); + } + bool nearest(glm::dvec3 const &Point, double const Corridor, infra::station &Station) const + { + auto const cell{m_cells.find({key(Point.x), key(Point.z)})}; + if (cell == m_cells.end()) + return false; + std::vector> areas; + for (auto const index : cell->second) + areas.emplace_back(m_areas[index]); + return nearest_beside(areas, Point, std::min(Corridor, kReach), Station); + } + bool empty() const + { + return m_areas.empty(); + } + + private: + static constexpr double kCell{64.0}; + using cell_key = std::pair; + static long long key(double const Value) + { + return static_cast(std::floor(Value / kCell)); + } + std::vector m_areas; + std::map> m_cells; +}; + +struct cell_reader +{ + TTrack *track{nullptr}; + std::string event; + std::string cell; + bool loose{false}; +}; + +class event_links +{ + public: + event_links() + { + std::unordered_map named; + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && false == track->name().empty() && track->name() != "none") + named.emplace(track->name(), track); + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (auto const *sequence : {&track->m_events0, &track->m_events1, &track->m_events2, &track->m_events0all, &track->m_events1all, &track->m_events2all}) + for (auto const &entry : *sequence) + read(track, entry.second != nullptr ? entry.second : simulation::Events.FindEvent(entry.first), entry.first.empty()); + } + static std::set const kinds{"event0", "event1", "event2", "eventall0", "eventall1", "eventall2"}; + std::unordered_set models(simulation::Instances.sequence().begin(), simulation::Instances.sequence().end()); + std::unordered_set cells(simulation::Memory.sequence().begin(), simulation::Memory.sequence().end()); + for (auto *event : simulation::Events.sequence()) + { + if (event == nullptr) + continue; + auto const &name{event->name()}; + auto const colon{name.rfind(':')}; + if (colon != std::string::npos && colon > 0 && kinds.count(name.substr(colon + 1)) > 0) + { + auto const track{named.find(name.substr(0, colon))}; + if (track != named.end()) + read(track->second, event, true); + } + auto const *multi{dynamic_cast(event)}; + if (multi == nullptr) + continue; + std::vector shown; + std::vector set; + for (auto *child : multi->children()) + { + auto const setter{dynamic_cast(child) != nullptr || dynamic_cast(child) != nullptr}; + for (auto *target : child->target_nodes()) // NOSONAR + { + if (models.count(target) > 0 && false == contains(shown, static_cast(target))) + shown.push_back(target); + else if (setter && cells.count(target) > 0 && false == contains(set, static_cast(target))) + set.push_back(static_cast(target)); + } + } + if (shown.size() > 4 || set.size() > 4) + continue; + for (auto const *model : shown) + for (auto *cell : set) + if (plan_distance(model->location(), cell->location()) <= kCellReach && false == contains(m_cells[model], cell)) + m_cells[model].push_back(cell); + } + for (auto *cell : simulation::Memory.sequence()) + { + if (cell == nullptr) + continue; + if (cell->m_instance != 0) + m_included[cell->m_instance].push_back(cell); + if (false == cell->name().empty() && cell->name() != "none") + m_named.emplace_back(cell->name(), cell); + } + std::sort(m_named.begin(), m_named.end(), [](auto const &A, auto const &B) { return A.first < B.first; }); + } + std::vector cells_of(infra::binding const &Binding) const + { + std::vector result; + switch (Binding.type) + { + case infra::kind::memcell: result.push_back(static_cast(Binding.node)); break; + case infra::kind::include: + { + auto const found{m_included.find(Binding.include)}; + if (found != m_included.end()) + result = found->second; + break; + } + case infra::kind::model: + { + auto const found{m_cells.find(Binding.node)}; + if (found != m_cells.end()) + result = found->second; + auto const &name{Binding.node->name()}; + if (result.empty() && name.size() >= 3 && name != "none") + for (auto entry{std::lower_bound(m_named.begin(), m_named.end(), name, [](auto const &Entry, std::string const &Name) { return Entry.first < Name; })}; + entry != m_named.end() && entry->first.compare(0, name.size(), name) == 0; ++entry) + if (plan_distance(entry->second->location(), Binding.node->location()) <= kCellReach) + result.push_back(entry->second); + break; + } + default: break; + } + return result; + } + std::vector readers_of(std::vector const &Cells) const + { + std::vector result; + for (auto const *cell : Cells) + { + auto const found{m_readers.find(cell)}; + if (found == m_readers.end()) + continue; + for (auto const &reader : found->second) + if (std::none_of(result.begin(), result.end(), [&](cell_reader const &Reader) { return Reader.track == reader.track; })) + result.push_back(reader); + } + return result; + } + + private: + static constexpr double kCellReach{60.0}; + void read(TTrack *Track, basic_event *Event, bool const Loose) + { + std::vector pending{Event}; + std::unordered_set seen; + while (false == pending.empty()) + { + auto *event{pending.back()}; + pending.pop_back(); + if (event == nullptr || false == seen.insert(event).second) + continue; + if (auto const *multi{dynamic_cast(event)}) + { + auto const children{multi->children()}; + pending.insert(pending.end(), children.begin(), children.end()); + continue; + } + if (dynamic_cast(event) == nullptr) + continue; + auto const targets{event->target_nodes()}; + if (targets.empty()) + continue; + auto &list{m_readers[targets.front()]}; + if (std::none_of(list.begin(), list.end(), [&](cell_reader const &Reader) { return Reader.track == Track; })) + list.push_back({Track, Event->name(), targets.front()->name(), Loose}); + } + } + std::unordered_map> m_readers; + std::unordered_map> m_cells; + std::unordered_map> m_included; + std::vector> m_named; +}; + +// lookup of points by a grid of 1 m cells in the plan +template +class point_grid +{ + public: + void add(glm::dvec3 const &Point, Type_ Item) + { + m_cells[key(Point)].emplace_back(Point, Item); + } + template + std::optional find(glm::dvec3 const &Point, double const Tolerance, Match_ const &Match) const + { + auto const centre{key(Point)}; + for (auto x = centre.first - 1; x <= centre.first + 1; ++x) + for (auto z = centre.second - 1; z <= centre.second + 1; ++z) + { + auto const cell{m_cells.find({x, z})}; + if (cell == m_cells.end()) + continue; + for (auto const &entry : cell->second) + if (glm::distance(entry.first, Point) <= Tolerance && Match(entry.second)) + return entry.second; + } + return std::nullopt; + } + + private: + using cell_key = std::pair; + static cell_key key(glm::dvec3 const &Point) + { + return {static_cast(std::floor(Point.x)), static_cast(std::floor(Point.z))}; + } + std::map>> m_cells; +}; + +double turn_of(infra::binding const &Binding, double const Yaw) +{ + auto const axis{infra::frame_at(Binding.anchors.front().at)}; + return clamp_circular(Yaw - infra::heading(axis.forward)); +} + +// the objects which the stored bindings can name, by their locations +struct bindable_objects +{ + point_grid paths; + point_grid models; + point_grid traction; + point_grid logic; + point_grid includes; + + bindable_objects() + { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved) + for (auto const &path : track->m_paths) + paths.add(path.points[segment_data::point::start], track); + for (auto *model : simulation::Instances.sequence()) + if (model != nullptr && false == model->from_template() && model->Model() != nullptr) + models.add(model->location(), model); + for (auto *piece : simulation::Traction.sequence()) + if (piece != nullptr && false == piece->from_template()) + traction.add(piece->pPoint1, piece); + for (auto *memcell : simulation::Memory.sequence()) + if (memcell != nullptr && false == memcell->from_template()) + logic.add(memcell->location(), memcell); + for (auto *launcher : simulation::Events.launchers()) + if (launcher != nullptr && false == launcher->from_template()) + logic.add(launcher->location(), launcher); + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (include_place_of(handle, place)) + includes.add(include_location(place), handle); + } + } +}; + +// format of the lines, coordinates in the space of the file: +// { } +// the object is the model, the include (position from its parameters), the traction piece (its first point), the memory +// cell or the event launcher found at the location; the anchors name the paths by their ends +std::optional read_binding(std::string const &Text, scene::layer_context const &Context, bindable_objects const &Objects) +{ + double const tolerance{0.05}; + std::istringstream line{Text}; + std::string kindname, key; + int group{0}, turns{0}, count{0}; + glm::dvec3 location; + line >> kindname >> group >> location.x >> location.y >> location.z >> key >> turns >> count; + if (line.fail() || count < 1 || count > 4) + return std::nullopt; + location = Context.to_world(location); + infra::binding binding; + binding.group = static_cast(std::clamp(group, 0, static_cast(infra::category::other))); + binding.turns = (turns != 0); + auto found{false}; + if (kindname == infra::name(infra::kind::model)) + { + binding.type = infra::kind::model; + auto const model{Objects.models.find(location, tolerance, [&](TAnimModel *Model) { return Model->Model()->NameGet() == key; })}; + if ((found = model.has_value())) + binding.node = *model; + } + else if (kindname == infra::name(infra::kind::include)) + { + binding.type = infra::kind::include; + auto const handle{Objects.includes.find(location, tolerance, [&](scene::instance_handle Handle) { return *scene::Layers.instance(Handle).file == key; })}; + if ((found = handle.has_value())) + binding.include = *handle; + } + else if (kindname == infra::name(infra::kind::traction)) + { + binding.type = infra::kind::traction; + auto const piece{Objects.traction.find(location, tolerance, [](TTraction *) { return true; })}; + if ((found = piece.has_value())) + binding.node = *piece; + } + else if (kindname == infra::name(infra::kind::memcell) || kindname == infra::name(infra::kind::launcher)) + { + binding.type = (kindname == infra::name(infra::kind::memcell) ? infra::kind::memcell : infra::kind::launcher); + auto const node{Objects.logic.find(location, tolerance, [&](scene::basic_node *Node) { + auto const ismemcell{dynamic_cast(Node) != nullptr}; + return ismemcell == (binding.type == infra::kind::memcell) && (key == "-" || Node->name() == key); + })}; + if ((found = node.has_value())) + binding.node = *node; + } + std::vector points; + std::optional yaw; + if (false == found || false == object_points(binding, points, yaw)) + return std::nullopt; + for (int i = 0; i < count && found; ++i) + { + int path{0}; + glm::dvec3 start, end; + line >> path >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z; + start = Context.to_world(start); + end = Context.to_world(end); + auto const track{Objects.paths.find(start, tolerance, [&](TTrack *Track) { + return path >= 0 && path < static_cast(Track->m_paths.size()) && + glm::distance(Track->m_paths[path].points[segment_data::point::start], start) <= tolerance && + glm::distance(Track->m_paths[path].points[segment_data::point::end], end) <= tolerance; + })}; + if (line.fail() || false == track.has_value()) + { + found = false; + break; + } + auto const &point{points[std::min(i, points.size() - 1)]}; + binding.anchors.push_back(infra::make_anchor(*track, path, point)); + } + if (false == found || binding.anchors.size() != points.size()) + return std::nullopt; + binding.turns = binding.turns && yaw.has_value(); + if (binding.turns) + binding.yaw = turn_of(binding, *yaw); + binding.label = object_label(binding); + return binding; +} + +} // namespace + +void editor_mode::infra_load() +{ + if (m_bindings_loaded || scene::Layers.empty()) + return; + m_bindings_loaded = true; + auto const marked{scene::Layers.marked_layers(kBindingMark)}; + if (marked.empty()) + return; + + bindable_objects const objects; + auto resolved{0}; + for (auto const layer : marked) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + for (auto const &text : scene::Layers.marked(layer, kBindingMark)) + { + if (text.empty() || text.front() == '#') + continue; + auto binding{read_binding(text, context, objects)}; + if (false == binding.has_value()) + { + m_bindings_unresolved.emplace_back(layer, text); + continue; + } + m_bindings.push_back(std::move(*binding)); + ++resolved; + } + } + WriteLog("Editor: " + std::to_string(resolved) + " object(s) bound to the paths read from the scenery" + + (m_bindings_unresolved.empty() ? std::string{} : ", " + std::to_string(m_bindings_unresolved.size()) + " binding(s) whose objects or paths weren't found are kept as they are"), + logtype::generic); +} + +void editor_mode::infra_store() +{ + if (false == m_bindings_loaded || scene::Layers.empty()) + return; + Live.refresh(); + m_bindings.erase(std::remove_if(m_bindings.begin(), m_bindings.end(), [](infra::binding const &Binding) { return false == alive(Binding); }), m_bindings.end()); + + std::map> lines; + for (auto const layer : scene::Layers.marked_layers(kBindingMark)) + lines[layer]; + for (auto const &entry : m_bindings_unresolved) + lines[entry.first].push_back(entry.second); + for (auto const &binding : m_bindings) + { + std::vector points; + std::optional yaw; + if (binding.anchors.empty() || false == object_points(binding, points, yaw)) + continue; + auto layer{scene::Layers.resolve(binding.anchors.front().at.track->layer())}; + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + if (false == scene::Layers.valid(layer)) + layer = 1; + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const local = [&](glm::dvec3 const &Point) { + auto const point{context.to_local(Point)}; + return format("%.4f %.4f %.4f", point.x, point.y, point.z); + }; + auto text{std::string{infra::name(binding.type)} + ' ' + std::to_string(static_cast(binding.group)) + ' ' + local(points.front()) + ' ' + object_key(binding) + ' ' + + (binding.turns ? "1 " : "0 ") + std::to_string(binding.anchors.size())}; + for (auto const &anchor : binding.anchors) + { + auto const &path{anchor.at.track->m_paths[anchor.at.path]}; + text += ' ' + std::to_string(anchor.at.path) + ' ' + local(path.points[segment_data::point::start]) + ' ' + local(path.points[segment_data::point::end]); + } + lines[layer].push_back(std::move(text)); + } + for (auto &entry : lines) + { + if (false == entry.second.empty()) + entry.second.insert(entry.second.begin(), "# objects bound to the paths in the track editor, they follow changes of the track; the simulation skips these lines"); + scene::Layers.mark(entry.first, kBindingMark, std::move(entry.second)); + } +} + +std::vector editor_mode::infra_scope() const +{ + std::vector result; + auto const add = [&](TTrack *Track) { + if (Track != nullptr && false == Track->m_editorremoved && editor_track::is_supported(*Track) && false == contains(result, Track)) + result.push_back(Track); + }; + auto *selected{selected_track()}; + switch (m_infra.scope) + { + case 0: add(selected); break; + case 1: + if (selected != nullptr) + for (auto const &span : editor_track::run_route(*selected, m_infra.reach).spans) + add(span.track); + break; + case 2: + for (auto const &span : m_profile.route.spans) + add(span.track); + break; + case 3: + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && editor_track::is_supported(*track)) + result.push_back(track); + break; + default: break; + } + return result; +} + +infra::binding *editor_mode::infra_find(infra::binding const &Binding) +{ + for (auto &binding : m_bindings) + if (binding.same(Binding)) + return &binding; + return nullptr; +} + +void editor_mode::infra_recognize() +{ + auto &state{m_infra}; + infra_load(); + state.candidates.clear(); + state.hovered = -1; + state.error.clear(); + state.status.clear(); + state.tracks = infra_scope(); + if (state.tracks.empty()) + { + state.error = (state.scope == 2 ? STR_C("Open the vertical profile of a route first") : state.scope == 3 ? STR_C("The scenery has no paths the editor can handle") : STR_C("Select a path first")); + return; + } + state.read = state.overruled = state.unread = state.elsewhere = 0; + track_index index; + std::unordered_set names; + std::unordered_set const scope(state.tracks.begin(), state.tracks.end()); + for (auto *track : state.tracks) + { + index.add(*track); + if (false == track->name().empty() && track->name() != "none") + names.insert(track->name()); + } + Live.refresh(); + event_links const links; + std::unordered_map> threads; + auto const thread_of = [&](TTrack *Reader) -> std::vector const & { + auto found{threads.find(Reader)}; + if (found != threads.end()) + return found->second; + std::vector areas; + for (auto const &span : editor_track::run_route(*Reader, 250.0).spans) + if (scope.count(span.track) > 0 && std::none_of(areas.begin(), areas.end(), [&](track_area const &Area) { return Area.track == span.track; })) + areas.push_back(area_of(*span.track)); + return threads.emplace(Reader, std::move(areas)).first->second; + }; + + auto const corridor{static_cast(state.corridor)}; + auto const add = [&](infra::binding Binding, std::string const &Reason, double const Reach) { + std::vector points; + std::optional yaw; + if (false == object_points(Binding, points, yaw)) + return; + infra_candidate candidate; + auto const cells{points.size() == 1 ? links.cells_of(Binding) : std::vector{}}; + auto const readers{links.readers_of(cells)}; + infra::station geometric; + auto const nearby{index.nearest(points.front(), Reach, geometric)}; + if (false == readers.empty()) + { + auto best{-1.0}; + infra::station station; + for (auto const &reader : readers) + { + infra::station at; + auto const &thread{thread_of(reader.track)}; + if (false == nearest_beside(thread, points.front(), std::max(Reach, 25.0), at)) + continue; + auto const distance{plan_distance(infra::frame_at(at).point, points.front())}; + if (best < 0.0 || distance < best) + { + best = distance; + station = at; + candidate.reader = reader.track; + candidate.event = reader.event; + candidate.cell = reader.cell; + candidate.loose = reader.loose; + } + } + if (candidate.reader == nullptr) + { + state.elsewhere += nearby ? 1 : 0; + return; + } + auto const &thread{thread_of(candidate.reader)}; + if (nearby && geometric.track != station.track && std::none_of(thread.begin(), thread.end(), [&](track_area const &Area) { return Area.track == geometric.track; })) + { + candidate.nearer = geometric.track; + candidate.nearer_foot = infra::frame_at(geometric).point; + } + Binding.anchors.push_back(infra::make_anchor(station, points.front())); + if (Binding.group == infra::category::other && Binding.type != infra::kind::memcell) + Binding.group = infra::category::signal; + } + else + { + for (auto const &point : points) + { + infra::station station; + if (false == index.nearest(point, Reach, station)) + return; + Binding.anchors.push_back(infra::make_anchor(station, point)); + } + candidate.unread = (false == cells.empty() && Binding.type != infra::kind::memcell && Binding.group == infra::category::signal); + } + if (std::abs(Binding.anchors.front().height) > 40.0) + return; + Binding.turns = yaw.has_value() && Binding.type != infra::kind::traction; + if (Binding.turns) + Binding.yaw = turn_of(Binding, *yaw); + Binding.label = object_label(Binding); + candidate.offset = Binding.anchors.front().offset; + candidate.reason = Reason; + candidate.bound = (infra_find(Binding) != nullptr); + candidate.chosen = (false == candidate.bound && Binding.group != infra::category::other); + candidate.binding = std::move(Binding); + state.read += candidate.reader != nullptr ? 1 : 0; + state.overruled += candidate.nearer != nullptr ? 1 : 0; + state.unread += candidate.unread ? 1 : 0; + state.candidates.push_back(std::move(candidate)); + }; + auto const rule_reason = [](infra::category const Group, char const *What) { return Group == infra::category::other ? std::string{"nearby, no rule matches the "} + What : std::string{"the "} + What + " matches a rule"; }; + + for (auto *model : simulation::Instances.sequence()) + { + // models of the includes follow along with their include + if (model == nullptr || model->from_template() || model->Model() == nullptr) + continue; + infra::binding binding; + binding.type = infra::kind::model; + binding.node = model; + binding.group = infra::classify(model->Model()->NameGet() + ' ' + model->name(), state.rules); + add(binding, rule_reason(binding.group, "file name"), corridor); + } + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + include_place place; + if (false == include_place_of(handle, place)) + continue; + infra::binding binding; + binding.type = infra::kind::include; + binding.include = handle; + binding.group = infra::classify(place.file + ' ' + place.description, state.rules); + add(binding, rule_reason(binding.group, "template or its description"), corridor); + } + for (auto *piece : simulation::Traction.sequence()) + { + if (piece == nullptr || piece->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::traction; + binding.node = piece; + binding.group = infra::category::catenary; + add(binding, "wire over the track", corridor); + } + for (auto *memcell : simulation::Memory.sequence()) + { + if (memcell == nullptr || memcell->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::memcell; + binding.node = memcell; + binding.group = infra::category::logic; + auto const named{false == memcell->asTrackName.empty() && names.count(memcell->asTrackName) > 0}; + add(binding, named ? "names the path " + memcell->asTrackName : std::string{"nearby"}, named ? std::max(corridor, 50.0) : corridor); + } + for (auto *launcher : simulation::Events.launchers()) + { + if (launcher == nullptr || launcher->from_template()) + continue; + infra::binding binding; + binding.type = infra::kind::launcher; + binding.node = launcher; + binding.group = infra::category::logic; + add(binding, "nearby", corridor); + } + std::stable_sort(state.candidates.begin(), state.candidates.end(), [](infra_candidate const &A, infra_candidate const &B) { return A.binding.group < B.binding.group; }); + state.status = format(STR_C("%d object(s) found along %d path(s)"), static_cast(state.candidates.size()), static_cast(state.tracks.size())); + if (state.elsewhere > 0) + state.status += format(STR_C("; %d left out: they stand by the scope, but the paths which read them are elsewhere"), state.elsewhere); +} + +void editor_mode::infra_bind_chosen() +{ + auto &state{m_infra}; + auto count{0}; + Live.refresh(); + for (auto &candidate : state.candidates) + { + if (false == candidate.chosen || candidate.bound || false == alive(candidate.binding)) + continue; + if (auto *existing{infra_find(candidate.binding)}) + *existing = candidate.binding; + else + m_bindings.push_back(candidate.binding); + candidate.bound = true; + candidate.chosen = false; + ++count; + } + state.status = format(STR_C("%d object(s) bound, they follow the changes of the track. The bindings are kept in the scenery files on save"), count); +} + +void editor_mode::infra_unbind(std::vector Indices) +{ + std::sort(Indices.begin(), Indices.end()); + Indices.erase(std::unique(Indices.begin(), Indices.end()), Indices.end()); + for (auto index{Indices.rbegin()}; index != Indices.rend(); ++index) + { + if (*index >= m_bindings.size()) + continue; + for (auto &candidate : m_infra.candidates) + if (candidate.binding.same(m_bindings[*index])) + candidate.bound = false; + m_bindings.erase(m_bindings.begin() + *index); + } +} + +infra::object_state editor_mode::infra_state_of(infra::binding const &Binding) const +{ + infra::object_state result; + result.saved = Binding; + switch (Binding.type) + { + case infra::kind::model: + { + auto const *model{static_cast(Binding.node)}; + result.location = model->location(); + result.angles = model->Angles(); + break; + } + case infra::kind::include: result.directive = scene::Layers.directive(Binding.include); break; + case infra::kind::traction: + { + auto const *traction{static_cast(Binding.node)}; + result.points = {traction->pPoint1, traction->pPoint2, traction->pPoint3, traction->pPoint4}; + break; + } + case infra::kind::memcell: + case infra::kind::launcher: result.location = Binding.node->location(); break; + } + return result; +} + +void editor_mode::infra_restore_state(infra::object_state const &State) +{ + Live.refresh(); + if (false == alive(State.saved)) + return; + if (auto *binding{infra_find(State.saved)}) + *binding = State.saved; + switch (State.saved.type) + { + case infra::kind::model: move_model(static_cast(State.saved.node), State.location, State.angles); break; + case infra::kind::include: + if (false == State.directive.empty() && State.directive != scene::Layers.directive(State.saved.include)) + set_include_directive(State.saved.include, State.directive); + break; + case infra::kind::traction: move_traction(static_cast(State.saved.node), State.points); break; + case infra::kind::memcell: move_memcell(static_cast(State.saved.node), State.location); break; + case infra::kind::launcher: move_launcher(static_cast(State.saved.node), State.location); break; + } +} + +void editor_mode::infra_buffer(infra::binding const &Binding, bool const Refresh) +{ + for (auto &entry : m_infra_buffer) + { + if (false == entry.saved.same(Binding)) + continue; + if (Refresh && false == entry.moved) + entry = infra_state_of(Binding); + return; + } + if (false == Refresh && false == m_history.empty() && m_history.back().action == EditorSnapshot::Action::TrackEdit) + { + // a drag keeps a single undo step, which holds the state from before the drag already + for (auto const &entry : m_history.back().infra) + if (entry.saved.same(Binding)) + return; + } + m_infra_buffer.push_back(infra_state_of(Binding)); +} + +void editor_mode::track_captured(TTrack const &Track) +{ + sweeps_captured(Track); + if (m_infra_suspended || false == m_infra.follow) + return; + infra_load(); + if (m_bindings.empty()) + return; + Live.refresh(); + for (auto &binding : m_bindings) + { + if (std::none_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { return Anchor.at.track == &Track; }) || false == alive(binding)) + continue; + // an object moved by hand since it was bound is measured again, as long as the path is the way it was measured + std::vector points; + std::optional yaw; + if (object_points(binding, points, yaw) && points.size() == binding.anchors.size()) + { + for (std::size_t i = 0; i < points.size(); ++i) + { + auto &anchor{binding.anchors[i]}; + if (anchor.at.track != &Track || anchor.at.path >= static_cast(Track.m_paths.size())) + continue; + auto const &path{Track.m_paths[anchor.at.path]}; + if (glm::distance(path.points[segment_data::point::start], anchor.start) > 1e-4 || glm::distance(path.points[segment_data::point::end], anchor.end) > 1e-4) + continue; + auto placed{anchor}; + if (glm::distance(infra::place(placed), points[i]) <= 0.01) + continue; + anchor = infra::make_anchor(anchor.at.track, anchor.at.path, points[i]); + } + if (binding.turns && yaw.has_value() && binding.anchors.front().at.track == &Track) + binding.yaw = turn_of(binding, *yaw); + } + infra_buffer(binding, true); + } +} + +void editor_mode::tracks_committed(std::vector const &Tracks) +{ + sweeps_committed(Tracks); + if (m_infra_suspended || false == m_infra.follow) + return; + infra_load(); + if (m_bindings.empty()) + return; + Live.refresh(); + std::vector areas; + for (auto *track : Tracks) + if (false == track->m_editorremoved) + areas.push_back(area_of(*track)); + // paths which took over may have been committed before the old ones were retired + std::optional> everywhere; + auto const take_over = [&](glm::dvec3 const &Foot, infra::station &Station) { + if (nearest_beside(areas, Foot, 30.0, Station)) + return true; + if (false == everywhere.has_value()) + { + everywhere.emplace(); + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && false == track->m_editorremoved && editor_track::is_supported(*track)) + everywhere->push_back(area_of(*track)); + } + // only a path running where the old one did, not the one alongside + return nearest_beside(*everywhere, Foot, 1.0, Station); + }; + for (auto &binding : m_bindings) + { + auto const affected{std::any_of(binding.anchors.begin(), binding.anchors.end(), [&](infra::anchor const &Anchor) { + return Anchor.at.track != nullptr && (Anchor.at.track->m_editorremoved || contains(Tracks, Anchor.at.track)); + })}; + if (false == affected || false == alive(binding)) + continue; + auto moved{binding}; + auto lost{false}; + for (auto &anchor : moved.anchors) + { + auto *track{anchor.at.track}; + if (false == track->m_editorremoved && false == contains(Tracks, track)) + continue; + // the object goes square to the new axis from the point of the old one, so a shift of the track carries it + // sideways rather than along; its own path goes first, then the ones which took over + infra::station station; + auto found{false}; + if (false == track->m_editorremoved && anchor.at.path < static_cast(track->m_paths.size())) + { + double s; + bool interior; + auto const distance{infra::project(track->m_paths[anchor.at.path], anchor.foot, s, interior)}; + station = {track, anchor.at.path, s}; + found = (distance <= 30.0 && beside(station, anchor.foot, interior)); + } + if (false == found && false == take_over(anchor.foot, station)) + { + lost = true; + break; + } + auto const offset{anchor.offset}; + auto const height{anchor.height}; + anchor = infra::make_anchor(station, anchor.foot); + anchor.offset = offset; + anchor.height = height; + } + if (lost) + { + binding.lost = true; + continue; + } + infra_buffer(binding, false); + moved.lost = false; + binding = std::move(moved); + infra_move(binding); + for (auto &entry : m_infra_buffer) + if (entry.saved.same(binding)) + entry.moved = true; + } +} + +void editor_mode::track_retired(TTrack &) +{ + tracks_committed({}); +} + +void editor_mode::track_split(TTrack &Original, TTrack &Created, segment_data const &First) +{ + sweeps_split(Original, Created); + if (m_infra_suspended || Created.m_paths.empty()) + return; + auto const length{infra::plan_length(First)}; + auto const &second{Created.m_paths.front()}; + for (auto &binding : m_bindings) + for (auto &anchor : binding.anchors) + { + if (anchor.at.track != &Original || anchor.at.path != 0) + continue; + if (anchor.at.s > length) + { + anchor.at.track = &Created; + anchor.at.s -= length; + anchor.start = second.points[segment_data::point::start]; + anchor.end = second.points[segment_data::point::end]; + anchor.length = infra::plan_length(second); + } + else + { + anchor.end = First.points[segment_data::point::end]; + anchor.length = length; + } + } +} + +void editor_mode::infra_move(infra::binding &Binding) +{ + if (Binding.anchors.empty()) + return; + auto const location{infra::place(Binding.anchors.front())}; + auto const facing = [&]() { return clamp_circular(infra::heading(infra::frame_at(Binding.anchors.front().at).forward) + Binding.yaw); }; + switch (Binding.type) + { + case infra::kind::model: + { + auto *model{static_cast(Binding.node)}; + auto angles{model->Angles()}; + if (Binding.turns) + angles.y = static_cast(facing()); + move_model(model, location, angles); + break; + } + case infra::kind::include: + { + include_place place; + if (false == include_place_of(Binding.include, place)) + return; + for (int axis = 0; axis < 3; ++axis) + place.values[place.position[axis]] = editor_includes::number(location[axis] - place.offset[axis]); + if (Binding.turns && place.yaw >= 0) + place.values[place.yaw] = editor_includes::number(facing()); + auto const directive{editor_includes::compose_directive(place.file, place.values)}; + if (directive != scene::Layers.directive(Binding.include)) + set_include_directive(Binding.include, directive); + break; + } + case infra::kind::traction: + { + if (Binding.anchors.size() != 4) + return; + std::array points; + for (std::size_t i = 0; i < 4; ++i) + points[i] = infra::place(Binding.anchors[i]); + move_traction(static_cast(Binding.node), points); + break; + } + case infra::kind::memcell: move_memcell(static_cast(Binding.node), location); break; + case infra::kind::launcher: move_launcher(static_cast(Binding.node), location); break; + } +} + +void editor_mode::infra_attach(EditorSnapshot &Snapshot) +{ + auto const involved = [&](infra::object_state const &State) { + if (State.moved) + return true; + for (auto const &anchor : State.saved.anchors) + { + if (contains(Snapshot.created, anchor.at.track) || contains(Snapshot.removed, anchor.at.track)) + return true; + for (auto const &entry : Snapshot.tracks) + if (entry.first == anchor.at.track) + return true; + } + return false; + }; + for (auto &entry : m_infra_buffer) + if (involved(entry)) + Snapshot.infra.push_back(std::move(entry)); + m_infra_buffer.clear(); +} + +void editor_mode::render_infra_body() +{ + auto &state{m_infra}; + infra_load(); + Live.refresh(); + + auto follow{state.follow}; + if (ImGui::Checkbox(STR_C("Bound objects follow the changes of the track"), &follow)) + { + state.follow = follow; + if (follow) + infra_rebase(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Bound objects keep their chainage along the path, the offset from its axis and the height above the rail top;\n" + "models and includes turn with the path and stay upright. The move is a part of the undo step of the change.")); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("Show in 3D"), &state.show); + ImGui::Text(STR_C("Objects bound in the scenery: %d"), static_cast(m_bindings.size())); + if (false == m_bindings_unresolved.empty()) + { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.f, 0.8f, 0.3f, 1.f), STR_C("(%d stored binding(s) not found, kept as they are)"), static_cast(m_bindings_unresolved.size())); + } + + ImGui::Separator(); + render_infra_search(); + render_infra_candidates(); + render_infra_bound(); + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.f, 0.4f, 0.35f, 1.f), "%s", state.error.c_str()); + else if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); +} + +// what changed while the bindings did not follow the track becomes their new starting point +void editor_mode::infra_rebase() +{ + for (auto &binding : m_bindings) + { + std::vector points; + std::optional yaw; + if (false == alive(binding) || false == object_points(binding, points, yaw) || points.size() != binding.anchors.size()) + continue; + for (std::size_t i = 0; i < points.size(); ++i) + { + auto &anchor{binding.anchors[i]}; + if (anchor.at.track == nullptr || anchor.at.track->m_editorremoved || anchor.at.path >= static_cast(anchor.at.track->m_paths.size())) + { + binding.lost = true; + continue; + } + anchor = infra::make_anchor(anchor.at.track, anchor.at.path, points[i]); + } + if (binding.turns && yaw.has_value()) + binding.yaw = turn_of(binding, *yaw); + } +} + +void editor_mode::render_infra_search() +{ + auto &state{m_infra}; + char const *const scopes[] = {STR_C("Selected path"), STR_C("Line through the selected path"), STR_C("Route of the vertical profile"), STR_C("Whole scenery")}; + ImGui::SetNextItemWidth(260.f); + ImGui::Combo(STR_C("Look along"), &state.scope, scopes, IM_ARRAYSIZE(scopes)); + if (state.scope == 1) + { + ImGui::SetNextItemWidth(120.f); + ImGui::InputFloat(STR_C("Reach each way, m"), &state.reach, 100.f, 500.f, "%.0f"); + state.reach = std::clamp(state.reach, 10.f, 20000.f); + } + ImGui::SetNextItemWidth(120.f); + ImGui::InputFloat(STR_C("Corridor each side of the axis, m"), &state.corridor, 0.5f, 2.f, "%.1f"); + state.corridor = std::clamp(state.corridor, 0.5f, 50.f); + if (ImGui::TreeNode(STR_C("Rules for names of the files"))) + { + ImGui::TextDisabled(STR_C("Parts of the names, separated by spaces; the first rule which matches decides")); + for (std::size_t i = 0; i < state.rules.size(); ++i) + { + auto &rule{state.rules[i]}; + char buffer[512]; + std::snprintf(buffer, sizeof(buffer), "%s", rule.keywords.c_str()); + ImGui::SetNextItemWidth(360.f); + if (ImGui::InputText((std::string{infra::name(rule.group)} + "##rule" + std::to_string(i)).c_str(), buffer, sizeof(buffer))) + rule.keywords = buffer; + } + if (ImGui::SmallButton(STR_C("Default rules"))) + state.rules = infra::default_rules(); + ImGui::TextDisabled(STR_C("Includes are matched by the template name and its //$e description, models by the file and node name;\n" + "traction is always the catenary, memory cells and event launchers the logic")); + ImGui::TreePop(); + } + if (ImGui::Button(STR_C("Find objects"))) + infra_recognize(); + +} + +void editor_mode::render_infra_candidates() +{ + auto &state{m_infra}; + auto &candidates{state.candidates}; + ImVec4 const readcolour{0.45f, 0.85f, 1.f, 1.f}; + ImVec4 const warncolour{1.f, 0.75f, 0.3f, 1.f}; + auto const shown = [&](infra_candidate const &Candidate) { + switch (state.filter) + { + case 1: return Candidate.reader != nullptr; + case 2: return Candidate.nearer != nullptr; + case 3: return Candidate.unread; + default: return true; + } + }; + auto const choose = [&](std::optional const Group, bool const Chosen) { + for (auto &candidate : candidates) + if ((false == Group.has_value() || candidate.binding.group == *Group) && shown(candidate)) + candidate.chosen = Chosen && false == candidate.bound; + }; + state.hovered = -1; + if (candidates.empty()) + return; + int chosen{0}; + for (auto const &candidate : candidates) + chosen += candidate.chosen ? 1 : 0; + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Bind the chosen (%d)"), chosen).c_str())) + infra_bind_chosen(); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("All"))) + choose({}, true); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("None"))) + choose({}, false); + + if (state.read > 0 || state.unread > 0) + { + ImGui::TextColored(readcolour, STR_C("%d read by the paths"), state.read); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("The memory cell of the object is read by a getvalues event of a path, listed by the path, inside a multiple\n" + "event or written loose as :event. The object is bound to the thread of that path, not to the nearest one")); + if (state.overruled > 0) + { + ImGui::SameLine(); + ImGui::TextColored(warncolour, STR_C("%d of them nearer to another path"), state.overruled); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("By the distance alone these would go to the wrong thread. Check them in 3D: the dashed line goes to the nearer path")); + } + if (state.unread > 0) + { + ImGui::SameLine(); + ImGui::TextColored(warncolour, STR_C("%d signal(s) not read by any path"), state.unread); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("The signal has a memory cell, but no event of a path reads it: the vehicles won't see it.\nBound to the nearest path")); + } + static char const *const filters[] = {"All", "Read by a path", "Nearer to another path", "Not read"}; + for (int i = 0; i < IM_ARRAYSIZE(filters); ++i) + { + if (i > 0) + ImGui::SameLine(); + ImGui::RadioButton(STR_C(filters[i]), &state.filter, i); + } + } + + ImGui::BeginChild("##infracandidates", ImVec2(0.f, 260.f), true); + std::vector rows; + for (auto const group : infra::categories()) + { + rows.clear(); + for (int i = 0; i < static_cast(candidates.size()); ++i) + if (candidates[i].binding.group == group && shown(candidates[i])) + rows.push_back(i); + if (rows.empty()) + continue; + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(colour(group))); + auto const open{ImGui::TreeNodeEx(format("%s (%d)", infra::name(group), static_cast(rows.size())).c_str(), group == infra::category::other ? 0 : ImGuiTreeNodeFlags_DefaultOpen)}; + ImGui::PopStyleColor(); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{STR_C("all##g")} + infra::name(group)).c_str())) + choose(group, true); + ImGui::SameLine(); + if (ImGui::SmallButton((std::string{STR_C("none##g")} + infra::name(group)).c_str())) + choose(group, false); + if (false == open) + continue; + ImGuiListClipper clipper; + clipper.Begin(static_cast(rows.size())); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const i{rows[row]}; + auto &candidate{candidates[i]}; + auto const &anchor{candidate.binding.anchors.front()}; + ImGui::PushID(i); + if (candidate.bound) + ImGui::TextDisabled(STR_C("[bound]")); + else + ImGui::Checkbox("##chosen", &candidate.chosen); + ImGui::SameLine(); + if (ImGui::Selectable(candidate.binding.label.c_str(), false, ImGuiSelectableFlags_None, ImVec2(ImGui::CalcTextSize(candidate.binding.label.c_str()).x, 0.f)) && anchor.at.track != nullptr) + focus_track(*anchor.at.track, anchor.at.path, anchor.foot); + if (ImGui::IsItemHovered()) + { + state.hovered = i; + std::string text{format(STR_C("%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s"), infra::name(candidate.binding.type), anchor.at.track != nullptr ? anchor.at.track->name().c_str() : "-", std::abs(candidate.offset), + candidate.offset >= 0.0 ? "right" : "left", anchor.height, candidate.reason.c_str())}; + if (candidate.reader != nullptr) + text += format(STR_C("\nRead by the path %s: event %s, getvalues of the memory cell %s"), candidate.reader->name().c_str(), candidate.event.c_str(), candidate.cell.c_str()) + + (candidate.loose ? STR(" (written loose, the path takes it by the name)") : std::string{}); + if (candidate.nearer != nullptr) + text += format(STR_C("\nThe path %s is nearer, but it doesn't read the object"), candidate.nearer->name().c_str()); + if (candidate.unread) + text += STR("\nNo path reads the memory cell of the signal"); + text += STR("\nClick to look at it"); + ImGui::SetTooltip("%s", text.c_str()); + } + ImGui::SameLine(); + ImGui::TextDisabled("%s %+.1f m", infra::name(candidate.binding.type), candidate.offset); + if (candidate.reader != nullptr) + { + ImGui::SameLine(); + ImGui::TextColored(readcolour, STR_C("read by %s"), candidate.reader->name().c_str()); + } + if (candidate.nearer != nullptr) + { + ImGui::SameLine(); + ImGui::TextColored(warncolour, STR_C("(nearer: %s)"), candidate.nearer->name().c_str()); + } + if (candidate.unread) + { + ImGui::SameLine(); + ImGui::TextColored(warncolour, "%s", STR_C("not read")); + } + ImGui::PopID(); + } + } + ImGui::TreePop(); + } + ImGui::EndChild(); +} + +void editor_mode::render_infra_bound() +{ + if (ImGui::TreeNode(STR_C("Bound objects along the scope"))) + { + auto const scope{infra_scope()}; + std::unordered_set const tracks(scope.begin(), scope.end()); + std::vector along; + for (std::size_t i = 0; i < m_bindings.size(); ++i) + if (std::any_of(m_bindings[i].anchors.begin(), m_bindings[i].anchors.end(), [&](infra::anchor const &Anchor) { return tracks.count(Anchor.at.track) > 0; })) + along.push_back(i); + ImGui::Text(STR_C("%d of %d"), static_cast(along.size()), static_cast(m_bindings.size())); + if (false == along.empty()) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Unbind all of these"))) + { + infra_unbind(along); + along.clear(); + } + } + std::optional unbind; + ImGui::BeginChild("##infrabound", ImVec2(0.f, 160.f), true); + ImGuiListClipper clipper; + clipper.Begin(static_cast(along.size())); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const index{along[row]}; + auto const &binding{m_bindings[index]}; + ImGui::PushID(static_cast(index)); + if (ImGui::SmallButton(STR_C("unbind"))) + unbind = index; + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(binding.lost ? IM_COL32(255, 80, 70, 255) : colour(binding.group))); + ImGui::Text("%s%s", binding.label.c_str(), binding.lost ? STR_C(" (no path to follow)") : ""); + ImGui::PopStyleColor(); + ImGui::PopID(); + } + } + ImGui::EndChild(); + if (unbind) + infra_unbind({*unbind}); + ImGui::TreePop(); + } +} + +void editor_mode::draw_infra_overlay() const +{ + auto const &state{m_infra}; + if (false == state.open || false == state.show) + return; + Live.refresh(); + screen_projection const projection; + ImDrawList *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const camera{GfxRenderer->Camera_Position()}; + auto const draw = [&](infra::binding const &Binding, ImU32 const Colour, float const Size) { + if (Binding.anchors.empty() || glm::distance(Binding.anchors.front().foot, camera) > 2000.0) + return; + std::vector points; + std::optional yaw; + if (false == alive(Binding) || false == object_points(Binding, points, yaw)) + return; + for (std::size_t i = 0; i < points.size() && i < Binding.anchors.size(); ++i) + { + auto anchor{Binding.anchors[i]}; + auto const foot{infra::frame_at(anchor.at).point}; + projection.line(drawlist, foot, points[i], Colour, 1.5f); + } + ImVec2 screen; + if (projection.project(points.front(), screen)) + drawlist->AddCircleFilled(screen, Size, Colour); + }; + for (auto const &binding : m_bindings) + draw(binding, binding.lost ? overlay_color::invalid : IM_COL32(90, 230, 110, 220), 3.5f); + auto const object = [&](infra_candidate const &Candidate) { + std::vector points; + std::optional yaw; + return object_points(Candidate.binding, points, yaw) ? points.front() : Candidate.binding.anchors.front().foot; + }; + auto const dashed = [&](glm::dvec3 const &From, glm::dvec3 const &To, ImU32 const Colour) { + auto const steps{std::clamp(static_cast(glm::distance(From, To) / 0.5), 2, 40)}; + for (int k = 0; k < steps; k += 2) + projection.line(drawlist, glm::mix(From, To, static_cast(k) / steps), glm::mix(From, To, static_cast(k + 1) / steps), Colour, 2.f); + }; + for (int i = 0; i < static_cast(state.candidates.size()); ++i) + { + auto const &candidate{state.candidates[i]}; + if (candidate.nearer != nullptr && alive(candidate.binding) && glm::distance(candidate.nearer_foot, camera) <= 2000.0) + { + auto const point{object(candidate)}; + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddCircle(screen, i == state.hovered ? 13.f : 8.f, IM_COL32(255, 190, 70, 230), 16, 2.f); + if (i == state.hovered) + dashed(point, candidate.nearer_foot, IM_COL32(255, 190, 70, 230)); + } + if (candidate.bound) + continue; + draw(candidate.binding, colour(candidate.binding.group, candidate.chosen ? 255 : 110), i == state.hovered ? 9.f : (candidate.chosen ? 5.f : 3.f)); + } + if (state.hovered >= 0 && state.hovered < static_cast(state.candidates.size())) + { + auto const &candidate{state.candidates[state.hovered]}; + auto const outline = [&](TTrack const *Track, ImU32 const Colour, float const Width) { + if (Track == nullptr || Track->m_editorremoved) + return; + for (auto const &path : Track->m_paths) + { + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, Colour, Width); + previous = next; + } + } + }; + outline(candidate.binding.anchors.front().at.track, IM_COL32(255, 255, 255, 200), 3.f); + if (candidate.reader != candidate.binding.anchors.front().at.track) + outline(candidate.reader, IM_COL32(110, 215, 255, 220), 3.f); + } +} diff --git a/application/editormodejoints.cpp b/application/editormodejoints.cpp new file mode 100644 index 00000000..c50d9d82 --- /dev/null +++ b/application/editormodejoints.cpp @@ -0,0 +1,522 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::grade_of; +using geometry::plan_of; + +double const kLinked{0.02}; // m, ends this close are joined by the simulation +double const kOverlayRange{3000.0}; +double const kGauge{1.5}; // m between the rails, for the cant +int const kRailCategory{1}; + +using kind = editor_mode::joint_issue::kind; + +bool rail(TTrack const &Track) +{ + return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && editor_track::is_supported(Track); +} + +bool at_start(editor_track::point_ref const &Point) +{ + return Point.kind == editor_track::point_kind::start; +} + +// direction of the path at its end, into the path +glm::dvec3 into(TTrack const &Track, editor_track::point_ref const &Point) +{ + auto const &path{Track.m_paths[Point.path]}; + bool const start{at_start(Point)}; + auto const &point{path.points[start ? segment_data::point::start : segment_data::point::end]}; + auto const &control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; + glm::dvec3 const direction{control != glm::dvec3{} ? control : path.points[start ? segment_data::point::end : segment_data::point::start] - point}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec3{0.0, 0.0, 1.0}; +} + +// roll of the path at its end, for the travel into the path, in degrees +double roll_into(TTrack const &Track, editor_track::point_ref const &Point) +{ + auto const &path{Track.m_paths[Point.path]}; + return at_start(Point) ? path.rolls[0] : -path.rolls[1]; +} + +double plan_angle(glm::dvec3 const &A, glm::dvec3 const &B) +{ + auto const a{plan_of(A)}; + auto const b{plan_of(B)}; + if (glm::length(a) < 1e-9 || glm::length(b) < 1e-9) + return 0.0; + return glm::degrees(std::acos(std::clamp(glm::dot(glm::normalize(a), glm::normalize(b)), -1.0, 1.0))); +} + +using end_key = std::tuple; + +end_key key_of(TTrack const *Track, editor_track::point_ref const &Point) +{ + return {Track, Point.path, static_cast(Point.kind)}; +} + +// pairs of the ends already looked at in the check under way +std::set> reported; + +char const *kind_name(kind const Kind) +{ + switch (Kind) + { + case kind::gap: return "gap"; + case kind::step: return "step"; + case kind::kink: return "kink"; + case kind::cant: return "cant jump"; + case kind::grade: return "grade break"; + default: return "free end"; + } +} + +std::string value_text(editor_mode::joint_issue const &Issue) +{ + switch (Issue.type) + { + case kind::gap: return format("%.3f m", Issue.value); + case kind::step: return format("%.1f mm", Issue.value * 1000.0); + case kind::kink: return format("%.3f deg", Issue.value); + case kind::cant: return format("%.1f mm", Issue.value); + case kind::grade: return format("%.1f %%o", Issue.value); + default: return "-"; + } +} + +ImU32 kind_colour(kind const Kind) +{ + switch (Kind) + { + case kind::gap: return IM_COL32(255, 70, 60, 235); + case kind::step: return IM_COL32(255, 140, 50, 230); + case kind::kink: return IM_COL32(255, 190, 60, 225); + case kind::cant: return IM_COL32(200, 120, 255, 225); + case kind::grade: return IM_COL32(90, 200, 255, 220); + default: return IM_COL32(200, 200, 200, 200); + } +} + +std::string name_of(TTrack const *Track) +{ + if (Track == nullptr) + return "-"; + return Track->name().empty() ? std::string{"(noname)"} : Track->name(); +} + +} // namespace + +void editor_mode::joints_start() +{ + auto &state{m_joints}; + state.queue.clear(); + state.next = 0; + state.issues.clear(); + state.selected = -1; + state.status.clear(); + reported.clear(); + glm::dvec3 const camera{Global.pCamera.Pos}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track)) + continue; + if (state.scope == 0 && glm::distance(track->location(), camera) > state.reach) + continue; + state.queue.push_back(track); + } + state.history = m_history.size(); +} + +void editor_mode::joints_check(TTrack &Track) +{ + auto &state{m_joints}; + auto const once = [&](end_key const &A, end_key const &B) { return reported.insert(A < B ? std::make_pair(A, B) : std::make_pair(B, A)).second; }; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + for (auto const kind_of_end : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + editor_track::point_ref const point{i, kind_of_end}; + auto const position{editor_track::point_position(Track, point)}; + auto const connections{editor_track::connected_points(Track, position)}; + if (connections.empty()) + { + joint_issue nearest; + nearest.value = std::numeric_limits::max(); + for (auto *section : simulation::Region->sections(position, static_cast(state.gap))) + for (auto const &cell : section->m_cells) + for (auto *other : cell.m_directories.paths) + { + if (other == nullptr || other == &Track || false == rail(*other)) + continue; + for (int k = 0; k < static_cast(other->m_paths.size()); ++k) + for (auto const theirkind : {editor_track::point_kind::start, editor_track::point_kind::end}) + { + editor_track::point_ref const theirs{k, theirkind}; + auto const distance{glm::distance(position, editor_track::point_position(*other, theirs))}; + if (distance > state.gap || distance >= nearest.value) + continue; + nearest.track = &Track; + nearest.point = point; + nearest.other = other; + nearest.theirs = theirs; + nearest.value = distance; + } + } + if (nearest.other != nullptr) + { + if (once(key_of(&Track, point), key_of(nearest.other, nearest.theirs))) + { + nearest.type = kind::gap; + nearest.position = position; + state.issues.push_back(nearest); + } + } + else if (state.free_ends) + { + joint_issue open; + open.type = kind::open; + open.track = &Track; + open.point = point; + open.position = position; + state.issues.push_back(open); + } + continue; + } + for (auto const &connection : connections) + { + auto *other{connection.first}; + auto const &theirs{connection.second}; + if (false == rail(*other) || false == once(key_of(&Track, point), key_of(other, theirs))) + continue; + joint_issue issue; + issue.track = &Track; + issue.point = point; + issue.other = other; + issue.theirs = theirs; + issue.position = position; + auto const step{glm::distance(position, editor_track::point_position(*other, theirs))}; + auto const ours{into(Track, point)}; + auto const their{into(*other, theirs)}; + auto const angle{plan_angle(ours, -their)}; + auto const cant{kGauge * 1000.0 * std::abs(std::sin(glm::radians(roll_into(Track, point) + roll_into(*other, theirs))))}; + auto const grade{std::abs(grade_of(ours) + grade_of(their)) * 1000.0}; + auto const add = [&](kind const Kind, double const Value) { + issue.type = Kind; + issue.value = Value; + state.issues.push_back(issue); + }; + if (step > state.step) + add(kind::step, step); + if (angle > state.angle) + add(kind::kink, angle); + if (cant > state.cant) + add(kind::cant, cant); + if (grade > state.grade) + add(kind::grade, grade); + } + } + } +} + +void editor_mode::joints_step() +{ + auto &state{m_joints}; + if (state.next >= state.queue.size()) + return; + auto const deadline{std::chrono::steady_clock::now() + std::chrono::milliseconds(15)}; + while (state.next < state.queue.size() && std::chrono::steady_clock::now() < deadline) + { + auto *track{state.queue[state.next++]}; + if (track->m_editorremoved) + continue; + joints_check(*track); + } + if (state.next < state.queue.size()) + return; + std::stable_sort(state.issues.begin(), state.issues.end(), [](joint_issue const &A, joint_issue const &B) { + if (A.type != B.type) + return A.type < B.type; + return A.value > B.value; + }); + std::array counts{}; + for (auto const &issue : state.issues) + ++counts[static_cast(issue.type)]; + state.status = format(STR_C("%zu paths checked: %d gaps, %d steps, %d kinks, %d cant jumps, %d grade breaks"), state.queue.size(), counts[0], counts[1], counts[2], counts[3], counts[4]); + if (state.free_ends) + state.status += format(STR_C(", %d free ends"), counts[5]); + WriteLog("Editor: joints check - " + state.status, logtype::generic); +} + +void editor_mode::joints_focus(int const Index) +{ + auto &state{m_joints}; + if (Index < 0 || Index >= static_cast(state.issues.size())) + return; + state.selected = Index; + auto const &issue{state.issues[Index]}; + auto const &paths{simulation::Paths.sequence()}; + if (std::find(paths.begin(), paths.end(), issue.track) == paths.end() || issue.track->m_editorremoved) + return; + m_node = issue.track; + m_track_point = {}; + ui()->set_node(m_node); + focus_track(*issue.track, issue.point.path, issue.position); +} + +bool editor_mode::joint_fix(joint_issue &Issue, std::vector> &States, std::vector &Changed) +{ + if (Issue.fixed || Issue.type == kind::open || Issue.track == nullptr || Issue.other == nullptr || Issue.track->m_editorremoved || Issue.other->m_editorremoved) + return false; + auto const movable = [](TTrack const *Track) { + std::string reason; + return Track->eType == tt_Normal && editor_track::can_edit_geometry(*Track, reason) && scene::Layers.editable(Track); + }; + TTrack *mover{nullptr}; + editor_track::point_ref moved; + TTrack *fixed{nullptr}; + editor_track::point_ref held; + if (movable(Issue.track)) + { + mover = Issue.track; + moved = Issue.point; + fixed = Issue.other; + held = Issue.theirs; + } + else if (movable(Issue.other) && (Issue.type != kind::gap || false == editor_track::is_connected(*Issue.other, Issue.theirs))) + { + mover = Issue.other; + moved = Issue.theirs; + fixed = Issue.track; + held = Issue.point; + } + if (mover == nullptr) + return false; + if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == mover; })) + States.emplace_back(mover, editor_track::capture(*mover)); + if (std::find(Changed.begin(), Changed.end(), mover) == Changed.end()) + Changed.push_back(mover); + editor_track::snap_target target; + target.track = fixed; + target.point = held; + target.position = editor_track::point_position(*fixed, held); + target.direction = into(*fixed, held); + switch (Issue.type) + { + case kind::gap: + case kind::kink: + case kind::grade: + editor_track::snap_point(*mover, moved, target, true); + break; + case kind::step: + editor_track::snap_point(*mover, moved, target, false); + break; + case kind::cant: + { + auto &path{mover->m_paths[moved.path]}; + auto const roll{static_cast(roll_into(*fixed, held))}; + if (at_start(moved)) + path.rolls[0] = -roll; + else + path.rolls[1] = roll; + break; + } + default: + break; + } + Issue.fixed = true; + return true; +} + +void editor_mode::joints_fix(std::vector const &Indices) +{ + auto &state{m_joints}; + std::vector> states; + std::vector changed; + std::vector chosen; + for (auto const index : Indices) + if (index >= 0 && index < static_cast(state.issues.size())) + chosen.push_back(&state.issues[index]); + std::size_t count{0}; + for (auto *issue : chosen) + if (joint_fix(*issue, states, changed)) + ++count; + if (count == 0) + { + state.status = STR_C("Nothing could be fixed: the paths are switches or in the files which can't be changed"); + return; + } + push_track_snapshot(std::move(states)); + editor_track::commit(changed); + for (auto *track : changed) + track->mark_dirty(); + state.history = m_history.size(); + state.status = format(STR_C("%zu joints fixed, Ctrl+Z takes them back"), count); + WriteLog("Editor: joints check - " + state.status, logtype::generic); +} + +void editor_mode::render_joints_body() +{ + auto &state{m_joints}; + ImGui::TextDisabled("%s", STR_C("Ends of the paths which almost meet, steps, kinks,\njumps of the cant and of the grade at the joints")); + ImGui::RadioButton(STR_C("Around the camera"), &state.scope, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Whole scenery"), &state.scope, 1); + bool const running{state.next < state.queue.size()}; + if (running) + { + auto const progress{static_cast(state.next) / static_cast(state.queue.size())}; + ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Checking %zu / %zu"), state.next, state.queue.size()).c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Cancel"))) + state.queue.resize(state.next); + } + else if (ImGui::Button(STR_C("Check"))) + { + joints_start(); + } + if (ImGui::TreeNode(STR_C("Limits"))) + { + ImGui::PushItemWidth(100.0f); + if (state.scope == 0) + ImGui::InputDouble(STR_C("Around the camera (m)"), &state.reach, 0.0, 0.0, "%.0f"); + ImGui::InputDouble(STR_C("Gap up to (m)"), &state.gap, 0.0, 0.0, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A free end closer than this to the end of another path is a gap: the trains don't go across it.\nThe simulation joins the ends no more than 2 cm apart")); + ImGui::InputDouble(STR_C("Step (m)"), &state.step, 0.0, 0.0, "%.3f"); + ImGui::InputDouble(STR_C("Kink (deg)"), &state.angle, 0.0, 0.0, "%.2f"); + ImGui::InputDouble(STR_C("Cant jump (mm)"), &state.cant, 0.0, 0.0, "%.1f"); + ImGui::InputDouble(STR_C("Grade break (per mille)"), &state.grade, 0.0, 0.0, "%.1f"); + ImGui::Checkbox(STR_C("Free ends too"), &state.free_ends); + ImGui::PopItemWidth(); + state.reach = std::clamp(state.reach, 50.0, 50000.0); + state.gap = std::clamp(state.gap, 0.03, 20.0); + state.step = std::max(0.001, state.step); + state.angle = std::max(0.01, state.angle); + state.cant = std::max(0.5, state.cant); + state.grade = std::max(0.5, state.grade); + ImGui::TextDisabled("%s", STR_C("Change the limits, then Check again")); + ImGui::TreePop(); + } + if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + if (state.issues.empty()) + return; + if (state.history != m_history.size()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), "%s", STR_C("The scenery changed since the check")); + int filter{state.filter + 1}; + ImGui::SetNextItemWidth(150.0f); + auto const filters{std::string{STR_C("All")} + '\0' + STR_C("gap") + '\0' + STR_C("step") + '\0' + STR_C("kink") + '\0' + STR_C("cant jump") + '\0' + STR_C("grade break") + '\0' + STR_C("free end") + '\0' + '\0'}; + if (ImGui::Combo("##jointfilter", &filter, filters.c_str())) + state.filter = filter - 1; + std::vector listed; + std::vector fixable; + for (int i = 0; i < static_cast(state.issues.size()); ++i) + { + auto const &issue{state.issues[i]}; + if (state.filter >= 0 && static_cast(issue.type) != state.filter) + continue; + listed.push_back(i); + if (false == issue.fixed && issue.type != kind::open) + fixable.push_back(i); + } + if (false == fixable.empty()) + { + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Fix %zu listed"), fixable.size()).c_str())) + joints_fix(fixable); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Gaps: the free end goes to the other end, along it. Steps: the end goes to the other one.\n" + "Kinks and grade breaks: the end is turned in line with the other path. Cant: the cant of the other path.\n" + "The plain path moves, a switch stays as it is. One Ctrl+Z takes it all back")); + } + auto const namewidth{std::max(110.0f, ImGui::GetContentRegionAvail().x * 0.32f)}; + bool const chosen{state.selected >= 0 && state.selected < static_cast(state.issues.size())}; + auto const details{chosen ? ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 2.0f : 0.0f}; + ImGui::BeginChild("jointresults", ImVec2(0.0f, -details), true); + ImGui::Columns(4, "jointrows", false); + ImGui::SetColumnWidth(0, 90.0f); + ImGui::SetColumnWidth(1, 80.0f); + ImGui::SetColumnWidth(2, namewidth); + ImGuiListClipper clipper; + clipper.Begin(static_cast(listed.size())); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const index{listed[row]}; + auto const &issue{state.issues[index]}; + auto const colour{ImGui::ColorConvertU32ToFloat4(kind_colour(issue.type))}; + ImGui::PushStyleColor(ImGuiCol_Text, issue.fixed ? ImVec4(0.5f, 0.8f, 0.5f, 1.0f) : colour); + if (ImGui::Selectable(format("%s##joint%d", STR_C(kind_name(issue.type)), index).c_str(), state.selected == index, ImGuiSelectableFlags_SpanAllColumns)) + joints_focus(index); + ImGui::PopStyleColor(); + ImGui::NextColumn(); + ImGui::TextUnformatted(issue.fixed ? STR_C("fixed") : value_text(issue).c_str()); + ImGui::NextColumn(); + ImGui::TextUnformatted(name_of(issue.track).c_str()); + ImGui::NextColumn(); + ImGui::TextUnformatted(name_of(issue.other).c_str()); + ImGui::NextColumn(); + } + ImGui::Columns(1); + ImGui::EndChild(); + if (chosen) + { + auto &issue{state.issues[state.selected]}; + ImGui::Text("%s %s: %s / %s", STR_C(kind_name(issue.type)), value_text(issue).c_str(), name_of(issue.track).c_str(), name_of(issue.other).c_str()); + if (false == issue.fixed && issue.type != kind::open && ImGui::Button(STR_C("Fix this one"))) + joints_fix({state.selected}); + } +} + +void editor_mode::draw_joints_overlay() const +{ + auto const &state{m_joints}; + if (false == state.open || state.issues.empty()) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (int i = 0; i < static_cast(state.issues.size()); ++i) + { + auto const &issue{state.issues[i]}; + if (issue.fixed || (state.filter >= 0 && static_cast(issue.type) != state.filter) || glm::distance(issue.position, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(issue.position, screen)) + continue; + auto const colour{i == state.selected ? IM_COL32(255, 255, 80, 255) : kind_colour(issue.type)}; + drawlist->AddCircle(screen, 11.0f, colour, 16, 2.5f); + auto const text{std::string{STR_C(kind_name(issue.type))} + " " + value_text(issue)}; + drawlist->AddText(ImVec2(screen.x + 13.0f, screen.y - 7.0f), colour, text.c_str()); + } +} diff --git a/application/editormodelineside.cpp b/application/editormodelineside.cpp new file mode 100644 index 00000000..f28ff72a --- /dev/null +++ b/application/editormodelineside.cpp @@ -0,0 +1,983 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_distance; +using geometry::plan_of; + +double const kSampleStep{2.0}; // m between the samples of the line +double const kLineCorridor{15.0}; // m from the axis, posts farther away belong to another line +double const kOverlayRange{3000.0}; +double const kFoulingTrace{250.0}; // m from the start of a switch the tracks are followed for +double const kRailHeight{0.18}; // m from the points of a track to the heads of its rails +int const kRailCategory{1}; +char const *const kHektoStyles[] = {"nowy", "stary", "zgnity"}; +char const *const kFoulingModels[] = {"ip/pkp/w17.inc", "ip/pkp/w17_new.inc", "ip/pkp/w17_old.inc", "ip/pkp/w17_dwuteo.inc"}; + +std::string normalized(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(Character == '\\' ? '/' : std::tolower(Character)); }); + return Text; +} + +bool numeric(std::string const &Text, double &Value) +{ + char *end{nullptr}; + Value = std::strtod(Text.c_str(), &end); + return false == Text.empty() && *end == '\0'; +} + +std::string number(double const Value, int const Decimals) +{ + auto text{format("%.*f", Decimals, Value)}; + if (text == "-0" || text.find_first_not_of("-0.") == std::string::npos) + text.erase(0, text.front() == '-' ? 1 : 0); + return text; +} + +double heading_of(glm::dvec2 const &Direction) +{ + auto const degrees{glm::degrees(std::atan2(Direction.x, Direction.y))}; + return degrees < 0.0 ? degrees + 360.0 : degrees; +} + +// index of the sample at or before the chainage +std::size_t sample_index(std::vector const &Samples, double const Chainage) +{ + auto const found{std::upper_bound(Samples.begin(), Samples.end(), Chainage, [](double const Value, editor_track::route_sample const &Sample) { return Value < Sample.chainage; })}; + return found == Samples.begin() ? 0 : std::min(std::distance(Samples.begin(), found) - 1, Samples.size() - 2); +} + +glm::dvec2 sampled_direction(std::vector const &Samples, double const Chainage) +{ + auto const index{sample_index(Samples, Chainage)}; + auto const &a{Samples[index]}; + auto const &b{Samples[index + 1]}; + auto const span{b.chainage - a.chainage}; + auto const f{span > 1e-9 ? std::clamp((Chainage - a.chainage) / span, 0.0, 1.0) : 0.0}; + auto const direction{a.direction * (1.0 - f) + b.direction * f}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; +} + +// chainage of the point of the line nearest to the point, and how far from the line it is +double project(std::vector const &Samples, glm::dvec3 const &Point, double &Distance) +{ + Distance = std::numeric_limits::max(); + double chainage{0.0}; + auto const point{plan_of(Point)}; + for (std::size_t i = 1; i < Samples.size(); ++i) + { + auto const a{plan_of(Samples[i - 1].position)}; + auto const b{plan_of(Samples[i].position)}; + auto const ab{b - a}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < Distance) + { + Distance = distance; + chainage = Samples[i - 1].chainage + (Samples[i].chainage - Samples[i - 1].chainage) * t; + } + } + return chainage; +} + +// the digits after the rotation make the number of the post, the last one the hectometre +bool hectometres_of(std::vector const &Values, int &Hectometres) +{ + if (Values.size() < 6) + return false; + Hectometres = 0; + for (std::size_t i = 5; i < Values.size(); ++i) + { + double digit; + if (false == numeric(Values[i], digit) || digit < 0.0 || digit > 9.0) + return false; + Hectometres = Hectometres * 10 + static_cast(digit); + } + return true; +} + +std::string hekto_file(int const Hectometres, int const Style) +{ + auto style{Style}; + if (style > 2) + { + auto const pick{LocalRandom(0.0, 1.0)}; + style = pick < 0.6 ? 0 : pick < 0.9 ? 1 : 2; + } + auto const size{Hectometres < 100 ? "1" : Hectometres < 1000 ? "10" : "100"}; + return std::string{"przytorowe/slupek_hekto_"} + kHektoStyles[style] + "_" + size + ".inc"; +} + +std::vector hekto_digits(int const Hectometres) +{ + return {std::to_string((Hectometres / 1000) % 10), std::to_string((Hectometres / 100) % 10), std::to_string((Hectometres / 10) % 10), std::to_string(Hectometres % 10)}; +} + +std::string km_text(int const Hectometres) +{ + return format("%d.%d", Hectometres / 10, Hectometres % 10); +} + +// points along the track from the end of a path on, the way a train goes: through a switch along its main track +std::vector trace(TTrack &Track, int const Path, bool const Forward, double const Limit) +{ + std::vector points; + auto *track{&Track}; + auto path{Path}; + auto forward{Forward}; + double length{0.0}; + for (int guard = 0; guard < 64 && track != nullptr && length < Limit; ++guard) + { + bezier const curve{track->m_paths[path]}; + auto const count{std::clamp(static_cast(curve.plan_length() / 1.0), 4, 400)}; + for (int k = points.empty() ? 0 : 1; k <= count; ++k) + { + auto const point{curve.point(forward ? static_cast(k) / count : 1.0 - static_cast(k) / count)}; + if (false == points.empty()) + length += plan_distance(points.back(), point); + points.push_back(point); + } + auto const exit{points.back()}; + TTrack *next{nullptr}; + int nextpath{0}; + bool nextforward{true}; + for (auto const &connection : editor_track::connected_points(*track, exit)) + { + auto *other{connection.first}; + if (other == track || (other->iCategoryFlag & 15) != kRailCategory) + continue; + bool const start{connection.second.kind == editor_track::point_kind::start}; + // entering a switch at its start the train takes the main track + if (next == nullptr || (other == next && start && connection.second.path == 0)) + { + next = other; + nextpath = connection.second.path; + nextforward = start; + } + } + track = next; + path = nextpath; + forward = nextforward; + } + return points; +} + +// nearest point of the polyline +glm::dvec3 nearest_on(std::vector const &Line, glm::dvec3 const &Point, double &Distance) +{ + Distance = std::numeric_limits::max(); + glm::dvec3 result{Point}; + auto const point{plan_of(Point)}; + for (std::size_t i = 1; i < Line.size(); ++i) + { + auto const a{plan_of(Line[i - 1])}; + auto const ab{plan_of(Line[i]) - a}; + auto const length2{glm::dot(ab, ab)}; + auto const t{length2 > 1e-12 ? std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0) : 0.0}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < Distance) + { + Distance = distance; + result = glm::mix(Line[i - 1], Line[i], t); + } + } + return result; +} + +void draw_label(ImDrawList *Draw, ImVec2 const At, ImU32 const Colour, std::string const &Text) +{ + auto const size{ImGui::CalcTextSize(Text.c_str())}; + Draw->AddRectFilled(ImVec2(At.x + 8.0f, At.y - 8.0f), ImVec2(At.x + 14.0f + size.x, At.y - 6.0f + size.y), IM_COL32(0, 0, 0, 150), 3.0f); + Draw->AddText(ImVec2(At.x + 11.0f, At.y - 7.0f), Colour, Text.c_str()); +} + +} // namespace + +std::vector editor_mode::standing_templates(std::function const &Accept) const +{ + std::vector result; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + if (false == scene::Layers.tracked(handle)) + continue; + auto const &included{scene::Layers.instance(handle)}; + if (included.removed || included.dead || included.file == nullptr) + continue; + auto const file{normalized(*included.file)}; + if (false == Accept(file) || included.context.rotation != glm::vec3{0.f} || included.context.scale != glm::vec3{1.f}) + continue; + std::string target; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || values.size() < 5) + continue; + glm::dvec3 location; + double yaw; + if (false == numeric(values[1], location.x) || false == numeric(values[2], location.y) || false == numeric(values[3], location.z) || false == numeric(values[4], yaw)) + continue; + standing_template standing; + standing.instance = handle; + standing.file = file; + standing.location = included.context.offset + location; + standing.yaw = yaw; + standing.values = std::move(values); + result.push_back(std::move(standing)); + } + return result; +} + +std::size_t editor_mode::place_templates(std::vector const &Items, std::string &Error) +{ + if (Items.empty()) + return 0; + if (scene::Layers.empty()) + { + Error = STR_C("Templates can be placed only in a scenery opened for editing (-edit)"); + return 0; + } + auto const layer{scene::Layers.resolve(scene::Layers.active())}; + if (std::string reason; false == scene::Layers.accepts(layer, &reason)) + { + Error = STR_C("The active layer can't take them: ") + (reason.empty() ? std::string{STR_C("there's no active layer")} : reason); + return 0; + } + auto const context{scene::Layers.layer(layer).context_insert()}; + if (context.rotation != glm::vec3{0.f} || context.scale != glm::vec3{1.f}) + { + Error = STR_C("The file of the active layer would receive them with a rotation or a scale in effect, pick another layer"); + return 0; + } + EditorSnapshot snap; + for (auto const &item : Items) + { + auto const local{context.to_local(item.location)}; + std::string directive; + if (item.described) + { + include_info info; + if (false == editor_includes::load(item.file, info, Error)) + continue; + directive = editor_includes::directive(item.file, info, editor_includes::parameter_count(item.file), local, static_cast(item.yaw), item.track, item.tilt, item.values); + } + else + { + std::vector values{"none", number(local.x, 3), number(local.y, 3), number(local.z, 3), number(item.yaw, 2)}; + values.insert(values.end(), item.rest.begin(), item.rest.end()); + directive = editor_includes::compose_directive(item.file, values); + } + auto const instance{scene::Layers.place(layer, item.file, directive)}; + if (instance == 0) + continue; + simulation::State.preview_include(directive, context, layer, instance); + snap.instances.push_back(instance); + } + if (snap.instances.empty()) + { + Error = STR_C("The active layer can't take them"); + return 0; + } + auto const count{snap.instances.size()}; + snap.node_name = Items.front().file; + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + return count; +} + +std::size_t editor_mode::move_templates(std::vector> const &Moves) +{ + EditorSnapshot snap; + for (auto const &[standing, item] : Moves) + { + if (false == scene::Layers.tracked(standing.instance) || false == scene::Layers.removable(standing.instance)) + continue; + auto const &included{scene::Layers.instance(standing.instance)}; + auto values{standing.values}; + auto const local{item.location - included.context.offset}; + values[1] = number(local.x, 3); + values[2] = number(local.y, 3); + values[3] = number(local.z, 3); + values[4] = number(item.yaw, 2); + auto before{scene::Layers.directive(standing.instance)}; + set_include_directive(standing.instance, editor_includes::compose_directive(*included.file, values)); + snap.directives.emplace_back(standing.instance, std::move(before)); + } + if (snap.directives.empty()) + return 0; + auto const count{snap.directives.size()}; + snap.node_name = Moves.front().first.file; + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + return count; +} + +std::size_t editor_mode::remove_templates(std::vector const &Instances) +{ + EditorSnapshot snap; + for (auto const instance : Instances) + { + if (false == scene::Layers.tracked(instance) || false == scene::Layers.removable(instance)) + continue; + scene::Layers.removed(instance, true); + snap.instances.push_back(instance); + } + if (snap.instances.empty()) + return 0; + auto const count{snap.instances.size()}; + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + select_include(0); + return count; +} + +void editor_mode::hekto_start(TTrack &Track, glm::dvec3 const &Point) +{ + auto &tool{m_hekto}; + tool.track = &Track; + tool.point = Point; + tool.km_read = false; + tool.dirty = true; + tool.expand = true; + tool.status.clear(); +} + +void editor_mode::hekto_read_kilometrage() +{ + auto &tool{m_hekto}; + auto const &found{tool.found}; + if (found.empty()) + return; + if (found.size() == 1) + { + // one post doesn't tell the way the kilometrage grows: it's taken to grow along the longer part of the line past the post + auto const length{tool.samples.empty() ? 0.0 : tool.samples.back().chainage}; + tool.growth = found.front().chainage > 0.5 * length ? -1 : 1; + tool.km = found.front().hectometres / 10.0 + tool.growth * (tool.origin - found.front().chainage) / 1000.0; + tool.status = format(STR_C("Kilometrage read from the post %s, check which way it grows"), km_text(found.front().hectometres).c_str()); + return; + } + // a straight line through the numbers of the posts against where they stand + double sc{0.0}, sk{0.0}, scc{0.0}, sck{0.0}; + for (auto const &post : found) + { + auto const km{post.hectometres / 10.0}; + sc += post.chainage; + sk += km; + scc += post.chainage * post.chainage; + sck += post.chainage * km; + } + auto const n{static_cast(found.size())}; + auto const denominator{n * scc - sc * sc}; + if (std::abs(denominator) < 1e-9) + return; + auto const slope{(n * sck - sc * sk) / denominator}; + auto const intercept{(sk - slope * sc) / n}; + if (std::abs(std::abs(slope) * 1000.0 - 1.0) > 0.2) + { + tool.status = format(STR_C("The numbers of the %zu posts along the line don't go by 100 m, the kilometrage wasn't read from them"), found.size()); + return; + } + tool.growth = slope > 0.0 ? 1 : -1; + tool.km = std::round((intercept + slope * tool.origin) * 1000.0) / 1000.0; + tool.status = format(STR_C("Kilometrage read from %zu posts standing along the line"), found.size()); +} + +void editor_mode::hekto_update() +{ + auto &tool{m_hekto}; + if (false == tool.dirty && tool.history == m_history.size()) + return; + tool.dirty = false; + tool.history = m_history.size(); + tool.posts.clear(); + tool.found.clear(); + tool.error.clear(); + tool.samples.clear(); + if (tool.track == nullptr || tool.track->m_editorremoved) + { + tool.track = nullptr; + return; + } + tool.route = editor_track::run_route(*tool.track, tool.reach); + if (tool.route.spans.empty()) + { + tool.error = STR_C("No line through this path"); + return; + } + tool.samples = editor_track::sample_route(tool.route, kSampleStep); + if (tool.samples.size() < 2) + { + tool.error = STR_C("No line through this path"); + return; + } + double distance; + tool.origin = project(tool.samples, tool.point, distance); + auto const length{tool.samples.back().chainage}; + for (auto const &standing : standing_templates([](std::string const &File) { return File.find("hekto") != std::string::npos; })) + { + hekto_found post; + post.post = standing; + if (false == hectometres_of(standing.values, post.hectometres)) + continue; + post.chainage = project(tool.samples, standing.location, distance); + if (distance > kLineCorridor || post.chainage <= 0.0 || post.chainage >= length) + continue; + tool.found.push_back(post); + } + if (false == tool.km_read) + { + tool.km_read = true; + hekto_read_kilometrage(); + } + auto const chainage_of = [&](int const Hectometres) { return tool.origin + tool.growth * (Hectometres / 10.0 - tool.km) * 1000.0; }; + for (auto &post : tool.found) + { + post.expected = chainage_of(post.hectometres); + post.error = post.chainage - post.expected; + post.outside = true; + } + auto const km_at = [&](double const Chainage) { return tool.km + tool.growth * (Chainage - tool.origin) / 1000.0; }; + auto const low{std::max(0, static_cast(std::ceil(std::min(km_at(0.0), km_at(length)) * 10.0 - 1e-6)))}; + auto const high{static_cast(std::floor(std::max(km_at(0.0), km_at(length)) * 10.0 + 1e-6))}; + for (int hectometres = low; hectometres <= high && tool.posts.size() < 5000; ++hectometres) + { + hekto_post post; + post.hectometres = hectometres; + post.chainage = chainage_of(hectometres); + if (post.chainage < 0.0 || post.chainage > length) + continue; + auto const axis{editor_track::sampled_position(tool.samples, post.chainage)}; + auto const growing{sampled_direction(tool.samples, post.chainage) * static_cast(tool.growth)}; + glm::dvec2 const left{-growing.y, growing.x}; + // alternately: the even hectometres on the right, the odd ones on the left of the growing kilometrage + auto const side{tool.side != 0 ? tool.side : (hectometres % 2 == 0 ? 1 : -1)}; + auto const planar{plan_of(axis) + left * (side * tool.offset)}; + post.position = placement_on_ground({planar.x, axis.y, planar.y}); + post.yaw = std::fmod(heading_of(growing) + tool.turn, 360.0); + // of the posts with this number the one nearest to the place stands for it, the others are duplicates. it stands wrong + // when it's farther from the place than allowed: along the line, across it, or on the other side + auto best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto &found{tool.found[i]}; + if (found.hectometres != hectometres) + continue; + found.outside = false; + found.distance = plan_distance(found.post.location, post.position); + if (found.distance < best) + { + best = found.distance; + post.standing = i; + } + } + for (int i = 0; i < static_cast(tool.found.size()); ++i) + { + auto &found{tool.found[i]}; + if (found.hectometres != hectometres) + continue; + found.duplicate = i != post.standing; + found.wrong = false == found.duplicate && found.distance > tool.tolerance; + } + tool.posts.push_back(post); + } +} + +void editor_mode::hekto_remove_duplicates() +{ + auto &tool{m_hekto}; + hekto_update(); + std::vector instances; + for (auto const &found : tool.found) + if (found.duplicate) + instances.push_back(found.post.instance); + auto const removed{remove_templates(instances)}; + tool.status = format(STR_C("%zu duplicate hectometre posts removed, Ctrl+Z brings them back"), removed); + WriteLog("Editor: " + tool.status); + tool.dirty = true; +} + +void editor_mode::hekto_place() +{ + auto &tool{m_hekto}; + hekto_update(); + std::vector items; + for (auto const &post : tool.posts) + { + if (post.standing >= 0) + continue; + template_item item; + item.file = hekto_file(post.hectometres, tool.style); + item.location = post.position; + item.yaw = post.yaw; + item.rest = hekto_digits(post.hectometres); + items.push_back(std::move(item)); + } + std::string error; + auto const placed{place_templates(items, error)}; + tool.status = placed > 0 ? format(STR_C("%zu hectometre posts placed, Ctrl+Z takes them back"), placed) : error; + WriteLog("Editor: " + tool.status, logtype::generic); + tool.dirty = true; +} + +void editor_mode::hekto_fix() +{ + auto &tool{m_hekto}; + hekto_update(); + std::vector> moves; + for (auto const &post : tool.posts) + { + if (post.standing < 0 || false == tool.found[post.standing].wrong) + continue; + template_item item; + item.location = post.position; + item.yaw = post.yaw; + moves.emplace_back(tool.found[post.standing].post, item); + } + auto const moved{move_templates(moves)}; + tool.status = format(STR_C("%zu hectometre posts moved to their places, Ctrl+Z takes it back"), moved); + WriteLog("Editor: " + tool.status, logtype::generic); + tool.dirty = true; +} + +void editor_mode::render_hekto_ui() +{ + auto &tool{m_hekto}; + auto *selected{selected_track()}; + if (tool.track == nullptr || (selected != nullptr && selected != tool.track && ImGui::SmallButton(STR_C("Along the line of the selected path")))) + { + if (selected == nullptr) + { + ImGui::TextDisabled("%s", STR_C("Select a path of the line, or RMB on it: Hectometre posts")); + return; + } + hekto_start(*selected, bezier{selected->m_paths.front()}.point(0.0)); + } + auto const changed = [&](bool const Changed) { + if (Changed) + tool.dirty = true; + }; + ImGui::PushItemWidth(110.0f); + changed(ImGui::InputDouble(STR_C("Kilometre at the point (km)"), &tool.km, 0.0, 0.0, "%.3f")); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Read from the posts"))) + { + tool.km_read = false; + tool.dirty = true; + } + ImGui::TextDisabled("%s", STR_C("The kilometrage grows")); + ImGui::SameLine(); + changed(ImGui::RadioButton(STR_C("along the line"), &tool.growth, 1)); + ImGui::SameLine(); + changed(ImGui::RadioButton(STR_C("against it"), &tool.growth, -1)); + ImGui::TextDisabled("%s", STR_C("Side, looking where it grows")); + ImGui::SameLine(); + changed(ImGui::RadioButton(STR_C("right"), &tool.side, 1)); + ImGui::SameLine(); + changed(ImGui::RadioButton(STR_C("left"), &tool.side, -1)); + ImGui::SameLine(); + changed(ImGui::RadioButton(STR_C("alternately"), &tool.side, 0)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The even hectometres on the right, the odd ones on the left")); + changed(ImGui::InputDouble(STR_C("From the axis (m)"), &tool.offset, 0.0, 0.0, "%.2f")); + tool.offset = std::clamp(tool.offset, 1.5, 30.0); + char const *styles[] = {"nowy", "stary", "zgnity", "mixed"}; + ImGui::Combo(STR_C("Posts"), &tool.style, styles, IM_ARRAYSIZE(styles)); + char const *turns[] = {"0", "90", "180", "270"}; + int turn{tool.turn / 90}; + if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns))) + { + tool.turn = turn * 90; + tool.dirty = true; + } + changed(ImGui::InputDouble(STR_C("Along the line each way (m)"), &tool.reach, 0.0, 0.0, "%.0f")); + tool.reach = std::clamp(tool.reach, 100.0, 100000.0); + changed(ImGui::InputDouble(STR_C("A post standing off by more (m) is wrong"), &tool.tolerance, 0.0, 0.0, "%.1f")); + tool.tolerance = std::clamp(tool.tolerance, 0.5, 50.0); + ImGui::PopItemWidth(); + hekto_update(); + if (false == tool.error.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); + return; + } + auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; + auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.wrong; })}; + auto const duplicates{std::count_if(tool.found.begin(), tool.found.end(), [](hekto_found const &Post) { return Post.duplicate; })}; + ImGui::Text(STR_C("Line %.0f m, km %s - %s: %d posts to place, %zu standing, %d of them wrong"), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, tool.posts.empty() ? "-" : km_text(std::min(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), + tool.posts.empty() ? "-" : km_text(std::max(tool.posts.front().hectometres, tool.posts.back().hectometres)).c_str(), static_cast(missing), tool.found.size(), static_cast(wrong)); + if (missing > 0 && ImGui::Button(format(STR_C("Place %d posts"), static_cast(missing)).c_str())) + hekto_place(); + if (duplicates > 0) + { + if (missing > 0) + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Remove %d duplicate posts"), static_cast(duplicates)).c_str())) + hekto_remove_duplicates(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Posts with a number another post nearer to its place already has")); + } + if (wrong > 0 || duplicates > 0) + { + if (wrong > 0) + { + if (missing > 0 || duplicates > 0) + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Move %d wrong posts to their places"), static_cast(wrong)).c_str())) + hekto_fix(); + } + ImGui::BeginChild("##hektowrong", ImVec2(0.0f, std::min(6, static_cast(wrong + duplicates)) * ImGui::GetTextLineHeightWithSpacing() + 6.0f), true); + for (auto const &post : tool.found) + { + if (false == post.wrong && false == post.duplicate) + continue; + auto const label{post.duplicate ? format(STR_C("km %s duplicate##%u"), km_text(post.hectometres).c_str(), post.post.instance) : format(STR_C("km %s stands %.1f m from its place##%u"), km_text(post.hectometres).c_str(), post.distance, post.post.instance)}; + if (ImGui::Selectable(label.c_str())) + { + glm::dvec3 const look{glm::normalize(glm::dvec3{-1.0, -0.8, -1.0})}; + m_focus_start_pos = Camera.Pos; + m_focus_start_angle = Camera.Angle; + m_focus_target_pos = post.post.location - look * 20.0; + m_focus_target_angle = glm::vec3(static_cast(std::asin(look.y)), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + m_focus_active = true; + m_focus_time = 0.0; + m_focus_duration = 0.6; + } + } + ImGui::EndChild(); + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_hekto_overlay() const +{ + auto const &tool{m_hekto}; + if (false == tool.open || tool.track == nullptr) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (auto const &post : tool.posts) + { + if (glm::distance(post.position, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(post.position, screen)) + continue; + bool const standing{post.standing >= 0 && false == tool.found[post.standing].wrong}; + auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)}; + drawlist->AddCircleFilled(screen, 4.0f, colour); + draw_label(drawlist, screen, colour, km_text(post.hectometres)); + } + for (auto const &post : tool.found) + { + if ((false == post.wrong && false == post.duplicate) || glm::distance(post.post.location, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(post.post.location, screen)) + continue; + auto const colour{post.duplicate ? IM_COL32(255, 160, 40, 235) : IM_COL32(255, 70, 60, 235)}; + drawlist->AddCircle(screen, 9.0f, colour, 16, 2.5f); + draw_label(drawlist, screen, colour, post.duplicate ? format(STR_C("km %s duplicate"), km_text(post.hectometres).c_str()) : format(STR_C("km %s, %.1f m from its place"), km_text(post.hectometres).c_str(), post.distance)); + if (post.duplicate) + continue; + for (auto const &target : tool.posts) + if (target.hectometres == post.hectometres) + projection.line(drawlist, post.post.location, target.position, IM_COL32(255, 70, 60, 160), 1.5f); + } + ImVec2 screen; + if (projection.project(tool.point, screen)) + { + drawlist->AddCircle(screen, 7.0f, IM_COL32(90, 200, 255, 235), 16, 2.5f); + draw_label(drawlist, screen, IM_COL32(90, 200, 255, 235), format("km %.3f", tool.km)); + } +} + +void editor_mode::fouling_update() +{ + auto &tool{m_fouling}; + auto *selected{selected_track()}; + if (tool.scope == 0 && selected != tool.selected) + tool.dirty = true; + if (false == tool.dirty && tool.history == m_history.size()) + return; + tool.dirty = false; + tool.history = m_history.size(); + tool.selected = selected; + tool.points.clear(); + tool.found.clear(); + tool.error.clear(); + std::vector switches; + glm::dvec3 const camera{Global.pCamera.Pos}; + if (tool.scope == 0) + { + if (selected == nullptr || selected->eType != tt_Switch) + { + tool.error = STR_C("Select a switch, or check around the camera"); + return; + } + switches.push_back(selected); + } + else + { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && track->eType == tt_Switch && false == track->m_editorremoved && (track->iCategoryFlag & 15) == kRailCategory && track->m_paths.size() >= 2 && + (tool.scope == 2 || glm::distance(track->location(), camera) <= tool.reach)) + switches.push_back(track); + } + for (auto *track : switches) + { + if (track->m_paths.size() < 2 || false == editor_track::is_supported(*track)) + continue; + auto const main{trace(*track, 0, true, kFoulingTrace)}; + auto const branch{trace(*track, 1, true, kFoulingTrace)}; + if (main.size() < 2 || branch.size() < 2) + continue; + glm::dvec3 previous{branch.front()}; + double previousdistance{0.0}; + for (std::size_t i = 1; i < branch.size(); ++i) + { + double distance; + nearest_on(main, branch[i], distance); + if (distance < tool.spacing) + { + previous = branch[i]; + previousdistance = distance; + continue; + } + auto const f{distance - previousdistance > 1e-9 ? (tool.spacing - previousdistance) / (distance - previousdistance) : 1.0}; + auto const onbranch{glm::mix(previous, branch[i], std::clamp(f, 0.0, 1.0))}; + double unused; + auto const onmain{nearest_on(main, onbranch, unused)}; + fouling_point point; + point.track = track; + auto const middle{(onbranch + onmain) * 0.5}; + // the points of the paths are at the rail foot, the heads of the rails are the height of the rail over them + point.position = tool.height_mode == 1 ? glm::dvec3{middle.x, 0.5 * (onbranch.y + onmain.y) + kRailHeight + tool.height, middle.z} : placement_on_ground({middle.x, std::max(onbranch.y, onmain.y), middle.z}); + auto const back{plan_of(previous - branch[i])}; + point.yaw = std::fmod(heading_of(glm::length(back) > 1e-9 ? glm::normalize(back) : glm::dvec2{0.0, 1.0}) + tool.turn, 360.0); + tool.points.push_back(point); + break; + } + } + // markers standing: each goes to the nearest fouling point, the others are left alone + auto const markers{standing_templates([](std::string const &File) { return File.find("w17") != std::string::npos || File.find("ukres") != std::string::npos; })}; + for (auto const &marker : markers) + { + int nearest{-1}; + double best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(tool.points.size()); ++i) + { + auto const distance{plan_distance(marker.location, tool.points[i].position)}; + if (distance < best) + { + best = distance; + nearest = i; + } + } + if (nearest < 0 || best > 40.0) + continue; + auto &point{tool.points[nearest]}; + if (point.standing >= 0 && tool.found[point.standing].error <= best) + continue; + fouling_found found; + found.marker = marker; + found.point = nearest; + found.error = best; + found.height_error = marker.location.y - point.position.y; + found.wrong = best > tool.tolerance || (tool.height_mode == 1 && std::abs(found.height_error) > tool.height_tolerance); + if (point.standing >= 0) + tool.found[point.standing] = found; + else + { + point.standing = static_cast(tool.found.size()); + tool.found.push_back(found); + } + } + auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })}; + auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })}; + tool.status = format(STR_C("%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong"), switches.size(), tool.points.size(), static_cast(missing), static_cast(wrong)); +} + +void editor_mode::fouling_place() +{ + auto &tool{m_fouling}; + fouling_update(); + std::vector items; + for (auto const &point : tool.points) + { + if (point.standing >= 0) + continue; + template_item item; + item.file = kFoulingModels[std::clamp(tool.model, 0, 3)]; + item.location = point.position; + item.yaw = point.yaw; + if (tool.model != 2) + item.rest = {"0"}; + items.push_back(std::move(item)); + } + std::string error; + auto const placed{place_templates(items, error)}; + tool.status = placed > 0 ? format(STR_C("%zu fouling point markers placed, Ctrl+Z takes them back"), placed) : error; + WriteLog("Editor: " + tool.status, logtype::generic); + tool.dirty = true; +} + +void editor_mode::fouling_fix() +{ + auto &tool{m_fouling}; + fouling_update(); + std::vector> moves; + for (auto const &found : tool.found) + { + if (false == found.wrong) + continue; + template_item item; + auto const &point{tool.points[found.point]}; + if (found.error <= tool.tolerance) + { + // in its place, only too high or too low + item.location = {found.marker.location.x, point.position.y, found.marker.location.z}; + item.yaw = found.marker.yaw; + } + else + { + item.location = point.position; + item.yaw = point.yaw; + } + moves.emplace_back(found.marker, item); + } + auto const moved{move_templates(moves)}; + tool.status = format(STR_C("%zu fouling point markers moved to their places, Ctrl+Z takes it back"), moved); + WriteLog("Editor: " + tool.status, logtype::generic); + tool.dirty = true; +} + +void editor_mode::render_fouling_ui() +{ + auto &tool{m_fouling}; + ImGui::TextDisabled("%s", STR_C("W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them")); + auto const changed = [&](bool const Changed) { + if (Changed) + tool.dirty = true; + }; + changed(ImGui::RadioButton(STR_C("The selected switch"), &tool.scope, 0)); + ImGui::SameLine(); + changed(ImGui::RadioButton(STR_C("Around the camera"), &tool.scope, 1)); + ImGui::SameLine(); + changed(ImGui::RadioButton(STR_C("Whole scenery"), &tool.scope, 2)); + ImGui::PushItemWidth(110.0f); + if (tool.scope == 1) + changed(ImGui::InputDouble(STR_C("Around the camera (m)"), &tool.reach, 0.0, 0.0, "%.0f")); + changed(ImGui::InputDouble(STR_C("Spacing of the axes (m)"), &tool.spacing, 0.0, 0.0, "%.2f")); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("3.5 m on the straight track; more on the curves, where the cars lean out")); + changed(ImGui::InputDouble(STR_C("A marker off by more (m) is wrong"), &tool.tolerance, 0.0, 0.0, "%.1f")); + changed(ImGui::RadioButton(STR_C("On the ground"), &tool.height_mode, 0)); + ImGui::SameLine(); + changed(ImGui::RadioButton(STR_C("Over the rail heads"), &tool.height_mode, 1)); + if (tool.height_mode == 1) + { + ImGui::SameLine(); + changed(ImGui::InputDouble(STR_C("by (m)"), &tool.height, 0.0, 0.0, "%.3f")); + changed(ImGui::InputDouble(STR_C("A marker higher or lower by more (m) is wrong"), &tool.height_tolerance, 0.0, 0.0, "%.3f")); + tool.height_tolerance = std::clamp(tool.height_tolerance, 0.005, 1.0); + } + char const *models[] = {"w17", "w17_new", "w17_old", "w17_dwuteo"}; + ImGui::Combo(STR_C("Marker"), &tool.model, models, IM_ARRAYSIZE(models)); + char const *turns[] = {"0", "90", "180", "270"}; + int turn{tool.turn / 90}; + if (ImGui::Combo(STR_C("Turned by (deg)"), &turn, turns, IM_ARRAYSIZE(turns))) + { + tool.turn = turn * 90; + tool.dirty = true; + } + ImGui::PopItemWidth(); + tool.reach = std::clamp(tool.reach, 50.0, 50000.0); + tool.spacing = std::clamp(tool.spacing, 2.0, 10.0); + tool.tolerance = std::clamp(tool.tolerance, 0.2, 20.0); + fouling_update(); + if (false == tool.error.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); + return; + } + ImGui::TextWrapped("%s", tool.status.c_str()); + auto const missing{std::count_if(tool.points.begin(), tool.points.end(), [](fouling_point const &Point) { return Point.standing < 0; })}; + auto const wrong{std::count_if(tool.found.begin(), tool.found.end(), [](fouling_found const &Found) { return Found.wrong; })}; + if (missing > 0 && ImGui::Button(format(STR_C("Place %d markers"), static_cast(missing)).c_str())) + fouling_place(); + if (wrong > 0) + { + if (missing > 0) + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Move %d wrong markers to their places"), static_cast(wrong)).c_str())) + fouling_fix(); + } +} + +void editor_mode::draw_fouling_overlay() const +{ + auto const &tool{m_fouling}; + if (false == tool.open) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (auto const &point : tool.points) + { + if (glm::distance(point.position, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(point.position, screen)) + continue; + bool const standing{point.standing >= 0 && false == tool.found[point.standing].wrong}; + auto const colour{standing ? IM_COL32(180, 180, 180, 200) : IM_COL32(90, 255, 120, 235)}; + drawlist->AddCircleFilled(screen, 4.0f, colour); + draw_label(drawlist, screen, colour, "W17"); + } + for (auto const &found : tool.found) + { + if (false == found.wrong || glm::distance(found.marker.location, camera) > kOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(found.marker.location, screen)) + continue; + drawlist->AddCircle(screen, 9.0f, IM_COL32(255, 70, 60, 235), 16, 2.5f); + draw_label(drawlist, screen, IM_COL32(255, 70, 60, 235), format(STR_C("W17 %.1f m off, %+.2f m high"), found.error, found.height_error)); + projection.line(drawlist, found.marker.location, tool.points[found.point].position, IM_COL32(255, 70, 60, 160), 1.5f); + } +} diff --git a/application/editormodeparallel.cpp b/application/editormodeparallel.cpp new file mode 100644 index 00000000..24090fd0 --- /dev/null +++ b/application/editormodeparallel.cpp @@ -0,0 +1,390 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::cross; +using geometry::plan_of; + +double const kParallelReach{5000.0}; // m each way along the line +double const kSpacings[] = {3.5, 4.0, 4.5, 4.75, 5.0, 6.0}; + +std::string name_of(TTrack const &Track) +{ + return Track.name().empty() ? std::string{"(noname)"} : Track.name(); +} + +segment_data reversed(segment_data const &Path) +{ + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; +} + +// the control vectors as they're drawn, also for a straight given without them +std::pair controls(segment_data const &Path) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + return {(end - start) / 3.0, (start - end) / 3.0}; + return {control1, control2}; +} + +// curvature in the plan at the start (false) or the end (true) of the path, 1/m, positive turning left +double curvature(segment_data const &Path, bool const Atend) +{ + auto const [control1, control2] = controls(Path); + auto const p0{plan_of(Path.points[segment_data::point::start])}; + auto const p3{plan_of(Path.points[segment_data::point::end])}; + auto const p1{p0 + plan_of(control1)}; + auto const p2{p3 + plan_of(control2)}; + auto const first{Atend ? 3.0 * (p3 - p2) : 3.0 * (p1 - p0)}; + auto const second{Atend ? 6.0 * (p1 - 2.0 * p2 + p3) : 6.0 * (p0 - 2.0 * p1 + p2)}; + auto const speed{glm::length(first)}; + return speed > 1e-9 ? cross(first, second) / (speed * speed * speed) : 0.0; +} + +glm::dvec2 direction(segment_data const &Path, bool const Atend) +{ + auto const [control1, control2] = controls(Path); + auto const along{Atend ? -plan_of(control2) : plan_of(control1)}; + return glm::length(along) > 1e-9 ? glm::normalize(along) : glm::dvec2{0.0, 1.0}; +} + +glm::dvec3 midpoint(TTrack const &Track) +{ + if (Track.m_paths.empty()) + return Track.location(); + bezier const curve{Track.m_paths.front()}; + return curve.point(0.5); +} + +// signed offset of Dst from Src, along Src's left normal; positive is the Left radio +double lateral_of(TTrack const &Src, TTrack const &Dst) +{ + if (Src.m_paths.empty()) + return 0.0; + auto const heading{direction(Src.m_paths.front(), false)}; + glm::dvec2 const left{-heading.y, heading.x}; + return glm::dot(plan_of(midpoint(Dst)) - plan_of(Src.m_paths.front().points[segment_data::point::start]), left); +} + +} // namespace + +TTrack *editor_mode::parallel_source() const +{ + if (m_track_set.size() >= 2) + return m_track_set.front(); + return selected_track(); +} + +TTrack *editor_mode::parallel_target() const +{ + if (m_track_set.size() != 2) + return nullptr; + return m_track_set.back() != m_track_set.front() ? m_track_set.back() : nullptr; +} + +void editor_mode::parallel_update() +{ + auto &tool{m_parallel}; + auto *track{parallel_source()}; + auto *target{parallel_target()}; + if (track != tool.pair_src || target != tool.pair_dst) + { + tool.pair_src = track; + tool.pair_dst = target; + if (track != nullptr && target != nullptr) + { + auto const lateral{lateral_of(*track, *target)}; + tool.side = lateral >= 0.0 ? 1 : -1; + tool.measured = std::abs(lateral); + } + else + tool.measured = 0.0; + } + else if (track != nullptr && target != nullptr) + tool.measured = std::abs(lateral_of(*track, *target)); + + if (track == tool.built_for && tool.spacing == tool.built_spacing && tool.side == tool.built_side && tool.scope == tool.built_scope && m_history.size() == tool.built_history) + return; + tool.built_for = track; + tool.built_spacing = tool.spacing; + tool.built_side = tool.side; + tool.built_scope = tool.scope; + tool.built_history = m_history.size(); + tool.pieces.clear(); + tool.styles.clear(); + tool.length = 0.0; + tool.error.clear(); + if (track == nullptr) + return; + if (track->eType != tt_Normal || false == editor_track::is_supported(*track)) + { + tool.error = STR_C("A switch has no line of its own: select a plain path"); + return; + } + std::vector> sources; + if (tool.scope == 1) + { + editor_track::chain run; + std::string error; + if (editor_track::find_run(*track, kParallelReach, run, error)) + for (std::size_t i = 0; i < run.tracks.size(); ++i) + sources.emplace_back(run.tracks[i], run.forward[i] ? run.tracks[i]->m_paths.front() : reversed(run.tracks[i]->m_paths.front())); + } + if (sources.empty()) + sources.emplace_back(track, track->m_paths.front()); + auto const offset{tool.spacing * tool.side}; + // the joints are moved once, along the mean direction of the paths meeting there, so the new paths meet too + std::vector joints; + for (std::size_t k = 0; k <= sources.size(); ++k) + { + bool const first{k == 0}; + bool const last{k == sources.size()}; + auto const &before{sources[last ? k - 1 : (first ? 0 : k - 1)].second}; + auto const &after{sources[last ? k - 1 : k].second}; + auto const point{first ? after.points[segment_data::point::start] : before.points[segment_data::point::end]}; + glm::dvec2 heading{first ? direction(after, false) : last ? direction(before, true) : direction(before, true) + direction(after, false)}; + heading = glm::length(heading) > 1e-9 ? glm::normalize(heading) : glm::dvec2{0.0, 1.0}; + auto const roll{first ? after.rolls[0] : last ? before.rolls[1] : 0.5f * (before.rolls[1] + after.rolls[0])}; + glm::dvec2 const left{-heading.y, heading.x}; + auto const planar{plan_of(point) + left * offset}; + joints.emplace_back(planar.x, point.y - offset * std::tan(glm::radians(static_cast(roll))), planar.y); + } + for (std::size_t i = 0; i < sources.size(); ++i) + { + auto const &source{sources[i].second}; + auto const [control1, control2] = controls(source); + auto const startscale{1.0 - curvature(source, false) * offset}; + auto const endscale{1.0 - curvature(source, true) * offset}; + if (startscale < 0.05 || endscale < 0.05) + { + tool.pieces.clear(); + tool.styles.clear(); + tool.error = format(STR_C("The spacing is larger than the radius of %s"), sources[i].first->name().c_str()); + return; + } + segment_data piece; + piece.points[segment_data::point::start] = joints[i]; + piece.points[segment_data::point::end] = joints[i + 1]; + piece.points[segment_data::point::control1] = control1 * startscale; + piece.points[segment_data::point::control2] = control2 * endscale; + piece.rolls = source.rolls; + auto const middle{0.5 * (curvature(source, false) + curvature(source, true))}; + if (source.radius != 0.0f && std::abs(middle) > 1e-7) + piece.radius = static_cast(std::copysign(std::abs((1.0 - middle * offset) / middle), static_cast(source.radius))); + tool.length += bezier{piece}.plan_length(); + tool.pieces.push_back(piece); + tool.styles.push_back(sources[i].first); + } +} + +void editor_mode::parallel_build() +{ + auto &tool{m_parallel}; + parallel_update(); + if (tool.pieces.empty()) + return; + std::vector created; + for (std::size_t i = 0; i < tool.pieces.size(); ++i) + created.push_back(editor_track::create_path(*tool.styles[i], tool.pieces[i])); + editor_track::commit(created); + push_track_snapshot({}, created); + tool.status = format(STR_C("Parallel track of %zu paths, %.1f m, %.2f m away; Ctrl+Z takes it back"), created.size(), tool.length, tool.spacing); + if (scene::Layers.active() == null_handle) + tool.status += "\n" + std::string{STR_C("The scenery isn't opened for editing, the new track won't be saved")}; + WriteLog("Editor: " + tool.status, logtype::generic); + tool.built_for = nullptr; +} + +void editor_mode::parallel_apply() +{ + auto &tool{m_parallel}; + auto *target{parallel_target()}; + parallel_update(); + if (target == nullptr || tool.pieces.empty()) + return; + if (tool.pieces.size() != 1) + { + tool.error = STR_C("Two selected paths: set the scope to this path, so the other takes its shape at the spacing"); + return; + } + std::string reason; + if (false == editor_track::can_edit_geometry(*target, reason)) + { + tool.error = reason; + return; + } + auto const joints{editor_track::joints(*target)}; + std::vector> states; + auto const remember = [&](TTrack *Track) { + if (Track == nullptr) + return; + if (std::any_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == Track; })) + return; + states.emplace_back(Track, editor_track::capture(*Track)); + }; + remember(target); + for (auto const &joint : joints) + remember(joint.other); + target->m_paths.front() = tool.pieces.front(); + editor_track::follow(*target, joints); + std::vector committed{target}; + for (auto const &joint : joints) + if (joint.other != nullptr) + committed.push_back(joint.other); + editor_track::commit(committed); + push_track_snapshot(std::move(states)); + tool.status = format(STR_C("%s set parallel to %s, %.2f m away; Ctrl+Z takes it back"), name_of(*target).c_str(), name_of(*parallel_source()).c_str(), tool.spacing); + WriteLog("Editor: " + tool.status, logtype::generic); + tool.built_for = nullptr; + straight_refresh(); +} + +void editor_mode::render_parallel_controls(bool const Scope) +{ + auto &tool{m_parallel}; + if (Scope) + { + ImGui::RadioButton(STR_C("This path"), &tool.scope, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("The line, up to the switches"), &tool.scope, 1); + } + ImGui::RadioButton(STR_C("Left"), &tool.side, -1); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Right"), &tool.side, 1); + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("(of the direction of the line, see the preview)")); + ImGui::SetNextItemWidth(90.0f); + ImGui::InputDouble(STR_C("Spacing (m)"), &tool.spacing, 0.0, 0.0, "%.2f"); + tool.spacing = std::clamp(tool.spacing, 1.5, 100.0); + for (auto const spacing : kSpacings) + { + ImGui::SameLine(); + if (ImGui::SmallButton(format("%.2f", spacing).c_str())) + tool.spacing = spacing; + } +} + +void editor_mode::render_parallel_ui() +{ + auto &tool{m_parallel}; + ImGui::TextDisabled("%s", STR_C("First select a straight path (LMB). Then a second track is laid beside it.\nAlong the line the curves are included: R minus or plus the spacing, same transitions and cant.")); + if (parallel_target() != nullptr) + ImGui::TextDisabled("%s", STR_C("Two paths selected: set the spacing of the other in Select. The first must be a straight.")); + else if (auto *source{parallel_source()}; source == nullptr) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("First select a straight path in the view.")); + else if (source->eType != tt_Normal || false == editor_track::is_straight(*source, m_straights.tolerance)) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("The selected path isn't a straight. Select a straight first.")); + render_parallel_controls(); + parallel_update(); + if (false == tool.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); + else if (false == tool.pieces.empty()) + { + if (ImGui::Button(format(STR_C("Lay %zu paths, %.1f m"), tool.pieces.size(), tool.length).c_str())) + parallel_build(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("First select a straight. Creates a new track beside it; the selected path stays.")); + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::render_parallel_set_ui() +{ + auto &tool{m_parallel}; + auto *source{parallel_source()}; + auto *target{parallel_target()}; + if (source == nullptr || target == nullptr) + return; + if (false == ImGui::CollapsingHeader(STR_C("Make parallel"), ImGuiTreeNodeFlags_DefaultOpen)) + return; + ImGui::TextWrapped("%s", STR_C("First selected must be a straight. It stays; the other is moved to this spacing.")); + ImGui::TextWrapped("%s", format(STR_C("%s stays. %s is moved to this spacing, taking its curves."), name_of(*source).c_str(), name_of(*target).c_str()).c_str()); + if (tool.measured > 0.05) + ImGui::TextDisabled("%s", format(lateral_of(*source, *target) >= 0.0 ? STR_C("Now %.2f m on the right") : STR_C("Now %.2f m on the left"), tool.measured).c_str()); + tool.scope = 0; + render_parallel_controls(false); + parallel_update(); + if (false == tool.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", tool.error.c_str()); + else if (false == tool.pieces.empty() && ImGui::Button(format(STR_C("Set %s to %.2f m"), name_of(*target).c_str(), tool.spacing).c_str())) + parallel_apply(); + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +bool editor_mode::parallel_preview_visible() const +{ + if (m_parallel.pieces.empty() || parallel_source() != m_parallel.built_for) + return false; + if (m_track_tab == track_tab::lay) + return m_lay.points.empty(); + if (m_track_tab == track_tab::path) + return true; + return m_track_tab == track_tab::lineside && m_parallel.open; +} + +void editor_mode::draw_parallel_preview() const +{ + if (false == parallel_preview_visible()) + return; + auto const &tool{m_parallel}; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + for (auto const &piece : tool.pieces) + { + bezier const curve{piece}; + auto const count{std::clamp(static_cast(curve.plan_length() / 2.0), 4, 128)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, k % 2 ? IM_COL32(90, 255, 120, 230) : IM_COL32(90, 255, 120, 90), 3.0f); + previous = next; + } + } + ImVec2 screen; + if (projection.project(tool.pieces.front().points[segment_data::point::start], screen)) + { + drawlist->AddCircleFilled(screen, 5.0f, IM_COL32(90, 255, 120, 230)); + auto const label{format("%.2f m", tool.spacing)}; + auto const size{ImGui::CalcTextSize(label.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x + 8.0f, screen.y - 8.0f), ImVec2(screen.x + 16.0f + size.x, screen.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f); + drawlist->AddText(ImVec2(screen.x + 12.0f, screen.y - 8.0f), IM_COL32(90, 255, 120, 255), label.c_str()); + } +} diff --git a/application/editormodeprofile.cpp b/application/editormodeprofile.cpp new file mode 100644 index 00000000..4fafe1d3 --- /dev/null +++ b/application/editormodeprofile.cpp @@ -0,0 +1,1882 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "world/Track.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::plan_distance; + +ImU32 const kGradeLine{IM_COL32(90, 230, 110, 255)}; +ImU32 const kTangents{IM_COL32(255, 210, 60, 130)}; +ImU32 const kTrack{IM_COL32(190, 190, 190, 255)}; +ImU32 const kTerrain{IM_COL32(170, 120, 70, 255)}; +ImU32 const kGrid{IM_COL32(255, 255, 255, 22)}; +ImU32 const kLabel{IM_COL32(200, 200, 200, 200)}; +ImU32 const kSwitch{IM_COL32(160, 100, 220, 45)}; +ImU32 const kError{IM_COL32(255, 80, 70, 255)}; +ImU32 const kWarning{IM_COL32(255, 200, 60, 255)}; +ImU32 const kPlan{IM_COL32(120, 190, 255, 255)}; +ImU32 const kSwitchLabel{IM_COL32(200, 160, 255, 220)}; +ImU32 const kChip{IM_COL32(14, 15, 19, 210)}; + +// text on a dark plate, readable over the lines of the plot +void plot_label(ImDrawList &Draw, ImVec2 const At, ImU32 const Colour, std::string const &Text) +{ + auto const size{ImGui::CalcTextSize(Text.c_str())}; + Draw.AddRectFilled(ImVec2(At.x - 3.0f, At.y - 1.0f), ImVec2(At.x + size.x + 3.0f, At.y + size.y + 1.0f), kChip, 3.0f); + Draw.AddText(At, Colour, Text.c_str()); +} + +double nice_step(double const Approximate) +{ + auto const power{std::pow(10.0, std::floor(std::log10(std::max(Approximate, 1e-6))))}; + for (auto const factor : {1.0, 2.0, 5.0, 10.0}) + if (power * factor >= Approximate) + return power * factor; + return power * 10.0; +} + +double route_speed(editor_track::route const &Route) +{ + double speed{-1.0}; + for (auto const &span : Route.spans) + speed = std::max(speed, editor_track::velocity(*span.track)); + return speed > 0.0 ? speed : 100.0; +} + +std::string const kProfileMark{"//$p"}; + +profile::line reversed_line(profile::line Line, double const Length) +{ + std::reverse(Line.points.begin(), Line.points.end()); + for (auto &point : Line.points) + point.chainage = Length - point.chainage; + std::swap(Line.join_start, Line.join_end); + return Line; +} + +bool end_locked(profile::line const &Line, profile::context const &Context, int const Index) +{ + if (Index == 0) + return Line.join_start && Context.start_joined; + if (Index + 1 == static_cast(Line.points.size())) + return Line.join_end && Context.end_joined; + return false; +} + +bool has_errors(std::vector const &Issues) +{ + return std::any_of(Issues.begin(), Issues.end(), [](profile::issue const &Issue) { return Issue.error; }); +} + +} + +// format of the lines, coordinates in the space of the file: +// profile +// speed origin join +// norms = k*V^2> +// point +// end +void editor_mode::profile_library_load() +{ + if (m_profiles_loaded) + return; + m_profiles_loaded = true; + m_profiles.clear(); + for (auto const layer : scene::Layers.marked_layers(kProfileMark)) + { + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const height = [&](double const Local) { return context.to_world({0.0, Local, 0.0}).y; }; + std::optional current; + for (auto const &text : scene::Layers.marked(layer, kProfileMark)) + { + std::istringstream line{text}; + std::string keyword; + line >> keyword; + if (keyword == "profile") + { + stored_profile entry; + entry.layer = layer; + glm::dvec3 start, end, middle; + line >> entry.length >> start.x >> start.y >> start.z >> end.x >> end.y >> end.z >> middle.x >> middle.y >> middle.z; + if (line.fail()) + { + current.reset(); + continue; + } + entry.start = context.to_world(start); + entry.end = context.to_world(end); + entry.middle = context.to_world(middle); + current = entry; + continue; + } + if (false == current.has_value()) + continue; + auto &entry{*current}; + std::string word; + if (keyword == "speed") + { + int start{1}, end{1}; + line >> entry.line.speed >> word >> entry.origin >> word >> start >> end; + entry.line.join_start = (start != 0); + entry.line.join_end = (end != 0); + } + else if (keyword == "norms") + { + auto &norms{entry.line.norms}; + line >> norms.grade_max >> norms.grade_switch_max >> norms.radius_factor >> norms.radius_min >> norms.curve_threshold >> norms.element_min >> norms.fixed_tolerance; + } + else if (keyword == "point") + { + profile::pvi point; + int automatic{0}; + line >> point.chainage >> point.elevation >> point.radius >> automatic; + if (line.fail()) + continue; + point.elevation = height(point.elevation); + point.automatic = (automatic != 0); + entry.line.points.push_back(point); + } + else if (keyword == "end") + { + if (entry.line.points.size() >= 2 && entry.length > 0.0) + m_profiles.push_back(entry); + current.reset(); + } + } + } + if (false == m_profiles.empty()) + WriteLog("Editor: " + std::to_string(m_profiles.size()) + " vertical profile design(s) read from the scenery", logtype::generic); +} + +void editor_mode::profile_library_mark(std::vector Layers) +{ + std::sort(Layers.begin(), Layers.end()); + Layers.erase(std::unique(Layers.begin(), Layers.end()), Layers.end()); + for (auto const layer : Layers) + { + if (false == scene::Layers.valid(layer)) + continue; + auto const &context{scene::Layers.layer(layer).context_insert()}; + auto const local = [&](glm::dvec3 const &Point) { + auto const point{context.to_local(Point)}; + return format("%.4f %.4f %.4f", point.x, point.y, point.z); + }; + std::vector lines; + for (auto const &entry : m_profiles) + { + if (entry.layer != layer) + continue; + if (lines.empty()) + lines.emplace_back("# vertical profiles (grade lines) designed in the track editor, the simulation skips these lines"); + lines.push_back(format("profile %.4f ", entry.length) + local(entry.start) + ' ' + local(entry.end) + ' ' + local(entry.middle)); + lines.push_back(format("speed %.1f origin %.4f join %d %d", entry.line.speed, entry.origin, entry.line.join_start ? 1 : 0, entry.line.join_end ? 1 : 0)); + auto const &norms{entry.line.norms}; + lines.push_back(format("norms %.4f %.4f %.4f %.1f %.4f %.2f %.4f", norms.grade_max, norms.grade_switch_max, norms.radius_factor, norms.radius_min, norms.curve_threshold, norms.element_min, norms.fixed_tolerance)); + for (auto const &point : entry.line.points) + lines.push_back(format("point %.4f %.4f %.1f %d", point.chainage, context.to_local({0.0, point.elevation, 0.0}).y, point.radius, point.automatic ? 1 : 0)); + lines.emplace_back("end"); + } + scene::Layers.mark(layer, kProfileMark, std::move(lines)); + } +} + +int editor_mode::profile_find(bool &Reversed) const +{ + auto const &state{m_profile}; + Reversed = false; + if (state.samples.empty()) + return -1; + auto const start{state.samples.front().position}; + auto const end{state.samples.back().position}; + auto const middle{editor_track::sampled_position(state.samples, state.route.length * 0.5)}; + for (std::size_t i = 0; i < m_profiles.size(); ++i) + { + auto const &entry{m_profiles[i]}; + if (std::abs(entry.length - state.route.length) > 0.5 || plan_distance(entry.middle, middle) > 0.5) + continue; + if (plan_distance(entry.start, start) <= 0.1 && plan_distance(entry.end, end) <= 0.1) + return static_cast(i); + if (plan_distance(entry.start, end) <= 0.1 && plan_distance(entry.end, start) <= 0.1) + { + Reversed = true; + return static_cast(i); + } + } + return -1; +} + +bool editor_mode::profile_restore() +{ + profile_library_load(); + auto &state{m_profile}; + bool reversed; + auto const index{profile_find(reversed)}; + if (index < 0) + return false; + auto const &entry{m_profiles[index]}; + state.line = reversed ? reversed_line(entry.line, entry.length) : entry.line; + state.origin = entry.origin; + state.selected = -1; + state.changed = false; + profile_check(); + state.status = STR_C("Design read from the scenery file ") + scene::Layers.layer(entry.layer).name + format(STR_C(", largest departure from the track %.3f m"), state.departure); + return true; +} + +void editor_mode::profile_store() +{ + auto &state{m_profile}; + if (state.route.spans.empty() || state.samples.empty() || state.line.points.size() < 2) + return; + if (scene::Layers.empty()) + { + state.error = STR_C("The scenery wasn't opened for editing, the design can't be kept in its files"); + return; + } + profile_library_load(); + stored_profile entry; + entry.length = state.route.length; + entry.start = state.samples.front().position; + entry.end = state.samples.back().position; + entry.middle = editor_track::sampled_position(state.samples, state.route.length * 0.5); + entry.origin = state.origin; + entry.line = state.line; + auto &points{entry.line.points}; + points.erase(std::remove_if(points.begin(), points.end(), [](profile::pvi const &Point) { return Point.joint; }), points.end()); + + bool reversed; + auto const index{profile_find(reversed)}; + if (index >= 0) + { + entry.layer = m_profiles[index].layer; + m_profiles[index] = entry; + } + else + { + auto layer{scene::Layers.resolve(state.route.spans.front().track->layer())}; + if (false == scene::Layers.valid(layer) || false == scene::Layers.accepts(layer)) + layer = scene::Layers.active(); + if (false == scene::Layers.valid(layer)) + layer = 1; + entry.layer = layer; + m_profiles.push_back(entry); + } + profile_library_mark({entry.layer}); + state.changed = false; +} + +void editor_mode::profile_forget() +{ + auto &state{m_profile}; + profile_library_load(); + bool reversed; + auto const index{profile_find(reversed)}; + if (index < 0) + return; + auto const layer{m_profiles[index].layer}; + m_profiles.erase(m_profiles.begin() + index); + profile_library_mark({layer}); + state.status = STR_C("The design will be removed from ") + scene::Layers.layer(layer).name + STR_C(" on save"); +} + +void editor_mode::profile_open_stored(std::size_t const Index) +{ + auto &state{m_profile}; + if (Index >= m_profiles.size()) + return; + auto const entry{m_profiles[Index]}; + auto const touching = [](glm::dvec3 const &Point) { + std::vector result; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || false == editor_track::is_supported(*track)) + continue; + for (auto const &path : track->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + if (plan_distance(path.points[index], Point) <= 0.1 && std::abs(path.points[index].y - Point.y) < 3.0) + { + if (std::find(result.begin(), result.end(), track) == result.end()) + result.push_back(track); + } + } + return result; + }; + for (auto *from : touching(entry.start)) + for (auto *to : touching(entry.end)) + { + editor_track::route route; + std::string error; + if (false == editor_track::find_route(from, to, route, error) || std::abs(route.length - entry.length) > 0.5) + continue; + auto const samples{editor_track::sample_route(route, 1.0)}; + if (samples.empty() || plan_distance(samples.front().position, entry.start) > 0.1 || plan_distance(editor_track::sampled_position(samples, route.length * 0.5), entry.middle) > 0.5) + continue; + profile_open(from, to); + return; + } + state.open = true; + state.error = STR_C("The route of the stored design isn't in the scenery any more (paths moved or removed)"); +} + +void editor_mode::profile_edited() +{ + m_profile.changed = true; + profile_check(); +} + +void editor_mode::profile_open(TTrack *From, TTrack *To) +{ + auto &state{m_profile}; + state.open = true; + state.status.clear(); + editor_track::route route; + if (false == editor_track::find_route(From, To, route, state.error)) + return; + profile_take(std::move(route), From, To); +} + +void editor_mode::profile_open_run(TTrack &Track) +{ + auto &state{m_profile}; + state.open = true; + state.status.clear(); + state.error.clear(); + auto route{editor_track::run_route(Track, state.run_length)}; + if (route.spans.empty()) + { + state.error = STR_C("The path can't carry a profile"); + return; + } + auto *from{route.spans.front().track}; + auto *to{route.spans.back().track}; + profile_take(std::move(route), from, to); +} + +void editor_mode::profile_take(editor_track::route Route, TTrack *From, TTrack *To) +{ + auto &state{m_profile}; + state.from = state.picked_from = From; + state.to = state.picked_to = To; + state.route = std::move(Route); + state.selected = -1; + profile_resample(); + if (false == profile_restore()) + profile_recognize(); + profile_fit_view(); +} + +void editor_mode::profile_resample() +{ + auto &state{m_profile}; + state.samples = editor_track::sample_route(state.route, 1.0); + auto const &samples{state.samples}; + state.terrain.assign(samples.size(), std::numeric_limits::quiet_NaN()); + std::vector points; + points.reserve(samples.size()); + for (auto const &sample : samples) + points.push_back(sample.position); + std::vector found; + auto const heights{ground_heights(points, true, &found)}; + for (std::size_t i = 0; i < samples.size(); ++i) + if (found[i] != 0) + state.terrain[i] = heights[i]; + + auto &context{state.context}; + context = {}; + context.length = state.route.length; + if (samples.empty()) + return; + for (auto const &span : state.route.spans) + if (span.track->eType != tt_Normal) + context.switches.push_back({span.from, span.to, span.track->name()}); + context.start_elevation = samples.front().position.y; + context.end_elevation = samples.back().position.y; + context.start_joined = editor_track::adjoining_grade(state.route, false, context.start_grade); + context.end_joined = editor_track::adjoining_grade(state.route, true, context.end_grade); + profile_check(); +} + +void editor_mode::profile_recognize() +{ + auto &state{m_profile}; + std::vector chainages, elevations, grades; + for (auto const &sample : state.samples) + { + chainages.push_back(sample.chainage); + elevations.push_back(sample.position.y); + grades.push_back(sample.grade); + } + auto const norms{state.line.norms}; + state.line = profile::recognize(chainages, elevations, grades, {}); + state.line.norms = norms; + state.line.speed = route_speed(state.route); + state.selected = -1; + profile_check(); + state.status = format(STR_C("Grade line recognized from the track: %zu points, largest departure %.3f m"), state.line.points.size(), state.departure); +} + +void editor_mode::profile_fit_ground() +{ + auto &state{m_profile}; + std::vector chainages; + for (auto const &sample : state.samples) + chainages.push_back(sample.chainage); + if (std::none_of(state.terrain.begin(), state.terrain.end(), [](double const Height) { return std::isfinite(Height); })) + { + state.error = STR_C("There's no ground under the route"); + return; + } + auto settings{state.line}; + settings.speed = route_speed(state.route); + auto line{profile::fit_ground(chainages, state.terrain, state.context, settings, state.fit)}; + if (line.points.size() < 2) + return; + state.line = std::move(line); + state.selected = -1; + state.error.clear(); + profile_check(); + double fill{0.0}, cut{0.0}; + for (std::size_t i = 0; i < state.samples.size(); ++i) + { + if (false == std::isfinite(state.terrain[i])) + continue; + auto const difference{profile::elevation(state.line, state.samples[i].chainage) - state.terrain[i]}; + fill = std::max(fill, difference); + cut = std::max(cut, -difference); + } + state.status = format(STR_C("Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m"), state.line.points.size(), fill, cut); +} + +std::pair editor_mode::profile_ballast(TTrack const &Track) +{ + auto const depth{std::max(0.0, static_cast(Track.fTexHeight1 - Track.fTexHeightOffset))}; + auto const foot{0.5 * std::abs(Track.fTrackWidth) + std::abs(Track.fTexWidth) + std::abs(Track.fTexSlope)}; + return {depth, std::max(0.5, foot)}; +} + +std::pair editor_mode::profile_generate_ground(std::vector> const &Beds, double const Reach) +{ + auto &state{m_profile}; + auto &works{state.earthworks}; + auto const &samples{state.samples}; + if (samples.empty() || false == ensure_terrain()) + return {0, 0}; + auto const size{heightmap::chunk_size}; + std::set keys; + for (auto const &sample : samples) + { + auto const &p{sample.position}; + auto const low{heightmap::chunk_at(p.x - Reach, p.z - Reach)}; + auto const high{heightmap::chunk_at(p.x + Reach, p.z + Reach)}; + for (auto x = low.first; x <= high.first; ++x) + for (auto z = low.second; z <= high.second; ++z) + keys.insert({x, z}); + } + auto const cells{heightmap::cells_for(m_terrain_spacing)}; + auto const cellsize{size / cells}; + auto const side{static_cast(cells) + 1}; + int made{0}, skipped{0}; + for (auto const &key : keys) + { + if (m_streamer.exists(key)) + continue; + auto const low{heightmap::chunk_corner(key)}; + glm::dvec2 const middle{low + glm::dvec2{size * 0.5}}; + std::vector around; + for (std::size_t i = 0; i < samples.size(); ++i) + if (std::abs(samples[i].position.x - middle.x) <= size * 0.5 + Reach && std::abs(samples[i].position.z - middle.y) <= size * 0.5 + Reach) + around.push_back(i); + if (around.empty()) + continue; + // where the scenery has its own ground, the chunk isn't made: the two would cover each other + std::vector probes; + for (int iz = 1; iz < 10; ++iz) + for (int ix = 1; ix < 10; ++ix) + probes.emplace_back(low.x + ix * size / 10.0, profile::elevation(state.line, samples[around.front()].chainage), low.y + iz * size / 10.0); + std::vector found; + ground_heights(probes, true, &found); + if (std::count(found.begin(), found.end(), 1) * 20 > static_cast(probes.size())) + { + ++skipped; + continue; + } + // the ground of the chunk: the ground under the nearest point of the route, or the formation there + std::vector heights(side * side); + for (std::size_t iz = 0; iz < side; ++iz) + for (std::size_t ix = 0; ix < side; ++ix) + { + glm::dvec3 const point{low.x + ix * cellsize, 0.0, low.y + iz * cellsize}; + auto best{around.front()}; + auto distance{std::numeric_limits::max()}; + for (auto const i : around) + { + auto const d{plan_distance(point, samples[i].position)}; + if (d < distance) + { + distance = d; + best = i; + } + } + heights[iz * side + ix] = static_cast(best < state.terrain.size() && std::isfinite(state.terrain[best]) ? state.terrain[best] : profile::elevation(state.line, samples[best].chainage) - Beds[best].first); + } + auto const added{m_streamer.add_chunk(key.first, key.second, m_terrain_spacing, [&](double const X, double const Z) { + auto const ix{static_cast(std::clamp(std::lround((X - low.x) / cellsize), 0, cells))}; + auto const iz{static_cast(std::clamp(std::lround((Z - low.y) / cellsize), 0, cells))}; + return heights[iz * side + ix]; + })}; + if (false == added) + continue; + works.generated.push_back(key); + ++made; + } + return {made, skipped}; +} + +void editor_mode::profile_shape_ground() +{ + auto &state{m_profile}; + auto &works{state.earthworks}; + auto const &samples{state.samples}; + if (samples.size() < 2 || state.line.points.size() < 2) + return; + if (false == ensure_terrain()) + { + state.error = STR_C("The heightmap terrain can't be opened"); + return; + } + auto const slope{std::max(0.1, works.slope)}; + std::vector> beds(samples.size(), {works.depth, works.half_width}); + if (works.from_ballast) + for (std::size_t i = 0; i < samples.size(); ++i) + if (samples[i].span < state.route.spans.size()) + if (auto const *track{state.route.spans[samples[i].span].track}; track != nullptr) + beds[i] = profile_ballast(*track); + double widest{0.0}; + for (auto const &bed : beds) + widest = std::max(widest, bed.second); + auto const reach{widest + works.reach}; + works.generated.clear(); + auto const generated{works.generate ? profile_generate_ground(beds, reach) : std::pair{0, 0}}; + + // the shaping is a modifier of the terrain: the formation along the axis of the route, followed by the ground + // around it. shaped again, the route replaces its modifier, and the terrain follows the new grade line + heightmap::modifier modifier; + auto const name = [](TTrack const *Track) { return Track != nullptr && false == Track->name().empty() ? Track->name() : std::string{"?"}; }; + modifier.name = "route " + name(state.from) + " - " + name(state.to); + modifier.slope = slope; + modifier.reach = works.reach; + modifier.rounding = std::max(0.0, works.rounding); + std::vector points; + points.reserve(samples.size()); + for (std::size_t i = 0; i < samples.size(); ++i) + points.push_back({{samples[i].position.x, samples[i].position.z}, profile::elevation(state.line, samples[i].chainage) - beds[i].first, beds[i].second}); + // the points the line between their neighbours passes within a few millimetres of are left out + double constexpr tolerance{0.005}; + modifier.points.push_back(points.front()); + std::size_t anchor{0}; + for (std::size_t next = 2; next < points.size(); ++next) + { + auto const &a{points[anchor]}; + auto const &b{points[next]}; + auto const run{b.position - a.position}; + auto const length{glm::length(run)}; + bool straight{true}; + for (auto i = anchor + 1; i < next && straight; ++i) + { + auto const t{length > 1e-9 ? glm::dot(points[i].position - a.position, run) / (length * length) : 0.0}; + auto const offset{glm::length(points[i].position - (a.position + run * t))}; + auto const formation{a.formation + (b.formation - a.formation) * t}; + auto const width{a.half_width + (b.half_width - a.half_width) * t}; + straight = offset <= tolerance && std::abs(points[i].formation - formation) <= tolerance && std::abs(points[i].half_width - width) <= tolerance; + } + if (false == straight) + { + anchor = next - 1; + modifier.points.push_back(points[anchor]); + } + } + modifier.points.push_back(points.back()); + + double fill{0.0}, cut{0.0}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + if (i >= state.terrain.size() || false == std::isfinite(state.terrain[i])) + continue; + fill = std::max(fill, points[i].formation - state.terrain[i]); + cut = std::max(cut, state.terrain[i] - points[i].formation); + } + if (false == works.modifier.empty() && works.modifier != modifier.name) + m_streamer.remove_modifier(works.modifier); + works.modifier = modifier.name; + auto const count{modifier.points.size()}; + m_streamer.modifier(std::move(modifier)); + forget_ground(); + + auto status{format(STR_C("Terrain shaped by the modifier \"%s\" (%zu points): embankment up to %.2f m, cutting up to %.2f m"), works.modifier.c_str(), count, fill, cut)}; + if (generated.first > 0) + status += format(STR_C("; %d chunk(s) of terrain made under the route"), generated.first); + if (generated.second > 0) + status += format(STR_C("; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it"), generated.second); + state.error.clear(); + profile_resample(); + state.status = status; + WriteLog("Editor: vertical profile - " + state.status, logtype::generic); +} + +void editor_mode::profile_restore_ground() +{ + auto &state{m_profile}; + auto &works{state.earthworks}; + std::size_t restored{0}; + if (false == works.modifier.empty() && m_streamer.remove_modifier(works.modifier)) + ++restored; + works.modifier.clear(); + for (auto const &key : works.generated) + { + m_streamer.remove_chunk(key.first, key.second); + ++restored; + } + works.generated.clear(); + forget_ground(); + profile_resample(); + state.status = restored > 0 ? STR_C("The terrain is back as it was before the shaping") : STR_C("The shaping of the terrain is gone already"); +} + +void editor_mode::render_ground_conversion() +{ + auto &conversion{m_ground_conversion}; + ImGui::PushID("ground_conversion"); + if (false == conversion.open) + { + if (ImGui::Button(STR_C("Convert the terrain files to heightmap terrain..."))) + { + conversion.open = true; + load_ground_files(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Replaces the triangles of the terrain files of the scenery (terrain directive, .txtf/.btf) with heightmap terrain\n" + "of the same shape, painted with their materials; the files lose these triangles on save")); + if (false == conversion.status.empty()) + ImGui::TextWrapped("%s", conversion.status.c_str()); + ImGui::PopID(); + return; + } + if (conversion.files.empty()) + ImGui::TextWrapped("%s", STR_C("The scenery has no terrain files: no terrain directive names a .txtf or .btf file")); + else + ImGui::TextUnformatted(STR_C("Terrain files of the scenery:")); + for (std::size_t index = 0; index < conversion.files.size(); ++index) + { + auto &source{conversion.files[index]}; + ImGui::PushID(static_cast(index)); + ImGui::BeginDisabled(false == source.good); + ImGui::Checkbox(source.file.c_str(), &source.chosen); + ImGui::EndDisabled(); + ImGui::SameLine(); + if (source.good) + ImGui::TextDisabled(source.text ? STR_C("(text, %zu triangles)") : STR_C("(binary, %zu triangles)"), source.count); + else + ImGui::TextDisabled("(%s)", source.message.c_str()); + ImGui::PopID(); + } + // the materials of the chosen files + std::vector counts(conversion.names.size(), 0); + for (auto const &source : conversion.files) + if (source.good && source.chosen) + for (auto index = source.first; index < source.first + source.count; ++index) + ++counts[conversion.materials[index]]; + if (false == conversion.names.empty()) + ImGui::TextUnformatted(STR_C("Materials: tick the ones which are the ground")); + for (std::size_t index = 0; index < conversion.names.size(); ++index) + { + if (counts[index] == 0) + continue; + bool chosen{conversion.chosen.count(index) != 0}; + ImGui::PushID(static_cast(index) + 0x10000); + if (ImGui::Checkbox(format(STR_C("%s (%zu triangles)"), conversion.names[index].c_str(), counts[index]).c_str(), &chosen)) + { + if (chosen) + conversion.chosen.insert(index); + else + conversion.chosen.erase(index); + } + ImGui::PopID(); + } + ImGui::TextWrapped("%s", STR_C("The chunks are painted with the materials of the triangles, up to 8 of them per chunk, added to the palette of the terrain. " + "Until saved, loading the scenery again takes it all back.")); + auto const ready{std::any_of(conversion.files.begin(), conversion.files.end(), [](auto const &Source) { return Source.good && Source.chosen; }) && false == conversion.chosen.empty()}; + ImGui::BeginDisabled(false == ready); + if (ImGui::Button(STR_C("Convert"))) + { + conversion.status = convert_ground(); + conversion.open = false; + profile_resample(); + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + conversion.open = false; + if (false == conversion.open) + { + // the triangles take memory, they're read again the next time + conversion.triangles = {}; + conversion.materials = {}; + } + ImGui::PopID(); +} + +void editor_mode::render_profile_earthworks() +{ + auto &state{m_profile}; + auto &works{state.earthworks}; + if (false == ImGui::TreeNode(STR_C("Shape the terrain to the grade line"))) + return; + ImGui::TextWrapped("%s", STR_C("Only the heightmap terrain changes: a formation under the track and slopes to the ground, kept as a modifier of the terrain which follows the grade line when it's shaped again; the triangles and models of the scenery stay as they are.")); + render_ground_conversion(); + auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); }; + ImGui::PushItemWidth(90.0f); + ImGui::Checkbox(STR_C("Make terrain where the route has none"), &works.generate); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Chunks of heightmap terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead")); + ImGui::Checkbox(STR_C("Formation under the ballast of the track"), &works.from_ballast); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The terrain goes as deep under the track as its ballast reaches and as wide as its foot,\nread from each path of the route; off: the values given below")); + if (works.from_ballast) + { + if (false == state.route.spans.empty() && state.route.spans.front().track != nullptr) + { + auto const bed{profile_ballast(*state.route.spans.front().track)}; + ImGui::TextDisabled(STR_C("Ballast %.2f m deep, its foot %.2f m from the axis"), bed.first, bed.second); + } + } + else + { + drag("Formation below the track (m)", works.depth, 0.01f, "%.2f"); + drag("Half width of the formation (m)", works.half_width, 0.05f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("From the axis of the route to the edge of the formation;\nfor two tracks add the distance between them")); + } + drag("Slopes 1 : n, n", works.slope, 0.05f, "%.2f"); + drag("Slopes reach at most (m)", works.reach, 1.0f, "%.0f"); + drag("Rounding of the edges (m)", works.rounding, 0.1f, "%.1f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Length over which the slope bends into the formation and into the ground; 0: sharp edges")); + ImGui::PopItemWidth(); + works.rounding = std::clamp(works.rounding, 0.0, 20.0); + works.depth = std::clamp(works.depth, 0.0, 5.0); + works.half_width = std::clamp(works.half_width, 0.5, 30.0); + works.slope = std::clamp(works.slope, 0.1, 10.0); + works.reach = std::clamp(works.reach, 1.0, 200.0); + if (has_errors(state.issues)) + ImGui::TextDisabled("%s", STR_C("Correct the errors of the grade line first")); + else if (ImGui::Button(STR_C("Shape the terrain"))) + profile_shape_ground(); + if (false == works.modifier.empty() || false == works.generated.empty()) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Restore the terrain"))) + profile_restore_ground(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Removes the modifier of the route, and takes away the terrain the shaping made")); + } + if (m_streamer.active()) + { + // the terrain tools take the tool options window over, the profile stays open under them + if (ImGui::Button(STR_C("Sculpting..."))) + terrain_workspace(true); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Opens the terrain tools: raising, lowering and smoothing with a brush")); + } + ImGui::TreePop(); +} + +void editor_mode::profile_check() +{ + auto &state{m_profile}; + auto &points{state.line.points}; + // joint points come and go, the selection follows the point it was made on + struct mark + { + int *index; + double chainage; + bool joint; + }; + std::vector marks; + for (auto *index : {&state.selected, &state.dragging, &state.curve_grip}) + if (*index >= 0 && *index < static_cast(points.size())) + marks.push_back({index, points[*index].chainage, points[*index].joint}); + profile::complete(state.line, state.context); + for (auto const &mark : marks) + { + int best{-1}; + double distance{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(points.size()); ++i) + if (points[i].joint == mark.joint && std::abs(points[i].chainage - mark.chainage) < distance) + { + distance = std::abs(points[i].chainage - mark.chainage); + best = i; + } + *mark.index = (mark.joint || distance < 1e-6) ? best : -1; + } + state.issues = profile::check(state.line, state.context); + state.departure = 0.0; + for (auto const &sample : state.samples) + state.departure = std::max(state.departure, std::abs(profile::elevation(state.line, sample.chainage) - sample.position.y)); +} + +void editor_mode::profile_fit_view() +{ + auto &state{m_profile}; + auto const length{std::max(1.0, state.route.length)}; + state.view_from = -0.02 * length; + state.view_to = length * 1.02; + state.exaggeration = 0.0f; +} + +void editor_mode::profile_apply() +{ + auto &state{m_profile}; + state.status.clear(); + state.error.clear(); + if (state.route.spans.empty() || state.line.points.size() < 2) + return; + if (has_errors(state.issues)) + { + state.error = STR_C("Correct the errors to apply the grade line"); + return; + } + std::vector tracks; + for (auto const &span : state.route.spans) + tracks.push_back(span.track); + if (false == tracks_editable(tracks, state.error)) + { + state.error = STR_C("The grade line can't be applied, ") + state.error; + return; + } + auto const line{state.line}; + std::vector> states; + std::vector created; + auto const gaps{editor_track::apply_profile( + state.route, [&](double const Chainage) { return profile::elevation(line, Chainage); }, [&](double const Chainage) { return profile::grade(line, Chainage); }, profile::breaks(line), states, created)}; + auto const changed{states.size()}; + auto const added{created.size()}; + // the stored design changes with the paths, an undo takes both back + profile_library_load(); + auto library{m_profiles}; + bool const pushed{false == states.empty() || false == created.empty()}; + push_track_snapshot(std::move(states), std::move(created)); + if (pushed) + { + m_history.back().profiles = true; + m_history.back().profile_library = std::move(library); + } + state.from = state.route.spans.front().track; + state.to = state.route.spans.back().track; + profile_resample(); + profile_store(); + state.status = format(STR_C("Grade line applied: %zu paths changed, %zu added, largest departure %.3f m"), changed, added, state.departure); + if (false == scene::Layers.empty()) + state.status += "\nThe paths and the design (//$p lines) go to the scenery files on save"; + auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const closed{std::count_if(gaps.begin(), gaps.end(), [](auto const &Gap) { return Gap.closed; })}; + if (closed > 0) + state.status += "\nPaths off the route brought to the height of the switches:"; + for (auto const &gap : gaps) + if (gap.closed) + state.status += format("\n %s at %s: %+.3f m", name(gap.neighbour).c_str(), name(gap.track).c_str(), -gap.gap); + if (closed < static_cast(gaps.size())) + state.status += "\nSwitch ends off the adjoining paths, which can't be changed here:"; + for (auto const &gap : gaps) + if (false == gap.closed) + state.status += format("\n %s -> %s: %+.3f m", name(gap.track).c_str(), name(gap.neighbour).c_str(), gap.gap); + WriteLog("Editor: vertical profile - " + state.status, logtype::generic); +} + +void editor_mode::profile_after_undo() +{ + auto &state{m_profile}; + if (state.route.spans.empty()) + return; + auto const removed = [](TTrack const *Track) { return Track == nullptr || Track->m_editorremoved; }; + if (removed(state.from) || removed(state.to)) + { + state.from = state.picked_from; + state.to = state.picked_to; + } + std::string error; + if (removed(state.from) || removed(state.to) || false == editor_track::find_route(state.from, state.to, state.route, error)) + { + state.route = {}; + state.samples.clear(); + state.selected = state.dragging = state.curve_grip = -1; + return; + } + profile_resample(); +} + +void editor_mode::render_profile_body() +{ + auto &state{m_profile}; + render_profile_source(); + if (state.route.spans.empty()) + { + ImGui::TextWrapped("%s", STR_C("Select a path in the view, then build the route along the line, or set its start and end paths (switches are passed through)")); + if (state.picked_from != nullptr) + ImGui::Text(STR_C("Start: %s"), state.picked_from->name().c_str()); + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); + return; + } + ImGui::Separator(); + ImGui::TextWrapped(STR_C("Route: %zu paths, %.2f m, %s ... %s"), state.route.spans.size(), state.route.length, state.route.spans.front().track->name().c_str(), state.route.spans.back().track->name().c_str()); + { + bool reversed; + auto const stored{profile_find(reversed)}; + if (stored >= 0) + { + ImGui::TextDisabled(STR_C("Design kept in %s%s"), scene::Layers.layer(m_profiles[stored].layer).name.c_str(), state.changed ? STR_C(", changes go there on save") : ""); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Forget the design"))) + profile_forget(); + } + else if (state.changed) + ImGui::TextDisabled(STR_C("The design goes to the scenery file of the route on save")); + } + + if (render_profile_parameters()) + profile_edited(); +} + +void editor_mode::render_profile_strip() +{ + auto &state{m_profile}; + auto const &io{ImGui::GetIO()}; + ImGui::SetNextWindowPos(ImVec2(8.0f, io.DisplaySize.y - 360.0f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(std::max(600.0f, io.DisplaySize.x - 500.0f), 320.0f), ImGuiCond_FirstUseEver); + auto const title{std::string{STR_C("Vertical profile")} + format(" %.2f m###profilestrip", state.route.length)}; + if (ImGui::Begin(title.c_str(), nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) + { + auto const side{std::clamp(ImGui::GetContentRegionAvail().x * 0.28f, 280.0f, 420.0f)}; + ImGui::BeginChild("##profileplot", ImVec2(-side, 0.0f), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + render_profile_toolbar(); + render_profile_canvas(ImGui::GetContentRegionAvail().y); + ImGui::EndChild(); + ImGui::SameLine(); + ImGui::BeginChild("##profileside", ImVec2(0.0f, 0.0f), false); + if (ImGui::BeginTabBar("##profiletabs")) + { + // a click on a point in the plot brings its tab forward + bool const picked{state.selected >= 0 && state.selected != state.shown}; + state.shown = state.selected; + if (ImGui::BeginTabItem(STR_C("Point"), nullptr, picked ? ImGuiTabItemFlags_SetSelected : 0)) + { + render_profile_point(); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem(format("%s (%zu)###profilepoints", STR_C("Points"), state.line.points.size()).c_str())) + { + ImGui::BeginChild("##profilepointlist"); + render_profile_points(); + ImGui::EndChild(); + ImGui::EndTabItem(); + } + bool const errors{has_errors(state.issues)}; + if (false == state.issues.empty()) + ImGui::PushStyleColor(ImGuiCol_Text, errors ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f)); + bool const remarks{ImGui::BeginTabItem(format("%s (%zu)###profileremarks", STR_C("Remarks"), state.issues.size()).c_str())}; + if (false == state.issues.empty()) + ImGui::PopStyleColor(); + if (remarks) + { + render_profile_issues(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + ImGui::EndChild(); + } + ImGui::End(); +} + +void editor_mode::render_profile_source() +{ + auto &state{m_profile}; + auto *selected{selected_track()}; + if (ImGui::Button(STR_C("Along the line from the selected path")) && selected != nullptr) + profile_open_run(*selected); + ImGui::SameLine(); + ImGui::PushItemWidth(70.0f); + ImGui::DragScalar(STR_C("m each way"), ImGuiDataType_Double, &state.run_length, 10.0f, nullptr, nullptr, "%.0f"); + ImGui::PopItemWidth(); + state.run_length = std::clamp(state.run_length, 10.0, 50000.0); + if (ImGui::Button(STR_C("Start: selected path")) && selected != nullptr) + state.picked_from = selected; + ImGui::SameLine(); + if (ImGui::Button(STR_C("End: selected path")) && selected != nullptr && state.picked_from != nullptr) + profile_open(state.picked_from, selected); + profile_library_load(); + if (false == m_profiles.empty() && ImGui::TreeNode("##profilestored", STR_C("Designs kept in the scenery (%zu)"), m_profiles.size())) + { + for (std::size_t i = 0; i < m_profiles.size(); ++i) + { + auto const &entry{m_profiles[i]}; + ImGui::PushID(static_cast(i)); + if (ImGui::SmallButton(STR_C("Open"))) + profile_open_stored(i); + ImGui::SameLine(); + ImGui::Text(STR_C("%s %.1f m, %zu points, from (%.1f, %.1f) to (%.1f, %.1f)"), scene::Layers.layer(entry.layer).name.c_str(), entry.length, entry.line.points.size(), entry.start.x, entry.start.z, entry.end.x, entry.end.z); + ImGui::PopID(); + } + ImGui::TreePop(); + } +} + +bool editor_mode::render_profile_parameters() +{ + auto &state{m_profile}; + auto &line{state.line}; + bool changed{false}; + auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); }; + ImGui::PushItemWidth(120.0f); + drag("Chainage of the start (m)", state.origin, 1.0f, "%.2f"); + changed |= drag("Design speed (km/h)", line.speed, 1.0f, "%.0f"); + ImGui::PopItemWidth(); + line.speed = std::max(1.0, line.speed); + auto const join_tooltip = [] { + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Keeps the elevation of the joint and takes over the grade of the adjoining track,\nwith a vertical curve which starts right at the joint")); + }; + if (state.context.start_joined) + { + changed |= ImGui::Checkbox(STR_C("Join the adjoining track at the start"), &line.join_start); + join_tooltip(); + } + if (state.context.end_joined) + { + changed |= ImGui::Checkbox(STR_C("Join the adjoining track at the end"), &line.join_end); + join_tooltip(); + } + if (ImGui::TreeNode(STR_C("Limits"))) + { + auto &norms{line.norms}; + ImGui::PushItemWidth(90.0f); + changed |= drag("Maximum grade (per mille)", norms.grade_max, 0.1f, "%.2f"); + changed |= drag("Maximum grade over switches (per mille)", norms.grade_switch_max, 0.1f, "%.2f"); + changed |= drag("Vertical curve R >= k*V^2, k", norms.radius_factor, 0.01f, "%.2f"); + changed |= drag("Vertical curve R min (m)", norms.radius_min, 10.0f, "%.0f"); + changed |= drag("Vertical curve needed above a change of (per mille)", norms.curve_threshold, 0.1f, "%.2f"); + changed |= drag("Constant grade between curves at least (m)", norms.element_min, 1.0f, "%.0f"); + changed |= drag("Tolerance at fixed points (m)", norms.fixed_tolerance, 0.001f, "%.3f"); + ImGui::PopItemWidth(); + if (ImGui::SmallButton(STR_C("Automatic R at all points"))) + { + for (auto &point : line.points) + point.automatic = true; + changed = true; + } + ImGui::TextDisabled(STR_C("Required R for %.0f km/h: %.0f m"), line.speed, profile::required_radius(line)); + ImGui::TreePop(); + } + + ImGui::Separator(); + auto const errors{std::count_if(state.issues.begin(), state.issues.end(), [](profile::issue const &Issue) { return Issue.error; })}; + auto const warnings{static_cast(state.issues.size()) - errors}; + if (errors > 0) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), STR_C("%d errors, %d warnings: see the Remarks in the strip"), static_cast(errors), static_cast(warnings)); + else if (warnings > 0) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), STR_C("%d warnings: see the Remarks in the strip"), static_cast(warnings)); + else + ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "%s", STR_C("The grade line meets the limits")); + ImGui::TextDisabled(STR_C("Largest departure from the track: %.3f m"), state.departure); + if (ImGui::Button(STR_C("Recognize from the track"))) + { + profile_recognize(); + state.changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Sets the points of the grade line from the existing track")); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Fit to the ground"))) + { + profile_fit_ground(); + state.changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Places the breaks of the grade line along the ground under the route,\nwithin the limits and with the least earthworks; the ground itself stays as it is")); + ImGui::PushItemWidth(70.0f); + ImGui::DragScalar(STR_C("m off the ground##fittolerance"), ImGuiDataType_Double, &state.fit.tolerance, 0.05f, nullptr, nullptr, "%.2f"); + ImGui::PopItemWidth(); + state.fit.tolerance = std::clamp(state.fit.tolerance, 0.05, 50.0); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("How far the grade line may depart from the ground before it breaks;\nmore gives fewer breaks and higher embankments and deeper cuttings")); + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.68f, 0.3f, 1.0f)); + if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 0.0f))) + profile_apply(); + ImGui::PopStyleColor(2); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Sets the heights of the track along the route to the grade line, Ctrl+Z takes it back")); + ImGui::Separator(); + render_profile_earthworks(); + return changed; +} + +void editor_mode::render_profile_issues() +{ + auto &state{m_profile}; + ImGui::BeginChild("##profileissues", ImVec2(0.0f, 0.0f), false); + if (state.issues.empty()) + ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "%s", STR_C("The grade line meets the limits")); + else + ImGui::TextDisabled("%s", STR_C("LMB on a remark shows its place")); + for (std::size_t i = 0; i < state.issues.size(); ++i) + { + auto const &issue{state.issues[i]}; + ImGui::PushID(static_cast(i)); + ImGui::PushStyleColor(ImGuiCol_Text, issue.error ? ImVec4(1.0f, 0.4f, 0.3f, 1.0f) : ImVec4(1.0f, 0.8f, 0.3f, 1.0f)); + auto const text{format("%s km %s %s", issue.error ? "x" : "!", profile::format_chainage(issue.chainage + state.origin).c_str(), issue.text.c_str())}; + if (ImGui::Selectable(text.c_str())) + { + if (issue.point >= 0) + state.selected = issue.point; + auto const half{(state.view_to - state.view_from) * 0.5}; + state.view_from = issue.chainage - half; + state.view_to = issue.chainage + half; + } + ImGui::PopStyleColor(); + ImGui::PopID(); + } + if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + if (false == state.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", state.error.c_str()); + ImGui::EndChild(); +} + +void editor_mode::render_profile_point() +{ + auto &state{m_profile}; + auto &points{state.line.points}; + bool changed{false}; + auto const count{static_cast(points.size())}; + if (state.selected >= count) + state.selected = -1; + auto const drag = [](char const *Label, double &Value, float const Speed, char const *Format) { return ImGui::DragScalar(STR_C(Label), ImGuiDataType_Double, &Value, Speed, nullptr, nullptr, Format); }; + auto const kind_of = [](double const Change) { return Change > 0.0 ? STR_C("sag") : STR_C("crest"); }; + if (state.selected < 0) + { + ImGui::TextWrapped("%s", STR_C("No point selected. Click a point of the grade line in the plot, or double click the line to add one.")); + } + else if (points[state.selected].joint) + { + auto const index{state.selected}; + auto const &point{points[index]}; + auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0}; + ImGui::Text(STR_C("Point %d: joint with the adjoining track"), index + 1); + ImGui::Text(STR_C("km %s H %.3f m R %.0f m"), profile::format_chainage(point.chainage + state.origin).c_str(), point.elevation, point.radius); + ImGui::Text(STR_C("Grade %+.2f -> %+.2f per mille (%s)"), profile::grade_after(state.line, index - 1) * 1000.0, profile::grade_after(state.line, index) * 1000.0, kind_of(change)); + ImGui::TextWrapped("%s", STR_C("Placed automatically, moves with the neighbouring point; untick the joining in the inspector to edit the end freely")); + } + else + { + auto const index{state.selected}; + auto &point{points[index]}; + bool const end{index == 0 || index == count - 1}; + bool const locked{end_locked(state.line, state.context, index)}; + ImGui::Text(STR_C("Point %d"), index + 1); + ImGui::SameLine(); + ImGui::TextDisabled(STR_C("km %s%s"), profile::format_chainage(point.chainage + state.origin).c_str(), locked ? STR_C(", joined to the adjoining track") : end ? STR_C(", end of the route") : ""); + ImGui::PushItemWidth(std::max(90.0f, ImGui::GetContentRegionAvail().x * 0.4f)); + auto chainage{point.chainage + state.origin}; + if (false == end && drag("Chainage (m)", chainage, 0.5f, "%.2f")) + { + point.chainage = std::clamp(chainage - state.origin, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); + changed = true; + } + if (locked) + ImGui::Text(STR_C("Elevation %.3f m"), point.elevation); + else + changed |= drag("Elevation (m)", point.elevation, 0.01f, "%.3f"); + if (index > 0 && false == locked) + { + auto grade{profile::grade_after(state.line, index - 1) * 1000.0}; + if (drag("Grade before (per mille)", grade, 0.05f, "%+.2f")) + { + point.elevation = points[index - 1].elevation + grade / 1000.0 * (point.chainage - points[index - 1].chainage); + changed = true; + } + } + if (index + 1 < count) + { + auto grade{profile::grade_after(state.line, index) * 1000.0}; + if (drag("Grade after (per mille)", grade, 0.05f, "%+.2f")) + { + points[index + 1].elevation = point.elevation + grade / 1000.0 * (points[index + 1].chainage - point.chainage); + changed = true; + } + } + if (false == end) + { + if (drag("Vertical curve R (m)", point.radius, 10.0f, "%.0f")) + { + point.automatic = false; + changed = true; + } + point.radius = std::max(0.0, point.radius); + ImGui::SameLine(); + changed |= ImGui::Checkbox(STR_C("auto"), &point.automatic); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("R follows the limits: the required radius where the change of grade needs a curve; 0: no curve")); + auto const change{(profile::grade_after(state.line, index) - profile::grade_after(state.line, index - 1)) * 1000.0}; + auto const tangent{point.radius * std::abs(change) / 2000.0}; + ImGui::TextDisabled(STR_C("Change of grade %+.2f per mille, %s"), change, kind_of(change)); + if (point.radius > 0.0) + ImGui::TextDisabled(STR_C("Tangent T %.2f m, sagitta f %.3f m"), tangent, tangent * tangent / (2.0 * point.radius)); + } + ImGui::PopItemWidth(); + if (index + 1 < count && ImGui::Button(STR_C("Add a point after"))) + { + profile::pvi inserted; + inserted.chainage = (point.chainage + points[index + 1].chainage) * 0.5; + inserted.elevation = (point.elevation + points[index + 1].elevation) * 0.5; + points.insert(points.begin() + index + 1, inserted); + state.selected = index + 1; + changed = true; + } + if (false == end) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Remove Del"))) + { + points.erase(points.begin() + index); + state.selected = -1; + changed = true; + } + } + } + if (changed) + profile_edited(); +} + +void editor_mode::render_profile_points() +{ + auto &state{m_profile}; + auto const &points{state.line.points}; + ImGui::Columns(4, "##profilepointcolumns", false); + for (auto const *header : {"#", "km", "H (m)", "R (m)"}) + { + ImGui::TextDisabled("%s", header); + ImGui::NextColumn(); + } + ImGui::Separator(); + for (int i = 0; i < static_cast(points.size()); ++i) + { + auto const &point{points[i]}; + if (ImGui::Selectable(format("%d##point%d", i + 1, i).c_str(), state.selected == i, ImGuiSelectableFlags_SpanAllColumns)) + { + state.selected = i; + auto const half{(state.view_to - state.view_from) * 0.5}; + if (point.chainage < state.view_from || point.chainage > state.view_to) + { + state.view_from = point.chainage - half; + state.view_to = point.chainage + half; + } + } + ImGui::NextColumn(); + ImGui::TextUnformatted(profile::format_chainage(point.chainage + state.origin).c_str()); + ImGui::NextColumn(); + ImGui::Text("%.3f", point.elevation); + ImGui::NextColumn(); + if (point.radius > 0.0) + ImGui::Text("%.0f%s", point.radius, point.joint ? STR_C(" joint") : point.automatic ? STR_C(" auto") : ""); + else + ImGui::TextDisabled("-"); + ImGui::NextColumn(); + } + ImGui::Columns(1); +} + +// screen mapping of the profile canvas, chainage across and elevation upwards +struct editor_mode::profile_view +{ + ImVec2 corner; + ImVec2 size; + float strip; + float top; + float height; + double across; + double vertical; + double from; + double centre; + + profile_view(profile_state const &State, ImVec2 const Corner, ImVec2 const Size) : + corner{Corner}, size{Size}, strip{State.show_plan ? 44.0f : 0.0f}, top{18.0f}, height{Size.y - strip - top}, + across{std::max(1.0, State.view_to - State.view_from) / Size.x}, vertical{across / State.exaggeration}, from{State.view_from}, centre{State.view_centre} + { + } + float x_of(double const Chainage) const { return corner.x + static_cast((Chainage - from) / across); } + float y_of(double const Height) const { return corner.y + top + height * 0.5f - static_cast((Height - centre) / vertical); } + double chainage_of(float const X) const { return from + (X - corner.x) * across; } + double height_of(float const Y) const { return centre - (Y - corner.y - top - height * 0.5f) * vertical; } + float plot_top() const { return corner.y + top; } + float plot_bottom() const { return corner.y + top + height; } + ImVec2 bottom_right() const { return {corner.x + size.x, corner.y + size.y}; } + float mouse_distance2(double const Chainage, double const Height) const + { + auto const &mouse{ImGui::GetIO().MousePos}; + auto const dx{x_of(Chainage) - mouse.x}; + auto const dy{y_of(Height) - mouse.y}; + return dx * dx + dy * dy; + } +}; + +void editor_mode::render_profile_toolbar() +{ + auto &state{m_profile}; + auto *draw{ImGui::GetWindowDrawList()}; + auto const swatch = [&](ImU32 const Colour) { + auto const at{ImGui::GetCursorScreenPos()}; + auto const height{ImGui::GetFrameHeight()}; + draw->AddLine(ImVec2(at.x, at.y + height * 0.5f), ImVec2(at.x + 16.0f, at.y + height * 0.5f), Colour, 3.0f); + ImGui::Dummy(ImVec2(16.0f, height)); + ImGui::SameLine(0.0f, 4.0f); + }; + auto const legend = [&](ImU32 const Colour, char const *Label) { + swatch(Colour); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(STR_C(Label)); + ImGui::SameLine(0.0f, 14.0f); + }; + auto const toggle = [&](ImU32 const Colour, char const *Label, bool &Value) { + swatch(Colour); + ImGui::Checkbox(STR_C(Label), &Value); + ImGui::SameLine(0.0f, 14.0f); + }; + legend(kGradeLine, "Grade line"); + legend(kTangents, "Tangents"); + toggle(kTrack, STR_C("Track"), state.show_track); + toggle(kTerrain, STR_C("Terrain"), state.show_terrain); + toggle(kPlan, STR_C("Plan curvature"), state.show_plan); + legend(kSwitchLabel, "Switches"); + ImGui::SetNextItemWidth(130.0f); + ImGui::SliderFloat("##exaggeration", &state.exaggeration, 1.0f, 500.0f, STR_C("heights x%.0f"), ImGuiSliderFlags_Logarithmic); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Exaggeration of the heights against the lengths, also Ctrl+wheel over the plot")); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Fit"))) + profile_fit_view(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Shows the whole route")); +} + +void editor_mode::render_profile_canvas(float const Height) +{ + auto &state{m_profile}; + auto const available{ImGui::GetContentRegionAvail()}; + ImVec2 const size{std::max(200.0f, available.x), std::max(100.0f, Height)}; + ImVec2 const corner{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##profilecanvas", size); + bool const hovered{ImGui::IsItemHovered()}; + + if (state.exaggeration <= 0.0f) + profile_fit_exaggeration(profile_view{state, corner, size}); + profile_navigate(profile_view{state, corner, size}, hovered); + profile_view const view{state, corner, size}; + if (profile_canvas_edit(view, hovered)) + profile_edited(); + state.hover = hovered ? view.chainage_of(ImGui::GetIO().MousePos.x) : -1.0; + + auto *draw{ImGui::GetWindowDrawList()}; + draw->AddRectFilled(corner, view.bottom_right(), IM_COL32(22, 24, 28, 255)); + draw->PushClipRect(corner, view.bottom_right(), true); + draw_profile_canvas(view, *draw); + if (hovered && state.hover >= 0.0 && state.hover <= state.route.length && false == state.samples.empty()) + { + auto const x{view.x_of(state.hover)}; + draw->AddLine(ImVec2(x, view.plot_top()), ImVec2(x, view.bottom_right().y), IM_COL32(255, 255, 255, 90)); + if (state.hover_point < 0 && state.hover_grip < 0 && state.curve_grip < 0) + profile_canvas_tooltip(); + } + + // what the mouse does over the thing under it + char const *hint{"wheel: zoom Ctrl+wheel: heights RMB/MMB drag: pan double click on the line: add a point"}; + if (state.dragging >= 0) + hint = "Ctrl: only the height Shift: keep the grade before"; + else if (state.curve_grip >= 0 || state.hover_grip >= 0) + hint = "drag up or down: radius of the vertical curve"; + else if (state.hover_point >= 0) + hint = "drag: move the point Ctrl+drag: only the height Shift+drag: keep the grade before Del: remove"; + else if (state.hover_line) + hint = "double click: add a point here"; + if (hovered || state.dragging >= 0 || state.curve_grip >= 0) + { + auto const text{std::string{STR_C(hint)}}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + plot_label(*draw, ImVec2(view.bottom_right().x - size.x - 8.0f, view.plot_bottom() - size.y - 6.0f), IM_COL32(255, 255, 255, 230), text); + } + draw->PopClipRect(); + + auto const &points{state.line.points}; + if (state.dragging >= 0 && state.dragging < static_cast(points.size())) + { + auto const index{state.dragging}; + ImGui::BeginTooltip(); + ImGui::Text(STR_C("Point %d"), index + 1); + ImGui::Text(STR_C("km %s H %.3f m"), profile::format_chainage(points[index].chainage + state.origin).c_str(), points[index].elevation); + if (index > 0) + ImGui::Text(STR_C("before %+.2f per mille"), profile::grade_after(state.line, index - 1) * 1000.0); + if (index + 1 < static_cast(points.size())) + ImGui::Text(STR_C("after %+.2f per mille"), profile::grade_after(state.line, index) * 1000.0); + ImGui::EndTooltip(); + } + else if (state.curve_grip > 0 && state.curve_grip < static_cast(points.size())) + { + ImGui::BeginTooltip(); + ImGui::Text("R %.0f m", points[state.curve_grip].radius); + ImGui::EndTooltip(); + } +} + +void editor_mode::profile_fit_exaggeration(profile_view const &View) +{ + auto &state{m_profile}; + double low{std::numeric_limits::max()}, high{std::numeric_limits::lowest()}; + for (auto const &sample : state.samples) + { + low = std::min(low, sample.position.y); + high = std::max(high, sample.position.y); + } + for (auto const &point : state.line.points) + { + low = std::min(low, point.elevation); + high = std::max(high, point.elevation); + } + if (low > high) + low = high = 0.0; + auto const range{std::max(1.0, (high - low) * 1.3)}; + state.exaggeration = static_cast(std::clamp(View.across / (range / View.height), 1.0, 500.0)); + state.view_centre = (low + high) * 0.5; +} + +void editor_mode::profile_navigate(profile_view const &View, bool const Hovered) +{ + auto &state{m_profile}; + ImGuiIO const &io{ImGui::GetIO()}; + if (Hovered && io.MouseWheel != 0.0f) + { + if (io.KeyCtrl) + state.exaggeration = std::clamp(state.exaggeration * std::pow(1.2f, io.MouseWheel), 1.0f, 500.0f); + else + { + auto const anchor{View.chainage_of(io.MousePos.x)}; + auto const factor{std::pow(0.8, static_cast(io.MouseWheel))}; + state.view_from = anchor - (anchor - state.view_from) * factor; + state.view_to = anchor + (state.view_to - anchor) * factor; + } + } + if (Hovered && (ImGui::IsMouseClicked(1) || ImGui::IsMouseClicked(2))) + state.panning = true; + if (state.panning) + { + if (ImGui::IsMouseDown(1) || ImGui::IsMouseDown(2)) + { + state.view_from -= io.MouseDelta.x * View.across; + state.view_to -= io.MouseDelta.x * View.across; + state.view_centre += io.MouseDelta.y * View.vertical; + } + else + state.panning = false; + } +} + +bool editor_mode::profile_canvas_edit(profile_view const &View, bool const Hovered) +{ + auto &state{m_profile}; + auto &line{state.line}; + auto &points{line.points}; + ImGuiIO const &io{ImGui::GetIO()}; + ImVec2 const mouse{io.MousePos}; + + auto const hit_point = [&]() { + int hit{-1}; + float best{64.0f}; + for (int i = 0; i < static_cast(points.size()); ++i) + { + if (points[i].joint) + continue; + if (auto const distance{View.mouse_distance2(points[i].chainage, points[i].elevation)}; distance < best) + { + best = distance; + hit = i; + } + } + return hit; + }; + auto const hit_grip = [&]() { + for (auto const &c : profile::curves(line)) + { + if (points[c.point].joint) + continue; + auto const chainage{points[c.point].chainage}; + if (View.mouse_distance2(chainage, profile::elevation(line, chainage)) < 64.0f) + return static_cast(c.point); + } + return -1; + }; + bool changed{false}; + bool const busy{state.dragging >= 0 || state.curve_grip >= 0 || state.panning}; + state.hover_point = Hovered && false == busy ? hit_point() : -1; + state.hover_grip = Hovered && false == busy && state.hover_point < 0 ? hit_grip() : -1; + state.hover_line = false; + if (Hovered && false == busy && state.hover_point < 0 && state.hover_grip < 0 && points.size() >= 2) + { + auto const chainage{View.chainage_of(mouse.x)}; + state.hover_line = chainage > points.front().chainage && chainage < points.back().chainage && std::abs(View.y_of(profile::elevation(line, chainage)) - mouse.y) < 12.0f; + } + if (state.dragging >= 0 || state.panning) + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); + else if (state.curve_grip >= 0 || state.hover_grip >= 0) + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS); + else if (state.hover_point >= 0 || state.hover_line) + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + if (Hovered && ImGui::IsMouseClicked(0)) + { + auto const point{hit_point()}; + auto const grip{point < 0 ? hit_grip() : -1}; + if (point >= 0) + state.selected = state.dragging = point; + else if (grip >= 0) + state.selected = state.curve_grip = grip; + else + state.selected = -1; + } + if (Hovered && ImGui::IsMouseDoubleClicked(0) && state.dragging < 0 && state.curve_grip < 0 && points.size() >= 2) + { + auto const chainage{View.chainage_of(mouse.x)}; + if (chainage > points.front().chainage + 0.5 && chainage < points.back().chainage - 0.5 && std::abs(View.y_of(profile::elevation(line, chainage)) - mouse.y) < 12.0f) + { + std::size_t index{0}; + while (index + 2 < points.size() && points[index + 1].chainage < chainage) + ++index; + profile::pvi inserted; + inserted.chainage = std::round(chainage * 100.0) / 100.0; + inserted.elevation = points[index].elevation + profile::grade_after(line, index) * (inserted.chainage - points[index].chainage); + points.insert(points.begin() + index + 1, inserted); + state.selected = static_cast(index) + 1; + changed = true; + } + } + if (state.dragging >= 0 && state.dragging < static_cast(points.size())) + { + if (ImGui::IsMouseDown(0)) + { + auto const index{state.dragging}; + auto &point{points[index]}; + bool const end{index == 0 || index + 1 == static_cast(points.size())}; + auto const incoming{index > 0 ? profile::grade_after(line, index - 1) : 0.0}; + if (false == end && false == io.KeyCtrl) + point.chainage = std::clamp(std::round(View.chainage_of(mouse.x) * 100.0) / 100.0, points[index - 1].chainage + 0.5, points[index + 1].chainage - 0.5); + if (end_locked(line, state.context, index)) + ; + else if (io.KeyShift && index > 0) + point.elevation = points[index - 1].elevation + incoming * (point.chainage - points[index - 1].chainage); + else + point.elevation = std::round(View.height_of(mouse.y) * 1000.0) / 1000.0; + changed = true; + } + else + state.dragging = -1; + } + else + state.dragging = -1; + if (state.curve_grip > 0 && state.curve_grip + 1 < static_cast(points.size())) + { + if (ImGui::IsMouseDown(0)) + { + auto const sagitta{View.height_of(mouse.y) - points[state.curve_grip].elevation}; + points[state.curve_grip].radius = std::max(10.0, std::round(profile::radius_for_sagitta(line, state.curve_grip, sagitta) / 10.0) * 10.0); + points[state.curve_grip].automatic = false; + changed = true; + } + else + state.curve_grip = -1; + } + else + state.curve_grip = -1; + if ((Hovered || ImGui::IsWindowFocused()) && state.selected > 0 && state.selected + 1 < static_cast(points.size()) && false == points[state.selected].joint && ImGui::IsKeyPressed(ImGuiKey_Delete)) + { + points.erase(points.begin() + state.selected); + state.selected = -1; + changed = true; + } + return changed; +} + +void editor_mode::draw_profile_canvas(profile_view const &View, ImDrawList &Draw) const +{ + auto const &state{m_profile}; + auto const &line{state.line}; + auto const &points{line.points}; + auto const &samples{state.samples}; + auto const bottomright{View.bottom_right()}; + auto const plottop{View.plot_top()}; + auto const plotbottom{View.plot_bottom()}; + + auto const step{nice_step(View.across * 110.0)}; + auto const firsts{std::ceil((state.view_from + state.origin) / step) * step - state.origin}; + for (int i = 0; firsts + i * step <= state.view_to; ++i) + { + auto const s{firsts + i * step}; + auto const x{View.x_of(s)}; + Draw.AddLine(ImVec2(x, plottop), ImVec2(x, plotbottom), kGrid); + Draw.AddText(ImVec2(x + 3.0f, View.corner.y + 2.0f), kLabel, profile::format_chainage(s + state.origin).c_str()); + } + auto const heightstep{nice_step(View.vertical * 45.0)}; + auto const firsth{std::floor(View.height_of(plotbottom) / heightstep) * heightstep}; + for (int i = 0; firsth + i * heightstep <= View.height_of(plottop); ++i) + { + auto const y{View.y_of(firsth + i * heightstep)}; + Draw.AddLine(ImVec2(View.corner.x, y), ImVec2(bottomright.x, y), kGrid); + } + for (auto const &zone : state.context.switches) + { + Draw.AddRectFilled(ImVec2(View.x_of(zone.from), plottop), ImVec2(std::max(View.x_of(zone.to), View.x_of(zone.from) + 2.0f), plotbottom), kSwitch); + if (View.x_of(zone.to) - View.x_of(zone.from) > 20.0f || View.across < 0.5) + Draw.AddText(ImVec2(View.x_of(zone.from) + 2.0f, plotbottom - 16.0f), kSwitchLabel, zone.name.c_str()); + } + + auto const polyline = [&](auto const &Height, ImU32 const Colour, float const Thickness) { + ImVec2 previous{}; + bool open{false}; + float lastx{-1e9f}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const x{View.x_of(samples[i].chainage)}; + auto const h{Height(i)}; + if (std::isnan(h) || x < View.corner.x - 50.0f || x > bottomright.x + 50.0f) + { + open = false; + continue; + } + if (open && x - lastx < 1.0f && i + 1 < samples.size()) + continue; + ImVec2 const next{x, View.y_of(h)}; + if (open) + Draw.AddLine(previous, next, Colour, Thickness); + previous = next; + lastx = x; + open = true; + } + }; + if (state.show_terrain) + { + auto const fill{(kTerrain & 0x00FFFFFFu) | 0x30000000u}; + float lastx{-1e9f}; + ImVec2 previous{}; + bool open{false}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const x{View.x_of(samples[i].chainage)}; + auto const h{state.terrain[i]}; + if (std::isnan(h) || x < View.corner.x - 50.0f || x > bottomright.x + 50.0f) + { + open = false; + continue; + } + if (open && x - lastx < 2.0f && i + 1 < samples.size()) + continue; + ImVec2 const next{x, std::min(View.y_of(h), plotbottom)}; + if (open) + Draw.AddQuadFilled(previous, next, ImVec2(next.x, plotbottom), ImVec2(previous.x, plotbottom), fill); + previous = next; + lastx = x; + open = true; + } + polyline([&](std::size_t const I) { return state.terrain[I]; }, kTerrain, 1.5f); + } + if (state.show_track) + polyline([&](std::size_t const I) { return samples[I].position.y; }, kTrack, 1.5f); + + for (std::size_t i = 0; i + 1 < points.size(); ++i) + Draw.AddLine(ImVec2(View.x_of(points[i].chainage), View.y_of(points[i].elevation)), ImVec2(View.x_of(points[i + 1].chainage), View.y_of(points[i + 1].elevation)), kTangents, 1.0f); + { + auto const from{std::max(points.front().chainage, View.chainage_of(View.corner.x))}; + auto const to{std::min(points.back().chainage, View.chainage_of(bottomright.x))}; + ImVec2 previous{View.x_of(from), View.y_of(profile::elevation(line, from))}; + auto const firstx{previous.x}; + for (int i = 1; firstx + 2.0f * i <= View.x_of(to) + 2.0f; ++i) + { + auto const x{firstx + 2.0f * i}; + auto const chainage{std::min(to, View.chainage_of(x))}; + ImVec2 const next{View.x_of(chainage), View.y_of(profile::elevation(line, chainage))}; + Draw.AddLine(previous, next, kGradeLine, 2.5f); + previous = next; + } + } + for (std::size_t i = 0; i + 1 < points.size(); ++i) + { + auto const middle{(points[i].chainage + points[i + 1].chainage) * 0.5}; + auto const x{View.x_of(middle)}; + if (View.x_of(points[i + 1].chainage) - View.x_of(points[i].chainage) < 70.0f) + continue; + auto const text{format("%+.2f o/oo L %.0f m", profile::grade_after(line, i) * 1000.0, points[i + 1].chainage - points[i].chainage)}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + if (View.x_of(points[i + 1].chainage) - View.x_of(points[i].chainage) < size.x + 30.0f) + continue; + plot_label(Draw, ImVec2(x - size.x * 0.5f, View.y_of(profile::elevation(line, middle)) - size.y - 10.0f), kGradeLine, text); + } + for (auto const &c : profile::curves(line)) + { + for (auto const at : {c.start, c.end}) + { + auto const y{View.y_of(profile::elevation(line, at))}; + Draw.AddLine(ImVec2(View.x_of(at), y - 7.0f), ImVec2(View.x_of(at), y + 7.0f), kGradeLine, 1.5f); + } + auto const chainage{points[c.point].chainage}; + ImVec2 const grip{View.x_of(chainage), View.y_of(profile::elevation(line, chainage))}; + bool const lit{static_cast(c.point) == state.curve_grip || static_cast(c.point) == state.hover_grip}; + Draw.AddCircleFilled(grip, lit ? 6.5f : 4.5f, overlay_color::grip, 4); + if (lit) + Draw.AddCircle(grip, 10.0f, overlay_color::highlight, 12, 2.0f); + plot_label(Draw, ImVec2(grip.x + 8.0f, grip.y + 6.0f), overlay_color::grip, format("R %.0f", points[c.point].radius)); + } + for (int i = 0; i < static_cast(points.size()); ++i) + { + ImVec2 const at{View.x_of(points[i].chainage), View.y_of(points[i].elevation)}; + bool const lit{i == state.hover_point || i == state.dragging}; + auto const radius{lit ? 7.0f : 5.5f}; + if (i == 0 || i + 1 == static_cast(points.size())) + Draw.AddRectFilled(ImVec2(at.x - radius, at.y - radius), ImVec2(at.x + radius, at.y + radius), IM_COL32(70, 140, 255, 255)); + else if (points[i].joint) + Draw.AddQuadFilled(ImVec2(at.x, at.y - radius), ImVec2(at.x + radius, at.y), ImVec2(at.x, at.y + radius), ImVec2(at.x - radius, at.y), IM_COL32(90, 210, 230, 255)); + else + Draw.AddCircleFilled(at, radius, overlay_color::marked); + if (i == state.selected) + Draw.AddCircle(at, 11.0f, overlay_color::highlight, 16, 2.5f); + else if (lit) + Draw.AddCircle(at, 11.0f, IM_COL32(255, 255, 255, 160), 16, 1.5f); + // the heights only where they're asked for, the numbers of the points always + auto const text{i == state.selected || lit ? format("%d H %.3f", i + 1, points[i].elevation) : std::to_string(i + 1)}; + plot_label(Draw, ImVec2(at.x + 8.0f, at.y - 22.0f), overlay_color::marked, text); + } + // heights along the left edge, over everything else + for (int i = 0; firsth + i * heightstep <= View.height_of(plottop); ++i) + plot_label(Draw, ImVec2(View.corner.x + 4.0f, View.y_of(firsth + i * heightstep) - 8.0f), kLabel, format("%.1f", firsth + i * heightstep)); + for (auto const &issue : state.issues) + { + auto const x{View.x_of(issue.chainage)}; + Draw.AddTriangleFilled(ImVec2(x - 5.0f, plottop), ImVec2(x + 5.0f, plottop), ImVec2(x, plottop + 8.0f), issue.error ? kError : kWarning); + } + if (state.show_plan && false == samples.empty()) + { + auto const middle{plotbottom + View.strip * 0.5f}; + Draw.AddLine(ImVec2(View.corner.x, plotbottom), ImVec2(bottomright.x, plotbottom), IM_COL32(255, 255, 255, 60)); + Draw.AddLine(ImVec2(View.corner.x, middle), ImVec2(bottomright.x, middle), IM_COL32(255, 255, 255, 40)); + ImVec2 previous{}; + bool open{false}; + float lastx{-1e9f}; + for (std::size_t i = 0; i < samples.size(); ++i) + { + auto const x{View.x_of(samples[i].chainage)}; + if (x < View.corner.x - 50.0f || x > bottomright.x + 50.0f || (open && x - lastx < 1.0f)) + continue; + ImVec2 const next{x, middle - std::clamp(static_cast(samples[i].curvature * 300.0 * View.strip * 0.4), -View.strip * 0.45f, View.strip * 0.45f)}; + if (open) + Draw.AddLine(previous, next, IM_COL32(120, 190, 255, 255), 1.5f); + previous = next; + lastx = x; + open = true; + } + Draw.AddText(ImVec2(View.corner.x + 4.0f, plotbottom + 1.0f), kLabel, STR_C("plan: curvature (up: left)")); + } +} + +void editor_mode::profile_canvas_tooltip() const +{ + auto const &state{m_profile}; + auto const &samples{state.samples}; + auto const designed{profile::elevation(state.line, state.hover)}; + auto const existing{editor_track::sampled_elevation(samples, state.hover)}; + ImGui::BeginTooltip(); + ImGui::Text(STR_C("km %s"), profile::format_chainage(state.hover + state.origin).c_str()); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kGradeLine), STR_C("Grade line %.3f m, %+.2f per mille"), designed, profile::grade(state.line, state.hover) * 1000.0); + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kTrack), STR_C("Track %.3f m (%+.3f), %+.2f per mille"), existing, existing - designed, editor_track::sampled_grade(samples, state.hover) * 1000.0); + auto const next{std::lower_bound(samples.begin(), samples.end(), state.hover, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + auto const index{static_cast(std::distance(samples.begin(), std::min(next, std::prev(samples.end()))))}; + if (index < state.terrain.size() && false == std::isnan(state.terrain[index])) + ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(kTerrain), STR_C("Terrain %.3f m, %s %.2f m"), state.terrain[index], designed > state.terrain[index] ? STR_C("fill") : STR_C("cut"), std::abs(designed - state.terrain[index])); + auto const &span{state.route.spans[samples[index].span]}; + ImGui::TextDisabled("%s%s", span.track->name().c_str(), span.track->eType != tt_Normal ? STR_C(" (switch, tilted as a whole with its branch)") : ""); + ImGui::EndTooltip(); +} + +void editor_mode::draw_profile_overlay() const +{ + auto const &state{m_profile}; + if (false == state.open || state.samples.size() < 2) + return; + screen_projection const projection; + ImDrawList *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const &samples{state.samples}; + glm::dvec3 previous{samples.front().position}; + glm::dvec3 designedprevious{previous.x, profile::elevation(state.line, 0.0), previous.z}; + for (std::size_t i = 1; i < samples.size(); ++i) + { + if (glm::distance(samples[i].position, previous) < 2.0 && i + 1 < samples.size()) + continue; + auto const &next{samples[i].position}; + projection.line(drawlist, previous, next, IM_COL32(255, 150, 40, 200), 2.5f); + glm::dvec3 const designed{next.x, profile::elevation(state.line, samples[i].chainage), next.z}; + if (std::abs(designed.y - next.y) > 0.01 || std::abs(designedprevious.y - previous.y) > 0.01) + projection.line(drawlist, designedprevious, designed, kGradeLine, 2.0f); + previous = next; + designedprevious = designed; + } + auto const &points{state.line.points}; + for (int i = 0; i < static_cast(points.size()); ++i) + { + auto position{editor_track::sampled_position(samples, points[i].chainage)}; + position.y = profile::elevation(state.line, points[i].chainage); + ImVec2 screen; + if (false == projection.project(position, screen)) + continue; + drawlist->AddCircleFilled(screen, i == state.selected ? 7.0f : 5.0f, overlay_color::marked); + drawlist->AddText(ImVec2(screen.x + 8.0f, screen.y - 16.0f), overlay_color::marked, format("%d", i + 1).c_str()); + } + if (state.hover >= 0.0 && state.hover <= state.route.length) + { + ImVec2 screen; + if (projection.project(editor_track::sampled_position(samples, state.hover), screen)) + drawlist->AddCircle(screen, 12.0f, IM_COL32(255, 255, 255, 230), 16, 2.5f); + } +} diff --git a/application/editormoderoad.cpp b/application/editormoderoad.cpp new file mode 100644 index 00000000..a07da795 --- /dev/null +++ b/application/editormoderoad.cpp @@ -0,0 +1,2822 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "world/Road.h" +#include "world/RoadPoint.h" +#include "simulation/simulation.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include + +namespace +{ + +// the same helper the track tools use to draw over the viewport +class screen_projection +{ + public: + screen_projection() + { + ImGuiIO const &io = ImGui::GetIO(); + m_size = io.DisplaySize; + m_camera = GfxRenderer->Camera_Position(); + float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; + m_viewprojection = editor_mode::projection_matrix(aspect) * GfxRenderer->Camera_View_Matrix(); + } + + glm::vec4 clip(glm::dvec3 const &Point) const + { + return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); + } + ImVec2 screen(glm::vec4 const &Clip) const + { + return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); + } + bool project(glm::dvec3 const &Point, ImVec2 &Screen) const + { + auto const c = clip(Point); + if (c.w < kNear) + return false; + Screen = screen(c); + return true; + } + // true while there's room in the draw list for more. the user interface is drawn with 16 bit indices, so a list + // can't hold more than 65536 vertices; past that the lines come out as triangles strewn all over the screen, + // which takes the frame rate down with it. the limit doesn't apply if the renderer can deal with larger lists + bool room(ImDrawList const *Drawlist) const + { + return (Drawlist->Flags & ImDrawListFlags_AllowVtxOffset) != 0 || Drawlist->VtxBuffer.Size < limit; + } + // true if a line between two points on the screen can't be seen + bool outside(ImVec2 const &A, ImVec2 const &B) const + { + return (A.x < 0.0f && B.x < 0.0f) || (A.y < 0.0f && B.y < 0.0f) || (A.x > m_size.x && B.x > m_size.x) || (A.y > m_size.y && B.y > m_size.y); + } + void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const + { + if (false == room(Drawlist)) + return; + glm::vec4 a = clip(A); + glm::vec4 b = clip(B); + if (a.w < kNear && b.w < kNear) + return; + if (a.w < kNear) + a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); + else if (b.w < kNear) + b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); + auto const from{screen(a)}; + auto const to{screen(b)}; + if (outside(from, to)) + return; + Drawlist->AddLine(from, to, Color, Thickness); + } + // members + int limit{kVertexLimit}; // vertices the draw list can hold before the drawing stops + + static constexpr int kVertexLimit{56000}; // leaves room for what's drawn after the overlay + static constexpr int kLaneVertexLimit{36000}; // the lanes give way to the tools, which are drawn after them + + private: + static constexpr float kNear{0.1f}; + ImVec2 m_size; + glm::dvec3 m_camera; + glm::mat4 m_viewprojection; +}; + +glm::dvec3 path_point(segment_data const &Path, double const T) +{ + auto const &p0 = Path.points[segment_data::point::start]; + auto const &p3 = Path.points[segment_data::point::end]; + auto const &cp1 = Path.points[segment_data::point::control1]; + auto const &cp2 = Path.points[segment_data::point::control2]; + if (cp1 == glm::dvec3{0.0} && cp2 == glm::dvec3{0.0}) + return glm::mix(p0, p3, T); + auto const p1 = p0 + cp1; + auto const p2 = p3 + cp2; + double const u = 1.0 - T; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; +} + +bool is_curved(segment_data const &Path) +{ + return Path.points[segment_data::point::control1] != glm::dvec3{0.0} || Path.points[segment_data::point::control2] != glm::dvec3{0.0}; +} + +void copy_text(char *Buffer, std::size_t const Size, std::string const &Text) +{ + std::strncpy(Buffer, Text.c_str(), Size - 1); + Buffer[Size - 1] = 0; +} + +std::string lower_case(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return Text; +} + +double const kSnapRadius{4.0}; // an end of a road this close to the cursor is what the click is taken to mean +double const kPointRadius{2.5}; // a point where pieces meet this close to the click is what gets selected, instead of the piece +double const kPointRange{400.0}; // the points are drawn for the roads this close to the camera +double const kSideMargin{1.0}; // how far past the edge of a road the cursor still counts as being on it +double const kMarkerRadius{2.0}; // a level crossing or a traffic point this close to the click is what gets selected +double const kRailReach{40.0}; // rails crossing the road this close to the cursor are taken for a single level crossing +double const kLaneReach{2.5}; // a lane with its middle this close to the cursor is the one a traffic point is put on +double const kFullTurn{6.283185307179586}; +double const kJoinClearance{15.0}; // a road isn't led into the side of another this close to the point it's led from +double const kReach{3000.0}; // clicks landing further than this are taken for misses +double const kLaneRange{600.0}; // lanes are drawn for the roads this close to the camera +double const kLongestStretch{2000.0}; // a single click doesn't lead a road further than this +// lengths of the pieces a road following the ground is tried with, from the longest; the first one to fit the ground is taken +double const kProfileSteps[]{50.0, 25.0, 12.5}; +double const kSunkTolerance{0.05}; // how far under the height it's built at the road can get between the points it's led through +double const kRaisedTolerance{0.5}; // and how far over it + +ImU32 const kForwardColor{IM_COL32(70, 230, 110, 230)}; +ImU32 const kBackwardColor{IM_COL32(255, 170, 50, 230)}; +ImU32 const kTurnColor{IM_COL32(210, 210, 210, 150)}; +ImU32 const kWayColor{IM_COL32(120, 200, 255, 170)}; +ImU32 const kPointColor{IM_COL32(255, 255, 255, 200)}; +ImU32 const kSelectedColor{IM_COL32(40, 220, 255, 230)}; +ImU32 const kPreviewColor{IM_COL32(255, 210, 60, 255)}; +ImU32 const kRefusedColor{IM_COL32(240, 60, 60, 255)}; +ImU32 const kSnapColor{IM_COL32(255, 60, 255, 255)}; +ImU32 const kCrossingColor{IM_COL32(255, 220, 60, 230)}; +ImU32 const kSpawnColor{IM_COL32(90, 230, 120, 230)}; +ImU32 const kDespawnColor{IM_COL32(255, 110, 110, 230)}; +ImU32 const kCrosswalkColor{IM_COL32(235, 235, 235, 230)}; +double const kBankReach{30.0}; // how far from the road the ground is looked for, for a bank to be led to it +double const kBankStep{0.5}; + +roadpoint_node::kind_type point_kind(int const Index) +{ + return Index == 1 ? roadpoint_node::kind_type::spawn : Index == 2 ? roadpoint_node::kind_type::despawn : Index == 3 ? roadpoint_node::kind_type::crosswalk : roadpoint_node::kind_type::crossing; +} + +char const *point_label(roadpoint_node::kind_type const Kind) +{ + return Kind == roadpoint_node::kind_type::spawn ? "spawn point" : + Kind == roadpoint_node::kind_type::despawn ? "removal point" : + Kind == roadpoint_node::kind_type::crosswalk ? "pedestrian crossing" : + "level crossing"; +} + +// gathers new definitions for the pieces which meet or end at provided points. +// Change: receives a definition, which end of it is at the point, and the number of the point +template +std::vector> point_changes(std::vector const &Points, Change_ Change) +{ + std::vector> changes; + for (std::size_t index = 0; index < Points.size(); ++index) + { + for (auto const &end : editor_road::ends_at(Points[index])) + { + // a piece with both its ends among the points gets both changed + auto entry{std::find_if(changes.begin(), changes.end(), [&end](std::pair const &Listed) { return Listed.first == end.road; })}; + if (entry == changes.end()) + { + changes.emplace_back(end.road, end.road->definition()); + entry = std::prev(changes.end()); + } + Change(entry->second, end.atend, index); + } + } + return changes; +} + +// direction of a path for specified value of its curve parameter. like path_point() it's cheap enough to be asked for a lot each frame +glm::dvec3 path_direction(segment_data const &Path, double const T) +{ + auto const &p0 = Path.points[segment_data::point::start]; + auto const &p3 = Path.points[segment_data::point::end]; + auto const &cp1 = Path.points[segment_data::point::control1]; + auto const &cp2 = Path.points[segment_data::point::control2]; + if (cp1 == glm::dvec3{0.0} && cp2 == glm::dvec3{0.0}) + return p3 - p0; + auto const p1 = p0 + cp1; + auto const p2 = p3 + cp2; + double const u = 1.0 - T; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); +} + +} + +void editor_mode::update_road_tool() +{ + auto &tool{m_roadtool}; + if (tool.selected != nullptr && tool.selected->m_editorremoved) + tool.selected = nullptr; + if (tool.junction != nullptr && tool.junction->m_editorremoved) + tool.junction = nullptr; + if (tool.marker != nullptr && tool.marker->m_editorremoved) + tool.marker = nullptr; + if ((tool.branchroad != nullptr && tool.branchroad->m_editorremoved) || (tool.branchjunction != nullptr && tool.branchjunction->m_editorremoved)) + { + // what the road was to be led out of is gone + tool.branchroad = nullptr; + tool.branchjunction = nullptr; + tool.chain = false; + } + // so are the points whose pieces were removed or changed behind the back of the selection + tool.points.erase(std::remove_if(tool.points.begin(), tool.points.end(), [](glm::dvec3 const &Point) { return editor_road::ends_at(Point).empty(); }), tool.points.end()); + tool.plan = road_plan{}; + tool.hashover = false; + if (false == tool.window || tool.tool != 2) + { + tool.hastarget = false; + tool.aimedkind = -1; + } + if (false == tool.window) + { + tool.chain = false; + return; + } + glm::dvec3 const camera{Global.pCamera.Pos}; + tool.mouse = camera + GfxRenderer->Mouse_Position(); + if (glm::distance(tool.mouse, camera) > kReach) + { + tool.plan.error = "The cursor isn't over anything near enough"; + tool.hastarget = false; + tool.aimedkind = -1; + return; + } + if (tool.tool == 2) + { + // the point a click would make at the moment, to be drawn over the view + roadpoint_aim(false); + return; + } + if (tool.tool != 1) + return; + if (tool.ring) + { + // a click puts a roundabout around the cursor, there's nothing to lead from a point to another + tool.chain = false; + return; + } + if (tool.chain) + { + // what a click would make at the moment, to be drawn over the view + tool.plan = road_preview(false); + return; + } + // where a click would lead a road out of the side of a road, or out of a junction + if (editor_road::find_end(tool.mouse, kSnapRadius).valid()) + return; + if (auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; road != nullptr) + { + tool.hover = road->definition().point(editor_road::nearest_parameter(road->definition(), tool.mouse)); + tool.hashover = true; + } + else if (auto const *junction{editor_road::nearest_junction(tool.mouse)}; junction != nullptr) + { + tool.hover = junction->definition().centre; + tool.hashover = true; + } +} + +void editor_mode::road_select(road_node *Road) +{ + auto &tool{m_roadtool}; + tool.selected = Road; + if (Road != nullptr) + { + tool.junction = nullptr; + tool.settings = Road->definition(); + std::string reason; + tool.status = "Selected: " + (Road->name().empty() ? std::string{"(unnamed road)"} : Road->name()); + if (false == editor_road::can_edit(*Road, &reason)) + tool.status += ". It can't be changed: " + reason; + } + else + { + // the layout stays for the build tool, which makes pieces with the same lanes all the way + tool.settings.missing = {}; + } + tool.varied = tool.settings.changing(); + copy_text(tool.surface, sizeof(tool.surface), tool.settings.surface); + for (int side = 0; side < 2; ++side) + copy_text(tool.sides[side], sizeof(tool.sides[side]), tool.settings.sides[side].material); + copy_text(tool.kerbtext, sizeof(tool.kerbtext), tool.settings.kerbmaterial); + copy_text(tool.mediantext, sizeof(tool.mediantext), tool.settings.median.material); + copy_text(tool.banktext, sizeof(tool.banktext), tool.settings.bankmaterial); +} + +void editor_mode::junction_select(junction_node *Junction) +{ + auto &tool{m_roadtool}; + tool.junction = Junction; + if (Junction == nullptr) + return; + tool.selected = nullptr; + std::string reason; + tool.status = "Selected: " + Junction->name(); + if (false == editor_road::can_edit(*Junction, &reason)) + tool.status += ". It can't be changed: " + reason; + // the fields for the names of materials are shared with the roads + copy_text(tool.surface, sizeof(tool.surface), Junction->definition().surface); + copy_text(tool.sides[0], sizeof(tool.sides[0]), Junction->definition().side.material); + copy_text(tool.kerbtext, sizeof(tool.kerbtext), Junction->definition().kerbmaterial); + copy_text(tool.banktext, sizeof(tool.banktext), Junction->definition().bankmaterial); + copy_text(tool.mediantext, sizeof(tool.mediantext), Junction->definition().medianmaterial); +} + +void editor_mode::roadpoint_select(roadpoint_node *Point) +{ + auto &tool{m_roadtool}; + tool.marker = Point; + if (Point == nullptr) + return; + std::string reason; + tool.status = "Selected: " + Point->name(); + if (false == editor_road::can_edit(*Point, &reason)) + tool.status += ". It can't be changed: " + reason; +} + +// works out the point a click of the place tool would make with the cursor where it is +void editor_mode::roadpoint_aim(bool const Fresh) +{ + auto &tool{m_roadtool}; + if (false == Fresh && tool.aimedkind == tool.placekind && glm::distance(tool.aimed, tool.mouse) < 0.1) + { + // the cursor didn't move, so neither did what it points at + return; + } + tool.aimed = tool.mouse; + tool.aimedkind = tool.placekind; + auto const kind{point_kind(tool.placekind)}; + tool.target = tool.placing[tool.placekind]; + tool.target.kind = kind; + tool.hastarget = false; + tool.targetnote.clear(); + tool.targetdirection = glm::dvec3{0.0}; + if (kind == roadpoint_node::kind_type::crosswalk) + { + auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; + if (road == nullptr) + { + tool.targetnote = "Point at a road"; + return; + } + // the stripes go across the road, whichever part of it the cursor is over + auto const t{editor_road::nearest_parameter(road->definition(), tool.mouse)}; + tool.target.position = road->definition().point(t); + tool.targetdirection = road->definition().tangent(t); + tool.hastarget = true; + return; + } + if (kind == roadpoint_node::kind_type::crossing) + { + auto const *road{editor_road::nearest(tool.mouse, kSideMargin)}; + if (road == nullptr) + { + tool.targetnote = "Point at the place a road crosses the rails"; + return; + } + if (road->definition().changing()) + { + tool.targetnote = "The road gains or loses lanes on this piece; a level crossing goes on a piece which keeps its lanes"; + return; + } + glm::dvec3 middle{0.0}; + double halfspan{0.0}; + if (editor_road::rails_across(*road, tool.mouse, kRailReach, middle, halfspan)) + { + // the crossing goes halfway between the outermost tracks. the distance is rounded up, so it reads well in the scenery file + tool.target.position = middle; + tool.target.clearance = static_cast(std::ceil((halfspan + tool.stopmargin) * 2.0) * 0.5); + } + else + { + tool.target.position = road->definition().point(editor_road::nearest_parameter(road->definition(), tool.mouse)); + tool.target.clearance = std::max(tool.stopmargin, 2.0f); + tool.targetnote = "No rails cross the road here; a crossing put here won't close until some do"; + } + tool.hastarget = true; + return; + } + if (false == editor_road::nearest_lane(tool.mouse, kLaneReach, tool.target.position, tool.targetdirection)) + { + tool.targetnote = "Point at a lane of a road"; + return; + } + tool.hastarget = true; +} + +// puts down the point the place tool is set for, as a step which can be taken back +void editor_mode::roadpoint_place() +{ + auto &tool{m_roadtool}; + roadpoint_aim(true); + if (false == tool.hastarget) + { + tool.status = tool.targetnote; + return; + } + auto const created{editor_road::create(std::vector{tool.target})}; + if (created.empty()) + { + tool.status = "No scenery is loaded"; + return; + } + auto const *point{created.front()}; + auto const &state{point->definition()}; + editor_road::record record; + record.points_created = created; + push_road_snapshot(std::move(record)); + char text[64]; + switch (state.kind) + { + case roadpoint_node::kind_type::crossing: + { + std::snprintf(text, sizeof(text), "%.1f", state.clearance); + tool.status = "Level crossing made: " + std::to_string(point->stops().size()) + " lanes stop " + text + " m from its middle, "; + tool.status += std::to_string(point->rails()) + " tracks are watched for trains."; + break; + } + case roadpoint_node::kind_type::spawn: + { + tool.status = (state.vehicles.empty() ? std::string{"Spawn point made, with no vehicles: nothing appears there until it's given some (Select, click the point)."} : + "Spawn point made, with up to " + std::to_string(state.count) + " vehicles on the roads at a time. They appear while the simulation runs."); + break; + } + case roadpoint_node::kind_type::despawn: + { + tool.status = "Removal point made, it takes vehicles from " + std::to_string(point->lanes().size()) + (point->lanes().size() == 1 ? " lane." : " lanes."); + break; + } + case roadpoint_node::kind_type::crosswalk: + { + tool.status = "Pedestrian crossing painted."; + break; + } + } + if (false == tool.targetnote.empty()) + tool.status += " " + tool.targetnote + "."; +} + +// replaces the definition of a level crossing or a traffic point, as a step which can be taken back +void editor_mode::roadpoint_apply(roadpoint_node &Point, roadpoint_node::state const &State) +{ + auto &tool{m_roadtool}; + std::string reason; + if (false == editor_road::can_edit(Point, &reason)) + { + tool.status = "\"" + Point.name() + "\" can't be changed: " + reason; + return; + } + editor_road::record record; + record.points.emplace_back(&Point, Point.definition()); + editor_road::apply(std::vector>{{&Point, State}}); + push_road_snapshot(std::move(record)); + tool.status = "Changed"; +} + +// draws controls for a level crossing or a traffic point. returns: true if something was changed +bool editor_mode::render_roadpoint_layout(roadpoint_node::state &State, roadpoint_node const *Point) +{ + auto &tool{m_roadtool}; + bool changed{false}; + // values typed for a point which is there already are taken when confirmed, so the point isn't made anew with each digit + auto const flags{Point != nullptr ? ImGuiInputTextFlags_EnterReturnsTrue : ImGuiInputTextFlags_None}; + auto const number = [flags](char const *Label, float &Value, float const Step, char const *Format) { + ImGui::SetNextItemWidth(120.0f); + return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, flags); + }; + ImVec4 const warning{1.0f, 0.6f, 0.3f, 1.0f}; + switch (State.kind) + { + case roadpoint_node::kind_type::crossing: + { + if (Point != nullptr) + { + ImGui::Text("%d lanes stop here, %d tracks are watched for trains", static_cast(Point->stops().size()), static_cast(Point->rails())); + if (Point->rails() == 0) + ImGui::TextColored(warning, "No rails pass through it, so it never closes"); + // the crossing is tied to what's there when it's made or changed, and when the roads change + if (ImGui::Button("Look for the rails again")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "For when the tracks were laid or changed after the crossing was made."); + if (number("Stop this far from the middle [m]", State.clearance, 0.5f, "%.1f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Where the vehicles stop, measured along the road from the middle of the crossing.\n" + "The tracks passing within that distance from the middle are the ones the crossing is closed for."); + } + else + { + if (number("Stop this far from the outermost track [m]", tool.stopmargin, 0.5f, "%.1f")) + tool.stopmargin = std::clamp(tool.stopmargin, 2.0f, 50.0f); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Where the vehicles stop, measured along the road from the axis of the first track they'd get to."); + } + if (number("Closes with a train this far away [m]", State.warning, 50.0f, "%.0f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Measured along the tracks, both ways, with both ways of each switch counted.\n" + "A rail vehicle within that distance which gets nearer closes the crossing, and so does one right at it.\n" + "It opens a few seconds after the last one is gone."); + if (ImGui::Checkbox("Paint the stop lines", &State.stoplines)) + changed = true; + break; + } + case roadpoint_node::kind_type::spawn: + { + if (Point != nullptr) + { + if (Point->lanes().empty()) + ImGui::TextColored(warning, "It isn't on a lane, so nothing appears here"); + else if (Point->vehicles() == 0) + ImGui::TextDisabled("The vehicles are made when the simulation runs"); + else + ImGui::Text("%d vehicles made, %d of them wait for their turn", static_cast(Point->vehicles()), static_cast(Point->waiting())); + } + if (number("A vehicle every [s]", State.interval, 1.0f, "%.0f")) + changed = true; + if (number("Uneven by [0-1]", State.variation, 0.1f, "%.2f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Part of the time between vehicles which is drawn at random each time.\n" + "0: they come like clockwork, 1: anything from one right after another to twice the time."); + if (number("Speed [km/h]", State.velocity, 5.0f, "%.0f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The speed the vehicles appear with. From there on the drivers go by the speed limits of the lanes."); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt("Vehicles on the roads at a time", &State.count, 1, 5, flags)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "How many vehicles of this point can be on the scenery at once. That many are made along with the scenery,\n" + "drawn from the listed ones in turn; one of those which wait is put on the road each time its turn comes,\n" + "until all of them are out. A vehicle taken by a removal point goes back to wait for its next turn,\n" + "and so does one which stood still for 15 seconds, other than at a closed level crossing, or went backwards."); + // the set + ImGui::Separator(); + ImGui::Text("Vehicles of the set: %d", static_cast(State.vehicles.size())); + int removed{-1}; + for (std::size_t index = 0; index < State.vehicles.size(); ++index) + { + auto const &vehicle{State.vehicles[index]}; + ImGui::PushID(static_cast(index)); + if (ImGui::SmallButton("x")) + removed = static_cast(index); + ImGui::SameLine(); + if (vehicle.load > 0) + ImGui::Text("%s / %s, %s, %d %s", vehicle.folder.c_str(), vehicle.type.c_str(), vehicle.skin.c_str(), vehicle.load, vehicle.loadtype.c_str()); + else + ImGui::Text("%s / %s, %s", vehicle.folder.c_str(), vehicle.type.c_str(), vehicle.skin.c_str()); + ImGui::PopID(); + } + if (removed >= 0) + { + State.vehicles.erase(State.vehicles.begin() + removed); + changed = true; + } + if (State.vehicles.empty()) + ImGui::TextColored(warning, "No vehicles, so nothing appears here"); + ImGui::InputTextMultiline("##vehicles", tool.vehicles, sizeof(tool.vehicles), ImVec2(-1.0f, ImGui::GetTextLineHeight() * 5.0f)); + if (ImGui::Button("Add to the set")) + { + auto const parsed{roadpoint_node::parse_vehicles(tool.vehicles)}; + if (parsed.empty()) + { + tool.status = "No vehicle found in the text"; + } + else + { + State.vehicles.insert(State.vehicles.end(), parsed.begin(), parsed.end()); + tool.vehicles[0] = 0; + changed = true; + } + } + ImGui::SameLine(); + if (ImGui::Button("Empty the set") && false == State.vehicles.empty()) + { + State.vehicles.clear(); + changed = true; + } + ImGui::TextDisabled("Paste the vehicles the way they're put in a scenery:"); + ImGui::TextDisabled("node -1 0 none dynamic "); + ImGui::TextDisabled(" enddynamic"); + ImGui::TextDisabled("or as , one vehicle a line."); + ImGui::TextDisabled("The path, the offset, the driver and the velocity don't matter."); + break; + } + case roadpoint_node::kind_type::despawn: + { + if (Point != nullptr) + ImGui::Text("Takes vehicles from %d %s", static_cast(Point->lanes().size()), Point->lanes().size() == 1 ? "lane" : "lanes"); + if (number("Takes vehicles this close [m]", State.radius, 0.5f, "%.1f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Road vehicles on the lanes passing within this distance from the point are taken off the road when they get here,\n" + "except for the one the user drives. Vehicles which came from a spawn point go back to wait for their next turn.\n" + "The distance it starts with takes in a single lane."); + break; + } + case roadpoint_node::kind_type::crosswalk: + { + if (number("Length along the road [m]", State.length, 0.5f, "%.1f")) + changed = true; + ImGui::TextDisabled("Stripes painted from one edge of the road to the other."); + ImGui::TextDisabled("The drivers don't stop for it: there's nobody to cross."); + break; + } + } + if (changed && Point != nullptr) + State.normalize(); + return changed; +} + +// replaces the definition of a junction, as a step which can be taken back +void editor_mode::junction_apply(junction_node &Junction, junction_node::state const &State) +{ + auto &tool{m_roadtool}; + std::string reason; + if (false == editor_road::can_edit(Junction, &reason)) + { + tool.status = "\"" + Junction.name() + "\" can't be changed: " + reason; + // what the window shows goes back to what the junction is like + junction_select(&Junction); + tool.status = "\"" + Junction.name() + "\" can't be changed: " + reason; + return; + } + editor_road::record record; + record.junctions.emplace_back(&Junction, Junction.definition()); + editor_road::apply(std::vector>{{&Junction, State}}); + push_road_snapshot(std::move(record)); + tool.status = "Changed"; +} + +// draws controls for what a junction is like, and makes the changes as they're made +void editor_mode::render_junction_layout(junction_node &Junction) +{ + auto &tool{m_roadtool}; + auto state{Junction.definition()}; + bool changed{false}; + // typed values are taken when confirmed, so the junction isn't made anew with each digit + auto const number = [](char const *Label, float &Value, float const Step, char const *Format) { + ImGui::SetNextItemWidth(120.0f); + return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, ImGuiInputTextFlags_EnterReturnsTrue); + }; + + auto limit{state.velocity > 0.f ? state.velocity : 0.f}; + if (number("Speed limit [km/h], 0: none", limit, 5.0f, "%.0f")) + { + state.velocity = (limit > 0.f ? limit : -1.f); + changed = true; + } + auto centre{static_cast(state.centre.y)}; + if (number("Height of the middle [m]", centre, 0.1f, "%.2f")) + { + state.centre.y = centre; + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The surface is spanned between the ends of the roads and this point.\nThe ends of the roads are raised or lowered as points: select the white dot at the end of a road."); + + // appearance + ImGui::Separator(); + char const *colours[]{"none", "white", "orange"}; + int colour{state.markings == road_node::marking_colour::white ? 1 : state.markings == road_node::marking_colour::orange ? 2 : 0}; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::Combo("Markings", &colour, colours, 3)) + { + state.markings = (colour == 1 ? road_node::marking_colour::white : colour == 2 ? road_node::marking_colour::orange : road_node::marking_colour::none); + changed = true; + } + if (render_road_material("Surface", tool.surface, sizeof(tool.surface), state.surface)) + changed = true; + if (number("Texture length [m]", state.texturelength, 0.5f, "%.2f")) + { + state.texturelength = std::max(0.1f, state.texturelength); + changed = true; + } + // what the corners between the roads are lined with + if (ImGui::Checkbox("Kerb around the corners", &state.kerbs)) + changed = true; + if (state.kerbs) + { + ImGui::Indent(); + if (number("Kerb width [m]", state.kerbwidth, 0.05f, "%.2f")) + { + state.kerbwidth = std::clamp(state.kerbwidth, 0.05f, 2.0f); + changed = true; + } + if (render_road_material("Kerb material", tool.kerbtext, sizeof(tool.kerbtext), state.kerbmaterial)) + changed = true; + if (state.side.type != road_node::side_type::sidewalk && number("Kerb height [m]", state.kerbheight, 0.01f, "%.2f")) + { + state.kerbheight = std::clamp(state.kerbheight, 0.0f, 1.0f); + changed = true; + } + ImGui::Unindent(); + } + char const *sidetypes[]{"none", "shoulder", "sidewalk"}; + int type{state.side.type == road_node::side_type::shoulder ? 1 : state.side.type == road_node::side_type::sidewalk ? 2 : 0}; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::Combo("Sides of the corners", &type, sidetypes, 3)) + { + state.side.type = (type == 1 ? road_node::side_type::shoulder : type == 2 ? road_node::side_type::sidewalk : road_node::side_type::none); + if (state.side.type != road_node::side_type::none && state.side.width <= 0.0f) + state.side.width = 1.5f; + changed = true; + } + if (state.side.type != road_node::side_type::none) + { + ImGui::Indent(); + if (number("Width [m]", state.side.width, 0.25f, "%.2f")) + { + state.side.width = std::clamp(state.side.width, 0.1f, 20.0f); + changed = true; + } + if (render_road_material("Material", tool.sides[0], sizeof(tool.sides[0]), state.side.material)) + changed = true; + if (state.side.material.empty() || state.side.material == "none") + ImGui::TextDisabled("Pick a material to have the sides drawn"); + if (state.side.type == road_node::side_type::sidewalk) + { + if (number("Kerb height [m]", state.kerbheight, 0.01f, "%.2f")) + { + state.kerbheight = std::clamp(state.kerbheight, 0.0f, 1.0f); + changed = true; + } + } + else + { + if (number("Bank width [m]", state.slope.x, 0.25f, "%.2f")) + { + state.slope.x = std::clamp(state.slope.x, 0.0f, 20.0f); + changed = true; + } + if (number("Bank drop [m]", state.slope.y, 0.1f, "%.2f")) + { + state.slope.y = std::clamp(state.slope.y, 0.0f, 20.0f); + changed = true; + } + } + ImGui::Unindent(); + } + + // banks leading from the corners to the ground + if (ImGui::CollapsingHeader("Banks to the ground")) + { + if (render_road_material("Bank material", tool.banktext, sizeof(tool.banktext), state.bankmaterial)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "What the banks are covered with next to a sidewalk, a kerb or the bare edge.\nA bank of a shoulder is covered with the material of the shoulder."); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Run for a metre of drop [m]", &tool.bankgrade, 0.25f, 0.5f, "%.2f")) + tool.bankgrade = std::clamp(tool.bankgrade, 0.25f, 10.0f); + if (ImGui::Button("Lead the banks to the ground")) + { + if (changed) + junction_apply(Junction, state); + junction_bank_to_ground(Junction); + return; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Looks for the ground past each corner, at both roads the corner runs between,\n" + "and sets the bank of the corner to reach it. Done for the roads too, the banks meet at the ends of the roads."); + auto banked{false}; + for (auto const &arm : state.arms) + banked = banked || arm.bank.set; + if (banked) + { + ImGui::SameLine(); + if (ImGui::Button("Remove them")) + { + for (auto &arm : state.arms) + arm.bank = road_node::bank_data{}; + changed = true; + } + } + } + if (state.arms.size() == 2) + { + // where a road changes its lanes: what's between its two directions, if the roads keep them apart + ImGui::Separator(); + char const *mediantypes[]{"nothing, the ground shows", "surface closed to the traffic", "island"}; + int median{state.median == road_node::median_type::gap ? 0 : state.median == road_node::median_type::island ? 2 : 1}; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::Combo("Between the directions", &median, mediantypes, 3)) + { + state.median = (median == 0 ? road_node::median_type::gap : median == 2 ? road_node::median_type::island : road_node::median_type::painted); + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Shows where a road which keeps its two directions apart meets this stretch:\n" + "what's between the directions is led on to the other road, where it closes or carries on."); + if (state.median == road_node::median_type::island && render_road_material("Island material", tool.mediantext, sizeof(tool.mediantext), state.medianmaterial)) + changed = true; + } + + // the roads + if (ImGui::CollapsingHeader("Roads of the junction", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::TextDisabled("The lanes are taken from the roads: change them on the road, the junction follows."); + ImGui::TextDisabled("The roads are numbered in the view. Lanes are counted from the middle of the road;"); + ImGui::TextDisabled("automatic: the inner lane turns left, the outer one right, all go straight."); + for (std::size_t arm = 0; arm < state.arms.size(); ++arm) + { + ImGui::PushID(static_cast(arm)); + auto &data{state.arms[arm]}; + ImGui::Text("%d: %d in, %d out, %.2f m a lane", static_cast(arm) + 1, data.incoming, data.outgoing, data.width); + if (state.arms.size() > 2) + { + ImGui::SameLine(); + if (ImGui::Checkbox("Stop line", &data.stopline)) + changed = true; + // stripes for the pedestrians, across the whole road where it meets the junction + ImGui::SameLine(); + bool zebra{data.crosswalk > 0.f}; + if (ImGui::Checkbox("Pedestrian crossing", &zebra)) + { + data.crosswalk = (zebra ? 4.f : 0.f); + changed = true; + } + if (zebra) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(90.0f); + if (ImGui::InputFloat("m##crosswalk", &data.crosswalk, 0.5f, 1.0f, "%.1f", ImGuiInputTextFlags_EnterReturnsTrue)) + { + data.crosswalk = std::clamp(data.crosswalk, 1.0f, 20.0f); + changed = true; + } + } + } + if (data.incoming > 0 && state.arms.size() > 2) + { + ImGui::Indent(); + // the entries are in the order of the stored values + char const *priorities[]{"no sign: gives way to the right", "has the right of way", "gives way", "stops, then gives way"}; + int priority{static_cast(data.priority)}; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::Combo("##priority", &priority, priorities, 4)) + { + data.priority = static_cast(std::clamp(priority, 0, 3)); + changed = true; + } + // ways each lane can be left by + char const *ways[]{"automatic", "left", "straight", "left, straight", "right", "left, right", "straight, right", "any"}; + for (int lane = 0; lane < data.incoming; ++lane) + { + ImGui::PushID(100 + lane); + int turns{static_cast(lane) < data.turns.size() ? data.turns[lane] & 7 : 0}; + auto const label{"lane " + std::to_string(lane + 1) + " goes"}; + ImGui::SetNextItemWidth(140.0f); + if (ImGui::Combo(label.c_str(), &turns, ways, 8)) + { + data.turns.resize(std::max(data.turns.size(), static_cast(lane) + 1), 0); + data.turns[lane] = turns; + changed = true; + } + ImGui::PopID(); + } + ImGui::Unindent(); + } + ImGui::PopID(); + } + } + if (changed) + junction_apply(Junction, state); +} + +void editor_mode::road_cancel() +{ + auto &tool{m_roadtool}; + tool.chain = false; + tool.branchroad = nullptr; + tool.branchjunction = nullptr; +} + +// works out what a click of the build tool would make with the cursor where it is +editor_mode::road_plan editor_mode::road_preview(bool const Fresh) +{ + auto const &tool{m_roadtool}; + auto const &layout{tool.settings}; + road_plan plan; + // lanes of the road being built which lead to its far end, and the ones leading back from there + auto const arriving{tool.reversed ? layout.backward : layout.forward}; + auto const leaving{tool.reversed ? layout.forward : layout.backward}; + float lanewidth{0.f}; + auto const evenlanes{editor_road::lane_width(layout, 0.0, lanewidth)}; + char const *unevenlanes{"Lanes of the road being built differ in width, and a junction takes roads with lanes of the same width"}; + + // where the road is led to. a loose end of a road or an arm of a junction near the cursor comes first + plan.snap = editor_road::find_end(tool.mouse, kSnapRadius); + // the end the road is being drawn from isn't somewhere to lead it to + if (plan.snap.valid() && glm::distance(plan.snap.position, tool.point) < 0.5) + plan.snap = {}; + auto target{tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}}; + glm::dvec2 enddirection{0.0, 1.0}; + double endgrade{0.0}; + bool tied{false}; // the road has to arrive at the target in set direction, with set slope + if (plan.snap.valid()) + { + glm::dvec2 const outwards{plan.snap.outwards.x, plan.snap.outwards.z}; + if (glm::length(outwards) > 1e-6) + { + target = plan.snap.position; + enddirection = -glm::normalize(outwards); + endgrade = -plan.snap.outwards.y / glm::length(outwards); + tied = true; + } + else + { + plan.snap = {}; + } + } + if (plan.snap.junction != nullptr || plan.snap.half != 0) + { + // the junction has its ways through made for a set of lanes at each arm; a single roadway of a road takes the lanes it has + if (false == evenlanes || arriving != plan.snap.backward || leaving != plan.snap.forward || std::abs(lanewidth - plan.snap.width) > 0.01f) + { + char width[32]; + std::snprintf(width, sizeof(width), "%.2f", plan.snap.width); + plan.error = std::string{plan.snap.junction != nullptr ? "This arm of the junction" : "This roadway"} + " takes a road with "; + plan.error += std::to_string(plan.snap.backward) + " lane(s) leading to it and " + std::to_string(plan.snap.forward) + " away from it, " + width + " m wide each"; + return plan; + } + } + else if (false == plan.snap.valid() && glm::distance(tool.mouse, tool.point) > kJoinClearance) + { + // then the side of a road, or a junction between its roads: the road is taken in with a junction made there, or with another arm of the one which is there + auto *road{editor_road::nearest(tool.mouse, kSideMargin)}; + auto *junction{road == nullptr ? editor_road::nearest_junction(tool.mouse) : nullptr}; + if (road != nullptr && road == tool.branchroad) + road = nullptr; + if (junction != nullptr && junction == tool.branchjunction) + junction = nullptr; + if (road != nullptr || junction != nullptr) + { + if (false == evenlanes) + { + plan.error = unevenlanes; + return plan; + } + auto const t{road != nullptr ? editor_road::nearest_parameter(road->definition(), tool.mouse) : 0.0}; + auto const centre{road != nullptr ? road->definition().point(t) : junction->definition().centre}; + // seen from the junction the road comes from where it's being led from + glm::dvec2 const from{tool.point.x - centre.x, tool.point.z - centre.z}; + if (false == (road != nullptr ? editor_road::branch_from(*road, t, from, arriving, leaving, lanewidth, plan.end, plan.error) : editor_road::branch_from(*junction, from, arriving, leaving, lanewidth, plan.end, plan.error))) + return plan; + plan.hasend = true; + auto const &arm{plan.end.layout.arms[plan.end.arm]}; + target = arm.position; + enddirection = -arm.direction; + tied = true; + } + } + + // where the road starts + auto start{tool.point}; + auto direction{tool.direction}; + auto hasdirection{tool.hasdirection}; + auto startgrade{tool.grade}; + auto hasgrade{tool.hasgrade}; + if (tool.branchroad != nullptr || tool.branchjunction != nullptr) + { + // out of the side of a road, or out of a junction, in the direction of where it's led to + if (false == evenlanes) + { + plan.error = unevenlanes; + return plan; + } + glm::dvec2 const away{target.x - tool.point.x, target.z - tool.point.z}; + if (glm::length(away) < 1.0) + { + plan.error = "Move the cursor to where the road is to go"; + return plan; + } + // the axis of the road goes away from the junction, so the lanes going against it are the ones leading into the junction + if (false == (tool.branchroad != nullptr ? editor_road::branch_from(*tool.branchroad, tool.branchat, away, layout.backward, layout.forward, lanewidth, plan.start, plan.error) : + editor_road::branch_from(*tool.branchjunction, away, layout.backward, layout.forward, lanewidth, plan.start, plan.error))) + return plan; + plan.hasstart = true; + auto const &arm{plan.start.layout.arms[plan.start.arm]}; + start = arm.position; + direction = arm.direction; + hasdirection = true; + startgrade = 0.0; + hasgrade = true; + } + if (plan.hasstart && plan.hasend) + { + // the two junctions are laid out for the roads as they are now, which stops being true for the second once the first is made + bool shared{plan.start.junction != nullptr && plan.start.junction == plan.end.junction}; + auto const touches = [](editor_road::branch const &Branch, road_node const *Road) { + return std::any_of(Branch.changes.begin(), Branch.changes.end(), [Road](std::pair const &Change) { return Change.first == Road; }) || + std::find(Branch.removed.begin(), Branch.removed.end(), Road) != Branch.removed.end(); + }; + for (auto const &change : plan.start.changes) + shared = shared || touches(plan.end, change.first); + for (auto const *road : plan.start.removed) + shared = shared || touches(plan.end, road); + if (shared) + { + plan.error = "The junctions at both ends would take the same piece of road; lead the road somewhere else first"; + return plan; + } + } + if (glm::distance(start, target) > kLongestStretch) + { + plan.error = "Too far from the last point"; + return plan; + } + plan.pieces = editor_road::plan(start, hasdirection ? &direction : nullptr, target, tied ? &enddirection : nullptr, Global.ctrlState, plan.error); + road_profile(plan.pieces, hasgrade ? &startgrade : nullptr, tied ? &endgrade : nullptr, Fresh); + return plan; +} + +// gives the pieces laid out by the planner their heights: over the ground if the road is to follow it, with the slopes +// of the pieces matched where they meet, and with what the road is attached to +void editor_mode::road_profile(std::vector &Pieces, double const *Firstgrade, double const *Lastgrade, bool const Fresh) +{ + auto const &tool{m_roadtool}; + if (Pieces.empty()) + return; + if (false == tool.follow) + { + // straight from a point to the next one, the way a ramp or a viaduct goes + std::vector heights; + for (auto const &piece : Pieces) + heights.emplace_back(piece.points[segment_data::point::start].y); + heights.emplace_back(Pieces.back().points[segment_data::point::end].y); + editor_road::profile(Pieces, heights, Firstgrade, Lastgrade); + return; + } + auto const planned{Pieces}; + for (auto const step : kProfileSteps) + { + Pieces = editor_road::refine(planned, step); + auto const count{Pieces.size()}; + // the ground is looked up where the pieces meet, and in the middle of each to see how the result fits it + std::vector points; + points.reserve(2 * count + 1); + for (auto const &piece : Pieces) + points.emplace_back(piece.points[segment_data::point::start]); + points.emplace_back(Pieces.back().points[segment_data::point::end]); + for (auto const &piece : Pieces) + points.emplace_back(path_point(piece, 0.5)); + // the planner leads the road between its ends, which are over the ground already + for (auto &point : points) + point.y -= tool.offset; + auto ground{ground_heights(points, Fresh)}; + for (auto &height : ground) + height += tool.offset; + std::vector heights(ground.begin(), ground.begin() + count + 1); + // the ends are where they were put + heights.front() = Pieces.front().points[segment_data::point::start].y; + heights.back() = Pieces.back().points[segment_data::point::end].y; + editor_road::profile(Pieces, heights, Firstgrade, Lastgrade); + // shorter pieces are called for if the ground shows through the road, or the road hangs over it. + // the pieces attached to something are let off, as they have to get to its height one way or another + bool fits{true}; + for (std::size_t i = 0; i < count; ++i) + { + if ((i == 0 && Firstgrade != nullptr) || (i + 1 == count && Lastgrade != nullptr)) + continue; + auto const over{path_point(Pieces[i], 0.5).y - ground[count + 1 + i]}; + if (over < -kSunkTolerance || over > kRaisedTolerance) + { + fits = false; + break; + } + } + if (fits) + break; + } +} + +void editor_mode::road_click() +{ + auto &tool{m_roadtool}; + glm::dvec3 const camera{Global.pCamera.Pos}; + glm::dvec3 const ground{camera + GfxRenderer->Mouse_Position()}; + if (glm::distance(ground, camera) > kReach) + { + tool.status = "The click didn't land on anything near enough"; + return; + } + tool.mouse = ground; + if (tool.tool == 2) + { + roadpoint_place(); + return; + } + if (tool.tool == 0) + { + if (auto *marker{editor_road::nearest_point(ground, kMarkerRadius)}; marker != nullptr) + { + // a level crossing or a traffic point + tool.points.clear(); + tool.junction = nullptr; + road_select(nullptr); + roadpoint_select(marker); + return; + } + tool.marker = nullptr; + glm::dvec3 joint; + if (editor_road::nearest_joint(ground, kPointRadius, joint)) + { + // a point where pieces meet or end. with Shift it's added to the selected ones, or taken out of them + auto const listed{std::find_if(tool.points.begin(), tool.points.end(), [&joint](glm::dvec3 const &Point) { return glm::distance(Point, joint) < 0.25; })}; + if (false == Global.shiftState) + tool.points.assign(1, joint); + else if (listed != tool.points.end()) + tool.points.erase(listed); + else + tool.points.emplace_back(joint); + tool.selected = nullptr; + tool.junction = nullptr; + tool.status = std::to_string(tool.points.size()) + (tool.points.size() == 1 ? " point selected" : " points selected"); + return; + } + tool.points.clear(); + auto *road{editor_road::nearest(ground, 1.5)}; + auto *junction{road == nullptr ? editor_road::nearest_junction(ground) : nullptr}; + if (road == nullptr && junction == nullptr) + tool.status = "No road here"; + road_select(road); + junction_select(junction); + return; + } + + std::string reason; + if (false == editor_road::available(&reason)) + { + tool.status = reason; + return; + } + if (tool.ring) + { + road_roundabout(ground); + return; + } + if (false == tool.chain) + { + // first click sets where the road starts + tool.branchroad = nullptr; + tool.branchjunction = nullptr; + tool.selected = nullptr; + tool.junction = nullptr; + tool.points.clear(); + auto const end{editor_road::find_end(ground, kSnapRadius)}; + glm::dvec2 const outwards{end.outwards.x, end.outwards.z}; + if (end.valid() && glm::length(outwards) > 1e-6) + { + // started from a loose end of a road it carries that road on, started from an arm of a junction it's the road the junction is made for there + if (false == editor_road::can_join(end, &reason)) + { + tool.status = "Can't build from here: " + reason; + return; + } + tool.point = end.position; + tool.direction = glm::normalize(outwards); + tool.hasdirection = true; + tool.grade = end.outwards.y / glm::length(outwards); + tool.hasgrade = true; + if (end.road != nullptr && end.half == 0) + { + // carried on from its start the road keeps the direction of its axis, so the lanes stay what they were + tool.reversed = (false == end.atend); + // with the lanes it has at that end + tool.settings = end.road->definition().section(end.atend ? 1 : 0); + // and it stays as wide as it is at that end, with its directions as far apart + auto const scale{tool.settings.taper[end.atend ? 1 : 0]}; + tool.settings.taper = {scale, scale}; + auto const gap{tool.settings.median.width[end.atend ? 1 : 0]}; + tool.settings.median.width = {gap, gap}; + // the banks were made for the ground where they are + tool.settings.banks = {}; + tool.status = "Carrying on \"" + end.road->name() + "\" with its layout"; + } + else + { + tool.reversed = false; + if (end.road != nullptr) + { + // one of the roadways of a road which keeps its directions apart is carried on as a road of its own, which looks like the road it leaves + tool.settings = end.road->definition(); + tool.settings.sides[end.half > 0 ? 0 : 1] = road_node::side_data{}; + tool.settings.banks = {}; + tool.settings.missing = {}; + } + tool.settings.forward = end.forward; + tool.settings.backward = end.backward; + tool.settings.lanewidth = end.width; + tool.settings.taper = {1.f, 1.f}; + tool.settings.median.width = {0.f, 0.f}; + tool.settings.lanes.clear(); + tool.settings.changes.clear(); + tool.settings.normalize(); + tool.status = (end.junction != nullptr ? "Leading a road out of \"" + end.junction->name() + "\", with the lanes the junction has there" : + "Carrying on one of the roadways of \"" + end.road->name() + "\" as a one-way road"); + } + road_select(nullptr); + } + else + { + tool.hasdirection = false; + tool.hasgrade = false; + tool.reversed = false; + tool.settings.taper = {1.f, 1.f}; + if (auto *road{editor_road::nearest(ground, kSideMargin)}; road != nullptr) + { + // started from the side of a road it leaves that road at a junction, made along with its first piece + tool.branchroad = road; + tool.branchat = editor_road::nearest_parameter(road->definition(), ground); + tool.point = road->definition().point(tool.branchat); + tool.status = "Leading a road out of the side of \"" + road->name() + "\": click where it goes"; + } + else if (auto *junction{editor_road::nearest_junction(ground)}; junction != nullptr) + { + // started from a junction it's another road of that junction + if (junction->definition().arms.size() >= 4) + { + tool.status = "\"" + junction->name() + "\" has four roads already"; + return; + } + tool.branchjunction = junction; + tool.point = junction->definition().centre; + tool.status = "Leading another road out of \"" + junction->name() + "\": click where it goes"; + } + else + { + tool.point = ground + glm::dvec3{0.0, tool.offset, 0.0}; + tool.status = "Start set"; + } + } + tool.chain = true; + return; + } + + auto plan{road_preview(true)}; + if (plan.pieces.empty()) + { + tool.status = "Can't build this: " + plan.error; + return; + } + if (plan.snap.valid() && false == editor_road::can_join(plan.snap, &reason)) + { + tool.status = "Can't join here: " + reason; + return; + } + auto pieces{plan.pieces}; + // where the next piece is going to carry on from + auto const &last{pieces.back()}; + auto const endpoint{last.points[segment_data::point::end]}; + auto const tangent{last.points[segment_data::point::control2] != glm::dvec3{0.0} ? -last.points[segment_data::point::control2] : last.points[segment_data::point::end] - last.points[segment_data::point::start]}; + auto const grade{editor_road::end_grade(last)}; + if (tool.reversed) + { + for (auto &piece : pieces) + { + std::swap(piece.points[segment_data::point::start], piece.points[segment_data::point::end]); + std::swap(piece.points[segment_data::point::control1], piece.points[segment_data::point::control2]); + } + std::reverse(pieces.begin(), pieces.end()); + } + // the junctions the road starts or ends with are made first, so the road finds them when it's made + editor_road::record record; + if (plan.hasstart) + { + if (plan.start.junction == nullptr) + plan.start.layout.velocity = tool.crossingspeed; + editor_road::carry_out(plan.start, record); + } + if (plan.hasend) + { + if (plan.end.junction == nullptr) + plan.end.layout.velocity = tool.crossingspeed; + editor_road::carry_out(plan.end, record); + } + std::vector states; + // what changes from the start of a piece to its end is spread over the whole stretch being built, + // so the pieces don't each go through all of it + std::vector stations{0.0}; + for (auto const &piece : pieces) + stations.emplace_back(stations.back() + editor_road::planar_length(piece)); + auto const along = [&stations](std::size_t const Index) { + auto const fraction{stations.back() > 0.0 ? stations[Index] / stations.back() : 0.0}; + return static_cast(fraction * fraction * (3.0 - 2.0 * fraction)); + }; + for (std::size_t index = 0; index < pieces.size(); ++index) + { + auto state{tool.settings}; + state.axis = pieces[index]; + state.missing = {}; + auto const from{along(index)}; + auto const to{along(index + 1)}; + auto const &median{tool.settings.median}; + state.median.width = {glm::mix(median.width[0], median.width[1], from), glm::mix(median.width[0], median.width[1], to)}; + // an island is rounded where the stretch starts and ends + state.median.round = {index == 0 ? median.round[0] : 0.f, index + 1 == pieces.size() ? median.round[1] : 0.f}; + for (std::size_t side = 0; side < state.banks.size(); ++side) + { + auto const &bank{tool.settings.banks[side]}; + state.banks[side].width = {glm::mix(bank.width[0], bank.width[1], from), glm::mix(bank.width[0], bank.width[1], to)}; + state.banks[side].drop = {glm::mix(bank.drop[0], bank.drop[1], from), glm::mix(bank.drop[0], bank.drop[1], to)}; + } + states.emplace_back(state); + } + auto const created{editor_road::create(states)}; + record.roads_created.insert(record.roads_created.end(), created.begin(), created.end()); + push_road_snapshot(std::move(record)); + tool.branchroad = nullptr; + tool.branchjunction = nullptr; + if (created.empty()) + { + tool.chain = false; + tool.status = "The road wasn't created"; + return; + } + if (plan.snap.valid() || plan.hasend) + { + tool.chain = false; + tool.status = (plan.snap.valid() ? "Joined with \"" + (plan.snap.road != nullptr ? plan.snap.road->name() : plan.snap.junction->name()) + "\"" : + plan.end.junction != nullptr ? "Led into \"" + plan.end.junction->name() + "\" as another of its roads" : + std::string{"Led into the side of the road, with a junction made there"}); + return; + } + glm::dvec2 const planar{tangent.x, tangent.z}; + tool.point = endpoint; + if (glm::length(planar) > 1e-6) + { + tool.direction = glm::normalize(planar); + tool.hasdirection = true; + } + tool.grade = grade; + tool.hasgrade = true; + tool.status = std::to_string(created.size()) + (created.size() == 1 ? " piece added" : " pieces added"); + if (plan.hasstart) + tool.status += (plan.start.junction != nullptr ? ", as another road of the junction" : ", with a junction where the road leaves"); +} + +void editor_mode::road_apply() +{ + auto &tool{m_roadtool}; + if (tool.selected == nullptr) + return; + tool.settings.normalize(); + // other lanes at the end are something the selected piece has for itself; the rest of the road keeps the lanes it has + auto const pieces{tool.whole && false == tool.settings.changing() ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; + editor_road::record record; + std::vector> changes; + // the banks are kept for each piece as they are, unless it's the banks which were just changed + auto const banked{tool.settings.banks != tool.selected->definition().banks}; + for (auto *road : pieces) + { + std::string reason; + if (false == editor_road::can_edit(*road, &reason)) + { + // what the window shows goes back to what the road is like + road_select(tool.selected); + tool.status = "\"" + road->name() + "\" can't be changed: " + reason; + return; + } + auto state{tool.settings}; + state.axis = road->definition().axis; + // how wide the road is at its points is set at these points, for each piece on its own + state.taper = road->definition().taper; + if (road != tool.selected) + { + // and so is how far apart the two directions are led, and the rounded ends of what's between them + state.median.width = road->definition().median.width; + state.median.round = road->definition().median.round; + // banks led to the ground differ from piece to piece, so a change of something else leaves them alone + if (false == banked) + state.banks = road->definition().banks; + } + if (road != tool.selected && road->definition().changes.size() == state.changes.size()) + { + // where the lanes can be changed is set for each piece on its own + state.changes = road->definition().changes; + } + record.roads.emplace_back(road, road->definition()); + changes.emplace_back(road, state); + } + editor_road::apply(changes); + // what's next to the changed pieces is fitted to them: junctions take the lanes they have now, the pieces of the road + // which stay as they were get wider or narrower towards them, or give up their ends for the lanes to change + std::string notes; + for (auto *road : pieces) + editor_road::settle(*road, record, notes); + auto const fitted{record.roads.size() + record.roads_removed.size() + record.junctions.size() + record.junctions_created.size() - pieces.size()}; + push_road_snapshot(std::move(record)); + tool.settings = tool.selected->definition(); + tool.status = (pieces.size() == 1 ? std::string{"Changed."} : "Changed " + std::to_string(pieces.size()) + " pieces."); + if (fitted > 0) + tool.status += " What's next to it was fitted to it."; + if (false == notes.empty()) + tool.status += " " + notes; +} + +bool editor_mode::road_delete() +{ + auto &tool{m_roadtool}; + if (false == tool.window) + return false; + std::string reason; + editor_road::record record; + if (tool.marker != nullptr) + { + auto *point{tool.marker}; + if (false == editor_road::can_edit(*point, &reason)) + { + tool.status = "Can't delete: " + reason; + return true; + } + editor_road::remove(std::vector{point}); + record.points_removed.emplace_back(point); + push_road_snapshot(std::move(record)); + tool.marker = nullptr; + tool.status = "\"" + point->name() + "\" deleted, Ctrl+Z brings it back"; + return true; + } + if (false == tool.points.empty()) + return road_points_delete(); + if (tool.selected == nullptr && tool.junction == nullptr) + return false; + if (tool.junction != nullptr) + { + auto *junction{tool.junction}; + if (false == editor_road::can_edit(*junction, &reason)) + { + tool.status = "Can't delete: " + reason; + return true; + } + editor_road::remove(std::vector{junction}); + record.junctions_removed.emplace_back(junction); + push_road_snapshot(std::move(record)); + tool.junction = nullptr; + tool.status = "\"" + junction->name() + "\" deleted, Ctrl+Z brings it back"; + return true; + } + auto *road{tool.selected}; + if (false == editor_road::can_edit(*road, &reason)) + { + tool.status = "Can't delete: " + reason; + return true; + } + editor_road::remove(std::vector{road}); + record.roads_removed.emplace_back(road); + push_road_snapshot(std::move(record)); + tool.selected = nullptr; + tool.status = "\"" + road->name() + "\" deleted, Ctrl+Z brings it back"; + return true; +} + +// puts a roundabout around specified point +void editor_mode::road_roundabout(glm::dvec3 const &Ground) +{ + auto &tool{m_roadtool}; + std::string reason; + if (false == editor_road::available(&reason)) + { + tool.status = reason; + return; + } + auto const states{editor_road::roundabout(Ground + glm::dvec3{0.0, tool.offset, 0.0}, tool.ringradius, tool.settings, tool.island)}; + if (states.empty()) + { + tool.status = "The roundabout is too small for its lanes: give it a larger radius"; + return; + } + for (auto const &state : states) + { + for (auto const t : {0.0, 0.5}) + { + if (editor_road::nearest(state.point(t), 0.0) != nullptr || editor_road::nearest_junction(state.point(t)) != nullptr) + { + tool.status = "There's a road in the way. Make the roundabout next to it, then lead the road into its side"; + return; + } + } + } + auto const created{editor_road::create(states)}; + if (created.empty()) + { + tool.status = "The roundabout wasn't created"; + return; + } + editor_road::record record; + record.roads_created = created; + push_road_snapshot(std::move(record)); + tool.status = "Roundabout made. Untick Roundabout and lead roads out of its side, or into it: the traffic joining it gives way to the traffic going around."; +} + +// how far from specified point, and how far down, a slope of the grade set for the banks gets to the ground +std::pair editor_mode::bank_reach(glm::dvec3 const &Edge, glm::dvec3 const &Outwards) +{ + auto const grade{std::clamp(static_cast(m_roadtool.bankgrade), 0.25, 10.0)}; + auto const steps{static_cast(kBankReach / kBankStep)}; + // the ground further and further from the edge, without the roads themselves + std::vector probes; + for (int step = 1; step <= steps; ++step) + probes.emplace_back(Edge + Outwards * (step * kBankStep)); + auto const ground{ground_heights(probes, true)}; + // the bank ends where a slope of the set grade, going down or up, gets to the ground + auto const rising{ground.front() > Edge.y}; + auto width{kBankReach}; + auto drop{Edge.y - ground.back()}; + for (int step = 1; step <= steps; ++step) + { + auto const reach{step * kBankStep}; + auto const height{ground[step - 1]}; + if (rising ? height <= Edge.y + reach / grade : height >= Edge.y - reach / grade) + { + width = reach; + drop = Edge.y - height; + break; + } + } + return {static_cast(width), static_cast(drop)}; +} + +// leads the banks of the corners of a junction to the ground beside it +void editor_mode::junction_bank_to_ground(junction_node &Junction) +{ + auto state{Junction.definition()}; + auto const order{state.arm_order()}; + auto const sided{state.side.type != road_node::side_type::none && state.side.width > 0.f}; + // where the banks start: past the kerb and whatever lines the corners + auto const lining{(state.kerbs ? state.kerbwidth : 0.f) + (sided ? state.side.width : 0.f)}; + auto const raised{sided && state.side.type == road_node::side_type::sidewalk ? state.kerbheight : 0.f}; + for (std::size_t idx = 0; idx < order.size(); ++idx) + { + // a corner starts at the left edge of a road, seen from the junction, and ends at the right edge of the next one + auto &arm{state.arms[order[idx]]}; + auto const &next{state.arms[order[(idx + 1) % order.size()]]}; + glm::dvec3 const outwards[2]{glm::dvec3{arm.direction.y, 0.0, -arm.direction.x}, -glm::dvec3{next.direction.y, 0.0, -next.direction.x}}; + glm::dvec3 const edges[2]{arm.position + outwards[0] * (0.5 * state.arm_width(order[idx]) + lining) + glm::dvec3{0.0, raised, 0.0}, + next.position + outwards[1] * (0.5 * state.arm_width(order[(idx + 1) % order.size()]) + lining) + glm::dvec3{0.0, raised, 0.0}}; + arm.bank.set = true; + for (int end = 0; end < 2; ++end) + { + auto const reach{bank_reach(edges[end], outwards[end])}; + arm.bank.width[end] = reach.first; + arm.bank.drop[end] = reach.second; + } + } + junction_apply(Junction, state); + auto &tool{m_roadtool}; + if (tool.status == "Changed") + { + tool.status = "Banks of the junction led to the ground."; + if ((false == sided || state.side.type != road_node::side_type::shoulder) && (state.bankmaterial.empty() || state.bankmaterial == "none")) + tool.status += " Pick a bank material to have them drawn."; + } +} + +// leads the banks of the selected piece, or of the whole road, to the ground beside it +void editor_mode::road_bank_to_ground() +{ + auto &tool{m_roadtool}; + if (tool.selected == nullptr) + return; + auto const pieces{tool.whole ? editor_road::chain(*tool.selected) : std::vector{tool.selected}}; + std::string reason; + for (auto const *road : pieces) + { + if (false == editor_road::can_edit(*road, &reason)) + { + tool.status = "\"" + road->name() + "\" can't be changed: " + reason; + return; + } + } + std::vector> changes; + for (auto *road : pieces) + { + auto state{road->definition()}; + for (int side = 0; side < 2; ++side) + { + auto const &data{state.sides[side]}; + auto const sided{data.type != road_node::side_type::none && data.width > 0.f}; + for (int end = 0; end < 2; ++end) + { + auto const t{static_cast(end)}; + // where the bank starts: past the roadway, the kerb and whatever lies beside it + auto const tangent{state.tangent(t)}; + glm::dvec3 across{tangent.z, 0.0, -tangent.x}; + if (glm::length(across) < 1e-9) + continue; + across = glm::normalize(across) * (side == 0 ? 1.0 : -1.0); + auto edge{state.point(t) + across * (0.5 * state.span(t) + (state.kerbs[side] ? state.kerbwidth : 0.f) + (sided ? data.width : 0.f))}; + if (sided && data.type == road_node::side_type::sidewalk) + edge.y += state.kerbheight; + auto const reach{bank_reach(edge, across)}; + state.banks[side].set = true; + state.banks[side].width[end] = reach.first; + state.banks[side].drop[end] = reach.second; + } + } + changes.emplace_back(road, state); + } + editor_road::record record; + for (auto const &change : changes) + record.roads.emplace_back(change.first, change.first->definition()); + editor_road::apply(changes); + push_road_snapshot(std::move(record)); + road_select(tool.selected); + tool.status = "Banks of " + std::to_string(changes.size()) + (changes.size() == 1 ? " piece" : " pieces") + " led to the ground."; + auto const &layout{tool.selected->definition()}; + if ((layout.sides[0].type != road_node::side_type::shoulder || layout.sides[1].type != road_node::side_type::shoulder) && (layout.bankmaterial.empty() || layout.bankmaterial == "none")) + tool.status += " Pick a bank material to have them drawn where the road has no shoulder."; +} + +// takes the selected points out of their roads +bool editor_mode::road_points_delete() +{ + auto &tool{m_roadtool}; + editor_road::record record; + std::string notes; + int taken{0}; + for (auto const &point : tool.points) + { + std::string error; + if (editor_road::dissolve(point, record, error)) + ++taken; + else if (notes.find(error) == std::string::npos) + notes += " A point was left: " + error + "."; + } + push_road_snapshot(std::move(record)); + // the points which stayed are still selected, so it's clear which ones these are + tool.points.erase(std::remove_if(tool.points.begin(), tool.points.end(), [](glm::dvec3 const &Point) { return editor_road::ends_at(Point).empty(); }), tool.points.end()); + tool.status = (taken == 0 ? std::string{"Nothing deleted."} : taken == 1 ? std::string{"Point deleted, Ctrl+Z brings it back."} : + std::to_string(taken) + " points deleted, Ctrl+Z brings them back."); + tool.status += notes; + return true; +} + +bool editor_mode::road_split() +{ + auto &tool{m_roadtool}; + if (false == tool.window || tool.selected == nullptr) + return false; + auto *road{tool.selected}; + std::string reason; + if (false == editor_road::can_edit(*road, &reason)) + { + tool.status = "Can't split: " + reason; + return true; + } + if (road->definition().changing()) + { + tool.status = "Can't split a piece which gains or loses lanes; give it the same lanes at both ends first"; + return true; + } + editor_road::record record; + record.roads.emplace_back(road, road->definition()); + auto *created{editor_road::split(*road, editor_road::nearest_parameter(road->definition(), tool.mouse))}; + if (created == nullptr) + { + tool.status = "Can't split this close to the end"; + return true; + } + record.roads_created.emplace_back(created); + push_road_snapshot(std::move(record)); + road_select(road); + tool.status = "Split in two: \"" + road->name() + "\" and \"" + created->name() + "\". Click the point between them to set the width or the height of the road there"; + return true; +} + +// makes the changes gathered for the selected points. returns: true if they were made +bool editor_mode::road_points_apply(std::vector> const &Changes, std::vector> const &Junctions) +{ + auto &tool{m_roadtool}; + if (Changes.empty() && Junctions.empty()) + { + tool.status = "There's nothing to change at the selected points"; + return false; + } + editor_road::record record; + std::string reason; + for (auto const &change : Changes) + { + if (false == editor_road::can_edit(*change.first, &reason)) + { + tool.status = "\"" + change.first->name() + "\" can't be changed: " + reason; + return false; + } + record.roads.emplace_back(change.first, change.first->definition()); + } + for (auto const &change : Junctions) + { + if (false == editor_road::can_edit(*change.first, &reason)) + { + tool.status = "\"" + change.first->name() + "\" can't be changed: " + reason; + return false; + } + record.junctions.emplace_back(change.first, change.first->definition()); + } + editor_road::apply(Changes); + editor_road::apply(Junctions); + // junctions the changed pieces lead to are made for the roads as they are now + std::string notes; + for (auto const &change : Changes) + editor_road::settle(*change.first, record, notes); + push_road_snapshot(std::move(record)); + tool.status = (Changes.size() == 1 ? std::string{"Changed 1 piece."} : "Changed " + std::to_string(Changes.size()) + " pieces."); + if (false == notes.empty()) + tool.status += " " + notes; + return true; +} + +void editor_mode::road_point_width(float const Width) +{ + // the pieces on both sides of a point get to that width there, and from there go back to their own along their length + auto const changes{point_changes(m_roadtool.points, [Width](road_node::state &State, bool const Atend, std::size_t const) { + auto const width{State.width(Atend ? 1 : 0)}; + if (width > 0.0) + State.taper[Atend ? 1 : 0] = std::clamp(static_cast(Width / width), 0.25f, 4.f); + })}; + road_points_apply(changes, {}); +} + +// Heights: one for each of the selected points +void editor_mode::road_point_heights(std::vector const &Heights) +{ + auto &tool{m_roadtool}; + if (Heights.size() != tool.points.size()) + return; + // the control points are kept relative to the ends, so the slopes the pieces have at a point stay what they are + auto const changes{point_changes(tool.points, [&Heights](road_node::state &State, bool const Atend, std::size_t const Point) { State.axis.points[Atend ? segment_data::point::end : segment_data::point::start].y = Heights[Point]; })}; + // an arm of a junction goes up or down with the end of the road it's made for + std::vector> junctions; + for (std::size_t index = 0; index < tool.points.size(); ++index) + { + for (auto const &arm : editor_road::arms_at(tool.points[index])) + { + auto entry{std::find_if(junctions.begin(), junctions.end(), [&arm](std::pair const &Listed) { return Listed.first == arm.first; })}; + if (entry == junctions.end()) + { + junctions.emplace_back(arm.first, arm.first->definition()); + entry = std::prev(junctions.end()); + } + entry->second.arms[arm.second].position.y = Heights[index]; + } + } + if (false == road_points_apply(changes, junctions)) + return; + // the selection follows what it's made of + for (std::size_t index = 0; index < tool.points.size(); ++index) + tool.points[index].y = Heights[index]; +} + +void editor_mode::push_road_snapshot(editor_road::record Record) +{ + if (Record.empty()) + return; + + if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) + m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); + + // the entry is listed under the name of the first thing it's about + scene::basic_node const *node{nullptr}; + if (false == Record.roads.empty()) + node = Record.roads.front().first; + else if (false == Record.roads_created.empty()) + node = Record.roads_created.front(); + else if (false == Record.roads_removed.empty()) + node = Record.roads_removed.front(); + else if (false == Record.junctions.empty()) + node = Record.junctions.front().first; + else if (false == Record.junctions_created.empty()) + node = Record.junctions_created.front(); + else if (false == Record.junctions_removed.empty()) + node = Record.junctions_removed.front(); + else if (false == Record.points.empty()) + node = Record.points.front().first; + else if (false == Record.points_created.empty()) + node = Record.points_created.front(); + else + node = Record.points_removed.front(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::RoadEdit; + snap.node_name = node->name(); + snap.position = node->location(); + snap.roads = std::move(Record); + m_history.push_back(std::move(snap)); + g_redo.clear(); +} + +void editor_mode::restore_road_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo) +{ + auto const &record{Snapshot.roads}; + if (record.empty()) + return; + + // what the roads are like now is what the opposite operation brings back + EditorSnapshot current{Snapshot}; + for (auto &entry : current.roads.roads) + entry.second = entry.first->definition(); + for (auto &entry : current.roads.junctions) + entry.second = entry.first->definition(); + for (auto &entry : current.roads.points) + entry.second = entry.first->definition(); + Opposite.push_back(std::move(current)); + + // things are taken out before the rest is changed and put in after it, so nothing gets joined with what's on its way out + if (Undo) + { + editor_road::remove(record.roads_created); + editor_road::remove(record.junctions_created); + editor_road::remove(record.points_created); + } + else + { + editor_road::remove(record.roads_removed); + editor_road::remove(record.junctions_removed); + editor_road::remove(record.points_removed); + } + editor_road::apply(record.roads); + editor_road::apply(record.junctions); + editor_road::apply(record.points); + if (Undo) + { + editor_road::revive(record.roads_removed); + editor_road::revive(record.junctions_removed); + editor_road::revive(record.points_removed); + } + else + { + editor_road::revive(record.roads_created); + editor_road::revive(record.junctions_created); + editor_road::revive(record.points_created); + } + + auto &tool{m_roadtool}; + road_cancel(); + if (tool.selected != nullptr) + road_select(tool.selected->m_editorremoved ? nullptr : tool.selected); + if (tool.junction != nullptr) + junction_select(tool.junction->m_editorremoved ? nullptr : tool.junction); + if (tool.marker != nullptr && tool.marker->m_editorremoved) + tool.marker = nullptr; + tool.aimedkind = -1; +} + +// a field for the name of a material, with a list of the ones in the texture folder to pick from. returns: true if the material was changed +bool editor_mode::render_road_material(char const *Label, char *Buffer, std::size_t const Size, std::string &Value) +{ + auto &tool{m_roadtool}; + bool picked{false}; + ImGui::PushID(Label); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::InputText("##name", Buffer, Size, ImGuiInputTextFlags_EnterReturnsTrue) || ImGui::IsItemDeactivatedAfterEdit()) + { + auto const name{lower_case(Buffer)}; + if (name != Value && name.find(' ') == std::string::npos) + { + Value = name.empty() ? std::string{"none"} : name; + picked = true; + } + copy_text(Buffer, Size, Value); + } + ImGui::SameLine(); + if (ImGui::Button("...")) + ImGui::OpenPopup("pick"); + ImGui::SameLine(); + ImGui::TextUnformatted(Label); + if (ImGui::BeginPopup("pick")) + { + ImGui::TextDisabled("Materials and images found in the texture folder"); + ImGui::SetNextItemWidth(200.0f); + ImGui::InputText("Filter", tool.filter, sizeof(tool.filter)); + auto const filter{lower_case(tool.filter)}; + std::vector listed; + for (auto const &name : editor_road::materials()) + if (filter.empty() || name.find(filter) != std::string::npos) + listed.emplace_back(&name); + ImGui::BeginChild("list", ImVec2(340.0f, 280.0f), true); + ImGuiListClipper clipper; + clipper.Begin(static_cast(listed.size())); + while (clipper.Step()) + { + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; ++i) + { + if (ImGui::Selectable(listed[i]->c_str(), *listed[i] == Value)) + { + Value = *listed[i]; + copy_text(Buffer, Size, Value); + picked = true; + ImGui::CloseCurrentPopup(); + } + } + } + ImGui::EndChild(); + ImGui::EndPopup(); + } + ImGui::PopID(); + return picked; +} + +// draws controls for the layout of a road. returns: true if the layout was changed in a way which calls for the road to be made anew +bool editor_mode::render_road_layout(road_node::state &State) +{ + auto &tool{m_roadtool}; + bool changed{false}; + // typed values are taken when confirmed, so the road isn't made anew with each digit + auto const number = [](char const *Label, float &Value, float const Step, char const *Format) { + ImGui::SetNextItemWidth(120.0f); + return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, ImGuiInputTextFlags_EnterReturnsTrue); + }; + auto const material = [this](char const *Label, char *Buffer, std::size_t const Size, std::string &Value) { return render_road_material(Label, Buffer, Size, Value); }; + + // lanes. a piece of a road which is there already can end with other lanes than it starts with + { + auto const selecting{tool.tool == 0 && tool.selected != nullptr}; + int counts[2][2]{{State.lanes_at(0, true), State.lanes_at(0, false)}, {State.lanes_at(1, true), State.lanes_at(1, false)}}; + bool relaned{false}; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt(selecting && tool.varied ? "Lanes along the axis, at the start" : "Lanes along the axis", &counts[0][0])) + relaned = true; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt(selecting && tool.varied ? "Lanes against the axis, at the start" : "Lanes against the axis", &counts[0][1])) + relaned = true; + if (selecting) + { + // ticked it only shows the lanes of the end to be set; unticked it gives the end the lanes of the start + if (ImGui::Checkbox("Other lanes at the end of the piece", &tool.varied) && false == tool.varied && State.changing()) + relaned = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The road gains or loses lanes along this piece: it gets wider or narrower, a lane it gains branches off the outermost lane\n" + "of its direction (two at most), a lane it loses joins the outermost one left.\n" + "What's next to the piece is fitted to the lanes the piece has at that end.\n" + "The axis goes the way the lanes drawn in green do: the start of the piece is where these come from.\n" + "Split the piece (K) first to have the lanes change over a part of it."); + if (tool.varied) + { + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt("Lanes along the axis, at the end", &counts[1][0])) + relaned = true; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputInt("Lanes against the axis, at the end", &counts[1][1])) + relaned = true; + } + } + // the traffic going along the axis gets on the piece at its start, the one going against it at its end + if (State.lanes_at(1, true) - State.lanes_at(0, true) > 2 || State.lanes_at(0, false) - State.lanes_at(1, false) > 2) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "A lane forks into three at most: lanes past the second one gained can't be got on"); + if (relaned) + { + if (false == selecting || false == tool.varied) + { + counts[1][0] = counts[0][0]; + counts[1][1] = counts[0][1]; + } + // a different set of lanes starts with the common width and speed limit, and the default rules for changing lanes + State.lanes_between(counts[0][0], counts[0][1], counts[1][0], counts[1][1]); + changed = true; + } + } + if (number("Lane width [m]", State.lanewidth, 0.25f, "%.2f")) + { + State.lanewidth = std::clamp(State.lanewidth, 1.0f, 10.0f); + for (auto &lane : State.lanes) + lane.width = State.lanewidth; + changed = true; + } + float limit{State.velocity > 0.0f ? State.velocity : 0.0f}; + if (number("Speed limit [km/h], 0: none", limit, 10.0f, "%.0f")) + { + State.velocity = (limit > 0.0f ? limit : -1.0f); + for (auto &lane : State.lanes) + lane.velocity = State.velocity; + changed = true; + } + + // appearance + ImGui::Separator(); + char const *colours[]{"none", "white", "orange"}; + int colour{State.markings == road_node::marking_colour::white ? 1 : State.markings == road_node::marking_colour::orange ? 2 : 0}; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::Combo("Markings", &colour, colours, 3)) + { + State.markings = (colour == 1 ? road_node::marking_colour::white : colour == 2 ? road_node::marking_colour::orange : road_node::marking_colour::none); + changed = true; + } + if (material("Surface", tool.surface, sizeof(tool.surface), State.surface)) + changed = true; + if (number("Texture length [m]", State.texturelength, 0.5f, "%.2f")) + { + State.texturelength = std::max(0.1f, State.texturelength); + changed = true; + } + char const *sidenames[]{"Left side", "Right side"}; + char const *sidetypes[]{"none", "shoulder", "sidewalk"}; + for (int side = 0; side < 2; ++side) + { + ImGui::PushID(side); + auto &data{State.sides[side]}; + int type{data.type == road_node::side_type::shoulder ? 1 : data.type == road_node::side_type::sidewalk ? 2 : 0}; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::Combo(sidenames[side], &type, sidetypes, 3)) + { + data.type = (type == 1 ? road_node::side_type::shoulder : type == 2 ? road_node::side_type::sidewalk : road_node::side_type::none); + if (data.type != road_node::side_type::none && data.width <= 0.0f) + data.width = 1.5f; + changed = true; + } + if (data.type != road_node::side_type::none) + { + ImGui::Indent(); + if (number("Width [m]", data.width, 0.25f, "%.2f")) + { + data.width = std::clamp(data.width, 0.1f, 20.0f); + changed = true; + } + if (material("Material", tool.sides[side], sizeof(tool.sides[side]), data.material)) + changed = true; + if (data.material.empty() || data.material == "none") + ImGui::TextDisabled("Pick a material to have the side drawn"); + ImGui::Unindent(); + } + ImGui::PopID(); + } + // kerbs + if (ImGui::Checkbox("Kerb on the left", &State.kerbs[0])) + changed = true; + ImGui::SameLine(); + if (ImGui::Checkbox("on the right", &State.kerbs[1])) + changed = true; + auto const island{State.divided() && State.median.type == road_node::median_type::island}; + if (State.kerbs[0] || State.kerbs[1] || island) + { + ImGui::Indent(); + if (number("Kerb width [m]", State.kerbwidth, 0.05f, "%.2f")) + { + State.kerbwidth = std::clamp(State.kerbwidth, 0.05f, 2.0f); + changed = true; + } + if (material("Kerb material", tool.kerbtext, sizeof(tool.kerbtext), State.kerbmaterial)) + changed = true; + if ((State.kerbs[0] || State.kerbs[1]) && (State.kerbmaterial.empty() || State.kerbmaterial == "none")) + ImGui::TextDisabled("Pick a material to have the kerbs drawn"); + ImGui::Unindent(); + } + if (State.kerbs[0] || State.kerbs[1] || island || State.sides[0].type == road_node::side_type::sidewalk || State.sides[1].type == road_node::side_type::sidewalk) + { + if (number("Kerb height [m]", State.kerbheight, 0.01f, "%.2f")) + { + State.kerbheight = std::clamp(State.kerbheight, 0.0f, 1.0f); + changed = true; + } + } + if (State.sides[0].type == road_node::side_type::shoulder || State.sides[1].type == road_node::side_type::shoulder) + { + if (number("Bank width [m]", State.slope.x, 0.25f, "%.2f")) + { + State.slope.x = std::clamp(State.slope.x, 0.0f, 20.0f); + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "The bank every shoulder of the road is closed with, unless the side has a bank of its own set below."); + if (number("Bank drop [m]", State.slope.y, 0.1f, "%.2f")) + { + State.slope.y = std::clamp(State.slope.y, 0.0f, 20.0f); + changed = true; + } + } + + // banks leading to the ground + if (ImGui::CollapsingHeader("Banks to the ground")) + { + ImGui::TextDisabled("A slope from the edge of the road down, or up, to the ground."); + ImGui::TextDisabled("Set here it goes to every piece the changes go to; the button below"); + ImGui::TextDisabled("works out each piece on its own."); + char const *banknames[]{"Bank on the left", "Bank on the right"}; + for (int side = 0; side < 2; ++side) + { + ImGui::PushID(100 + side); + auto &bank{State.banks[side]}; + if (ImGui::Checkbox(banknames[side], &bank.set)) + { + if (bank.set && bank.width[0] <= 0.f && bank.width[1] <= 0.f) + { + bank.width = {State.slope.x, State.slope.x}; + bank.drop = {State.slope.y, State.slope.y}; + } + changed = true; + } + if (bank.set) + { + ImGui::Indent(); + if (number("Width at the start [m]", bank.width[0], 0.5f, "%.2f")) + changed = true; + if (number("Drop at the start [m]", bank.drop[0], 0.25f, "%.2f")) + changed = true; + if (number("Width at the end [m]", bank.width[1], 0.5f, "%.2f")) + changed = true; + if (number("Drop at the end [m]", bank.drop[1], 0.25f, "%.2f")) + changed = true; + ImGui::Unindent(); + } + ImGui::PopID(); + } + if (material("Bank material", tool.banktext, sizeof(tool.banktext), State.bankmaterial)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "What a bank is covered with next to a sidewalk, a kerb or the bare edge of the road.\nA bank of a shoulder is covered with the material of the shoulder."); + if (tool.selected != nullptr) + { + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Run for a metre of drop [m]", &tool.bankgrade, 0.25f, 0.5f, "%.2f")) + tool.bankgrade = std::clamp(tool.bankgrade, 0.25f, 10.0f); + if (ImGui::Button("Lead the banks to the ground")) + { + road_bank_to_ground(); + // the layout shown was replaced with what the road is like now + return false; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Looks for the ground on both sides of the road, at the start and at the end of each piece,\n" + "and sets the banks to reach it with the slope given above: down where the road is over the ground, up where it's cut into it.\n" + "Done for the selected piece, or for every piece of the road if the changes go to the whole road."); + } + } + + // the two directions kept apart + if (State.forward > 0 && State.backward > 0 && ImGui::CollapsingHeader("Between the directions")) + { + char const *mediantypes[]{"nothing, the ground shows", "surface closed to the traffic", "island"}; + int type{State.median.type == road_node::median_type::gap ? 0 : State.median.type == road_node::median_type::island ? 2 : 1}; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::Combo("##median", &type, mediantypes, 3)) + { + State.median.type = (type == 0 ? road_node::median_type::gap : type == 2 ? road_node::median_type::island : road_node::median_type::painted); + changed = true; + } + if (number("Apart at the start [m]", State.median.width[0], 0.5f, "%.2f")) + changed = true; + if (number("Apart at the end [m]", State.median.width[1], 0.5f, "%.2f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "How far apart the two directions are led at each end of the piece; 0 at one end and more at the other makes them part along it.\n" + "Set for each piece on its own. The pieces next to it follow it where they meet, and so do the junctions.\n" + "A road with its directions apart can be carried on as two one-way roads: start the Build tool\n" + "on the middle of one of its roadways, at an end of the road with nothing attached."); + if (State.median.type == road_node::median_type::island) + { + if (material("Island material", tool.mediantext, sizeof(tool.mediantext), State.median.material)) + changed = true; + ImGui::TextDisabled("The island is as high as the kerbs; with a kerb material set it gets kerbs of its own."); + if (number("Rounded at the start [m]", State.median.round[0], 0.5f, "%.2f")) + changed = true; + if (number("Rounded at the end [m]", State.median.round[1], 0.5f, "%.2f")) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Length of the rounded end of the island at that end of the piece; half of the width of the island makes it a half circle.\n" + "0: the island is cut straight, with a face. Use it where the next piece has no island."); + } + } + + // traffic + if (number("Traffic share, 100: like any other", State.weight, 10.0f, "%.0f")) + { + State.weight = std::clamp(State.weight, 0.0f, 10000.0f); + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "How willing the drivers are to take this road when they leave a junction, next to the other roads they can take there.\n" + "With 100 for the main road and 20 for a dirt road leaving it, one vehicle in six turns into the dirt road.\n" + "0: nobody takes it, unless there's no other way."); + if (ImGui::Checkbox("Part of a roundabout", &State.roundabout)) + changed = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "A junction made on this road gives the right of way to the traffic going along it,\nand tells the traffic of the road joining it to give way."); + + // single lanes, and the rules for changing between them + if (ImGui::CollapsingHeader("Lanes and lane changes", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::TextDisabled("Left to right when facing along the axis. f: along the axis, b: against it"); + for (std::size_t lane = 0; lane < State.lanes.size(); ++lane) + { + ImGui::PushID(static_cast(lane)); + auto &data{State.lanes[lane]}; + auto const id{State.lane_id(lane)}; + ImGui::TextColored(lane >= static_cast(State.backward) ? ImVec4(0.3f, 0.9f, 0.45f, 1.0f) : ImVec4(1.0f, 0.67f, 0.2f, 1.0f), "%-3s", id.c_str()); + ImGui::SameLine(); + if (number("m##width", data.width, 0.25f, "%.2f")) + { + data.width = std::clamp(data.width, 1.0f, 10.0f); + changed = true; + } + ImGui::SameLine(); + float lanelimit{data.velocity > 0.0f ? data.velocity : 0.0f}; + if (number("km/h##velocity", lanelimit, 10.0f, "%.0f")) + { + data.velocity = (lanelimit > 0.0f ? lanelimit : -1.0f); + changed = true; + } + if (lane + 1 < State.lanes.size() && lane < State.changes.size()) + { + // the entries are in the order of the stored values: none, to the right, to the left, both + auto const next{State.lane_id(lane + 1)}; + auto const toright{"only " + id + " to " + next}; + auto const toleft{"only " + next + " to " + id}; + char const *modes[]{"no lane change", toright.c_str(), toleft.c_str(), "lane change both ways"}; + ImGui::Indent(); + ImGui::SetNextItemWidth(200.0f); + if (ImGui::Combo("##change", &State.changes[lane], modes, 4)) + changed = true; + ImGui::Unindent(); + } + ImGui::PopID(); + } + } + return changed; +} + +void editor_mode::road_choose_tool(int const Tool) +{ + auto &tool{m_roadtool}; + if (Tool == tool.tool) + return; + tool.tool = Tool; + road_cancel(); + if (tool.tool != 0) + { + tool.junction = nullptr; + tool.points.clear(); + tool.marker = nullptr; + // the fields for the names of materials show the layout for the roads to be built again + road_select(nullptr); + } +} + +// the road tools and what the next pieces get, in the tool options window; the selection is drawn in the inspector by render_road_selection() +void editor_mode::render_road_tool_options() +{ + auto &tool{m_roadtool}; + std::string reason; + if (false == editor_road::available(&reason)) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", reason.c_str()); + + // the tools and what the place tool puts are chosen in the toolbar + + if (tool.tool == 1) + { + if (ImGui::Checkbox("Roundabout", &tool.ring)) + road_cancel(); + if (tool.ring) + { + ImGui::TextDisabled("LMB: put a roundabout around the cursor. It's a one-way road going around,"); + ImGui::TextDisabled("with the lanes set below as the lanes along the axis."); + ImGui::TextDisabled("Then untick this and lead the roads out of its side, or into it."); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Radius of the middle of its road [m]", &tool.ringradius, 1.0f, 5.0f, "%.1f")) + tool.ringradius = std::clamp(tool.ringradius, 8.0f, 200.0f); + render_road_material("Island in the middle", tool.islandtext, sizeof(tool.islandtext), tool.island); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "What the middle is covered with; it's raised on a kerb as high as the kerbs of the road.\nnone: the middle is left as it is."); + } + else + { + ImGui::TextDisabled("LMB: start, then each next point. Ctrl: straight ahead. Esc: finish"); + ImGui::TextDisabled("Start or end on a loose end of a road to join it."); + ImGui::TextDisabled("Start or end on the side of a road to make a junction there,"); + ImGui::TextDisabled("or on a junction to give it another road."); + } + if (tool.chain) + { + if (ImGui::Button("Finish (Esc)")) + road_cancel(); + } + // how the road is put on the ground + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Height above the ground [m]", &tool.offset, 0.05f, 0.25f, "%.2f")) + tool.offset = std::clamp(tool.offset, -10.0f, 100.0f); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "How far over the ground under the cursor the points are put.\nA few centimetres keep the surface from flickering where it lies on the ground."); + ImGui::Checkbox("Follow the ground", &tool.follow); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "On: the road is led over the ground between the clicked points, in pieces as short as the ground calls for,\nwith the profile rounded so the slope doesn't change abruptly.\nOff: the road goes straight from a point to the next one, like a ramp or a viaduct."); + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputFloat("Speed limit on new junctions [km/h]", &tool.crossingspeed, 5.0f, 10.0f, "%.0f")) + tool.crossingspeed = std::clamp(tool.crossingspeed, 5.0f, 200.0f); + ImGui::Separator(); + // what's set here is what the next pieces get + render_road_layout(tool.settings); + } + else if (tool.tool == 2) + { + tool.placekind = std::clamp(tool.placekind, 0, 3); + switch (tool.placekind) + { + case 0: + ImGui::TextDisabled("LMB on a road where the rails cross it. The vehicles stop ahead"); + ImGui::TextDisabled("of the rails for as long as a train is coming."); + break; + case 1: + ImGui::TextDisabled("LMB on a lane. The vehicles of the set appear there one by one"); + ImGui::TextDisabled("and drive on the way the lane goes."); + break; + case 3: + ImGui::TextDisabled("LMB on a road: stripes are painted across it there."); + break; + default: + ImGui::TextDisabled("LMB on a lane. The vehicles which get there are taken off the road;"); + ImGui::TextDisabled("put one where the road leaves the scenery."); + break; + } + ImGui::Separator(); + // what's set here is what the next points get + auto &placing{tool.placing[tool.placekind]}; + placing.kind = point_kind(tool.placekind); + render_roadpoint_layout(placing, nullptr); + } + else + ImGui::TextDisabled("The selected road is set up in the inspector"); + if (false == tool.status.empty()) + { + ImGui::Separator(); + ImGui::TextWrapped("%s", tool.status.c_str()); + } +} + +// what the select tool of the roads picked, in the inspector +void editor_mode::render_road_selection() +{ + auto &tool{m_roadtool}; + // typed values are taken when confirmed, so nothing is made anew with each digit + auto const number = [](char const *Label, float &Value, float const Step, char const *Format) { + ImGui::SetNextItemWidth(120.0f); + return ImGui::InputFloat(Label, &Value, Step, Step * 4.0f, Format, ImGuiInputTextFlags_EnterReturnsTrue); + }; + if (tool.tool != 0) + ImGui::TextDisabled("The select tool shows the road picked in the view here"); + else if (tool.marker != nullptr) + { + auto &point{*tool.marker}; + auto state{point.definition()}; + ImGui::Text("%s, %s", point.name().c_str(), point_label(state.kind)); + if (render_roadpoint_layout(state, &point)) + roadpoint_apply(point, state); + if (ImGui::Button("Delete (Del)")) + road_delete(); + } + else if (false == tool.points.empty()) + { + // the road at the selected points + ImGui::Text("%d %s selected", static_cast(tool.points.size()), tool.points.size() == 1 ? "point" : "points"); + ImGui::TextDisabled("Shift+LMB: add a point, or take it out. K on a selected piece: make a point at the cursor"); + auto const ends{editor_road::ends_at(tool.points.front())}; + if (false == ends.empty()) + { + auto const &layout{ends.front().road->definition()}; + auto width{static_cast(layout.width(ends.front().atend ? 1 : 0) * layout.taper[ends.front().atend ? 1 : 0])}; + if (number("Road width here [m]", width, 0.25f, "%.2f")) + road_point_width(std::clamp(width, 1.0f, 100.0f)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Combined width of the lanes at this point; each lane takes its share of it.\nThe pieces on both sides get wider or narrower along their length to meet it.\nSelect several points to keep the width over the pieces between them."); + // heights: set outright, moved by a step, or taken from the ground + auto height{static_cast(tool.points.front().y)}; + if (number("Height [m]", height, 0.1f, "%.2f")) + road_point_heights(std::vector(tool.points.size(), height)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Height of the axis of the road at the point; with several points selected all are put at this height.\nThe slopes of the pieces at the point are kept, so the road stays smooth."); + std::vector heights; + for (auto const &point : tool.points) + heights.emplace_back(point.y); + auto const moved{ImGui::Button("Lower") ? -1.0 : 0.0}; + ImGui::SameLine(); + auto const raised{ImGui::Button("Raise") ? 1.0 : 0.0}; + ImGui::SameLine(); + ImGui::SetNextItemWidth(80.0f); + if (ImGui::InputFloat("by [m]", &tool.heightstep, 0.0f, 0.0f, "%.2f")) + tool.heightstep = std::clamp(tool.heightstep, 0.01f, 50.0f); + if (moved + raised != 0.0) + { + // each point moves from where it is, so the shape of the road between them is kept + for (auto &entry : heights) + entry += (moved + raised) * tool.heightstep; + road_point_heights(heights); + } + if (ImGui::Button("Put on the ground")) + { + // a point with nothing under it comes back with the height it has, and is left where it is + auto const ground{ground_heights(tool.points, true)}; + for (std::size_t index = 0; index < heights.size(); ++index) + if (ground[index] != tool.points[index].y) + heights[index] = ground[index] + tool.offset; + road_point_heights(heights); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Puts each point %.2f m over the ground under it; the height above the ground is set in the Build tool.", tool.offset); + } + if (ImGui::Button(tool.points.size() == 1 ? "Delete the point (Del)" : "Delete the points (Del)")) + road_points_delete(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "Takes the point out of the road: the pieces on its two sides become one piece,\n" + "led from the far end of one to the far end of the other the way the road goes there.\n" + "A point the road ends at takes the last piece of the road with it."); + } + else if (tool.junction != nullptr) + { + auto &junction{*tool.junction}; + if (junction.definition().arms.size() == 2) + { + ImGui::Text("%s, a stretch where the road changes its lanes", junction.name().c_str()); + ImGui::TextDisabled("It's drawn as a part of the road, and leads the lanes of one side to the lanes of the other."); + } + else + { + ImGui::Text("%s, %d roads, %d ways through", junction.name().c_str(), static_cast(junction.definition().arms.size()), static_cast(junction.movements().size())); + ImGui::TextDisabled("The ways through are made for the roads attached at the moment."); + ImGui::TextDisabled("Build tool, LMB on the junction: lead another road out of it"); + } + render_junction_layout(junction); + if (ImGui::Button("Delete (Del)")) + road_delete(); + } + else if (tool.selected != nullptr) + { + auto const &road{*tool.selected}; + auto const &layout{road.definition()}; + if (layout.changing()) + ImGui::Text("%s, %.1f m, %d + %d to %d + %d lanes", road.name().c_str(), layout.length(), layout.lanes_at(0, true), layout.lanes_at(0, false), layout.lanes_at(1, true), + layout.lanes_at(1, false)); + else + ImGui::Text("%s, %.1f m, %d + %d lanes", road.name().c_str(), layout.length(), layout.forward, layout.backward); + if (ImGui::Button("Delete (Del)")) + road_delete(); + ImGui::SameLine(); + ImGui::TextDisabled("K: split at the cursor"); + if (tool.settings.changing()) + ImGui::TextDisabled("This piece has lanes of its own at its end, so it's changed alone"); + else + ImGui::Checkbox("Changes go to the whole road", &tool.whole); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", "A road is a string of pieces. With this on a change of the layout is made to all of them,\nexcept for the lane change rules, which are always set for the selected piece alone.\nWith this off the selected piece alone is changed, and the road next to it is fitted to it:\nmade wider or narrower towards it, or given a stretch where the lanes change if their number differs.\nSplit a piece (K) to get a piece of the length you need."); + ImGui::Separator(); + if (tool.selected != nullptr && render_road_layout(tool.settings)) + road_apply(); + } + else + { + ImGui::TextDisabled("LMB: select a road piece, a junction, a point where pieces meet,"); + ImGui::TextDisabled("a level crossing or a traffic point"); + } +} + +void editor_mode::draw_road_overlay() const +{ + auto const &tool{m_roadtool}; + if (false == tool.lanes && false == tool.window) + return; + + screen_projection projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + glm::dvec3 const camera{Global.pCamera.Pos}; + // pieces of a line needed to draw a path: one for a straight, more the longer a bend is and the nearer it is + auto const pieces = [&camera](segment_data const &Path, glm::dvec3 const &Location) { + auto const range{glm::distance(Location, camera)}; + auto const length{glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end])}; + return std::clamp(static_cast(length / (range < 100.0 ? 3.0 : range < 300.0 ? 8.0 : 20.0)), 2, 12); + }; + auto const drawpath = [&](segment_data const &Path, ImU32 const Color, float const Thickness, int const Samples) { + auto previous{path_point(Path, 0.0)}; + for (int i = 1; i <= Samples; ++i) + { + auto const next{path_point(Path, static_cast(i) / Samples)}; + projection.line(drawlist, previous, next, Color, Thickness); + previous = next; + } + }; + + // lanes, as the vehicles see them: a line for each, with an arrow pointing where the traffic goes + if (tool.lanes) + { + projection.limit = screen_projection::kLaneVertexLimit; + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + auto const range{glm::distance(road->location(), camera)}; + if (range > kLaneRange + 0.5 * glm::distance(road->definition().axis.points[segment_data::point::start], road->definition().axis.points[segment_data::point::end])) + continue; + if (false == projection.room(drawlist)) + break; + auto const against{static_cast(road->definition().backward)}; + // a lane of a piece which gains or loses lanes is a path at each end of the piece; the ways between these are drawn below + auto const lanecount{road->definition().lanes.size()}; + for (std::size_t slot = 0; slot < 2 * lanecount; ++slot) + { + auto const lane{slot / 2}; + auto const *track{road->lane_track(lane, static_cast(slot % 2))}; + if (track == nullptr || track->m_paths.empty() || (slot % 2 == 1 && track == road->lane_track(lane, 0))) + continue; + auto const &path{track->m_paths.front()}; + auto const color{lane >= against ? kForwardColor : kBackwardColor}; + drawpath(path, color, 2.0f, is_curved(path) ? pieces(path, road->location()) : 1); + if (range > 300.0 || road->definition().changing()) + continue; + auto const middle{path_point(path, 0.5)}; + auto direction{path_point(path, 0.52) - middle}; + if (glm::length(direction) < 1e-6) + continue; + direction = glm::normalize(direction); + glm::dvec3 const across{direction.z, 0.0, -direction.x}; + auto const tip{middle + direction * 1.2}; + projection.line(drawlist, tip, middle - direction * 0.6 + across * 0.6, color, 2.0f); + projection.line(drawlist, tip, middle - direction * 0.6 - across * 0.6, color, 2.0f); + } + for (auto const *turn : road->turns()) + { + if (turn != nullptr && false == turn->m_paths.empty() && range < 300.0) + drawpath(turn->m_paths.front(), kTurnColor, 1.5f, 8); + } + // ways from the lanes at one end to the lanes at the other + for (auto const &way : road->ways()) + drawpath(way, kWayColor, 1.5f, pieces(way, road->location())); + } + // ways through the junctions + for (auto const *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved || glm::distance(junction->location(), camera) > kLaneRange) + continue; + for (auto const &way : junction->movements()) + drawpath(way, kWayColor, 1.5f, pieces(way, junction->location())); + } + projection.limit = screen_projection::kVertexLimit; + } + if (false == tool.window) + return; + + // outline of a road piece: its axis and the edges of its surface + auto const drawroad = [&](road_node::state const &State, ImU32 const Color) { + auto const samples{is_curved(State.axis) || State.taper[0] != State.taper[1] ? pieces(State.axis, State.axis.points[segment_data::point::start]) : 1}; + drawpath(State.axis, Color, 3.0f, samples); + glm::dvec3 previous[2]; + for (int i = 0; i <= samples; ++i) + { + auto const t{static_cast(i) / samples}; + auto const position{path_point(State.axis, t)}; + auto const tangent{path_direction(State.axis, t)}; + glm::dvec3 across{tangent.z, 0.0, -tangent.x}; + if (glm::length(across) > 1e-6) + across = glm::normalize(across); + auto const halfwidth{0.5 * State.span(t)}; + glm::dvec3 const edges[2]{position + across * halfwidth, position - across * halfwidth}; + if (i > 0) + { + projection.line(drawlist, previous[0], edges[0], Color, 1.5f); + projection.line(drawlist, previous[1], edges[1], Color, 1.5f); + } + previous[0] = edges[0]; + previous[1] = edges[1]; + } + }; + auto const drawpoint = [&](glm::dvec3 const &Point, ImU32 const Color, float const Radius, bool const Filled) { + ImVec2 screen; + if (false == projection.room(drawlist) || false == projection.project(Point, screen) || projection.outside(screen, screen)) + return; + if (Filled) + drawlist->AddCircleFilled(screen, Radius, Color); + else + drawlist->AddCircle(screen, Radius, Color, 20, 3.0f); + }; + + // outline of a junction: the edge of its surface, with the ends of the roads it's made for + auto const drawjunction = [&](junction_node::state const &State, ImU32 const Color) { + auto const outline{State.outline()}; + for (std::size_t i = 0; i < outline.size(); ++i) + projection.line(drawlist, outline[i], outline[(i + 1) % outline.size()], Color, 2.0f); + for (auto const &arm : State.arms) + drawpoint(arm.position, Color, 5.0f, true); + }; + + // a circle lying on the ground + auto const drawring = [&](glm::dvec3 const &Centre, double const Radius, ImU32 const Color) { + int const pieces{24}; + glm::dvec3 previous{Centre + glm::dvec3{Radius, 0.0, 0.0}}; + for (int i = 1; i <= pieces; ++i) + { + auto const angle{kFullTurn * i / pieces}; + glm::dvec3 const next{Centre + glm::dvec3{Radius * std::cos(angle), 0.0, Radius * std::sin(angle)}}; + projection.line(drawlist, previous, next, Color, 2.0f); + previous = next; + } + }; + auto const drawlabel = [&](glm::dvec3 const &Point, ImU32 const Color, char const *Text) { + ImVec2 screen; + if (false == projection.room(drawlist) || false == projection.project(Point, screen) || projection.outside(screen, screen)) + return; + drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 8.0f), Color, Text); + }; + // a level crossing or a traffic point. Point: the node it's drawn for, nullptr for one which isn't made yet + auto const drawmarker = [&](roadpoint_node::state const &State, roadpoint_node const *Point, ImU32 const Color) { + drawpoint(State.position, Color, 6.0f, true); + auto const labelled{glm::distance(State.position, camera) < 200.0}; + switch (State.kind) + { + case roadpoint_node::kind_type::crossing: + { + // the tracks within the circle are the ones it's closed for, the lines are where the vehicles stop + drawring(State.position, State.clearance, Color); + auto const closed{Point != nullptr && Point->closed()}; + if (Point != nullptr) + { + for (auto const &stop : Point->stops()) + { + glm::dvec3 across{stop.direction.z, 0.0, -stop.direction.x}; + if (glm::length(across) < 1e-6) + continue; + across = glm::normalize(across) * (0.5 * stop.width); + projection.line(drawlist, stop.position - across, stop.position + across, closed ? kRefusedColor : Color, 4.0f); + } + } + if (labelled) + drawlabel(State.position, Color, closed ? "crossing, closed" : "crossing"); + break; + } + case roadpoint_node::kind_type::spawn: + { + drawpoint(State.position, Color, 11.0f, false); + if (labelled) + drawlabel(State.position, Color, "spawn"); + break; + } + case roadpoint_node::kind_type::despawn: + { + drawring(State.position, State.radius, Color); + if (labelled) + drawlabel(State.position, Color, "removal"); + break; + } + case roadpoint_node::kind_type::crosswalk: + { + drawpoint(State.position, Color, 11.0f, false); + if (labelled) + drawlabel(State.position, Color, "pedestrians"); + break; + } + } + }; + for (auto const *point : simulation::Roadpoints.sequence()) + { + if (point == nullptr || point->m_editorremoved || glm::distance(point->location(), camera) > kPointRange) + continue; + auto const kind{point->definition().kind}; + drawmarker(point->definition(), point, + point == tool.marker ? kSelectedColor : + kind == roadpoint_node::kind_type::spawn ? kSpawnColor : + kind == roadpoint_node::kind_type::despawn ? kDespawnColor : + kind == roadpoint_node::kind_type::crosswalk ? kCrosswalkColor : + kCrossingColor); + if (point == tool.marker) + drawpoint(point->definition().position, kSelectedColor, 15.0f, false); + } + + if (tool.selected != nullptr && false == tool.selected->m_editorremoved) + { + auto const &state{tool.selected->definition()}; + drawroad(state, kSelectedColor); + drawpoint(state.axis.points[segment_data::point::start], kSelectedColor, 6.0f, true); + drawpoint(state.axis.points[segment_data::point::end], kSelectedColor, 6.0f, true); + } + if (tool.junction != nullptr && false == tool.junction->m_editorremoved) + { + drawjunction(tool.junction->definition(), kSelectedColor); + // the roads are numbered the way the window lists them + auto const &arms{tool.junction->definition().arms}; + for (std::size_t arm = 0; arm < arms.size(); ++arm) + drawlabel(arms[arm].position, kSelectedColor, std::to_string(arm + 1).c_str()); + } + + std::string hint; + if (tool.tool == 1 && tool.ring) + { + // the road going around, as wide as its lanes + auto layout{tool.settings}; + layout.backward = 0; + layout.forward = std::max(1, layout.forward); + layout.lanes.clear(); + layout.normalize(); + auto const centre{tool.mouse + glm::dvec3{0.0, tool.offset, 0.0}}; + drawring(centre, tool.ringradius + 0.5 * layout.width(), kPreviewColor); + drawring(centre, std::max(0.0, tool.ringradius - 0.5 * layout.width()), kPreviewColor); + hint = "LMB: put a roundabout here"; + } + else if (tool.tool == 1) + { + if (tool.chain) + { + auto const &plan{tool.plan}; + drawpoint(tool.point, kPreviewColor, 6.0f, true); + if (plan.pieces.empty()) + { + projection.line(drawlist, tool.point, tool.mouse, kRefusedColor, 2.0f); + hint = plan.error + " "; + } + else + { + // junctions the road would start or end with + if (plan.hasstart) + drawjunction(plan.start.layout, kPreviewColor); + if (plan.hasend) + drawjunction(plan.end.layout, kPreviewColor); + } + for (auto const &piece : plan.pieces) + { + auto state{tool.settings}; + state.axis = piece; + drawroad(state, kPreviewColor); + } + if (plan.snap.valid()) + drawpoint(plan.snap.position, kSnapColor, 14.0f, false); + hint += (tool.branchroad != nullptr ? "LMB: lead the road out to the cursor, with a junction where it leaves Esc: give up" : + tool.branchjunction != nullptr ? "LMB: lead the road out to the cursor, as another road of the junction Esc: give up" : + "LMB: build up to the cursor Ctrl: straight ahead Esc: finish Ctrl+Z: undo the last piece"); + } + else + { + auto const end{editor_road::find_end(tool.mouse, kSnapRadius)}; + if (end.valid()) + { + drawpoint(end.position, kSnapColor, 14.0f, false); + hint = "LMB: carry on from this end"; + } + else if (tool.hashover) + { + drawpoint(tool.hover, kSnapColor, 5.0f, true); + drawpoint(tool.hover, kSnapColor, 14.0f, false); + hint = "LMB: lead a road out of here, with a junction"; + } + else + { + hint = "LMB: set where the road starts; on a loose end of a road to carry it on, on the side of a road or on a junction to lead a road out of it"; + } + } + } + else if (tool.tool == 2) + { + if (tool.hastarget) + { + drawmarker(tool.target, nullptr, kPreviewColor); + if (glm::length(tool.targetdirection) > 1e-6) + { + // the way the traffic goes on the lane + auto const direction{glm::normalize(tool.targetdirection)}; + glm::dvec3 const across{direction.z, 0.0, -direction.x}; + auto const tip{tool.target.position + direction * 5.0}; + projection.line(drawlist, tool.target.position, tip, kPreviewColor, 2.0f); + projection.line(drawlist, tip, tip - direction * 1.2 + across * 0.8, kPreviewColor, 2.0f); + projection.line(drawlist, tip, tip - direction * 1.2 - across * 0.8, kPreviewColor, 2.0f); + } + hint = std::string{"LMB: put a "} + point_label(tool.target.kind) + " here"; + if (false == tool.targetnote.empty()) + hint += " " + tool.targetnote; + } + else + { + hint = (tool.targetnote.empty() ? std::string{"LMB: put a "} + point_label(point_kind(tool.placekind)) + " on a road" : tool.targetnote); + } + } + else + { + // points where the pieces meet or end, to pick from + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved || glm::distance(road->location(), camera) > kPointRange) + continue; + drawpoint(road->definition().axis.points[segment_data::point::start], kPointColor, 3.5f, true); + drawpoint(road->definition().axis.points[segment_data::point::end], kPointColor, 3.5f, true); + } + for (auto const &point : tool.points) + { + drawpoint(point, kSelectedColor, 6.0f, true); + drawpoint(point, kSelectedColor, 11.0f, false); + } + hint = tool.marker != nullptr ? "LMB: select a piece, a junction, a point or a marker Del: delete Ctrl+Z: undo" : + false == tool.points.empty() ? "LMB: select a point Shift+LMB: add a point or take it out Del: delete the points Ctrl+Z: undo" : + tool.selected != nullptr ? "LMB: select a piece, a junction or a point (white dot) K: split at the cursor Del: delete Ctrl+Z: undo" : + tool.junction != nullptr ? "LMB: select a piece, a junction or a point (white dot) Del: delete Ctrl+Z: undo" : + "LMB: select a piece, a junction, a point (white dot) or a marker"; + } + // shown above the place the track tools put their hints at + ImVec2 const position{editor_ui::view_min().x + 12.0f, editor_ui::view_max().y - 56.0f}; + auto const size{ImGui::CalcTextSize(hint.c_str())}; + drawlist->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 150), 4.0f); + drawlist->AddText(position, IM_COL32(255, 255, 255, 230), hint.c_str()); +} diff --git a/application/editormodeselection.cpp b/application/editormodeselection.cpp new file mode 100644 index 00000000..a8c84533 --- /dev/null +++ b/application/editormodeselection.cpp @@ -0,0 +1,362 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// more models selected at once, the clipboard, groups and the menu at the cursor of the scenery editor + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" + +#include "model/AnimModel.h" +#include "rendering/renderer.h" +#include "scene/scenelayers.h" +#include "scene/scenenodegroups.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/translation.h" +#include "imgui/imgui.h" + +#include +#include +#include + +namespace +{ + +// the models of the selection can be grouped, copied and dropped onto the ground; tracks and other nodes are left out +TAnimModel *as_model(scene::basic_node *Node) +{ + return Node != nullptr ? dynamic_cast(Node) : nullptr; +} + +} // namespace + +std::vector editor_mode::selected_nodes() const +{ + std::vector nodes; + if (m_node == nullptr) + return nodes; + nodes.push_back(m_node); + // the selection of more models is a thing of the surroundings + if (ui()->mode() != nodebank_panel::MODIFY) + return nodes; + for (auto *node : m_selection) + if (node != nullptr && std::find(nodes.begin(), nodes.end(), node) == nodes.end()) + nodes.push_back(node); + return nodes; +} + +std::vector editor_mode::selection_with_groups() const +{ + auto nodes{selected_nodes()}; + auto const count{nodes.size()}; + for (std::size_t index = 0; index < count; ++index) + { + // NOTE: the groups up to the first one stand for no group, see scene::basic_editor + if (nodes[index]->group() <= 1) + continue; + for (auto *member : scene::Groups.group(nodes[index]->group()).nodes) + if (std::find(nodes.begin(), nodes.end(), member) == nodes.end()) + nodes.push_back(member); + } + return nodes; +} + +void editor_mode::select_node(scene::basic_node *Node, bool const Additive) +{ + if (false == Additive) + { + m_selection.clear(); + m_node = Node; + ui()->set_node(m_node); + return; + } + if (Node == nullptr) + return; + if (m_node == nullptr) + { + m_node = Node; + } + else if (Node == m_node) + { + // taken out; the one selected before it takes the gizmo + if (m_selection.empty()) + { + m_node = nullptr; + } + else + { + m_node = m_selection.back(); + m_selection.pop_back(); + } + } + else if (auto const found{std::find(m_selection.begin(), m_selection.end(), Node)}; found != m_selection.end()) + { + m_selection.erase(found); + } + else + { + // the model selected last gets the gizmo + m_selection.push_back(m_node); + m_node = Node; + } + ui()->set_node(m_node); + auto const count{selected_nodes().size()}; + if (count > 1) + ui()->set_status(format(STR_C("%zu models selected; Shift+LMB adds or takes one out"), count)); +} + +void editor_mode::draw_selection_overlay() +{ + if (ui()->mode() != nodebank_panel::MODIFY || m_node == nullptr) + return; + auto const nodes{selection_with_groups()}; + if (nodes.size() < 2) + return; + // a ring around each model of the selection, the one with the gizmo brighter, the models of their groups dimmer + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const direct{selected_nodes()}; + for (auto *node : nodes) + { + ImVec2 point; + if (false == projection.project(node->location(), point)) + continue; + auto const chosen{std::find(direct.begin(), direct.end(), node) != direct.end()}; + auto const color{node == m_node ? overlay_color::highlight : chosen ? overlay_color::selected : overlay_color::marked}; + drawlist->AddCircle(point, node == m_node ? 9.0f : 7.0f, color, 16, 2.0f); + } +} + +void editor_mode::selection_snapshots(EditorSnapshot::Action const Action) +{ + // one step of the history for all of them + auto joined{false}; + for (auto *node : selection_with_groups()) + { + push_snapshot(node, Action); + if (false == m_history.empty()) + m_history.back().joined = joined; + joined = true; + } +} + +void editor_mode::selection_follow(glm::dvec3 const &Moved, glm::vec3 const &Turned, glm::dvec3 const &Pivot) +{ + // a group is moved by any of its models, once; the one of m_node is moved with it already + std::vector moved; + if (m_node != nullptr && m_node->group() > 1) + moved.push_back(m_node->group()); + for (auto *node : selected_nodes()) + { + if (node == m_node) + continue; + if (node->group() > 1) + { + if (std::find(moved.begin(), moved.end(), node->group()) != moved.end()) + continue; + moved.push_back(node->group()); + } + if (Turned != glm::vec3(0.0f)) + { + // turned in place, then carried around the turning point + m_editor.rotate(node, Turned, 0.0f); + auto const location{Pivot + glm::rotateY(node->location() - Pivot, glm::radians(Turned.y))}; + m_editor.translate(node, location, true); + } + if (Moved != glm::dvec3(0.0)) + m_editor.translate(node, node->location() + Moved, true); + } +} + +glm::dvec3 editor_mode::cursor_ground() +{ + return placement_on_ground(Camera.Pos + clamp_mouse_offset_to_max(GfxRenderer->Mouse_Position())); +} + +void editor_mode::copy_selection() +{ + auto const nodes{selection_with_groups()}; + auto const *anchor{as_model(m_node)}; + if (anchor == nullptr) + { + ui()->set_status(STR("Select a model to copy"), true); + return; + } + m_clipboard.clear(); + std::map groups; + for (auto *node : nodes) + { + auto *model{as_model(node)}; + if (model == nullptr) + continue; + clipboard_entry entry; + model->export_as_text(entry.definition); + entry.offset = model->location() - anchor->location(); + entry.angles = model->Angles(); + entry.scale = model->Scale(); + if (model->group() > 1) + entry.group = groups.emplace(model->group(), static_cast(groups.size())).first->second; + m_clipboard.push_back(std::move(entry)); + } + ui()->set_status(format(STR_C("%zu models copied; Ctrl+V puts them at the cursor"), m_clipboard.size())); +} + +void editor_mode::paste_clipboard(glm::dvec3 const &Location) +{ + if (m_clipboard.empty()) + { + ui()->set_status(STR("Nothing copied yet; Ctrl+C copies the selected models"), true); + return; + } + // one step of the history, the way an array is: the copies are taken out together + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Array; + std::vector made; + std::map> groups; + for (auto const &entry : m_clipboard) + { + // NOTE: no names for the copies, same as with the brush and the array + auto *model{simulation::State.create_model(entry.definition, std::string{}, Location + entry.offset)}; + if (model == nullptr) + continue; + model->location(Location + entry.offset); + model->Angles(entry.angles); + model->Scale(entry.scale); + add_to_hierarchy(model); + EditorSnapshot::array_copy copy; + copy.model = model; + copy.uuid = model->uuid; + snap.copies.push_back(std::move(copy)); + snap.layer = model->layer(); + made.push_back(model); + if (entry.group >= 0) + groups[entry.group].push_back(model); + } + if (made.empty()) + return; + m_history.push_back(std::move(snap)); + g_redo.clear(); + for (auto const &group : groups) + if (group.second.size() > 1) + scene::Groups.make(group.second); + // the copies are the selection now + m_selection.assign(made.begin() + 1, made.end()); + m_node = made.front(); + ui()->set_node(m_node); + ui()->set_status(format(STR_C("%zu models pasted; Ctrl+Z takes them back"), made.size())); +} + +void editor_mode::group_selection() +{ + auto nodes{selection_with_groups()}; + nodes.erase(std::remove_if(nodes.begin(), nodes.end(), [](scene::basic_node *Node) { return as_model(Node) == nullptr; }), nodes.end()); + if (nodes.size() < 2) + { + ui()->set_status(STR("Select two models or more to group them: Shift+LMB adds one to the selection"), true); + return; + } + for (auto *node : nodes) + { + std::string reason; + if (node->from_template() || false == scene::Layers.editable(node, &reason)) + { + ui()->set_status("\"" + (node->name().empty() ? std::string{"(unnamed node)"} : node->name()) + "\" can't be grouped: " + reason, true); + return; + } + } + // a group is written to the scenery as one block, in the layer of the model with the gizmo + for (auto *node : nodes) + if (node->layer() != m_node->layer()) + scene::Layers.move(node, m_node->layer()); + scene::Groups.make(nodes); + m_selection.clear(); + ui()->set_status(format(STR_C("%zu models grouped: a click on one selects them all; Ctrl+Shift+G takes the group apart"), nodes.size())); +} + +void editor_mode::ungroup_selection() +{ + std::vector groups; + for (auto *node : selected_nodes()) + { + if (node->group() <= 1 || std::find(groups.begin(), groups.end(), node->group()) != groups.end()) + continue; + // the groups of the scenery templates are their includes, they can't be taken apart here + auto const &members{scene::Groups.group(node->group()).nodes}; + if (std::any_of(members.begin(), members.end(), [](scene::basic_node const *Member) { return Member->from_template(); })) + continue; + groups.push_back(node->group()); + } + if (groups.empty()) + { + ui()->set_status(STR("The selected model isn't in a group"), true); + return; + } + for (auto const group : groups) + scene::Groups.dissolve(group); + ui()->set_status(format(STR_C("%zu groups taken apart"), groups.size())); +} + +void editor_mode::drop_selection_to_ground() +{ + // each model by itself, a group follows the ground under its models + auto joined{false}; + for (auto *node : selection_with_groups()) + { + auto const size{m_history.size()}; + snap_to_ground(node, true, joined); + joined = joined || m_history.size() != size; + } +} + +void editor_mode::render_context_menu() +{ + if (m_contextopen) + { + ImGui::OpenPopup("##editorcontext"); + m_contextopen = false; + } + if (false == ImGui::BeginPopup("##editorcontext")) + return; + auto const selected{m_node != nullptr}; + auto const model{as_model(m_node) != nullptr}; + auto const count{selected_nodes().size()}; + if (count > 1) + ImGui::TextDisabled(STR_C("%zu models selected"), count); + else if (selected) + ImGui::TextDisabled("%s", m_node->name().empty() ? "(unnamed node)" : m_node->name().c_str()); + else + ImGui::TextDisabled("%s", STR_C("Nothing selected")); + ImGui::Separator(); + auto const item = [](char const *Label, char const *Keys, bool const Enabled, char const *Tooltip) { + auto const chosen{ImGui::MenuItem(Label, Keys, false, Enabled)}; + if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + ImGui::SetTooltip("%s", Tooltip); + return chosen; + }; + if (item(STR_C("Copy"), "Ctrl+C", model, STR_C("Copies the selected models with their groups"))) + copy_selection(); + if (item(STR_C("Paste here"), "Ctrl+V", false == m_clipboard.empty(), STR_C("Puts the copied models here, around this point as they were around the selected one"))) + paste_clipboard(m_contextpoint); + ImGui::Separator(); + if (item(STR_C("Group"), "Ctrl+G", model && count > 1, STR_C("The selected models move together from now on; a click on one selects them all"))) + group_selection(); + if (item(STR_C("Ungroup"), "Ctrl+Shift+G", selected && m_node->group() > 1, STR_C("The models of the group move each by itself again"))) + ungroup_selection(); + if (item(STR_C("Drop to the ground"), STR_C("End"), selected, STR_C("Puts each selected model, also each model of their groups, down onto the terrain or the object under it"))) + drop_selection_to_ground(); + ImGui::Separator(); + if (item(STR_C("Fly to the selected"), "F", selected, STR_C("The camera flies to the selected model and looks at it"))) + start_focus(m_node, 0.6); + if (item(STR_C("Delete"), STR_C("Del"), selected, STR_C("Deletes the selected models"))) + delete_selected(); + ImGui::EndPopup(); +} diff --git a/application/editormodeselftest.cpp b/application/editormodeselftest.cpp new file mode 100644 index 00000000..98d4a1e2 --- /dev/null +++ b/application/editormodeselftest.cpp @@ -0,0 +1,1365 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "editor/editorSettings.hpp" +#include "editor/editorGeometry.hpp" +#include "application/application.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" + +#include "simulation/simulation.h" +#include "simulation/simulationtime.h" +#include "scene/scenelayers.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Track.h" +#include "world/MemCell.h" +#include "world/Event.h" +#include "vehicle/DynObj.h" +#include "model/AnimModel.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "world/Sweep.h" +#include "input/keyboardinput.h" +#include +#include +#include +#include "editor/editorFormat.hpp" + +#include +#include +#include +#include +#include + +// the script is a text file, one action a line: +// wait +// camera from the first point, looking at the second +// select +// sweep =... model, params (separated by commas), lateral, height, side (origin, near, far, +// centre), top (origin, bottom, top, edge), mode (bend, repeat), step, +// length (original, all, or metres), from, to, widen (0, 1), face (0, 1), tilt (0, 1) +// selectmodel the model instance nearest the point +// bend =... the selected model bent along a track: track (index on the list), axis (auto, z, x), +// turned (0, 1), along (start, origin, middle, end, point), side (origin, near, far, +// middle, point), height (origin, bottom, top, edge, point), point=,,, +// pick=,, (a point of the scene picked as the reference point), go (0, 1) +// selectinclude the include placed nearest the point +// addmodel the model instance nearest the point added to the selection, or taken out (Shift+LMB) +// selection logs the selected models, with the models of their groups +// moveselection the selection moved by the offset, as the gizmo moves it +// turnselection the selection turned around the model with the gizmo +// copy, paste , group, ungroup, drop, delete as the keys of the selection do them +// bendset =... the bent model being edited: length, lateral, height, along, side, height +// bendprobe logs the bent model being edited +// undo, redo +// key [ctrl] a key or right button event, as the window gives it +// lineside the tab of the objects along the track in the track window +// event queues the event +// memcell logs the memory cell +// bendpanel unfolds the bend section of the toolset +// openscenerypopup the window of File / Open scenery, its list searched for zz_ +// openscenery the editor starts again with the scenery +// checkspans logs whether the text the scenery files hold at each node's place starts and ends it +// spread logs the paths the spread from the track takes +// newscenery the editor starts again with the wizard of a new scenery +// vehicles logs each vehicle: its head, facing, and what its couplers hold +// terrainmode the terrain field of work, with its texture browser +// terrainsculpt , terrainpaint , +// terrainlevel [mode] as a stroke of the brush +// terrainadd chunks made around the camera, count x count, as the button of the chunks panel does it +// terrainprobe logs the height of the heightmap terrain and the materials of the chunk +// terrainconvert converts every material of the terrain files of the scenery +// terrainmodifier a straight modifier, as the vertical profile makes them +// terraintouchup <0|1> the brushes touch up over the modifiers, or shape the ground under them +// terrainrestore a stroke of the restore brush +// terrainbake the modifier baked into the ground +// terrainselect [add] the chunk selected with the spacing tool (add: Ctrl) +// terrainresolution [paint 0|1] the selected chunks converted, as the button of the resolution panel does it +// terrainfill ... the area outlined filled, as the fill tool does it +// terrainturn the textures of the palette entry turned +// terrainwater [size] material and repeat of a body of water; terrainwaterdefault +// orthophoto [in scene 0|1] the imagery layer on, placed and fitted as given +// screenshot +// save +// log +// quit +void editor_mode::selftest_step() +{ + auto &test{m_selftest}; + if (false == test.loaded) + { + test.loaded = true; + auto const *file{std::getenv("EU07_EDITOR_SELFTEST")}; + if (file == nullptr) + return; + std::ifstream stream(file); + std::string line; + while (std::getline(stream, line)) + if (false == line.empty() && line.front() != '#') + test.lines.push_back(line); + WriteLog("SELFTEST: " + std::to_string(test.lines.size()) + " steps from " + file); + } + if (test.wait > 0) + { + --test.wait; + return; + } + while (test.next < test.lines.size()) + { + std::istringstream words(test.lines[test.next++]); + std::string command; + words >> command; + if (command == "wait") + { + words >> test.wait; + return; + } + if (command == "camera") + { + glm::dvec3 from, to; + words >> from.x >> from.y >> from.z >> to.x >> to.y >> to.z; + auto const look{glm::normalize(to - from)}; + Camera.Pos = from; + Camera.Angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + Global.pCamera = Camera; + m_focus_active = false; + } + else if (command == "select") + { + std::string name; + words >> name; + m_node = simulation::Paths.find(name); + m_track_point = {}; + ui()->set_node(m_node); + WriteLog("SELFTEST select " + name + (m_node != nullptr ? "" : ": no such track")); + } + else if (command == "sweep") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto &tool{m_sweep}; + auto const get = [&](char const *Key, std::string const &Default) { auto const found{values.find(Key)}; return found != values.end() ? found->second : Default; }; + tool.edited = nullptr; + tool.curve_for = nullptr; + std::snprintf(tool.model, sizeof(tool.model), "%s", get("model", "").c_str()); + tool.parameters.clear(); + std::stringstream parameters(get("params", "")); + std::string parameter; + while (std::getline(parameters, parameter, ',')) + { + tool.parameters.emplace_back(); + std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", parameter.c_str()); + } + tool.parameters_for = tool.model; + auto &settings{tool.settings}; + settings.lateral = std::stod(get("lateral", "0")); + settings.height = std::stod(get("height", "0")); + auto const side{get("side", "origin")}; + settings.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : 0; + auto const top{get("top", "origin")}; + settings.height_anchor = top == "bottom" ? 1 : top == "top" ? 2 : top == "edge" ? 3 : 0; + settings.bend = get("mode", "bend") != "repeat"; + settings.step = std::stod(get("step", "0")); + settings.widen = get("widen", "0") == "1"; + settings.platform = get("platform", "0") == "1"; + settings.face = get("face", "1") == "1"; + settings.tilt = get("tilt", "0") == "1"; + settings.from = std::stod(get("from", "0")); + settings.to = std::stod(get("to", "-1")); + auto const length{get("length", "all")}; + tool.length_mode = length == "original" ? 0 : length == "all" ? 2 : 1; + if (tool.length_mode == 1) + tool.length = std::stod(length); + if (values.count("edge")) + EditorSettings.platform_edge(std::stod(values["edge"])); + tool.measured.clear(); + tool.open = true; + sweep_curve(); + sweep_measure(); + if (values.count("widen")) + settings.widen = get("widen", "0") == "1"; + if (values.count("platform")) + settings.platform = get("platform", "0") == "1"; + if (get("create", "1") == "1") + { + sweep_create(); + WriteLog("SELFTEST sweep: " + tool.status); + } + } + else if (command == "drag") + { + glm::dvec3 from{0.0}, to{0.0}; + words >> from.x >> from.z >> to.x >> to.z; + m_sweep.open = true; + if (false == sweep_press_at(from, false)) + { + WriteLog("SELFTEST drag: the press wasn't taken"); + continue; + } + sweep_drag_at(to); + sweep_release(); + WriteLog("SELFTEST drag: " + m_sweep.status); + } + else if (command == "probe") + { + // edge of the last laid model as it was made: its distance from the axis and the height of its top over the rail head + std::string name; + double step{20.0}; + words >> name >> step; + if (name == "-") + name.clear(); + auto *sweep{name.empty() ? m_sweep.edited : simulation::Sweeps.find(name)}; + if (sweep == nullptr) + { + WriteLog("SELFTEST probe: nothing laid"); + continue; + } + auto const &definition{sweep->definition()}; + double const sign{definition.lateral >= 0.0 ? 1.0 : -1.0}; + struct bin + { + double edge{std::numeric_limits::max()}; + std::vector> points; // lateral, height over the axis + }; + std::map bins; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + double lateral; + auto const station{sweep->project(vertex.position, lateral)}; + if (lateral * sign <= 0.0) + continue; + auto const axis{sweep->frame_at(station)}; + auto &entry{bins[static_cast(std::floor(station / step))]}; + entry.edge = std::min(entry.edge, std::abs(lateral)); + entry.points.emplace_back(std::abs(lateral), vertex.position.y - axis.position.y); + } + WriteLog(format("SELFTEST probe %s: %zu stretches with the model, %.1f - %.1f m of %.1f m", sweep->name().c_str(), bins.size(), sweep->start(), sweep->end(), sweep->length())); + for (auto const &[index, entry] : bins) + { + auto const station{(index + 0.5) * step}; + if (station < sweep->start() || station > sweep->end()) + continue; + double top{-1000.0}; + for (auto const &point : entry.points) + if (point.first < entry.edge + 0.15) + top = std::max(top, point.second); + auto const at{sweep->frame_at(station)}; + auto const head{0.18 + sweep_node::rail_head(at.roll, definition.lateral)}; + WriteLog(format("SELFTEST probe %s at %.0f m: edge %.3f m from the axis, its top %.3f m over the rail head (cant %.1f deg, gauge widening %.3f m, cant part %.3f m)", sweep->name().c_str(), station, entry.edge, top - head, at.roll, + at.widening[definition.lateral >= 0.0 ? 1 : 0], std::max(0.0, at.cant[definition.lateral >= 0.0 ? 1 : 0]))); + } + } + else if (command == "profile") + { + std::string name; + double station{0.0}; + words >> name >> station; + auto *sweep{simulation::Sweeps.find(name)}; + if (sweep == nullptr) + { + WriteLog("SELFTEST profile: no such sweep"); + continue; + } + auto const &definition{sweep->definition()}; + std::map> columns; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + double lateral; + auto const at{sweep->project(vertex.position, lateral)}; + if (std::abs(at - station) > 1.0) + continue; + auto const axis{sweep->frame_at(at)}; + auto const height{vertex.position.y - axis.position.y - 0.18 - sweep_node::rail_head(axis.roll, definition.lateral)}; + auto &column{columns.try_emplace(static_cast(std::floor(lateral / 0.1)), std::numeric_limits::max(), -std::numeric_limits::max()).first->second}; + column.first = std::min(column.first, height); + column.second = std::max(column.second, height); + } + std::string text; + for (auto const &[index, column] : columns) + text += format(" [%.1f %.2f..%.2f]", index * 0.1, column.first, column.second); + WriteLog(format("SELFTEST profile %s at %.0f m:", name.c_str(), station) + text); + } + else if (command == "curvedswitch") + { + std::string name, label; + glm::dvec3 at{0.0}, toward{0.0}; + words >> name >> at.x >> at.z >> toward.x >> toward.z >> label; + m_node = simulation::Paths.find(name); + ui()->set_node(m_node); + show_track_tab(track_tab::turnout); + arm_switch(); + auto &tool{m_switch}; + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + if (false == label.empty() && tool.templates[i].label.find(label) != std::string::npos) + { + tool.armed = i; + break; + } + m_cursor_override = at; + auto const started{start_switch_placement()}; + m_cursor_override.reset(); + if (false == started) + { + WriteLog("SELFTEST curvedswitch: not started, " + tool.status); + continue; + } + tool.mouse = glm::dvec3{toward.x, tool.point.y, toward.z}; + finish_switch_placement(); + WriteLog("SELFTEST curvedswitch " + (tool.armed >= 0 ? tool.templates[tool.armed].label : std::string{}) + ": " + tool.status); + } + else if (command == "conecheck") + { + std::string name; + words >> name; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || track->m_paths.size() < 2) + { + WriteLog("SELFTEST conecheck: no switch " + name); + continue; + } + geometry::bezier const main{track->m_paths[0]}; + geometry::bezier const other{track->m_paths[1]}; + std::string text; + for (int i = 1; i <= 8; ++i) + { + auto const t{i * 0.125}; + auto const point{other.point(t)}; + double best{std::numeric_limits::max()}, along{0.0}; + for (int k = 0; k <= 2000; ++k) + if (auto const distance{geometry::plan_distance(main.point(k / 2000.0), point)}; distance < best) + { + best = distance; + along = k / 2000.0; + } + auto const base{main.point(along)}; + auto const direction{glm::normalize(geometry::plan_of(main.first(along)))}; + auto const lateral{(point.x - base.x) * -direction.y + (point.z - base.z) * direction.x}; + auto const &path{track->m_paths[0]}; + auto const roll{glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * along)}; + auto const cone{base.y - lateral * std::tan(roll)}; + text += format(" [t %.3f off %.3f dy %.4f]", t, lateral, point.y - cone); + } + WriteLog("SELFTEST conecheck " + name + ":" + text); + } + else if (command == "frames") + { + std::string name; + double from{0.0}, to{0.0}, step{0.1}; + words >> name >> from >> to >> step; + auto *sweep{simulation::Sweeps.find(name)}; + if (sweep == nullptr) + continue; + auto const side{sweep->definition().lateral >= 0.0 ? 1 : 0}; + if (step <= 0.0) + continue; + for (int i = 0; from + i * step <= to + 1e-9; ++i) + { + auto const station{from + i * step}; + auto const at{sweep->frame_at(station)}; + WriteLog(format("SELFTEST frame %s %.2f: %.3f %.3f %.3f widening %.4f cant %.4f setback %.4f", name.c_str(), station, at.position.x, at.position.y, at.position.z, at.widening[side], at.cant[side], sweep->setback(at))); + } + } + else if (command == "switchdrive") + { + int index{-1}, side{0}; + words >> index >> side; + scan_switch_drives(); + m_switch.drive = index; + m_switch.drive_side = side; + WriteLog("SELFTEST switchdrive: " + (index >= 0 && index < static_cast(m_switch.drives.size()) ? m_switch.drives[index].name : std::string{"none"})); + } + else if (command == "drivetilt") + { + std::string name; + words >> name; + auto *model{simulation::Instances.find(name)}; + auto *track{simulation::Paths.find(name.substr(0, name.find('_')))}; + if (model == nullptr || track == nullptr) + { + WriteLog("SELFTEST drivetilt: no " + name); + continue; + } + auto const &path{track->m_paths[0]}; + auto const transform{glm::dmat3(model->rotation_scale())}; + auto const across{transform * glm::dvec3{1.0, 0.0, 0.0}}; + auto const forward{transform * glm::dvec3{0.0, 0.0, 1.0}}; + auto const direction{glm::normalize(glm::dvec2{forward.x, forward.z})}; + auto const lateral{across.x * -direction.y + across.z * direction.x}; + auto const cone{-lateral * std::tan(glm::radians(static_cast(path.rolls[0])))}; + auto const control{path.points[segment_data::point::control1]}; + auto const grade{control.y / std::hypot(control.x, control.z)}; + WriteLog(format("SELFTEST drivetilt %s: angles %.3f %.3f %.3f, +x rises %.4f (cone %.4f), +z rises %.4f (grade %.4f), origin %.3f %.3f %.3f, points %.3f", name.c_str(), model->Angles().x, model->Angles().y, model->Angles().z, across.y, cone, forward.y, grade, + model->location().x, model->location().y, model->location().z, path.points[segment_data::point::start].y)); + } + else if (command == "weather") + { + auto const &time{simulation::Time.data()}; + WriteLog(format("SELFTEST weather: %02d:%02d, fog end %.0f m, overcast %.2f", time.wHour, time.wMinute, Global.fFogEnd, Global.Overcast)); + } + else if (command == "fouling") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto &tool{m_fouling}; + tool.scope = std::stoi(values.count("scope") ? values["scope"] : "2"); + tool.height_mode = values["mode"] == "ground" ? 0 : 1; + tool.height = std::stod(values.count("height") ? values["height"] : "0"); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling: " + tool.status); + int shown{0}; + for (auto const &found : tool.found) + if (found.wrong && shown++ < 5) + WriteLog(format("SELFTEST fouling: %s %.2f m off, %+.3f m high", found.marker.file.c_str(), found.error, found.height_error)); + if (values["place"] == "1") + { + fouling_place(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after placing: " + tool.status); + } + if (values["fix"] == "1") + { + fouling_fix(); + tool.dirty = true; + fouling_update(); + WriteLog("SELFTEST fouling after the fix: " + tool.status); + } + } + else if (command == "hekto") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto *track{simulation::Paths.find(values["track"])}; + if (track == nullptr) + { + WriteLog("SELFTEST hekto: no such track"); + continue; + } + hekto_start(*track, track->m_paths.front().points[segment_data::point::start]); + auto &tool{m_hekto}; + tool.dirty = true; + hekto_update(); + if (values.count("km")) + { + tool.km = std::stod(values["km"]); + tool.growth = values["growth"] == "-1" ? -1 : 1; + } + tool.side = values["side"] == "alt" ? 0 : values["side"] == "left" ? -1 : 1; + tool.dirty = true; + hekto_update(); + auto const missing{std::count_if(tool.posts.begin(), tool.posts.end(), [](hekto_post const &Post) { return Post.standing < 0; })}; + WriteLog(format("SELFTEST hekto: km %.3f growth %d, %zu posts along %.0f m, %d to place, %zu standing. %s", tool.km, tool.growth, tool.posts.size(), tool.samples.empty() ? 0.0 : tool.samples.back().chainage, static_cast(missing), tool.found.size(), tool.status.c_str())); + for (std::size_t i = 0; i < tool.posts.size() && i < 4; ++i) + { + auto const &post{tool.posts[i]}; + double lateral; + glm::dvec3 const axis{editor_track::sampled_position(tool.samples, post.chainage)}; + auto const direction{editor_track::sampled_position(tool.samples, post.chainage + 1.0) - axis}; + glm::dvec2 const offset{post.position.x - axis.x, post.position.z - axis.z}; + lateral = glm::length(offset); + auto const right{(direction.x * offset.y - direction.z * offset.x) * tool.growth >= 0.0}; + WriteLog(format("SELFTEST hekto post %d.%d at %.1f m, %.2f m from the axis, %s of the growing kilometrage, at %.1f %.1f %.1f", post.hectometres / 10, post.hectometres % 10, post.chainage, lateral, right ? "right" : "left", post.position.x, post.position.y, + post.position.z)); + } + if (values["place"] == "1") + { + hekto_place(); + WriteLog("SELFTEST hekto: " + tool.status); + } + } + else if (command == "tracks") + { + int tab{0}; + words >> tab; + show_track_tab(track_tab::lineside); + m_sweep.expand = tab == 0; + m_hekto.expand = tab == 1; + m_fouling.expand = tab == 2; + m_parallel.expand = tab == 3; + m_vehicle.expand = tab == 4; + } + else if (command == "curve") + { + sweep_curve(); + for (auto const &piece : m_sweep.curve) + { + auto const &a{piece.points[segment_data::point::start]}; + auto const &b{piece.points[segment_data::point::end]}; + WriteLog(format("SELFTEST curve piece %.1f %.1f -> %.1f %.1f", a.x, a.z, b.x, b.z)); + } + if (false == m_sweep.outline.empty()) + WriteLog(format("SELFTEST curve outline %zu points, %.1f %.1f -> %.1f %.1f, %.1f m", m_sweep.outline.size(), m_sweep.outline.front().x, m_sweep.outline.front().z, m_sweep.outline.back().x, m_sweep.outline.back().z, m_sweep.curve_length)); + } + else if (command == "lay") + { + std::string name, side; + words >> name >> side; + auto *track{simulation::Paths.find(name)}; + if (track == nullptr || track->m_paths.empty()) + { + WriteLog("SELFTEST lay: no track " + name); + return; + } + auto const &path{track->m_paths.front()}; + auto const atend{side == "end"}; + editor_track::snap_target start; + start.track = track; + start.point = {0, atend ? editor_track::point_kind::end : editor_track::point_kind::start}; + start.position = path.points[atend ? segment_data::point::end : segment_data::point::start]; + auto const &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; + start.direction = glm::normalize(control != glm::dvec3{} ? control : path.points[atend ? segment_data::point::start : segment_data::point::end] - start.position); + m_lay.points = {start.position}; + m_lay.start = start; + for (glm::dvec3 point{0.0, start.position.y, 0.0}; words >> point.x >> point.z;) + m_lay.points.push_back(point); + lay_finish({}); + WriteLog("SELFTEST lay: " + m_lay.status); + } + else if (command == "analyse") + { + std::string name; + words >> name; + editor_track::curve curve; + if (auto *track{simulation::Paths.find(name)}; track != nullptr && editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + { + std::string arcs; + for (auto const &arc : curve.arcs) + arcs += format(" R%.0f/%.1fdeg/after %.1fm", arc.radius, glm::degrees(arc.turn), arc.transition); + WriteLog(format("SELFTEST analyse %s: R %.0f, compound %d, in %.1f m, out %.1f m, turn %.1f deg,", name.c_str(), curve.radius, curve.compound ? 1 : 0, curve.transition_in, curve.transition_out, glm::degrees(curve.turn)) + arcs); + } + else + WriteLog("SELFTEST analyse " + name + ": no curve"); + } + else if (command == "projtest") + { + screen_projection const projection; + for (std::size_t i = 0; i < m_sweep.outline.size() && i < 45; i += 4) + { + ImVec2 screen; + auto const shown{projection.project(m_sweep.outline[i], screen)}; + auto const clip{projection.clip(m_sweep.outline[i])}; + WriteLog(format("SELFTEST proj %zu: %.1f %.1f %.1f -> %d %.0f %.0f w %.2f", i, m_sweep.outline[i].x, m_sweep.outline[i].y, m_sweep.outline[i].z, shown ? 1 : 0, screen.x, screen.y, clip.w)); + } + } + else if (command == "spread") + { + std::string name; + words >> name >> m_spread.stop >> m_spread.reach; + if (auto *track{simulation::Paths.find(name)}) + { + std::string names; + for (auto const *other : track_spread_from(*track)) + names += ' ' + other->name(); + WriteLog("SELFTEST spread " + name + ":" + names + " | prev " + (track->trPrev ? track->trPrev->name() : std::string{"-"}) + " next " + (track->trNext ? track->trNext->name() : std::string{"-"})); + } + } + else if (command == "newscenery") + { + WriteLog(restart_for_new_scenery() ? "SELFTEST newscenery: started" : "SELFTEST newscenery: failed"); + return; + } + else if (command == "vehicles") + { + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if (vehicle == nullptr) + continue; + auto const head{vehicle->HeadPosition()}; + auto const front{vehicle->VectorFront()}; + auto const coupler = [&](int const End) { + auto const &coupler{vehicle->MoverParameters->Couplers[End]}; + return coupler.Connected != nullptr ? coupler.Connected->Name + "/" + std::to_string(coupler.CouplingFlag) : std::string{"-"}; + }; + WriteLog(format("SELFTEST vehicle %s head %.2f %.2f front %.2f %.2f dir %d coupler0 %s coupler1 %s", vehicle->name().c_str(), head.x, head.z, front.x, front.z, vehicle->iDirection, coupler(0).c_str(), coupler(1).c_str())); + } + } + else if (command == "screenshot") + { + Application.queue_screenshot(); + test.wait = 3; + return; + } + else if (command == "save") + { + save(); + } + else if (command == "turntable") + { + glm::dvec3 centre{0.0}; + double yaw{0.0}; + words >> centre.x >> centre.y >> centre.z >> yaw; + turntable_place(centre, yaw); + for (double angle; words >> angle;) + { + m_turntable.heading = m_turntable.table_yaw + angle; + m_turntable.exit_length = 10.0; + track_intent intent; + intent.what = track_intent::kind::turntable_exit; + turntable_press(intent); + } + WriteLog("SELFTEST turntable: " + (m_turntable.error.empty() ? m_turntable.status : m_turntable.error)); + } + else if (command == "selectmodel") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + TAnimModel *nearest{nullptr}; + auto best{50.0}; + for (auto *model : simulation::Instances.sequence()) + { + if (model == nullptr) + continue; + auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})}; + if (distance < best) + { + best = distance; + nearest = model; + } + } + m_node = nearest; + select_include(0); + ui()->set_node(m_node); + WriteLog("SELFTEST selectmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); + } + else if (command == "addmodel") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + TAnimModel *nearest{nullptr}; + auto best{50.0}; + for (auto *model : simulation::Instances.sequence()) + { + if (model == nullptr) + continue; + auto const distance{glm::length(glm::dvec2{model->location().x - point.x, model->location().z - point.z})}; + if (distance < best) + { + best = distance; + nearest = model; + } + } + select_node(nearest, true); + WriteLog("SELFTEST addmodel: " + (nearest != nullptr ? format("%s at %.2f %.2f %.2f", nearest->name().c_str(), nearest->location().x, nearest->location().y, nearest->location().z) : std::string{"none"})); + } + else if (command == "selection") + { + std::string text; + for (auto *node : selection_with_groups()) + text += format(" [%s %.2f %.2f %.2f group %d]", node->name().c_str(), node->location().x, node->location().y, node->location().z, static_cast(node->group())); + WriteLog("SELFTEST selection:" + text); + } + else if (command == "moveselection" && m_node != nullptr) + { + glm::dvec3 offset{0.0}; + words >> offset.x >> offset.y >> offset.z; + selection_snapshots(EditorSnapshot::Action::Move); + auto const before{m_node->location()}; + m_editor.translate(m_node, before + offset, true); + selection_follow(m_node->location() - before, glm::vec3(0.0f), m_node->location()); + } + else if (command == "turnselection" && m_node != nullptr) + { + float degrees{0.0f}; + words >> degrees; + selection_snapshots(EditorSnapshot::Action::Rotate); + glm::vec3 const turned{0.0f, degrees, 0.0f}; + m_editor.rotate(m_node, turned, 0.0f); + selection_follow(glm::dvec3(0.0), turned, m_node->location()); + } + else if (command == "copy") + copy_selection(); + else if (command == "paste") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.y >> point.z; + paste_clipboard(point); + } + else if (command == "group") + group_selection(); + else if (command == "ungroup") + ungroup_selection(); + else if (command == "drop") + drop_selection_to_ground(); + else if (command == "delete") + delete_selected(); + else if (command == "selectinclude") + { + glm::dvec3 point{0.0}; + words >> point.x >> point.z; + scene::instance_handle nearest{0}; + auto best{50.0}; + m_node = nullptr; + for (scene::instance_handle instance = 1; instance <= scene::Layers.instance_count(); ++instance) + { + if (false == scene::Layers.removable(instance)) + continue; + select_include(instance); + bend_source source; + if (false == bend_source_of_selection(source)) + continue; + auto const at{glm::dvec3(source.transform[3])}; + auto const distance{glm::length(glm::dvec2{at.x - point.x, at.z - point.z})}; + if (distance < best) + { + best = distance; + nearest = instance; + } + } + select_include(nearest); + WriteLog("SELFTEST selectinclude: " + (nearest != 0 ? *scene::Layers.instance(nearest).file : std::string{"none"})); + } + else if (command == "bend" || command == "bendset") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + auto const vector = [](std::string Text) { + std::replace(Text.begin(), Text.end(), ',', ' '); + std::istringstream numbers(Text); + glm::dvec3 result{0.0}; + numbers >> result.x >> result.y >> result.z; + return result; + }; + auto const index = [](std::string const &Text, std::vector const &Names) { + return static_cast(std::find(Names.begin(), Names.end(), Text) - Names.begin()); + }; + auto &tool{m_bend}; + auto const along{values.count("along") ? index(values["along"], {"start", "origin", "middle", "end", "point"}) : tool.along_anchor}; + auto const side{values.count("side") ? index(values["side"], {"origin", "near", "far", "middle", "point"}) : tool.side_anchor}; + auto const height{values.count("height") ? index(values["height"], {"origin", "bottom", "top", "edge", "point"}) : tool.height_anchor}; + auto const point{values.count("point") ? vector(values["point"]) : tool.point}; + if (command == "bend") + { + tool.edited = nullptr; + tool.along_anchor = along; + tool.side_anchor = side; + tool.height_anchor = height; + tool.point = point; + tool.axis = values.count("axis") ? index(values["axis"], {"auto", "z", "x"}) : 0; + tool.turned = values["turned"] == "1"; + bend_source source; + if (false == bend_source_of_selection(source)) + { + std::string reason; + auto const editable{m_node != nullptr && scene::Layers.editable(m_node, &reason)}; + WriteLog(format("SELFTEST bend: nothing to bend (node %p template %d editable %d %s, instance %d)", static_cast(m_node), m_node != nullptr && m_node->from_template() ? 1 : 0, editable ? 1 : 0, reason.c_str(), static_cast(m_instance))); + continue; + } + tool.tracks_for = nullptr; + bend_find_tracks(glm::dvec3(source.transform[3]), source.model != nullptr ? static_cast(source.model) : reinterpret_cast(static_cast(source.instance))); + tool.track = values.count("track") ? std::stoi(values["track"]) : 0; + if (values.count("pick")) + bend_pick(vector(values["pick"])); + if (tool.tracks.empty()) + { + WriteLog("SELFTEST bend: no track nearby"); + continue; + } + sweep_node::state state; + std::vector tracks; + bend_definition(source, *tool.tracks[tool.track].first, state, tracks); + WriteLog(format("SELFTEST bend: track %s, %zu paths, anchor before %.3f %.3f %.3f, lateral %.3f height %.3f from %.3f to %.3f, axis %s flip %d; %s", tool.tracks[tool.track].first->name().c_str(), tracks.size(), + tool.marker.x, tool.marker.y, tool.marker.z, state.lateral, state.height, state.from, state.to, state.along_x ? "x" : "z", state.flip ? 1 : 0, tool.status.c_str())); + if (values["go"] == "1") + { + bend_selection(); + WriteLog("SELFTEST bend: " + tool.status); + } + } + else if (auto *sweep{tool.edited}) + { + auto state{sweep->definition()}; + tool.along_anchor = along; + if (side != tool.side_anchor || height != tool.height_anchor || point != tool.point) + { + tool.side_anchor = side; + tool.height_anchor = height; + tool.point = point; + bend_reanchor(state, side, height, point); + } + if (values.count("lateral")) + state.lateral = std::stod(values["lateral"]); + if (values.count("up")) + state.height = std::stod(values["up"]); + if (values.count("widen")) + state.widen = values["widen"] == "1"; + if (values.count("pick")) + { + bend_pick(vector(values["pick"])); + state = sweep->definition(); + } + if (values.count("length")) + { + bend_stretch(*sweep, bend_fixed(*sweep, state.point), std::stod(values["length"]), state.point, state); + } + bend_edit(state); + WriteLog("SELFTEST bendset done"); + } + } + else if (command == "bendprobe") + { + auto *sweep{m_bend.edited}; + if (sweep == nullptr) + { + WriteLog("SELFTEST bendprobe: nothing bent"); + continue; + } + glm::dvec3 low, high, shift; + sweep->bounds(low, high, shift); + auto const &state{sweep->definition()}; + auto const marker{bend_marker(*sweep)}; + glm::dvec3 boxlow{std::numeric_limits::max()}, boxhigh{-std::numeric_limits::max()}; + std::size_t vertices{0}; + for (auto const &shape : sweep->create_shapes()) + for (auto const &vertex : shape.data().vertices) + { + boxlow = glm::min(boxlow, vertex.position); + boxhigh = glm::max(boxhigh, vertex.position); + ++vertices; + } + WriteLog(format("SELFTEST bendprobe %s: stations %.3f - %.3f, along the reference line %.3f m, model %.3f m, lateral %.3f height %.3f anchors %d/%d point %.3f %.3f %.3f, marker %.3f %.3f %.3f, box %.2f %.2f %.2f - %.2f %.2f %.2f, %zu vertices, removed %d", + sweep->name().c_str(), sweep->start(), sweep->end(), sweep->distance_at(sweep->end()) - sweep->distance_at(sweep->start()), high.x - low.x, state.lateral, state.height, state.side_anchor, state.height_anchor, state.point.x, + state.point.y, state.point.z, marker.x, marker.y, marker.z, boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z, vertices, sweep->m_editorremoved ? 1 : 0)); + } + else if (command == "modelbox") + { + auto *model{dynamic_cast(m_node)}; + bend_source source; + if (model == nullptr || false == bend_source_of_selection(source)) + { + WriteLog("SELFTEST modelbox: no model selected"); + continue; + } + scene::node_data data; + sweep_node probe{data}; + sweep_node::state state; + state.model = source.file; + state.skin = source.skin; + state.scale = source.scale; + probe.define(state); + glm::dvec3 boxlow{std::numeric_limits::max()}, boxhigh{-std::numeric_limits::max()}; + for (auto const &item : probe.items()) + { + auto const transform{source.transform * glm::scale(glm::dmat4(1.0), source.scale) * item.transform}; + std::vector pending{item.model->GetSMRoot()}; + while (false == pending.empty()) + { + auto *submodel{pending.back()}; + pending.pop_back(); + for (; submodel != nullptr; submodel = submodel->Next) + { + if (submodel->Child != nullptr) + pending.push_back(submodel->Child); + if (submodel->eType != GL_TRIANGLES || submodel->fSquareMinDist > 0.f || (submodel->m_geometry.handle.bank == 0 && submodel->m_geometry.handle.chunk == 0)) + continue; + glm::dmat4 local{1.0}; + for (auto *parent = submodel; parent != nullptr; parent = parent->Parent) + if ((parent->iFlags & 0xC000) != 0 && parent->GetMatrix() != nullptr) + local = glm::dmat4(glm::make_mat4(parent->GetMatrix()->readArray())) * local; + for (auto const &vertex : GfxRenderer->Vertices(submodel->m_geometry.handle)) + { + auto const world{glm::dvec3(transform * local * glm::dvec4(glm::dvec3(vertex.position), 1.0))}; + boxlow = glm::min(boxlow, world); + boxhigh = glm::max(boxhigh, world); + } + } + } + } + WriteLog(format("SELFTEST modelbox: %.2f %.2f %.2f - %.2f %.2f %.2f", boxlow.x, boxlow.y, boxlow.z, boxhigh.x, boxhigh.y, boxhigh.z)); + } + else if (command == "key") + { + std::string key, action, mods; + words >> key >> action >> mods; + if (key == "RMB") + m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT, action == "press" ? GLFW_PRESS : GLFW_RELEASE); + else + { + auto const code{key == "S" ? GLFW_KEY_S : key == "W" ? GLFW_KEY_W : key == "CTRL" ? GLFW_KEY_LEFT_CONTROL : 0}; + on_key(code, 0, action == "press" ? GLFW_PRESS : GLFW_RELEASE, mods == "ctrl" ? GLFW_MOD_CONTROL : 0); + } + WriteLog(format("SELFTEST key %s %s %s: camera %.2f %.2f %.2f, key S %d, right button %d", key.c_str(), action.c_str(), mods.c_str(), Camera.Pos.x, Camera.Pos.y, Camera.Pos.z, static_cast(input::keys[GLFW_KEY_S]), m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT))); + } + else if (command == "lineside") + { + std::string tab; + words >> tab; + show_track_tab(track_tab::lineside); + (tab == "vehicle" ? m_vehicle.expand : tab == "hekto" ? m_hekto.expand : tab == "fouling" ? m_fouling.expand : m_parallel.expand) = true; + } + else if (command == "event") + { + std::string name; + words >> name; + auto *event{simulation::Events.FindEvent(name)}; + if (event != nullptr) + simulation::Events.AddToQuery(event, nullptr); + WriteLog("SELFTEST event " + name + (event != nullptr ? " queued" : ": no such event")); + } + else if (command == "memcell") + { + std::string name; + words >> name; + auto *cell{simulation::Memory.find(name)}; + WriteLog("SELFTEST memcell " + name + (cell != nullptr ? format(": %s %.1f %.1f", cell->Text().c_str(), cell->Value1(), cell->Value2()) : std::string{": none"})); + } + else if (command == "bendpanel") + { + ui()->expand_bend(); + } + else if (command == "newscenerypopup") + { + m_newscenery_asked = true; + } + else if (command == "openscenerypopup") + { + m_openscenery_asked = true; + std::snprintf(m_openscenery_search, sizeof(m_openscenery_search), "%s", "zz_"); + } + else if (command == "openscenery") + { + std::string name; + words >> name; + WriteLog(restart_editor(name) ? "SELFTEST openscenery: started " + name : "SELFTEST openscenery: failed"); + } + else if (command == "gauge") + { + std::string name; + words >> name; + if (auto *track{simulation::Paths.find(name)}) + { + scan_gauge(*track); + WriteLog(format("SELFTEST gauge %s: %d hits", name.c_str(), static_cast(m_gauge.line.hits.size()))); + for (auto const &hit : m_gauge.line.hits) + WriteLog(format("SELFTEST gauge hit %s depth %.3f at %.2f %.2f %.2f", hit.model.c_str(), hit.depth, hit.point.x, hit.point.y, hit.point.z)); + } + else + WriteLog("SELFTEST gauge " + name + ": no such track"); + } + else if (command == "deletetrack") + { + delete_selected_track(); + WriteLog("SELFTEST deletetrack"); + } + else if (command == "ttincludes") + { + std::string name; + words >> name; + auto const *track{simulation::Paths.find(name)}; + WriteLog(format("SELFTEST ttincludes %s: track %s, includes %d", name.c_str(), track == nullptr ? "none" : track->m_editorremoved ? "removed" : "present", + track != nullptr ? static_cast(turntable_includes(*track).size()) : -1)); + } + else if (command == "undo") + { + undo_last(); + WriteLog("SELFTEST undo"); + } + else if (command == "redo") + { + redo_last(); + WriteLog("SELFTEST redo"); + } + else if (command == "checkspans") + { + WriteLog("SELFTEST spans: " + scene::Layers.check_sources()); + } + else if (command == "log") + { + std::string text; + std::getline(words, text); + WriteLog("SELFTEST" + text); + } + else if (command == "signal" || command == "signalmove" || command == "signaldelete" || command == "signalrename" || command == "signalprobe") + { + std::map values; + std::string pair; + while (words >> pair) + { + auto const equals{pair.find('=')}; + if (equals != std::string::npos) + values[pair.substr(0, equals)] = pair.substr(equals + 1); + } + show_track_tab(track_tab::signals); + signal_refresh(true); + auto &tool{m_signal}; + auto const standing_named = [&](std::string const &Name) -> signal_standing const * { + auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.name == Name; })}; + return found != tool.standing.end() ? &*found : nullptr; + }; + auto const spot_on = [&](signal_template const &Template, int const Facing, int const Side, signal_spot &Spot) { + auto *track{simulation::Paths.find(values["track"])}; + if (track == nullptr) + return false; + auto const path{values.count("path") ? std::stoi(values["path"]) : 0}; + auto const at{values.count("at") ? std::stod(values["at"]) : 0.5}; + geometry::bezier const curve{track->m_paths[std::clamp(path, 0, static_cast(track->m_paths.size()) - 1)]}; + return signal_spot_at(*track, path, curve.point(at), curve.point(at), Facing, Side, Template, Spot); + }; + if (command == "signal") + { + auto const *item{signal_template_of(values["file"])}; + if (item == nullptr) + { + WriteLog("SELFTEST signal: no signal template " + values["file"]); + continue; + } + for (int k = 0; k < static_cast(tool.templates.size()); ++k) + if (&tool.templates[k] == item) + { + static char const *const kinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"}; + for (int i = 0; i < 7; ++i) + if (item->kind == kinds[i]) + { + tool.kind = i; + tool.chosen[i] = k; + } + } + if (values.count("name")) + std::snprintf(tool.name, sizeof(tool.name), "%s", values["name"].c_str()); + if (values.count("plate")) + std::snprintf(tool.plate, sizeof(tool.plate), "%s", values["plate"].c_str()); + auto const side{values["side"] == "left" ? -1 : 1}; + auto const facing{values["facing"] == "along" ? 1 : values["facing"] == "against" ? -1 : 0}; + signal_spot spot; + if (false == spot_on(*signal_armed(), facing, side, spot)) + { + WriteLog("SELFTEST signal: no such track " + values["track"]); + continue; + } + auto const placed{signal_place(spot, *signal_armed())}; + WriteLog(format("SELFTEST signal %s: %s", placed ? "placed" : "failed", tool.status.c_str())); + } + else if (command == "signalmove") + { + auto const *standing{standing_named(values["name"])}; + auto const *item{standing != nullptr ? signal_template_of(standing->include.file) : nullptr}; + if (item == nullptr) + { + WriteLog("SELFTEST signalmove: no signal " + values["name"]); + continue; + } + auto const yaw{glm::radians(standing->include.yaw + 180.0)}; + glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)}; + signal_spot probe; + if (false == spot_on(*item, 1, 1, probe)) + { + WriteLog("SELFTEST signalmove: no such track " + values["track"]); + continue; + } + auto const facing{glm::dot(probe.travel, travel) >= 0.0 ? 1 : -1}; + auto const sideoftravel{glm::dot(geometry::plan_of(standing->include.location) - geometry::plan_of(standing->spot.axis), glm::dvec2{-travel.y, travel.x}) >= 0.0 ? 1 : -1}; + signal_select(standing->include.instance); + tool.moving = true; + tool.moved_valid = spot_on(*item, facing, sideoftravel * facing, tool.moved); + tool.pressed = {-1000.f, -1000.f}; + signal_release(); + WriteLog("SELFTEST signalmove: " + tool.status); + } + else if (command == "signaldelete") + { + auto const *standing{standing_named(values["name"])}; + auto const deleted{standing != nullptr && signal_delete(standing->include.instance)}; + WriteLog(std::string{"SELFTEST signaldelete "} + values["name"] + (deleted ? ": removed with its event" : ": not removed by the signal tool")); + } + else if (command == "signalrename") + { + auto const *standing{standing_named(values["name"])}; + if (standing == nullptr) + { + WriteLog("SELFTEST signalrename: no signal " + values["name"]); + continue; + } + select_include(standing->include.instance); + m_include.values[0] = values["to"]; + apply_include(); + m_include_gesture = false; + WriteLog("SELFTEST signalrename " + values["name"] + " -> " + values["to"]); + } + else + { + signal_refresh(true); + WriteLog(format("SELFTEST signalprobe: %zu signals", tool.standing.size())); + for (auto const &standing : tool.standing) + { + std::string where{"not read"}; + if (standing.track != nullptr) + { + auto const &curve{standing.track->m_paths.front()}; + geometry::bezier const bez{curve}; + auto best{std::numeric_limits::max()}; + double parameter{0.0}; + for (int k = 0; k <= 1024; ++k) + { + auto const d{geometry::plan_distance(bez.point(k / 1024.0), standing.include.location)}; + if (d < best) + { + best = d; + parameter = k / 1024.0; + } + } + auto const axis{bez.point(parameter)}; + auto tangent{bez.first(parameter)}; + glm::dvec2 travel{tangent.x, tangent.z}; + travel = glm::normalize(travel) * (standing.list == 1 ? -1.0 : 1.0); + glm::dvec2 const offset{standing.include.location.x - axis.x, standing.include.location.z - axis.z}; + auto const cross{travel.x * offset.y - travel.y * offset.x}; + auto heading{glm::degrees(std::atan2(travel.x, travel.y))}; + auto turn{std::fmod(standing.include.yaw - heading + 720.0, 360.0)}; + auto const &list{editor_track::events(*standing.track, standing.list)}; + auto const bound{std::any_of(list.begin(), list.end(), [&](auto const &Event) { return Event.second != nullptr && ToLower(Event.first) == ToLower(standing.read); })}; + where = std::string{bound ? "bound, " : "unbound, "} + format("%s of %s, %.3f m from the axis, %s of the trains, %.3f m over the axis, model turned %.1f deg from the travel", editor_track::event_keyword(standing.list), standing.track->name().c_str(), glm::length(offset), cross > 0.0 ? "right" : "left", + standing.include.location.y - axis.y, turn); + } + WriteLog(format("SELFTEST signalprobe %s %s at %.3f %.3f %.3f yaw %.2f read %s: %s", standing.name.c_str(), standing.include.file.c_str(), standing.include.location.x, standing.include.location.y, standing.include.location.z, standing.include.yaw, + standing.read.empty() ? "-" : standing.read.c_str(), where.c_str())); + } + } + } + else if (command == "orthophoto") + { + auto config{m_orthophoto.settings()}; + int fit{1}, inscene{0}; + words >> config.north >> config.east >> config.radius >> fit >> inscene; + config.drape = fit != 0; + config.in_scene = inscene != 0; + m_orthophoto.settings(config); + m_orthophoto.enabled(true); + WriteLog(format("SELFTEST orthophoto at %.0f %.0f, radius %d, %s%s", config.north, config.east, config.radius, config.drape ? "fitted" : "flat", config.in_scene ? ", in the scene" : "")); + } + else if (command == "terrainmode") + { + m_terrain_open = true; + WriteLog("SELFTEST terrainmode"); + } + else if (command == "terrainsculpt" || command == "terrainpaint" || command == "terrainlevel") + { + double x{0.0}, z{0.0}, radius{10.0}, value{1.0}; + int mode{0}; + words >> x >> z >> radius >> value >> mode; + auto const changed{command == "terrainsculpt" ? m_streamer.sculpt(x, z, radius, value) + : command == "terrainlevel" ? m_streamer.level(x, z, radius, value, 1.0, mode) + : m_streamer.paint(x, z, radius, 1.0, static_cast(value))}; + forget_ground(); + WriteLog(format("SELFTEST %s %.1f %.1f: %s", command.c_str(), x, z, changed ? "changed" : "nothing changed")); + } + else if (command == "terrainmodifier") + { + heightmap::modifier modifier; + heightmap::corridor_point a, b; + double formation{0.0}, halfwidth{3.5}; + words >> modifier.name >> a.position.x >> a.position.y >> b.position.x >> b.position.y >> formation >> halfwidth; + a.formation = b.formation = formation; + a.half_width = b.half_width = halfwidth; + modifier.points = {a, b}; + m_streamer.modifier(std::move(modifier)); + forget_ground(); + WriteLog(format("SELFTEST terrainmodifier: %zu modifiers", m_streamer.modifiers().size())); + } + else if (command == "terrainfill") + { + int layer{0}; + words >> layer >> m_paint_feather >> m_paint_rotation; + m_terrain_layer = layer; + m_paint_outline.clear(); + double x, z; + while (words >> x >> z) + m_paint_outline.emplace_back(x, 0.0, z); + fill_terrain_area(); + std::string palette; + for (auto const &entry : m_streamer.manifest().layers) + palette += format(" [%s %.1f m %.0f deg]", entry.material.c_str(), entry.size, entry.rotation); + WriteLog("SELFTEST terrainfill: " + m_terrain_status + ", palette" + palette); + } + else if (command == "terrainturn") + { + int layer{0}; + float degrees{0.f}; + words >> layer >> degrees; + m_streamer.layer_rotation(static_cast(layer), degrees); + WriteLog(format("SELFTEST terrainturn %d: %.1f deg", layer, m_streamer.manifest().layers.at(layer).rotation)); + } + else if (command == "terraintouchup") + { + int state{1}; + words >> state; + m_streamer.touch_up(state != 0); + WriteLog(format("SELFTEST terraintouchup %d", state)); + } + else if (command == "terrainrestore") + { + double x{0.0}, z{0.0}, radius{10.0}; + words >> x >> z >> radius; + auto const changed{m_streamer.restore_stitching(x, z, radius, 1.0)}; + forget_ground(); + WriteLog(format("SELFTEST terrainrestore %.1f %.1f: %s, touch-ups left %zu", x, z, changed ? "changed" : "nothing changed", m_streamer.touched_up())); + } + else if (command == "terrainbake") + { + std::string name; + words >> name; + auto const baked{m_streamer.bake_modifier(name)}; + forget_ground(); + WriteLog(format("SELFTEST terrainbake %s: %s, %zu modifiers left", name.c_str(), baked ? "baked" : "not found", m_streamer.modifiers().size())); + } + else if (command == "terrainselect") + { + int cx{0}, cz{0}; + std::string add; + words >> cx >> cz >> add; + if (add != "add") + m_terrain_selection.clear(); + if (m_streamer.exists({cx, cz})) + m_terrain_selection.insert({cx, cz}); + WriteLog(format("SELFTEST terrainselect: %zu selected", m_terrain_selection.size())); + } + else if (command == "terrainresolution") + { + float spacing{1.f}; + int paint{1}; + words >> spacing >> paint; + m_terrain_convert_spacing = heightmap::valid_spacing(spacing); + m_terrain_convert_paint = paint != 0; + convert_selected_chunks(); + WriteLog("SELFTEST terrainresolution: " + m_terrain_status); + } + else if (command == "terrainwater" || command == "terrainwaterdefault") + { + if (command == "terrainwaterdefault") + { + std::string material; + words >> material; + m_streamer.water_material(material); + } + else + { + std::size_t index{0}; + std::string material; + float size{0.f}; + words >> index >> material >> size; + auto water{m_streamer.water()}; + if (index < water.size()) + { + water[index].material = material == "-" ? std::string{} : material; + water[index].size = size; + m_streamer.water(std::move(water)); + } + } + std::string list; + for (auto const &body : m_streamer.water()) + list += format(" [%s: %s, %.1f m]", body.name.c_str(), body.material.empty() ? "default" : body.material.c_str(), body.size); + WriteLog("SELFTEST " + command + ": default " + m_streamer.manifest().water_material + list); + } + else if (command == "terrainadd") + { + int count{1}; + float spacing{m_terrain_spacing}; + words >> count >> spacing; + m_terrain_spacing = heightmap::valid_spacing(spacing); + add_chunks_around(std::clamp(count, 1, 8)); + WriteLog("SELFTEST terrainadd: " + m_terrain_status); + } + else if (command == "terrainprobe") + { + double x{0.0}, z{0.0}, height{0.0}; + words >> x >> z; + auto const *chunk{m_streamer.terrain_at(x, z)}; + std::string materials; + if (chunk != nullptr) + for (std::size_t slot = 0; slot < std::max(1, chunk->data().layer_count()); ++slot) + { + auto const layer{chunk->data().layers[slot]}; + auto const &palette{m_streamer.manifest().layers}; + materials += (layer < palette.size() ? palette[layer].material : std::string{"?"}) + " "; + } + auto const found{m_streamer.height_at(x, z, height)}; + std::string grid; + if (chunk != nullptr) + { + auto const &data{chunk->data()}; + double base{0.0}, adjust{0.0}; + if (false == data.base.empty()) + base = data.height_in(data.base, x, z); + if (false == data.adjust.empty()) + adjust = data.height_in(data.adjust, x, z); + grid = format(", spacing %.1f, paint %d, base %s %.3f, touch-up %.3f", data.spacing(), data.paint, data.base.empty() ? "none" : "", base, adjust); + if (data.paint > 0) + { + // weights at the paint sample nearest the point + auto const corner{data.corner()}; + auto const ix{static_cast(std::lround((x - corner.x) / heightmap::chunk_size * data.paint))}; + auto const iz{static_cast(std::lround((z - corner.y) / heightmap::chunk_size * data.paint))}; + grid += ", weights"; + for (std::size_t slot = 0; slot < data.layer_count(); ++slot) + grid += format(" %d", data.weight(slot, std::clamp(ix, 0, data.paint), std::clamp(iz, 0, data.paint))); + } + } + WriteLog(format("SELFTEST terrainprobe %.1f %.1f: %s %.3f, %zu chunks, materials %s%s", x, z, found ? "height" : "no terrain", height, m_streamer.resident(), materials.c_str(), grid.c_str())); + } + else if (command == "terrainconvert") + { + load_ground_files(); + for (auto const &source : m_ground_conversion.files) + { + glm::dvec3 low{1e30}, high{-1e30}; + for (auto index = source.first; index < source.first + source.count; ++index) + for (auto const &vertex : m_ground_conversion.triangles[index]) + { + low = glm::min(low, vertex); + high = glm::max(high, vertex); + } + WriteLog("SELFTEST terrain file " + source.file + (source.text ? " (text): " : " (binary): ") + source.message + + format(", from %.1f %.1f %.1f to %.1f %.1f %.1f", low.x, low.y, low.z, high.x, high.y, high.z)); + } + m_ground_conversion.status = convert_ground(); + WriteLog("SELFTEST terrainconvert: " + m_ground_conversion.status); + } + else if (command == "quit") + { + if (false == test.quitting) + { + WriteLog("SELFTEST done"); + test.quitting = true; + test.wait = 30; + --test.next; + return; + } + Application.queue_quit(true); + return; + } + } +} diff --git a/application/editormodesignal.cpp b/application/editormodesignal.cpp new file mode 100644 index 00000000..53840611 --- /dev/null +++ b/application/editormodesignal.cpp @@ -0,0 +1,1018 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" +#include "editor/editorSettings.hpp" + +#include "rendering/renderer.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_of; + +double const kRailTop{0.18}; +double const kSignalRange{1500.0}; +double const kSignalHead{4.0}; +float const kSignalPick{12.0f}; +float const kDragStart{6.0f}; +double const kDragTravel{1.0}; +double const kRefreshPeriod{2.0}; + +char const *const kKinds[] = {"main", "block", "warning", "shunt", "stop", "substitute", "repeater"}; +char const *const kKindLabels[] = {"Semaphore", "Automatic block", "Warning signal", "Shunting signal", "Stop signal", "Substitute signal", "Repeater"}; +int const kKindCount{7}; + +int kind_index(std::string const &Kind) +{ + for (int i = 0; i < kKindCount; ++i) + if (Kind == kKinds[i]) + return i; + return -1; +} + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +double nearest_on(TTrack const &Track, int const Path, glm::dvec3 const &Point) +{ + bezier const curve{Track.m_paths[Path]}; + auto parameter{0.0}; + auto best{std::numeric_limits::max()}; + for (int k = 0; k <= 256; ++k) + { + auto const distance{geometry::plan_distance(curve.point(k / 256.0), Point)}; + if (distance < best) + { + best = distance; + parameter = k / 256.0; + } + } + return parameter; +} + +glm::dvec2 forward_at(TTrack const &Track, int const Path, double const Parameter) +{ + auto const tangent{bezier{Track.m_paths[Path]}.first(Parameter)}; + glm::dvec2 const planar{tangent.x, tangent.z}; + return glm::length(planar) > 1e-9 ? glm::normalize(planar) : glm::dvec2{0.0, 1.0}; +} + +glm::dvec2 right_of(glm::dvec2 const &Direction) +{ + return {-Direction.y, Direction.x}; +} + +double heading_of(glm::dvec2 const &Direction) +{ + auto const degrees{glm::degrees(std::atan2(Direction.x, Direction.y))}; + return degrees < 0.0 ? degrees + 360.0 : degrees; +} + +int lean_wanted(int const Side) +{ + return Side > 0 ? 1 : 2; +} + +int lean_of(std::string const &Lean) +{ + return Lean == "left" ? 1 : Lean == "right" ? 2 : 0; +} + +std::size_t edit_distance(std::string const &A, std::string const &B) +{ + std::vector row(B.size() + 1); + for (std::size_t j = 0; j <= B.size(); ++j) + row[j] = j; + for (std::size_t i = 1; i <= A.size(); ++i) + { + auto diagonal{row[0]}; + row[0] = i; + for (std::size_t j = 1; j <= B.size(); ++j) + { + auto const above{row[j]}; + row[j] = std::min({row[j] + 1, row[j - 1] + 1, diagonal + (A[i - 1] == B[j - 1] ? 0 : 1)}); + diagonal = above; + } + } + return row[B.size()]; +} + +ImU32 lamp_colour(char const Lamp) +{ + switch (Lamp) + { + case 'z': return IM_COL32(40, 210, 80, 255); + case 'c': return IM_COL32(230, 40, 40, 255); + case 'p': return IM_COL32(255, 160, 30, 255); + case 'b': return IM_COL32(235, 235, 235, 255); + case 'n': return IM_COL32(60, 110, 255, 255); + default: return IM_COL32(70, 70, 70, 255); + } +} + +std::string group_of(std::string const &Mount, std::string const &Lamps) +{ + return Mount + "|" + Lamps; +} + +std::vector const &plate_textures() +{ + static std::vector plates; + static bool scanned{false}; + if (scanned) + return plates; + scanned = true; + std::error_code error; + for (std::filesystem::directory_iterator file{"textures/tabl", error}, end; file != end; file.increment(error)) + { + auto const stem{lower(file->path().stem().string())}; + if (std::find(plates.begin(), plates.end(), stem) == plates.end()) + plates.push_back(stem); + } + std::sort(plates.begin(), plates.end()); + return plates; +} + +std::string plate_for(std::string const &Plate, std::string const &Name) +{ + if (false == Plate.empty()) + return Plate; + auto const &plates{plate_textures()}; + auto const name{lower(Name)}; + return std::binary_search(plates.begin(), plates.end(), name) ? name : std::string{"0_nothing"}; +} + +void draw_signal_icon(ImDrawList *Draw, ImVec2 const Origin, ImVec2 const Size, std::string const &Mount, std::string const &Lamps, ImU32 const Frame) +{ + auto const lamps{std::max(1, static_cast(Lamps.size()))}; + auto const centre{Origin.x + Size.x * 0.5f}; + auto const bottom{Origin.y + Size.y - 4.0f}; + auto const lamp{std::min(7.0f, (Size.y - 26.0f) / (lamps * 2.2f))}; + auto const headwidth{lamp * 2.0f + 6.0f}; + auto const headheight{lamps * lamp * 2.2f + 4.0f}; + ImU32 const pole{IM_COL32(150, 150, 150, 255)}; + float headtop; + if (Mount == "dwarf") + { + headtop = bottom - headheight; + Draw->AddRectFilled(ImVec2(centre - headwidth * 0.7f, bottom - 3.0f), ImVec2(centre + headwidth * 0.7f, bottom), pole); + } + else if (Mount == "gantry") + { + headtop = Origin.y + 10.0f; + Draw->AddLine(ImVec2(Origin.x + 4.0f, Origin.y + 6.0f), ImVec2(Origin.x + Size.x - 4.0f, Origin.y + 6.0f), pole, 3.0f); + Draw->AddLine(ImVec2(centre, Origin.y + 6.0f), ImVec2(centre, headtop), pole, 2.0f); + } + else + { + headtop = Origin.y + 6.0f; + Draw->AddLine(ImVec2(centre, headtop + headheight), ImVec2(centre, bottom), pole, 3.0f); + Draw->AddLine(ImVec2(centre - 6.0f, bottom), ImVec2(centre + 6.0f, bottom), pole, 2.0f); + } + if (Lamps.empty()) + { + Draw->AddLine(ImVec2(centre, headtop), ImVec2(centre, headtop + headheight), pole, 3.0f); + Draw->AddLine(ImVec2(centre, headtop + 6.0f), ImVec2(centre + 14.0f, headtop - 2.0f), IM_COL32(230, 60, 60, 255), 4.0f); + return; + } + Draw->AddRectFilled(ImVec2(centre - headwidth * 0.5f, headtop), ImVec2(centre + headwidth * 0.5f, headtop + headheight), IM_COL32(25, 25, 25, 255), 3.0f); + Draw->AddRect(ImVec2(centre - headwidth * 0.5f, headtop), ImVec2(centre + headwidth * 0.5f, headtop + headheight), Frame, 3.0f); + for (int i = 0; i < static_cast(Lamps.size()); ++i) + Draw->AddCircleFilled(ImVec2(centre, headtop + 2.0f + lamp * 1.1f + i * lamp * 2.2f), lamp, lamp_colour(Lamps[i])); +} + +} // namespace + +void editor_mode::signal_scan() +{ + auto &tool{m_signal}; + if (false == EditorIncludes.scanned()) + EditorIncludes.scan(); + if (tool.revision == EditorIncludes.revision()) + return; + tool.revision = EditorIncludes.revision(); + std::string chosen[kKindCount]; + for (int i = 0; i < kKindCount; ++i) + if (tool.chosen[i] >= 0 && tool.chosen[i] < static_cast(tool.templates.size())) + chosen[i] = tool.templates[tool.chosen[i]].file; + tool.templates.clear(); + for (auto const &entry : EditorIncludes.entries()) + { + if (entry.category != "signal" || false == entry.complete) + continue; + include_info info; + std::string error; + if (false == editor_includes::load(entry.file, info, error) || kind_index(info.signal_kind) < 0) + continue; + signal_template item; + item.file = entry.file; + item.name = info.name.empty() ? entry.file : info.name; + item.description = info.description; + item.kind = info.signal_kind; + item.mount = info.signal_mount.empty() ? std::string{"mast"} : info.signal_mount; + item.lamps = info.signal_lamps; + item.lean = info.signal_lean; + item.read = info.signal_read; + for (auto const ¶meter : info.parameters) + { + item.parameters = std::max(item.parameters, parameter.id); + if (parameter.id == 6 && parameter.role == "texture") + item.plate = true; + if (parameter.id == 7) + item.linked = true; + } + tool.templates.push_back(std::move(item)); + } + std::sort(tool.templates.begin(), tool.templates.end(), [](signal_template const &A, signal_template const &B) { + return std::make_tuple(kind_index(A.kind), A.lamps.empty(), A.mount != "mast", A.lamps.size(), A.lamps, A.name.size(), A.name) < std::make_tuple(kind_index(B.kind), B.lamps.empty(), B.mount != "mast", B.lamps.size(), B.lamps, B.name.size(), B.name); + }); + for (int i = 0; i < kKindCount; ++i) + { + tool.chosen[i] = -1; + std::unordered_map sizes; + for (auto const &item : tool.templates) + if (item.kind == kKinds[i]) + ++sizes[group_of(item.mount, item.lamps)]; + auto best{-1}; + for (int k = 0; k < static_cast(tool.templates.size()); ++k) + { + auto const &item{tool.templates[k]}; + if (item.kind != kKinds[i]) + continue; + if (item.file == chosen[i]) + { + best = k; + break; + } + if (best < 0 || sizes[group_of(item.mount, item.lamps)] > sizes[group_of(tool.templates[best].mount, tool.templates[best].lamps)]) + best = k; + } + tool.chosen[i] = best; + } +} + +editor_mode::signal_template const *editor_mode::signal_armed() const +{ + auto const &tool{m_signal}; + if (tool.kind < 0 || tool.kind >= kKindCount) + return nullptr; + auto const index{tool.chosen[tool.kind]}; + return index >= 0 && index < static_cast(tool.templates.size()) ? &tool.templates[index] : nullptr; +} + +editor_mode::signal_template const *editor_mode::signal_template_of(std::string const &File) const +{ + auto const file{lower(File)}; + for (auto const &item : m_signal.templates) + if (item.file == file) + return &item; + return nullptr; +} + +bool editor_mode::signal_spot_at(TTrack &Track, int const Path, glm::dvec3 const &Point, glm::dvec3 const &Cursor, int const Facing, int const Side, signal_template const &Template, signal_spot &Spot) const +{ + if (Track.m_paths.empty() || Track.m_editorremoved) + return false; + auto const path{std::clamp(Path, 0, static_cast(Track.m_paths.size()) - 1)}; + auto const parameter{nearest_on(Track, path, Point)}; + auto const axis{bezier{Track.m_paths[path]}.point(parameter)}; + auto const forward{forward_at(Track, path, parameter)}; + auto const right{right_of(forward)}; + auto side{Side}; + if (side == 0) + side = glm::dot(plan_of(Cursor) - plan_of(axis), right) >= 0.0 ? 1 : -1; + auto facing{Facing}; + if (facing == 0) + facing = side; + Spot.track = &Track; + Spot.path = path; + Spot.axis = axis; + Spot.travel = forward * static_cast(facing); + Spot.side = side; + Spot.list = facing > 0 ? 2 : 1; + Spot.offset = Template.mount == "gantry" ? 0.0 : EditorSettings.signal_offset(); + auto const across{right * static_cast(side) * Spot.offset}; + Spot.position = {axis.x + across.x, axis.y + kRailTop + (Template.mount == "gantry" ? static_cast(m_signal.height) : 0.0), axis.z + across.y}; + auto const yaw{heading_of(Spot.travel) + 180.0}; + Spot.yaw = yaw >= 360.0 ? yaw - 360.0 : yaw; + return true; +} + +std::string editor_mode::signal_intent_label(signal_template const &Template, signal_spot const &Spot) const +{ + auto const name{m_signal.name[0] != '\0' ? std::string{m_signal.name} : std::string{"?"}}; + auto const right{glm::dot(right_of(Spot.travel), plan_of(Spot.position) - plan_of(Spot.axis)) >= 0.0}; + auto const where{right ? STR(" on the right of the trains") : STR(" on the left of the trains")}; + auto variant{signal_variant(Template, Spot)}; + variant = variant.substr(0, variant.rfind('.')); + if (Template.read.empty()) + return format(STR_C("Click: %s %s (%s)%s, the train doesn't read it"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag"); + auto const event{editor_includes::substitute(Template.read, {name})}; + auto const path{Spot.track->name().empty() ? std::string{"(noname)"} : Spot.track->name()}; + return format(STR_C("Click: %s %s (%s)%s, read by %s %s of %s"), STR(kKindLabels[kind_index(Template.kind)]).c_str(), name.c_str(), variant.c_str(), where.c_str(), editor_track::event_keyword(Spot.list), event.c_str(), path.c_str()) + "\n" + STR("Drag along: the trains it faces run the way of the drag"); +} + +void editor_mode::signal_refresh(bool const Force) +{ + auto &tool{m_signal}; + signal_scan(); + auto const now{ImGui::GetTime()}; + if (false == Force && tool.standing_history == m_history.size() && tool.standing_redo == g_redo.size() && now - tool.standing_time < kRefreshPeriod) + return; + tool.standing_history = m_history.size(); + tool.standing_redo = g_redo.size(); + tool.standing_time = now; + tool.standing.clear(); + if (scene::Layers.empty()) + return; + auto const found{standing_templates([this](std::string const &File) { return signal_template_of(File) != nullptr; })}; + std::unordered_map> readers; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (int list = 0; list < 6; ++list) + for (auto const &event : editor_track::events(*track, list)) + if (false == event.first.empty()) + readers.emplace(lower(event.first), std::make_pair(track, list)); + } + for (auto const &include : found) + { + auto const *item{signal_template_of(include.file)}; + signal_standing standing; + standing.include = include; + standing.kind = kind_index(item->kind); + standing.name = include.values.empty() ? std::string{} : include.values.front(); + if (false == item->read.empty()) + { + standing.read = editor_includes::substitute(item->read, include.values); + if (auto const reader{readers.find(lower(standing.read))}; reader != readers.end()) + { + standing.track = reader->second.first; + standing.list = reader->second.second; + } + } + tool.standing.push_back(std::move(standing)); + } + if (tool.selected != 0 && std::none_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })) + tool.selected = 0; +} + +int editor_mode::signal_hit() const +{ + screen_projection const projection; + auto const &mouse{ImGui::GetIO().MousePos}; + auto best{kSignalPick * kSignalPick}; + int result{-1}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (int i = 0; i < static_cast(m_signal.standing.size()); ++i) + { + auto const &location{m_signal.standing[i].include.location}; + if (glm::distance(location, camera) > kSignalRange) + continue; + ImVec2 foot, head; + if (false == projection.project(location, foot) || false == projection.project(location + glm::dvec3{0.0, kSignalHead, 0.0}, head)) + continue; + ImVec2 const span{head.x - foot.x, head.y - foot.y}; + auto const length2{span.x * span.x + span.y * span.y}; + auto const t{length2 > 1e-6f ? std::clamp(((mouse.x - foot.x) * span.x + (mouse.y - foot.y) * span.y) / length2, 0.0f, 1.0f) : 0.0f}; + auto const dx{foot.x + span.x * t - mouse.x}; + auto const dy{foot.y + span.y * t - mouse.y}; + auto const distance2{dx * dx + dy * dy}; + if (distance2 < best) + { + best = distance2; + result = i; + } + } + return result; +} + +void editor_mode::signal_press(track_intent const &Intent) +{ + auto &tool{m_signal}; + auto const &mouse{ImGui::GetIO().MousePos}; + tool.pressed = {mouse.x, mouse.y}; + tool.facing = 0; + tool.status.clear(); + if (Intent.what == track_intent::kind::signal_move) + { + if (Intent.index < 0 || Intent.index >= static_cast(tool.standing.size())) + return; + signal_select(tool.standing[Intent.index].include.instance); + tool.moving = true; + tool.moved_valid = false; + return; + } + auto const *armed{signal_armed()}; + if (armed == nullptr || Intent.track == nullptr) + return; + if (false == signal_spot_at(*Intent.track, Intent.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, tool.press)) + return; + tool.placing = true; +} + +void editor_mode::signal_move_update() +{ + auto &tool{m_signal}; + auto const &mouse{ImGui::GetIO().MousePos}; + auto const dragged{glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) > kDragStart}; + if (tool.placing) + { + auto const *armed{signal_armed()}; + if (armed == nullptr || tool.press.track == nullptr || tool.press.track->m_editorremoved) + { + tool.placing = false; + return; + } + if (false == dragged) + return; + auto const cursor{cursor_level(tool.press.axis.y)}; + auto const forward{tool.press.travel * static_cast(tool.press.list == 2 ? 1 : -1)}; + auto const along{glm::dot(plan_of(cursor) - plan_of(tool.press.axis), forward)}; + if (std::abs(along) < kDragTravel) + return; + tool.facing = along > 0.0 ? 1 : -1; + signal_spot spot; + if (signal_spot_at(*tool.press.track, tool.press.path, tool.press.axis, cursor, tool.facing, tool.press.side, *armed, spot)) + tool.press = spot; + return; + } + if (false == tool.moving) + return; + if (false == dragged) + return; + auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; + if (found == tool.standing.end() || m_hover.track == nullptr) + { + tool.moved_valid = false; + return; + } + auto const *item{signal_template_of(found->include.file)}; + if (item == nullptr) + return; + auto const yaw{glm::radians(found->include.yaw + 180.0)}; + glm::dvec2 const travel{std::sin(yaw), std::cos(yaw)}; + auto const parameter{nearest_on(*m_hover.track, std::clamp(m_hover.path, 0, static_cast(m_hover.track->m_paths.size()) - 1), m_hover.point)}; + auto const forward{forward_at(*m_hover.track, std::clamp(m_hover.path, 0, static_cast(m_hover.track->m_paths.size()) - 1), parameter)}; + auto const facing{glm::dot(forward, travel) >= 0.0 ? 1 : -1}; + auto const sideoftravel{glm::dot(plan_of(found->include.location) - plan_of(m_hover.point), right_of(travel)) >= 0.0 ? 1 : -1}; + tool.moved_valid = signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, m_hover.point, facing, sideoftravel * facing, *item, tool.moved); +} + +void editor_mode::signal_release() +{ + auto &tool{m_signal}; + if (tool.placing) + { + tool.placing = false; + if (auto const *armed{signal_armed()}; armed != nullptr && tool.press.track != nullptr && false == tool.press.track->m_editorremoved) + signal_place(tool.press, *armed); + return; + } + if (false == tool.moving) + return; + tool.moving = false; + auto const &mouse{ImGui::GetIO().MousePos}; + if (false == tool.moved_valid || glm::length(glm::vec2{mouse.x, mouse.y} - tool.pressed) <= kDragStart) + return; + tool.moved_valid = false; + auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; + if (found == tool.standing.end() || false == scene::Layers.tracked(found->include.instance) || false == scene::Layers.removable(found->include.instance)) + return; + auto const &spot{tool.moved}; + auto const &included{scene::Layers.instance(found->include.instance)}; + auto values{found->include.values}; + auto const local{spot.position - included.context.offset}; + values[1] = editor_includes::number(local.x); + values[2] = editor_includes::number(local.y); + values[3] = editor_includes::number(local.z); + values[4] = editor_includes::number(spot.yaw); + auto before{scene::Layers.directive(found->include.instance)}; + std::vector> states; + if (false == found->read.empty() && spot.track->Dynamics.empty()) + { + signal_detach_events(found->read, states); + if (std::none_of(states.begin(), states.end(), [&](auto const &State) { return State.first == spot.track; })) + states.emplace_back(spot.track, editor_track::capture(*spot.track)); + editor_track::events(*spot.track, spot.list).emplace_back(lower(found->read), nullptr); + std::string missing; + editor_track::bind_events(*spot.track, missing); + spot.track->mark_dirty(); + } + set_include_directive(found->include.instance, editor_includes::compose_directive(*included.file, values)); + if (states.empty()) + { + EditorSnapshot snap; + snap.directives.emplace_back(found->include.instance, std::move(before)); + snap.node_name = found->include.file; + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + } + else + { + push_track_snapshot(std::move(states)); + m_history.back().directives.emplace_back(found->include.instance, std::move(before)); + } + tool.status = found->read.empty() ? format(STR_C("Signal %s moved"), found->name.c_str()) : format(STR_C("Signal %s moved, read by %s %s of %s"), found->name.c_str(), editor_track::event_keyword(spot.list), found->read.c_str(), spot.track->name().empty() ? "(noname)" : spot.track->name().c_str()); + signal_refresh(true); + signal_select(found->include.instance); +} + +std::string editor_mode::signal_variant(signal_template const &Template, signal_spot const &Spot) const +{ + auto file{Template.file}; + auto const wanted{lean_wanted(glm::dot(right_of(Spot.travel), plan_of(Spot.position) - plan_of(Spot.axis)) >= 0.0 ? 1 : -1)}; + if (lean_of(Template.lean) == 0 || lean_of(Template.lean) == wanted) + return file; + auto const group{group_of(Template.mount, Template.lamps)}; + auto best{std::numeric_limits::max()}; + for (auto const &other : m_signal.templates) + { + if (other.kind != Template.kind || group_of(other.mount, other.lamps) != group || lean_of(other.lean) != wanted) + continue; + auto const distance{edit_distance(other.name, Template.name)}; + if (distance < best) + { + best = distance; + file = other.file; + } + } + return file; +} + +std::string editor_mode::signal_next_name(std::string const &Name) const +{ + if (Name.empty()) + return Name; + auto digits{Name.size()}; + while (digits > 0 && std::isdigit(static_cast(Name[digits - 1]))) + --digits; + if (digits < Name.size() && Name.size() - digits < 9) + return Name.substr(0, digits) + std::to_string(std::stoi(Name.substr(digits)) + 1); + auto const last{static_cast(Name.back())}; + if (std::isalpha(last) && last != 'z' && last != 'Z') + return Name.substr(0, Name.size() - 1) + static_cast(last + 1); + return Name + "2"; +} + +bool editor_mode::signal_place(signal_spot const &Spot, signal_template const &Template) +{ + auto &tool{m_signal}; + std::string name{tool.name}; + if (name.empty() || name.find_first_of(" \t\";") != std::string::npos) + { + tool.status = STR_C("Give the signal a name without spaces first"); + return false; + } + signal_refresh(true); + auto const taken = [&](std::string const &Name) { + return std::any_of(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return lower(Standing.name) == lower(Name); }); + }; + for (int guard = 0; taken(name) && guard < 1000; ++guard) + name = signal_next_name(name); + auto const file{signal_variant(Template, Spot)}; + template_item item; + item.file = file; + item.location = Spot.position; + item.yaw = Spot.yaw; + item.described = true; + item.values["name"] = name; + if (Template.plate) + item.values["#6"] = plate_for(tool.plate, name); + if (Template.linked) + item.values["#7"] = tool.linked[0] != '\0' ? std::string{tool.linked} : std::string{"none"}; + std::string error; + if (place_templates({item}, error) == 0) + { + tool.status = STR_C("The signal couldn't be placed: ") + error; + return false; + } + auto const instance{m_history.back().instances.front()}; + auto const *placed{signal_template_of(file)}; + auto const read{placed != nullptr && false == placed->read.empty() ? editor_includes::substitute(placed->read, {name}) : std::string{}}; + auto const path{Spot.track->name().empty() ? std::string{"(noname)"} : Spot.track->name()}; + std::string reason; + if (read.empty()) + tool.status = format(STR_C("Signal %s placed (%s), the train doesn't read it"), name.c_str(), file.c_str()); + else if (false == Spot.track->Dynamics.empty()) + tool.status = format(STR_C("Signal %s placed (%s), but vehicles stand on the path: add %s to its %s by hand"), name.c_str(), file.c_str(), read.c_str(), editor_track::event_keyword(Spot.list)); + else if (false == scene::Layers.editable(Spot.track, &reason)) + tool.status = format(STR_C("Signal %s placed (%s), but the path can't take its event: %s"), name.c_str(), file.c_str(), reason.c_str()); + else + { + auto snap{std::move(m_history.back())}; + m_history.pop_back(); + auto const before{editor_track::capture(*Spot.track)}; + editor_track::events(*Spot.track, Spot.list).emplace_back(lower(read), nullptr); + std::string missing; + editor_track::bind_events(*Spot.track, missing); + Spot.track->mark_dirty(); + push_track_snapshot({{Spot.track, before}}); + m_history.back().instances = std::move(snap.instances); + tool.status = format(STR_C("Signal %s placed (%s), read by %s %s of %s. Its logic works after the scenery is saved and opened again"), name.c_str(), file.c_str(), editor_track::event_keyword(Spot.list), read.c_str(), path.c_str()); + } + std::snprintf(tool.name, sizeof(tool.name), "%s", signal_next_name(name).c_str()); + signal_refresh(true); + signal_select(instance); + return true; +} + +void editor_mode::signal_detach_events(std::string const &Read, std::vector> &States) +{ + auto const name{lower(Read)}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + bool carries{false}; + for (int list = 0; list < 6 && false == carries; ++list) + for (auto const &event : editor_track::events(*track, list)) + carries = carries || lower(event.first) == name; + if (false == carries) + continue; + if (std::none_of(States.begin(), States.end(), [&](auto const &State) { return State.first == track; })) + States.emplace_back(track, editor_track::capture(*track)); + for (int list = 0; list < 6; ++list) + { + auto &events{editor_track::events(*track, list)}; + events.erase(std::remove_if(events.begin(), events.end(), [&](auto const &Event) { return lower(Event.first) == name; }), events.end()); + } + std::string missing; + editor_track::bind_events(*track, missing); + track->mark_dirty(); + } +} + +bool editor_mode::signal_delete(scene::instance_handle const Instance) +{ + if (false == scene::Layers.tracked(Instance) || false == scene::Layers.removable(Instance)) + return false; + signal_scan(); + auto const &included{scene::Layers.instance(Instance)}; + auto const *item{signal_template_of(*included.file)}; + if (item == nullptr || item->read.empty()) + return false; + std::string file; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(Instance), file, values)) + return false; + auto const read{editor_includes::substitute(item->read, values)}; + std::vector> states; + signal_detach_events(read, states); + if (states.empty()) + return false; + scene::Layers.removed(Instance, true); + push_track_snapshot(std::move(states)); + m_history.back().instances.push_back(Instance); + ui()->set_status(format(STR_C("Signal %s removed with its event %s, Ctrl+Z brings both back"), values.empty() ? "" : values.front().c_str(), read.c_str())); + select_include(0); + m_signal.selected = 0; + signal_refresh(true); + return true; +} + +void editor_mode::signal_renamed(std::string const &Before, std::string const &After, EditorSnapshot &Snapshot) +{ + auto const name{lower(Before)}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + bool carries{false}; + for (int list = 0; list < 6 && false == carries; ++list) + for (auto const &event : editor_track::events(*track, list)) + carries = carries || lower(event.first) == name; + if (false == carries) + continue; + if (std::none_of(Snapshot.tracks.begin(), Snapshot.tracks.end(), [&](auto const &State) { return State.first == track; })) + Snapshot.tracks.emplace_back(track, editor_track::capture(*track)); + for (int list = 0; list < 6; ++list) + for (auto &event : editor_track::events(*track, list)) + if (lower(event.first) == name) + event = {lower(After), nullptr}; + std::string missing; + editor_track::bind_events(*track, missing); + track->mark_dirty(); + } + if (Snapshot.tracks.empty() || Snapshot.instance == 0) + return; + Snapshot.directives.emplace_back(Snapshot.instance, std::move(Snapshot.serialized)); + Snapshot.instance = 0; + Snapshot.serialized.clear(); + Snapshot.action = EditorSnapshot::Action::TrackEdit; + Snapshot.node_ptr = Snapshot.tracks.front().first; + Snapshot.node_name = Snapshot.tracks.front().first->name(); +} + +void editor_mode::signal_select(scene::instance_handle const Instance) +{ + m_signal.selected = Instance; + if (Instance != 0) + select_include(Instance); +} + +void editor_mode::render_signal_palette() +{ + auto &tool{m_signal}; + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##signalfilter", STR_C("filter: name, w24, sbl..."), tool.filter, sizeof(tool.filter)); + auto const filter{lower(tool.filter)}; + auto const *armed{signal_armed()}; + std::vector> groups; + std::vector keys; + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &item{tool.templates[i]}; + if (item.kind != kKinds[tool.kind]) + continue; + if (false == filter.empty() && item.name.find(filter) == std::string::npos && lower(item.description).find(filter) == std::string::npos) + continue; + auto const key{group_of(item.mount, item.lamps)}; + auto const found{std::find(keys.begin(), keys.end(), key)}; + if (found == keys.end()) + { + keys.push_back(key); + groups.push_back({i}); + } + else + groups[found - keys.begin()].push_back(i); + } + if (groups.empty()) + { + ImGui::TextDisabled("%s", STR_C("No template of this kind matches the filter")); + return; + } + ImVec2 const tile{52.0f, 86.0f}; + auto const spacing{ImGui::GetStyle().ItemSpacing.x}; + ImGui::BeginChild("##signaltiles", ImVec2(0.0f, std::min(2.0f * (tile.y + 8.0f) + 20.0f, ImGui::GetContentRegionAvail().y * 0.45f)), true); + auto const columns{std::max(1, static_cast((ImGui::GetContentRegionAvail().x + spacing) / (tile.x + spacing)))}; + auto *draw{ImGui::GetWindowDrawList()}; + for (int g = 0; g < static_cast(groups.size()); ++g) + { + auto const &group{groups[g]}; + auto const &first{tool.templates[group.front()]}; + auto const chosen{armed != nullptr && group_of(armed->mount, armed->lamps) == keys[g] && armed->kind == first.kind}; + if (g % columns != 0) + ImGui::SameLine(); + ImGui::PushID(g); + auto const origin{ImGui::GetCursorScreenPos()}; + if (ImGui::InvisibleButton("##tile", tile)) + { + auto pick{group.front()}; + if (armed != nullptr && chosen) + pick = tool.chosen[tool.kind]; + tool.chosen[tool.kind] = pick; + } + auto const hovered{ImGui::IsItemHovered()}; + draw->AddRectFilled(origin, ImVec2(origin.x + tile.x, origin.y + tile.y), chosen ? IM_COL32(60, 90, 140, 255) : hovered ? IM_COL32(60, 60, 60, 255) : IM_COL32(40, 40, 40, 255), 4.0f); + draw_signal_icon(draw, origin, ImVec2(tile.x, tile.y - 14.0f), first.mount, first.lamps, chosen ? overlay_color::selected : IM_COL32(120, 120, 120, 255)); + auto const caption{first.lamps.empty() ? first.name : first.lamps}; + auto const captionsize{ImGui::CalcTextSize(caption.c_str())}; + draw->AddText(ImVec2(origin.x + std::max(2.0f, (tile.x - captionsize.x) * 0.5f), origin.y + tile.y - 15.0f), IM_COL32(220, 220, 220, 255), caption.c_str()); + if (group.size() > 1) + draw->AddText(ImVec2(origin.x + 3.0f, origin.y + 2.0f), IM_COL32(170, 170, 170, 255), std::to_string(group.size()).c_str()); + if (hovered) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(first.description.empty() ? first.name.c_str() : first.description.c_str()); + ImGui::TextDisabled(STR_C("%zu variants, e.g. %s"), group.size(), first.file.c_str()); + ImGui::TextDisabled("%s", first.read.empty() ? STR_C("The train doesn't read it") : STR_C("The train reads it by an event of the path")); + ImGui::EndTooltip(); + } + ImGui::PopID(); + } + ImGui::EndChild(); + if (armed == nullptr) + return; + auto const key{group_of(armed->mount, armed->lamps)}; + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::BeginCombo("##signalvariant", armed->name.c_str())) + { + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &item{tool.templates[i]}; + if (item.kind != armed->kind || group_of(item.mount, item.lamps) != key) + continue; + auto const label{item.name + (item.lean == "left" ? std::string{" \xE2\x86\x96"} : item.lean == "right" ? std::string{" \xE2\x86\x97"} : std::string{}) + "##" + item.file}; + if (ImGui::Selectable(label.c_str(), &item == armed)) + tool.chosen[tool.kind] = i; + if (ImGui::IsItemHovered() && false == item.description.empty()) + ImGui::SetTooltip("%s\n%s", item.description.c_str(), item.file.c_str()); + } + ImGui::EndCombo(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Variant of the chosen type. A head leaning to one side is swapped for its counterpart leaning towards the track on the other side")); + if (false == armed->description.empty()) + ImGui::TextWrapped("%s", armed->description.c_str()); + ImGui::TextDisabled("%s", armed->read.empty() ? (armed->kind == "warning" || armed->kind == "repeater" ? STR_C("The train doesn't read it: it repeats what the signal ahead of it shows") : STR_C("The train doesn't read it")) : format(STR_C("The train reads it by %s on the path it stands by"), editor_includes::substitute(armed->read, {tool.name[0] != '\0' ? std::string{tool.name} : std::string{"?"}}).c_str()).c_str()); +} + +void editor_mode::render_signal_selected() +{ + auto &tool{m_signal}; + auto const found{std::find_if(tool.standing.begin(), tool.standing.end(), [&](signal_standing const &Standing) { return Standing.include.instance == tool.selected; })}; + if (found == tool.standing.end()) + return; + ImGui::Separator(); + ImGui::TextUnformatted(format(STR_C("Selected: %s (%s)"), found->name.c_str(), found->include.file.c_str()).c_str()); + if (found->read.empty()) + ImGui::TextDisabled("%s", STR_C("The train doesn't read it")); + else if (found->track != nullptr) + ImGui::TextDisabled(STR_C("Read by %s %s of %s"), editor_track::event_keyword(found->list), found->read.c_str(), found->track->name().empty() ? "(noname)" : found->track->name().c_str()); + else + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No path has %s among its events: the train doesn't see it"), found->read.c_str()); + ImGui::TextDisabled("%s", STR_C("Name, plate and the linked signal: in the node properties. Del removes it with its event")); +} + +void editor_mode::render_signal_ui() +{ + auto &tool{m_signal}; + signal_scan(); + if (tool.templates.empty()) + { + ImGui::TextWrapped("%s", STR_C("No signal templates: no template of the scenery directory is described with category: signal. tools/editor/signal_headers.py writes the descriptions into the signal templates.")); + return; + } + if (ImGui::BeginTabBar("##signalkinds")) + { + for (int k = 0; k < kKindCount; ++k) + { + if (std::none_of(tool.templates.begin(), tool.templates.end(), [&](signal_template const &Item) { return Item.kind == kKinds[k]; })) + continue; + if (ImGui::BeginTabItem(STR(kKindLabels[k]).c_str())) + { + tool.kind = k; + ImGui::EndTabItem(); + } + } + ImGui::EndTabBar(); + } + render_signal_palette(); + auto const *armed{signal_armed()}; + ImGui::Separator(); + ImGui::PushItemWidth(140.0f); + ImGui::InputText(STR_C("Name"), tool.name, sizeof(tool.name)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Name of the next signal; it counts up after each one placed. Its events and memory cell are named after it")); + if (armed != nullptr && armed->plate) + { + auto const automatic{format(STR_C("auto: %s"), plate_for({}, tool.name).c_str())}; + ImGui::InputTextWithHint(STR_C("Plate"), automatic.c_str(), tool.plate, sizeof(tool.plate)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Texture of the plate from textures/tabl, e.g. a-12 or tob. Left empty: the one named like the signal, if there is one")); + auto const &plates{plate_textures()}; + auto const typed{lower(tool.plate)}; + auto const exists{typed.empty() || std::binary_search(plates.begin(), plates.end(), typed)}; + if (false == exists) + { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("no such texture")); + } + if (ImGui::IsItemActive() || (false == exists && false == typed.empty())) + { + int shown{0}; + for (auto const &plate : plates) + { + if (typed.empty() || plate.compare(0, typed.size(), typed) != 0 || plate == typed) + continue; + if (shown % 6 != 0) + ImGui::SameLine(); + if (ImGui::SmallButton(plate.c_str())) + std::snprintf(tool.plate, sizeof(tool.plate), "%s", plate.c_str()); + if (++shown >= 18) + break; + } + } + } + if (armed != nullptr && armed->linked && ImGui::TreeNode(STR_C("Linked signal"))) + { + ImGui::InputText("##linked", tool.linked, sizeof(tool.linked)); + ImGui::TextDisabled("%s", STR_C("(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link")); + ImGui::TreePop(); + } + auto offset{static_cast(EditorSettings.signal_offset())}; + if (ImGui::InputFloat(STR_C("From the axis (m)"), &offset, 0.05f, 0.25f, "%.2f")) + { + EditorSettings.signal_offset(std::clamp(static_cast(offset), 0.0, 10.0)); + EditorSettings.save(); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Distance of the mast from the axis of the track. 2.25 m is what the signals of the sceneries stand at")); + if (armed != nullptr && armed->mount == "gantry") + { + ImGui::InputFloat(STR_C("Over the rails (m)"), &tool.height, 0.1f, 0.5f, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("A head for a gantry hangs over the axis of the track this high over the rail heads")); + } + ImGui::PopItemWidth(); + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); + render_signal_selected(); +} + +void editor_mode::draw_signal_overlay() const +{ + auto const &tool{m_signal}; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + auto const arrow = [&](glm::dvec3 const &From, glm::dvec2 const &Direction, ImU32 const Colour) { + glm::dvec3 const tip{From.x + Direction.x * 6.0, From.y, From.z + Direction.y * 6.0}; + ImVec2 a, b; + if (false == projection.project(From, a) || false == projection.project(tip, b)) + return; + drawlist->AddLine(a, b, Colour, 2.5f); + auto const dx{b.x - a.x}; + auto const dy{b.y - a.y}; + auto const size{std::sqrt(dx * dx + dy * dy)}; + if (size < 1.0f) + return; + ImVec2 const unit{dx / size, dy / size}; + ImVec2 const side{-unit.y, unit.x}; + drawlist->AddTriangleFilled(ImVec2(b.x + unit.x * 9.0f, b.y + unit.y * 9.0f), ImVec2(b.x + side.x * 6.0f, b.y + side.y * 6.0f), ImVec2(b.x - side.x * 6.0f, b.y - side.y * 6.0f), Colour); + }; + auto const mast = [&](glm::dvec3 const &Foot, ImU32 const Colour, float const Width) { projection.line(drawlist, Foot, Foot + glm::dvec3{0.0, kSignalHead, 0.0}, Colour, Width); }; + for (int i = 0; i < static_cast(tool.standing.size()); ++i) + { + auto const &standing{tool.standing[i]}; + auto const &location{standing.include.location}; + if (glm::distance(location, camera) > kSignalRange) + continue; + auto const selected{standing.include.instance == tool.selected}; + auto const hovered{m_intent.what == track_intent::kind::signal_move && m_intent.index == i}; + auto const unread{false == standing.read.empty() && standing.track == nullptr}; + auto const colour{selected ? overlay_color::selected : hovered ? overlay_color::highlight : unread ? overlay_color::invalid : IM_COL32(255, 210, 60, 160)}; + mast(location, colour, selected || hovered ? 3.0f : 1.5f); + ImVec2 head; + if ((selected || hovered || unread) && projection.project(location + glm::dvec3{0.0, kSignalHead, 0.0}, head)) + { + auto const label{standing.name + (unread ? std::string{" "} + STR(" not read") : std::string{})}; + drawlist->AddText(ImVec2(head.x + 6.0f, head.y - 6.0f), colour, label.c_str()); + } + if (selected || hovered) + { + auto const yaw{glm::radians(standing.include.yaw + 180.0)}; + arrow(location, glm::dvec2{std::sin(yaw), std::cos(yaw)}, colour); + } + } + auto const preview = [&](signal_spot const &Spot, ImU32 const Colour) { + projection.line(drawlist, Spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, Spot.position, Colour, 2.0f); + mast(Spot.position, Colour, 3.0f); + arrow(Spot.axis + glm::dvec3{0.0, kRailTop, 0.0}, Spot.travel, Colour); + ImVec2 head; + auto const *armed{signal_armed()}; + if (armed != nullptr && false == armed->lamps.empty() && projection.project(Spot.position + glm::dvec3{0.0, kSignalHead, 0.0}, head)) + { + auto const lamps{static_cast(armed->lamps.size())}; + drawlist->AddRectFilled(ImVec2(head.x - 6.0f, head.y - 4.0f), ImVec2(head.x + 6.0f, head.y + lamps * 10.0f), IM_COL32(20, 20, 20, 220), 3.0f); + for (int i = 0; i < lamps; ++i) + drawlist->AddCircleFilled(ImVec2(head.x, head.y + 1.0f + i * 10.0f), 4.0f, lamp_colour(armed->lamps[i])); + } + }; + if (tool.placing) + preview(tool.press, overlay_color::grip); + else if (tool.moving && tool.moved_valid) + preview(tool.moved, overlay_color::selected); + else if (m_intent.what == track_intent::kind::signal_place && m_hover.track != nullptr) + { + if (auto const *armed{signal_armed()}; armed != nullptr) + { + signal_spot spot; + if (signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot)) + preview(spot, overlay_color::marked); + } + } +} diff --git a/application/editormodespeed.cpp b/application/editormodespeed.cpp new file mode 100644 index 00000000..9843c7b0 --- /dev/null +++ b/application/editormodespeed.cpp @@ -0,0 +1,368 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include + +namespace +{ + +double const kSpeedLineReach{5000.0}; // m each way, for the line through the selected path +double const kSpeedOverlayRange{3000.0}; + +bool rail(TTrack const &Track) +{ + return (Track.iCategoryFlag & 15) == 1 && false == Track.m_editorremoved && Track.m_road == nullptr && editor_track::is_supported(Track); +} + +std::string speed_text(double const Speed) +{ + return Speed > 0.0 ? format("%.0f", Speed) : std::string{"none"}; +} + +} // namespace + +void editor_mode::speed_start() +{ + auto &state{m_speed}; + state.queue.clear(); + state.next = 0; + state.results.clear(); + state.selected = -1; + state.status.clear(); + state.options.norms = m_route.design.norms; + auto const add = [&](TTrack *Track) { + for (int i = 0; i < static_cast(Track->m_paths.size()); ++i) + state.queue.emplace_back(Track, i); + }; + if (state.scope == 0) + { + auto *track{selected_track()}; + if (track == nullptr || false == rail(*track)) + { + state.status = STR_C("Select a track first"); + return; + } + editor_track::chain line; + std::string error; + if (track->eType == tt_Normal && editor_track::find_run(*track, kSpeedLineReach, line, error)) + { + for (auto *member : line.tracks) + add(member); + // the switches at the ends of the line too + for (auto *end : {line.tracks.front(), line.tracks.back()}) + for (auto *neighbour : editor_track::neighbours(*end)) + if (neighbour->eType == tt_Switch && rail(*neighbour) && std::none_of(state.queue.begin(), state.queue.end(), [&](auto const &Entry) { return Entry.first == neighbour; })) + add(neighbour); + } + else + { + add(track); + } + } + else + { + for (auto *track : simulation::Paths.sequence()) + if (track != nullptr && rail(*track)) + add(track); + } + state.history = m_history.size(); +} + +void editor_mode::speed_step() +{ + auto &state{m_speed}; + if (state.next >= state.queue.size()) + return; + auto const deadline{std::chrono::steady_clock::now() + std::chrono::milliseconds(15)}; + while (state.next < state.queue.size() && std::chrono::steady_clock::now() < deadline) + { + auto const &entry{state.queue[state.next++]}; + if (entry.first->m_editorremoved) + continue; + state.results.push_back(speed_check::check(*entry.first, entry.second, state.options)); + } + if (state.next < state.queue.size()) + return; + // the worst first: the violations, then the warnings, each by the largest excess of the speed set over the safe one + auto const excess = [&](speed_check::verdict const &Verdict) { return Verdict.set > 0.0 ? Verdict.set - Verdict.safe : state.options.top - Verdict.safe; }; + auto const rank = [&](speed_check::verdict const &Verdict) { return Verdict.violated(state.options) ? 0 : Verdict.warned(state.options) ? 1 : 2; }; + std::stable_sort(state.results.begin(), state.results.end(), [&](auto const &A, auto const &B) { + auto const a{rank(A)}; + auto const b{rank(B)}; + if (a != b) + return a < b; + return a < 2 ? excess(A) > excess(B) : A.safe < B.safe; + }); + auto const count{std::count_if(state.results.begin(), state.results.end(), [&](auto const &Verdict) { return Verdict.violated(state.options); })}; + auto const warnings{std::count_if(state.results.begin(), state.results.end(), [&](auto const &Verdict) { return Verdict.warned(state.options); })}; + state.status = format(STR_C("%zu paths checked, %d with the speed above what the geometry allows, %d switch branch warnings"), state.results.size(), static_cast(count), static_cast(warnings)); + WriteLog("Editor: speed check - " + state.status, logtype::generic); +} + +std::vector editor_mode::speed_listed() const +{ + std::vector listed; + for (int i = 0; i < static_cast(m_speed.results.size()); ++i) + { + auto const &verdict{m_speed.results[i]}; + if ((m_speed.limits == 1 && verdict.set <= 0.0) || (m_speed.limits == 2 && verdict.set > 0.0)) + continue; + if (m_speed.show_all || verdict.violated(m_speed.options) || verdict.warned(m_speed.options)) + listed.push_back(i); + } + return listed; +} + +void editor_mode::speed_focus(int const Index) +{ + auto &state{m_speed}; + if (Index < 0 || Index >= static_cast(state.results.size())) + return; + state.selected = Index; + auto const &verdict{state.results[Index]}; + auto const &paths{simulation::Paths.sequence()}; + if (std::find(paths.begin(), paths.end(), verdict.track) == paths.end() || verdict.track->m_editorremoved) + return; + m_node = verdict.track; + ui()->set_node(m_node); + focus_track(*verdict.track, verdict.path, verdict.position); +} + +void editor_mode::speed_apply(std::vector const &Indices, bool const Branches) +{ + auto &state{m_speed}; + struct change + { + TTrack *track; + double limits[2]; // km/h for the path, and the diverging track of a switch, -1: none + }; + std::vector> states; + std::vector changes; + std::size_t locked{0}; + for (auto const index : Indices) + { + auto const &verdict{state.results[index]}; + auto const safe{Branches ? verdict.safe : verdict.hard}; + if (safe >= state.options.top || verdict.track->m_editorremoved) + continue; + if (false == scene::Layers.editable(verdict.track)) + { + ++locked; + continue; + } + auto existing{std::find_if(changes.begin(), changes.end(), [&](change const &Change) { return Change.track == verdict.track; })}; + if (existing == changes.end()) + { + changes.push_back({verdict.track, {speed_check::path_velocity(*verdict.track, 0), speed_check::path_velocity(*verdict.track, 1)}}); + states.emplace_back(verdict.track, editor_track::capture(*verdict.track)); + existing = std::prev(changes.end()); + } + auto &limit{existing->limits[verdict.track->eType == tt_Switch ? std::clamp(verdict.path, 0, 1) : 0]}; + auto const speed{speed_check::suggested(safe)}; + limit = limit > 0.0 ? std::min(limit, speed) : speed; + } + auto const skipped{locked > 0 ? format(", %zu in the files which can't be changed are left as they are", locked) : std::string{}}; + if (changes.empty()) + { + if (locked > 0) + state.status = STR_C("Nothing changed") + skipped; + return; + } + push_track_snapshot(std::move(states)); + for (auto const &entry : changes) + { + auto const velocity{entry.track->eType == tt_Switch ? speed_check::switch_velocity(entry.limits[0], entry.limits[1]) : entry.limits[0]}; + editor_track::velocity(*entry.track, velocity); + entry.track->mark_dirty(); + } + for (auto &verdict : state.results) + verdict.set = speed_check::path_velocity(*verdict.track, verdict.path); + state.history = m_history.size(); + state.status = format(STR_C("Speed limits of %zu paths lowered to the safe ones, Ctrl+Z brings them back"), changes.size()) + skipped; + WriteLog("Editor: speed check - " + state.status, logtype::generic); +} + +void editor_mode::render_speed_body() +{ + auto &state{m_speed}; + ImGui::TextDisabled(STR_C("Speed limits of the paths against the speed their geometry allows:\nradius and cant, cant ramps, curves without the transitions, switch branches")); + ImGui::RadioButton(STR_C("Line through the selected path"), &state.scope, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Whole scenery"), &state.scope, 1); + bool const running{state.next < state.queue.size()}; + if (running) + { + auto const progress{static_cast(state.next) / static_cast(state.queue.size())}; + ImGui::ProgressBar(progress, ImVec2(-70.0f, 0.0f), format(STR_C("Checking %zu / %zu"), state.next, state.queue.size()).c_str()); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Cancel"))) + state.queue.resize(state.next); + } + else if (ImGui::Button(STR_C("Check"))) + { + speed_start(); + } + if (ImGui::TreeNode(STR_C("Limits"))) + { + auto &options{state.options}; + ImGui::PushItemWidth(100.0f); + ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &options.norms.unbalanced, 0.0, 0.0, "%.2f"); + ImGui::InputDouble(STR_C("Switch branches (m/s2)"), &options.branch_unbalanced, 0.0, 0.0, "%.2f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Unbalanced acceleration allowed on the diverging tracks of the switches, which have no cant.\n0.65 m/s2 gives 40 km/h for R 190 m and 100 km/h for R 1200 m")); + ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &options.norms.cant_rate, 0.0, 0.0, "%.0f"); + ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &options.norms.ramp_factor, 0.0, 0.0, "%.1f"); + ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &options.norms.jerk, 0.0, 0.0, "%.2f"); + ImGui::InputDouble(STR_C("Reference length (m)"), &options.reference_length, 0.0, 0.0, "%.0f"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Length over which an abrupt change of the curvature or the cant is felt,\nabout the length of a car. Shorter: stricter for the curves without the transitions")); + ImGui::PopItemWidth(); + options.norms.unbalanced = std::max(0.05, options.norms.unbalanced); + options.branch_unbalanced = std::max(0.05, options.branch_unbalanced); + options.norms.cant_rate = std::max(1.0, options.norms.cant_rate); + options.norms.ramp_factor = std::max(1.0, options.norms.ramp_factor); + options.norms.jerk = std::max(0.01, options.norms.jerk); + options.reference_length = std::clamp(options.reference_length, 2.0, 200.0); + ImGui::TextDisabled(STR_C("Change the limits, then Check again")); + ImGui::TreePop(); + } + if (false == state.status.empty()) + ImGui::TextWrapped("%s", state.status.c_str()); + if (state.results.empty()) + return; + if (state.history != m_history.size()) + ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.3f, 1.0f), STR_C("The scenery changed since the check")); + ImGui::SetNextItemWidth(190.0f); + ImGui::Combo("##speedlimits", &state.limits, "All the paths\0With a speed limit set\0Without a speed limit\0"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Paths without a speed limit of their own run at the line speed,\nthose are listed whenever the geometry limits the speed at all")); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("Within their limits too"), &state.show_all); + auto const listed{speed_listed()}; + std::vector violated; + for (auto const index : listed) + if (state.results[index].violated(state.options)) + violated.push_back(index); + if (false == violated.empty()) + { + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Set the safe speed on %zu paths"), violated.size()).c_str())) + speed_apply(violated, false); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("Lowers the speed limit of each listed path to its safe speed, rounded down to 10 km/h (5 km/h below 40).\n" + "The limits of the switch branches are only warnings and aren't applied here, the button of a single path does.\n" + "A switch has one velocity for both its tracks: negative limits only the diverging one, the main one keeping no limit.\n" + "When the main track needs a limit of its own, the lower of the two applies to both")); + } + auto const namewidth{std::max(120.0f, ImGui::GetContentRegionAvail().x * 0.3f)}; + ImGui::Columns(6, STR_C("speedcolumns"), false); + ImGui::SetColumnWidth(0, namewidth + 8.0f); + for (auto const *header : {"Path", "Set", "Safe", "Suggested", "Cause", "R / cant"}) + { + ImGui::TextDisabled("%s", header); + ImGui::NextColumn(); + } + ImGui::Columns(1); + bool const chosen{state.selected >= 0 && state.selected < static_cast(state.results.size())}; + auto const details{chosen ? ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 5.0f : 0.0f}; + ImGui::BeginChild("speedresults", ImVec2(0.0f, -details), true); + ImGui::Columns(6, STR_C("speedrows"), false); + ImGui::SetColumnWidth(0, namewidth); + ImGuiListClipper clipper; + clipper.Begin(static_cast(listed.size())); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const index{listed[row]}; + auto const &verdict{state.results[index]}; + bool const violated{verdict.violated(state.options)}; + bool const bad{violated || verdict.warned(state.options)}; + auto const colour{violated ? ImVec4(1.0f, 0.45f, 0.4f, 1.0f) : ImVec4(1.0f, 0.75f, 0.3f, 1.0f)}; + auto name{verdict.track->name().empty() ? std::string{"(noname)"} : verdict.track->name()}; + if (verdict.track->eType == tt_Switch) + name += verdict.path == 0 ? " (main)" : " (branch)"; + if (bad) + ImGui::PushStyleColor(ImGuiCol_Text, colour); + if (ImGui::Selectable((name + "##speed" + std::to_string(index)).c_str(), state.selected == index, ImGuiSelectableFlags_SpanAllColumns)) + speed_focus(index); + if (ImGui::IsItemHovered()) + { + if (bad) + ImGui::PopStyleColor(); + ImGui::SetTooltip("%s", speed_check::explain(verdict, state.options).c_str()); + if (bad) + ImGui::PushStyleColor(ImGuiCol_Text, colour); + } + ImGui::NextColumn(); + ImGui::TextUnformatted(speed_text(verdict.set).c_str()); + ImGui::NextColumn(); + ImGui::TextUnformatted(verdict.safe >= state.options.top ? STR_C("any") : format("%.0f", verdict.safe).c_str()); + ImGui::NextColumn(); + ImGui::TextUnformatted(verdict.safe >= state.options.top ? "-" : format("%.0f", speed_check::suggested(verdict.safe)).c_str()); + ImGui::NextColumn(); + ImGui::TextUnformatted(speed_check::describe(verdict.why)); + ImGui::NextColumn(); + if (verdict.radius > 0.0) + ImGui::Text(STR_C("%.0f m / %.0f mm"), verdict.radius, verdict.cant); + else + ImGui::TextUnformatted("-"); + ImGui::NextColumn(); + if (bad) + ImGui::PopStyleColor(); + } + ImGui::Columns(1); + ImGui::EndChild(); + if (chosen) + { + auto const &verdict{state.results[state.selected]}; + ImGui::TextWrapped("%s", speed_check::explain(verdict, state.options).c_str()); + if ((verdict.violated(state.options) || verdict.warned(state.options)) && ImGui::Button(STR_C("Set the safe speed on this path"))) + speed_apply({state.selected}, true); + } +} + +void editor_mode::draw_speed_overlay() const +{ + auto const &state{m_speed}; + if (false == state.open || state.results.empty()) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + glm::dvec3 const camera{Global.pCamera.Pos}; + for (int i = 0; i < static_cast(state.results.size()); ++i) + { + auto const &verdict{state.results[i]}; + bool const violated{verdict.violated(state.options)}; + if ((false == violated && false == verdict.warned(state.options)) || glm::distance(verdict.position, camera) > kSpeedOverlayRange) + continue; + ImVec2 screen; + if (false == projection.project(verdict.position, screen)) + continue; + auto const colour{i == state.selected ? IM_COL32(255, 255, 80, 255) : violated ? IM_COL32(255, 70, 60, 230) : IM_COL32(255, 180, 60, 200)}; + drawlist->AddCircle(screen, 12.0f, colour, 16, 2.5f); + auto const text{format("%s > %.0f", speed_text(verdict.set).c_str(), verdict.safe)}; + drawlist->AddText(ImVec2(screen.x + 14.0f, screen.y - 7.0f), colour, text.c_str()); + } +} diff --git a/application/editormodesweep.cpp b/application/editormodesweep.cpp new file mode 100644 index 00000000..ee57378a --- /dev/null +++ b/application/editormodesweep.cpp @@ -0,0 +1,2091 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" +#include "editor/editorSettings.hpp" + +#include "rendering/renderer.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "world/Sweep.h" +#include "model/AnimModel.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::plan_distance; +using geometry::plan_of; + +double const kSweepRouteReach{2000.0}; // m each way through the switches +double const kSweepOutlineStep{1.0}; +double const kSweepNear{4.0}; // m, a model along a curve this close to the selected path belongs to its line +double const kSweepGrab{25.0}; // m from the track, a press this close marks the stretch of the model +std::size_t const kSweepModelList{400}; +double const kRailTop{0.18}; // m from the points of a track to the top of its rails + +glm::dvec3 flat(glm::dvec3 Vector) +{ + Vector.y = 0.0; + return glm::length(Vector) > 1e-9 ? glm::normalize(Vector) : glm::dvec3{0.0, 0.0, 1.0}; +} + +// the edges of the platforms in peronowe/ are named by the count of the edges and the height: 1k96w200m, 2k30w..., 1kraw200m55cm, +// old_1k76w..., old2_peron1k30w..., old_peron1k_prosty_h70...; the rest there are the benches, the lamps and such. their side facing the track is the edge +double platform_height(std::string File) +{ + std::transform(File.begin(), File.end(), File.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + std::replace(File.begin(), File.end(), '\\', '/'); + auto const slash{File.rfind('/')}; + if (slash == std::string::npos || File.substr(0, slash).find("peron") == std::string::npos) + return 0.0; + static std::regex const pattern{R"(^(?:old2?_)?(?:(?:peron)?\dk(\d\d)|\dkraw\d+m(\d\d)cm|peron.*_h(\d\d))(?:[^\d]|$))"}; + std::smatch match; + auto const name{File.substr(slash + 1)}; + if (false == std::regex_search(name, match, pattern)) + return 0.0; + for (std::size_t i = 1; i < match.size(); ++i) + if (match[i].matched) + return std::stoi(match[i].str()) / 100.0; + return 0.0; +} + +bool platform_model(std::string const &File) +{ + return platform_height(File) > 0.0; +} + +// the edge of the platform 1.725 m from the axis on the side it stands, set back further by the widening of the gauge +void platform_edge(sweep_node::state &State, std::string const &Model, double const Base) +{ + State.lateral = std::copysign(EditorSettings.platform_edge(), State.lateral == 0.0 ? 1.0 : State.lateral); + State.side_anchor = 1; + State.height_anchor = 3; + if (auto const height{platform_height(Model)}; height > 0.0) + State.height = Base + height; + State.widen = true; + State.platform = true; +} + +bool platform_distance() +{ + double const distances[]{1.725, 1.675, 1.650}; + char const *labels[]{STR_C("1.725 m: lines of out-of-gauge loads, older platforms"), STR_C("1.675 m: nominal by the 0.55 and 0.76 m platforms"), STR_C("1.650 m: the least allowed in service")}; + auto const current{EditorSettings.platform_edge()}; + int chosen{0}; + for (int i = 0; i < IM_ARRAYSIZE(distances); ++i) + if (std::abs(current - distances[i]) < std::abs(current - distances[chosen])) + chosen = i; + ImGui::SetNextItemWidth(300.0f); + if (false == ImGui::Combo(STR_C("edge of the platform from the axis"), &chosen, labels, IM_ARRAYSIZE(labels))) + return false; + EditorSettings.platform_edge(distances[chosen]); + EditorSettings.save(); + return true; +} + +segment_data reversed(segment_data const &Path) +{ + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; +} + +bool same_piece(segment_data const &A, segment_data const &B) +{ + for (int i = 0; i < 4; ++i) + if (glm::distance(A.points[i], B.points[i]) > 1e-3) + return false; + return std::abs(A.rolls[0] - B.rolls[0]) < 1e-3f && std::abs(A.rolls[1] - B.rolls[1]) < 1e-3f; +} + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +} // namespace + +void editor_mode::sweep_scan_models() +{ + auto &tool{m_sweep}; + if (tool.scanned) + return; + tool.scanned = true; + std::error_code error; + std::set found; + for (auto const &entry : std::filesystem::recursive_directory_iterator("models", std::filesystem::directory_options::skip_permission_denied, error)) + { + if (false == entry.is_regular_file(error)) + continue; + auto const extension{lower(entry.path().extension().string())}; + if (extension != ".e3d" && extension != ".t3d") + continue; + auto name{std::filesystem::relative(entry.path(), "models", error).generic_string()}; + name.erase(name.size() - extension.size()); + found.insert(name); + } + for (auto const &entry : std::filesystem::recursive_directory_iterator("scenery", std::filesystem::directory_options::skip_permission_denied, error)) + { + if (entry.is_regular_file(error) && lower(entry.path().extension().string()) == ".inc") + found.insert(std::filesystem::relative(entry.path(), "scenery", error).generic_string()); + } + tool.models.assign(found.begin(), found.end()); +} + + +void editor_mode::sweep_curve() +{ + auto &tool{m_sweep}; + auto *track{selected_track()}; + if (track == tool.curve_for && tool.curve_history == m_history.size()) + return; + tool.curve_for = track; + tool.curve_history = m_history.size(); + tool.curve.clear(); + tool.curve_pieces.clear(); + if (track != nullptr && false == track->m_paths.empty() && editor_track::is_supported(*track)) + { + tool.curve.push_back(track->m_paths.front()); + tool.curve_pieces.push_back({track, 0, true}); + } + sweep_outline(); +} + +void editor_mode::sweep_outline() +{ + auto &tool{m_sweep}; + tool.outline.clear(); + tool.stations.clear(); + tool.curve_length = 0.0; + for (auto const &piece : tool.curve) + { + bezier const curve{piece}; + auto const count{std::clamp(static_cast(curve.plan_length() / kSweepOutlineStep), 2, 2000)}; + for (int k = tool.outline.empty() ? 0 : 1; k <= count; ++k) + { + auto const point{curve.point(static_cast(k) / count)}; + if (false == tool.outline.empty()) + tool.curve_length += plan_distance(tool.outline.back(), point); + tool.outline.push_back(point); + tool.stations.push_back(tool.curve_length); + } + } +} + +bool editor_mode::sweep_extend(bool const Atend, glm::dvec3 const &Cursor) +{ + auto &tool{m_sweep}; + if (tool.curve_pieces.empty() || tool.curve_length > kSweepRouteReach) + return false; + auto const &edge{Atend ? tool.curve_pieces.back() : tool.curve_pieces.front()}; + auto const &edgepiece{Atend ? tool.curve.back() : tool.curve.front()}; + auto const point{Atend ? edgepiece.points[segment_data::point::end] : edgepiece.points[segment_data::point::start]}; + std::optional best; + segment_data bestpiece; + auto bestdistance{std::numeric_limits::max()}; + for (auto const &[other, end] : editor_track::connected_points(*edge.track, point)) + { + if (other == nullptr || other->m_editorremoved || other->eType == tt_Table || false == editor_track::is_supported(*other) || end.path >= static_cast(other->m_paths.size())) + continue; + if (std::any_of(tool.curve_pieces.begin(), tool.curve_pieces.end(), [&](sweep_tool::piece const &Piece) { return Piece.track == other && Piece.path == end.path; })) + continue; + auto const startshere{end.kind == editor_track::point_kind::start}; + auto const forward{Atend ? startshere : false == startshere}; + auto const piece{forward ? other->m_paths[end.path] : reversed(other->m_paths[end.path])}; + bezier const curve{piece}; + auto distance{std::numeric_limits::max()}; + for (int k = 0; k <= 32; ++k) + distance = std::min(distance, plan_distance(curve.point(k / 32.0), Cursor)); + if (distance < bestdistance) + { + bestdistance = distance; + best = sweep_tool::piece{other, end.path, forward}; + bestpiece = piece; + } + } + if (false == best.has_value()) + return false; + if (Atend) + { + tool.curve_pieces.push_back(*best); + tool.curve.push_back(bestpiece); + sweep_outline(); + } + else + { + auto const before{tool.curve_length}; + tool.curve_pieces.insert(tool.curve_pieces.begin(), *best); + tool.curve.insert(tool.curve.begin(), bestpiece); + sweep_outline(); + tool.anchor += tool.curve_length - before; + } + return true; +} + +double editor_mode::sweep_station(glm::dvec3 const &Point, double &Lateral) const +{ + auto const &tool{m_sweep}; + double best{std::numeric_limits::max()}; + double station{0.0}; + Lateral = 0.0; + auto const point{plan_of(Point)}; + for (std::size_t i = 1; i < tool.outline.size(); ++i) + { + auto const a{plan_of(tool.outline[i - 1])}; + auto const ab{plan_of(tool.outline[i]) - a}; + auto const length2{glm::dot(ab, ab)}; + if (length2 < 1e-12) + continue; + auto const t{std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0)}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < best) + { + best = distance; + station = tool.stations[i - 1] + (tool.stations[i] - tool.stations[i - 1]) * t; + Lateral = geometry::cross(glm::normalize(ab), point - a) >= 0.0 ? distance : -distance; + } + } + return station; +} + +void editor_mode::sweep_measure() +{ + auto &tool{m_sweep}; + std::string const model{tool.model}; + if (model == tool.measured) + return; + tool.measured = model; + if (tool.edited == nullptr || tool.edited->definition().model != model) + { + if (platform_model(model)) + platform_edge(tool.settings, model, 0.0); + else + tool.settings.platform = tool.settings.widen = false; + } + tool.model_length = 0.0; + if (model.empty()) + return; + scene::node_data data; + sweep_node probe{data}; + auto state{sweep_definition()}; + state.pieces.clear(); + state.along_x = false; + probe.define(state); + auto const alongz{probe.model_length()}; + state.along_x = true; + probe.define(state); + auto const alongx{probe.model_length()}; + tool.settings.along_x = tool.axis == 2 || (tool.axis == 0 && alongx > alongz); + tool.model_length = tool.settings.along_x ? alongx : alongz; +} + +sweep_node::state editor_mode::sweep_definition() const +{ + auto const &tool{m_sweep}; + auto state{tool.settings}; + state.model = tool.model; + state.skin = tool.skin[0] != '\0' ? tool.skin : "none"; + state.pieces = tool.curve; + state.height = tool.settings.height + kRailTop; + if (tool.length_mode == 0 && tool.model_length > 0.0) + state.to = state.from + tool.model_length; + else if (tool.length_mode == 1) + state.to = state.from + std::max(0.5, tool.length); + state.parameters.clear(); + for (auto const &value : tool.parameters) + state.parameters.emplace_back(value[0] != '\0' ? value.data() : "none"); + return state; +} + +void editor_mode::sweep_create() +{ + auto &tool{m_sweep}; + sweep_curve(); + if (tool.curve.empty() || tool.model[0] == '\0') + return; + for (auto wanted{sweep_definition().to}; wanted > tool.curve_length && tool.outline.size() > 1;) + { + auto const end{tool.outline.back()}; + auto const ahead{end + flat(end - tool.outline[tool.outline.size() - 2]) * (wanted - tool.curve_length)}; + if (false == sweep_extend(true, ahead)) + break; + } + static int counter{0}; + scene::node_data data; + data.type = "sweep"; + data.range_max = -1.0; + data.layer = scene::Layers.active(); + do + { + data.name = "sweep_" + std::to_string(++counter); + } while (simulation::Sweeps.find(data.name) != nullptr); + auto *sweep{new sweep_node(data)}; + sweep->define(sweep_definition()); + sweep->mark_dirty(); + simulation::Sweeps.insert(sweep); + scene::Layers.count(sweep->layer(), scene::layer_item::model); + sweep->show(); + // the pieces are the paths of the line, so the model follows them when they change + for (auto const &piece : tool.curve_pieces) + if (piece.track != nullptr && false == piece.track->m_editorremoved) + sweeps_captured(*piece.track); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = sweep->name(); + snap.sweeps_toggled.push_back(sweep); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + tool.edited = sweep; + m_bend.edited = sweep; + tool.status = format(STR_C("%s laid along %.1f m, Ctrl+Z takes it back"), tool.model, sweep->end() - sweep->start()); + if (scene::Layers.active() == null_handle) + tool.status += "\n" + std::string{STR_C("The scenery isn't opened for editing, it won't be saved")}; + WriteLog("Editor: " + tool.status, logtype::generic); +} + +void editor_mode::sweep_apply() +{ + auto &tool{m_sweep}; + auto *sweep{tool.edited}; + if (sweep == nullptr || sweep->m_editorremoved) + return; + auto state{sweep_definition()}; + state.pieces = sweep->definition().pieces; + if (state == sweep->definition()) + return; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = sweep->name(); + snap.sweeps.emplace_back(sweep, sweep->definition()); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + sweep->define(state); + sweep->mark_dirty(); +} + +void editor_mode::sweep_delete(sweep_node &Sweep) +{ + Sweep.m_editorremoved = true; + Sweep.hide(); + Sweep.mark_dirty(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = Sweep.name(); + snap.sweeps_toggled.push_back(&Sweep); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + if (m_sweep.edited == &Sweep) + m_sweep.edited = nullptr; +} + +void editor_mode::sweep_edit(sweep_node &Sweep) +{ + auto &tool{m_sweep}; + tool.edited = &Sweep; + auto const &state{Sweep.definition()}; + tool.settings = state; + tool.settings.height = state.height - kRailTop; + tool.settings.pieces.clear(); + std::snprintf(tool.model, sizeof(tool.model), "%s", state.model.c_str()); + std::snprintf(tool.skin, sizeof(tool.skin), "%s", state.skin.c_str()); + tool.axis = state.along_x ? 2 : 1; + tool.measured.clear(); + tool.parameters_for.clear(); + tool.parameters.clear(); + for (auto const &value : state.parameters) + { + tool.parameters.emplace_back(); + std::snprintf(tool.parameters.back().data(), tool.parameters.back().size(), "%s", value.c_str()); + } + tool.parameters_for = state.model; +} + +void editor_mode::restore_sweeps(EditorSnapshot &Snapshot) +{ + for (auto &entry : Snapshot.sweeps) + { + auto current{entry.first->definition()}; + entry.first->define(entry.second); + entry.first->mark_dirty(); + entry.second = std::move(current); + } + for (auto *sweep : Snapshot.sweeps_toggled) + { + sweep->m_editorremoved = false == sweep->m_editorremoved; + if (sweep->m_editorremoved) + sweep->hide(); + else + sweep->show(); + sweep->mark_dirty(); + if (m_sweep.edited == sweep && sweep->m_editorremoved) + m_sweep.edited = nullptr; + } +} + +std::vector editor_mode::sweeps_near(TTrack const &Track) const +{ + std::vector result; + std::vector points; + for (auto const &path : Track.m_paths) + { + bezier const curve{path}; + for (int k = 0; k <= 8; ++k) + points.push_back(curve.point(k / 8.0)); + } + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->m_editorremoved) + continue; + auto const &definition{sweep->definition()}; + bool close{false}; + for (auto const &piece : definition.pieces) + { + bezier const curve{piece}; + for (int k = 0; k <= 8 && false == close; ++k) + { + auto const point{curve.point(k / 8.0)}; + close = std::any_of(points.begin(), points.end(), [&](glm::dvec3 const &Other) { return plan_distance(point, Other) < kSweepNear; }); + } + if (close) + break; + } + if (close) + result.push_back(sweep); + } + return result; +} + +bool editor_mode::sweep_press() +{ + return sweep_press_at(Global.pCamera.Pos + GfxRenderer->Mouse_Position(), false); +} + +bool editor_mode::sweep_grabs(glm::dvec3 const &Point) +{ + auto &tool{m_sweep}; + if (false == tool.open || tool.model[0] == '\0') + return false; + sweep_curve(); + if (tool.outline.size() < 2) + return false; + sweep_measure(); + if (tool.model_length <= 0.0) + return false; + double lateral; + sweep_station(Point, lateral); + return std::abs(lateral) <= kSweepGrab; +} + +bool editor_mode::sweep_press_at(glm::dvec3 const &Point, bool const Anywhere) +{ + auto &tool{m_sweep}; + sweep_curve(); + if (false == tool.open || tool.outline.size() < 2 || tool.model[0] == '\0') + return false; + sweep_measure(); + if (tool.model_length <= 0.0) + return false; + auto *track{selected_track()}; + if (track != nullptr && track->eType == tt_Switch && track->m_paths.size() > 1 && tool.curve_pieces.size() == 1) + { + auto nearest{0}; + auto best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + bezier const curve{track->m_paths[i]}; + for (int k = 0; k <= 32; ++k) + if (auto const distance{plan_distance(curve.point(k / 32.0), Point)}; distance < best) + { + best = distance; + nearest = i; + } + } + tool.curve = {track->m_paths[nearest]}; + tool.curve_pieces = {{track, nearest, true}}; + sweep_outline(); + } + double lateral; + auto const station{sweep_station(Point, lateral)}; + // a press away from the track is left to the selection of the tracks + if (false == Anywhere && std::abs(lateral) > kSweepGrab) + return false; + // the press tells the side, the distance from the track is the one set + auto const distance{std::abs(tool.settings.lateral) > 1e-6 ? std::abs(tool.settings.lateral) : std::round(std::abs(lateral) / 0.05) * 0.05}; + tool.settings.lateral = lateral >= 0.0 ? distance : -distance; + if (tool.settings.side_anchor == 0) + tool.settings.side_anchor = 1; + tool.anchor = std::round(station * 10.0) / 10.0; + tool.settings.from = tool.settings.to = tool.anchor; + tool.dragging = true; + return true; +} + +void editor_mode::sweep_drag() +{ + if (m_sweep.dragging) + sweep_drag_at(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); +} + +void editor_mode::sweep_drag_at(glm::dvec3 const &Point) +{ + auto &tool{m_sweep}; + if (false == tool.dragging) + return; + double lateral; + for (int i = 0; i < 4; ++i) + { + auto const raw{sweep_station(Point, lateral)}; + if (raw >= tool.curve_length - 0.5 && sweep_extend(true, Point)) + continue; + if (raw <= 0.5 && sweep_extend(false, Point)) + continue; + break; + } + auto const station{std::round(sweep_station(Point, lateral) * 10.0) / 10.0}; + tool.settings.from = std::min(tool.anchor, station); + tool.settings.to = std::max(tool.anchor, station); +} + +void editor_mode::sweep_release() +{ + auto &tool{m_sweep}; + if (false == tool.dragging) + return; + tool.dragging = false; + // a click lays one copy as long as the original from the place clicked, a drag the stretch dragged over + if (tool.settings.to - tool.settings.from < 1.0) + { + tool.length_mode = 0; + tool.settings.from = tool.anchor; + } + else + { + tool.length_mode = 1; + tool.length = tool.settings.to - tool.settings.from; + } + if (tool.edited == nullptr) + { + sweep_create(); + return; + } + // the model being edited is laid again along the line, over the stretch marked + auto state{sweep_definition()}; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = tool.edited->name(); + snap.sweeps.emplace_back(tool.edited, tool.edited->definition()); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + tool.edited->define(state); + tool.edited->mark_dirty(); + if (auto *track{selected_track()}) + sweeps_captured(*track); + tool.status = format(STR_C("%s laid along %.1f m, Ctrl+Z takes it back"), tool.model, tool.edited->end() - tool.edited->start()); +} + +void editor_mode::sweeps_captured(TTrack const &Track) +{ + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->m_editorremoved) + continue; + auto const &pieces{sweep->definition().pieces}; + for (std::size_t i = 0; i < pieces.size(); ++i) + for (int path = 0; path < static_cast(Track.m_paths.size()); ++path) + { + bool const along{same_piece(pieces[i], Track.m_paths[path])}; + bool const against{false == along && same_piece(pieces[i], reversed(Track.m_paths[path]))}; + if (false == along && false == against) + continue; + if (std::none_of(m_sweep_links.begin(), m_sweep_links.end(), [&](sweep_link const &Link) { return Link.sweep == sweep && Link.piece == i; })) + m_sweep_links.push_back({sweep, i, const_cast(&Track), path, against}); + } + } +} + +void editor_mode::sweeps_committed(std::vector const &Tracks) +{ + std::vector changed; + std::vector> states; + for (auto const &link : m_sweep_links) + { + if (link.track == nullptr || link.track->m_editorremoved || link.sweep->m_editorremoved || std::find(Tracks.begin(), Tracks.end(), link.track) == Tracks.end() || + link.path >= static_cast(link.track->m_paths.size())) + continue; + auto entry{std::find_if(states.begin(), states.end(), [&](auto const &State) { return State.first == link.sweep; })}; + if (entry == states.end()) + { + states.emplace_back(link.sweep, link.sweep->definition()); + entry = std::prev(states.end()); + } + if (link.piece >= entry->second.pieces.size()) + continue; + auto const &path{link.track->m_paths[link.path]}; + entry->second.pieces[link.piece] = link.reversed ? reversed(path) : path; + } + for (auto &entry : states) + { + if (entry.second == entry.first->definition()) + continue; + entry.first->define(entry.second); + entry.first->mark_dirty(); + } + for (auto &entry : states) + { + auto *sweep{entry.first}; + if (false == sweep->bridge()) + continue; + sweep->mark_dirty(); + m_sweep_links.erase(std::remove_if(m_sweep_links.begin(), m_sweep_links.end(), [&](sweep_link const &Link) { return Link.sweep == sweep; }), m_sweep_links.end()); + auto const &pieces{sweep->definition().pieces}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (std::size_t i = 0; i < pieces.size(); ++i) + for (int path = 0; path < static_cast(track->m_paths.size()); ++path) + { + auto const along{same_piece(pieces[i], track->m_paths[path])}; + if (along || same_piece(pieces[i], reversed(track->m_paths[path]))) + m_sweep_links.push_back({sweep, i, track, path, false == along}); + } + } + } +} + +void editor_mode::sweeps_split(TTrack &Original, TTrack &Created) +{ + if (Created.m_paths.empty()) + return; + std::vector added; + for (auto &link : m_sweep_links) + { + if (link.track != &Original || link.path != 0) + continue; + auto state{link.sweep->definition()}; + if (link.piece >= state.pieces.size()) + continue; + // the second part goes after the first one along the curve, or ahead of it on a curve laid against the path + auto const at{link.piece + (link.reversed ? 0 : 1)}; + state.pieces.insert(state.pieces.begin() + at, link.reversed ? reversed(Created.m_paths.front()) : Created.m_paths.front()); + for (auto &other : m_sweep_links) + if (other.sweep == link.sweep && other.piece >= at && &other != &link) + ++other.piece; + if (link.reversed) + ++link.piece; + added.push_back({link.sweep, at, &Created, 0, link.reversed}); + link.sweep->define(state); + } + m_sweep_links.insert(m_sweep_links.end(), added.begin(), added.end()); +} + +void editor_mode::render_sweep_ui() +{ + auto &tool{m_sweep}; + sweep_curve(); + sweep_scan_models(); + ImGui::TextDisabled("%s", STR_C("A model bent to follow the track, or repeated along it: platforms, walls, fences, barriers, lamps")); + + bool changed{false}; + auto const edited = [&](bool const Changed) { changed |= Changed; }; + + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::InputTextWithHint("##sweepmodel", STR_C("model, e.g. peronowe/1k96w200m"), tool.model, sizeof(tool.model))) + changed = true; + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##sweepsearch", STR_C("search the models"), tool.search, sizeof(tool.search)); + std::string const text{lower(tool.search)}; + if (false == text.empty()) + { + ImGui::BeginChild("##sweepmodels", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 7.0f + 6.0f), true); + std::size_t listed{0}; + for (auto const &model : tool.models) + { + if (lower(model).find(text) == std::string::npos) + continue; + if (++listed > kSweepModelList) + break; + if (ImGui::Selectable(model.c_str(), model == tool.model)) + { + std::snprintf(tool.model, sizeof(tool.model), "%s", model.c_str()); + changed = true; + } + } + ImGui::EndChild(); + } + // a template takes its parameters, the ones which place it are zeros: it's laid out in its own space + std::string const model{tool.model}; + bool const istemplate{model.size() > 4 && lower(model.substr(model.size() - 4)) == ".inc"}; + if (istemplate && model != tool.parameters_for) + { + tool.parameters_for = model; + include_info info; + std::string error; + editor_includes::load(model, info, error); + editor_includes::suggest(model, info); + auto const count{editor_includes::parameter_count(model)}; + tool.parameters.assign(static_cast(count), {}); + tool.parameter_labels.assign(static_cast(count), {}); + tool.parameter_placement.assign(static_cast(count), false); + tool.parameter_choices.assign(static_cast(count), {}); + // a parameter which makes a part of the name of a texture gets the variants of that texture to choose from + { + std::ifstream file(Global.asCurrentSceneryPath + model); + std::stringstream content; + content << file.rdbuf(); + std::string word; + while (content >> word) + { + auto const open{word.find("(p")}; + auto const close{open != std::string::npos ? word.find(')', open) : std::string::npos}; + if (close == std::string::npos || word.find('/') == std::string::npos || word.find('/') > open) + continue; + auto const id{std::atoi(word.substr(open + 2, close - open - 2).c_str())}; + if (id < 1 || id > count || false == tool.parameter_choices[id - 1].empty()) + continue; + auto const prefix{lower(word.substr(0, open))}; + auto suffix{word.substr(close + 1)}; + suffix = suffix.substr(0, suffix.find(':')); + auto const slash{prefix.rfind('/')}; + auto const folder{std::string{"textures/"} + prefix.substr(0, slash)}; + auto const start{prefix.substr(slash + 1)}; + std::set variants; + std::error_code error; + for (auto const &entry : std::filesystem::directory_iterator(folder, error)) + { + auto const stem{lower(entry.path().stem().string())}; + if (stem.size() > start.size() + suffix.size() && stem.compare(0, start.size(), start) == 0 && (suffix.empty() || stem.compare(stem.size() - suffix.size(), suffix.size(), lower(suffix)) == 0)) + variants.insert(stem.substr(start.size(), stem.size() - start.size() - suffix.size())); + } + tool.parameter_choices[id - 1].assign(variants.begin(), variants.end()); + } + } + for (int id = 1; id <= count; ++id) + { + auto const ¶meter{info.parameter(id)}; + auto const placement{parameter.role.rfind("pos.", 0) == 0 || parameter.role.rfind("rot.", 0) == 0}; + auto const &choices{tool.parameter_choices[id - 1]}; + auto const value{placement ? std::string{"0"} : false == parameter.value.empty() ? parameter.value : choices.empty() ? std::string{"none"} : choices.front()}; + std::snprintf(tool.parameters[id - 1].data(), tool.parameters[id - 1].size(), "%s", value.c_str()); + tool.parameter_labels[id - 1] = parameter.label.empty() ? "p" + std::to_string(id) + (parameter.role != "free" ? " (" + parameter.role + ")" : std::string{}) : parameter.label; + tool.parameter_placement[id - 1] = placement; + } + tool.measured.clear(); + changed = true; + } + else if (false == istemplate && false == tool.parameters.empty()) + { + tool.parameters.clear(); + tool.parameters_for.clear(); + } + if (false == tool.parameters.empty() && ImGui::TreeNodeEx(STR_C("Parameters of the template"), ImGuiTreeNodeFlags_DefaultOpen)) + { + for (std::size_t i = 0; i < tool.parameters.size(); ++i) + { + ImGui::PushID(static_cast(i)); + ImGui::SetNextItemWidth(160.0f); + auto const label{i < tool.parameter_labels.size() ? tool.parameter_labels[i] : "p" + std::to_string(i + 1)}; + auto const *choices{i < tool.parameter_choices.size() && false == tool.parameter_choices[i].empty() ? &tool.parameter_choices[i] : nullptr}; + if (choices != nullptr && ImGui::BeginCombo(label.c_str(), tool.parameters[i].data())) + { + for (auto const &choice : *choices) + if (ImGui::Selectable(choice.c_str(), choice == tool.parameters[i].data())) + { + std::snprintf(tool.parameters[i].data(), tool.parameters[i].size(), "%s", choice.c_str()); + tool.measured.clear(); + changed = true; + } + ImGui::EndCombo(); + } + else if (choices == nullptr) + { + ImGui::InputText(label.c_str(), tool.parameters[i].data(), tool.parameters[i].size()); + if (ImGui::IsItemDeactivatedAfterEdit()) + { + tool.measured.clear(); + changed = true; + } + } + ImGui::PopID(); + } + ImGui::TextDisabled("%s", STR_C("The location and the rotation are zeros, the template is laid out along the track")); + ImGui::TreePop(); + } + sweep_measure(); + if (tool.model_length > 0.0) + ImGui::TextDisabled(STR_C("The model is %.2f m long along the track"), tool.model_length); + else if (tool.model[0] != '\0') + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), "%s", STR_C("The model has no triangles to lay, or it can't be loaded")); + + auto &settings{tool.settings}; + int mode{settings.bend ? 0 : 1}; + edited(ImGui::RadioButton(STR_C("Bent along the track"), &mode, 0)); + ImGui::SameLine(); + edited(ImGui::RadioButton(STR_C("Repeated"), &mode, 1)); + settings.bend = mode == 0; + if (false == settings.bend) + { + ImGui::SameLine(); + ImGui::SetNextItemWidth(80.0f); + edited(ImGui::InputDouble(STR_C("every (m, 0: its length)"), &settings.step, 0.0, 0.0, "%.2f")); + settings.step = std::max(0.0, settings.step); + } + ImGui::TextDisabled("%s", STR_C("Platform edge:")); + for (auto const height : {0.55, 0.76, 0.96}) + { + ImGui::SameLine(); + if (ImGui::SmallButton(format("%.2f m##platform", height).c_str())) + { + platform_edge(settings, tool.model, 0.0); + settings.height = height; + settings.face = true; + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(STR_C("The edge of the platform %.3f m from the axis of the track, its top this high over the rail top"), EditorSettings.platform_edge()); + } + if (platform_distance() && settings.platform) + { + settings.lateral = std::copysign(EditorSettings.platform_edge(), settings.lateral == 0.0 ? 1.0 : settings.lateral); + changed = true; + } + ImGui::TextDisabled(STR_C("%.2f m %s of the axis, %.2f m over the rail top"), std::abs(settings.lateral), settings.lateral >= 0.0 ? STR_C("right") : STR_C("left"), settings.height); + if (false == tool.curve.empty() && tool.model_length > 0.0 && ImGui::Button(STR_C("Lay it along the selected path"))) + sweep_create(); + + changed = false; + if (ImGui::CollapsingHeader(STR_C("More settings"))) + { + ImGui::SetNextItemWidth(200.0f); + edited(ImGui::InputText(STR_C("Skin"), tool.skin, sizeof(tool.skin), ImGuiInputTextFlags_EnterReturnsTrue)); + edited(ImGui::IsItemDeactivatedAfterEdit()); + ImGui::PushItemWidth(90.0f); + edited(ImGui::InputDouble(STR_C("Sideways (m, + right)"), &settings.lateral, 0.0, 0.0, "%.3f")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(150.0f); + char const *sides[] = {STR_C("model origin"), STR_C("side facing the track"), STR_C("far side"), STR_C("middle")}; + edited(ImGui::Combo("##sweepsideanchor", &settings.side_anchor, sides, IM_ARRAYSIZE(sides))); + edited(ImGui::InputDouble(STR_C("Up (m, from the rail top)"), &settings.height, 0.0, 0.0, "%.3f")); + ImGui::SameLine(); + ImGui::SetNextItemWidth(150.0f); + char const *heights[] = {STR_C("model origin"), STR_C("bottom"), STR_C("top"), STR_C("top of the edge by the track")}; + edited(ImGui::Combo("##sweepheightanchor", &settings.height_anchor, heights, IM_ARRAYSIZE(heights))); + if (settings.bend) + { + edited(ImGui::InputDouble(STR_C("Copy length (m, 0: the model's)"), &settings.step, 0.0, 0.0, "%.2f")); + settings.step = std::max(0.0, settings.step); + } + ImGui::PopItemWidth(); + char const *axes[] = {"auto", "z", "x"}; + ImGui::SetNextItemWidth(70.0f); + if (ImGui::Combo(STR_C("Axis of the model along the track"), &tool.axis, axes, IM_ARRAYSIZE(axes))) + { + tool.measured.clear(); + changed = true; + } + edited(ImGui::Checkbox(STR_C("Edge to the track"), &settings.face)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The model is turned so its -x side faces the track, the way the platforms and the walls of the scenery are made")); + ImGui::SameLine(); + edited(ImGui::Checkbox(STR_C("Turned around"), &settings.flip)); + ImGui::SameLine(); + edited(ImGui::Checkbox(STR_C("Mirrored"), &settings.mirror)); + ImGui::SameLine(); + edited(ImGui::Checkbox(STR_C("Leans with the cant"), &settings.tilt)); + edited(ImGui::Checkbox(STR_C("Set back by the widening of the structure gauge in the curves"), &settings.widen)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\n" + "more in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\n" + "it begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check")); + if (ImGui::Checkbox(STR_C("Platform"), &settings.platform)) + { + if (settings.platform) + platform_edge(settings, tool.model, 0.0); + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\n" + "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" + "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5")); + ImGui::TextDisabled("%s", STR_C("Length")); + ImGui::SameLine(); + edited(ImGui::RadioButton(STR_C("as the original"), &tool.length_mode, 0)); + ImGui::SameLine(); + edited(ImGui::RadioButton(STR_C("given"), &tool.length_mode, 1)); + ImGui::SameLine(); + edited(ImGui::RadioButton(STR_C("the whole stretch"), &tool.length_mode, 2)); + ImGui::PushItemWidth(90.0f); + edited(ImGui::InputDouble(STR_C("From (m)"), &settings.from, 0.0, 0.0, "%.1f")); + ImGui::SameLine(); + if (tool.length_mode == 1) + edited(ImGui::InputDouble(STR_C("Length (m)"), &tool.length, 0.0, 0.0, "%.1f")); + else if (tool.length_mode == 2) + edited(ImGui::InputDouble(STR_C("To (m, -1: the end)"), &settings.to, 0.0, 0.0, "%.1f")); + ImGui::PopItemWidth(); + settings.from = std::max(0.0, settings.from); + if (tool.model_length > 0.0) + { + auto const state{sweep_definition()}; + auto const end{state.to < 0.0 ? tool.curve_length : std::min(state.to, tool.curve_length)}; + auto const stretch{std::max(0.0, end - state.from)}; + auto const own{settings.step > 0.01 ? settings.step : tool.model_length}; + auto const copies{settings.bend ? std::max(1, static_cast(std::round(stretch / own))) : static_cast(std::floor(stretch / own + 1e-6)) + 1}; + if (settings.bend) + ImGui::TextDisabled(STR_C("%.1f m: %d copies of %.2f m (the original %.2f m)"), stretch, copies, stretch / copies, tool.model_length); + else + ImGui::TextDisabled(STR_C("%.1f m: %d copies every %.2f m"), stretch, copies, own); + } + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_sweep_overlay() const +{ + auto const &tool{m_sweep}; + if (false == tool.open || tool.outline.size() < 2) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const &settings{tool.settings}; + auto const from{std::max(0.0, settings.from)}; + auto const to{settings.to < 0.0 ? tool.curve_length : std::min(settings.to, tool.curve_length)}; + glm::dvec3 previous{0.0}; + bool started{false}; + for (std::size_t i = 1; i < tool.outline.size(); ++i) + { + if (tool.stations[i] < from || tool.stations[i - 1] > to) + continue; + auto const direction{plan_of(tool.outline[i] - tool.outline[i - 1])}; + if (glm::length(direction) < 1e-9) + continue; + auto const unit{glm::normalize(direction)}; + glm::dvec3 const left{-unit.y, 0.0, unit.x}; + auto const point{tool.outline[i] + left * settings.lateral + glm::dvec3{0.0, settings.height + kRailTop, 0.0}}; + if (started) + projection.line(drawlist, previous, point, IM_COL32(255, 160, 40, 230), 3.0f); + else + { + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddCircleFilled(screen, 5.0f, IM_COL32(255, 160, 40, 230)); + } + previous = point; + started = true; + } + ImVec2 screen; + if (started && projection.project(previous, screen)) + drawlist->AddCircle(screen, 6.0f, IM_COL32(255, 160, 40, 230), 12, 2.0f); +} + +namespace +{ + +double const kBendReach{60.0}; +double const kBendMargin{1.0}; + +glm::dmat3 axes_matrix(sweep_node const &Sweep) +{ + return glm::dmat3(Sweep.axes({1.0, 0.0, 0.0}), Sweep.axes({0.0, 1.0, 0.0}), Sweep.axes({0.0, 0.0, 1.0})); +} + +double along_anchor(sweep_node const &Sweep, int const Anchor, glm::dvec3 const &Low, glm::dvec3 const &High, glm::dvec3 const &Point) +{ + auto const &state{Sweep.definition()}; + switch (Anchor) + { + case 0: return Low.x; + case 2: return 0.5 * (Low.x + High.x); + case 3: return High.x; + case 4: return Sweep.axes(state.scale * Point).x; + default: return 0.0; + } +} + +int copies_of(sweep_node const &Sweep, double const Own) +{ + auto const &state{Sweep.definition()}; + auto const stretch{Sweep.distance_at(Sweep.end()) - Sweep.distance_at(Sweep.start())}; + auto const length{state.step > 0.01 ? state.step : Own}; + if (length < 1e-3) + return 1; + return state.bend ? std::max(1, static_cast(std::round(stretch / length))) : static_cast(std::floor(stretch / length + 1e-6)) + 1; +} + +} // namespace + +bool editor_mode::bend_source_of_selection(bend_source &Source) const +{ + Source = bend_source{}; + if (auto *model = dynamic_cast(m_node)) + { + if (model->from_template() || false == scene::Layers.editable(model)) + return false; + std::string text; + model->export_as_text(text); + std::istringstream words(text); + std::string token; + double number; + while (words >> token && token != "model") + ; + words >> number >> number >> number >> number >> Source.file; + words >> std::ws; + if (words.peek() == '"') + { + words.get(); + std::getline(words, Source.skin, '"'); + } + else + words >> Source.skin; + if (Source.file.empty() || Source.file == "none") + return false; + auto const angles{glm::dvec3(model->Angles())}; + Source.transform = glm::translate(glm::dmat4(1.0), model->location()); + Source.transform = glm::rotate(Source.transform, glm::radians(angles.y), glm::dvec3{0.0, 1.0, 0.0}); + Source.transform = glm::rotate(Source.transform, glm::radians(angles.x), glm::dvec3{1.0, 0.0, 0.0}); + Source.transform = glm::rotate(Source.transform, glm::radians(angles.z), glm::dvec3{0.0, 0.0, 1.0}); + Source.scale = glm::dvec3(model->Scale()); + Source.layer = model->layer(); + Source.model = model; + return true; + } + if (m_instance == 0 || false == m_include.placement || false == m_include.issue.empty() || false == scene::Layers.removable(m_instance)) + return false; + auto info{m_include.info}; + editor_includes::suggest(m_include.target, info); + glm::dvec3 position{0.0}, rotation{0.0}; + Source.parameters = m_include.values; + for (std::size_t i = 0; i < Source.parameters.size(); ++i) + { + auto const &role{info.parameter(static_cast(i) + 1).role}; + if (role.rfind("pos.", 0) != 0 && role.rfind("rot.", 0) != 0) + continue; + auto const value{std::atof(Source.parameters[i].c_str())}; + auto const axis{role.back() == 'x' ? 0 : role.back() == 'y' ? 1 : 2}; + (role[0] == 'p' ? position : rotation)[axis] = value; + Source.parameters[i] = "0"; + } + auto const &included{scene::Layers.instance(m_instance)}; + Source.file = m_include.target; + Source.transform = glm::translate(glm::dmat4(1.0), included.context.offset + position); + Source.transform = glm::rotate(Source.transform, glm::radians(rotation.y), glm::dvec3{0.0, 1.0, 0.0}); + Source.transform = glm::rotate(Source.transform, glm::radians(rotation.x), glm::dvec3{1.0, 0.0, 0.0}); + Source.transform = glm::rotate(Source.transform, glm::radians(rotation.z), glm::dvec3{0.0, 0.0, 1.0}); + Source.layer = included.layer; + Source.instance = m_instance; + return true; +} + +void editor_mode::bend_find_tracks(glm::dvec3 const &Point, void const *Key) +{ + auto &tool{m_bend}; + if (Key == tool.tracks_for && glm::distance(Point, tool.tracks_at) < 1.0) + return; + tool.tracks_for = Key; + tool.tracks_at = Point; + tool.tracks.clear(); + tool.track = 0; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType == tt_Table || track->m_paths.empty()) + continue; + auto best{std::numeric_limits::max()}; + for (auto const &path : track->m_paths) + { + if (plan_distance(path.points[segment_data::point::start], Point) > 2000.0 + kBendReach) + continue; + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / 2.0), 8, 400)}; + for (int k = 0; k <= count; ++k) + best = std::min(best, plan_distance(curve.point(static_cast(k) / count), Point)); + } + if (best <= kBendReach) + tool.tracks.emplace_back(track, best); + } + std::sort(tool.tracks.begin(), tool.tracks.end(), [](auto const &A, auto const &B) { return A.second < B.second; }); + if (tool.tracks.size() > 12) + tool.tracks.resize(12); +} + +bool editor_mode::bend_definition(bend_source const &Source, TTrack &Track, sweep_node::state &State, std::vector &Tracks) +{ + auto &tool{m_bend}; + State = sweep_node::state{}; + State.model = Source.file; + State.skin = Source.skin.empty() ? "none" : Source.skin; + State.parameters = Source.parameters; + State.scale = Source.scale; + State.face = false; + State.platform = State.widen = platform_model(Source.file); + State.side_anchor = tool.side_anchor; + State.height_anchor = tool.height_anchor; + State.point = tool.point; + auto const origin{glm::dvec3(Source.transform[3])}; + int path{0}; + { + auto best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + bezier const curve{Track.m_paths[i]}; + for (int k = 0; k <= 64; ++k) + if (auto const distance{plan_distance(curve.point(k / 64.0), origin)}; distance < best) + { + best = distance; + path = i; + } + } + } + std::vector records{{&Track, path, true}}; + State.pieces = {Track.m_paths[path]}; + scene::node_data data; + sweep_node probe{data}; + auto const define = [&]() { probe.define(State); }; + define(); + double lateral; + auto const forward{flat(probe.frame_at(probe.project(origin, lateral)).forward)}; + glm::dmat3 const rotation{Source.transform}; + auto const ex{flat(rotation * glm::dvec3{1.0, 0.0, 0.0})}; + auto const ez{flat(rotation * glm::dvec3{0.0, 0.0, 1.0})}; + State.along_x = tool.axis == 2 || (tool.axis == 0 && std::abs(glm::dot(ex, forward)) > std::abs(glm::dot(ez, forward))); + auto const direction{State.along_x ? ex : ez}; + State.flip = (glm::dot(direction, forward) < 0.0) != tool.turned; + State.lateral = lateral != 0.0 ? lateral : 1e-3; + define(); + glm::dvec3 low, high, shift; + if (false == probe.bounds(low, high, shift)) + { + tool.status = STR("The model has no triangles to bend, or it can't be loaded"); + return false; + } + if (State.platform && probe.edge_far()) + { + State.flip = false == State.flip; + define(); + probe.bounds(low, high, shift); + } + auto const inverse{glm::inverse(axes_matrix(probe))}; + auto const world = [&](glm::dvec3 const &Local) { return glm::dvec3(Source.transform * glm::dvec4(inverse * Local, 1.0)); }; + auto const ends{std::array{world({low.x, 0.0, 0.0}), world({high.x, 0.0, 0.0})}}; + auto const beyond = [&](glm::dvec3 const &Point, bool const Atend) { + auto const at{probe.frame_at(Atend ? probe.length() : 0.0)}; + auto const along{glm::dot(glm::dvec3{Point.x - at.position.x, 0.0, Point.z - at.position.z}, flat(at.forward))}; + return Atend ? along > -kBendMargin : along < kBendMargin; + }; + auto const extend = [&](bool const Atend, glm::dvec3 const &Target) { + auto const &edge{Atend ? records.back() : records.front()}; + auto const &edgepiece{Atend ? State.pieces.back() : State.pieces.front()}; + auto const point{Atend ? edgepiece.points[segment_data::point::end] : edgepiece.points[segment_data::point::start]}; + std::optional best; + segment_data bestpiece; + auto bestdistance{std::numeric_limits::max()}; + for (auto const &[other, end] : editor_track::connected_points(*edge.track, point)) + { + if (other == nullptr || other->m_editorremoved || other->eType == tt_Table || false == editor_track::is_supported(*other) || end.path >= static_cast(other->m_paths.size())) + continue; + if (std::any_of(records.begin(), records.end(), [&](sweep_tool::piece const &Piece) { return Piece.track == other && Piece.path == end.path; })) + continue; + auto const startshere{end.kind == editor_track::point_kind::start}; + auto const forward{Atend ? startshere : false == startshere}; + auto const piece{forward ? other->m_paths[end.path] : reversed(other->m_paths[end.path])}; + bezier const curve{piece}; + auto distance{std::numeric_limits::max()}; + for (int k = 0; k <= 32; ++k) + distance = std::min(distance, plan_distance(curve.point(k / 32.0), Target)); + if (distance < bestdistance) + { + bestdistance = distance; + best = sweep_tool::piece{other, end.path, forward}; + bestpiece = piece; + } + } + if (false == best.has_value()) + return false; + if (Atend) + { + records.push_back(*best); + State.pieces.push_back(bestpiece); + } + else + { + records.insert(records.begin(), *best); + State.pieces.insert(State.pieces.begin(), bestpiece); + } + define(); + return true; + }; + for (int i = 0; i < 40 && probe.length() < kSweepRouteReach; ++i) + { + auto grown{false}; + for (auto const &point : ends) + { + if (beyond(point, false) && extend(false, point)) + grown = true; + else if (beyond(point, true) && extend(true, point)) + grown = true; + } + if (false == grown) + break; + } + glm::dvec3 anchor{0.0}; + double station{0.0}; + auto const place = [&]() { + for (int pass = 0; pass < 3; ++pass) + { + define(); + probe.bounds(low, high, shift); + auto const a{along_anchor(probe, tool.along_anchor, low, high, State.point)}; + anchor = world({a, -shift.y, -shift.z}); + double side; + station = probe.project(anchor, side); + for (int k = 0; k < 2; ++k) + { + auto const at{probe.frame_at(station)}; + station += glm::dot(glm::dvec3{anchor.x - at.position.x, 0.0, anchor.z - at.position.z}, flat(at.forward)); + } + auto const at{probe.frame_at(station)}; + side = glm::dot(glm::dvec3{anchor.x - at.position.x, 0.0, anchor.z - at.position.z}, flat(at.left)); + auto const flipped{(side >= 0.0) != (State.lateral >= 0.0)}; + State.lateral = side != 0.0 ? side : 1e-3; + State.height = anchor.y - at.position.y - probe.rise(at); + if (false == flipped) + break; + } + define(); + probe.bounds(low, high, shift); + auto const a{along_anchor(probe, tool.along_anchor, low, high, State.point)}; + auto const first{probe.distance_at(station) - (a - low.x)}; + State.from = probe.station_at(first); + State.to = probe.station_at(first + high.x - low.x); + }; + place(); + // the straight model leaves the track where it curves on, short of the length it takes bent along it + for (int i = 0; i < 40 && probe.length() < kSweepRouteReach; ++i) + { + auto const ahead = [&](bool const Atend) { + auto const at{probe.frame_at(Atend ? probe.length() : 0.0)}; + auto const reach{Atend ? State.to - probe.length() : State.from}; + return at.position + flat(at.forward) * reach; + }; + if ((State.to > probe.length() + 0.01 && extend(true, ahead(true))) || (State.from < -0.01 && extend(false, ahead(false)))) + place(); + else + break; + } + if (State.platform) + { + platform_edge(State, Source.file, kRailTop); + define(); + anchor = bend_marker(probe); + } + if (State.to <= 0.0) + { + tool.status = STR("The model lies before the start of the track"); + return false; + } + tool.marker = anchor; + tool.marker_valid = true; + Tracks.clear(); + for (auto const &record : records) + Tracks.push_back(record.track); + auto const residual{glm::degrees(std::acos(std::clamp(std::abs(glm::dot(direction, forward)), 0.0, 1.0)))}; + tool.status = residual > 1.0 ? format(STR_C("The model stands %.1f deg off the track, bent it'll follow the track"), residual) : std::string{}; + return true; +} + +void editor_mode::bend_selection() +{ + auto &tool{m_bend}; + bend_source source; + if (false == bend_source_of_selection(source) || tool.tracks.empty()) + return; + auto *track{tool.tracks[std::clamp(tool.track, 0, static_cast(tool.tracks.size()) - 1)].first}; + sweep_node::state state; + std::vector tracks; + if (false == bend_definition(source, *track, state, tracks)) + return; + static int counter{0}; + scene::node_data data; + data.type = "sweep"; + data.range_max = -1.0; + data.layer = scene::Layers.accepts(source.layer) ? source.layer : scene::Layers.active(); + do + { + data.name = "sweep_" + std::to_string(++counter); + } while (simulation::Sweeps.find(data.name) != nullptr); + auto *sweep{new sweep_node(data)}; + sweep->define(state); + sweep->mark_dirty(); + simulation::Sweeps.insert(sweep); + scene::Layers.count(sweep->layer(), scene::layer_item::model); + sweep->show(); + for (auto *path : tracks) + if (path != nullptr && false == path->m_editorremoved) + sweeps_captured(*path); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Bend; + snap.sweeps_toggled.push_back(sweep); + snap.layer = source.layer; + if (source.model != nullptr) + { + auto *model{source.model}; + model->export_as_text(snap.serialized); + snap.node_name = model->name(); + snap.position = model->location(); + snap.rotation = model->Angles(); + snap.scale = model->Scale(); + snap.uuid = model->uuid; + nullify_history_pointers(model); + remove_from_hierarchy(model); + m_node = nullptr; + m_dragging = false; + ui()->set_node(nullptr); + simulation::State.delete_model(model); + } + else + { + snap.instance = source.instance; + snap.node_name = source.file; + scene::Layers.removed(source.instance, true); + select_include(0); + } + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + tool.edited = sweep; + tool.signature.clear(); + tool.status = format(STR_C("%s bent along %.1f m of the track, Ctrl+Z takes it back"), source.file.c_str(), sweep->distance_at(sweep->end()) - sweep->distance_at(sweep->start())); + WriteLog("Editor: " + tool.status, logtype::generic); +} + +void editor_mode::restore_bend(EditorSnapshot &Snapshot, bool const Undo) +{ + if (Snapshot.sweeps_toggled.empty()) + return; + auto *sweep{Snapshot.sweeps_toggled.front()}; + sweep->m_editorremoved = Undo; + if (Undo) + sweep->hide(); + else + sweep->show(); + sweep->mark_dirty(); + if (Snapshot.instance != 0) + { + if (scene::Layers.tracked(Snapshot.instance) && false == scene::Layers.instance(Snapshot.instance).dead) + scene::Layers.removed(Snapshot.instance, false == Undo); + select_include(0); + } + else if (Undo) + { + auto *created{simulation::State.create_model(Snapshot.serialized, Snapshot.node_name, Snapshot.position)}; + if (created != nullptr) + { + scene::Layers.move(created, Snapshot.layer); + created->location(Snapshot.position); + created->Angles(Snapshot.rotation); + created->Scale(Snapshot.scale); + created->uuid = Snapshot.uuid; + add_to_hierarchy(created); + Snapshot.node_ptr = created; + m_node = created; + ui()->set_node(m_node); + } + } + else if (auto *model{dynamic_cast(find_node_by_any(Snapshot.node_ptr, Snapshot.uuid.to_string(), Snapshot.node_name))}) + { + nullify_history_pointers(model); + remove_from_hierarchy(model); + simulation::State.delete_model(model); + Snapshot.node_ptr = nullptr; + m_node = nullptr; + ui()->set_node(nullptr); + } + m_bend.edited = Undo ? nullptr : sweep; + m_bend.signature.clear(); +} + +void editor_mode::bend_edit(sweep_node::state const &State) +{ + auto *sweep{m_bend.edited}; + if (sweep == nullptr || sweep->m_editorremoved || State == sweep->definition()) + return; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = sweep->name(); + snap.sweeps.emplace_back(sweep, sweep->definition()); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + sweep->define(State); + sweep->mark_dirty(); +} + +double editor_mode::bend_fixed(sweep_node const &Sweep, glm::dvec3 const &Point) const +{ + auto const from{Sweep.distance_at(Sweep.start())}; + auto const stretch{Sweep.distance_at(Sweep.end()) - from}; + glm::dvec3 low, high, shift; + if (false == Sweep.bounds(low, high, shift)) + return from; + switch (m_bend.along_anchor) + { + case 0: return from; + case 2: return from + 0.5 * stretch; + case 3: return from + stretch; + default: break; + } + auto const own{high.x - low.x}; + auto const scale{Sweep.definition().bend && own > 1e-3 ? stretch / copies_of(Sweep, own) / own : 1.0}; + return from + (along_anchor(Sweep, m_bend.along_anchor, low, high, Point) - low.x) * scale; +} + +void editor_mode::bend_stretch(sweep_node const &Sweep, double const Fixed, double const Length, glm::dvec3 const &Point, sweep_node::state &State) const +{ + glm::dvec3 low, high, shift; + auto first{Fixed}; + switch (m_bend.along_anchor) + { + case 0: break; + case 2: first = Fixed - 0.5 * Length; break; + case 3: first = Fixed - Length; break; + default: + if (Sweep.bounds(low, high, shift)) + { + auto const own{high.x - low.x}; + auto const step{State.step > 0.01 ? State.step : own}; + auto const copies{State.bend && step > 1e-3 ? std::max(1, static_cast(std::round(Length / step))) : 1}; + auto const scale{State.bend && own > 1e-3 ? Length / copies / own : 1.0}; + first = Fixed - (along_anchor(Sweep, m_bend.along_anchor, low, high, Point) - low.x) * scale; + } + break; + } + State.from = Sweep.station_at(first); + State.to = Sweep.station_at(first + Length); +} + +void editor_mode::bend_reanchor(sweep_node::state &State, int const Side, int const Height, glm::dvec3 const &Point) const +{ + scene::node_data data; + sweep_node probe{data}; + probe.define(State); + glm::dvec3 low, high, before; + if (false == probe.bounds(low, high, before)) + return; + auto const stretch{probe.distance_at(probe.end()) - probe.distance_at(probe.start())}; + auto const fixed{probe.station_at(bend_fixed(probe, Point))}; + auto next{State}; + next.side_anchor = Side; + next.height_anchor = Height; + next.point = Point; + probe.define(next); + glm::dvec3 after; + if (false == probe.bounds(low, high, after)) + return; + next.lateral = State.lateral + before.y - after.y; + next.height = State.height + before.z - after.z; + probe.define(next); + auto const whole{State.to < 0.0 && State.from == 0.0}; + bend_stretch(probe, probe.distance_at(fixed), stretch, Point, next); + if (whole) + { + next.from = 0.0; + next.to = -1.0; + } + State = next; +} + +glm::dvec3 editor_mode::bend_marker(sweep_node const &Sweep) const +{ + glm::dvec3 low, high, shift; + if (false == Sweep.bounds(low, high, shift)) + return glm::dvec3{0.0}; + auto const &state{Sweep.definition()}; + auto const from{Sweep.distance_at(Sweep.start())}; + auto const stretch{Sweep.distance_at(Sweep.end()) - from}; + auto const own{high.x - low.x}; + auto const copies{copies_of(Sweep, own)}; + auto const scale{state.bend && own > 1e-3 ? stretch / copies / own : 1.0}; + auto const at{Sweep.frame_at(Sweep.station_at(bend_fixed(Sweep, state.point)))}; + return at.position + at.left * (state.lateral + Sweep.setback(at)) + glm::dvec3{0.0, state.height + Sweep.rise(at), 0.0}; +} + +void editor_mode::bend_pick(glm::dvec3 const &Point) +{ + auto &tool{m_bend}; + tool.picking = false; + if (auto *sweep{tool.edited}; sweep != nullptr && false == sweep->m_editorremoved) + { + glm::dvec3 low, high, shift; + if (false == sweep->bounds(low, high, shift)) + return; + auto const &state{sweep->definition()}; + double lateral; + auto const station{sweep->project(Point, lateral)}; + auto const at{sweep->frame_at(station)}; + auto const from{sweep->distance_at(sweep->start())}; + auto const stretch{sweep->distance_at(sweep->end()) - from}; + auto const own{high.x - low.x}; + auto const copies{copies_of(*sweep, own)}; + auto const copy{stretch / copies}; + auto const scale{state.bend && own > 1e-3 ? copy / own : 1.0}; + auto const along{std::fmod(std::max(0.0, sweep->distance_at(station) - from), copy)}; + glm::dvec3 const local{low.x + along / scale, lateral - state.lateral - sweep->setback(at) - shift.y, + Point.y - at.position.y - state.height - sweep->rise(at) - shift.z}; + auto const raw{glm::inverse(axes_matrix(*sweep)) * local / state.scale}; + tool.point = raw; + tool.along_anchor = tool.side_anchor = tool.height_anchor = 4; + auto next{state}; + bend_reanchor(next, 4, 4, raw); + bend_edit(next); + } + else + { + bend_source source; + if (false == bend_source_of_selection(source)) + return; + auto const local{glm::dvec3(glm::inverse(source.transform) * glm::dvec4(Point, 1.0))}; + tool.point = local / source.scale; + tool.along_anchor = tool.side_anchor = tool.height_anchor = 4; + tool.signature.clear(); + } + tool.status = format(STR_C("Reference point %.2f %.2f %.2f in the model"), tool.point.x, tool.point.y, tool.point.z); +} + +void editor_mode::render_bend() +{ + auto &tool{m_bend}; + if (tool.edited != nullptr && tool.edited->m_editorremoved) + tool.edited = nullptr; + ImGui::PushID("bend"); + char const *alongs[] = {STR_C("start"), STR_C("model origin"), STR_C("middle"), STR_C("end"), STR_C("picked point")}; + char const *sides[] = {STR_C("model origin"), STR_C("side facing the track"), STR_C("far side"), STR_C("middle"), STR_C("picked point")}; + char const *heights[] = {STR_C("model origin"), STR_C("bottom"), STR_C("top"), STR_C("top of the edge by the track"), STR_C("picked point")}; + auto anchors = [&](bool &Changed) { + ImGui::SetNextItemWidth(170.0f); + Changed |= ImGui::Combo(STR_C("along the track"), &tool.along_anchor, alongs, IM_ARRAYSIZE(alongs)); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Reference point: the model bends about it, the line through it keeps its length, the offsets are its place")); + ImGui::SetNextItemWidth(170.0f); + Changed |= ImGui::Combo(STR_C("across"), &tool.side_anchor, sides, IM_ARRAYSIZE(sides)); + ImGui::SetNextItemWidth(170.0f); + Changed |= ImGui::Combo(STR_C("up"), &tool.height_anchor, heights, IM_ARRAYSIZE(heights)); + if (tool.along_anchor == 4 || tool.side_anchor == 4 || tool.height_anchor == 4) + { + ImGui::SetNextItemWidth(220.0f); + glm::vec3 point{tool.point}; + if (ImGui::DragFloat3(STR_C("point (x, y, z of the model)"), &point.x, 0.01f, 0.0f, 0.0f, "%.3f")) + { + tool.point = glm::dvec3(point); + Changed = true; + } + } + if (ImGui::Button(tool.picking ? STR_C("Click the point on the model...") : STR_C("Pick the point on the model"))) + tool.picking = false == tool.picking; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The next click in the view sets the reference point where it lands on the model")); + }; + + if (auto *sweep{tool.edited}) + { + auto state{sweep->definition()}; + glm::dvec3 low, high, shift; + auto const measured{sweep->bounds(low, high, shift)}; + auto const own{measured ? high.x - low.x : 0.0}; + auto const from{sweep->distance_at(sweep->start())}; + auto const stretch{sweep->distance_at(sweep->end()) - from}; + ImGui::Text("%s %s", sweep->name().c_str(), state.model.c_str()); + if (own > 0.0) + ImGui::TextDisabled(STR_C("%.2f m along the reference line, %d copies (the model is %.2f m)"), stretch, copies_of(*sweep, own), own); + bool changed{false}; + tool.side_anchor = state.side_anchor; + tool.height_anchor = state.height_anchor; + tool.point = state.point; + auto const side{tool.side_anchor}, height{tool.height_anchor}; + auto const point{tool.point}; + bool anchored{false}; + anchors(anchored); + if (anchored && (side != tool.side_anchor || height != tool.height_anchor || point != tool.point)) + { + bend_reanchor(state, tool.side_anchor, tool.height_anchor, tool.point); + changed = true; + } + ImGui::PushItemWidth(110.0f); + changed |= ImGui::InputDouble(STR_C("from the track axis (m, + right)"), &state.lateral, 0.05, 0.5, "%.3f"); + double overhead{state.height - kRailTop}; + if (ImGui::InputDouble(STR_C("over the rail head (m)"), &overhead, 0.01, 0.1, "%.3f")) + { + state.height = overhead + kRailTop; + changed = true; + } + double along{sweep->distance_at(bend_fixed(*sweep, state.point))}; + if (ImGui::InputDouble(STR_C("along the track (m)"), &along, 0.1, 1.0, "%.2f")) + { + bend_stretch(*sweep, along, stretch, state.point, state); + changed = true; + } + double length{stretch}; + if (ImGui::InputDouble(STR_C("length (m)"), &length, 0.5, 5.0, "%.2f") && length > 0.1) + { + bend_stretch(*sweep, bend_fixed(*sweep, state.point), length, state.point, state); + changed = true; + } + ImGui::PopItemWidth(); + if (own > 0.0 && ImGui::SmallButton(STR_C("As long as the model"))) + { + bend_stretch(*sweep, bend_fixed(*sweep, state.point), own, state.point, state); + changed = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Along the whole track"))) + { + state.from = 0.0; + state.to = -1.0; + changed = true; + } + int mode{state.bend ? 0 : 1}; + changed |= ImGui::RadioButton(STR_C("Bent"), &mode, 0); + ImGui::SameLine(); + changed |= ImGui::RadioButton(STR_C("Repeated"), &mode, 1); + state.bend = mode == 0; + ImGui::SameLine(); + ImGui::SetNextItemWidth(80.0f); + changed |= ImGui::InputDouble(STR_C("copy every (m, 0: its length)"), &state.step, 0.0, 0.0, "%.2f"); + state.step = std::max(0.0, state.step); + changed |= ImGui::Checkbox(STR_C("Turned around"), &state.flip); + ImGui::SameLine(); + changed |= ImGui::Checkbox(STR_C("Mirrored"), &state.mirror); + ImGui::SameLine(); + changed |= ImGui::Checkbox(STR_C("Leans with the cant"), &state.tilt); + changed |= ImGui::Checkbox(STR_C("Set back by the widening of the structure gauge in the curves"), &state.widen); + if (ImGui::Checkbox(STR_C("Platform"), &state.platform)) + { + if (state.platform) + { + bend_reanchor(state, 1, 3, state.point); + platform_edge(state, state.model, kRailTop); + } + changed = true; + } + if (state.platform && platform_distance()) + { + state.lateral = std::copysign(EditorSettings.platform_edge(), state.lateral); + changed = true; + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\n" + "Stands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\n" + "higher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5")); + ImGui::TextDisabled("%s", STR_C("In the view: drag the blue cross along the track and aside; Shift: height; Ctrl: only along")); + if (changed) + bend_edit(state); + if (ImGui::Button(STR_C("Done"))) + tool.edited = nullptr; + ImGui::SameLine(); + if (ImGui::Button(STR_C("Delete"))) + { + sweep_delete(*sweep); + tool.edited = nullptr; + } + } + else + { + bend_source source; + if (bend_source_of_selection(source)) + { + void const *key{source.model != nullptr ? static_cast(source.model) : reinterpret_cast(static_cast(source.instance))}; + bend_find_tracks(glm::dvec3(source.transform[3]), key); + ImGui::Text("%s", source.file.c_str()); + if (tool.tracks.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.3f, 1.0f), STR_C("No track within %.0f m of the model"), kBendReach); + else + { + tool.track = std::clamp(tool.track, 0, static_cast(tool.tracks.size()) - 1); + auto const label = [](std::pair const &Entry) { return format("%s (%.1f m)", Entry.first->name().empty() ? "(unnamed)" : Entry.first->name().c_str(), Entry.second); }; + ImGui::SetNextItemWidth(220.0f); + if (ImGui::BeginCombo(STR_C("track"), label(tool.tracks[tool.track]).c_str())) + { + for (int i = 0; i < static_cast(tool.tracks.size()); ++i) + if (ImGui::Selectable(label(tool.tracks[i]).c_str(), i == tool.track)) + tool.track = i; + ImGui::EndCombo(); + } + char const *axes[] = {STR_C("the one nearest the track"), "z", "x"}; + ImGui::SetNextItemWidth(170.0f); + ImGui::Combo(STR_C("axis of the model along the track"), &tool.axis, axes, IM_ARRAYSIZE(axes)); + ImGui::Checkbox(STR_C("Turned around"), &tool.turned); + if (platform_model(source.file)) + platform_distance(); + bool anchored{false}; + anchors(anchored); + auto const signature{format("%p %d %d %d %d %d %d %.4f %.4f %.4f %.3f", key, tool.track, tool.axis, tool.turned ? 1 : 0, tool.along_anchor, tool.side_anchor, tool.height_anchor, tool.point.x, tool.point.y, tool.point.z, EditorSettings.platform_edge())}; + if (signature != tool.signature) + { + tool.signature = signature; + tool.marker_valid = false; + sweep_node::state state; + std::vector tracks; + bend_definition(source, *tool.tracks[tool.track].first, state, tracks); + } + if (ImGui::Button(STR_C("Bend it along the track (B)"))) + bend_selection(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The model stays where it stands and bends with the track; Ctrl+Z gives it back")); + } + } + else + { + tool.marker_valid = false; + tool.picking = false; + ImGui::TextDisabled("%s", STR_C("Select a model by a track, or click a bent one")); + } + auto *track{selected_track()}; + auto const nearby{track != nullptr ? sweeps_near(*track) : std::vector{}}; + std::vector> listed; + if (track != nullptr) + for (auto *sweep : nearby) + listed.emplace_back(sweep, 0.0); + else + for (auto *sweep : simulation::Sweeps.sequence()) + if (sweep != nullptr && false == sweep->m_editorremoved) + { + auto const distance{glm::distance(sweep->location(), glm::dvec3{Global.pCamera.Pos})}; + if (distance < 0.5 * (sweep->end() - sweep->start()) + 300.0) + listed.emplace_back(sweep, distance); + } + if (false == listed.empty()) + { + ImGui::Separator(); + ImGui::TextDisabled("%s", track != nullptr ? STR_C("Bent models by this track:") : STR_C("Bent models nearby:")); + for (auto const &entry : listed) + { + auto *sweep{entry.first}; + ImGui::PushID(sweep); + if (ImGui::Selectable(format("%s %s, %.0f m", sweep->name().c_str(), sweep->definition().model.c_str(), sweep->end() - sweep->start()).c_str(), false)) + { + tool.edited = sweep; + tool.picking = false; + } + ImGui::PopID(); + } + } + if (track != nullptr && ImGui::TreeNodeEx(STR_C("New model along the selected track"))) + { + m_sweep.open = true; + render_sweep_ui(); + ImGui::TreePop(); + } + else + m_sweep.open = false; + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); + ImGui::PopID(); +} + +void editor_mode::draw_bend_overlay() const +{ + auto const &tool{m_bend}; + if (false == tool.label.empty()) + { + auto const &io{ImGui::GetIO()}; + auto *foreground{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + auto const labelsize{ImGui::CalcTextSize(tool.label.c_str())}; + auto const detailsize{tool.details.empty() ? ImVec2(0.0f, 0.0f) : ImGui::CalcTextSize(tool.details.c_str())}; + ImVec2 const size{std::max(labelsize.x, detailsize.x), labelsize.y + (tool.details.empty() ? 0.0f : detailsize.y + 4.0f)}; + ImVec2 position{io.MousePos.x + 18.0f, io.MousePos.y + 18.0f}; + position.x = std::min(position.x, io.DisplaySize.x - size.x - 10.0f); + position.y = std::min(position.y, io.DisplaySize.y - size.y - 10.0f); + foreground->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 170), 4.0f); + foreground->AddText(position, IM_COL32(40, 220, 255, 255), tool.label.c_str()); + if (false == tool.details.empty()) + foreground->AddText(ImVec2(position.x, position.y + labelsize.y + 4.0f), IM_COL32(255, 255, 255, 235), tool.details.c_str()); + } + if (auto const *model{dynamic_cast(m_node)}; model != nullptr && tool.edited == nullptr && false == tool.tracks.empty() && tool.tracks_for == static_cast(model)) + { + screen_projection const projection; + ImVec2 at; + if (projection.project(model->location(), at)) + { + auto const text{format(STR_C("B: bend along %s"), tool.tracks.front().first->name().c_str())}; + auto *foreground{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + auto const size{ImGui::CalcTextSize(text.c_str())}; + foreground->AddRectFilled(ImVec2(at.x + 10.0f, at.y - size.y - 14.0f), ImVec2(at.x + size.x + 22.0f, at.y - 6.0f), IM_COL32(0, 0, 0, 150), 4.0f); + foreground->AddText(ImVec2(at.x + 16.0f, at.y - size.y - 10.0f), IM_COL32(40, 220, 255, 255), text.c_str()); + } + } + glm::dvec3 marker; + if (tool.edited != nullptr && false == tool.edited->m_editorremoved) + marker = bend_marker(*tool.edited); + else if (tool.marker_valid) + marker = tool.marker; + else + return; + screen_projection const projection; + ImVec2 screen; + if (false == projection.project(marker, screen)) + return; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + if (tool.over_marker || tool.dragging != 0) + drawlist->AddCircleFilled(screen, 9.0f, IM_COL32(40, 220, 255, 120), 16); + drawlist->AddCircle(screen, 9.0f, IM_COL32(40, 220, 255, 255), 16, 2.5f); + drawlist->AddLine(ImVec2(screen.x - 14.0f, screen.y), ImVec2(screen.x + 14.0f, screen.y), IM_COL32(40, 220, 255, 255), 2.0f); + drawlist->AddLine(ImVec2(screen.x, screen.y - 14.0f), ImVec2(screen.x, screen.y + 14.0f), IM_COL32(40, 220, 255, 255), 2.0f); +} + +bool editor_mode::bend_drag_start(int const Mods) +{ + auto &tool{m_bend}; + auto *sweep{tool.edited}; + if (sweep == nullptr || sweep->m_editorremoved || ImGui::GetIO().WantCaptureMouse) + return false; + auto const marker{bend_marker(*sweep)}; + screen_projection const projection; + ImVec2 screen; + if (false == projection.project(marker, screen)) + return false; + auto const &mouse{ImGui::GetIO().MousePos}; + if ((mouse.x - screen.x) * (mouse.x - screen.x) + (mouse.y - screen.y) * (mouse.y - screen.y) > 18.0f * 18.0f) + return false; + tool.dragging = (Mods & GLFW_MOD_SHIFT) != 0 ? 2 : (Mods & GLFW_MOD_CONTROL) != 0 ? 3 : 1; + tool.drag_state = sweep->definition(); + tool.drag_anchor = marker; + double lateral; + tool.drag_station = sweep->project(marker, lateral); + tool.drag_label.clear(); + return true; +} + +void editor_mode::bend_drag_update() +{ + auto &tool{m_bend}; + tool.label.clear(); + tool.details.clear(); + tool.hover = nullptr; + tool.over_marker = false; + auto const &io{ImGui::GetIO()}; + if (auto *model{dynamic_cast(m_node)}; model != nullptr && tool.edited == nullptr && false == model->from_template() && scene::Layers.editable(model)) + bend_find_tracks(model->location(), model); + if (tool.dragging == 0 && false == io.WantCaptureMouse) + { + if (tool.picking) + tool.label = STR("Click: the reference point, where it lands on the model"); + else + { + if (auto *sweep{tool.edited}; sweep != nullptr && false == sweep->m_editorremoved) + { + screen_projection const projection; + ImVec2 screen; + if (projection.project(bend_marker(*sweep), screen)) + tool.over_marker = (io.MousePos.x - screen.x) * (io.MousePos.x - screen.x) + (io.MousePos.y - screen.y) * (io.MousePos.y - screen.y) < 14.0f * 14.0f; + } + if (tool.over_marker) + { + tool.label = STR("Drag: along the track and aside"); + tool.details = STR("Shift: height Ctrl: only along the track"); + } + else if (ui()->mode() == nodebank_panel::MODIFY && m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) != GLFW_PRESS) + { + tool.hover = sweep_under(Global.pCamera.Pos + GfxRenderer->Mouse_Position()); + if (tool.hover != nullptr && tool.hover != tool.edited) + { + tool.label = STR("Click: edit the bent model"); + tool.details = format("%s %s, %.1f m", tool.hover->name().c_str(), tool.hover->definition().model.c_str(), tool.hover->distance_at(tool.hover->end()) - tool.hover->distance_at(tool.hover->start())); + } + } + } + } + if (tool.dragging == 0) + return; + if (false == tool.drag_label.empty()) + tool.label = tool.drag_label; + auto *sweep{tool.edited}; + if (sweep == nullptr || sweep->m_editorremoved) + { + tool.dragging = 0; + return; + } + if (false == ImGui::GetIO().MouseDown[0]) + { + tool.dragging = 0; + tool.drag_label.clear(); + if (sweep->definition() == tool.drag_state) + return; + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::Other; + snap.node_name = sweep->name(); + snap.sweeps.emplace_back(sweep, tool.drag_state); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + sweep->mark_dirty(); + return; + } + screen_projection const projection; + auto const &mouse{ImGui::GetIO().MousePos}; + auto const step{0.01}; + auto const snap = [&](double const Value) { return std::round(Value / step) * step; }; + auto state{tool.drag_state}; + auto const slide = [&](double const Shift) { + auto const span{(tool.drag_state.to < 0.0 ? sweep->length() : tool.drag_state.to) - tool.drag_state.from}; + auto from{tool.drag_state.from + Shift}; + auto to{from + span}; + if (span > 0.1 && span <= sweep->length() + 1e-6) + { + if (from < 0.0) + { + to -= from; + from = 0.0; + } + if (to > sweep->length()) + { + from = std::max(0.0, sweep->length() - span); + to = from + span; + } + } + state.from = from; + state.to = to; + }; + if (tool.dragging == 2) + { + glm::dvec3 origin, direction; + projection.ray(mouse, origin, direction); + glm::dvec3 normal{Global.pCamera.Pos.x - tool.drag_anchor.x, 0.0, Global.pCamera.Pos.z - tool.drag_anchor.z}; + if (glm::length(normal) < 1e-6) + return; + normal = glm::normalize(normal); + auto const facing{glm::dot(direction, normal)}; + if (std::abs(facing) < 1e-6) + return; + auto const hit{origin + direction * (glm::dot(tool.drag_anchor - origin, normal) / facing)}; + state.height = kRailTop + snap(tool.drag_state.height - kRailTop + hit.y - tool.drag_anchor.y); + tool.drag_label = format(STR_C("%.2f m over the rail head"), state.height - kRailTop); + } + else + { + glm::dvec3 point; + if (false == projection.on_level(mouse, tool.drag_anchor.y, point)) + return; + double lateral; + auto const station{sweep->project(point, lateral)}; + auto const shift{snap(station - tool.drag_station)}; + slide(shift); + if (tool.dragging == 1) + { + auto const at{sweep->frame_at(station)}; + state.lateral = snap(lateral - sweep->setback(at)); + if (std::abs(state.lateral) < step) + state.lateral = std::copysign(step, tool.drag_state.lateral); + tool.drag_label = format(STR_C("%.2f m from the track, %+.2f m along"), std::abs(state.lateral), shift); + } + else + tool.drag_label = format(STR_C("%+.2f m along the track"), shift); + } + if (false == (state == sweep->definition())) + sweep->define(state); +} + +sweep_node *editor_mode::sweep_under(glm::dvec3 const &Point) const +{ + sweep_node *best{nullptr}; + auto bestdistance{std::numeric_limits::max()}; + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->m_editorremoved) + continue; + auto const &state{sweep->definition()}; + if (plan_distance(Point, sweep->location()) > 0.5 * sweep->length() + std::abs(state.lateral) + 30.0) + continue; + double lateral; + auto const station{sweep->project(Point, lateral)}; + if (station < sweep->start() - 0.5 || station > sweep->end() + 0.5) + continue; + glm::dvec3 low, high, shift; + if (false == sweep->bounds(low, high, shift)) + continue; + auto const at{sweep->frame_at(station)}; + auto const across{lateral - state.lateral - sweep->setback(at) - shift.y}; + auto const up{Point.y - at.position.y - state.height - sweep->rise(at) - shift.z}; + if (across < low.y - 0.3 || across > high.y + 0.3 || up < low.z - 0.3 || up > high.z + 0.3) + continue; + auto const distance{std::abs(across - 0.5 * (low.y + high.y))}; + if (distance < bestdistance) + { + bestdistance = distance; + best = sweep; + } + } + return best; +} + +bool editor_mode::bend_click() +{ + auto &tool{m_bend}; + if (tool.hover == nullptr || tool.hover == tool.edited || tool.over_marker) + return false; + tool.edited = tool.hover; + tool.picking = false; + tool.signature.clear(); + m_node = nullptr; + m_dragging = false; + select_include(0); + ui()->set_node(nullptr); + ui()->expand_bend(); + return true; +} + +bool editor_mode::bend_shortcut() +{ + auto &tool{m_bend}; + bend_source source; + if (false == bend_source_of_selection(source)) + return false; + void const *key{source.model != nullptr ? static_cast(source.model) : reinterpret_cast(static_cast(source.instance))}; + bend_find_tracks(glm::dvec3(source.transform[3]), key); + if (tool.tracks.empty()) + { + ui()->set_status(format(STR_C("No track within %.0f m of the model"), kBendReach), true); + return true; + } + bend_selection(); + ui()->expand_bend(); + return true; +} diff --git a/application/editormodetextures.cpp b/application/editormodetextures.cpp new file mode 100644 index 00000000..f18dff0a --- /dev/null +++ b/application/editormodetextures.cpp @@ -0,0 +1,279 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" +#include "utilities/translation.h" +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include + +namespace +{ + +// payload of a material dragged from the texture browser: its name, with the terminating zero +char const *const material_payload{"EU07_MATERIAL"}; + +// files the browser shows: textures, and the materials, which are kept with them +bool texture_file(std::string const &Extension) +{ + return Extension == ".dds" || Extension == ".tga" || Extension == ".png" || Extension == ".bmp" || Extension == ".jpg" || Extension == ".ktx" || Extension == ".tex"; +} + +std::string lowercase(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +// the text cut to fit the width, with an ellipsis +std::string fitted(std::string const &Text, float const Width) +{ + if (ImGui::CalcTextSize(Text.c_str()).x <= Width) + return Text; + auto result{Text}; + while (false == result.empty() && ImGui::CalcTextSize((result + "...").c_str()).x > Width) + result.pop_back(); + return result + "..."; +} + +} // namespace + +bool editor_mode::material_drop(std::string &Name) +{ + if (false == ImGui::BeginDragDropTarget()) + return false; + auto dropped{false}; + if (auto const *payload{ImGui::AcceptDragDropPayload(material_payload)}) + { + auto const *data{static_cast(payload->Data)}; + Name.assign(data, std::find(data, data + payload->DataSize, '\0')); + dropped = false == Name.empty(); + } + ImGui::EndDragDropTarget(); + return dropped; +} + +bool editor_mode::material_drop(char *Buffer, std::size_t const Size) +{ + std::string name; + if (Size == 0 || false == material_drop(name)) + return false; + auto const length{std::min(name.size(), Size - 1)}; + std::memcpy(Buffer, name.data(), length); + Buffer[length] = '\0'; + return true; +} + +std::uint64_t editor_mode::texture_thumbnail(texture_browser::item const &Item) +{ + auto &browser{m_textures}; + auto lookup{browser.thumbnails.find(Item.name)}; + if (lookup == browser.thumbnails.end()) + { + // reading a texture takes a moment, a few of them go in a frame + if (browser.loads >= 2) + return 0; + ++browser.loads; + texture_handle texture{null_handle}; + if (Item.material) + { + auto const material{GfxRenderer->Fetch_Material(Item.name)}; + if (auto const *data{GfxRenderer->Material(material)}; material != null_handle && data != nullptr) + texture = data->GetTexture(0); + } + else + texture = GfxRenderer->Fetch_Texture(Item.name, true); + lookup = browser.thumbnails.emplace(Item.name, texture).first; + } + if (lookup->second == null_handle) + return 0; + auto &texture{GfxRenderer->Texture(lookup->second)}; + if (false == texture.get_is_ready()) + texture.create(); + return texture.get_is_ready() ? static_cast(texture.get_id()) : 0; +} + +void editor_mode::render_texture_browser() +{ + auto &browser{m_textures}; + browser.loads = 0; + if (false == browser.listed) + { + browser.listed = true; + browser.folders.clear(); + browser.items.clear(); + std::map items; + std::error_code error; + for (std::filesystem::directory_iterator entry{std::string{paths::textures} + browser.folder, error}, end; false == static_cast(error) && entry != end; entry.increment(error)) + { + auto const path{entry->path()}; + if (entry->is_directory(error)) + { + browser.folders.push_back(path.filename().string()); + continue; + } + auto const extension{lowercase(path.extension().string())}; + auto const material{extension == ".mat"}; + if (false == material && false == texture_file(extension)) + continue; + auto const stem{path.stem().string()}; + auto &item{items[stem]}; + item.name = browser.folder + stem; + item.label = stem; + item.material |= material; + } + for (auto &item : items) + browser.items.push_back(std::move(item.second)); + std::sort(browser.folders.begin(), browser.folders.end()); + } + + // where the browser is, and the folders under it + ImGui::BeginDisabled(browser.folder.empty()); + if (ImGui::Button(STR_C("Up"))) + { + auto folder{browser.folder}; + if (false == folder.empty()) + folder.pop_back(); + auto const slash{folder.rfind('/')}; + browser.folder = (slash == std::string::npos ? std::string{} : folder.substr(0, slash + 1)); + browser.listed = false; + } + ImGui::EndDisabled(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Refresh"))) + { + browser.listed = false; + browser.thumbnails.clear(); + } + ImGui::SameLine(); + ImGui::TextUnformatted((std::string{paths::textures} + browser.folder).c_str()); + ImGui::SetNextItemWidth(std::max(80.0f, ImGui::GetContentRegionAvail().x * 0.5f)); + ImGui::InputTextWithHint("##texturefilter", STR_C("Filter"), browser.filter, IM_ARRAYSIZE(browser.filter)); + ImGui::SameLine(); + ImGui::SetNextItemWidth(std::max(60.0f, ImGui::GetContentRegionAvail().x)); + ImGui::SliderFloat("##texturesize", &browser.size, 48.0f, 256.0f, "%.0f px"); + if (false == browser.folders.empty()) + { + // the folders flow along the width of the window + auto const right{ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x}; + for (std::size_t index = 0; index < browser.folders.size(); ++index) + { + auto const label{"[" + browser.folders[index] + "]"}; + auto const width{ImGui::CalcTextSize(label.c_str()).x + ImGui::GetStyle().FramePadding.x * 2.0f}; + if (index > 0) + { + ImGui::SameLine(); + if (ImGui::GetCursorScreenPos().x + width > right) + ImGui::NewLine(); + } + ImGui::PushID(static_cast(index)); + if (ImGui::SmallButton(label.c_str())) + { + browser.folder += browser.folders[index] + "/"; + browser.listed = false; + } + ImGui::PopID(); + } + if (false == browser.listed) + { + // the list is made anew in the next frame + return; + } + } + ImGui::Separator(); + + std::vector shown; + auto const filter{lowercase(browser.filter)}; + for (auto const &item : browser.items) + if (filter.empty() || lowercase(item.label).find(filter) != std::string::npos) + shown.push_back(&item); + if (shown.empty()) + { + ImGui::TextDisabled("%s", STR_C("No textures here")); + return; + } + ImGui::TextDisabled("%s", STR_C("Drag a texture onto a field of a material; double click: add it to the palette of the terrain")); + + ImGui::BeginChild("##texturecards"); + auto const &style{ImGui::GetStyle()}; + auto const side{std::max(16.0f, browser.size * std::max(1.0f, Global.ui_scale))}; + auto const cellheight{side + ImGui::GetTextLineHeight() + style.ItemInnerSpacing.y}; + auto const columns{std::max(1, static_cast((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (side + style.ItemSpacing.x)))}; + auto const rows{(static_cast(shown.size()) + columns - 1) / columns}; + auto *drawlist{ImGui::GetWindowDrawList()}; + ImGuiListClipper clipper; + clipper.Begin(rows, cellheight + style.ItemSpacing.y); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + for (int column = 0; column < columns; ++column) + { + auto const index{static_cast(row) * columns + column}; + if (index >= shown.size()) + break; + auto const &item{*shown[index]}; + if (column > 0) + ImGui::SameLine(); + ImGui::PushID(item.name.c_str()); + ImGui::BeginGroup(); + auto const corner{ImGui::GetCursorScreenPos()}; + ImGui::InvisibleButton("##card", ImVec2(side, cellheight)); + auto const hovered{ImGui::IsItemHovered()}; + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + { + browser.selected = item.name; + std::snprintf(m_terrain_material, IM_ARRAYSIZE(m_terrain_material), "%s", item.name.c_str()); + } + if (hovered && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left) && m_streamer.active()) + m_terrain_layer = m_streamer.layer(item.name, m_terrain_material_size); + auto const texture{texture_thumbnail(item)}; + if (ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload(material_payload, item.name.c_str(), item.name.size() + 1); + if (texture != 0) + ImGui::Image(static_cast(texture), ImVec2(64.0f, 64.0f), ImVec2(0, 1), ImVec2(1, 0)); + ImGui::TextUnformatted(item.name.c_str()); + ImGui::EndDragDropSource(); + } + ImVec2 const low{corner.x, corner.y}, high{corner.x + side, corner.y + side}; + if (texture != 0) + drawlist->AddImage(static_cast(texture), low, high, ImVec2(0, 1), ImVec2(1, 0)); + else + { + drawlist->AddRectFilled(low, high, ImGui::GetColorU32(ImGuiCol_FrameBg)); + drawlist->AddText(ImVec2(low.x + style.FramePadding.x, low.y + style.FramePadding.y), ImGui::GetColorU32(ImGuiCol_TextDisabled), "..."); + } + if (item.material) + drawlist->AddText(ImVec2(low.x + style.FramePadding.x, high.y - ImGui::GetTextLineHeight() - style.FramePadding.y), IM_COL32(255, 220, 120, 255), "mat"); + if (browser.selected == item.name || hovered) + drawlist->AddRect(low, high, ImGui::GetColorU32(browser.selected == item.name ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered), 0.0f, 0, 2.0f); + drawlist->AddText(ImVec2(low.x, high.y + style.ItemInnerSpacing.y), ImGui::GetColorU32(ImGuiCol_Text), fitted(item.label, side).c_str()); + if (hovered) + { + ImGui::BeginTooltip(); + ImGui::TextUnformatted(item.name.c_str()); + ImGui::TextDisabled("%s", item.material ? STR_C("material") : STR_C("texture")); + ImGui::EndTooltip(); + } + ImGui::EndGroup(); + ImGui::PopID(); + } + } + } + ImGui::EndChild(); +} diff --git a/application/editormodetrack.cpp b/application/editormodetrack.cpp new file mode 100644 index 00000000..0b264b78 --- /dev/null +++ b/application/editormodetrack.cpp @@ -0,0 +1,6082 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http: +*/ + + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::cross; +using geometry::grade_of; +using geometry::plan_distance; +using geometry::plan_of; +using geometry::signed_angle; +using geometry::turned; +using geometry::arc_pieces; + +double constexpr kParallelFindAngle{15.0}; // deg, neighbours counted as beside the straight +double constexpr kParallelSnapAngle{5.0}; // deg, close enough to snap parallel +double constexpr kParallelSnapWindow{0.45}; // m from 4.00 / 4.50 / 4.75 m +double constexpr kParallelHintWindow{1.25}; // m, hint before the snap takes + +double piece_plan_radius(segment_data const &Path) +{ + if (std::abs(Path.radius) > 0.5f) + return std::abs(Path.radius); + bezier const curve{Path}; + auto const begin{plan_of(curve.first(0.0))}; + auto const finish{plan_of(curve.first(1.0))}; + if (glm::length(begin) < 1e-9 || glm::length(finish) < 1e-9) + return 0.0; + auto const turn{std::abs(signed_angle(begin, finish))}; + if (turn < 1e-4) + return 0.0; + return curve.plan_length() / turn; +} + +std::string describe(editor_track::straight const &Line) +{ + return format("L %.2f m az %.4f deg i %.2f per mille %zu paths", Line.length, Line.azimuth, Line.grade * 1000.0, Line.tracks.size()); +} + +char const *point_label(editor_track::point_kind const Kind) +{ + switch (Kind) + { + case editor_track::point_kind::start: return "Start"; + case editor_track::point_kind::control1: return "Control 1"; + case editor_track::point_kind::control2: return "Control 2"; + default: return "End"; + } +} + +editor_track::point_kind const kPointKinds[] = {editor_track::point_kind::start, editor_track::point_kind::control1, editor_track::point_kind::control2, editor_track::point_kind::end}; + +float const kHandleRadius{10.0f}; +double const kLaySnapRadius{5.0}; +float const kSnapPixels{16.0f}; +float const kDragPixels{5.0f}; +double const kSwitchReach{6.0}; // a press farther from the selected track selects another one instead of placing the switch +double const kLineReach{2000.0}; // how far each way the Curve tab follows the line +double const kJointRange{400.0}; // the ends of the paths are drawn for the tracks this close to the camera +ImU32 const kJointColor{IM_COL32(255, 255, 255, 200)}; +int const kRailCategory{1}; + +void item_tooltip(char const *Text) +{ + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C(Text)); +} + +// the curve a track ends with at Joint, as it goes on beyond the joint, and a transition curve leading out of it to the straight +struct easing +{ + double curvature{0.0}; // positive turning from x towards z + double length{0.0}; + glm::dvec3 end{0.0}; + glm::dvec2 direction{0.0}; + std::vector pieces; +}; + +easing ease_out_of(TTrack const &Neighbour, glm::dvec3 const &Joint, double const Grade, double const Speed, alignment::limits const &Limits) +{ + easing result; + for (auto const &path : Neighbour.m_paths) + for (auto const atend : {false, true}) + { + if (glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], Joint) > 0.05) + continue; + if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) + return result; + bezier const curve{path}; + auto const first{curve.first(atend ? 1.0 : 0.0)}; + auto const second{curve.second(atend ? 1.0 : 0.0)}; + auto const speed{std::hypot(first.x, first.z)}; + if (speed < 1e-9) + return result; + auto curvature{(first.x * second.z - first.z * second.x) / (speed * speed * speed)}; + glm::dvec2 direction{first.x / speed, first.z / speed}; + double roll{path.rolls[1]}; + if (false == atend) + { + curvature = -curvature; + direction = -direction; + roll = -path.rolls[0]; + } + result.curvature = curvature; + if (std::abs(curvature) < 1e-4) + return result; + result.length = std::max(20.0, alignment::recommend(Speed, 1.0 / std::abs(curvature), Limits).transition); + auto const count{static_cast(std::ceil(result.length / 10.0))}; + auto const piece{result.length / count}; + glm::dvec3 point{Joint}; + for (int i = 0; i < count; ++i) + { + segment_data part; + part.points[segment_data::point::start] = point; + auto const from{direction}; + int const steps{static_cast(std::ceil(piece / 0.05))}; + auto const step{piece / steps}; + for (int j = 0; j < steps; ++j) + { + auto const along{i * piece + (j + 0.5) * step}; + auto const angle{curvature * (1.0 - along / result.length) * step}; + auto const half{turned(direction, angle * 0.5)}; + point += glm::dvec3{half.x * step, Grade * step, half.y * step}; + direction = turned(direction, angle); + } + part.points[segment_data::point::end] = point; + part.points[segment_data::point::control1] = glm::dvec3{from.x, Grade, from.y} * (piece / 3.0); + part.points[segment_data::point::control2] = -glm::dvec3{direction.x, Grade, direction.y} * (piece / 3.0); + part.rolls = {static_cast(roll * (1.0 - i * piece / result.length)), static_cast(roll * (1.0 - (i + 1) * piece / result.length))}; + result.pieces.push_back(part); + } + result.end = point; + result.direction = direction; + return result; + } + return result; +} + +segment_data turned_around(segment_data const &Path) +{ + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; +} + +// curvature of the shape at Chainage, positive turning from x towards z +double curvature_at(alignment::result const &Shape, double const Chainage) +{ + auto const a{std::clamp(Chainage - 0.5, 0.0, Shape.length)}; + auto const b{std::clamp(Chainage + 0.5, 0.0, Shape.length)}; + if (b - a < 1e-3) + return 0.0; + auto const from{plan_of(alignment::evaluate(Shape, a).direction)}; + auto const to{plan_of(alignment::evaluate(Shape, b).direction)}; + return signed_angle(glm::normalize(from), glm::normalize(to)) / (b - a); +} + +// circular curve from a point along a direction, cut into pieces of 90 degrees at most. Side: 1 to the left, -1 to the right +void compound_from(editor_track::curve const &Curve, alignment::vertex &Vertex) +{ + if (false == Curve.compound || Curve.arcs.size() < 2) + return; + Vertex.compound = true; + Vertex.radius = std::round(Curve.arcs.front().radius); + Vertex.share = Curve.arcs.front().turn; + Vertex.arcs.clear(); + for (std::size_t i = 1; i < Curve.arcs.size(); ++i) + Vertex.arcs.push_back({std::round(Curve.arcs[i].radius), std::round(Curve.arcs[i].transition), Curve.arcs[i].turn}); +} + +} + +TTrack *editor_mode::selected_track() const +{ + return dynamic_cast(m_node); +} + +void editor_mode::select_track(scene::basic_node *Node) +{ + auto *track = dynamic_cast(Node); + if (track == nullptr) + { + m_track_point = {}; + return; + } + if (track != m_node) + { + m_track_point = {}; + m_track_mode_notice.clear(); + } + m_node = track; + ui()->set_node(m_node); + if (ui()->mode() != nodebank_panel::TRACK) + return; + m_track_window_open = true; + m_track_mode_notice.clear(); + // the mode stays as it is, the path is taken up by it if it can be + switch (m_track_tab) + { + case track_tab::straights: + if (track->eType == tt_Switch || false == editor_track::is_straight(*track, m_straights.tolerance)) + m_track_mode_notice = STR_C("The selected path isn't straight: C edits it as a curve, Esc goes back to Select"); + break; + case track_tab::route: + if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end()) + break; + if (track->eType != tt_Normal || false == editor_track::is_supported(*track)) + m_track_mode_notice = STR_C("Switches aren't a part of a line: T edits the switch, Esc goes back to Select"); + else if (false == route_load(*track)) + m_track_mode_notice = m_route.error.empty() ? std::string{STR_C("The curve can't be read from this path, edit it as a single path")} : m_route.error; + break; + default: + break; + } +} + +editor_track::point_ref editor_mode::track_handle_hit() const +{ + auto *track = selected_track(); + if (track == nullptr || false == editor_track::is_supported(*track) || m_track_tab != track_tab::path) + return {}; + + screen_projection const projection; + float best = kHandleRadius * kHandleRadius; + editor_track::point_ref hit; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + for (auto const kind : kPointKinds) + { + editor_track::point_ref const point{i, kind}; + float const distance = projection.mouse_distance2(editor_track::point_position(*track, point)) * (editor_track::is_end(kind) ? 0.5f : 1.0f); + if (distance < best) + { + best = distance; + hit = point; + } + } + } + return hit; +} + +void editor_mode::draw_track_overlay() const +{ + auto const *track = selected_track(); + if (track == nullptr) + return; + + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + bool const trackmode = ui()->mode() == nodebank_panel::TRACK; + + ImU32 const coursecolor = IM_COL32(40, 220, 255, 220); + int constexpr samples{32}; + for (auto const &path : track->m_paths) + { + auto previous = bezier{path}.point(0.0); + for (int i = 1; i <= samples; ++i) + { + auto const next = bezier{path}.point(static_cast(i) / samples); + projection.line(drawlist, previous, next, coursecolor, 2.5f); + previous = next; + } + } + if (false == trackmode || m_route_tab || false == editor_track::is_supported(*track)) + return; + + ImU32 const vectorcolor = IM_COL32(200, 200, 200, 160); + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::start}), editor_track::point_position(*track, {i, editor_track::point_kind::control1}), vectorcolor, 1.0f); + projection.line(drawlist, editor_track::point_position(*track, {i, editor_track::point_kind::end}), editor_track::point_position(*track, {i, editor_track::point_kind::control2}), vectorcolor, 1.0f); + } + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + for (auto const kind : kPointKinds) + { + editor_track::point_ref const point{i, kind}; + ImVec2 screen; + if (false == projection.project(editor_track::point_position(*track, point), screen)) + continue; + bool const selected = (point == m_track_point); + if (editor_track::is_end(kind)) + { + ImU32 const color = editor_track::is_connected(*track, point) ? IM_COL32(60, 220, 60, 255) : IM_COL32(240, 60, 60, 255); + drawlist->AddCircleFilled(screen, 6.0f, color); + } + else + { + drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 4.0f), ImVec2(screen.x + 4.0f, screen.y + 4.0f), IM_COL32(230, 230, 230, 255)); + } + if (selected) + drawlist->AddCircle(screen, 10.0f, IM_COL32(255, 220, 40, 255), 16, 2.5f); + } + } + if (m_track_snap.track != nullptr) + { + ImVec2 screen; + if (projection.project(m_track_snap.position, screen)) + drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f); + projection.line(drawlist, m_track_snap.position, m_track_snap.position + m_track_snap.direction * 5.0, IM_COL32(255, 60, 255, 200), 2.0f); + } +} + +void editor_mode::render_track_gizmo() +{ + auto *track = selected_track(); + std::string reason; + if (track == nullptr || false == editor_track::can_edit_geometry(*track, reason)) + { + m_track_gizmo_using = false; + return; + } + bool const pointmode = ui()->mode() == nodebank_panel::TRACK && m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size()); + + gizmo_frame const frame; + + if (false == m_track_gizmo_using) + { + auto const anchor = pointmode ? editor_track::point_position(*track, m_track_point) : editor_track::pivot(*track); + m_track_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - frame.camera)); + } + + glm::mat4 delta(1.0f); + frame.manipulate(ImGuizmo::TRANSLATE, m_track_gizmo, m_gizmo_snap, &delta); + + if (ImGuizmo::IsUsing()) + { + if (false == m_track_gizmo_using) + { + m_track_gizmo_using = true; + m_track_pivot = editor_track::pivot(*track); + m_track_drag = {track}; + m_track_drag_points.clear(); + m_track_joints.clear(); + m_track_snap = {}; + if (false == pointmode && m_track_drag_connected) + { + m_track_joints = editor_track::joints(*track); + for (auto const &joint : m_track_joints) + if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) + m_track_drag.emplace_back(joint.other); + } + if (pointmode && editor_track::is_end(m_track_point.kind) && m_track_drag_connected) + { + for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point))) + { + std::string neighbourreason; + if (false == editor_track::can_edit_geometry(*connection.first, neighbourreason)) + continue; + m_track_drag_points.emplace_back(connection); + if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) + m_track_drag.emplace_back(connection.first); + } + } + std::vector> states; + for (auto *dragged : m_track_drag) + states.emplace_back(dragged, editor_track::capture(*dragged)); + push_track_snapshot(std::move(states)); + } + + if (pointmode) + { + glm::dvec3 const position = frame.camera + glm::dvec3(m_track_gizmo[3]); + editor_track::move_point(*track, m_track_point, position); + for (auto const &connection : m_track_drag_points) + editor_track::move_point(*connection.first, connection.second, position); + m_track_dirty = true; + if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) + { + std::vector const exclude(m_track_drag.begin(), m_track_drag.end()); + screen_projection const projection; + ImVec2 screen; + m_track_snap = projection.project(position, screen) ? snap_free_end(position, {screen.x, screen.y}, track, track->iCategoryFlag & 15, exclude) : editor_track::snap_target{}; + } + } + else + { + glm::dvec3 const offset{delta[3]}; + if (glm::length(offset) > 1e-9) + { + editor_track::translate(*track, offset); + editor_track::follow(*track, m_track_joints); + m_track_dirty = true; + } + } + commit_track_drag(false); + } + else if (m_track_gizmo_using) + { + if (m_track_snap.track != nullptr && pointmode) + { + editor_track::snap_point(*track, m_track_point, m_track_snap, m_track_align_tangent); + m_track_dirty = true; + } + m_track_snap = {}; + commit_track_drag(true); + m_track_gizmo_using = false; + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_joints.clear(); + } +} + +void editor_mode::commit_track_drag(bool const Force) +{ + if (false == m_track_dirty || m_track_drag.empty()) + return; + auto const now = std::chrono::steady_clock::now(); + if (false == Force && now - m_track_last_commit < std::chrono::milliseconds(100)) + return; + editor_track::commit(m_track_drag); + m_track_dirty = false; + m_track_last_commit = now; +} + +void editor_mode::trim_history() +{ + if (m_max_history_size >= 0 && (int)m_history.size() >= m_max_history_size) + m_history.erase(m_history.begin(), m_history.begin() + ((int)m_history.size() - m_max_history_size + 1)); +} + +void editor_mode::push_track_snapshot(std::vector> States, std::vector Created, std::vector Removed) +{ + if (States.empty() && Created.empty() && Removed.empty()) + return; + + trim_history(); + auto *track = false == States.empty() ? States.front().first : false == Created.empty() ? Created.front() : Removed.front(); + EditorSnapshot snap; + snap.action = EditorSnapshot::Action::TrackEdit; + snap.node_name = track->name(); + snap.node_ptr = track; + snap.position = track->location(); + snap.uuid = track->uuid; + snap.tracks = std::move(States); + snap.created = std::move(Created); + snap.removed = std::move(Removed); + infra_attach(snap); + m_history.push_back(std::move(snap)); + g_redo.clear(); +} + +bool editor_mode::tracks_editable(std::vector const &Tracks, std::string &Reason) +{ + for (auto const *track : Tracks) + { + std::string reason; + if (track != nullptr && false == editor_track::can_edit_geometry(*track, reason)) + { + Reason = (track->name().empty() ? std::string{"(noname)"} : track->name()) + ": " + reason; + return false; + } + } + Reason.clear(); + return true; +} + +void editor_mode::restore_track_snapshot(EditorSnapshot const &Snapshot, std::vector &Opposite, bool const Undo) +{ + if (Snapshot.tracks.empty() && Snapshot.created.empty() && Snapshot.removed.empty()) + return; + + std::vector tracks; + for (auto const &entry : Snapshot.tracks) + tracks.emplace_back(entry.first); + m_infra_suspended = true; + EditorSnapshot current{Snapshot}; + current.tracks.clear(); + auto involved{tracks}; + if (Undo) + involved.insert(involved.end(), Snapshot.created.begin(), Snapshot.created.end()); + for (auto *track : involved) + current.tracks.emplace_back(track, editor_track::capture(*track)); + current.infra.clear(); + for (auto const &entry : Snapshot.infra) + if (auto const *binding{infra_find(entry.saved)}) + current.infra.push_back(infra_state_of(*binding)); + if (Snapshot.profiles) + { + std::vector layers; + for (auto const *library : {&std::as_const(m_profiles), &Snapshot.profile_library}) + for (auto const &entry : *library) + layers.push_back(entry.layer); + current.profile_library = std::move(m_profiles); + m_profiles = Snapshot.profile_library; + profile_library_mark(std::move(layers)); + } + Opposite.push_back(std::move(current)); + + if (Undo) + { + for (auto *track : Snapshot.created) + editor_track::retire(*track); + for (auto *track : Snapshot.removed) + { + track->m_editorremoved = false; + tracks.push_back(track); + } + } + else + { + for (auto *track : Snapshot.created) + track->m_editorremoved = false; + for (auto *track : Snapshot.removed) + editor_track::retire(*track); + } + for (auto const &entry : Snapshot.tracks) + editor_track::apply(*entry.first, entry.second); + editor_track::commit(tracks); + for (auto const &entry : Snapshot.infra) + infra_restore_state(entry); + m_infra_buffer.clear(); + m_infra_suspended = false; + if (false == m_route.chain.tracks.empty()) + { + std::string error; + if (m_route.from->m_editorremoved || m_route.to->m_editorremoved || false == editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true)) + { + m_route.chain = {}; + m_route.result = {}; + m_route.vertex = m_route.grip = -1; + } + else + { + route_bind_ends(); + route_update(); + } + } + + m_track_snap = {}; + m_track_dirty = false; + m_track_point = {}; + m_node = tracks.empty() || tracks.front()->m_editorremoved ? nullptr : tracks.front(); + ui()->set_node(m_node); + straight_refresh(); + profile_after_undo(); + if (Snapshot.profiles && false == m_profile.route.spans.empty()) + profile_restore(); +} + +std::array const &editor_mode::track_modes() +{ + static std::array const modes{{ + {"Select", "Esc", track_tab::path, "LMB on a path selects it: its points, control vectors and parameters"}, + {"Lay track", "L", track_tab::lay, "Lays new track through the clicked points, on the ground or from a free end of a track"}, + {"Switch", "T", track_tab::turnout, "Puts switches into the selected straight or curve, edits the geometry of the selected switch"}, + {"Turntable", "U", track_tab::turntable, "Places a turntable, leads tracks out of it and fits the tracks around to it"}, + {"Straight", "G", track_tab::straights, "The whole straight through the selected path: drag its ends or the middle, break it, shift it"}, + {"Curve", "C", track_tab::route, "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves"}, + {"Signals", "H", track_tab::signals, "Signals by the track, from the templates of the scenery, with the event the train reads them by"}, + {"Objects", "B", track_tab::lineside, "Along the track: hectometre posts, fouling point markers, a vehicle to drive"}, + {"Profile", "P", track_tab::profile, "Vertical profile (grade line) along the line"}, + {"Speed", "V", track_tab::speed, "Speed limits of the paths against the speed their geometry allows"}, + {"Joints", "J", track_tab::joints, "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints"}, + {"Infra", "I", track_tab::infra, "Infrastructure along the track, bound to follow its changes"}, + }}; + return modes; +} + +void editor_mode::show_track_tab(track_tab const Tab) +{ + m_track_window_open = true; + m_roadtool.window = false; + if (Tab == m_track_tab) + return; + m_track_mode_notice.clear(); + // what was going on in the mode left stays behind + cancel_track_tools(); + m_straights.tool = 0; + m_lay.points.clear(); + m_lay.start = {}; + m_lay.preview.clear(); + m_lay.status.clear(); + m_lay.active = Tab == track_tab::lay; + m_switch.armed = -1; + m_track_tab = Tab; + auto *track{selected_track()}; + switch (Tab) + { + case track_tab::turnout: + arm_switch(); + break; + case track_tab::straights: + if (track != nullptr && (track->eType == tt_Switch || false == editor_track::is_straight(*track, m_straights.tolerance))) + m_track_mode_notice = STR_C("The selected path isn't straight: LMB on a straight"); + break; + case track_tab::route: + if (track != nullptr && std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) == m_route.chain.tracks.end()) + { + if (track->eType != tt_Normal || false == editor_track::is_supported(*track)) + m_track_mode_notice = STR_C("Switches aren't a part of a line: LMB on a plain path"); + else if (false == route_load(*track)) + m_track_mode_notice = m_route.error.empty() ? std::string{STR_C("The curve can't be read from this path, edit it as a single path")} : m_route.error; + } + break; + case track_tab::profile: + if (track != nullptr && m_profile.route.spans.empty()) + profile_open_run(*track); + break; + case track_tab::turntable: + scan_turntables(); + turntable_read(track); + break; + case track_tab::signals: + signal_scan(); + signal_refresh(true); + break; + default: + break; + } +} + +void editor_mode::arm_switch() +{ + auto &tool{m_switch}; + if (false == tool.collected) + { + tool.templates = editor_track::standard_switch_templates(); + tool.collected = true; + } + tool.status.clear(); + tool.armed = tool.templates.empty() ? -1 : std::clamp(tool.last, 0, static_cast(tool.templates.size()) - 1); + if (tool.armed >= 0 && tool.templates[tool.armed].double_slip) + { + auto const ordinary{std::find_if(tool.templates.begin(), tool.templates.end(), [](editor_track::switch_template const &Template) { return false == Template.double_slip; })}; + tool.armed = ordinary != tool.templates.end() ? static_cast(ordinary - tool.templates.begin()) : -1; + } +} + +bool editor_mode::track_shortcut(int const Key) +{ + for (auto const &mode : track_modes()) + { + if (mode.key[1] != '\0' || Key != GLFW_KEY_A + (mode.key[0] - 'A')) + continue; + // the key of the mode on leaves it + show_track_tab(m_track_window_open && m_track_tab == mode.tab ? track_tab::path : mode.tab); + return true; + } + return false; +} + +bool editor_mode::track_busy() const +{ + auto const &straights{m_straights}; + return (m_lay.active && false == m_lay.points.empty()) || m_turntable.placing || m_signal.placing || m_signal.moving || straights.dragging || m_extend.active || m_point_drag.active || m_handle_drag.active || m_switch.placing || m_track_point.valid() || m_route.vertex >= 0 || m_route.grip >= 0 || false == straights.detour.empty() || straights.tool != 0 || straights.handle >= 0; +} + +std::string editor_mode::track_mode_name() const +{ + if (false == m_track_window_open) + return STR_C("Select: LMB on a path"); + switch (m_track_tab) + { + case track_tab::lay: return m_extend.active ? STR_C("Laying track: new path from the free end") : STR_C("Laying track: LMB adds the points"); + case track_tab::turnout: + if (m_switch.placing) + return STR_C("Switch: drag to set its direction and side"); + if (m_switch.armed < 0) + return selected_track() != nullptr && selected_track()->eType == tt_Switch ? STR_C("Switch: the selected one. Another: LMB on a track, then its type") : STR_C("Switch: choose its type from the list"); + return STR_C("Switch: press and drag on the selected track"); + case track_tab::straights: + if (false == m_straights.detour.empty()) + return STR_C("Straight: shift around, Shift+click the points"); + if (m_straights.tool == 1) + return STR_C("Straight: break, LMB on the straight"); + if (m_straights.tool == 2) + return STR_C("Straight: S-curve, LMB on the straight"); + return STR_C("Straight: drag its ends or the middle"); + case track_tab::route: return STR_C("Curve: drag the vertices and the radius grips"); + case track_tab::profile: return STR_C("Vertical profile: edit the grade line in the profile window"); + case track_tab::speed: return STR_C("Speed check: LMB on a row shows the path"); + case track_tab::joints: return STR_C("Joints: LMB on a row shows the place"); + case track_tab::lineside: + return STR_C("Objects along the track"); + case track_tab::infra: return STR_C("Infrastructure along the track"); + case track_tab::signals: + if (m_signal.moving) + return STR_C("Signal: drag along the track, release to put it there"); + if (m_signal.placing) + return STR_C("Signal: drag along the track sets the direction of the trains, release to place it"); + return signal_armed() != nullptr ? STR_C("Signal: LMB by the track places it on that side") : STR_C("Signal: choose its type below"); + case track_tab::turntable: + if (m_turntable.placing) + return STR_C("Turntable: drag to turn the bridge, release to place it"); + return m_turntable.table != nullptr ? STR_C("Turntable: LMB around it leads a track out, on a free end fits that track") : STR_C("Turntable: LMB places a new one"); + default: return m_point_drag.moved ? STR_C("Select: moving the point") : selected_track() != nullptr ? STR_C("Select: drag the points of the path") : STR_C("Select: LMB on a path"); + } +} + +void editor_mode::render_track_inspector() +{ + auto const &io{ImGui::GetIO()}; + m_profile.open = m_track_window_open && m_track_tab == track_tab::profile; + m_speed.open = m_track_window_open && m_track_tab == track_tab::speed; + m_infra.open = m_track_window_open && m_track_tab == track_tab::infra; + m_joints.open = m_track_window_open && m_track_tab == track_tab::joints; + if (false == m_track_window_open || m_track_tab != track_tab::lineside) + m_sweep.open = m_hekto.open = m_fouling.open = m_parallel.open = m_vehicle.open = false; + ui()->set_track(m_track_window_open); + if (false == m_track_window_open) + { + // a closed window takes its mode with it + if (m_track_tab != track_tab::path) + show_track_tab(track_tab::path); + m_track_window_open = false; + return; + } + + m_route_tab = m_track_tab != track_tab::path; + + // the tools of the mode are drawn in the tool options window and the selected path in the inspector, see + // render_track_tool_options() and render_track_selection(); the checks list what they find in a window of their own + if (m_track_tab == track_tab::speed || m_track_tab == track_tab::joints || m_track_tab == track_tab::infra) + { + auto const *title{m_track_tab == track_tab::speed ? STR_C("Speed check") : m_track_tab == track_tab::joints ? STR_C("Joints") : STR_C("Infrastructure")}; + ImGui::SetNextWindowSize(ImVec2(600.0f, std::min(320.0f, io.DisplaySize.y - 80.0f)), ImGuiCond_FirstUseEver); + if (ImGui::Begin((std::string{title} + "###trackanalysis").c_str(), nullptr, ImGuiWindowFlags_NoCollapse)) + { + switch (m_track_tab) + { + case track_tab::speed: render_speed_body(); break; + case track_tab::joints: render_joints_body(); break; + default: render_infra_body(); break; + } + } + ImGui::End(); + } + + if (m_track_tab == track_tab::profile && m_track_window_open && false == m_profile.route.spans.empty()) + render_profile_strip(); +} + +void editor_mode::render_track_tool_options() +{ + auto *track{selected_track()}; + // the tools themselves are in the toolbar + render_track_search(); + ImGui::Spacing(); + render_track_guide(); + switch (m_track_tab) + { + case track_tab::lay: + render_lay_ui(); + break; + case track_tab::turnout: + if (track != nullptr && track->eType == tt_Switch) + render_turnout_ui(); + else + render_switch_ui(); + break; + case track_tab::straights: + render_straight_ui(); + if (ImGui::CollapsingHeader(STR_C("Straights in the scenery"))) + render_straights_ui(); + break; + case track_tab::route: + render_route_ui(); + break; + case track_tab::profile: render_profile_body(); break; + case track_tab::speed: + case track_tab::joints: + case track_tab::infra: + ImGui::TextDisabled("%s", STR_C("What the check finds is listed in its window")); + break; + case track_tab::lineside: render_lineside_ui(); break; + case track_tab::signals: render_signal_ui(); break; + case track_tab::turntable: render_turntable_ui(); break; + default: + render_track_set_ui(); + render_track_spread_ui(); + break; + } +} + +void editor_mode::render_track_selection() +{ + auto *track{selected_track()}; + if (track == nullptr) + { + ImGui::TextDisabled("%s", STR_C("LMB on a path in the view selects it")); + return; + } + char const *type = track->eType == tt_Normal ? "normal" : track->eType == tt_Switch ? "switch" : track->eType == tt_Cross ? "cross" : track->eType == tt_Table ? "turntable" : track->eType == tt_Tributary ? "tributary" : "unknown"; + ImGui::TextUnformatted((track->name().empty() ? std::string{"(noname)"} : track->name()).c_str()); + ImGui::TextDisabled("%s, %.2f m", STR_C(type), track->Length()); + // the parameters of the path belong to the select tool, the other tools show the path they work with + if (m_track_tab == track_tab::path) + render_path_ui(); +} + +void editor_mode::render_track_guide() +{ + auto const hints{track_key_hints(false)}; + auto *drawlist{ImGui::GetWindowDrawList()}; + auto const &style{ImGui::GetStyle()}; + drawlist->ChannelsSplit(2); + drawlist->ChannelsSetCurrent(1); + auto const origin{ImGui::GetCursorScreenPos()}; + ImGui::SetCursorScreenPos(ImVec2(origin.x + 8.0f, origin.y + 6.0f)); + ImGui::BeginGroup(); + auto const mode{track_mode_name()}; + ImGui::TextColored(ImVec4(0.55f, 0.8f, 1.0f, 1.0f), "%s", mode.c_str()); + ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize("(?)").x - 8.0f); + ImGui::TextDisabled("(?)"); + if (ImGui::IsItemHovered()) + { + std::string text; + for (auto const &hint : track_key_hints(true)) + text += std::string{hint.key} + " " + hint.action + "\n"; + ImGui::SetTooltip("%s", text.c_str()); + } + float keywidth{0.0f}; + for (auto const &hint : hints) + keywidth = std::max(keywidth, ImGui::CalcTextSize(hint.key).x); + auto const left{ImGui::GetCursorPosX()}; + for (auto const &hint : hints) + { + ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.25f, 1.0f), "%s", hint.key); + ImGui::SameLine(left + keywidth + 12.0f); + ImGui::PushTextWrapPos(ImGui::GetWindowContentRegionMax().x - 8.0f); + ImGui::TextUnformatted(hint.action.c_str()); + ImGui::PopTextWrapPos(); + } + if (false == m_track_mode_notice.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", m_track_mode_notice.c_str()); + ImGui::EndGroup(); + auto const bottom{ImGui::GetItemRectMax().y + 6.0f}; + drawlist->ChannelsSetCurrent(0); + drawlist->AddRectFilled(origin, ImVec2(ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x, bottom), IM_COL32(40, 70, 110, 110), style.FrameRounding + 3.0f); + drawlist->ChannelsMerge(); + ImGui::SetCursorScreenPos(ImVec2(origin.x, bottom + style.ItemSpacing.y)); + ImGui::Dummy(ImVec2(0.0f, 0.0f)); +} + +void editor_mode::render_track_name(TTrack &Track) +{ + auto &edit{m_track_name}; + auto const current{Track.name() == "none" ? std::string{} : Track.name()}; + auto const sync = [&]() { + edit.track = &Track; + edit.synced = current; + std::snprintf(edit.text, sizeof(edit.text), "%s", current.c_str()); + edit.uses = editor_track::uses_of_name(Track); + }; + if (edit.track != &Track) + { + sync(); + edit.status.clear(); + } + else if (edit.synced != current && edit.synced == edit.text) + sync(); + + std::string why; + if (false == scene::Layers.editable(&Track, &why)) + { + ImGui::Text(STR_C("Name: %s"), current.empty() ? STR_C("(no name)") : current.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled(STR_C("(can't be changed: %s)"), why.c_str()); + return; + } + auto const apply = [&]() { + std::string name{edit.text}; + std::string reason; + if (false == editor_track::name_valid(Track, name, reason)) + return; + auto const before{editor_track::capture(Track)}; + editor_track::rename(Track, name); + push_track_snapshot({{&Track, before}}); + edit.status = format(STR_C("Renamed from %s"), current.empty() ? STR_C("(no name)") : current.c_str()); + sync(); + }; + ImGui::SetNextItemWidth(220.f); + if (ImGui::InputTextWithHint(STR_C("Name##trackname"), STR_C("(no name)"), edit.text, sizeof(edit.text), ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CharsNoBlank)) + apply(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Enter renames the path, Esc while typing takes the text back.\nThe events, memory cells and the map of the game find the path by its name")); + std::string typed{edit.text}; + if (typed == edit.synced) + { + if (false == edit.status.empty()) + { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.f), "%s", edit.status.c_str()); + } + return; + } + std::string reason; + TTrack *owner{nullptr}; + auto const valid{editor_track::name_valid(Track, typed, reason, &owner)}; + ImGui::SameLine(); + if (valid) + { + auto const warned{false == edit.uses.events.empty() || false == edit.uses.loose.empty()}; + if (ImGui::SmallButton(warned ? STR_C("Rename anyway") : STR_C("Rename"))) + apply(); + } + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Keep the old"))) + { + std::snprintf(edit.text, sizeof(edit.text), "%s", edit.synced.c_str()); + return; + } + if (false == valid) + { + ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", STR_C(reason.c_str())); + if (owner != nullptr) + { + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Look at it"))) + focus_track(*owner, 0, owner->location()); + } + return; + } + if (typed != edit.text) + ImGui::TextDisabled(STR_C("Saved as %s: the scenery keeps the names in lower case"), typed.c_str()); + if (edit.uses.cells > 0 || edit.uses.includes > 0) + ImGui::TextDisabled(STR_C("The new name goes to %d memory cell(s) and %d include(s), the drives of a switch among them"), edit.uses.cells, edit.uses.includes); + auto const listed = [](std::vector const &Names) { + std::string result; + for (std::size_t i = 0; i < Names.size() && i < 4; ++i) + result += (i > 0 ? ", " : "") + Names[i]; + if (Names.size() > 4) + result += format(" (+%d)", static_cast(Names.size() - 4)); + return result; + }; + if (false == edit.uses.events.empty()) + ImGui::TextColored(ImVec4(1.f, 0.75f, 0.3f, 1.f), STR_C("These events aim at the path by the old name and the editor doesn't write them: %s"), listed(edit.uses.events).c_str()); + if (false == edit.uses.loose.empty()) + ImGui::TextColored(ImVec4(1.f, 0.75f, 0.3f, 1.f), STR_C("These events belong to the path by the old name: %s"), listed(edit.uses.loose).c_str()); + if (false == edit.uses.events.empty() || false == edit.uses.loose.empty()) + ImGui::TextDisabled("%s", STR_C("They work until the scenery is loaded again; then they need the new name in their files")); +} + +void editor_mode::render_turnout_ui() +{ + auto *track{selected_track()}; + if (track == nullptr || track->eType != tt_Switch || track->m_paths.size() < 2) + return; + auto const &main{track->m_paths[0]}; + auto const &diverging{track->m_paths[1]}; + auto const origin{main.points[segment_data::point::start]}; + auto const chord{plan_of(main.points[segment_data::point::end] - origin)}; + auto const &control2{diverging.points[segment_data::point::control2]}; + auto const control{glm::length(plan_of(control2)) > 1e-6 ? control2 : diverging.points[segment_data::point::start] - diverging.points[segment_data::point::end]}; + if (glm::length(chord) < 1e-3 || glm::length(plan_of(control)) < 1e-6) + { + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), STR_C("The geometry of this switch is degenerate, it can't be edited here")); + return; + } + glm::dvec2 const axis{glm::normalize(chord)}; + glm::dvec2 const normal{-axis.y, axis.x}; + auto const length{glm::dot(chord, axis)}; + glm::dvec2 const end{diverging.points[segment_data::point::end].x - origin.x, diverging.points[segment_data::point::end].z - origin.z}; + auto const offset{glm::dot(end, normal)}; + glm::dvec2 const tangent{glm::normalize(glm::dvec2{-control.x, -control.z})}; + auto const angle{std::atan2(std::abs(tangent.x * axis.y - tangent.y * axis.x), glm::dot(tangent, axis))}; + ImGui::Text(STR_C("Length %.3f m, diverging %s, end offset %.3f m"), length, offset > 0.0 ? STR_C("left") : STR_C("right"), std::abs(offset)); + render_track_name(*track); + if ((simulation::Events.FindEvent(track->name() + "+") != nullptr && simulation::Events.FindEvent(track->name() + "-") != nullptr) || (m_track_name.track == track && m_track_name.uses.includes > 0)) + ImGui::TextDisabled(STR_C("Drive: events %s+ and %s- switch it, also in the map"), track->name().c_str(), track->name().c_str()); + else + { + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", STR_C("No drive: the switch can't be switched during the game")); + render_switch_drive_choice(); + if (m_switch.drive >= 0 && ImGui::Button(STR_C("Put the drive by the switch"))) + place_switch_drive(*track); + } + if (false == m_switch.status.empty()) + ImGui::TextWrapped("%s", m_switch.status.c_str()); + if (angle > 1e-6) + ImGui::Text(STR_C("Angle 1:%.2f (%.4f deg), R in the entry %.0f m"), 1.0 / std::tan(angle), glm::degrees(angle), diverging.radius); + + auto &tool{m_switch}; + if (false == tool.collected) + { + tool.templates = editor_track::standard_switch_templates(); + tool.collected = true; + } + if (tool.templates.empty()) + return; + m_turnout_template = std::clamp(m_turnout_template, 0, static_cast(tool.templates.size()) - 1); + if (tool.templates[m_turnout_template].double_slip) + { + auto const ordinary{std::find_if(tool.templates.begin(), tool.templates.end(), [](auto const &Template) { return false == Template.double_slip; })}; + if (ordinary == tool.templates.end()) + return; + m_turnout_template = static_cast(ordinary - tool.templates.begin()); + } + ImGui::Separator(); + ImGui::PushItemWidth(-1.0f); + if (ImGui::BeginCombo("##turnouttemplate", tool.templates[m_turnout_template].label.c_str())) + { + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + if (tool.templates[i].double_slip) + continue; + auto const label{(tool.templates[i].source != nullptr ? "Scenery: " : "PLK: ") + tool.templates[i].label}; + if (ImGui::Selectable(label.c_str(), i == m_turnout_template)) + m_turnout_template = i; + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + auto const replace = [&](editor_track::switch_template const &Template, int const Side) { + auto const grade{length > 0.0 ? (main.points[segment_data::point::end].y - origin.y) / length : 0.0}; + auto paths{editor_track::place_switch(Template, origin, axis, Side, grade)}; + if (paths.size() < 2) + return; + for (int i = 0; i < 2; ++i) + if (paths[i].rolls[0] == 0.f && paths[i].rolls[1] == 0.f) + paths[i].rolls = track->m_paths[i].rolls; + auto const joints{editor_track::joints(*track)}; + std::vector> states{{track, editor_track::capture(*track)}}; + track->m_paths.assign(paths.begin(), paths.begin() + 2); + // the neighbours follow the ends which moved a little, those which went far are left loose + std::vector following; + std::vector changed{track}; + std::string loose; + for (auto const &joint : joints) + { + auto const &theirs{joint.other->m_paths[joint.theirs.path]}; + auto const shift{glm::distance(editor_track::point_position(*track, joint.own), editor_track::point_position(*joint.other, joint.theirs))}; + if (shift > 0.3 * bezier{theirs}.plan_length()) + { + loose += (loose.empty() ? "" : ", ") + (joint.other->name().empty() ? std::string{"(noname)"} : joint.other->name()); + continue; + } + following.push_back(joint); + if (std::find(changed.begin(), changed.end(), joint.other) == changed.end()) + { + changed.push_back(joint.other); + states.emplace_back(joint.other, editor_track::capture(*joint.other)); + } + } + push_track_snapshot(std::move(states)); + editor_track::follow(*track, following); + editor_track::commit(changed); + if (false == loose.empty()) + ui()->set_status(STR_C("Too far from the new ends of the switch, left unjoined: ") + loose, true); + }; + int const side{offset > 0.0 ? 1 : -1}; + if (ImGui::Button(STR_C("Replace the geometry with the template"))) + replace(tool.templates[m_turnout_template], side); + item_tooltip("Keeps the start and the direction of the switch, takes the shape of the chosen template"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Flip the side"))) + { + editor_track::switch_template own; + own.source = track; + for (int i = 0; i < 2; ++i) + { + auto &local{own.local[i]}; + local = track->m_paths[i]; + auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), normal) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; }; + local.points[segment_data::point::start] = tolocal(track->m_paths[i].points[segment_data::point::start] - origin); + local.points[segment_data::point::end] = tolocal(track->m_paths[i].points[segment_data::point::end] - origin); + local.points[segment_data::point::control1] = tolocal(track->m_paths[i].points[segment_data::point::control1]); + local.points[segment_data::point::control2] = tolocal(track->m_paths[i].points[segment_data::point::control2]); + if (side < 0) + local.rolls = {-local.rolls[0], -local.rolls[1]}; + } + replace(own, -side); + } + item_tooltip("Mirrors the switch: the diverging track goes to the other side"); + ImGui::Separator(); + render_path_parameters(*track); +} + +void editor_mode::render_path_parameters(TTrack &Track) +{ + auto *track{&Track}; + auto const before = editor_track::capture(*track); + enum class rebuild { none, parameters, geometry }; + auto const finish_edit = [&](rebuild const Rebuild) { + if (ImGui::IsItemActivated()) + m_track_field_before = before; + if (ImGui::IsItemDeactivatedAfterEdit()) + { + push_track_snapshot({{track, m_track_field_before}}); + switch (Rebuild) + { + case rebuild::geometry: editor_track::commit({track}); break; + case rebuild::parameters: editor_track::commit_parameters(*track); break; + default: track->mark_dirty(); break; + } + } + }; + auto const apply_now = [&](rebuild const Rebuild) { + push_track_snapshot({{track, before}}); + if (Rebuild == rebuild::parameters) + editor_track::commit_parameters(*track); + else + track->mark_dirty(); + }; + + ImGui::PushItemWidth(120.0f); + double velocity = editor_track::velocity(*track); + if (ImGui::InputDouble(STR_C("Velocity (km/h, -1: none)"), &velocity, 0.0, 0.0, "%.1f")) + editor_track::velocity(*track, velocity); + item_tooltip("Speed limit of the path for the AI drivers and the timetable, -1: no limit of its own"); + finish_edit(rebuild::none); + char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; + int environment = std::clamp(static_cast(track->eEnvironment), 0, 5); + if (ImGui::Combo("Environment", &environment, environments, IM_ARRAYSIZE(environments))) + { + track->eEnvironment = static_cast(environment); + apply_now(rebuild::none); + } + item_tooltip("Surroundings of the path, they affect the sound of the passing vehicles"); + + ImGui::PopItemWidth(); + + if (ImGui::CollapsingHeader(STR_C("Ride and sound"))) + { + ImGui::PushItemWidth(120.0f); + if (ImGui::InputFloat(STR_C("Friction"), &track->fFriction, 0.0f, 0.0f, "%.3f")) + track->fFriction = std::max(0.0f, track->fFriction); + item_tooltip("Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip"); + finish_edit(rebuild::none); + ImGui::InputFloat(STR_C("Sound distance"), &track->fSoundDistance, 0.0f, 0.0f, "%.1f"); + item_tooltip("Distance between the rail joints in metres, sets the rhythm of the wheel clatter; -1: the default"); + finish_edit(rebuild::none); + ImGui::InputInt(STR_C("Quality"), &track->iQualityFlag, 0); + item_tooltip("Quality of the track, 20 is the nominal one; lower values give a rougher ride"); + finish_edit(rebuild::none); + int damage = track->iDamageFlag; + if (ImGui::InputInt(STR_C("Damage flags"), &damage, 0)) + editor_track::damage(*track, damage); + item_tooltip("0: track in order; any value adds jolts, 128: the vehicles derail on the path"); + finish_edit(rebuild::none); + + if (ImGui::InputFloat(STR_C("Width"), &track->fTrackWidth, 0.0f, 0.0f, "%.3f")) + track->fTrackWidth = std::max(0.01f, track->fTrackWidth); + item_tooltip("Gauge of the track (1.435 m for the standard gauge), width of the surface for the roads"); + finish_edit(rebuild::parameters); + ImGui::PopItemWidth(); + } + if (track->m_visible && ImGui::CollapsingHeader(STR_C("Textures and trackbed"))) + { + material_handle *materials[] = {&track->m_material1, &track->m_material2, track->SwitchExtension ? &track->SwitchExtension->m_material3 : nullptr}; + char const *pathlabels[] = {"Texture 1 (rails/surface)", "Texture 2 (trackbed/side)", "Trackbed (switch)"}; + char const *switchlabels[] = {"Rails P1-P2", "Rails P3-P4", "Trackbed"}; + auto const &labels{track->eType == tt_Switch ? switchlabels : pathlabels}; + if (false == ImGui::IsAnyItemActive()) + { + for (int i = 0; i < 3; ++i) + { + if (m_track_material_edited[i] != nullptr) + continue; + auto const name = materials[i] ? editor_track::material_name(*materials[i]) : std::string{}; + std::strncpy(m_track_materials[i].data(), name.c_str(), m_track_materials[i].size() - 1); + m_track_materials[i].back() = '\0'; + } + } + for (int i = 0; i < 3; ++i) + { + if (materials[i] == nullptr || (i == 2 && track->eType != tt_Switch)) + continue; + ImGui::PushItemWidth(-160.0f); + if (ImGui::InputText(labels[i], m_track_materials[i].data(), m_track_materials[i].size())) + m_track_material_edited[i] = track; + if (ImGui::IsItemDeactivated() && m_track_material_edited[i] != nullptr) + { + auto const *edited{m_track_material_edited[i]}; + m_track_material_edited[i] = nullptr; + if (edited == track && m_track_materials[i].data() != editor_track::material_name(*materials[i])) + { + *materials[i] = editor_track::fetch_material(m_track_materials[i].data()); + apply_now(rebuild::parameters); + } + } + ImGui::PopItemWidth(); + } + ImGui::TextDisabled(STR_C("The texture changes on Enter or leaving the field, \"none\" removes it")); + + ImGui::PushItemWidth(120.0f); + if (ImGui::InputFloat(STR_C("Texture length"), &track->fTexLength, 0.0f, 0.0f, "%.2f")) + track->fTexLength = std::max(0.01f, track->fTexLength); + item_tooltip("Length in metres after which the texture of the rails and the trackbed repeats"); + finish_edit(rebuild::parameters); + float height = editor_track::texture_height(*track); + if (ImGui::InputFloat(STR_C("Trackbed height"), &height, 0.0f, 0.0f, "%.3f")) + editor_track::texture_height(*track, height); + item_tooltip("Height of the trackbed edge below the rail top, in metres"); + finish_edit(rebuild::parameters); + ImGui::InputFloat(STR_C("Trackbed width"), &track->fTexWidth, 0.0f, 0.0f, "%.3f"); + item_tooltip("Width of the trackbed outside of the rail, in metres"); + finish_edit(rebuild::parameters); + ImGui::InputFloat(STR_C("Trackbed slope"), &track->fTexSlope, 0.0f, 0.0f, "%.3f"); + item_tooltip("Horizontal reach of the slope at the outer edge of the trackbed, in metres"); + finish_edit(rebuild::parameters); + ImGui::PopItemWidth(); + } + render_path_circuits(*track); +} + +void editor_mode::render_path_circuits(TTrack &Track) +{ + auto *track{&Track}; + auto const copy = [](std::array &Buffer, std::string const &Text) { + std::strncpy(Buffer.data(), Text.c_str(), Buffer.size() - 1); + Buffer.back() = '\0'; + }; + auto const isolated_text = [&]() { + std::string text; + for (auto const *isolated : track->Isolated) + text += (text.empty() ? "" : " ") + isolated->asName; + return text; + }; + if (m_track_names_for != track && false == ImGui::IsAnyItemActive()) + { + m_track_names_for = track; + copy(m_track_isolated, isolated_text()); + copy(m_track_sleeper_model, track->m_sleeper_model_name); + copy(m_track_sleeper_skin, track->m_sleeper_skin_name.empty() ? std::string{"none"} : track->m_sleeper_skin_name); + m_track_event[0] = '\0'; + m_track_events_missing.clear(); + } + // one undo step for the change made by the widget just used + auto const changed = [&](editor_track::state const &Before, bool const Geometry) { + push_track_snapshot({{track, Before}}); + if (Geometry) + editor_track::commit_parameters(*track); + else + track->mark_dirty(); + }; + + if (false == ImGui::CollapsingHeader(STR_C("Track circuit, overhead line, sleepers, events"))) + return; + auto const before{editor_track::capture(*track)}; + + ImGui::PushItemWidth(-160.0f); + ImGui::InputText(STR_C("Track circuits"), m_track_isolated.data(), m_track_isolated.size()); + item_tooltip("Names of the track circuits (isolated) the path belongs to, separated by spaces.\n" + "A name which doesn't exist yet makes a new circuit. Changes on leaving the field"); + if (ImGui::IsItemDeactivated() && m_track_isolated.data() != isolated_text()) + { + if (false == track->Dynamics.empty()) + { + ui()->set_status(STR_C("The track circuits can't change while vehicles stand on the path"), true); + copy(m_track_isolated, isolated_text()); + } + else + { + std::istringstream names{m_track_isolated.data()}; + std::vector list; + for (std::string name; names >> name;) + list.push_back(name); + editor_track::isolated(*track, list); + changed(before, false); + copy(m_track_isolated, isolated_text()); + } + } + ImGui::PopItemWidth(); + + int overheadmode{track->fOverhead < 0.f ? 0 : track->fOverhead == 0.f ? 1 : 2}; + ImGui::PushItemWidth(220.0f); + if (ImGui::Combo("Overhead line", &overheadmode, "Normal\0No current, coasting\0Pantographs lowered, speed limit\0")) + { + editor_track::overhead(*track, overheadmode == 0 ? -1.f : overheadmode == 1 ? 0.f : std::max(1.f, track->fOverhead > 0.f ? track->fOverhead : 40.f)); + changed(before, false); + } + ImGui::PopItemWidth(); + item_tooltip("State of the overhead line over the path: the trains coast through a section with no current,\nor lower the pantographs and keep to the speed limit given"); + if (track->fOverhead > 0.f) + { + ImGui::PushItemWidth(120.0f); + float limit{track->fOverhead}; + if (ImGui::InputFloat(STR_C("Speed with the pantographs lowered (km/h)"), &limit, 0.0f, 0.0f, "%.0f")) + editor_track::overhead(*track, std::max(1.f, limit)); + if (ImGui::IsItemActivated()) + m_track_field_before = before; + if (ImGui::IsItemDeactivatedAfterEdit()) + changed(m_track_field_before, false); + ImGui::PopItemWidth(); + } + + ImGui::Separator(); + bool sleepers{track->m_sleeper_enabled}; + if (ImGui::Checkbox(STR_C("Sleeper models"), &sleepers)) + { + if (sleepers && m_track_sleeper_model[0] == '\0') + { + ui()->set_status(STR_C("Give the sleeper model first"), true); + } + else + { + track->m_sleeper_enabled = sleepers; + track->m_sleeper_model_name = m_track_sleeper_model.data(); + changed(before, true); + } + } + item_tooltip("Repeats a model along the path, the sleepers in place of the drawn trackbed"); + ImGui::PushItemWidth(-160.0f); + auto const name_field = [&](char const *Label, std::array &Buffer, std::string &Value, char const *Tooltip) { + ImGui::InputText(Label, Buffer.data(), Buffer.size()); + item_tooltip(Tooltip); + std::string typed{Buffer.data()}; + if (&Value == &track->m_sleeper_skin_name && typed == "none") + typed.clear(); + if (ImGui::IsItemDeactivated() && typed != Value) + { + Value = typed; + changed(before, track->m_sleeper_enabled); + } + }; + name_field(STR_C("Sleeper model"), m_track_sleeper_model, track->m_sleeper_model_name, STR_C("Path of the .e3d model, as in the scenery file")); + name_field(STR_C("Sleeper skin"), m_track_sleeper_skin, track->m_sleeper_skin_name, STR_C("Replaceable skin of the model, \"none\": the one of the model")); + ImGui::PopItemWidth(); + ImGui::PushItemWidth(120.0f); + auto const number_field = [&](char const *Label, float &Value, char const *Format, char const *Tooltip) { + ImGui::InputFloat(Label, &Value, 0.0f, 0.0f, Format); + item_tooltip(Tooltip); + if (ImGui::IsItemActivated()) + m_track_field_before = before; + if (ImGui::IsItemDeactivatedAfterEdit()) + { + track->m_sleeper_frequency = std::max(0.1f, track->m_sleeper_frequency); + changed(m_track_field_before, track->m_sleeper_enabled); + } + }; + number_field(STR_C("Sleeper spacing (m)"), track->m_sleeper_frequency, "%.3f", STR_C("Distance between the sleepers along the path")); + ImGui::PopItemWidth(); + ImGui::PushItemWidth(200.0f); + ImGui::InputFloat3(STR_C("Sleeper offset (m)"), &track->m_sleeper_offset.x, "%.3f"); + item_tooltip("Shift of each model: sideways, along the path, up"); + if (ImGui::IsItemActivated()) + m_track_field_before = before; + if (ImGui::IsItemDeactivatedAfterEdit()) + changed(m_track_field_before, track->m_sleeper_enabled); + ImGui::PopItemWidth(); + ImGui::PushItemWidth(120.0f); + number_field(STR_C("Trackbed shift (m)"), track->m_sleeper_ballast_z, "%.3f", STR_C("Vertical shift of the drawn trackbed under the sleepers, negative lowers it")); + ImGui::PopItemWidth(); + + ImGui::Separator(); + ImGui::TextUnformatted(STR_C("Events")); + item_tooltip("event0: a vehicle with a crew stands on the path, event1: runs along it towards its start, event2: towards its end.\n" + "eventall0/1/2: the same for any vehicle"); + auto const editable_events{track->Dynamics.empty()}; + for (int list = 0; list < 6; ++list) + { + auto &events{editor_track::events(*track, list)}; + for (std::size_t i = 0; i < events.size(); ++i) + { + auto const &event{events[i]}; + // bound by the name of the path, not written in the file + if (event.first.empty()) + { + ImGui::TextDisabled(STR_C("%s %s (by the name of the path)"), editor_track::event_keyword(list), event.second != nullptr ? STR_C("auto") : "?"); + continue; + } + ImGui::PushID(list * 1000 + static_cast(i)); + if (ImGui::SmallButton("x") && editable_events) + { + events.erase(events.begin() + i); + editor_track::bind_events(*track, m_track_events_missing); + changed(before, false); + ImGui::PopID(); + break; + } + ImGui::SameLine(); + if (event.second == nullptr) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("%s %s (no such event)"), editor_track::event_keyword(list), event.first.c_str()); + else + ImGui::Text("%s %s", editor_track::event_keyword(list), event.first.c_str()); + ImGui::PopID(); + } + } + ImGui::PushItemWidth(100.0f); + ImGui::Combo("##eventlist", &m_track_event_list, "event0\0event1\0event2\0eventall0\0eventall1\0eventall2\0"); + ImGui::PopItemWidth(); + ImGui::SameLine(); + ImGui::PushItemWidth(-60.0f); + auto const entered{ImGui::InputText("##eventname", m_track_event.data(), m_track_event.size(), ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::PopItemWidth(); + ImGui::SameLine(); + if ((ImGui::Button(STR_C("Add")) || entered) && m_track_event[0] != '\0') + { + if (false == editable_events) + { + ui()->set_status(STR_C("The events can't change while vehicles stand on the path"), true); + } + else + { + editor_track::events(*track, m_track_event_list).emplace_back(m_track_event.data(), nullptr); + editor_track::bind_events(*track, m_track_events_missing); + changed(before, false); + m_track_event[0] = '\0'; + } + } + item_tooltip("Name of the event to add to the chosen list. An event which doesn't exist is kept, and reported"); + if (false == m_track_events_missing.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.3f, 1.0f), STR_C("No such events: %s"), m_track_events_missing.c_str()); +} + +void editor_mode::render_path_ui() +{ + auto *track = selected_track(); + if (track == nullptr) + { + ImGui::TextDisabled(STR_C("No path selected")); + return; + } + + if (false == editor_track::is_supported(*track)) + { + ImGui::TextDisabled(STR_C("Editing of this path type isn't supported")); + return; + } + render_track_name(*track); + std::string reason; + bool const geometry = editor_track::can_edit_geometry(*track, reason); + if (false == geometry) + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.2f, 1.0f), STR_C("Geometry locked: %s"), reason.c_str()); + + auto const before = editor_track::capture(*track); + enum class rebuild { none, parameters, geometry }; + auto const finish_edit = [&](rebuild const Rebuild) { + if (ImGui::IsItemActivated()) + m_track_field_before = before; + if (ImGui::IsItemDeactivatedAfterEdit()) + { + push_track_snapshot({{track, m_track_field_before}}); + switch (Rebuild) + { + case rebuild::geometry: editor_track::commit({track}); break; + case rebuild::parameters: editor_track::commit_parameters(*track); break; + default: track->mark_dirty(); break; + } + } + }; + + if (ImGui::CollapsingHeader(STR_C("Geometry"), ImGuiTreeNodeFlags_DefaultOpen)) + { + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + ImGui::PushID(i); + if (track->m_paths.size() > 1) + ImGui::TextDisabled(STR_C("Path %d"), i + 1); + for (auto const kind : kPointKinds) + { + editor_track::point_ref const point{i, kind}; + if (kind != editor_track::point_kind::start) + ImGui::SameLine(); + std::string label = point_label(kind); + if (editor_track::is_end(kind)) + label += editor_track::is_connected(*track, point) ? " (+)" : " (-)"; + if (ImGui::Selectable(label.c_str(), m_track_point == point, 0, ImVec2(90.0f, 0.0f))) + m_track_point = (m_track_point == point ? editor_track::point_ref{} : point); + } + item_tooltip("(+) joined with another track, (-) a free end"); + + auto &path = track->m_paths[i]; + if (false == geometry) + { + auto const &start = path.points[segment_data::point::start]; + auto const &end = path.points[segment_data::point::end]; + ImGui::TextDisabled(STR_C("Start (%.2f, %.2f, %.2f) End (%.2f, %.2f, %.2f)"), start.x, start.y, start.z, end.x, end.y, end.z); + ImGui::PopID(); + continue; + } + ImGui::PushItemWidth(-110.0f); + if (m_track_point.valid() && m_track_point.path == i) + { + auto const kind{m_track_point.kind}; + if (editor_track::is_end(kind)) + { + auto position = path.points[kind == editor_track::point_kind::start ? segment_data::point::start : segment_data::point::end]; + if (ImGui::DragScalarN("x, y, z##point", ImGuiDataType_Double, glm::value_ptr(position), 3, 0.01f, nullptr, nullptr, "%.3f")) + editor_track::move_point(*track, m_track_point, position); + item_tooltip("Drag to move, Ctrl+click to type the value"); + finish_edit(rebuild::geometry); + } + else + { + auto &vector{path.points[kind == editor_track::point_kind::control1 ? segment_data::point::control1 : segment_data::point::control2]}; + ImGui::DragScalarN(STR_C("vector##point"), ImGuiDataType_Double, glm::value_ptr(vector), 3, 0.01f, nullptr, nullptr, "%.3f"); + item_tooltip("Control vector, relative to its end; zero for a straight"); + finish_edit(rebuild::geometry); + } + } + if (ImGui::TreeNode(STR_C("Cant and radius"))) + { + ImGui::DragFloat2(STR_C("Roll (deg)"), path.rolls.data(), 0.01f, -15.0f, 15.0f, "%.2f"); + item_tooltip("Cant at the start and at the end as the roll angle of the track"); + finish_edit(rebuild::geometry); + ImGui::InputFloat(STR_C("Radius"), &path.radius, 0.0f, 0.0f, "%.1f"); + item_tooltip("Radius stored with the path in the scenery; the shape itself comes from the control vectors"); + finish_edit(rebuild::geometry); + if (ImGui::Button(STR_C("Straighten"))) + { + push_track_snapshot({{track, before}}); + path.points[segment_data::point::control1] = glm::dvec3{}; + path.points[segment_data::point::control2] = glm::dvec3{}; + path.radius = 0.0f; + editor_track::commit({track}); + } + item_tooltip("Removes the control vectors and the radius, the path becomes a straight between its ends"); + ImGui::TreePop(); + } + ImGui::PopItemWidth(); + ImGui::PopID(); + } + if (geometry && ImGui::TreeNode(STR_C("Snapping of the ends"))) + { + ImGui::PushItemWidth(120.0f); + ImGui::DragFloat(STR_C("Snap radius (m)"), &m_track_snap_radius, 0.05f, 0.0f, 50.0f, "%.2f"); + item_tooltip("A dragged free end within this distance of another free end joins it on release"); + ImGui::PopItemWidth(); + ImGui::Checkbox(STR_C("Align with snapped path"), &m_track_align_tangent); + item_tooltip("On joining, the end also takes over the direction of the other path, so the joint has no kink"); + ImGui::Checkbox(STR_C("Drag connected ends"), &m_track_drag_connected); + item_tooltip("Dragging a joined end, or moving and turning the whole path or switch,\nmoves the ends of the neighbouring paths with it"); + ImGui::TreePop(); + } + } + render_path_parameters(*track); +} + + +bool editor_mode::route_active() const +{ + return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::route && false == m_route.chain.tracks.empty(); +} + +void editor_mode::route_recommend(alignment::vertex &Vertex) const +{ + auto const recommended{alignment::recommend(m_route.design.speed, Vertex.radius, m_route.design.norms)}; + Vertex.cant = recommended.cant; + Vertex.transition_in = recommended.transition; + Vertex.transition_out = recommended.transition; +} + +void editor_mode::route_bind_ends() +{ + auto const &chain{m_route.chain}; + auto &design{m_route.design}; + design.start = chain.start; + design.end = chain.end; + design.start_direction = glm::normalize(plan_of(chain.start_direction)); + design.end_direction = glm::normalize(plan_of(chain.end_direction)); + design.start_grade = grade_of(chain.start_direction); + design.end_grade = grade_of(chain.end_direction); + design.start_radius = chain.start_radius; + design.end_radius = chain.end_radius; + design.start_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, false, m_straights.tolerance)); + design.end_reserve = editor_track::run_reserve(editor_track::straight_beyond(chain, true, m_straights.tolerance)); +} + +void editor_mode::route_reset() +{ + auto &route{m_route}; + route.chain = {}; + route.error.clear(); + route.status.clear(); + route.vertex = -1; + route.grip = -1; + route.result = {}; + if (route.from == nullptr || route.to == nullptr) + return; + if (false == editor_track::find_chain(route.from, route.to, route.chain, route.error, true)) + return; + if (false == route.chain.switches.empty()) + route.status = format(STR_C("The fragment runs through %zu switches along their main tracks. They move as a whole with the line, " + "so they have to fall on its straights"), + route.chain.switches.size()); + + auto const &chain{route.chain}; + alignment::design design; + design.norms = route.design.norms; + design.shape = route.design.shape; + design.transition_pieces = route.design.transition_pieces; + design.speed = chain.velocity > 0.0 ? chain.velocity : 100.0; + route.design = design; + route_bind_ends(); + design = route.design; + + glm::dvec2 intersection; + if (false == alignment::collinear(design)) + { + alignment::vertex vertex; + vertex.radius = chain.radius > 0.0 ? std::max(10.0, std::round(chain.radius / 10.0) * 10.0) : 1000.0; + route_recommend(vertex); + if (alignment::tangent_intersection(design, intersection)) + { + route.design.vertices.push_back(vertex); + } + else + { + auto const distance{plan_distance(design.start, design.end)}; + vertex.offset = distance / 3.0; + route.design.vertices.push_back(vertex); + route.design.vertices.push_back(vertex); + } + } + route_update(); +} + +bool editor_mode::route_from_curve(TTrack &Track) +{ + editor_track::curve curve; + if (false == editor_track::find_curve(Track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + { + m_route.chain = {}; + m_route.result = {}; + m_route.vertex = m_route.grip = -1; + m_route.error = STR_C("No curve here: LMB on a curve"); + return false; + } + m_route.from = curve.from; + m_route.to = curve.to; + route_reset(); + if (m_route.chain.tracks.empty()) + return false; + auto &design{m_route.design}; + auto vertex{route_vertex_of(curve)}; + if (curve.reversals == 0) + { + design.vertices = {vertex}; + } + else + { + auto const distance{plan_distance(design.start, design.end)}; + vertex.offset = distance / 3.0; + vertex.compound = false; + vertex.reverse_turn = false; + design.vertices = {vertex, vertex}; + } + route_update(); + m_route.status = STR_C("Curve of ") + std::to_string(m_route.chain.tracks.size()) + STR_C(" paths, turning ") + std::to_string(static_cast(std::round(glm::degrees(curve.turn)))) + STR_C(" deg"); + return true; +} + +alignment::vertex editor_mode::route_vertex_of(editor_track::curve const &Curve) const +{ + alignment::vertex vertex; + vertex.radius = Curve.radius > 0.0 ? std::max(10.0, std::round(Curve.radius)) : 1000.0; + route_recommend(vertex); + if (Curve.transition_in > 0.0) + vertex.transition_in = std::round(Curve.transition_in); + if (Curve.transition_out > 0.0) + vertex.transition_out = std::round(Curve.transition_out); + if (Curve.cant > 0.0) + vertex.cant = std::round(Curve.cant); + vertex.reverse_turn = std::abs(Curve.turn) > glm::pi(); + compound_from(Curve, vertex); + return vertex; +} + +bool editor_mode::route_from_line(TTrack &Track) +{ + auto &route{m_route}; + editor_track::chain run; + if (false == editor_track::find_run(Track, kLineReach, run, route.error)) + return false; + route.from = run.tracks.front(); + route.to = run.tracks.back(); + route_reset(); + if (route.chain.tracks.empty()) + return false; + auto &design{route.design}; + auto const found{editor_track::recognize_line(route.chain, m_straights.tolerance, design.norms.gauge)}; + if (false == found.empty()) + { + design.vertices.clear(); + for (auto const &entry : found) + { + alignment::vertex vertex; + if (entry.kink) + { + vertex.radius = std::max(10.0, std::round(alignment::recommend(design.speed, 1000.0, design.norms).radius_min / 10.0) * 10.0); + vertex.transition_in = vertex.transition_out = vertex.cant = 0.0; + } + else + { + vertex = route_vertex_of(entry.shape); + } + vertex.position = entry.position; + design.vertices.push_back(vertex); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = std::max(0.0, glm::dot(design.vertices.front().position - plan_of(design.start), design.start_direction)); + design.vertices.back().offset = std::max(0.0, glm::dot(plan_of(design.end) - design.vertices.back().position, design.end_direction)); + } + route_update(); + } + route.status = format(STR_C("Line of %zu paths, %.0f m, %zu vertices"), route.chain.tracks.size(), route.chain.length, design.vertices.size()); + return true; +} + +void editor_mode::route_apply() +{ + auto &route{m_route}; + route.status.clear(); + if (false == route.result.valid || route.chain.tracks.empty()) + return; + editor_track::chain chain; + if (false == editor_track::find_chain(route.from, route.to, chain, route.error, true)) + return; + if (glm::distance(chain.start, route.chain.start) > 1e-3 || glm::distance(chain.end, route.chain.end) > 1e-3) + { + route.error = STR_C("The fragment changed since it was selected, select it again"); + return; + } + auto involved{chain.tracks}; + for (auto const &passage : chain.switches) + involved.push_back(passage.track); + if (false == tracks_editable(involved, route.error)) + return; + + std::vector> states; + std::vector created; + // takes back what was done before the paths were re-laid + auto const undo = [&]() { + for (auto *track : created) + editor_track::retire(*track); + for (auto entry = states.rbegin(); entry != states.rend(); ++entry) + { + editor_track::apply(*entry->first, entry->second); + editor_track::commit({entry->first}); + } + }; + auto *from{route.from}; + auto *to{route.to}; + auto const startextension{route.result.start_extension}; + auto const endextension{route.result.end_extension}; + if (startextension > 0.0 || endextension > 0.0) + { + auto const before{chain}; + if (startextension > 0.0) + from = editor_track::take_straight(editor_track::straight_beyond(before, false, m_straights.tolerance), from, before.start, route.result.start, states, created); + if (endextension > 0.0) + to = editor_track::take_straight(editor_track::straight_beyond(before, true, m_straights.tolerance), to, before.end, route.result.end, states, created); + std::string error; + auto const found{editor_track::find_chain(from, to, chain, error, true)}; + auto const startoff{found ? plan_distance(chain.start, route.result.start) : 0.0}; + auto const endoff{found ? plan_distance(chain.end, route.result.end) : 0.0}; + if (false == found || startoff > 5.0 || endoff > 5.0) + { + auto const startrun{editor_track::straight_beyond(before, false, m_straights.tolerance)}; + auto const endrun{editor_track::straight_beyond(before, true, m_straights.tolerance)}; + WriteLog(format("editor: cutting the straights failed: found %d, start %.2f m off (extension %.2f, run %zu paths %.2f m), end %.2f m off (extension %.2f, run %zu paths %.2f m), chain %zu paths, from %s, to %s, error '%s'", found ? 1 : 0, startoff, + startextension, startrun.tracks.size(), startrun.length, endoff, endextension, endrun.tracks.size(), endrun.length, chain.tracks.size(), from != nullptr ? from->name().c_str() : "-", to != nullptr ? to->name().c_str() : "-", error.c_str())); + undo(); + route.error = error.empty() ? format(STR_C("The adjoining straights can't be cut at the ends of the curve: the start misses by %.1f m, the end by %.1f m"), startoff, endoff) : error; + return; + } + if (false == tracks_editable(chain.tracks, route.error)) + { + undo(); + return; + } + } + for (auto *track : chain.tracks) + if (std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == track; })) + states.emplace_back(track, editor_track::capture(*track)); + std::vector relaid; + if (chain.switches.empty()) + { + auto pieces{alignment::pieces(route.result, route.design, chain.tracks.size())}; + editor_track::keep_heights(chain, pieces); + relaid = editor_track::relay(chain, pieces); + } + else if (false == route_relay_through_switches(chain, states, relaid)) + { + undo(); + return; + } + // the straights were cut along their own direction, the curve ends where the design says: the ends are brought together + auto const added{relaid.size()}; + created.insert(created.end(), relaid.begin(), relaid.end()); + push_track_snapshot(std::move(states), std::move(created)); + + route.from = from; + route.to = to; + editor_track::find_chain(route.from, route.to, route.chain, route.error, true); + if (startextension > 0.0 || endextension > 0.0) + { + route_bind_ends(); + auto &vertices{route.design.vertices}; + if (vertices.size() >= 2) + { + vertices.front().offset += startextension; + vertices.back().offset += endextension; + } + route_update(); + } + route.status = STR_C("Re-laid ") + std::to_string(chain.tracks.size()) + STR_C(" paths") + (added > 0 ? STR_C(", added ") + std::to_string(added) : std::string{}); + if (startextension > 0.0 || endextension > 0.0) + route.status += format(", the curve took over %.2f / %.2f m of the adjoining straights", startextension, endextension); + WriteLog("Editor: route design - " + route.status, logtype::generic); +} + +// the switches of the chain are moved as a whole onto the designed line, the regular paths between them re-laid +bool editor_mode::route_relay_through_switches(editor_track::chain const &Chain, std::vector> &States, std::vector &Created) +{ + auto &route{m_route}; + auto const &result{route.result}; + auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const plan = [](glm::dvec3 const &Vector) { return glm::normalize(plan_of(Vector)); }; + struct placement + { + TTrack *track; + double from, length; // chainage along the new line + glm::dvec3 entry; // of the main track, where the chain comes in + double angle; + }; + // chainages along the old line, the switches keep their distance from the nearer end of the chain + double before{0.0}; + std::vector starts; + std::size_t next{0}; + for (std::size_t i = 0; i <= Chain.tracks.size(); ++i) + { + for (; next < Chain.switches.size() && Chain.switches[next].before == i; ++next) + { + starts.push_back(before); + before += bezier{Chain.switches[next].track->m_paths.front()}.plan_length(); + } + if (i < Chain.tracks.size()) + before += bezier{Chain.tracks[i]->m_paths.front()}.plan_length(); + } + auto const oldlength{before}; + std::vector placements; + std::vector breaks; + for (std::size_t k = 0; k < Chain.switches.size(); ++k) + { + auto const &passage{Chain.switches[k]}; + auto const &main{passage.track->m_paths.front()}; + auto const length{bezier{main}.plan_length()}; + auto const from{starts[k] + length * 0.5 < oldlength * 0.5 ? starts[k] : result.length - (oldlength - starts[k])}; + if (from < 0.0 || from + length > result.length || (false == breaks.empty() && from < breaks.back() - 0.01)) + { + route.error = STR_C("The switch ") + name(passage.track) + STR_C(" doesn't fit on the designed line"); + return false; + } + auto const entry{main.points[passage.forward ? segment_data::point::start : segment_data::point::end]}; + auto const exit{main.points[passage.forward ? segment_data::point::end : segment_data::point::start]}; + bezier const curve{main}; + auto const direction{passage.forward ? plan(curve.first(0.0)) : -plan(curve.first(1.0))}; + auto const first{alignment::evaluate(result, from)}; + auto const last{alignment::evaluate(result, from + length)}; + auto const angle{signed_angle(direction, plan(first.direction))}; + // where the other end of the main track goes, set against the line + auto const moved{plan_of(first.position) + turned(plan_of(exit - entry), angle)}; + auto const departure{glm::distance(moved, plan_of(last.position))}; + if (departure > 0.02) + { + route.error = format(STR_C("The switch %s would leave the designed line by %.2f m at its other end: it lies in a curve of the design. " + "Move the vertices so that the switch falls on a straight"), + name(passage.track).c_str(), departure); + return false; + } + placements.push_back({passage.track, from, length, entry, angle}); + breaks.push_back(from); + breaks.push_back(from + length); + } + // the regular parts between the switches, one interval each, the switches taking the ones between + auto const parts{editor_track::chain_parts(Chain)}; + std::vector counts; + std::vector partof; + std::size_t part{0}; + for (std::size_t k = 0; k <= Chain.switches.size(); ++k) + { + auto const first{k == 0 ? std::size_t{0} : Chain.switches[k - 1].before}; + auto const last{k == Chain.switches.size() ? Chain.tracks.size() : Chain.switches[k].before}; + counts.push_back(last - first); + partof.push_back(last > first ? static_cast(part++) : -1); + if (k < Chain.switches.size()) + counts.push_back(1); + } + if (part != parts.size()) + { + route.error = STR_C("The paths between the switches can't be read as a chain"); + return false; + } + std::vector intervals; + auto const pieces{alignment::pieces(result, route.design, 0, breaks, counts, &intervals)}; + std::vector> groups(parts.size()); + for (std::size_t i = 0; i < pieces.size(); ++i) + if (intervals[i] % 2 == 0 && partof[intervals[i] / 2] >= 0) + groups[partof[intervals[i] / 2]].push_back(pieces[i]); + for (std::size_t i = 0; i < parts.size(); ++i) + if (groups[i].size() < parts[i].tracks.size()) + { + route.error = STR_C("The designed line between the switches is too short for the paths there"); + return false; + } + + // all checked, the changes go in + auto const remember = [&](TTrack *Track) { + if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; })) + States.emplace_back(Track, editor_track::capture(*Track)); + }; + for (auto const &place : placements) + { + auto joints{editor_track::joints(*place.track)}; + joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return std::find(Chain.tracks.begin(), Chain.tracks.end(), Joint.other) != Chain.tracks.end(); }), joints.end()); + remember(place.track); + std::vector changed{place.track}; + for (auto const &joint : joints) + { + remember(joint.other); + if (std::find(changed.begin(), changed.end(), joint.other) == changed.end()) + changed.push_back(joint.other); + } + // the heights stay, the paths between the switches keep theirs too + auto target{alignment::evaluate(result, place.from).position}; + target.y = place.entry.y; + editor_track::place(*place.track, place.entry, target, place.angle); + editor_track::follow(*place.track, joints); + editor_track::commit(changed); + } + for (std::size_t i = 0; i < parts.size(); ++i) + { + auto group{groups[i]}; + editor_track::keep_heights(parts[i], group); + auto const added{editor_track::relay(parts[i], group)}; + Created.insert(Created.end(), added.begin(), added.end()); + } + return true; +} + +glm::dvec3 editor_mode::route_vertex_position(int const Vertex) const +{ + auto const &design{m_route.design}; + auto const &result{m_route.result}; + auto const count{static_cast(design.vertices.size())}; + glm::dvec2 const start{design.start.x, design.start.z}; + glm::dvec2 const end{design.end.x, design.end.z}; + glm::dvec2 position; + if (Vertex < static_cast(result.vertices.size()) && false == result.vertices.empty()) + position = result.vertices[Vertex]; + else if (count == 1) + { + if (false == alignment::tangent_intersection(design, position)) + position = (start + end) * 0.5; + } + else if (Vertex == 0) + position = start + glm::normalize(design.start_direction) * design.vertices.front().offset; + else if (Vertex == count - 1) + position = end - glm::normalize(design.end_direction) * design.vertices.back().offset; + else + position = design.vertices[Vertex].position; + + auto const height{Vertex < static_cast(result.vertex_elevations.size()) ? result.vertex_elevations[Vertex] : design.start.y + (design.end.y - design.start.y) * (Vertex + 1) / (count + 1.0)}; + return {position.x, height, position.y}; +} + +bool editor_mode::route_hit(int &Vertex, int &Grip) const +{ + if (false == route_active()) + return false; + screen_projection const projection; + float best = kHandleRadius * kHandleRadius; + int hit{-1}; + for (int i = 0; i < static_cast(m_route.design.vertices.size()); ++i) + { + if (auto const distance{projection.mouse_distance2(route_vertex_position(i))}; distance < best) + { + best = distance; + hit = i; + } + } + int griphit{-1}; + for (int i = 0; i < static_cast(m_route.result.vertex_chainages.size()) && m_route.result.length > 0.0; ++i) + { + if (auto const distance{projection.mouse_distance2(alignment::evaluate(m_route.result, m_route.result.vertex_chainages[i]).position)}; distance < best) + { + best = distance; + griphit = i; + hit = -1; + } + } + Vertex = hit; + Grip = griphit; + return hit >= 0 || griphit >= 0; +} + +void editor_mode::draw_route_overlay() const +{ + auto const &route{m_route}; + auto const &design{route.design}; + auto const &result{route.result}; + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + auto const &start{result.valid ? result.start : design.start}; + auto const &end{result.valid ? result.end : design.end}; + + std::vector polygon{start}; + for (int i = 0; i < static_cast(design.vertices.size()); ++i) + polygon.push_back(route_vertex_position(i)); + polygon.push_back(end); + for (std::size_t i = 0; i + 1 < polygon.size(); ++i) + projection.line(drawlist, polygon[i], polygon[i + 1], IM_COL32(255, 210, 60, 150), 1.5f); + + if (result.length > 0.0 && result.profile.size() >= 2) + { + for (auto const &e : result.elements) + { + ImU32 const color = e.kind == alignment::element_kind::straight ? IM_COL32(255, 255, 255, 230) : e.kind == alignment::element_kind::arc ? overlay_color::grip : IM_COL32(255, 140, 30, 255); + int const steps = std::clamp(static_cast(e.length / 2.0), 1, 400); + auto previous{alignment::evaluate(result, e.chainage).position}; + for (int j = 1; j <= steps; ++j) + { + auto const next{alignment::evaluate(result, e.chainage + e.length * j / steps).position}; + projection.line(drawlist, previous, next, color, 3.0f); + previous = next; + } + ImVec2 screen; + if (projection.project(alignment::evaluate(result, e.chainage).position, screen)) + drawlist->AddCircleFilled(screen, 3.5f, overlay_color::highlight); + } + } + + for (auto const atend : {false, true}) + { + auto const &point{atend ? end : start}; + auto const direction{atend ? design.end_direction : design.start_direction}; + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddRectFilled(ImVec2(screen.x - 5.0f, screen.y - 5.0f), ImVec2(screen.x + 5.0f, screen.y + 5.0f), IM_COL32(70, 140, 255, 255)); + auto const arrow{glm::dvec3{direction.x, 0.0, direction.y} * (atend ? 15.0 : -15.0)}; + projection.line(drawlist, point, point + arrow, IM_COL32(70, 140, 255, 255), 2.0f); + } + for (int i = 0; i < static_cast(design.vertices.size()); ++i) + { + ImVec2 screen; + if (false == projection.project(route_vertex_position(i), screen)) + continue; + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::marked); + if (i == route.vertex) + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); + if (i < static_cast(result.vertex_chainages.size()) && result.length > 0.0 && projection.project(alignment::evaluate(result, result.vertex_chainages[i]).position, screen)) + { + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::grip, 4); + if (i == route.grip) + drawlist->AddCircle(screen, 11.0f, overlay_color::highlight, 16, 2.5f); + auto const *report{i < static_cast(result.curves.size()) ? &result.curves[i] : nullptr}; + auto radius{format("R %.0f", report != nullptr ? report->radius : design.vertices[i].radius)}; + if (report != nullptr && report->radii.size() >= 2) + { + radius = "R"; + for (std::size_t k = 0; k < report->radii.size(); ++k) + radius += format(k == 0 ? " %.0f" : " / %.0f", report->radii[k]); + } + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y + 4.0f), overlay_color::grip, radius.c_str()); + } + drawlist->AddText(ImVec2(screen.x + 9.0f, screen.y - 18.0f), overlay_color::marked, format("W%d", i + 1).c_str()); + } +} + +void editor_mode::render_route_gizmo() +{ + auto &route{m_route}; + auto const count{static_cast(route.design.vertices.size())}; + bool const grip{route.grip >= 0 && route.grip < count && route.grip < static_cast(route.result.vertex_chainages.size())}; + if ((false == grip && (route.vertex < 0 || route.vertex >= count)) || false == m_gizmo_enabled) + { + m_route_gizmo_using = false; + return; + } + + gizmo_frame const frame; + + auto const position{grip ? alignment::evaluate(route.result, route.result.vertex_chainages[route.grip]).position : route_vertex_position(route.vertex)}; + glm::dvec3 moved{position}; + bool held{false}; + if (m_handle_drag.active) + { + held = handle_drag_step(moved); + if (false == held && false == m_route_gizmo_using) + return; + } + else + { + if (false == m_route_gizmo_using) + m_route_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(position - frame.camera)); + frame.manipulate(ImGuizmo::TRANSLATE, m_route_gizmo, m_gizmo_snap); + held = ImGuizmo::IsUsing(); + moved = frame.camera + glm::dvec3(m_route_gizmo[3]); + } + + if (false == held) + { + if (m_route_gizmo_using) + { + m_route_gizmo_using = false; + route_apply(); + } + return; + } + m_route_gizmo_using = true; + if (grip) + { + auto &vertex{route.design.vertices[route.grip]}; + auto const corner{route_vertex_position(route.grip)}; + auto const distance{plan_distance(moved, corner)}; + for (auto const &curve : route.result.curves) + { + if (curve.vertex != route.grip || vertex.reverse_turn) + continue; + auto const secant{1.0 / std::cos(curve.deflection * 0.5) - 1.0}; + if (secant > 1e-6) + vertex.radius = std::max(10.0, std::round(distance / secant)); + } + route_update(); + return; + } + auto &vertex{route.design.vertices[route.vertex]}; + glm::dvec2 const point{moved.x, moved.z}; + if (count >= 2) + { + if (route.vertex == 0) + vertex.offset = std::max(0.1, glm::dot(point - plan_of(route.design.start), glm::normalize(route.design.start_direction))); + else if (route.vertex == count - 1) + vertex.offset = std::max(0.1, glm::dot(plan_of(route.design.end) - point, glm::normalize(route.design.end_direction))); + else + vertex.position = point; + } + route_update(); +} + +void editor_mode::render_route_ui() +{ + auto &route{m_route}; + if (false == route.error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", route.error.c_str()); + if (route.chain.tracks.empty()) + return; + + ImGui::TextDisabled(STR_C("%zu paths, %.2f m"), route.chain.tracks.size(), route.chain.length); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Reset design"))) + { + auto *track{route.chain.tracks[route.chain.tracks.size() / 2]}; + route_load(*track); + return; + } + item_tooltip("Reads the curves of the line from the scenery again, dropping the changes not applied"); + auto changed{render_route_vertices()}; + if (ImGui::CollapsingHeader(STR_C("Design speed and limits"))) + changed |= render_route_parameters(); + if (changed) + route_update(); + + ImGui::Separator(); + render_route_result(); +} + +bool editor_mode::render_route_parameters() +{ + auto &design{m_route.design}; + bool changed{false}; + ImGui::PushItemWidth(120.0f); + changed |= ImGui::InputDouble(STR_C("Design speed (km/h)"), &design.speed, 0.0, 0.0, "%.0f"); + item_tooltip("Speed the cant, the transitions and the recommended radius are designed for"); + design.speed = std::max(1.0, design.speed); + int shape = static_cast(design.shape); + char const *shapes[] = {"Cubic parabola", "Clothoid"}; + if (ImGui::Combo("Transition curve", &shape, shapes, IM_ARRAYSIZE(shapes))) + { + design.shape = static_cast(shape); + changed = true; + } + item_tooltip("Cubic parabola: the usual one on the Polish lines; clothoid: curvature growing linearly with the length"); + ImGui::PopItemWidth(); + + if (ImGui::TreeNode(STR_C("Limits"))) + { + auto &norms{design.norms}; + ImGui::PushItemWidth(100.0f); + changed |= ImGui::InputDouble(STR_C("Rail axes distance (mm)"), &norms.gauge, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble(STR_C("Unbalanced acceleration (m/s2)"), &norms.unbalanced, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble(STR_C("Maximum cant (mm)"), &norms.cant_max, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble(STR_C("Cant change rate (mm/s)"), &norms.cant_rate, 0.0, 0.0, "%.0f"); + changed |= ImGui::InputDouble(STR_C("Cant ramp 1:(k*V), k"), &norms.ramp_factor, 0.0, 0.0, "%.1f"); + changed |= ImGui::InputDouble(STR_C("Unbalanced acc. change rate (m/s3)"), &norms.jerk, 0.0, 0.0, "%.2f"); + changed |= ImGui::InputDouble(STR_C("Straight between curves (s of travel)"), &norms.tangent_min_time, 0.0, 0.0, "%.1f"); + ImGui::PopItemWidth(); + norms.gauge = std::max(100.0, norms.gauge); + norms.cant_rate = std::max(1.0, norms.cant_rate); + norms.jerk = std::max(0.01, norms.jerk); + ImGui::TreePop(); + } + return changed; +} + +bool editor_mode::render_route_vertices() +{ + bool changed{false}; + auto const count{static_cast(m_route.design.vertices.size())}; + for (int i = 0; i < count; ++i) + { + ImGui::PushID(i); + changed |= render_route_vertex(i); + ImGui::PopID(); + } + return changed; +} + +bool editor_mode::render_route_vertex(int const Index) +{ + auto &route{m_route}; + auto &design{route.design}; + auto &vertex{design.vertices[Index]}; + auto const count{static_cast(design.vertices.size())}; + bool changed{false}; + if (count > 1) + ImGui::TextDisabled(STR_C("Curve %d of the S-curve"), Index + 1); + ImGui::PushItemWidth(110.0f); + changed |= ImGui::DragScalar(vertex.compound ? STR_C("Radius R1 (m)") : STR_C("Radius R (m)"), ImGuiDataType_Double, &vertex.radius, 5.0f, nullptr, nullptr, "%.0f"); + if (ImGui::Checkbox(STR_C("Compound curve (several radii)"), &vertex.compound)) + { + changed = true; + if (vertex.compound && vertex.arcs.empty()) + vertex.arcs.push_back({std::max(10.0, std::round(vertex.radius * 0.6 / 10.0) * 10.0), 0.0, 1.0}); + } + item_tooltip("Curve made of arcs of different radii, each one its own path, joined with transitions"); + if (vertex.compound) + { + auto total{std::max(0.0, vertex.share)}; + for (auto const &arc : vertex.arcs) + total += std::max(0.0, arc.share); + auto const percent = [&](double const Share) { return total > 0.0 ? 100.0 * std::max(0.0, Share) / total : 0.0; }; + changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), 1).c_str(), ImGuiDataType_Double, &vertex.share, 0.01f, nullptr, nullptr, "%.2f"); + ImGui::SameLine(); + ImGui::TextDisabled("%.0f%%", percent(vertex.share)); + int removed{-1}; + for (int k = 0; k < static_cast(vertex.arcs.size()); ++k) + { + auto &arc{vertex.arcs[k]}; + ImGui::PushID(k); + ImGui::Separator(); + changed |= ImGui::DragScalar(format(STR_C("Radius R%d (m)"), k + 2).c_str(), ImGuiDataType_Double, &arc.radius, 5.0f, nullptr, nullptr, "%.0f"); + ImGui::SameLine(); + if (ImGui::SmallButton("x")) + removed = k; + item_tooltip("Takes this radius out of the curve"); + changed |= ImGui::DragScalar(format(STR_C("Transition R%d-R%d (m)"), k + 1, k + 2).c_str(), ImGuiDataType_Double, &arc.transition, 1.0f, nullptr, nullptr, "%.0f"); + changed |= ImGui::DragScalar(format(STR_C("Share of R%d"), k + 2).c_str(), ImGuiDataType_Double, &arc.share, 0.01f, nullptr, nullptr, "%.2f"); + ImGui::SameLine(); + ImGui::TextDisabled("%.0f%%", percent(arc.share)); + arc.radius = std::max(1.0, arc.radius); + arc.transition = std::max(0.0, arc.transition); + arc.share = std::max(0.0, arc.share); + ImGui::PopID(); + } + vertex.share = std::max(0.0, vertex.share); + if (removed >= 0) + { + vertex.arcs.erase(vertex.arcs.begin() + removed); + if (vertex.arcs.empty()) + vertex.compound = false; + changed = true; + } + if (ImGui::SmallButton(STR_C("Add a radius"))) + { + auto const last{vertex.arcs.empty() ? vertex.radius : vertex.arcs.back().radius}; + vertex.arcs.push_back({std::max(10.0, std::round(last * 0.8 / 10.0) * 10.0), 0.0, 1.0}); + changed = true; + } + item_tooltip("Another arc after the last one, the angle of the curve is shared out by the shares"); + ImGui::Separator(); + } + changed |= ImGui::DragScalar(STR_C("Transition in (m)"), ImGuiDataType_Double, &vertex.transition_in, 1.0f, nullptr, nullptr, "%.0f"); + changed |= ImGui::DragScalar(STR_C("Transition out (m)"), ImGuiDataType_Double, &vertex.transition_out, 1.0f, nullptr, nullptr, "%.0f"); + changed |= ImGui::DragScalar(STR_C("Cant (mm)"), ImGuiDataType_Double, &vertex.cant, 1.0f, nullptr, nullptr, "%.0f"); + item_tooltip("Height of the outer rail above the inner one"); + ImGui::PopItemWidth(); + vertex.radius = std::max(1.0, vertex.radius); + vertex.transition_in = std::max(0.0, vertex.transition_in); + vertex.transition_out = std::max(0.0, vertex.transition_out); + vertex.cant = std::max(0.0, vertex.cant); + if (ImGui::SmallButton(STR_C("Apply recommended for V"))) + { + route_recommend(vertex); + changed = true; + } + item_tooltip("Cant and transitions recommended for the design speed and the radius"); + + auto const recommended{alignment::recommend(design.speed, vertex.radius, design.norms)}; + ImGui::TextDisabled(STR_C("For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m"), design.speed, recommended.radius_min, recommended.cant, recommended.cant_equilibrium, recommended.cant_min, recommended.transition); + if (ImGui::TreeNode(STR_C("Details"))) + { + for (auto const &curve : route.result.curves) + { + if (curve.vertex != Index) + continue; + ImGui::Text(STR_C("Deflection %.4f deg, tangents %.2f / %.2f m, arc %.2f m"), glm::degrees(curve.deflection), curve.tangent_in, curve.tangent_out, curve.arc_length); + ImGui::Text(STR_C("Unbalanced acc. %.3f m/s2, cant rate %.1f / %.1f mm/s, acc. rate %.3f / %.3f m/s3"), curve.unbalanced, curve.cant_rate_in, curve.cant_rate_out, curve.jerk_in, curve.jerk_out); + } + changed |= ImGui::Checkbox(STR_C("Turn the longer way (over 180 deg)"), &vertex.reverse_turn); + item_tooltip("For the loops: the curve turns by more than a half circle"); + ImGui::TreePop(); + } + return changed; +} + +void editor_mode::render_route_result() +{ + auto const &route{m_route}; + auto const &design{route.design}; + auto const &result{route.result}; + if (result.length > 0.0) + { + auto const pieces{alignment::minimum_pieces(result, design)}; + ImGui::Text(STR_C("Length %.2f m (now %.2f m), %zu elements"), result.length, route.chain.length, result.elements.size()); + ImGui::TextDisabled(STR_C("Paths: %zu in the fragment, %zu needed%s"), route.chain.tracks.size(), pieces, pieces > route.chain.tracks.size() ? STR_C(" (copies of the neighbours will be added)") : ""); + if (result.start_extension > 0.0 || result.end_extension > 0.0) + ImGui::Text(STR_C("Takes over %.2f m of the straight at the start, %.2f m at the end"), result.start_extension, result.end_extension); + } + for (auto const &error : result.errors) + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", error.c_str()); + for (auto const &warning : result.warnings) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", warning.c_str()); + + if (ImGui::TreeNode(STR_C("Elements"))) + { + ImGui::TextDisabled(STR_C("Straight available beyond the ends: %.2f / %.2f m"), design.start_reserve, design.end_reserve); + char const *kinds[] = {"straight", "transition", "curve", "transition"}; + for (auto const &e : result.elements) + ImGui::Text("%8.2f m %-10s L %8.2f m%s", e.chainage, kinds[static_cast(e.kind)], e.length, e.kind == alignment::element_kind::straight ? "" : (" R " + std::to_string(static_cast(std::round(e.radius))) + " m").c_str()); + ImGui::TreePop(); + } + + if (result.valid) + { + ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.55f, 0.25f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.65f, 0.3f, 1.0f)); + if (ImGui::Button(STR_C("Apply to the scenery"), ImVec2(-1.0f, 28.0f))) + route_apply(); + ImGui::PopStyleColor(2); + } + else + { + ImGui::TextDisabled(STR_C("Correct the errors to apply the design")); + } + if (false == route.status.empty()) + ImGui::TextUnformatted(route.status.c_str()); +} + +editor_track::straight const &editor_mode::current_straight() +{ + auto *track{selected_track()}; + if (track != m_straights.current_for) + { + m_straights.current_for = track; + m_straights.handle = -1; + m_straights.current = track != nullptr ? editor_track::find_straight(*track, m_straights.tolerance) : editor_track::straight{}; + find_neighbour_straights(); + } + return m_straights.current; +} + +void editor_mode::draw_straights_overlay() const +{ + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + glm::dvec3 const camera{Global.pCamera.Pos}; + auto const nearby = [&](editor_track::straight const &Line) { + auto const offset{camera - Line.start}; + auto const along{std::clamp(glm::dot(plan_of(offset), Line.direction), 0.0, Line.length)}; + auto const closest{Line.start + glm::dvec3{Line.direction.x, 0.0, Line.direction.y} * along}; + return plan_distance(closest, camera) < 3000.0; + }; + auto const draw = [&](editor_track::straight const &Line, ImU32 const Color, float const Thickness) { + if (Line.tracks.empty()) + return; + projection.line(drawlist, Line.start, Line.end, Color, Thickness); + for (auto const &point : {Line.start, Line.end}) + { + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddCircleFilled(screen, Thickness + 2.0f, Color); + } + }; + for (int i = 0; i < static_cast(m_straights.found.size()); ++i) + { + auto const &line{m_straights.found[i]}; + if (nearby(line)) + draw(line, i == m_straights.listed ? overlay_color::selected : IM_COL32(255, 255, 255, 110), i == m_straights.listed ? 4.0f : 2.0f); + } + for (auto const &member : m_straights.set) + draw(member, IM_COL32(255, 150, 30, 255), 4.0f); + draw(m_straights.current, overlay_color::selected, 4.0f); + if (m_straights.tool_placed) + { + auto const &state{m_straights}; + auto const &line{state.tool_line}; + ImVec2 screen; + if (projection.project(state.tool_point, screen)) + drawlist->AddCircleFilled(screen, 7.0f, overlay_color::invalid); + if (projection.project(state.tool_handle, screen)) + drawlist->AddCircleFilled(screen, 7.0f, overlay_color::marked); + if (state.tool == 1) + { + auto const direction{plan_of(state.tool_handle - state.tool_point)}; + if (glm::length(direction) > 1e-3) + { + auto const unit{glm::normalize(direction)}; + auto const rest{std::max(0.0, line.length - state.tool_at)}; + projection.line(drawlist, state.tool_point, state.tool_point + glm::dvec3{unit.x, line.grade, unit.y} * rest, overlay_color::marked, 3.0f); + } + } + else + { + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const offset{plan_of(state.tool_handle - state.tool_point)}; + auto const shift{glm::dot(offset, normal)}; + auto const length{glm::dot(offset, line.direction)}; + auto const shifted{glm::dvec3{normal.x, 0.0, normal.y} * shift}; + auto const along{glm::dvec3{line.direction.x, 0.0, line.direction.y}}; + projection.line(drawlist, state.tool_point, state.tool_point + along * length + shifted, IM_COL32(255, 210, 60, 160), 2.0f); + projection.line(drawlist, state.tool_point + along * length + shifted, line.end + shifted, overlay_color::marked, 3.0f); + } + } + if (m_straights.dragging && m_straights.fitting) + { + auto const &result{m_route.result}; + if (result.length > 0.0 && result.profile.size() >= 2) + { + int const steps{std::clamp(static_cast(result.length / 2.0), 2, 2000)}; + auto previous{alignment::evaluate(result, 0.0).position}; + for (int i = 1; i <= steps; ++i) + { + auto const next{alignment::evaluate(result, result.length * i / steps).position}; + projection.line(drawlist, previous, next, result.valid ? overlay_color::marked : overlay_color::invalid, 3.0f); + previous = next; + } + } + } + else if (m_straights.dragging) + { + for (auto const &member : m_straights.drag_lines) + { + for (auto const *track : member.tracks) + { + auto const &path{track->m_paths.front()}; + projection.line(drawlist, path.points[segment_data::point::start], path.points[segment_data::point::end], overlay_color::marked, 3.0f); + } + } + } + else if (false == m_straights.current.tracks.empty()) + { + auto const &line{m_straights.current}; + glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; + for (int i = 0; i < 3; ++i) + { + ImVec2 screen; + if (false == projection.project(handles[i], screen)) + continue; + if (i < 2) + drawlist->AddRectFilled(ImVec2(screen.x - 6.0f, screen.y - 6.0f), ImVec2(screen.x + 6.0f, screen.y + 6.0f), overlay_color::selected); + else + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::selected, 4); + if (i == m_straights.handle) + drawlist->AddCircle(screen, 12.0f, overlay_color::highlight, 16, 2.5f); + } + } + auto line{m_straights.dragging ? m_straights.drag_line : m_straights.current}; + if (line.tracks.empty()) + return; + glm::dvec3 const offset{m_straights.dragging ? m_straights.preview_start - m_straights.drag_line.start : glm::dvec3{}}; + if (m_straights.dragging) + { + line.start = m_straights.preview_start; + line.end = m_straights.preview_end; + auto const planar{plan_of(line.end - line.start)}; + auto const length{glm::length(planar)}; + if (length > 1e-6) + { + line.direction = planar / length; + line.length = length; + } + } + auto const offer{straight_parallel_offer(m_straights.dragging ? m_straights.drag_line : m_straights.current, offset)}; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + for (auto const &other : m_straights.neighbours) + { + auto const across = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - line.start), normal); }; + auto const a0{line.along(other.start)}, a1{line.along(other.end)}; + auto const from{std::max(0.0, std::min(a0, a1))}; + auto const to{std::min(line.length, std::max(a0, a1))}; + if (to <= from) + continue; + auto const middle{(from + to) * 0.5}; + auto const fraction{std::abs(a1 - a0) > 1e-6 ? (middle - a0) / (a1 - a0) : 0.0}; + auto const lateral{across(other.start) + (across(other.end) - across(other.start)) * fraction}; + glm::dvec3 const foot{line.start.x + line.direction.x * middle, line.start.y + line.grade * middle, line.start.z + line.direction.y * middle}; + glm::dvec3 const target{foot.x + normal.x * lateral, foot.y, foot.z + normal.y * lateral}; + bool const highlighted{offer.nearby && offer.neighbour == &other}; + auto const tick{highlighted ? (offer.snaps ? IM_COL32(80, 230, 255, 255) : IM_COL32(255, 210, 60, 255)) : IM_COL32(255, 230, 120, 230)}; + if (highlighted) + projection.line(drawlist, other.start, other.end, tick, 3.0f); + projection.line(drawlist, foot, target, tick, highlighted ? 3.0f : 1.5f); + ImVec2 screen; + if (projection.project((foot + target) * 0.5, screen)) + { + auto const angle{glm::degrees(std::abs(std::asin(std::clamp(line.direction.x * other.direction.y - line.direction.y * other.direction.x, -1.0, 1.0))))}; + auto const text{highlighted ? (offer.snaps ? format(STR_C("parallel %.2f m"), offer.spacing) : format("%.2f → %.2f m %.1f°", std::abs(lateral), offer.spacing, offer.angle)) : + (angle > 0.05 ? format("%.2f m %.1f°", std::abs(lateral), angle) : format("%.2f m", std::abs(lateral)))}; + auto const *label{text.c_str()}; + auto const size{ImGui::CalcTextSize(label)}; + drawlist->AddRectFilled(ImVec2(screen.x - 4.0f, screen.y - 2.0f), ImVec2(screen.x + size.x + 4.0f, screen.y + size.y + 2.0f), IM_COL32(0, 0, 0, 190), 3.0f); + drawlist->AddText(screen, highlighted ? tick : IM_COL32(255, 230, 120, 255), label); + } + } + if (offer.nearby && false == offer.snaps && std::abs(offer.correction) > 0.02) + { + auto const shift{glm::dvec3{normal.x, 0.0, normal.y} * offer.correction}; + projection.line(drawlist, line.start + shift, line.end + shift, IM_COL32(80, 230, 255, 210), 3.5f); + } +} + +void editor_mode::find_neighbour_straights() +{ + auto &state{m_straights}; + state.neighbours.clear(); + auto const &line{state.current}; + if (line.tracks.empty()) + return; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const middle{(line.start + line.end) * 0.5}; + std::vector visited(line.tracks.begin(), line.tracks.end()); + auto const sections{simulation::Region->sections(middle, static_cast(line.length * 0.5 + 60.0))}; + for (auto *section : sections) + { + for (auto const &cell : section->m_cells) + { + for (auto *track : cell.m_paths) + { + if (std::find(visited.begin(), visited.end(), track) != visited.end() || false == editor_track::is_straight(*track, state.tolerance)) + continue; + auto other{editor_track::find_straight(*track, state.tolerance)}; + visited.insert(visited.end(), other.tracks.begin(), other.tracks.end()); + if (other.tracks.empty() || std::abs(line.direction.x * other.direction.y - line.direction.y * other.direction.x) > std::sin(glm::radians(kParallelFindAngle))) + continue; + auto const offset{glm::dvec2{(other.start + other.end).x * 0.5 - line.start.x, (other.start + other.end).z * 0.5 - line.start.z}}; + if (std::abs(glm::dot(offset, normal)) > 60.0) + continue; + auto const a0{line.along(other.start)}; + auto const a1{line.along(other.end)}; + if (std::max(a0, a1) <= 0.0 || std::min(a0, a1) >= line.length) + continue; + state.neighbours.push_back(std::move(other)); + } + } + } +} + +editor_mode::parallel_offer editor_mode::straight_parallel_offer(editor_track::straight const &Line, glm::dvec3 const &Offset) const +{ + parallel_offer offer; + glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; + auto const middle{(Line.start + Line.end) * 0.5 + Offset}; + double best{kParallelHintWindow}; + for (auto const &other : m_straights.neighbours) + { + auto const sine{std::abs(Line.direction.x * other.direction.y - Line.direction.y * other.direction.x)}; + if (sine > std::sin(glm::radians(kParallelFindAngle))) + continue; + auto const distance{glm::dot(plan_of(other.start - middle), normal)}; + auto const angle{glm::degrees(std::abs(std::asin(std::clamp(sine, 0.0, 1.0))))}; + for (auto const spacing : m_straights.spacings) + { + auto const error{std::abs(std::abs(distance) - spacing)}; + if (error < best) + { + best = error; + offer.nearby = true; + offer.snaps = error < kParallelSnapWindow && angle <= kParallelSnapAngle; + offer.spacing = spacing; + offer.distance = std::abs(distance); + offer.angle = angle; + offer.correction = distance > 0.0 ? std::abs(distance) - spacing : spacing - std::abs(distance); + offer.neighbour = &other; + } + } + } + return offer; +} + +glm::dvec3 editor_mode::snap_straight_offset(editor_track::straight const &Line, glm::dvec3 const &Offset) const +{ + auto const offer{straight_parallel_offer(Line, Offset)}; + if (false == offer.snaps) + return Offset; + glm::dvec2 const normal{-Line.direction.y, Line.direction.x}; + return Offset + glm::dvec3{normal.x, 0.0, normal.y} * offer.correction; +} + +glm::dvec3 editor_mode::snap_straight_direction(glm::dvec3 const &Pivot, glm::dvec3 const &Moved) const +{ + glm::dvec2 const offset{Moved.x - Pivot.x, Moved.z - Pivot.z}; + auto const length{glm::length(offset)}; + if (length < 1e-3) + return Moved; + auto const direction{offset / length}; + for (auto const &other : m_straights.neighbours) + { + auto const sine{direction.x * other.direction.y - direction.y * other.direction.x}; + if (std::abs(sine) > std::sin(glm::radians(kParallelSnapAngle))) + continue; + auto const aligned{other.direction * (glm::dot(direction, other.direction) < 0.0 ? -1.0 : 1.0)}; + return {Pivot.x + aligned.x * length, Moved.y, Pivot.z + aligned.y * length}; + } + return Moved; +} + +void editor_mode::render_straights_ui() +{ + auto &state{m_straights}; + ImGui::TextDisabled(STR_C("Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to")); + + ImGui::PushItemWidth(100.0f); + double angle{glm::degrees(state.tolerance.angle)}; + if (ImGui::InputDouble(STR_C("Angle tolerance (deg)"), &angle, 0.0, 0.0, "%.4f")) + state.tolerance.angle = glm::radians(std::max(0.0, angle)); + item_tooltip("Largest change of the direction between the paths of one straight"); + ImGui::InputDouble(STR_C("Offset tolerance (m)"), &state.tolerance.offset, 0.0, 0.0, "%.3f"); + item_tooltip("Largest distance of the paths from the common axis of the straight"); + ImGui::InputDouble(STR_C("Treat as straight above R (m)"), &state.tolerance.radius, 0.0, 0.0, "%.0f"); + item_tooltip("Curves of a larger radius count as straight"); + ImGui::InputDouble(STR_C("Minimum length (m)"), &state.minimum_length, 0.0, 0.0, "%.0f"); + item_tooltip("Shorter straights aren't listed"); + ImGui::PopItemWidth(); + if (ImGui::Button(STR_C("Recognize straights in the scenery"))) + { + state.found = editor_track::find_straights(state.minimum_length, state.tolerance); + state.listed = -1; + state.current_for = nullptr; + } + if (false == state.found.empty()) + { + ImGui::Text(STR_C("%zu straights"), state.found.size()); + ImGui::SameLine(); + ImGui::PushItemWidth(150.0f); + ImGui::InputTextWithHint("##straightfilter", STR_C("path name"), state.filter, sizeof(state.filter)); + ImGui::PopItemWidth(); + ImGui::BeginChild("##straights", ImVec2(0.0f, 160.0f), true); + for (int i = 0; i < static_cast(state.found.size()); ++i) + { + auto const &line{state.found[i]}; + if (state.filter[0] != '\0' && std::none_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return Track->name().find(state.filter) != std::string::npos; })) + continue; + auto const label{std::to_string(i + 1) + ". " + describe(line) + "##straight" + std::to_string(i)}; + if (ImGui::Selectable(label.c_str(), state.listed == i)) + { + state.listed = i; + m_node = line.tracks[line.tracks.size() / 2]; + ui()->set_node(m_node); + start_focus(m_node); + } + } + ImGui::EndChild(); + } + +} + +void editor_mode::render_straight_ui() +{ + auto &state{m_straights}; + auto const &line{current_straight()}; + if (line.tracks.empty()) + { + if (false == state.status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); + return; + } + ImGui::TextUnformatted(describe(line).c_str()); + if (false == m_straights.neighbours.empty()) + ImGui::TextDisabled("%s", STR_C("Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m")); + + auto const toolbutton = [&](char const *Label, int const Tool, char const *Tooltip) { + bool const active{state.tool == Tool}; + if (active) + ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive)); + if (ImGui::Button(Label)) + { + state.tool = active ? 0 : Tool; + state.tool_placed = false; + state.handle = -1; + } + if (active) + ImGui::PopStyleColor(); + item_tooltip(Tooltip); + }; + toolbutton(STR_C("Break"), 1, STR_C("Turns the rest of the straight from a point, a curve goes at the break (Ctrl+drag in the view does the same)")); + ImGui::SameLine(); + toolbutton(STR_C("S-curve"), 2, STR_C("Shifts the rest of the straight sideways through two curves")); + ImGui::SameLine(); + bool const member{in_straight_set(line)}; + if (ImGui::Button(member ? STR_C("Leave the group") : STR_C("Move with others"))) + { + if (member) + state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Track) { return std::find(Member.tracks.begin(), Member.tracks.end(), Track) != Member.tracks.end(); }); }), state.set.end()); + else + state.set.push_back(line); + } + item_tooltip("Straights of a group move together and stay parallel; Alt+click in the view adds another one"); + if (state.tool != 0) + { + ImGui::PushItemWidth(100.0f); + ImGui::InputDouble(STR_C("Curve R (m, 0: automatic)"), &state.curve_radius, 0.0, 0.0, "%.0f"); + item_tooltip("Radius of the curves made by the break and the S-curve; automatic: 1.5 times the minimum radius for the speed of the line"); + ImGui::Checkbox(STR_C("Transitions from the line speed"), &state.auto_transitions); + item_tooltip("Lengths of the transition curves from the speed of the paths and the limits of the curve design"); + if (false == state.auto_transitions) + { + ImGui::SameLine(); + ImGui::InputDouble(STR_C("Transition (m)"), &state.transition, 0.0, 0.0, "%.1f"); + } + ImGui::PopItemWidth(); + } + if (false == state.status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", state.status.c_str()); + if (false == state.set.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.15f, 1.0f), STR_C("Group: %zu straights move together"), state.set.size()); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Clear"))) + state.set.clear(); + } + + if (false == ImGui::IsAnyItemActive()) + { + state.edit_start = line.start; + state.edit_end = line.end; + state.edit_length = line.length; + state.edit_azimuth = line.azimuth; + } + if (ImGui::CollapsingHeader(STR_C("Exact values"))) + { + ImGui::PushItemWidth(-110.0f); + ImGui::InputScalarN(STR_C("Start (x, y, z)"), ImGuiDataType_Double, glm::value_ptr(state.edit_start), 3, nullptr, nullptr, "%.3f"); + ImGui::InputScalarN(STR_C("End (x, y, z)"), ImGuiDataType_Double, glm::value_ptr(state.edit_end), 3, nullptr, nullptr, "%.3f"); + ImGui::PopItemWidth(); + if (ImGui::Button(STR_C("Apply ends"))) + straight_apply(line, state.edit_start, state.edit_end); + ImGui::PushItemWidth(120.0f); + ImGui::InputDouble(STR_C("Length (m)"), &state.edit_length, 0.0, 0.0, "%.3f"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Set from the start##length"))) + { + glm::dvec3 const direction{line.direction.x, line.grade, line.direction.y}; + straight_apply(line, line.start, line.start + direction * std::max(0.1, state.edit_length)); + } + ImGui::InputDouble(STR_C("Azimuth (deg)"), &state.edit_azimuth, 0.0, 0.0, "%.5f"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Turn around the start"))) + { + auto const azimuth{glm::radians(state.edit_azimuth)}; + glm::dvec3 const direction{std::sin(azimuth), line.grade, std::cos(azimuth)}; + straight_apply(line, line.start, line.start + direction * line.length); + } + ImGui::PopItemWidth(); + } +} + +bool editor_mode::straights_active() +{ + return ui()->mode() == nodebank_panel::TRACK && m_track_tab == track_tab::straights && false == current_straight().tracks.empty(); +} + +int editor_mode::straight_handle_hit() +{ + if (false == straights_active()) + return -1; + auto const &line{current_straight()}; + glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; + screen_projection const projection; + float best = kHandleRadius * kHandleRadius; + int hit{-1}; + for (int i = 0; i < 3; ++i) + { + if (auto const distance{projection.mouse_distance2(handles[i])}; distance < best) + { + best = distance; + hit = i; + } + } + return hit; +} + +void editor_mode::straight_refresh() +{ + auto &state{m_straights}; + state.current_for = selected_track(); + state.current = state.current_for != nullptr ? editor_track::find_straight(*state.current_for, state.tolerance) : editor_track::straight{}; + find_neighbour_straights(); + for (auto &line : state.found) + if (false == line.tracks.empty()) + line = editor_track::find_straight(*line.tracks.front(), state.tolerance); + for (auto &line : state.set) + if (false == line.tracks.empty()) + line = editor_track::find_straight(*line.tracks.front(), state.tolerance); +} + +bool editor_mode::in_straight_set(editor_track::straight const &Line) const +{ + for (auto const &member : m_straights.set) + for (auto const *track : Line.tracks) + if (std::find(member.tracks.begin(), member.tracks.end(), track) != member.tracks.end()) + return true; + return false; +} + +void editor_mode::straight_apply(editor_track::straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End) +{ + straights_apply({Line}, {{Start, End}}); +} + +void editor_mode::straights_apply(std::vector const &Lines, std::vector> const &Ends) +{ + auto &state{m_straights}; + state.status.clear(); + auto const tracks{editor_track::straight_affected(Lines)}; + if (false == tracks_editable(tracks, state.status)) + return; + std::vector> states; + for (auto *track : tracks) + states.emplace_back(track, editor_track::capture(*track)); + push_track_snapshot(std::move(states)); + editor_track::move_straights(Lines, Ends); + editor_track::commit(tracks); + straight_refresh(); +} + +void editor_mode::render_straight_gizmo() +{ + auto &state{m_straights}; + if (state.tool_mouse) + { + glm::dvec3 const ground{cursor_ground()}; + state.tool_handle = {ground.x, state.tool_handle.y, ground.z}; + typed_turn(); + return; + } + if (state.tool_placed && m_gizmo_enabled) + { + render_straight_tool_gizmo(); + return; + } + auto const &line{state.dragging ? state.drag_line : current_straight()}; + if (state.handle < 0 || line.tracks.empty() || false == m_gizmo_enabled) + { + state.dragging = false; + return; + } + + gizmo_frame const frame; + + glm::dvec3 const anchors[] = {line.start, line.end, (line.start + line.end) * 0.5}; + auto const anchor{anchors[state.handle]}; + glm::dvec3 handled{anchor}; + bool held{false}; + if (m_handle_drag.active) + { + held = handle_drag_step(handled); + if (false == held && false == state.dragging) + return; + } + else + { + if (false == state.dragging) + state.gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(anchor - frame.camera)); + frame.manipulate(ImGuizmo::TRANSLATE, state.gizmo, m_gizmo_snap); + held = ImGuizmo::IsUsing(); + handled = frame.camera + glm::dvec3(state.gizmo[3]); + } + + if (held) + { + if (false == state.dragging) + { + std::vector lines; + if (in_straight_set(line)) + { + for (auto const &member : state.set) + if (false == member.tracks.empty()) + lines.push_back(editor_track::find_straight(*member.tracks.front(), state.tolerance)); + } + else + { + lines.push_back(line); + } + state.fitting = lines.size() == 1 && start_curve_fit(line, state.handle); + auto const tracks{state.fitting ? std::vector{} : editor_track::straight_affected(lines)}; + if (false == tracks_editable(tracks, state.status)) + { + state.fitting = false; + return; + } + state.dragging = true; + state.drag_line = line; + state.drag_lines = lines; + state.drag_states.clear(); + for (auto *track : tracks) + state.drag_states.emplace_back(track, editor_track::capture(*track)); + if (false == state.fitting) + { + push_track_snapshot(state.drag_states); + m_track_drag = tracks; + } + } + auto const &grabbed{state.drag_line}; + glm::dvec3 moved{handled}; + std::function transform; + if (state.handle == 2) + { + auto offset{snap_straight_offset(grabbed, moved - (grabbed.start + grabbed.end) * 0.5)}; + if (auto const values{typed_values()}; false == values.empty()) + offset = glm::dvec3{-grabbed.direction.y, 0.0, grabbed.direction.x} * values[0]; + transform = [offset](glm::dvec3 const &Point) { return Point + offset; }; + } + else + { + auto const pivot{state.handle == 0 ? grabbed.end : grabbed.start}; + auto const original{state.handle == 0 ? grabbed.start : grabbed.end}; + moved = snap_straight_direction(pivot, moved); + if (auto const values{typed_values()}; false == values.empty() && values[0] > 0.1) + { + glm::dvec2 heading{moved.x - pivot.x, moved.z - pivot.z}; + if (glm::length(heading) < 1e-6) + heading = {original.x - pivot.x, original.z - pivot.z}; + heading = glm::normalize(heading) * values[0]; + moved.x = pivot.x + heading.x; + moved.z = pivot.z + heading.y; + } + glm::dvec2 const before{original.x - pivot.x, original.z - pivot.z}; + glm::dvec2 const after{moved.x - pivot.x, moved.z - pivot.z}; + auto const lengthbefore{glm::length(before)}; + auto const lengthafter{std::max(0.1, glm::length(after))}; + auto const axis{before / std::max(1e-9, lengthbefore)}; + auto const newaxis{after / lengthafter}; + auto const scale{lengthafter / std::max(1e-9, lengthbefore)}; + auto const rise{moved.y - original.y}; + transform = [=](glm::dvec3 const &Point) { + glm::dvec2 const offset{Point.x - pivot.x, Point.z - pivot.z}; + auto const along{glm::dot(offset, axis)}; + auto const across{axis.x * offset.y - axis.y * offset.x}; + auto const planar{plan_of(pivot) + newaxis * (along * scale) + glm::dvec2{-newaxis.y, newaxis.x} * across}; + return glm::dvec3{planar.x, Point.y + rise * along / std::max(1e-9, lengthbefore), planar.y}; + }; + } + std::vector> ends; + for (auto const &member : state.drag_lines) + ends.emplace_back(transform(member.start), transform(member.end)); + state.preview_start = transform(grabbed.start); + state.preview_end = transform(grabbed.end); + if (state.fitting) + { + update_curve_fit(state.preview_start, state.preview_end); + return; + } + for (auto const &entry : state.drag_states) + editor_track::apply(*entry.first, entry.second); + editor_track::move_straights(state.drag_lines, ends); + m_track_dirty = true; + commit_track_drag(false); + } + else if (state.dragging) + { + if (state.fitting) + { + state.fitting = false; + state.dragging = false; + state.drag_lines.clear(); + if (m_route.result.valid) + route_apply(); + straight_refresh(); + return; + } + commit_track_drag(true); + state.dragging = false; + m_track_drag.clear(); + state.drag_states.clear(); + state.drag_lines.clear(); + straight_refresh(); + } +} + +void editor_mode::cancel_track_tools() +{ + auto &state{m_straights}; + if (state.dragging) + { + // the geometry goes back to what it was before the drag + if (false == state.fitting) + { + for (auto const &entry : state.drag_states) + editor_track::apply(*entry.first, entry.second); + m_track_dirty = true; + commit_track_drag(true); + m_track_drag.clear(); + } + state.dragging = false; + state.fitting = false; + state.drag_states.clear(); + state.drag_lines.clear(); + straight_refresh(); + } + m_extend.active = false; + m_turntable.placing = false; + m_signal.placing = false; + m_signal.moving = false; + m_handle_drag = {}; + if (m_point_drag.active) + { + m_track_snap = {}; + point_drag_finish(); + } + if (m_switch.placing) + { + m_switch.placing = false; + m_switch.status = STR_C("Placing of the switch cancelled"); + } + else if (m_switch.armed >= 0) + { + m_switch.armed = -1; + m_switch.status.clear(); + } + m_track_point = {}; + m_route.vertex = -1; + m_route.grip = -1; + state.handle = -1; + state.tool_placed = false; + state.detour.clear(); +} + +void editor_mode::straight_reshape(editor_track::straight const &Line, double const From, double const To, std::function const &Tail, double const Radius) +{ + auto &state{m_straights}; + state.status.clear(); + struct span + { + TTrack *track; + int path; + double from, to; + bool forward; + }; + auto line{Line}; + auto const point = [&](double const Along) { + auto const planar{plan_of(line.start) + line.direction * Along}; + return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; + }; + std::vector spans; + auto const collect = [&]() { + spans.clear(); + for (std::size_t i = 0; i < line.tracks.size(); ++i) + { + auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; + auto const a{line.along(path.points[segment_data::point::start])}; + auto const b{line.along(path.points[segment_data::point::end])}; + spans.push_back({line.tracks[i], line.paths[i], std::min(a, b), std::max(a, b), a < b}); + } + std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); + }; + int first{-1}, last{-1}; + auto const select = [&](double const End) { + first = last = -1; + for (int i = 0; i < static_cast(spans.size()); ++i) + { + if (spans[i].to > From && spans[i].from < End) + { + if (first < 0) + first = i; + last = i; + } + } + }; + auto const keep_regular = [&]() { + if (first < 0) + return; + auto const centre{(std::max(From, 0.0) + std::min(To, line.length)) * 0.5}; + int middle{first}; + for (int i = first; i <= last; ++i) + if (spans[i].from <= centre && spans[i].to >= centre) + middle = i; + if (spans[middle].track->eType != tt_Normal) + { + int best{-1}; + for (int i = first; i <= last; ++i) + if (spans[i].track->eType == tt_Normal && (best < 0 || std::abs(i - middle) < std::abs(best - middle))) + best = i; + if (best < 0) + { + first = last = -1; + return; + } + middle = best; + } + int low{middle}, high{middle}; + while (low > first && spans[low - 1].track->eType == tt_Normal) + --low; + while (high < last && spans[high + 1].track->eType == tt_Normal) + ++high; + first = low; + last = high; + }; + collect(); + select(To); + keep_regular(); + if (first < 0) + return; + + std::vector> splitstates; + std::vector splitcreated; + auto end{To}; + if (last + 1 >= static_cast(spans.size())) + { + auto const final{spans.back()}; + if (final.track->eType != tt_Normal) + return; + auto const length{final.to - final.from}; + auto const cut{To < final.to - 1.0 ? std::max(To, final.from + std::min(1.0, length * 0.5)) : final.from + length * 0.7}; + splitstates.emplace_back(final.track, editor_track::capture(*final.track)); + editor_track::chain split; + split.tracks = {final.track}; + split.forward = {final.forward}; + std::vector pieces(2); + pieces[0].points[segment_data::point::start] = point(final.from); + pieces[0].points[segment_data::point::end] = point(cut); + pieces[1].points[segment_data::point::start] = point(cut); + pieces[1].points[segment_data::point::end] = point(final.to); + splitcreated = editor_track::relay(split, pieces); + line = editor_track::find_straight(*line.tracks.front(), state.tolerance); + collect(); + end = std::min(To, cut); + select(end); + keep_regular(); + if (first < 0 || last + 1 >= static_cast(spans.size())) + { + for (auto *track : splitcreated) + editor_track::retire(*track); + editor_track::apply(*splitstates.front().first, splitstates.front().second); + editor_track::commit({splitstates.front().first}); + return; + } + } + auto const undo_split = [&]() { + for (auto *track : splitcreated) + editor_track::retire(*track); + for (auto const &entry : splitstates) + { + editor_track::apply(*entry.first, entry.second); + editor_track::commit({entry.first}); + } + }; + + editor_track::straight tail; + tail.direction = line.direction; + for (int i = last + 1; i < static_cast(spans.size()); ++i) + { + tail.tracks.push_back(spans[i].track); + tail.paths.push_back(spans[i].path); + } + tail.start = point(spans[last + 1].from); + tail.end = line.end; + tail.length = line.length - spans[last + 1].from; + tail.grade = line.grade; + + std::vector chainpaths; + for (int i = first; i <= last; ++i) + chainpaths.push_back(spans[i].track); + auto tracks{editor_track::straight_affected({tail})}; + for (auto *track : chainpaths) + if (std::find(tracks.begin(), tracks.end(), track) == tracks.end()) + tracks.push_back(track); + if (false == tracks_editable(tracks, m_straights.status)) + { + undo_split(); + return; + } + std::vector> states; + for (auto *track : tracks) + states.emplace_back(track, editor_track::capture(*track)); + + editor_track::move_straights({tail}, {{Tail(tail.start), Tail(tail.end)}}); + + m_route.from = spans[first].track; + m_route.to = spans[last].track; + route_reset(); + if (m_route.chain.tracks.empty() || false == m_route.chain.switches.empty()) + { + if (false == m_route.chain.switches.empty()) + m_route.error = STR_C("The curve would run through a switch"); + for (auto const &entry : states) + editor_track::apply(*entry.first, entry.second); + editor_track::commit(tracks); + undo_split(); + state.status = m_route.error; + return; + } + m_route.design.start_reserve = m_route.design.end_reserve = 0.0; + for (auto &vertex : m_route.design.vertices) + { + vertex.radius = Radius; + route_recommend(vertex); + if (false == state.auto_transitions) + { + vertex.transition_in = state.transition; + vertex.transition_out = state.transition; + } + } + route_update(); + if (false == m_route.result.valid) + { + for (auto const &entry : states) + editor_track::apply(*entry.first, entry.second); + editor_track::commit(tracks); + undo_split(); + state.status = m_route.result.errors.empty() ? STR_C("The curve can't be fitted") : m_route.result.errors.front(); + return; + } + auto pieces{alignment::pieces(m_route.result, m_route.design, m_route.chain.tracks.size())}; + editor_track::keep_heights(m_route.chain, pieces); + auto created{editor_track::relay(m_route.chain, pieces)}; + editor_track::commit(tracks); + for (auto const &entry : splitstates) + { + auto const found{std::find_if(states.begin(), states.end(), [&](auto const &State) { return State.first == entry.first; })}; + if (found != states.end()) + found->second = entry.second; + else + states.push_back(entry); + } + created.insert(created.begin(), splitcreated.begin(), splitcreated.end()); + push_track_snapshot(std::move(states), std::move(created)); + std::string error; + editor_track::find_chain(m_route.from, m_route.to, m_route.chain, error, true); + straight_refresh(); + state.status = STR_C("Done, select a curve to adjust it in the Curve mode"); +} + +double editor_mode::straight_tool_radius(editor_track::straight const &Line) const +{ + if (m_straights.curve_radius > 0.0) + return m_straights.curve_radius; + double speed{-1.0}; + for (auto const *track : Line.tracks) + speed = std::max(speed, editor_track::velocity(*track)); + if (speed <= 0.0) + speed = 100.0; + auto const recommended{alignment::recommend(speed, 1000.0, m_route.design.norms)}; + return std::max(150.0, std::ceil(recommended.radius_min * 1.5 / 50.0) * 50.0); +} + +bool editor_mode::place_straight_tool() +{ + auto &state{m_straights}; + if (ui()->mode() != nodebank_panel::TRACK || m_track_tab != track_tab::straights || state.tool == 0) + return false; + if (state.tool == 2) + { + add_detour_point(); + return true; + } + auto const &line{current_straight()}; + if (line.tracks.empty()) + return false; + glm::dvec3 const ground{cursor_ground()}; + auto const along{std::clamp(line.along(ground), 0.0, line.length)}; + glm::dvec3 const axis{line.direction.x, line.grade, line.direction.y}; + state.tool_line = line; + state.tool_at = along; + state.tool_point = line.start + axis * along; + auto const reach{std::min(50.0, std::max(1.0, line.length - along))}; + state.tool_handle = state.tool_point + axis * reach; + state.tool_placed = true; + state.tool_dragging = false; + state.handle = -1; + return true; +} + +void editor_mode::render_straight_tool_gizmo() +{ + auto &state{m_straights}; + gizmo_frame const frame; + if (false == state.tool_dragging) + state.tool_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(state.tool_handle - frame.camera)); + frame.manipulate(ImGuizmo::TRANSLATE, state.tool_gizmo, 0.0f); + if (ImGuizmo::IsUsing()) + { + state.tool_dragging = true; + auto const moved{frame.camera + glm::dvec3(state.tool_gizmo[3])}; + state.tool_handle = {moved.x, state.tool_handle.y, moved.z}; + typed_turn(); + } + else if (state.tool_dragging) + { + state.tool_dragging = false; + apply_straight_tool(); + } +} + +void editor_mode::apply_straight_tool() +{ + auto &state{m_straights}; + auto const line{state.tool_line}; + auto const offset{plan_of(state.tool_handle - state.tool_point)}; + auto const radius{straight_tool_radius(line)}; + if (state.tool == 1) + { + if (glm::length(offset) < 1e-3) + return; + auto const unit{glm::normalize(offset)}; + auto const angle{std::atan2(line.direction.x * unit.y - line.direction.y * unit.x, glm::dot(line.direction, unit))}; + if (std::abs(angle) < 1e-5) + return; + glm::dvec2 const pivot{state.tool_point.x, state.tool_point.z}; + double speed{-1.0}; + for (auto const *track : line.tracks) + speed = std::max(speed, editor_track::velocity(*track)); + auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; + auto const reach{radius * std::tan(std::min(std::abs(angle), 3.0) * 0.5) + transition * 0.5 + 10.0}; + straight_reshape(line, state.tool_at - reach, state.tool_at + reach, + [pivot, angle](glm::dvec3 const &Point) { + glm::dvec2 const relative{Point.x - pivot.x, Point.z - pivot.y}; + auto const c{std::cos(angle)}; + auto const s{std::sin(angle)}; + return glm::dvec3{pivot.x + relative.x * c - relative.y * s, Point.y, pivot.y + relative.x * s + relative.y * c}; + }, + radius); + } + else + { + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const shift{glm::dot(offset, normal)}; + auto const length{glm::dot(offset, line.direction)}; + if (std::abs(shift) < 1e-3 || length < 1.0) + return; + auto const translation{normal * shift}; + straight_reshape(line, state.tool_at, state.tool_at + length, + [translation](glm::dvec3 const &Point) { return glm::dvec3{Point.x + translation.x, Point.y, Point.z + translation.y}; }, + radius); + } + state.tool_placed = false; +} + +void editor_mode::start_straight_gesture(int const Tool) +{ + auto &state{m_straights}; + auto const previous{state.tool}; + state.tool = Tool; + if (false == place_straight_tool()) + { + state.tool = previous; + return; + } + state.tool_mouse = true; +} + +void editor_mode::finish_straight_gesture() +{ + auto &state{m_straights}; + state.tool_mouse = false; + apply_straight_tool(); + state.tool = 0; + state.tool_placed = false; +} + +void editor_mode::toggle_straight_set(TTrack &Track) +{ + auto &state{m_straights}; + auto const line{editor_track::find_straight(Track, state.tolerance)}; + if (line.tracks.empty()) + return; + if (in_straight_set(line)) + state.set.erase(std::remove_if(state.set.begin(), state.set.end(), [&](editor_track::straight const &Member) { return std::any_of(line.tracks.begin(), line.tracks.end(), [&](TTrack const *Other) { return std::find(Member.tracks.begin(), Member.tracks.end(), Other) != Member.tracks.end(); }); }), state.set.end()); + else + state.set.push_back(line); + m_node = &Track; + ui()->set_node(m_node); + m_track_tab = track_tab::straights; +} + +void editor_mode::delete_selected_track() +{ + auto *track{selected_track()}; + if (track == nullptr) + return; + if (false == track->Dynamics.empty()) + { + ui()->set_status(STR_C("The track can't be deleted while vehicles stand on it"), true); + return; + } + // retired first, so the infrastructure it carried and moves elsewhere is in the same undo step + editor_track::retire(*track); + // a switch leaves its main track behind, so the line isn't broken, unless Shift asks for all of it + std::vector created; + if (track->eType == tt_Switch && false == track->m_paths.empty() && false == Global.shiftState) + { + auto *main{editor_track::create_path(*track, track->m_paths.front())}; + editor_track::velocity(*main, speed_check::path_velocity(*track, 0)); + editor_track::commit({main}); + created.push_back(main); + ui()->set_status(STR_C("The switch is replaced by its main track, Shift+Del removes all of it"), false); + } + push_track_snapshot({}, created, {track}); + // the model and the events of a turntable can't stay without its track + if (track->eType == tt_Table) + for (auto const instance : turntable_includes(*track)) + { + scene::Layers.removed(instance, true); + m_history.back().instances.push_back(instance); + } + if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end()) + m_route = {}; + m_node = nullptr; + ui()->set_node(nullptr); + m_track_point = {}; + straight_refresh(); +} + +std::vector editor_mode::track_key_hints(bool const All) const +{ + std::vector hints; + bool gizmo{false}; + auto const *selected{selected_track()}; + if (m_lay.active) + { + if (m_lay.points.empty()) + hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Drag from a free end", "straight along it, to another free end: a curve joining them"}, {"Parallel", "first select a straight, then a second track beside it in the panel"}, {"Esc", "stop laying"}}; + else + { + hints = {{"LMB", m_lay.points.size() == 1 ? "end of the straight, or a vertex of a curve" : "next vertex of the curve"}, {"LMB on a free end", "join and lay"}, {"Enter", "lay up to the cursor"}, {"Backspace", "take back the point"}, {"Esc", "cancel"}}; + if (glm::dvec2 heading; lay_heading(heading)) + hints.insert(hints.begin() + 1, {"Shift", "straight on in the direction of the track"}); + } + } + else if (false == m_straights.detour.empty()) + { + char const *const steps[] = {"leave the axis", "shifted", "start back", "back on the axis"}; + auto const step{std::min(m_straights.detour.size(), 3)}; + hints = {{"Shift+click", format("point %zu of 4: %s", step + 1, steps[step])}, {"Esc", "cancel"}}; + } + else if (m_extend.active && m_extend_freehand) + hints = {{"Drag", "curve through the cursor, along the end: straight"}, {"Ctrl", "straight"}, {"Release", "build"}, {"Esc", "cancel"}}; + else if (m_extend.active) + hints = {{"Drag", "straight along the end, to a free end: a curve joining it"}, {"Shift", "straight, joins nothing"}, {"Release", "build"}, {"Esc", "cancel"}}; + else if (m_switch.placing) + hints = {{"Drag along", "direction of the switch"}, {"Drag sideways", "side of the diverging track"}, {"Esc", "cancel"}}; + else if (m_track_tab == track_tab::lay) + hints = {{"LMB", "start of the new track, on the ground or at a free end"}, {"Esc", "back to Select"}}; + else if (m_track_tab == track_tab::turnout && (selected_track() == nullptr || selected_track()->eType == tt_Switch)) + { + hints = {{"LMB on a track", "choose the straight or the curve to put the switch in"}}; + if (selected != nullptr) + { + hints.push_back({"Gizmo", "move the selected switch"}); + hints.push_back({"Del", "remove the switch, its main track stays"}); + gizmo = true; + } + } + else if (m_track_tab == track_tab::turnout && m_switch.armed < 0) + hints = {{"List below", "choose the type of the switch to put in"}, {"LMB on a track", "choose the straight or the curve"}, {"Esc", "back to Select"}}; + else if (m_track_tab == track_tab::turnout) + hints = {{"1. List below", "choose the type of the switch"}, {"2. Press+drag", "on the selected track where the switch starts: along sets the direction, sideways the side"}, {"LMB elsewhere", "choose another track"}}; + else if (m_track_tab == track_tab::profile && m_profile.route.spans.empty()) + hints = {{"LMB on a track", "loads the grade line of its line"}}; + else if (m_track_tab == track_tab::profile) + hints = {{"Drag a point", "in the profile window: changes the grade"}, {"Double click", "on the line: adds a point"}, {"Apply", "writes the heights into the tracks"}}; + else if (m_track_window_open && m_track_tab == track_tab::speed) + hints = {{"LMB on a row", "show the path"}}; + else if (m_track_window_open && m_track_tab == track_tab::lineside) + hints = {{"LMB on a track", "the line the objects go along"}}; + else if (m_track_window_open && m_track_tab == track_tab::signals && (m_signal.placing || m_signal.moving)) + hints = {{"Drag along", m_signal.moving ? "moves the signal along the track" : "the trains it faces run that way"}, {"Release", "done"}, {"Esc", "cancel"}}; + else if (m_track_window_open && m_track_tab == track_tab::signals) + { + hints = {{"LMB by a track", "places the signal on that side, facing the trains which have it on their right"}, {"Press+drag along", "the trains it faces run the way of the drag"}, {"LMB on a signal", "selects it"}, {"Drag a signal", "moves it along the track"}}; + if (m_signal.selected != 0) + hints.push_back({"Del", "removes the selected signal with its event"}); + } + else if (m_track_window_open && m_track_tab == track_tab::joints) + hints = {{"LMB on a row", "show the place"}, {"Fix", "joins the ends, aligns them"}}; + else if (m_track_window_open && m_track_tab == track_tab::infra) + hints = {{"LMB", "select a path"}}; + else if (m_track_tab == track_tab::straights && m_straights.tool == 1) + hints = {{"LMB on the straight", "where it breaks, then drag the handle to turn the rest"}, {"Esc", "cancel"}}; + else if (m_track_tab == track_tab::straights && m_straights.tool == 2) + hints = {{"LMB on the straight", "where the shift starts, then drag the handle: sideways the shift, along its length"}, {"Esc", "cancel"}}; + else if (m_track_tab == track_tab::straights && m_straights.dragging && m_straights.handle == 2) + { + auto const offer{straight_parallel_offer(m_straights.drag_line, m_straights.preview_start - m_straights.drag_line.start)}; + if (offer.snaps) + hints = {{"Release", format(STR_C("parallel at %.2f m"), offer.spacing)}, {"Esc", "cancel"}}; + else if (offer.nearby) + hints = {{"Near another straight", format(STR_C("snap at %.2f m (%.1f°)"), offer.spacing, offer.angle)}, {"Esc", "cancel"}}; + else + hints = {{"Drag the diamond", "snaps parallel at 4.00 / 4.50 / 4.75 m"}, {"Esc", "cancel"}}; + } + else if (m_track_tab == track_tab::straights && m_straights.current.tracks.empty()) + hints = {{"LMB on a straight track", "selects the whole straight"}}; + else if (m_track_tab == track_tab::straights) + { + hints = {{"Drag a square", "moves an end, the curves at it follow"}, {"Drag the diamond", "shifts sideways; close: snaps parallel at 4.00 / 4.50 / 4.75 m"}, {"Ctrl+drag", "breaks the straight"}, {"Shift+click x4", "shifts a piece around an obstacle"}, {"Alt+click", "adds another straight to move together"}}; + gizmo = true; + } + else if (m_track_tab == track_tab::route && m_route.chain.tracks.empty()) + hints = {{"LMB on a curve", "loads it with the straights at its ends"}}; + else if (m_track_tab == track_tab::route) + hints = {{"Drag yellow", "moves a vertex, the curve follows"}, {"Drag green", "changes the radius"}, {"Apply", "writes the design into the tracks"}, {"Esc", "release the vertex"}}; + else if (selected == nullptr) + hints = {{"LMB on a track", "selects it"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"RMB on a track", "what can be done with it"}, {"L", "lay a new track"}}; + else if (m_point_drag.moved) + hints = {{"Drag", "moves the point, level"}, {"Over a free end", "the end joins it"}, {"Release", "done"}, {"Esc", "done"}}; + else if (m_track_point.valid()) + { + hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", "moves the point along the axes"}, {"Esc", "releases the point"}}; + gizmo = true; + } + else + { + hints = {{"Drag a point", "moves it, an end dropped on a free end joins it"}, {"Gizmo", "moves the whole track"}, {"Shift+LMB", "adds a track to the selection, drag: a box"}, {"L", "new track, also from a free end"}, {"Esc", "deselects"}, {"Del", "deletes the track"}}; + gizmo = true; + } + if (auto const *meaning{typed_meaning()}; meaning != nullptr) + hints.insert(hints.begin() + std::min(1, hints.size()), {"0-9", meaning}); + if (All) + { + if (gizmo && m_gizmo_snap > 0.0f) + hints.push_back({"Ctrl", format("gizmo snaps by %.2f m", m_gizmo_snap)}); + hints.push_back({"K", "split the path under the cursor"}); + hints.push_back({"O", "top view"}); + if (Global.EditorOrtho) + hints.push_back({"Hold the wheel", "pans the view"}); + hints.push_back({"Ctrl+Z", "undo"}); + hints.push_back({"Ctrl+Y", "redo"}); + hints.push_back({"L T G C B P V J I", "modes: lay, switch, straight, curve, objects, profile, speed, joints, infra"}); + } + for (auto &hint : hints) + { + hint.key = STR_C(hint.key); + hint.action = STR(hint.action); + } + return hints; +} + +// values of the change in progress, shown next to the cursor +std::string editor_mode::track_readout() const +{ + auto const name = [](TTrack const *Track) { return Track->name().empty() ? std::string{"(noname)"} : Track->name(); }; + auto const azimuth = [](glm::dvec3 const &From, glm::dvec3 const &To) { + auto const degrees{glm::degrees(std::atan2(To.x - From.x, To.z - From.z))}; + return degrees < 0.0 ? degrees + 360.0 : degrees; + }; + if (m_extend.active) + { + double length{0.0}, radius{0.0}; + auto const pieces{extend_pieces()}; + for (auto const &piece : pieces) + { + length += bezier{piece}.plan_length(); + if (piece.radius != 0.0f && (radius == 0.0 || std::abs(piece.radius) < radius)) + radius = std::abs(piece.radius); + } + editor_track::snap_target target; + auto const snapped{extend_snap(target) && target.track != nullptr}; + if (snapped && pieces.empty()) + return "no track fits to the end of " + name(target.track); + auto text{radius > 0.0 ? format("L %.2f m R %.0f m", length, radius) : format("L %.2f m straight", length)}; + if (snapped) + text += "\njoins " + name(target.track); + return text; + } + if (m_lay.active) + { + auto const &lay{m_lay}; + std::string text; + if (lay.points.empty()) + text = lay.mouse_snap.track != nullptr ? "starts at the end of " + name(lay.mouse_snap.track) : "start"; + else if (false == lay.preview.empty()) + { + double radius{0.0}; + bool straight{false}; + for (auto const &piece : lay.preview) + { + auto const r{piece_plan_radius(piece)}; + if (r < 0.5) + straight = true; + else if (radius == 0.0 || r < radius) + radius = r; + } + if (radius > 0.0) + text = straight ? format("L %.1f m %s + R %.0f m", lay.preview_length, STR_C("straight"), radius) : format("L %.1f m R %.0f m", lay.preview_length, radius); + else + text = format("L %.1f m %s", lay.preview_length, STR_C("straight")); + } + if (false == lay.points.empty() && lay.mouse_snap.track != nullptr) + text += "\njoins " + name(lay.mouse_snap.track); + if (false == lay.preview_error.empty()) + text += "\n" + lay.preview_error; + return text; + } + auto const &state{m_straights}; + if (state.tool_placed && state.tool_dragging) + { + auto const &line{state.tool_line}; + auto const offset{plan_of(state.tool_handle - state.tool_point)}; + if (state.tool == 1) + { + if (glm::length(offset) < 1e-3) + return {}; + auto const turn{glm::degrees(signed_angle(line.direction, glm::normalize(offset)))}; + return format("turn %+.3f deg R %.0f m", turn, straight_tool_radius(line)); + } + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + return format("shift %+.3f m over %.2f m", glm::dot(offset, normal), glm::dot(offset, line.direction)); + } + if (state.dragging && state.fitting) + { + auto const &result{m_route.result}; + if (false == result.valid) + return result.errors.empty() ? std::string{"no curve fits"} : result.errors.front(); + auto text{format("curve L %.2f m", result.length)}; + for (auto const &curve : result.curves) + text += format(" deflection %.3f deg", glm::degrees(curve.deflection)); + return text; + } + if (state.dragging) + { + auto const &grabbed{state.drag_line}; + if (state.handle == 2) + { + glm::dvec2 const normal{-grabbed.direction.y, grabbed.direction.x}; + auto const offset{state.preview_start - grabbed.start}; + auto text{format("shift %+.3f m", glm::dot(plan_of(offset), normal))}; + auto const offer{straight_parallel_offer(grabbed, offset)}; + if (offer.snaps) + text += "\n" + format(STR_C("parallel %.2f m"), offer.spacing); + else if (offer.nearby) + text += "\n" + format(STR_C("close: %.2f m %.1f°"), offer.spacing, offer.angle); + return text; + } + return format("L %.3f m (%+.3f) azimuth %.4f deg", plan_distance(state.preview_start, state.preview_end), plan_distance(state.preview_start, state.preview_end) - grabbed.length, + azimuth(state.preview_start, state.preview_end)); + } + auto const *track{selected_track()}; + if (m_track_gizmo_using && track != nullptr) + { + if (m_track_point.valid() && m_track_point.path < static_cast(track->m_paths.size())) + { + auto const &path{track->m_paths[m_track_point.path]}; + auto text{format("L %.3f m", bezier{path}.plan_length())}; + if (path.radius != 0.0f) + text += format(" R %.1f m", std::abs(path.radius)); + if (m_track_snap.track != nullptr) + text += "\njoins " + name(m_track_snap.track); + return text; + } + return format("moved %.3f m", glm::distance(editor_track::pivot(*track), m_track_pivot)); + } + return {}; +} + +void editor_mode::draw_track_hints() +{ + ImGuiIO const &io = ImGui::GetIO(); + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + float const margin{12.0f}; + float const gap{14.0f}; + float const pad{4.0f}; + float const lineheight{ImGui::GetTextLineHeight() + 2.0f * pad + 4.0f}; + auto const chip = [&](ImVec2 const At, char const *Text, ImU32 const Background, ImU32 const Foreground) { + auto const size{ImGui::CalcTextSize(Text)}; + drawlist->AddRectFilled(ImVec2(At.x, At.y - 2.0f), ImVec2(At.x + size.x + 2.0f * pad, At.y + size.y + 2.0f), Background, 3.0f); + drawlist->AddText(ImVec2(At.x + pad, At.y), Foreground, Text); + return size.x + 2.0f * pad; + }; + auto const width_of = [&](key_hint const &Hint) { return ImGui::CalcTextSize(Hint.key).x + 2.0f * pad + 5.0f + ImGui::CalcTextSize(Hint.action.c_str()).x; }; + + auto hints{track_key_hints(false)}; + if (hints.size() > 4) + hints.resize(4); + // in the part of the window the 3d view is seen in, between the docked windows + auto const left{editor_ui::view_min().x + margin}; + auto const bottom{editor_ui::view_max().y}; + auto const available{(editor_ui::view_max().x - editor_ui::view_min().x) * 0.6f - 2.0f * margin}; + auto const mode{track_mode_name()}; + std::vector> lines(1); + float used{ImGui::CalcTextSize(mode.c_str()).x + 2.0f * pad + gap}; + for (auto const &hint : hints) + { + auto const width{width_of(hint)}; + if (false == lines.back().empty() && used + gap + width > available) + { + lines.emplace_back(); + used = 0.0f; + } + used += (lines.back().empty() ? 0.0f : gap) + width; + lines.back().push_back(&hint); + } + auto y{bottom - margin - lineheight * static_cast(lines.size()) + pad}; + for (std::size_t i = 0; i < lines.size(); ++i, y += lineheight) + { + float width{i == 0 ? ImGui::CalcTextSize(mode.c_str()).x + 2.0f * pad : 0.0f}; + for (auto const *hint : lines[i]) + width += (width > 0.0f ? gap : 0.0f) + width_of(*hint); + drawlist->AddRectFilled(ImVec2(left - 6.0f, y - pad - 2.0f), ImVec2(left + width + 6.0f, y + lineheight - pad - 2.0f), IM_COL32(0, 0, 0, 160), 4.0f); + auto x{left}; + if (i == 0) + x += chip(ImVec2(x, y), mode.c_str(), IM_COL32(60, 140, 230, 235), IM_COL32(255, 255, 255, 255)); + for (auto const *hint : lines[i]) + { + if (x > left) + x += gap; + x += chip(ImVec2(x, y), hint->key, IM_COL32(255, 210, 60, 220), IM_COL32(20, 20, 20, 255)) + 5.0f; + drawlist->AddText(ImVec2(x, y), IM_COL32(255, 255, 255, 230), hint->action.c_str()); + x += ImGui::CalcTextSize(hint->action.c_str()).x; + } + } + + auto readout{track_readout()}; + if (auto const *meaning{typed_meaning()}; meaning != nullptr && false == m_typed.empty()) + readout += (readout.empty() ? "" : "\n") + std::string{"= "} + m_typed + "_ " + STR(meaning); + if (readout.empty() || ImGui::GetIO().WantCaptureMouse) + return; + auto *foreground{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + auto const size{ImGui::CalcTextSize(readout.c_str())}; + ImVec2 at{io.MousePos.x + 20.0f, io.MousePos.y + 20.0f}; + at.x = std::min(at.x, io.DisplaySize.x - size.x - 10.0f); + at.y = std::min(at.y, io.DisplaySize.y - size.y - 10.0f); + foreground->AddRectFilled(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), IM_COL32(0, 0, 0, 190), 4.0f); + foreground->AddRect(ImVec2(at.x - 6.0f, at.y - 4.0f), ImVec2(at.x + size.x + 6.0f, at.y + size.y + 4.0f), overlay_color::marked, 4.0f); + foreground->AddText(at, IM_COL32(255, 255, 255, 255), readout.c_str()); +} + +void editor_mode::update_build_tools() +{ + glm::dvec3 const ground{cursor_ground()}; + auto const &io{ImGui::GetIO()}; + glm::vec2 const cursor{io.MousePos.x, io.MousePos.y}; + m_straights.detour_mouse = ground; + if (m_extend.active) + { + screen_projection const projection; + glm::dvec3 level; + m_extend.mouse = projection.on_level(io.MousePos, m_extend.point.y, level) && plan_distance(level, m_extend.point) < static_cast(kMaxPlacementDistance) ? level : ground; + typed_extend(); + m_extend.snap = m_typed.empty() && false == io.KeyShift && m_extend.track != nullptr ? snap_free_end(m_extend.mouse, cursor, m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}) : editor_track::snap_target{}; + } + if (m_switch.placing) + m_switch.mouse = cursor_level(m_switch.point.y); + auto &lay{m_lay}; + lay.preview.clear(); + lay.preview_error.clear(); + lay.preview_length = 0.0; + if (false == lay.active) + return; + // geometric ray, same as extend: Mouse_Position() is a delayed depth pick and lags the cursor + screen_projection const lay_projection; + double const height{false == lay.points.empty() ? lay.points.back().y : lay.start.track != nullptr ? lay.start.position.y : 0.0}; + glm::dvec3 mouse{ground}; + if (m_cursor_override.has_value()) + mouse = glm::dvec3{m_cursor_override->x, height, m_cursor_override->z}; + else if (false == lay_projection.on_level(io.MousePos, height, mouse)) + mouse = ground; + glm::dvec2 heading; + bool const straight{io.KeyShift && lay_heading(heading)}; + if (straight) + { + auto const &last{lay.points.back()}; + auto const along{std::max(0.0, glm::dot(glm::dvec2{mouse.x - last.x, mouse.z - last.z}, heading))}; + mouse = {last.x + heading.x * along, mouse.y, last.z + heading.y * along}; + } + lay.mouse = typed_lay(mouse); + lay.mouse_snap = straight ? editor_track::snap_target{} : m_typed.empty() ? snap_free_end(lay.mouse, cursor, nullptr, kRailCategory, {}) : editor_track::find_free_end(nullptr, kRailCategory, lay.mouse, kLaySnapRadius, {}); + if (lay.points.empty()) + return; + auto points{lay.points}; + auto end{lay.mouse_snap}; + if (end.track != nullptr && glm::distance(end.position, points.front()) > 1.0) + points.push_back(end.position); + else + { + end = {}; + if (plan_distance(lay.mouse, points.back()) > 1.0) + points.push_back(lay.mouse); + } + lay.preview = lay_pieces(points, end, lay.preview_length, lay.preview_error); +} + +bool editor_mode::free_end(TTrack &Track, int const Path, bool const Atend, extend_tool &Tool) +{ + editor_track::point_ref const point{Path, Atend ? editor_track::point_kind::end : editor_track::point_kind::start}; + if (Path < 0 || Path >= static_cast(Track.m_paths.size()) || editor_track::is_connected(Track, point)) + return false; + auto const &path{Track.m_paths[Path]}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + auto const &control1{path.points[segment_data::point::control1]}; + auto const &control2{path.points[segment_data::point::control2]}; + glm::dvec3 tangent; + if (Atend) + tangent = control2 != glm::dvec3{} ? -control2 : end - (start + control1); + else + tangent = -(control1 != glm::dvec3{} ? control1 : (end + control2) - start); + glm::dvec2 const planar{tangent.x, tangent.z}; + if (glm::length(planar) < 1e-6) + return false; + Tool.path = Path; + Tool.atend = Atend; + Tool.track = &Track; + Tool.point = editor_track::point_position(Track, point); + Tool.direction = glm::normalize(planar); + Tool.grade = tangent.y / glm::length(planar); + Tool.mouse = Tool.point; + return true; +} + +bool editor_mode::chosen_free_end(extend_tool &Tool) +{ + auto *track{selected_track()}; + if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) + return false; + if (m_track_point.valid() && editor_track::is_end(m_track_point.kind) && free_end(*track, m_track_point.path, m_track_point.kind == editor_track::point_kind::end, Tool)) + return true; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + for (auto const atend : {true, false}) + if (free_end(*track, i, atend, Tool)) + return true; + return false; +} + +bool editor_mode::start_extend(editor_track::snap_target const &End) +{ + auto *track{End.track}; + if (track == nullptr || (track->eType != tt_Normal && track->eType != tt_Switch) || false == editor_track::is_supported(*track)) + return false; + extend_tool tool; + if (false == free_end(*track, End.point.path, End.point.kind == editor_track::point_kind::end, tool)) + return false; + auto const &io{ImGui::GetIO()}; + m_extend = tool; + m_extend.origin = End; + m_extend.pressed = {io.MousePos.x, io.MousePos.y}; + m_extend.active = true; + return true; +} + +bool editor_mode::extend_snap(editor_track::snap_target &Target) const +{ + auto const &tool{m_extend}; + if (false == tool.active || tool.track == nullptr) + return false; + Target = tool.snap; + return Target.track != nullptr && glm::distance(Target.position, tool.point) > 0.5; +} + +glm::dvec3 editor_mode::cursor_ground() const +{ + glm::dvec3 const ground{Global.pCamera.Pos + GfxRenderer->Mouse_Position()}; + if (GfxRenderer->Mouse_Hit()) + return ground; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(ImGui::GetIO().MousePos, 0.0, level)) + return level; + return {ground.x, 0.0, ground.z}; +} + +glm::dvec3 editor_mode::cursor_level(double const Height) const +{ + if (m_cursor_override.has_value()) + return glm::dvec3{m_cursor_override->x, Height, m_cursor_override->z}; + glm::dvec3 const ground{cursor_ground()}; + screen_projection const projection; + glm::dvec3 level; + if (false == projection.on_level(ImGui::GetIO().MousePos, Height, level) || (false == Global.editor_tracks && glm::distance(level, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance))) + return ground; + return level; +} + +editor_track::snap_target editor_mode::snap_free_end(glm::dvec3 const &Near, glm::vec2 const &Screen, TTrack const *Self, int const Category, std::vector const &Exclude) const +{ + screen_projection const projection; + auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; + editor_track::snap_target result; + auto best{kSnapPixels * kSnapPixels}; + for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) + { + ImVec2 screen; + if (false == projection.project(candidate.position, screen)) + continue; + auto const distance{(screen.x - Screen.x) * (screen.x - Screen.x) + (screen.y - Screen.y) * (screen.y - Screen.y)}; + if (distance < best) + { + best = distance; + result = candidate; + } + } + if (result.track == nullptr && m_track_snap_radius > 0.0f) + result = editor_track::find_free_end(Self, Category, Near, m_track_snap_radius, Exclude); + return result; +} + +void editor_mode::draw_free_ends(TTrack const *Self, int const Category, std::vector const &Exclude) const +{ + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const &display{ImGui::GetIO().DisplaySize}; + auto const reach{Global.EditorOrtho ? std::max(kJointRange, static_cast(Global.EditorOrthoExtent) * 2.0) : kJointRange}; + for (auto const &candidate : editor_track::free_ends(Self, Category, glm::dvec3{Global.pCamera.Pos}, reach, Exclude)) + { + ImVec2 screen; + if (projection.project(candidate.position, screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y) + drawlist->AddCircle(screen, 7.0f, IM_COL32(255, 60, 255, 170), 12, 1.5f); + } +} + +bool editor_mode::switch_reach(glm::dvec3 const &Ground, glm::dvec3 &Point) +{ + auto *selected{selected_track()}; + if (m_track_tab != track_tab::turnout || m_switch.armed < 0 || selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) + return false; + auto const &line{current_straight()}; + if (false == line.tracks.empty()) + { + auto const along{std::clamp(line.along(Ground), 0.0, line.length)}; + Point = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along; + } + else + { + Point = editor_track::point_at(*selected, editor_track::nearest_parameter(*selected, Ground)); + } + return plan_distance(Point, Ground) <= kSwitchReach; +} + +void editor_mode::handle_drag_start(glm::dvec3 const &Anchor) +{ + auto const &io{ImGui::GetIO()}; + m_handle_drag = {}; + m_handle_drag.active = true; + m_handle_drag.from = {io.MousePos.x, io.MousePos.y}; + m_handle_drag.height = Anchor.y; + m_handle_drag.position = Anchor; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(io.MousePos, Anchor.y, level)) + m_handle_drag.offset = {Anchor.x - level.x, 0.0, Anchor.z - level.z}; +} + +bool editor_mode::handle_drag_step(glm::dvec3 &Position) +{ + auto &drag{m_handle_drag}; + auto const &io{ImGui::GetIO()}; + if (false == drag.active) + return false; + if (false == io.MouseDown[0]) + { + drag = {}; + return false; + } + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + if (false == drag.moved && glm::length(mouse - drag.from) < kDragPixels) + return false; + drag.moved = true; + screen_projection const projection; + glm::dvec3 level; + if (projection.on_level(io.MousePos, drag.height, level) && glm::distance(level, glm::dvec3{Global.pCamera.Pos}) <= static_cast(kMaxPlacementDistance)) + drag.position = level + drag.offset; + Position = drag.position; + return true; +} + +void editor_mode::point_drag_start(editor_track::point_ref const &Point) +{ + auto *track{selected_track()}; + m_track_point = Point; + m_point_drag = {}; + if (track == nullptr || false == Point.valid() || Point.path >= static_cast(track->m_paths.size())) + return; + auto const &io{ImGui::GetIO()}; + m_point_drag.active = true; + m_point_drag.from = {io.MousePos.x, io.MousePos.y}; + m_point_drag.height = editor_track::point_position(*track, Point).y; +} + +void editor_mode::point_drag_update() +{ + auto &drag{m_point_drag}; + if (false == drag.active) + return; + auto const &io{ImGui::GetIO()}; + if (false == io.MouseDown[0]) + { + point_drag_finish(); + return; + } + auto *track{selected_track()}; + if (track == nullptr || false == m_track_point.valid() || m_track_point.path >= static_cast(track->m_paths.size())) + { + drag = {}; + return; + } + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + if (false == drag.moved) + { + if (glm::length(mouse - drag.from) < kDragPixels) + return; + std::string reason; + if (false == editor_track::can_edit_geometry(*track, reason)) + { + ui()->set_status(reason, true); + drag = {}; + return; + } + drag.moved = true; + m_track_drag = {track}; + m_track_drag_points.clear(); + m_track_joints.clear(); + m_track_snap = {}; + if (editor_track::is_end(m_track_point.kind) && m_track_drag_connected) + { + for (auto const &connection : editor_track::connected_points(*track, editor_track::point_position(*track, m_track_point))) + { + std::string neighbourreason; + if (false == editor_track::can_edit_geometry(*connection.first, neighbourreason)) + continue; + m_track_drag_points.emplace_back(connection); + if (std::find(m_track_drag.begin(), m_track_drag.end(), connection.first) == m_track_drag.end()) + m_track_drag.emplace_back(connection.first); + } + } + std::vector> states; + for (auto *dragged : m_track_drag) + states.emplace_back(dragged, editor_track::capture(*dragged)); + push_track_snapshot(std::move(states)); + } + screen_projection const projection; + glm::dvec3 position; + if (false == projection.on_level(io.MousePos, drag.height, position) || glm::distance(position, glm::dvec3{Global.pCamera.Pos}) > static_cast(kMaxPlacementDistance)) + return; + m_track_snap = {}; + if (editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) + { + std::vector const exclude(m_track_drag.begin(), m_track_drag.end()); + m_track_snap = snap_free_end(position, mouse, track, track->iCategoryFlag & 15, exclude); + if (m_track_snap.track != nullptr) + position = m_track_snap.position; + } + editor_track::move_point(*track, m_track_point, position); + for (auto const &connection : m_track_drag_points) + editor_track::move_point(*connection.first, connection.second, position); + m_track_dirty = true; + commit_track_drag(false); +} + +void editor_mode::point_drag_finish() +{ + auto const drag{m_point_drag}; + m_point_drag = {}; + if (false == drag.active || false == drag.moved) + return; + if (auto *track{selected_track()}; track != nullptr && m_track_snap.track != nullptr && m_track_point.valid()) + { + editor_track::snap_point(*track, m_track_point, m_track_snap, m_track_align_tangent); + m_track_dirty = true; + } + m_track_snap = {}; + commit_track_drag(true); + m_track_drag.clear(); + m_track_drag_points.clear(); + m_track_joints.clear(); +} + +std::vector editor_mode::extend_pieces() const +{ + std::vector result; + auto const &tool{m_extend}; + editor_track::snap_target target; + if (extend_snap(target)) + { + alignment::design design; + design.norms = m_route.design.norms; + design.shape = m_route.design.shape; + design.arc_piece_angle = 90.0; + design.start = tool.point; + design.end = target.position; + design.start_direction = tool.direction; + design.end_direction = glm::normalize(plan_of(target.direction)); + design.start_grade = tool.grade; + design.end_grade = grade_of(target.direction); + auto const speed{editor_track::velocity(*tool.track)}; + design.speed = speed > 0.0 ? speed : 100.0; + if (false == alignment::collinear(design)) + { + alignment::vertex vertex; + vertex.radius = 100000.0; + auto const recommended{alignment::recommend(design.speed, 1000.0, design.norms)}; + vertex.transition_in = vertex.transition_out = recommended.transition; + glm::dvec2 intersection; + if (alignment::tangent_intersection(design, intersection)) + { + design.vertices.push_back(vertex); + } + else + { + vertex.offset = plan_distance(design.start, design.end) / 3.0; + design.vertices = {vertex, vertex}; + } + } + // a track which doesn't fit to the free end isn't built towards the cursor instead, it'd miss the end + auto const shape{alignment::compute(design)}; + return shape.valid ? alignment::pieces(shape, design, 0) : result; + } + if (false == ImGui::GetIO().KeyShift && tool.track != nullptr && tool.track->eType == tt_Switch && tool.track->m_paths.size() > 1 && tool.path == 1 && tool.atend) + { + auto const &diverging{tool.track->m_paths[1]}; + bezier const curve{diverging}; + auto const begin{plan_of(curve.first(0.0))}; + auto const finish{plan_of(curve.first(1.0))}; + auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; + if (std::abs(turn) > 1e-4) + { + int const side{turn > 0.0 ? 1 : -1}; + auto const radius{curve.plan_length() / std::abs(turn)}; + auto const &direction{tool.direction}; + auto const centre{plan_of(tool.point) + glm::dvec2{-direction.y, direction.x} * (side * radius)}; + auto const angle{geometry::signed_angle(plan_of(tool.point) - centre, plan_of(tool.mouse) - centre) * side}; + if (angle * radius < 0.5) + return result; + auto pieces{arc_pieces(tool.point, direction, tool.grade, radius, std::min(angle, glm::pi()), side)}; + for (auto &piece : pieces) + piece.rolls = {diverging.rolls[1], diverging.rolls[1]}; + return pieces; + } + } + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const chord{glm::length(offset)}; + if (chord < 0.5) + return result; + auto const &direction{tool.direction}; + auto const along{glm::dot(offset, direction)}; + auto const lateral{direction.x * offset.y - direction.y * offset.x}; + auto const height = [&](double const Distance) { return tool.point.y + tool.grade * Distance; }; + if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl || ImGui::GetIO().KeyShift || std::abs(lateral) < 0.005 * chord + 0.05) + { + if (along < 0.5) + return result; + segment_data path; + path.points[segment_data::point::start] = tool.point; + path.points[segment_data::point::end] = {tool.point.x + direction.x * along, height(along), tool.point.z + direction.y * along}; + result.push_back(path); + return result; + } + auto const angle{2.0 * std::atan2(std::abs(lateral), along)}; + return arc_pieces(tool.point, direction, tool.grade, chord / (2.0 * std::sin(angle * 0.5)), angle, lateral > 0.0 ? 1 : -1); +} + +void editor_mode::finish_extend() +{ + auto const &io{ImGui::GetIO()}; + if (glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - m_extend.pressed) < kDragPixels && m_typed.empty()) + { + m_extend.active = false; + if (m_lay.active && m_lay.points.empty() && m_extend.origin.track != nullptr) + { + m_lay.status.clear(); + m_lay.start = m_extend.origin; + m_lay.points.push_back(m_extend.origin.position); + } + else if (false == m_lay.active && m_extend.origin.track != nullptr && m_extend.origin.track == selected_track()) + m_track_point = m_extend.origin.point; + return; + } + crossover_plan plan; + editor_track::snap_target target; + if (false == extend_snap(target) && plan_crossover(plan)) + { + auto *origin{m_extend.track}; + m_extend.active = false; + std::vector> states; + std::vector created; + std::vector removed; + if (false == place_switch_on_straight(plan.target, plan.shape, origin, plan.along, plan.direction, plan.side, false, states, created, removed)) + return; + if (glm::distance(plan.insert.points[segment_data::point::start], plan.insert.points[segment_data::point::end]) > 0.01) + { + auto *insert{editor_track::create_path(*origin, plan.insert)}; + editor_track::commit({insert}); + created.push_back(insert); + } + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + return; + } + auto const pieces{extend_pieces()}; + auto *style{m_extend.track}; + m_extend.active = false; + if (pieces.empty() || style == nullptr) + return; + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*style, piece)); + editor_track::commit(created); + push_track_snapshot({}, created); + m_node = created.back(); + ui()->set_node(m_node); + straight_refresh(); +} + +void editor_mode::extend_add(int const Side) +{ + extend_tool tool; + if (false == chosen_free_end(tool)) + return; + auto const length{std::max(0.5, m_extend_length)}; + std::vector pieces; + if (Side == 0) + { + segment_data path; + path.points[segment_data::point::start] = tool.point; + path.points[segment_data::point::end] = {tool.point.x + tool.direction.x * length, tool.point.y + tool.grade * length, tool.point.z + tool.direction.y * length}; + pieces.push_back(path); + } + else + { + auto const radius{std::max(10.0, m_extend_radius)}; + pieces = arc_pieces(tool.point, tool.direction, tool.grade, radius, std::min(length / radius, glm::radians(180.0)), Side); + } + std::vector created; + for (auto const &piece : pieces) + created.push_back(editor_track::create_path(*tool.track, piece)); + editor_track::commit(created); + push_track_snapshot({}, created); + m_node = created.back(); + ui()->set_node(m_node); + m_track_point = {}; + straight_refresh(); +} + +void editor_mode::render_extend_ui() +{ + extend_tool tool; + if (false == chosen_free_end(tool)) + return; + if (false == ImGui::CollapsingHeader(STR_C("Continue from the free end"), ImGuiTreeNodeFlags_DefaultOpen)) + return; + ImGui::PushItemWidth(100.0f); + ImGui::DragScalar(STR_C("Length (m)##extend"), ImGuiDataType_Double, &m_extend_length, 0.5f, nullptr, nullptr, "%.1f"); + item_tooltip("Length of the next piece, along the curve for the curves"); + ImGui::SameLine(); + ImGui::DragScalar("R (m)##extend", ImGuiDataType_Double, &m_extend_radius, 5.0f, nullptr, nullptr, "%.0f"); + item_tooltip("Radius of the curves added"); + ImGui::PopItemWidth(); + m_extend_length = std::clamp(m_extend_length, 0.5, 5000.0); + m_extend_radius = std::clamp(m_extend_radius, 10.0, 100000.0); + if (ImGui::Button(STR_C("Straight##extend"), ImVec2(100.0f, 0.0f))) + extend_add(0); + item_tooltip("Adds a straight at the free end; the new piece gets selected, so the next click goes on from it"); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Curve left##extend"), ImVec2(100.0f, 0.0f))) + extend_add(1); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Curve right##extend"), ImVec2(100.0f, 0.0f))) + extend_add(-1); + ImGui::Checkbox(STR_C("Freehand"), &m_extend_freehand); + item_tooltip("Dragging from a free end draws a curve through the cursor; off: it draws a straight along the end"); +} + +std::vector editor_mode::lay_pieces(std::vector const &Points, editor_track::snap_target const &End, double &Length, std::string &Error) const +{ + auto const &tool{m_lay}; + Length = 0.0; + if (Points.size() < 2) + return {}; + auto const startdirection{tool.start.track != nullptr ? -plan_of(tool.start.direction) : plan_of(Points[1] - Points[0])}; + auto const enddirection{End.track != nullptr ? plan_of(End.direction) : plan_of(Points.back() - Points[Points.size() - 2])}; + if (glm::length(startdirection) < 1e-3 || glm::length(enddirection) < 1e-3) + { + Error = STR_C("Two points lie on one another"); + return {}; + } + alignment::design design; + auto const shape{lay_shape(Points, glm::normalize(startdirection), glm::normalize(enddirection), design)}; + if (false == shape.valid) + { + Error = shape.errors.empty() ? std::string{STR_C("The track can't be laid through these points")} : shape.errors.front(); + return {}; + } + Error = shape.warnings.empty() ? std::string{} : shape.warnings.front(); + auto const count = [&](alignment::result const &Shape, alignment::design const &Design) { + return std::max(alignment::minimum_pieces(Shape, Design), static_cast(std::ceil(Shape.length / std::max(5.0, tool.piece_length)))); + }; + // the curvature of the track laid has to go on from the one of the track it joins, a jump gets a transition curve between them + auto const grade{(Points.back().y - Points.front().y) / std::max(1.0, plan_distance(Points.front(), Points.back()))}; + auto const eased = [&](editor_track::snap_target const &Joint, double const Chainage, double const Sign) { + if (Joint.track == nullptr || Joint.track->eType != tt_Normal) + return easing{}; + auto result{ease_out_of(*Joint.track, Joint.position, grade * Sign, design.speed, design.norms)}; + auto const own{Sign * curvature_at(shape, Chainage)}; + if (std::abs(own - result.curvature) <= 0.05 * std::abs(result.curvature)) + result.pieces.clear(); + return result; + }; + auto const in{eased(tool.start, 0.0, 1.0)}; + auto const out{eased(End, shape.length, -1.0)}; + if (false == in.pieces.empty() || false == out.pieces.empty()) + { + auto points{Points}; + auto starting{design.start_direction}; + auto ending{design.end_direction}; + if (false == in.pieces.empty()) + { + points.front() = in.end; + starting = in.direction; + } + if (false == out.pieces.empty()) + { + points.back() = out.end; + ending = -out.direction; + } + alignment::design easeddesign; + auto const easedshape{lay_shape(points, starting, ending, easeddesign)}; + if (easedshape.valid) + { + auto result{in.pieces}; + auto const middle{alignment::pieces(easedshape, easeddesign, count(easedshape, easeddesign))}; + result.insert(result.end(), middle.begin(), middle.end()); + for (auto piece{out.pieces.rbegin()}; piece != out.pieces.rend(); ++piece) + result.push_back(turned_around(*piece)); + Length = easedshape.length + (in.pieces.empty() ? 0.0 : in.length) + (out.pieces.empty() ? 0.0 : out.length); + Error = easedshape.warnings.empty() ? std::string{} : easedshape.warnings.front(); + return result; + } + Error = STR("The curvature changes abruptly where the track joins, there's no room for a transition curve") + (easedshape.errors.empty() ? std::string{} : ": " + easedshape.errors.front()); + } + Length = shape.length; + return alignment::pieces(shape, design, count(shape, design)); +} + +alignment::result editor_mode::lay_shape(std::vector const &Points, glm::dvec2 const &Startdirection, glm::dvec2 const &Enddirection, alignment::design &design) const +{ + auto const &tool{m_lay}; + design = {}; + design.norms = m_route.design.norms; + design.shape = m_route.design.shape; + design.speed = tool.style.velocity > 0.0 ? tool.style.velocity : 100.0; + design.start = Points.front(); + design.end = Points.back(); + design.start_direction = Startdirection; + design.end_direction = Enddirection; + + alignment::vertex corner; + corner.radius = std::max(1.0, tool.radius); + if (tool.transitions) + { + auto const recommended{alignment::recommend(design.speed, corner.radius, design.norms)}; + corner.cant = recommended.cant; + corner.transition_in = corner.transition_out = recommended.transition; + } + if (Points.size() == 2) + { + if (false == alignment::collinear(design)) + { + glm::dvec2 intersection; + if (alignment::tangent_intersection(design, intersection)) + { + design.vertices.push_back(corner); + } + else + { + corner.offset = plan_distance(design.start, design.end) / 3.0; + design.vertices = {corner, corner}; + } + } + } + else + { + for (std::size_t i = 1; i + 1 < Points.size(); ++i) + { + corner.position = plan_of(Points[i]); + design.vertices.push_back(corner); + } + if (design.vertices.size() >= 2) + { + design.vertices.front().offset = std::max(1.0, glm::dot(plan_of(Points[1] - Points.front()), design.start_direction)); + design.vertices.back().offset = std::max(1.0, glm::dot(plan_of(Points.back() - Points[Points.size() - 2]), design.end_direction)); + } + } + return alignment::compute(design); +} + +void editor_mode::lay_click() +{ + auto &tool{m_lay}; + tool.status.clear(); + auto const snap{tool.mouse_snap}; + if (snap.track != nullptr && false == tool.points.empty() && glm::distance(snap.position, tool.points.front()) > 1.0) + { + // reaching a free end finishes the track there, in line with the path it joins + tool.points.push_back(snap.position); + lay_finish(snap); + return; + } + if (tool.points.empty()) + { + tool.start = snap; + tool.points.push_back(snap.track != nullptr ? snap.position : placement_on_ground(tool.mouse)); + return; + } + auto const point{placement_on_ground(tool.mouse)}; + if (plan_distance(point, tool.points.back()) < 1.0) + return; + tool.points.push_back(point); + m_typed.clear(); +} + +void editor_mode::lay_finish(editor_track::snap_target const &End) +{ + auto &tool{m_lay}; + double length; + std::string error; + auto const pieces{lay_pieces(tool.points, End, length, error)}; + if (pieces.empty()) + { + tool.status = error.empty() ? std::string{STR_C("Click at least two points")} : error; + if (End.track != nullptr && false == tool.points.empty()) + tool.points.pop_back(); + return; + } + auto const *style{tool.start.track != nullptr ? tool.start.track : End.track}; + if (style == nullptr && tool.copy_selected) + style = selected_track(); + std::vector created; + for (auto const &piece : pieces) + created.push_back(style != nullptr ? editor_track::create_path(*style, piece) : editor_track::create_path(tool.style, piece)); + editor_track::commit(created); + push_track_snapshot({}, created); + tool.status = format(STR_C("Laid %zu paths, %.1f m"), created.size(), length); + if (false == error.empty()) + tool.status += "\n" + error; + { + std::vector> run; + if (tool.start.track != nullptr && tool.start.track->eType == tt_Normal) + run.emplace_back(tool.start.track, tool.start.point.kind == editor_track::point_kind::end); + for (auto *track : created) + run.emplace_back(track, true); + if (End.track != nullptr && End.track->eType == tt_Normal) + run.emplace_back(End.track, End.point.kind == editor_track::point_kind::start); + editor_track::curve curve; + if (run.size() > created.size() && editor_track::analyse_curve(run, m_route.design.norms.gauge, curve) && curve.compound) + for (std::size_t i = 1; i < curve.arcs.size(); ++i) + if (curve.arcs[i].transition < 1.0) + { + tool.status += "\n" + format(STR_C("Compound curve: R %.0f m meets R %.0f m with no transition curve between them"), curve.arcs[i - 1].radius, curve.arcs[i].radius); + break; + } + } + if (scene::Layers.active() == null_handle && style == nullptr) + tool.status += "\nThe scenery isn't opened for editing, the new track won't be saved"; + WriteLog("Editor: " + tool.status, logtype::generic); + tool.points.clear(); + tool.start = {}; + m_node = created.back(); + ui()->set_node(m_node); + straight_refresh(); +} + +bool editor_mode::lay_heading(glm::dvec2 &Heading) const +{ + auto const &tool{m_lay}; + glm::dvec2 direction{0.0}; + if (tool.points.size() >= 2) + direction = plan_of(tool.points.back() - tool.points[tool.points.size() - 2]); + else if (tool.points.size() == 1 && tool.start.track != nullptr) + direction = -plan_of(tool.start.direction); + if (glm::length(direction) < 1e-6) + return false; + Heading = glm::normalize(direction); + return true; +} + +void editor_mode::lay_enter() +{ + auto &tool{m_lay}; + if (false == m_typed.empty()) + { + lay_click(); + return; + } + if (tool.points.empty()) + return; + auto const end{tool.mouse_snap}; + if (end.track != nullptr && glm::distance(end.position, tool.points.front()) > 1.0) + { + tool.points.push_back(end.position); + lay_finish(end); + return; + } + auto const point{placement_on_ground(tool.mouse)}; + bool const appended{plan_distance(point, tool.points.back()) > 1.0}; + if (appended) + tool.points.push_back(point); + auto const count{tool.points.size()}; + lay_finish({}); + if (appended && tool.points.size() == count) + tool.points.pop_back(); +} + +void editor_mode::lay_cancel() +{ + auto &tool{m_lay}; + tool.points.clear(); + tool.start = {}; + tool.preview.clear(); +} + +void editor_mode::render_lay_ui() +{ + auto &tool{m_lay}; + if (auto *track{selected_track()}; track != nullptr) + { + std::string reason; + if (editor_track::can_edit_geometry(*track, reason)) + render_extend_ui(); + } + if (ImGui::CollapsingHeader(STR_C("Parallel track"), ImGuiTreeNodeFlags_DefaultOpen)) + render_parallel_ui(); + ImGui::Separator(); + ImGui::PushItemWidth(120.0f); + ImGui::DragScalar(STR_C("Curve radius (m)"), ImGuiDataType_Double, &tool.radius, 5.0f, nullptr, nullptr, "%.0f"); + item_tooltip("Drag to change, Ctrl+click to type the value"); + tool.radius = std::max(10.0, tool.radius); + ImGui::DragScalar(STR_C("Speed (km/h)"), ImGuiDataType_Double, &tool.style.velocity, 1.0f, nullptr, nullptr, "%.0f"); + tool.style.velocity = std::clamp(tool.style.velocity, 0.0, 400.0); + item_tooltip("Speed limit of the new paths, and the design speed of the transition curves and of the cant"); + ImGui::PopItemWidth(); + if (false == ImGui::TreeNode(STR_C("More settings"))) + { + render_lay_status(); + return; + } + ImGui::PushItemWidth(120.0f); + ImGui::Checkbox(STR_C("Transition curves and cant"), &tool.transitions); + item_tooltip("Lengths and the cant recommended for the speed and the radius; off: plain circular arcs with no cant"); + ImGui::DragScalar(STR_C("Longest path (m)"), ImGuiDataType_Double, &tool.piece_length, 1.0f, nullptr, nullptr, "%.0f"); + tool.piece_length = std::clamp(tool.piece_length, 5.0, 1000.0); + ImGui::PopItemWidth(); + ImGui::Checkbox(STR_C("Parameters of the selected track"), &tool.copy_selected); + item_tooltip("Takes the textures and the other parameters from the selected track.\nA track started or finished at a free end always takes them from the track it joins"); + if (false == tool.copy_selected) + { + auto const text_field = [](char const *Label, std::string &Value, char const *Tooltip) { + char buffer[256]; + std::snprintf(buffer, sizeof(buffer), "%s", Value.c_str()); + if (ImGui::InputText(Label, buffer, sizeof(buffer))) + Value = buffer; + item_tooltip(Tooltip); + }; + ImGui::PushItemWidth(-1.0f); + text_field("##layrail", tool.style.rail, STR_C("Texture of the rails")); + text_field("##layballast", tool.style.ballast, STR_C("Texture of the ballast")); + ImGui::PopItemWidth(); + } + ImGui::TreePop(); + render_lay_status(); +} + +void editor_mode::render_lay_status() +{ + auto const &tool{m_lay}; + if (scene::Layers.active() == null_handle) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), STR_C("The scenery isn't opened for editing: new track won't be saved")); + if (tool.active && false == tool.preview_error.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.3f, 1.0f), "%s", tool.preview_error.c_str()); + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +std::vector editor_mode::switch_preview() const +{ + auto const &tool{m_switch}; + if (false == tool.placing || tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return {}; + if (tool.curved) + { + auto const sample{sample_at(tool.frame, tool.along)}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{sample.tangent * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + return curved_switch_paths(tool.templates[tool.armed], tool.frame, tool.along, forward, side); + } + auto const &line{tool.line}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, line.direction) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{line.direction * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + return editor_track::place_switch(tool.templates[tool.armed], tool.point, direction, side, line.grade * forward); +} + +bool editor_mode::start_switch_placement() +{ + auto &tool{m_switch}; + if (tool.armed < 0 || m_track_tab != track_tab::turnout) + return false; + // with no plain track selected, or a press away from it, the click selects + auto *selected{selected_track()}; + if (selected == nullptr || selected->eType != tt_Normal || selected->m_paths.empty()) + return false; + glm::dvec3 const ground{cursor_level(selected->m_paths.front().points[segment_data::point::start].y)}; + tool.curved = false; + tool.status.clear(); + if (current_straight().tracks.empty()) + { + auto *track{selected}; + if (tool.templates[tool.armed].double_slip) + { + tool.status = STR_C("A double slip goes only into a straight crossing another one"); + return false; + } + editor_track::curve curve; + editor_track::chain chain; + std::string error; + if (false == editor_track::find_curve(*track, m_straights.tolerance, m_route.design.norms.gauge, curve)) + { + tool.status = STR_C("The selected track is neither a recognized straight nor a curve"); + return false; + } + if (false == editor_track::find_chain(curve.from, curve.to, chain, error, true)) + { + tool.status = error.empty() ? std::string{STR_C("The curve can't be followed")} : error; + return false; + } + for (auto const &part : editor_track::chain_parts(chain)) + if (std::find(part.tracks.begin(), part.tracks.end(), track) != part.tracks.end()) + { + chain = part; + break; + } + tool.frame = sample_chain(chain); + tool.frame_tracks = chain.tracks; + if (tool.frame.size() < 2) + return false; + tool.curved = true; + tool.along = nearest_station(tool.frame, ground); + tool.point = sample_at(tool.frame, tool.along).position; + if (plan_distance(tool.point, ground) > kSwitchReach) + return false; + tool.mouse = ground; + tool.placing = true; + return true; + } + auto const &line{current_straight()}; + auto const along{std::clamp(line.along(ground), 0.0, line.length)}; + auto const point{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * along}; + if (plan_distance(point, ground) > kSwitchReach) + return false; + if (tool.templates[tool.armed].double_slip) + { + insert_double_slip(line, ground, tool.templates[tool.armed]); + return true; + } + tool.along = along; + tool.point = point; + tool.mouse = ground; + tool.line = line; + tool.placing = true; + return true; +} + +void editor_mode::finish_switch_placement() +{ + auto &tool{m_switch}; + tool.placing = false; + if (tool.armed < 0 || tool.armed >= static_cast(tool.templates.size())) + return; + if (tool.curved) + { + auto const sample{sample_at(tool.frame, tool.along)}; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, sample.tangent) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{sample.tangent * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + insert_curved_switch(forward, side); + return; + } + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + auto const forward{glm::dot(offset, tool.line.direction) >= 0.0 ? 1 : -1}; + glm::dvec2 const direction{tool.line.direction * static_cast(forward)}; + auto const side{direction.x * offset.y - direction.y * offset.x >= 0.0 ? 1 : -1}; + insert_switch(tool.line, tool.along, forward, side); +} + +void editor_mode::insert_switch(editor_track::straight const &Line, double const Along, int const Direction, int const Side) +{ + std::vector> states; + std::vector created; + std::vector removed; + if (false == place_switch_on_straight(Line, m_switch.templates[m_switch.armed], nullptr, Along, Direction, Side, true, states, created, removed, switch_name_for_new())) + return; + auto const placed{created}; + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + switch_placed(placed); +} + +void editor_mode::switch_placed(std::vector const &Created) +{ + auto const found{std::find_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; })}; + if (found == Created.end()) + return; + auto const slip{std::count_if(Created.begin(), Created.end(), [](TTrack const *Track) { return Track != nullptr && Track->eType == tt_Switch; }) > 1}; + m_switch.armed = -1; + m_switch.placing = false; + m_track_point = {}; + m_node = *found; + ui()->set_node(m_node); + if (slip) + return; + if (m_switch.drive >= 0) + place_switch_drive(**found); + std::string name{m_switch.name}; + auto digits{name.size()}; + while (digits > 0 && std::isdigit(static_cast(name[digits - 1]))) + --digits; + if (digits < name.size() && name.size() - digits < 9) + name = name.substr(0, digits) + std::to_string(std::stoi(name.substr(digits)) + 1); + else + name.clear(); + std::snprintf(m_switch.name, sizeof(m_switch.name), "%s", name.c_str()); +} + +void editor_mode::scan_switch_drives() +{ + auto &tool{m_switch}; + if (tool.drives_scanned) + return; + tool.drives_scanned = true; + tool.drives.clear(); + if (false == EditorIncludes.scanned()) + EditorIncludes.scan(); + for (auto const &entry : EditorIncludes.entries()) + { + if (entry.category != "switch drive") + continue; + include_info info; + std::string error; + if (false == editor_includes::load(entry.file, info, error) || editor_includes::parameter_with_role(info, "track") == 0) + continue; + auto const side = [](std::string const &Side) { return Side == "left" ? 0 : Side == "right" ? 1 : -1; }; + auto const hand{side(info.switch_hand)}; + auto const drive{side(info.switch_drive)}; + if (hand < 0 || drive < 0) + continue; + auto const name{info.name.empty() ? entry.file : info.name}; + auto family{std::find_if(tool.drives.begin(), tool.drives.end(), [&](switch_tool::drive_family const &Family) { return Family.name == name; })}; + if (family == tool.drives.end()) + { + tool.drives.push_back({name, {}}); + family = std::prev(tool.drives.end()); + } + family->files[hand * 2 + drive] = entry.file; + } + std::sort(tool.drives.begin(), tool.drives.end(), [](switch_tool::drive_family const &A, switch_tool::drive_family const &B) { return A.name < B.name; }); + tool.drive = tool.drives.empty() ? -1 : 0; +} + +std::string editor_mode::switch_name_for_new() const +{ + if (m_switch.name[0] != '\0') + return m_switch.name; + for (int i = 1;; ++i) + { + auto const name{"z" + std::to_string(i)}; + if (simulation::Paths.find(name) == nullptr && simulation::Events.FindEvent(name + "+") == nullptr) + return name; + } +} + +bool editor_mode::place_switch_drive(TTrack &Switch) +{ + auto &tool{m_switch}; + if (tool.drive < 0 || tool.drive >= static_cast(tool.drives.size()) || Switch.eType != tt_Switch || Switch.m_paths.size() < 2) + return false; + auto const &main{Switch.m_paths[0]}; + auto const &diverging{Switch.m_paths[1]}; + auto const start{main.points[segment_data::point::start]}; + auto const &control{main.points[segment_data::point::control1]}; + auto heading{plan_of(control != glm::dvec3{} ? control : main.points[segment_data::point::end] - start)}; + if (glm::length(heading) < 1e-9) + return false; + heading = glm::normalize(heading); + glm::dvec2 const right{-heading.y, heading.x}; + bool const rightswitch{glm::dot(plan_of(diverging.points[segment_data::point::end] - start), right) > 0.0}; + bool const driveright{(tool.drive_side == 0) == rightswitch}; + auto const &family{tool.drives[tool.drive]}; + template_item item; + item.file = family.files[(rightswitch ? 2 : 0) + (driveright ? 1 : 0)]; + if (item.file.empty()) + { + tool.status = format(STR_C("The drive %s has no variant for a %s switch with the drive on the %s"), family.name.c_str(), rightswitch ? STR_C("right") : STR_C("left"), driveright ? STR_C("right") : STR_C("left")); + return false; + } + item.location = start; + auto yaw{glm::degrees(std::atan2(heading.x, heading.y))}; + item.yaw = yaw < 0.0 ? yaw + 360.0 : yaw; + item.described = true; + item.track = Switch.name(); + { + auto const along{control != glm::dvec3{} ? control : main.points[segment_data::point::end] - start}; + auto const run{glm::length(plan_of(along))}; + auto const roll{static_cast(main.rolls[0])}; + item.tilt = {run > 1e-9 ? -glm::degrees(std::atan(along.y / run)) : 0.0, roll}; + if (Global.bRollFix) + item.location.y += 0.75 * std::abs(std::sin(glm::radians(roll))); + } + std::string error; + if (place_templates({item}, error) == 0) + { + tool.status = STR_C("The drive couldn't be placed: ") + error; + return false; + } + tool.status = format(STR_C("Switch %s with the drive %s: events %s+ and %s- switch it, also in the map"), Switch.name().c_str(), item.file.c_str(), Switch.name().c_str(), Switch.name().c_str()); + return true; +} + +bool editor_mode::render_switch_drive_choice() +{ + auto &tool{m_switch}; + scan_switch_drives(); + if (tool.drives.empty()) + { + ImGui::TextDisabled("%s", STR_C("No switch drives: no template is described with category: switch drive")); + return false; + } + auto const label = [&](int const Index) { return Index < 0 ? std::string{STR_C("no drive")} : tool.drives[Index].name; }; + ImGui::PushItemWidth(200.0f); + bool changed{false}; + if (ImGui::BeginCombo(STR_C("Drive"), label(tool.drive).c_str())) + { + for (int i = -1; i < static_cast(tool.drives.size()); ++i) + if (ImGui::Selectable(label(i).c_str(), tool.drive == i)) + { + tool.drive = i; + changed = true; + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + item_tooltip("Templates described with category: switch drive, the side of the switch and of the drive, and a parameter with the role track for the name of the switch.\nIts events + and - switch it with the keys by the switch and in the map during the game"); + if (tool.drive >= 0) + { + ImGui::SameLine(); + changed |= ImGui::RadioButton(STR_C("diverging side"), &tool.drive_side, 0); + ImGui::SameLine(); + changed |= ImGui::RadioButton(STR_C("other side"), &tool.drive_side, 1); + } + return changed; +} + +bool editor_mode::cut_straight(editor_track::straight const &Line, double const From, double const To, std::vector> &States, std::vector &Created, std::vector &Removed, TTrack **Style) +{ + auto const from{From}; + auto const to{To}; + std::string reason; + if (false == tracks_editable(Line.tracks, reason)) + { + m_switch.status = reason; + ui()->set_status(reason, true); + return false; + } + struct span + { + TTrack *track; + int path; + double from, to; + bool forward; + }; + std::vector spans; + for (std::size_t i = 0; i < Line.tracks.size(); ++i) + { + auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; + auto const a{Line.along(path.points[segment_data::point::start])}; + auto const b{Line.along(path.points[segment_data::point::end])}; + spans.push_back({Line.tracks[i], Line.paths[i], std::min(a, b), std::max(a, b), a < b}); + } + std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); + int first{-1}, last{-1}; + for (int i = 0; i < static_cast(spans.size()); ++i) + { + if (spans[i].to > from + 1e-3 && spans[i].from < to - 1e-3) + { + if (first < 0) + first = i; + last = i; + } + } + if (first < 0) + return false; + editor_track::chain chain; + for (int i = first; i <= last; ++i) + { + if (spans[i].track->eType != tt_Normal) + return false; + chain.tracks.push_back(spans[i].track); + chain.forward.push_back(spans[i].forward); + } + if (Style != nullptr) + *Style = spans[first].track; + auto const before{std::max(0.0, from - spans[first].from)}; + auto const after{std::max(0.0, spans[last].to - to)}; + bool const hasbefore{before > 0.01}; + bool const hasafter{after > 0.01}; + auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; + if (false == hasbefore && false == hasafter) + { + for (auto *track : chain.tracks) + editor_track::retire(*track); + Removed.insert(Removed.end(), chain.tracks.begin(), chain.tracks.end()); + return true; + } + auto const count{std::max(chain.tracks.size(), (hasbefore ? 1 : 0) + (hasafter ? 1 : 0))}; + std::size_t countbefore{0}; + if (hasbefore && hasafter) + countbefore = std::clamp(static_cast(std::round(count * before / (before + after))), 1, count - 1); + else if (hasbefore) + countbefore = count; + std::vector pieces; + auto const straight = [&](double const Start, double const End, std::size_t const Count) { + for (std::size_t i = 0; i < Count; ++i) + { + segment_data path; + path.points[segment_data::point::start] = point(Start + (End - Start) * i / Count); + path.points[segment_data::point::end] = point(Start + (End - Start) * (i + 1) / Count); + pieces.push_back(path); + } + }; + if (hasbefore) + straight(spans[first].from, from, countbefore); + if (hasafter) + straight(to, spans[last].to, count - countbefore); + for (auto *track : chain.tracks) + States.emplace_back(track, editor_track::capture(*track)); + auto const relaid{editor_track::relay(chain, pieces)}; + Created.insert(Created.end(), relaid.begin(), relaid.end()); + return true; +} + +bool editor_mode::place_switch_on_straight(editor_track::straight const &Line, editor_track::switch_template const &Shape, TTrack const *Style, double const Along, int const Direction, int const Side, bool const Snap, std::vector> &States, std::vector &Created, std::vector &Removed, std::string const &Name) +{ + auto const &shape{Shape}; + if (shape.length > Line.length - 0.02) + { + m_switch.status = format(STR_C("The straight is %.1f m long, too short for the %.1f m switch"), Line.length, shape.length); + return false; + } + auto from{std::clamp(Direction > 0 ? Along : Along - shape.length, 0.0, Line.length - shape.length)}; + if (Snap) + { + std::vector joints{0.0, Line.length}; + for (std::size_t i = 0; i < Line.tracks.size(); ++i) + { + auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + joints.push_back(Line.along(path.points[index])); + } + auto const nearest = [&](double const Value) { + double best{Value}; + for (auto const joint : joints) + if (std::abs(joint - Value) < std::abs(best - Value) || best == Value) + best = std::abs(joint - Value) < 1.0 ? joint : best; + return best; + }; + auto const snappedstart{nearest(from)}; + auto const snappedend{nearest(from + shape.length)}; + if (snappedstart != from) + from = snappedstart; + else if (snappedend != from + shape.length) + from = snappedend - shape.length; + from = std::clamp(from, 0.0, Line.length - shape.length); + } + auto const to{from + shape.length}; + auto const origin{Direction > 0 ? from : to}; + TTrack *first{nullptr}; + if (false == cut_straight(Line, from, to, States, Created, Removed, &first)) + { + m_switch.status = STR_C("The switch would overlap another switch, move it onto plain track"); + return false; + } + auto const point = [&](double const Distance) { return Line.start + glm::dvec3{Line.direction.x, Line.grade, Line.direction.y} * Distance; }; + glm::dvec2 const direction{Line.direction * static_cast(Direction)}; + auto const paths{editor_track::place_switch(shape, point(origin), direction, Side, Line.grade * Direction)}; + auto *track{editor_track::create_switch(shape, paths, Style != nullptr ? *Style : *first, Name)}; + if (track != nullptr) + { + editor_track::commit({track}); + Created.push_back(track); + m_switch.status = STR_C("Switch ") + track->name() + STR_C(" inserted"); + } + else + m_switch.status = STR_C("The switch couldn't be created from this template"); + return true; +} + +void editor_mode::draw_build_overlay() const +{ + screen_projection const projection; + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + auto const drawpath = [&](segment_data const &Path, ImU32 const Color) { + auto previous{bezier{Path}.point(0.0)}; + for (int i = 1; i <= 24; ++i) + { + auto const next{bezier{Path}.point(i / 24.0)}; + projection.line(drawlist, previous, next, Color, 3.0f); + previous = next; + } + }; + auto const &display{ImGui::GetIO().DisplaySize}; + for (auto const *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->m_paths.empty()) + continue; + for (auto const &path : track->m_paths) + for (auto const point : {segment_data::point::start, segment_data::point::end}) + { + ImVec2 screen; + if (projection.project(path.points[point], screen) && screen.x >= 0.0f && screen.y >= 0.0f && screen.x <= display.x && screen.y <= display.y) + drawlist->AddCircleFilled(screen, 3.5f, kJointColor); + } + } + if (m_extend.active) + { + if (m_extend.track != nullptr) + draw_free_ends(m_extend.track, m_extend.track->iCategoryFlag & 15, {m_extend.track}); + crossover_plan plan; + editor_track::snap_target target; + if (extend_snap(target)) + { + ImVec2 screen; + if (projection.project(target.position, screen)) + drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f); + for (auto const &piece : extend_pieces()) + drawpath(piece, overlay_color::marked); + } + else if (plan_crossover(plan)) + { + for (auto const &piece : plan.paths) + drawpath(piece, overlay_color::marked); + drawpath(plan.insert, overlay_color::grip); + } + else + { + for (auto const &piece : extend_pieces()) + drawpath(piece, overlay_color::marked); + } + } + if (m_switch.placing) + for (auto const &piece : switch_preview()) + drawpath(piece, overlay_color::marked); + if (m_lay.active) + { + auto const &lay{m_lay}; + auto const piece_colour = [&](segment_data const &Path) { + if (false == lay.preview_error.empty()) + return overlay_color::invalid; + return piece_plan_radius(Path) < 0.5 ? IM_COL32(255, 255, 255, 230) : overlay_color::marked; + }; + for (auto const &piece : lay.preview) + drawpath(piece, piece_colour(piece)); + for (std::size_t i = 0, n = lay.preview.size(); i < n;) + { + auto const radius{piece_plan_radius(lay.preview[i])}; + bool const curved{radius >= 0.5}; + std::size_t j{i + 1}; + double length{bezier{lay.preview[i]}.plan_length()}; + double sharpest{radius}; + for (; j < n; ++j) + { + auto const next{piece_plan_radius(lay.preview[j])}; + if ((next >= 0.5) != curved) + break; + length += bezier{lay.preview[j]}.plan_length(); + if (curved && next < sharpest) + sharpest = next; + } + double along{0.0}; + glm::dvec3 middle{bezier{lay.preview[i]}.point(0.5)}; + for (std::size_t k = i; k < j; ++k) + { + auto const piece{bezier{lay.preview[k]}.plan_length()}; + if (along + piece >= length * 0.5) + { + middle = bezier{lay.preview[k]}.point(piece > 1e-6 ? std::clamp((length * 0.5 - along) / piece, 0.0, 1.0) : 0.5); + break; + } + along += piece; + } + ImVec2 screen; + if (projection.project(middle, screen)) + { + auto const label{curved ? format("R %.0f", sharpest) : std::string{STR_C("straight")}}; + auto const size{ImGui::CalcTextSize(label.c_str())}; + drawlist->AddRectFilled(ImVec2(screen.x + 6.0f, screen.y - 8.0f), ImVec2(screen.x + 14.0f + size.x, screen.y + 8.0f), IM_COL32(0, 0, 0, 180), 3.0f); + drawlist->AddText(ImVec2(screen.x + 10.0f, screen.y - 8.0f), curved ? overlay_color::marked : overlay_color::highlight, label.c_str()); + } + i = j; + } + for (std::size_t i = 0; i + 1 < lay.points.size(); ++i) + projection.line(drawlist, lay.points[i], lay.points[i + 1], IM_COL32(255, 255, 255, 90), 1.0f); + for (auto const &point : lay.points) + { + ImVec2 screen; + if (projection.project(point, screen)) + drawlist->AddCircleFilled(screen, 5.0f, overlay_color::grip); + } + ImVec2 screen; + if (lay.mouse_snap.track != nullptr && projection.project(lay.mouse_snap.position, screen)) + drawlist->AddCircle(screen, 14.0f, IM_COL32(255, 60, 255, 255), 20, 3.0f); + if (lay.start.track != nullptr && projection.project(lay.start.position, screen)) + drawlist->AddCircle(screen, 10.0f, IM_COL32(255, 60, 255, 255), 20, 2.0f); + if (false == m_extend.active) + draw_free_ends(nullptr, kRailCategory, {}); + } + if (false == m_straights.detour.empty()) + { + auto const outline{detour_outline()}; + for (std::size_t i = 0; i + 1 < outline.size(); ++i) + projection.line(drawlist, outline[i], outline[i + 1], overlay_color::marked, 3.0f); + for (std::size_t i = 0; i < outline.size() && i < m_straights.detour.size(); ++i) + { + ImVec2 screen; + if (projection.project(outline[i], screen)) + drawlist->AddCircleFilled(screen, 6.0f, overlay_color::invalid); + } + } + if ((m_point_drag.moved || (m_track_gizmo_using && m_track_point.valid())) && editor_track::is_end(m_track_point.kind) && m_track_drag_points.empty()) + if (auto const *track{selected_track()}; track != nullptr) + draw_free_ends(track, track->iCategoryFlag & 15, std::vector(m_track_drag.begin(), m_track_drag.end())); +} + +void editor_mode::render_switch_ui() +{ + auto &tool{m_switch}; + auto const list = [&](bool const Slips) { + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &entry{tool.templates[i]}; + if (entry.double_slip != Slips) + continue; + auto label{entry.source != nullptr ? STR("Scenery: ") + entry.label + " (" + std::to_string(entry.count) + "x)" : "PLK: " + entry.label}; + label += "##switch" + std::to_string(i); + if (ImGui::Selectable(label.c_str(), tool.armed == i)) + tool.armed = tool.last = i; + } + }; + ImGui::TextDisabled("%s", STR_C("Switch template")); + ImGui::BeginChild("##switchtemplates", ImVec2(0.0f, 240.0f), true); + list(false); + ImGui::EndChild(); + if (std::any_of(tool.templates.begin(), tool.templates.end(), [](editor_track::switch_template const &Template) { return Template.double_slip; })) + { + if (tool.armed >= 0 && tool.armed < static_cast(tool.templates.size()) && tool.templates[tool.armed].double_slip) + ImGui::SetNextItemOpen(true); + if (ImGui::CollapsingHeader(STR_C("Double slip switches"))) + { + ImGui::TextDisabled("%s", STR_C("Only at a crossing of two straights")); + list(true); + } + } + ImGui::PushItemWidth(120.0f); + ImGui::InputTextWithHint(STR_C("Name##switchname"), switch_name_for_new().c_str(), tool.name, sizeof(tool.name)); + ImGui::PopItemWidth(); + item_tooltip("Name of the next switch, its drive events are named after it; empty: the next free z1, z2...; a number at the end goes up with each switch"); + render_switch_drive_choice(); + if (false == tool.status.empty()) + ImGui::TextColored(ImVec4(1.0f, 0.7f, 0.2f, 1.0f), "%s", tool.status.c_str()); + if (ImGui::SmallButton(STR_C("Collect from the scenery"))) + { + tool.templates = editor_track::standard_switch_templates(); + auto const found{editor_track::find_switch_templates()}; + tool.templates.insert(tool.templates.end(), found.begin(), found.end()); + tool.collected = true; + arm_switch(); + } + item_tooltip("Looks for the switches of the scenery, to use their shapes as the templates"); +} + +std::vector editor_mode::detour_outline() const +{ + auto const &state{m_straights}; + std::vector result; + if (state.detour.empty()) + return result; + auto const &line{state.detour_line}; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto points{state.detour}; + if (points.size() < 4) + points.push_back(state.detour_mouse); + auto const offset{points.size() >= 2 ? glm::dot(plan_of(points[1] - line.start), normal) : 0.0}; + auto const place = [&](double const Along, double const Lateral) { + auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; + return glm::dvec3{planar.x, line.start.y + line.grade * Along, planar.y}; + }; + for (std::size_t i = 0; i < points.size(); ++i) + result.push_back(place(line.along(points[i]), (i == 1 || i == 2) ? offset : 0.0)); + return result; +} + +void editor_mode::add_detour_point() +{ + auto &state{m_straights}; + if (state.detour.empty()) + { + auto const &line{current_straight()}; + if (line.tracks.empty()) + return; + state.detour_line = line; + } + state.detour.push_back(cursor_ground()); + if (state.detour.size() >= 4) + { + apply_detour(); + state.detour.clear(); + } +} + +void editor_mode::apply_detour() +{ + auto &state{m_straights}; + auto line{editor_track::find_straight(*state.detour_line.tracks.front(), state.tolerance)}; + if (line.tracks.empty()) + return; + glm::dvec2 const normal{-line.direction.y, line.direction.x}; + auto const offset{glm::dot(plan_of(state.detour[1] - line.start), normal)}; + std::array stations{line.along(state.detour[0]), line.along(state.detour[1]), line.along(state.detour[2]), line.along(state.detour[3])}; + if (stations[0] > stations[3]) + { + std::swap(stations[0], stations[3]); + std::swap(stations[1], stations[2]); + } + for (auto &station : stations) + station = std::clamp(station, 0.0, line.length); + stations[1] = std::clamp(stations[1], stations[0] + 1.0, stations[3] - 1.0); + stations[2] = std::clamp(stations[2], stations[1], stations[3] - 1.0); + if (std::abs(offset) < 0.01 || stations[3] - stations[0] < 3.0) + return; + + auto const radius{straight_tool_radius(line)}; + auto const slope{std::atan(std::abs(offset) / std::max(1.0, std::min(stations[1] - stations[0], stations[3] - stations[2])))}; + double speed{-1.0}; + for (auto const *track : line.tracks) + speed = std::max(speed, editor_track::velocity(*track)); + auto const transition{state.auto_transitions ? alignment::recommend(speed > 0.0 ? speed : 100.0, radius, m_route.design.norms).transition : state.transition}; + auto const reach{radius * std::tan(slope * 0.5) + transition * 0.5 + 5.0}; + auto const from{std::max(0.0, stations[0] - reach)}; + auto const to{std::min(line.length, stations[3] + reach)}; + + struct span + { + TTrack *track; + double from, to; + }; + std::vector spans; + for (std::size_t i = 0; i < line.tracks.size(); ++i) + { + auto const &path{line.tracks[i]->m_paths[line.paths[i]]}; + auto const a{line.along(path.points[segment_data::point::start])}; + auto const b{line.along(path.points[segment_data::point::end])}; + spans.push_back({line.tracks[i], std::min(a, b), std::max(a, b)}); + } + std::sort(spans.begin(), spans.end(), [](span const &A, span const &B) { return A.from < B.from; }); + int first{-1}, last{-1}; + for (int i = 0; i < static_cast(spans.size()); ++i) + { + if (spans[i].to > from && spans[i].from < to && spans[i].track->eType == tt_Normal) + { + if (first < 0) + first = i; + last = i; + } + else if (first >= 0 && spans[i].from < to) + { + if (spans[i].from > stations[3]) + break; + first = last = -1; + } + } + if (first < 0) + return; + + m_route.from = spans[first].track; + m_route.to = spans[last].track; + route_reset(); + if (m_route.chain.tracks.empty()) + return; + auto &design{m_route.design}; + auto const startalong{line.along(design.start)}; + auto const endalong{line.along(design.end)}; + auto const place = [&](double const Along, double const Lateral) { + auto const planar{plan_of(line.start) + line.direction * Along + normal * Lateral}; + return planar; + }; + alignment::vertex vertex; + vertex.radius = radius; + route_recommend(vertex); + if (false == state.auto_transitions) + vertex.transition_in = vertex.transition_out = state.transition; + design.vertices.clear(); + auto first_vertex{vertex}; + first_vertex.offset = std::max(0.1, stations[0] - startalong); + design.vertices.push_back(first_vertex); + if (stations[2] - stations[1] < 1.0) + { + auto middle{vertex}; + middle.position = place((stations[1] + stations[2]) * 0.5, offset); + design.vertices.push_back(middle); + } + else + { + auto second{vertex}; + second.position = place(stations[1], offset); + auto third{vertex}; + third.position = place(stations[2], offset); + design.vertices.push_back(second); + design.vertices.push_back(third); + } + auto last_vertex{vertex}; + last_vertex.offset = std::max(0.1, endalong - stations[3]); + design.vertices.push_back(last_vertex); + route_update(); + if (false == m_route.result.valid) + return; + route_apply(); + straight_refresh(); +} + +bool editor_mode::start_curve_fit(editor_track::straight const &Line, int const Handle) +{ + auto &state{m_straights}; + auto *before{editor_track::outside_neighbour(Line, false)}; + auto *after{editor_track::outside_neighbour(Line, true)}; + auto const curve_at = [&](TTrack *Neighbour, editor_track::curve &Curve) { + Curve = {}; + return Neighbour != nullptr && Neighbour->eType == tt_Normal && editor_track::find_curve(*Neighbour, state.tolerance, m_route.design.norms.gauge, Curve) && Curve.reversals == 0 && std::abs(Curve.turn) > 1e-4; + }; + state.fit_has_before = curve_at(before, state.fit_before); + state.fit_has_after = curve_at(after, state.fit_after); + if (false == state.fit_has_before && false == state.fit_has_after) + return false; + auto const side_follows = [&](bool const Curve, TTrack const *Neighbour, bool const Pivot) { return Curve || Neighbour == nullptr || Pivot; }; + if (false == side_follows(state.fit_has_before, before, Handle == 1) || false == side_follows(state.fit_has_after, after, Handle == 0)) + return false; + auto const own_end = [&](glm::dvec3 const &Point) -> TTrack * { + for (auto *track : Line.tracks) + if (editor_track::touches(*track, Point)) + return track; + return Line.tracks.empty() ? nullptr : Line.tracks.front(); + }; + auto const other_end = [](editor_track::curve const &Curve, glm::dvec3 const &Joint) { return editor_track::touches(*Curve.from, Joint) ? Curve.to : Curve.from; }; + m_route.from = state.fit_has_before ? other_end(state.fit_before, Line.start) : own_end(Line.start); + m_route.to = state.fit_has_after ? other_end(state.fit_after, Line.end) : own_end(Line.end); + route_reset(); + if (m_route.chain.tracks.empty()) + return false; + return true; +} + +void editor_mode::update_curve_fit(glm::dvec3 const &Start, glm::dvec3 const &End) +{ + auto &state{m_straights}; + auto &design{m_route.design}; + glm::dvec2 const point{Start.x, Start.z}; + glm::dvec2 const direction{glm::normalize(plan_of(End - Start))}; + glm::dvec2 const start{design.start.x, design.start.z}; + glm::dvec2 const end{design.end.x, design.end.z}; + auto const startdirection{glm::normalize(design.start_direction)}; + auto const enddirection{glm::normalize(design.end_direction)}; + auto const first{cross(point - start, direction) / cross(startdirection, direction)}; + auto const last{cross(end - point, direction) / cross(enddirection, direction)}; + auto const make = [&](editor_track::curve const &Curve, double const Offset) { + alignment::vertex vertex; + vertex.radius = Curve.radius > 0.0 ? std::round(Curve.radius) : 1000.0; + route_recommend(vertex); + if (Curve.transition_in > 0.0) + vertex.transition_in = std::round(Curve.transition_in); + if (Curve.transition_out > 0.0) + vertex.transition_out = std::round(Curve.transition_out); + if (Curve.cant > 0.0) + vertex.cant = std::round(Curve.cant); + compound_from(Curve, vertex); + vertex.offset = std::max(0.1, Offset); + return vertex; + }; + design.vertices.clear(); + if (state.fit_has_before) + design.vertices.push_back(make(state.fit_before, first)); + else + { + design.start = Start; + design.start_direction = direction; + design.start_reserve = 0.0; + } + if (state.fit_has_after) + design.vertices.push_back(make(state.fit_after, last)); + else + { + design.end = End; + design.end_direction = direction; + design.end_reserve = 0.0; + } + route_update(); +} + +bool editor_mode::plan_crossover(crossover_plan &Plan) const +{ + auto const &tool{m_extend}; + if (false == tool.active || tool.track == nullptr || tool.track->eType != tt_Switch || tool.track->m_paths.size() < 2 || tool.path != 1 || false == tool.atend || ImGui::GetIO().KeyShift) + return false; + auto const &own{*tool.track}; + auto const &main{own.m_paths[0]}; + auto const &diverging{own.m_paths[1]}; + auto const origin{main.points[segment_data::point::start]}; + auto const axis{glm::normalize(plan_of(main.points[segment_data::point::end] - origin))}; + glm::dvec2 const perpendicular{-axis.y, axis.x}; + auto const &frog{diverging.points[segment_data::point::end]}; + auto const lateral{glm::dot(plan_of(frog - origin), perpendicular)}; + if (std::abs(lateral) < 0.01) + return false; + auto const side{lateral > 0.0 ? 1.0 : -1.0}; + auto const normal{perpendicular * side}; + auto const offset{std::abs(lateral)}; + auto const &control{diverging.points[segment_data::point::control2]}; + auto const tangent{glm::normalize(control != glm::dvec3{} ? -plan_of(control) : plan_of(frog - origin))}; + auto const angle{std::atan2(std::abs(cross(axis, tangent)), glm::dot(axis, tangent))}; + if (angle < 1e-4) + return false; + + auto const ground{tool.mouse}; + TTrack *target{nullptr}; + double best{8.0}; + auto const sections{simulation::Region->sections(ground, 40.f)}; + for (auto *section : sections) + { + for (auto const &cell : section->m_cells) + { + for (auto *track : cell.m_paths) + { + if (track == &own || track->eType != tt_Normal || track->m_editorremoved || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const b{plan_of(path.points[segment_data::point::end])}; + auto const segment{b - a}; + auto const t{std::clamp(glm::dot(plan_of(ground) - a, segment) / std::max(1e-9, glm::dot(segment, segment)), 0.0, 1.0)}; + auto const distance{glm::distance(plan_of(ground), a + segment * t)}; + if (distance < best) + { + best = distance; + target = track; + } + } + } + } + if (target == nullptr) + return false; + Plan.target = editor_track::find_straight(*target, m_straights.tolerance); + auto const &line{Plan.target}; + if (line.tracks.empty() || std::abs(cross(line.direction, axis)) > std::sin(glm::radians(1.0))) + return false; + auto const distance{glm::dot(plan_of(line.start) - plan_of(origin), normal)}; + auto const remaining{distance - 2.0 * offset}; + if (remaining < 0.01) + return false; + auto const length{remaining / std::sin(angle)}; + glm::dvec2 const crossing{axis * std::cos(angle) + normal * std::sin(angle)}; + auto const alongfrog{glm::dot(plan_of(frog - origin), axis)}; + auto const otherfrog{plan_of(frog) + crossing * length}; + auto const otherorigin{otherfrog + axis * alongfrog + normal * offset}; + Plan.along = glm::dot(otherorigin - plan_of(line.start), line.direction); + if (Plan.along < 0.0 || Plan.along > line.length) + return false; + Plan.direction = glm::dot(-axis, line.direction) > 0.0 ? 1 : -1; + glm::dvec2 const direction{line.direction * static_cast(Plan.direction)}; + Plan.side = glm::dot(glm::dvec2{-direction.y, direction.x}, -normal) > 0.0 ? 1 : -1; + + editor_track::switch_template shape; + shape.source = tool.track; + shape.length = glm::dot(plan_of(main.points[segment_data::point::end] - origin), axis); + for (int i = 0; i < 2; ++i) + { + auto const &path{own.m_paths[i]}; + auto &local{shape.local[i]}; + local = path; + auto const tolocal = [&](glm::dvec3 const &Vector) { return glm::dvec3{glm::dot(plan_of(Vector), perpendicular) * side, Vector.y, glm::dot(plan_of(Vector), axis)}; }; + local.points[segment_data::point::start] = tolocal(path.points[segment_data::point::start] - origin); + local.points[segment_data::point::end] = tolocal(path.points[segment_data::point::end] - origin); + local.points[segment_data::point::control1] = tolocal(path.points[segment_data::point::control1]); + local.points[segment_data::point::control2] = tolocal(path.points[segment_data::point::control2]); + if (side < 0.0) + local.rolls = {-local.rolls[0], -local.rolls[1]}; + } + Plan.shape = shape; + auto const station{line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * Plan.along}; + Plan.paths = editor_track::place_switch(shape, station, direction, Plan.side, line.grade * Plan.direction); + Plan.insert = segment_data{}; + Plan.insert.points[segment_data::point::start] = frog; + Plan.insert.points[segment_data::point::end] = Plan.paths[1].points[segment_data::point::end]; + return true; +} + +std::vector editor_mode::build_double_slip(glm::dvec3 const &PointA, glm::dvec3 const &PointB, glm::dvec3 const &PointC, glm::dvec3 const &PointD, glm::dvec2 const &Crossing, double const Height, glm::dvec2 const &First, glm::dvec2 const &Second, double const Angle, double const Radius, TTrack &Style) +{ + auto const at = [](glm::dvec2 const &Planar, double const Y) { return glm::dvec3{Planar.x, Y, Planar.y}; }; + auto const straight = [](glm::dvec3 const &Start, glm::dvec3 const &End) { + segment_data path; + path.points[segment_data::point::start] = Start; + path.points[segment_data::point::end] = End; + return path; + }; + // the ends of the two diagonals at the crossing are kept apart further than the simulation joins the ends, 2 cm + auto const halfsine{std::sqrt(std::max(1e-6, (1.0 - glm::dot(First, Second)) * 0.5))}; + auto const gap{std::max(0.25, 0.06 / halfsine)}; + auto const side{cross(First, Second) > 0.0 ? 1.0 : -1.0}; + auto const arc = [&](glm::dvec3 const &Start, glm::dvec2 const &Heading, double const Turn, glm::dvec3 const &Finish, double const From, double const To) { + glm::dvec2 const centre{plan_of(Start) + glm::dvec2{-Heading.y, Heading.x} * (Turn * Radius)}; + auto const radial{plan_of(Start) - centre}; + auto const p0{centre + turned(radial, Turn * From)}; + auto const p3{centre + turned(radial, Turn * To)}; + auto const d0{turned(Heading, Turn * From)}; + auto const d3{turned(Heading, Turn * To)}; + auto const handle{4.0 / 3.0 * std::tan((To - From) / 4.0) * Radius}; + auto const y0{Start.y + (Finish.y - Start.y) * From / Angle}; + auto const y3{Start.y + (Finish.y - Start.y) * To / Angle}; + segment_data path; + path.points[segment_data::point::start] = {p0.x, y0, p0.y}; + path.points[segment_data::point::end] = {p3.x, y3, p3.y}; + path.points[segment_data::point::control1] = {d0.x * handle, 0.0, d0.y * handle}; + path.points[segment_data::point::control2] = {-d3.x * handle, 0.0, -d3.y * handle}; + path.radius = static_cast(Radius); + return path; + }; + auto const reversed = [](segment_data const &Path) { + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; + }; + auto const middle{Angle * 0.5}; + auto const split{gap / Radius}; + auto const centreA{at(Crossing - First * gap, Height)}; + auto const centreB{at(Crossing + First * gap, Height)}; + auto const centreC{at(Crossing - Second * gap, Height)}; + auto const centreD{at(Crossing + Second * gap, Height)}; + std::string base; + for (int i = 1; base.empty() || simulation::Paths.find(base + "_a") != nullptr; ++i) + base = "editor_dks" + std::to_string(i); + editor_track::switch_template shape; + shape.length = glm::distance(plan_of(PointA), Crossing); + std::vector parts; + parts.push_back(editor_track::create_switch(shape, {straight(PointA, centreA), arc(PointA, First, side, PointD, 0.0, middle - split)}, Style, base + "_a")); + parts.push_back(editor_track::create_switch(shape, {straight(PointB, centreB), reversed(arc(PointC, Second, -side, PointB, middle + split, Angle))}, Style, base + "_b")); + parts.push_back(editor_track::create_switch(shape, {straight(PointC, centreC), arc(PointC, Second, -side, PointB, 0.0, middle - split)}, Style, base + "_c")); + parts.push_back(editor_track::create_switch(shape, {straight(PointD, centreD), reversed(arc(PointA, First, side, PointD, middle + split, Angle))}, Style, base + "_d")); + parts.push_back(editor_track::create_path(Style, straight(centreA, centreB))); + parts.push_back(editor_track::create_path(Style, straight(centreC, centreD))); + parts.push_back(editor_track::create_path(Style, arc(PointA, First, side, PointD, middle - split, middle + split))); + parts.push_back(editor_track::create_path(Style, arc(PointC, Second, -side, PointB, middle - split, middle + split))); + parts.erase(std::remove(parts.begin(), parts.end(), nullptr), parts.end()); + return parts; +} + +bool editor_mode::insert_double_slip(editor_track::straight const &Line, glm::dvec3 const &Point, editor_track::switch_template const &Shape) +{ + auto &state{m_straights}; + state.status.clear(); + { + auto const sections{simulation::Region->sections(Point, 40.f)}; + for (auto *section : sections) + for (auto const &cell : section->m_cells) + for (auto *track : cell.m_paths) + if (track->eType == tt_Switch && false == track->m_editorremoved && track->DoubleSlip() && glm::distance(plan_of(track->location()), plan_of(Point)) < 25.0) + return replace_double_slip(*track, Shape); + } + TTrack *other{nullptr}; + glm::dvec2 crossing{0.0}; + double best{40.0}; + auto const sections{simulation::Region->sections(Point, 60.f)}; + for (auto *section : sections) + { + for (auto const &cell : section->m_cells) + { + for (auto *track : cell.m_paths) + { + if (std::find(Line.tracks.begin(), Line.tracks.end(), track) != Line.tracks.end() || track->eType != tt_Normal || track->m_editorremoved || false == editor_track::is_straight(*track, m_straights.tolerance)) + continue; + auto const &path{track->m_paths.front()}; + auto const a{plan_of(path.points[segment_data::point::start])}; + auto const b{plan_of(path.points[segment_data::point::end])}; + auto const segment{b - a}; + auto const determinant{cross(Line.direction, segment)}; + if (std::abs(determinant) < 1e-9) + continue; + auto const offset{a - plan_of(Line.start)}; + auto const along{cross(offset, segment) / determinant}; + auto const fraction{cross(offset, Line.direction) / determinant}; + if (fraction < -0.01 || fraction > 1.01 || along < 0.0 || along > Line.length) + continue; + auto const position{plan_of(Line.start) + Line.direction * along}; + auto const distance{glm::distance(position, plan_of(Point))}; + if (distance < best) + { + best = distance; + other = track; + crossing = position; + } + } + } + } + if (other == nullptr) + { + state.status = STR_C("No straight crossing the selected one near the click"); + return false; + } + auto const second{editor_track::find_straight(*other, m_straights.tolerance)}; + if (second.tracks.empty()) + return false; + auto const first{Line.direction}; + auto direction{second.direction}; + if (glm::dot(first, direction) < 0.0) + direction = -direction; + auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; + if (angle < glm::radians(0.5) || angle > glm::radians(30.0)) + { + state.status = STR_C("The straights cross at an angle unsuitable for a double slip"); + return false; + } + auto const alongfirst{glm::dot(crossing - plan_of(Line.start), Line.direction)}; + auto const alongsecond{glm::dot(crossing - plan_of(second.start), second.direction)}; + auto const available{std::min({alongfirst, Line.length - alongfirst, alongsecond, second.length - alongsecond}) - 0.5}; + auto radius{Shape.radius}; + auto half{radius * std::tan(angle * 0.5)}; + if (half > available) + { + half = available; + radius = half / std::tan(angle * 0.5); + if (radius < 50.0) + { + state.status = STR_C("Not enough straight track around the crossing"); + return false; + } + state.status = STR_C("R reduced to ") + std::to_string(static_cast(radius)) + STR_C(" m, the straights end ") + std::to_string(static_cast(available)) + STR_C(" m from the crossing"); + } + + auto const heightfirst = [&](double const Along) { return Line.start.y + Line.grade * Along; }; + auto const heightsecond = [&](double const Along) { return second.start.y + second.grade * Along; }; + auto const sign{glm::dot(second.direction, direction) > 0.0 ? 1.0 : -1.0}; + auto const at = [&](glm::dvec2 const &Planar, double const Height) { return glm::dvec3{Planar.x, Height, Planar.y}; }; + auto const pointA{at(crossing - first * half, heightfirst(alongfirst - half))}; + auto const pointB{at(crossing + first * half, heightfirst(alongfirst + half))}; + auto const pointC{at(crossing - direction * half, heightsecond(alongsecond - sign * half))}; + auto const pointD{at(crossing + direction * half, heightsecond(alongsecond + sign * half))}; + + std::vector> states; + std::vector created; + std::vector removed; + TTrack *style{nullptr}; + if (false == cut_straight(Line, alongfirst - half, alongfirst + half, states, created, removed, &style)) + return false; + auto const secondline{editor_track::find_straight(*other, m_straights.tolerance)}; + auto const secondalong{glm::dot(crossing - plan_of(secondline.start), secondline.direction)}; + if (secondline.tracks.empty() || false == cut_straight(secondline, secondalong - half, secondalong + half, states, created, removed, nullptr)) + { + for (auto *track : created) + editor_track::retire(*track); + for (auto const &entry : states) + { + editor_track::apply(*entry.first, entry.second); + editor_track::commit({entry.first}); + } + for (auto *track : removed) + { + track->m_editorremoved = false; + editor_track::commit({track}); + } + return false; + } + auto const parts{build_double_slip(pointA, pointB, pointC, pointD, crossing, heightfirst(alongfirst), first, direction, angle, radius, style != nullptr ? *style : *Line.tracks.front())}; + editor_track::commit(parts); + created.insert(created.end(), parts.begin(), parts.end()); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + switch_placed(parts); + return true; +} + +bool editor_mode::replace_double_slip(TTrack &Part, editor_track::switch_template const &Shape) +{ + auto &state{m_straights}; + auto const base{Part.name().substr(0, Part.name().size() - 2)}; + std::array switches{}; + for (int i = 0; i < 4; ++i) + { + switches[i] = simulation::Paths.find(base + "_" + static_cast('a' + i)); + if (switches[i] == nullptr || switches[i]->m_editorremoved || switches[i]->eType != tt_Switch || switches[i]->m_paths.size() < 2) + { + state.status = STR_C("The double slip isn't complete, it can't be replaced"); + return false; + } + } + struct end + { + TTrack *track; + glm::dvec3 point; + glm::dvec2 inward; + TTrack *outer; + }; + std::vector ends; + std::vector parts(switches.begin(), switches.end()); + for (auto *track : switches) + { + segment_data const *straight{nullptr}; + for (auto const &path : track->m_paths) + if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) + straight = &path; + if (straight == nullptr) + straight = &track->m_paths.front(); + auto const start{straight->points[segment_data::point::start]}; + ends.push_back({track, start, glm::normalize(plan_of(straight->points[segment_data::point::end] - start)), track->SwitchExtension->pPrevs[0]}); + for (int i = 0; i < 2; ++i) + { + auto *inner{track->SwitchExtension->pNexts[i]}; + if (inner != nullptr && inner->eType == tt_Normal && std::find(parts.begin(), parts.end(), inner) == parts.end() && inner->Length() < 5.0) + parts.push_back(inner); + } + } + int partner{-1}; + for (int i = 1; i < 4; ++i) + if (glm::dot(ends[0].inward, ends[i].inward) < -0.999) + partner = i; + if (partner < 0) + { + state.status = STR_C("The double slip geometry isn't recognized"); + return false; + } + auto const &a{ends[0]}; + auto const &b{ends[partner]}; + std::vector others; + for (int i = 1; i < 4; ++i) + if (i != partner) + others.push_back(i); + if (glm::dot(ends[0].inward, ends[others[0]].inward) < 0.0) + std::swap(others[0], others[1]); + auto const &c{ends[others[0]]}; + auto const &d{ends[others[1]]}; + auto const first{a.inward}; + auto direction{c.inward}; + auto const determinant{cross(first, direction)}; + if (std::abs(determinant) < 1e-9) + return false; + auto const t{cross(plan_of(c.point) - plan_of(a.point), direction) / determinant}; + glm::dvec2 const crossing{plan_of(a.point) + first * t}; + auto const angle{std::atan2(std::abs(cross(first, direction)), glm::dot(first, direction))}; + auto const oldhalf{glm::distance(plan_of(a.point), crossing)}; + auto radius{Shape.radius}; + auto half{radius * std::tan(angle * 0.5)}; + auto const height{(a.point.y + b.point.y) * 0.5}; + + std::vector> states; + std::vector created; + std::vector removed; + if (half > oldhalf) + { + for (auto const *e : {&a, &b, &c, &d}) + { + if (e->outer == nullptr || false == editor_track::is_straight(*e->outer, m_straights.tolerance)) + { + half = oldhalf; + radius = half / std::tan(angle * 0.5); + state.status = STR_C("R limited to ") + std::to_string(static_cast(radius)) + STR_C(" m, a neighbouring path isn't straight"); + break; + } + auto const line{editor_track::find_straight(*e->outer, m_straights.tolerance)}; + auto const reach{std::abs(glm::dot(plan_of(line.end) - crossing, -e->inward))}; + auto const reachstart{std::abs(glm::dot(plan_of(line.start) - crossing, -e->inward))}; + if (std::max(reach, reachstart) - 0.5 < half) + { + half = std::min(half, std::max(reach, reachstart) - 0.5); + radius = half / std::tan(angle * 0.5); + state.status = STR_C("R reduced to ") + std::to_string(static_cast(radius)) + STR_C(" m to fit the straights"); + } + } + } + for (auto *track : parts) + editor_track::retire(*track); + removed.insert(removed.end(), parts.begin(), parts.end()); + TTrack *style{a.outer != nullptr && a.outer->eType == tt_Normal ? a.outer : switches[0]}; + std::array points; + std::array order{&a, &b, &c, &d}; + for (int i = 0; i < 4; ++i) + { + auto const &e{*order[i]}; + auto const outward{-e.inward}; + auto const target{crossing + outward * half}; + points[i] = {target.x, e.point.y, target.y}; + if (half < oldhalf - 0.01) + { + auto *filler{editor_track::create_path(*style, [&] { + segment_data path; + path.points[segment_data::point::start] = points[i]; + path.points[segment_data::point::end] = e.point; + return path; + }())}; + created.push_back(filler); + } + else if (half > oldhalf + 0.01 && e.outer != nullptr) + { + auto const line{editor_track::find_straight(*e.outer, m_straights.tolerance)}; + auto const alongold{glm::dot(plan_of(e.point) - plan_of(line.start), line.direction)}; + auto const alongnew{glm::dot(target - plan_of(line.start), line.direction)}; + cut_straight(line, std::min(alongold, alongnew), std::max(alongold, alongnew), states, created, removed, nullptr); + } + } + auto const parts2{build_double_slip(points[0], points[1], points[2], points[3], crossing, height, first, direction, angle, radius, *style)}; + std::vector commitlist(parts2.begin(), parts2.end()); + for (auto *track : created) + if (std::find(commitlist.begin(), commitlist.end(), track) == commitlist.end()) + commitlist.push_back(track); + editor_track::commit(commitlist); + created.insert(created.end(), parts2.begin(), parts2.end()); + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + return true; +} + +std::vector editor_mode::sample_chain(editor_track::chain const &Chain) +{ + std::vector result; + double station{0.0}; + for (std::size_t i = 0; i < Chain.tracks.size(); ++i) + { + auto const &path{Chain.tracks[i]->m_paths.front()}; + auto const forward{Chain.forward[i]}; + bezier const curve{path}; + auto const length{glm::distance(curve.p0, curve.p3)}; + int const steps{std::max(8, static_cast(std::ceil(length / 0.5)))}; + for (int k = (result.empty() ? 0 : 1); k <= steps; ++k) + { + auto const t{forward ? static_cast(k) / steps : 1.0 - static_cast(k) / steps}; + auto const position{curve.point(t)}; + auto const derivative{forward ? curve.first(t) : -curve.first(t)}; + if (false == result.empty()) + station += plan_distance(position, result.back().position); + curve_sample sample; + sample.station = station; + sample.position = position; + sample.tangent = glm::normalize(plan_of(derivative)); + auto const roll{path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t}; + sample.roll = static_cast(forward ? roll : -roll); + sample.radius = path.radius; + result.push_back(sample); + } + } + return result; +} + +editor_mode::curve_sample editor_mode::sample_at(std::vector const &Frame, double const Station) +{ + if (Frame.empty()) + return {}; + if (Station <= Frame.front().station) + return Frame.front(); + if (Station >= Frame.back().station) + return Frame.back(); + auto const next{std::lower_bound(Frame.begin(), Frame.end(), Station, [](curve_sample const &Sample, double const Value) { return Sample.station < Value; })}; + auto const previous{std::prev(next)}; + auto const span{std::max(1e-9, next->station - previous->station)}; + auto const f{(Station - previous->station) / span}; + curve_sample result; + result.station = Station; + result.position = glm::mix(previous->position, next->position, f); + result.tangent = glm::normalize(glm::mix(previous->tangent, next->tangent, f)); + result.roll = static_cast(previous->roll + (next->roll - previous->roll) * f); + result.radius = previous->radius; + return result; +} + +double editor_mode::nearest_station(std::vector const &Frame, glm::dvec3 const &Point) +{ + double best{std::numeric_limits::max()}; + double station{0.0}; + for (std::size_t i = 0; i + 1 < Frame.size(); ++i) + { + glm::dvec2 const a{Frame[i].position.x, Frame[i].position.z}; + glm::dvec2 const b{Frame[i + 1].position.x, Frame[i + 1].position.z}; + auto const segment{b - a}; + auto const t{std::clamp(glm::dot(plan_of(Point) - a, segment) / std::max(1e-12, glm::dot(segment, segment)), 0.0, 1.0)}; + auto const distance{glm::distance(plan_of(Point), a + segment * t)}; + if (distance < best) + { + best = distance; + station = Frame[i].station + (Frame[i + 1].station - Frame[i].station) * t; + } + } + return station; +} + +std::vector editor_mode::curved_switch_paths(editor_track::switch_template const &Shape, std::vector const &Frame, double const Station, int const Direction, int const Side) +{ + auto const map = [&](glm::dvec3 const &Local, glm::dvec2 &Tangent, glm::dvec2 const &Localtangent) { + auto const sample{sample_at(Frame, Station + Direction * Local.z)}; + auto const along{sample.tangent * static_cast(Direction)}; + glm::dvec2 const across{glm::dvec2{-along.y, along.x} * static_cast(Side)}; + Tangent = glm::normalize(along * Localtangent.y + across * Localtangent.x); + auto const planar{plan_of(sample.position) + across * Local.x}; + auto const cant{glm::radians(static_cast(sample.roll) * Direction)}; + auto const cone{-Local.x * Side * std::tan(cant)}; + return glm::dvec3{planar.x, sample.position.y + Local.y + cone, planar.y}; + }; + auto const hermite = [](glm::dvec3 const &Start, glm::dvec2 const &Startdirection, glm::dvec3 const &End, glm::dvec2 const &Enddirection) { + auto const handle{plan_distance(Start, End) / 3.0}; + segment_data path; + path.points[segment_data::point::start] = Start; + path.points[segment_data::point::end] = End; + path.points[segment_data::point::control1] = {Startdirection.x * handle, (End.y - Start.y) / 3.0, Startdirection.y * handle}; + path.points[segment_data::point::control2] = {-Enddirection.x * handle, -(End.y - Start.y) / 3.0, -Enddirection.y * handle}; + return path; + }; + std::vector result; + for (auto const &local : Shape.local) + { + auto const &start{local.points[segment_data::point::start]}; + auto const &end{local.points[segment_data::point::end]}; + auto const &control1{local.points[segment_data::point::control1]}; + auto const &control2{local.points[segment_data::point::control2]}; + glm::dvec2 const chord{end.x - start.x, end.z - start.z}; + auto const localstart{glm::normalize(control1 != glm::dvec3{} ? plan_of(control1) : chord)}; + auto const localend{glm::normalize(control2 != glm::dvec3{} ? -plan_of(control2) : chord)}; + glm::dvec2 startdirection, enddirection; + auto const p0{map(start, startdirection, localstart)}; + auto const p3{map(end, enddirection, localend)}; + auto path{hermite(p0, startdirection, p3, enddirection)}; + { + auto const cone = [&](double const T) { + auto const point{bezier{path}.point(T)}; + auto const sample{sample_at(Frame, nearest_station(Frame, point))}; + auto const lateral{glm::dot(plan_of(point) - plan_of(sample.position), glm::dvec2{-sample.tangent.y, sample.tangent.x})}; + return sample.position.y - lateral * std::tan(glm::radians(static_cast(sample.roll))); + }; + auto const h1{cone(1.0 / 3.0)}; + auto const h2{cone(2.0 / 3.0)}; + auto const y1{3.0 * h1 - 1.5 * h2 - 5.0 / 6.0 * p0.y + p3.y / 3.0}; + auto const y2{3.0 * h2 - 1.5 * h1 + p0.y / 3.0 - 5.0 / 6.0 * p3.y}; + path.points[segment_data::point::control1].y = y1 - p0.y; + path.points[segment_data::point::control2].y = y2 - p3.y; + } + auto const rollstart{sample_at(Frame, Station + Direction * start.z).roll * Direction}; + auto const rollend{sample_at(Frame, Station + Direction * end.z).roll * Direction}; + path.rolls = {static_cast(rollstart), static_cast(rollend)}; + path.radius = local.radius; + { + bezier const curve{path}; + auto const begin{plan_of(curve.first(0.0))}; + auto const finish{plan_of(curve.first(1.0))}; + auto const turn{glm::length(begin) > 1e-9 && glm::length(finish) > 1e-9 ? geometry::signed_angle(begin, finish) : 0.0}; + if (local.radius != 0.f && std::abs(turn) > 1e-6) + path.radius = static_cast(std::copysign(curve.plan_length() / std::abs(turn), static_cast(local.radius))); + } + result.push_back(path); + } + if (false == result.empty()) + result.front().radius = sample_at(Frame, Station).radius; + return result; +} + +void editor_mode::insert_curved_switch(int const Direction, int const Side) +{ + auto &tool{m_switch}; + auto const &shape{tool.templates[tool.armed]}; + auto const total{tool.frame.back().station}; + if (shape.length > total - 0.02) + { + tool.status = format(STR_C("The curve is %.1f m long, too short for the %.1f m switch"), total, shape.length); + return; + } + auto const station{std::clamp(tool.along, Direction > 0 ? 0.0 : shape.length, Direction > 0 ? total - shape.length : total)}; + auto const from{Direction > 0 ? station : station - shape.length}; + auto const to{from + shape.length}; + std::vector> states; + std::vector created; + std::vector removed; + std::vector candidates{tool.frame_tracks}; + for (auto const cut : {from, to}) + { + auto const point{sample_at(tool.frame, cut).position}; + for (auto *candidate : std::vector(candidates)) + { + if (candidate->m_editorremoved || candidate->eType != tt_Normal) + continue; + auto const t{editor_track::nearest_parameter(*candidate, point)}; + auto const found{editor_track::point_at(*candidate, t)}; + if (plan_distance(found, point) > 0.05) + continue; + if (t > 0.002 && t < 0.998) + { + if (std::find(created.begin(), created.end(), candidate) == created.end() && std::none_of(states.begin(), states.end(), [&](auto const &Entry) { return Entry.first == candidate; })) + states.emplace_back(candidate, editor_track::capture(*candidate)); + if (auto *second{editor_track::split_path(*candidate, t)}) + { + created.push_back(second); + candidates.push_back(second); + } + } + break; + } + } + for (auto *candidate : candidates) + { + if (candidate->m_editorremoved) + continue; + auto const middle{editor_track::point_at(*candidate, 0.5)}; + auto const where{nearest_station(tool.frame, middle)}; + if (where > from + 0.01 && where < to - 0.01) + { + editor_track::retire(*candidate); + removed.push_back(candidate); + } + } + auto const paths{curved_switch_paths(shape, tool.frame, station, Direction, Side)}; + auto *track{editor_track::create_switch(shape, paths, *tool.frame_tracks.front(), switch_name_for_new())}; + if (track != nullptr) + { + editor_track::commit({track}); + created.push_back(track); + tool.status = STR_C("Switch ") + track->name() + STR_C(" inserted into the curve"); + } + else + tool.status = STR_C("The switch couldn't be created from this template"); + auto const placed{created}; + push_track_snapshot(std::move(states), std::move(created), std::move(removed)); + straight_refresh(); + switch_placed(placed); +} diff --git a/application/editormodetrackux.cpp b/application/editormodetrackux.cpp new file mode 100644 index 00000000..9e19d1b2 --- /dev/null +++ b/application/editormodetrackux.cpp @@ -0,0 +1,1382 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "utilities/Globals.h" +#include "rendering/renderer.h" +#include "world/Track.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/translation.h" + +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; + +float const kHoverPixels{12.0f}; +double const kHoverLabelDelay{0.35}; // s the cursor rests on a path before its data shows up +double const kFocusDistance{25.0}; +auto const kContextClick{std::chrono::milliseconds(350)}; +float const kContextTurn{0.02f}; // rad the camera may turn during a click of the right button +std::size_t const kSearchLimit{200}; +int const kRailCategory{1}; + +bool viewport_click() +{ + return false == ImGui::GetIO().MouseDownOwned[0]; +} + +bool rail(TTrack const &Track) +{ + return (Track.iCategoryFlag & 15) == kRailCategory && false == Track.m_editorremoved && Track.visible(); +} + +std::string name_of(TTrack const &Track) +{ + return Track.name().empty() ? std::string{"(noname)"} : Track.name(); +} + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +char const *type_of(TTrack const &Track) +{ + switch (Track.eType) + { + case tt_Normal: return "normal"; + case tt_Switch: return "switch"; + case tt_Cross: return "cross"; + case tt_Table: return "turntable"; + case tt_Tributary: return "tributary"; + default: return "unknown"; + } +} + +double segment_distance2(glm::vec2 const &Point, glm::vec2 const &A, glm::vec2 const &B, double &T) +{ + auto const ab{B - A}; + auto const length2{glm::dot(ab, ab)}; + T = length2 > 1e-6f ? std::clamp(static_cast(glm::dot(Point - A, ab) / length2), 0.0, 1.0) : 0.0; + auto const closest{A + ab * static_cast(T)}; + return glm::dot(Point - closest, Point - closest); +} + +} // namespace + +editor_mode::track_hover editor_mode::track_under_cursor() const +{ + track_hover result; + auto const &io{ImGui::GetIO()}; + screen_projection const projection; + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + double best{kHoverPixels * kHoverPixels}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || track->m_editorremoved) + continue; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + bezier const curve{track->m_paths[i]}; + auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; + auto previous{curve.point(0.0)}; + ImVec2 from; + bool visible{projection.project(previous, from)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + ImVec2 to; + bool const shown{projection.project(next, to)}; + if (visible && shown) + { + double t; + auto const distance{segment_distance2(mouse, {from.x, from.y}, {to.x, to.y}, t)}; + if (distance < best) + { + best = distance; + result.track = track; + result.path = i; + result.point = glm::mix(previous, next, t); + } + } + previous = next; + from = to; + visible = shown; + } + } + } + return result; +} + +void editor_mode::update_track_hover() +{ + if (typed_meaning() == nullptr) + m_typed.clear(); + bool const blocked{ImGui::GetIO().WantCaptureMouse || mouseHold || m_track_set_using || m_track_box.active || m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS || m_track_gizmo_using || m_route_gizmo_using || m_extend.active || m_switch.placing || + m_straights.dragging || m_lay.active}; + if (blocked) + { + m_hover = {}; + return; + } + auto found{track_under_cursor()}; + found.since = found.track == m_hover.track ? m_hover.since : ImGui::GetTime(); + m_hover = found; +} + +void editor_mode::draw_track_hover() const +{ + using kind = track_intent::kind; + auto const *track{m_hover.track}; + if (track == nullptr || (m_intent.what != kind::select && m_intent.what != kind::box && m_intent.what != kind::straight_set) || m_intent.track != track) + return; + screen_projection const projection; + if (track != selected_track()) + { + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + bezier const curve{track->m_paths[i]}; + auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; + auto const colour{i == m_hover.path ? IM_COL32(255, 255, 255, 200) : IM_COL32(255, 255, 255, 110)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, colour, i == m_hover.path ? 3.5f : 2.0f); + previous = next; + } + } + } +} + +bool editor_mode::track_gesture_on() const +{ + return mouseHold || m_track_set_using || m_track_box.active || m_input.mouse.button(GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS || m_track_gizmo_using || m_route_gizmo_using || m_extend.active || m_switch.placing || m_straights.dragging || m_straights.tool_mouse || + m_point_drag.active || m_handle_drag.active || m_sweep.dragging || m_turntable.placing || m_signal.placing || m_signal.moving || ImGuizmo::IsUsing(); +} + +editor_mode::track_intent editor_mode::track_intent_at(int const Mods) +{ + using kind = track_intent::kind; + track_intent intent; + if (ImGui::GetIO().WantCaptureMouse) + return intent; + glm::dvec3 const ground{cursor_ground()}; + intent.position = ground; + bool const shift{(Mods & GLFW_MOD_SHIFT) != 0}; + bool const control{(Mods & GLFW_MOD_CONTROL) != 0}; + bool const alt{(Mods & GLFW_MOD_ALT) != 0}; + auto const make = [&](kind const What, char const *Action) { + intent.what = What; + intent.action = Action != nullptr ? STR(Action) : std::string{}; + return intent; + }; + auto *selected{selected_track()}; + + if (m_lay.active) + { + auto const &lay{m_lay}; + if (lay.mouse_snap.track != nullptr && (lay.points.empty() || glm::distance(lay.mouse_snap.position, lay.points.front()) > 1.0)) + { + intent.snap = lay.mouse_snap; + intent.track = lay.mouse_snap.track; + intent.position = lay.mouse_snap.position; + auto const diverging{lay.mouse_snap.track->eType == tt_Switch && lay.mouse_snap.point == editor_track::point_ref{1, editor_track::point_kind::end}}; + if (lay.points.empty() && diverging) + return make(kind::lay_end, "Click: start from this free end, drag: the curve of the switch goes on, Shift: straight"); + return make(kind::lay_end, lay.points.empty() ? "Click: start from this free end, drag: lead a track out of it" : "Click: join this end and lay"); + } + intent.position = lay.mouse; + return make(kind::lay, lay.points.empty() ? "Click: start of the new track" : "Click: next point"); + } + if (m_track_tab == track_tab::turntable) + return turntable_intent(ground, shift); + if (m_track_tab == track_tab::signals) + { + if (auto const hit{signal_hit()}; hit >= 0) + { + auto const &standing{m_signal.standing[hit]}; + intent.index = hit; + intent.position = standing.include.location; + intent.action = format(STR_C("Click: select the signal %s, drag: move it along the track"), standing.name.c_str()); + intent.what = kind::signal_move; + return intent; + } + auto const *armed{signal_armed()}; + if (m_hover.track != nullptr && armed != nullptr) + { + signal_spot spot; + if (signal_spot_at(*m_hover.track, m_hover.path, m_hover.point, cursor_level(m_hover.point.y), 0, 0, *armed, spot)) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = spot.position; + intent.action = signal_intent_label(*armed, spot); + intent.what = kind::signal_place; + return intent; + } + } + if (m_hover.track != nullptr) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + return make(kind::select, "Choose the type of the signal in the Tracks window first"); + } + return intent; + } + if (m_track_tab == track_tab::path) + { + if (shift && control && m_hover.track != nullptr && rail(*m_hover.track)) + { + intent.track = m_hover.track; + intent.position = m_hover.point; + auto const &preview{track_spread_preview(*m_hover.track)}; + intent.action = format(in_track_set(m_hover.track) ? STR_C("Click: take out the %zu joined paths, %.0f m") : STR_C("Click: add the %zu joined paths, %.0f m"), preview.size(), m_spread.length); + intent.what = kind::spread; + return intent; + } + if (shift && false == gizmo_frame::over()) + { + intent.track = m_hover.track; + return make(kind::box, m_hover.track == nullptr ? "Drag: the tracks in a box join the selection" : in_track_set(m_hover.track) ? "Click: take out of the selection, drag: a box" : "Click: add to the selection, drag: a box"); + } + if (auto const hit{track_handle_hit()}; hit.valid() && selected != nullptr) + { + std::string reason; + if (editor_track::can_edit_geometry(*selected, reason)) + { + intent.track = selected; + intent.point = hit; + intent.position = editor_track::point_position(*selected, hit); + if (false == editor_track::is_end(hit.kind)) + return make(kind::point, "Drag: the control vector, the curve follows"); + if (editor_track::is_connected(*selected, hit)) + return make(kind::point, "Drag: the joint, the neighbours follow"); + if (control) + return make(kind::point, "Drag: the end, dropped on a free end it joins it"); + intent.snap.track = selected; + intent.snap.point = hit; + intent.snap.position = intent.position; + auto const diverging{selected->eType == tt_Switch && hit == editor_track::point_ref{1, editor_track::point_kind::end}}; + return make(kind::extend_end, diverging ? "Drag: the curve of the switch goes on, Shift: straight; Ctrl+drag: move the end" : "Drag: a new track out of this end, as in laying; Ctrl+drag: move the end"); + } + } + } + if (m_track_tab == track_tab::straights) + { + auto const &line{current_straight()}; + if (alt) + { + intent.track = m_hover.track; + return m_hover.track != nullptr ? make(kind::straight_set, "Click: move this straight together with the others") : intent; + } + if (control && false == line.tracks.empty()) + return make(kind::straight_break, "Press: break the straight here, drag to turn the rest"); + if (shift && false == line.tracks.empty()) + return make(kind::detour, "Click: next point of the shift around an obstacle"); + } + if (m_track_tab == track_tab::straights && m_straights.tool == 0) + { + if (auto const hit{straight_handle_hit()}; hit >= 0) + { + auto const &line{current_straight()}; + glm::dvec3 const handles[] = {line.start, line.end, (line.start + line.end) * 0.5}; + intent.index = hit; + intent.position = handles[hit]; + if (hit == 2) + { + auto const offer{straight_parallel_offer(line, {})}; + if (offer.nearby) + { + intent.action = format(offer.snaps ? STR_C("Drag: shift the straight sideways — snaps parallel at %.2f m") : STR_C("Drag: shift the straight sideways — close: %.2f m %.1f°"), offer.spacing, offer.angle); + intent.what = kind::straight_handle; + return intent; + } + } + return make(kind::straight_handle, hit == 2 ? "Drag: shift the straight sideways" : "Drag: move this end, the curves at it follow"); + } + } + if (int vertex, grip; route_hit(vertex, grip)) + { + intent.index = vertex >= 0 ? vertex : grip; + intent.position = vertex >= 0 ? route_vertex_position(vertex) : alignment::evaluate(m_route.result, m_route.result.vertex_chainages[grip]).position; + return make(vertex >= 0 ? kind::route_vertex : kind::route_grip, vertex >= 0 ? "Drag: move this vertex, the curve follows" : "Drag: the radius of this curve"); + } + if (gizmo_frame::over()) + return make(kind::gizmo, nullptr); + if (m_track_tab == track_tab::straights) + { + auto const &line{current_straight()}; + if (m_straights.tool != 0 && false == line.tracks.empty()) + { + intent.position = line.start + glm::dvec3{line.direction.x, line.grade, line.direction.y} * std::clamp(line.along(ground), 0.0, line.length); + return make(kind::straight_tool, m_straights.tool == 1 ? "Click: break the straight here" : "Click: start of the shift"); + } + } + if (selected != nullptr && false == selected->m_paths.empty()) + { + glm::dvec3 point; + if (switch_reach(cursor_level(selected->m_paths.front().points[segment_data::point::start].y), point)) + { + intent.track = selected; + intent.position = point; + return make(kind::switch_at, "Press and drag: a switch here, along sets its direction, sideways its side"); + } + } + if (m_hover.track != nullptr) + { + intent.track = m_hover.track; + intent.path = m_hover.path; + intent.position = m_hover.point; + switch (m_track_tab) + { + case track_tab::straights: return make(kind::select, "Click: the whole straight through this track"); + case track_tab::route: return make(kind::select, "Click: the curve of this track with its straights"); + case track_tab::turnout: return make(kind::select, m_hover.track->eType == tt_Switch ? "Click: edit this switch" : m_switch.armed < 0 ? "Click: choose this track, then the type of the switch" : "Click: put a switch into this track"); + case track_tab::profile: return make(kind::select, "Click: the grade line through this track"); + case track_tab::lineside: return make(kind::select, "Click: the line the objects go along"); + default: return make(kind::select, "Click: select"); + } + } + return intent; +} + +void editor_mode::update_track_intent() +{ + using kind = track_intent::kind; + if (track_gesture_on()) + { + m_intent = {}; + return; + } + auto const &io{ImGui::GetIO()}; + m_intent = track_intent_at((io.KeyShift ? GLFW_MOD_SHIFT : 0) | (io.KeyCtrl ? GLFW_MOD_CONTROL : 0) | (io.KeyAlt ? GLFW_MOD_ALT : 0)); + switch (m_intent.what) + { + case kind::none: + case kind::gizmo: break; + case kind::point: + case kind::straight_handle: + case kind::route_vertex: + case kind::route_grip: ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); break; + default: ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); break; + } +} + +void editor_mode::track_press(track_intent const &Intent) +{ + using kind = track_intent::kind; + switch (Intent.what) + { + case kind::lay_end: + if (m_lay.points.empty() && start_extend(Intent.snap)) + return; + [[fallthrough]]; + case kind::lay: + GfxRenderer->Pick_Node_Callback([this](scene::basic_node * /*node*/) { + if (viewport_click()) + lay_click(); + }); + return; + case kind::sweep: sweep_press(); return; + case kind::box: start_track_box(); return; + case kind::spread: + if (Intent.track != nullptr) + track_spread_apply(*Intent.track); + return; + case kind::point: point_drag_start(Intent.point); return; + case kind::gizmo: return; + case kind::straight_set: + { + auto *hovered{Intent.track}; + GfxRenderer->Pick_Node_Callback([this, hovered](scene::basic_node *node) { + auto *track{hovered != nullptr ? hovered : dynamic_cast(node)}; + if (viewport_click() && track != nullptr) + toggle_straight_set(*track); + }); + return; + } + case kind::straight_break: start_straight_gesture(1); return; + case kind::detour: add_detour_point(); return; + case kind::straight_tool: place_straight_tool(); return; + case kind::straight_handle: + m_straights.handle = Intent.index; + handle_drag_start(Intent.position); + return; + case kind::route_vertex: + m_route.vertex = Intent.index; + m_route.grip = -1; + handle_drag_start(Intent.position); + return; + case kind::route_grip: + m_route.vertex = -1; + m_route.grip = Intent.index; + handle_drag_start(Intent.position); + return; + case kind::switch_at: start_switch_placement(); return; + case kind::extend_end: + if (false == start_extend(Intent.snap)) + point_drag_start(Intent.snap.point); + return; + case kind::turntable_place: + case kind::turntable_exit: + case kind::turntable_fit: turntable_press(Intent); return; + case kind::signal_place: + case kind::signal_move: signal_press(Intent); return; + case kind::select: + case kind::none: + { + auto *hovered{Intent.track}; + GfxRenderer->Pick_Node_Callback([this, hovered](scene::basic_node *node) { + if (false == viewport_click()) + return; + auto *track{hovered != nullptr ? hovered : dynamic_cast(node)}; + if (m_track_tab == track_tab::path) + { + m_track_set.clear(); + if (track == nullptr) + { + m_node = nullptr; + m_track_point = {}; + ui()->set_node(nullptr); + return; + } + } + select_track(track != nullptr ? track : node); + }); + return; + } + } +} + +void editor_mode::draw_track_intent() const +{ + using kind = track_intent::kind; + auto const &intent{m_intent}; + auto const &io{ImGui::GetIO()}; + if (intent.what == kind::none || intent.what == kind::gizmo || io.WantCaptureMouse) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + ImVec2 at; + if (projection.project(intent.position, at)) + { + switch (intent.what) + { + case kind::point: + case kind::straight_handle: + case kind::route_vertex: + case kind::route_grip: drawlist->AddCircle(at, 13.0f, overlay_color::marked, 24, 2.5f); break; + case kind::switch_at: + case kind::straight_tool: + drawlist->AddCircleFilled(at, 4.0f, overlay_color::grip); + drawlist->AddCircle(at, 11.0f, overlay_color::grip, 20, 2.0f); + break; + case kind::lay_end: + case kind::extend_end: + case kind::turntable_fit: drawlist->AddCircleFilled(at, 5.0f, IM_COL32(255, 60, 255, 255)); break; + case kind::turntable_exit: drawlist->AddCircle(at, 11.0f, overlay_color::grip, 20, 2.0f); break; + case kind::lay: drawlist->AddCircle(at, 6.0f, IM_COL32(255, 255, 255, 200), 12, 1.5f); break; + default: break; + } + } + if (intent.action.empty() || false == track_readout().empty()) + return; + std::string details; + auto const *track{m_hover.track}; + if (intent.what == kind::select && track != nullptr && track == intent.track && ImGui::GetTime() - m_hover.since >= kHoverLabelDelay) + { + auto const &path{track->m_paths[std::min(m_hover.path, track->m_paths.size() - 1)]}; + details = name_of(*track); + details += "\n" + std::string{STR_C(type_of(*track))}; + if (track->eType == tt_Switch) + details += m_hover.path == 0 ? STR(" (main)") : STR(" (branch)"); + details += format(" L %.1f m", bezier{path}.plan_length()); + details += path.radius != 0.0f ? format(" R %.0f m", std::abs(path.radius)) : std::string{" "} + STR_C("straight"); + auto const velocity{editor_track::velocity(*track)}; + details += velocity > 0.0 ? format(" V %.0f km/h", velocity) : std::string{}; + } + auto *foreground{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + auto const actionsize{ImGui::CalcTextSize(intent.action.c_str())}; + auto const detailsize{details.empty() ? ImVec2(0.0f, 0.0f) : ImGui::CalcTextSize(details.c_str())}; + ImVec2 const size{std::max(actionsize.x, detailsize.x), actionsize.y + (details.empty() ? 0.0f : detailsize.y + 4.0f)}; + ImVec2 position{io.MousePos.x + 18.0f, io.MousePos.y + 18.0f}; + position.x = std::min(position.x, io.DisplaySize.x - size.x - 10.0f); + position.y = std::min(position.y, io.DisplaySize.y - size.y - 10.0f); + foreground->AddRectFilled(ImVec2(position.x - 6.0f, position.y - 4.0f), ImVec2(position.x + size.x + 6.0f, position.y + size.y + 4.0f), IM_COL32(0, 0, 0, 170), 4.0f); + foreground->AddText(position, overlay_color::marked, intent.action.c_str()); + if (false == details.empty()) + foreground->AddText(ImVec2(position.x, position.y + actionsize.y + 4.0f), IM_COL32(255, 255, 255, 235), details.c_str()); +} + +void editor_mode::track_context_press() +{ + auto &context{m_track_context}; + auto const &io{ImGui::GetIO()}; + context.armed = m_hover.track != nullptr; + context.target = m_hover; + context.pressed = std::chrono::steady_clock::now(); + context.angle = Camera.Angle; + context.mouse = {io.MousePos.x, io.MousePos.y}; +} + +void editor_mode::track_context_release() +{ + auto &context{m_track_context}; + if (false == context.armed) + return; + context.armed = false; + if (std::chrono::steady_clock::now() - context.pressed < kContextClick && glm::length(Camera.Angle - context.angle) < kContextTurn) + context.pending = true; +} + +void editor_mode::split_track_at(TTrack &Track, glm::dvec3 const &Point) +{ + auto const parameter{editor_track::nearest_parameter(Track, Point)}; + auto const before{editor_track::capture(Track)}; + if (auto *created{editor_track::split_path(Track, parameter)}) + { + push_track_snapshot({{&Track, before}}, {created}); + straight_refresh(); + } +} + +void editor_mode::focus_track(TTrack &Track, int const Path, glm::dvec3 const &Point) +{ + auto const &path{Track.m_paths[std::min(std::max(Path, 0), Track.m_paths.size() - 1)]}; + bezier const curve{path}; + glm::dvec3 look{curve.first(std::clamp(editor_track::nearest_parameter(Track, Point), 0.0, 1.0))}; + look.y = 0.0; + look = glm::length(look) > 1e-6 ? glm::normalize(look) : glm::dvec3(0.0, 0.0, 1.0); + if (glm::dot(Camera.Pos - Point, look) > 0.0) + look = -look; + look = glm::normalize(look - glm::dvec3(0.0, 0.25, 0.0)); + m_focus_start_pos = Camera.Pos; + m_focus_start_angle = Camera.Angle; + m_focus_target_pos = Point - look * kFocusDistance; + m_focus_target_angle = glm::vec3(static_cast(std::asin(std::clamp(look.y, -1.0, 1.0))), static_cast(std::atan2(-look.x, -look.z)), 0.0f); + m_focus_active = true; + m_focus_time = 0.0; + m_focus_duration = 0.6; +} + +void editor_mode::render_track_context() +{ + auto &context{m_track_context}; + auto *track{context.target.track}; + if (context.pending) + { + context.pending = false; + if (track != nullptr && false == track->m_editorremoved) + { + select_track(track); + ImGui::OpenPopup("##trackcontext"); + ImGui::SetNextWindowPos(ImVec2(context.mouse.x, context.mouse.y)); + } + } + if (false == ImGui::BeginPopup("##trackcontext")) + return; + if (track == nullptr || track->m_editorremoved) + { + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + return; + } + auto const point{context.target.point}; + bool const normal{track->eType == tt_Normal && editor_track::is_supported(*track)}; + ImGui::TextDisabled("%s (%s, %.1f m)", name_of(*track).c_str(), STR_C(type_of(*track)), track->Length()); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Split here"), "K", false, normal)) + split_track_at(*track, point); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Switch"), "T", false, normal || track->eType == tt_Switch)) + show_track_tab(track_tab::turnout); + if (ImGui::MenuItem(STR_C("Whole straight"), "G", false, normal && editor_track::is_straight(*track, m_straights.tolerance))) + show_track_tab(track_tab::straights); + if (ImGui::MenuItem(STR_C("Curve"), "C", false, normal)) + show_track_tab(track_tab::route); + if (ImGui::MenuItem(STR_C("Parallel track"), "L", false, normal)) + show_track_tab(track_tab::lay); + if (ImGui::MenuItem(STR_C("Place a vehicle here"))) + { + show_track_tab(track_tab::lineside); + vehicle_start(*track, point); + } + if (ImGui::MenuItem(STR_C("Hectometre posts from here"))) + { + show_track_tab(track_tab::lineside); + hekto_start(*track, point); + } + if (ImGui::MenuItem(STR_C("Fouling point markers"), nullptr, false, track->eType == tt_Switch)) + { + show_track_tab(track_tab::lineside); + m_fouling.scope = 0; + m_fouling.dirty = true; + m_fouling.expand = true; + } + if (ImGui::MenuItem(STR_C("Vertical profile of the line"), "P")) + { + show_track_tab(track_tab::profile); + profile_open_run(*track); + } + if (ImGui::MenuItem(STR_C("Speed check of the line"), "V")) + { + m_speed.scope = 0; + show_track_tab(track_tab::speed); + speed_start(); + } + ImGui::Separator(); + if (ImGui::MenuItem(in_track_set(track) ? STR_C("Take out of the selection") : STR_C("Add to the selection"), "Shift+LMB")) + track_set_toggle(*track); + if (ImGui::MenuItem(STR_C("Look at it"))) + focus_track(*track, context.target.path, point); + if (ImGui::MenuItem(STR_C("Copy the name"), nullptr, false, false == track->name().empty())) + ImGui::SetClipboardText(track->name().c_str()); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Delete"), "Del")) + delete_selected_track(); + ImGui::EndPopup(); +} + +void editor_mode::render_track_search() +{ + auto &search{m_track_search}; + if (search.focus) + { + ImGui::SetKeyboardFocusHere(); + search.focus = false; + } + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##findtrack", STR_C("Find a track by its name (Ctrl+F)"), search.text, sizeof(search.text)); + std::string const text{lower(search.text)}; + if (text.empty()) + { + search.searched.clear(); + search.found.clear(); + return; + } + if (text != search.searched) + { + search.searched = text; + search.found.clear(); + search.total = 0; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || lower(track->name()).find(text) == std::string::npos) + continue; + ++search.total; + if (search.found.size() < kSearchLimit) + search.found.push_back(track); + } + } + if (search.found.empty()) + { + ImGui::TextDisabled("%s", STR_C("No track of that name")); + return; + } + if (search.total > search.found.size()) + ImGui::TextDisabled(STR_C("%zu found, the first %zu listed"), search.total, search.found.size()); + auto const rows{std::min(search.found.size(), 8)}; + ImGui::BeginChild("##foundtracks", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * static_cast(rows) + 6.0f), true); + for (std::size_t i = 0; i < search.found.size(); ++i) + { + auto *track{search.found[i]}; + if (track->m_editorremoved) + continue; + auto const label{format("%s##found%zu", name_of(*track).c_str(), i)}; + if (ImGui::Selectable(label.c_str(), track == selected_track())) + { + m_node = track; + m_track_point = {}; + ui()->set_node(m_node); + focus_track(*track, 0, bezier{track->m_paths.front()}.point(0.5)); + } + ImGui::SameLine(ImGui::GetWindowContentRegionMax().x * 0.6f); + ImGui::TextDisabled("%s, %.1f m", STR_C(type_of(*track)), track->Length()); + } + ImGui::EndChild(); +} + +char const *editor_mode::typed_meaning() const +{ + auto const &straights{m_straights}; + if (m_extend.active) + return m_extend_freehand && false == ImGui::GetIO().KeyCtrl ? "type the radius, a space, the length" : "type the length"; + if (straights.dragging) + return straights.handle == 2 ? "type the shift, + to the right" : "type the length"; + if (straights.tool == 1 && (straights.tool_mouse || straights.tool_dragging)) + return "type the turn in degrees, + to the right"; + if (m_lay.active && false == m_lay.points.empty()) + return "type the length of the next leg, Enter adds the point"; + return nullptr; +} + +std::vector editor_mode::typed_values() const +{ + std::vector values; + std::string text{m_typed}; + std::replace(text.begin(), text.end(), ',', '.'); + std::size_t at{0}; + while (at < text.size()) + { + auto const end{text.find(' ', at)}; + auto const word{text.substr(at, end == std::string::npos ? std::string::npos : end - at)}; + if (false == word.empty() && word != "-" && word != ".") + values.push_back(std::strtod(word.c_str(), nullptr)); + if (end == std::string::npos) + break; + at = end + 1; + } + return values; +} + +bool editor_mode::typed_key(int const Key) +{ + if (typed_meaning() == nullptr) + { + m_typed.clear(); + return false; + } + char character{'\0'}; + if (Key >= GLFW_KEY_0 && Key <= GLFW_KEY_9) + character = static_cast('0' + (Key - GLFW_KEY_0)); + else if (Key >= GLFW_KEY_KP_0 && Key <= GLFW_KEY_KP_9) + character = static_cast('0' + (Key - GLFW_KEY_KP_0)); + else if (Key == GLFW_KEY_PERIOD || Key == GLFW_KEY_COMMA || Key == GLFW_KEY_KP_DECIMAL) + character = '.'; + else if (Key == GLFW_KEY_MINUS || Key == GLFW_KEY_KP_SUBTRACT) + character = '-'; + else if (Key == GLFW_KEY_SPACE) + character = ' '; + else if (Key == GLFW_KEY_BACKSPACE && false == m_typed.empty()) + { + m_typed.pop_back(); + return true; + } + else + return false; + auto const start{m_typed.empty() || m_typed.back() == ' '}; + if ((character == '-' && false == start) || (character == ' ' && start) || (character == '.' && m_typed.substr(m_typed.rfind(' ') == std::string::npos ? 0 : m_typed.rfind(' ')).find('.') != std::string::npos)) + return true; + if (m_typed.size() < 24) + m_typed += character; + return true; +} + +void editor_mode::typed_extend() +{ + auto const values{typed_values()}; + if (values.empty()) + return; + auto &tool{m_extend}; + auto const &direction{tool.direction}; + if (false == m_extend_freehand || ImGui::GetIO().KeyCtrl) + { + auto const length{std::max(0.0, values[0])}; + tool.mouse = {tool.point.x + direction.x * length, tool.mouse.y, tool.point.z + direction.y * length}; + return; + } + auto const radius{std::abs(values[0])}; + if (radius < 1.0) + return; + glm::dvec2 const offset{tool.mouse.x - tool.point.x, tool.mouse.z - tool.point.z}; + double const side{geometry::cross(direction, offset) >= 0.0 ? 1.0 : -1.0}; + glm::dvec2 const point{tool.point.x, tool.point.z}; + glm::dvec2 const centre{point + glm::dvec2{-direction.y, direction.x} * (side * radius)}; + auto const radial{point - centre}; + auto angle{values.size() > 1 ? std::abs(values[1]) / radius : side * geometry::signed_angle(radial, glm::dvec2{tool.mouse.x, tool.mouse.z} - centre)}; + if (angle < 0.0) + angle += 2.0 * glm::pi(); + angle = std::clamp(angle, 1e-3, 2.0 * glm::pi() - 0.1); + auto const end{centre + geometry::turned(radial, side * angle)}; + tool.mouse = {end.x, tool.mouse.y, end.y}; +} + +glm::dvec3 editor_mode::typed_lay(glm::dvec3 const &Mouse) const +{ + auto const values{typed_values()}; + if (values.empty() || m_lay.points.empty() || values[0] <= 0.0) + return Mouse; + auto const &last{m_lay.points.back()}; + glm::dvec2 heading{Mouse.x - last.x, Mouse.z - last.z}; + if (glm::length(heading) < 1e-6) + return Mouse; + heading = glm::normalize(heading) * values[0]; + return {last.x + heading.x, Mouse.y, last.z + heading.y}; +} + +void editor_mode::typed_turn() +{ + auto &state{m_straights}; + auto const values{typed_values()}; + if (values.empty() || state.tool != 1) + return; + auto const reach{std::max(1.0, glm::length(glm::dvec2{state.tool_handle.x - state.tool_point.x, state.tool_handle.z - state.tool_point.z}))}; + auto const heading{geometry::turned(state.tool_line.direction, glm::radians(values[0])) * reach}; + state.tool_handle = {state.tool_point.x + heading.x, state.tool_handle.y, state.tool_point.z + heading.y}; +} + +bool editor_mode::in_track_set(TTrack const *Track) const +{ + return std::find(m_track_set.begin(), m_track_set.end(), Track) != m_track_set.end(); +} + +void editor_mode::track_set_prune() +{ + m_track_set.erase(std::remove_if(m_track_set.begin(), m_track_set.end(), [](TTrack const *Track) { return Track == nullptr || Track->m_editorremoved; }), m_track_set.end()); +} + +void editor_mode::track_set_toggle(TTrack &Track) +{ + track_set_prune(); + if (m_track_set.empty()) + if (auto *selected{selected_track()}; selected != nullptr && selected != &Track) + m_track_set.push_back(selected); + if (auto const found{std::find(m_track_set.begin(), m_track_set.end(), &Track)}; found != m_track_set.end()) + m_track_set.erase(found); + else + m_track_set.push_back(&Track); + m_node = m_track_set.empty() ? nullptr : m_track_set.back(); + m_track_point = {}; + ui()->set_node(m_node); +} + +void editor_mode::start_track_box() +{ + auto const &io{ImGui::GetIO()}; + m_track_box.active = true; + m_track_box.from = m_track_box.to = {io.MousePos.x, io.MousePos.y}; + m_track_box.hovered = m_hover.track; +} + +void editor_mode::finish_track_box() +{ + auto &box{m_track_box}; + if (false == box.active) + return; + box.active = false; + auto const &io{ImGui::GetIO()}; + box.to = {io.MousePos.x, io.MousePos.y}; + if (glm::length(box.to - box.from) < 6.0f) + { + if (box.hovered != nullptr && false == box.hovered->m_editorremoved) + track_set_toggle(*box.hovered); + return; + } + glm::vec2 const low{std::min(box.from.x, box.to.x), std::min(box.from.y, box.to.y)}; + glm::vec2 const high{std::max(box.from.x, box.to.x), std::max(box.from.y, box.to.y)}; + screen_projection const projection; + track_set_prune(); + if (m_track_set.empty()) + if (auto *selected{selected_track()}; selected != nullptr) + m_track_set.push_back(selected); + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || false == rail(*track) || in_track_set(track)) + continue; + bool inside{false}; + for (auto const &path : track->m_paths) + { + bezier const curve{path}; + for (int k = 0; k <= 8 && false == inside; ++k) + { + ImVec2 screen; + if (projection.project(curve.point(k / 8.0), screen)) + inside = screen.x >= low.x && screen.x <= high.x && screen.y >= low.y && screen.y <= high.y; + } + if (inside) + break; + } + if (inside) + m_track_set.push_back(track); + } + if (false == m_track_set.empty()) + { + m_node = m_track_set.back(); + m_track_point = {}; + ui()->set_node(m_node); + } +} + +std::vector editor_mode::track_spread_from(TTrack &Start) const +{ + auto const &rules{m_spread}; + auto const bed = [](TTrack const &Track) { return Track.eType == tt_Switch ? (Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle) : Track.m_material2; }; + auto const startspeed{editor_track::velocity(Start)}; + auto const alike = [&](TTrack const &Track) { + if (rules.same_rails && Track.m_material1 != Start.m_material1) + return false; + if (rules.same_bed && bed(Track) != bed(Start)) + return false; + if (rules.same_speed && std::abs(editor_track::velocity(Track) - startspeed) > 0.5) + return false; + return true; + }; + auto const exits = [&](TTrack const &Track) { + std::vector result; + if (Track.m_paths.empty()) + return result; + auto const &main{Track.m_paths.front()}; + result = {main.points[segment_data::point::start], main.points[segment_data::point::end]}; + if (Track.eType == tt_Switch && rules.stop == 2) + for (std::size_t i = 1; i < Track.m_paths.size(); ++i) + result.push_back(Track.m_paths[i].points[segment_data::point::end]); + return result; + }; + std::size_t constexpr limit{20000}; + std::vector result; + std::unordered_map reached{{&Start, 0.0}}; + std::unordered_set done; + using entry = std::pair; + std::priority_queue, std::greater> queue; + queue.push({0.0, &Start}); + while (false == queue.empty() && result.size() < limit) + { + auto const [distance, track] = queue.top(); + queue.pop(); + if (false == done.insert(track).second) + continue; + result.push_back(track); + auto const further{distance + track->Length()}; + if (rules.reach > 0.f && further > static_cast(rules.reach)) + continue; + for (auto const &end : exits(*track)) + for (auto const &[other, point] : editor_track::connected_points(*track, end)) + { + if (other == nullptr || done.count(other) > 0 || false == rail(*other) || other->eType == tt_Table || false == alike(*other)) + continue; + if (other->eType == tt_Switch && (rules.stop == 0 || (rules.stop == 1 && point.path != 0))) + continue; + auto const known{reached.find(other)}; + if (known != reached.end() && known->second <= further) + continue; + reached[other] = further; + queue.push({further, other}); + } + } + return result; +} + +std::vector const &editor_mode::track_spread_preview(TTrack &Start) const +{ + auto &spread{m_spread}; + auto const signature{spread.stop | (spread.same_rails ? 4 : 0) | (spread.same_bed ? 8 : 0) | (spread.same_speed ? 16 : 0) | (static_cast(spread.reach) << 5)}; + if (spread.from != &Start || spread.signature != signature) + { + spread.from = &Start; + spread.signature = signature; + spread.preview = track_spread_from(Start); + spread.length = 0.0; + for (auto const *track : spread.preview) + spread.length += track->Length(); + } + return spread.preview; +} + +void editor_mode::track_spread_apply(TTrack &Start) +{ + track_set_prune(); + if (m_track_set.empty()) + if (auto *selected{selected_track()}; selected != nullptr && selected != &Start) + m_track_set.push_back(selected); + auto const taken{track_spread_from(Start)}; + if (in_track_set(&Start)) + { + std::unordered_set const out(taken.begin(), taken.end()); + m_track_set.erase(std::remove_if(m_track_set.begin(), m_track_set.end(), [&](TTrack const *Track) { return out.count(Track) > 0; }), m_track_set.end()); + } + else + for (auto *track : taken) + if (false == in_track_set(track)) + m_track_set.push_back(track); + m_spread.from = nullptr; + m_node = m_track_set.empty() ? nullptr : m_track_set.back(); + m_track_point = {}; + ui()->set_node(m_node); +} + +void editor_mode::track_set_grow() +{ + track_set_prune(); + auto const reach{m_spread.reach}; + std::vector added; + for (auto *track : m_track_set) + { + m_spread.reach = std::max(0.01f, static_cast(track->Length()) - 0.01f); + for (auto *other : track_spread_from(*track)) + if (false == in_track_set(other) && std::find(added.begin(), added.end(), other) == added.end()) + added.push_back(other); + } + m_spread.reach = reach; + m_spread.from = nullptr; + m_track_set.insert(m_track_set.end(), added.begin(), added.end()); +} + +void editor_mode::render_track_spread_ui() +{ + auto &spread{m_spread}; + if (false == ImGui::CollapsingHeader(STR_C("Spread along the joins: Ctrl+Shift+LMB on a path"))) + return; + char const *const stops[] = {STR_C("Stop before the switches"), STR_C("Through the switches, along their main track"), STR_C("Through everything joined")}; + for (int i = 0; i < IM_ARRAYSIZE(stops); ++i) + ImGui::RadioButton(stops[i], &spread.stop, i); + ImGui::TextUnformatted(STR_C("Only to the paths like the clicked one:")); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("rails"), &spread.same_rails); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("trackbed"), &spread.same_bed); + ImGui::SameLine(); + ImGui::Checkbox(STR_C("speed"), &spread.same_speed); + ImGui::SetNextItemWidth(110.f); + ImGui::InputFloat(STR_C("Reach along the track, m (0: no limit)"), &spread.reach, 50.f, 500.f, "%.0f"); + spread.reach = std::clamp(spread.reach, 0.f, 100000.f); + ImGui::TextDisabled("%s", STR_C("Holding Ctrl+Shift shows what the click takes; on a path of the set it takes them out")); +} + +void editor_mode::draw_track_spread() const +{ + if (m_intent.what != track_intent::kind::spread || m_spread.from == nullptr) + return; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const removing{in_track_set(m_spread.from)}; + auto const colour{removing ? IM_COL32(255, 90, 70, 220) : IM_COL32(90, 220, 255, 220)}; + for (auto const *track : m_spread.preview) + { + if (track->m_editorremoved) + continue; + for (auto const &path : track->m_paths) + { + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / 4.0), 4, 48)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, colour, 2.5f); + previous = next; + } + } + } +} + +void editor_mode::draw_track_set() const +{ + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + if (m_track_box.active) + { + auto const &io{ImGui::GetIO()}; + ImVec2 const from{m_track_box.from.x, m_track_box.from.y}; + drawlist->AddRectFilled(from, io.MousePos, IM_COL32(255, 160, 40, 40)); + drawlist->AddRect(from, io.MousePos, IM_COL32(255, 160, 40, 220), 0.0f, 0, 1.5f); + } + if (m_track_set.size() < 2) + return; + for (auto const *track : m_track_set) + { + if (track->m_editorremoved) + continue; + for (auto const &path : track->m_paths) + { + bezier const curve{path}; + auto const count{std::clamp(static_cast(curve.plan_length() / 3.0), 6, 64)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + projection.line(drawlist, previous, next, IM_COL32(255, 160, 40, 230), 3.0f); + previous = next; + } + } + } +} + +void editor_mode::track_set_apply(std::function const &Change, bool const Rebuild, char const *What) +{ + track_set_prune(); + std::vector> states; + std::vector changed; + std::size_t locked{0}; + for (auto *track : m_track_set) + { + if (false == scene::Layers.editable(track)) + { + ++locked; + continue; + } + auto const before{editor_track::capture(*track)}; + if (false == Change(*track)) + continue; + states.emplace_back(track, before); + changed.push_back(track); + } + if (changed.empty()) + return; + push_track_snapshot(std::move(states)); + for (auto *track : changed) + { + if (Rebuild) + editor_track::commit_parameters(*track); + else + track->mark_dirty(); + } + auto status{format(STR_C("%s set on %zu paths, Ctrl+Z takes it back"), STR_C(What), changed.size())}; + if (locked > 0) + status += format(STR_C(", %zu in the files which can't be changed are left as they are"), locked); + ui()->set_status(status, false); +} + +void editor_mode::track_set_delete() +{ + track_set_prune(); + std::vector removed; + std::size_t kept{0}; + for (auto *track : m_track_set) + { + if (false == track->Dynamics.empty() || false == scene::Layers.editable(track)) + { + ++kept; + continue; + } + editor_track::retire(*track); + removed.push_back(track); + if (std::find(m_route.chain.tracks.begin(), m_route.chain.tracks.end(), track) != m_route.chain.tracks.end()) + m_route = {}; + } + push_track_snapshot({}, {}, removed); + m_track_set.clear(); + m_node = nullptr; + ui()->set_node(nullptr); + m_track_point = {}; + straight_refresh(); + auto status{format(STR_C("%zu paths deleted, Ctrl+Z brings them back"), removed.size())}; + if (kept > 0) + status += format(STR_C(", %zu with vehicles on them or in the files which can't be changed are left"), kept); + ui()->set_status(status, false); +} + +void editor_mode::render_track_set_ui() +{ + track_set_prune(); + if (m_track_set.size() < 2) + return; + auto &fields{m_track_set_fields}; + double length{0.0}; + for (auto const *track : m_track_set) + length += track->Length(); + ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.2f, 1.0f), STR_C("%zu paths selected, %.1f m"), m_track_set.size(), length); + ImGui::SameLine(); + if (ImGui::SmallButton(STR_C("Clear"))) + { + m_track_set.clear(); + return; + } + ImGui::TextDisabled("%s", STR_C("The gizmo moves them together; Shift+LMB adds or takes out a path")); + render_parallel_set_ui(); + if (ImGui::SmallButton(STR_C("Grow by the neighbours"))) + track_set_grow(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Adds the paths joined to the set, by the rules of the spread")); + auto const button_width{90.0f}; + ImGui::PushItemWidth(-button_width - ImGui::GetStyle().ItemSpacing.x); + ImGui::InputDouble("##setvelocity", &fields.velocity, 0.0, 0.0, "%.0f"); + ImGui::SameLine(); + if (ImGui::Button(format("%s##setv", STR_C("Velocity")).c_str(), ImVec2(button_width, 0.0f))) + { + auto const velocity{fields.velocity}; + track_set_apply([velocity](TTrack &Track) { editor_track::velocity(Track, velocity); return true; }, false, "Velocity"); + } + ImGui::InputTextWithHint("##setrail", STR_C("texture of the rails"), fields.rail, sizeof(fields.rail)); + ImGui::SameLine(); + if (ImGui::Button(format("%s##setr", STR_C("Rails")).c_str(), ImVec2(button_width, 0.0f)) && fields.rail[0] != '\0') + { + auto const material{editor_track::fetch_material(fields.rail)}; + track_set_apply([material](TTrack &Track) { + Track.m_material1 = material; + if (Track.eType == tt_Switch) + Track.m_material2 = material; + return true; + }, true, "Rails"); + } + ImGui::InputTextWithHint("##settrackbed", STR_C("texture of the trackbed"), fields.trackbed, sizeof(fields.trackbed)); + ImGui::SameLine(); + if (ImGui::Button(format("%s##setb", STR_C("Trackbed")).c_str(), ImVec2(button_width, 0.0f)) && fields.trackbed[0] != '\0') + { + auto const material{editor_track::fetch_material(fields.trackbed)}; + track_set_apply([material](TTrack &Track) { + if (Track.eType == tt_Switch) + { + if (Track.SwitchExtension == nullptr) + return false; + Track.SwitchExtension->m_material3 = material; + } + else + Track.m_material2 = material; + return true; + }, true, "Trackbed"); + } + ImGui::InputTextWithHint("##setcircuits", STR_C("track circuits, separated by spaces"), fields.circuits, sizeof(fields.circuits)); + ImGui::SameLine(); + if (ImGui::Button(format("%s##setc", STR_C("Circuits")).c_str(), ImVec2(button_width, 0.0f))) + { + std::vector names; + std::string const text{fields.circuits}; + std::size_t at{0}; + while (at < text.size()) + { + auto const end{text.find(' ', at)}; + auto const word{text.substr(at, end == std::string::npos ? std::string::npos : end - at)}; + if (false == word.empty()) + names.push_back(word); + if (end == std::string::npos) + break; + at = end + 1; + } + track_set_apply([names](TTrack &Track) { editor_track::isolated(Track, names); return true; }, false, "Circuits"); + } + char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; + ImGui::Combo("##setenvironment", &fields.environment, environments, IM_ARRAYSIZE(environments)); + ImGui::SameLine(); + if (ImGui::Button(format("%s##sete", STR_C("Environment")).c_str(), ImVec2(button_width, 0.0f))) + { + auto const environment{static_cast(fields.environment)}; + track_set_apply([environment](TTrack &Track) { Track.eEnvironment = environment; return true; }, false, "Environment"); + } + ImGui::PopItemWidth(); + if (ImGui::Button(format(STR_C("Delete %zu paths"), m_track_set.size()).c_str())) + track_set_delete(); + ImGui::Separator(); +} + +void editor_mode::render_track_set_gizmo() +{ + track_set_prune(); + if (m_track_set.size() < 2) + { + m_track_set_using = false; + return; + } + gizmo_frame const frame; + if (false == m_track_set_using) + { + glm::dvec3 centre{0.0}; + for (auto const *track : m_track_set) + centre += editor_track::pivot(*track); + m_track_set_pivot = centre / static_cast(m_track_set.size()); + m_track_set_gizmo = glm::translate(glm::mat4(1.0f), glm::vec3(m_track_set_pivot - frame.camera)); + } + glm::mat4 delta(1.0f); + frame.manipulate(ImGuizmo::TRANSLATE, m_track_set_gizmo, m_gizmo_snap, &delta); + if (ImGuizmo::IsUsing()) + { + if (false == m_track_set_using) + { + std::string reason; + if (false == tracks_editable(m_track_set, reason)) + { + ui()->set_status(reason, true); + return; + } + m_track_set_using = true; + m_track_drag = m_track_set; + m_track_set_joints.clear(); + for (auto *track : m_track_set) + { + auto joints{editor_track::joints(*track)}; + joints.erase(std::remove_if(joints.begin(), joints.end(), [&](editor_track::joint const &Joint) { return in_track_set(Joint.other); }), joints.end()); + for (auto const &joint : joints) + if (std::find(m_track_drag.begin(), m_track_drag.end(), joint.other) == m_track_drag.end()) + m_track_drag.push_back(joint.other); + m_track_set_joints.emplace_back(track, std::move(joints)); + } + std::vector> states; + for (auto *track : m_track_drag) + states.emplace_back(track, editor_track::capture(*track)); + push_track_snapshot(std::move(states)); + } + glm::dvec3 const offset{delta[3]}; + if (glm::length(offset) > 1e-9) + { + for (auto &entry : m_track_set_joints) + { + editor_track::translate(*entry.first, offset); + editor_track::follow(*entry.first, entry.second); + } + m_track_dirty = true; + } + commit_track_drag(false); + } + else if (m_track_set_using) + { + commit_track_drag(true); + m_track_set_using = false; + m_track_drag.clear(); + m_track_set_joints.clear(); + straight_refresh(); + } +} + +void editor_mode::render_lineside_ui() +{ + struct tab + { + char const *label; + bool *open; + bool *expand; + std::function render; + }; + std::array const tabs{{ + {"Hectometre posts", &m_hekto.open, &m_hekto.expand, [this]() { render_hekto_ui(); }}, + {"Fouling points", &m_fouling.open, &m_fouling.expand, [this]() { render_fouling_ui(); }}, + {"Parallel track", &m_parallel.open, &m_parallel.expand, [this]() { render_parallel_ui(); }}, + {"Vehicle", &m_vehicle.open, &m_vehicle.expand, [this]() { render_vehicle_ui(); }}, + }}; + if (selected_track() == nullptr) + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("LMB on a track: the line the objects go along")); + if (false == ImGui::BeginTabBar("##lineside")) + return; + for (int i = 0; i < static_cast(tabs.size()); ++i) + { + auto const &entry{tabs[i]}; + auto const wanted{*entry.expand}; + *entry.expand = false; + *entry.open = ImGui::BeginTabItem(STR_C(entry.label), nullptr, wanted ? ImGuiTabItemFlags_SetSelected : 0); + if (false == *entry.open) + continue; + m_lineside_tab = i; + entry.render(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); +} diff --git a/application/editormodeturntable.cpp b/application/editormodeturntable.cpp new file mode 100644 index 00000000..e6b78d9e --- /dev/null +++ b/application/editormodeturntable.cpp @@ -0,0 +1,1168 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editorprojection.h" +#include "application/editoruilayer.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorIncludeInfo.hpp" +#include "model/AnimModel.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/translation.h" +#include "utilities/utilities.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::arc_pieces; +using geometry::plan_distance; +using geometry::plan_of; +using geometry::signed_angle; + +double constexpr kRimTolerance{0.05}; +double constexpr kMinimumRadius{80.0}; +float constexpr kDragPixels{5.0f}; + +glm::dvec2 heading_vector(double const Heading) +{ + return {std::sin(glm::radians(Heading)), std::cos(glm::radians(Heading))}; +} + +double heading_of(glm::dvec2 const &Direction) +{ + return glm::degrees(std::atan2(Direction.x, Direction.y)); +} + +double circular(double Angle) +{ + Angle = std::fmod(Angle, 360.0); + if (Angle <= -180.0) + Angle += 360.0; + if (Angle > 180.0) + Angle -= 360.0; + return Angle; +} + +double line_angle(double const Angle) +{ + auto result{circular(Angle)}; + if (result >= 90.0) + result -= 180.0; + if (result < -90.0) + result += 180.0; + return result; +} + +bool numeric(std::string const &Text, double &Value) +{ + char *end{nullptr}; + Value = std::strtod(Text.c_str(), &end); + return false == Text.empty() && *end == '\0'; +} + +std::string normalized(std::string File) +{ + std::replace(File.begin(), File.end(), '\\', '/'); + return ToLower(File); +} + +std::vector fixed_angles(std::string const &File) +{ + std::vector result; + std::ifstream input{Global.asCurrentSceneryPath + File, std::ios_base::binary}; + if (false == input.is_open()) + return result; + std::string const content{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + static std::regex const pattern{R"(event\s+\S*tor(\d+)rot\s+animation\s+\S+\s+\S+\s+rotate\s+\S+\s+\S+\s+\S+\s+(\S+))", std::regex::icase}; + for (auto match{std::sregex_iterator(content.begin(), content.end(), pattern)}; match != std::sregex_iterator(); ++match) + { + double angle; + if (numeric((*match)[2].str(), angle) && std::none_of(result.begin(), result.end(), [&](double const Angle) { return std::abs(line_angle(Angle - angle)) < 0.01; })) + result.push_back(angle); + } + return result; +} + +glm::dvec3 centre_of(TTrack const &Table) +{ + auto const &path{Table.m_paths.front()}; + return glm::mix(path.points[segment_data::point::start], path.points[segment_data::point::end], 0.5); +} + +double length_of(TTrack const &Table) +{ + auto const &path{Table.m_paths.front()}; + return plan_distance(path.points[segment_data::point::start], path.points[segment_data::point::end]); +} + +struct rim_end +{ + double heading{0.0}; + TTrack *track{nullptr}; +}; + +std::vector rim_ends(TTrack const *Table, glm::dvec3 const &Centre, double const Rim) +{ + std::vector result; + for (auto *section : simulation::Region->sections(Centre, static_cast(Rim + 10.0))) + for (auto const &cell : section->m_cells) + for (auto *other : cell.m_directories.paths) + { + if (other == Table || other->m_editorremoved || (other->iCategoryFlag & 15) != 1 || other->eType == tt_Table) + continue; + for (auto const &path : other->m_paths) + for (auto const &point : {path.points[segment_data::point::start], path.points[segment_data::point::end]}) + { + if (std::abs(plan_distance(point, Centre) - Rim) > kRimTolerance || std::abs(point.y - Centre.y) > 0.5) + continue; + auto const heading{heading_of(plan_of(point - Centre))}; + if (std::none_of(result.begin(), result.end(), [&](rim_end const &End) { return End.track == other && std::abs(circular(End.heading - heading)) < 0.01; })) + result.push_back({heading, other}); + } + } + return result; +} + +void draw_circle(screen_projection const &Projection, ImDrawList *Drawlist, glm::dvec3 const &Centre, double const Radius, ImU32 const Colour, float const Thickness) +{ + int const count{48}; + glm::dvec3 previous{Centre.x, Centre.y, Centre.z + Radius}; + for (int i = 1; i <= count; ++i) + { + auto const angle{glm::two_pi() * i / count}; + glm::dvec3 const next{Centre.x + Radius * std::sin(angle), Centre.y, Centre.z + Radius * std::cos(angle)}; + Projection.line(Drawlist, previous, next, Colour, Thickness); + previous = next; + } +} + +void draw_pieces(screen_projection const &Projection, ImDrawList *Drawlist, std::vector const &Pieces, ImU32 const Colour, float const Thickness) +{ + for (auto const &piece : Pieces) + { + geometry::bezier const curve{piece}; + auto const count{std::clamp(static_cast(curve.plan_length() / 2.0), 2, 48)}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const next{curve.point(static_cast(k) / count)}; + Projection.line(Drawlist, previous, next, Colour, Thickness); + previous = next; + } + } +} + +void label(screen_projection const &Projection, glm::dvec3 const &Point, std::string const &Text) +{ + ImVec2 at; + if (false == Projection.project(Point, at)) + return; + auto *drawlist{ImGui::GetForegroundDrawList(ImGui::GetMainViewport())}; + auto const size{ImGui::CalcTextSize(Text.c_str())}; + drawlist->AddRectFilled(ImVec2(at.x + 8.f, at.y - 4.f), ImVec2(at.x + 16.f + size.x, at.y + size.y + 4.f), IM_COL32(0, 0, 0, 170), 4.f); + drawlist->AddText(ImVec2(at.x + 12.f, at.y), IM_COL32(255, 255, 255, 235), Text.c_str()); +} + +} // namespace + +void editor_mode::scan_turntables() +{ + auto &tool{m_turntable}; + if (tool.scanned) + return; + tool.scanned = true; + tool.templates.clear(); + if (false == EditorIncludes.scanned()) + EditorIncludes.scan(); + for (auto const &entry : EditorIncludes.entries()) + { + if (entry.category != "turntable") + continue; + include_info info; + std::string error; + if (false == editor_includes::load(entry.file, info, error) || info.turntable_length <= 0.0) + continue; + turntable_template item; + item.file = entry.file; + item.name = info.name.empty() ? entry.file : info.name; + item.length = info.turntable_length; + item.track = editor_includes::parameter_with_role(info, "track"); + item.yaw = editor_includes::parameter_with_role(info, "rot.y"); + if (info.turntable_angles > 0 && info.turntable_positions > 0) + { + item.angles = info.turntable_angles; + item.positions = info.turntable_positions; + } + else + item.fixed = fixed_angles(entry.file); + if (item.track == 0 || item.yaw == 0 || (item.angles == 0 && item.fixed.empty())) + continue; + tool.templates.push_back(std::move(item)); + } + std::stable_sort(tool.templates.begin(), tool.templates.end(), [](turntable_template const &A, turntable_template const &B) { return (A.angles > 0) != (B.angles > 0) ? A.angles > 0 : A.name < B.name; }); + tool.chosen = tool.templates.empty() ? -1 : 0; +} + +std::string editor_mode::turntable_name_for_new() const +{ + if (m_turntable.name[0] != '\0') + return ToLower(m_turntable.name); + for (int i = 1;; ++i) + { + auto const name{"obr" + std::to_string(i)}; + if (simulation::Paths.find(name) == nullptr && simulation::Instances.find(name) == nullptr) + return name; + } +} + +bool editor_mode::turntable_read(TTrack *Table) +{ + auto &tool{m_turntable}; + if (Table == nullptr || Table->eType != tt_Table || Table->m_editorremoved || Table->m_paths.empty()) + { + tool.table = nullptr; + tool.include = 0; + tool.kind = -1; + tool.exits.clear(); + tool.fits.clear(); + return false; + } + if (tool.table != Table) + { + tool.fits.clear(); + tool.hovered = -1; + tool.status.clear(); + tool.error.clear(); + } + scan_turntables(); + tool.table = Table; + tool.length = length_of(*Table); + auto const &path{Table->m_paths.front()}; + tool.table_yaw = heading_of(plan_of(path.points[segment_data::point::start] - centre_of(*Table))); + tool.include = 0; + tool.kind = -1; + tool.exits.clear(); + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count() && tool.include == 0; ++handle) + { + if (false == scene::Layers.tracked(handle)) + continue; + auto const &included{scene::Layers.instance(handle)}; + if (included.removed || included.dead || included.file == nullptr) + continue; + auto const file{normalized(*included.file)}; + auto const kind{std::find_if(tool.templates.begin(), tool.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == file; })}; + if (kind == tool.templates.end()) + continue; + std::string target; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || kind->track > static_cast(values.size()) || ToLower(values[kind->track - 1]) != Table->name()) + continue; + tool.include = handle; + tool.kind = static_cast(kind - tool.templates.begin()); + if (double yaw; kind->yaw <= static_cast(values.size()) && numeric(values[kind->yaw - 1], yaw)) + tool.table_yaw = yaw; + if (kind->angles > 0) + { + for (int i = 0; i < kind->positions && kind->angles - 1 + i < static_cast(values.size()); ++i) + if (double angle; numeric(values[kind->angles - 1 + i], angle) && std::none_of(tool.exits.begin(), tool.exits.end(), [&](double const Exit) { return std::abs(line_angle(Exit - angle)) < 0.01; })) + tool.exits.push_back(angle); + } + else + tool.exits = kind->fixed; + } + return true; +} + +glm::dvec3 editor_mode::turntable_centre() const +{ + return m_turntable.table != nullptr ? centre_of(*m_turntable.table) : glm::dvec3{0.0}; +} + +std::vector editor_mode::turntable_lines() const +{ + std::vector result; + for (auto const angle : m_turntable.exits) + result.push_back(m_turntable.table_yaw + angle); + return result; +} + +double editor_mode::turntable_snap(double const Heading) const +{ + auto const &tool{m_turntable}; + auto const fixed{tool.kind >= 0 && tool.templates[tool.kind].angles == 0}; + std::optional nearest; + for (auto const line : turntable_lines()) + for (auto const heading : {line, line + 180.0}) + if (false == nearest.has_value() || std::abs(circular(heading - Heading)) < std::abs(circular(*nearest - Heading))) + nearest = heading; + if (nearest.has_value() && (fixed || std::abs(circular(*nearest - Heading)) < 1.0)) + return circular(*nearest); + auto const step{tool.step > 0.f ? static_cast(tool.step) : 0.1}; + return circular(tool.table_yaw + std::round(circular(Heading - tool.table_yaw) / step) * step); +} + +bool editor_mode::turntable_exit_at(glm::dvec3 const &Ground, double &Heading, double &Length) const +{ + auto const &tool{m_turntable}; + if (tool.table == nullptr) + return false; + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + auto const distance{plan_distance(Ground, centre)}; + if (distance < rim + 1.0 || distance > rim + tool.reach) + return false; + Heading = turntable_snap(heading_of(plan_of(Ground - centre))); + Length = std::max(5.0, std::round((distance - rim) * 2.0) * 0.5); + for (auto const &end : rim_ends(tool.table, centre, rim)) + if (std::abs(circular(end.heading - Heading)) < 0.05) + return false; + return true; +} + +bool editor_mode::turntable_fit_end(editor_track::snap_target const &End, turntable_fit &Fit) const +{ + auto const &tool{m_turntable}; + Fit = {}; + Fit.end = End; + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + glm::dvec2 const c{plan_of(centre)}; + auto const into{plan_of(End.direction)}; + if (tool.table == nullptr || glm::length(into) < 1e-6) + return false; + glm::dvec2 const t{glm::normalize(into)}; + auto target{End.position}; + glm::dvec2 e{plan_of(target)}; + struct shape + { + glm::dvec2 radial{0.0}; + double radius{0.0}; + double first{0.0}; + double turn{0.0}; + double second{0.0}; + }; + auto const connect = [&](glm::dvec2 const &Radial, double const Radius) -> std::optional { + auto const rimpoint{c + Radial * rim}; + auto const delta{signed_angle(Radial, t)}; + auto const offset{e - rimpoint}; + if (std::abs(delta) < 1e-5) + { + auto const along{glm::dot(offset, Radial)}; + if (along < 0.5 || std::abs(geometry::cross(Radial, offset)) > 0.02) + return std::nullopt; + return shape{Radial, 0.0, along, 0.0, 0.0}; + } + if (std::abs(delta) > glm::radians(90.0)) + return std::nullopt; + auto const a{geometry::cross(t, offset) / geometry::cross(t, Radial)}; + auto const s{geometry::cross(Radial, offset) / geometry::cross(Radial, t)}; + auto const tangent{Radius * std::tan(std::abs(delta) * 0.5)}; + if (a - tangent < -1e-3 || s - tangent < -1e-3) + return std::nullopt; + return shape{Radial, Radius, std::max(0.0, a - tangent), delta, std::max(0.0, s - tangent)}; + }; + std::optional best; + std::string issue; + auto const better = [&](shape const &Candidate) { + if (false == best.has_value()) + return true; + auto const straight = [](shape const &Shape) { return Shape.radius == 0.0; }; + if (straight(Candidate) != straight(*best)) + return straight(Candidate); + return Candidate.radius > best->radius + 1e-6; + }; + auto const solve = [&]() { + auto const fixed{tool.kind >= 0 && tool.templates[tool.kind].angles == 0}; + if (fixed) + { + for (auto const line : turntable_lines()) + for (auto const heading : {line, line + 180.0}) + { + auto const radial{heading_vector(heading)}; + for (int i = 0; tool.radius * std::pow(0.9, i) >= kMinimumRadius; ++i) + if (auto const candidate{connect(radial, tool.radius * std::pow(0.9, i))}) + { + if (better(*candidate)) + best = candidate; + break; + } + } + if (false == best.has_value()) + issue = STR("It doesn't line up with any position of the turntable"); + } + else if (std::abs(geometry::cross(t, c - e)) < 0.02 && glm::dot(t, e - c) > rim + 0.5) + { + best = connect(t, 0.0); + } + else + { + for (int r = 0; tool.radius * std::pow(0.9, r) >= kMinimumRadius && false == best.has_value(); ++r) + { + auto const radius{tool.radius * std::pow(0.9, r)}; + auto const gap = [&](double const S) { + auto const vertex{e - t * S}; + auto const radial{vertex - c}; + if (glm::length(radial) < 1e-6) + return -1.0; + auto const delta{signed_angle(glm::normalize(radial), t)}; + return S - radius * std::tan(std::abs(delta) * 0.5); + }; + auto const limit{plan_distance(End.position, centre) + 400.0}; + double low{0.0}; + std::optional high; + for (int i = 1; i * 0.25 <= limit; ++i) + { + auto const s{i * 0.25}; + if (gap(s) >= 0.0) + { + high = s; + break; + } + low = s; + } + if (false == high.has_value()) + continue; + auto upper{*high}; + for (int i = 0; i < 50; ++i) + { + auto const middle{(low + upper) * 0.5}; + (gap(middle) >= 0.0 ? upper : low) = middle; + } + auto const vertex{e - t * upper}; + if (glm::length(vertex - c) <= rim) + continue; + best = connect(glm::normalize(vertex - c), radius); + } + if (false == best.has_value()) + issue = format(STR_C("Too close to the turntable for a curve of %.0f m or more"), kMinimumRadius); + } + }; + solve(); + auto *own{End.track}; + if (false == best.has_value() && own != nullptr && own->eType == tt_Normal && own->m_paths.size() == 1 && own->Dynamics.empty()) + { + auto const &path{own->m_paths.front()}; + auto const opposite{End.point.kind == editor_track::point_kind::start ? path.points[segment_data::point::end] : path.points[segment_data::point::start]}; + auto const straight{std::abs(geometry::cross(t, plan_of(opposite - End.position))) < 0.01 && glm::dot(t, plan_of(opposite - End.position)) > 1.0}; + std::string reason; + if (straight && editor_track::can_edit_geometry(*own, reason)) + { + auto const first{issue}; + target = opposite; + e = plan_of(target); + solve(); + if (best.has_value()) + Fit.replaces = own; + else + { + target = End.position; + e = plan_of(target); + issue = first; + } + } + } + if (false == best.has_value()) + { + Fit.issue = issue.empty() ? STR("It points away from the turntable") : issue; + return false; + } + auto const &shape{*best}; + Fit.heading = heading_of(shape.radial); + Fit.radius = shape.radius; + Fit.length = shape.first + shape.radius * std::abs(shape.turn) + shape.second; + auto const grade{Fit.length > 1e-6 ? (target.y - centre.y) / Fit.length : 0.0}; + glm::dvec3 point{c.x + shape.radial.x * rim, centre.y, c.y + shape.radial.y * rim}; + auto const straight = [&](glm::dvec2 const &Direction, double const Length) { + segment_data piece; + piece.points[segment_data::point::start] = point; + point = {point.x + Direction.x * Length, point.y + grade * Length, point.z + Direction.y * Length}; + piece.points[segment_data::point::end] = point; + Fit.pieces.push_back(piece); + }; + if (shape.first > 0.05) + straight(shape.radial, shape.first); + if (shape.radius > 0.0) + { + auto const arc{arc_pieces(point, shape.radial, grade, shape.radius, std::abs(shape.turn), shape.turn > 0.0 ? 1 : -1)}; + Fit.pieces.insert(Fit.pieces.end(), arc.begin(), arc.end()); + point = arc.back().points[segment_data::point::end]; + } + if (shape.second > 0.05) + straight(t, shape.second); + if (false == Fit.pieces.empty()) + Fit.pieces.back().points[segment_data::point::end] = target; + return false == Fit.pieces.empty(); +} + +void editor_mode::turntable_find_fits() +{ + auto &tool{m_turntable}; + tool.fits.clear(); + tool.hovered = -1; + tool.error.clear(); + if (tool.table == nullptr) + return; + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + for (auto const &end : editor_track::free_ends(nullptr, 1, centre, rim + tool.reach, {tool.table})) + { + if (std::abs(plan_distance(end.position, centre) - rim) <= kRimTolerance) + continue; + turntable_fit fit; + turntable_fit_end(end, fit); + fit.chosen = fit.issue.empty(); + tool.fits.push_back(std::move(fit)); + } + std::sort(tool.fits.begin(), tool.fits.end(), [](turntable_fit const &A, turntable_fit const &B) { return A.end.distance < B.end.distance; }); + tool.status = tool.fits.empty() ? format(STR_C("No free ends within %.0f m of the rim"), tool.reach) : std::string{}; +} + +std::vector editor_mode::turntable_includes(TTrack const &Table) +{ + scan_turntables(); + std::vector result; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + if (false == scene::Layers.tracked(handle)) + continue; + auto const &included{scene::Layers.instance(handle)}; + if (included.removed || included.dead || included.file == nullptr) + continue; + auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })}; + std::string target; + std::vector values; + if (kind != m_turntable.templates.end() && editor_includes::parse_directive(scene::Layers.directive(handle), target, values) && kind->track <= static_cast(values.size()) && ToLower(values[kind->track - 1]) == Table.name()) + result.push_back(handle); + } + return result; +} + +void editor_mode::turntable_build(std::vector const &Fits) +{ + auto &tool{m_turntable}; + if (tool.table == nullptr || Fits.empty()) + return; + std::vector created; + std::vector removed; + for (auto const *fit : Fits) + { + if (false == fit->issue.empty() || fit->end.track == nullptr || fit->end.track->m_editorremoved) + continue; + for (auto const &piece : fit->pieces) + created.push_back(editor_track::create_path(*fit->end.track, piece)); + if (fit->replaces != nullptr && false == fit->replaces->m_editorremoved) + { + editor_track::retire(*fit->replaces); + removed.push_back(fit->replaces); + } + } + if (created.empty()) + return; + editor_track::commit(created); + push_track_snapshot({}, created, removed); + turntable_store_exits(); + tool.status = format(STR_C("%d track(s) led to the turntable"), static_cast(Fits.size())); +} + +void editor_mode::turntable_store_exits() +{ + auto &tool{m_turntable}; + if (tool.table == nullptr || tool.include == 0 || tool.kind < 0) + { + turntable_read(tool.table); + return; + } + auto const &kind{tool.templates[tool.kind]}; + if (kind.angles == 0) + { + turntable_read(tool.table); + return; + } + std::vector lines; + for (auto const &end : rim_ends(tool.table, turntable_centre(), tool.length * 0.5)) + { + auto const angle{line_angle(end.heading - tool.table_yaw)}; + if (std::none_of(lines.begin(), lines.end(), [&](double const Line) { return std::abs(line_angle(Line - angle)) < 0.01; })) + lines.push_back(angle); + } + std::sort(lines.begin(), lines.end()); + if (static_cast(lines.size()) > kind.positions) + tool.error = format(STR_C("The turntable has %d positions, %d track lines lead to it: the rest won't be reached"), kind.positions, static_cast(lines.size())); + std::string target; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(tool.include), target, values)) + return; + for (int i = 0; i < kind.positions && kind.angles - 1 + i < static_cast(values.size()); ++i) + values[kind.angles - 1 + i] = editor_includes::number(i < static_cast(lines.size()) ? lines[i] : lines.empty() ? 0.0 : lines.front()); + auto const directive{editor_includes::compose_directive(target, values)}; + auto before{scene::Layers.directive(tool.include)}; + if (directive != before) + { + EditorSnapshot snap; + snap.node_name = target; + snap.directives.emplace_back(tool.include, std::move(before)); + set_include_directive(tool.include, directive); + trim_history(); + m_history.push_back(std::move(snap)); + g_redo.clear(); + } + turntable_read(tool.table); +} + +void editor_mode::turntable_place(glm::dvec3 const &Centre, double const Yaw) +{ + auto &tool{m_turntable}; + scan_turntables(); + tool.error.clear(); + if (tool.chosen < 0 || tool.chosen >= static_cast(tool.templates.size())) + return; + auto const &kind{tool.templates[tool.chosen]}; + auto const name{turntable_name_for_new()}; + if (simulation::Paths.find(name) != nullptr) + { + tool.error = format(STR_C("The name %s is taken by another path"), name.c_str()); + return; + } + auto const rim{kind.length * 0.5}; + auto const direction{heading_vector(Yaw)}; + segment_data path; + path.points[segment_data::point::start] = {Centre.x + direction.x * rim, Centre.y, Centre.z + direction.y * rim}; + path.points[segment_data::point::end] = {Centre.x - direction.x * rim, Centre.y, Centre.z - direction.y * rim}; + auto *table{editor_track::create_turntable(path, tool.snap.track, name)}; + editor_track::commit({table}); + push_track_snapshot({}, {table}); + template_item item; + item.file = kind.file; + item.location = Centre; + item.yaw = Yaw; + item.described = true; + item.track = table->name(); + std::string error; + if (place_templates({item}, error) == 0) + tool.error = error; + tool.name[0] = '\0'; + m_node = table; + m_track_point = {}; + ui()->set_node(m_node); + turntable_read(table); + tool.status = format(STR_C("Turntable %s: click around it for the tracks out of it, or fit the tracks around. It turns after the scenery is saved and loaded again"), table->name().c_str()); +} + +void editor_mode::turntable_press(track_intent const &Intent) +{ + using kind = track_intent::kind; + auto &tool{m_turntable}; + auto const &io{ImGui::GetIO()}; + switch (Intent.what) + { + case kind::turntable_place: + tool.placing = true; + tool.pressed = {io.MousePos.x, io.MousePos.y}; + tool.snap = Intent.snap; + tool.centre = Intent.position; + return; + case kind::turntable_exit: + { + if (tool.table == nullptr) + return; + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + auto const direction{heading_vector(tool.heading)}; + segment_data piece; + piece.points[segment_data::point::start] = {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; + piece.points[segment_data::point::end] = {centre.x + direction.x * (rim + tool.exit_length), centre.y, centre.z + direction.y * (rim + tool.exit_length)}; + auto const ends{rim_ends(tool.table, centre, rim)}; + auto *track{ends.empty() ? editor_track::create_path(m_lay.style, piece) : editor_track::create_path(*ends.front().track, piece)}; + editor_track::commit({track}); + push_track_snapshot({}, {track}); + turntable_store_exits(); + tool.status = format(STR_C("Track out of the turntable at %.1f deg, %.1f m"), circular(tool.heading - tool.table_yaw), tool.exit_length); + return; + } + case kind::turntable_fit: + { + turntable_fit fit; + if (turntable_fit_end(Intent.snap, fit)) + turntable_build({&fit}); + else + tool.error = fit.issue; + return; + } + default: return; + } +} + +void editor_mode::turntable_finish_placement() +{ + auto &tool{m_turntable}; + if (false == tool.placing) + return; + tool.placing = false; + scan_turntables(); + if (tool.chosen < 0) + return; + auto const rim{tool.templates[tool.chosen].length * 0.5}; + auto const &io{ImGui::GetIO()}; + auto const dragged{glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - tool.pressed) >= kDragPixels}; + if (tool.snap.track != nullptr && false == dragged) + { + auto const into{glm::normalize(plan_of(tool.snap.direction))}; + glm::dvec3 const centre{tool.snap.position.x - into.x * rim, tool.snap.position.y, tool.snap.position.z - into.y * rim}; + tool.yaw = heading_of(into); + turntable_place(centre, tool.yaw); + return; + } + tool.snap = {}; + if (dragged) + { + auto const cursor{cursor_level(tool.centre.y)}; + if (plan_distance(cursor, tool.centre) > 0.5) + tool.yaw = std::round(heading_of(plan_of(cursor - tool.centre)) * 10.0) / 10.0; + } + turntable_place(tool.centre, tool.yaw); +} + +editor_mode::track_intent editor_mode::turntable_intent(glm::dvec3 const &Ground, bool const Shift) +{ + using kind = track_intent::kind; + auto &tool{m_turntable}; + track_intent intent; + intent.position = Ground; + auto const make = [&](kind const What, std::string const &Action) { + intent.what = What; + intent.action = Action; + return intent; + }; + auto const &io{ImGui::GetIO()}; + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + if (tool.table != selected_track()) + turntable_read(selected_track()); + if (tool.table != nullptr) + { + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + auto const end{snap_free_end(Ground, mouse, nullptr, 1, {tool.table})}; + if (end.track != nullptr && std::abs(plan_distance(end.position, centre) - rim) > kRimTolerance && plan_distance(end.position, centre) <= rim + tool.reach) + { + intent.snap = end; + intent.track = end.track; + intent.position = end.position; + return make(kind::turntable_fit, STR("Click: lead this track to the turntable")); + } + double heading, length; + if (turntable_exit_at(cursor_level(centre.y), heading, length)) + { + tool.heading = heading; + tool.exit_length = length; + auto const direction{heading_vector(heading)}; + intent.position = {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; + return make(kind::turntable_exit, format(STR_C("Click: a track out of the turntable, %.1f deg, %.1f m"), circular(heading - tool.table_yaw), length)); + } + } + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->eType != tt_Table || track->m_editorremoved || track->m_paths.empty()) + continue; + if (plan_distance(centre_of(*track), Ground) <= length_of(*track) * 0.5) + { + intent.track = track; + return make(kind::select, track == tool.table ? std::string{} : STR("Click: this turntable")); + } + } + scan_turntables(); + if (tool.chosen < 0) + return intent; + if (false == Shift) + { + auto const end{snap_free_end(Ground, mouse, nullptr, 1, {})}; + if (end.track != nullptr) + { + intent.snap = end; + intent.position = end.position; + return make(kind::turntable_place, STR("Click: a turntable at the end of this track")); + } + } + return make(kind::turntable_place, STR("Press: the centre of a new turntable, drag: the direction of its bridge")); +} + +void editor_mode::render_turntable_ui() +{ + auto &tool{m_turntable}; + scan_turntables(); + auto *selected{selected_track()}; + if (tool.table != selected) + turntable_read(selected); + ImVec4 const warncolour{1.f, 0.75f, 0.3f, 1.f}; + + if (tool.table != nullptr) + { + auto &table{*tool.table}; + ImGui::Text(STR_C("Turntable %s, bridge %.1f m"), table.name().c_str(), tool.length); + if (tool.kind >= 0) + { + auto const &kind{tool.templates[tool.kind]}; + ImGui::TextDisabled("%s", kind.name.c_str()); + ImGui::SameLine(); + ImGui::TextDisabled(kind.angles > 0 ? STR_C("(positions set by the tracks around, up to %d)") : STR_C("(%d positions fixed by the template)"), kind.angles > 0 ? kind.positions : static_cast(kind.fixed.size())); + } + else + ImGui::TextColored(warncolour, "%s", STR_C("No turntable template takes this path as its track: the positions aren't known")); + render_track_name(table); + + auto const centre{turntable_centre()}; + auto const ends{rim_ends(&table, centre, tool.length * 0.5)}; + if (false == tool.exits.empty() && ImGui::TreeNodeEx(format(STR_C("Positions (%d)"), static_cast(tool.exits.size())).c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + { + for (std::size_t i = 0; i < tool.exits.size(); ++i) + { + auto const heading{tool.table_yaw + tool.exits[i]}; + auto const occupied = [&](double const Heading) { return std::any_of(ends.begin(), ends.end(), [&](rim_end const &End) { return std::abs(circular(End.heading - Heading)) < 0.05; }); }; + auto const front{occupied(heading)}; + auto const back{occupied(heading + 180.0)}; + ImGui::Text("%2d %7.2f deg", static_cast(i + 1), tool.exits[i]); + ImGui::SameLine(); + if (front || back) + ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.f), "%s%s%s", front ? STR_C("track") : "", front && back ? " + " : "", back ? STR_C("track on the other end") : ""); + else + ImGui::TextDisabled("%s", STR_C("no track")); + } + ImGui::TreePop(); + } + + ImGui::Separator(); + ImGui::TextUnformatted(STR_C("Tracks around")); + ImGui::PushItemWidth(110.f); + ImGui::InputFloat(STR_C("Reach from the rim, m"), &tool.reach, 10.f, 50.f, "%.0f"); + tool.reach = std::clamp(tool.reach, 5.f, 500.f); + ImGui::InputFloat(STR_C("Radius of the curves, m"), &tool.radius, 10.f, 50.f, "%.0f"); + tool.radius = std::clamp(tool.radius, static_cast(kMinimumRadius), 2000.f); + if (tool.kind < 0 || tool.templates[tool.kind].angles > 0) + { + ImGui::InputFloat(STR_C("Angle step of a new track, deg (0: free)"), &tool.step, 0.5f, 2.5f, "%.2f"); + tool.step = std::clamp(tool.step, 0.f, 45.f); + } + ImGui::PopItemWidth(); + ImGui::TextDisabled("%s", STR_C("Click around the turntable: a track out of it, as long as the cursor is far.\nClick a free end near it: that track is led to the rim, straight or with a curve")); + if (ImGui::Button(STR_C("Find the free ends around"))) + turntable_find_fits(); + if (false == tool.fits.empty()) + { + int chosen{0}; + for (auto const &fit : tool.fits) + chosen += fit.chosen && fit.issue.empty() ? 1 : 0; + ImGui::SameLine(); + if (ImGui::Button(format(STR_C("Lead the chosen to the turntable (%d)"), chosen).c_str())) + { + std::vector fits; + for (auto const &fit : tool.fits) + if (fit.chosen && fit.issue.empty()) + fits.push_back(&fit); + turntable_build(fits); + turntable_find_fits(); + } + tool.hovered = -1; + ImGui::BeginChild("##turntablefits", ImVec2(0.f, 180.f), true); + for (int i = 0; i < static_cast(tool.fits.size()); ++i) + { + auto &fit{tool.fits[i]}; + ImGui::PushID(i); + if (fit.issue.empty()) + ImGui::Checkbox("##chosen", &fit.chosen); + else + ImGui::TextDisabled(" - "); + ImGui::SameLine(); + auto const name{fit.end.track->name().empty() ? std::string{"(noname)"} : fit.end.track->name()}; + if (ImGui::Selectable(name.c_str(), false, ImGuiSelectableFlags_None, ImVec2(ImGui::CalcTextSize(name.c_str()).x, 0.f))) + focus_track(*fit.end.track, fit.end.point.path, fit.end.position); + if (ImGui::IsItemHovered()) + tool.hovered = i; + ImGui::SameLine(); + if (false == fit.issue.empty()) + ImGui::TextColored(warncolour, "%s", fit.issue.c_str()); + else if (fit.radius > 0.0) + ImGui::TextDisabled(STR_C("%.1f deg, curve R %.0f m, %.1f m"), circular(fit.heading - tool.table_yaw), fit.radius, fit.length); + else + ImGui::TextDisabled(STR_C("%.1f deg, straight %.1f m"), circular(fit.heading - tool.table_yaw), fit.length); + if (fit.replaces != nullptr) + { + ImGui::SameLine(); + ImGui::TextDisabled(STR_C("in place of its last straight %s"), fit.replaces->name().c_str()); + } + ImGui::PopID(); + } + ImGui::EndChild(); + } + } + else + { + if (selected != nullptr && selected->eType == tt_Table) + ImGui::TextColored(warncolour, "%s", STR_C("This turntable can't be read")); + ImGui::TextUnformatted(STR_C("New turntable")); + if (tool.templates.empty()) + ImGui::TextColored(warncolour, "%s", STR_C("No turntables: no template is described with category: turntable and its turntable: {length}")); + for (int i = 0; i < static_cast(tool.templates.size()); ++i) + { + auto const &kind{tool.templates[i]}; + auto const text{kind.angles > 0 ? format(STR_C("%s %.0f m, up to %d positions set by the tracks"), kind.name.c_str(), kind.length, kind.positions) + : format(STR_C("%s %.0f m, %d fixed positions"), kind.name.c_str(), kind.length, static_cast(kind.fixed.size()))}; + if (ImGui::Selectable((text + "##turntable" + std::to_string(i)).c_str(), tool.chosen == i)) + tool.chosen = i; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", kind.file.c_str()); + } + ImGui::SetNextItemWidth(160.f); + ImGui::InputTextWithHint(STR_C("Name##turntablename"), turntable_name_for_new().c_str(), tool.name, sizeof(tool.name), ImGuiInputTextFlags_CharsNoBlank); + ImGui::TextDisabled("%s", STR_C("Press where the centre goes and drag to turn the bridge.\nClick a free end of a track: the turntable lines up with it, at its end. Shift: no lining up")); + } + if (false == tool.error.empty()) + ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", tool.error.c_str()); + else if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_turntable_overlay() const +{ + using kind = track_intent::kind; + auto const &tool{m_turntable}; + screen_projection const projection; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const camera{glm::dvec3{Global.pCamera.Pos}}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->eType != tt_Table || track->m_editorremoved || track->m_paths.empty() || track == tool.table) + continue; + auto const centre{centre_of(*track)}; + if (glm::distance(centre, camera) < 1500.0) + draw_circle(projection, drawlist, centre, length_of(*track) * 0.5, IM_COL32(255, 255, 255, 90), 1.5f); + } + if (tool.table != nullptr) + { + auto const centre{turntable_centre()}; + auto const rim{tool.length * 0.5}; + draw_circle(projection, drawlist, centre, rim, overlay_color::selected, 2.5f); + auto const ends{rim_ends(tool.table, centre, rim)}; + for (auto const line : turntable_lines()) + for (auto const heading : {line, line + 180.0}) + { + auto const direction{heading_vector(heading)}; + auto const occupied{std::any_of(ends.begin(), ends.end(), [&](rim_end const &End) { return std::abs(circular(End.heading - heading)) < 0.05; })}; + glm::dvec3 const from{centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}; + glm::dvec3 const to{centre.x + direction.x * (rim + 6.0), centre.y, centre.z + direction.y * (rim + 6.0)}; + projection.line(drawlist, from, to, occupied ? IM_COL32(90, 230, 110, 230) : IM_COL32(255, 255, 255, 150), occupied ? 2.5f : 1.5f); + } + for (int i = 0; i < static_cast(tool.fits.size()); ++i) + { + auto const &fit{tool.fits[i]}; + auto const colour{false == fit.issue.empty() ? IM_COL32(255, 150, 60, 200) : fit.chosen ? IM_COL32(90, 230, 110, 230) : IM_COL32(200, 200, 200, 140)}; + draw_pieces(projection, drawlist, fit.pieces, colour, i == tool.hovered ? 4.f : 2.f); + ImVec2 at; + if (projection.project(fit.end.position, at)) + drawlist->AddCircle(at, i == tool.hovered ? 10.f : 6.f, colour, 16, 2.f); + } + } + auto const &intent{m_intent}; + switch (intent.what) + { + case kind::turntable_exit: + { + auto const centre{turntable_centre()}; + auto const direction{heading_vector(tool.heading)}; + auto const rim{tool.length * 0.5}; + glm::dvec3 const to{centre.x + direction.x * (rim + tool.exit_length), centre.y, centre.z + direction.y * (rim + tool.exit_length)}; + projection.line(drawlist, intent.position, to, overlay_color::grip, 3.f); + break; + } + case kind::turntable_fit: + { + turntable_fit fit; + auto const fits{turntable_fit_end(intent.snap, fit)}; + draw_pieces(projection, drawlist, fit.pieces, fits ? overlay_color::grip : overlay_color::invalid, 3.f); + if (false == fits) + label(projection, intent.position, fit.issue); + break; + } + case kind::turntable_place: + { + if (tool.chosen < 0 || tool.chosen >= static_cast(tool.templates.size())) + break; + auto const rim{tool.templates[tool.chosen].length * 0.5}; + auto centre{intent.position}; + auto yaw{tool.yaw}; + if (intent.snap.track != nullptr) + { + auto const into{glm::normalize(plan_of(intent.snap.direction))}; + centre = {intent.snap.position.x - into.x * rim, intent.snap.position.y, intent.snap.position.z - into.y * rim}; + yaw = heading_of(into); + } + auto const direction{heading_vector(yaw)}; + draw_circle(projection, drawlist, centre, rim, IM_COL32(255, 255, 255, 170), 1.5f); + projection.line(drawlist, {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}, {centre.x - direction.x * rim, centre.y, centre.z - direction.y * rim}, IM_COL32(255, 255, 255, 170), 2.f); + break; + } + default: break; + } + if (tool.placing && tool.chosen >= 0 && tool.chosen < static_cast(tool.templates.size())) + { + auto const rim{tool.templates[tool.chosen].length * 0.5}; + auto const cursor{cursor_level(tool.centre.y)}; + auto const &io{ImGui::GetIO()}; + auto const dragged{glm::length(glm::vec2{io.MousePos.x, io.MousePos.y} - tool.pressed) >= kDragPixels}; + auto centre{tool.centre}; + auto yaw{tool.yaw}; + if (tool.snap.track != nullptr && false == dragged) + { + auto const into{glm::normalize(plan_of(tool.snap.direction))}; + centre = {tool.snap.position.x - into.x * rim, tool.snap.position.y, tool.snap.position.z - into.y * rim}; + yaw = heading_of(into); + } + else if (dragged && plan_distance(cursor, centre) > 0.5) + yaw = std::round(heading_of(plan_of(cursor - centre)) * 10.0) / 10.0; + auto const direction{heading_vector(yaw)}; + draw_circle(projection, drawlist, centre, rim, overlay_color::marked, 2.5f); + projection.line(drawlist, {centre.x + direction.x * rim, centre.y, centre.z + direction.y * rim}, {centre.x - direction.x * rim, centre.y, centre.z - direction.y * rim}, overlay_color::marked, 3.f); + if (dragged) + label(projection, cursor, format("%.1f deg", yaw)); + } +} + +bool editor_mode::turntable_controls(std::vector &Rootstatements) +{ + if (scene::Layers.empty()) + return false; + std::string root{scene::Layers.layer(scene::layer_handle{1}).name}; + if (auto const slash{root.find_last_of("/\\")}; slash != std::string::npos) + root.erase(0, slash + 1); + if (auto const dot{root.rfind('.')}; dot != std::string::npos) + root.erase(dot); + if (root.empty()) + return false; + auto const file{root + "_obrotnice.scm"}; + std::ostringstream text; + auto tables{0}; + for (auto const *table : simulation::Paths.sequence()) + { + if (table == nullptr || table->eType != tt_Table || table->m_editorremoved || table->m_paths.empty() || table->name().empty()) + continue; + auto const &name{table->name()}; + std::vector slots; + std::string events; + scan_turntables(); + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count() && slots.empty(); ++handle) + { + if (false == scene::Layers.tracked(handle)) + continue; + auto const &included{scene::Layers.instance(handle)}; + if (included.removed || included.dead || included.file == nullptr) + continue; + auto const kind{std::find_if(m_turntable.templates.begin(), m_turntable.templates.end(), [&](turntable_template const &Template) { return normalized(Template.file) == normalized(*included.file); })}; + if (kind == m_turntable.templates.end() || kind->angles == 0) + continue; + std::string target; + std::vector values; + if (false == editor_includes::parse_directive(scene::Layers.directive(handle), target, values) || kind->track > static_cast(values.size()) || ToLower(values[kind->track - 1]) != name) + continue; + { + std::ifstream input{Global.asCurrentSceneryPath + kind->file, std::ios_base::binary}; + events.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + events = ToLower(events); + std::replace_if(events.begin(), events.end(), [](char const Character) { return Character == '\t' || Character == '\r' || Character == '\n'; }, ' '); + auto const parameter{"(p" + std::to_string(kind->track) + ")"}; + for (auto at{events.find(parameter)}; at != std::string::npos; at = events.find(parameter, at + name.size())) + events.replace(at, parameter.size(), name); + } + std::vector seen; + for (int i = 0; i < kind->positions && kind->angles - 1 + i < static_cast(values.size()); ++i) + if (double angle; numeric(values[kind->angles - 1 + i], angle) && std::none_of(seen.begin(), seen.end(), [&](double const Other) { return std::abs(circular(Other - angle)) < 0.01; })) + { + seen.push_back(angle); + slots.push_back(i + 1); + } + } + if (slots.empty()) + for (int position = 1; position <= 64; ++position) + slots.push_back(position); + std::vector positions; + for (auto const position : slots) + { + // the events of a turntable placed in this session get loaded only with the scenery loaded again + auto const event{name + "_tor" + std::to_string(position)}; + if (events.find(" " + event + " ") != std::string::npos || simulation::Events.FindEvent(event) != nullptr) + positions.push_back(position); + } + if (positions.size() < 2) + continue; + ++tables; + auto const count{positions.size()}; + auto const centre{centre_of(*table)}; + auto const at{format("%.3f %.3f %.3f", centre.x, centre.y, centre.z)}; + text << "node -1 0 " << name << "_keylock memcell " << at << " none 0 0 none endmemcell\r\n"; + text << "event " << name << "_lock updatevalues 0 " << name << "_keylock * 1 * endevent\r\n"; + text << "event " << name << "_unlock updatevalues 1.5 " << name << "_keylock * 0 * endevent\r\n"; + for (auto const forward : {true, false}) + { + auto const step{std::string{forward ? "_next" : "_prev"}}; + text << "event " << name << step << " multiple 0 " << name << "_keylock " << name << "_lock " << name << "_unlock " << name << step << '0'; + for (auto const position : positions) + text << ' ' << name << step << position; + text << " condition memcompare * 0 * endevent\r\n"; + // the turntable starts with no position set, the first step takes it to the first one + text << "event " << name << step << "0 multiple 0 " << name << ' ' << name << "_tor" << positions.front() << " condition memcompare * 0 * endevent\r\n"; + for (std::size_t i = 0; i < count; ++i) + { + auto const target{positions[forward ? (i + 1) % count : (i + count - 1) % count]}; + text << "event " << name << step << positions[i] << " multiple 0 " << name << ' ' << name << "_tor" << target << " condition memcompare * " << positions[i] << " * endevent\r\n"; + } + } + text << "node -1 0 " << name << "_key eventlauncher " << at << " 150 t 0 " << name << "_next " << name << "_prev end\r\n"; + } + auto const path{Global.asCurrentSceneryPath + file}; + std::error_code error; + auto const exists{std::filesystem::exists(path, error)}; + if (tables == 0 && false == exists) + return false; + auto const content{"// made by the scenery editor on each save: T turns each turntable to its next position, Shift+T to the previous one\r\n" + text.str()}; + std::string current; + { + std::ifstream input{path, std::ios_base::binary}; + current.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + } + if (current != content) + { + std::ofstream output{path, std::ios_base::binary | std::ios_base::trunc}; + output << content; + } + auto included{m_turntable_included}; + for (std::size_t handle = 1; handle <= scene::Layers.size() && false == included; ++handle) + { + auto name{scene::Layers.layer(static_cast(handle)).name}; + std::replace(name.begin(), name.end(), '\\', '/'); + included = ToLower(name) == ToLower(file) || ToLower(name).ends_with("/" + ToLower(file)); + } + if (included) + return false; + Rootstatements.push_back("include " + file + " end"); + return true; +} diff --git a/application/editormodevehicle.cpp b/application/editormodevehicle.cpp new file mode 100644 index 00000000..58562caa --- /dev/null +++ b/application/editormodevehicle.cpp @@ -0,0 +1,320 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "application/editorprojection.h" +#include "application/application.h" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "launcher/textures_scanner.h" + +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "vehicle/DynObj.h" +#include "world/Track.h" + +#include "imgui/imgui.h" +#include "utilities/translation.h" +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; + +std::size_t const kVehicleList{300}; +double const kHalfVehicle{8.0}; // m, the vehicle is placed about centred on the point + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + return Text; +} + +std::string skin_label(ui::vehicle_desc const &Vehicle, ui::skin_set const &Skin) +{ + auto label{Skin.meta != nullptr && false == Skin.meta->name.empty() ? Skin.meta->name : Skin.skin}; + return label + " (" + Vehicle.path.generic_string() + ")"; +} + +int path_under(TTrack const &Track, glm::dvec3 const &Point) +{ + int result{0}; + auto best{std::numeric_limits::max()}; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + bezier const curve{Track.m_paths[i]}; + for (int k = 0; k <= 64; ++k) + { + auto const distance{geometry::plan_distance(curve.point(k / 64.0), Point)}; + if (distance < best) + { + best = distance; + result = i; + } + } + } + return result; +} + +double nearest_on(TTrack const &Track, int const Path, glm::dvec3 const &Point) +{ + bezier const curve{Track.m_paths[Path]}; + auto parameter{0.0}; + auto best{std::numeric_limits::max()}; + for (int k = 0; k <= 256; ++k) + { + auto const distance{geometry::plan_distance(curve.point(k / 256.0), Point)}; + if (distance < best) + { + best = distance; + parameter = k / 256.0; + } + } + return parameter; +} + +glm::dvec2 heading_at(TTrack const &Track, int const Path, glm::dvec3 const &Point) +{ + auto const tangent{bezier{Track.m_paths[Path]}.first(nearest_on(Track, Path, Point))}; + glm::dvec2 const planar{tangent.x, tangent.z}; + return glm::length(planar) > 1e-9 ? glm::normalize(planar) : glm::dvec2{0.0, 1.0}; +} + +// distance along the path of the track from its start to the point nearest to the one given +double distance_along(TTrack const &Track, int const Path, glm::dvec3 const &Point) +{ + bezier const curve{Track.m_paths[Path]}; + auto const parameter{nearest_on(Track, Path, Point)}; + int const count{64}; + double length{0.0}, total{0.0}; + auto previous{curve.point(0.0)}; + for (int k = 1; k <= count; ++k) + { + auto const t{static_cast(k) / count}; + auto const next{curve.point(t)}; + auto const step{glm::distance(previous, next)}; + total += step; + if (t <= parameter) + length += step; + else if (t - 1.0 / count < parameter) + length += step * (parameter - (t - 1.0 / count)) * count; + previous = next; + } + return std::clamp(length * Track.Length() / std::max(total, 1e-6), 0.0, Track.Length()); +} + +} // namespace + +void editor_mode::vehicle_start(TTrack &Track, glm::dvec3 const &Point) +{ + m_vehicle.track = &Track; + m_vehicle.point = Point; + m_vehicle.expand = true; + m_vehicle.status.clear(); + auto const heading{heading_at(Track, Track.eType == tt_Switch ? path_under(Track, Point) : 0, Point)}; + glm::dvec2 const away{Point.x - Global.pCamera.Pos.x, Point.z - Global.pCamera.Pos.z}; + m_vehicle.facing = glm::dot(heading, away) >= 0.0 ? 1 : -1; +} + +bool editor_mode::vehicle_place() +{ + auto &tool{m_vehicle}; + if (tool.track == nullptr || tool.track->m_editorremoved) + return false; + auto train{tool.consist}; + if (train.empty()) + { + if (tool.vehicle == nullptr || tool.skin == nullptr) + return false; + train.push_back({tool.vehicle, tool.skin, false}); + } + auto const path{tool.track->eType == tt_Switch ? path_under(*tool.track, tool.point) : 0}; + if (tool.track->eType == tt_Switch && tool.track->GetSwitchState() != path) + tool.track->Switch(path); + auto const along{distance_along(*tool.track, path, tool.point)}; + auto const against{tool.facing < 0}; + std::vector names; + std::string nodes; + std::string head; + for (std::size_t i = 0; i < train.size(); ++i) + { + auto const &item{train[i]}; + auto const stem{item.vehicle->path.stem().generic_string()}; + std::string name; + for (int index = 1; index < 10000; ++index) + { + name = lower("editor_" + stem + "_" + std::to_string(index)); + if (simulation::Vehicles.find(name) == nullptr && std::find(names.begin(), names.end(), name) == names.end()) + break; + } + names.push_back(name); + auto const leads{i == 0}; + if (leads) + head = name; + nodes += "node -1 0 " + name + " dynamic " + item.vehicle->path.parent_path().generic_string() + " " + item.skin->skin + " " + stem + " " + (item.turned ? "-1" : "0") + " " + (leads && tool.driver == 0 ? "headdriver" : "nobody") + " 3 0 enddynamic\n"; + } + auto const offset{against ? along - kHalfVehicle : along + kHalfVehicle}; + auto const vehicles{simulation::State.insert_trainset(head, tool.track, offset, nodes, against)}; + if (vehicles.empty()) + { + tool.status = STR_C("The vehicle couldn't be placed, the log says why"); + return false; + } + tool.placed = head; + tool.status = format(STR_C("%s and %d vehicle(s) behind it stand on %s. It isn't saved with the scenery"), head.c_str(), static_cast(vehicles.size()) - 1, tool.track->name().empty() ? "(noname)" : tool.track->name().c_str()); + WriteLog("Editor: " + tool.status, logtype::generic); + return true; +} + +void editor_mode::render_vehicle_ui() +{ + auto &tool{m_vehicle}; + auto *selected{selected_track()}; + if (selected != nullptr && selected == tool.track && m_hover.track == selected && ImGui::IsMouseClicked(0) && false == ImGui::GetIO().WantCaptureMouse) + vehicle_start(*selected, m_hover.point); + else if (selected != nullptr && selected != tool.track) + vehicle_start(*selected, m_hover.track == selected ? m_hover.point : bezier{selected->m_paths.front()}.point(0.5)); + if (tool.track == nullptr) + { + ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", STR_C("LMB on a track: where the vehicle stands")); + return; + } + if (tool.bank == nullptr) + { + tool.bank = std::make_shared(); + tool.bank->scan_textures(); + } + ImGui::TextDisabled("%s", STR_C("A vehicle to drive, placed where RMB on the track was clicked, or in the middle of the path.\nIt isn't written to the scenery files")); + ImGui::SetNextItemWidth(-1.0f); + ImGui::InputTextWithHint("##vehiclesearch", STR_C("search: name, series, skin"), tool.search, sizeof(tool.search)); + ImGui::Checkbox(STR_C("Only the ones with a cab"), &tool.controllable_only); + std::string const text{lower(tool.search)}; + std::vector, std::shared_ptr>> listed; + std::size_t total{0}; + for (auto const &[path, vehicle] : tool.bank->vehicles) + { + if (vehicle == nullptr || (tool.controllable_only && false == vehicle->controllable)) + continue; + for (auto const &skin : vehicle->matching_skinsets) + { + if (skin == nullptr) + continue; + if (false == text.empty() && lower(skin_label(*vehicle, *skin)).find(text) == std::string::npos && (skin->meta == nullptr || skin->meta->search_lowered.find(text) == std::string::npos)) + continue; + ++total; + if (listed.size() < kVehicleList) + listed.emplace_back(vehicle, skin); + } + } + ImGui::BeginChild("##vehicles", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 8.0f + 6.0f), true); + for (std::size_t i = 0; i < listed.size(); ++i) + { + auto const &[vehicle, skin] = listed[i]; + if (ImGui::Selectable(format("%s##vehicle%zu", skin_label(*vehicle, *skin).c_str(), i).c_str(), tool.skin == skin)) + { + tool.vehicle = vehicle; + tool.skin = skin; + } + } + ImGui::EndChild(); + if (total > listed.size()) + ImGui::TextDisabled(STR_C("%zu found, the first %zu listed"), total, listed.size()); + if (tool.vehicle != nullptr && tool.skin != nullptr && ImGui::Button(STR_C("Add to the train"))) + tool.consist.push_back({tool.vehicle, tool.skin, false}); + if (false == tool.consist.empty()) + { + ImGui::SameLine(); + ImGui::TextDisabled(STR_C("Train: %d vehicle(s), the first one leads"), static_cast(tool.consist.size())); + std::optional remove; + std::optional raise; + for (std::size_t i = 0; i < tool.consist.size(); ++i) + { + auto &item{tool.consist[i]}; + auto const vehicle{item.skin->vehicle.lock()}; + ImGui::PushID(static_cast(i)); + ImGui::Checkbox("##turned", &item.turned); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Turned around in the train")); + ImGui::SameLine(); + ImGui::Text("%2d. %s", static_cast(i + 1), vehicle != nullptr ? skin_label(*vehicle, *item.skin).c_str() : item.skin->skin.c_str()); + ImGui::SameLine(); + if (i > 0 && ImGui::SmallButton(STR_C("up"))) + raise = i; + ImGui::SameLine(); + if (ImGui::SmallButton("x")) + remove = i; + ImGui::PopID(); + } + if (raise) + std::swap(tool.consist[*raise], tool.consist[*raise - 1]); + if (remove) + tool.consist.erase(tool.consist.begin() + static_cast(*remove)); + if (ImGui::SmallButton(STR_C("Empty the train"))) + tool.consist.clear(); + } + if (ImGui::Button(STR_C("Turn the train around"))) + tool.facing = -tool.facing; + ImGui::SameLine(); + ImGui::TextDisabled("%s", STR_C("the head goes where the arrow at the marker points")); + ImGui::RadioButton(STR_C("With a driver, to drive"), &tool.driver, 0); + ImGui::SameLine(); + ImGui::RadioButton(STR_C("Nobody, parked"), &tool.driver, 1); + if (tool.consist.empty() && (tool.vehicle == nullptr || tool.skin == nullptr)) + ImGui::TextDisabled("%s", STR_C("Pick a vehicle from the list")); + else + { + if (ImGui::Button(STR_C("Place"))) + vehicle_place(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Place and drive")) && vehicle_place()) + { + Global.editor_enter_vehicle = tool.placed; + tool.leave = true; + } + } + if (false == tool.status.empty()) + ImGui::TextWrapped("%s", tool.status.c_str()); +} + +void editor_mode::draw_vehicle_marker() const +{ + auto const &tool{m_vehicle}; + if (false == tool.open || tool.track == nullptr) + return; + screen_projection const projection; + ImVec2 screen; + if (false == projection.project(tool.point, screen)) + return; + auto *drawlist{ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())}; + auto const colour{IM_COL32(255, 220, 60, 235)}; + drawlist->AddCircle(screen, 9.0f, colour, 16, 2.5f); + if (tool.track->m_editorremoved || tool.track->m_paths.empty()) + return; + auto const heading{heading_at(*tool.track, tool.track->eType == tt_Switch ? path_under(*tool.track, tool.point) : 0, tool.point) * static_cast(tool.facing)}; + glm::dvec3 const tip{tool.point.x + heading.x * 8.0, tool.point.y, tool.point.z + heading.y * 8.0}; + ImVec2 end; + if (false == projection.project(tip, end)) + return; + drawlist->AddLine(screen, end, colour, 3.0f); + auto const dx{end.x - screen.x}; + auto const dy{end.y - screen.y}; + auto const size{std::sqrt(dx * dx + dy * dy)}; + if (size < 1.0f) + return; + ImVec2 const unit{dx / size, dy / size}; + ImVec2 const side{-unit.y, unit.x}; + drawlist->AddTriangleFilled(ImVec2(end.x + unit.x * 10.0f, end.y + unit.y * 10.0f), ImVec2(end.x + side.x * 7.0f, end.y + side.y * 7.0f), ImVec2(end.x - side.x * 7.0f, end.y - side.y * 7.0f), colour); +} diff --git a/application/editormodeviewport.cpp b/application/editormodeviewport.cpp new file mode 100644 index 00000000..817658e3 --- /dev/null +++ b/application/editormodeviewport.cpp @@ -0,0 +1,541 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +// over the 3d view: the tools of the field of work along its left edge, the modes of the gizmo at its top, the axes of the +// world and the top view in its top right corner. the same tools in the toolbar under the menu, with their names + +#include "stdafx.h" +#include "application/editormode.h" +#include "application/editoruilayer.h" +#include "editor/editorIcons.hpp" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/translation.h" +#include "imgui/imgui.h" + +#include +#include +#include +#include +#include + +struct editor_tool_button +{ + editor_icons::icon picture; // icon::count if it has none: then it's only in the toolbar, as its name + char const *label; + char const *key; // nullptr if there's none + char const *tooltip; // nullptr if the label says enough + bool chosen; + std::function action; + bool group{false}; // a gap before it, it starts another group + bool enabled{true}; +}; + +namespace +{ + +using editor_icons::icon; +using tool_button = editor_tool_button; + +float scale() +{ + return std::max(1.0f, Global.ui_scale); +} + +// side of a button, the icon with some room around it +float button_side() +{ + return editor_icons::size() + 10.0f * scale(); +} + +// a window of no looks of its own, for the buttons over the 3d view; it takes the mouse only where it lies +bool begin_overlay(char const *Name, ImVec2 const &Position, ImVec2 const &Size) +{ + ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID); + ImGui::SetNextWindowPos(Position); + ImGui::SetNextWindowSize(Size); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(1.0f, 1.0f)); + auto const flags{ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | + ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoScrollWithMouse}; + auto const open{ImGui::Begin(Name, nullptr, flags)}; + ImGui::PopStyleVar(3); + return open; +} + +// the name of the tool with its key, and what it does +void button_tooltip(tool_button const &Button) +{ + std::string tip{Button.label}; + if (Button.key != nullptr) + tip += std::string{" ["} + Button.key + "]"; + if (Button.tooltip != nullptr) + tip += std::string{"\n"} + Button.tooltip; + ImGui::SetTooltip("%s", tip.c_str()); +} + +// the first letters of the label in place of the icon, if the renderer can't show images +void icon_or_letters(ImDrawList *List, tool_button const &Button, ImVec2 const &Center, ImU32 const Color) +{ + if (editor_icons::draw(List, Button.picture, Center, Color)) + return; + char const text[]{Button.label[0], (Button.label[0] != '\0' ? Button.label[1] : '\0'), '\0'}; + auto const size{ImGui::CalcTextSize(text)}; + List->AddText(ImVec2(Center.x - size.x * 0.5f, Center.y - size.y * 0.5f), Color, text); +} + +// the button with the icon at the place; the label stands in for the icon if the renderer can't show images +void icon_button(tool_button const &Button, ImVec2 const &Position, int const Id) +{ + auto const side{button_side()}; + ImGui::SetCursorScreenPos(Position); + ImGui::PushID(Id); + auto const clicked{ImGui::InvisibleButton("##tool", ImVec2(side, side))}; + ImGui::PopID(); + auto const hovered{ImGui::IsItemHovered()}; + auto *list{ImGui::GetWindowDrawList()}; + ImVec2 const end{Position.x + side, Position.y + side}; + auto const rounding{4.0f * scale()}; + auto const background{Button.chosen ? ImGui::GetColorU32(ImGuiCol_ButtonActive) : + hovered && Button.enabled ? ImGui::GetColorU32(ImGuiCol_ButtonHovered) : + IM_COL32(24, 26, 30, 200)}; + list->AddRectFilled(Position, end, background, rounding); + if (Button.chosen) + list->AddRect(Position, end, ImGui::GetColorU32(ImGuiCol_CheckMark), rounding, 0, 1.5f * scale()); + auto const color{Button.enabled ? (Button.chosen || hovered ? IM_COL32(255, 255, 255, 255) : IM_COL32(215, 220, 225, 235)) : IM_COL32(150, 150, 150, 110)}; + icon_or_letters(list, Button, ImVec2(Position.x + side * 0.5f, Position.y + side * 0.5f), color); + if (hovered) + button_tooltip(Button); + if (clicked && Button.enabled && Button.action) + Button.action(); +} + +} // namespace + +void editor_mode::render_viewport_overlays() +{ + auto const width{editor_ui::view_max().x - editor_ui::view_min().x}; + auto const height{editor_ui::view_max().y - editor_ui::view_min().y}; + // a view squeezed between the docks has no room for them + if (width < 8.0f * button_side() || height < 6.0f * button_side()) + return; + render_viewport_tools(); + render_viewport_gizmo(); + render_view_axes(); +} + +void editor_mode::work_area_tools(std::vector &Buttons) +{ + switch (current_work_area()) + { + case work_area::surroundings: + { + struct mode + { + icon picture; + char const *label; + char const *key; + nodebank_panel::edit_mode which; + }; + mode const modes[] = {{icon::select, STR_C("Select"), "1", nodebank_panel::MODIFY}, + {icon::insert, STR_C("Insert"), "2", nodebank_panel::ADD}, + {icon::brush, STR_C("Brush"), "3", nodebank_panel::BRUSH}, + {icon::area_fill, STR_C("Area fill"), "4", nodebank_panel::FILL}, + {icon::copy_to_bank, STR_C("Copy to bank"), "5", nodebank_panel::COPY}}; + for (auto const &entry : modes) + Buttons.push_back({entry.picture, entry.label, entry.key, nullptr, ui()->mode() == entry.which, [this, which = entry.which]() { choose_edit_mode(which); }}); + break; + } + case work_area::tracks: + { + auto const vehicle{m_track_tab == track_tab::lineside && m_vehicle.open}; + auto const add = [&](track_tab const Tab, icon const Picture, bool const Group) { + auto const &modes{track_modes()}; + auto const found{std::find_if(modes.begin(), modes.end(), [&](track_mode const &Mode) { return Mode.tab == Tab; })}; + if (found == modes.end()) + return; + // a tool chosen again gives way to the select one + auto const chosen{m_track_tab == Tab && false == (Tab == track_tab::lineside && vehicle)}; + Buttons.push_back({Picture, STR_C(found->label), found->key, STR_C(found->tooltip), chosen, [this, Tab, chosen]() { show_track_tab(chosen ? track_tab::path : Tab); }, Group}); + }; + add(track_tab::path, icon::select, false); + add(track_tab::lay, icon::lay_track, false); + add(track_tab::turnout, icon::turnout, false); + add(track_tab::straights, icon::straight, false); + add(track_tab::route, icon::curve, false); + add(track_tab::turntable, icon::turntable, false); + add(track_tab::signals, icon::signal, true); + add(track_tab::lineside, icon::objects, false); + Buttons.push_back({icon::vehicle, STR_C("Vehicle"), nullptr, STR_C("A vehicle to drive, put on the selected track; it isn't written to the scenery files"), vehicle, [this, vehicle]() { + show_track_tab(vehicle ? track_tab::path : track_tab::lineside); + m_vehicle.expand = false == vehicle; + }}); + add(track_tab::profile, icon::profile, true); + add(track_tab::speed, icon::speed, false); + add(track_tab::joints, icon::joints, false); + add(track_tab::infra, icon::infra, false); + Buttons.push_back({icon::gauge, STR_C("Structure gauge"), nullptr, STR_C("Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)"), m_gauge.open, + [this]() { m_gauge.open = !m_gauge.open; }}); + break; + } + case work_area::roads: + { + auto &tool{m_roadtool}; + Buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB: a road piece, a junction, a point where pieces meet, a level crossing or a traffic point"), tool.tool == 0, [this]() { road_choose_tool(0); }}); + Buttons.push_back({icon::road, STR_C("Build"), nullptr, STR_C("LMB: start a road, then each next point; on a loose end, the side of a road or a junction it's joined to them"), tool.tool == 1, [this]() { road_choose_tool(1); }}); + Buttons.push_back({icon::place, STR_C("Place"), nullptr, STR_C("Level crossings, and the points where vehicles appear on the roads or are taken off them"), tool.tool == 2, [this]() { road_choose_tool(2); }}); + if (tool.tool == 2) + { + // what the place tool puts + char const *const kinds[] = {STR_C("Level crossing"), STR_C("Spawn point"), STR_C("Removal point"), STR_C("Pedestrian crossing")}; + for (int index = 0; index < static_cast(std::size(kinds)); ++index) + Buttons.push_back({icon::count, kinds[index], nullptr, nullptr, tool.placekind == index, [&tool, index]() { tool.placekind = index; }, index == 0}); + } + Buttons.push_back({icon::lanes, STR_C("Show lanes"), nullptr, STR_C("The lanes of the roads drawn as the paths the vehicles take"), tool.lanes, [&tool]() { tool.lanes = !tool.lanes; }, true}); + break; + } + case work_area::terrain: + { + // what the left button does on the terrain + auto const choose = [this](terrain_tool const Tool) { return [this, Tool]() { m_terrain_tool = Tool; }; }; + auto const chosen = [this](terrain_tool const Tool) { return m_terrain_tool == Tool; }; + Buttons.push_back({icon::select, STR_C("Select"), nullptr, STR_C("LMB picks the models, as in the surroundings"), chosen(terrain_tool::none), choose(terrain_tool::none)}); + Buttons.push_back({icon::sculpt, STR_C("Sculpt"), nullptr, STR_C("LMB raises the terrain under the brush, Shift+LMB lowers it"), chosen(terrain_tool::sculpt), choose(terrain_tool::sculpt)}); + Buttons.push_back({icon::smooth, STR_C("Smooth"), nullptr, STR_C("LMB evens the terrain out under the brush"), chosen(terrain_tool::smooth), choose(terrain_tool::smooth)}); + Buttons.push_back({icon::profile, STR_C("Level"), nullptr, STR_C("LMB leads the terrain under the brush to the target height, Ctrl+LMB takes the target height from what's under the cursor"), + chosen(terrain_tool::level), choose(terrain_tool::level)}); + Buttons.push_back({icon::brush, STR_C("Paint"), nullptr, STR_C("LMB paints the material chosen in the palette, Shift+LMB the first material of the palette"), chosen(terrain_tool::paint), choose(terrain_tool::paint)}); + Buttons.push_back({icon::chunks, STR_C("Chunks"), nullptr, STR_C("LMB adds a chunk next to the clicked one, Shift+LMB removes it"), chosen(terrain_tool::chunks), choose(terrain_tool::chunks)}); + Buttons.push_back({icon::count, STR_C("Point spacing"), nullptr, STR_C("LMB selects the clicked chunk, Ctrl+LMB adds one or takes it away; the tool options convert the point spacing of the selected"), + chosen(terrain_tool::spacing), choose(terrain_tool::spacing)}); + Buttons.push_back({icon::area_fill, STR_C("Water"), nullptr, STR_C("LMB adds a point of the outline of a body of water, Shift+LMB takes the last one back"), chosen(terrain_tool::water), choose(terrain_tool::water)}); + Buttons.push_back({icon::count, STR_C("Fill"), nullptr, STR_C("LMB adds a point of the outline of an area to fill with a material, Shift+LMB takes the last one back"), chosen(terrain_tool::fill), + choose(terrain_tool::fill)}); + Buttons.push_back({icon::joints, STR_C("Restore"), nullptr, STR_C("LMB takes back the touch-ups made over the tracks: the terrain returns to the shape the tracks give it"), chosen(terrain_tool::restore), + choose(terrain_tool::restore)}); + auto const orthophoto{m_orthophoto.enabled()}; + Buttons.push_back({icon::orthophoto, STR_C("Orthophoto"), nullptr, STR_C("Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery"), orthophoto, + [this, orthophoto]() { m_orthophoto.enabled(false == orthophoto); }, true}); + Buttons.push_back({icon::count, STR_C("Orthophoto settings..."), nullptr, nullptr, m_orthophoto_window, [this]() { m_orthophoto_window = !m_orthophoto_window; }}); + break; + } + } +} + +void editor_mode::render_viewport_tools() +{ + // the tools of the field of work which have icons, as in the toolbar + std::vector buttons; + buttons.reserve(24); + work_area_tools(buttons); + std::erase_if(buttons, [](tool_button const &Button) { return Button.picture == icon::count; }); + if (buttons.empty()) + return; + + // down the left edge, in more columns if the view is too low for them; the bottom is left to the hints of the tools + auto const side{button_side()}; + auto const gap{2.0f * scale()}; + auto const groupgap{8.0f * scale()}; + auto const margin{8.0f * scale()}; + ImVec2 const origin{editor_ui::view_min().x + margin, editor_ui::view_min().y + margin}; + auto const bottom{std::max(origin.y + 4.0f * side, editor_ui::view_max().y - 130.0f * scale())}; + std::vector places; + places.reserve(buttons.size()); + ImVec2 place{origin}; + for (auto const &button : buttons) + { + if (button.group && place.y > origin.y) + place.y += groupgap - gap; + if (place.y > origin.y && place.y + side > bottom) + place = ImVec2(place.x + side + gap, origin.y); + places.push_back(place); + place.y += side + gap; + } + ImVec2 extent{0.0f, 0.0f}; + for (auto const &at : places) + extent = ImVec2(std::max(extent.x, at.x + side - origin.x), std::max(extent.y, at.y + side - origin.y)); + auto const pad{3.0f * scale()}; + if (begin_overlay("##viewporttools", ImVec2(origin.x - pad, origin.y - pad), ImVec2(extent.x + 2.0f * pad, extent.y + 2.0f * pad))) + { + ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(origin.x - pad, origin.y - pad), ImVec2(origin.x + extent.x + pad, origin.y + extent.y + pad), IM_COL32(16, 18, 21, 140), 6.0f * scale()); + for (std::size_t index = 0; index < buttons.size(); ++index) + icon_button(buttons[index], places[index], static_cast(index)); + } + ImGui::End(); +} + +void editor_mode::render_toolbar_caption(char const *Text) +{ + // as wide in both rows, so the buttons of the rows start one under the other. the rows are drawn with no spacing of items + auto const margin{std::round(ImGui::GetFontSize() * 0.75f)}; + auto const width{std::max(ImGui::CalcTextSize(STR_C("Work modes")).x, ImGui::CalcTextSize(STR_C("Tools")).x) + 2.0f * margin}; + auto const position{ImGui::GetWindowPos()}; + auto const height{ImGui::GetWindowHeight()}; + auto *list{ImGui::GetWindowDrawList()}; + auto const text{ImGui::CalcTextSize(Text)}; + list->AddText(ImVec2(position.x + margin, std::round(position.y + (height - text.y) * 0.5f)), ImGui::GetColorU32(ImGuiCol_TextDisabled), Text); + list->AddLine(ImVec2(position.x + width, position.y + height * 0.2f), ImVec2(position.x + width, position.y + height * 0.8f), ImGui::GetColorU32(ImGuiCol_Separator)); + ImGui::SetCursorScreenPos(position); + ImGui::Dummy(ImVec2(width + margin * 0.5f, height)); + ImGui::SameLine(0.0f, 0.0f); +} + +void editor_mode::render_toolbar_tools() +{ + render_toolbar_caption(STR_C("Tools")); + std::vector buttons; + buttons.reserve(24); + work_area_tools(buttons); + if (buttons.empty()) + return; + + auto const s{scale()}; + auto const iconside{editor_icons::size()}; + auto const height{ImGui::GetWindowHeight()}; + auto const margin{3.0f * s}; // over and under the buttons + auto const pad{7.0f * s}; // inside the button, at its ends + auto const spacing{5.0f * s}; // between the icon and the name + auto const gap{3.0f * s}; + auto const groupgap{15.0f * s}; + auto const buttonheight{height - 2.0f * margin}; + auto const left{ImGui::GetCursorScreenPos().x}; + auto const top{ImGui::GetWindowPos().y + margin}; + auto const right{ImGui::GetWindowPos().x + ImGui::GetWindowWidth() - 4.0f * s}; + // the names next to the icons if there's room for them; if there isn't, the name of the chosen tool only, or none + enum class names + { + all, + chosen, + none + }; + auto const named = [](tool_button const &Button, names const Names) { + return Button.picture == icon::count || Names == names::all || (Names == names::chosen && Button.chosen); + }; + auto const width = [&](tool_button const &Button, names const Names) { + if (false == named(Button, Names)) + return std::max(buttonheight, iconside + pad); + auto result{2.0f * pad + ImGui::CalcTextSize(Button.label).x}; + if (Button.picture != icon::count) + result += iconside + spacing; + return result; + }; + auto const total = [&](names const Names) { + auto result{0.0f}; + for (std::size_t index = 0; index < buttons.size(); ++index) + result += width(buttons[index], Names) + (index == 0 ? 0.0f : buttons[index].group ? groupgap : gap); + return result; + }; + auto shown{names::none}; + for (auto const option : {names::all, names::chosen}) + if (left + total(option) <= right) + { + shown = option; + break; + } + + auto *list{ImGui::GetWindowDrawList()}; + auto const rounding{3.0f * s}; + auto x{left}; + for (std::size_t index = 0; index < buttons.size(); ++index) + { + auto const &button{buttons[index]}; + if (index > 0) + { + x += button.group ? groupgap : gap; + if (button.group) + list->AddLine(ImVec2(std::round(x - groupgap * 0.5f), top + buttonheight * 0.15f), ImVec2(std::round(x - groupgap * 0.5f), top + buttonheight * 0.85f), ImGui::GetColorU32(ImGuiCol_Separator)); + } + auto const size{ImVec2(width(button, shown), buttonheight)}; + ImVec2 const position{x, top}; + ImGui::SetCursorScreenPos(position); + ImGui::PushID(static_cast(index)); + auto const clicked{ImGui::InvisibleButton("##tool", size)}; + ImGui::PopID(); + auto const hovered{ImGui::IsItemHovered()}; + ImVec2 const end{position.x + size.x, position.y + size.y}; + // the chosen one stands out as the chosen field of work does, the others as buttons + auto const background{button.chosen ? ImGui::GetColorU32(ImGuiCol_Header) : hovered && button.enabled ? ImGui::GetColorU32(ImGuiCol_HeaderHovered) : ImGui::GetColorU32(ImGuiCol_FrameBg)}; + list->AddRectFilled(position, end, background, rounding); + if (button.chosen) + list->AddRectFilled(ImVec2(position.x, end.y - 2.0f * s), end, ImGui::GetColorU32(ImGuiCol_CheckMark), rounding, ImDrawFlags_RoundCornersBottom); + auto const color{button.enabled ? ImGui::GetColorU32(ImGuiCol_Text, button.chosen || hovered ? 1.0f : 0.85f) : ImGui::GetColorU32(ImGuiCol_TextDisabled)}; + auto const center{position.y + size.y * 0.5f}; + if (named(button, shown)) + { + auto textx{position.x + pad}; + if (button.picture != icon::count) + { + editor_icons::draw(list, button.picture, ImVec2(textx + iconside * 0.5f, center), color); + textx += iconside + spacing; + } + list->AddText(ImVec2(textx, std::round(center - ImGui::GetFontSize() * 0.5f)), color, button.label); + } + else + icon_or_letters(list, button, ImVec2(position.x + size.x * 0.5f, center), color); + if (hovered) + button_tooltip(button); + if (clicked && button.enabled && button.action) + button.action(); + x = end.x; + } +} + +void editor_mode::render_viewport_gizmo() +{ + // the gizmo moves the models and the tracks; on the terrain the mouse is the brush's + if (false == m_gizmo_enabled || current_work_area() == work_area::terrain) + return; + auto const tooltip{STR_C("What the handles of the gizmo on the selection do when dragged; Ctrl held snaps the values")}; + std::array const buttons{{ + {icon::translate, STR_C("Translate"), "Q", tooltip, m_gizmo_op == gizmo_operation::translate, [this]() { m_gizmo_op = gizmo_operation::translate; }}, + {icon::rotate, STR_C("Rotate"), "W", tooltip, m_gizmo_op == gizmo_operation::rotate, [this]() { m_gizmo_op = gizmo_operation::rotate; }}, + {icon::scale, STR_C("Scale"), "E", tooltip, m_gizmo_op == gizmo_operation::scale, [this]() { m_gizmo_op = gizmo_operation::scale; }}, + // the gizmo always scales along the axes of the model + {icon::local_space, STR_C("Local space"), "R", STR_C("The handles along the axes of the selected model instead of the axes of the world"), m_gizmo_local || m_gizmo_op == gizmo_operation::scale, + [this]() { m_gizmo_local = !m_gizmo_local; }, true, m_gizmo_op != gizmo_operation::scale}, + }}; + // in a row at the top, in the middle + auto const side{button_side()}; + auto const gap{2.0f * scale()}; + auto const groupgap{8.0f * scale()}; + auto const margin{8.0f * scale()}; + auto const pad{3.0f * scale()}; + auto const width{4.0f * side + 2.0f * gap + groupgap}; + ImVec2 const origin{std::round((editor_ui::view_min().x + editor_ui::view_max().x - width) * 0.5f), editor_ui::view_min().y + margin}; + if (begin_overlay("##viewportgizmo", ImVec2(origin.x - pad, origin.y - pad), ImVec2(width + 2.0f * pad, side + 2.0f * pad))) + { + ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(origin.x - pad, origin.y - pad), ImVec2(origin.x + width + pad, origin.y + side + pad), IM_COL32(16, 18, 21, 140), 6.0f * scale()); + auto x{origin.x}; + for (std::size_t index = 0; index < buttons.size(); ++index) + { + if (buttons[index].group) + x += groupgap - gap; + icon_button(buttons[index], ImVec2(x, origin.y), static_cast(index)); + x += side + gap; + } + } + ImGui::End(); +} + +void editor_mode::render_view_axes() +{ + // the axes of the world as the camera sees them, as in blender: a click on one looks along it + auto const s{scale()}; + auto const radius{40.0f * s}; + auto const margin{8.0f * s}; + auto const side{button_side()}; + auto const size{2.0f * radius}; + ImVec2 const origin{editor_ui::view_max().x - margin - size, editor_ui::view_min().y + margin}; + if (begin_overlay("##viewaxes", origin, ImVec2(size, size + 6.0f * s + side))) + { + auto *list{ImGui::GetWindowDrawList()}; + ImVec2 const center{origin.x + radius, origin.y + radius}; + ImGui::SetCursorScreenPos(origin); + ImGui::InvisibleButton("##axes", ImVec2(size, size)); + auto const hovered{ImGui::IsItemHovered()}; + auto const clicked{ImGui::IsItemClicked(ImGuiMouseButton_Left)}; + if (hovered) + list->AddCircleFilled(center, radius, IM_COL32(255, 255, 255, 28), 48); + // the view matrix is of the camera turned only, the axes go through it as they are + glm::mat3 const view{GfxRenderer->Camera_View_Matrix()}; + struct end + { + int axis; + bool positive; + glm::vec3 seen; + }; + std::array ends; + for (int axis = 0; axis < 3; ++axis) + { + glm::vec3 direction{0.0f}; + direction[axis] = 1.0f; + auto const seen{view * direction}; + ends[axis * 2] = {axis, true, seen}; + ends[axis * 2 + 1] = {axis, false, -seen}; + } + // the farther ones first, the nearer ones over them + std::sort(ends.begin(), ends.end(), [](end const &Left, end const &Right) { return Left.seen.z < Right.seen.z; }); + ImU32 const colors[]{IM_COL32(230, 70, 80, 255), IM_COL32(130, 200, 40, 255), IM_COL32(60, 135, 235, 255)}; + char const *const names[]{"X", "Y", "Z"}; + auto const reach{radius - 10.0f * s}; + auto const mouse{ImGui::GetIO().MousePos}; + end const *pointed{nullptr}; + auto const place = [&](end const &End) { return ImVec2(center.x + End.seen.x * reach, center.y - End.seen.y * reach); }; + for (auto const &entry : ends) + { + auto const at{place(entry)}; + auto const dot{(entry.positive ? 8.0f : 6.0f) * s}; + if (hovered && (mouse.x - at.x) * (mouse.x - at.x) + (mouse.y - at.y) * (mouse.y - at.y) <= (dot + 2.0f * s) * (dot + 2.0f * s)) + pointed = &entry; // the nearest one under the mouse, as it's drawn over the others + } + for (auto const &entry : ends) + { + auto const at{place(entry)}; + auto const color{colors[entry.axis]}; + if (entry.positive) + { + list->AddLine(center, at, color, 2.0f * s); + list->AddCircleFilled(at, 8.0f * s, color, 24); + auto const text{ImGui::CalcTextSize(names[entry.axis])}; + list->AddText(ImVec2(at.x - text.x * 0.5f, at.y - text.y * 0.5f), IM_COL32(20, 20, 20, 255), names[entry.axis]); + } + else + { + list->AddCircleFilled(at, 6.0f * s, (color & ~IM_COL32_A_MASK) | IM_COL32(0, 0, 0, 90), 24); + list->AddCircle(at, 6.0f * s, color, 24, 1.5f * s); + } + if (&entry == pointed) + list->AddCircle(at, (entry.positive ? 9.5f : 7.5f) * s, IM_COL32(255, 255, 255, 255), 24, 1.5f * s); + } + if (pointed != nullptr) + { + ImGui::SetTooltip(STR_C("Look along %s%s; from over the scenery it's the top view"), pointed->positive ? "-" : "+", names[pointed->axis]); + if (clicked) + look_along_axis(pointed->axis, pointed->positive); + } + // the top view, under the axes + tool_button const ortho{Global.EditorOrtho ? icon::ortho : icon::perspective, + STR_C("Top view, orthographic"), + "O", + STR_C("The view straight down without perspective, as on a map; again for the camera as it was"), + Global.EditorOrtho, + [this]() { toggle_ortho(); }}; + icon_button(ortho, ImVec2(std::round(center.x - side * 0.5f), origin.y + size + 6.0f * s), 0); + } + ImGui::End(); +} + +void editor_mode::look_along_axis(int const Axis, bool const Positive) +{ + m_focus_active = false; + if (Axis == 1) + { + if (Positive) + { + // from over the scenery: the top view + if (false == Global.EditorOrtho) + toggle_ortho(); + return; + } + if (Global.EditorOrtho) + toggle_ortho(); + Camera.Angle.x = M_PI_2; + Camera.Angle.z = 0.0; + return; + } + if (Global.EditorOrtho) + toggle_ortho(); + // from the side of the axis the camera's on, towards the other one; the yaw as the camera works it out of where it looks + glm::dvec3 where{0.0}; + where[Axis] = Positive ? -1.0 : 1.0; + Camera.Angle.y = std::atan2(-where.x, -where.z); + Camera.Angle.x = 0.0; + Camera.Angle.z = 0.0; +} diff --git a/application/editorprojection.h b/application/editorprojection.h new file mode 100644 index 00000000..75aa20ea --- /dev/null +++ b/application/editorprojection.h @@ -0,0 +1,140 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/editormode.h" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "imgui/imgui.h" +#include "imgui/ImGuizmo.h" + +#include + +namespace overlay_color +{ +ImU32 constexpr marked{IM_COL32(255, 210, 60, 255)}; +ImU32 constexpr grip{IM_COL32(60, 230, 90, 255)}; +ImU32 constexpr selected{IM_COL32(40, 220, 255, 255)}; +ImU32 constexpr invalid{IM_COL32(255, 60, 60, 255)}; +ImU32 constexpr highlight{IM_COL32(255, 255, 255, 255)}; +} // namespace overlay_color + +// world to screen projection of the current camera, for overlays drawn with ImGui +class screen_projection +{ + public: + screen_projection() + { + ImGuiIO const &io = ImGui::GetIO(); + m_size = io.DisplaySize; + m_camera = GfxRenderer->Camera_Position(); + float const aspect = m_size.y > 0.0f ? m_size.x / m_size.y : 1.0f; + m_viewprojection = editor_mode::projection_matrix(aspect) * GfxRenderer->Camera_View_Matrix(); + } + + glm::vec4 clip(glm::dvec3 const &Point) const + { + return m_viewprojection * glm::vec4(glm::vec3(Point - m_camera), 1.0f); + } + ImVec2 screen(glm::vec4 const &Clip) const + { + return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * m_size.x, (0.5f - Clip.y / Clip.w * 0.5f) * m_size.y); + } + bool project(glm::dvec3 const &Point, ImVec2 &Screen) const + { + auto const c = clip(Point); + if (c.w < kNear) + return false; + Screen = screen(c); + return true; + } + void line(ImDrawList *Drawlist, glm::dvec3 const &A, glm::dvec3 const &B, ImU32 const Color, float const Thickness) const + { + glm::vec4 a = clip(A); + glm::vec4 b = clip(B); + if (a.w < kNear && b.w < kNear) + return; + if (a.w < kNear) + a = glm::mix(a, b, (kNear - a.w) / (b.w - a.w)); + else if (b.w < kNear) + b = glm::mix(b, a, (kNear - b.w) / (a.w - b.w)); + Drawlist->AddLine(screen(a), screen(b), Color, Thickness); + } + // squared distance on the screen from the mouse cursor to the point, the largest float for points behind the camera + float mouse_distance2(glm::dvec3 const &Point) const + { + ImVec2 position; + if (false == project(Point, position)) + return std::numeric_limits::max(); + auto const &mouse{ImGui::GetIO().MousePos}; + return (position.x - mouse.x) * (position.x - mouse.x) + (position.y - mouse.y) * (position.y - mouse.y); + } + // world space ray through the point of the screen + void ray(ImVec2 const &Screen, glm::dvec3 &Origin, glm::dvec3 &Direction) const + { + auto const inverse{glm::inverse(m_viewprojection)}; + glm::vec2 const ndc{m_size.x > 0.0f ? Screen.x / m_size.x * 2.0f - 1.0f : 0.0f, m_size.y > 0.0f ? 1.0f - Screen.y / m_size.y * 2.0f : 0.0f}; + auto nearpoint{inverse * glm::vec4(ndc, -1.0f, 1.0f)}; + auto farpoint{inverse * glm::vec4(ndc, 1.0f, 1.0f)}; + nearpoint /= nearpoint.w; + farpoint /= farpoint.w; + Origin = m_camera + glm::dvec3(glm::vec3(nearpoint)); + Direction = glm::normalize(glm::dvec3(glm::vec3(farpoint - nearpoint))); + } + // where the ray through the point of the screen crosses the level of the height; false if it runs along it or away + bool on_level(ImVec2 const &Screen, double const Height, glm::dvec3 &Point) const + { + glm::dvec3 origin, direction; + ray(Screen, origin, direction); + if (std::abs(direction.y) < 1e-6) + return false; + auto const distance{(Height - origin.y) / direction.y}; + if (distance < 0.0) + return false; + Point = origin + direction * distance; + return true; + } + + private: + static constexpr float kNear{0.1f}; + ImVec2 m_size; + glm::dvec3 m_camera; + glm::mat4 m_viewprojection; +}; + +// starts a gizmo frame over the whole display, the view and the projection are those of the current camera +struct gizmo_frame +{ + static inline int drawn{-1}; + // the cursor is over a gizmo shown in this frame, ImGuizmo alone tells it of the last one shown even when it's gone + static bool over() { return ImGui::GetFrameCount() - drawn <= 1 && ImGuizmo::IsOver(); } + glm::mat4 view; + glm::mat4 projection; + glm::dvec3 camera; + + gizmo_frame() + { + ImGuizmo::BeginFrame(); + ImGuizmo::SetOrthographic(Global.EditorOrtho); + ImGuiIO const &io = ImGui::GetIO(); + ImGuizmo::SetRect(0.0f, 0.0f, io.DisplaySize.x, io.DisplaySize.y); + view = GfxRenderer->Camera_View_Matrix(); + camera = GfxRenderer->Camera_Position(); + projection = editor_mode::projection_matrix(io.DisplaySize.y > 0.0f ? io.DisplaySize.x / io.DisplaySize.y : 1.0f); + } + // moves the gizmo, placed at the point given in world space, with snapping to the step when ctrl is held + bool manipulate(ImGuizmo::OPERATION const Operation, glm::mat4 &Gizmo, float const Snap, glm::mat4 *Delta = nullptr) const + { + glm::vec3 snapvalue(Snap); + float const *snap = Global.ctrlState && Snap > 0.0f ? &snapvalue.x : nullptr; + drawn = ImGui::GetFrameCount(); + return ImGuizmo::Manipulate(&view[0][0], &projection[0][0], Operation, ImGuizmo::WORLD, &Gizmo[0][0], Delta != nullptr ? &(*Delta)[0][0] : nullptr, snap); + } +}; diff --git a/application/editoruilayer.cpp b/application/editoruilayer.cpp index b00b1e51..7a7c4332 100644 --- a/application/editoruilayer.cpp +++ b/application/editoruilayer.cpp @@ -13,7 +13,19 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Globals.h" #include "utilities/utilities.h" #include "scene/scenenode.h" +#include "scene/scenelayers.h" +#include "scene/scene.h" #include "rendering/renderer.h" +#include "utilities/translation.h" +#include "imgui/imgui_internal.h" +#include "editor/editorSettings.hpp" +#include "editor/editorIcons.hpp" + +#include +#include + +glm::vec2 editor_ui::m_viewmin{0.0f}; +glm::vec2 editor_ui::m_viewmax{0.0f}; editor_ui::editor_ui() { @@ -21,10 +33,616 @@ editor_ui::editor_ui() clear_panels(); // bind the panels with ui object. maybe not the best place for this but, eh - add_external_panel(&m_itempropertiespanel); add_external_panel(&m_nodebankpanel); - add_external_panel(&m_functionspanel); - add_external_panel(&m_brushobjects); + add_external_panel(&m_layerspanel); + add_external_panel(&m_includespanel); + + // the menu is a part of the layout of the editor, not a pop-up as in the driver mode + m_menu_always = true; + // the window menu of the docks can unpin them + GImGui->DockNodeWindowMenuHandler = &editor_ui::autohide_menu; +} + +void editor_ui::render_dockspace() +{ + // windows of the editor are docked around the 3d view, which is left free in the middle + auto *viewport{ImGui::GetMainViewport()}; + auto const dockspace{ImGui::GetID("###editordockspace")}; + m_dockspace = dockspace; + if (m_layoutreset || ImGui::DockBuilderGetNode(dockspace) == nullptr) + { + m_layoutreset = false; + // the windows go to their default docks, none is left unpinned + m_autohide.clear(); + m_autohideshown = 0; + ImGui::MarkIniSettingsDirty(); + build_default_layout(dockspace); + } + autohide_begin(); + ImGui::DockSpaceOverViewport(dockspace, viewport, ImGuiDockNodeFlags_PassthruCentralNode); + if (auto const *central{ImGui::DockBuilderGetCentralNode(dockspace)}; central != nullptr) + { + m_viewmin = {central->Pos.x, central->Pos.y}; + m_viewmax = {central->Pos.x + central->Size.x, central->Pos.y + central->Size.y}; + } + else + { + m_viewmin = {viewport->WorkPos.x, viewport->WorkPos.y}; + m_viewmax = {viewport->WorkPos.x + viewport->WorkSize.x, viewport->WorkPos.y + viewport->WorkSize.y}; + } + autohide_place(); +} + +void editor_ui::build_default_layout(unsigned int const Dockspace) +{ + auto *viewport{ImGui::GetMainViewport()}; + ImGui::DockBuilderRemoveNode(Dockspace); + ImGui::DockBuilderAddNode(Dockspace, ImGuiDockNodeFlags_DockSpace | ImGuiDockNodeFlags_PassthruCentralNode); + ImGui::DockBuilderSetNodePos(Dockspace, viewport->WorkPos); + ImGui::DockBuilderSetNodeSize(Dockspace, viewport->WorkSize); + // widths in pixels the windows need, as parts of the room there is + auto const width{std::max(viewport->WorkSize.x, 1.0f)}; + auto const height{std::max(viewport->WorkSize.y, 1.0f)}; + auto const left_width{std::min(400.0f * Global.ui_scale, width * 0.3f)}; + auto const right_width{std::min(460.0f * Global.ui_scale, width * 0.3f)}; + auto const bottom_height{std::min(280.0f * Global.ui_scale, height * 0.35f)}; + ImGuiID center{Dockspace}; + auto const right{ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, right_width / width, nullptr, ¢er)}; + auto const left{ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, left_width / (width - right_width), nullptr, ¢er)}; + auto const bottom{ImGui::DockBuilderSplitNode(center, ImGuiDir_Down, bottom_height / height, nullptr, ¢er)}; + // left: settings of the tool in use above, the scenery below + ImGuiID lefttop{left}; + auto const leftbottom{ImGui::DockBuilderSplitNode(lefttop, ImGuiDir_Down, 0.5f, nullptr, &lefttop)}; + // windows are found by the stable part of their names, after ### + ImGui::DockBuilderDockWindow("###tooloptions", lefttop); + for (auto const *window : {"###scenetree", "###Layers", "###scenehierarchy"}) + ImGui::DockBuilderDockWindow(window, leftbottom); + for (auto const *window : {"###inspector", "###editorsettings", "###Include database", "###orthophoto", "###environment"}) + ImGui::DockBuilderDockWindow(window, right); + // bottom: the node bank, and the model sets by its side, the models are dragged from the one to the other + ImGuiID bottomleft{bottom}; + auto const bottomright{ImGui::DockBuilderSplitNode(bottomleft, ImGuiDir_Right, 0.45f, nullptr, &bottomleft)}; + for (auto const *window : {"###Node bank", "###profilestrip", "###trackanalysis", "###structuregauge", "###editorhistory"}) + ImGui::DockBuilderDockWindow(window, bottomleft); + ImGui::DockBuilderDockWindow("###modelsets", bottomright); + ImGui::DockBuilderFinish(Dockspace); +} + +// unpinned docks. imgui has no auto-hide of its own: the windows of an unpinned dock are undocked, kept hidden while they aren't shown, +// and the one shown is placed over the edge of the 3d view its dock was at. a window docked again by hand is pinned again +std::vector editor_ui::m_autohide; +unsigned int editor_ui::m_dockspace{0}; + +namespace +{ + +// imgui math operators aren't defined here, imgui.h is included before imgui_internal.h asks for them +ImVec2 vec_add(ImVec2 const A, ImVec2 const B) +{ + return {A.x + B.x, A.y + B.y}; +} +ImVec2 vec_sub(ImVec2 const A, ImVec2 const B) +{ + return {A.x - B.x, A.y - B.y}; +} +ImVec2 vec_mul(ImVec2 const A, float const B) +{ + return {A.x * B, A.y * B}; +} + +bool autohide_vertical(int const Edge) +{ + return Edge == ImGuiDir_Left || Edge == ImGuiDir_Right; +} + +// leaf nodes of the dockspace which hold windows, the central node aside +void autohide_leaves(ImGuiDockNode *Node, std::vector &Leaves) +{ + if (Node == nullptr) + return; + if (Node->IsLeafNode()) + { + if (false == Node->IsCentralNode() && Node->Windows.Size > 0) + Leaves.push_back(Node); + return; + } + autohide_leaves(Node->ChildNodes[0], Leaves); + autohide_leaves(Node->ChildNodes[1], Leaves); +} + +// edge of the central node the node lies at +int autohide_edge(ImGuiDockNode const *Node, ImGuiDockNode const *Central) +{ + if (Central == nullptr) + return ImGuiDir_Left; + auto const center{vec_add(Node->Pos, vec_mul(Node->Size, 0.5f))}; + if (center.x < Central->Pos.x) + return ImGuiDir_Left; + if (center.x > Central->Pos.x + Central->Size.x) + return ImGuiDir_Right; + if (center.y > Central->Pos.y + Central->Size.y) + return ImGuiDir_Down; + return ImGuiDir_Up; +} + +// pin drawn with primitives, there's no icon font; a pinned one stands, an unpinned one lies +void autohide_draw_pin(ImDrawList *List, ImRect const &Rect, bool const Pinned, bool const Hovered) +{ + auto const color{ImGui::GetColorU32(ImGuiCol_Text)}; + if (Hovered) + List->AddRectFilled(Rect.Min, Rect.Max, ImGui::GetColorU32(ImGuiCol_ButtonHovered), 2.0f); + auto const center{Rect.GetCenter()}; + auto const s{Rect.GetWidth()}; + // along the pin: from the head to the point, and across it + ImVec2 const along{Pinned ? ImVec2(0.0f, 1.0f) : ImVec2(-1.0f, 0.0f)}; + ImVec2 const across{along.y, -along.x}; + auto const head{vec_sub(center, vec_mul(along, s * 0.30f))}; + auto const collar{vec_add(center, vec_mul(along, s * 0.05f))}; + auto const point{vec_add(center, vec_mul(along, s * 0.40f))}; + auto const half{vec_mul(across, s * 0.16f)}; + auto const guard{vec_mul(across, s * 0.30f)}; + // the head and the collar the needle comes out of + List->AddQuadFilled(vec_sub(head, half), vec_add(head, half), vec_add(collar, half), vec_sub(collar, half), color); + List->AddLine(vec_sub(collar, guard), vec_add(collar, guard), color, 1.5f); + List->AddLine(collar, point, color, 1.0f); +} + +// text turned to read from the top down, for the strips at the sides +void autohide_vertical_text(ImDrawList *List, ImVec2 const Position, ImU32 const Color, char const *Begin, char const *End) +{ + auto const height{ImGui::GetFontSize()}; + auto const first{List->VtxBuffer.Size}; + // the glyphs are laid out across first, from the left edge of the main window: out of the clip rectangle or the screen + // they'd be left out, as they would at the right edge + ImVec2 const origin{ImGui::GetMainViewport()->Pos.x, Position.y}; + List->PushClipRectFullScreen(); + List->AddText(origin, Color, Begin, End); + List->PopClipRect(); + for (auto index = first; index < List->VtxBuffer.Size; ++index) + { + auto &position{List->VtxBuffer[index].pos}; + auto const offset{vec_sub(position, origin)}; + position = ImVec2(Position.x + height - offset.y, Position.y + offset.x); + } +} + +} // namespace + +std::size_t editor_ui::autohide_group_of(unsigned int const Window) +{ + for (std::size_t index = 0; index < m_autohide.size(); ++index) + if (std::find(m_autohide[index].windows.begin(), m_autohide[index].windows.end(), Window) != m_autohide[index].windows.end()) + return index; + return static_cast(-1); +} + +void editor_ui::autohide_node(ImGuiDockNode *Node) +{ + if (Node == nullptr || Node->Windows.Size == 0 || Node->IsCentralNode()) + return; + autohide_group group; + group.edge = autohide_edge(Node, ImGui::DockNodeGetRootNode(Node)->CentralNode); + group.size = autohide_vertical(group.edge) ? Node->Size.x : Node->Size.y; + // place in the column of docks at that edge; a dock alone there goes back before the one which comes back to it + auto const along{autohide_vertical(group.edge) ? ImGuiAxis_Y : ImGuiAxis_X}; + group.first = true; + if (auto const *parent{Node->ParentNode}; parent != nullptr && parent->SplitAxis == along) + { + group.first = parent->ChildNodes[0] == Node; + group.share = along == ImGuiAxis_Y ? Node->Size.y / std::max(parent->Size.y, 1.0f) : Node->Size.x / std::max(parent->Size.x, 1.0f); + } + // docks split along the edge make one column; the docks put back all at once are put back from the outer columns in, + // so each spans as far as it did + auto const *column{Node}; + while (column->ParentNode != nullptr && column->ParentNode->SplitAxis == along) + column = column->ParentNode; + for (auto const *parent{column->ParentNode}; parent != nullptr; parent = parent->ParentNode) + ++group.depth; + for (auto *window : Node->Windows) + { + group.windows.push_back(window->ID); + // carried out at the start of the next frame + ImGui::DockContextQueueUndockWindow(GImGui, window); + } + m_autohide.push_back(std::move(group)); + ImGui::MarkIniSettingsDirty(); +} + +void editor_ui::autohide_pin(std::size_t const Group) +{ + if (Group >= m_autohide.size()) + return; + auto &group{m_autohide[Group]}; + auto *central{ImGui::DockBuilderGetCentralNode(m_dockspace)}; + ImGuiID target{0}; + if (central != nullptr) + { + // the node went with its last window, and its id may have been given to another one since: another dock at that edge + // gets split, so that docks taken there one by one come back the way they were, or the room of the view does + std::vector leaves; + autohide_leaves(ImGui::DockBuilderGetNode(m_dockspace), leaves); + ImGuiDockNode *neighbour{nullptr}; + for (auto *leaf : leaves) + if (autohide_edge(leaf, central) == group.edge) + neighbour = leaf; + ImGuiID rest{0}; + if (neighbour != nullptr) + { + auto const vertical{autohide_vertical(group.edge)}; + auto const direction{vertical ? (group.first ? ImGuiDir_Up : ImGuiDir_Down) : (group.first ? ImGuiDir_Left : ImGuiDir_Right)}; + target = ImGui::DockBuilderSplitNode(neighbour->ID, direction, std::clamp(group.share, 0.15f, 0.85f), nullptr, &rest); + } + else + { + auto const room{std::max(autohide_vertical(group.edge) ? central->Size.x : central->Size.y, 1.0f)}; + target = ImGui::DockBuilderSplitNode(central->ID, static_cast(group.edge), std::clamp(group.size / room, 0.1f, 0.45f), nullptr, &rest); + } + } + if (target == 0) + return; + for (auto const id : group.windows) + if (auto const *window{ImGui::FindWindowByID(id)}; window != nullptr) + ImGui::DockBuilderDockWindow(window->Name, target); + ImGui::DockBuilderFinish(m_dockspace); + ImGui::MarkIniSettingsDirty(); +} + +void editor_ui::autohide_show(unsigned int const Window, bool const Focus) +{ + m_autohideshown = Window; + m_autohideappear = true; + m_autohidefocus = Focus; + m_autohideleft = -1.0; +} + +void editor_ui::autohide_begin() +{ + auto &g{*GImGui}; + // all docks unpinned, or all pinned back when none is left. the unpinned ones are undocked at the start of the next frame + auto unpinall{false}; + if (m_dockstoggle) + { + m_dockstoggle = false; + std::vector leaves; + autohide_leaves(ImGui::DockBuilderGetNode(m_dockspace), leaves); + unpinall = false == leaves.empty(); + if (false == unpinall) + { + for (std::size_t index = 0; index < m_autohide.size(); ++index) + m_autohidepin.push_back(index); + std::stable_sort(m_autohidepin.begin(), m_autohidepin.end(), [](std::size_t const Left, std::size_t const Right) { return m_autohide[Left].depth < m_autohide[Right].depth; }); + } + } + for (auto const index : m_autohidepin) + autohide_pin(index); + m_autohidepin.clear(); + // windows docked again, by the pin or by hand, are pinned + auto changed{false}; + for (auto &group : m_autohide) + { + auto const count{group.windows.size()}; + std::erase_if(group.windows, [](unsigned int const Id) { + auto const *window{ImGui::FindWindowByID(Id)}; + return window != nullptr && window->DockId != 0; + }); + changed |= group.windows.size() != count; + } + std::erase_if(m_autohide, [](autohide_group const &Group) { return Group.windows.empty(); }); + if (changed) + ImGui::MarkIniSettingsDirty(); + if (autohide_group_of(m_autohideshown) == static_cast(-1)) + m_autohideshown = 0; + + // the shown window goes back when the cursor has been away from it for a while, or at once on a click elsewhere; + // it stays while it's in use: a button held, a field being typed in, a popup open, a drag + if (auto *shown{m_autohideshown != 0 ? ImGui::FindWindowByID(m_autohideshown) : nullptr}; shown != nullptr && false == m_autohideappear) + { + auto const &io{ImGui::GetIO()}; + auto area{shown->Rect()}; + area.Expand(8.0f); + auto const over{area.Contains(io.MousePos) || m_autohidestrip}; + auto const popup{ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)}; + auto const typing{io.WantTextInput && g.ActiveIdWindow != nullptr && g.ActiveIdWindow->RootWindow == shown}; + auto const busy{ImGui::IsAnyMouseDown() || g.MovingWindow != nullptr || popup || typing || ImGui::IsDragDropActive()}; + if (false == over && false == popup && (ImGui::IsMouseClicked(ImGuiMouseButton_Left) || ImGui::IsMouseClicked(ImGuiMouseButton_Right))) + m_autohideshown = 0; + else if (over || busy) + m_autohideleft = -1.0; + else if (m_autohideleft < 0.0) + m_autohideleft = ImGui::GetTime(); + else if (ImGui::GetTime() - m_autohideleft > 0.5) + m_autohideshown = 0; + } + // the rest stay hidden. the count goes down in Begin before it's checked, so it's set to 2 to hide the window in this frame + for (auto const &group : m_autohide) + for (auto const id : group.windows) + if (id != m_autohideshown) + if (auto *window{ImGui::FindWindowByID(id)}; window != nullptr) + window->HiddenFramesCanSkipItems = static_cast(std::max(window->HiddenFramesCanSkipItems, 2)); + // after the check above, the windows are still docked until the next frame + if (unpinall) + { + std::vector leaves; + autohide_leaves(ImGui::DockBuilderGetNode(m_dockspace), leaves); + for (auto *leaf : leaves) + autohide_node(leaf); + } +} + +void editor_ui::autohide_place() +{ + auto *shown{m_autohideshown != 0 ? ImGui::FindWindowByID(m_autohideshown) : nullptr}; + if (shown == nullptr) + return; + auto &group{m_autohide[autohide_group_of(m_autohideshown)]}; + auto const vertical{autohide_vertical(group.edge)}; + // the size the user gave it by dragging its edge is kept for the next time + if (false == m_autohideappear) + group.size = vertical ? shown->SizeFull.x : shown->SizeFull.y; + auto const room{vertical ? m_viewmax.x - m_viewmin.x : m_viewmax.y - m_viewmin.y}; + auto const size{std::clamp(group.size, std::min(120.0f, room), std::max(room * 0.85f, 1.0f))}; + ImVec2 position{m_viewmin.x, m_viewmin.y}; + ImVec2 extent{vertical ? size : m_viewmax.x - m_viewmin.x, vertical ? m_viewmax.y - m_viewmin.y : size}; + if (group.edge == ImGuiDir_Right) + position.x = m_viewmax.x - size; + else if (group.edge == ImGuiDir_Down) + position.y = m_viewmax.y - size; + ImGui::SetWindowCollapsed(shown, false, ImGuiCond_Always); + ImGui::SetWindowSize(shown, extent, ImGuiCond_Always); + // dragged by its title it may be docked somewhere, it's left to go + if (GImGui->MovingWindow != shown) + ImGui::SetWindowPos(shown, position, ImGuiCond_Always); + if (m_autohideappear) + { + m_autohideappear = false; + ImGui::BringWindowToDisplayFront(shown); + if (m_autohidefocus) + ImGui::FocusWindow(shown); + } +} + +void editor_ui::autohide_strips() +{ + auto *viewport{ImGui::GetMainViewport()}; + auto const &style{ImGui::GetStyle()}; + auto const thickness{ImGui::GetFrameHeight()}; + auto const padding{style.FramePadding.x}; + auto hovered{false}; + m_autohidestrip = false; + for (auto const edge : {ImGuiDir_Left, ImGuiDir_Right, ImGuiDir_Up, ImGuiDir_Down}) + { + std::vector windows; + for (auto const &group : m_autohide) + { + if (group.edge != edge) + continue; + for (auto const id : group.windows) + if (auto *window{ImGui::FindWindowByID(id)}; window != nullptr && window->Active) + windows.push_back(window); + } + if (windows.empty()) + continue; + auto const vertical{autohide_vertical(edge)}; + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + auto const name{std::string{"##autohidestrip"} + std::to_string(static_cast(edge))}; + if (ImGui::BeginViewportSideBar(name.c_str(), viewport, edge, thickness, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing)) + { + auto *list{ImGui::GetWindowDrawList()}; + auto position{vec_add(ImGui::GetCursorScreenPos(), vertical ? ImVec2(0.0f, 4.0f) : ImVec2(4.0f, 0.0f))}; + for (auto *window : windows) + { + auto const *end{ImGui::FindRenderedTextEnd(window->Name)}; + auto const text{ImGui::CalcTextSize(window->Name, end)}; + ImVec2 const size{vertical ? ImVec2(thickness, text.x + padding * 2.0f) : ImVec2(text.x + padding * 2.0f, thickness)}; + ImGui::SetCursorScreenPos(position); + ImGui::PushID(static_cast(window->ID)); + ImGui::InvisibleButton("##tab", size); + ImGui::PopID(); + auto const over{ImGui::IsItemHovered()}; + auto const id{window->ID}; + auto const color{m_autohideshown == id ? ImGuiCol_TabSelected : over ? ImGuiCol_TabHovered : ImGuiCol_Tab}; + list->AddRectFilled(position, vec_add(position, size), ImGui::GetColorU32(color), style.TabRounding); + if (vertical) + autohide_vertical_text(list, vec_add(position, ImVec2((thickness - ImGui::GetFontSize()) * 0.5f, padding)), ImGui::GetColorU32(ImGuiCol_Text), window->Name, end); + else + list->AddText(vec_add(position, ImVec2(padding, style.FramePadding.y)), ImGui::GetColorU32(ImGuiCol_Text), window->Name, end); + // a click shows the window and gives it the focus, or puts it away; pointing at the tab for a while shows it too + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) + { + // shown by pointing at it, the click keeps it out and gives it the focus + if (m_autohideshown == id && m_autohidefocus) + m_autohideshown = 0; + else + autohide_show(id, true); + m_autohidehovered = id; + m_autohidehovertime = std::numeric_limits::max(); + } + else if (over && false == ImGui::IsAnyMouseDown()) + { + if (m_autohidehovered != id) + { + m_autohidehovered = id; + m_autohidehovertime = ImGui::GetTime(); + } + else if (m_autohideshown != id && ImGui::GetTime() - m_autohidehovertime >= 0.25) + autohide_show(id, false); + } + if (over) + { + hovered = true; + m_autohidestrip = true; + } + position = vec_add(position, vertical ? ImVec2(0.0f, size.y + 4.0f) : ImVec2(size.x + 4.0f, 0.0f)); + } + } + ImGui::End(); + ImGui::PopStyleVar(2); + } + if (false == hovered) + m_autohidehovered = 0; +} + +void editor_ui::autohide_pins() +{ + auto &g{*GImGui}; + auto const &style{g.Style}; + auto const size{g.FontSize}; + auto const popup{ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)}; + // pins of the docks, in their tab bars by the close button, where the tabs leave room for them; the window menu of a dock + // (the arrow at the left of its tabs) unpins it too + std::vector leaves; + autohide_leaves(ImGui::DockBuilderGetNode(m_dockspace), leaves); + for (auto *node : leaves) + { + if (node->HostWindow == nullptr || false == node->IsVisible || node->IsHiddenTabBar() || node->IsNoTabBar() || node->TabBar == nullptr) + continue; + auto const x{node->Pos.x + node->Size.x - style.WindowBorderSize - style.FramePadding.x - (node->HasCloseButton ? size + style.ItemInnerSpacing.x : 0.0f) - size}; + if (node->TabBar->BarRect.Min.x + node->TabBar->WidthAllTabsIdeal + style.ItemInnerSpacing.x > x) + continue; + ImRect const rect{ImVec2(x, node->Pos.y + style.FramePadding.y), ImVec2(x + size, node->Pos.y + style.FramePadding.y + size)}; + auto const over{false == popup && g.HoveredWindow != nullptr && g.HoveredWindow->RootWindowDockTree == node->HostWindow->RootWindowDockTree && ImGui::IsMouseHoveringRect(rect.Min, rect.Max, false)}; + auto *list{node->HostWindow->DrawList}; + list->PushClipRect(node->Pos, vec_add(node->Pos, node->Size), false); + autohide_draw_pin(list, rect, true, over); + list->PopClipRect(); + if (over) + { + ImGui::SetTooltip("%s", STR_C("Unpin: the dock hides at the edge of the window, its tabs show it over the view (Ctrl+Space: all docks)")); + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + autohide_node(node); + } + } + // the pin of the shown window puts its dock back + auto *shown{m_autohideshown != 0 ? ImGui::FindWindowByID(m_autohideshown) : nullptr}; + if (shown == nullptr || false == shown->Active || shown->Hidden || shown->DockIsActive || (shown->Flags & ImGuiWindowFlags_NoTitleBar) != 0) + return; + auto const title{shown->TitleBarRect()}; + auto const x{title.Max.x - style.FramePadding.x - (shown->HasCloseButton ? size + style.ItemInnerSpacing.x : 0.0f) - size}; + ImRect const rect{ImVec2(x, title.Min.y + style.FramePadding.y), ImVec2(x + size, title.Min.y + style.FramePadding.y + size)}; + auto const over{false == popup && g.HoveredWindow == shown && ImGui::IsMouseHoveringRect(rect.Min, rect.Max, false)}; + shown->DrawList->PushClipRect(title.Min, title.Max, false); + autohide_draw_pin(shown->DrawList, rect, false, over); + shown->DrawList->PopClipRect(); + if (over) + { + ImGui::SetTooltip("%s", STR_C("Pin: the dock goes back to its place")); + if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + m_autohidepin.push_back(autohide_group_of(m_autohideshown)); + } +} + +void editor_ui::autohide_menu(ImGuiContext *Context, ImGuiDockNode *Node, ImGuiTabBar *TabBar) +{ + ImGui::DockNodeWindowMenuHandler_Default(Context, Node, TabBar); + if (m_dockspace == 0 || Node == nullptr || Node->IsCentralNode() || ImGui::DockNodeGetRootNode(Node)->ID != m_dockspace) + return; + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Unpin (hide at the edge)"))) + autohide_node(Node); +} + +void editor_ui::register_settings() +{ + ImGuiSettingsHandler handler; + handler.TypeName = "MaSzynaAutoHide"; + handler.TypeHash = ImHashStr(handler.TypeName); + handler.ClearAllFn = [](ImGuiContext *, ImGuiSettingsHandler *) { m_autohide.clear(); }; + handler.ReadOpenFn = [](ImGuiContext *, ImGuiSettingsHandler *, char const *) -> void * { + m_autohide.emplace_back(); + return &m_autohide.back(); + }; + handler.ReadLineFn = [](ImGuiContext *, ImGuiSettingsHandler *, void *Entry, char const *Line) { + auto &group{*static_cast(Entry)}; + int edge{0}; + unsigned int id{0}; + // whole numbers only, the decimal point of the locale doesn't matter then + if (std::sscanf(Line, "Edge=%d", &edge) == 1) + group.edge = std::clamp(edge, static_cast(ImGuiDir_Left), static_cast(ImGuiDir_Down)); + else if (std::sscanf(Line, "Size=%d", &edge) == 1) + group.size = static_cast(edge); + else if (std::sscanf(Line, "First=%d", &edge) == 1) + group.first = edge != 0; + else if (std::sscanf(Line, "Share=%d", &edge) == 1) + group.share = edge / 1000.0f; + else if (std::sscanf(Line, "Depth=%d", &edge) == 1) + group.depth = edge; + else if (std::sscanf(Line, "Window=0x%X", &id) == 1) + group.windows.push_back(id); + }; + handler.ApplyAllFn = [](ImGuiContext *, ImGuiSettingsHandler *) { + std::erase_if(m_autohide, [](autohide_group const &Group) { return Group.windows.empty(); }); + }; + handler.WriteAllFn = [](ImGuiContext *, ImGuiSettingsHandler *Handler, ImGuiTextBuffer *Buffer) { + for (auto const &group : m_autohide) + { + if (group.windows.empty()) + continue; + Buffer->appendf("[%s][Dock]\nEdge=%d\nSize=%d\nFirst=%d\nShare=%d\nDepth=%d\n", Handler->TypeName, group.edge, static_cast(group.size), group.first ? 1 : 0, static_cast(group.share * 1000.0f), group.depth); + for (auto const id : group.windows) + Buffer->appendf("Window=0x%08X\n", id); + Buffer->append("\n"); + } + }; + ImGui::AddSettingsHandler(&handler); +} + +void editor_ui::render_() +{ + auto *viewport{ImGui::GetMainViewport()}; + auto const flags{ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar}; + // the fields of work of the editor under the menu, in a row twice as high; the tools of the one chosen in the toolbar under it + if (m_workspacebar) + { + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.0f, 0.0f)); + if (ImGui::BeginViewportSideBar("##editorworkspaces", viewport, ImGuiDir_Up, ImGui::GetFrameHeight() * 2.0f, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollWithMouse)) + { + m_workspacebar(); + } + ImGui::End(); + ImGui::PopStyleVar(2); + } + // toolbar under them: the tools of the field of work, as buttons with their icons, high enough for them. + // the buttons are laid out by hand, the padding is given back for their tooltips + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImGui::GetStyleColorVec4(ImGuiCol_MenuBarBg)); + auto const toolbarheight{std::max(ImGui::GetFrameHeight(), editor_icons::size() + 12.0f * std::max(1.0f, Global.ui_scale))}; + auto const toolbar{ImGui::BeginViewportSideBar("##editortoolbar", viewport, ImGuiDir_Up, toolbarheight, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollWithMouse)}; + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + if (toolbar && m_toolbaroptions) + m_toolbaroptions(); + ImGui::End(); + render_tool_options(); + render_inspector(); + render_scene(); + // status bar: the outcome of the last operation, and the selected node + if (ImGui::BeginViewportSideBar("##editorstatusbar", viewport, ImGuiDir_Down, ImGui::GetFrameHeight(), flags)) + { + if (ImGui::BeginMenuBar()) + { + if (false == m_layerspanel.status.empty()) + { + if (m_layerspanel.status_error) + ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.4f, 1.0f), "%s", m_layerspanel.status.c_str()); + else + ImGui::TextUnformatted(m_layerspanel.status.c_str()); + } + if (m_node != nullptr) + { + auto const name{m_node->name().empty() ? std::string{"(none)"} : m_node->name()}; + auto const label{std::string{STR_C("Selected:")} + " " + name}; + ImGui::SameLine(std::max(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() - ImGui::CalcTextSize(label.c_str()).x - ImGui::GetStyle().ItemSpacing.x * 2.0f)); + ImGui::TextUnformatted(label.c_str()); + } + ImGui::EndMenuBar(); + } + } + ImGui::End(); + // strips of the unpinned docks, inside the toolbar and the status bar; the pins over the tab bars drawn by now + autohide_strips(); + autohide_pins(); } // updates state of UI elements @@ -43,31 +661,385 @@ void editor_ui::update() ui_layer::update(); m_itempropertiespanel.update(m_node); m_functionspanel.update(m_node); - - auto ptr = get_active_node_template(true); - if (ptr) - m_brushobjects.update(*ptr); } -void editor_ui::toggleBrushSettings(bool isVisible) +void editor_ui::render_mode_options(nodebank_panel::edit_mode const Mode) { - if (m_brushobjects.is_open != isVisible) - m_brushobjects.is_open = isVisible; + switch (Mode) + { + case nodebank_panel::MODIFY: + ImGui::TextDisabled(STR_C("LMB: select node F: focus End: drop to ground Del: delete")); + break; + case nodebank_panel::COPY: + ImGui::TextDisabled(STR_C("LMB: copy the clicked model to the node bank")); + break; + case nodebank_panel::ADD: + { + ImGui::TextDisabled(STR_C("LMB: insert a model at the cursor")); + ImGui::Checkbox(STR_C("Random model from set"), &m_insertrandom); + if (m_insertrandom) + { + m_nodebankpanel.set_combo(STR_C("Set##insert"), m_insertset, nullptr); + auto const count{m_nodebankpanel.set_entries(m_insertset).size()}; + ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : ""); + } + else + { + ImGui::TextDisabled(STR_C("Inserts the template selected in the node bank")); + } + m_functionspanel.render_controls(); + break; + } + case nodebank_panel::BRUSH: + ImGui::TextDisabled(STR_C("Hold LMB: paint models along the cursor path")); + m_brushobjects.render_options(m_nodebankpanel); + m_functionspanel.render_controls(); + break; + case nodebank_panel::FILL: + if (m_filloptions) + m_filloptions(); + break; + default: + break; + } } -std::vector editor_ui::nodebank_groups() const +void editor_ui::render_tool_options() { - return m_nodebankpanel.group_names(); + if (false == m_tooloptionsopen) + return; + ImGui::SetNextWindowSize(ImVec2S(400, 300), ImGuiCond_FirstUseEver); + auto const current{mode()}; + auto const active{m_workspace == workspace::terrain || (m_workspace == workspace::roads && current != nodebank_panel::TRACK) ? m_workspace : workspace::none}; + // a change of the mode brings the window forward, in case it shares its dock with another one + auto const shown{active != workspace::none ? 100 + static_cast(active) : static_cast(current)}; + if (shown != m_toolmode) + { + m_toolmode = shown; + ImGui::SetNextWindowFocus(); + } + if (ImGui::Begin((std::string(STR_C("Tool options")) + "###tooloptions").c_str(), &m_tooloptionsopen, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) + { + char const *name{STR_C("Select")}; + switch (current) + { + case nodebank_panel::ADD: + name = STR_C("Insert"); + break; + case nodebank_panel::BRUSH: + name = STR_C("Brush"); + break; + case nodebank_panel::FILL: + name = STR_C("Area fill"); + break; + case nodebank_panel::COPY: + name = STR_C("Copy to bank"); + break; + case nodebank_panel::TRACK: + name = STR_C("Tracks"); + break; + default: + break; + } + if (active == workspace::roads) + name = "Roads"; + else if (active == workspace::terrain) + name = STR_C("Terrain"); + ImGui::SeparatorText(name); + if (active != workspace::none) + { + auto const &tools{m_workspacetools[static_cast(active)]}; + if (tools) + tools(); + } + else if (current == nodebank_panel::TRACK) + { + if (m_tracktools) + m_tracktools(); + } + else + render_mode_options(current); + if (ImGui::CollapsingHeader(STR_C("Gizmo"), ImGuiTreeNodeFlags_DefaultOpen)) + { + if (m_gizmooptions) + m_gizmooptions(); + } + } + ImGui::End(); } -std::vector editor_ui::nodebank_group_templates(std::size_t const Group) const +void editor_ui::render_inspector() { - return m_nodebankpanel.group_templates(Group); + if (false == m_inspectoropen) + return; + ImGui::SetNextWindowSize(ImVec2S(420, 480), ImGuiCond_FirstUseEver); + if (ImGui::Begin((std::string(STR_C("Inspector")) + "###inspector").c_str(), &m_inspectoropen, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) + { + if (mode() == nodebank_panel::TRACK) + { + // in the track mode the selection is a path + if (m_trackselection) + m_trackselection(); + ImGui::End(); + return; + } + if (auto const &selection{m_workspaceselection[static_cast(m_workspace)]}) + { + selection(); + ImGui::End(); + return; + } + if (ImGui::CollapsingHeader(STR_C("Node properties"), ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Indent(); + m_itempropertiespanel.render_body(); + ImGui::Unindent(); + } + if (ImGui::CollapsingHeader(STR_C("Array"))) + { + if (m_arrayoptions) + m_arrayoptions(); + } + if (m_bendexpand) + { + ImGui::SetNextItemOpen(true); + m_bendexpand = false; + } + if (ImGui::CollapsingHeader(STR_C("Bend along the track"))) + { + if (m_bendoptions) + m_bendoptions(); + } + } + ImGui::End(); +} + +void editor_ui::scene_rebuild() +{ + m_sceneentries.clear(); + m_sceneentries.reserve(scene::Hierarchy.size()); + for (auto const &entry : scene::Hierarchy) + { + if (entry.second == nullptr) + continue; + m_sceneentries.push_back({entry.first, entry.second->layer(), entry.second->group(), entry.second->name()}); + } + std::sort(m_sceneentries.begin(), m_sceneentries.end(), [](scene_entry const &Left, scene_entry const &Right) { + if (Left.layer != Right.layer) + return Left.layer < Right.layer; + if (Left.group != Right.group) + return Left.group < Right.group; + return Left.name < Right.name; + }); + m_scenesize = scene::Hierarchy.size(); + m_scenefilterused = "\x01"; // matches are found again +} + +void editor_ui::scene_row(std::size_t const Entry) +{ + auto const &entry{m_sceneentries[Entry]}; + // the node is looked up again, it may have been removed since the list was made + auto const lookup{scene::Hierarchy.find(entry.uuid)}; + auto *node{lookup != scene::Hierarchy.end() ? lookup->second : nullptr}; + if (node == nullptr) + { + ImGui::TextDisabled("%s", entry.name.c_str()); + return; + } + auto const label{(node->name().empty() ? std::string{"(none)"} : node->name()) + "##" + entry.uuid}; + if (ImGui::Selectable(label.c_str(), node == m_node, ImGuiSelectableFlags_AllowDoubleClick) && m_sceneselect) + m_sceneselect(node, ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)); + if (ImGui::IsItemHovered()) + { + auto const location{node->location()}; + ImGui::SetTooltip("%s\n%.1f, %.1f, %.1f\n%s", entry.uuid.c_str(), location.x, location.y, location.z, STR_C("Double click: fly to it")); + } +} + +void editor_ui::render_scene() +{ + if (false == m_sceneopen) + return; + ImGui::SetNextWindowSize(ImVec2S(400, 420), ImGuiCond_FirstUseEver); + if (ImGui::Begin((std::string(STR_C("Scene")) + "###scenetree").c_str(), &m_sceneopen, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) + { + if (m_scenesize != scene::Hierarchy.size()) + scene_rebuild(); + if (ImGui::Button(STR_C("Refresh"))) + scene_rebuild(); + ImGui::SameLine(); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputTextWithHint("##scenefilter", STR_C("Filter: name or uuid"), m_scenefilter, IM_ARRAYSIZE(m_scenefilter)); + ImGui::TextDisabled(STR_C("%zu models"), m_sceneentries.size()); + if (ImGui::BeginChild("##scenelist", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders)) + { + std::string const filter{m_scenefilter}; + if (false == filter.empty()) + { + // a flat list of what matches the filter + if (filter != m_scenefilterused) + { + m_scenematches.clear(); + for (std::size_t idx = 0; idx < m_sceneentries.size(); ++idx) + if (contains(m_sceneentries[idx].name, filter) || contains(m_sceneentries[idx].uuid, filter)) + m_scenematches.push_back(idx); + m_scenefilterused = filter; + } + ImGuiListClipper clipper; + clipper.Begin(static_cast(m_scenematches.size())); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + scene_row(m_scenematches[row]); + } + else + { + // layers, in them the groups (made by the includes), in these the models + std::size_t layerstart{0}; + while (layerstart < m_sceneentries.size()) + { + auto const layer{m_sceneentries[layerstart].layer}; + auto layerend{layerstart}; + while (layerend < m_sceneentries.size() && m_sceneentries[layerend].layer == layer) + ++layerend; + auto const layername{scene::Layers.valid(layer) ? scene::Layers.layer(layer).name : std::string{STR_C("(no layer)")}}; + auto const layerlabel{layername + " (" + std::to_string(layerend - layerstart) + ")##layer" + std::to_string(layer)}; + if (ImGui::TreeNodeEx(layerlabel.c_str(), m_sceneentries.size() == layerend - layerstart ? ImGuiTreeNodeFlags_DefaultOpen : 0)) + { + auto groupstart{layerstart}; + while (groupstart < layerend) + { + auto const group{m_sceneentries[groupstart].group}; + auto groupend{groupstart}; + while (groupend < layerend && m_sceneentries[groupend].group == group) + ++groupend; + auto const rows = [&](std::size_t const From, std::size_t const To) { + ImGuiListClipper clipper; + clipper.Begin(static_cast(To - From)); + while (clipper.Step()) + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + scene_row(From + row); + }; + if (group == null_handle) + { + rows(groupstart, groupend); + } + else + { + auto const grouplabel{std::string{STR_C("Group")} + " " + std::to_string(group) + " (" + std::to_string(groupend - groupstart) + ")##group" + std::to_string(group)}; + if (ImGui::TreeNode(grouplabel.c_str())) + { + rows(groupstart, groupend); + ImGui::TreePop(); + } + } + groupstart = groupend; + } + ImGui::TreePop(); + } + layerstart = layerend; + } + } + } + ImGui::EndChild(); + } + ImGui::End(); +} + +void editor_ui::render_menu_contents() +{ + if (ImGui::BeginMenu(STR_C("File"))) + { + if (ImGui::MenuItem(STR_C("New scenery...")) && m_newscenery) + m_newscenery(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The editor starts again with the wizard of a new scenery: its name and its centre on the map")); + if (ImGui::MenuItem(STR_C("Open scenery...")) && m_openscenery) + m_openscenery(); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The editor starts again with another scenery of the scenery folder")); + // changes go to the scenery files only if the scenery was loaded with its sources tracked + auto const editsession{false == scene::Layers.empty()}; + if (ImGui::MenuItem(STR_C("Save"), STR_C("Ctrl+S"), false, editsession) && m_save) + { + m_save(); + } + if (false == editsession && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) + { + ImGui::SetTooltip("%s", STR_C("Start the simulator with -edit to save changes to the scenery files")); + } + if (ImGui::MenuItem(STR_C("Export scenery dump"), STR_C("Ctrl+Shift+F11")) && m_export) + { + m_export(); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", STR_C("Writes the whole scene to _export files next to the scenario, the scenery files are left as they are")); + } + ImGui::EndMenu(); + } + if (m_editmenu) + m_editmenu(); + + ui_layer::render_menu_contents(); + + if (m_menuoptions) + m_menuoptions(); +} + +// the windows of the editor and their layout, in the windows menu after the common ones (log, hierarchy, history, settings) +void editor_ui::render_windows_menu() +{ + ImGui::Separator(); + ImGui::MenuItem(STR_C("Tool options"), nullptr, &m_tooloptionsopen); + ImGui::MenuItem(STR_C("Inspector"), nullptr, &m_inspectoropen); + ImGui::MenuItem(STR_C("Scene"), nullptr, &m_sceneopen); + ImGui::MenuItem(STR_C("Node bank"), nullptr, &m_nodebankpanel.is_open); + if (ImGui::MenuItem(STR_C("Model sets"))) + m_nodebankpanel.open_sets_window(); + ImGui::MenuItem(STR_C("Layers"), nullptr, &m_layerspanel.is_open); + ImGui::MenuItem(STR_C("Include database"), nullptr, &m_includespanel.is_open); + ImGui::Separator(); + if (ImGui::MenuItem(STR_C("Reset window layout"))) + m_layoutreset = true; + std::vector pinned; + autohide_leaves(ImGui::DockBuilderGetNode(m_dockspace), pinned); + if (ImGui::MenuItem(pinned.empty() ? STR_C("Pin the docks back") : STR_C("Unpin all docks"), "Ctrl+Space", false, false == pinned.empty() || false == m_autohide.empty())) + toggle_docks(); +} + +int editor_ui::palette() const +{ + return EditorSettings.palette(); +} + +void editor_ui::set_file_actions(std::function Save, std::function Export) +{ + m_save = Save; + m_export = std::move(Export); + m_layerspanel.save = std::move(Save); +} + +void editor_ui::set_status(std::string const &Status, bool const Error) +{ + m_layerspanel.status = Status; + m_layerspanel.status_error = Error; +} + +void editor_ui::render_rotation_controls() +{ + m_functionspanel.render_controls(); } void editor_ui::set_node(scene::basic_node *Node) { m_node = Node; + m_itempropertiespanel.update(m_node); + m_functionspanel.update(m_node); +} + +void editor_ui::set_include(include_selection *Include) +{ + m_itempropertiespanel.include(Include); } void editor_ui::add_node_template(const std::string &desc) @@ -77,16 +1049,35 @@ void editor_ui::add_node_template(const std::string &desc) std::string const *editor_ui::get_active_node_template(bool bypassRandom) { - if (!bypassRandom && m_brushobjects.is_open && m_brushobjects.useRandom && m_brushobjects.Objects.size() > 0) + if (!bypassRandom) { - return m_brushobjects.GetRandomObject(); + // random pick from the active tool's set; an empty set falls back to the node bank selection + std::string const *pick{nullptr}; + if (m_nodebankpanel.mode == nodebank_panel::BRUSH && m_brushobjects.useRandom) + { + if (m_brushobjects.source.kind == model_set_ref::source::manual) + pick = m_brushobjects.Objects.empty() ? nullptr : m_brushobjects.GetRandomObject(); + else + pick = m_nodebankpanel.random_template(m_brushobjects.source); + } + else if (m_nodebankpanel.mode == nodebank_panel::ADD && m_insertrandom) + { + pick = m_nodebankpanel.random_template(m_insertset); + } + if (pick != nullptr) + return pick; } return m_nodebankpanel.get_active_template(); } nodebank_panel::edit_mode editor_ui::mode() { - return m_nodebankpanel.mode; + return m_track ? nodebank_panel::TRACK : m_nodebankpanel.mode; +} +void editor_ui::set_mode(nodebank_panel::edit_mode const Mode) +{ + m_nodebankpanel.mode = Mode; + m_nodebankpanel.is_open = true; } float editor_ui::getSpacing() { @@ -104,4 +1095,4 @@ float editor_ui::rot_val() bool editor_ui::rot_from_last() { return m_functionspanel.rot_from_last; -} \ No newline at end of file +} \ No newline at end of file diff --git a/application/editoruilayer.h b/application/editoruilayer.h index 57d75466..020fd4be 100644 --- a/application/editoruilayer.h +++ b/application/editoruilayer.h @@ -19,6 +19,9 @@ class basic_node; } +struct ImGuiDockNode; +struct ImGuiTabBar; + class editor_ui : public ui_layer { @@ -29,22 +32,227 @@ class editor_ui : public ui_layer // updates state of UI elements void update() override; void set_node(scene::basic_node *Node); + // include of a scenery template to show in the node properties in place of a node; nullptr if there's none + void set_include(include_selection *Include); void add_node_template(const std::string &desc); float rot_val(); bool rot_from_last(); functions_panel::rotation_mode rot_mode(); const std::string *get_active_node_template(bool bypassRandom = false); nodebank_panel::edit_mode mode(); + void set_mode(nodebank_panel::edit_mode const Mode); float getSpacing(); - void toggleBrushSettings(bool isVisible); - std::vector nodebank_groups() const; - std::vector nodebank_group_templates(std::size_t const Group) const; + // node bank and model set access (set selection widgets, set contents) + nodebank_panel &nodebank() + { + return m_nodebankpanel; + } + // rotation settings widgets, for tools drawn by the editor mode + void render_rotation_controls(); + // draws the area fill settings in its node bank tab (the fill state lives in the editor mode) + void set_fill_options(std::function Renderer) + { + m_filloptions = std::move(Renderer); + } + // the open track window takes the mouse over, whichever toolset tab is chosen + void set_track(bool const Track) + { + m_track = Track; + } + // draws menus of the editor mode in the menu bar, past the ones of the user interface + void set_menu_options(std::function Renderer) + { + m_menuoptions = std::move(Renderer); + } + // draws the tools of the editor mode in the toolbar under the menu + void set_toolbar_options(std::function Renderer) + { + m_toolbaroptions = std::move(Renderer); + } + // draws the edit menu of the editor mode, next to the file menu + void set_edit_menu(std::function Renderer) + { + m_editmenu = std::move(Renderer); + } + // draws the fields of work of the editor mode in a row above the toolbar + void set_workspace_bar(std::function Renderer) + { + m_workspacebar = std::move(Renderer); + } + // draws the gizmo settings in the toolset window (the gizmo state lives in the editor mode) + void set_gizmo_options(std::function Renderer) + { + m_gizmooptions = std::move(Renderer); + } + // draws the array settings in the toolset window (the array state lives in the editor mode) + void set_array_options(std::function Renderer) + { + m_arrayoptions = std::move(Renderer); + } + void set_bend_options(std::function Renderer) + { + m_bendoptions = std::move(Renderer); + } + void expand_bend() + { + m_bendexpand = true; + } + // actions of the file menu, performed by the editor mode + void set_file_actions(std::function Save, std::function Export); + void set_new_scenery(std::function New) { m_newscenery = std::move(New); } + void set_open_scenery(std::function Open) { m_openscenery = std::move(Open); } + // part of the main window between the docked windows, where the 3d view is seen; for the overlays placed in it + static glm::vec2 view_min() + { + return m_viewmin; + } + static glm::vec2 view_max() + { + return m_viewmax; + } + // tools of the track mode in the tool options window, the selected path in the inspector + void set_track_options(std::function Tools, std::function Selection) + { + m_tracktools = std::move(Tools); + m_trackselection = std::move(Selection); + } + // fields of work of the editor mode besides the edit modes, chosen in its toolbar: their tools are drawn in the tool options window, + // what they have selected in the inspector. the terrain is shown over the track mode, the roads give way to it + enum class workspace + { + none, + roads, + terrain, + count_ + }; + void set_workspace(workspace const Workspace) + { + m_workspace = Workspace; + auto const &bank{m_workspacebank[static_cast(Workspace)]}; + m_nodebankpanel.stand_in(bank.first, bank.second); + } + // content of the window of the node bank while the field of work is chosen, in place of the node bank + void set_workspace_bank(workspace const Workspace, std::function Content, std::string Title) + { + m_workspacebank[static_cast(Workspace)] = {std::move(Content), std::move(Title)}; + } + void set_workspace_options(workspace const Workspace, std::function Tools, std::function Selection = {}) + { + m_workspacetools[static_cast(Workspace)] = std::move(Tools); + m_workspaceselection[static_cast(Workspace)] = std::move(Selection); + } + // selection of a node in the scene window; Focus is set when the camera is to fly to it + void set_scene_select(std::function Select) + { + m_sceneselect = std::move(Select); + } + // pins of the docks: an unpinned dock hides its windows in a strip at the edge of the main window, and they come out over the 3d view + // when their tab in the strip is pointed at or clicked. the shortcut unpins all docks, or pins them back + void toggle_docks() + { + m_dockstoggle = true; + } + // the unpinned docks are kept in imgui.ini along with the layout; the handler has to be there before the file is read + static void register_settings(); + // shows outcome of an operation in the layers window + void set_status(std::string const &Status, bool const Error = false); private: + // methods + void render_mode_options(nodebank_panel::edit_mode const Mode); + // windows of the editor drawn by the user interface itself + void render_tool_options(); + void render_inspector(); + void render_scene(); + void scene_rebuild(); + void scene_row(std::size_t const Entry); + void render_menu_contents() override; + void render_windows_menu() override; + int palette() const override; + void render_dockspace() override; + void render_() override; + // docks the windows of the editor in their default places + void build_default_layout(unsigned int const Dockspace); + // unpinned docks: at the start of the frame the windows are hidden, docked back or placed over the view, at its end the strips and pins are drawn + void autohide_begin(); + void autohide_place(); + void autohide_strips(); + void autohide_pins(); + void autohide_show(unsigned int const Window, bool const Focus); + void autohide_pin(std::size_t const Group); + static void autohide_node(ImGuiDockNode *Node); + static std::size_t autohide_group_of(unsigned int const Window); + static void autohide_menu(ImGuiContext *Context, ImGuiDockNode *Node, ImGuiTabBar *TabBar); // members - itemproperties_panel m_itempropertiespanel{"Node Properties", true}; - functions_panel m_functionspanel{"Functions", true}; - nodebank_panel m_nodebankpanel{"Node Bank", true}; - brush_object_list m_brushobjects{"Brush properties", false}; + itemproperties_panel m_itempropertiespanel{"Node Properties", true}; // not a window of its own, drawn in the toolset window + functions_panel m_functionspanel{"Functions", true}; // not a window of its own, its settings are drawn in the toolset tabs + nodebank_panel m_nodebankpanel{"Node bank", true}; // templates to insert, paint or fill with + layers_panel m_layerspanel{"Layers", true}; + includes_panel m_includespanel{"Include database", false}; + std::function m_save; + std::function m_export; + std::function m_newscenery; + std::function m_openscenery; + brush_object_list m_brushobjects; + bool m_insertrandom{false}; // insert mode picks a random template from m_insertset + model_set_ref m_insertset; + std::function m_filloptions; + std::function m_menuoptions; + std::function m_toolbaroptions; + std::function m_workspacebar; + std::function m_editmenu; + bool m_layoutreset{false}; // the default layout is to be built again on the next frame + bool m_tooloptionsopen{true}; + bool m_inspectoropen{true}; + bool m_sceneopen{true}; + std::function m_sceneselect; + std::function m_tracktools; + std::function m_trackselection; + int m_toolmode{-1}; // edit mode or field of work the tool options were drawn for the last time + workspace m_workspace{workspace::none}; + std::array, static_cast(workspace::count_)> m_workspacetools; + std::array, static_cast(workspace::count_)> m_workspaceselection; + std::array, std::string>, static_cast(workspace::count_)> m_workspacebank; + // scene window: the models of the scenery by layer and group, rebuilt when their number changes + struct scene_entry + { + std::string uuid; // key in scene::Hierarchy, the node is looked up by it when drawn + scene::layer_handle layer; + scene::group_handle group; + std::string name; + }; + std::vector m_sceneentries; + std::size_t m_scenesize{static_cast(-1)}; + char m_scenefilter[64]{}; + std::string m_scenefilterused; + std::vector m_scenematches; + static glm::vec2 m_viewmin; + static glm::vec2 m_viewmax; + std::function m_gizmooptions; + std::function m_arrayoptions; + std::function m_bendoptions; + bool m_bendexpand{false}; scene::basic_node *m_node{nullptr}; // currently bound scene node, if any + bool m_track{false}; + // windows of an unpinned dock, and the edge they're hidden at + struct autohide_group + { + int edge{0}; // ImGuiDir + float size{300.0f}; // width of the side docks, height of the top and bottom ones, in pixels + bool first{false}; // the node was above or left of the other one in its split, and took this share of it + float share{0.5f}; + int depth{0}; // of the column of docks the node was in, in the tree of the dockspace; outer docks are put back first + std::vector windows; + }; + static std::vector m_autohide; + static unsigned int m_dockspace; + unsigned int m_autohideshown{0}; // window of an unpinned dock shown over the view + bool m_autohideappear{false}; // the shown window is to be brought forward + bool m_autohidefocus{false}; // and given the focus, it was clicked in the strip + unsigned int m_autohidehovered{0}; // tab in the strips under the cursor, and since when + double m_autohidehovertime{0.0}; + double m_autohideleft{-1.0}; // when the cursor left the shown window, -1 while it's over it + bool m_autohidestrip{false}; // the cursor was over a tab of the strips in the last frame + std::vector m_autohidepin; // groups to be docked back at the start of the next frame + bool m_dockstoggle{false}; }; diff --git a/application/editoruipanels.cpp b/application/editoruipanels.cpp index 80eec0ba..79808c2d 100644 --- a/application/editoruipanels.cpp +++ b/application/editoruipanels.cpp @@ -10,6 +10,7 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "application/editoruipanels.h" #include "scene/scenenodegroups.h" +#include "scene/scenelayers.h" #include "utilities/Globals.h" #include "vehicle/Camera.h" @@ -19,6 +20,111 @@ http://mozilla.org/MPL/2.0/. #include "world/MemCell.h" #include "application/editoruilayer.h" #include "rendering/renderer.h" +#include "simulation/simulation.h" +#include "editor/editorModelSets.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorSettings.hpp" +#include "imgui/imgui_internal.h" + +#include +#include + +namespace +{ +// tells why provided text can't be the name of a node, empty text if it can. the scenery parser has to get it back as a single token +std::string node_name_issue(std::string const &Name) +{ + if (Name.find_first_of(" \t\r\n;\"") != std::string::npos) + { + return "a name can't hold spaces, semicolons or quotes"; + } + if (Name.find("//") != std::string::npos || Name.find("/*") != std::string::npos) + { + return "a name can't hold a comment mark"; + } + if (Name == "include") + { + return "this word has a meaning of its own in scenery files"; + } + return {}; +} + +// list entry; the selected one is drawn with the accent colour, like selections in the starter +bool list_item(char const *Label, bool const Selected) +{ + if (Selected) + ImGui::PushStyleColor(ImGuiCol_Header, ImGui::GetStyleColorVec4(ImGuiCol_HeaderActive)); + auto const clicked{ImGui::Selectable(Label, Selected)}; + if (Selected) + ImGui::PopStyleColor(); + return clicked; +} + +// name of the payload of a node definition dragged out of the node bank +char const *const template_payload{"EU07_NODEBANK_ENTRY"}; + +// a node definition of the node bank dropped on the item drawn last. returns: true if one was, with its text +bool dropped_template(std::string &Template) +{ + auto dropped{false}; + if (ImGui::BeginDragDropTarget()) + { + if (auto const *payload{ImGui::AcceptDragDropPayload(template_payload)}; payload != nullptr && payload->DataSize > 0) + { + Template.assign(static_cast(payload->Data), static_cast(payload->DataSize)); + dropped = true; + } + ImGui::EndDragDropTarget(); + } + return dropped; +} + +// a card: the image fills its square, or what's missing is said in its place, and the label is under it, cut short with an +// ellipsis. returns: true if it was clicked; the card is the item drawn last +bool card(ImVec2 const &Cell, float const Side, std::uint64_t const Texture, char const *Placeholder, char const *Label, bool const Selected) +{ + auto const &style{ImGui::GetStyle()}; + auto const position{ImGui::GetCursorScreenPos()}; + auto const clicked{ImGui::InvisibleButton("##card", Cell)}; + auto const hovered{ImGui::IsItemHovered()}; + auto *list{ImGui::GetWindowDrawList()}; + ImVec2 const end{position.x + Cell.x, position.y + Cell.y}; + list->AddRectFilled(position, end, ImGui::GetColorU32(Selected ? ImGuiCol_Header : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_FrameBg), style.FrameRounding); + ImVec2 const imagemin{position.x + 2.0f, position.y + 2.0f}; + ImVec2 const imagemax{position.x + Side - 2.0f, position.y + Side - 2.0f}; + if (Texture != 0) + { + list->AddImage(static_cast(Texture), imagemin, imagemax); + } + else + { + list->AddRect(imagemin, imagemax, ImGui::GetColorU32(ImGuiCol_Border), style.FrameRounding); + auto const size{ImGui::CalcTextSize(Placeholder)}; + list->PushClipRect(imagemin, imagemax, true); + list->AddText(ImVec2(imagemin.x + std::max(0.0f, (imagemax.x - imagemin.x - size.x) * 0.5f), imagemin.y + (imagemax.y - imagemin.y - size.y) * 0.5f), ImGui::GetColorU32(ImGuiCol_TextDisabled), Placeholder); + list->PopClipRect(); + } + while (*Label == ' ') + { + ++Label; + } + ImVec2 const labelmin{position.x + style.FramePadding.x, position.y + Side + style.FramePadding.y}; + ImVec2 const labelmax{end.x - style.FramePadding.x, end.y}; + ImGui::RenderTextEllipsis(list, labelmin, labelmax, labelmax.x, labelmax.x, Label, nullptr, nullptr); + return clicked; +} + +// cells of the cards of the side, and how many of them fit the width in a row +ImVec2 card_cell(float const Side) +{ + return ImVec2(Side, Side + ImGui::GetTextLineHeight() + ImGui::GetStyle().FramePadding.y * 2.0f); +} +int card_columns(ImVec2 const &Cell) +{ + auto const &style{ImGui::GetStyle()}; + return std::max(1, static_cast((ImGui::GetContentRegionAvail().x + style.ItemSpacing.x) / (Cell.x + style.ItemSpacing.x))); +} +} // namespace void itemproperties_panel::update(scene::basic_node *Node) { @@ -63,6 +169,10 @@ void itemproperties_panel::update(scene::basic_node *Node) textline = "name: " + (node->name().empty() ? "(none)" : Bezogonkow(node->name())) + "\ntype: " + node->node_type + "\nlocation: [" + to_string(node->location().x, 2) + ", " + to_string(node->location().y, 2) + ", " + to_string(node->location().z, 2) + "]" + " (distance: " + to_string(glm::length(glm::dvec3{node->location().x, 0.0, node->location().z} - glm::dvec3{camera.Pos.x, 0.0, camera.Pos.z}), 1) + " m)" + "\nUUID: " + node->uuid.to_string(); + if (scene::Layers.valid(node->layer())) + { + textline += "\nlayer: " + Bezogonkow(scene::Layers.layer(node->layer()).name); + } text_lines.emplace_back(textline, Global.UITextColor); // subclass-specific data @@ -285,19 +395,92 @@ void itemproperties_panel::render() auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) { - // header section - for (auto const &line : text_lines) - { - ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str()); - } - // transform editor (position/rotation/scale) — TAnimModel only - render_transform_editor(); - // group section - render_group(); + render_body(); } ImGui::End(); } +void itemproperties_panel::render_body() +{ + if (m_include != nullptr) + { + render_include(); + return; + } + // header section + for (auto const &line : text_lines) + { + ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str()); + } + // name of the node — TAnimModel only + render_name_editor(); + // transform editor (position/rotation/scale) — TAnimModel only + render_transform_editor(); + // group section + render_group(); +} + +// Name of a picked TAnimModel. The editor places decorations without names, as each name takes its share of the time +// the scenery needs to load; this is where an instance gets one, for the events to refer to it. +void itemproperties_panel::render_name_editor() +{ + if (m_node == nullptr) { return; } + if (typeid(*m_node) != typeid(TAnimModel)) { return; } + auto *picked = static_cast(m_node); + // the name of an instance defined by a template, or by a file which can't be rewritten, wouldn't make it to the scenery files + if (false == scene::Layers.editable(picked)) { return; } + + if (m_namednode != m_node || false == m_nameactive) + { + // the field shows the name of the node unless it's being typed in + if (m_namednode != m_node) + { + m_nameissue.clear(); + } + m_namednode = m_node; + std::snprintf(m_namebuffer, sizeof(m_namebuffer), "%s", picked->name().c_str()); + } + auto const entered{ImGui::InputTextWithHint(STR_C("name"), STR_C("(none)"), m_namebuffer, IM_ARRAYSIZE(m_namebuffer), ImGuiInputTextFlags_EnterReturnsTrue)}; + m_nameactive = ImGui::IsItemActive(); + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it")); + } + if (entered || ImGui::IsItemDeactivatedAfterEdit()) + { + // names are lower case, like everything else the scenery parser hands over + std::string name{m_namebuffer}; + for (auto &character : name) + { + if (character >= 'A' && character <= 'Z') + { + character = static_cast(character - 'A' + 'a'); + } + } + if (name == "none") + { + name.clear(); + } + if (name != picked->name()) + { + m_nameissue = node_name_issue(name); + if (m_nameissue.empty() && false == simulation::State.rename_model(picked, name)) + { + m_nameissue = "another model instance goes by this name"; + } + } + else + { + m_nameissue.clear(); + } + m_nameactive = false; + } + if (false == m_nameissue.empty()) + { + ImGui::TextColored(ImVec4(1.0f, 0.35f, 0.3f, 1.0f), "%s", m_nameissue.c_str()); + } +} + // In-place editor for position (double precision), rotation (degrees, 0-360), // and uniform scale (per-axis float, 1.000) of a picked TAnimModel. // Other node subclasses don't expose these knobs through the same API, so the @@ -308,7 +491,7 @@ void itemproperties_panel::render_transform_editor() if (typeid(*m_node) != typeid(TAnimModel)) { return; } auto *picked = static_cast(m_node); - if (false == ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) + if (false == ImGui::CollapsingHeader(STR_C("Transform"), ImGuiTreeNodeFlags_DefaultOpen)) { return; } @@ -330,7 +513,7 @@ void itemproperties_panel::render_transform_editor() clamp_circular(picked->Angles().x), clamp_circular(picked->Angles().y), clamp_circular(picked->Angles().z)}; - if (ImGui::DragFloat3("rotation (deg)", &angles.x, 0.5f, 0.0f, 360.0f, "%.3f")) + if (ImGui::DragFloat3(STR_C("rotation (deg)"), &angles.x, 0.5f, 0.0f, 360.0f, "%.3f")) { picked->Angles(angles); } @@ -339,15 +522,15 @@ void itemproperties_panel::render_transform_editor() // Scale — per-axis float, 1.000 display. Clamped to a reasonable positive range. { glm::vec3 scale = picked->Scale(); - if (ImGui::DragFloat3("scale (x,y,z)", &scale.x, 0.01f, 0.001f, 100.0f, "%.3f")) + if (ImGui::DragFloat3(STR_C("scale (x,y,z)"), &scale.x, 0.01f, 0.001f, 100.0f, "%.3f")) { picked->Scale(scale); } } - if (ImGui::Button("reset rotation")) { picked->Angles(glm::vec3(0.0f)); } + if (ImGui::Button(STR_C("reset rotation"))) { picked->Angles(glm::vec3(0.0f)); } ImGui::SameLine(); - if (ImGui::Button("reset scale")) { picked->Scale(glm::vec3(1.0f)); } + if (ImGui::Button(STR_C("reset scale"))) { picked->Scale(glm::vec3(1.0f)); } } bool itemproperties_panel::render_group() @@ -362,7 +545,7 @@ bool itemproperties_panel::render_group() return false; } - if (false == ImGui::CollapsingHeader("Parent Group")) + if (false == ImGui::CollapsingHeader(STR_C("Parent Group"))) { return false; } @@ -375,27 +558,6 @@ bool itemproperties_panel::render_group() return true; } -brush_object_list::brush_object_list(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) -{ - size_min = {50, 100}; - size_max = {1000, 500}; -} - -bool brush_object_list::VectorGetter(void *data, int idx, const char **out_text) -{ - auto *vec = static_cast *>(data); - - if (idx < 0 || idx >= vec->size()) - return false; - - *out_text = (*vec)[idx].c_str(); - return true; -} - -void brush_object_list::update(std::string nodeTemplate) -{ - Template = nodeTemplate; -} std::string *brush_object_list::GetRandomObject() { static std::string empty; // fallback @@ -408,39 +570,25 @@ std::string *brush_object_list::GetRandomObject() return &Objects[dist(Global.local_random_engine)]; } -void brush_object_list::render() +void brush_object_list::render_options(nodebank_panel &Bank) { - if (false == is_open) + ImGui::SliderFloat(STR_C("Spacing"), &spacing, 0.1f, 20.0f, "%.1f m"); + ImGui::Checkbox(STR_C("Random model from set"), &useRandom); + if (false == useRandom) { + ImGui::TextDisabled(STR_C("Paints the template selected in the node bank")); return; } - - auto flags = ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse | (size.x > 0 ? ImGuiWindowFlags_NoResize : 0); - if (ImGui::Begin("Brush random set", nullptr, flags)) + Bank.set_combo(STR_C("Preset"), source, STR_C("Manual list")); + if (source.kind == model_set_ref::source::manual) { - ImGui::SliderFloat("Spacing", &spacing, 0.1f, 20.0f, "%.1f m"); - ImGui::Checkbox("Enable brush random from set", &useRandom); - if (useRandom) - { - ImGui::Text("Set of objects to choose from:"); - ImGui::ListBox("", &idx, VectorGetter, (void *)&Objects, Objects.size(), 6); - if (ImGui::Button("Add")) - { - if (!Template.empty()) - Objects.push_back(Template); - } - ImGui::SameLine(); - if (ImGui::Button("Remove") && idx >= 0 && idx < Objects.size()) - { - Objects.erase(Objects.begin() + idx); - } - if (ImGui::Button("Remove all")) - { - Objects.clear(); - } - } + Bank.manual_list("brushset", Objects, idx); + } + else + { + auto const count{Bank.set_entries(source).size()}; + ImGui::TextDisabled(STR_C("%zu templates in set%s"), count, count == 0 ? ", the node bank selection is used" : ""); } - ImGui::End(); } nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) @@ -466,6 +614,7 @@ nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_ m_nodebank.push_back({label, std::make_shared(nodedata)}); } + index_previews(); // sort alphabetically content of each group auto groupbegin{m_nodebank.begin()}; auto groupend{groupbegin}; @@ -479,6 +628,15 @@ nodebank_panel::nodebank_panel(std::string const &Name, bool const Isopen) : ui_ void nodebank_panel::nodebank_reload() { m_nodebank.clear(); + // images made since are read anew + if (m_previews) + { + m_previews->clear(); + } + if (m_largepreviews) + { + m_largepreviews->clear(); + } std::ifstream file; file.open("nodebank.txt", std::ios_base::in | std::ios_base::binary); std::string line; @@ -494,6 +652,7 @@ void nodebank_panel::nodebank_reload() m_nodebank.push_back({label, std::make_shared(nodedata)}); } + index_previews(); // sort alphabetically content of each group auto groupbegin{m_nodebank.begin()}; auto groupend{groupbegin}; @@ -505,6 +664,255 @@ void nodebank_panel::nodebank_reload() } } +void nodebank_panel::index_previews() +{ + m_previewpaths.clear(); + m_templatepreviews.clear(); + std::error_code error; + m_previewfolder = std::filesystem::is_directory(editor_previews::folder, error); + // entries with the same model and skin are numbered in the order of the file, the way the generator numbers them + std::set taken; + for (auto const &entry : m_nodebank) + { + if (entry.second->empty()) + { + continue; + } + auto const path{editor_previews::entry_preview_path(*entry.second)}; + if (path.empty()) + { + continue; + } + auto const name{editor_previews::variant(path, taken)}; + taken.insert(name); + m_previewpaths[entry.second.get()] = name + ".png"; + m_templatepreviews.try_emplace(*entry.second, name + ".png"); + } +} + +void nodebank_panel::preview_popup(std::pair> const &Entry) +{ + // not while something's dragged, the tooltip of the drag is there then + if (false == ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay) || ImGui::GetDragDropPayload() != nullptr) + { + return; + } + // the generator makes the images 1024 pixels big; they're shown half as big, as much as the window has room for + auto const size{std::clamp(static_cast(512.0f * Global.ui_scale), 256, 1024)}; + if (m_largepreviews == nullptr) + { + m_largepreviews = std::make_unique(size, 16); + } + auto const *label{Entry.first.c_str()}; + while (*label == ' ') + { + ++label; + } + ImGui::BeginTooltip(); + ImGui::TextUnformatted(label); + std::string model, skin; + if (editor_previews::entry_model(*Entry.second, model, skin)) + { + ImGui::TextDisabled("%s%s%s", model.c_str(), (skin.empty() || skin == "none") ? "" : " ", (skin.empty() || skin == "none") ? "" : skin.c_str()); + } + auto const lookup{m_previewpaths.find(Entry.second.get())}; + if (lookup != m_previewpaths.end()) + { + auto const side{std::min(static_cast(size), ImGui::GetMainViewport()->WorkSize.y * 0.6f)}; + auto texture{m_largepreviews->image(lookup->second)}; + if (texture == 0 && m_previews) + { + // the small image, until the big one is read + texture = m_previews->image(lookup->second); + } + if (texture != 0) + { + ImGui::Image(static_cast(texture), ImVec2(side, side)); + } + else + { + ImGui::TextDisabled("%s", m_largepreviews->missing(lookup->second) ? STR_C("no preview") : "..."); + } + } + ImGui::EndTooltip(); +} + +std::uint64_t nodebank_panel::preview_texture(std::string const &Path, char const *&Placeholder) +{ + if (Path.empty() || m_previews == nullptr) + { + Placeholder = STR_C("not a model"); + return 0; + } + auto const texture{m_previews->image(Path)}; + Placeholder = (m_previews->missing(Path) ? STR_C("no preview") : "..."); + return texture; +} + +std::string const &nodebank_panel::template_preview(std::string const &Template) const +{ + static std::string const none; + auto const lookup{m_templatepreviews.find(Template)}; + return lookup != m_templatepreviews.end() ? lookup->second : none; +} + +void nodebank_panel::drag_entry(std::pair> const &Entry) +{ + // the scenery templates are placed with include directives, the lists of the models have no use for them + if (Entry.second->empty() || Entry.second->starts_with(editor_includes::directive_mark)) + { + return; + } + if (false == ImGui::BeginDragDropSource()) + { + return; + } + ImGui::SetDragDropPayload(template_payload, Entry.second->data(), Entry.second->size()); + auto const lookup{m_previewpaths.find(Entry.second.get())}; + if (lookup != m_previewpaths.end() && m_previews != nullptr) + { + if (auto const texture{m_previews->image(lookup->second)}; texture != 0) + { + auto const side{64.0f * std::max(1.0f, Global.ui_scale)}; + ImGui::Image(static_cast(texture), ImVec2(side, side)); + } + } + auto const *label{Entry.first.c_str()}; + while (*label == ' ') + { + ++label; + } + ImGui::TextUnformatted(label); + ImGui::TextDisabled("%s", STR_C("Drop it on a model set, or on the list of the brush or the area fill")); + ImGui::EndDragDropSource(); +} + +void nodebank_panel::render_cards(std::vector> const *> const &Entries) +{ + auto const &style{ImGui::GetStyle()}; + auto const side{std::max(16.0f, EditorSettings.nodebank_card() * Global.ui_scale)}; + auto const cell{card_cell(side)}; + auto const columns{card_columns(cell)}; + auto const rows{(static_cast(Entries.size()) + columns - 1) / columns}; + ImGuiListClipper clipper; + clipper.Begin(rows, cell.y + style.ItemSpacing.y); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + for (int column = 0; column < columns; ++column) + { + auto const index{static_cast(row * columns + column)}; + if (index >= Entries.size()) + { + break; + } + auto const &entry{*Entries[index]}; + if (column > 0) + { + ImGui::SameLine(); + } + ImGui::PushID(entry.second.get()); + auto const lookup{m_previewpaths.find(entry.second.get())}; + char const *placeholder{nullptr}; + auto const texture{preview_texture(lookup != m_previewpaths.end() ? lookup->second : std::string{}, placeholder)}; + if (card(cell, side, texture, placeholder, entry.first.c_str(), entry.second == m_selectedtemplate)) + { + m_selectedtemplate = entry.second; + } + drag_entry(entry); + if (ImGui::IsItemHovered()) + { + preview_popup(entry); + } + ImGui::PopID(); + } + } + } + clipper.End(); +} + +void nodebank_panel::render_list(std::vector> const *> const &Entries) +{ + for (auto const *entry : Entries) + { + ImGui::PushID(entry->second.get()); + auto const label{" " + entry->first}; + if (list_item(label.c_str(), entry->second == m_selectedtemplate)) + { + m_selectedtemplate = entry->second; + } + drag_entry(*entry); + preview_popup(*entry); + ImGui::PopID(); + } +} + +void nodebank_panel::render_templates_list(std::string const &Filter) +{ + if (false == EditorIncludes.scanned()) + { + EditorIncludes.scan(); + } + auto const &templates{EditorIncludes.entries()}; + std::string const *category{nullptr}; + auto listed{0}; + for (auto const entryidx : EditorIncludes.ready()) + { + auto const &entry{templates[entryidx]}; + if (false == Filter.empty() && false == contains(entry.name, Filter) && false == contains(entry.file, Filter) && false == contains(entry.category, Filter)) + { + continue; + } + if (false == entry.category.empty() && (category == nullptr || *category != entry.category)) + { + ImGui::TextDisabled(" %s", entry.category.c_str()); + } + category = &entry.category; + auto const label{(entry.category.empty() ? " " : " ") + entry.name + " (" + Bezogonkow(entry.file) + ")##inc" + std::to_string(entryidx)}; + if (list_item(label.c_str(), entry.statement == m_selectedtemplate)) + m_selectedtemplate = entry.statement; + ++listed; + } + if (listed == 0) + { + ImGui::TextDisabled(EditorIncludes.ready().empty() ? " (none yet, describe the templates in the Include database window)" : " (no match)"); + } +} + +void nodebank_panel::render_set_cards(int const Setid) +{ + auto const *set{EditorModelSets.find(Setid)}; + if (set == nullptr) + { + return; + } + // a bit smaller than in the node bank, the window of the sets is the smaller one + auto const side{std::max(48.0f, EditorSettings.nodebank_card() * Global.ui_scale * 0.75f)}; + auto const cell{card_cell(side)}; + auto const columns{card_columns(cell)}; + for (int index = 0; index < static_cast(set->templates.size()); ++index) + { + if (index % columns != 0) + { + ImGui::SameLine(); + } + ImGui::PushID(index); + char const *placeholder{nullptr}; + auto const texture{preview_texture(template_preview(set->templates[index]), placeholder)}; + auto const &label{index < static_cast(set->labels.size()) ? set->labels[index] : set->templates[index]}; + if (card(cell, side, texture, placeholder, label.c_str(), index == m_setsentry)) + { + m_setsentry = index; + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", label.c_str()); + } + ImGui::PopID(); + } +} + void nodebank_panel::render() { if (false == is_open) @@ -518,68 +926,585 @@ void nodebank_panel::render() { ImGui::SetNextWindowSize(ImVec2S(size.x, size.y)); } + else + { + ImGui::SetNextWindowSize(ImVec2S(440, 400), ImGuiCond_FirstUseEver); + } if (size_min.x > 0) { ImGui::SetNextWindowSizeConstraints(ImVec2S(size_min.x, size_min.y), ImVec2S(size_max.x, size_max.y)); } + if (m_standin) + { + // the same window, so it keeps its place in the dock + if (ImGui::Begin((m_standintitle + "###" + name()).c_str(), nullptr, flags)) + m_standin(); + ImGui::End(); + return; + } auto const panelname{(title.empty() ? name() : title) + "###" + name()}; if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) { - - ImGui::RadioButton("Modify node", (int *)&mode, MODIFY); - ImGui::SameLine(); - ImGui::RadioButton("Insert from bank", (int *)&mode, ADD); - ImGui::SameLine(); - ImGui::RadioButton("Brush mode", (int *)&mode, BRUSH); - ImGui::SameLine(); - ImGui::RadioButton("Copy to bank", (int *)&mode, COPY); - ImGui::SameLine(); - ImGui::RadioButton("Area fill", (int *)&mode, FILL); - ImGui::SameLine(); - if (ImGui::Button("Reload Nodebank")) + if (m_largepreviews) + { + m_largepreviews->update(); + } + if (ImGui::Button(STR_C("Reload node bank"))) { nodebank_reload(); } - - if (mode == BRUSH) + ImGui::SameLine(); + if (ImGui::Button(m_setsopen ? "Close model sets" : "Model sets...")) { - // ImGui::SliderFloat("Spacing", &spacing, 0.1f, 20.0f, "%.1f m"); + m_setsopen = !m_setsopen; + } + // cards with the previews of the models, or the list + ImGui::SameLine(); + auto previews{EditorSettings.nodebank_previews()}; + if (ImGui::Checkbox(STR_C("Previews"), &previews)) + { + EditorSettings.nodebank_previews(previews); + EditorSettings.save(); + } + if (previews) + { + ImGui::SameLine(); + auto card{EditorSettings.nodebank_card()}; + ImGui::SetNextItemWidth(std::min(ImGui::GetContentRegionAvail().x, 120.0f * Global.ui_scale)); + if (ImGui::SliderInt("##cardsize", &card, 48, 256, STR_C("%d px"))) + { + EditorSettings.nodebank_card(card); + } + if (ImGui::IsItemDeactivatedAfterEdit()) + { + EditorSettings.save(); + } } + // the edit modes are chosen in the toolbar and set up in the tool options window, the gizmo and the node + // properties are in the inspector: what's left here is the node bank itself + ImGui::Separator(); ImGui::PushItemWidth(-1); - ImGui::InputTextWithHint("Search", "Search node bank", m_nodesearch, IM_ARRAYSIZE(m_nodesearch)); - if (ImGui::ListBoxHeader("##nodebank", ImVec2(-1, -1))) + ImGui::InputTextWithHint(STR_C("Search"), STR_C("Search node bank"), m_nodesearch, IM_ARRAYSIZE(m_nodesearch)); + // the list takes the rest of the window, but keeps a usable height when the sections above are expanded (the window scrolls then) + auto const listheight{std::max(ImGui::GetContentRegionAvail().y, ImGui::GetTextLineHeightWithSpacing() * 10.0f)}; + if (previews) { - auto idx{0}; - auto isvisible{false}; - auto const searchfilter{std::string(m_nodesearch)}; - for (auto const &entry : m_nodebank) + if (m_previews == nullptr) { - if (entry.second->empty()) + m_previews = std::make_unique(); + } + m_previews->update(); + if (false == m_previewfolder) + { + ImGui::TextDisabled("%s", STR_C("No previews yet: they're made by eu07 -generate-nodebank-previews")); + } + } + // two panes: the groups of the node bank on the left, like folders, and the entries of the chosen one on the right. + // a search goes through all of them + struct category + { + std::string const *name; + std::vector> const *> entries; + int matches{0}; + }; + static std::string const ungrouped{"(ungrouped)"}; + auto const searchfilter{std::string(m_nodesearch)}; + std::vector categories; + for (auto const &entry : m_nodebank) + { + if (entry.second->empty()) + { + categories.push_back({&entry.first, {}}); + continue; + } + if (categories.empty()) + { + // templates listed before the first header, as group_names() has them + categories.push_back({&ungrouped, {}}); + } + auto &group{categories.back()}; + group.entries.push_back(&entry); + if (false == searchfilter.empty() && contains(entry.first, searchfilter)) + { + ++group.matches; + } + } + auto const templates{Global.editor_session}; + if (m_category >= static_cast(categories.size()) || (m_category == scenery_templates && false == templates) || m_category < scenery_templates) + { + m_category = 0; + } + // the panes of the height of the table, inside the padding of its cells + auto const paneheight{listheight - 2.0f * ImGui::GetStyle().CellPadding.y - 1.0f}; + if (ImGui::BeginTable("##nodebankpanes", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV, ImVec2(-1, listheight))) + { + ImGui::TableSetupColumn("##groups", ImGuiTableColumnFlags_WidthFixed, 170.0f * Global.ui_scale); + ImGui::TableSetupColumn("##entries", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + if (ImGui::BeginChild("##groups", ImVec2(0, paneheight))) + { + auto const searching{false == searchfilter.empty()}; + for (int index = 0; index < static_cast(categories.size()); ++index) { - // special case, header indicator - isvisible = ImGui::CollapsingHeader(entry.first.c_str()); + auto const &group{categories[index]}; + auto const count{searching ? group.matches : static_cast(group.entries.size())}; + auto const label{*group.name + " (" + std::to_string(count) + ")##group" + std::to_string(index)}; + if (searching && count == 0) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + } + // a group chosen while searching is shown whole, the search is done + if (list_item(label.c_str(), false == searching && index == m_category)) + { + m_category = index; + m_nodesearch[0] = '\0'; + } + if (searching && count == 0) + { + ImGui::PopStyleColor(); + } } - else + if (templates) { - if (false == isvisible) + ImGui::Separator(); + if (list_item(STR_C("Scenery templates (.inc)"), m_category == scenery_templates)) { - continue; + m_category = scenery_templates; } - if (false == searchfilter.empty() && false == contains(entry.first, searchfilter)) - { - continue; - } - auto const label{" " + entry.first + "##" + std::to_string(idx)}; - if (ImGui::Selectable(label.c_str(), entry.second == m_selectedtemplate)) - m_selectedtemplate = entry.second; - ++idx; } } - ImGui::ListBoxFooter(); + ImGui::EndChild(); + ImGui::TableSetColumnIndex(1); + if (ImGui::BeginChild("##entries", ImVec2(0, paneheight))) + { + if (m_category == scenery_templates) + { + render_templates_list(searchfilter); + } + else if (false == searchfilter.empty()) + { + // what was found, under the names of the groups + std::vector> const *> found; + for (auto const &group : categories) + { + if (group.matches == 0) + { + continue; + } + found.clear(); + for (auto const *entry : group.entries) + { + if (contains(entry->first, searchfilter)) + { + found.push_back(entry); + } + } + ImGui::SeparatorText(group.name->c_str()); + if (previews) + { + render_cards(found); + } + else + { + render_list(found); + } + } + } + else if (false == categories.empty()) + { + if (previews) + { + render_cards(categories[m_category].entries); + } + else + { + render_list(categories[m_category].entries); + } + } + } + ImGui::EndChild(); + ImGui::EndTable(); + } + ImGui::PopItemWidth(); + } + + ImGui::End(); + + if (m_setsopen) + { + render_sets_window(); + } +} + +bool nodebank_panel::set_combo(char const *Label, model_set_ref &Ref, char const *Manuallabel) +{ + auto changed{false}; + auto const preview{set_name(Ref, Manuallabel)}; + if (false == ImGui::BeginCombo(Label, preview.c_str())) + { + return false; + } + if (Manuallabel != nullptr) + { + if (ImGui::Selectable(Manuallabel, Ref.kind == model_set_ref::source::manual)) + { + Ref = model_set_ref{}; + changed = true; } } + ImGui::TextDisabled(STR_C("User sets")); + auto const &sets{EditorModelSets.sets()}; + if (sets.empty()) + { + ImGui::TextDisabled(STR_C(" (none yet, see Model sets...)")); + } + for (auto const &set : sets) + { + auto const label{" " + set.name + " (" + std::to_string(set.templates.size()) + ")##userset" + std::to_string(set.id)}; + if (ImGui::Selectable(label.c_str(), Ref.kind == model_set_ref::source::user && Ref.id == set.id)) + { + Ref = {model_set_ref::source::user, set.id}; + changed = true; + } + } + ImGui::TextDisabled(STR_C("Node bank groups")); + auto const groups{group_names()}; + for (int idx = 0; idx < static_cast(groups.size()); ++idx) + { + auto const label{" " + groups[idx] + "##bankgroup" + std::to_string(idx)}; + if (ImGui::Selectable(label.c_str(), Ref.kind == model_set_ref::source::nodebank && Ref.id == idx)) + { + Ref = {model_set_ref::source::nodebank, idx}; + changed = true; + } + } + ImGui::EndCombo(); + return changed; +} + +void nodebank_panel::manual_list(char const *Id, std::vector &List, int &Selected) +{ + ImGui::PushID(Id); + if (ImGui::BeginListBox("##list", ImVec2(-1, ImGui::GetTextLineHeightWithSpacing() * 6.5f))) + { + for (int idx = 0; idx < static_cast(List.size()); ++idx) + { + auto const label{generate_node_label(List[idx]) + "##" + std::to_string(idx)}; + if (list_item(label.c_str(), Selected == idx)) + Selected = idx; + } + if (List.empty()) + { + ImGui::TextDisabled(STR_C("(empty)")); + } + ImGui::EndListBox(); + } + // a model dragged from the node bank onto the list goes to its end + if (std::string dropped; dropped_template(dropped)) + { + List.push_back(dropped); + } + if (ImGui::Button(STR_C("Add selected"))) + { + if (node_selected()) + List.push_back(*m_selectedtemplate); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list")); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Remove")) && Selected >= 0 && Selected < static_cast(List.size())) + { + List.erase(List.begin() + Selected); + Selected = -1; + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Clear"))) + { + List.clear(); + Selected = -1; + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Save as set")) && false == List.empty()) + { + open_sets_window(EditorModelSets.create("New set", List)); + } + ImGui::PopID(); +} + +std::vector nodebank_panel::set_entries(model_set_ref const &Ref) const +{ + std::vector entries; + if (Ref.kind == model_set_ref::source::user) + { + if (auto const *set = EditorModelSets.find(Ref.id)) + { + for (auto const &entry : set->templates) + entries.push_back(&entry); + } + } + else if (Ref.kind == model_set_ref::source::nodebank && Ref.id >= 0) + { + // same grouping as group_templates(), without copying the templates + int group{0}; + bool started{false}; + for (auto const &entry : m_nodebank) + { + if (entry.second->empty()) + { + if (started) + ++group; + started = true; + continue; + } + started = true; + if (group == Ref.id) + entries.push_back(entry.second.get()); + else if (group > Ref.id) + break; + } + } + return entries; +} + +std::string const *nodebank_panel::random_template(model_set_ref const &Ref) const +{ + auto const entries{set_entries(Ref)}; + if (entries.empty()) + return nullptr; + + std::uniform_int_distribution dist(0, entries.size() - 1); + return entries[dist(Global.local_random_engine)]; +} + +std::string nodebank_panel::set_name(model_set_ref const &Ref, char const *Manuallabel) const +{ + switch (Ref.kind) + { + case model_set_ref::source::user: + { + auto const *set = EditorModelSets.find(Ref.id); + return set != nullptr ? set->name : "(deleted set)"; + } + case model_set_ref::source::nodebank: + { + auto const groups{group_names()}; + return Ref.id >= 0 && Ref.id < static_cast(groups.size()) ? "Node bank: " + groups[Ref.id] : "(missing node bank group)"; + } + default: + return Manuallabel != nullptr ? Manuallabel : "(none)"; + } +} + +void nodebank_panel::open_sets_window(int const Setid) +{ + m_setsopen = true; + if (Setid != 0) + { + m_setsselected = Setid; + m_setsentry = -1; + } +} + +void nodebank_panel::render_sets_window() +{ + ImGui::SetNextWindowSize(ImVec2S(600, 380), ImGuiCond_FirstUseEver); + if (false == ImGui::Begin((std::string(STR_C("Model sets")) + "###modelsets").c_str(), &m_setsopen, ImGuiWindowFlags_NoCollapse)) + { + ImGui::End(); + return; + } + + auto const &sets{EditorModelSets.sets()}; + if (EditorModelSets.find(m_setsselected) == nullptr) + { + m_setsselected = sets.empty() ? 0 : sets.front().id; + m_setsentry = -1; + } + + // left side: the sets + auto const buttonsheight{ImGui::GetFrameHeightWithSpacing()}; + ImGui::BeginGroup(); + // the list of the sets takes up to a part of a narrow window, the templates of the set need the room + ImGui::BeginChild("##setlist", ImVec2(std::min(190.0f * Global.ui_scale, ImGui::GetContentRegionAvail().x * 0.4f), -buttonsheight), true); + for (auto const &set : sets) + { + auto const label{set.name + " (" + std::to_string(set.templates.size()) + ")##set" + std::to_string(set.id)}; + if (list_item(label.c_str(), set.id == m_setsselected)) + { + m_setsselected = set.id; + m_setsentry = -1; + } + // a model dragged from the node bank onto a set goes to its end + if (std::string dropped; dropped_template(dropped)) + { + EditorModelSets.add(set.id, {dropped}); + m_setsselected = set.id; + m_setsentry = -1; + } + } + if (sets.empty()) + { + ImGui::TextDisabled(STR_C("No sets yet")); + } + ImGui::EndChild(); + if (ImGui::Button(STR_C("New"))) + { + open_sets_window(EditorModelSets.create("New set")); + } + auto const *edited{EditorModelSets.find(m_setsselected)}; + if (edited != nullptr) + { + ImGui::SameLine(); + if (ImGui::Button(STR_C("Duplicate"))) + { + open_sets_window(EditorModelSets.create(edited->name + " copy", edited->templates)); + edited = EditorModelSets.find(m_setsselected); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Delete"))) + { + ImGui::OpenPopup("##deleteset"); + } + if (ImGui::BeginPopup("##deleteset")) + { + ImGui::Text(STR_C("Delete set \"%s\"?"), edited->name.c_str()); + if (ImGui::Button(STR_C("Delete##confirm"))) + { + EditorModelSets.remove(edited->id); + edited = nullptr; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + ImGui::EndGroup(); + + ImGui::SameLine(); + + // right side: content of the selected set + ImGui::BeginGroup(); + if (edited == nullptr) + { + ImGui::TextDisabled(STR_C("Create a set with \"New\", or use \"Save as set\" on a brush or area fill list.")); + ImGui::TextDisabled(STR_C("Sets can be chosen in Insert (random model), Brush and Area fill.")); + } + else + { + if (m_setsnameid != edited->id) + { + // selection changed, load its name into the edit buffer + std::snprintf(m_setsname, sizeof(m_setsname), "%s", edited->name.c_str()); + m_setsnameid = edited->id; + } + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Rename").x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x); + auto const entered{ImGui::InputText("##setname", m_setsname, IM_ARRAYSIZE(m_setsname), ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Rename")) || entered) + { + EditorModelSets.rename(edited->id, m_setsname); + m_setsnameid = 0; // reload, the name could have been adjusted to stay unique + } + + ImGui::BeginChild("##setentries", ImVec2(0, -buttonsheight * 2), true); + // with the previews of the models, as in the node bank + if (EditorSettings.nodebank_previews() && m_previews != nullptr) + { + render_set_cards(edited->id); + } + else + { + for (int idx = 0; idx < static_cast(edited->labels.size()); ++idx) + { + auto const label{edited->labels[idx] + "##entry" + std::to_string(idx)}; + if (list_item(label.c_str(), idx == m_setsentry)) + m_setsentry = idx; + } + } + if (edited->templates.empty()) + { + ImGui::TextDisabled(STR_C("(empty) select a template in the node bank and press \"Add selected\"")); + ImGui::TextDisabled("%s", STR_C("or drag models here from the node bank")); + } + ImGui::EndChild(); + if (std::string dropped; dropped_template(dropped)) + { + EditorModelSets.add(edited->id, {dropped}); + } + + auto const setid{edited->id}; + if (ImGui::Button(STR_C("Add selected"))) + { + if (node_selected()) + EditorModelSets.add(setid, {*m_selectedtemplate}); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Adds the template selected in the node bank list")); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Remove")) && m_setsentry >= 0) + { + EditorModelSets.erase(setid, static_cast(m_setsentry)); + m_setsentry = -1; + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Clear"))) + { + ImGui::OpenPopup("##clearset"); + } + if (ImGui::BeginPopup("##clearset")) + { + ImGui::TextUnformatted(STR_C("Remove all templates from this set?")); + if (ImGui::Button(STR_C("Clear##confirm"))) + { + EditorModelSets.clear(setid); + m_setsentry = -1; + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + // whole node bank group at once + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize("Add group").x - ImGui::GetStyle().FramePadding.x * 2 - ImGui::GetStyle().ItemSpacing.x); + auto const groups{group_names()}; + m_setsgroup.id = groups.empty() ? -1 : std::clamp(m_setsgroup.id, 0, static_cast(groups.size()) - 1); + if (ImGui::BeginCombo("##addgroup", m_setsgroup.id >= 0 ? groups[m_setsgroup.id].c_str() : "(no node bank groups)")) + { + for (int idx = 0; idx < static_cast(groups.size()); ++idx) + { + auto const label{groups[idx] + "##addgroup" + std::to_string(idx)}; + if (ImGui::Selectable(label.c_str(), idx == m_setsgroup.id)) + m_setsgroup.id = idx; + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + ImGui::SameLine(); + if (ImGui::Button(STR_C("Add group"))) + { + std::vector templates; + for (auto const *entry : set_entries(m_setsgroup)) + templates.push_back(*entry); + EditorModelSets.add(setid, templates); + } + } + ImGui::EndGroup(); ImGui::End(); } @@ -600,6 +1525,12 @@ const std::string *nodebank_panel::get_active_template() return m_selectedtemplate.get(); } +bool nodebank_panel::node_selected() const +{ + // scenery templates are placed with include directives, the tools which scatter nodes have no use for them + return m_selectedtemplate && false == m_selectedtemplate->empty() && false == m_selectedtemplate->starts_with(editor_includes::directive_mark); +} + std::vector nodebank_panel::group_names() const { std::vector names; @@ -691,19 +1622,798 @@ void functions_panel::render() if (true == ImGui::Begin(panelname.c_str(), nullptr, flags)) { // header section - - ImGui::RadioButton("Random rotation", (int *)&rot_mode, RANDOM); - ImGui::RadioButton("Fixed rotation", (int *)&rot_mode, FIXED); - if (rot_mode == FIXED) - { - // ImGui::Checkbox("Get rotation from last object", &rot_from_last); - ImGui::SliderFloat("Rotation Value", &rot_value, 0.0f, 360.0f, "%.1f"); - }; - ImGui::RadioButton("Default rotation", (int *)&rot_mode, DEFAULT); + render_controls(); for (auto const &line : text_lines) { ImGui::TextColored(ImVec4(line.color.r, line.color.g, line.color.b, line.color.a), line.data.c_str()); } } ImGui::End(); -} \ No newline at end of file +} + +void functions_panel::render_controls() +{ + // order matches rotation_mode + char const *const modes[] = {STR_C("Random"), STR_C("Fixed"), STR_C("Default (from template)")}; + auto current{static_cast(rot_mode)}; + if (ImGui::Combo(STR_C("Rotation"), ¤t, modes, IM_ARRAYSIZE(modes))) + { + rot_mode = static_cast(current); + } + if (rot_mode == FIXED) + { + // ImGui::Checkbox(STR_C("Get rotation from last object"), &rot_from_last); + ImGui::SliderFloat(STR_C("Rotation value"), &rot_value, 0.0f, 360.0f, STR_C("%.1f deg")); + } +} + +namespace +{ + +std::pair const layer_itemlabels[] = {{"models", scene::layer_item::model}, + {"tracks", scene::layer_item::track}, + {"traction", scene::layer_item::traction}, + {"power sources", scene::layer_item::powersource}, + {"memory cells", scene::layer_item::memcell}, + {"event launchers", scene::layer_item::launcher}, + {"events", scene::layer_item::event}, + {"vehicles", scene::layer_item::vehicle}, + {"sounds", scene::layer_item::sound}, + {"terrain shapes", scene::layer_item::shape}, + {"lines", scene::layer_item::lines}}; + +// what the layer holds, a line per kind of item +std::string layer_content(scene::basic_layer const &Layer) +{ + std::string content; + for (auto const &itemlabel : layer_itemlabels) + { + auto const count{Layer.items[static_cast(itemlabel.second)]}; + if (count > 0) + { + content += (content.empty() ? "" : "\n") + std::string{itemlabel.first} + ": " + std::to_string(count); + } + } + return content; +} + +std::string layer_name(scene::layer_handle const Layer) +{ + return scene::Layers.valid(Layer) ? Bezogonkow(scene::Layers.layer(Layer).name) : "(none)"; +} + +// button which can be greyed out, with an explanation shown when it's hovered +bool action_button(char const *Label, bool const Enabled, std::string const &Reason = std::string()) +{ + if (false == Enabled) + { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); + } + auto const clicked{ImGui::Button(Label) && Enabled}; + if (false == Enabled) + { + ImGui::PopStyleVar(); + if (false == Reason.empty() && ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", Reason.c_str()); + } + } + return clicked; +} + +// text field bound to a string +bool input_text(char const *Label, std::string &Value) +{ + std::array buffer{}; + Value.copy(buffer.data(), buffer.size() - 1); + if (false == ImGui::InputText(Label, buffer.data(), buffer.size())) + { + return false; + } + Value = buffer.data(); + return true; +} + +ImVec4 const status_errorcolor{1.0f, 0.45f, 0.4f, 1.0f}; + +} // namespace + +void itemproperties_panel::render_include() +{ + auto &include{*m_include}; + include.active = false; + + ImGui::Text(STR_C("include: %s"), Bezogonkow(include.target).c_str()); + if (false == include.info.name.empty()) + { + ImGui::TextDisabled("%s", include.info.name.c_str()); + } + if (false == include.issue.empty()) + { + ImGui::PushTextWrapPos(0.0f); + ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(include.issue).c_str()); + ImGui::PopTextWrapPos(); + return; + } + if (include.values.empty()) + { + ImGui::TextDisabled(STR_C("The template takes no parameters.")); + return; + } + + ImGui::PushItemWidth(ImGui::GetFontSize() * 14.0f); + for (std::size_t idx = 0; idx < include.values.size(); ++idx) + { + auto const id{static_cast(idx + 1)}; + auto const lookup{std::find_if(std::begin(include.info.parameters), std::end(include.info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })}; + auto const described{lookup != std::end(include.info.parameters)}; + auto const role{described ? lookup->role : std::string{"free"}}; + // the label from the description, or what the description tells about the parameter + auto const label{described && false == lookup->label.empty() ? lookup->label : role != "free" ? role : "(p" + std::to_string(id) + ")"}; + auto &value{include.values[idx]}; + + ImGui::PushID(id); + char *end{nullptr}; + auto number{std::strtod(value.c_str(), &end)}; + if ((role.starts_with("pos.") || role.starts_with("rot.") || role == "number") && false == value.empty() && *end == '\0') + { + if (ImGui::DragScalar(label.c_str(), ImGuiDataType_Double, &number, role.starts_with("pos.") ? 0.05f : 0.5f, nullptr, nullptr, "%.3f")) + { + value = editor_includes::number(number); + include.changed = true; + } + } + else + { + // applied once the field is left, so the template isn't processed with a half-typed value + input_text(label.c_str(), value); + if (ImGui::IsItemDeactivatedAfterEdit()) + { + include.changed = true; + } + } + include.active |= ImGui::IsItemActive(); + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("(p%d), %s", id, role.c_str()); + } + ImGui::PopID(); + } + ImGui::PopItemWidth(); + + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped(STR_C("Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again.")); + ImGui::PopStyleColor(); +} + +void layers_panel::report(std::string const &Status, bool const Error) +{ + status = Status; + status_error = Error; +} + +void layers_panel::render() +{ + if (false == is_open) + { + return; + } + + // initially next to the toolset window + ImGui::SetNextWindowPos(ImVec2S(520, 60), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2S(440, 340), ImGuiCond_FirstUseEver); + auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; + if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) + { + if (scene::Layers.empty()) + { + // layers are established only when the scenery is loaded for editing, regular load skips the bookkeeping + ImGui::TextDisabled(STR_C("Scenery layers are available in an edit session.\nStart the simulator with: -edit ")); + } + else + { + if (false == scene::Layers.listed(m_selected)) + { + m_selected = null_handle; + } + auto const selected{m_selected != null_handle}; + auto const removed{selected && scene::Layers.layer(m_selected).removed}; + std::string reason{STR("Select a layer first")}; + + if (ImGui::Button(STR_C("New..."))) + { + m_newname[0] = '\0'; + m_newparent = (selected && false == removed && false == scene::Layers.layer(m_selected).binary ? m_selected : scene::layer_handle{1}); + m_popuperror.clear(); + ImGui::OpenPopup(STR_C("New layer")); + } + ImGui::SameLine(); + if (removed) + { + if (ImGui::Button(STR_C("Restore"))) + { + scene::Layers.restore(m_selected); + report(format(STR_C("Layer \"%s\" restored"), layer_name(m_selected).c_str())); + } + } + else if (action_button(STR_C("Remove..."), selected && scene::Layers.can_remove(m_selected, reason), reason)) + { + ImGui::OpenPopup(STR_C("Remove layer")); + } + ImGui::SameLine(); + // the scenario file is what holds the scenery together, it stays where it is + auto const mergeable{selected && false == removed && (scene::Layers.layer(m_selected).created || false == scene::Layers.layer(m_selected).sites.empty())}; + if (action_button(STR_C("Merge into..."), mergeable, selected && false == removed ? STR("The scenario file can't be merged into another layer") : STR("Select a layer first"))) + { + // the layer which includes the selected one is the most likely target + m_mergetarget = scene::Layers.resolve(scene::Layers.layer(m_selected).parent); + if (false == scene::Layers.can_merge(m_selected, m_mergetarget, reason)) + { + m_mergetarget = null_handle; + } + ImGui::OpenPopup(STR_C("Merge layer")); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Save")) && save) + { + save(); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Writes changed nodes and layers to the scenery files (Ctrl+S)")); + } + ImGui::Separator(); + + render_list(); + + if (false == status.empty()) + { + ImGui::PushTextWrapPos(0.0f); + if (status_error) + { + ImGui::TextColored(status_errorcolor, "%s", status.c_str()); + } + else + { + ImGui::TextUnformatted(status.c_str()); + } + ImGui::PopTextWrapPos(); + } + + render_popups(); + } + } + ImGui::End(); +} + +void layers_panel::render_list() +{ + // the list takes the window except for the room left for the status + ImGui::BeginChild("##layers", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing() * 2.5f)); + ImGui::TextDisabled(STR_C("visible, locked, active, layer file")); + + for (auto const handle : scene::Layers.tree()) + { + // NOTE: copies of the flags, as the calls below can change the layer + auto const &layer{scene::Layers.layer(handle)}; + auto const isactive{handle == scene::Layers.active()}; + auto const isremoved{layer.removed}; + std::string readonly; + auto const iswritable{scene::Layers.writable(handle, &readonly)}; + + ImGui::PushID(static_cast(handle)); + if (isremoved) + { + ImGui::PushStyleVar(ImGuiStyleVar_Alpha, ImGui::GetStyle().Alpha * 0.5f); + } + // active layer receives new nodes, so it can't be hidden nor locked. removed layer is out of the picture altogether + auto const fixed{isactive || isremoved}; + auto visible{layer.visible}; + if (ImGui::Checkbox("##visible", &visible) && false == fixed) + { + scene::Layers.visible(handle, visible); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", isactive ? "Active layer is always visible" : "Show or hide models, tracks and traction of the layer"); + } + ImGui::SameLine(); + auto locked{layer.locked}; + if (ImGui::Checkbox("##locked", &locked) && false == fixed) + { + scene::Layers.locked(handle, locked); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip("%s", isactive ? "Active layer can't be locked" : "Nodes of locked layer can't be selected in the viewport"); + } + ImGui::SameLine(); + if (ImGui::RadioButton("##active", isactive) && false == isactive) + { + if (std::string reason; false == scene::Layers.accepts(handle, &reason)) + { + report(format(STR_C("Layer \"%s\" can't take new nodes"), layer_name(handle).c_str()) + (reason.empty() ? "" : ": " + reason), true); + } + else + { + scene::Layers.active(handle); + } + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Active layer: nodes created in the editor are placed in it")); + } + ImGui::SameLine(); + auto label{std::string(2 * scene::Layers.depth(handle), ' ') + layer_name(handle) + " (" + (layer.binary ? "binary terrain" : std::to_string(layer.item_count())) + ")"}; + if (layer.created) + { + label += " [new]"; + } + if (isremoved) + { + label += " [removed]"; + } + else if (false == iswritable) + { + label += " [read-only]"; + } + else if (layer.late) + { + label += " [after FirstInit]"; + } + if (ImGui::Selectable(label.c_str(), handle == m_selected)) + { + m_selected = handle; + } + if (ImGui::IsItemHovered()) + { + std::string content{layer.created ? STR("New layer, its file is created on save") : isremoved ? STR("Dropped from the scenery on save") : STR("Scenery file")}; + if (scene::Layers.valid(layer.parent)) + { + content += "\nincluded by: " + layer_name(scene::Layers.resolve(layer.parent)); + } + if (false == iswritable) + { + content += "\nread-only: " + readonly; + } + if (layer.late) + { + content += "\nloaded after the scenario initialization (FirstInit): takes no new nodes, only vehicles belong there"; + } + if (auto const items{layer_content(layer)}; false == items.empty()) + { + content += "\n" + items; + } + ImGui::SetTooltip("%s", content.c_str()); + } + if (isremoved) + { + ImGui::PopStyleVar(); + } + ImGui::PopID(); + } + ImGui::EndChild(); +} + +void layers_panel::render_popups() +{ + if (ImGui::BeginPopupModal(STR_C("New layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::TextUnformatted(STR_C("File of the new layer, in the scenery directory:")); + ImGui::InputTextWithHint("##name", STR_C("name.scm"), m_newname, IM_ARRAYSIZE(m_newname)); + if (ImGui::BeginCombo("Included by", layer_name(m_newparent).c_str())) + { + for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx) + { + auto const handle{static_cast(idx)}; + if (false == scene::Layers.listed(handle) || scene::Layers.layer(handle).removed || scene::Layers.layer(handle).binary) + { + continue; + } + if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_newparent)) + { + m_newparent = handle; + } + } + ImGui::EndCombo(); + } + if (false == m_popuperror.empty()) + { + ImGui::TextColored(status_errorcolor, "%s", m_popuperror.c_str()); + } + if (ImGui::Button(STR_C("Create"))) + { + auto const created{scene::Layers.create(m_newname, m_newparent, m_popuperror)}; + if (created != null_handle) + { + m_selected = created; + report(format(STR_C("Layer \"%s\" created and made active. Its file is written on save."), layer_name(created).c_str())); + ImGui::CloseCurrentPopup(); + } + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopupModal(STR_C("Remove layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (false == scene::Layers.listed(m_selected)) + { + ImGui::CloseCurrentPopup(); + } + else + { + auto const &layer{scene::Layers.layer(m_selected)}; + auto included{0}; + for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx) + { + auto const handle{static_cast(idx)}; + if (handle != m_selected && scene::Layers.listed(handle) && scene::Layers.is_descendant(handle, m_selected)) + { + ++included; + } + } + ImGui::Text(STR_C("Remove layer \"%s\" from the scenery?"), layer_name(m_selected).c_str()); + ImGui::TextUnformatted(layer.created ? "The layer wasn't saved yet, so it leaves no file behind." : "Its include directive is erased on save. The file itself stays on the disk."); + if (included > 0) + { + ImGui::Text(STR_C("%d layer(s) it includes are removed with it."), included); + } + if (auto const items{layer_content(layer)}; false == items.empty()) + { + ImGui::Separator(); + ImGui::TextUnformatted(STR_C("The layer holds:")); + ImGui::TextUnformatted(items.c_str()); + ImGui::TextDisabled(STR_C("Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells.")); + } + if (ImGui::Button(STR_C("Remove"))) + { + auto const name{layer_name(m_selected)}; + if (scene::Layers.remove(m_selected)) + { + report(format(STR_C("Layer \"%s\" removed. It can be restored until the scenery is saved."), name.c_str())); + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + { + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopupModal(STR_C("Merge layer"), nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + { + if (false == scene::Layers.listed(m_selected)) + { + ImGui::CloseCurrentPopup(); + } + else + { + ImGui::Text(STR_C("Move content of layer \"%s\" to:"), layer_name(m_selected).c_str()); + std::string reason; + auto targets{0}; + if (ImGui::BeginCombo("##target", layer_name(m_mergetarget).c_str())) + { + for (std::size_t idx = 1; idx <= scene::Layers.size(); ++idx) + { + auto const handle{static_cast(idx)}; + if (false == scene::Layers.listed(handle) || false == scene::Layers.can_merge(m_selected, handle, reason)) + { + continue; + } + ++targets; + if (ImGui::Selectable((layer_name(handle) + "##" + std::to_string(idx)).c_str(), handle == m_mergetarget)) + { + m_mergetarget = handle; + } + } + if (targets == 0) + { + ImGui::TextDisabled(STR_C("no layer can take it")); + } + ImGui::EndCombo(); + } + ImGui::TextDisabled(STR_C("The text of the file is moved on save, the emptied file stays on the disk.\n" + "Merged into the layer which includes it, the content stays where the include was.\n" + "Merged into any other layer it's placed at its end, which changes the load order.")); + auto const possible{m_mergetarget != null_handle && scene::Layers.can_merge(m_selected, m_mergetarget, reason)}; + if (action_button(STR_C("Merge"), possible, m_mergetarget != null_handle ? reason : STR("Pick the target layer"))) + { + auto const name{layer_name(m_selected)}; + auto const target{m_mergetarget}; + if (scene::Layers.merge(m_selected, target)) + { + report(format(STR_C("Layer \"%s\" merged into \"%s\". The files are changed on save."), name.c_str(), layer_name(target).c_str())); + m_selected = target; + } + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Cancel"))) + { + ImGui::CloseCurrentPopup(); + } + } + ImGui::EndPopup(); + } +} + +void includes_panel::open(std::string const &File) +{ + m_file = File; + m_statuserror = false; + m_parameters = 0; + std::string error; + m_loaded = editor_includes::load(File, m_info, error); + if (false == m_loaded) + { + m_status = error; + m_statuserror = true; + return; + } + auto const described{false == m_info.name.empty() || false == m_info.parameters.empty()}; + // every parameter the template uses gets an entry, described or not + m_parameters = editor_includes::parameter_count(File); + for (auto id = 1; id <= m_parameters; ++id) + { + m_info.parameter(id); + } + m_status = "The template uses " + std::to_string(m_parameters) + " parameter(s)."; + if (false == described) + { + // starting point for a new description + editor_includes::suggest(File, m_info); + m_status += " It has no description yet; the roles are a suggestion based on how the template uses the parameters."; + } +} + +void includes_panel::render_list() +{ + if (false == EditorIncludes.scanned()) + { + EditorIncludes.scan(); + } + auto const &entries{EditorIncludes.entries()}; + + ImGui::PushItemWidth(ImGui::GetFontSize() * 12.0f); + auto changed{ImGui::InputTextWithHint("##filter", STR_C("Search templates"), m_filter, IM_ARRAYSIZE(m_filter))}; + ImGui::PopItemWidth(); + ImGui::SameLine(); + changed |= ImGui::Checkbox(STR_C("Described"), &m_describedonly); + if (false == scene::Layers.templates().empty()) + { + // known only for scenery opened for editing + ImGui::SameLine(); + changed |= ImGui::Checkbox(STR_C("Used by the scenery"), &m_usedonly); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Rescan"))) + { + EditorIncludes.scan(); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Looks through the scenery directory again, for the templates added or changed outside of the editor")); + } + + if (changed || m_listrevision != EditorIncludes.revision()) + { + m_listrevision = EditorIncludes.revision(); + m_listed.clear(); + m_described = 0; + std::string const filter{m_filter}; + auto const &used{scene::Layers.templates()}; + for (std::size_t idx = 0; idx < entries.size(); ++idx) + { + auto const &entry{entries[idx]}; + m_described += (entry.described ? 1 : 0); + if ((m_describedonly && false == entry.described) || (m_usedonly && false == used.empty() && used.count(entry.file) == 0)) + { + continue; + } + if (false == filter.empty() && false == contains(entry.file, filter) && false == contains(entry.name, filter) && false == contains(entry.category, filter)) + { + continue; + } + m_listed.emplace_back(idx); + } + } + ImGui::TextDisabled(STR_C("%d template(s) in the scenery directory, %d described, %d ready for the node bank"), static_cast(entries.size()), m_described, + static_cast(EditorIncludes.ready().size())); + + ImGui::BeginChild("##templates", ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 9.0f), true); + ImGuiListClipper clipper; + clipper.Begin(static_cast(m_listed.size())); + while (clipper.Step()) + { + for (auto row = clipper.DisplayStart; row < clipper.DisplayEnd; ++row) + { + auto const &entry{entries[m_listed[row]]}; + auto const marker{entry.complete ? "[ready] " : entry.described ? "[incomplete] " : ""}; + auto const label{marker + Bezogonkow(entry.file) + (entry.name.empty() ? "" : " - " + entry.name) + "##" + std::to_string(m_listed[row])}; + if (false == entry.described) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + } + auto const clicked{list_item(label.c_str(), entry.file == m_file)}; + if (false == entry.described) + { + ImGui::PopStyleColor(); + } + if (false == entry.complete && false == entry.issue.empty() && ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Not in the node bank: %s"), Bezogonkow(entry.issue).c_str()); + } + if (clicked) + { + open(entry.file); + } + } + } + if (m_listed.empty()) + { + ImGui::TextDisabled(entries.empty() ? "(no .inc files in the scenery directory)" : "(no match)"); + } + ImGui::EndChild(); +} + +void includes_panel::render_description() +{ + ImGui::Separator(); + ImGui::Text("%s", Bezogonkow(m_file).c_str()); + input_text("Name", m_info.name); + input_text("Category", m_info.category); + input_text("Description", m_info.description); + + ImGui::Columns(4, "##parameters", false); + ImGui::SetColumnWidth(0, ImGui::CalcTextSize("(p000) ").x); + ImGui::SetColumnWidth(1, ImGui::CalcTextSize("texture ").x + ImGui::GetFrameHeight() * 2.0f); + ImGui::TextDisabled(STR_C("param")); + ImGui::NextColumn(); + ImGui::TextDisabled(STR_C("role")); + ImGui::NextColumn(); + ImGui::TextDisabled(STR_C("label")); + ImGui::NextColumn(); + ImGui::TextDisabled(STR_C("default")); + ImGui::NextColumn(); + for (auto ¶meter : m_info.parameters) + { + ImGui::PushID(parameter.id); + ImGui::AlignTextToFramePadding(); + // parameters which keep the template out of the node bank stand out + if (parameter.id <= m_parameters && parameter.value.empty() && false == editor_includes::automatic(parameter.role)) + { + ImGui::TextColored(status_errorcolor, "(p%d)", parameter.id); + } + else + { + ImGui::Text("(p%d)", parameter.id); + } + ImGui::NextColumn(); + ImGui::PushItemWidth(-1.0f); + if (ImGui::BeginCombo("##role", parameter.role.c_str())) + { + for (auto const &role : editor_includes::roles) + { + if (ImGui::Selectable(role.c_str(), role == parameter.role)) + { + parameter.role = role; + } + } + ImGui::EndCombo(); + } + ImGui::PopItemWidth(); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1.0f); + input_text("##label", parameter.label); + ImGui::PopItemWidth(); + ImGui::NextColumn(); + ImGui::PushItemWidth(-1.0f); + input_text("##default", parameter.value); + ImGui::PopItemWidth(); + ImGui::NextColumn(); + ImGui::PopID(); + } + ImGui::Columns(1); + + // what the node bank is going to make of the description + ImGui::PushTextWrapPos(0.0f); + if (std::string issue; editor_includes::complete(m_info, m_parameters, &issue)) + { + ImGui::TextUnformatted(STR_C("Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates).")); + } + else + { + ImGui::TextColored(status_errorcolor, STR_C("Not for the node bank yet: %s."), issue.c_str()); + } + ImGui::PopTextWrapPos(); + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\n" + "Insert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\n" + "The other parameters receive their default values.")); + } + + if (ImGui::Button(STR_C("Suggest roles"))) + { + editor_includes::suggest(m_file, m_info); + } + if (ImGui::IsItemHovered()) + { + ImGui::SetTooltip(STR_C("Sets roles of the parameters still marked as free, where the way the template uses them tells what they are")); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Add parameter"))) + { + m_info.parameter(m_info.parameters.empty() ? 1 : m_info.parameters.back().id + 1); + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Save to file"))) + { + std::string error; + m_statuserror = false == editor_includes::save(m_file, m_info, error); + m_status = m_statuserror ? error : "Description saved in \"" + m_file + "\""; + if (false == m_statuserror) + { + EditorIncludes.update(m_file); + } + } + ImGui::SameLine(); + if (ImGui::Button(STR_C("Reload"))) + { + open(m_file); + } +} + +void includes_panel::render() +{ + if (false == is_open) + { + return; + } + + // next to the layers window where the screen is wide enough for both + auto const position{ImVec2S(970, 60)}; + auto const windowsize{ImVec2S(600, 600)}; + ImGui::SetNextWindowPos(position.x + windowsize.x <= ImGui::GetIO().DisplaySize.x ? position : ImVec2S(560, 110), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(windowsize, ImGuiCond_FirstUseEver); + auto const panelname{(title.empty() ? m_name : title) + "###" + m_name}; + if (ImGui::Begin(panelname.c_str(), &is_open, ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse)) + { + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped(STR_C("The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; " + "the description is stored in the template file, as //$e comment lines.")); + if (false == Global.editor_session) + { + ImGui::TextWrapped(STR_C("The node bank offers the templates in a scenery opened for editing (-edit).")); + } + ImGui::PopStyleColor(); + + render_list(); + if (m_loaded) + { + render_description(); + } + if (false == m_status.empty()) + { + ImGui::PushTextWrapPos(0.0f); + if (m_statuserror) + { + ImGui::TextColored(status_errorcolor, "%s", Bezogonkow(m_status).c_str()); + } + else + { + ImGui::TextUnformatted(Bezogonkow(m_status).c_str()); + } + ImGui::PopTextWrapPos(); + } + } + ImGui::End(); +} diff --git a/application/editoruipanels.h b/application/editoruipanels.h index 50e8bf99..f79946b4 100644 --- a/application/editoruipanels.h +++ b/application/editoruipanels.h @@ -11,6 +11,10 @@ http://mozilla.org/MPL/2.0/. #include "application/uilayer.h" #include "utilities/Classes.h" +#include "editor/editorIncludeInfo.hpp" +#include "editor/editorPreviews.hpp" + +#include /* // helper, associated bool is set when the primary value was changed and expects processing at the observer's leisure template @@ -26,6 +30,20 @@ struct item_properties { changeable rotation {}; }; */ +// include of a scenery template selected in the editor, with the values its directive passes to the template. +// owned by the editor mode; the node properties let the user change the values +struct include_selection +{ + scene::instance_handle instance{0}; + std::string target; // name of the template, the way the directive spells it + include_info info; // description of the template + std::vector values; // value of each parameter in turn + bool placement{false}; // the position and rotation parameters can be driven by the gizmo + std::string issue; // why the values can't be changed, if they can't + bool changed{false}; // set by the UI when a value was changed, cleared by the editor mode once the change is applied + bool active{false}; // the UI is in the middle of changing a value +}; + class itemproperties_panel : public ui_panel { @@ -36,7 +54,14 @@ class itemproperties_panel : public ui_panel // position/rotation/scale of TAnimModel nodes in place. Other node types // are still treated as read-only. void update(scene::basic_node *Node); + // binds include of a scenery template to the panel, shown in place of a node; nullptr unbinds + void include(include_selection *Include) + { + m_include = Include; + } void render() override; + // panel content without the window, also drawn inside the toolset window + void render_body(); private: // methods @@ -45,30 +70,52 @@ class itemproperties_panel : public ui_panel // renders DragFloat3/DragScalarN widgets for position, rotation and scale // of the currently bound TAnimModel; no-op for other node subclasses. void render_transform_editor(); + // renders the field with the name of the currently bound TAnimModel, which gives the instance another name; no-op for other node subclasses + void render_name_editor(); + // renders widgets for the parameters of the bound include + void render_include(); // members scene::basic_node *m_node{nullptr}; // scene node bound to the panel + scene::basic_node *m_namednode{nullptr}; // scene node whose name is held by the edit buffer + char m_namebuffer[128]{}; // name of the node, as shown and typed in + bool m_nameactive{false}; // the name is being typed in + std::string m_nameissue; // why the name typed in wasn't taken + include_selection *m_include{nullptr}; // include of a scenery template bound to the panel scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel std::string m_groupprefix; std::vector m_grouplines; }; -class brush_object_list : public ui_panel +// reference to a set of node templates, used by the random insert, the brush and the area fill +struct model_set_ref +{ + enum class source + { + manual, // list assembled by hand in the tool itself (or no set, where the tool has no such list) + user, // user-defined set (see EditorModelSets), id == set id + nodebank // node bank group, id == group index (as returned by nodebank_panel::group_names()) + }; + source kind{source::manual}; + int id{-1}; +}; + +class nodebank_panel; + +// brush settings, drawn inside the node bank window when the brush mode is active +class brush_object_list { private: - int idx; - static bool VectorGetter(void *data, int idx, const char **out_text); - std::string Template; + int idx{-1}; public: - brush_object_list(std::string const &Name, bool const Isopen); - void render() override; - void update(std::string nodeTemplate); + void render_options(nodebank_panel &Bank); // class use - std::vector Objects; + std::vector Objects; // manual list, used when source is set to manual std::string *GetRandomObject(); bool useRandom = {false}; + model_set_ref source; float spacing{1.0f}; }; @@ -82,7 +129,8 @@ class nodebank_panel : public ui_panel COPY, ADD, BRUSH, - FILL + FILL, + TRACK }; edit_mode mode = MODIFY; @@ -96,13 +144,134 @@ class nodebank_panel : public ui_panel // node templates of the group with specified index (as returned by group_names()) std::vector group_templates(std::size_t const Group) const; + // model sets (user-defined sets and node bank groups) + // combo selecting a set. Manuallabel names the tool's own hand-made list; nullptr hides that choice. returns: true on change + bool set_combo(char const *Label, model_set_ref &Ref, char const *Manuallabel); + // list of templates assembled by hand, with buttons to add the node bank selection, remove entries and save it as a user set + void manual_list(char const *Id, std::vector &List, int &Selected); + // node templates of referenced set; empty for the manual list or a set which no longer exists + std::vector set_entries(model_set_ref const &Ref) const; + // random template from referenced set, or nullptr if the set is empty + std::string const *random_template(model_set_ref const &Ref) const; + std::string set_name(model_set_ref const &Ref, char const *Manuallabel) const; + // shows the set manager window, optionally with specified user set selected + void open_sets_window(int const Setid = 0); + // content drawn in the window of the node bank in its place, with its own title (a field of work of the editor uses the window + // for its own browser); none: the node bank + void stand_in(std::function Content, std::string Title) + { + m_standin = std::move(Content); + m_standintitle = std::move(Title); + } + + private: // methods: std::string generate_node_label(std::string Input) const; + void render_sets_window(); + // finds the preview images of the entries, in the order of the file, as the generator names them + void index_previews(); + // entries of a group as cards with their previews, in rows as many as fit the width + void render_cards(std::vector> const *> const &Entries); + // the same entries as a list, for when the previews are off + void render_list(std::vector> const *> const &Entries); + // the scenery templates (.inc), as a list; they're placed with include directives, so they take a scenery opened for editing + void render_templates_list(std::string const &Filter); + // the entry dragged from the item drawn last, to drop it on a list of the model sets or of a tool + void drag_entry(std::pair> const &Entry); + // image of a node definition as the node bank has it, or an empty string + std::string const &template_preview(std::string const &Template) const; + // texture of the small preview of the image, 0 if there's none (yet) + std::uint64_t preview_texture(std::string const &Path, char const *&Placeholder); + // the templates of the edited model set as cards with their previews + void render_set_cards(int const Setid); + // the entry pointed at for a moment: a popup with its preview, big, and the model and skin it shows + void preview_popup(std::pair> const &Entry); + // true if the node bank selection is a definition of a node, which the hand-made lists and the model sets can take + bool node_selected() const; // members: + std::function m_standin; + std::string m_standintitle; std::vector>> m_nodebank; char m_nodesearch[128]; std::shared_ptr m_selectedtemplate; + // previews of the models: image of each node definition, by its address, and the images read so far + std::unordered_map m_previewpaths; + std::unique_ptr m_previews; + std::unique_ptr m_largepreviews; // a few, for the popup + bool m_previewfolder{false}; // the generator was run, there's the folder of the images + std::unordered_map m_templatepreviews; // images of the node definitions by their text, for the model sets + // the group of the node bank shown, by its place among the groups; scenery_templates for the scenery templates + static int constexpr scenery_templates{-1}; + int m_category{0}; + // set manager window + bool m_setsopen{false}; + int m_setsselected{0}; // id of the user set being edited + int m_setsentry{-1}; // selected template of the edited set + int m_setsnameid{0}; // id of the set whose name is held in the edit buffer + char m_setsname[128]{}; + model_set_ref m_setsgroup{model_set_ref::source::nodebank, 0}; // node bank group to append to the edited set +}; + +// scenery layers of a scenery opened for editing: their visibility, selection lock, the layer receiving new nodes, +// and layer management. the changes are applied to the scenery files on save +class layers_panel : public ui_panel +{ + + public: + layers_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {} + + void render() override; + + // saves the scenery; provided by the editor mode + std::function save; + // outcome of the most recent operation, shown at the bottom of the window + std::string status; + bool status_error{false}; + + private: + // methods + void render_list(); + void render_popups(); + void report(std::string const &Status, bool const Error = false); + // members + scene::layer_handle m_selected{null_handle}; // layer the buttons act on + char m_newname[128]{}; // file name of the layer being created + scene::layer_handle m_newparent{null_handle}; // layer which is going to include it + scene::layer_handle m_mergetarget{null_handle}; + std::string m_popuperror; +}; + +// database of the *.inc scenery templates: the templates found in the scenery directory, with the editor of their +// descriptions (see editorIncludeInfo.hpp). templates whose descriptions are complete are offered by the node bank +class includes_panel : public ui_panel +{ + + public: + includes_panel(std::string const &Name, bool const Isopen) : ui_panel(Name, Isopen) {} + + void render() override; + + private: + // methods + void open(std::string const &File); + void render_list(); + void render_description(); + // members + // template list + char m_filter[128]{}; + bool m_describedonly{false}; + bool m_usedonly{false}; + std::vector m_listed; // entries of the template bank matching the filter + int m_listrevision{-1}; // revision of the template bank the list was made for + int m_described{0}; + // description editor + std::string m_file; // template being edited, relative to the scenery directory + include_info m_info; + int m_parameters{0}; // number of parameters the template uses + bool m_loaded{false}; + std::string m_status; + bool m_statuserror{false}; }; class functions_panel : public ui_panel @@ -124,6 +293,8 @@ class functions_panel : public ui_panel void update(scene::basic_node const *Node); void render() override; + // rotation settings widgets, also drawn inside the node bank window + void render_controls(); private: // methods @@ -133,4 +304,4 @@ class functions_panel : public ui_panel scene::group_handle m_grouphandle{null_handle}; // scene group bound to the panel std::string m_groupprefix; std::vector m_grouplines; -}; \ No newline at end of file +}; \ No newline at end of file diff --git a/application/scenarioloadermode.cpp b/application/scenarioloadermode.cpp index f05c3869..dbdce606 100644 --- a/application/scenarioloadermode.cpp +++ b/application/scenarioloadermode.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/renderer.h" #include "utilities/Logs.h" #include "utilities/translation.h" +#include "audio/audiorenderer.h" scenarioloader_mode::scenarioloader_mode() { m_userinterface = std::make_shared(); @@ -55,6 +56,11 @@ bool scenarioloader_mode::update() { ErrorLog( "Bad init: scenario loading failed" ); Application.pop_mode(); } + // the loading state holds the scenario file open, which on some systems keeps the file from being replaced + // when the scenery is saved in the editor. it'd also get in the way of another load + state.reset(); + // sound files decoded in the background are all ready before the simulation starts + audio::renderer.complete_buffers(); WriteLog( "Scenario loading time: " + std::to_string( std::chrono::duration_cast( std::chrono::system_clock::now() - timestart ).count() ) + " seconds" ); // TODO: implement and use next mode cue diff --git a/application/scenarioloaderuilayer.cpp b/application/scenarioloaderuilayer.cpp index de0d4e64..207a1756 100644 --- a/application/scenarioloaderuilayer.cpp +++ b/application/scenarioloaderuilayer.cpp @@ -193,6 +193,8 @@ void scenarioloader_ui::render_() ImVec2 screen_size(Global.window_size.x, Global.window_size.y); ImGui::SetNextWindowPos(ImVec2(-padding, -padding)); ImGui::SetNextWindowSize(ImVec2(Global.window_size.x + padding * 2, Global.window_size.y + padding * 2)); + // it reaches past the edges of the main window, which would make it a window of its own with the panels able to leave the main one + ImGui::SetNextWindowViewport(ImGui::GetMainViewport()->ID); ImGui::Begin("Neo Loading Screen", nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground); ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImGui::PushFont(font_loading); diff --git a/application/uilayer.cpp b/application/uilayer.cpp index 60c8a778..29bbb06c 100644 --- a/application/uilayer.cpp +++ b/application/uilayer.cpp @@ -19,16 +19,20 @@ http://mozilla.org/MPL/2.0/. #include "utilities/translation.h" #include "application/application.h" #include "application/editormode.h" +#include "application/editoruilayer.h" #include "imgui/imgui_impl_glfw.h" GLFWwindow *ui_layer::m_window{nullptr}; +GLFWwindow *ui_layer::m_keywindow{nullptr}; ImGuiIO *ui_layer::m_imguiio{nullptr}; GLint ui_layer::m_textureunit{GL_TEXTURE0}; bool ui_layer::m_cursorvisible; +int ui_layer::m_palette{0}; ImFont *ui_layer::font_default{nullptr}; ImFont *ui_layer::font_mono{nullptr}; ImFont *ui_layer::font_loading{nullptr}; +ImFont *ui_layer::font_bold{nullptr}; ui_panel::ui_panel(std::string Identifier, bool const Isopen) : is_open(Isopen), m_name(std::move(Identifier)) {} @@ -108,7 +112,7 @@ ui_layer::~ui_layer() {} bool ui_layer::key_callback(int key, int scancode, int action, int mods) { - ImGui_ImplGlfw_KeyCallback(m_window, key, scancode, action, mods); + ImGui_ImplGlfw_KeyCallback(m_keywindow != nullptr ? m_keywindow : m_window, key, scancode, action, mods); return m_imguiio->WantCaptureKeyboard; } @@ -130,67 +134,146 @@ bool ui_layer::mouse_button_callback(int button, int action, int mods) return m_imguiio->WantCaptureMouse; } -static ImVec4 imvec_lerp(const ImVec4 &a, const ImVec4 &b, float t) +// the glfw backend of imgui works with events since 1.87: position, hover and focus of the window are passed to it as they come +void ui_layer::cursor_pos_callback(double x, double y) { - return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); + if (ImGui::GetCurrentContext() == nullptr) + return; + ImGui_ImplGlfw_CursorPosCallback(m_window, x, y); +} + +void ui_layer::cursor_enter_callback(int entered) +{ + if (ImGui::GetCurrentContext() == nullptr) + return; + ImGui_ImplGlfw_CursorEnterCallback(m_window, entered); +} + +void ui_layer::focus_callback(int focused) +{ + if (ImGui::GetCurrentContext() == nullptr) + return; + ImGui_ImplGlfw_WindowFocusCallback(m_window, focused); +} + +void ui_layer::viewport_key_callback(GLFWwindow *Window, int key, int scancode, int action, int mods) +{ + // imgui gets the key with the window it came from (the state of modifiers is read from it), the simulator gets it unless imgui takes it + m_keywindow = Window; + Application.on_key(key, scancode, action, mods); + m_keywindow = nullptr; +} + +namespace +{ +// window creation of the glfw backend, wrapped to give the new windows the key callback of the simulator +void (*imgui_create_window)(ImGuiViewport *Viewport){nullptr}; + +void create_viewport_window(ImGuiViewport *Viewport) +{ + imgui_create_window(Viewport); + if (Viewport->PlatformHandle != nullptr) + glfwSetKeyCallback(static_cast(Viewport->PlatformHandle), ui_layer::viewport_key_callback); +} +} // namespace + +void ui_layer::imgui_colors(int const Palette) +{ + m_palette = Palette; + auto const rgb = [](int const Color, float const Alpha = 1.0f) { + return ImVec4(((Color >> 16) & 0xff) / 255.0f, ((Color >> 8) & 0xff) / 255.0f, (Color & 0xff) / 255.0f, Alpha); + }; + auto const editor{Palette == 1}; + // palette shared with the MaSzyna starter: flat dark panels, thin borders, green accents; the one of the editor + // has amber accents on cooler grey panels + int const panel{editor ? 0x1f2226 : 0x1e2327}; // window background + int const base{editor ? 0x1b1d21 : 0x15171b}; // darker background, between panels + int const header{editor ? 0x2c3036 : 0x2a3036}; // section headers, hovered items + int const border{editor ? 0x3a3f47 : 0x3a424a}; + int const field{editor ? 0x15171a : 0x121517}; // input fields, combos + int const accent{editor ? 0xb07a1c : 0x177f00}; // selection, active elements + int const accentbright{editor ? 0xe5a73a : 0x41c400}; // marks: checkmarks, active tab underline + int const accentdim{editor ? 0x5a4319 : 0x164b0e}; + + ImVec4 *colors = ImGui::GetStyle().Colors; + + colors[ImGuiCol_Text] = rgb(editor ? 0xe6e8eb : 0xffffff); + colors[ImGuiCol_TextDisabled] = rgb(editor ? 0x8a929c : 0xa0a0a2); + colors[ImGuiCol_WindowBg] = rgb(panel, Global.UIBgOpacity); // ui.bg.opacity from config file + colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_PopupBg] = rgb(base, 0.98f); + colors[ImGuiCol_Border] = rgb(border); + colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_FrameBg] = rgb(field); + colors[ImGuiCol_FrameBgHovered] = rgb(header); + colors[ImGuiCol_FrameBgActive] = rgb(border); + colors[ImGuiCol_TitleBg] = rgb(base); + colors[ImGuiCol_TitleBgActive] = rgb(header); + colors[ImGuiCol_TitleBgCollapsed] = rgb(base, 0.75f); + colors[ImGuiCol_MenuBarBg] = rgb(panel); + colors[ImGuiCol_ScrollbarBg] = rgb(base, 0.60f); + colors[ImGuiCol_ScrollbarGrab] = rgb(border); + colors[ImGuiCol_ScrollbarGrabHovered] = rgb(0x5a636c); + colors[ImGuiCol_ScrollbarGrabActive] = rgb(0x858585); + colors[ImGuiCol_CheckMark] = rgb(accentbright); + colors[ImGuiCol_SliderGrab] = rgb(accent); + colors[ImGuiCol_SliderGrabActive] = rgb(accentbright); + colors[ImGuiCol_Button] = rgb(header); + colors[ImGuiCol_ButtonHovered] = rgb(border); + colors[ImGuiCol_ButtonActive] = rgb(accent); + // shared by section headers and selected list items + colors[ImGuiCol_Header] = rgb(header); + colors[ImGuiCol_HeaderHovered] = rgb(accentdim); + colors[ImGuiCol_HeaderActive] = rgb(accent); + colors[ImGuiCol_Separator] = rgb(border); + colors[ImGuiCol_SeparatorHovered] = rgb(accent); + colors[ImGuiCol_SeparatorActive] = rgb(accentbright); + colors[ImGuiCol_ResizeGrip] = rgb(border, 0.50f); + colors[ImGuiCol_ResizeGripHovered] = rgb(accent); + colors[ImGuiCol_ResizeGripActive] = rgb(accentbright); + colors[ImGuiCol_Tab] = rgb(header); + colors[ImGuiCol_TabHovered] = rgb(border); + colors[ImGuiCol_TabActive] = rgb(accent); // also the line under the tab bar + colors[ImGuiCol_TabUnfocused] = rgb(header); + colors[ImGuiCol_TabUnfocusedActive] = rgb(accentdim); + // imgui 1.90+ marks the selected tab with a line over it as well, in the same colours + colors[ImGuiCol_TabSelectedOverline] = rgb(accent); + colors[ImGuiCol_TabDimmedSelectedOverline] = rgb(accentdim); + colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); + colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); + colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TextSelectedBg] = rgb(accent, 0.60f); + colors[ImGuiCol_DragDropTarget] = rgb(accentbright, 0.90f); + colors[ImGuiCol_NavHighlight] = rgb(accentbright); + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); + colors[ImGuiCol_ModalWindowDimBg] = rgb(base, 0.60f); + // where a dragged window would be docked, and an empty dock node + colors[ImGuiCol_DockingPreview] = rgb(accentbright, 0.70f); + colors[ImGuiCol_DockingEmptyBg] = rgb(base); } void ui_layer::imgui_style() { - ImVec4 *colors = ImGui::GetStyle().Colors; + imgui_colors(0); - colors[ImGuiCol_Text] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); - colors[ImGuiCol_TextDisabled] = ImVec4(0.35f, 0.35f, 0.35f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.04f, 0.04f, 0.04f, Global.UIBgOpacity); // ui.bg.opacity from config file - colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_PopupBg] = ImVec4(0.06f, 0.06f, 0.06f, 0.94f); - colors[ImGuiCol_Border] = ImVec4(0.31f, 0.34f, 0.31f, 0.50f); - colors[ImGuiCol_BorderShadow] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); - colors[ImGuiCol_FrameBg] = ImVec4(0.21f, 0.28f, 0.17f, 0.54f); - colors[ImGuiCol_FrameBgHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.40f); - colors[ImGuiCol_FrameBgActive] = ImVec4(0.42f, 0.64f, 0.23f, 0.67f); - colors[ImGuiCol_TitleBg] = ImVec4(0.03f, 0.03f, 0.03f, 1.00f); - colors[ImGuiCol_TitleBgActive] = ImVec4(0.21f, 0.28f, 0.17f, 1.00f); - colors[ImGuiCol_TitleBgCollapsed] = ImVec4(0.00f, 0.00f, 0.00f, 0.51f); - colors[ImGuiCol_MenuBarBg] = ImVec4(0.10f, 0.10f, 0.10f, 1.00f); - colors[ImGuiCol_ScrollbarBg] = ImVec4(0.01f, 0.01f, 0.01f, 0.53f); - colors[ImGuiCol_ScrollbarGrab] = ImVec4(0.22f, 0.22f, 0.22f, 1.00f); - colors[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.29f, 0.29f, 0.29f, 1.00f); - colors[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.36f, 0.36f, 0.36f, 1.00f); - colors[ImGuiCol_CheckMark] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f); - colors[ImGuiCol_SliderGrab] = ImVec4(0.37f, 0.53f, 0.25f, 1.00f); - colors[ImGuiCol_SliderGrabActive] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f); - colors[ImGuiCol_Button] = ImVec4(0.42f, 0.64f, 0.23f, 0.40f); - colors[ImGuiCol_ButtonHovered] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f); - colors[ImGuiCol_ButtonActive] = ImVec4(0.37f, 0.54f, 0.19f, 1.00f); - colors[ImGuiCol_Header] = ImVec4(0.42f, 0.64f, 0.23f, 0.31f); - colors[ImGuiCol_HeaderHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.80f); - colors[ImGuiCol_HeaderActive] = ImVec4(0.42f, 0.64f, 0.23f, 1.00f); - colors[ImGuiCol_SeparatorHovered] = ImVec4(0.29f, 0.41f, 0.18f, 0.78f); - colors[ImGuiCol_SeparatorActive] = ImVec4(0.29f, 0.41f, 0.18f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(0.42f, 0.64f, 0.23f, 0.25f); - colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.42f, 0.64f, 0.23f, 0.67f); - colors[ImGuiCol_ResizeGripActive] = ImVec4(0.42f, 0.64f, 0.23f, 0.95f); - colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); - colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); - colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); + // flat, square elements with thin borders + auto &style = ImGui::GetStyle(); + style.WindowRounding = 0.0f; + style.ChildRounding = 0.0f; + style.FrameRounding = 0.0f; + style.PopupRounding = 0.0f; + style.ScrollbarRounding = 0.0f; + style.GrabRounding = 0.0f; + style.TabRounding = 0.0f; + style.WindowBorderSize = 1.0f; + style.ChildBorderSize = 1.0f; + style.PopupBorderSize = 1.0f; + style.FrameBorderSize = 1.0f; + style.TabBorderSize = 0.0f; - colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); - colors[ImGuiCol_TextSelectedBg] = ImVec4(0.42f, 0.64f, 0.23f, 0.35f); - colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); - colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); - colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); - colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); - colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); - - colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; - colors[ImGuiCol_Tab] = imvec_lerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); - colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; - colors[ImGuiCol_TabActive] = imvec_lerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); - colors[ImGuiCol_TabUnfocused] = imvec_lerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); - colors[ImGuiCol_TabUnfocusedActive] = imvec_lerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); - - ImGui::GetStyle().ScaleAllSizes(Global.ui_scale); + style.ScaleAllSizes(Global.ui_scale); } bool ui_layer::init(GLFWwindow *Window) @@ -200,6 +283,8 @@ bool ui_layer::init(GLFWwindow *Window) IMGUI_CHECKVERSION(); ImGui::CreateContext(); m_imguiio = &ImGui::GetIO(); + // imgui.ini is read with the first frame, which may be of another mode than the editor + editor_ui::register_settings(); m_imguiio->ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; // m_imguiio->ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; @@ -256,6 +341,14 @@ bool ui_layer::init(GLFWwindow *Window) } } } + for (auto const *bold : {"fonts/dejavusans-bold.ttf", "fonts/DejaVuSans-Bold.ttf"}) + { + if (font_bold == nullptr && FileExists(bold)) + { + ImFontConfig bold_config; + font_bold = m_imguiio->Fonts->AddFontFromFileTTF(bold, Global.ui_fontsize * 1.25f, &bold_config, &ranges[0]); + } + } if (FileExists("fonts/bahnschrift.ttf")) { ImFontConfig loading_config; font_loading = m_imguiio->Fonts->AddFontFromFileTTF("fonts/bahnschrift.ttf", 48, &loading_config, &ranges[0]); @@ -294,6 +387,20 @@ bool ui_layer::init(GLFWwindow *Window) return false; } + // panels can be docked together, and dragged out of the simulator window into windows of their own (e.g. on another monitor) + // when both backends can do it. glfw can't place windows under wayland. both flags have to be set before the first frame + m_imguiio->ConfigFlags |= ImGuiConfigFlags_DockingEnable; + auto const viewportsupport{(m_imguiio->BackendFlags & ImGuiBackendFlags_PlatformHasViewports) && (m_imguiio->BackendFlags & ImGuiBackendFlags_RendererHasViewports) && + glfwGetPlatform() != GLFW_PLATFORM_WAYLAND}; + if (Global.ui_viewports && viewportsupport) + { + m_imguiio->ConfigFlags |= ImGuiConfigFlags_ViewportsEnable; + auto &platformio{ImGui::GetPlatformIO()}; + imgui_create_window = platformio.Platform_CreateWindow; + platformio.Platform_CreateWindow = create_viewport_window; + } + WriteLog(std::string("ui: panels in separate windows ") + ((m_imguiio->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) ? "enabled" : (viewportsupport ? "disabled in settings" : "not supported here"))); + return true; } @@ -377,6 +484,13 @@ void ui_layer::update() void ui_layer::render() { + // the frame may have been started by the ui of another mode, when the mode changes; the windows docked in the dockspace + // of this one would come undocked if they were drawn in a frame without it + if (m_dockspaceframe != ImGui::GetFrameCount()) + { + render_dockspace(); + m_dockspaceframe = ImGui::GetFrameCount(); + } render_background(); render_panels(); render_tooltip(); @@ -393,11 +507,25 @@ void ui_layer::render_internal() { ImGui::Render(); GfxRenderer->GetImguiRenderer()->Render(); + + if (m_imguiio->ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + // panels outside of the main window are drawn in their own windows, whose gl contexts are shared with the main one + auto *context{glfwGetCurrentContext()}; + ImGui::UpdatePlatformWindows(); + ImGui::RenderPlatformWindowsDefault(); + glfwMakeContextCurrent(context); + } } void ui_layer::begin_ui_frame() { + // the colours follow the ui of the mode drawn + if (palette() != m_palette) + imgui_colors(palette()); begin_ui_frame_internal(); + render_dockspace(); + m_dockspaceframe = ImGui::GetFrameCount(); } void ui_layer::begin_ui_frame_internal() @@ -428,8 +556,9 @@ void ui_layer::render_hierarchy(){ if(!m_editor_hierarchy) return; - ImGui::SetNextWindowSize(ImVec2(0, 0)); - ImGui::Begin(STR_C("Scene Hierarchy"), &m_editor_hierarchy, ImGuiWindowFlags_AlwaysAutoResize); + // fitted to the contents when it first appears; no auto resize, which a docked window can't do + ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_FirstUseEver); + ImGui::Begin((std::string(STR_C("Scene Hierarchy")) + "###scenehierarchy").c_str(), &m_editor_hierarchy); ImGui::Text("Registered nodes: %zu", scene::Hierarchy.size()); ImGui::BeginChild("hierarchy_list", ImVec2(500, 300), true); @@ -583,6 +712,7 @@ void ui_layer::render_menu_contents() editor_mode::set_settings_open(settings_open); } } + render_windows_menu(); ImGui::EndMenu(); } } @@ -591,7 +721,7 @@ void ui_layer::render_menu() { glm::dvec2 mousepos = Global.cursor_pos; - if (!((Global.ControlPicking && mousepos.y < 50.0f) || m_imguiio->WantCaptureMouse) || m_suppress_menu) + if (m_suppress_menu || (false == m_menu_always && !((Global.ControlPicking && mousepos.y < 50.0f) || m_imguiio->WantCaptureMouse))) return; if (ImGui::BeginMainMenuBar()) @@ -623,5 +753,5 @@ void ui_layer::render_background() ImVec2 end_position(start_position.x + image_size.x, start_position.y + image_size.y); // obrazek jest odwrócony w pionie – odwracamy UV - ImGui::GetBackgroundDrawList()->AddImage(reinterpret_cast(tex.get_id()), start_position, end_position, ImVec2(0, 1), ImVec2(1, 0)); + ImGui::GetBackgroundDrawList(ImGui::GetMainViewport())->AddImage((ImTextureID)(intptr_t)(tex.get_id()), start_position, end_position, ImVec2(0, 1), ImVec2(1, 0)); } diff --git a/application/uilayer.h b/application/uilayer.h index 6d4019c6..7b68646f 100644 --- a/application/uilayer.h +++ b/application/uilayer.h @@ -78,6 +78,8 @@ public: // methods static bool init( GLFWwindow *Window ); static void imgui_style(); + // colours of the user interface: 0 the green ones shared with the starter, 1 the amber ones of the editor + static void imgui_colors(int const Palette); // assign texturing hardware unit static void set_unit( GLint const Textureunit ) { m_textureunit = GL_TEXTURE0 + Textureunit; } @@ -118,20 +120,39 @@ public: static bool char_callback(unsigned int c); static bool scroll_callback(double xoffset, double yoffset); static bool mouse_button_callback(int button, int action, int mods); + // state updates only, nothing to consume + static void cursor_pos_callback(double x, double y); + static void cursor_enter_callback(int entered); + static void focus_callback(int focused); + // key presses in windows of the ui outside of the main window, passed on to the simulator like those of the main window + static void viewport_key_callback(GLFWwindow *Window, int key, int scancode, int action, int mods); static ImFont *font_default; static ImFont *font_mono; static ImFont *font_loading; + // bigger and bold, for headings like the fields of work of the editor; nullptr if the font isn't there + static ImFont *font_bold; protected: // members static GLFWwindow *m_window; + static GLFWwindow *m_keywindow; // window of the key event being handled static ImGuiIO *m_imguiio; static bool m_cursorvisible; virtual void render_menu_contents(); + // items of the windows menu which belong to the ui of a mode, after the common ones + virtual void render_windows_menu() {} + // colours this ui is drawn in, see imgui_colors() + virtual int palette() const + { + return 0; + } + static int m_palette; // colours set at the moment ui_log_panel m_logpanel { "Log", true }; bool m_suppress_menu = false; // if `true`, the menu at the top of the window will not be present + bool m_menu_always = false; // if `true`, the menu is shown all the time, not only when the mouse is at the top of the window + int m_dockspaceframe{-1}; // frame the dockspace was last submitted in // progress bar config float m_progress { 0.0f }; // percentage of filled progres bar, to indicate lengthy operations. float m_subtaskprogress{ 0.0f }; // percentage of filled progres bar, to indicate lengthy operations. @@ -141,6 +162,8 @@ protected: // methods // render() subclass details virtual void render_() {} + // submitted right after the start of the frame, before any window that may be docked in it + virtual void render_dockspace() {} // draws background quad with specified earlier texture void render_background(); void render_tooltip(); diff --git a/audio/audio.cpp b/audio/audio.cpp index 1c72d1bc..3be2810d 100644 --- a/audio/audio.cpp +++ b/audio/audio.cpp @@ -19,35 +19,42 @@ http://mozilla.org/MPL/2.0/. namespace audio { -openal_buffer::openal_buffer( std::string const &Filename ) : - name( Filename ) { +namespace { + +// content of a sound file, mixed down to 16 bit mono +struct decoded_sound { + std::vector samples; + unsigned int rate { 0 }; + bool mixed { false }; // the file had more than one channel +}; + +// reads and decodes whole sound file. throws std::runtime_error on failure +// NOTE: uses no simulator state, it's also run by the background decoder threads +decoded_sound decode_sound( std::string const &File ) { SF_INFO si; si.format = 0; - std::string file = Filename; - - WriteLog("sound: loading file: " + file); - - SNDFILE *sf = sf_open(file.c_str(), SFM_READ, &si); + SNDFILE *sf = sf_open(File.c_str(), SFM_READ, &si); if (sf == nullptr) throw std::runtime_error("sound: sf_open failed"); sf_command(sf, SFC_SET_NORM_FLOAT, nullptr, SF_TRUE); - float *fbuf = new float[si.frames * si.channels]; - if (sf_readf_float(sf, fbuf, si.frames) != si.frames) + std::unique_ptr fbuf { new float[si.frames * si.channels] }; + if (sf_readf_float(sf, fbuf.get(), si.frames) != si.frames) { + sf_close(sf); throw std::runtime_error("sound: incomplete file"); + } sf_close(sf); - rate = si.samplerate; + decoded_sound sound; + sound.rate = si.samplerate; + sound.mixed = ( si.channels != 1 ); + sound.samples.resize( si.frames ); - if (si.channels != 1) - WriteLog("sound: warning: mixing multichannel file to mono"); - - int16_t *buf = new int16_t[si.frames]; for (size_t i = 0; i < si.frames; i++) { float accum = 0; @@ -59,26 +66,149 @@ openal_buffer::openal_buffer( std::string const &Filename ) : val = 32767; if (val < -32767) val = -32767; - buf[i] = val; + sound.samples[i] = val; } + return sound; +} + +// creates AL buffer holding provided sound. returns: id of the buffer, or null_resource on failure +ALuint upload_sound( decoded_sound const &Sound ) { + + ALuint id { null_resource }; alGenBuffers(1, &id); if (id != null_resource && alIsBuffer(id)) { alGetError(); - alBufferData(id, AL_FORMAT_MONO16, buf, si.frames * 2, rate); + alBufferData(id, AL_FORMAT_MONO16, Sound.samples.data(), static_cast(Sound.samples.size() * 2), Sound.rate); } else { id = null_resource; const char *str = alGetString(alGetError()); ErrorLog("sound: failed to create AL buffer: " + (str != nullptr ? std::string(str) : "")); } + return id; +} - delete[] buf; - delete[] fbuf; +} // namespace + +openal_buffer::openal_buffer( std::string const &Filename ) : + name( Filename ) { + + WriteLog("sound: loading file: " + Filename); + + auto const sound { decode_sound( Filename ) }; + + rate = sound.rate; + + if (sound.mixed) + WriteLog("sound: warning: mixing multichannel file to mono"); + + id = upload_sound( sound ); fetch_caption(); } +// sound files decoded by worker threads. AL buffers for them are made on the main thread +struct buffer_manager::decoder { + + struct job { + audio::buffer_handle handle; + std::string file; + decoded_sound sound; // written by the thread decoding the file + std::string error; + bool done { false }; // guarded by the mutex + }; + + std::mutex mutex; + std::condition_variable queued; // wakes workers when files are added + std::condition_variable decoded; // wakes the main thread waiting for a file + std::deque> queue; // files no worker has taken yet + std::vector> pending; // files without AL buffer yet, in order of request. main thread only + bool stop { false }; + std::vector workers; + + decoder() { + auto const count { std::clamp( std::thread::hardware_concurrency(), 2, 9 ) - 1 }; + for( unsigned int idx = 0; idx < count; ++idx ) { + workers.emplace_back( [ this ]() { work(); } ); + } + } + ~decoder() { + { + std::lock_guard lock( mutex ); + stop = true; + } + queued.notify_all(); + for( auto &worker : workers ) { + worker.join(); + } + } + + static void decode( job &Item ) { + try { + Item.sound = decode_sound( Item.file ); + } + catch( std::exception const &Error ) { + Item.error = Error.what(); + } + } + + void work() { + std::unique_lock lock( mutex ); + while( true ) { + queued.wait( lock, [ this ]() { return stop || ( false == queue.empty() ); } ); + if( stop ) { + return; + } + auto item { queue.front() }; + queue.pop_front(); + lock.unlock(); + decode( *item ); + lock.lock(); + item->done = true; + decoded.notify_all(); + } + } + + // waits until the specified file is decoded. a file no worker has taken yet is decoded right away + void wait( std::shared_ptr const &Item ) { + std::unique_lock lock( mutex ); + if( Item->done ) { + return; + } + auto const lookup { std::find( std::begin( queue ), std::end( queue ), Item ) }; + if( lookup != std::end( queue ) ) { + queue.erase( lookup ); + lock.unlock(); + decode( *Item ); + lock.lock(); + Item->done = true; + return; + } + decoded.wait( lock, [ &Item ]() { return Item->done; } ); + } + + bool is_done( job const &Item ) { + std::lock_guard lock( mutex ); + return Item.done; + } + + // makes the AL buffer out of the decoded file + static void finish( job &Item, openal_buffer &Buffer ) { + if( false == Item.error.empty() ) { + ErrorLog( "Bad file: failed to load audio file \"" + Item.file + "\" (" + Item.error + ")", logtype::file ); + return; + } + Buffer.rate = Item.sound.rate; + if( Item.sound.mixed ) { + WriteLog( "sound: warning: mixing multichannel file \"" + Item.file + "\" to mono" ); + } + Buffer.id = upload_sound( Item.sound ); + Item.sound = {}; + Buffer.fetch_caption(); + } +}; + // retrieves sound caption in currently set language void openal_buffer::fetch_caption() { @@ -93,8 +223,16 @@ openal_buffer::fetch_caption() { } } +buffer_manager::buffer_manager() { + + m_buffers.emplace_back( openal_buffer() ); // empty bindings for null buffer +} + buffer_manager::~buffer_manager() { + // workers are stopped before the buffers go + m_decoder.reset(); + for( auto &buffer : m_buffers ) { if( buffer.id != null_resource ) { ::alDeleteBuffers( 1, &buffer.id ); @@ -152,15 +290,81 @@ buffer_manager::create( std::string const &Filename ) { audio::openal_buffer const & buffer_manager::buffer( audio::buffer_handle const Buffer ) const { + if( ( m_decoder != nullptr ) + && ( false == m_decoder->pending.empty() ) + && ( m_buffers[ Buffer ].id == null_resource ) ) { + complete( Buffer ); + } return m_buffers[ Buffer ]; } +// creates AL buffers for files decoded in the background so far, or for all of them if requested +void +buffer_manager::update( bool const Wait ) { + + if( ( m_decoder == nullptr ) + || ( m_decoder->pending.empty() ) ) { + return; + } + std::vector> undone; + for( auto &item : m_decoder->pending ) { + if( Wait ) { + m_decoder->wait( item ); + } + else if( false == m_decoder->is_done( *item ) ) { + undone.emplace_back( item ); + continue; + } + decoder::finish( *item, m_buffers[ item->handle ] ); + } + m_decoder->pending.swap( undone ); +} + +// creates AL buffer for a file decoded in the background, waiting for the decoder if needed +void +buffer_manager::complete( audio::buffer_handle const Buffer ) const { + + auto &pending { m_decoder->pending }; + auto const lookup { + std::find_if( + std::begin( pending ), std::end( pending ), + [ Buffer ]( std::shared_ptr const &Item ) { return Item->handle == Buffer; } ) }; + if( lookup == std::end( pending ) ) { + return; + } + auto const item { *lookup }; + pending.erase( lookup ); + m_decoder->wait( item ); + decoder::finish( *item, m_buffers[ Buffer ] ); +} + // places in the bank a buffer containing data stored in specified file. returns: handle to the buffer audio::buffer_handle buffer_manager::emplace( std::string Filename ) { buffer_handle const handle { m_buffers.size() }; - m_buffers.emplace_back( Filename ); + if( true == Global.AudioAsyncLoad ) { + // the file is decoded in the background, the AL buffer is made once the data is ready or when the buffer is needed + WriteLog( "sound: loading file: " + Filename ); + openal_buffer buffer; + buffer.name = Filename; + m_buffers.emplace_back( std::move( buffer ) ); + if( m_decoder == nullptr ) { + m_decoder = std::make_unique(); + } + auto item { std::make_shared() }; + item->handle = handle; + item->file = Filename; + m_decoder->pending.emplace_back( item ); + { + std::lock_guard lock( m_decoder->mutex ); + m_decoder->queue.emplace_back( item ); + } + m_decoder->queued.notify_one(); + } + else { + m_buffers.emplace_back( Filename ); + } // NOTE: we store mapping without file type extension, to simplify lookups erase_extension( Filename ); diff --git a/audio/audio.h b/audio/audio.h index 5e306d25..391bf1eb 100644 --- a/audio/audio.h +++ b/audio/audio.h @@ -47,21 +47,25 @@ class buffer_manager { public: // constructors - buffer_manager() { m_buffers.emplace_back( openal_buffer() ); } // empty bindings for null buffer + buffer_manager(); // makes empty bindings for null buffer // destructor ~buffer_manager(); // methods // creates buffer object out of data stored in specified file. returns: handle to the buffer or null_handle if creation failed buffer_handle create( std::string const &Filename ); - // provides direct access to a specified buffer + // provides direct access to a specified buffer. a buffer still decoded in the background is completed first audio::openal_buffer const & buffer( audio::buffer_handle const Buffer ) const; + // creates AL buffers for files decoded in the background so far, or for all of them if requested + void + update( bool const Wait = false ); private: // types using buffer_sequence = std::vector; using index_map = std::unordered_map; + struct decoder; // background decoding of sound files, used with sound.asyncload // methods // places in the bank a buffer containing data stored in specified file. returns: handle to the buffer buffer_handle @@ -72,9 +76,13 @@ private: // checks whether specified file exists. returns: name of the located file, or empty string. std::string find_file( std::string const &Filename ) const; + // creates AL buffer for a file decoded in the background, waiting for the decoder if needed + void + complete( audio::buffer_handle const Buffer ) const; // members - buffer_sequence m_buffers; + mutable buffer_sequence m_buffers; // buffers decoded in the background are completed on first access index_map m_buffermappings; + mutable std::unique_ptr m_decoder; }; } // audio diff --git a/audio/audiorenderer.cpp b/audio/audiorenderer.cpp index 50a6f727..080ab66f 100644 --- a/audio/audiorenderer.cpp +++ b/audio/audiorenderer.cpp @@ -305,6 +305,13 @@ openal_renderer::buffer( audio::buffer_handle const Buffer ) const { return m_buffers.buffer( Buffer ); } +// makes buffers of all sound files still decoded in the background, waiting for them +void +openal_renderer::complete_buffers() { + + m_buffers.update( true ); +} + // initializes the service bool openal_renderer::init() { @@ -350,6 +357,9 @@ openal_renderer::erase( sound_source const *Controller ) { void openal_renderer::update( double const Deltatime ) { + // buffers of sound files decoded in the background since the last update + m_buffers.update(); + ALenum err = alGetError(); if (err != AL_NO_ERROR) { diff --git a/audio/audiorenderer.h b/audio/audiorenderer.h index ec26ec82..3f462eb4 100644 --- a/audio/audiorenderer.h +++ b/audio/audiorenderer.h @@ -109,6 +109,8 @@ public: audio::buffer_handle fetch_buffer( std::string const &Filename ); audio::openal_buffer const &buffer( audio::buffer_handle const Buffer ) const; + // makes buffers of all sound files still decoded in the background (sound.asyncload), waiting for them + void complete_buffers(); bool init(); diff --git a/editor/editorAlignment.cpp b/editor/editorAlignment.cpp new file mode 100644 index 00000000..cf894d63 --- /dev/null +++ b/editor/editorAlignment.cpp @@ -0,0 +1,1109 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http: +*/ + +#include "stdafx.h" +#include "editor/editorAlignment.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include +#include +#include +#include +#include + +namespace alignment +{ + +namespace +{ + +double const kGravity{9.81}; +double const kPi{3.14159265358979323846}; + +using geometry::cross; +using geometry::plan_of; +using geometry::turned; + +glm::dvec2 perpendicular(glm::dvec2 const &Vector) +{ + return {-Vector.y, Vector.x}; +} + +double ceil_to(double const Value, double const Step) +{ + return std::ceil(Value / Step - 1e-9) * Step; +} + +struct local_point +{ + double x{0.0}; + double y{0.0}; + double angle{0.0}; +}; + +local_point transition_point(transition_shape const Shape, double const Length, double const Radius, double const Parameter) +{ + if (Length <= 0.0 || Radius <= 0.0) + return {}; + if (Shape == transition_shape::cubic_parabola) + { + auto const x{Length * Parameter}; + return {x, x * x * x / (6.0 * Radius * Length), std::atan(x * x / (2.0 * Radius * Length))}; + } + auto const distance{Length * Parameter}; + auto const angle = [&](double const S) { return S * S / (2.0 * Radius * Length); }; + int const steps{std::max(8, static_cast(std::ceil(distance / 0.5)) * 2)}; + double const h{distance / steps}; + double x{0.0}, y{0.0}; + for (int i = 0; i <= steps; ++i) + { + double const weight = (i == 0 || i == steps) ? 1.0 : (i % 2 ? 4.0 : 2.0); + auto const a{angle(i * h)}; + x += weight * std::cos(a); + y += weight * std::sin(a); + } + return {x * h / 3.0, y * h / 3.0, angle(distance)}; +} + +local_point spiral_point(double const Start, double const End, double const Length, double const Distance) +{ + if (Distance <= 0.0) + return {}; + auto const heading = [&](double const S) { return Start * S + (End - Start) * S * S / (2.0 * std::max(Length, 1e-9)); }; + int const steps{std::max(8, static_cast(std::ceil(Distance / 0.5)) * 2)}; + double const h{Distance / steps}; + double x{0.0}, y{0.0}; + for (int i = 0; i <= steps; ++i) + { + double const weight = (i == 0 || i == steps) ? 1.0 : (i % 2 ? 4.0 : 2.0); + auto const a{heading(i * h)}; + x += weight * std::cos(a); + y += weight * std::sin(a); + } + return {x * h / 3.0, y * h / 3.0, heading(Distance)}; +} + +struct compound_curve +{ + std::vector> segments; + local_point end; + double shortened{1.0}; +}; + +compound_curve build_compound(double const Deflection, std::vector const &Arcs, double const Lengthin, double const Lengthout) +{ + compound_curve result; + if (Arcs.empty()) + return result; + std::vector curvatures; + for (auto const &arc : Arcs) + curvatures.push_back(1.0 / std::max(1.0, arc.radius)); + auto transitions{curvatures.front() * Lengthin + curvatures.back() * Lengthout}; + for (std::size_t i = 1; i < Arcs.size(); ++i) + transitions += (curvatures[i - 1] + curvatures[i]) * std::max(0.0, Arcs[i].transition); + transitions *= 0.5; + if (transitions > Deflection) + result.shortened = Deflection / transitions * (1.0 - 1e-9); + auto const f{result.shortened}; + auto const arcs{std::max(0.0, Deflection - transitions * f)}; + double total{0.0}; + for (auto const &arc : Arcs) + total += std::max(0.0, arc.share); + std::vector> segments{{0.0, curvatures.front(), Lengthin * f}}; + for (std::size_t i = 0; i < Arcs.size(); ++i) + { + if (i > 0) + segments.push_back({curvatures[i - 1], curvatures[i], std::max(0.0, Arcs[i].transition) * f}); + auto const share{total > 0.0 ? std::max(0.0, Arcs[i].share) / total : 1.0 / static_cast(Arcs.size())}; + segments.push_back({curvatures[i], curvatures[i], share * arcs / curvatures[i]}); + } + segments.push_back({curvatures.back(), 0.0, Lengthout * f}); + double x{0.0}, y{0.0}, heading{0.0}; + for (auto const &segment : segments) + { + if (segment[2] < 1e-6) + continue; + result.segments.push_back(segment); + auto const local{spiral_point(segment[0], segment[1], segment[2], segment[2])}; + x += local.x * std::cos(heading) - local.y * std::sin(heading); + y += local.x * std::sin(heading) + local.y * std::cos(heading); + heading += local.angle; + } + result.end = {x, y, heading}; + return result; +} + +void element_point(element const &Element, transition_shape const Shape, double const Distance, glm::dvec2 &Position, glm::dvec2 &Direction) +{ + switch (Element.kind) + { + case element_kind::straight: + Position = Element.origin + Element.direction * Distance; + Direction = Element.direction; + break; + case element_kind::arc: + { + auto const angle{Element.turn * Distance / Element.radius}; + Position = Element.origin + turned(Element.normal, angle); + Direction = turned(Element.direction, angle); + break; + } + case element_kind::transition_in: + { + auto const local{transition_point(Shape, Element.transition, Element.radius, Distance / Element.transition)}; + Position = Element.origin + Element.direction * local.x + Element.normal * local.y; + Direction = Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle); + break; + } + case element_kind::spiral: + { + auto const local{spiral_point(Element.curvature_start, Element.curvature_end, Element.length, Distance)}; + Position = Element.origin + Element.direction * local.x + Element.normal * local.y; + Direction = Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle); + break; + } + case element_kind::transition_out: + { + auto const local{transition_point(Shape, Element.transition, Element.radius, 1.0 - Distance / Element.transition)}; + Position = Element.origin + Element.direction * local.x + Element.normal * local.y; + Direction = -(Element.direction * std::cos(local.angle) + Element.normal * std::sin(local.angle)); + break; + } + } +} + +double element_cant(element const &Element, double const Distance) +{ + switch (Element.kind) + { + case element_kind::transition_in: return Element.cant * Distance / Element.transition; + case element_kind::transition_out: return Element.cant * (1.0 - Distance / Element.transition); + case element_kind::arc: return Element.cant; + case element_kind::spiral: + { + auto const curvature{Element.curvature_start + (Element.curvature_end - Element.curvature_start) * Distance / std::max(Element.length, 1e-9)}; + return Element.curvature_peak > 0.0 ? Element.cant * curvature / Element.curvature_peak : 0.0; + } + default: return 0.0; + } +} + +element const &element_at(result const &Result, double const Chainage) +{ + auto const lookup = std::upper_bound(Result.elements.begin(), Result.elements.end(), Chainage, [](double const Value, element const &Element) { return Value < Element.chainage; }); + return lookup == Result.elements.begin() ? Result.elements.front() : *std::prev(lookup); +} + +glm::dvec2 blossom(std::array const &Points, double const A, double const B, double const C) +{ + glm::dvec2 r[3], s[2]; + for (int i = 0; i < 3; ++i) + r[i] = glm::mix(Points[i], Points[i + 1], A); + for (int i = 0; i < 2; ++i) + s[i] = glm::mix(r[i], r[i + 1], B); + return glm::mix(s[0], s[1], C); +} + +std::size_t element_pieces(element const &Element, transition_shape const Shape, int const Transitions, double const Arcangle) +{ + if (Element.length < 1e-3) + return 0; + switch (Element.kind) + { + case element_kind::arc: + return static_cast(std::max(1.0, std::ceil(Element.length / Element.radius / (Arcangle * kPi / 180.0) - 1e-9))); + case element_kind::transition_in: + case element_kind::transition_out: + return std::max(std::max(1, Transitions), Shape == transition_shape::cubic_parabola ? 1 : static_cast(std::max(1.0, std::ceil(Element.length / 20.0 - 1e-9)))); + case element_kind::spiral: + { + auto const turn{(Element.curvature_start + Element.curvature_end) * 0.5 * Element.length}; + auto const arcs{std::max(1.0, std::ceil(turn / (Arcangle * kPi / 180.0) - 1e-9))}; + if (Element.curvature_start == Element.curvature_end) + return static_cast(arcs); + auto const count{static_cast(std::max(arcs, std::ceil(Element.length / 20.0 - 1e-9)))}; + return std::max(count, std::max(1, Transitions)); + } + default: + return 1; + } +} + +} + +double unbalanced_acceleration(double const Speed, double const Radius, double const Cant, limits const &Limits) +{ + if (Radius <= 0.0) + return 0.0; + return Speed * Speed / (12.96 * Radius) - kGravity * Cant / Limits.gauge; +} + +double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits) +{ + double length{0.0}; + if (Cant > 0.0) + { + length = std::max(length, Speed * Cant / (3.6 * Limits.cant_rate)); + length = std::max(length, Cant / 1000.0 * Limits.ramp_factor * Speed); + } + auto const unbalanced{std::max(0.0, unbalanced_acceleration(Speed, Radius, Cant, Limits))}; + length = std::max(length, unbalanced * Speed / (3.6 * Limits.jerk)); + return length > 0.0 ? ceil_to(length, 5.0) : 0.0; +} + +recommendation recommend(double const Speed, double const Radius, limits const &Limits) +{ + recommendation result; + auto const factor{Limits.gauge / (kGravity * 12.96)}; + auto const allowance{Limits.gauge * Limits.unbalanced / kGravity}; + result.radius_min = factor * Speed * Speed / (Limits.cant_max + allowance); + if (Radius <= 0.0) + return result; + result.cant_equilibrium = factor * Speed * Speed / Radius; + result.cant_min = std::max(0.0, result.cant_equilibrium - allowance); + result.cant = std::min(Limits.cant_max, ceil_to(std::max(result.cant_min, result.cant_equilibrium * 2.0 / 3.0), 5.0)); + result.transition = transition_length(Speed, Radius, result.cant, Limits); + return result; +} + +namespace +{ + +result fit_between(design const &Design) +{ + result r; + r.shape = Design.shape; + r.gauge = Design.norms.gauge; + r.start = Design.start; + r.end = Design.end; + auto const startreserve{std::max(0.0, Design.start_reserve)}; + auto const endreserve{std::max(0.0, Design.end_reserve)}; + + auto const start{plan_of(Design.start)}; + auto const end{plan_of(Design.end)}; + if (glm::length(Design.start_direction) < 1e-9 || glm::length(Design.end_direction) < 1e-9) + { + r.errors.emplace_back("The direction of an end of the fragment is undefined, its end path has zero length"); + return r; + } + auto const startdirection{glm::normalize(Design.start_direction)}; + auto const enddirection{glm::normalize(Design.end_direction)}; + auto const count{Design.vertices.size()}; + + r.vertices.resize(count); + if (count == 0) + { + auto const offset{end - start}; + if (std::abs(cross(startdirection, offset)) > 0.01 || glm::dot(startdirection, enddirection) < std::cos(1e-4) || glm::dot(startdirection, offset) <= 0.0) + r.errors.emplace_back("The ends don't lie on a common straight line, add a vertex"); + } + else if (count == 1) + { + auto const offset{end - start}; + auto const determinant{cross(startdirection, enddirection)}; + if (std::abs(determinant) < 1e-9) + { + r.errors.emplace_back("Tangents of the ends are parallel, use two vertices"); + } + else + { + auto const t{cross(offset, enddirection) / determinant}; + auto const u{cross(startdirection, offset) / determinant}; + if ((t <= 0.0 || u <= 0.0) && false == Design.vertices.front().reverse_turn) + r.errors.emplace_back("Tangents of the ends intersect outside of the fragment, turn the longer way or use two vertices"); + r.vertices[0] = start + startdirection * t; + } + } + else + { + for (std::size_t i = 1; i + 1 < count; ++i) + r.vertices[i] = Design.vertices[i].position; + r.vertices.front() = start + startdirection * Design.vertices.front().offset; + r.vertices.back() = end - enddirection * Design.vertices.back().offset; + } + if (false == r.errors.empty()) + return r; + + std::vector polygon{start}; + polygon.insert(polygon.end(), r.vertices.begin(), r.vertices.end()); + polygon.push_back(end); + for (std::size_t i = 0; i + 1 < polygon.size(); ++i) + { + if (glm::distance(polygon[i], polygon[i + 1]) < 1e-3) + { + r.errors.emplace_back(format("Tangent %zu has zero length", i + 1)); + return r; + } + } + + double chainage{0.0}; + auto const push = [&](element Element) { + Element.chainage = chainage; + chainage += Element.length; + r.elements.push_back(Element); + }; + auto const push_straight = [&](glm::dvec2 const &From, glm::dvec2 const &Direction, double const Length) { + if (Length <= 1e-6) + return; + element straight; + straight.kind = element_kind::straight; + straight.origin = From; + straight.direction = Direction; + straight.length = Length; + push(straight); + }; + auto const starts_early = [&](double const Straight) { + if (Straight < 0.0) + r.start_extension = -Straight; + if (Straight >= -1e-4 || -Straight <= startreserve + 1e-4) + return; + if (startreserve > 0.0) + r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the adjoining straight", -Straight - startreserve)); + else + r.errors.emplace_back(format("Vertex 1: the curve starts %.2f m before the fixed start", -Straight)); + }; + + struct fitted + { + double radius{0.0}; + double transition_in{0.0}; + double transition_out{0.0}; + double tangent_in{0.0}; + double tangent_out{0.0}; + double deflection{0.0}; + bool compound{false}; + std::vector arcs; + std::function tangents; + }; + auto const scaled = [](std::vector Arcs, double const Scale) { + for (auto &arc : Arcs) + { + arc.radius *= Scale; + arc.transition *= Scale; + } + return Arcs; + }; + std::vector fits(count); + for (std::size_t k = 0; k < count; ++k) + { + auto const &vertex{Design.vertices[k]}; + auto &fit{fits[k]}; + fit.radius = std::max(1.0, vertex.radius); + fit.transition_in = std::max(0.0, vertex.transition_in); + fit.transition_out = std::max(0.0, vertex.transition_out); + auto const directionin{glm::normalize(polygon[k + 1] - polygon[k])}; + auto const directionout{glm::normalize(polygon[k + 2] - polygon[k + 1])}; + auto const turn{cross(directionin, directionout)}; + auto const geometric{std::atan2(std::abs(turn), glm::dot(directionin, directionout))}; + auto const reverse{Design.vertices[k].reverse_turn}; + auto const deflection{reverse ? 2.0 * kPi - geometric : geometric}; + int const side{(turn > 0.0) != reverse ? 1 : -1}; + if (deflection < 1e-7) + continue; + if (geometric > kPi - 1e-3 || (reverse && geometric < 1e-3)) + { + r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); + return r; + } + if (vertex.compound && false == vertex.arcs.empty()) + { + fit.compound = true; + fit.arcs = {{fit.radius, 0.0, vertex.share}}; + for (auto const &arc : vertex.arcs) + fit.arcs.push_back({std::max(1.0, arc.radius), std::max(0.0, arc.transition), arc.share}); + fit.deflection = deflection; + fit.tangents = [&, k, directionin, directionout, side, scaled](double const Radius, double &Tangentin, double &Tangentout) { + auto const &own{fits[k]}; + auto const scale{Radius / own.radius}; + auto const shape{build_compound(own.deflection, scaled(own.arcs, scale), own.transition_in * scale, own.transition_out * scale)}; + auto const normalin{perpendicular(directionin) * static_cast(side)}; + auto const end{directionin * shape.end.x + normalin * shape.end.y}; + auto const along{cross(directionout, end) / cross(directionout, directionin)}; + Tangentin = std::max(0.0, along); + Tangentout = std::max(0.0, glm::dot(end - directionin * along, directionout)); + }; + fit.tangents(fit.radius, fit.tangent_in, fit.tangent_out); + continue; + } + auto const angles = [&](double const Factor) { + return transition_point(Design.shape, fit.transition_in * Factor, fit.radius, 1.0).angle + transition_point(Design.shape, fit.transition_out * Factor, fit.radius, 1.0).angle; + }; + if (angles(1.0) > deflection) + { + double low{0.0}, high{1.0}; + for (int i = 0; i < 50; ++i) + { + auto const middle{(low + high) * 0.5}; + (angles(middle) > deflection ? high : low) = middle; + } + r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to %.1f / %.1f m to fit the deflection angle", k + 1, fit.transition_in * low, fit.transition_out * low)); + fit.transition_in *= low; + fit.transition_out *= low; + } + fit.deflection = deflection; + fit.tangents = [&, k, directionin, directionout, side](double const Radius, double &Tangentin, double &Tangentout) { + auto const endin{transition_point(Design.shape, fits[k].transition_in, Radius, 1.0)}; + auto const endout{transition_point(Design.shape, fits[k].transition_out, Radius, 1.0)}; + auto const a{Radius + endin.y - Radius * (1.0 - std::cos(endin.angle))}; + auto const b{Radius + endout.y - Radius * (1.0 - std::cos(endout.angle))}; + auto const normalin{perpendicular(directionin) * static_cast(side)}; + auto const normalout{perpendicular(directionout) * static_cast(side)}; + auto const determinant{cross(normalin, normalout)}; + glm::dvec2 const offset{(a * normalout.y - normalin.y * b) / determinant, (normalin.x * b - a * normalout.x) / determinant}; + auto const curvestart{offset - normalin * a - directionin * (endin.x - Radius * std::sin(endin.angle))}; + auto const curveend{offset - normalout * b + directionout * (endout.x - Radius * std::sin(endout.angle))}; + Tangentin = std::max(0.0, -glm::dot(curvestart, directionin)); + Tangentout = std::max(0.0, glm::dot(curveend, directionout)); + }; + fit.tangents(fit.radius, fit.tangent_in, fit.tangent_out); + } + std::vector scales(count, 1.0); + for (std::size_t leg = 0; leg + 1 < polygon.size(); ++leg) + { + auto const available{glm::distance(polygon[leg], polygon[leg + 1]) + (leg == 0 ? startreserve : 0.0) + (leg == count ? endreserve : 0.0)}; + auto const required{(leg > 0 ? fits[leg - 1].tangent_out : 0.0) + (leg < count ? fits[leg].tangent_in : 0.0)}; + if (required <= available + 1e-6) + continue; + auto const scale{available / required * (1.0 - 1e-9)}; + if (leg > 0) + scales[leg - 1] = std::min(scales[leg - 1], scale); + if (leg < count) + scales[leg] = std::min(scales[leg], scale); + } + for (std::size_t k = 0; k < count; ++k) + { + if (scales[k] >= 1.0) + continue; + auto &fit{fits[k]}; + auto const budgetin{fit.tangent_in * scales[k]}; + auto const budgetout{fit.tangent_out * scales[k]}; + auto const fits_budget = [&](double const Radius) { + if (transition_point(Design.shape, fit.transition_in, Radius, 1.0).angle + transition_point(Design.shape, fit.transition_out, Radius, 1.0).angle > fit.deflection) + return false; + double tangentin, tangentout; + fit.tangents(Radius, tangentin, tangentout); + return tangentin <= budgetin && tangentout <= budgetout; + }; + double low{1e-3}; + double high{fit.radius}; + for (int i = 0; i < 60; ++i) + { + auto const middle{(low + high) * 0.5}; + (transition_point(Design.shape, fit.transition_in, middle, 1.0).angle + transition_point(Design.shape, fit.transition_out, middle, 1.0).angle > fit.deflection ? low : high) = middle; + } + low = high * (1.0 + 1e-9); + high = fit.radius; + if (false == fit.compound && fits_budget(low)) + { + for (int i = 0; i < 60; ++i) + { + auto const middle{(low + high) * 0.5}; + (fits_budget(middle) ? low : high) = middle; + } + fit.radius = low; + } + else + { + fit.radius *= scales[k]; + fit.arcs = scaled(fit.arcs, scales[k]); + fit.transition_in *= scales[k]; + fit.transition_out *= scales[k]; + } + r.warnings.emplace_back(format("Vertex %zu: R reduced from %.1f to %.1f m, transitions to %.1f / %.1f m, to fit between the ends", k + 1, Design.vertices[k].radius, fit.radius, fit.transition_in, fit.transition_out)); + } + + std::vector vertexchainage(count, 0.0); + glm::dvec2 cursor{start}; + for (std::size_t k = 0; k < count; ++k) + { + auto const &vertex{Design.vertices[k]}; + auto const &fit{fits[k]}; + auto const &position{polygon[k + 1]}; + auto const directionin{glm::normalize(polygon[k + 1] - polygon[k])}; + auto const directionout{glm::normalize(polygon[k + 2] - polygon[k + 1])}; + auto const turn{cross(directionin, directionout)}; + auto const geometric{std::atan2(std::abs(turn), glm::dot(directionin, directionout))}; + auto const reverse{Design.vertices[k].reverse_turn}; + auto const deflection{reverse ? 2.0 * kPi - geometric : geometric}; + int const side{(turn > 0.0) != reverse ? 1 : -1}; + + auto const lead{glm::dot(position - cursor, directionin)}; + if (deflection < 1e-7) + { + if (lead < -1e-4) + r.errors.emplace_back(format("Vertex %zu lies inside the curve of the previous vertex", k + 1)); + push_straight(cursor, directionin, lead); + vertexchainage[k] = chainage; + cursor = position; + continue; + } + if (geometric > kPi - 1e-3 || (reverse && geometric < 1e-3)) + { + r.errors.emplace_back(format("Vertex %zu: a turn of about 180 or 360 degrees needs one more vertex", k + 1)); + return r; + } + if (fit.compound) + { + auto const shape{build_compound(deflection, fit.arcs, fit.transition_in, fit.transition_out)}; + if (shape.shortened < 1.0) + r.warnings.emplace_back(format("Vertex %zu: transition curves shortened to fit the deflection angle", k + 1)); + auto const normalin{perpendicular(directionin) * static_cast(side)}; + auto const endoffset{directionin * shape.end.x + normalin * shape.end.y}; + auto const along{cross(directionout, endoffset) / cross(directionout, directionin)}; + auto const curvestart{position - directionin * along}; + auto const straight{glm::dot(curvestart - cursor, directionin)}; + if (k == 0) + starts_early(straight); + else if (straight < -1e-4) + r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); + push_straight(cursor, directionin, straight); + auto const startchainage{chainage}; + glm::dvec2 origin{curvestart}; + glm::dvec2 heading{directionin}; + auto sharpest{fit.radius}; + for (auto const &arc : fit.arcs) + sharpest = std::min(sharpest, arc.radius); + auto const peak{1.0 / sharpest}; + for (auto const &segment : shape.segments) + { + element piece; + piece.kind = element_kind::spiral; + piece.vertex = static_cast(k); + piece.turn = side; + piece.cant = std::max(0.0, vertex.cant); + piece.curvature_start = segment[0]; + piece.curvature_end = segment[1]; + piece.curvature_peak = peak; + piece.radius = std::max(segment[0], segment[1]) > 0.0 ? 1.0 / std::max(segment[0], segment[1]) : 0.0; + piece.length = segment[2]; + piece.origin = origin; + piece.direction = heading; + piece.normal = perpendicular(heading) * static_cast(side); + push(piece); + auto const local{spiral_point(segment[0], segment[1], segment[2], segment[2])}; + origin += heading * local.x + piece.normal * local.y; + heading = turned(heading, side * local.angle); + } + vertexchainage[k] = (startchainage + chainage) * 0.5; + cursor = origin; + + curve_report report; + report.vertex = static_cast(k); + report.radius = fit.radius; + for (auto const &arc : fit.arcs) + report.radii.push_back(arc.radius); + report.transition_in = fit.transition_in * shape.shortened; + report.transition_out = fit.transition_out * shape.shortened; + report.deflection = deflection; + report.arc_length = chainage - startchainage; + report.tangent_in = glm::distance(position, curvestart); + report.tangent_out = glm::distance(position, cursor); + report.unbalanced = unbalanced_acceleration(Design.speed, sharpest, std::max(0.0, vertex.cant), Design.norms); + report.recommended = recommend(Design.speed, sharpest, Design.norms); + r.curves.push_back(report); + if (sharpest < report.recommended.radius_min - 1e-6) + r.warnings.emplace_back(format("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h", k + 1, sharpest, report.recommended.radius_min, Design.speed)); + if (report.unbalanced > Design.norms.unbalanced + 1e-6) + r.warnings.emplace_back(format("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2", k + 1, report.unbalanced, Design.norms.unbalanced)); + continue; + } + auto const radius{fit.radius}; + auto const lengthin{fit.transition_in}; + auto const lengthout{fit.transition_out}; + auto const endin{transition_point(Design.shape, lengthin, radius, 1.0)}; + auto const endout{transition_point(Design.shape, lengthout, radius, 1.0)}; + auto const shiftin{endin.y - radius * (1.0 - std::cos(endin.angle))}; + auto const shiftout{endout.y - radius * (1.0 - std::cos(endout.angle))}; + auto const middlein{endin.x - radius * std::sin(endin.angle)}; + auto const middleout{endout.x - radius * std::sin(endout.angle)}; + + auto const normalin{perpendicular(directionin) * static_cast(side)}; + auto const normalout{perpendicular(directionout) * static_cast(side)}; + auto const a{radius + shiftin}; + auto const b{radius + shiftout}; + auto const determinant{cross(normalin, normalout)}; + glm::dvec2 const centre{position + glm::dvec2{(a * normalout.y - normalin.y * b) / determinant, (normalin.x * b - a * normalout.x) / determinant}}; + auto const curvestart{centre - normalin * a - directionin * middlein}; + auto const curveend{centre - normalout * b + directionout * middleout}; + auto const arcangle{deflection - endin.angle - endout.angle}; + if (arcangle < -1e-9) + { + r.errors.emplace_back(format("Vertex %zu: transition curves are too long for the deflection angle", k + 1)); + return r; + } + + auto const straight{glm::dot(curvestart - cursor, directionin)}; + if (k == 0) + starts_early(straight); + else if (straight < -1e-4) + r.errors.emplace_back(format("Curves of vertices %zu and %zu overlap by %.2f m", k, k + 1, -straight)); + push_straight(cursor, directionin, straight); + + element curve; + curve.vertex = static_cast(k); + curve.radius = radius; + curve.turn = side; + curve.cant = std::max(0.0, vertex.cant); + if (lengthin > 0.0) + { + curve.kind = element_kind::transition_in; + curve.transition = curve.length = lengthin; + curve.origin = curvestart; + curve.direction = directionin; + curve.normal = normalin; + push(curve); + } + auto const arcstart{curvestart + directionin * endin.x + normalin * endin.y}; + curve.kind = element_kind::arc; + curve.transition = 0.0; + curve.length = radius * std::max(0.0, arcangle); + curve.origin = centre; + curve.normal = arcstart - centre; + curve.direction = turned(directionin, side * endin.angle); + vertexchainage[k] = chainage + curve.length * 0.5; + if (curve.length > 1e-6) + push(curve); + if (lengthout > 0.0) + { + curve.kind = element_kind::transition_out; + curve.transition = curve.length = lengthout; + curve.origin = curveend; + curve.direction = -directionout; + curve.normal = normalout; + push(curve); + } + cursor = curveend; + + curve_report report; + report.vertex = static_cast(k); + report.radius = radius; + report.transition_in = lengthin; + report.transition_out = lengthout; + report.deflection = deflection; + report.arc_length = radius * std::max(0.0, arcangle); + report.tangent_in = glm::distance(position, curvestart); + report.tangent_out = glm::distance(position, curveend); + report.unbalanced = unbalanced_acceleration(Design.speed, radius, curve.cant, Design.norms); + auto const speed{Design.speed / 3.6}; + if (lengthin > 0.0) + { + report.cant_rate_in = curve.cant * speed / lengthin; + report.jerk_in = std::max(0.0, report.unbalanced) * speed / lengthin; + } + if (lengthout > 0.0) + { + report.cant_rate_out = curve.cant * speed / lengthout; + report.jerk_out = std::max(0.0, report.unbalanced) * speed / lengthout; + } + report.recommended = recommend(Design.speed, radius, Design.norms); + r.curves.push_back(report); + + auto const &norms{Design.norms}; + if (radius < report.recommended.radius_min - 1e-6) + r.warnings.emplace_back(format("Vertex %zu: R %.0f m is below the minimum %.0f m for %.0f km/h", k + 1, radius, report.recommended.radius_min, Design.speed)); + if (curve.cant > norms.cant_max + 1e-6) + r.warnings.emplace_back(format("Vertex %zu: cant %.0f mm exceeds %.0f mm", k + 1, curve.cant, norms.cant_max)); + if (report.unbalanced > norms.unbalanced + 1e-6) + r.warnings.emplace_back(format("Vertex %zu: unbalanced acceleration %.2f m/s2 exceeds %.2f m/s2 (cant at least %.0f mm)", k + 1, report.unbalanced, norms.unbalanced, report.recommended.cant_min)); + auto const required{transition_length(Design.speed, radius, curve.cant, norms)}; + if (lengthin + 1e-6 < required || lengthout + 1e-6 < required) + r.warnings.emplace_back(format("Vertex %zu: transition curves shorter than the required %.0f m", k + 1, required)); + if (curve.cant > 0.0 && (lengthin <= 0.0 || lengthout <= 0.0)) + r.warnings.emplace_back(format("Vertex %zu: cant without a transition curve to ramp it", k + 1)); + } + auto const lastdirection{glm::normalize(polygon.back() - polygon[polygon.size() - 2])}; + auto const tail{glm::dot(end - cursor, lastdirection)}; + if (tail < 0.0) + r.end_extension = -tail; + if (tail < -1e-4 && -tail > endreserve + 1e-4) + { + if (endreserve > 0.0) + r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the adjoining straight", count, -tail - endreserve)); + else + r.errors.emplace_back(format("Vertex %zu: the curve ends %.2f m past the fixed end", count, -tail)); + } + push_straight(cursor, lastdirection, tail); + r.length = chainage; + + if (Design.norms.tangent_min_time > 0.0) + { + auto const minimum{Design.norms.tangent_min_time * Design.speed / 3.6}; + for (std::size_t i = 1; i + 1 < r.elements.size(); ++i) + { + auto const &e{r.elements[i]}; + if (e.kind == element_kind::straight && e.length < minimum && r.elements[i - 1].kind != element_kind::straight && r.elements[i + 1].kind != element_kind::straight) + r.warnings.emplace_back(format("Straight between curves is %.1f m long, less than %.0f m", e.length, minimum)); + } + } + + r.vertex_chainages = vertexchainage; + r.profile.push_back({0.0, Design.start.y, 0.0, 0.0}); + auto const radius{std::max(Design.norms.vertical_min, Design.norms.vertical_factor * Design.speed * Design.speed)}; + for (std::size_t k = 0; k < count; ++k) + { + auto const elevation{Design.start.y + (Design.end.y - Design.start.y) * vertexchainage[k] / std::max(r.length, 1e-6)}; + r.vertex_elevations.push_back(elevation); + if (vertexchainage[k] <= r.profile.back().chainage + 1e-3) + continue; + r.profile.push_back({vertexchainage[k], elevation, radius, 0.0}); + } + r.profile.push_back({r.length, Design.end.y, 0.0, 0.0}); + for (std::size_t i = 1; i + 1 < r.profile.size(); ++i) + { + auto &point{r.profile[i]}; + auto const gradebefore{(point.elevation - r.profile[i - 1].elevation) / (point.chainage - r.profile[i - 1].chainage)}; + auto const gradeafter{(r.profile[i + 1].elevation - point.elevation) / (r.profile[i + 1].chainage - point.chainage)}; + point.tangent = point.radius * std::abs(gradeafter - gradebefore) / 2.0; + } + if (Design.start_radius > 0.0 && Design.start_radius < 10000.0) + r.warnings.emplace_back(format("The adjoining track at the start is curved (R %.0f m), the curvature changes abruptly", Design.start_radius)); + if (Design.end_radius > 0.0 && Design.end_radius < 10000.0) + r.warnings.emplace_back(format("The adjoining track at the end is curved (R %.0f m), the curvature changes abruptly", Design.end_radius)); + + r.valid = r.errors.empty() && r.length > 1e-3; + return r; +} + +glm::dvec3 along_end(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Distance) +{ + auto const direction{glm::normalize(Direction)}; + return {Point.x + direction.x * Distance, Point.y + Grade * Distance, Point.z + direction.y * Distance}; +} + +} // namespace + +result compute(design const &Design) +{ + auto first{fit_between(Design)}; + auto const startextension{first.start_extension > 1e-4 ? first.start_extension : 0.0}; + auto const endextension{first.end_extension > 1e-4 ? first.end_extension : 0.0}; + if (false == first.errors.empty() || (startextension == 0.0 && endextension == 0.0)) + { + first.start_extension = first.end_extension = 0.0; + return first; + } + auto extended{Design}; + extended.start = along_end(Design.start, Design.start_direction, Design.start_grade, -startextension); + extended.end = along_end(Design.end, Design.end_direction, Design.end_grade, endextension); + extended.start_reserve = extended.end_reserve = 0.0; + if (extended.vertices.size() >= 2) + { + extended.vertices.front().offset += startextension; + extended.vertices.back().offset += endextension; + } + auto r{fit_between(extended)}; + r.start_extension = startextension; + r.end_extension = endextension; + return r; +} + +bool tangent_intersection(design const &Design, glm::dvec2 &Point) +{ + auto const start{plan_of(Design.start)}; + auto const offset{plan_of(Design.end) - start}; + auto const startdirection{glm::normalize(Design.start_direction)}; + auto const enddirection{glm::normalize(Design.end_direction)}; + auto const determinant{cross(startdirection, enddirection)}; + if (std::abs(determinant) < 1e-9) + return false; + auto const t{cross(offset, enddirection) / determinant}; + auto const u{cross(startdirection, offset) / determinant}; + if (t <= 0.0 || u <= 0.0) + return false; + Point = start + startdirection * t; + return true; +} + +bool collinear(design const &Design) +{ + auto const offset{plan_of(Design.end) - plan_of(Design.start)}; + auto const startdirection{glm::normalize(Design.start_direction)}; + auto const enddirection{glm::normalize(Design.end_direction)}; + return std::abs(cross(startdirection, offset)) <= 0.01 && glm::dot(startdirection, enddirection) >= std::cos(1e-4) && glm::dot(startdirection, offset) > 0.0; +} + +double elevation(result const &Result, double const Chainage) +{ + auto const &profile{Result.profile}; + if (profile.size() < 2) + return 0.0; + auto const s{std::clamp(Chainage, 0.0, Result.length)}; + std::size_t segment{0}; + while (segment + 2 < profile.size() && s > profile[segment + 1].chainage) + ++segment; + auto const &from{profile[segment]}; + auto const &to{profile[segment + 1]}; + double height{from.elevation + (to.elevation - from.elevation) * (s - from.chainage) / (to.chainage - from.chainage)}; + for (std::size_t i = 1; i + 1 < profile.size(); ++i) + { + auto const &point{profile[i]}; + auto const distance{std::abs(s - point.chainage)}; + if (point.tangent <= 0.0 || distance >= point.tangent) + continue; + auto const gradebefore{(point.elevation - profile[i - 1].elevation) / (point.chainage - profile[i - 1].chainage)}; + auto const gradeafter{(profile[i + 1].elevation - point.elevation) / (profile[i + 1].chainage - point.chainage)}; + auto const offset{(point.tangent - distance) * (point.tangent - distance) / (2.0 * point.radius)}; + height += gradeafter > gradebefore ? offset : -offset; + } + return height; +} + +double grade(result const &Result, double const Chainage) +{ + auto const &profile{Result.profile}; + if (profile.size() < 2) + return 0.0; + auto const s{std::clamp(Chainage, 0.0, Result.length)}; + std::size_t segment{0}; + while (segment + 2 < profile.size() && s > profile[segment + 1].chainage) + ++segment; + auto const &from{profile[segment]}; + auto const &to{profile[segment + 1]}; + double slope{(to.elevation - from.elevation) / (to.chainage - from.chainage)}; + for (std::size_t i = 1; i + 1 < profile.size(); ++i) + { + auto const &point{profile[i]}; + auto const distance{s - point.chainage}; + if (point.tangent <= 0.0 || std::abs(distance) >= point.tangent) + continue; + auto const gradebefore{(point.elevation - profile[i - 1].elevation) / (point.chainage - profile[i - 1].chainage)}; + auto const gradeafter{(profile[i + 1].elevation - point.elevation) / (profile[i + 1].chainage - point.chainage)}; + auto const change{(point.tangent - std::abs(distance)) / point.radius * (distance < 0.0 ? 1.0 : -1.0)}; + slope += gradeafter > gradebefore ? change : -change; + } + return slope; +} + +station evaluate(result const &Result, double const Chainage) +{ + station result; + if (Result.elements.empty()) + return result; + auto const s{std::clamp(Chainage, 0.0, Result.length)}; + auto const &e{element_at(Result, s)}; + auto const distance{std::clamp(s - e.chainage, 0.0, e.length)}; + glm::dvec2 position, direction; + element_point(e, Result.shape, distance, position, direction); + result.position = {position.x, elevation(Result, s), position.y}; + result.direction = glm::normalize(glm::dvec3{direction.x, grade(Result, s), direction.y}); + result.cant = element_cant(e, distance); + result.turn = e.turn; + return result; +} + +std::size_t minimum_pieces(result const &Result, design const &Design) +{ + std::size_t count{0}; + for (auto const &e : Result.elements) + count += element_pieces(e, Design.shape, Design.transition_pieces, Design.arc_piece_angle); + return count; +} + +std::vector pieces(result const &Result, design const &Design, std::size_t const Count, std::vector const &Breaks, std::vector const &Counts, + std::vector *Intervals) +{ + struct span + { + std::size_t element; + double from, to; + bool generic{false}; + double start{0.0}; + double end{0.0}; + }; + auto const extent = [&](span const &Span) { return Span.generic ? Span.end - Span.start : Span.to - Span.from; }; + auto const chainage_from = [&](span const &Span) { return Span.generic ? Span.start : Result.elements[Span.element].chainage + Span.from; }; + auto const chainage_to = [&](span const &Span) { return Span.generic ? Span.end : Result.elements[Span.element].chainage + Span.to; }; + std::vector spans; + for (std::size_t i = 0; i < Result.elements.size(); ++i) + { + auto const &e{Result.elements[i]}; + auto const count{element_pieces(e, Design.shape, Design.transition_pieces, Design.arc_piece_angle)}; + for (std::size_t j = 0; j < count; ++j) + spans.push_back({i, e.length * j / count, e.length * (j + 1) / count}); + } + double const shortest{1.0}; + for (std::size_t i = 0; i < spans.size() && spans.size() > 1;) + { + auto const &e{Result.elements[spans[i].element]}; + if (e.kind != element_kind::straight || extent(spans[i]) >= shortest) + { + ++i; + continue; + } + auto const start{chainage_from(spans[i])}; + auto const end{chainage_to(spans[i])}; + if (i > 0) + { + auto &previous{spans[i - 1]}; + previous.start = chainage_from(previous); + previous.end = end; + previous.generic = true; + } + else + { + auto &next{spans[i + 1]}; + next.end = chainage_to(next); + next.start = start; + next.generic = true; + } + spans.erase(spans.begin() + i); + } + // splits the span at the chainage + auto const split = [&](std::vector::iterator Span, double const Chainage) { + span second{*Span}; + if (Span->generic) + { + second.start = Chainage; + Span->end = Chainage; + } + else + { + auto const local{Chainage - Result.elements[Span->element].chainage}; + second.from = local; + Span->to = local; + } + spans.insert(std::next(Span), second); + }; + double constexpr margin{0.01}; + for (auto const chainage : Breaks) + { + auto inside{std::find_if(spans.begin(), spans.end(), [&](span const &Span) { return chainage_from(Span) < chainage - margin && chainage_to(Span) > chainage + margin; })}; + if (inside != spans.end()) + split(inside, chainage); + } + auto const interval = [&](span const &Span) { + return static_cast(std::upper_bound(Breaks.begin(), Breaks.end(), (chainage_from(Span) + chainage_to(Span)) * 0.5) - Breaks.begin()); + }; + for (std::size_t k = 0; k < Counts.size(); ++k) + { + while (true) + { + auto const inside{static_cast(std::count_if(spans.begin(), spans.end(), [&](span const &Span) { return interval(Span) == k; }))}; + if (inside == 0 || inside >= Counts[k]) + break; + auto longest{spans.end()}; + for (auto candidate = spans.begin(); candidate != spans.end(); ++candidate) + if (interval(*candidate) == k && (longest == spans.end() || extent(*candidate) > extent(*longest))) + longest = candidate; + split(longest, (chainage_from(*longest) + chainage_to(*longest)) * 0.5); + } + } + while (false == spans.empty() && spans.size() < Count) + { + auto longest = std::max_element(spans.begin(), spans.end(), [&](span const &A, span const &B) { return extent(A) < extent(B); }); + split(longest, (chainage_from(*longest) + chainage_to(*longest)) * 0.5); + } + if (Intervals != nullptr) + { + Intervals->clear(); + for (auto const &piece : spans) + Intervals->push_back(interval(piece)); + } + + std::vector result; + for (auto const &piece : spans) + { + if (piece.generic) + { + auto const first{evaluate(Result, piece.start)}; + auto const last{evaluate(Result, piece.end)}; + glm::dvec2 const p0{first.position.x, first.position.z}; + glm::dvec2 const p3{last.position.x, last.position.z}; + auto const d0{glm::normalize(glm::dvec2{first.direction.x, first.direction.z})}; + auto const d3{glm::normalize(glm::dvec2{last.direction.x, last.direction.z})}; + auto const handle{glm::distance(p0, p3) / 3.0}; + auto const p1{p0 + d0 * handle}; + auto const p2{p3 - d3 * handle}; + auto const y0{first.position.y}; + auto const y3{last.position.y}; + auto const y1{y0 + grade(Result, piece.start) * handle}; + auto const y2{y3 - grade(Result, piece.end) * handle}; + auto const roll = [&](station const &Station) { return static_cast(Station.turn * glm::degrees(std::asin(std::clamp(Station.cant / Design.norms.gauge, -1.0, 1.0)))); }; + segment_data path; + path.points[segment_data::point::start] = first.position; + path.points[segment_data::point::end] = last.position; + path.points[segment_data::point::control1] = glm::dvec3{p1.x, y1, p1.y} - first.position; + path.points[segment_data::point::control2] = glm::dvec3{p2.x, y2, p2.y} - last.position; + path.rolls[0] = roll(first); + path.rolls[1] = roll(last); + auto const &e{Result.elements[piece.element]}; + path.radius = e.kind == element_kind::straight ? 0.f : static_cast(e.radius); + result.push_back(path); + continue; + } + auto const &e{Result.elements[piece.element]}; + glm::dvec2 p0, p3, d0, d3; + element_point(e, Design.shape, piece.from, p0, d0); + element_point(e, Design.shape, piece.to, p3, d3); + glm::dvec2 p1, p2; + switch (e.kind) + { + case element_kind::straight: + p1 = p0 + (p3 - p0) / 3.0; + p2 = p0 + (p3 - p0) * (2.0 / 3.0); + break; + case element_kind::arc: + { + auto const angle{(piece.to - piece.from) / e.radius}; + auto const handle{4.0 / 3.0 * std::tan(angle / 4.0) * e.radius}; + p1 = p0 + d0 * handle; + p2 = p3 - d3 * handle; + break; + } + default: + { + if (Design.shape == transition_shape::cubic_parabola && e.kind != element_kind::spiral) + { + std::array const local{glm::dvec2{0.0, 0.0}, glm::dvec2{e.transition / 3.0, 0.0}, glm::dvec2{e.transition * 2.0 / 3.0, 0.0}, glm::dvec2{e.transition, e.transition * e.transition / (6.0 * e.radius)}}; + auto const a{e.kind == element_kind::transition_in ? piece.from / e.transition : 1.0 - piece.from / e.transition}; + auto const b{e.kind == element_kind::transition_in ? piece.to / e.transition : 1.0 - piece.to / e.transition}; + auto const world = [&](glm::dvec2 const &Point) { return e.origin + e.direction * Point.x + e.normal * Point.y; }; + p0 = world(blossom(local, a, a, a)); + p1 = world(blossom(local, a, a, b)); + p2 = world(blossom(local, a, b, b)); + p3 = world(blossom(local, b, b, b)); + } + else + { + auto const chord{glm::distance(p0, p3) / 3.0}; + p1 = p0 + d0 * chord; + p2 = p3 - d3 * chord; + } + break; + } + } + auto const s0{e.chainage + piece.from}; + auto const s3{e.chainage + piece.to}; + auto const y0{elevation(Result, s0)}; + auto const y3{elevation(Result, s3)}; + auto const y1{y0 + grade(Result, s0) * glm::distance(p0, p1)}; + auto const y2{y3 - grade(Result, s3) * glm::distance(p2, p3)}; + + auto const roll = [&](double const Cant) { return static_cast(e.turn * glm::degrees(std::asin(std::clamp(Cant / Design.norms.gauge, -1.0, 1.0)))); }; + + segment_data path; + path.points[segment_data::point::start] = {p0.x, y0, p0.y}; + path.points[segment_data::point::end] = {p3.x, y3, p3.y}; + path.points[segment_data::point::control1] = glm::dvec3{p1.x, y1, p1.y} - path.points[segment_data::point::start]; + path.points[segment_data::point::control2] = glm::dvec3{p2.x, y2, p2.y} - path.points[segment_data::point::end]; + path.rolls[0] = roll(element_cant(e, piece.from)); + path.rolls[1] = roll(element_cant(e, piece.to)); + path.radius = e.kind == element_kind::straight ? 0.f : static_cast(e.radius); + if (e.kind == element_kind::spiral) + { + auto const curvature{e.curvature_start + (e.curvature_end - e.curvature_start) * (piece.from + piece.to) * 0.5 / std::max(e.length, 1e-9)}; + path.radius = curvature > 1e-9 ? static_cast(1.0 / curvature) : 0.f; + } + result.push_back(path); + } + return result; +} + +} diff --git a/editor/editorAlignment.hpp b/editor/editorAlignment.hpp new file mode 100644 index 00000000..732bd454 --- /dev/null +++ b/editor/editorAlignment.hpp @@ -0,0 +1,187 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http: +*/ + +#pragma once + +#include +#include +#include + +#include "world/Segment.h" + +namespace alignment +{ + +enum class transition_shape +{ + cubic_parabola, + clothoid +}; + +struct limits +{ + double gauge{1500.0}; + double unbalanced{0.6}; + double cant_max{150.0}; + double cant_rate{35.0}; + double ramp_factor{10.0}; + double jerk{0.3}; + double vertical_factor{0.4}; + double vertical_min{2000.0}; + double tangent_min_time{0.0}; +}; + +struct compound_arc +{ + double radius{600.0}; + double transition{0.0}; + double share{1.0}; +}; + +struct vertex +{ + glm::dvec2 position{0.0}; + double offset{100.0}; + double radius{1000.0}; + double transition_in{0.0}; + double transition_out{0.0}; + double cant{0.0}; + bool reverse_turn{false}; + bool compound{false}; + double share{1.0}; + std::vector arcs; +}; + +struct design +{ + glm::dvec3 start{0.0}; + glm::dvec3 end{0.0}; + glm::dvec2 start_direction{0.0, 1.0}; + glm::dvec2 end_direction{0.0, 1.0}; + double start_grade{0.0}; + double end_grade{0.0}; + double start_radius{0.0}; + double end_radius{0.0}; + // length of the straight beyond the start and the end, which the curves can take over + double start_reserve{0.0}; + double end_reserve{0.0}; + std::vector vertices; + transition_shape shape{transition_shape::cubic_parabola}; + double speed{100.0}; + int transition_pieces{1}; + double arc_piece_angle{10.0}; + limits norms; +}; + +struct recommendation +{ + double cant_equilibrium{0.0}; + double cant_min{0.0}; + double cant{0.0}; + double radius_min{0.0}; + double transition{0.0}; +}; +recommendation recommend(double const Speed, double const Radius, limits const &Limits); +double transition_length(double const Speed, double const Radius, double const Cant, limits const &Limits); +double unbalanced_acceleration(double const Speed, double const Radius, double const Cant, limits const &Limits); + +enum class element_kind +{ + straight, + transition_in, + arc, + transition_out, + spiral +}; + +struct element +{ + element_kind kind{element_kind::straight}; + int vertex{-1}; + double chainage{0.0}; + double length{0.0}; + double radius{0.0}; + double transition{0.0}; + int turn{0}; + double cant{0.0}; + double curvature_start{0.0}; + double curvature_end{0.0}; + double curvature_peak{0.0}; + glm::dvec2 origin{0.0}; + glm::dvec2 direction{0.0, 1.0}; + glm::dvec2 normal{0.0}; +}; + +struct curve_report +{ + int vertex{-1}; + double radius{0.0}; + std::vector radii; + double transition_in{0.0}; + double transition_out{0.0}; + double deflection{0.0}; + double arc_length{0.0}; + double tangent_in{0.0}; + double tangent_out{0.0}; + double unbalanced{0.0}; + double cant_rate_in{0.0}, cant_rate_out{0.0}; + double jerk_in{0.0}, jerk_out{0.0}; + recommendation recommended; +}; + +struct profile_point +{ + double chainage{0.0}; + double elevation{0.0}; + double radius{0.0}; + double tangent{0.0}; +}; + +struct result +{ + bool valid{false}; + transition_shape shape{transition_shape::cubic_parabola}; + double gauge{1500.0}; + std::vector errors; + std::vector warnings; + std::vector vertices; + std::vector vertex_chainages; + std::vector vertex_elevations; + std::vector elements; + std::vector curves; + std::vector profile; + double length{0.0}; + glm::dvec3 start{0.0}; + glm::dvec3 end{0.0}; + double start_extension{0.0}; + double end_extension{0.0}; +}; + +result compute(design const &Design); +bool tangent_intersection(design const &Design, glm::dvec2 &Point); +bool collinear(design const &Design); + +struct station +{ + glm::dvec3 position{0.0}; + glm::dvec3 direction{0.0}; + double cant{0.0}; + int turn{0}; +}; +station evaluate(result const &Result, double const Chainage); +double elevation(result const &Result, double const Chainage); +double grade(result const &Result, double const Chainage); + +std::size_t minimum_pieces(result const &Result, design const &Design); +// Breaks: ascending chainages at which pieces have to end. Counts: least number of the pieces between the consecutive breaks, +// one more than the breaks; Intervals receives the number of the interval of each piece +std::vector pieces(result const &Result, design const &Design, std::size_t const Count, std::vector const &Breaks = {}, std::vector const &Counts = {}, + std::vector *Intervals = nullptr); + +} diff --git a/editor/editorFormat.hpp b/editor/editorFormat.hpp new file mode 100644 index 00000000..e22a9745 --- /dev/null +++ b/editor/editorFormat.hpp @@ -0,0 +1,26 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +// printf-style formatting into a string, for the messages and the data lines of the editor +template +std::string format(char const *Format, Args... Arguments) +{ + auto const length{std::snprintf(nullptr, 0, Format, Arguments...)}; + if (length <= 0) + return {}; + std::string text(static_cast(length) + 1, '\0'); + std::snprintf(text.data(), text.size(), Format, Arguments...); + text.resize(static_cast(length)); + return text; +} diff --git a/editor/editorGauge.cpp b/editor/editorGauge.cpp new file mode 100644 index 00000000..bb78f25a --- /dev/null +++ b/editor/editorGauge.cpp @@ -0,0 +1,320 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorGauge.hpp" + +#include +#include +#include +#include + +namespace gauge +{ + +namespace +{ + +// the widening runs up ahead of the change: on the inner side from 20 m before it, on the outer side from 26 m +// to 6 m before it (figures 3 to 5 of the standard) +double constexpr inner_lead{20.0}; +double constexpr outer_lead{26.0}; +double constexpr outer_full{6.0}; + +struct side_widening +{ + double inner{0.0}; + double outer{0.0}; +}; + +// above the lower part the gauge includes the curves of R >= 250 m already +side_widening upper_widening(double const Radius) +{ + if (Radius >= 250.0) + return {}; + return {std::max(0.0, 50.0 / Radius - 0.185), std::max(0.0, 60.0 / Radius - 0.225)}; +} + +side_widening lower_widening(double const Radius) +{ + if (Radius >= 250.0) + return {3.75 / Radius, 3.75 / Radius}; + return upper_widening(Radius); +} + +// values at the points of a route, read as linear between them and continued past the ends +class field +{ +public: + field(std::vector const &Chainage, std::vector Values) : m_chainage{Chainage}, m_values{std::move(Values)}, m_integral(m_values.size(), 0.0) + { + for (std::size_t i = 1; i < m_values.size(); ++i) + m_integral[i] = m_integral[i - 1] + 0.5 * (m_values[i - 1] + m_values[i]) * (m_chainage[i] - m_chainage[i - 1]); + } + + double mean(double const From, double const To) const + { + return (integral(To) - integral(From)) / (To - From); + } + + // larger of the means over the window placed ahead of the point in both directions + double ahead(double const Chainage, double const Near, double const Far) const + { + return std::max(mean(Chainage + Near, Chainage + Far), mean(Chainage - Far, Chainage - Near)); + } + +private: + double integral(double const Chainage) const + { + if (Chainage <= m_chainage.front()) + return m_values.front() * (Chainage - m_chainage.front()); + if (Chainage >= m_chainage.back()) + return m_integral.back() + m_values.back() * (Chainage - m_chainage.back()); + auto const next{static_cast(std::upper_bound(m_chainage.begin(), m_chainage.end(), Chainage) - m_chainage.begin())}; + auto const i{next - 1}; + auto const span{m_chainage[next] - m_chainage[i]}; + auto const offset{Chainage - m_chainage[i]}; + auto const value{span > 0.0 ? m_values[i] + (m_values[next] - m_values[i]) * offset / span : m_values[i]}; + return m_integral[i] + 0.5 * (m_values[i] + value) * offset; + } + + std::vector const &m_chainage; + std::vector m_values; + std::vector m_integral; +}; + +// half of the distance between the heads of the rails +double constexpr rail_spread{0.75}; + +// the point turned by the angle of the sine about the head of the lower rail +place turn(section const &Section, place const &Point, double const Sine) +{ + auto const pivot{Section.tilt > 0.0 ? -rail_spread : rail_spread}; + auto const cosine{std::sqrt(std::max(0.0, 1.0 - Sine * Sine))}; + auto const lateral{Point.lateral - pivot}; + return {pivot + lateral * cosine - Point.height * Sine, lateral * Sine + Point.height * cosine}; +} + +// lower part common to all gauges, from the axis between the rails: the wheel zone at the running edge (717.5 mm +// from the axis) with the flangeway of the dimension "b" 58 mm wide and 38 mm deep, the dimension "a" of 150 mm +// at the rail top outside of it, then the limit installation gauge up to 380 mm +std::vector const common_lower{{0.6595, 0.055}, {0.6595, -0.038}, {0.7175, -0.038}, {0.7175, 0.0}, {0.8675, 0.0}, + {0.8675, 0.055}, {1.211, 0.055}, {1.585, 0.380}, {1.675, 0.380}}; + +// the wheel zone follows the rails, without the widening in the curves +bool wheel_zone(profile::point const &Point) +{ + return Point.height <= 0.055 + 1e-9 && Point.half_width < 1.0; +} + +// names of the kinds in the file +char const *const kind_names[]{"unified", "installation", "road"}; + +// outline from the points listed top down, in millimetres, as in the tables of the standard, over the lower part +profile installation(std::string const &Name, std::vector> const &Points) +{ + profile result{Name, kind::installation, common_lower}; + for (auto point = Points.rbegin(); point != Points.rend(); ++point) + result.outline.push_back({point->second * 0.001, point->first * 0.001}); + return result; +} + +// point of the outline on the side (+1 left, -1 right) at the section +place outline_point(profile const &Profile, section const &Section, std::size_t const Index, int const Side) +{ + auto const &point{Profile.outline[Index]}; + // the step at the top of the lower part belongs to it from below + bool const lower{point.height < lower_part - 1e-9 || (point.height < lower_part + 1e-9 && Index > 0 && Profile.outline[Index - 1].height < lower_part - 1e-9)}; + auto const i{Side > 0 ? 1 : 0}; + auto const widening{wheel_zone(point) ? 0.0 : lower ? Section.lower[i] : Section.upper[i] + Section.cant[i] * point.height / 1.5}; + auto const width{Section.base + point.half_width + widening}; + place const result{Side * width, point.height}; + return lower && Section.tilt != 0.0 ? turn(Section, result, Section.tilt) : result; +} + +// sign of the curvature at the points, from the nearest curve along the line where it's straight +std::vector curve_sides(std::vector const &Curvature) +{ + auto const side{[](double const Curvature) { return std::abs(Curvature) > 1e-6 ? (Curvature > 0.0 ? 1.0 : -1.0) : 0.0; }}; + std::vector sides(Curvature.size(), 0.0); + double last{0.0}; + for (std::size_t i = 0; i < Curvature.size(); ++i) + sides[i] = last = side(Curvature[i]) != 0.0 ? side(Curvature[i]) : last; + last = 0.0; + for (std::size_t i = Curvature.size(); i-- > 0;) + { + if (side(Curvature[i]) != 0.0) + last = side(Curvature[i]); + else if (sides[i] == 0.0) + sides[i] = last; + } + return sides; +} + +} // namespace + +profile const *choice::of(int const Category) const +{ + if ((Category & 1) != 0) + return railway; + if ((Category & 2) != 0) + return road; + return nullptr; +} + +std::vector default_profiles() +{ + // limit installation gauge up to 1170 mm, then the outline of the unified gauge + auto lower{common_lower}; + lower.insert(lower.end(), {{1.675, 1.170}, {2.000, 1.170}}); + std::vector const gpl1{{2.000, 3.050}, {1.900, 3.850}, {1.800, 4.250}, {1.600, 4.500}, {1.450, 4.632}}; + std::vector const gpl2{{2.000, 3.625}, {1.920, 4.900}}; + std::vector const pantograph{{1.450, 6.600}, {1.150, 6.900}, {0.0, 6.900}}; + auto const make{[&](std::string const &Name, std::vector> const &Parts) { + profile result{Name, kind::unified, lower}; + for (auto const &part : Parts) + result.outline.insert(result.outline.end(), part.begin(), part.end()); + return result; + }}; + // tables 3, 5, 7, 9 of the type cards and the drawing of GC, above 380 mm + return {make("GPL-1", {gpl1, {{1.260, 4.800}, {1.180, 4.850}, {0.0, 4.850}}}), + make("GPL-1 + pantograf", {gpl1, pantograph}), + make("GPL-2", {gpl2, {{0.0, 4.900}}}), + make("GPL-2 + pantograf", {gpl2, {{1.450, 4.900}}, pantograph}), + installation("G1 graniczna (GSZ)", {{4375, 0}, {4375, 666}, {4010, 1255}, {3700, 1550}, {3250, 1755}, {1170, 1695}, {1170, 1675}}), + installation("G1 nominalna (NSZ)", {{4385, 0}, {4385, 785}, {4010, 1365}, {3700, 1655}, {3250, 1860}, {1170, 1860}, {1170, 1675}}), + installation("G2 graniczna (GSZ)", {{4765, 0}, {4765, 940}, {3835, 1600}, {3530, 1765}, {1170, 1695}, {1170, 1675}}), + installation("G2 nominalna (NSZ)", {{4770, 0}, {4770, 1060}, {3835, 1710}, {3530, 1870}, {1170, 1870}, {1170, 1675}}), + installation("GA graniczna (GSZ)", {{4415, 0}, {4415, 687}, {4080, 1225}, {3808, 1490}, {3250, 1755}, {1170, 1700}, {1170, 1675}}), + installation("GA nominalna (NSZ)", {{4425, 0}, {4425, 805}, {4080, 1340}, {3880, 1600}, {3250, 1860}, {1170, 1860}, {1170, 1675}}), + installation("GB graniczna (GSZ)", {{4415, 0}, {4415, 687}, {4080, 1495}, {3250, 1755}, {1170, 1700}, {1170, 1675}}), + installation("GB nominalna (NSZ)", {{4425, 0}, {4425, 805}, {4110, 1610}, {3250, 1860}, {1170, 1860}, {1170, 1675}}), + installation("GC graniczna (GSZ)", {{4800, 0}, {4800, 1693}, {3550, 1765}, {1170, 1765}, {1170, 1675}}), + installation("GC nominalna (NSZ)", {{4820, 0}, {4820, 1815}, {3550, 1870}, {1170, 1870}, {1170, 1675}}), + {"Droga 4,70 m", kind::road, {{0.5, 0.0}, {0.5, 4.70}}}, + {"Droga 4,50 m", kind::road, {{0.5, 0.0}, {0.5, 4.50}}}}; +} + +// one outline per line: the name in quotes, the kind, then pairs of half-width and height +std::vector load_profiles(std::string const &File) +{ + std::ifstream input{File}; + std::vector profiles; + std::string line; + while (std::getline(input, line)) + { + std::istringstream stream{line}; + profile profile; + std::string kind; + if (false == static_cast(stream >> std::quoted(profile.name) >> kind)) + continue; + auto const known{std::find(std::begin(kind_names), std::end(kind_names), kind)}; + if (known == std::end(kind_names)) + continue; + profile.kind = static_cast(known - std::begin(kind_names)); + profile::point point; + while (stream >> point.half_width >> point.height) + profile.outline.push_back(point); + if (profile.outline.size() >= 2) + profiles.push_back(std::move(profile)); + } + return profiles.empty() ? default_profiles() : profiles; +} + +void save_profiles(std::string const &File, std::vector const &Profiles) +{ + std::ofstream output{File}; + for (auto const &profile : Profiles) + { + output << std::quoted(profile.name) << ' ' << kind_names[static_cast(profile.kind)]; + for (auto const &point : profile.outline) + output << ' ' << point.half_width << ' ' << point.height; + output << '\n'; + } +} + +std::vector
sections(kind const Kind, std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant) +{ + auto const count{Chainage.size()}; + std::vector
result(count); + if (count == 0 || Kind == kind::road) + return result; + // per side, the parts when it's the inner side of the curve and when it's the outer one + for (int side = 0; side < 2; ++side) + { + std::vector upperinner(count), upperouter(count), lowerinner(count), lowerouter(count), cantinner(count), cantouter(count); + for (std::size_t i = 0; i < count; ++i) + { + if (std::abs(Curvature[i]) < 1e-6) + continue; + auto const radius{1.0 / std::abs(Curvature[i])}; + bool const inner{(Curvature[i] > 0.0) == (side == 1)}; + auto const upper{Kind == kind::unified ? upper_widening(radius) : lower_widening(radius)}; + auto const lower{lower_widening(radius)}; + (inner ? upperinner : upperouter)[i] = inner ? upper.inner : upper.outer; + (inner ? lowerinner : lowerouter)[i] = inner ? lower.inner : lower.outer; + (inner ? cantinner : cantouter)[i] = Cant[i]; + } + field const fields[]{{Chainage, std::move(upperinner)}, {Chainage, std::move(upperouter)}, {Chainage, std::move(lowerinner)}, + {Chainage, std::move(lowerouter)}, {Chainage, std::move(cantinner)}, {Chainage, std::move(cantouter)}}; + for (std::size_t i = 0; i < count; ++i) + { + auto const inner{[&](field const &Field) { return Field.ahead(Chainage[i], 0.0, inner_lead); }}; + auto const outer{[&](field const &Field) { return Field.ahead(Chainage[i], outer_full, outer_lead); }}; + result[i].upper[side] = inner(fields[0]) + outer(fields[1]); + result[i].lower[side] = inner(fields[2]) + outer(fields[3]); + result[i].cant[side] = inner(fields[4]) - outer(fields[5]); + } + } + auto const sides{curve_sides(Curvature)}; + for (std::size_t i = 0; i < count; ++i) + result[i].tilt = -sides[i] * std::min(1.0, Cant[i] / 1.5); + return result; +} + +void ring(profile const &Profile, section const &Section, std::vector &Points) +{ + auto const count{Profile.outline.size()}; + Points.clear(); + for (std::size_t j = 0; j < count; ++j) + Points.push_back(outline_point(Profile, Section, j, -1)); + for (std::size_t j = count; j-- > 0;) + Points.push_back(outline_point(Profile, Section, j, 1)); +} + +double intrusion(std::vector const &Ring, place const &Point) +{ + if (Ring.size() < 3) + return 0.0; + bool inside{false}; + auto nearest{std::numeric_limits::max()}; + auto const *previous{&Ring.back()}; + for (auto const ¤t : Ring) + { + auto const lateral{current.lateral - previous->lateral}; + auto const height{current.height - previous->height}; + if ((current.height > Point.height) != (previous->height > Point.height) && Point.lateral < previous->lateral + lateral * (Point.height - previous->height) / height) + inside = false == inside; + auto const length{lateral * lateral + height * height}; + auto const t{length > 0.0 ? std::clamp(((Point.lateral - previous->lateral) * lateral + (Point.height - previous->height) * height) / length, 0.0, 1.0) : 0.0}; + nearest = std::min(nearest, std::hypot(Point.lateral - previous->lateral - lateral * t, Point.height - previous->height - height * t)); + previous = ¤t; + } + return inside ? nearest : -nearest; +} + +double widest(profile const &Profile) +{ + double result{0.0}; + for (auto const &point : Profile.outline) + result = std::max(result, point.half_width); + return result; +} + +} // namespace gauge diff --git a/editor/editorGauge.hpp b/editor/editorGauge.hpp new file mode 100644 index 00000000..caae3a4c --- /dev/null +++ b/editor/editorGauge.hpp @@ -0,0 +1,95 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +// structure gauge (skrajnia budowli) of the PKP PLK technical standards, volume II: the space along the track +// which has to stay clear of the objects +namespace gauge +{ + +// how the outline widens along the track +enum class kind +{ + unified, // skrajnia budowli ujednolicona GPL: the curves of R >= 250 m are included above the lower part + installation, // graniczna or nominalna skrajnia zabudowy: widened by 3750/R in all curves + road // skrajnia drogi: measured from the edge of the carriageway, no widening +}; + +// right half of the outline on a straight track, in metres: half-width from the axis (for the roads from the edge +// of the carriageway) and height above the rail top or the road surface, listed from the bottom up; the left half +// is the mirror +struct profile +{ + std::string name; + gauge::kind kind{kind::unified}; + struct point + { + double half_width{0.0}; + double height{0.0}; + }; + std::vector outline; +}; + +// the outlines checked along the paths: the railway one on the tracks, the road one on the roads +struct choice +{ + profile const *railway{nullptr}; + profile const *road{nullptr}; + // by the category flags of the path, 1 a track and 2 a road; null when there's no outline for it + profile const *of(int Category) const; +}; + +// GPL-1 and GPL-2 over the limit installation gauge below 1170 mm, with and without the pantograph gauge, the limit +// and nominal installation gauges G1, G2, GA, GB, GC of the PKP PLK technical standards, and the road gauges +std::vector default_profiles(); +// outlines kept in the file, the defaults when there's none +std::vector load_profiles(std::string const &File); +void save_profiles(std::string const &File, std::vector const &Profiles); + +// the points of the paths in the scenery lie at the rail foot, the gauge is measured from the rail head +double constexpr rail_height{0.18}; + +// below this height the outline is the limit installation gauge, which leaves the room for the platforms +double constexpr lower_part{1.17}; + +// widening of the sides of the gauge at a place of the track, metres; index 0 is the right side, 1 the left one +struct section +{ + double base{0.0}; // added to the half-width of the outline: half of the carriageway of a road + std::array upper{}; // from the radius, above the lower part + std::array lower{}; // from the radius, in the lower part + std::array cant{}; // cant in metres, positive on the inner side of the curve and negative on the outer one + double tilt{0.0}; // sine of the slope of the plane of the rail heads, positive with the left rail raised +}; +// point of the cross-section, metres: lateral positive to the left, height above the rail top +struct place +{ + double lateral{0.0}; + double height{0.0}; +}; +// sections at the points of a route, from their chainage, curvature (1/m, positive turning left) and cant (m). +// The widening begins and ends ahead of the changes of the curvature and cant, as the vehicles enter a curve; +// the plane of the rail heads tilts with the cant, the outer rail raised +std::vector
sections(kind Kind, std::vector const &Chainage, std::vector const &Curvature, std::vector const &Cant); + +// the outline across the track at the section, from the bottom right up and down to the bottom left, closed across +// the bottom. With the widening: the cant shifts the part above the lower one, the lower part turns with the plane +// of the rail heads instead +void ring(profile const &Profile, section const &Section, std::vector &Points); +// distance of the point from the ring in any direction, metres, positive inside and negative outside +double intrusion(std::vector const &Ring, place const &Point); +// largest half-width of the outline, metres +double widest(profile const &Profile); + +} // namespace gauge diff --git a/editor/editorGeometry.hpp b/editor/editorGeometry.hpp new file mode 100644 index 00000000..2253f43f --- /dev/null +++ b/editor/editorGeometry.hpp @@ -0,0 +1,162 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "world/Segment.h" + +// plan (x, z) geometry and cubic Bezier curves of the paths, shared by the track tools +namespace geometry +{ + +// points of the paths closer than this are taken for the same point +double constexpr same_point{0.05}; + +inline glm::dvec2 plan_of(glm::dvec3 const &Point) +{ + return {Point.x, Point.z}; +} + +inline double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return std::hypot(A.x - B.x, A.z - B.z); +} + +// z component of the 3d cross product; for a unit Direction it's the offset of the Point to its left +inline double cross(glm::dvec2 const &Direction, glm::dvec2 const &Point) +{ + return Direction.x * Point.y - Direction.y * Point.x; +} + +inline glm::dvec2 turned(glm::dvec2 const &Vector, double const Angle) +{ + auto const c{std::cos(Angle)}; + auto const s{std::sin(Angle)}; + return {Vector.x * c - Vector.y * s, Vector.x * s + Vector.y * c}; +} + +inline double signed_angle(glm::dvec2 const &From, glm::dvec2 const &To) +{ + return std::atan2(cross(From, To), glm::dot(From, To)); +} + +// slope of the direction, rise over the run in the plan +inline double grade_of(glm::dvec3 const &Direction) +{ + return Direction.y / std::max(1e-9, std::hypot(Direction.x, Direction.z)); +} + +inline std::vector arc_pieces(glm::dvec3 const &Point, glm::dvec2 const &Direction, double const Grade, double const Radius, double const Angle, int const Side) +{ + std::vector result; + double const side{static_cast(Side)}; + glm::dvec2 const centre{plan_of(Point) + glm::dvec2{-Direction.y, Direction.x} * (side * Radius)}; + auto const count{std::max(1, static_cast(std::ceil(Angle / glm::radians(90.0) - 1e-9)))}; + auto const step{Angle / count}; + auto const handle{4.0 / 3.0 * std::tan(step / 4.0) * Radius}; + glm::dvec2 const radial{plan_of(Point) - centre}; + for (int i = 0; i < count; ++i) + { + auto const a0{side * step * i}; + auto const a1{side * step * (i + 1)}; + auto const p0{centre + turned(radial, a0)}; + auto const p3{centre + turned(radial, a1)}; + auto const d0{turned(Direction, a0)}; + auto const d3{turned(Direction, a1)}; + auto const y0{Point.y + Grade * Radius * step * i}; + auto const y3{Point.y + Grade * Radius * step * (i + 1)}; + segment_data path; + path.points[segment_data::point::start] = {p0.x, y0, p0.y}; + path.points[segment_data::point::end] = {p3.x, y3, p3.y}; + path.points[segment_data::point::control1] = {d0.x * handle, Grade * handle, d0.y * handle}; + path.points[segment_data::point::control2] = {-d3.x * handle, -Grade * handle, -d3.y * handle}; + path.radius = static_cast(Radius); + result.push_back(path); + } + return result; +} + +// path as a cubic Bezier curve; a path without control points is a straight with evenly spread parameter +struct bezier +{ + glm::dvec3 p0, p1, p2, p3; + + explicit bezier(segment_data const &Path) + { + p0 = Path.points[segment_data::point::start]; + p3 = Path.points[segment_data::point::end]; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + p1 = glm::mix(p0, p3, 1.0 / 3.0); + p2 = glm::mix(p0, p3, 2.0 / 3.0); + } + else + { + p1 = p0 + control1; + p2 = p3 + control2; + } + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + } + glm::dvec3 first(double const T) const + { + auto const u{1.0 - T}; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); + } + glm::dvec3 second(double const T) const + { + return 6.0 * (1.0 - T) * (p2 - 2.0 * p1 + p0) + 6.0 * T * (p3 - 2.0 * p2 + p1); + } + double plan_length(int const Steps = 32) const + { + double length{0.0}; + auto previous{plan_of(p0)}; + for (int i = 1; i <= Steps; ++i) + { + auto const next{plan_of(point(static_cast(i) / Steps))}; + length += glm::distance(previous, next); + previous = next; + } + return length; + } + // parameter at specified plan distance from the start + double parameter(double const Distance, int const Steps = 256) const + { + double length{0.0}; + auto previous{plan_of(p0)}; + for (int i = 1; i <= Steps; ++i) + { + auto const next{plan_of(point(static_cast(i) / Steps))}; + auto const step{glm::distance(previous, next)}; + if (length + step >= Distance) + return (i - 1 + (step > 1e-12 ? (Distance - length) / step : 0.0)) / Steps; + length += step; + previous = next; + } + return 1.0; + } +}; + +// distance between the ends of the path, in the plan +inline double chord_length(segment_data const &Path) +{ + return glm::length(plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])); +} + +} // namespace geometry diff --git a/editor/editorGroundMesh.hpp b/editor/editorGroundMesh.hpp new file mode 100644 index 00000000..4fc3f10b --- /dev/null +++ b/editor/editorGroundMesh.hpp @@ -0,0 +1,150 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +// triangles of a single piece of the scenery - a shape or a model - sorted on a grid seen from above, so the ones +// overlapping an area can be picked without going through all of them. +// the editor asks about the ground a tile at a time, frame after frame, and a terrain model can hold a million +// triangles: walking it whole for every question is what used to bring the simulator down to a frame a second +class ground_mesh +{ + public: + using triangle = std::array; // relative to the origin of the mesh + using world_triangle = std::array; + + // takes over provided triangles. Origin: the point their vertices are relative to + void assign(std::vector Triangles, glm::dvec3 const &Origin) + { + m_triangles = std::move(Triangles); + m_origin = Origin; + m_starts.clear(); + m_items.clear(); + m_seen.assign(m_triangles.size(), 0); + m_query = 0; + if (m_triangles.empty()) + return; + + glm::vec2 low{m_triangles.front()[0].x, m_triangles.front()[0].z}; + glm::vec2 high{low}; + for (auto const &entry : m_triangles) + { + for (auto const &vertex : entry) + { + low = glm::min(low, glm::vec2{vertex.x, vertex.z}); + high = glm::max(high, glm::vec2{vertex.x, vertex.z}); + } + } + m_low = low; + auto const extent{glm::max(high - low, glm::vec2{1.f})}; + // cells of a few metres, larger for a large mesh so the grid stays small + m_cellsize = std::max(8.f, std::max(extent.x, extent.y) / 96.f); + m_columns = static_cast(extent.x / m_cellsize) + 1; + m_rows = static_cast(extent.y / m_cellsize) + 1; + + // a triangle is listed in every cell its bounds reach. the lists are kept one after another, with the place each starts at + std::vector counts(static_cast(m_columns) * m_rows + 1, 0); + for (auto const &entry : m_triangles) + { + auto const span{cells(entry)}; + for (int row = span[1]; row <= span[3]; ++row) + for (int column = span[0]; column <= span[2]; ++column) + ++counts[static_cast(row) * m_columns + column + 1]; + } + for (std::size_t cell = 1; cell < counts.size(); ++cell) + counts[cell] += counts[cell - 1]; + m_starts = counts; + m_items.resize(m_starts.back()); + for (std::uint32_t index = 0; index < m_triangles.size(); ++index) + { + auto const span{cells(m_triangles[index])}; + for (int row = span[1]; row <= span[3]; ++row) + for (int column = span[0]; column <= span[2]; ++column) + m_items[counts[static_cast(row) * m_columns + column]++] = index; + } + } + std::size_t size() const + { + return m_triangles.size(); + } + // appends the triangles whose bounds overlap specified rectangle of the world, seen from above (x, z) + void collect(glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector &Out) const + { + if (m_triangles.empty()) + return; + // the rectangle as the mesh sees it, widened by what's lost turning the numbers into single precision + glm::vec2 const low{static_cast(Min.x - m_origin.x) - 0.001f, static_cast(Min.y - m_origin.z) - 0.001f}; + glm::vec2 const high{static_cast(Max.x - m_origin.x) + 0.001f, static_cast(Max.y - m_origin.z) + 0.001f}; + auto const firstcolumn{column(low.x)}; + auto const lastcolumn{column(high.x)}; + auto const firstrow{row(low.y)}; + auto const lastrow{row(high.y)}; + if (++m_query == 0) + { + // the counter went all the way round + std::fill(m_seen.begin(), m_seen.end(), 0); + m_query = 1; + } + for (int row = firstrow; row <= lastrow; ++row) + { + for (int column = firstcolumn; column <= lastcolumn; ++column) + { + auto const cell{static_cast(row) * m_columns + column}; + for (auto item = m_starts[cell]; item < m_starts[cell + 1]; ++item) + { + auto const index{m_items[item]}; + if (m_seen[index] == m_query) + continue; + m_seen[index] = m_query; + auto const &entry{m_triangles[index]}; + if (std::max({entry[0].x, entry[1].x, entry[2].x}) < low.x || std::min({entry[0].x, entry[1].x, entry[2].x}) > high.x || std::max({entry[0].z, entry[1].z, entry[2].z}) < low.y || + std::min({entry[0].z, entry[1].z, entry[2].z}) > high.y) + continue; + Out.push_back({m_origin + glm::dvec3{entry[0]}, m_origin + glm::dvec3{entry[1]}, m_origin + glm::dvec3{entry[2]}}); + } + } + } + } + + private: + // NOTE: clamped before the conversion: a rectangle asked about can reach far beyond the mesh (to infinity even), + // and a float out of the range of int doesn't turn into anything sensible + int column(float const X) const + { + return static_cast(std::clamp(std::floor((X - m_low.x) / m_cellsize), 0.f, static_cast(m_columns - 1))); + } + int row(float const Z) const + { + return static_cast(std::clamp(std::floor((Z - m_low.y) / m_cellsize), 0.f, static_cast(m_rows - 1))); + } + // cells the bounds of a triangle reach: first column, first row, last column, last row + std::array cells(triangle const &Triangle) const + { + return {column(std::min({Triangle[0].x, Triangle[1].x, Triangle[2].x})), row(std::min({Triangle[0].z, Triangle[1].z, Triangle[2].z})), column(std::max({Triangle[0].x, Triangle[1].x, Triangle[2].x})), + row(std::max({Triangle[0].z, Triangle[1].z, Triangle[2].z}))}; + } + + std::vector m_triangles; + glm::dvec3 m_origin{0.0}; + glm::vec2 m_low{0.f}; + float m_cellsize{8.f}; + int m_columns{1}; + int m_rows{1}; + std::vector m_starts; // for each cell: where its list starts; one more entry closes the last list + std::vector m_items; // numbers of the triangles, cell after cell + mutable std::vector m_seen; // number of the last lookup each triangle was reported in + mutable std::uint32_t m_query{0}; +}; diff --git a/editor/editorIcons.cpp b/editor/editorIcons.cpp new file mode 100644 index 00000000..6056763f --- /dev/null +++ b/editor/editorIcons.cpp @@ -0,0 +1,176 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorIcons.hpp" + +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "stb/stb_image.h" + +#include +#include +#include +#include +#include + +namespace editor_icons +{ + +namespace +{ + +// editor_icons.png, written out by CMake +unsigned char const png[] = { +#include "editor_icons_png.inc" +}; + +int const cell{64}; // pixels of an icon in the png +int const columns{8}; +int const rows{(static_cast(icon::count) + columns - 1) / columns}; + +// the coverage of the icons, a byte a pixel, as they're in the png. empty if it couldn't be read +std::vector const &coverage() +{ + static std::vector const pixels = []() { + std::vector result; + // read in a thread of its own: the texture loader turns the images upside down for the threads which don't say otherwise, + // and what a thread says stays with it + std::thread reader([&result]() { + stbi_set_flip_vertically_on_load_thread(0); + int width{0}, height{0}, components{0}; + auto *rgba{stbi_load_from_memory(png, static_cast(sizeof(png)), &width, &height, &components, 4)}; + if (rgba == nullptr) + return; + if (width == columns * cell && height >= rows * cell) + { + result.resize(static_cast(width) * height); + for (std::size_t index = 0; index < result.size(); ++index) + result[index] = rgba[index * 4 + 3]; + } + stbi_image_free(rgba); + }); + reader.join(); + return result; + }(); + return pixels; +} + +// the icons scaled down to the size they're drawn at, each with a pixel of empty space around it, so that the filtering +// doesn't pick up a neighbour +struct atlas +{ + std::uint64_t texture{0}; + int size{0}; // pixels of an icon + bool failed{false}; +}; +atlas g_atlas; + +int stride(int const Size) +{ + return Size + 2; +} + +// part of the source pixel Pixel the target pixel Target of the size Scale covers +float overlap(int const Target, float const Scale, int const Pixel) +{ + auto const start{std::max(Target * Scale, static_cast(Pixel))}; + auto const end{std::min((Target + 1) * Scale, static_cast(Pixel + 1))}; + return std::max(0.0f, end - start); +} + +std::vector scaled(int const Size) +{ + auto const &source{coverage()}; + auto const width{columns * stride(Size)}; + auto const height{rows * stride(Size)}; + std::vector rgba(static_cast(width) * height * 4, 255); + for (std::size_t index = 3; index < rgba.size(); index += 4) + rgba[index] = 0; + // each target pixel the average of the source pixels under it, the ones under its edges by the part they're under it + auto const scale{static_cast(cell) / Size}; + std::vector across(static_cast(Size) * cell); + for (int index = 0; index < static_cast(icon::count); ++index) + { + auto const sourcex{(index % columns) * cell}; + auto const sourcey{(index / columns) * cell}; + for (int y = 0; y < cell; ++y) + { + for (int x = 0; x < Size; ++x) + { + float sum{0.0f}; + for (int pixel = static_cast(x * scale); pixel < std::min(cell, static_cast(std::ceil((x + 1) * scale))); ++pixel) + sum += overlap(x, scale, pixel) * source[static_cast(sourcey + y) * columns * cell + sourcex + pixel]; + across[static_cast(y) * Size + x] = sum / scale; + } + } + auto const targetx{(index % columns) * stride(Size) + 1}; + auto const targety{(index / columns) * stride(Size) + 1}; + for (int y = 0; y < Size; ++y) + { + for (int x = 0; x < Size; ++x) + { + float sum{0.0f}; + for (int pixel = static_cast(y * scale); pixel < std::min(cell, static_cast(std::ceil((y + 1) * scale))); ++pixel) + sum += overlap(y, scale, pixel) * across[static_cast(pixel) * Size + x]; + rgba[(static_cast(targety + y) * width + targetx + x) * 4 + 3] = static_cast(std::clamp(sum / scale + 0.5f, 0.0f, 255.0f)); + } + } + } + return rgba; +} + +// the texture of the icons at the size, made again when the size changes +bool prepare(int const Size) +{ + if (g_atlas.failed) + return false; + if (g_atlas.texture != 0 && g_atlas.size == Size) + return true; + auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr}; + if (renderer == nullptr || coverage().empty()) + { + g_atlas.failed = true; + return false; + } + if (g_atlas.texture != 0) + renderer->Release_Image(g_atlas.texture); + auto const rgba{scaled(Size)}; + g_atlas.texture = renderer->Create_Image(rgba.data(), columns * stride(Size), rows * stride(Size)); + g_atlas.size = Size; + // a renderer which can't show them gets no more requests + g_atlas.failed = (g_atlas.texture == 0); + return false == g_atlas.failed; +} + +} // namespace + +float size() +{ + return std::round(20.0f * std::max(1.0f, Global.ui_scale)); +} + +bool draw(ImDrawList *List, icon const Icon, ImVec2 const &Center, ImU32 const Color) +{ + // all at the one size, the texture is made for it + auto const pixels{static_cast(size())}; + if (List == nullptr || Icon >= icon::count || false == prepare(pixels)) + return false; + auto const index{static_cast(Icon)}; + auto const width{static_cast(columns * stride(pixels))}; + auto const height{static_cast(rows * stride(pixels))}; + ImVec2 const uv0{((index % columns) * stride(pixels) + 1) / width, ((index / columns) * stride(pixels) + 1) / height}; + ImVec2 const uv1{uv0.x + pixels / width, uv0.y + pixels / height}; + // on whole pixels, the icon was scaled for them + ImVec2 const min{std::round(Center.x - pixels * 0.5f), std::round(Center.y - pixels * 0.5f)}; + List->AddImage(static_cast(g_atlas.texture), min, ImVec2(min.x + pixels, min.y + pixels), uv0, uv1, Color); + return true; +} + +} // namespace editor_icons diff --git a/editor/editorIcons.hpp b/editor/editorIcons.hpp new file mode 100644 index 00000000..91c9a5f3 --- /dev/null +++ b/editor/editorIcons.hpp @@ -0,0 +1,66 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "imgui/imgui.h" + +// icons of the scenery editor. they're drawn by editor/icons/editor_icons.py into editor_icons.png, which CMake writes out +// as the bytes of an array built into the executable, so they're there without any file of the simulator +namespace editor_icons +{ + +// in the order of the cells of editor_icons.png +enum class icon +{ + select, + insert, + brush, + area_fill, + copy_to_bank, + translate, + rotate, + scale, + local_space, + ortho, + perspective, + sculpt, + smooth, + chunks, + orthophoto, + road, + place, + lanes, + lay_track, + turnout, + straight, + curve, + signal, + objects, + vehicle, + profile, + speed, + joints, + infra, + gauge, + turntable, + // the fields of work + work_surroundings, + work_tracks, + work_roads, + work_terrain, + count +}; + +// pixels of the side of an icon, at the scale of the user interface +float size(); +// draws the icon in the colour given, centred on the point. returns: false if the renderer can't show images +bool draw(ImDrawList *List, icon const Icon, ImVec2 const &Center, ImU32 const Color); + +} // namespace editor_icons diff --git a/editor/editorIncludeInfo.cpp b/editor/editorIncludeInfo.cpp new file mode 100644 index 00000000..1a3ea1e6 --- /dev/null +++ b/editor/editorIncludeInfo.cpp @@ -0,0 +1,849 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorIncludeInfo.hpp" + +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +#include + +#include +#include +#include +#include +#include + +namespace +{ + +std::string const linemark{"//$e"}; + +// template file split into the description and the rest +struct template_text +{ + std::string content; // whole file + std::string yaml; // description, with the line marks taken off; utf-8 + std::vector> lines; // ranges of the content taken by the description lines, line ends included + std::string eol{"\r\n"}; + std::size_t start{0}; // location of the first character past the byte order mark + bool utf8{false}; // the file is utf-8 encoded, rather than windows-1250 like most of the scenery files +}; + +// true if specified text is valid utf-8. HasExtended: set if the text has any characters outside of ascii +bool is_utf8(std::string const &Text, bool &HasExtended) +{ + HasExtended = false; + for (std::size_t idx = 0; idx < Text.size();) + { + auto const lead{static_cast(Text[idx])}; + if (lead < 0x80) + { + ++idx; + continue; + } + HasExtended = true; + auto const length{(lead & 0xE0) == 0xC0 ? 2 : (lead & 0xF0) == 0xE0 ? 3 : (lead & 0xF8) == 0xF0 ? 4 : 0}; + if (length == 0 || idx + length > Text.size()) + { + return false; + } + for (auto offset = 1; offset < length; ++offset) + { + if ((static_cast(Text[idx + offset]) & 0xC0) != 0x80) + { + return false; + } + } + idx += length; + } + return true; +} + +// counterpart of win1250_to_utf8(). characters outside of the supported set are replaced with question marks +std::string utf8_to_win1250(std::string const &Text) +{ + // polish letters, as utf-8 sequences; the same set win1250_to_utf8() handles + static std::unordered_map const charmap{{"\xC4\x84", '\xA5'}, {"\xC4\x86", '\xC6'}, {"\xC4\x98", '\xCA'}, {"\xC5\x81", '\xA3'}, {"\xC5\x83", '\xD1'}, {"\xC3\x93", '\xD3'}, + {"\xC5\x9A", '\x8C'}, {"\xC5\xB9", '\x8F'}, {"\xC5\xBB", '\xAF'}, {"\xC4\x85", '\xB9'}, {"\xC4\x87", '\xE6'}, {"\xC4\x99", '\xEA'}, + {"\xC5\x82", '\xB3'}, {"\xC5\x84", '\xF1'}, {"\xC3\xB3", '\xF3'}, {"\xC5\x9B", '\x9C'}, {"\xC5\xBA", '\x9F'}, {"\xC5\xBC", '\xBF'}}; + std::string output; + for (std::size_t idx = 0; idx < Text.size();) + { + auto const lead{static_cast(Text[idx])}; + if (lead < 0x80) + { + output += Text[idx++]; + continue; + } + auto const length{std::min((lead & 0xE0) == 0xC0 ? 2 : (lead & 0xF0) == 0xE0 ? 3 : (lead & 0xF8) == 0xF0 ? 4 : 1, Text.size() - idx)}; + auto const lookup{charmap.find(Text.substr(idx, length))}; + output += (lookup != charmap.end() ? lookup->second : '?'); + idx += length; + } + return output; +} + +bool read_template(std::string const &File, template_text &Text, std::string &Error) +{ + std::ifstream input{Global.asCurrentSceneryPath + File, std::ios_base::binary}; + if (false == input.is_open()) + { + Error = "can't open file \"" + File + "\""; + return false; + } + auto &content{Text.content}; + content.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + + auto const bom{content.compare(0, 3, "\xEF\xBB\xBF") == 0}; + Text.start = bom ? 3 : 0; + bool extended; + Text.utf8 = bom || (is_utf8(content, extended) && extended); + if (auto const linebreak{content.find('\n')}; linebreak != std::string::npos) + { + Text.eol = (linebreak > 0 && content[linebreak - 1] == '\r') ? "\r\n" : "\n"; + } + + for (auto linebegin{Text.start}; linebegin < content.size();) + { + auto const linebreak{content.find('\n', linebegin)}; + auto const next{linebreak == std::string::npos ? content.size() : linebreak + 1}; + if (content.compare(linebegin, linemark.size(), linemark) == 0) + { + auto line{content.substr(linebegin + linemark.size(), next - linebegin - linemark.size())}; + line.erase(line.find_last_not_of("\r\n") + 1); + // a single space separates the mark from the document; the rest is indentation, which carries meaning + if (false == line.empty() && line.front() == ' ') + { + line.erase(0, 1); + } + Text.yaml += line + '\n'; + Text.lines.emplace_back(linebegin, next); + } + linebegin = next; + } + if (false == is_utf8(Text.yaml, extended)) + { + Text.yaml = win1250_to_utf8(Text.yaml); + } + return true; +} + +// number of the parameter if specified text holds (pN) placeholder at specified location, 0 otherwise. Length: size of the placeholder +int placeholder(std::string const &Text, std::size_t const Location, std::size_t &Length) +{ + if (Text.compare(Location, 2, "(p") != 0) + { + return 0; + } + auto idx{Location + 2}; + auto id{0}; + while (idx < Text.size() && std::isdigit(static_cast(Text[idx])) && id < 1000) + { + id = id * 10 + (Text[idx] - '0'); + ++idx; + } + if (idx == Location + 2 || idx >= Text.size() || Text[idx] != ')') + { + return 0; + } + Length = idx + 1 - Location; + return id; +} + +// number of the parameter if the token is nothing but its placeholder, 0 otherwise +int exact_placeholder(std::string const &Token) +{ + std::size_t length{0}; + auto const id{placeholder(Token, 0, length)}; + return length == Token.size() ? id : 0; +} + +// number of the first parameter whose placeholder is a part of the token, 0 if there's none +int first_placeholder(std::string const &Token) +{ + std::size_t length{0}; + for (auto location{Token.find("(p")}; location != std::string::npos; location = Token.find("(p", location + 1)) + { + if (auto const id{placeholder(Token, location, length)}; id != 0) + { + return id; + } + } + return 0; +} + +// tokens of scenery text, in lower case and with the comments left out +std::vector tokenize(std::string const &Text) +{ + std::vector tokens; + std::string token; + auto const flush = [&]() { + if (false == token.empty()) + { + tokens.emplace_back(ToLower(token)); + token.clear(); + } + }; + for (std::size_t idx = 0; idx < Text.size(); ++idx) + { + if (Text.compare(idx, 2, "//") == 0) + { + flush(); + idx = Text.find('\n', idx); + if (idx == std::string::npos) + { + break; + } + } + else if (Text.compare(idx, 2, "/*") == 0) + { + flush(); + idx = Text.find("*/", idx + 2); + if (idx == std::string::npos) + { + break; + } + ++idx; + } + else if (Text[idx] == ' ' || Text[idx] == '\t' || Text[idx] == '\n' || Text[idx] == '\r' || Text[idx] == ';') + { + flush(); + } + else + { + token += Text[idx]; + } + } + flush(); + return tokens; +} + +// reads description held by specified template text +bool parse(template_text const &Text, include_info &Info, std::string &Error) +{ + Info = include_info(); + auto const &text{Text}; + try + { + auto const document{YAML::Load(text.yaml)}; + if (document.IsNull()) + { + return true; + } + if (false == document.IsMap()) + { + Error = "the description isn't a list of named values"; + return false; + } + Info.name = document["name"].as(""); + Info.category = document["category"].as(""); + Info.description = document["description"].as(""); + if (auto const drive{document["switch"]}; drive.IsDefined() && drive.IsMap()) + { + Info.switch_hand = drive["hand"].as(""); + Info.switch_drive = drive["drive"].as(""); + } + if (auto const signal{document["signal"]}; signal.IsDefined() && signal.IsMap()) + { + Info.signal_kind = signal["kind"].as(""); + Info.signal_mount = signal["mount"].as(""); + Info.signal_lamps = signal["lamps"].as(""); + Info.signal_lean = signal["lean"].as(""); + Info.signal_read = signal["read"].as(""); + } + if (auto const table{document["turntable"]}; table.IsDefined() && table.IsMap()) + { + Info.turntable_length = table["length"].as(0.0); + Info.turntable_angles = table["angles"].as(0); + Info.turntable_positions = table["positions"].as(0); + } + if (auto const parameters{document["params"]}; parameters.IsDefined() && parameters.IsSequence()) + { + for (auto const &entry : parameters) + { + auto const id{entry.IsMap() ? entry["id"].as(0) : 0}; + if (id <= 0) + { + continue; + } + auto ¶meter{Info.parameter(id)}; + parameter.role = entry["role"].as("free"); + parameter.label = entry["label"].as(""); + parameter.value = entry["default"].as(""); + } + } + } + catch (YAML::Exception const &exception) + { + Error = std::string{"the description is malformed: "} + exception.what(); + return false; + } + return true; +} + +// highest number of (pN) placeholder used by specified template text +int count_parameters(template_text const &Text) +{ + // the description lines may mention parameters the template doesn't use + auto content{Text.content}; + for (auto line{Text.lines.rbegin()}; line != Text.lines.rend(); ++line) + { + content.erase(line->first, line->second - line->first); + } + auto count{0}; + std::size_t length{0}; + for (auto location{content.find("(p")}; location != std::string::npos; location = content.find("(p", location + 1)) + { + count = std::max(count, placeholder(content, location, length)); + } + return count; +} + +} // namespace + +include_parameter &include_info::parameter(int const Id) +{ + auto lookup{std::find_if(std::begin(parameters), std::end(parameters), [=](include_parameter const &Parameter) { return Parameter.id >= Id; })}; + if (lookup == std::end(parameters) || lookup->id != Id) + { + lookup = parameters.emplace(lookup); + lookup->id = Id; + } + return *lookup; +} + +namespace editor_includes +{ + +std::vector const roles{"free", "name", "pos.x", "pos.y", "pos.z", "rot.x", "rot.y", "rot.z", "track", "memcell", "event", "model", "texture", "number", "text"}; + +bool load(std::string const &File, include_info &Info, std::string &Error) +{ + template_text text; + return read_template(File, text, Error) && parse(text, Info, Error); +} + +bool save(std::string const &File, include_info const &Info, std::string &Error) +{ + template_text text; + if (false == read_template(File, text, Error)) + { + return false; + } + std::string yaml; + try + { + // start with the existing description, to keep the entries we don't know + auto document{YAML::Load(text.yaml)}; + if (false == document.IsNull() && false == document.IsMap()) + { + Error = "the existing description isn't a list of named values"; + return false; + } + if (false == document["version"].IsDefined()) + { + document["version"] = 1; + } + auto const set = [&](char const *Key, std::string const &Value) { + if (false == Value.empty()) + { + document[Key] = Value; + } + else + { + document.remove(Key); + } + }; + set("name", Info.name); + set("category", Info.category); + set("description", Info.description); + + YAML::Node parameters{YAML::NodeType::Sequence}; + for (auto const ¶meter : Info.parameters) + { + YAML::Node entry{YAML::NodeType::Map}; + if (auto const existing{document["params"]}; existing.IsDefined() && existing.IsSequence()) + { + for (auto const &candidate : existing) + { + if (candidate.IsMap() && candidate["id"].as(0) == parameter.id) + { + entry = YAML::Clone(candidate); + break; + } + } + } + entry["id"] = parameter.id; + entry["role"] = parameter.role.empty() ? std::string{"free"} : parameter.role; + if (false == parameter.label.empty()) + { + entry["label"] = parameter.label; + } + else + { + entry.remove("label"); + } + if (false == parameter.value.empty()) + { + entry["default"] = parameter.value; + } + else + { + entry.remove("default"); + } + entry.SetStyle(YAML::EmitterStyle::Flow); + parameters.push_back(entry); + } + if (parameters.size() > 0) + { + document["params"] = parameters; + } + else + { + document.remove("params"); + } + + YAML::Emitter emitter; + emitter << document; + yaml = emitter.c_str(); + } + catch (YAML::Exception const &exception) + { + Error = std::string{"the existing description is malformed, fix or remove it first: "} + exception.what(); + return false; + } + if (false == text.utf8) + { + yaml = utf8_to_win1250(yaml); + } + + std::string block; + std::istringstream lines{yaml}; + for (std::string line; std::getline(lines, line);) + { + block += linemark + (line.empty() ? "" : " " + line) + text.eol; + } + // the description replaces the old one, or goes to the top of the file if there was none + auto content{text.content}; + auto location{text.lines.empty() ? text.start : text.lines.front().first}; + for (auto line{text.lines.rbegin()}; line != text.lines.rend(); ++line) + { + content.erase(line->first, line->second - line->first); + } + content.insert(location, block); + + auto const filepath{Global.asCurrentSceneryPath + File}; + std::ofstream output{filepath + ".tmp", std::ios_base::binary | std::ios_base::trunc}; + output << content; + output.close(); + std::error_code error; + if (output.fail()) + { + std::filesystem::remove(filepath + ".tmp", error); + Error = "can't write file \"" + File + "\""; + return false; + } + // the first save of a file leaves its original content next to it + if (false == std::filesystem::exists(filepath + ".bak", error)) + { + std::filesystem::copy_file(filepath, filepath + ".bak", error); + } + std::filesystem::rename(filepath + ".tmp", filepath, error); + if (error) + { + Error = "can't replace file \"" + File + "\": " + error.message(); + std::filesystem::remove(filepath + ".tmp", error); + return false; + } + return true; +} + +int parameter_count(std::string const &File) +{ + template_text text; + std::string error; + return read_template(File, text, error) ? count_parameters(text) : 0; +} + +void suggest(std::string const &File, include_info &Info) +{ + template_text text; + std::string error; + if (false == read_template(File, text, error)) + { + return; + } + auto const tokens{tokenize(text.content)}; + auto const assign = [&](std::size_t const Index, char const *Role, bool const Exact = true) { + if (Index >= tokens.size()) + { + return; + } + auto const id{Exact ? exact_placeholder(tokens[Index]) : first_placeholder(tokens[Index])}; + if (id != 0 && Info.parameter(id).role == "free") + { + Info.parameter(id).role = Role; + } + }; + for (std::size_t idx = 0; idx < tokens.size(); ++idx) + { + auto const &token{tokens[idx]}; + if (token == "origin") + { + assign(idx + 1, "pos.x"); + assign(idx + 2, "pos.y"); + assign(idx + 3, "pos.z"); + } + else if (token == "rotate") + { + assign(idx + 1, "rot.x"); + assign(idx + 2, "rot.y"); + assign(idx + 3, "rot.z"); + } + else if (token == "node" && idx + 4 < tokens.size()) + { + // node ... + assign(idx + 3, "name", false); + auto const &type{tokens[idx + 4]}; + if (type == "model" || type == "memcell" || type == "eventlauncher" || type == "sound") + { + assign(idx + 5, "pos.x"); + assign(idx + 6, "pos.y"); + assign(idx + 7, "pos.z"); + } + if (type == "model") + { + assign(idx + 8, "rot.y"); + assign(idx + 9, "model"); + assign(idx + 10, "texture"); + } + if (type == "memcell") + { + assign(idx + 11, "track"); + } + } + } +} + +std::string const directive_mark{"include "}; + +bool automatic(std::string const &Role) +{ + return Role == "name" || Role.starts_with("pos.") || Role.starts_with("rot."); +} + +bool complete(include_info const &Info, int const Parameters, std::string *Issue) +{ + std::string issue; + if (Info.name.empty()) + { + issue = "the template needs a name"; + } + for (auto id = 1; id <= Parameters && issue.empty(); ++id) + { + auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })}; + if (lookup == std::end(Info.parameters) || (false == automatic(lookup->role) && lookup->value.empty())) + { + issue = "(p" + std::to_string(id) + ") needs a default value, or a role filled in by the editor (name, pos.*, rot.*)"; + } + } + if (false == issue.empty() && Issue != nullptr) + { + *Issue = issue; + } + return issue.empty(); +} + +std::string number(double const Value) +{ + std::ostringstream converter; + converter.imbue(std::locale::classic()); + converter << std::fixed << std::setprecision(3) << Value; + return converter.str(); +} + +int parameter_with_role(include_info const &Info, std::string const &Role) +{ + auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [&](include_parameter const &Parameter) { return Parameter.role == Role; })}; + return lookup != std::end(Info.parameters) ? lookup->id : 0; +} + +bool parse_directive(std::string const &Directive, std::string &File, std::vector &Values) +{ + // the same rules the scenery parser goes by, except the text is taken as it is written + std::vector tokens; + std::string token; + auto const flush = [&]() { + if (false == token.empty()) + { + tokens.emplace_back(token); + token.clear(); + } + }; + for (std::size_t idx = 0; idx < Directive.size(); ++idx) + { + auto const character{Directive[idx]}; + if (character == '\"') + { + flush(); + auto const end{Directive.find('\"', idx + 1)}; + if (end == std::string::npos) + { + return false; + } + tokens.emplace_back(Directive, idx + 1, end - idx - 1); + idx = end; + } + else if (Directive.compare(idx, 2, "//") == 0) + { + flush(); + idx = Directive.find('\n', idx); + if (idx == std::string::npos) + { + break; + } + } + else if (Directive.compare(idx, 2, "/*") == 0) + { + flush(); + idx = Directive.find("*/", idx + 2); + if (idx == std::string::npos) + { + break; + } + ++idx; + } + else if (character == ' ' || character == '\t' || character == '\n' || character == '\r' || character == ';') + { + flush(); + } + else + { + token += character; + } + } + flush(); + if (tokens.size() < 3 || ToLower(tokens.front()) != "include" || tokens.back() != "end") + { + return false; + } + File = tokens[1]; + Values.assign(std::begin(tokens) + 2, std::end(tokens) - 1); + return true; +} + +std::string compose_directive(std::string const &File, std::vector const &Values) +{ + auto const quoted = [](std::string const &Text) { return Text.find_first_of(" \t;") != std::string::npos ? '\"' + Text + '\"' : Text; }; + std::string text{"include " + quoted(File)}; + for (auto const &value : Values) + { + // a parameter can't be left out, as the ones after it would take its place + text += ' ' + (value.empty() ? std::string{"none"} : quoted(value)); + } + return text + " end"; +} + +std::string substitute(std::string const &Text, std::vector const &Values) +{ + std::string result; + std::size_t length{0}; + for (std::size_t idx = 0; idx < Text.size();) + { + auto const id{placeholder(Text, idx, length)}; + if (id > 0 && id <= static_cast(Values.size())) + { + result += Values[id - 1]; + idx += length; + } + else + { + result += Text[idx++]; + } + } + return result; +} + +std::string directive(std::string const &File, include_info const &Info, int const Parameters, glm::dvec3 const &Location, std::optional Yaw, std::string const &Track, glm::dvec2 const &Tilt, std::map const &Values) +{ + std::vector values; + for (auto id = 1; id <= Parameters; ++id) + { + auto const lookup{std::find_if(std::begin(Info.parameters), std::end(Info.parameters), [=](include_parameter const &Parameter) { return Parameter.id == id; })}; + auto const role{lookup != std::end(Info.parameters) ? lookup->role : std::string{"free"}}; + auto value{lookup != std::end(Info.parameters) ? lookup->value : std::string{}}; + if (auto const given{Values.find("#" + std::to_string(id))}; given != Values.end()) + { + value = given->second; + } + else if (auto const given{Values.find(role)}; given != Values.end()) + { + value = given->second; + } + else if (role == "pos.x") + { + value = number(Location.x); + } + else if (role == "pos.y") + { + value = number(Location.y); + } + else if (role == "pos.z") + { + value = number(Location.z); + } + else if (role == "rot.y" && Yaw) + { + value = number(*Yaw); + } + else if (role == "rot.x" && Tilt.x != 0.0) + { + value = number(Tilt.x); + } + else if (role == "rot.z" && Tilt.y != 0.0) + { + value = number(Tilt.y); + } + else if (role.starts_with("rot.") && value.empty()) + { + value = "0"; + } + else if (role == "track" && false == Track.empty()) + { + value = Track; + } + else if (role == "name") + { + // names have to be unique in the scenery. the default value, or the name of the template, makes them recognizable + static std::mt19937 engine{std::random_device{}()}; // NOSONAR + char suffix[16]; + std::snprintf(suffix, sizeof(suffix), "_%08x", static_cast(engine())); + value = (value.empty() ? std::filesystem::path(File).stem().string() : value) + suffix; + } + values.emplace_back(value); + } + return compose_directive(File, values); +} + +} // namespace editor_includes + +editorIncludeBank EditorIncludes; + +namespace +{ + +include_entry examine(std::string const &File) +{ + include_entry entry; + entry.file = File; + entry.statement = std::make_shared(editor_includes::directive_mark + File); + template_text text; + if (false == read_template(File, text, entry.issue)) + { + return entry; + } + entry.described = false == text.lines.empty(); + if (false == entry.described) + { + entry.issue = "the template has no description"; + return entry; + } + include_info info; + if (false == parse(text, info, entry.issue)) + { + return entry; + } + entry.name = info.name; + entry.category = info.category; + entry.complete = editor_includes::complete(info, count_parameters(text), &entry.issue); + return entry; +} + +} // namespace + +void editorIncludeBank::scan() +{ + // the node bank may hold one of the templates selected, the statements stay in place across the scans + std::unordered_map> statements; + for (auto &entry : m_entries) + { + statements.emplace(entry.file, entry.statement); + } + m_entries.clear(); + m_scanned = true; + std::error_code error; + auto const root{std::filesystem::path(Global.asCurrentSceneryPath)}; + // NOTE: on an error the iterator turns into the end one + for (std::filesystem::recursive_directory_iterator file{root, std::filesystem::directory_options::skip_permission_denied, error}, end; file != end; file.increment(error)) + { + if (false == file->is_regular_file(error)) + { + continue; + } + // the scenery refers to its files with lower case names, relative to the scenery directory + std::string name; + try + { + name = ToLower(file->path().lexically_relative(root).generic_string()); + } + catch (std::exception const &) + { + // the name can't be expressed in the encoding the scenery files use, so nothing can include the file anyway + continue; + } + if (false == name.ends_with(".inc")) + { + continue; + } + m_entries.emplace_back(examine(name)); + if (auto const lookup{statements.find(name)}; lookup != statements.end()) + { + m_entries.back().statement = lookup->second; + } + } + std::sort(std::begin(m_entries), std::end(m_entries), [](include_entry const &Left, include_entry const &Right) { return Left.file < Right.file; }); + index(); +} + +void editorIncludeBank::index() +{ + m_ready.clear(); + for (std::size_t idx = 0; idx < m_entries.size(); ++idx) + { + if (m_entries[idx].complete) + { + m_ready.emplace_back(idx); + } + } + std::sort(std::begin(m_ready), std::end(m_ready), [this](std::size_t const Left, std::size_t const Right) { + auto const &left{m_entries[Left]}; + auto const &right{m_entries[Right]}; + return std::tie(left.category, left.name, left.file) < std::tie(right.category, right.name, right.file); + }); + ++m_revision; +} + +void editorIncludeBank::update(std::string const &File) +{ + auto const lookup{std::find_if(std::begin(m_entries), std::end(m_entries), [&](include_entry const &Entry) { return Entry.file == File; })}; + if (lookup == std::end(m_entries)) + { + m_entries.insert(std::upper_bound(std::begin(m_entries), std::end(m_entries), File, [](std::string const &Name, include_entry const &Entry) { return Name < Entry.file; }), examine(File)); + } + else + { + // the node bank may hold the template selected, keep its statement + auto const statement{lookup->statement}; + *lookup = examine(File); + lookup->statement = statement; + } + index(); +} diff --git a/editor/editorIncludeInfo.hpp b/editor/editorIncludeInfo.hpp new file mode 100644 index 00000000..d2565a2f --- /dev/null +++ b/editor/editorIncludeInfo.hpp @@ -0,0 +1,147 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +// description of an *.inc scenery template: what the template is, and what its (pN) parameters mean. +// it's kept in the template file itself, as a YAML document made of the comment lines starting with "//$e", e.g. +// //$e version: 1 +// //$e name: Semaphore, 5 lights +// //$e params: +// //$e - {id: 1, role: name, label: Name} +// being a comment it's invisible to the scenery parser and to the older tools +struct include_parameter +{ + int id{0}; // number of the (pN) placeholder + std::string role{"free"}; // what the value is; one of editor_includes::roles + std::string label; // name to show to the user + std::string value; // default value +}; + +struct include_info +{ + std::string name; + std::string category; + std::string description; + std::string switch_hand; // a switch drive: side of the diverging track, left or right, looking from the start of the switch + std::string switch_drive; // and the side the drive stands on + double turntable_length{0.0}; + int turntable_angles{0}; + int turntable_positions{0}; + std::string signal_kind; + std::string signal_mount; + std::string signal_lamps; + std::string signal_lean; + std::string signal_read; + std::vector parameters; + + // description of specified parameter, created if there's none + include_parameter ¶meter(int Id); +}; + +namespace editor_includes +{ + +// recognized parameter roles. "free" is a value typed in by the user, with no meaning to the editor; +// the placement roles (pos.*, rot.*) and "name" are the ones the editor can fill in on its own +extern std::vector const roles; + +// reads description of specified template, located in the scenery directory. a template without the description +// yields an empty one. returns: false if the file can't be read or its description isn't a valid YAML document +bool load(std::string const &File, include_info &Info, std::string &Error); +// stores provided description in specified template. entries of the existing description which aren't +// a part of include_info are preserved. returns: false if the file can't be updated +bool save(std::string const &File, include_info const &Info, std::string &Error); +// highest number of (pN) placeholder used by specified template, 0 if it has none +int parameter_count(std::string const &File); +// sets roles of the parameters whose use in the template makes their meaning clear. roles already set are left alone +void suggest(std::string const &File, include_info &Info); +// true if value of a parameter with specified role is supplied by the editor when the template is placed +bool automatic(std::string const &Role); +// true if the description has everything it takes to place the template straight from the node bank: a name, and +// either a default value or a role filled in by the editor for each of specified number of parameters. +// optionally tells what is missing +bool complete(include_info const &Info, int Parameters, std::string *Issue = nullptr); +// builds include directive which places specified template. Location and Yaw are the placement to pass through +// the parameters with matching roles, in the coordinates of the place the directive goes to; without Yaw the +// rotation parameters receive their default values +std::string directive(std::string const &File, include_info const &Info, int Parameters, glm::dvec3 const &Location, std::optional Yaw, std::string const &Track = {}, glm::dvec2 const &Tilt = glm::dvec2{0.0}, std::map const &Values = {}); +std::string substitute(std::string const &Text, std::vector const &Values); + +// splits include directive into the name of the included file and the values of the parameters, both as they're +// written. returns: false if the text isn't a complete include directive +bool parse_directive(std::string const &Directive, std::string &File, std::vector &Values); +// builds include directive out of the name of the included file and the values of the parameters +std::string compose_directive(std::string const &File, std::vector const &Values); +// text of a number passed as a parameter value +std::string number(double Value); +// number of the parameter with specified role, 0 if the description has none +int parameter_with_role(include_info const &Info, std::string const &Role); + +// marks node bank template which stands for a scenery template rather than for definition of a single node +extern std::string const directive_mark; + +} // namespace editor_includes + +// *.inc template found in the scenery directory +struct include_entry +{ + std::string file; // name of the template, relative to the scenery directory + std::string name; // from the description + std::string category; + bool described{false}; + bool complete{false}; // can be placed from the node bank, see editor_includes::complete() + std::string issue; // what keeps the template out of the node bank + std::shared_ptr statement; // node bank template standing for the include +}; + +// the scenery templates available to the editor: every *.inc file in the scenery directory. the templates whose +// descriptions are complete are offered by the node bank +class editorIncludeBank +{ + public: + // looks through the scenery directory for the templates, and reads their descriptions + void scan(); + bool scanned() const + { + return m_scanned; + } + std::vector const &entries() const + { + return m_entries; + } + // indices of the entries offered by the node bank, ordered by category and name + std::vector const &ready() const + { + return m_ready; + } + // reads description of specified template again, after it was changed + void update(std::string const &File); + // changes with each change of the entries + int revision() const + { + return m_revision; + } + + private: + void index(); + + std::vector m_entries; // sorted by file name + std::vector m_ready; + bool m_scanned{false}; + int m_revision{0}; +}; + +extern editorIncludeBank EditorIncludes; diff --git a/editor/editorInfra.cpp b/editor/editorInfra.cpp new file mode 100644 index 00000000..ba38961f --- /dev/null +++ b/editor/editorInfra.cpp @@ -0,0 +1,284 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorInfra.hpp" + +#include "editor/editorGeometry.hpp" +#include "world/Track.h" + +#include +#include +#include +#include + +namespace infra +{ + +namespace +{ + +using geometry::bezier; +using geometry::plan_distance; +using geometry::plan_of; + +int const kSteps{128}; + +// arc length in the plan at the evenly spread parameters +std::array lengths(bezier const &Curve) +{ + std::array result{}; + auto previous{Curve.p0}; + for (int i = 1; i <= kSteps; ++i) + { + auto const next{Curve.point(static_cast(i) / kSteps)}; + result[i] = result[i - 1] + plan_distance(previous, next); + previous = next; + } + return result; +} + +double parameter(std::array const &Lengths, double const S) +{ + if (S <= 0.0) + return 0.0; + if (S >= Lengths.back()) + return 1.0; + auto const next{std::upper_bound(Lengths.begin(), Lengths.end(), S)}; + auto const index{static_cast(std::distance(Lengths.begin(), next)) - 1}; + auto const run{Lengths[index + 1] - Lengths[index]}; + auto const fraction{run > 1e-12 ? (S - Lengths[index]) / run : 0.0}; + return (index + fraction) / kSteps; +} + +double length_at(std::array const &Lengths, double const T) +{ + auto const position{std::clamp(T, 0.0, 1.0) * kSteps}; + auto const index{std::min(static_cast(position), kSteps - 1)}; + return Lengths[index] + (Lengths[index + 1] - Lengths[index]) * (position - index); +} + +glm::dvec2 plan_direction(bezier const &Curve, double const T) +{ + auto tangent{Curve.first(T)}; + glm::dvec2 result{tangent.x, tangent.z}; + if (glm::length(result) < 1e-9) + result = {Curve.p3.x - Curve.p0.x, Curve.p3.z - Curve.p0.z}; + return glm::length(result) > 1e-9 ? glm::normalize(result) : glm::dvec2{0.0, 1.0}; +} + +std::string lower(std::string Text) +{ + std::transform(Text.begin(), Text.end(), Text.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return Text; +} + +} // namespace + +char const *name(kind const Kind) +{ + switch (Kind) + { + case kind::model: return "model"; + case kind::include: return "include"; + case kind::traction: return "traction"; + case kind::memcell: return "memcell"; + case kind::launcher: return "eventlauncher"; + } + return ""; +} + +char const *name(category const Category) +{ + switch (Category) + { + case category::signal: return "Signals"; + case category::sign: return "Signs"; + case category::pole: return "Poles and gantries"; + case category::catenary: return "Catenary"; + case category::logic: return "Logic"; + case category::other: return "Other"; + } + return ""; +} + +std::vector const &categories() +{ + static std::vector const result{category::signal, category::sign, category::pole, category::catenary, category::logic, category::other}; + return result; +} + +double plan_length(segment_data const &Path) +{ + return lengths(bezier{Path}).back(); +} + +std::vector outline(segment_data const &Path, double const Step) +{ + bezier const shape{Path}; + // the speed along the curve doesn't exceed three times the longest leg of the control polygon + auto const leg{std::max({plan_distance(shape.p0, shape.p1), plan_distance(shape.p1, shape.p2), plan_distance(shape.p2, shape.p3)})}; + auto const count{std::clamp(static_cast(std::ceil(3.0 * leg / std::max(Step, 0.1))), 1, 4096)}; + std::vector result; + result.reserve(count + 1); + for (int i = 0; i <= count; ++i) + result.push_back(shape.point(static_cast(i) / count)); + return result; +} + +frame frame_at(segment_data const &Path, double const S) +{ + bezier const shape{Path}; + auto const t{parameter(lengths(shape), S)}; + return {shape.point(t), plan_direction(shape, t)}; +} + +frame frame_at(station const &Station) +{ + if (Station.track == nullptr || Station.path < 0 || Station.path >= static_cast(Station.track->m_paths.size())) + return {}; + return frame_at(Station.track->m_paths[Station.path], Station.s); +} + +double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior) +{ + bezier const shape{Path}; + auto const table{lengths(shape)}; + auto const distance = [&](double const T) { return plan_distance(shape.point(T), Point); }; + auto best{0}; + auto bestdistance{distance(0.0)}; + for (int i = 1; i <= kSteps; ++i) + { + auto const d{distance(static_cast(i) / kSteps)}; + if (d < bestdistance) + { + bestdistance = d; + best = i; + } + } + // golden section search around the nearest sample + auto low{std::max(0, best - 1) / static_cast(kSteps)}; + auto high{std::min(kSteps, best + 1) / static_cast(kSteps)}; + double const ratio{0.6180339887498949}; + auto a{high - ratio * (high - low)}; + auto b{low + ratio * (high - low)}; + auto da{distance(a)}, db{distance(b)}; + for (int i = 0; i < 40; ++i) + { + if (da < db) + { + high = b; + b = a; + db = da; + a = high - ratio * (high - low); + da = distance(a); + } + else + { + low = a; + a = b; + da = db; + b = low + ratio * (high - low); + db = distance(b); + } + } + auto t{(low + high) * 0.5}; + if (distance(0.0) <= distance(t)) + t = 0.0; + if (distance(1.0) <= distance(t)) + t = 1.0; + S = length_at(table, t); + Interior = (t > 1e-6 && t < 1.0 - 1e-6); + return distance(t); +} + +double project(TTrack &Track, glm::dvec3 const &Point, station &Station, bool &Interior) +{ + auto best{-1.0}; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + double s; + bool interior; + auto const d{project(Track.m_paths[i], Point, s, interior)}; + // an interior point takes precedence over an end shared with the other path + if (best < 0.0 || d < best - 1e-6 || (std::abs(d - best) <= 1e-6 && interior && false == Interior)) + { + best = d; + Station = {&Track, i, s}; + Interior = interior; + } + } + return best; +} + +double heading(glm::dvec2 const &Forward) +{ + return glm::degrees(std::atan2(Forward.x, Forward.y)); +} + +anchor make_anchor(TTrack *Track, int const Path, glm::dvec3 const &Point) +{ + double s; + bool interior; + project(Track->m_paths[Path], Point, s, interior); + return make_anchor({Track, Path, s}, Point); +} + +anchor make_anchor(station const &At, glm::dvec3 const &Point) +{ + anchor result; + result.at = At; + auto const axis{frame_at(At)}; + glm::dvec2 const side{axis.forward.y, -axis.forward.x}; + result.offset = glm::dot(plan_of(Point - axis.point), side); + result.height = Point.y - axis.point.y; + result.foot = axis.point; + if (At.track != nullptr && At.path >= 0 && At.path < static_cast(At.track->m_paths.size())) + { + auto const &path{At.track->m_paths[At.path]}; + result.start = path.points[segment_data::point::start]; + result.end = path.points[segment_data::point::end]; + result.length = plan_length(path); + } + return result; +} + +glm::dvec3 place(anchor &Anchor) +{ + auto const axis{frame_at(Anchor.at)}; + Anchor.foot = axis.point; + glm::dvec2 const side{axis.forward.y, -axis.forward.x}; + return {axis.point.x + side.x * Anchor.offset, axis.point.y + Anchor.height, axis.point.z + side.y * Anchor.offset}; +} + +std::vector default_rules() +{ + return { + {category::signal, "semafor sem_ sem- tarcza tarcz_ tz_ tm_ tzw powtarz sygnaliz sygnal signal"}, + {category::sign, "wskaz wsk_ wsk- w4 w8 w11 w24 w27 kilometr hektometr slupek_km znak tablic sign"}, + {category::catenary, "trakcj siec_ siec- catenary przewod lina_nosna"}, + {category::pole, "slup maszt wysieg bramk konstrukcj kotw odciag pole gantry"}, + }; +} + +category classify(std::string const &Name, std::vector const &Rules) +{ + auto const text{lower(Name)}; + for (auto const &rule : Rules) + { + std::istringstream words{lower(rule.keywords)}; + std::string word; + while (words >> word) + if (text.find(word) != std::string::npos) + return rule.group; + } + return category::other; +} + +} // namespace infra diff --git a/editor/editorInfra.hpp b/editor/editorInfra.hpp new file mode 100644 index 00000000..64d32b25 --- /dev/null +++ b/editor/editorInfra.hpp @@ -0,0 +1,130 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "world/Segment.h" +#include "scene/scenenode.h" + +class TTrack; + +// objects which belong to the railway infrastructure (signals, signs, catenary, poles, the logic placed along the +// line) bound to the paths they stand by, so they follow the changes of the track geometry +namespace infra +{ + +enum class kind +{ + model, + include, + traction, + memcell, + launcher +}; +enum class category +{ + signal, + sign, + pole, + catenary, + logic, + other +}; +char const *name(kind Kind); +char const *name(category Category); +std::vector const &categories(); + +// point of a path, measured in the plan from the start of the path +struct station +{ + TTrack *track{nullptr}; + int path{0}; + double s{0.0}; +}; +struct frame +{ + glm::dvec3 point{0.0}; + glm::dvec2 forward{0.0, 1.0}; // in the plan, x and z +}; + +double plan_length(segment_data const &Path); +// points of the path, no farther apart in the plan than about specified step; the ends included +std::vector outline(segment_data const &Path, double Step); +frame frame_at(segment_data const &Path, double S); +frame frame_at(station const &Station); +// nearest point of specified path in the plan. returns: distance in the plan; Interior tells the nearest point +// isn't one of the ends of the path +double project(segment_data const &Path, glm::dvec3 const &Point, double &S, bool &Interior); +// nearest point among the paths of the track +double project(TTrack &Track, glm::dvec3 const &Point, station &Station, bool &Interior); +// angle around the vertical axis which turns a model to face specified direction, in degrees +double heading(glm::dvec2 const &Forward); + +// location of a point relative to the rail axis: the station, a sideways offset and a height above the rail top +struct anchor +{ + station at; + double offset{0.0}; // positive to the right of the direction of the path + double height{0.0}; + glm::dvec3 foot{0.0}; // point of the axis the location was measured from + // ends and length of the path when the station was measured, to tell whether the path changed since + glm::dvec3 start{0.0}, end{0.0}; + double length{0.0}; +}; +anchor make_anchor(station const &At, glm::dvec3 const &Point); +// anchor at the place of the path nearest to the point +anchor make_anchor(TTrack *Track, int const Path, glm::dvec3 const &Point); +// location for the current geometry of the path +glm::dvec3 place(anchor &Anchor); + +// rules telling infrastructure apart by the names of the files and nodes +struct rule +{ + category group{category::other}; + std::string keywords; // separated by spaces, matched as parts of the lower case name +}; +std::vector default_rules(); +// first rule in the list which matches. returns: category::other if none does +category classify(std::string const &Name, std::vector const &Rules); + +struct binding +{ + kind type{kind::model}; + category group{category::other}; + scene::basic_node *node{nullptr}; // the object, unless it's an include + scene::instance_handle include{0}; + std::vector anchors; // one per point which defines the object: four for a traction piece + double yaw{0.0}; // rotation of the object relative to the direction of the path + bool turns{false}; // the object has a rotation which follows the path + std::string label; + bool lost{false}; // none of the paths around could take the object over + + bool same(binding const &Other) const + { + return type == Other.type && node == Other.node && include == Other.include; + } +}; + +// placement of a bound object, to take a change back +struct object_state +{ + binding saved; + glm::dvec3 location{0.0}; + glm::vec3 angles{0.f}; + std::array points{}; + std::string directive; + bool moved{false}; // the object followed a change since the state was taken +}; + +} // namespace infra diff --git a/editor/editorModelSets.cpp b/editor/editorModelSets.cpp new file mode 100644 index 00000000..4a9c10ec --- /dev/null +++ b/editor/editorModelSets.cpp @@ -0,0 +1,235 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorModelSets.hpp" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" + +#include +#include +#include + +editorModelSets EditorModelSets; + +// file layout: +// set +// +// ... +// endset +namespace +{ +namespace fs = std::filesystem; + +fs::path sets_path() +{ + fs::path p = user_config_path("eu07_editor_sets.txt"); + return p.empty() ? fs::path("eu07_editor_sets.txt") : p; +} + +std::string trimmed(std::string Text) +{ + auto const first = Text.find_first_not_of(" \t"); + if (first == std::string::npos) + return {}; + auto const last = Text.find_last_not_of(" \t"); + return Text.substr(first, last - first + 1); +} + +// the file stores one template per line, so line breaks inside a template (e.g. from export_as_text) become plain separators +std::string single_line(std::string Text) +{ + std::replace_if(Text.begin(), Text.end(), [](char const Character) { return Character == '\r' || Character == '\n' || Character == '\t'; }, ' '); + return trimmed(Text); +} +} // namespace + +bool editorModelSets::load() +{ + m_sets.clear(); + + const fs::path path = sets_path(); + std::error_code ec; + if (!fs::exists(path, ec)) + return false; + + std::ifstream stream(path, std::ios_base::in | std::ios_base::binary); + if (!stream.is_open()) + return false; + + editor_model_set *current{nullptr}; + std::string line; + while (std::getline(stream, line)) + { + line = single_line(line); + if (line.empty() || line.rfind("//", 0) == 0) + continue; + + // a set header also closes a set with missing "endset"; templates always start with "node" + if (line.rfind("set", 0) == 0 && (line.size() == 3 || line[3] == ' ')) + { + editor_model_set set; + set.id = m_nextid++; + set.name = unique_name(line.substr(3)); + m_sets.push_back(std::move(set)); + current = &m_sets.back(); + continue; + } + if (line == "endset") + { + current = nullptr; + continue; + } + if (current == nullptr) + continue; + current->labels.push_back(label(line)); + current->templates.push_back(std::move(line)); + } + return true; +} + +bool editorModelSets::save() const +{ + const fs::path path = sets_path(); + std::error_code ec; + fs::create_directories(path.parent_path(), ec); + + std::ofstream stream(path, std::ios_base::out | std::ios_base::trunc | std::ios_base::binary); + if (!stream.is_open()) + { + ErrorLog("failed to save editor model sets"); + return false; + } + + stream << "// MaSzyna editor: user-defined model sets\n"; + for (auto const &set : m_sets) + { + stream << "\nset " << set.name << "\n"; + for (auto const &entry : set.templates) + stream << entry << "\n"; + stream << "endset\n"; + } + return true; +} + +editor_model_set const *editorModelSets::find(int const Id) const +{ + auto const lookup = std::find_if(m_sets.begin(), m_sets.end(), [Id](auto const &Set) { return Set.id == Id; }); + return lookup != m_sets.end() ? &(*lookup) : nullptr; +} + +editor_model_set *editorModelSets::find_(int const Id) +{ + auto const lookup = std::find_if(m_sets.begin(), m_sets.end(), [Id](auto const &Set) { return Set.id == Id; }); + return lookup != m_sets.end() ? &(*lookup) : nullptr; +} + +int editorModelSets::create(std::string const &Name, std::vector const &Templates) +{ + editor_model_set set; + set.id = m_nextid++; + set.name = unique_name(Name); + for (auto const &entry : Templates) + { + auto line = single_line(entry); + if (line.empty()) + continue; + set.labels.push_back(label(line)); + set.templates.push_back(std::move(line)); + } + m_sets.push_back(std::move(set)); + save(); + return m_sets.back().id; +} + +void editorModelSets::remove(int const Id) +{ + auto const lookup = std::find_if(m_sets.begin(), m_sets.end(), [Id](auto const &Set) { return Set.id == Id; }); + if (lookup == m_sets.end()) + return; + m_sets.erase(lookup); + save(); +} + +void editorModelSets::rename(int const Id, std::string const &Name) +{ + auto *set = find_(Id); + if (set == nullptr) + return; + auto const name = unique_name(Name, Id); + if (name == set->name) + return; + set->name = name; + save(); +} + +void editorModelSets::add(int const Id, std::vector const &Templates) +{ + auto *set = find_(Id); + if (set == nullptr) + return; + auto const count{set->templates.size()}; + for (auto const &entry : Templates) + { + auto line = single_line(entry); + if (line.empty()) + continue; + set->labels.push_back(label(line)); + set->templates.push_back(std::move(line)); + } + if (set->templates.size() != count) + save(); +} + +void editorModelSets::erase(int const Id, std::size_t const Index) +{ + auto *set = find_(Id); + if (set == nullptr || Index >= set->templates.size()) + return; + set->templates.erase(set->templates.begin() + Index); + set->labels.erase(set->labels.begin() + Index); + save(); +} + +void editorModelSets::clear(int const Id) +{ + auto *set = find_(Id); + if (set == nullptr || set->templates.empty()) + return; + set->templates.clear(); + set->labels.clear(); + save(); +} + +std::string editorModelSets::unique_name(std::string const &Base, int const IgnoreId) const +{ + auto base = single_line(Base); + if (base.empty()) + base = "New set"; + auto const taken = [&](std::string const &Name) { + return std::any_of(m_sets.begin(), m_sets.end(), [&](auto const &Set) { return Set.id != IgnoreId && Set.name == Name; }); + }; + auto name = base; + for (int suffix = 2; taken(name); ++suffix) + name = base + " " + std::to_string(suffix); + return name; +} + +std::string editorModelSets::label(std::string const &Template) +{ + cParser tokenizer(Template); + tokenizer.getTokens(9, false); // node, ranges, name, type, position, rotation + auto model{tokenizer.getToken(false)}; + auto texture{tokenizer.getToken(false)}; + replace_slashes(model); + erase_extension(model); + replace_slashes(texture); + return texture == "none" || texture.empty() ? model : model + " (" + texture + ")"; +} diff --git a/editor/editorModelSets.hpp b/editor/editorModelSets.hpp new file mode 100644 index 00000000..9e9f6362 --- /dev/null +++ b/editor/editorModelSets.hpp @@ -0,0 +1,56 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include + +// user-defined sets of node templates (e.g. "deciduous trees", "bushes"), used by the editor brush, +// random insert and area fill. stored next to the user's ini files +struct editor_model_set +{ + int id{0}; // runtime identifier, stable while the editor runs (renames don't break references) + std::string name; + std::vector templates; // node definitions, one per entry, same format as nodebank entries + std::vector labels; // short readable names of the templates, cached for the UI +}; + +class editorModelSets +{ + public: + bool load(); + bool save() const; + + std::vector const &sets() const + { + return m_sets; + } + editor_model_set const *find(int Id) const; + // all modifying methods save the sets right away + // returns: id of the new set + int create(std::string const &Name, std::vector const &Templates = {}); + void remove(int Id); + void rename(int Id, std::string const &Name); + void add(int Id, std::vector const &Templates); + void erase(int Id, std::size_t Index); + void clear(int Id); + // name not used by any other set, built from specified base + std::string unique_name(std::string const &Base, int IgnoreId = 0) const; + // short readable name of a node template (model file and texture) + static std::string label(std::string const &Template); + + private: + editor_model_set *find_(int Id); + + std::vector m_sets; + int m_nextid{1}; +}; + +extern editorModelSets EditorModelSets; diff --git a/editor/editorOrthophoto.cpp b/editor/editorOrthophoto.cpp new file mode 100644 index 00000000..f7646e29 --- /dev/null +++ b/editor/editorOrthophoto.cpp @@ -0,0 +1,1523 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorOrthophoto.hpp" + +#include "rendering/renderer.h" +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "model/vertex.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" +#include "imgui/imgui.h" +#include "stb/stb_image.h" + +#if defined(_WIN32) +#include +#elif defined(EU07_WITH_CURL) +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +namespace fs = std::filesystem; + +constexpr int standard_pixels{1024}; // ~25 cm/px for a 256 m tile, matches the standard orthophoto +constexpr int hires_pixels{4096}; // ~6 cm/px, the WMS MaxWidth/MaxHeight of the geoportal services +constexpr int probe_pixels{64}; // coverage test of the high-resolution product +constexpr int worker_count{3}; // concurrent requests; the service is slow but shouldn't be hammered +constexpr std::size_t upload_budget{16u << 20}; // bytes of texture data uploaded per frame (always at least one tile) +constexpr std::size_t vertex_budget{60000}; // ImGui uses 16-bit indices and not every backend supports vertex offsets +constexpr float near_w{0.5f}; + +using stop_check = std::function; + +double steady_seconds() +{ + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +// --------------------------------------------------------------------------------------------- +// minimal blocking https GET; one instance per worker thread + +class http_client +{ + public: + http_client(); + ~http_client(); + http_client(http_client const &) = delete; + http_client &operator=(http_client const &) = delete; + + bool get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop); + + private: +#if defined(_WIN32) + HINTERNET m_session{nullptr}; +#elif defined(EU07_WITH_CURL) + CURL *m_curl{nullptr}; +#endif +}; + +#if defined(_WIN32) + +http_client::http_client() +{ + wchar_t const *agent = L"MaSzyna-editor"; +#ifdef WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY + m_session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); +#endif + if (m_session == nullptr) // automatic proxy needs windows 8.1+ + m_session = ::WinHttpOpen(agent, WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (m_session != nullptr) + ::WinHttpSetTimeouts(m_session, 10000, 10000, 15000, 20000); +} + +http_client::~http_client() +{ + if (m_session != nullptr) + ::WinHttpCloseHandle(m_session); +} + +bool http_client::get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop) +{ + Out.clear(); + if (m_session == nullptr) + { + Error = "WinHttpOpen failed (" + std::to_string(::GetLastError()) + ")"; + return false; + } + + std::wstring const url(Url.begin(), Url.end()); // the requests are plain ascii + URL_COMPONENTS parts{}; + parts.dwStructSize = sizeof(parts); + parts.dwSchemeLength = static_cast(-1); + parts.dwHostNameLength = static_cast(-1); + parts.dwUrlPathLength = static_cast(-1); + parts.dwExtraInfoLength = static_cast(-1); + if (!::WinHttpCrackUrl(url.c_str(), 0, 0, &parts)) + { + Error = "invalid url"; + return false; + } + std::wstring const host(parts.lpszHostName, parts.dwHostNameLength); + std::wstring const path = std::wstring(parts.lpszUrlPath, parts.dwUrlPathLength) + std::wstring(parts.lpszExtraInfo, parts.dwExtraInfoLength); + + HINTERNET connection = ::WinHttpConnect(m_session, host.c_str(), parts.nPort, 0); + HINTERNET request = connection ? ::WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, + parts.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0) + : nullptr; + auto const close = [&]() { + if (request != nullptr) + ::WinHttpCloseHandle(request); + if (connection != nullptr) + ::WinHttpCloseHandle(connection); + }; + + if (request == nullptr || !::WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || !::WinHttpReceiveResponse(request, nullptr)) + { + Error = "WinHTTP error " + std::to_string(::GetLastError()); + close(); + return false; + } + + DWORD status{0}; + DWORD statussize{sizeof(status)}; + ::WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status, &statussize, WINHTTP_NO_HEADER_INDEX); + + for (;;) + { + if (Stop()) + { + Error = "cancelled"; + close(); + return false; + } + DWORD available{0}; + if (!::WinHttpQueryDataAvailable(request, &available)) + { + Error = "WinHTTP error " + std::to_string(::GetLastError()); + close(); + return false; + } + if (available == 0) + break; + std::size_t const offset = Out.size(); + Out.resize(offset + available); + DWORD received{0}; + if (!::WinHttpReadData(request, Out.data() + offset, available, &received)) + { + Error = "WinHTTP error " + std::to_string(::GetLastError()); + close(); + return false; + } + Out.resize(offset + received); + } + close(); + + if (status != 200) + { + Error = "HTTP " + std::to_string(status); + return false; + } + return true; +} + +#elif defined(EU07_WITH_CURL) + +http_client::http_client() +{ + static std::once_flag initialized; + std::call_once(initialized, []() { ::curl_global_init(CURL_GLOBAL_DEFAULT); }); + m_curl = ::curl_easy_init(); +} + +http_client::~http_client() +{ + if (m_curl != nullptr) + ::curl_easy_cleanup(m_curl); +} + +bool http_client::get(std::string const &Url, std::vector &Out, std::string &Error, stop_check const &Stop) +{ + Out.clear(); + if (m_curl == nullptr) + { + Error = "curl_easy_init failed"; + return false; + } + + auto const write = +[](char *Data, std::size_t Size, std::size_t Count, void *User) -> std::size_t { + auto *out = static_cast *>(User); + out->insert(out->end(), Data, Data + Size * Count); + return Size * Count; + }; + auto const progress = +[](void *User, curl_off_t, curl_off_t, curl_off_t, curl_off_t) -> int { return (*static_cast(User))() ? 1 : 0; }; + + ::curl_easy_reset(m_curl); + ::curl_easy_setopt(m_curl, CURLOPT_URL, Url.c_str()); + ::curl_easy_setopt(m_curl, CURLOPT_USERAGENT, "MaSzyna-editor"); + ::curl_easy_setopt(m_curl, CURLOPT_FOLLOWLOCATION, 1L); + ::curl_easy_setopt(m_curl, CURLOPT_NOSIGNAL, 1L); + ::curl_easy_setopt(m_curl, CURLOPT_CONNECTTIMEOUT, 10L); + ::curl_easy_setopt(m_curl, CURLOPT_LOW_SPEED_LIMIT, 1L); + ::curl_easy_setopt(m_curl, CURLOPT_LOW_SPEED_TIME, 20L); + ::curl_easy_setopt(m_curl, CURLOPT_WRITEFUNCTION, write); + ::curl_easy_setopt(m_curl, CURLOPT_WRITEDATA, &Out); + ::curl_easy_setopt(m_curl, CURLOPT_NOPROGRESS, 0L); + ::curl_easy_setopt(m_curl, CURLOPT_XFERINFOFUNCTION, progress); + ::curl_easy_setopt(m_curl, CURLOPT_XFERINFODATA, const_cast(&Stop)); + + CURLcode const code = ::curl_easy_perform(m_curl); + if (code != CURLE_OK) + { + Error = ::curl_easy_strerror(code); + return false; + } + long status{0}; + ::curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &status); + if (status != 200) + { + Error = "HTTP " + std::to_string(status); + return false; + } + return true; +} + +#else + +http_client::http_client() = default; +http_client::~http_client() = default; + +bool http_client::get(std::string const &, std::vector &Out, std::string &Error, stop_check const &) +{ + Out.clear(); + Error = "no http client in this build"; + return false; +} + +#endif + +} // namespace + +bool editor_http_get(std::string const &Url, std::vector &Out, std::string &Error) +{ + thread_local http_client client; + return client.get(Url, Out, Error, []() { return false; }); +} + +namespace +{ + +// --------------------------------------------------------------------------------------------- +// geoportal requests and the disk cache + +struct tile_request +{ + int ix{0}; // easting index + int iy{0}; // northing index + int level{standard_pixels}; + int year{0}; + bool hires{false}; +}; + +struct image +{ + int width{0}; + int height{0}; + std::vector rgba; +}; + +// the archival services return the newest imagery taken up to TIME, but a TIME in the future yields a blank +// image, so the end of the requested year is clamped to the current month +std::string time_parameter(int Year) +{ + using namespace std::chrono; + year_month_day const today{floor(system_clock::now())}; + int year = Year; + unsigned month = 12; + int const thisyear = static_cast(today.year()); + if (year >= thisyear) + { + year = thisyear; + month = static_cast(today.month()); + } + char buffer[32]; + std::snprintf(buffer, sizeof(buffer), "%04d-%02u-01", year, month); + return buffer; +} + +std::string wms_url(tile_request const &Request, bool Hires, int Pixels, bool Png) +{ + std::string const service = Hires ? (Request.year ? "HighResolutionTime" : "HighResolution") : (Request.year ? "StandardResolutionTime" : "StandardResolution"); + std::string const layer = (Hires && Request.year) ? "Image" : "Raster"; + auto const size = static_cast(editor_orthophoto::tile_size); + long long const minn = Request.iy * size; + long long const mine = Request.ix * size; + + std::string url = "https://mapy.geoportal.gov.pl/wss/service/PZGIK/ORTO/WMS/" + service + + "?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&CRS=EPSG:2180&STYLES=&LAYERS=" + layer; + url += Png ? "&FORMAT=image/png&TRANSPARENT=TRUE" : "&FORMAT=image/jpeg"; + url += "&WIDTH=" + std::to_string(Pixels) + "&HEIGHT=" + std::to_string(Pixels); + // WMS 1.3.0 uses the axis order of the CRS, which for EPSG:2180 is northing first + url += "&BBOX=" + std::to_string(minn) + "," + std::to_string(mine) + "," + std::to_string(minn + size) + "," + std::to_string(mine + size); + if (Request.year) + url += "&TIME=" + time_parameter(Request.year); + return url; +} + +fs::path cache_root() +{ + fs::path root = user_config_path(".cache"); + if (root.empty()) + root = ".cache"; + return root / "geoportal"; +} + +fs::path cache_file(tile_request const &Request, bool Hires, std::string const &Suffix) +{ + std::string const product = std::string(Hires ? "hr_" : "sr_") + (Request.year ? std::to_string(Request.year) : std::string("latest")); + std::string const name = std::to_string(static_cast(editor_orthophoto::tile_size)) + "_" + std::to_string(Request.iy) + "_" + std::to_string(Request.ix) + "_" + Suffix; + return cache_root() / product / name; +} + +bool is_image(std::vector const &Data) +{ + if (Data.size() >= 3 && Data[0] == 0xFF && Data[1] == 0xD8 && Data[2] == 0xFF) + return true; // jpeg + return Data.size() >= 4 && Data[0] == 0x89 && Data[1] == 'P' && Data[2] == 'N' && Data[3] == 'G'; +} + +bool read_file(fs::path const &Path, std::vector &Out) +{ + std::ifstream file(Path, std::ios::binary); + if (!file.is_open()) + return false; + Out.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + return !Out.empty(); +} + +void write_file(fs::path const &Path, std::vector const &Data) +{ + std::error_code ec; + fs::create_directories(Path.parent_path(), ec); + // write under a per-thread name and rename, so an interrupted write never leaves a truncated tile behind + std::ostringstream suffix; + suffix << ".part" << std::this_thread::get_id(); + fs::path temporary = Path; + temporary += suffix.str(); + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + if (!file.is_open()) + return; + file.write(reinterpret_cast(Data.data()), static_cast(Data.size())); + if (!file) + { + file.close(); + fs::remove(temporary, ec); + return; + } + } + fs::rename(temporary, Path, ec); + if (ec) + fs::remove(temporary, ec); +} + +// cached file if present, otherwise downloads (with a few retries, the service drops connections and answers 404 now and then) +bool fetch(http_client &Client, std::string const &Url, fs::path const &Path, std::vector &Out, std::string &Error, stop_check const &Stop) +{ + if (read_file(Path, Out) && is_image(Out)) + return true; + + for (int attempt = 0; attempt < 4; ++attempt) + { + if (Stop()) + { + Error = "cancelled"; + return false; + } + if (attempt > 0) + { + // back off, but stay responsive to shutdown + for (int wait = 0; wait < attempt * 5 && !Stop(); ++wait) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + if (!Client.get(Url, Out, Error, Stop)) + continue; + if (!is_image(Out)) + { + // service exception report (xml) or similar + Error = "unexpected response: " + std::string(Out.begin(), Out.begin() + std::min(Out.size(), 200)); + continue; + } + write_file(Path, Out); + return true; + } + return false; +} + +bool decode(std::vector const &Data, image &Out, std::string &Error) +{ + int width{0}, height{0}, components{0}; + // WMS GetMap is north-up. the texture loader sets a process-wide stb flip for OpenGL; + // pin this thread to no flip immediately before load so the first row stays north + stbi_set_flip_vertically_on_load_thread(0); + stbi_uc *pixels = stbi_load_from_memory(Data.data(), static_cast(Data.size()), &width, &height, &components, 4); + if (pixels == nullptr) + { + Error = std::string("image decoding failed: ") + stbi_failure_reason(); + return false; + } + Out.width = width; + Out.height = height; + Out.rgba.assign(pixels, pixels + static_cast(width) * height * 4); + stbi_image_free(pixels); + return true; +} + +// 2x2 box filter +void halve(image &Image) +{ + int const width = std::max(1, Image.width / 2); + int const height = std::max(1, Image.height / 2); + std::vector out(static_cast(width) * height * 4); + for (int y = 0; y < height; ++y) + for (int x = 0; x < width; ++x) + for (int c = 0; c < 4; ++c) + { + auto const at = [&](int X, int Y) { return static_cast(Image.rgba[(static_cast(std::min(Y, Image.height - 1)) * Image.width + std::min(X, Image.width - 1)) * 4 + c]); }; + out[(static_cast(y) * width + x) * 4 + c] = static_cast((at(2 * x, 2 * y) + at(2 * x + 1, 2 * y) + at(2 * x, 2 * y + 1) + at(2 * x + 1, 2 * y + 1) + 2) / 4); + } + Image.width = width; + Image.height = height; + Image.rgba = std::move(out); +} + +// areas without imagery come back plain white (jpeg) or fully transparent (png) +bool is_blank(image const &Image) +{ + for (std::size_t i = 0; i + 3 < Image.rgba.size(); i += 4) + if (Image.rgba[i + 3] != 0 && (Image.rgba[i] < 250 || Image.rgba[i + 1] < 250 || Image.rgba[i + 2] < 250)) + return false; + return true; +} + +bool load_standard(http_client &Client, tile_request const &Request, image &Out, std::string &Error, stop_check const &Stop) +{ + std::vector data; + return fetch(Client, wms_url(Request, false, standard_pixels, false), cache_file(Request, false, std::to_string(standard_pixels) + ".jpg"), data, Error, Stop) && decode(data, Out, Error); +} + +// returns false on failure (to be retried later); Empty is set when the place has no imagery +bool load_tile(http_client &Client, tile_request const &Request, image &Out, bool &Empty, std::string &Error, stop_check const &Stop) +{ + Empty = false; + if (Request.hires && Request.level >= hires_pixels) + { + // the high-resolution product covers only parts of the country; a tiny transparent png tells whether this tile is covered + std::vector data; + image probe; + if (!fetch(Client, wms_url(Request, true, probe_pixels, true), cache_file(Request, true, "probe.png"), data, Error, Stop) || !decode(data, probe, Error)) + return false; + std::size_t covered{0}; + for (std::size_t i = 3; i < probe.rgba.size(); i += 4) + covered += (probe.rgba[i] != 0); + std::size_t const total = probe.rgba.size() / 4; + + if (covered == total) + { + return fetch(Client, wms_url(Request, true, hires_pixels, false), cache_file(Request, true, std::to_string(hires_pixels) + ".jpg"), data, Error, Stop) && decode(data, Out, Error); + } + if (covered > 0) + { + // edge of the covered area: transparent png, holes filled from the standard imagery + if (!fetch(Client, wms_url(Request, true, hires_pixels, true), cache_file(Request, true, std::to_string(hires_pixels) + ".png"), data, Error, Stop) || !decode(data, Out, Error)) + return false; + image standard; + std::string ignored; + if (load_standard(Client, Request, standard, ignored, Stop) && standard.width > 0 && standard.height > 0) + { + for (int y = 0; y < Out.height; ++y) + for (int x = 0; x < Out.width; ++x) + { + std::uint8_t *pixel = &Out.rgba[(static_cast(y) * Out.width + x) * 4]; + if (pixel[3] == 255) + continue; + int const sx = std::min(standard.width - 1, x * standard.width / Out.width); + int const sy = std::min(standard.height - 1, y * standard.height / Out.height); + std::uint8_t const *source = &standard.rgba[(static_cast(sy) * standard.width + sx) * 4]; + unsigned const alpha = pixel[3]; + for (int c = 0; c < 3; ++c) + pixel[c] = static_cast((pixel[c] * alpha + source[c] * (255u - alpha) + 127u) / 255u); + pixel[3] = 255; + } + } + return true; + } + // not covered at all: fall back to the standard imagery + } + + if (!load_standard(Client, Request, Out, Error, Stop)) + return false; + if (is_blank(Out)) + { + Empty = true; + Out = image{}; + return true; + } + while (Out.width > Request.level && Out.width > 1) + halve(Out); + return true; +} + +// the services cover Poland only (EPSG:2180 extent from their capabilities, plus a margin); no point asking about other places +bool in_service_area(int Ix, int Iy) +{ + double const east = (Ix + 0.5) * editor_orthophoto::tile_size; + double const north = (Iy + 0.5) * editor_orthophoto::tile_size; + return north > 100000.0 && north < 900000.0 && east > 100000.0 && east < 900000.0; +} + +// homogeneous clip-space vertex with texture coordinates +struct clip_vertex +{ + glm::vec4 position; + glm::vec2 uv; +}; + +// Sutherland-Hodgman against a single clip plane; Distance(v) >= 0 is inside +template int clip_polygon(clip_vertex const *In, int Count, clip_vertex *Out, Distance_ const &Distance) +{ + int count{0}; + for (int i = 0; i < Count; ++i) + { + clip_vertex const &a = In[i]; + clip_vertex const &b = In[(i + 1) % Count]; + float const da = Distance(a.position); + float const db = Distance(b.position); + if (da >= 0.0f) + Out[count++] = a; + if ((da >= 0.0f) != (db >= 0.0f)) + { + float const t = da / (da - db); + Out[count++] = {glm::mix(a.position, b.position, t), glm::mix(a.uv, b.uv, t)}; + } + } + return count; +} + +// --------------------------------------------------------------------------------------------- +// ground fit and in-scene geometry + +constexpr int ground_grid{32}; // ground samples per tile side (8 m) for the draped overlay +constexpr double steepest_ground{0.3}; // minimal normal.y of ground triangles receiving the imagery +constexpr float no_height{-std::numeric_limits::max()}; + +// a bank of its own for every geometry chunk: adding a chunk to a bank makes the renderer upload the whole bank again, +// and the tiles fitted onto a heightmap terrain carry its full grid (hundreds of thousands of vertices each) +std::vector scene_banks; + +std::string slot_name(int Index) +{ + // script-style generated resource: no file behind it, the content is supplied through update_from_memory() + return "internal_src:editor_orthophoto_" + std::to_string(Index); +} + +// clips a polygon in the XZ plane against Point[Axis] <= Limit (Below) or Point[Axis] >= Limit +int clip_xz(glm::dvec3 const *In, int Count, glm::dvec3 *Out, int Axis, double Limit, bool Below) +{ + auto const inside = [&](glm::dvec3 const &Point) { return Below ? Point[Axis] <= Limit : Point[Axis] >= Limit; }; + int count{0}; + for (int i = 0; i < Count; ++i) + { + glm::dvec3 const &a = In[i]; + glm::dvec3 const &b = In[(i + 1) % Count]; + bool const ina = inside(a); + if (ina) + Out[count++] = a; + if (ina != inside(b)) + Out[count++] = a + (b - a) * ((Limit - a[Axis]) / (b[Axis] - a[Axis])); + } + return count; +} + +// height of triangle abc above (X,Z), if the vertical line through it hits the triangle +bool triangle_height(glm::dvec3 const &A, glm::dvec3 const &B, glm::dvec3 const &C, double X, double Z, double &Out) +{ + double const ux = B.x - A.x, uz = B.z - A.z; + double const vx = C.x - A.x, vz = C.z - A.z; + double const den = ux * vz - vx * uz; + if (std::abs(den) < 1e-9) + return false; + double const wx = X - A.x, wz = Z - A.z; + double const s = (wx * vz - vx * wz) / den; + double const t = (ux * wz - wx * uz) / den; + double constexpr epsilon{1e-9}; + if (s < -epsilon || t < -epsilon || s + t > 1.0 + epsilon) + return false; + Out = A.y + s * (B.y - A.y) + t * (C.y - A.y); + return true; +} + +// bilinear lookup in a tile heightfield; U runs west->east, V north->south, both 0..1 +double sample_heights(std::vector const &Heights, double U, double V) +{ + double const fx = std::clamp(U, 0.0, 1.0) * ground_grid; + double const fy = std::clamp(V, 0.0, 1.0) * ground_grid; + int const ix = std::min(static_cast(fx), ground_grid - 1); + int const iy = std::min(static_cast(fy), ground_grid - 1); + double const tx = fx - ix, ty = fy - iy; + auto const at = [&](int X, int Y) { return static_cast(Heights[static_cast(Y) * (ground_grid + 1) + X]); }; + double const top = at(ix, iy) + (at(ix + 1, iy) - at(ix, iy)) * tx; + double const bottom = at(ix, iy + 1) + (at(ix + 1, iy + 1) - at(ix, iy + 1)) * tx; + return top + (bottom - top) * ty; +} + +// smooth normal from the heightfield (u grows towards -x, v towards -z) +glm::vec3 heights_normal(std::vector const &Heights, double U, double V) +{ + double constexpr delta{1.0 / ground_grid}; + double const dhdu = (sample_heights(Heights, U + delta, V) - sample_heights(Heights, U - delta, V)) / (2.0 * delta * editor_orthophoto::tile_size); + double const dhdv = (sample_heights(Heights, U, V + delta) - sample_heights(Heights, U, V - delta)) / (2.0 * delta * editor_orthophoto::tile_size); + return glm::normalize(glm::vec3(static_cast(dhdu), 1.0f, static_cast(dhdv))); +} +} // namespace + +// --------------------------------------------------------------------------------------------- + +editor_orthophoto::~editor_orthophoto() +{ + // the scene geometry is left alone: on shutdown the region may already be gone + { + std::lock_guard lock(m_mutex); + m_stop = true; + m_queue.clear(); + } + m_wakeup.notify_all(); + for (auto &worker : m_workers) + if (worker.joinable()) + worker.join(); +} + +bool editor_orthophoto::can_download() +{ +#if defined(_WIN32) || defined(EU07_WITH_CURL) + return true; +#else + return false; +#endif +} + +std::string editor_orthophoto::cache_directory() +{ + auto const path = cache_root().u8string(); + return std::string(path.begin(), path.end()); +} + +bool editor_orthophoto::owns(gfx::geometry_handle const &Geometry) +{ + return Geometry.bank != 0 && std::find(scene_banks.begin(), scene_banks.end(), Geometry.bank) != scene_banks.end(); +} + +void editor_orthophoto::enabled(bool State) +{ + if (State == m_enabled) + return; + m_enabled = State; + if (m_enabled) + { + start_workers(); + return; + } + cancel_pending(); + for (auto &entry : m_tiles) + release_tile(entry.second); + m_tiles.clear(); + ++m_generation; // results still in flight are now stale + std::lock_guard lock(m_mutex); + m_done.clear(); +} + +void editor_orthophoto::settings(config const &Config) +{ + bool const sourcechanged = (Config.year != m_config.year || Config.hires != m_config.hires); + bool const shapechanged = (Config.height != m_config.height || Config.drape != m_config.drape || Config.in_scene != m_config.in_scene || Config.lift != m_config.lift); + // the surface of the ground is gathered only for the geometry in the scene, the overlay makes do with the heights + bool const groundchanged = (Config.in_scene != m_config.in_scene); + m_config = Config; + if (groundchanged) + refit(); + m_config.lift = std::clamp(m_config.lift, 0.0f, 50.0f); + m_config.radius = std::clamp(m_config.radius, 0, max_radius); + m_config.opacity = std::clamp(m_config.opacity, 0.0f, 1.0f); + if (shapechanged) + for (auto &entry : m_tiles) + entry.second.scene_dirty = true; + if (!sourcechanged) + return; + cancel_pending(); + for (auto &entry : m_tiles) + release_tile(entry.second); + m_tiles.clear(); + ++m_generation; +} + +void editor_orthophoto::refit() +{ + // the current shapes stay until their tile is sampled again, so nothing blinks + for (auto &entry : m_tiles) + entry.second.grounded = false; +} + +void editor_orthophoto::detach_scene() +{ + for (auto &entry : m_tiles) + { + remove_shape(entry.second); + release_geometry(entry.second); + } +} + +void editor_orthophoto::cancel_pending() +{ + { + std::lock_guard lock(m_mutex); + m_queue.clear(); + } + // jobs already picked up by a worker still deliver their result, which is accepted if the tile is still wanted + for (auto &entry : m_tiles) + entry.second.requested = 0; +} + +void editor_orthophoto::clear_cache() +{ + cancel_pending(); + for (auto &entry : m_tiles) + release_tile(entry.second); + m_tiles.clear(); + ++m_generation; + { + std::lock_guard lock(m_mutex); + m_done.clear(); + } + std::error_code ec; + fs::remove_all(cache_root(), ec); +} + +void editor_orthophoto::retry_failed() +{ + for (auto &entry : m_tiles) + { + entry.second.failures = 0; + entry.second.retry_at = 0.0; + } +} + +editor_orthophoto::statistics editor_orthophoto::stats() const +{ + statistics out; + for (auto const &entry : m_tiles) + { + tile const &t = entry.second; + if (t.level > 0 && !t.empty && t.slot.texture != null_handle) + ++out.resident; + if (t.requested != 0) + ++out.loading; + else if (t.failures > 0 && t.level == 0) + ++out.failed; + } + return out; +} + +int editor_orthophoto::level_for(int Distance, bool Hires) +{ + // coarser imagery further away keeps the memory use of large radii in check + if (Distance <= 1) + return Hires ? hires_pixels : standard_pixels; + if (Distance <= 3) + return standard_pixels; + if (Distance <= 6) + return standard_pixels / 2; + return standard_pixels / 4; +} + +editor_orthophoto::tile_key editor_orthophoto::camera_tile(glm::dvec3 const &Camera) const +{ + double const east = m_config.east - Camera.x; + double const north = m_config.north + Camera.z; + return {static_cast(std::floor(east / tile_size)), static_cast(std::floor(north / tile_size))}; +} + +glm::dvec2 editor_orthophoto::tile_corner(tile_key const &Key) const +{ + // image u runs west->east (towards -x), v north->south (towards -z) + return {m_config.east - Key.first * tile_size, (Key.second + 1) * tile_size - m_config.north}; +} + +void editor_orthophoto::start_workers() +{ + if (!m_workers.empty()) + return; + for (int i = 0; i < worker_count; ++i) + m_workers.emplace_back(&editor_orthophoto::worker, this); +} + +editor_orthophoto::texture_slot editor_orthophoto::acquire_slot() +{ + if (!m_free_slots.empty()) + { + texture_slot const slot = m_free_slots.back(); + m_free_slots.pop_back(); + return slot; + } + texture_slot slot; + slot.index = m_texture_count++; + slot.texture = GfxRenderer->Fetch_Texture(slot_name(slot.index), true); + return slot; +} + +void editor_orthophoto::release_tile(tile &Tile) +{ + remove_shape(Tile); + release_geometry(Tile); + if (Tile.slot.texture == null_handle) + return; + // shrink to a single texel to give the gpu memory back; the slot is reused by the next tile + std::uint8_t const texel[4] = {0, 0, 0, 0}; + GfxRenderer->Texture(Tile.slot.texture).update_from_memory(1, 1, texel); + m_free_slots.push_back(Tile.slot); + Tile.slot = texture_slot{}; +} + +void editor_orthophoto::schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha) +{ + { + std::lock_guard lock(m_mutex); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return Job.key == Key; }), m_queue.end()); + m_queue.push_back(job{Key, Level, Alpha, source{m_config.year, m_config.hires}, m_generation.load()}); + } + Tile.requested = Level; + Tile.requested_alpha = Alpha; + m_wakeup.notify_one(); +} + +std::uint8_t editor_orthophoto::texture_alpha() const +{ + // the overlay applies its opacity when drawing, the scene shapes can only take it from the texture + return m_config.in_scene ? static_cast(std::lround(m_config.opacity * 255.0f)) : 255; +} + +std::size_t editor_orthophoto::apply(result &Result, double Now) +{ + if (Result.generation != m_generation.load()) + return 0; + auto const lookup = m_tiles.find(Result.key); + if (lookup == m_tiles.end()) + return 0; // left the area in the meantime; the imagery stays in the disk cache + tile &t = lookup->second; + bool const current = (t.requested == Result.level && t.requested_alpha == Result.alpha); + if (current) + t.requested = 0; + + if (!Result.ok) + { + if (current) + { + ++t.failures; + t.retry_at = (t.failures >= 5) ? std::numeric_limits::max() : Now + std::min(120.0, 5.0 * std::pow(2.0, t.failures - 1)); + } + return 0; + } + + t.failures = 0; + t.level = Result.level; + if ((t.alpha < 255) != (Result.alpha < 255)) + t.scene_dirty = true; // the shape moves between the opaque and the translucent pass + t.alpha = Result.alpha; + t.empty = Result.empty; + if (Result.empty) + { + release_tile(t); + return 0; + } + if (t.slot.texture == null_handle) + t.slot = acquire_slot(); + if (t.slot.texture == null_handle) + { + t.level = 0; + return 0; + } + // a shape already in the scene shows the new content right away, it refers to the same texture + GfxRenderer->Texture(t.slot.texture).update_from_memory(Result.width, Result.height, Result.pixels.data()); + return Result.pixels.size(); +} + +void editor_orthophoto::sample_ground(tile_key const &Key, tile &Tile) +{ + Tile.grounded = true; + Tile.scene_dirty = true; + Tile.heights.clear(); + Tile.surface.clear(); + + glm::dvec2 const corner = tile_corner(Key); + glm::dvec2 const min(corner.x - tile_size, corner.y - tile_size); + glm::dvec2 const max(corner.x, corner.y); + // the clipped surface is drawn only by the geometry in the scene. the overlay needs just the heights, and the heightmap + // terrain gives these straight away: its triangles (hundreds of thousands in a tile) aren't gathered for it + bool const surface = m_config.in_scene || !m_height; + std::vector triangles; + m_ground(min, max, surface, triangles); + + int constexpr samples{ground_grid + 1}; + double constexpr step{tile_size / ground_grid}; + std::vector heights(static_cast(samples) * samples, no_height); + if (!surface) + for (int j = 0; j < samples; ++j) + for (int i = 0; i < samples; ++i) + { + double y; + if (m_height(corner.x - i * step, corner.y - j * step, y)) + heights[static_cast(j) * samples + i] = static_cast(y); + } + + for (auto const &triangle : triangles) + { + glm::dvec3 a = triangle[0], b = triangle[1], c = triangle[2]; + glm::dvec3 normal = glm::cross(b - a, c - a); + if (normal.y < 0.0) + { + // keep every triangle facing up, with the winding the editor terrain uses + std::swap(b, c); + normal = -normal; + } + double const length = glm::length(normal); + if (length < 1e-9 || normal.y / length < steepest_ground) + continue; // walls and the like don't receive the imagery + + if (surface) + { + glm::dvec3 polygon[8] = {a, b, c}; + glm::dvec3 scratch[8]; + int count = clip_xz(polygon, 3, scratch, 0, max.x, true); + count = clip_xz(scratch, count, polygon, 0, min.x, false); + count = clip_xz(polygon, count, scratch, 2, max.y, true); + count = clip_xz(scratch, count, polygon, 2, min.y, false); + for (int k = 1; k + 1 < count; ++k) + { + Tile.surface.push_back(polygon[0]); + Tile.surface.push_back(polygon[k]); + Tile.surface.push_back(polygon[k + 1]); + } + } + + // heightfield samples under the (whole) triangle, the highest surface wins + double const xlo = std::min({a.x, b.x, c.x}), xhi = std::max({a.x, b.x, c.x}); + double const zlo = std::min({a.z, b.z, c.z}), zhi = std::max({a.z, b.z, c.z}); + int const i0 = std::clamp(static_cast(std::ceil((corner.x - xhi) / step)), 0, samples - 1); + int const i1 = std::clamp(static_cast(std::floor((corner.x - xlo) / step)), 0, samples - 1); + int const j0 = std::clamp(static_cast(std::ceil((corner.y - zhi) / step)), 0, samples - 1); + int const j1 = std::clamp(static_cast(std::floor((corner.y - zlo) / step)), 0, samples - 1); + for (int j = j0; j <= j1; ++j) + for (int i = i0; i <= i1; ++i) + { + double y; + if (triangle_height(a, b, c, corner.x - i * step, corner.y - j * step, y)) + { + float &sample = heights[static_cast(j) * samples + i]; + sample = std::max(sample, static_cast(y)); + } + } + } + + // holes (places without ground) take the average of their neighbours, spreading inwards + std::size_t missing = std::count(heights.begin(), heights.end(), no_height); + if (missing == heights.size()) + return; // no ground at all: the tile stays flat at the layer height + while (missing > 0) + { + std::vector next = heights; + for (int j = 0; j < samples; ++j) + for (int i = 0; i < samples; ++i) + { + if (heights[static_cast(j) * samples + i] != no_height) + continue; + double sum{0.0}; + int found{0}; + int const offsets[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; + for (auto const &offset : offsets) + { + int const x = i + offset[0], y = j + offset[1]; + if (x < 0 || y < 0 || x >= samples || y >= samples) + continue; + float const neighbour = heights[static_cast(y) * samples + x]; + if (neighbour == no_height) + continue; + sum += neighbour; + ++found; + } + if (found > 0) + { + next[static_cast(j) * samples + i] = static_cast(sum / found); + --missing; + } + } + heights.swap(next); + } + Tile.heights = std::move(heights); + auto const range = std::minmax_element(Tile.heights.begin(), Tile.heights.end()); + Tile.height_min = *range.first; + Tile.height_max = *range.second; +} + +void editor_orthophoto::remove_shape(tile &Tile) +{ + if (Tile.section == nullptr) + return; + if (m_region == simulation::Region) + { + auto &shapes = (Tile.cell != nullptr ? Tile.cell->m_shapestranslucent : Tile.section->m_shapes); + for (auto it = shapes.begin(); it != shapes.end(); ++it) + { + auto const &geometry = it->data().geometry; + if (geometry.bank == Tile.geometry.bank && geometry.chunk == Tile.geometry.chunk) + { + shapes.erase(it); + break; + } + } + } + Tile.section = nullptr; + Tile.cell = nullptr; +} + +void editor_orthophoto::release_geometry(tile &Tile) +{ + // the chunk stays in the bank and is reused by a tile needing the same capacity + if (Tile.geometry.chunk != 0) + m_free_chunks.emplace(Tile.capacity, Tile.geometry); + Tile.geometry = gfx::geometry_handle{0, 0}; + Tile.capacity = 0; +} + +void editor_orthophoto::build_shape(tile_key const &Key, tile &Tile) +{ + remove_shape(Tile); + Tile.scene_dirty = false; + if (Tile.level == 0 || Tile.empty || Tile.slot.texture == null_handle || simulation::Region == nullptr) + return; + if (Tile.slot.material == null_handle) + Tile.slot.material = GfxRenderer->Fetch_Material(slot_name(Tile.slot.index), true); + + glm::dvec2 const corner = tile_corner(Key); + bool const draped = m_config.drape && !Tile.surface.empty(); + auto const uv_of = [&](glm::dvec3 const &Point) { return glm::vec2((corner.x - Point.x) / tile_size, (corner.y - Point.z) / tile_size); }; + + std::vector vertices; + auto const add = [&](glm::dvec3 const &Point, double Lift) { + world_vertex vertex; + vertex.position = Point + glm::dvec3(0.0, Lift, 0.0); + vertex.texture = uv_of(Point); + // smooth normals from the heightfield, the clipped triangles alone would look faceted + vertex.normal = (draped && !Tile.heights.empty()) ? heights_normal(Tile.heights, vertex.texture.x, vertex.texture.y) : glm::vec3(0.0f, 1.0f, 0.0f); + vertices.push_back(vertex); + }; + double ylo, yhi; + if (draped) + { + vertices.reserve(Tile.surface.size()); + for (auto const &point : Tile.surface) + add(point, m_config.lift); + ylo = Tile.height_min; + yhi = Tile.height_max + m_config.lift; + } + else + { + // flat quad at the layer height, triangles facing up like the draped surface + double const y = m_config.height; + glm::dvec3 const nw(corner.x, y, corner.y), ne(corner.x - tile_size, y, corner.y); + glm::dvec3 const sw(corner.x, y, corner.y - tile_size), se(corner.x - tile_size, y, corner.y - tile_size); + for (auto const &point : {nw, sw, ne, se, ne, sw}) + add(point, 0.0); + ylo = yhi = y; + } + + glm::dvec3 const centre(corner.x - tile_size * 0.5, (ylo + yhi) * 0.5, corner.y - tile_size * 0.5); + scene::basic_section §ion = simulation::Region->section(centre); + section.create_geometry(); // existing section geometry has to be in place before we add ours (idempotent) + // opaque imagery goes with the section geometry; translucent one has to be drawn in the translucent pass, + // which only covers the cells. shapes are drawn relative to the centre of their container + bool const translucent = (Tile.alpha < 255); + scene::basic_cell *cell = translucent ? §ion.cell(centre) : nullptr; + glm::dvec3 const origin = translucent ? cell->m_area.center : section.m_area.center; + + // chunks are padded to a few fixed sizes (whole triangles), so ones freed by other tiles can be refilled in place + std::size_t capacity{6}; + while (capacity < vertices.size()) + capacity *= 2; + gfx::vertex_array gpuvertices; + gpuvertices.reserve(capacity); + for (auto const &vertex : vertices) + gpuvertices.emplace_back(gfx::basic_vertex::convert(vertex, origin)); + world_vertex padding; // degenerate triangles at the origin + padding.position = origin; + padding.normal = glm::vec3(0.0f, 1.0f, 0.0f); + padding.texture = glm::vec2(0.0f); + gpuvertices.resize(capacity, gfx::basic_vertex::convert(padding, origin)); + gfx::userdata_array userdata; + + if (Tile.geometry.chunk != 0 && Tile.capacity != capacity) + release_geometry(Tile); + if (Tile.geometry.chunk == 0) + { + auto const reusable = m_free_chunks.find(capacity); + if (reusable != m_free_chunks.end()) + { + Tile.geometry = reusable->second; + Tile.capacity = capacity; + m_free_chunks.erase(reusable); + } + } + if (Tile.geometry.chunk != 0) + { + GfxRenderer->Replace(gpuvertices, userdata, Tile.geometry, GL_TRIANGLES); + } + else + { + auto const bank = GfxRenderer->Create_Bank(); + scene_banks.push_back(bank.bank); + Tile.geometry = GfxRenderer->Insert(gpuvertices, userdata, bank, GL_TRIANGLES); + Tile.capacity = capacity; + if (Tile.geometry.chunk == 0) + { + Tile.capacity = 0; + return; + } + } + + // the shape gets just two bounding vertices: they give it its area, and leave no triangles behind for + // code which reads the source vertices of the section shapes (e.g. snapping nodes to the ground) + std::vector bounds(2, padding); + bounds[0].position = glm::dvec3(corner.x - tile_size, ylo, corner.y - tile_size); + bounds[1].position = glm::dvec3(corner.x, yhi, corner.y); + scene::shape_node shape; + shape.make_terrain(Tile.slot.material, std::move(bounds), origin); + shape.geometry(Tile.geometry); + // extend the bounds of the containers so the tile isn't culled at its edges + double const reach = glm::length(glm::dvec3(tile_size * 0.5, (yhi - ylo) * 0.5, tile_size * 0.5)); + section.m_area.radius = std::max(section.m_area.radius, static_cast(glm::length(section.m_area.center - centre) + reach)); + if (cell != nullptr) + { + cell->m_shapestranslucent.emplace_back(std::move(shape)); + cell->m_area.radius = std::max(cell->m_area.radius, static_cast(glm::length(cell->m_area.center - centre) + reach)); + cell->m_active = true; // the renderer skips cells which held nothing so far + } + else + { + section.m_shapes.emplace_back(std::move(shape)); + } + + Tile.section = §ion; + Tile.cell = cell; + m_region = simulation::Region; +} + +void editor_orthophoto::update_scene() +{ + if (m_region != simulation::Region) + { + // another scenery was loaded: the sections holding our shapes are gone, forget them without touching + for (auto &entry : m_tiles) + entry.second.section = nullptr; + m_region = simulation::Region; + } + if (!m_config.in_scene) + { + detach_scene(); + return; + } + if (simulation::Region == nullptr) + return; + + // vertices handed to the renderer per frame; at least one tile goes in every frame however large it is + std::size_t constexpr vertex_budget_scene{256u * 1024u}; + std::size_t built{0}; + for (auto &entry : m_tiles) + { + tile &t = entry.second; + if (t.section != nullptr && !t.scene_dirty) + continue; + if (t.level == 0 || t.empty) + continue; + if (m_config.drape && !t.grounded && m_ground) + continue; // wait for the ground fit; a shape already in place stays until then + build_shape(entry.first, t); + built += std::max(t.capacity, 1); + if (built >= vertex_budget_scene) + break; // the rest next frame + } +} + +void editor_orthophoto::update(glm::dvec3 const &Camera) +{ + double const now = steady_seconds(); + + // finished work first, within a per-frame upload budget + std::size_t uploaded{0}; + while (uploaded < upload_budget) + { + result done; + { + std::lock_guard lock(m_mutex); + if (m_done.empty()) + break; + done = std::move(m_done.front()); + m_done.pop_front(); + } + if (m_enabled) + uploaded += apply(done, now); + } + + if (!m_enabled) + return; + + tile_key const centre = camera_tile(Camera); + if (centre != m_camera_tile) + { + m_camera_tile = centre; + std::lock_guard lock(m_mutex); + m_focus = centre; + } + int const radius = m_config.radius; + auto const distance = [&](tile_key const &Key) { return std::max(std::abs(Key.first - centre.first), std::abs(Key.second - centre.second)); }; + + // opacity of the scene shapes: the tiles are read again with the new alpha once the slider stops moving + std::uint8_t const alpha = texture_alpha(); + if (alpha != m_alpha_pending) + { + m_alpha_pending = alpha; + m_alpha_changed = now; + } + if (m_alpha != m_alpha_pending && now - m_alpha_changed > 0.3) + m_alpha = m_alpha_pending; + + // tiles which left the area + bool evicted{false}; + for (auto it = m_tiles.begin(); it != m_tiles.end();) + { + if (distance(it->first) > radius) + { + release_tile(it->second); + it = m_tiles.erase(it); + evicted = true; + } + else + ++it; + } + if (evicted) + { + std::lock_guard lock(m_mutex); + m_queue.erase(std::remove_if(m_queue.begin(), m_queue.end(), [&](job const &Job) { return distance(Job.key) > radius; }), m_queue.end()); + } + + // tiles which need (different) imagery + for (int dy = -radius; dy <= radius; ++dy) + for (int dx = -radius; dx <= radius; ++dx) + { + tile_key const key{centre.first + dx, centre.second + dy}; + tile &t = m_tiles[key]; + if (!in_service_area(key.first, key.second)) + t.empty = true; + int const level = level_for(std::max(std::abs(dx), std::abs(dy)), m_config.hires); + if (t.empty || (t.level == level && t.alpha == m_alpha) || (t.requested == level && t.requested_alpha == m_alpha) || now < t.retry_at) + continue; + schedule(key, t, level, m_alpha); + } + + // ground fit, one tile per frame (it walks the scene geometry), nearest first + if (m_config.drape && m_ground && simulation::Region != nullptr) + { + tile *nearest{nullptr}; + tile_key nearestkey{0, 0}; + int nearestdistance{INT_MAX}; + for (auto &entry : m_tiles) + { + if (entry.second.grounded || entry.second.empty) + continue; + int const d = distance(entry.first); + if (d < nearestdistance) + { + nearestdistance = d; + nearest = &entry.second; + nearestkey = entry.first; + } + } + if (nearest != nullptr) + sample_ground(nearestkey, *nearest); + } + + update_scene(); +} + +void editor_orthophoto::worker() +{ + stbi_set_flip_vertically_on_load_thread(0); // the texture loader flips globally; tiles keep the north edge in the first row + http_client client; + stop_check const stopping = [this]() { return m_stop.load(); }; + + for (;;) + { + job current; + { + std::unique_lock lock(m_mutex); + m_wakeup.wait(lock, [this]() { return m_stop.load() || !m_queue.empty(); }); + if (m_stop) + return; + // nearest tile first + auto best = m_queue.begin(); + long long bestdistance = LLONG_MAX; + for (auto it = m_queue.begin(); it != m_queue.end(); ++it) + { + long long const dx = it->key.first - m_focus.first; + long long const dy = it->key.second - m_focus.second; + long long const d = dx * dx + dy * dy; + if (d < bestdistance) + { + bestdistance = d; + best = it; + } + } + current = *best; + m_queue.erase(best); + } + + result out; + out.key = current.key; + out.level = current.level; + out.generation = current.generation; + out.alpha = current.alpha; + + image picture; + std::string error; + tile_request const request{current.key.first, current.key.second, current.level, current.src.year, current.src.hires}; + out.ok = load_tile(client, request, picture, out.empty, error, stopping); + if (m_stop) + return; + if (!out.ok) + ErrorLog("Editor orthophoto: tile " + std::to_string(current.key.second) + "_" + std::to_string(current.key.first) + " failed: " + error); + if (out.ok && current.alpha < 255) + for (std::size_t i = 3; i < picture.rgba.size(); i += 4) + picture.rgba[i] = static_cast((picture.rgba[i] * current.alpha + 127u) / 255u); + out.width = picture.width; + out.height = picture.height; + out.pixels = std::move(picture.rgba); + + std::lock_guard lock(m_mutex); + m_done.push_back(std::move(out)); + } +} + +void editor_orthophoto::draw(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const +{ + if (!m_enabled || m_config.in_scene || m_config.opacity <= 0.0f || ScreenWidth <= 0.0f || ScreenHeight <= 0.0f) + return; + + ImDrawList *drawlist = ImGui::GetBackgroundDrawList(ImGui::GetMainViewport()); + // without vertex offset support in the backend the whole list has to fit 16-bit indices + std::size_t const vertexbudget = (ImGui::GetIO().BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) ? vertex_budget * 8 : vertex_budget; + ImU32 const color = IM_COL32(255, 255, 255, static_cast(m_config.opacity * 255.0f + 0.5f)); + auto const toscreen = [&](glm::vec4 const &Clip) { return ImVec2((Clip.x / Clip.w * 0.5f + 0.5f) * ScreenWidth, (0.5f - Clip.y / Clip.w * 0.5f) * ScreenHeight); }; + // slightly wider than the view so clipped edges never show on screen + float constexpr guard{1.02f}; + auto const outside = [&](glm::vec4 const *Corners, int Count) { + auto const all = [&](auto const &Test) { + for (int i = 0; i < Count; ++i) + if (!Test(Corners[i])) + return false; + return true; + }; + return all([](glm::vec4 const &v) { return v.w < near_w; }) || all([&](glm::vec4 const &v) { return v.x > v.w * guard; }) || + all([&](glm::vec4 const &v) { return v.x < -v.w * guard; }) || all([&](glm::vec4 const &v) { return v.y > v.w * guard; }) || + all([&](glm::vec4 const &v) { return v.y < -v.w * guard; }); + }; + // clip-space change per metre of height + glm::vec4 const up = ViewProjection * glm::vec4(0.0f, 1.0f, 0.0f, 0.0f); + + for (auto const &entry : m_tiles) + { + tile const &t = entry.second; + if (t.level == 0 || t.empty || t.slot.texture == null_handle) + continue; + std::size_t const textureid = GfxRenderer->Texture(t.slot.texture).get_id(); + if (textureid == 0 || textureid == static_cast(static_cast(-1))) + continue; + + bool const draped = m_config.drape && !t.heights.empty(); + double const flat = m_config.height; + double const lift = m_config.lift; + double const ylo = draped ? t.height_min + lift : flat; + double const yhi = draped ? t.height_max + lift : flat; + + // world extent of the tile; u runs west->east (-x), v runs north->south (-z), matching the image rows + glm::dvec2 const corner = tile_corner(entry.first); + auto const relative = [&](double U, double V) { return glm::vec3(glm::dvec3(corner.x - U * tile_size, 0.0, corner.y - V * tile_size) - CameraPos); }; + // the projection is linear before the divide, so clip positions interpolate exactly: c = c00 + du*u + dv*v + up*height + glm::vec4 const c00 = ViewProjection * glm::vec4(relative(0.0, 0.0), 1.0f); + glm::vec4 const du = ViewProjection * glm::vec4(relative(1.0, 0.0), 1.0f) - c00; + glm::vec4 const dv = ViewProjection * glm::vec4(relative(0.0, 1.0), 1.0f) - c00; + auto const clip = [&](float U, float V, double Height) { return c00 + du * U + dv * V + up * static_cast(Height); }; + glm::vec4 const box[8] = {clip(0, 0, ylo), clip(1, 0, ylo), clip(1, 1, ylo), clip(0, 1, ylo), clip(0, 0, yhi), clip(1, 0, yhi), clip(1, 1, yhi), clip(0, 1, yhi)}; + if (outside(box, 8)) + continue; + + // ImGui interpolates texture coordinates affinely, so subdivide finer where the camera is close; + // draped tiles also need enough segments to follow the ground + double const nearestx = std::clamp(CameraPos.x, corner.x - tile_size, corner.x); + double const nearestz = std::clamp(CameraPos.z, corner.y - tile_size, corner.y); + double const nearesty = std::clamp(CameraPos.y, ylo, yhi); + double const distance = glm::length(glm::dvec3(nearestx, nearesty, nearestz) - CameraPos); + double const step = std::clamp(distance * 0.2, 4.0, tile_size); + int segments = std::clamp(static_cast(std::ceil(tile_size / step)), 1, 64); + if (draped) + segments = std::max(segments, distance < 300.0 ? ground_grid : distance < 800.0 ? ground_grid / 2 : ground_grid / 4); + float const segmentsize = 1.0f / static_cast(segments); + // the grid points of the heights are taken as they are where the segments fall on them + int const stride = (draped && ground_grid % segments == 0) ? ground_grid / segments : 0; + auto const height_at = [&](int I, int J) { + if (!draped) + return flat; + if (stride > 0) + return static_cast(t.heights[static_cast(J * stride) * (ground_grid + 1) + I * stride]) + lift; + return sample_heights(t.heights, I * segmentsize, J * segmentsize) + lift; + }; + + // each point of the grid is projected once, with the sides of the view it's beyond: a quad wholly inside goes + // straight to the draw list, one wholly beyond a side is skipped, only the ones across an edge are clipped + int const side = segments + 1; + static std::vector points; + static std::vector codes; + points.resize(static_cast(side) * side); + codes.resize(points.size()); + for (int j = 0; j < side; ++j) + for (int i = 0; i < side; ++i) + { + auto const index = static_cast(j) * side + i; + float const u = i * segmentsize, v = j * segmentsize; + glm::vec4 const position = clip(u, v, height_at(i, j)); + points[index] = {position, {u, v}}; + codes[index] = static_cast((position.w < near_w ? 1 : 0) | (position.x > position.w * guard ? 2 : 0) | (position.x < -position.w * guard ? 4 : 0) | + (position.y > position.w * guard ? 8 : 0) | (position.y < -position.w * guard ? 16 : 0)); + } + + drawlist->PushTextureID((ImTextureID)(intptr_t)(textureid)); + for (int j = 0; j < segments; ++j) + for (int i = 0; i < segments; ++i) + { + if (static_cast(drawlist->VtxBuffer.Size) + 16 > vertexbudget) + { + drawlist->PopTextureID(); + return; + } + std::size_t const corners[4] = {static_cast(j) * side + i, static_cast(j) * side + i + 1, static_cast(j + 1) * side + i + 1, + static_cast(j + 1) * side + i}; + auto const all = codes[corners[0]] & codes[corners[1]] & codes[corners[2]] & codes[corners[3]]; + if (all != 0) + continue; // beyond one side of the view + auto const any = codes[corners[0]] | codes[corners[1]] | codes[corners[2]] | codes[corners[3]]; + if (any == 0) + { + drawlist->PrimReserve(6, 4); + auto const base = static_cast(drawlist->_VtxCurrentIdx); + drawlist->PrimWriteIdx(base); + drawlist->PrimWriteIdx(static_cast(base + 1)); + drawlist->PrimWriteIdx(static_cast(base + 2)); + drawlist->PrimWriteIdx(base); + drawlist->PrimWriteIdx(static_cast(base + 2)); + drawlist->PrimWriteIdx(static_cast(base + 3)); + for (auto const corner : corners) + drawlist->PrimWriteVtx(toscreen(points[corner].position), ImVec2(points[corner].uv.x, points[corner].uv.y), color); + continue; + } + + // clip against the near plane and the (slightly widened) sides of the view + clip_vertex quad[4] = {points[corners[0]], points[corners[1]], points[corners[2]], points[corners[3]]}; + clip_vertex buffer0[12], buffer1[12]; + int count = clip_polygon(quad, 4, buffer0, [](glm::vec4 const &v) { return v.w - near_w; }); + count = clip_polygon(buffer0, count, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.x; }); + count = clip_polygon(buffer1, count, buffer0, [&](glm::vec4 const &v) { return v.w * guard + v.x; }); + count = clip_polygon(buffer0, count, buffer1, [&](glm::vec4 const &v) { return v.w * guard - v.y; }); + count = clip_polygon(buffer1, count, buffer0, [&](glm::vec4 const &v) { return v.w * guard + v.y; }); + if (count < 3) + continue; + + drawlist->PrimReserve((count - 2) * 3, count); + auto const base = static_cast(drawlist->_VtxCurrentIdx); + for (int k = 1; k + 1 < count; ++k) + { + drawlist->PrimWriteIdx(base); + drawlist->PrimWriteIdx(static_cast(base + k)); + drawlist->PrimWriteIdx(static_cast(base + k + 1)); + } + for (int k = 0; k < count; ++k) + drawlist->PrimWriteVtx(toscreen(buffer0[k].position), ImVec2(buffer0[k].uv.x, buffer0[k].uv.y), color); + } + drawlist->PopTextureID(); + } +} diff --git a/editor/editorOrthophoto.hpp b/editor/editorOrthophoto.hpp new file mode 100644 index 00000000..8d7d942f --- /dev/null +++ b/editor/editorOrthophoto.hpp @@ -0,0 +1,227 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "interfaces/ITexture.h" // texture_handle +#include "utilities/Classes.h" // material_handle +#include "rendering/geometrybank.h" // gfx::geometry_handle + +bool editor_http_get(std::string const &Url, std::vector &Out, std::string &Error); + +namespace scene +{ +class basic_region; +class basic_section; +class basic_cell; +} // namespace scene + +// Editor aid: aerial orthophoto tiles from geoportal.gov.pl (GUGiK WMS), shown around the camera either +// on a horizontal plane or fitted onto the terrain. +// +// Tiles are aligned to an absolute 256 m grid in PUWG 1992 (EPSG:2180), so the disk cache is shared +// between sceneries. The scenery is placed on that grid through the PUWG coordinates of its (0,0,0) +// point; MaSzyna's +Z points north and +X points west, hence easting = E0 - x, northing = N0 + z. +// +// Downloading, cache I/O and image decoding run on worker threads; the main thread only uploads +// finished images (a bounded amount per frame) and emits the geometry. By default the layer goes into +// the ImGui background draw list, like the other editor viewport overlays: translucent and drawn over +// everything. Optionally it is put into the scene instead (shapes in the region sections, like the editor +// terrain, or translucent shapes in the cells when it isn't fully opaque), so tracks, models and terrain in +// front of it cover it. +class editor_orthophoto +{ + public: + struct config + { + double north{0.0}; // PUWG 1992 X (northing) of the scenery origin + double east{0.0}; // PUWG 1992 Y (easting) of the scenery origin + int radius{2}; // tiles loaded around the camera tile (Chebyshev distance) + float height{0.0f}; // world Y of the layer (with drape: only where no ground was found) + float opacity{0.6f}; // 0..1 + int year{0}; // 0 = newest available imagery, otherwise newest imagery taken up to the end of that year + bool hires{false}; // 4096 px tiles from the high-resolution product next to the camera, where it exists + bool drape{true}; // fit the imagery onto the ground geometry instead of a flat plane + float lift{0.05f}; // with drape: distance above the ground (in the scene it has to clear the ground to be seen) + bool in_scene{false}; // render as opaque scene geometry, covered by whatever is in front of it + }; + + using world_triangle = std::array; + // supplies the ground triangles overlapping an XZ rectangle (Min/Max are x,z); used to drape the imagery. + // Heightmap: the heightmap terrain as triangles too; without it the terrain is asked through the height query alone + using ground_query = std::function &Out)>; + // height of the heightmap terrain at a point, false where there's none + using height_query = std::function; + + struct statistics + { + int resident{0}; // tiles with imagery on screen + int loading{0}; // tiles waiting for or being processed by a worker + int failed{0}; // tiles whose last attempt failed + }; + + static constexpr double tile_size{256.0}; // metres per tile side + static constexpr int max_radius{10}; + static constexpr int oldest_year{1995}; // first year offered by the archival service + + editor_orthophoto() = default; + ~editor_orthophoto(); + editor_orthophoto(editor_orthophoto const &) = delete; + editor_orthophoto &operator=(editor_orthophoto const &) = delete; + + bool enabled() const { return m_enabled; } + // disabling drops the queue and returns the gpu memory of the resident tiles + void enabled(bool State); + config const &settings() const { return m_config; } + // changes of year / resolution invalidate resident tiles, the rest applies immediately + void settings(config const &Config); + void ground_source(ground_query Query) { m_ground = std::move(Query); } + void height_source(height_query Query) { m_height = std::move(Query); } + // samples the ground again (after it was edited); tiles are refitted progressively + void refit(); + // takes the scene geometry out of the region (e.g. when the editor is left); it's re-created on the next update + void detach_scene(); + // true for geometry created by the layer, so ground queries can skip it + static bool owns(gfx::geometry_handle const &Geometry); + + // main thread, once per frame: schedules tiles around the camera and uploads finished ones + void update(glm::dvec3 const &Camera); + // main thread, inside an ImGui frame: emits the overlay into the background draw list. + // ViewProjection must map camera-relative world positions (world - CameraPos) to clip space. + void draw(glm::mat4 const &ViewProjection, glm::dvec3 const &CameraPos, float ScreenWidth, float ScreenHeight) const; + // forgets pending (not yet started) downloads, e.g. when the editor is left + void cancel_pending(); + // drops resident tiles and deletes the on-disk cache; enabled tiles are fetched again + void clear_cache(); + // clears failure state so failed tiles are attempted again + void retry_failed(); + + statistics stats() const; + // false when the build has no http client; cached tiles are still shown + static bool can_download(); + static std::string cache_directory(); + + private: + using tile_key = std::pair; // (easting index, northing index) on the PUWG tile grid + + struct source + { + int year{0}; + bool hires{false}; + }; + + struct job + { + tile_key key; + int level{0}; // requested texture size in pixels + std::uint8_t alpha{255}; // baked into the texture, for translucency in the scene + source src; + std::uint32_t generation{0}; + }; + + struct result + { + tile_key key; + int level{0}; + std::uint8_t alpha{255}; + std::uint32_t generation{0}; + bool ok{false}; + bool empty{false}; // no imagery for this place/year: nothing to draw + int width{0}; + int height{0}; + std::vector pixels; // RGBA8, first row = north edge + }; + + struct texture_slot + { + int index{-1}; // names the texture and its material + texture_handle texture{null_handle}; + material_handle material{null_handle}; // created on first use by the scene geometry + }; + + struct tile + { + texture_slot slot; + int level{0}; // size of the imagery currently held (0 = nothing yet) + int requested{0}; // level of the job scheduled for this tile (0 = none) + std::uint8_t alpha{255}; // alpha of the imagery currently held + std::uint8_t requested_alpha{255}; // alpha of the scheduled job + bool empty{false}; + int failures{0}; + double retry_at{0.0}; // steady clock seconds + // ground fit + bool grounded{false}; // ground was sampled + std::vector heights; // (ground_grid+1)^2 world Y, rows north->south, columns west->east; empty if no ground + float height_min{0.0f}; + float height_max{0.0f}; + std::vector surface; // upward facing ground triangles clipped to the tile (3 vertices each) + // scene geometry + scene::basic_section *section{nullptr}; // section holding the shape (nullptr = not in the scene) + scene::basic_cell *cell{nullptr}; // set instead of the section's shape list when the shape is translucent + gfx::geometry_handle geometry{0, 0}; + std::size_t capacity{0}; // vertex count of the geometry chunk + bool scene_dirty{true}; // the shape needs to be rebuilt + }; + + static int level_for(int Distance, bool Hires); + tile_key camera_tile(glm::dvec3 const &Camera) const; + glm::dvec2 tile_corner(tile_key const &Key) const; // world (x,z) of the north-west corner + void schedule(tile_key const &Key, tile &Tile, int Level, std::uint8_t Alpha); + std::uint8_t texture_alpha() const; // alpha the textures should carry with the current settings + std::size_t apply(result &Result, double Now); // returns bytes uploaded + void release_tile(tile &Tile); + texture_slot acquire_slot(); + void sample_ground(tile_key const &Key, tile &Tile); + void build_shape(tile_key const &Key, tile &Tile); + void remove_shape(tile &Tile); + void release_geometry(tile &Tile); + void update_scene(); + void start_workers(); + void worker(); + + config m_config; + bool m_enabled{false}; + std::map m_tiles; // main thread only + tile_key m_camera_tile{0, 0}; + std::vector m_free_slots; // pool of textures, reused between tiles + int m_texture_count{0}; + ground_query m_ground; + height_query m_height; + scene::basic_region *m_region{nullptr}; // region the scene geometry was put into + std::multimap m_free_chunks; // geometry chunks by capacity, reused between tiles + // the scene opacity is baked into the textures, which means reading the tiles again; applied once the value settles + std::uint8_t m_alpha{255}; // in effect + std::uint8_t m_alpha_pending{255}; // latest requested + double m_alpha_changed{0.0}; // when the requested value last changed + + // shared with the workers, guarded by m_mutex + mutable std::mutex m_mutex; + std::condition_variable m_wakeup; + std::deque m_queue; + std::deque m_done; + tile_key m_focus{0, 0}; // workers pick the queued tile closest to it + + std::atomic m_stop{false}; + std::atomic m_generation{1}; + std::vector m_workers; +}; diff --git a/editor/editorPreviews.cpp b/editor/editorPreviews.cpp new file mode 100644 index 00000000..27f84a9f --- /dev/null +++ b/editor/editorPreviews.cpp @@ -0,0 +1,349 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorPreviews.hpp" + +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" +#include "stb/stb_image.h" + +#include +#include + +namespace editor_previews +{ + +std::string const folder{"textures/previews/nodebank/"}; + +std::string file_name_part(std::string Text) +{ + for (auto &character : Text) + { + auto const c{static_cast(character)}; + if ((false == std::isalnum(c)) && (c != '_') && (c != '-') && (c != '.') && (c != '+') && (c != '(') && (c != ')')) + { + character = '_'; + } + } + if ((Text.empty()) || (Text == ".") || (Text == "..")) + { + Text = "_"; + } + return Text; +} + +std::string preview_path(std::string Model, std::string const &Skin) +{ + replace_slashes(Model); + erase_extension(Model); + std::string path; + std::size_t start{0}; + while (start <= Model.size()) + { + auto end{Model.find('/', start)}; + if (end == std::string::npos) + { + end = Model.size(); + } + if (end > start) + { + if (false == path.empty()) + { + path += '/'; + } + path += file_name_part(Model.substr(start, end - start)); + } + start = end + 1; + } + if ((false == Skin.empty()) && (Skin != "none")) + { + auto skin{Skin}; + replace_slashes(skin); + erase_extension(skin); + std::replace(skin.begin(), skin.end(), '/', '_'); + path += '@' + file_name_part(skin); + } + return folder + path; +} + +std::string entry_preview_path(std::string const &Entry) +{ + std::string model, skin; + if (false == entry_model(Entry, model, skin)) + { + return {}; + } + return preview_path(model, skin); +} + +bool entry_model(std::string const &Entry, std::string &Model, std::string &Skin) +{ + auto const nodestart{Entry.find("node")}; + if (nodestart == std::string::npos) + { + return false; + } + auto const entry{Entry.substr(nodestart)}; + // node + cParser parser(entry, cParser::buffer_TEXT); + parser.getTokens(5); + std::string type; + for (int index = 0; index < 5; ++index) + { + parser >> type; + } + if (type != "model") + { + return false; + } + // the model and skin are the first two tokens after the location and rotation, read as the generator reads them + cParser names(entry, cParser::buffer_TEXT); + names.getTokens(9, false); + Model = names.getToken(); + Skin = names.getToken(false); + return false == Model.empty(); +} + +std::string variant(std::string const &Path, std::set const &Taken) +{ + if (false == Taken.contains(Path)) + { + return Path; + } + auto number{2}; + while (Taken.contains(Path + "~" + std::to_string(number))) + { + ++number; + } + return Path + "~" + std::to_string(number); +} + +namespace +{ + +// uploaded in a frame at most +int const uploads_per_frame{8}; + +} // namespace + +image_cache::image_cache(int const Size, std::size_t const Limit) : m_limit{std::max(Limit, 2)} +{ + // at a bigger ui the cards are drawn bigger too; 512 of them at 128 pixels take 32 MB + m_size = (Size > 0 ? Size : std::clamp(static_cast(128.0f * std::max(1.0f, Global.ui_scale)), 128, 256)); + m_thread = std::thread(&image_cache::work, this); +} + +image_cache::~image_cache() +{ + { + std::lock_guard lock(m_lock); + m_quit = true; + } + m_wake.notify_all(); + if (m_thread.joinable()) + { + m_thread.join(); + } + // the textures aren't released: the cache goes when the simulator closes, and the gl context may be gone by then +} + +std::uint64_t image_cache::image(std::string const &Path) +{ + auto &entry{m_entries[Path]}; + entry.used = m_frame; + if (entry.texture != 0 || entry.missing || entry.pending) + { + return entry.texture; + } + entry.pending = true; + { + std::lock_guard lock(m_lock); + m_queue.push_front(Path); + } + m_wake.notify_one(); + return 0; +} + +bool image_cache::missing(std::string const &Path) const +{ + auto const lookup{m_entries.find(Path)}; + return lookup != m_entries.end() && lookup->second.missing; +} + +void image_cache::update() +{ + ++m_frame; + std::vector done; + { + std::lock_guard lock(m_lock); + // the queue keeps only what was asked for lately; the rest is asked for again when it's scrolled back into sight + while (m_queue.size() > m_limit / 2) + { + auto const lookup{m_entries.find(m_queue.back())}; + if (lookup != m_entries.end()) + { + lookup->second.pending = false; + } + m_queue.pop_back(); + } + auto const count{std::min(m_done.size(), uploads_per_frame)}; + done.assign(std::make_move_iterator(m_done.begin()), std::make_move_iterator(m_done.begin() + count)); + m_done.erase(m_done.begin(), m_done.begin() + count); + } + auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr}; + for (auto &image : done) + { + if (image.generation != m_generation) + { + continue; + } + auto const lookup{m_entries.find(image.path)}; + if (lookup == m_entries.end()) + { + continue; + } + auto &entry{lookup->second}; + entry.pending = false; + if (image.rgba.empty()) + { + entry.missing = true; + continue; + } + entry.texture = (renderer != nullptr ? renderer->Create_Image(image.rgba.data(), image.width, image.height) : 0); + // a renderer which can't show them gets no more requests + entry.missing = (entry.texture == 0); + } + // over the limit, the images not asked for the longest are dropped + std::size_t textures{0}; + for (auto const &entry : m_entries) + { + textures += (entry.second.texture != 0 ? 1 : 0); + } + if (textures <= m_limit) + { + return; + } + std::vector> loaded; + loaded.reserve(textures); + for (auto const &entry : m_entries) + { + if (entry.second.texture != 0) + { + loaded.emplace_back(entry.second.used, &entry.first); + } + } + std::sort(loaded.begin(), loaded.end()); + std::vector dropped; + for (std::size_t index = 0; index < textures - m_limit; ++index) + { + dropped.push_back(*loaded[index].second); + } + for (auto const &path : dropped) + { + auto const lookup{m_entries.find(path)}; + release(lookup->second); + m_entries.erase(lookup); + } +} + +void image_cache::clear() +{ + { + std::lock_guard lock(m_lock); + m_queue.clear(); + m_done.clear(); + ++m_generation; + } + for (auto &entry : m_entries) + { + release(entry.second); + } + m_entries.clear(); +} + +void image_cache::release(entry &Entry) +{ + if (Entry.texture != 0 && GfxRenderer && GfxRenderer->GetImguiRenderer() != nullptr) + { + GfxRenderer->GetImguiRenderer()->Release_Image(Entry.texture); + } + Entry.texture = 0; +} + +void image_cache::work() +{ + // the texture loader turns its images upside down for all threads; these are read as they are + stbi_set_flip_vertically_on_load_thread(0); + while (true) + { + decoded image; + { + std::unique_lock lock(m_lock); + m_wake.wait(lock, [this]() { return m_quit || false == m_queue.empty(); }); + if (m_quit) + { + return; + } + image.path = m_queue.front(); + image.generation = m_generation; + m_queue.pop_front(); + } + int width{0}, height{0}, components{0}; + auto *pixels{stbi_load(image.path.c_str(), &width, &height, &components, 4)}; + if (pixels != nullptr && width > 0 && height > 0) + { + // scaled down by the average of the pixels which make each one, the longer side to the size of the cache + auto const scale{std::max(1.0f, static_cast(std::max(width, height)) / m_size)}; + image.width = std::max(1, static_cast(width / scale)); + image.height = std::max(1, static_cast(height / scale)); + image.rgba.resize(static_cast(image.width) * image.height * 4); + for (int y = 0; y < image.height; ++y) + { + auto const y0{static_cast(y * scale)}; + auto const y1{std::max(y0 + 1, std::min(height, static_cast((y + 1) * scale)))}; + for (int x = 0; x < image.width; ++x) + { + auto const x0{static_cast(x * scale)}; + auto const x1{std::max(x0 + 1, std::min(width, static_cast((x + 1) * scale)))}; + std::uint32_t sum[4]{0, 0, 0, 0}; + for (int sy = y0; sy < y1; ++sy) + { + auto const *row{pixels + (static_cast(sy) * width + x0) * 4}; + for (int sx = x0; sx < x1; ++sx, row += 4) + { + sum[0] += row[0]; + sum[1] += row[1]; + sum[2] += row[2]; + sum[3] += row[3]; + } + } + auto const count{static_cast((y1 - y0) * (x1 - x0))}; + auto *target{image.rgba.data() + (static_cast(y) * image.width + x) * 4}; + for (int channel = 0; channel < 4; ++channel) + { + target[channel] = static_cast(sum[channel] / count); + } + } + } + } + if (pixels != nullptr) + { + stbi_image_free(pixels); + } + { + std::lock_guard lock(m_lock); + m_done.emplace_back(std::move(image)); + } + } +} + +} // namespace editor_previews diff --git a/editor/editorPreviews.hpp b/editor/editorPreviews.hpp new file mode 100644 index 00000000..4e61738e --- /dev/null +++ b/editor/editorPreviews.hpp @@ -0,0 +1,98 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// preview images of the models of the node bank: made by -generate-nodebank-previews, shown by the node bank of the editor. +// both find an image by the same name, made here +namespace editor_previews +{ + +// folder of the images, under the folder of the simulator +extern std::string const folder; + +// characters which can be safely used in a file name, the others replaced by _ +std::string file_name_part(std::string Text); +// path of the image of the node with specified model and skin, without the extension: the path of the model in the preview folder, +// the skin added to the name +std::string preview_path(std::string Model, std::string const &Skin); +// path of the image of a node bank entry ("node ... model ..."), without the extension; +// empty if the entry isn't a model +std::string entry_preview_path(std::string const &Entry); +// model and skin of a node bank entry, as above. returns: false if the entry isn't a model +bool entry_model(std::string const &Entry, std::string &Model, std::string &Skin); +// name given to another entry with the same model and skin (other lights or angles): ~2, ~3... added in the order of the +// node bank, after the names already Taken +std::string variant(std::string const &Path, std::set const &Taken); + +// images of the node bank as textures of the user interface, small and kept for a while: the files are read and scaled down +// in a thread of their own, and uploaded in the main thread, a few in a frame +class image_cache +{ + public: + // Size: pixels of the longer side of the images, 0 for the size of the cards of the node bank; Limit: images kept at most + explicit image_cache(int const Size = 0, std::size_t const Limit = 512); + ~image_cache(); + image_cache(image_cache const &) = delete; + image_cache &operator=(image_cache const &) = delete; + + // texture of the image at specified path (with the extension), 0 while it's being read or when there's no such image. + // an image not asked for longer than others is dropped when there are too many + std::uint64_t image(std::string const &Path); + // true if there's no image at specified path + bool missing(std::string const &Path) const; + // to be called once in a frame: uploads the images read since the last frame + void update(); + // drops all images, so that they're read again (e.g. after new ones were generated) + void clear(); + + private: + struct entry + { + std::uint64_t texture{0}; + bool missing{false}; + bool pending{false}; // waits in the queue or is being read + std::uint64_t used{0}; // frame it was asked for in the last time + }; + struct decoded + { + std::string path; + int width{0}; + int height{0}; + std::vector rgba; // empty if there's no such image + unsigned int generation{0}; + }; + void work(); + void release(entry &Entry); + // members + std::unordered_map m_entries; + std::uint64_t m_frame{0}; + int m_size{128}; // pixels of the longer side of the images + std::size_t m_limit{512}; + // shared with the thread + std::mutex m_lock; + std::condition_variable m_wake; + std::deque m_queue; // the image asked for in the last is read first, it's the one on the screen + std::vector m_done; + unsigned int m_generation{0}; // images read before the last clear() are thrown away + bool m_quit{false}; + std::thread m_thread; +}; + +} // namespace editor_previews diff --git a/editor/editorProfile.cpp b/editor/editorProfile.cpp new file mode 100644 index 00000000..b4d889b1 --- /dev/null +++ b/editor/editorProfile.cpp @@ -0,0 +1,664 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorProfile.hpp" +#include "editor/editorFormat.hpp" + +#include +#include +#include +#include + +namespace profile +{ + +namespace +{ + +double polyline(line const &Line, double const Chainage) +{ + auto const &points{Line.points}; + if (points.empty()) + return 0.0; + if (points.size() == 1 || Chainage <= points.front().chainage) + return points.front().elevation + (points.size() > 1 ? grade_after(Line, 0) * (Chainage - points.front().chainage) : 0.0); + if (Chainage >= points.back().chainage) + return points.back().elevation + grade_after(Line, points.size() - 2) * (Chainage - points.back().chainage); + auto const next{std::upper_bound(points.begin(), points.end(), Chainage, [](double const Value, pvi const &Point) { return Value < Point.chainage; })}; + auto const index{static_cast(std::distance(points.begin(), next)) - 1}; + return points[index].elevation + grade_after(Line, index) * (Chainage - points[index].chainage); +} + +bool overlaps(double const From, double const To, context::zone const &Zone) +{ + return From < Zone.to - 1e-6 && To > Zone.from + 1e-6; +} + +} // namespace + +double grade_after(line const &Line, std::size_t const Index) +{ + auto const &points{Line.points}; + if (Index + 1 >= points.size()) + return 0.0; + auto const run{points[Index + 1].chainage - points[Index].chainage}; + return run > 1e-9 ? (points[Index + 1].elevation - points[Index].elevation) / run : 0.0; +} + +double required_radius(line const &Line) +{ + return std::max(Line.norms.radius_min, Line.norms.radius_factor * Line.speed * Line.speed); +} + +void assign_radii(line &Line) +{ + auto const required{std::ceil(required_radius(Line) / 10.0) * 10.0}; + for (std::size_t k = 1; k + 1 < Line.points.size(); ++k) + { + auto &point{Line.points[k]}; + if (false == point.automatic || point.joint) + continue; + auto const change{std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) * 1000.0}; + point.radius = change > Line.norms.curve_threshold + 1e-6 ? required : 0.0; + } +} + +void complete(line &Line, context const &Context) +{ + auto &points{Line.points}; + points.erase(std::remove_if(points.begin(), points.end(), [](pvi const &Point) { return Point.joint; }), points.end()); + if (points.size() < 2) + return; + bool const start{Line.join_start && Context.start_joined}; + bool const end{Line.join_end && Context.end_joined}; + if (start) + points.front().elevation = Context.start_elevation; + if (end) + points.back().elevation = Context.end_elevation; + + // the vertical curve of the joint starts right at the end of the route, off the adjoining track + auto const joint = [&](pvi const &End, pvi const &Neighbour, double const Grade, double const Direction) -> std::optional { + auto const run{(Neighbour.chainage - End.chainage) * Direction}; + if (run < 1.0) + return std::nullopt; + auto const offset{std::abs(Neighbour.elevation - (End.elevation + Grade * run * Direction))}; + if (offset / run * 1000.0 <= Line.norms.curve_threshold + 1e-6) + return std::nullopt; + auto radius{std::ceil(required_radius(Line) / 10.0) * 10.0}; + double tangent; + if (run * run >= 2.0 * radius * offset) + tangent = (run - std::sqrt(run * run - 2.0 * radius * offset)) * 0.5; + else + { + tangent = run * 0.5; + radius = run * run / (2.0 * offset); + } + pvi result; + result.chainage = End.chainage + tangent * Direction; + result.elevation = End.elevation + Grade * tangent * Direction; + result.radius = radius; + result.automatic = false; + result.joint = true; + return result; + }; + if (start) + if (auto const point{joint(points.front(), points[1], Context.start_grade, 1.0)}) + points.insert(points.begin() + 1, *point); + if (end) + if (auto const point{joint(points.back(), points[points.size() - 2], Context.end_grade, -1.0)}) + points.insert(points.end() - 1, *point); + assign_radii(Line); +} + +std::vector curves(line const &Line) +{ + std::vector result; + auto const &points{Line.points}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + auto const radius{points[k].radius}; + if (radius <= 0.0) + continue; + curve c; + c.point = k; + c.change = grade_after(Line, k) - grade_after(Line, k - 1); + c.tangent = radius * std::abs(c.change) * 0.5; + if (c.tangent < 1e-6) + continue; + c.sagitta = c.tangent * c.tangent / (2.0 * radius); + c.start = points[k].chainage - c.tangent; + c.end = points[k].chainage + c.tangent; + result.push_back(c); + } + return result; +} + +double elevation(line const &Line, double const Chainage) +{ + auto height{polyline(Line, Chainage)}; + for (auto const &c : curves(Line)) + { + auto const distance{std::abs(Chainage - Line.points[c.point].chainage)}; + if (distance >= c.tangent) + continue; + height += c.change * (c.tangent - distance) * (c.tangent - distance) / (4.0 * c.tangent); + } + return height; +} + +double grade(line const &Line, double const Chainage) +{ + auto const &points{Line.points}; + if (points.size() < 2) + return 0.0; + std::size_t index{0}; + while (index + 2 < points.size() && Chainage >= points[index + 1].chainage) + ++index; + auto slope{grade_after(Line, index)}; + for (auto const &c : curves(Line)) + { + auto const offset{Chainage - points[c.point].chainage}; + if (std::abs(offset) >= c.tangent) + continue; + slope -= c.change * (c.tangent - std::abs(offset)) / (2.0 * c.tangent) * (offset < 0.0 ? -1.0 : 1.0); + } + return slope; +} + +double radius_for_sagitta(line const &Line, std::size_t const Point, double const Sagitta) +{ + if (Point == 0 || Point + 1 >= Line.points.size()) + return 0.0; + auto const change{std::abs(grade_after(Line, Point) - grade_after(Line, Point - 1))}; + if (change < 1e-9) + return Line.points[Point].radius; + return std::max(0.0, 8.0 * std::abs(Sagitta) / (change * change)); +} + +std::vector check(line const &Line, context const &Context) +{ + std::vector result; + auto const &points{Line.points}; + auto const &norms{Line.norms}; + auto const add = [&](bool const Error, int const Point, double const Chainage, std::string Text) { + result.push_back({Error, Point, Chainage, std::move(Text)}); + }; + if (points.size() < 2) + { + add(true, -1, 0.0, "The grade line needs at least the two ends"); + return result; + } + for (std::size_t k = 0; k + 1 < points.size(); ++k) + { + if (points[k + 1].chainage - points[k].chainage < 0.5) + add(true, static_cast(k + 1), points[k + 1].chainage, format("Points %zu and %zu are closer than 0.5 m", k + 1, k + 2)); + auto const slope{grade_after(Line, k) * 1000.0}; + if (std::abs(slope) > norms.grade_max + 1e-6) + add(false, static_cast(k), points[k].chainage, format("Grade %.2f per mille between points %zu and %zu exceeds %.2f", slope, k + 1, k + 2, norms.grade_max)); + } + + auto const list{curves(Line)}; + auto const required{required_radius(Line)}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + auto const change{std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) * 1000.0}; + auto const radius{points[k].radius}; + if (radius <= 0.0 && change > norms.curve_threshold + 1e-6) + add(false, static_cast(k), points[k].chainage, format("Point %zu: change of grade %.2f per mille needs a vertical curve", k + 1, change)); + else if (radius > 0.0 && radius < required - 1e-6) + add(false, static_cast(k), points[k].chainage, format("Point %zu: R %.0f m is below the required %.0f m for %.0f km/h", k + 1, radius, required, Line.speed)); + if (radius <= 0.0 && change > 1e-3) + for (auto const &zone : Context.switches) + if (points[k].chainage > zone.from + 1e-3 && points[k].chainage < zone.to - 1e-3) + add(true, static_cast(k), points[k].chainage, format("Point %zu: the grade changes inside switch %s", k + 1, zone.name.c_str())); + } + for (std::size_t i = 0; i < list.size(); ++i) + { + auto const &c{list[i]}; + auto const point{static_cast(c.point)}; + if (c.start < points.front().chainage - 1e-6 || c.end > points.back().chainage + 1e-6) + add(true, point, points[c.point].chainage, format("Point %zu: the vertical curve reaches past the end of the route", c.point + 1)); + if (i + 1 < list.size() && c.end > list[i + 1].start + 1e-6) + add(true, point, c.end, format("Vertical curves of points %zu and %zu overlap by %.2f m", c.point + 1, list[i + 1].point + 1, c.end - list[i + 1].start)); + for (auto const &zone : Context.switches) + if (overlaps(c.start, c.end, zone)) + add(true, point, points[c.point].chainage, format("Point %zu: the vertical curve lies on switch %s", c.point + 1, zone.name.c_str())); + } + if (norms.element_min > 0.0) + { + for (std::size_t k = 1; k + 2 < points.size(); ++k) + { + auto const tangent = [&](std::size_t const Point) { + for (auto const &c : list) + if (c.point == Point) + return c.tangent; + return 0.0; + }; + auto const free{(points[k + 1].chainage - tangent(k + 1)) - (points[k].chainage + tangent(k))}; + if (free < norms.element_min - 1e-6) + add(false, static_cast(k), points[k].chainage, format("Constant grade between points %zu and %zu is %.1f m long, less than %.0f m", k + 1, k + 2, std::max(0.0, free), norms.element_min)); + } + } + for (auto const &zone : Context.switches) + { + auto const slope{std::abs(grade(Line, (zone.from + zone.to) * 0.5)) * 1000.0}; + if (slope > norms.grade_switch_max + 1e-6) + add(false, -1, zone.from, format("Grade %.2f per mille over switch %s exceeds %.2f", slope, zone.name.c_str(), norms.grade_switch_max)); + } + for (auto const &fixed : Context.fixed) + { + auto const departure{elevation(Line, fixed.first) - fixed.second}; + if (std::abs(departure) > norms.fixed_tolerance) + { + add(true, -1, fixed.first, format("At %s the grade line departs %.3f m from a fixed point", format_chainage(fixed.first).c_str(), departure)); + break; + } + } + auto const ends = [&](bool const Atend) { + auto const joined{Atend ? Context.end_joined : Context.start_joined}; + if (false == joined) + return; + auto const chainage{Atend ? points.back().chainage : points.front().chainage}; + auto const departure{elevation(Line, chainage) - (Atend ? Context.end_elevation : Context.start_elevation)}; + if (std::abs(departure) > norms.fixed_tolerance) + add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format("The %s of the grade line is %.3f m off the adjoining track", Atend ? "end" : "start", departure)); + auto const slope{grade(Line, chainage)}; + auto const change{std::abs(slope - (Atend ? Context.end_grade : Context.start_grade)) * 1000.0}; + if (change > norms.curve_threshold + 1e-6) + add(false, Atend ? static_cast(points.size()) - 1 : 0, chainage, format("Change of grade %.2f per mille against the adjoining track at the %s", change, Atend ? "end" : "start")); + }; + ends(false); + ends(true); + std::stable_sort(result.begin(), result.end(), [](issue const &A, issue const &B) { return A.error && false == B.error; }); + return result; +} + +line recognize(std::vector const &Chainages, std::vector const &Elevations, std::vector const &Grades, recognition const &Options) +{ + line result; + auto const count{std::min({Chainages.size(), Elevations.size(), Grades.size()})}; + if (count < 2) + return result; + + struct part + { + std::size_t first, last; + double slope{0.0}, offset{0.0}; + }; + auto const fit = [&](part &Part) { + double sx{0.0}, sy{0.0}, sxx{0.0}, sxy{0.0}; + auto const origin{Chainages[Part.first]}; + auto const n{static_cast(Part.last - Part.first + 1)}; + for (auto i = Part.first; i <= Part.last; ++i) + { + auto const x{Chainages[i] - origin}; + sx += x; + sy += Elevations[i]; + sxx += x * x; + sxy += x * Elevations[i]; + } + auto const denominator{n * sxx - sx * sx}; + Part.slope = std::abs(denominator) > 1e-12 ? (n * sxy - sx * sy) / denominator : 0.0; + Part.offset = (sy - Part.slope * sx) / n - Part.slope * origin; + }; + + std::vector parts; + for (std::size_t i = 0; i < count;) + { + auto j{i}; + auto low{Grades[i]}, high{Grades[i]}; + while (j + 1 < count && std::max(high, Grades[j + 1]) - std::min(low, Grades[j + 1]) <= Options.grade_tolerance) + { + ++j; + low = std::min(low, Grades[j]); + high = std::max(high, Grades[j]); + } + if (Chainages[j] - Chainages[i] >= Options.minimum_length) + { + part p{i, j}; + fit(p); + parts.push_back(p); + i = j + 1; + } + else + ++i; + } + for (std::size_t i = 0; i + 1 < parts.size();) + { + if (std::abs(parts[i].slope - parts[i + 1].slope) < Options.grade_tolerance * 0.5) + { + parts[i].last = parts[i + 1].last; + fit(parts[i]); + parts.erase(parts.begin() + i + 1); + } + else + ++i; + } + + result.points.push_back({Chainages.front(), Elevations.front(), 0.0}); + for (std::size_t i = 0; i + 1 < parts.size(); ++i) + { + auto const &a{parts[i]}; + auto const &b{parts[i + 1]}; + auto const from{Chainages[a.last]}; + auto const to{Chainages[b.first]}; + auto const change{b.slope - a.slope}; + auto chainage{std::abs(change) > 1e-9 ? (a.offset - b.offset) / change : (from + to) * 0.5}; + chainage = std::clamp(chainage, from, std::max(from, to)); + pvi point; + point.automatic = false; + point.chainage = chainage; + point.elevation = a.slope * chainage + a.offset; + auto const gap{to - from}; + if (gap >= Options.sharp_gap && std::abs(change) > 1e-9) + point.radius = std::round(gap / std::abs(change)); + if (point.chainage - result.points.back().chainage >= 0.5) + result.points.push_back(point); + } + if (Chainages[count - 1] - result.points.back().chainage < 0.5 && result.points.size() > 1) + result.points.pop_back(); + result.points.push_back({Chainages[count - 1], Elevations[count - 1], 0.0}); + + // the gap between constant grades is shorter than the curve, the sagitta gives the radius + for (std::size_t i = 1; i + 1 < result.points.size(); ++i) + { + auto &point{result.points[i]}; + if (point.radius <= 0.0) + continue; + auto const next{std::lower_bound(Chainages.begin(), Chainages.begin() + count, point.chainage)}; + if (next == Chainages.begin() || next == Chainages.begin() + count) + continue; + auto const index{static_cast(std::distance(Chainages.begin(), next))}; + auto const run{Chainages[index] - Chainages[index - 1]}; + auto const measured{run > 1e-9 ? Elevations[index - 1] + (Elevations[index] - Elevations[index - 1]) * (point.chainage - Chainages[index - 1]) / run : Elevations[index]}; + auto const radius{radius_for_sagitta(result, i, measured - point.elevation)}; + if (radius > 0.0) + point.radius = std::round(radius); + } + return result; +} + +line fit_ground(std::vector const &Chainages, std::vector const &Ground, context const &Context, line const &Settings, ground_fit const &Options) +{ + line result; + result.speed = Settings.speed; + result.norms = Settings.norms; + result.join_start = Settings.join_start; + result.join_end = Settings.join_end; + auto const count{std::min(Chainages.size(), Ground.size())}; + if (count < 2) + return result; + + // gaps in the ground bridged straight, past the outermost samples it stays level + std::vector ground(Ground.begin(), Ground.begin() + count); + std::vector known; + for (std::size_t i = 0; i < count; ++i) + if (std::isfinite(ground[i])) + known.push_back(i); + if (known.empty()) + return result; + for (std::size_t i = 0, k = 0; i < count; ++i) + { + if (std::isfinite(ground[i])) + continue; + while (k + 1 < known.size() && known[k + 1] < i) + ++k; + if (i < known.front()) + ground[i] = Ground[known.front()]; + else if (k + 1 >= known.size()) + ground[i] = Ground[known.back()]; + else + { + auto const a{known[k]}, b{known[k + 1]}; + ground[i] = Ground[a] + (Ground[b] - Ground[a]) * (Chainages[i] - Chainages[a]) / std::max(1e-9, Chainages[b] - Chainages[a]); + } + } + if (Options.smoothing > 1.0) + { + std::vector sums(count + 1, 0.0); + for (std::size_t i = 0; i < count; ++i) + sums[i + 1] = sums[i] + ground[i]; + std::vector smooth(count); + std::size_t low{0}, high{0}; + for (std::size_t i = 0; i < count; ++i) + { + while (Chainages[i] - Chainages[low] > Options.smoothing * 0.5) + ++low; + while (high + 1 < count && Chainages[high + 1] - Chainages[i] <= Options.smoothing * 0.5) + ++high; + high = std::max(high, i); + smooth[i] = (sums[high + 1] - sums[low]) / static_cast(high - low + 1); + } + ground.swap(smooth); + } + + bool const start_fixed{Settings.join_start && Context.start_joined}; + bool const end_fixed{Settings.join_end && Context.end_joined}; + + // first guess of the points: the ground simplified to within the tolerance + std::vector knots{0, count - 1}; + { + std::vector keep(count, 0); + keep.front() = keep.back() = 1; + std::vector> stack{{0, count - 1}}; + while (false == stack.empty()) + { + auto const [a, b]{stack.back()}; + stack.pop_back(); + if (b <= a + 1) + continue; + auto const run{std::max(1e-9, Chainages[b] - Chainages[a])}; + double worst{0.0}; + std::size_t index{a}; + for (auto i = a + 1; i < b; ++i) + { + auto const chord{ground[a] + (ground[b] - ground[a]) * (Chainages[i] - Chainages[a]) / run}; + auto const departure{std::abs(ground[i] - chord)}; + if (departure > worst) + { + worst = departure; + index = i; + } + } + if (worst > Options.tolerance) + { + keep[index] = 1; + stack.push_back({a, index}); + stack.push_back({index, b}); + } + } + knots.clear(); + for (std::size_t i = 0; i < count; ++i) + if (keep[i] != 0) + knots.push_back(i); + } + + auto const segment_limit = [&](double const From, double const To) { + auto limit{result.norms.grade_max}; + for (auto const &zone : Context.switches) + if (overlaps(From, To, zone)) + limit = std::min(limit, result.norms.grade_switch_max); + return limit / 1000.0; + }; + // elevations of the points with the least squares of the departures from the ground + auto const solve = [&](std::vector const &Knots) { + auto const n{Knots.size()}; + std::vector lower(n, 0.0), diagonal(n, 1e-9), upper(n, 0.0), right(n, 0.0); + auto const add = [&](std::size_t const Interval, double const T, double const Value, double const Weight) { + diagonal[Interval] += Weight * (1.0 - T) * (1.0 - T); + diagonal[Interval + 1] += Weight * T * T; + upper[Interval] += Weight * (1.0 - T) * T; + lower[Interval + 1] += Weight * (1.0 - T) * T; + right[Interval] += Weight * (1.0 - T) * Value; + right[Interval + 1] += Weight * T * Value; + }; + auto const interval_of = [&](double const Chainage) { + std::size_t k{0}; + while (k + 2 < n && Chainage >= Chainages[Knots[k + 1]]) + ++k; + return k; + }; + for (std::size_t k = 0; k + 1 < n; ++k) + { + auto const from{Chainages[Knots[k]]}; + auto const run{std::max(1e-9, Chainages[Knots[k + 1]] - from)}; + auto const last{k + 2 == n ? Knots[k + 1] : Knots[k + 1] - 1}; + for (auto i = Knots[k]; i <= last; ++i) + add(k, (Chainages[i] - from) / run, ground[i], 1.0); + } + for (auto const &fixed : Context.fixed) + { + auto const k{interval_of(fixed.first)}; + auto const from{Chainages[Knots[k]]}; + auto const t{std::clamp((fixed.first - from) / std::max(1e-9, Chainages[Knots[k + 1]] - from), 0.0, 1.0)}; + add(k, t, fixed.second, 1e6); + } + auto const pin = [&](std::size_t const Index, double const Value) { + lower[Index] = upper[Index] = 0.0; + diagonal[Index] = 1.0; + right[Index] = Value; + }; + if (start_fixed) + pin(0, Context.start_elevation); + if (end_fixed) + pin(n - 1, Context.end_elevation); + for (std::size_t k = 1; k < n; ++k) + { + auto const factor{lower[k] / diagonal[k - 1]}; + diagonal[k] -= factor * upper[k - 1]; + right[k] -= factor * right[k - 1]; + } + std::vector z(n); + z[n - 1] = right[n - 1] / diagonal[n - 1]; + for (auto k = n - 1; k-- > 0;) + z[k] = (right[k] - upper[k] * z[k + 1]) / diagonal[k]; + + // grades kept within the limits, the fixed ends don't move + for (int pass = 0; pass < 4; ++pass) + { + for (std::size_t k = 0; k + 1 < n; ++k) + { + if (k + 2 == n && end_fixed) + continue; + auto const run{Chainages[Knots[k + 1]] - Chainages[Knots[k]]}; + auto const step{segment_limit(Chainages[Knots[k]], Chainages[Knots[k + 1]]) * run}; + z[k + 1] = std::clamp(z[k + 1], z[k] - step, z[k] + step); + } + for (auto k = n - 1; k-- > 0;) + { + if (k == 0 && start_fixed) + continue; + auto const run{Chainages[Knots[k + 1]] - Chainages[Knots[k]]}; + auto const step{segment_limit(Chainages[Knots[k]], Chainages[Knots[k + 1]]) * run}; + z[k] = std::clamp(z[k], z[k + 1] - step, z[k + 1] + step); + } + } + return z; + }; + + auto const radius{std::ceil(required_radius(result) / 10.0) * 10.0}; + auto const threshold{result.norms.curve_threshold / 1000.0}; + std::vector z; + for (int iteration = 0; iteration < 1000; ++iteration) + { + z = solve(knots); + auto const n{knots.size()}; + if (n <= 2) + break; + auto const chainage = [&](std::size_t const K) { return Chainages[knots[K]]; }; + auto const slope = [&](std::size_t const K) { return (z[K + 1] - z[K]) / std::max(1e-9, chainage(K + 1) - chainage(K)); }; + std::vector change(n, 0.0), tangent(n, 0.0); + for (std::size_t k = 1; k + 1 < n; ++k) + { + change[k] = std::abs(slope(k) - slope(k - 1)); + tangent[k] = change[k] > threshold + 1e-9 ? radius * change[k] * 0.5 : 0.0; + } + // the curve of a joint starts at the end of the route and takes its whole length + if (start_fixed) + { + change[0] = std::abs(slope(0) - Context.start_grade); + tangent[0] = change[0] > threshold + 1e-9 ? radius * change[0] : 0.0; + } + if (end_fixed) + { + change[n - 1] = std::abs(slope(n - 2) - Context.end_grade); + tangent[n - 1] = change[n - 1] > threshold + 1e-9 ? radius * change[n - 1] : 0.0; + } + + std::size_t removed{0}; + for (std::size_t k = 1; k + 1 < n && removed == 0; ++k) + for (auto const &zone : Context.switches) + if (overlaps(chainage(k) - tangent[k] - 1.0, chainage(k) + tangent[k] + 1.0, zone)) + { + removed = k; + break; + } + if (removed == 0) + { + double worst{0.0}; + for (std::size_t k = 0; k + 1 < n; ++k) + { + bool const inner{(k > 0 || tangent[0] > 0.0) && (k + 2 < n || tangent[n - 1] > 0.0)}; + auto const needed{tangent[k] + tangent[k + 1] + (inner ? result.norms.element_min : 0.0)}; + auto const shortage{needed - (chainage(k + 1) - chainage(k))}; + if (shortage <= 1e-6 || shortage <= worst) + continue; + worst = shortage; + if (k == 0) + removed = 1; + else if (k + 2 == n) + removed = n - 2; + else + removed = change[k] <= change[k + 1] ? k : k + 1; + } + } + if (removed == 0) + break; + knots.erase(knots.begin() + removed); + } + + for (std::size_t k = 0; k < knots.size(); ++k) + { + pvi point; + point.chainage = Chainages[knots[k]]; + point.elevation = z[k]; + result.points.push_back(point); + } + return result; +} + +std::vector breaks(line const &Line) +{ + std::vector result; + auto const &points{Line.points}; + for (std::size_t k = 1; k + 1 < points.size(); ++k) + { + if (points[k].radius > 0.0) + continue; + if (std::abs(grade_after(Line, k) - grade_after(Line, k - 1)) > 1e-7) + result.push_back(points[k].chainage); + } + for (auto const &c : curves(Line)) + { + result.push_back(c.start); + result.push_back(c.end); + } + std::sort(result.begin(), result.end()); + return result; +} + +std::string format_chainage(double const Chainage) +{ + auto const value{std::abs(Chainage)}; + auto const kilometres{static_cast(value / 1000.0)}; + return format("%s%ld+%06.2f", Chainage < 0.0 ? "-" : "", kilometres, value - kilometres * 1000.0); +} + +} diff --git a/editor/editorProfile.hpp b/editor/editorProfile.hpp new file mode 100644 index 00000000..9e0712c8 --- /dev/null +++ b/editor/editorProfile.hpp @@ -0,0 +1,128 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +// vertical alignment (grade line) of the rail top along the chainage of a route +namespace profile +{ + +// point of intersection of two grades, with a parabolic vertical curve of radius R (0: none) +struct pvi +{ + double chainage{0.0}; + double elevation{0.0}; + double radius{0.0}; + bool automatic{true}; // radius follows the limits + bool joint{false}; // placed by complete(), takes the grade over from the adjoining track +}; + +struct limits +{ + double grade_max{6.0}; // per mille + double grade_switch_max{2.5}; // per mille, over switches + double radius_factor{0.4}; // R >= k * V^2 + double radius_min{2000.0}; + double curve_threshold{2.0}; // per mille, change of grade which needs a vertical curve + double element_min{0.0}; // m, constant grade between vertical curves + double fixed_tolerance{0.005}; // m, allowed departure from fixed points +}; + +struct line +{ + std::vector points; // ordered by chainage; the first and the last are the ends of the route + double speed{100.0}; + limits norms; + // at the joined ends: keep the elevation and take over the grade of the adjoining track + bool join_start{true}; + bool join_end{true}; +}; + +double elevation(line const &Line, double const Chainage); +// dy/ds +double grade(line const &Line, double const Chainage); +// grade of the element between points Index and Index + 1 +double grade_after(line const &Line, std::size_t const Index); +double required_radius(line const &Line); +// radii of the automatic points: the required one where the change of grade needs a curve, none elsewhere +void assign_radii(line &Line); + +struct curve +{ + std::size_t point{0}; + double start{0.0}; + double end{0.0}; + double tangent{0.0}; + double sagitta{0.0}; + double change{0.0}; // grade after - grade before +}; +std::vector curves(line const &Line); + +// radius which gives specified elevation of the curve at the point of intersection +double radius_for_sagitta(line const &Line, std::size_t const Point, double const Sagitta); + +// facts about the route which the checks take into account +struct context +{ + double length{0.0}; + struct zone + { + double from{0.0}; + double to{0.0}; + std::string name; + }; + std::vector switches; + // elevations which the grade line has to keep (chainage, elevation) + std::vector> fixed; + // adjoining track at the ends; grades rise along the route + double start_elevation{0.0}, end_elevation{0.0}; + double start_grade{0.0}, end_grade{0.0}; + bool start_joined{false}, end_joined{false}; +}; + +// locks the joined ends, places the joint points and sets the automatic radii +void complete(line &Line, context const &Context); + +struct issue +{ + bool error{false}; + int point{-1}; + double chainage{0.0}; + std::string text; +}; +std::vector check(line const &Line, context const &Context); + +struct recognition +{ + double grade_tolerance{0.0004}; + double minimum_length{15.0}; + double sharp_gap{2.0}; +}; +// grade line of existing track, from its elevations and grades sampled along the chainage +line recognize(std::vector const &Chainages, std::vector const &Elevations, std::vector const &Grades, recognition const &Options); + +struct ground_fit +{ + double tolerance{1.0}; // m, departure from the ground which doesn't yet need a change of grade + double smoothing{20.0}; // m, length over which the ground is averaged +}; +// grade line which follows the ground within the limits of Settings, with the least earthworks. +// Ground is NaN where there's none; the joined ends of the route keep the elevation of the adjoining track +line fit_ground(std::vector const &Chainages, std::vector const &Ground, context const &Context, line const &Settings, ground_fit const &Options); + +// chainages where the paths have to end so that each of them carries a single grade or a single curve +std::vector breaks(line const &Line); + +std::string format_chainage(double const Chainage); + +} diff --git a/editor/editorRoad.cpp b/editor/editorRoad.cpp new file mode 100644 index 00000000..8e2113d1 --- /dev/null +++ b/editor/editorRoad.cpp @@ -0,0 +1,2084 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorRoad.hpp" + +#include "editor/editorTrack.hpp" +#include "world/Track.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include + +namespace +{ + +double const kSamePoint{0.25}; // ends of two pieces this close are taken for joined +double const kPieceLength{50.0}; // longest piece the planner makes +double const kPieceTurn{glm::radians(30.0)}; // largest turn of a single piece +double const kShortestRoad{2.0}; +double const kTightestRadius{6.0}; +double const kCornerRoom{4.0}; // length of an arm of a junction past the point where its edge meets the edge of the next one, taken by the rounded corner +double const kSharpestBranch{glm::radians(30.0)}; // smallest angle between two roads leaving a junction +double const kWidestCorner{glm::radians(120.0)}; // arms further apart than this don't have to make room for each other +double const kTransitionLength{30.0}; // length of the road taken by a change of its lanes + +void add_unique(std::vector &Roads, road_node *Road) +{ + if (Road != nullptr && std::find(Roads.begin(), Roads.end(), Road) == Roads.end()) + Roads.emplace_back(Road); +} + +glm::dvec3 axis_end(road_node::state const &State, bool const Atend) +{ + return State.axis.points[Atend ? segment_data::point::end : segment_data::point::start]; +} + +// roads other than the specified one, with an end at specified point +std::vector roads_at(glm::dvec3 const &Point, road_node const *Exclude) +{ + std::vector result; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road == Exclude || road->m_editorremoved) + continue; + for (auto const atend : {false, true}) + { + if (glm::distance(axis_end(road->definition(), atend), Point) < kSamePoint) + { + result.emplace_back(road); + break; + } + } + } + return result; +} + +// junctions with an arm at specified point +std::vector junctions_at(glm::dvec3 const &Point) +{ + std::vector result; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved) + continue; + for (auto const &arm : junction->definition().arms) + { + if (glm::distance(arm.position, Point) < kSamePoint) + { + result.emplace_back(junction); + break; + } + } + } + return result; +} + +void add_unique(std::vector &Junctions, junction_node *Junction) +{ + if (Junction != nullptr && std::find(Junctions.begin(), Junctions.end(), Junction) == Junctions.end()) + Junctions.emplace_back(Junction); +} + +// part of a path between two values of its curve parameter +segment_data subpath(segment_data const &Path, double const From, double const To) +{ + segment_data part{Path}; + auto const &p0{Path.points[segment_data::point::start]}; + auto const &p3{Path.points[segment_data::point::end]}; + part.rolls = {static_cast(Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * From), static_cast(Path.rolls[0] + (Path.rolls[1] - Path.rolls[0]) * To)}; + if (Path.points[segment_data::point::control1] == glm::dvec3{0.0} && Path.points[segment_data::point::control2] == glm::dvec3{0.0}) + { + part.points[segment_data::point::start] = glm::mix(p0, p3, From); + part.points[segment_data::point::end] = glm::mix(p0, p3, To); + return part; + } + auto const p1{p0 + Path.points[segment_data::point::control1]}; + auto const p2{p3 + Path.points[segment_data::point::control2]}; + auto const point = [&](double const T) { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + }; + auto const derivative = [&](double const T) { + auto const u{1.0 - T}; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); + }; + // a part of a cubic curve is a cubic curve, with the tangents scaled to the part of the parameter range it takes + part.points[segment_data::point::start] = point(From); + part.points[segment_data::point::end] = point(To); + part.points[segment_data::point::control1] = derivative(From) * ((To - From) / 3.0); + part.points[segment_data::point::control2] = derivative(To) * (-(To - From) / 3.0); + return part; +} + +// true if there's a vehicle on any of the paths turning the traffic back at the loose ends of the road +bool turning(road_node const &Road) +{ + for (auto const *turn : Road.turns()) + if (turn != nullptr && false == turn->Dynamics.empty()) + return true; + return false; +} + +// definition of a part of a piece, between two values of the curve parameter of its axis +road_node::state part(road_node::state const &State, double const From, double const To) +{ + auto result{State}; + result.axis = subpath(State.axis, From, To); + result.taper = {static_cast(State.scale(From)), static_cast(State.scale(To))}; + if (State.divided()) + result.median.width = {static_cast(State.median_width(From)), static_cast(State.median_width(To))}; + // an island is rounded at the ends of the piece, not where the piece is cut + result.median.round = {From <= 0.0 ? State.median.round[0] : 0.f, To >= 1.0 ? State.median.round[1] : 0.f}; + // and it's at its ends a piece is short of lanes + if (From > 0.0) + result.missing[0] = {0, 0}; + if (To < 1.0) + result.missing[1] = {0, 0}; + for (std::size_t side = 0; side < result.banks.size(); ++side) + { + auto const &bank{State.banks[side]}; + result.banks[side].width = {glm::mix(bank.width[0], bank.width[1], static_cast(From)), glm::mix(bank.width[0], bank.width[1], static_cast(To))}; + result.banks[side].drop = {glm::mix(bank.drop[0], bank.drop[1], static_cast(From)), glm::mix(bank.drop[0], bank.drop[1], static_cast(To))}; + } + return result; +} + +// points of an end of a road something can be joined to: the end of its axis, and where it keeps its two directions apart, +// the middle of each of its roadways +std::vector end_points(road_node::state const &State, bool const Atend) +{ + std::vector result{axis_end(State, Atend)}; + auto const t{Atend ? 1.0 : 0.0}; + auto const gap{State.median_width(t)}; + if (gap <= 0.0) + return result; + auto const tangent{State.tangent(t)}; + glm::dvec3 left{tangent.z, 0.0, -tangent.x}; + if (glm::length(left) < 1e-9) + return result; + left = glm::normalize(left); + auto const halfwidth{0.5 * State.width(Atend ? 1 : 0)}; + auto const divide{State.divide(Atend ? 1 : 0)}; + // lanes going against the axis are on its left + result.emplace_back(result.front() + left * (0.5 * (halfwidth + divide) * State.scale(t) + 0.5 * gap)); + result.emplace_back(result.front() + left * (0.5 * (divide - halfwidth) * State.scale(t) - 0.5 * gap)); + return result; +} + +// points of a path as a bezier curve, and a point of that curve. unlike the methods of a road definition +// these don't measure the whole axis for each call, which matters for what's done every frame +std::array curve_points(segment_data const &Path) +{ + auto const &p0{Path.points[segment_data::point::start]}; + auto const &p3{Path.points[segment_data::point::end]}; + if (Path.points[segment_data::point::control1] == glm::dvec3{0.0} && Path.points[segment_data::point::control2] == glm::dvec3{0.0}) + return {p0, p0 + (p3 - p0) / 3.0, p3 - (p3 - p0) / 3.0, p3}; + return {p0, p0 + Path.points[segment_data::point::control1], p3 + Path.points[segment_data::point::control2], p3}; +} + +glm::dvec3 curve_point(std::array const &Points, double const T) +{ + auto const u{1.0 - T}; + return u * u * u * Points[0] + 3.0 * u * u * T * Points[1] + 3.0 * u * T * T * Points[2] + T * T * T * Points[3]; +} + +int const kStations{64}; + +// distances along a path for evenly spaced values of its curve parameter +std::vector stations(road_node::state const &State) +{ + std::vector result(kStations + 1, 0.0); + auto const points{curve_points(State.axis)}; + auto previous{points[0]}; + for (int i = 1; i <= kStations; ++i) + { + auto const current{curve_point(points, static_cast(i) / kStations)}; + result[i] = result[i - 1] + glm::distance(previous, current); + previous = current; + } + return result; +} + +// adds to a record what a piece or a junction is like, ahead of changing it. the first entry stays, as it's what taking the lot back has to end with +void remember(editor_road::record &Record, road_node *Road) +{ + if (std::none_of(Record.roads.begin(), Record.roads.end(), [Road](std::pair const &Entry) { return Entry.first == Road; })) + Record.roads.emplace_back(Road, Road->definition()); +} + +void remember(editor_road::record &Record, junction_node *Junction) +{ + if (std::none_of(Record.junctions.begin(), Record.junctions.end(), [Junction](std::pair const &Entry) { return Entry.first == Junction; })) + Record.junctions.emplace_back(Junction, Junction->definition()); +} + +// distance along a path for specified value of its curve parameter, and the other way around +double station_at(std::vector const &Stations, double const T) +{ + auto const position{std::clamp(T, 0.0, 1.0) * kStations}; + auto const index{std::min(kStations - 1, static_cast(position))}; + return glm::mix(Stations[index], Stations[index + 1], position - index); +} + +double parameter_at(std::vector const &Stations, double const Station) +{ + if (Station <= 0.0) + return 0.0; + if (Station >= Stations.back()) + return 1.0; + auto const index{static_cast(std::distance(Stations.begin(), std::upper_bound(Stations.begin(), Stations.end(), Station)))}; + auto const span{Stations[index] - Stations[index - 1]}; + return (index - 1 + (span > 0.0 ? (Station - Stations[index - 1]) / span : 0.0)) / kStations; +} + +// direction seen from above +glm::dvec2 planar(glm::dvec3 const &Direction) +{ + glm::dvec2 const result{Direction.x, Direction.z}; + return glm::length(result) > 1e-9 ? glm::normalize(result) : glm::dvec2{0.0, 1.0}; +} + +// where a junction made in the middle of a road ends on one of its sides +struct road_cut +{ + road_node *road{nullptr}; + double t{0.0}; + bool keepstart{false}; // what stays of the piece: from its start up to the cut, or from the cut to its end +}; + +// follows a road from a point of a piece for specified distance, to find where a junction made at that point ends. +// Consumed: receives the pieces past the first one which lie within the distance whole +bool follow(road_node &Road, double const T, bool const Forward, double const Distance, road_cut &Cut, std::vector &Consumed, std::string &Error) +{ + auto *piece{&Road}; + auto from{T}; + auto forward{Forward}; + auto remaining{Distance}; + for (int guard = 0; guard < 64; ++guard) + { + auto const &state{piece->definition()}; + auto const lengths{stations(state)}; + auto const here{station_at(lengths, from)}; + auto const available{forward ? lengths.back() - here : here}; + if (remaining <= 0.0 || available >= remaining + kShortestRoad) + { + // the cut is put here; with nothing left to go the piece is kept whole + Cut.road = piece; + Cut.t = (remaining <= 0.0 ? from : parameter_at(lengths, forward ? here + remaining : here - remaining)); + Cut.keepstart = (false == forward); + if (state.changing() && Cut.t > 0.001 && Cut.t < 0.999) + { + // partway along such a piece the road has no whole number of lanes to make an arm for + Error = "Too close to a place where the road changes its lanes"; + return false; + } + return true; + } + // the junction takes this piece up to its end, as what would be left of it is too short to be a road + remaining = std::max(0.0, remaining - available); + if (piece != &Road) + Consumed.emplace_back(piece); + auto const end{axis_end(state, forward)}; + if (false == junctions_at(end).empty()) + { + Error = "Too close to a junction"; + return false; + } + auto const next{roads_at(end, piece)}; + if (next.size() != 1) + { + Error = (next.empty() ? "Too close to the end of the road" : "Too close to a place where roads meet"); + return false; + } + if (next.front() == &Road || std::find(Consumed.begin(), Consumed.end(), next.front()) != Consumed.end()) + { + Error = "The road is too short for a junction"; + return false; + } + piece = next.front(); + forward = (glm::distance(axis_end(piece->definition(), false), end) < kSamePoint); + from = (forward ? 0.0 : 1.0); + } + Error = "The road is cut into too many pieces here"; + return false; +} + +// arm of a junction for the part of a road left on one side of it. returns: false if the lanes there differ in width +bool cut_arm(road_cut const &Cut, junction_node::arm_data &Arm) +{ + auto const &state{Cut.road->definition()}; + Arm.position = state.point(Cut.t); + // the way out of the junction is the way the kept part of the piece goes from the cut + Arm.direction = planar(state.tangent(Cut.t)) * (Cut.keepstart ? -1.0 : 1.0); + // lanes heading for the cut lead into the junction. a piece which gains or loses lanes is only cut at one of its ends + auto const end{Cut.t > 0.5 ? 1 : 0}; + Arm.incoming = state.lanes_at(end, Cut.keepstart); + Arm.outgoing = state.lanes_at(end, false == Cut.keepstart); + Arm.median = static_cast(state.median_width(Cut.t)); + return editor_road::lane_width(state, Cut.t, Arm.width); +} + +// how far from the centre of a junction the edges of two of its roads meet, measured along the first one. +// Halfwidth, Otherhalfwidth: of the two roads, Angle: between the ways they leave +double edge_reach(double const Halfwidth, double const Otherhalfwidth, double const Angle) +{ + return (Otherhalfwidth + Halfwidth * std::cos(Angle)) / std::sin(Angle); +} + +// the look of a junction goes after the road it's made on +void dress(junction_node::state &Junction, road_node::state const &Road) +{ + Junction.surface = Road.surface; + Junction.texturelength = Road.texturelength; + Junction.markings = Road.markings; + // one kind of side goes around the whole junction; the right side of the road is the one facing the corner a road leaves by + Junction.side = (Road.sides[1].type != road_node::side_type::none ? Road.sides[1] : Road.sides[0]); + Junction.kerbheight = Road.kerbheight; + Junction.kerbs = (Road.kerbs[0] || Road.kerbs[1]); + Junction.kerbwidth = Road.kerbwidth; + Junction.kerbmaterial = Road.kerbmaterial; + Junction.bankmaterial = Road.bankmaterial; + Junction.median = Road.median.type; + Junction.medianmaterial = Road.median.material; + Junction.slope = Road.slope; + Junction.friction = Road.friction; + Junction.sounddistance = Road.sounddistance; + Junction.quality = Road.quality; + Junction.environment = Road.environment; +} + +void retire(std::vector const &Tracks) +{ + for (auto *track : Tracks) + if (track != nullptr) + editor_track::retire(*track); +} + +// value of the curve parameter for the point of a curve nearest to specified point, seen from above +double nearest_on(std::array const &Points, glm::dvec3 const &Point) +{ + auto const distance = [&](double const T) { + auto const position{curve_point(Points, T)}; + return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z}); + }; + double best{0.0}; + for (int i = 1; i <= 64; ++i) + if (distance(i / 64.0) < distance(best)) + best = i / 64.0; + double step{1.0 / 64.0}; + for (int i = 0; i < 24; ++i) + { + step *= 0.5; + if (best - step >= 0.0 && distance(best - step) < distance(best)) + best -= step; + else if (best + step <= 1.0 && distance(best + step) < distance(best)) + best += step; + } + return best; +} + +double planar_distance(glm::dvec3 const &Left, glm::dvec3 const &Right) +{ + return glm::distance(glm::dvec2{Left.x, Left.z}, glm::dvec2{Right.x, Right.z}); +} + +// true if two lines cross when seen from above. Along, Otheralong: receive how far along each of them the crossing is, 0 to 1 +bool lines_cross(glm::dvec3 const &Start, glm::dvec3 const &End, glm::dvec3 const &Otherstart, glm::dvec3 const &Otherend, double &Along, double &Otheralong) +{ + glm::dvec2 const line{End.x - Start.x, End.z - Start.z}; + glm::dvec2 const other{Otherend.x - Otherstart.x, Otherend.z - Otherstart.z}; + auto const denominator{line.x * other.y - line.y * other.x}; + if (std::abs(denominator) < 1e-12) + return false; + glm::dvec2 const offset{Otherstart.x - Start.x, Otherstart.z - Start.z}; + Along = (offset.x * other.y - offset.y * other.x) / denominator; + Otheralong = (offset.x * line.y - offset.y * line.x) / denominator; + return Along >= 0.0 && Along <= 1.0 && Otheralong >= 0.0 && Otheralong <= 1.0; +} + +// the same piece of a road laid out from its other end: what it looks like and where its lanes lead stays as it was +road_node::state flipped(road_node::state const &State) +{ + auto result{State}; + std::swap(result.axis.points[segment_data::point::start], result.axis.points[segment_data::point::end]); + std::swap(result.axis.points[segment_data::point::control1], result.axis.points[segment_data::point::control2]); + result.axis.rolls = {-State.axis.rolls[1], -State.axis.rolls[0]}; + std::swap(result.forward, result.backward); + result.missing[0] = {State.missing[1][1], State.missing[1][0]}; + result.missing[1] = {State.missing[0][1], State.missing[0][0]}; + // the lanes are listed left to right for someone facing along the axis, who's now looking the other way + std::reverse(result.lanes.begin(), result.lanes.end()); + std::reverse(result.changes.begin(), result.changes.end()); + for (auto &change : result.changes) + change = ((change & road_node::change_toright) != 0 ? road_node::change_toleft : 0) | ((change & road_node::change_toleft) != 0 ? road_node::change_toright : 0); + std::swap(result.sides[0], result.sides[1]); + std::swap(result.taper[0], result.taper[1]); + std::swap(result.median.width[0], result.median.width[1]); + std::swap(result.median.round[0], result.median.round[1]); + std::swap(result.kerbs[0], result.kerbs[1]); + std::swap(result.banks[0], result.banks[1]); + for (auto &bank : result.banks) + { + std::swap(bank.width[0], bank.width[1]); + std::swap(bank.drop[0], bank.drop[1]); + } + return result; +} + +// smallest radius of the bend made by a bezier curve, seen from above; 0 if the curve is practically straight +double tightest_radius(segment_data const &Path) +{ + auto const &p0{Path.points[segment_data::point::start]}; + auto const &p3{Path.points[segment_data::point::end]}; + auto const p1{p0 + Path.points[segment_data::point::control1]}; + auto const p2{p3 + Path.points[segment_data::point::control2]}; + double curvature{0.0}; + for (int i = 0; i <= 16; ++i) + { + auto const t{i / 16.0}; + auto const u{1.0 - t}; + auto const first{3.0 * u * u * (p1 - p0) + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (p3 - p2)}; + auto const second{6.0 * u * (p2 - 2.0 * p1 + p0) + 6.0 * t * (p3 - 2.0 * p2 + p1)}; + auto const speedsquared{first.x * first.x + first.z * first.z}; + if (speedsquared < 1e-9) + continue; + curvature = std::max(curvature, std::abs(second.x * first.z - second.z * first.x) / (speedsquared * std::sqrt(speedsquared))); + } + return curvature > 1e-5 ? 1.0 / curvature : 0.0; +} + +} + +bool editor_road::available(std::string *Reason) +{ + if (Global.NvRenderer) + { + if (Reason != nullptr) + *Reason = "Geometry of the roads can't be rebuilt with the experimental renderer"; + return false; + } + if (simulation::Region == nullptr) + { + if (Reason != nullptr) + *Reason = "No scenery is loaded"; + return false; + } + return true; +} + +bool editor_road::can_edit(road_node const &Road, std::string *Reason) +{ + std::string reason; + if (false == available(&reason)) + { + // reason was filled by the call + } + else if (Road.merged()) + { + reason = "the scenery wasn't opened for editing (-edit), geometry of the roads it came with can't be taken apart"; + } + else if (false == scene::Layers.editable(&Road, &reason)) + { + // reason was filled by the call + } + else if (Road.occupied()) + { + reason = "there are vehicles on it"; + } + else + { + // the paths made to turn the vehicles back at the loose ends of the neighbours go away too + for (auto const *neighbour : neighbours(Road)) + for (auto const *turn : neighbour->turns()) + if (turn != nullptr && false == turn->Dynamics.empty()) + reason = "there are vehicles turning back next to it"; + } + if (reason.empty()) + { + // so do the ways through the junctions it leads to + for (auto const atend : {false, true}) + for (auto const *junction : junctions_at(axis_end(Road.definition(), atend))) + if (junction->occupied()) + reason = "there are vehicles on the junction it leads to"; + } + if (false == reason.empty() && Reason != nullptr) + *Reason = reason; + return reason.empty(); +} + +bool editor_road::can_edit(junction_node const &Junction, std::string *Reason) +{ + std::string reason; + if (false == available(&reason)) + { + // reason was filled by the call + } + else if (Junction.merged()) + { + reason = "the scenery wasn't opened for editing (-edit), geometry of the junctions it came with can't be taken apart"; + } + else if (false == scene::Layers.editable(&Junction, &reason)) + { + // reason was filled by the call + } + else if (Junction.occupied()) + { + reason = "there are vehicles on it"; + } + else + { + // the roads attached to it get their loose ends closed anew + for (auto const &arm : Junction.definition().arms) + for (auto const *road : roads_at(arm.position, nullptr)) + if (turning(*road)) + reason = "there are vehicles turning back on the roads it leads to"; + } + if (false == reason.empty() && Reason != nullptr) + *Reason = reason; + return reason.empty(); +} + +bool editor_road::can_join(loose_end const &End, std::string *Reason) +{ + // what gets attached takes the place of the paths made for the end as it was + std::string reason; + if (End.road != nullptr && turning(*End.road)) + reason = "there are vehicles turning back on the road"; + if (End.junction != nullptr) + { + if (End.junction->occupied()) + reason = "there are vehicles on the junction"; + for (auto const &arm : End.junction->definition().arms) + for (auto const *road : roads_at(arm.position, nullptr)) + if (turning(*road)) + reason = "there are vehicles turning back on the roads the junction leads to"; + } + if (false == reason.empty() && Reason != nullptr) + *Reason = reason; + return reason.empty(); +} + +std::vector editor_road::neighbours(road_node const &Road) +{ + std::vector result; + if (Road.m_editorremoved) + return result; + for (auto const atend : {false, true}) + { + for (auto *road : roads_at(axis_end(Road.definition(), atend), &Road)) + add_unique(result, road); + // a road which keeps its directions apart can have a road joined to each of its roadways, and be one for the road joined + auto const points{end_points(Road.definition(), atend)}; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road == &Road || road->m_editorremoved) + continue; + for (auto const otheratend : {false, true}) + { + auto const otherpoints{end_points(road->definition(), otheratend)}; + if (points.size() == 1 && otherpoints.size() == 1) + continue; + for (auto const &point : points) + for (auto const &otherpoint : otherpoints) + if (glm::distance(point, otherpoint) < kSamePoint) + add_unique(result, road); + } + } + } + return result; +} + +template +void editor_road::refresh(std::vector const &Roads, std::vector const &Junctions, Change_ Change) +{ + // the pieces and junctions next to the changed ones keep their shape, but what they're joined with changes. + // this takes down the paths made for the joints, so they can be made again once the change is done + std::vector affected; + std::vector crossings; + // level crossings and traffic points hold on to the lanes, which are about to be replaced + simulation::Roadpoints.unbind(); + auto const gather = [&]() { + std::vector roads{affected}; + std::vector junctions{crossings}; + for (auto *road : Roads) + { + add_unique(roads, road); + if (road->m_editorremoved) + continue; + for (auto *neighbour : neighbours(*road)) + add_unique(roads, neighbour); + for (auto const atend : {false, true}) + for (auto *junction : junctions_at(axis_end(road->definition(), atend))) + add_unique(junctions, junction); + } + for (auto *junction : Junctions) + add_unique(junctions, junction); + // a junction changes what the roads attached to it do at their ends + for (auto *junction : junctions) + if (false == junction->m_editorremoved) + for (auto const &arm : junction->definition().arms) + for (auto *road : roads_at(arm.position, nullptr)) + add_unique(roads, road); + for (auto *road : roads) + { + if (std::find(affected.begin(), affected.end(), road) == affected.end()) + { + retire(road->release_turns()); + affected.emplace_back(road); + } + } + for (auto *junction : junctions) + { + if (std::find(crossings.begin(), crossings.end(), junction) == crossings.end()) + { + retire(junction->release_links()); + crossings.emplace_back(junction); + } + } + }; + gather(); + for (auto *road : Roads) + { + road->hide(); + retire(road->release_lanes()); + } + for (auto *junction : Junctions) + junction->hide(); + + Change(); + + // the change could have moved things next to other ones + gather(); + std::vector lanes; + for (auto *road : Roads) + { + if (road->m_editorremoved) + continue; + road->create_lanes(); + road->show(); + lanes.insert(lanes.end(), road->tracks().begin(), road->tracks().end()); + } + // joins the lanes with the lanes of the neighbours, by the ends they share + editor_track::commit(lanes); + // the pieces which gain or lose lanes lead the ones they have at one end to the ones they have at the other + for (auto *road : Roads) + if (false == road->m_editorremoved) + road->create_links(); + for (auto *junction : Junctions) + if (false == junction->m_editorremoved) + junction->show(); + // the junctions take the lanes which end at them, what's left loose after that is closed by the roads + for (auto *junction : crossings) + if (false == junction->m_editorremoved) + junction->create_links(); + for (auto *road : affected) + if (false == road->m_editorremoved) + road->close_ends(); + simulation::Roadpoints.bind(); +} + +std::vector editor_road::create(std::vector const &States) +{ + static int counter{0}; + std::vector created; + if (false == available()) + return created; + for (auto const &state : States) + { + scene::node_data data; + data.type = "road"; + data.range_max = -1.0; + data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing + // the name is what the lanes are known by to the vehicles put on them, so each piece needs one of its own + do + { + data.name = "road_" + std::to_string(++counter); + } while (simulation::Roads.find(data.name) != nullptr); + auto *road{new road_node(data)}; + road->define(state); + road->mark_dirty(); + simulation::Roads.insert(road); + scene::Layers.count(road->layer(), scene::layer_item::track); + created.emplace_back(road); + } + refresh(created, {}, []() {}); + return created; +} + +void editor_road::apply(std::vector> const &Changes) +{ + std::vector roads; + for (auto const &change : Changes) + add_unique(roads, change.first); + if (roads.empty()) + return; + refresh(roads, {}, [&]() { + for (auto const &change : Changes) + { + change.first->define(change.second); + change.first->mark_dirty(); + } + }); +} + +void editor_road::remove(std::vector const &Roads) +{ + std::vector roads; + for (auto *road : Roads) + if (road != nullptr && false == road->m_editorremoved) + add_unique(roads, road); + if (roads.empty()) + return; + refresh(roads, {}, [&]() { + for (auto *road : roads) + { + road->m_editorremoved = true; + scene::Layers.count(road->layer(), scene::layer_item::track, -1); + } + }); +} + +void editor_road::revive(std::vector const &Roads) +{ + std::vector roads; + for (auto *road : Roads) + if (road != nullptr && road->m_editorremoved) + add_unique(roads, road); + if (roads.empty()) + return; + refresh(roads, {}, [&]() { + for (auto *road : roads) + { + road->m_editorremoved = false; + road->mark_dirty(); + scene::Layers.count(road->layer(), scene::layer_item::track); + } + }); +} + +void editor_road::apply(std::vector> const &Changes) +{ + std::vector junctions; + for (auto const &change : Changes) + add_unique(junctions, change.first); + if (junctions.empty()) + return; + refresh({}, junctions, [&]() { + for (auto const &change : Changes) + { + if (change.first == nullptr) + continue; + change.first->define(change.second); + change.first->mark_dirty(); + } + }); +} + +std::vector editor_road::create(std::vector const &States) +{ + static int counter{0}; + std::vector created; + if (false == available()) + return created; + for (auto const &state : States) + { + scene::node_data data; + data.type = "junction"; + data.range_max = -1.0; + data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing + do + { + data.name = "junction_" + std::to_string(++counter); + } while (simulation::Junctions.find(data.name) != nullptr); + auto *junction{new junction_node(data)}; + junction->define(state); + junction->mark_dirty(); + simulation::Junctions.insert(junction); + scene::Layers.count(junction->layer(), scene::layer_item::track); + created.emplace_back(junction); + } + refresh({}, created, []() {}); + return created; +} + +void editor_road::remove(std::vector const &Junctions) +{ + std::vector junctions; + for (auto *junction : Junctions) + if (junction != nullptr && false == junction->m_editorremoved) + add_unique(junctions, junction); + if (junctions.empty()) + return; + refresh({}, junctions, [&]() { + for (auto *junction : junctions) + { + junction->m_editorremoved = true; + scene::Layers.count(junction->layer(), scene::layer_item::track, -1); + } + }); +} + +void editor_road::revive(std::vector const &Junctions) +{ + std::vector junctions; + for (auto *junction : Junctions) + if (junction != nullptr && junction->m_editorremoved) + add_unique(junctions, junction); + if (junctions.empty()) + return; + refresh({}, junctions, [&]() { + for (auto *junction : junctions) + { + junction->m_editorremoved = false; + junction->mark_dirty(); + scene::Layers.count(junction->layer(), scene::layer_item::track); + } + }); +} + +bool editor_road::can_edit(roadpoint_node const &Point, std::string *Reason) +{ + std::string reason; + auto editable{true}; + if (simulation::Region == nullptr) + { + reason = "No scenery is loaded"; + editable = false; + } + else + { + editable = scene::Layers.editable(&Point, &reason); + } + if (false == editable && Reason != nullptr) + *Reason = reason; + return editable; +} + +std::vector editor_road::create(std::vector const &States) +{ + static int counter{0}; + std::vector created; + if (simulation::Region == nullptr) + return created; + for (auto const &state : States) + { + scene::node_data data; + data.type = roadpoint_node::keyword(state.kind); + data.range_max = -1.0; + data.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing + // the vehicles of a spawn point are named after it, so each point needs a name of its own + do + { + data.name = data.type + "_" + std::to_string(++counter); + } while (simulation::Roadpoints.find(data.name) != nullptr); + auto *point{new roadpoint_node(data)}; + point->define(state); + point->mark_dirty(); + simulation::Roadpoints.insert(point); + scene::Layers.count(point->layer(), scene::layer_item::track); + point->bind(); + point->show(); + created.emplace_back(point); + } + return created; +} + +void editor_road::apply(std::vector> const &Changes) +{ + for (auto const &change : Changes) + { + auto *point{change.first}; + if (point == nullptr) + continue; + point->hide(); + point->unbind(); + point->define(change.second); + point->mark_dirty(); + point->bind(); + point->show(); + } +} + +void editor_road::remove(std::vector const &Points) +{ + for (auto *point : Points) + { + if (point == nullptr || point->m_editorremoved) + continue; + point->hide(); + // a crossing which goes lets the vehicles it was holding drive on + point->unbind(); + point->m_editorremoved = true; + scene::Layers.count(point->layer(), scene::layer_item::track, -1); + } +} + +void editor_road::revive(std::vector const &Points) +{ + for (auto *point : Points) + { + if (point == nullptr || false == point->m_editorremoved) + continue; + point->m_editorremoved = false; + point->mark_dirty(); + scene::Layers.count(point->layer(), scene::layer_item::track); + point->bind(); + point->show(); + } +} + +roadpoint_node *editor_road::nearest_point(glm::dvec3 const &Point, double const Radius) +{ + roadpoint_node *result{nullptr}; + auto best{Radius}; + for (auto *point : simulation::Roadpoints.sequence()) + { + if (point == nullptr || point->m_editorremoved) + continue; + auto const &position{point->definition().position}; + auto const distance{planar_distance(position, Point)}; + if (distance > best || std::abs(position.y - Point.y) > 3.0) + continue; + best = distance; + result = point; + } + return result; +} + +bool editor_road::nearest_lane(glm::dvec3 const &Point, double const Reach, glm::dvec3 &Position, glm::dvec3 &Direction) +{ + auto best{Reach}; + auto found{false}; + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + auto const &state{road->definition()}; + auto const axis{curve_points(state.axis)}; + // the axis is no longer than the lines joining its points, which is good enough to tell the road is nowhere near + if (glm::distance(road->location(), Point) > + 0.5 * (glm::distance(axis[0], axis[1]) + glm::distance(axis[1], axis[2]) + glm::distance(axis[2], axis[3])) + 0.5 * std::max(state.span(0.0), state.span(1.0)) + Reach + 10.0) + continue; + for (auto const *lane : road->tracks()) + { + if (lane == nullptr || lane->m_paths.empty()) + continue; + auto const points{curve_points(lane->m_paths.front())}; + auto const t{nearest_on(points, Point)}; + auto const position{curve_point(points, t)}; + auto const distance{planar_distance(position, Point)}; + // lanes passing over or under the point don't count + if (distance > best || std::abs(position.y - Point.y) > 3.0) + continue; + best = distance; + Position = position; + // the lanes are laid out the way the traffic goes + Direction = curve_point(points, std::min(1.0, t + 0.01)) - curve_point(points, std::max(0.0, t - 0.01)); + found = true; + } + } + return found; +} + +bool editor_road::rails_across(road_node const &Road, glm::dvec3 const &Near, double const Reach, glm::dvec3 &Middle, double &Halfspan) +{ + int const pieces{32}; // lines the axis is taken for + int const railpieces{16}; // and each of the rails + auto const axis{curve_points(Road.definition().axis)}; + std::vector line; + for (int i = 0; i <= pieces; ++i) + line.emplace_back(curve_point(axis, static_cast(i) / pieces)); + // values of the curve parameter of the axis the rails cross it at + auto first{2.0}; + auto last{-1.0}; + for (auto const *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0) + continue; + for (auto const &path : track->m_paths) + { + auto const rail{curve_points(path)}; + // the path is no longer than the lines joining its points, which is good enough to tell it's nowhere near + if (planar_distance(rail[0], Near) > glm::distance(rail[0], rail[1]) + glm::distance(rail[1], rail[2]) + glm::distance(rail[2], rail[3]) + Reach) + continue; + auto previous{rail[0]}; + for (int j = 1; j <= railpieces; ++j) + { + auto const next{curve_point(rail, static_cast(j) / railpieces)}; + for (int i = 0; i < pieces; ++i) + { + double along{0.0}; + double railalong{0.0}; + if (false == lines_cross(line[i], line[i + 1], previous, next, along, railalong)) + continue; + auto const point{glm::mix(line[i], line[i + 1], along)}; + // rails passing over or under the road don't make a level crossing + if (planar_distance(point, Near) > Reach || std::abs(glm::mix(previous.y, next.y, railalong) - point.y) > 3.0) + continue; + auto const t{(i + along) / pieces}; + first = std::min(first, t); + last = std::max(last, t); + } + previous = next; + } + } + } + if (last < first) + return false; + Middle = curve_point(axis, 0.5 * (first + last)); + Halfspan = std::max(planar_distance(curve_point(axis, first), Middle), planar_distance(curve_point(axis, last), Middle)); + return true; +} + +bool editor_road::dissolve(glm::dvec3 const &Joint, record &Record, std::string &Error) +{ + if (false == junctions_at(Joint).empty()) + { + Error = "the point is where a road meets a junction. Delete the junction or the road instead"; + return false; + } + auto const ends{ends_at(Joint)}; + if (ends.empty()) + { + Error = "there's no point of a road there"; + return false; + } + if (ends.size() > 2 || (ends.size() == 2 && ends[0].road == ends[1].road)) + { + Error = "more than two ends of roads meet at the point"; + return false; + } + std::string reason; + for (auto const &end : ends) + { + if (false == can_edit(*end.road, &reason)) + { + Error = "\"" + end.road->name() + "\" can't be changed: " + reason; + return false; + } + } + if (ends.size() == 1) + { + // the point a road ends at: the last piece goes + remove(std::vector{ends.front().road}); + Record.roads_removed.emplace_back(ends.front().road); + return true; + } + // the piece which leads to the point stays, and takes the place of the two + auto kept{ends[0]}; + auto dropped{ends[1]}; + if (false == kept.atend && dropped.atend) + std::swap(kept, dropped); + auto before{kept.road->definition()}; + auto after{dropped.road->definition()}; + if (false == kept.atend) + before = flipped(before); + if (dropped.atend) + after = flipped(after); + if (before.changing() || after.changing()) + { + Error = "the road changes its lanes next to the point. Give that piece the same lanes at both its ends first"; + return false; + } + if (before.forward != after.forward || before.backward != after.backward) + { + Error = "the road has other lanes on each side of the point. Give both sides the same lanes first"; + return false; + } + auto const lengths{std::make_pair(before.length(), after.length())}; + auto const whole{lengths.first + lengths.second}; + if (lengths.first < 0.01 || lengths.second < 0.01 || before.width() <= 0.0) + { + Error = "the pieces at the point are too short"; + return false; + } + auto const start{curve_points(before.axis)}; + auto const end{curve_points(after.axis)}; + auto merged{before}; + merged.axis.points[segment_data::point::end] = end[3]; + merged.axis.rolls[1] = after.axis.rolls[1]; + // the far ends keep the way they go, with their control points moved out to make up for the ones which go with the point. + // a piece cut in two has the control points at the cut as far from it as the parts of the piece each of the two took, + // so going by these brings such a piece back to what it was + auto const reach{std::make_pair(glm::distance(start[2], start[3]), glm::distance(end[0], end[1]))}; + auto const share{std::clamp(reach.first + reach.second > 1e-6 ? reach.first / (reach.first + reach.second) : lengths.first / whole, 0.05, 0.95)}; + auto control1{(start[1] - start[0]) / share}; + auto control2{(end[2] - end[3]) / (1.0 - share)}; + auto const straight{end[3] - start[0]}; + auto const along = [&straight](glm::dvec3 const &Direction) { return glm::length(glm::cross(glm::normalize(Direction), glm::normalize(straight))) < 1e-4; }; + if (glm::length(straight) > 0.01 && along(control1) && along(-control2) && glm::dot(control1, straight) > 0.0 && glm::dot(control2, straight) < 0.0) + { + // two pieces of a straight make a straight + control1 = glm::dvec3{0.0}; + control2 = glm::dvec3{0.0}; + } + merged.axis.points[segment_data::point::control1] = control1; + merged.axis.points[segment_data::point::control2] = control2; + // the road is as wide at its far ends as it was + merged.taper[1] = static_cast(after.taper[1] * after.width() / before.width()); + if (after.divided() || before.divided()) + { + if (false == before.divided()) + { + merged.median = after.median; + merged.median.width[0] = 0.f; + } + merged.median.width[1] = (after.divided() ? after.median.width[1] : 0.f); + } + merged.median.round[1] = after.median.round[1]; + for (std::size_t side = 0; side < merged.banks.size(); ++side) + { + if (merged.banks[side].set && after.banks[side].set) + { + merged.banks[side].width[1] = after.banks[side].width[1]; + merged.banks[side].drop[1] = after.banks[side].drop[1]; + } + } + remember(Record, kept.road); + remove(std::vector{dropped.road}); + Record.roads_removed.emplace_back(dropped.road); + apply(std::vector>{{kept.road, merged}}); + return true; +} + +road_node *editor_road::split(road_node &Road, double const T) +{ + // partway along a piece which gains or loses lanes the road has no whole number of them + if (T <= 0.001 || T >= 0.999 || Road.definition().changing()) + return nullptr; + auto first{Road.definition()}; + auto second{first}; + auto const &path{Road.definition().axis}; + auto const p0{path.points[segment_data::point::start]}; + auto const p3{path.points[segment_data::point::end]}; + if (path.points[segment_data::point::control1] == glm::dvec3{0.0} && path.points[segment_data::point::control2] == glm::dvec3{0.0}) + { + auto const middle{glm::mix(p0, p3, T)}; + first.axis.points[segment_data::point::end] = middle; + second.axis.points[segment_data::point::start] = middle; + } + else + { + auto const p1{p0 + path.points[segment_data::point::control1]}; + auto const p2{p3 + path.points[segment_data::point::control2]}; + auto const p01{glm::mix(p0, p1, T)}; + auto const p12{glm::mix(p1, p2, T)}; + auto const p23{glm::mix(p2, p3, T)}; + auto const p012{glm::mix(p01, p12, T)}; + auto const p123{glm::mix(p12, p23, T)}; + auto const middle{glm::mix(p012, p123, T)}; + first.axis.points[segment_data::point::control1] = p01 - p0; + first.axis.points[segment_data::point::control2] = p012 - middle; + first.axis.points[segment_data::point::end] = middle; + second.axis.points[segment_data::point::start] = middle; + second.axis.points[segment_data::point::control1] = p123 - middle; + second.axis.points[segment_data::point::control2] = p23 - p3; + } + auto const roll{static_cast(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * T)}; + first.axis.rolls[1] = roll; + second.axis.rolls[0] = roll; + // the width at the cut is what it was there + auto const scale{static_cast(Road.definition().scale(T))}; + first.taper[1] = scale; + second.taper[0] = scale; + // and so is what's between its directions, and what leads from it to the ground + if (Road.definition().divided()) + { + auto const gap{static_cast(Road.definition().median_width(T))}; + first.median.width[1] = gap; + second.median.width[0] = gap; + } + first.median.round[1] = 0.f; + second.median.round[0] = 0.f; + for (std::size_t side = 0; side < first.banks.size(); ++side) + { + auto const &bank{Road.definition().banks[side]}; + first.banks[side].width[1] = second.banks[side].width[0] = glm::mix(bank.width[0], bank.width[1], static_cast(T)); + first.banks[side].drop[1] = second.banks[side].drop[0] = glm::mix(bank.drop[0], bank.drop[1], static_cast(T)); + } + apply(std::vector>{{&Road, first}}); + auto const created{create(std::vector{second})}; + return created.empty() ? nullptr : created.front(); +} + +std::vector editor_road::chain(road_node &Road) +{ + std::vector result{&Road}; + // a piece which gains or loses lanes is where a road of one layout ends and one of another begins + if (Road.definition().changing()) + return result; + for (std::size_t idx = 0; idx < result.size(); ++idx) + { + for (auto const atend : {false, true}) + { + // a road goes on only where exactly two pieces meet + auto const next{roads_at(axis_end(result[idx]->definition(), atend), result[idx])}; + if (next.size() == 1 && false == next.front()->definition().changing()) + add_unique(result, next.front()); + } + } + return result; +} + +double editor_road::nearest_parameter(road_node::state const &State, glm::dvec3 const &Point) +{ + return nearest_on(curve_points(State.axis), Point); +} + +road_node *editor_road::nearest(glm::dvec3 const &Point, double const Margin) +{ + road_node *result{nullptr}; + double best{std::numeric_limits::max()}; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + auto const &state{road->definition()}; + auto const reach{0.5 * std::max(state.span(0.0), state.span(1.0)) + std::max(state.sides[0].width, state.sides[1].width) + Margin}; + // the axis is no longer than the lines joining its points, which is good enough to tell the road is nowhere near + auto const points{curve_points(state.axis)}; + if (glm::distance(road->location(), Point) > 0.5 * (glm::distance(points[0], points[1]) + glm::distance(points[1], points[2]) + glm::distance(points[2], points[3])) + reach + 10.0) + continue; + auto const position{curve_point(points, nearest_parameter(state, Point))}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})}; + // roads passing over or under the point don't count + if (distance > reach || std::abs(position.y - Point.y) > 3.0) + continue; + if (distance < best) + { + best = distance; + result = road; + } + } + return result; +} + +junction_node *editor_road::nearest_junction(glm::dvec3 const &Point) +{ + junction_node *result{nullptr}; + double best{std::numeric_limits::max()}; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved) + continue; + auto const &state{junction->definition()}; + // the junction takes the ground up to the ends of the roads it's made for + double reach{0.0}; + for (auto const &arm : state.arms) + reach = std::max(reach, glm::distance(glm::dvec2{arm.position.x, arm.position.z}, glm::dvec2{state.centre.x, state.centre.z})); + auto const distance{glm::distance(glm::dvec2{state.centre.x, state.centre.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > reach || std::abs(state.centre.y - Point.y) > 3.0 || distance > best) + continue; + best = distance; + result = junction; + } + return result; +} + +editor_road::loose_end editor_road::find_end(glm::dvec3 const &Point, double const Radius, bool const Junctions) +{ + loose_end result; + double best{Radius}; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + auto const &state{road->definition()}; + for (auto const atend : {false, true}) + { + auto const position{axis_end(state, atend)}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty() || false == junctions_at(position).empty()) + continue; + best = distance; + result = loose_end{}; + result.road = road; + result.atend = atend; + result.position = position; + result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0)); + } + // a road which keeps its two directions apart can be carried on as two roads, one for each of its roadways + for (auto const atend : {false, true}) + { + auto const points{end_points(state, atend)}; + float lanewidth{0.f}; + if (points.size() < 3 || state.median_width(atend ? 1.0 : 0.0) < 0.5 || false == lane_width(state, atend ? 1.0 : 0.0, lanewidth)) + continue; + // the whole end has to be loose + if (false == roads_at(points[0], road).empty() || false == junctions_at(points[0]).empty()) + continue; + for (int half = 1; half <= 2; ++half) + { + auto const &position{points[half]}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > best || std::abs(position.y - Point.y) > 3.0 || false == roads_at(position, road).empty()) + continue; + best = distance; + result = loose_end{}; + result.road = road; + result.atend = atend; + result.position = position; + result.outwards = (atend ? state.tangent(1.0) : -state.tangent(0.0)); + // the first of the points is the roadway of the lanes going against the axis + result.half = (half == 1 ? -1 : 1); + auto const lanes{state.lanes_at(atend ? 1 : 0, half != 1)}; + // lanes which leave the road there go along the axis of the road carrying them on, the ones which come to it go against that axis + auto const leaving{(half == 2) == atend}; + result.forward = (leaving ? lanes : 0); + result.backward = (leaving ? 0 : lanes); + result.width = lanewidth; + } + } + } + if (false == Junctions) + return result; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved) + continue; + for (auto const &arm : junction->definition().arms) + { + auto const distance{glm::distance(glm::dvec2{arm.position.x, arm.position.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > best || std::abs(arm.position.y - Point.y) > 3.0 || false == roads_at(arm.position, nullptr).empty()) + continue; + best = distance; + result = loose_end{}; + result.junction = junction; + result.position = arm.position; + result.outwards = glm::dvec3{arm.direction.x, 0.0, arm.direction.y}; + // a road leading away from the junction has the lanes going out of it as the ones going along its axis + result.forward = arm.outgoing; + result.backward = arm.incoming; + result.width = arm.width; + } + } + return result; +} + +std::vector editor_road::plan(glm::dvec3 const &Start, glm::dvec2 const *Direction, glm::dvec3 const &Target, glm::dvec2 const *Enddirection, bool const Straight, std::string &Error) +{ + std::vector pieces; + glm::dvec2 const chord{Target.x - Start.x, Target.z - Start.z}; + auto const span{glm::length(chord)}; + if (span < kShortestRoad) + { + Error = "Too short"; + return pieces; + } + // straight road, cut into pieces of reasonable length + auto const straight = [&](glm::dvec3 const &End) { + auto const count{std::max(1, static_cast(std::ceil(glm::distance(Start, End) / kPieceLength)))}; + for (int i = 0; i < count; ++i) + { + segment_data piece; + piece.points[segment_data::point::start] = glm::mix(Start, End, static_cast(i) / count); + piece.points[segment_data::point::end] = glm::mix(Start, End, static_cast(i + 1) / count); + pieces.emplace_back(piece); + } + }; + + if (Direction != nullptr && Enddirection != nullptr) + { + // both ends are tied, which leaves a single curve with the tangents set + if (glm::dot(*Direction, chord) < 0.1 * span || glm::dot(*Enddirection, chord) < 0.1 * span) + { + Error = "The road would have to turn back to meet the end"; + return pieces; + } + auto const rise{(Target.y - Start.y) / 3.0}; + segment_data piece; + piece.points[segment_data::point::start] = Start; + piece.points[segment_data::point::end] = Target; + piece.points[segment_data::point::control1] = glm::dvec3{Direction->x * span / 3.0, rise, Direction->y * span / 3.0}; + piece.points[segment_data::point::control2] = glm::dvec3{-Enddirection->x * span / 3.0, -rise, -Enddirection->y * span / 3.0}; + auto const radius{tightest_radius(piece)}; + if (radius == 0.0) + { + pieces.clear(); + straight(Target); + return pieces; + } + if (radius < kTightestRadius) + { + Error = "The bend is too tight"; + return pieces; + } + piece.radius = static_cast(radius); + pieces.emplace_back(piece); + return pieces; + } + if (Direction == nullptr && Enddirection != nullptr) + { + // only the far end is tied: the same road as the one leading back from there, turned around + glm::dvec2 const back{-*Enddirection}; + auto reversed{plan(Target, &back, Start, nullptr, Straight, Error)}; + for (auto &piece : reversed) + { + std::swap(piece.points[segment_data::point::start], piece.points[segment_data::point::end]); + std::swap(piece.points[segment_data::point::control1], piece.points[segment_data::point::control2]); + } + std::reverse(reversed.begin(), reversed.end()); + return reversed; + } + if (Direction == nullptr) + { + straight(Target); + return pieces; + } + + // the road leaves the start in set direction, and reaches the target with an arc + auto const forward{*Direction}; + glm::dvec2 const left{forward.y, -forward.x}; + auto const along{glm::dot(chord, forward)}; + auto const side{glm::dot(chord, left)}; + if (Straight || std::abs(side) < 0.02 * span) + { + if (along < kShortestRoad) + { + Error = "The target is behind the start"; + return pieces; + } + straight(glm::dvec3{Start.x + forward.x * along, Target.y, Start.z + forward.y * along}); + return pieces; + } + auto const turn{2.0 * std::atan2(side, along)}; // positive to the left + auto const radius{span * span / (2.0 * side)}; // likewise + if (std::abs(turn) > glm::radians(170.0)) + { + Error = "The road would have to turn back to reach the target"; + return pieces; + } + if (std::abs(radius) < kTightestRadius) + { + Error = "The bend is too tight"; + return pieces; + } + glm::dvec2 const start{Start.x, Start.z}; + auto const centre{start + left * radius}; + auto const count{std::max(1, static_cast(std::ceil(std::max(std::abs(turn) / kPieceTurn, std::abs(radius * turn) / kPieceLength))))}; + auto const handle{4.0 / 3.0 * std::tan(std::abs(turn) / count / 4.0) * std::abs(radius)}; + auto const rise{(Target.y - Start.y) / count}; + for (int i = 0; i < count; ++i) + { + auto const angle0{turn * i / count}; + auto const angle1{turn * (i + 1) / count}; + auto const point0{centre - left * (radius * std::cos(angle0)) + forward * (radius * std::sin(angle0))}; + auto const point1{centre - left * (radius * std::cos(angle1)) + forward * (radius * std::sin(angle1))}; + auto const tangent0{forward * std::cos(angle0) + left * std::sin(angle0)}; + auto const tangent1{forward * std::cos(angle1) + left * std::sin(angle1)}; + segment_data piece; + piece.points[segment_data::point::start] = glm::dvec3{point0.x, Start.y + rise * i, point0.y}; + piece.points[segment_data::point::end] = glm::dvec3{point1.x, Start.y + rise * (i + 1), point1.y}; + piece.points[segment_data::point::control1] = glm::dvec3{tangent0.x * handle, rise / 3.0, tangent0.y * handle}; + piece.points[segment_data::point::control2] = glm::dvec3{-tangent1.x * handle, -rise / 3.0, -tangent1.y * handle}; + piece.radius = static_cast(std::abs(radius)); + pieces.emplace_back(piece); + } + // the arc is worked out from the start, make sure it ends exactly at the target + pieces.back().points[segment_data::point::end] = Target; + return pieces; +} + +double editor_road::planar_length(segment_data const &Path) +{ + auto const whole{subpath(Path, 0.0, 1.0)}; + auto const &p0{whole.points[segment_data::point::start]}; + auto const &p3{whole.points[segment_data::point::end]}; + if (whole.points[segment_data::point::control1] == glm::dvec3{0.0} && whole.points[segment_data::point::control2] == glm::dvec3{0.0}) + return glm::distance(glm::dvec2{p0.x, p0.z}, glm::dvec2{p3.x, p3.z}); + auto const p1{p0 + whole.points[segment_data::point::control1]}; + auto const p2{p3 + whole.points[segment_data::point::control2]}; + double length{0.0}; + glm::dvec2 previous{p0.x, p0.z}; + for (int i = 1; i <= 32; ++i) + { + auto const t{i / 32.0}; + auto const u{1.0 - t}; + auto const point{u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3}; + glm::dvec2 const current{point.x, point.z}; + length += glm::distance(previous, current); + previous = current; + } + return length; +} + +double editor_road::end_grade(segment_data const &Path) +{ + auto const &control{Path.points[segment_data::point::control2]}; + if (control != glm::dvec3{0.0}) + { + auto const run{glm::length(glm::dvec2{control.x, control.z})}; + return run > 1e-6 ? -control.y / run : 0.0; + } + auto const length{planar_length(Path)}; + return length > 1e-6 ? (Path.points[segment_data::point::end].y - Path.points[segment_data::point::start].y) / length : 0.0; +} + +std::vector editor_road::refine(std::vector const &Pieces, double const Length) +{ + std::vector result; + for (auto const &piece : Pieces) + { + auto const count{std::max(1, static_cast(std::ceil(planar_length(piece) / std::max(1.0, Length) - 0.01)))}; + for (int i = 0; i < count; ++i) + result.emplace_back(count == 1 ? piece : subpath(piece, static_cast(i) / count, static_cast(i + 1) / count)); + } + return result; +} + +void editor_road::profile(std::vector &Pieces, std::vector Heights, double const *Startgrade, double const *Endgrade) +{ + auto const count{Pieces.size()}; + if (count == 0 || Heights.size() != count + 1) + return; + std::vector lengths(count); + for (std::size_t i = 0; i < count; ++i) + lengths[i] = std::max(0.01, planar_length(Pieces[i])); + // the heights in between are evened out, so the road doesn't ride every dip. it's never lowered though, + // as that would let the ground the heights were taken from come through the surface + { + auto const sampled{Heights}; + for (std::size_t i = 1; i < count; ++i) + Heights[i] = std::max(sampled[i], 0.25 * sampled[i - 1] + 0.5 * sampled[i] + 0.25 * sampled[i + 1]); + } + std::vector slopes(count); + for (std::size_t i = 0; i < count; ++i) + slopes[i] = (Heights[i + 1] - Heights[i]) / lengths[i]; + // slope at each joint is shared by the pieces on both its sides, which is what rounds the profile + std::vector grades(count + 1); + grades.front() = (Startgrade != nullptr ? *Startgrade : slopes.front()); + grades.back() = (Endgrade != nullptr ? *Endgrade : slopes.back()); + for (std::size_t i = 1; i < count; ++i) + { + auto const before{slopes[i - 1]}; + auto const after{slopes[i]}; + if (before * after <= 0.0) + { + // a crest or a bottom + grades[i] = 0.0; + continue; + } + auto grade{(before * lengths[i] + after * lengths[i - 1]) / (lengths[i - 1] + lengths[i])}; + // kept from overshooting the heights the road is led through + auto const limit{3.0 * std::min(std::abs(before), std::abs(after))}; + grades[i] = std::clamp(grade, -limit, limit); + } + for (std::size_t i = 0; i < count; ++i) + { + auto &piece{Pieces[i]}; + auto &start{piece.points[segment_data::point::start]}; + auto &end{piece.points[segment_data::point::end]}; + auto &control1{piece.points[segment_data::point::control1]}; + auto &control2{piece.points[segment_data::point::control2]}; + start.y = Heights[i]; + end.y = Heights[i + 1]; + if (control1 == glm::dvec3{0.0} && control2 == glm::dvec3{0.0}) + { + if (std::abs(grades[i] - slopes[i]) < 1e-4 && std::abs(grades[i + 1] - slopes[i]) < 1e-4) + continue; // even slope, the piece stays a plain straight + // a straight needs its control points spelled out to bend up or down + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + control1.y = grades[i] * glm::length(glm::dvec2{control1.x, control1.z}); + control2.y = -grades[i + 1] * glm::length(glm::dvec2{control2.x, control2.z}); + } +} + +bool editor_road::nearest_joint(glm::dvec3 const &Point, double const Radius, glm::dvec3 &Joint) +{ + auto best{Radius}; + bool found{false}; + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + for (auto const atend : {false, true}) + { + auto const position{axis_end(road->definition(), atend)}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z})}; + if (distance > best || std::abs(position.y - Point.y) > 3.0) + continue; + best = distance; + Joint = position; + found = true; + } + } + return found; +} + +std::vector editor_road::ends_at(glm::dvec3 const &Point) +{ + std::vector result; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + continue; + for (auto const atend : {false, true}) + if (glm::distance(axis_end(road->definition(), atend), Point) < kSamePoint) + result.push_back({road, atend}); + } + return result; +} + +std::vector> editor_road::arms_at(glm::dvec3 const &Point) +{ + std::vector> result; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->m_editorremoved) + continue; + auto const &arms{junction->definition().arms}; + for (std::size_t arm = 0; arm < arms.size(); ++arm) + if (glm::distance(arms[arm].position, Point) < kSamePoint) + result.emplace_back(junction, arm); + } + return result; +} + +void editor_road::settle(road_node &Road, record &Record, std::string &Notes) +{ + for (auto const atend : {false, true}) + { + if (Road.m_editorremoved) + return; + auto const &layout{Road.definition()}; + auto const position{axis_end(layout, atend)}; + // lanes heading for this end of the piece, and the ones leading away from it + auto const arriving{layout.lanes_at(atend ? 1 : 0, atend)}; + auto const leaving{layout.lanes_at(atend ? 1 : 0, false == atend)}; + float lanewidth{0.f}; + auto const evenlanes{lane_width(layout, atend ? 1.0 : 0.0, lanewidth)}; + auto const gap{static_cast(layout.median_width(atend ? 1.0 : 0.0))}; + char const *unevenlanes{" differ in width, and a junction takes roads with lanes of the same width. "}; + std::string reason; + + auto const junctions{arms_at(position)}; + if (false == junctions.empty()) + { + // a junction is made for the lanes of the road at each arm + for (auto const &entry : junctions) + { + auto *junction{entry.first}; + auto state{junction->definition()}; + auto &arm{state.arms[entry.second]}; + if (arm.incoming == arriving && arm.outgoing == leaving && std::abs(arm.width - lanewidth) < 0.001f && std::abs(arm.median - gap) < 0.001f) + continue; + if (false == evenlanes) + { + Notes += "Lanes of \"" + Road.name() + "\"" + unevenlanes; + continue; + } + if (false == can_edit(*junction, &reason)) + { + Notes += "\"" + junction->name() + "\" wasn't fitted: " + reason + ". "; + continue; + } + arm.incoming = arriving; + arm.outgoing = leaving; + arm.width = lanewidth; + arm.median = gap; + remember(Record, junction); + apply(std::vector>{{junction, state}}); + } + continue; + } + + auto const neighbours{roads_at(position, &Road)}; + if (neighbours.size() != 1) + continue; + auto *other{neighbours.front()}; + auto const otherlayout{other->definition()}; + auto const otheratend{glm::distance(axis_end(otherlayout, true), position) < kSamePoint}; + // lanes of the neighbour heading for the place they meet, and the ones leading away from it + auto const otherarriving{otherlayout.lanes_at(otheratend ? 1 : 0, otheratend)}; + auto const otherleaving{otherlayout.lanes_at(otheratend ? 1 : 0, false == otheratend)}; + auto const othergap{static_cast(otherlayout.median_width(otheratend ? 1.0 : 0.0))}; + if (otherarriving == leaving && otherleaving == arriving) + { + // the same lanes: the neighbour is made as wide as the piece where they meet, and gets to that width along its length + auto const width{layout.width(atend ? 1 : 0) * layout.taper[atend ? 1 : 0]}; + auto const otherwidth{otherlayout.width(otheratend ? 1 : 0)}; + if (std::abs(width - otherwidth * otherlayout.taper[otheratend ? 1 : 0]) < 0.001 && std::abs(gap - othergap) < 0.001f) + continue; + if (false == can_edit(*other, &reason)) + { + Notes += "\"" + other->name() + "\" wasn't fitted: " + reason + ". "; + continue; + } + auto state{otherlayout}; + state.taper[otheratend ? 1 : 0] = std::clamp(static_cast(width / otherwidth), 0.25f, 4.f); + // and it leads its two directions as far apart as the piece has them there + if (std::abs(gap - othergap) >= 0.001f) + { + if (false == otherlayout.divided()) + { + state.median.type = layout.median.type; + state.median.material = layout.median.material; + state.median.width = {0.f, 0.f}; + } + state.median.width[otheratend ? 1 : 0] = gap; + } + remember(Record, other); + apply(std::vector>{{other, state}}); + continue; + } + + // other lanes: the neighbour is led from the lanes it has to the lanes of the piece. a long one gives up its end for that, + // which is made a piece of its own; a short one, or one which changes its lanes already, does it along its whole length + float otherlanewidth{0.f}; + if (false == evenlanes || false == lane_width(otherlayout, otheratend ? 1.0 : 0.0, otherlanewidth)) + { + Notes += "Lanes of \"" + (evenlanes ? other->name() : Road.name()) + "\" differ in width, and a road changes the number of its lanes only if they're all as wide. "; + continue; + } + if (false == can_edit(*other, &reason)) + { + Notes += "The lanes next to \"" + Road.name() + "\" weren't fitted: " + reason + ". "; + continue; + } + auto *stretch{other}; + if (false == otherlayout.changing() && otherlayout.length() > 1.5 * kTransitionLength) + { + auto const lengths{stations(otherlayout)}; + auto *rest{split(*other, parameter_at(lengths, otheratend ? lengths.back() - kTransitionLength : kTransitionLength))}; + if (rest == nullptr) + { + Notes += "\"" + other->name() + "\" couldn't be cut for the lanes to change. "; + continue; + } + // the neighbour goes on record the way it was before the cut + if (std::none_of(Record.roads.begin(), Record.roads.end(), [other](std::pair const &Entry) { return Entry.first == other; })) + Record.roads.emplace_back(other, otherlayout); + Record.roads_created.emplace_back(rest); + // the part past the cut is the one at the end of the neighbour + if (otheratend) + stretch = rest; + } + auto state{stretch->definition()}; + // the end the stretch shares with the piece gets the lanes of the piece, the other one keeps what it has + auto const end{otheratend ? 1 : 0}; + int counts[2][2]{{state.lanes_at(0, true), state.lanes_at(0, false)}, {state.lanes_at(1, true), state.lanes_at(1, false)}}; + // the lanes of the piece leading away from where they meet are the ones of the stretch heading for it + counts[end][0] = (otheratend ? leaving : arriving); + counts[end][1] = (otheratend ? arriving : leaving); + if (false == state.lanes.empty()) + state.lanewidth = state.lanes.front().width; + state.lanes_between(counts[0][0], counts[0][1], counts[1][0], counts[1][1]); + if (state.lanes_at(end, true) != counts[end][0] || state.lanes_at(end, false) != counts[end][1]) + { + Notes += "The lanes of \"" + Road.name() + "\" can't be led on to the road next to it. "; + continue; + } + // and it's as wide there as the piece is, with its directions as far apart + state.taper[end] = std::clamp(lanewidth / state.lanes.front().width, 0.25f, 4.f); + if (std::abs(gap - othergap) >= 0.001f) + { + if (false == state.divided()) + { + state.median.type = layout.median.type; + state.median.material = layout.median.material; + state.median.width = {0.f, 0.f}; + } + state.median.width[end] = gap; + } + if (stretch == other) + remember(Record, other); + apply(std::vector>{{stretch, state}}); + } +} + +bool editor_road::lane_width(road_node::state const &State, double const T, float &Width) +{ + if (State.lanes.empty()) + return false; + for (auto const &lane : State.lanes) + if (std::abs(lane.width - State.lanes.front().width) > 0.005f) + return false; + Width = static_cast(State.lanes.front().width * State.scale(T)); + return true; +} + +bool editor_road::branch_from(road_node &Road, double const T, glm::dvec2 const &Direction, int const Incoming, int const Outgoing, float const Width, branch &Result, std::string &Error) +{ + Result = branch{}; + auto const &layout{Road.definition()}; + auto const centre{layout.point(T)}; + auto const along{planar(layout.tangent(T))}; + auto const direction{glm::length(Direction) > 1e-9 ? glm::normalize(Direction) : glm::dvec2{0.0, 1.0}}; + // the sharper the angle the road leaves at, the further from the centre the edges of the roads part + auto const angle{std::acos(std::clamp(std::abs(glm::dot(direction, along)), 0.0, 1.0))}; + if (angle < kSharpestBranch) + { + Error = "The road would leave at too sharp an angle"; + return false; + } + if (layout.changing()) + { + Error = "\"" + Road.name() + "\" changes its lanes along the way, which leaves no place for a junction. Lead the road out of a piece next to it"; + return false; + } + float lanewidth{0.f}; + if (false == lane_width(layout, T, lanewidth)) + { + Error = "Lanes of \"" + Road.name() + "\" differ in width, and a junction takes roads with lanes of the same width"; + return false; + } + auto const halfwidth{0.5 * (layout.forward + layout.backward) * lanewidth + 0.5 * layout.median_width(T)}; + auto const branchhalfwidth{0.5 * (Incoming + Outgoing) * Width}; + auto const reach{edge_reach(halfwidth, branchhalfwidth, angle) + kCornerRoom}; + auto const branchreach{edge_reach(branchhalfwidth, halfwidth, angle) + kCornerRoom}; + + // where the road is cut on both sides of the centre + road_cut cuts[2]; + std::vector consumed; + if (false == follow(Road, T, false, reach, cuts[0], consumed, Error) || false == follow(Road, T, true, reach, cuts[1], consumed, Error)) + return false; + if (cuts[0].road == cuts[1].road && cuts[0].road != &Road) + { + Error = "The road is too short for a junction"; + return false; + } + // what's left of the pieces + if (cuts[0].road == &Road && cuts[1].road == &Road) + { + Result.changes.emplace_back(&Road, part(layout, 0.0, cuts[0].t)); + Result.added.emplace_back(part(layout, cuts[1].t, 1.0)); + } + else if (cuts[0].road != &Road && cuts[1].road != &Road) + { + Result.removed.emplace_back(&Road); + } + for (auto const &cut : cuts) + { + if (cut.road == &Road && cuts[0].road == cuts[1].road) + continue; + // a cut at the very end the piece is kept from leaves it whole + if (cut.keepstart ? cut.t < 0.999 : cut.t > 0.001) + Result.changes.emplace_back(cut.road, cut.keepstart ? part(cut.road->definition(), 0.0, cut.t) : part(cut.road->definition(), cut.t, 1.0)); + } + for (auto *piece : consumed) + Result.removed.emplace_back(piece); + std::string reason; + for (auto const &change : Result.changes) + { + if (false == can_edit(*change.first, &reason)) + { + Error = "\"" + change.first->name() + "\" can't be changed: " + reason; + return false; + } + } + for (auto const *piece : Result.removed) + { + if (false == can_edit(*piece, &reason)) + { + Error = "\"" + piece->name() + "\" can't be changed: " + reason; + return false; + } + } + + // the junction: the road on both its sides, and the one leaving + auto &junction{Result.layout}; + junction.centre = centre; + for (auto const &cut : cuts) + { + junction_node::arm_data arm; + if (false == cut_arm(cut, arm)) + { + Error = "Lanes of \"" + cut.road->name() + "\" differ in width, and a junction takes roads with lanes of the same width"; + return false; + } + junction.arms.emplace_back(arm); + } + junction_node::arm_data arm; + arm.position = centre + glm::dvec3{direction.x, 0.0, direction.y} * branchreach; + arm.direction = direction; + arm.incoming = Incoming; + arm.outgoing = Outgoing; + arm.width = Width; + junction.arms.emplace_back(arm); + Result.arm = junction.arms.size() - 1; + dress(junction, layout); + if (layout.roundabout) + { + // the traffic going around goes ahead of the traffic joining it + for (auto &entry : junction.arms) + entry.priority = junction_node::right_of_way::priority; + junction.arms.back().priority = junction_node::right_of_way::yield; + } + junction.normalize(); + return true; +} + +bool editor_road::branch_from(junction_node &Junction, glm::dvec2 const &Direction, int const Incoming, int const Outgoing, float const Width, branch &Result, std::string &Error) +{ + Result = branch{}; + auto const &layout{Junction.definition()}; + if (layout.arms.size() >= 4) + { + Error = "The junction has four roads already"; + return false; + } + std::string reason; + if (false == can_edit(Junction, &reason)) + { + Error = "\"" + Junction.name() + "\" can't be changed: " + reason; + return false; + } + auto const direction{glm::length(Direction) > 1e-9 ? glm::normalize(Direction) : glm::dvec2{0.0, 1.0}}; + auto const halfwidth{0.5 * (Incoming + Outgoing) * Width}; + // the roads which are there stay where they are, so the new one has to fit between them + auto reach{kCornerRoom}; + for (std::size_t idx = 0; idx < layout.arms.size(); ++idx) + { + auto const &other{layout.arms[idx]}; + auto const angle{std::acos(std::clamp(glm::dot(direction, other.direction), -1.0, 1.0))}; + if (angle < kSharpestBranch) + { + Error = "The road would leave too close to another road of the junction"; + return false; + } + if (angle > kWidestCorner) + continue; + auto const otherhalfwidth{0.5 * layout.arm_width(idx)}; + auto const otherreach{glm::distance(glm::dvec2{other.position.x, other.position.z}, glm::dvec2{layout.centre.x, layout.centre.z})}; + if (edge_reach(otherhalfwidth, halfwidth, angle) > otherreach - 0.5) + { + Error = "There's no room for the road between the roads the junction has; lead it out at a wider angle to them"; + return false; + } + reach = std::max(reach, edge_reach(halfwidth, otherhalfwidth, angle) + kCornerRoom); + } + Result.junction = &Junction; + Result.layout = layout; + junction_node::arm_data arm; + arm.position = layout.centre + glm::dvec3{direction.x, 0.0, direction.y} * reach; + arm.direction = direction; + arm.incoming = Incoming; + arm.outgoing = Outgoing; + arm.width = Width; + Result.layout.arms.emplace_back(arm); + Result.arm = Result.layout.arms.size() - 1; + Result.layout.normalize(); + return true; +} + +junction_node *editor_road::carry_out(branch const &Branch, record &Record) +{ + if (Branch.junction != nullptr) + { + remember(Record, Branch.junction); + apply(std::vector>{{Branch.junction, Branch.layout}}); + return Branch.junction; + } + // room is made first, so nothing gets joined with what's about to go + remove(Branch.removed); + Record.roads_removed.insert(Record.roads_removed.end(), Branch.removed.begin(), Branch.removed.end()); + for (auto const &change : Branch.changes) + remember(Record, change.first); + apply(Branch.changes); + auto const added{create(Branch.added)}; + Record.roads_created.insert(Record.roads_created.end(), added.begin(), added.end()); + auto const junctions{create(std::vector{Branch.layout})}; + if (junctions.empty()) + return nullptr; + Record.junctions_created.emplace_back(junctions.front()); + return junctions.front(); +} + +std::vector editor_road::roundabout(glm::dvec3 const &Centre, double const Radius, road_node::state const &Layout, std::string const &Island) +{ + std::vector pieces; + auto layout{Layout}; + // a single direction, going around with the middle on its left + if (layout.forward < 1) + layout.forward = std::max(1, layout.backward); + layout.backward = 0; + layout.missing = {}; + layout.lanes.clear(); + layout.changes.clear(); + layout.taper = {1.f, 1.f}; + layout.median.width = {0.f, 0.f}; + layout.roundabout = true; + layout.normalize(); + auto const halfwidth{0.5 * layout.width()}; + if (Radius - halfwidth < 1.0) + return pieces; + if (false == Island.empty() && Island != "none") + { + // the island takes all that's inside the road + layout.sides[0].type = road_node::side_type::sidewalk; + layout.sides[0].width = static_cast(Radius - halfwidth); + layout.sides[0].material = Island; + } + else + { + layout.sides[0] = road_node::side_data{}; + } + layout.banks[0] = road_node::bank_data{}; + // arcs short enough for a curve of this kind to pass for a circle + int const count{Radius > 40.0 ? 8 : 4}; + auto const angle{6.283185307179586 / count}; + auto const reach{4.0 / 3.0 * std::tan(0.25 * angle) * Radius}; + auto const point = [&](double const Angle) { return Centre + glm::dvec3{std::cos(Angle), 0.0, std::sin(Angle)} * Radius; }; + // the way the road goes at a point, with the angles going down + auto const heading = [](double const Angle) { return glm::dvec3{std::sin(Angle), 0.0, -std::cos(Angle)}; }; + for (int index = 0; index < count; ++index) + { + auto const from{-angle * index}; + auto const to{-angle * (index + 1)}; + auto state{layout}; + state.axis = segment_data{}; + state.axis.points[segment_data::point::start] = point(from); + state.axis.points[segment_data::point::control1] = heading(from) * reach; + state.axis.points[segment_data::point::control2] = -heading(to) * reach; + state.axis.points[segment_data::point::end] = point(to); + state.axis.radius = static_cast(Radius); + pieces.emplace_back(state); + } + // the last piece ends exactly where the first one starts + pieces.back().axis.points[segment_data::point::end] = pieces.front().axis.points[segment_data::point::start]; + return pieces; +} + +std::vector const &editor_road::materials() +{ + static std::vector names; + static bool scanned{false}; + if (scanned) + return names; + scanned = true; + std::set found; + std::error_code error; + for (std::filesystem::directory_iterator file{Global.asCurrentTexturePath, std::filesystem::directory_options::skip_permission_denied, error}, end; file != end; file.increment(error)) + { + if (error || false == file->is_regular_file(error)) + continue; + auto extension{file->path().extension().string()}; + std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (extension != ".mat" && extension != ".dds" && extension != ".tga" && extension != ".png" && extension != ".bmp" && extension != ".jpg" && extension != ".ktx" && extension != ".tex") + continue; + auto name{file->path().stem().string()}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + // names with spaces can't be put in a scenery file + if (name.empty() || name.find(' ') != std::string::npos) + continue; + found.emplace(name); + } + names.assign(found.begin(), found.end()); + return names; +} diff --git a/editor/editorRoad.hpp b/editor/editorRoad.hpp new file mode 100644 index 00000000..20553b01 --- /dev/null +++ b/editor/editorRoad.hpp @@ -0,0 +1,182 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "world/Road.h" +#include "world/RoadPoint.h" + +// editor operations on the roads. a road in the scenery is a string of pieces joined end to end, +// each piece being a road node with lanes and geometry of its own; roads meet at junctions +class editor_road +{ + public: + // an end of a road piece, or an arm of a junction, nothing is attached to + struct loose_end + { + road_node *road{nullptr}; + junction_node *junction{nullptr}; + bool atend{false}; // of a road: the end of its axis rather than the start + glm::dvec3 position{0.0}; + glm::dvec3 outwards{0.0}; // direction a road would continue in + // of a road which keeps its two directions apart: the roadway of one of them, 1 for the lanes going along the axis, -1 for the ones going against it. + // 0: the end of the whole road + int half{0}; + // of a junction arm, or of a single roadway: lanes of a road leading away from it + int forward{0}; + int backward{0}; + float width{0.f}; + + bool valid() const + { + return road != nullptr || junction != nullptr; + } + }; + // an end of a road piece + struct piece_end + { + road_node *road{nullptr}; + bool atend{false}; // the end of its axis rather than the start + }; + // what an operation changed, in the form needed to take it back + struct record + { + std::vector> roads; // pieces with what they were like before + std::vector roads_created; + std::vector roads_removed; + std::vector> junctions; // junctions with what they were like before + std::vector junctions_created; + std::vector junctions_removed; + std::vector> points; // crossings and traffic points with what they were like before + std::vector points_created; + std::vector points_removed; + + bool empty() const + { + return roads.empty() && roads_created.empty() && roads_removed.empty() && junctions.empty() && junctions_created.empty() && junctions_removed.empty() && points.empty() && + points_created.empty() && points_removed.empty(); + } + }; + // a road led out of the side of another road, or out of a junction between its arms: the junction this takes, + // and what has to be done with the roads to make room for it + struct branch + { + junction_node *junction{nullptr}; // the junction which gets another arm, nullptr if a new one is made + junction_node::state layout; // the junction as it's going to be + std::size_t arm{0}; // the arm of it made for the road + std::vector> changes; // pieces cut short, with their new definitions + std::vector added; // part of a piece left past the junction, to be made a piece of its own + std::vector removed; // pieces which lie within the junction + }; + // true if the roads can be built and changed at all. optionally explains why they can't + static bool available(std::string *Reason = nullptr); + // true if specified road piece or junction can be changed or removed. optionally explains why it can't + static bool can_edit(road_node const &Road, std::string *Reason = nullptr); + static bool can_edit(junction_node const &Junction, std::string *Reason = nullptr); + // true if a road or a junction can be attached to specified loose end. optionally explains why it can't + static bool can_join(loose_end const &End, std::string *Reason = nullptr); + // makes road pieces out of provided definitions, with their lanes joined to the neighbours and geometry in the scene + static std::vector create(std::vector const &States); + // replaces definitions of the pieces and makes their lanes and geometry anew + static void apply(std::vector> const &Changes); + // replaces definitions of the junctions, and makes their geometry and the ways through them anew + static void apply(std::vector> const &Changes); + // takes the pieces out of the scenery. they're kept around, so they can be brought back + static void remove(std::vector const &Roads); + static void revive(std::vector const &Roads); + // the same for the junctions + static std::vector create(std::vector const &States); + static void remove(std::vector const &Junctions); + static void revive(std::vector const &Junctions); + // level crossings, and the points the vehicles appear at or are taken away at. these go by the lanes and the rails they find + // around them, so unlike the roads they can be changed with vehicles around + static bool can_edit(roadpoint_node const &Point, std::string *Reason = nullptr); + static std::vector create(std::vector const &States); + static void apply(std::vector> const &Changes); + static void remove(std::vector const &Points); + static void revive(std::vector const &Points); + // the crossing or traffic point nearest to specified point, within specified distance from it + static roadpoint_node *nearest_point(glm::dvec3 const &Point, double const Radius); + // place on the middle of a lane nearest to specified point, within specified distance from it. + // Direction: receives the way the traffic goes there. returns: false if there's no lane that close + static bool nearest_lane(glm::dvec3 const &Point, double const Reach, glm::dvec3 &Position, glm::dvec3 &Direction); + // rails crossing specified piece within specified distance from a point. Middle: receives the point of the axis halfway between + // the outermost of them, Halfspan: how far along the road these are from it. returns: false if no rails cross the piece there + static bool rails_across(road_node const &Road, glm::dvec3 const &Near, double const Reach, glm::dvec3 &Middle, double &Halfspan); + // takes a point out of a road. the two pieces which meet at it are made one, led from the far end of one to the far end + // of the other the way these ends go; a point a road ends at takes the last piece of the road with it. + // what gets done is added to Record. returns: false if it can't be done, with the reason in Error + static bool dissolve(glm::dvec3 const &Joint, record &Record, std::string &Error); + // cuts the piece in two at specified point of its axis. returns: the new piece, holding the part past the cut + static road_node *split(road_node &Road, double const T); + // the pieces which make a single road together with specified one, in no particular order + static std::vector chain(road_node &Road); + // the piece under specified point, or within specified distance from it + static road_node *nearest(glm::dvec3 const &Point, double const Margin); + // the junction specified point lies on + static junction_node *nearest_junction(glm::dvec3 const &Point); + // the point where pieces meet or end which is nearest to specified one, within specified distance from it. returns: false if there's none + static bool nearest_joint(glm::dvec3 const &Point, double const Radius, glm::dvec3 &Joint); + // ends of the pieces which meet at specified point + static std::vector ends_at(glm::dvec3 const &Point); + // junctions with an arm at specified point, each with the number of that arm + static std::vector> arms_at(glm::dvec3 const &Point); + // brings what's next to the ends of a piece in line with the piece, after its lanes or its width were changed: + // a junction gets its arm made for the lanes the piece has now, a neighbouring piece with the same lanes is made as wide + // as the piece where they meet, and one with other lanes is led to the lanes of the piece: its end, or the whole of it if it's short, + // becomes a piece which gains or loses lanes along the way. what gets done is added to Record, what couldn't be done to Notes + static void settle(road_node &Road, record &Record, std::string &Notes); + // width shared by all lanes of the road at specified value of the curve parameter of its axis. returns: false if the lanes differ in width + static bool lane_width(road_node::state const &State, double const T, float &Width); + // lays out a junction for a road leaving the side of specified piece at specified value of the curve parameter of its axis. + // Direction: the way the road leaves; Incoming, Outgoing: lanes of that road leading into the junction and out of it. + // nothing is changed until the result is carried out. returns: false if it can't be done, with the reason in Error + static bool branch_from(road_node &Road, double const T, glm::dvec2 const &Direction, int const Incoming, int const Outgoing, float const Width, branch &Result, std::string &Error); + // the same for a road leaving a junction between its arms, which gives the junction another arm + static bool branch_from(junction_node &Junction, glm::dvec2 const &Direction, int const Incoming, int const Outgoing, float const Width, branch &Result, std::string &Error); + // makes the junction laid out for a branch, or gives the existing one its new arm, and cuts the roads to fit. what it does is added to Record. + // returns: the junction + static junction_node *carry_out(branch const &Branch, record &Record); + // value of the curve parameter for the point of the axis nearest to specified point + static double nearest_parameter(road_node::state const &State, glm::dvec3 const &Point); + // end of a road with nothing attached, within specified distance from specified point; optionally arms of junctions too + static loose_end find_end(glm::dvec3 const &Point, double const Radius, bool const Junctions = true); + // lays out axes of the pieces for a road leading from a point to another. the directions are optional requirements + // for the road at its ends; without them it starts straight at the target, with them it bends to meet them. + // returns: the axes in order, or nothing if there's no reasonable road to make, with the reason in Error + static std::vector plan(glm::dvec3 const &Start, glm::dvec2 const *Direction, glm::dvec3 const &Target, glm::dvec2 const *Enddirection, bool const Straight, std::string &Error); + // cuts the pieces so none is longer than specified + static std::vector refine(std::vector const &Pieces, double const Length); + // gives a string of pieces a smooth profile running through provided heights. + // Heights: one for the start of each piece and one for the end of the last. these of the ends are kept, the others are evened out: + // the dips are filled while the tops stay, so the ground the heights were taken from doesn't come through the road. + // Startgrade, Endgrade: slopes to keep at the ends, to carry on what the road joins; nullptr for none + static void profile(std::vector &Pieces, std::vector Heights, double const *Startgrade, double const *Endgrade); + // length of the path seen from above + static double planar_length(segment_data const &Path); + // slope of the path at its end, in the direction it's laid out + static double end_grade(segment_data const &Path); + // lays out the pieces of a roundabout: a one-way road going around specified point, to the left of the traffic. + // Radius: of the axis of that road; Layout: what the road is like. the lanes it has along its axis are the ones the roundabout gets. + // Island: what the middle is covered with, "none" to leave the ground showing + static std::vector roundabout(glm::dvec3 const &Centre, double const Radius, road_node::state const &Layout, std::string const &Island); + // names of materials and images present in the texture folder, to pick from + static std::vector const &materials(); + + private: + // takes down what depends on the listed pieces and junctions, lets the caller change them, then builds it all again + template + static void refresh(std::vector const &Roads, std::vector const &Junctions, Change_ Change); + static std::vector neighbours(road_node const &Road); +}; diff --git a/editor/editorSettings.cpp b/editor/editorSettings.cpp index 86214d6b..4b242132 100644 --- a/editor/editorSettings.cpp +++ b/editor/editorSettings.cpp @@ -12,9 +12,13 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "utilities/utilities.h" +#include #include #include #include +#include +#include +#include editorSettings EditorSettings; @@ -32,6 +36,28 @@ const char *scheme_to_string(editorSettings::movement_scheme scheme) { return scheme == editorSettings::movement_scheme::legacy ? "legacy" : "wsad"; } + +// numbers are stored locale-independent +template void parse(std::string const &Text, Type_ &Out) +{ + std::istringstream stream(Text); + stream.imbue(std::locale::classic()); + Type_ value; + if (stream >> value) + Out = value; +} + +const std::string origin_prefix{"ortho_origin@"}; + +// settings are whitespace separated key/value pairs, so the scenery name used in the key can't contain spaces +std::string origin_key(std::string const &Scenery) +{ + std::string key = origin_prefix + Scenery; + for (char &c : key) + if (std::isspace(static_cast(c))) + c = '_'; + return key; +} } bool editorSettings::load() @@ -50,6 +76,43 @@ bool editorSettings::load() { if (key == "movement_scheme") m_movement = (value == "legacy") ? movement_scheme::legacy : movement_scheme::wsad; + else if (key == "ortho_radius") + parse(value, m_orthophoto.radius); + else if (key == "ortho_height") + parse(value, m_orthophoto.height); + else if (key == "ortho_opacity") + parse(value, m_orthophoto.opacity); + else if (key == "ortho_year") + parse(value, m_orthophoto.year); + else if (key == "ortho_4k") + m_orthophoto.hires = (value == "1"); + else if (key == "ortho_drape") + m_orthophoto.drape = (value == "1"); + else if (key == "ortho_lift") + parse(value, m_orthophoto.lift); + else if (key == "ortho_in_scene") + m_orthophoto.in_scene = (value == "1"); + else if (key == "platform_edge") + parse(value, m_platform_edge); + else if (key == "signal_offset") + parse(value, m_signal_offset); + else if (key == "nodebank_previews") + m_nodebank_previews = (value == "1"); + else if (key == "nodebank_card") + parse(value, m_nodebank_card); + else if (key == "colours") + parse(value, m_palette); + else if (key.rfind(origin_prefix, 0) == 0) + { + // northing;easting + auto const separator = value.find(';'); + if (separator == std::string::npos) + continue; + double north{0.0}, east{0.0}; + parse(value.substr(0, separator), north); + parse(value.substr(separator + 1), east); + m_orthophoto_origins[key] = {north, east}; + } } return true; } @@ -67,6 +130,38 @@ bool editorSettings::save() return false; } + stream.imbue(std::locale::classic()); stream << "movement_scheme " << scheme_to_string(m_movement) << "\n"; + stream << "ortho_radius " << m_orthophoto.radius << "\n"; + stream << "ortho_height " << m_orthophoto.height << "\n"; + stream << "ortho_opacity " << m_orthophoto.opacity << "\n"; + stream << "ortho_year " << m_orthophoto.year << "\n"; + stream << "ortho_4k " << (m_orthophoto.hires ? 1 : 0) << "\n"; + stream << "ortho_drape " << (m_orthophoto.drape ? 1 : 0) << "\n"; + stream << "ortho_lift " << m_orthophoto.lift << "\n"; + stream << "ortho_in_scene " << (m_orthophoto.in_scene ? 1 : 0) << "\n"; + stream << "platform_edge " << m_platform_edge << "\n"; + stream << "signal_offset " << m_signal_offset << "\n"; + stream << "nodebank_previews " << (m_nodebank_previews ? 1 : 0) << "\n"; + stream << "nodebank_card " << m_nodebank_card << "\n"; + stream << "colours " << m_palette << "\n"; + stream << std::fixed << std::setprecision(3); + for (auto const &origin : m_orthophoto_origins) + stream << origin.first << " " << origin.second.first << ";" << origin.second.second << "\n"; return true; } + +bool editorSettings::orthophoto_origin(std::string const &Scenery, double &North, double &East) const +{ + auto const lookup = m_orthophoto_origins.find(origin_key(Scenery)); + if (lookup == m_orthophoto_origins.end()) + return false; + North = lookup->second.first; + East = lookup->second.second; + return true; +} + +void editorSettings::orthophoto_origin(std::string const &Scenery, double North, double East) +{ + m_orthophoto_origins[origin_key(Scenery)] = {North, East}; +} diff --git a/editor/editorSettings.hpp b/editor/editorSettings.hpp index 4ed09444..7bfb1d36 100644 --- a/editor/editorSettings.hpp +++ b/editor/editorSettings.hpp @@ -1,5 +1,9 @@ #pragma once +#include +#include +#include + class editorSettings { public: @@ -18,8 +22,47 @@ public: movement_scheme movement() const { return m_movement; } void movement(movement_scheme scheme) { m_movement = scheme; } + // geoportal orthophoto layer (editor_orthophoto); the origin is kept per scenery + struct orthophoto_settings + { + int radius{2}; + float height{0.0f}; + float opacity{0.6f}; + int year{0}; + bool hires{false}; + bool drape{true}; + float lift{0.05f}; + bool in_scene{false}; + }; + orthophoto_settings &orthophoto() { return m_orthophoto; } + // PUWG 1992 northing/easting of the scenery's (0,0,0) point; false when none was stored + bool orthophoto_origin(std::string const &Scenery, double &North, double &East) const; + void orthophoto_origin(std::string const &Scenery, double North, double East); + + double platform_edge() const { return m_platform_edge; } + void platform_edge(double Distance) { m_platform_edge = Distance; } + double signal_offset() const { return m_signal_offset; } + void signal_offset(double Distance) { m_signal_offset = Distance; } + + // node bank: cards with the previews of the models or a list, and the size of the cards in pixels + bool nodebank_previews() const { return m_nodebank_previews; } + void nodebank_previews(bool Previews) { m_nodebank_previews = Previews; } + int nodebank_card() const { return m_nodebank_card; } + void nodebank_card(int Size) { m_nodebank_card = Size; } + + // colours of the editor: 0 green, as the rest of the simulator; 1 amber + int palette() const { return m_palette; } + void palette(int Palette) { m_palette = Palette; } + private: movement_scheme m_movement{movement_scheme::wsad}; + orthophoto_settings m_orthophoto; + std::map> m_orthophoto_origins; + double m_platform_edge{1.725}; + double m_signal_offset{2.25}; + bool m_nodebank_previews{true}; + int m_nodebank_card{96}; + int m_palette{0}; }; // global editor settings instance diff --git a/editor/editorSpeed.cpp b/editor/editorSpeed.cpp new file mode 100644 index 00000000..9f4a449e --- /dev/null +++ b/editor/editorSpeed.cpp @@ -0,0 +1,341 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "editor/editorSpeed.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" +#include "editor/editorTrack.hpp" + +#include "world/Track.h" + +#include +#include + +namespace speed_check +{ + +namespace +{ + +double constexpr kGravity{9.81}; + +struct sample +{ + double chainage; + double curvature; // signed, 1/m + double cant; // m, signed as the curvature: raise of the outer rail +}; + +double value_at(std::vector const &Samples, double const Chainage, double sample::*Field) +{ + auto const next{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + if (next == Samples.begin()) + return (*next).*Field; + if (next == Samples.end()) + return Samples.back().*Field; + auto const &before{*std::prev(next)}; + auto const run{next->chainage - before.chainage}; + return run > 1e-9 ? before.*Field + ((*next).*Field - before.*Field) * (Chainage - before.chainage) / run : before.*Field; +} + +// highest speed up to Top at which the unbalanced acceleration, changing by Curvature and Cant per metre, changes no faster than Jerk +double jerk_limit(double const Curvature, double const Cant, double const Gauge, double const Jerk, double const Top) +{ + auto const rate = [&](double const Speed) { return std::abs(Speed * Speed / 12.96 * Curvature - kGravity * Cant / Gauge) * Speed / 3.6; }; + if (rate(Top) <= Jerk) + return Top; + double low{0.0}; + for (int i = 1; i * 10.0 <= Top; ++i) + { + auto const speed{i * 10.0}; + if (rate(speed) > Jerk) + { + double high{speed}; + for (int i = 0; i < 12; ++i) + { + auto const middle{(low + high) * 0.5}; + (rate(middle) > Jerk ? high : low) = middle; + } + return low; + } + low = speed; + } + return Top; +} + +// track the simulation joined to the start or the end of the path +TTrack *linked(TTrack const &Track, int const Path, bool const Atend) +{ + if (Track.SwitchExtension && Path < 2) + return Atend ? Track.SwitchExtension->pNexts[Path] : Track.SwitchExtension->pPrevs[Path]; + return Atend ? Track.trNext : Track.trPrev; +} + +// paths which continue the path through the point, run from or to it. only the joints the trains take count: +// in a double slip the ends of the other diagonal lie a few centimetres away, but aren't joined to this path +std::vector continuations(TTrack &Track, int const Path, bool const Atend, glm::dvec3 const &Direction, bool const Arriving) +{ + std::vector result; + auto *other{linked(Track, Path, Atend)}; + if (other == nullptr || other == &Track || other->m_editorremoved) + return result; + auto const &point{Track.m_paths[Path].points[Atend ? segment_data::point::end : segment_data::point::start]}; + for (int path = 0; path < static_cast(other->m_paths.size()); ++path) + { + auto const &candidate{other->m_paths[path]}; + auto const atstart{glm::distance(candidate.points[segment_data::point::start], point) < geometry::same_point}; + if (false == atstart && glm::distance(candidate.points[segment_data::point::end], point) >= geometry::same_point) + continue; + if (linked(*other, path, false == atstart) != &Track) + continue; + geometry::bezier const curve{candidate}; + // direction of travel through the other path at the point + auto const along{atstart ? curve.first(0.0) : -curve.first(1.0)}; + auto const travel{Arriving ? -along : along}; + if (glm::dot(geometry::plan_of(travel), geometry::plan_of(Direction)) <= 0.0) + continue; + result.push_back({other, path, Arriving ? false == atstart : atstart}); + } + return result; +} + +} // namespace + +char const *describe(cause const Cause) +{ + switch (Cause) + { + case cause::radius: return "radius"; + case cause::cant_rate: return "cant change rate"; + case cause::cant_ramp: return "cant ramp"; + case cause::jerk: return "no transition"; + case cause::branch: return "switch branch"; + default: return "-"; + } +} + +bool verdict::violated(options const &Options) const +{ + if (hard >= Options.top) + return false; + if (set <= 0.0) + return true; + return set > hard + 0.5; +} + +bool verdict::warned(options const &Options) const +{ + if (safe >= Options.top || violated(Options)) + return false; + return set <= 0.0 || set > safe + 0.5; +} + +std::string explain(verdict const &Verdict, options const &Options) +{ + if (Verdict.safe >= Options.top) + return format("The geometry allows any speed up to %.0f km/h", Options.top); + auto const gauge{Options.norms.gauge / 1000.0}; + // the numbers are given at the speed set, or at the safe one when none is set + auto const speed{Verdict.set > 0.0 ? Verdict.set : Verdict.safe}; + auto const radius_text = [](double const Radius) { + return Radius == 0.0 ? std::string{"straight"} : format("R %.0f m%s", std::abs(Radius), Radius < 0.0 ? " the other way" : ""); + }; + auto const unbalanced = [&](double const Radius, double const Cant) { return (Radius != 0.0 ? speed * speed / (12.96 * Radius) : 0.0) - kGravity * Cant / 1000.0 / gauge; }; + std::string text; + if (Verdict.warned(Options)) + text = "Warning only, the limit of a switch branch: "; + if (Verdict.set <= 0.0) + text += format("No speed limit is set, so the trains run at the line speed, while the geometry allows %.0f km/h.\n", Verdict.safe); + else + text += format("The speed set, %.0f km/h, is %s the %.0f km/h the geometry allows.\n", Verdict.set, Verdict.set > Verdict.safe + 0.5 ? "above" : "within", Verdict.safe); + if (Verdict.why == cause::branch) + text += Verdict.hard < Options.top ? format("Without the switch branch limit the geometry allows %.0f km/h (%s).\n", Verdict.hard, describe(Verdict.hard_why)) + : std::string{"Without the switch branch limit the geometry allows any speed.\n"}; + switch (Verdict.why) + { + case cause::radius: + case cause::branch: + { + bool const branch{Verdict.why == cause::branch}; + text += format("%s, %s, cant %.0f mm.\n", branch ? "Diverging track of the switch" : "Curve", radius_text(Verdict.radius).c_str(), Verdict.cant); + text += format("Unbalanced acceleration V^2/(12.96 R) - g h/s at %.0f km/h: %.2f m/s2, the limit is %.2f m/s2%s.\n", speed, unbalanced(Verdict.radius, Verdict.cant), + branch ? Options.branch_unbalanced : Options.norms.unbalanced, branch ? " for the switch branches, which have no cant" : ""); + text += branch ? "A switch of a larger radius would allow more" : "More cant or a larger radius would allow more"; + break; + } + case cause::cant_rate: + text += format("The cant changes from %.0f to %.0f mm over %.0f m, %.2f mm per m.\n", Verdict.cant_from, Verdict.cant_to, Options.reference_length, Verdict.cant_slope); + text += format("A train at %.0f km/h feels it change by %.0f mm/s, the limit is %.0f mm/s.\n", speed, Verdict.cant_slope * speed / 3.6, Options.norms.cant_rate); + text += "A longer cant ramp, along a longer transition curve, would allow more"; + break; + case cause::cant_ramp: + text += format("The cant changes from %.0f to %.0f mm over %.0f m, a ramp of 1:%.0f.\n", Verdict.cant_from, Verdict.cant_to, Options.reference_length, 1000.0 / std::max(1e-6, Verdict.cant_slope)); + text += format("At %.0f km/h the ramp can't be steeper than 1:(k V) = 1:%.0f, k = %.1f.\n", speed, Options.norms.ramp_factor * speed, Options.norms.ramp_factor); + text += "A longer cant ramp would allow more"; + break; + case cause::jerk: + { + auto const from{unbalanced(Verdict.radius_from, Verdict.cant_from)}; + auto const to{unbalanced(Verdict.radius_to, Verdict.cant_to)}; + auto const time{Options.reference_length / std::max(1.0, speed / 3.6)}; + text += format("Within %.0f m the track goes from %s, cant %.0f mm, to %s, cant %.0f mm.\n", Options.reference_length, radius_text(Verdict.radius_from).c_str(), Verdict.cant_from, + radius_text(Verdict.radius_to).c_str(), Verdict.cant_to); + text += format("At %.0f km/h the unbalanced acceleration changes from %.2f to %.2f m/s2 in %.1f s: %.2f m/s3, the limit is %.2f m/s3.\n", speed, from, to, time, std::abs(to - from) / time, + Options.norms.jerk); + text += "The curve lacks a transition, or it is too short"; + break; + } + default: break; + } + return text; +} + +double suggested(double const Safe) +{ + if (Safe >= 40.0) + return std::floor(Safe / 10.0) * 10.0; + return std::max(5.0, std::floor(Safe / 5.0) * 5.0); +} + +double path_velocity(TTrack const &Track, int const Path) +{ + auto const velocity{editor_track::velocity(Track)}; + if (Track.eType == tt_Switch && velocity <= -2.0) + return Path == 0 ? -1.0 : -velocity; + return velocity > 0.0 ? velocity : -1.0; +} + +double switch_velocity(double const Main, double const Diverging) +{ + if (Main <= 0.0) + return Diverging > 0.0 ? -std::max(2.0, Diverging) : -1.0; + return Diverging > 0.0 ? std::min(Main, Diverging) : Main; +} + +verdict check(TTrack &Track, int const Path, options const &Options) +{ + verdict result; + result.track = &Track; + result.path = Path; + result.set = path_velocity(Track, Path); + result.safe = Options.top; + result.hard = Options.top; + if (Path < 0 || Path >= static_cast(Track.m_paths.size())) + return result; + + auto const gauge{Options.norms.gauge / 1000.0}; + bool const branch{Track.eType == tt_Switch && Path == 1}; + auto const &own{Track.m_paths[Path]}; + geometry::bezier const curve{own}; + auto const before{continuations(Track, Path, false, curve.first(0.0), true)}; + auto const after{continuations(Track, Path, true, curve.first(1.0), false)}; + std::vector previous{nullptr}, next{nullptr}; + if (false == before.empty()) + previous.clear(); + for (auto const &span : before) + previous.push_back(&span); + if (false == after.empty()) + next.clear(); + for (auto const &span : after) + next.push_back(&span); + + auto const half{Options.reference_length * 0.5}; + for (auto const *first : previous) + { + for (auto const *last : next) + { + editor_track::route route; + if (first != nullptr) + route.spans.push_back(*first); + route.spans.push_back({&Track, Path, true}); + if (last != nullptr) + route.spans.push_back(*last); + auto const own_span{first != nullptr ? std::size_t{1} : std::size_t{0}}; + auto const sampled{editor_track::sample_route(route, Options.step)}; + std::vector samples; + samples.reserve(sampled.size()); + for (auto const &entry : sampled) + { + auto const sign{entry.curvature < 0.0 ? -1.0 : 1.0}; + samples.push_back({entry.chainage, entry.curvature, entry.cant * sign}); + } + auto const from{route.spans[own_span].from}; + auto const to{route.spans[own_span].to}; + for (std::size_t i = 0; i < sampled.size(); ++i) + { + if (sampled[i].span != own_span) + continue; + auto const &here{samples[i]}; + auto const curvature{std::abs(here.curvature)}; + auto const cant{std::abs(here.cant)}; + double safe{Options.top}; + cause why{cause::none}; + auto const limit = [&](double const Speed, cause const Cause) { + if (Speed < safe) + { + safe = Speed; + why = Cause; + } + if (Cause != cause::branch && Speed < result.hard) + { + result.hard = Speed; + result.hard_why = Cause; + } + }; + if (curvature > 1e-7) + { + auto const allowed{(branch ? Options.branch_unbalanced : Options.norms.unbalanced) + kGravity * cant / gauge}; + limit(3.6 * std::sqrt(std::max(0.0, allowed) / curvature), branch ? cause::branch : cause::radius); + } + auto const start{std::max(0.0, here.chainage - half)}; + auto const end{std::min(route.length, here.chainage + half)}; + auto const run{end - start}; + double cantchange{0.0}; + if (run > Options.step * 0.5) + { + cantchange = (value_at(samples, end, &sample::cant) - value_at(samples, start, &sample::cant)) / run; + auto const curvaturechange{(value_at(samples, end, &sample::curvature) - value_at(samples, start, &sample::curvature)) / run}; + if (std::abs(cantchange) > 1e-7) + { + limit(3.6 * Options.norms.cant_rate / 1000.0 / std::abs(cantchange), cause::cant_rate); + limit(1.0 / (Options.norms.ramp_factor * std::abs(cantchange)), cause::cant_ramp); + } + // switches have no transitions by design, their limits cover it + auto const regular{std::all_of(route.spans.begin(), route.spans.end(), [&](editor_track::route_span const &Span) { return Span.to < start || Span.from > end || Span.track->eType == tt_Normal; })}; + if (regular && (std::abs(curvaturechange) > 1e-9 || std::abs(cantchange) > 1e-9)) + limit(jerk_limit(curvaturechange, cantchange, gauge, Options.norms.jerk, Options.top), cause::jerk); + } + if (safe < result.safe) + { + auto const share{to > from ? (here.chainage - from) / (to - from) : 0.0}; + result.safe = safe; + result.why = why; + result.position = curve.point(std::clamp(curve.parameter(share * curve.plan_length()), 0.0, 1.0)); + result.radius = curvature > 1e-7 ? 1.0 / curvature : 0.0; + result.cant = cant * 1000.0; + // signed as the curvature here, a reverse curve getting the other sign + auto const sign{here.curvature < 0.0 ? -1.0 : 1.0}; + auto const radius_at = [&](double const Chainage) { + auto const value{value_at(samples, Chainage, &sample::curvature) * sign}; + return std::abs(value) > 1e-7 ? 1.0 / value : 0.0; + }; + result.radius_from = radius_at(start); + result.radius_to = radius_at(end); + result.cant_from = value_at(samples, start, &sample::cant) * sign * 1000.0; + result.cant_to = value_at(samples, end, &sample::cant) * sign * 1000.0; + result.cant_slope = std::abs(cantchange) * 1000.0; + } + } + } + } + return result; +} + +} // namespace speed_check diff --git a/editor/editorSpeed.hpp b/editor/editorSpeed.hpp new file mode 100644 index 00000000..bea32fc2 --- /dev/null +++ b/editor/editorSpeed.hpp @@ -0,0 +1,80 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +#include "editor/editorAlignment.hpp" + +class TTrack; + +// speed the geometry of the paths allows, set against the speed limits written in them +namespace speed_check +{ + +enum class cause +{ + none, + radius, // unbalanced acceleration in the curve + cant_rate, // cant changes too fast for the passengers + cant_ramp, // the cant ramp is too steep for the wheels + jerk, // the unbalanced acceleration changes too fast, curves without the transitions + branch // diverging track of a switch, without cant +}; +char const *describe(cause const Cause); + +struct options +{ + alignment::limits norms; + double branch_unbalanced{0.65}; // m/s2, for the diverging tracks of the switches + double reference_length{20.0}; // m, over which an abrupt change of the curvature or of the cant is felt + double step{2.0}; // m, between the checked points + double top{300.0}; // km/h, nothing above is looked into +}; + +struct verdict +{ + TTrack *track{nullptr}; + int path{0}; + double set{-1.0}; // km/h, -1: no limit of its own + double safe{0.0}; // km/h, Options.top when the geometry allows any speed + cause why{cause::none}; + glm::dvec3 position{0.0}; // where the geometry is the tightest + double radius{0.0}; // m, there + double cant{0.0}; // mm, there + // over the reference length around the point: the radius and the cant at its ends, 0 m for a straight. + // negative where the curve turns the other way than at the point + double radius_from{0.0}, radius_to{0.0}; + double cant_from{0.0}, cant_to{0.0}; // mm + double cant_slope{0.0}; // mm per m + // km/h, the safe speed without the limit of the switch branches, which is only a warning + double hard{0.0}; + cause hard_why{cause::none}; + // true when the speed set is above the safe one, or isn't set over a tight geometry; the switch branches not counted + bool violated(options const &Options) const; + // true when only the limit of a switch branch is exceeded + bool warned(options const &Options) const; +}; +// what limits the speed, with the numbers which do +std::string explain(verdict const &Verdict, options const &Options); + +verdict check(TTrack &Track, int const Path, options const &Options); +// the safe speed rounded down to the usual steps of the speed limits +double suggested(double const Safe); +// speed limit in force on the path, -1 for none. a switch keeps one for both its tracks: positive applies to both, +// -2 and below only to the diverging one, its main track having no limit then +double path_velocity(TTrack const &Track, int const Path); +// velocity of a switch which keeps its tracks to the limits given, -1 for none. one written value can't hold +// a limit on each track, the main one gets the diverging one's limit too when that's lower +double switch_velocity(double const Main, double const Diverging); + +} // namespace speed_check diff --git a/editor/editorTerrain.cpp b/editor/editorTerrain.cpp index 55e71b5c..16daaf1e 100644 --- a/editor/editorTerrain.cpp +++ b/editor/editorTerrain.cpp @@ -10,331 +10,33 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorTerrain.hpp" -#include "scene/scene.h" -#include "scene/scenenode.h" -#include "simulation/simulation.h" -#include "rendering/renderer.h" -#include "model/vertex.h" - -#include -#include -#include - -namespace +heightmap::chunk_data &editor_terrain::edit(bool const Shape, bool const Paint) { - constexpr double kPi = 3.14159265358979323846; + // a worker may still be making a mesh of the current data, so a copy is changed instead + if (m_data.use_count() > 1) + m_data = std::make_shared(*m_data); + m_modified = true; + if (Shape) + ++m_shapeversion; + if (Paint) + ++m_paintversion; + // the pointer is to const for everyone else; this chunk is its only holder now + return const_cast(*m_data); } -bool editor_terrain::create(glm::dvec3 const &Center, int Cells, float CellSize, std::string const &TextureName, - height_sampler const &Sampler) +void editor_terrain::replace(heightmap::chunk_ptr Data, bool const Shape, bool const Paint) { - if (Cells < 1 || CellSize <= 0.0f || simulation::Region == nullptr) - return false; - - m_cells = Cells; - m_cellsize = CellSize; - double const half = 0.5 * static_cast(Cells) * CellSize; - m_x0 = Center.x - half; - m_z0 = Center.z - half; - m_heights.assign(static_cast(Cells + 1) * (Cells + 1), static_cast(Center.y)); - - // optionally seed the grid by sampling whatever geometry is already there (terrain capture) - if (Sampler) - { - for (int iz = 0; iz <= Cells; ++iz) - for (int ix = 0; ix <= Cells; ++ix) - { - double const vx = m_x0 + static_cast(ix) * CellSize; - double const vz = m_z0 + static_cast(iz) * CellSize; - double y; - if (Sampler(vx, vz, y)) - m_heights[index(ix, iz)] = static_cast(y); - } - } - - m_material = TextureName.empty() ? null_handle : GfxRenderer->Fetch_Material(TextureName); - - // section-level shapes are rendered relative to the section centre, so that is our geometry origin - scene::basic_section &sec = simulation::Region->section(Center); - sec.create_geometry(); // ensure existing section geometry is already built (idempotent) - m_origin = sec.m_area.center; - m_section = &sec; - - std::vector verts; - build_vertices(verts, false); - m_vertexcount = verts.size(); - - scene::shape_node shape; - shape.make_terrain(m_material, std::move(verts), m_origin); - - // upload to a dedicated bank; the renderer resolves draw calls by handle regardless of bank - m_bank = GfxRenderer->Create_Bank(); - shape.create_geometry(m_bank); // sets the shape's geometry handle, clears its CPU vertices - m_geometry = shape.data().geometry; - - glm::dvec3 const shapecenter = shape.data().area.center; - float const shaperadius = shape.radius(); // cached inside make_terrain, vertices already gone - - sec.m_shapes.emplace_back(std::move(shape)); - // extend the section bounds so the new terrain isn't frustum-culled at its edges - sec.m_area.radius = std::max( - sec.m_area.radius, - static_cast(glm::length(sec.m_area.center - shapecenter) + shaperadius)); - - return true; -} - -glm::dvec3 editor_terrain::vertex_position(int Ix, int Iz) const -{ - return glm::dvec3( - m_x0 + static_cast(Ix) * m_cellsize, - static_cast(m_heights[index(Ix, Iz)]), - m_z0 + static_cast(Iz) * m_cellsize); -} - -glm::vec3 editor_terrain::vertex_normal(int Ix, int Iz) const -{ - // central differences on the heightfield; clamp to edges - int const xl = std::max(0, Ix - 1), xr = std::min(m_cells, Ix + 1); - int const zl = std::max(0, Iz - 1), zr = std::min(m_cells, Iz + 1); - float const hl = m_heights[index(xl, Iz)], hr = m_heights[index(xr, Iz)]; - float const hd = m_heights[index(Ix, zl)], hu = m_heights[index(Ix, zr)]; - float const dx = static_cast((xr - xl)) * m_cellsize; - float const dz = static_cast((zr - zl)) * m_cellsize; - glm::vec3 n(-(hr - hl) / (dx > 0.f ? dx : 1.f), 1.0f, -(hu - hd) / (dz > 0.f ? dz : 1.f)); - return glm::normalize(n); -} - -world_vertex editor_terrain::make_vertex(int Ix, int Iz) const -{ - world_vertex v; - v.position = vertex_position(Ix, Iz); - v.normal = vertex_normal(Ix, Iz); - v.texture = glm::vec2(static_cast(Ix), static_cast(Iz)); - return v; -} - -// emits one quad (two upward-facing triangles) spanning grid corners (X0,Z0)..(X1,Z1) -void editor_terrain::emit_quad(int X0, int Z0, int X1, int Z1, std::vector &Out) const -{ - world_vertex const v00 = make_vertex(X0, Z0); - world_vertex const v10 = make_vertex(X1, Z0); - world_vertex const v01 = make_vertex(X0, Z1); - world_vertex const v11 = make_vertex(X1, Z1); - - Out.push_back(v00); - Out.push_back(v01); - Out.push_back(v10); - - Out.push_back(v11); - Out.push_back(v10); - Out.push_back(v01); -} - -// true if every grid vertex inside the block stays within Error of the bilinear plane of its corners -bool editor_terrain::block_flat(int X0, int Z0, int X1, int Z1, float Error) const -{ - float const h00 = m_heights[index(X0, Z0)]; - float const h10 = m_heights[index(X1, Z0)]; - float const h01 = m_heights[index(X0, Z1)]; - float const h11 = m_heights[index(X1, Z1)]; - double const wx = X1 - X0, wz = Z1 - Z0; - - for (int iz = Z0; iz <= Z1; ++iz) - for (int ix = X0; ix <= X1; ++ix) - { - double const tx = (wx > 0.0) ? (ix - X0) / wx : 0.0; - double const tz = (wz > 0.0) ? (iz - Z0) / wz : 0.0; - double const top = h00 + tx * (h10 - h00); - double const bot = h01 + tx * (h11 - h01); - double const interp = top + tz * (bot - top); - if (std::abs(static_cast(m_heights[index(ix, iz)]) - interp) > Error) - return false; - } - return true; -} - -// adaptive quadtree: collapse flat blocks into a single quad, otherwise split into four -void editor_terrain::emit_block(int X0, int Z0, int X1, int Z1, float Error, std::vector &Out) const -{ - bool const splitx = (X1 - X0) > 1; - bool const splitz = (Z1 - Z0) > 1; - - if ((!splitx && !splitz) || block_flat(X0, Z0, X1, Z1, Error)) - { - emit_quad(X0, Z0, X1, Z1, Out); - return; - } - - int const xm = splitx ? (X0 + X1) / 2 : X1; - int const zm = splitz ? (Z0 + Z1) / 2 : Z1; - - emit_block(X0, Z0, xm, zm, Error, Out); - if (splitx) - emit_block(xm, Z0, X1, zm, Error, Out); - if (splitz) - emit_block(X0, zm, xm, Z1, Error, Out); - if (splitx && splitz) - emit_block(xm, zm, X1, Z1, Error, Out); -} - -void editor_terrain::build_vertices(std::vector &Out, bool Simplify) const -{ - Out.clear(); - Out.reserve(static_cast(m_cells) * m_cells * 6); - - if (Simplify) - { - emit_block(0, 0, m_cells, m_cells, m_simplify_error, Out); - return; - } - - for (int iz = 0; iz < m_cells; ++iz) - for (int ix = 0; ix < m_cells; ++ix) - emit_quad(ix, iz, ix + 1, iz + 1, Out); -} - -void editor_terrain::regenerate(bool Simplify) -{ - if (!valid()) - return; - - std::vector verts; - build_vertices(verts, Simplify); - - gfx::vertex_array gpuverts; - gpuverts.reserve(verts.size()); - for (auto const &v : verts) - gpuverts.emplace_back(gfx::basic_vertex::convert(v, m_origin)); - gfx::userdata_array nouserdata; - - // fast path: same vertex count -> in-place swap into the existing chunk - if (gpuverts.size() == m_vertexcount && (m_geometry.bank != 0 || m_geometry.chunk != 0)) - { - GfxRenderer->Replace(gpuverts, nouserdata, m_geometry, GL_TRIANGLES); - return; - } - - // count changed (optimize / un-optimize): upload a fresh chunk and point the shape at it - gfx::geometry_handle const newhandle = GfxRenderer->Insert(gpuverts, nouserdata, m_bank, GL_TRIANGLES); - if (m_section != nullptr) - { - for (auto &shape : m_section->m_shapes) - { - auto const h = shape.data().geometry; - if (h.bank == m_geometry.bank && h.chunk == m_geometry.chunk) - { - shape.geometry(newhandle); - break; - } - } - } - m_geometry = newhandle; - m_vertexcount = gpuverts.size(); -} - -void editor_terrain::optimize(float ErrorMetres) -{ - m_simplify = true; - m_simplify_error = (ErrorMetres > 0.0f ? ErrorMetres : 0.01f); - m_dirty = false; - regenerate(true); -} - -void editor_terrain::unoptimize() -{ - m_simplify = false; - regenerate(false); -} - -void editor_terrain::destroy() -{ - if (m_section != nullptr) - { - // erase the shape whose geometry handle is ours; other shapes keep their handles (so they - // keep rendering), and the geometry GC reclaims our now-undrawn chunk's GPU memory - for (auto it = m_section->m_shapes.begin(); it != m_section->m_shapes.end(); ++it) - { - auto const h = it->data().geometry; - if (h.bank == m_geometry.bank && h.chunk == m_geometry.chunk) - { - m_section->m_shapes.erase(it); - break; - } - } - } - m_section = nullptr; - m_geometry = gfx::geometry_handle{0, 0}; - m_cells = 0; // mark invalid - m_heights.clear(); -} - -bool editor_terrain::contains(double X, double Z) const -{ - double const x1 = m_x0 + static_cast(m_cells) * m_cellsize; - double const z1 = m_z0 + static_cast(m_cells) * m_cellsize; - return (X >= m_x0 && X <= x1 && Z >= m_z0 && Z <= z1); -} - -double editor_terrain::height_at(double X, double Z) const -{ - double const fx = (X - m_x0) / m_cellsize; - double const fz = (Z - m_z0) / m_cellsize; - int ix = static_cast(std::floor(fx)); - int iz = static_cast(std::floor(fz)); - ix = std::clamp(ix, 0, m_cells - 1); - iz = std::clamp(iz, 0, m_cells - 1); - double const tx = std::clamp(fx - ix, 0.0, 1.0); - double const tz = std::clamp(fz - iz, 0.0, 1.0); - - double const h00 = m_heights[index(ix, iz)]; - double const h10 = m_heights[index(ix + 1, iz)]; - double const h01 = m_heights[index(ix, iz + 1)]; - double const h11 = m_heights[index(ix + 1, iz + 1)]; - - // matches the triangulation in build_vertices - if (tx + tz <= 1.0) - return h00 + tx * (h10 - h00) + tz * (h01 - h00); - return h11 + (1.0 - tx) * (h01 - h11) + (1.0 - tz) * (h10 - h11); -} - -bool editor_terrain::sculpt(double X, double Z, double Radius, double Strength) -{ - if (!valid() || Radius <= 0.0) - return false; - - bool changed = false; - for (int iz = 0; iz <= m_cells; ++iz) - for (int ix = 0; ix <= m_cells; ++ix) - { - double const vx = m_x0 + static_cast(ix) * m_cellsize; - double const vz = m_z0 + static_cast(iz) * m_cellsize; - double const d = std::sqrt((vx - X) * (vx - X) + (vz - Z) * (vz - Z)); - if (d > Radius) - continue; - // smooth cosine falloff: full strength at the centre, zero at the rim - double const falloff = 0.5 * (std::cos(kPi * d / Radius) + 1.0); - m_heights[index(ix, iz)] += static_cast(Strength * falloff); - changed = true; - } - - if (changed) - { - // sculpting edits the full-resolution mesh (fixed vertex count => fast in-place update); - // mark dirty so it can be auto-simplified once the stroke finishes, and modified for saving - m_simplify = false; - m_dirty = true; - m_modified = true; - regenerate(false); - } - return changed; + m_data = std::move(Data); + m_modified = true; + if (Shape) + ++m_shapeversion; + if (Paint) + ++m_paintversion; } glm::dvec3 editor_terrain::centre() const { - double const c = 0.5 * static_cast(m_cells) * m_cellsize; - double y = 0.0; - if (!m_heights.empty()) - y = m_heights[index(m_cells / 2, m_cells / 2)]; - return glm::dvec3(m_x0 + c, y, m_z0 + c); + auto const low{m_data->corner()}; + auto const half{heightmap::chunk_size * 0.5}; + return {low.x + half, m_data->height_at(low.x + half, low.y + half), low.y + half}; } diff --git a/editor/editorTerrain.hpp b/editor/editorTerrain.hpp index 04436685..d26c4c52 100644 --- a/editor/editorTerrain.hpp +++ b/editor/editorTerrain.hpp @@ -9,105 +9,72 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include #include -#include -#include #include +#include "scene/heightmapterrain.h" #include "utilities/Classes.h" // material_handle #include "interfaces/ITexture.h" // null_handle -#include "rendering/geometrybank.h" // gfx::geometry_handle / geometrybank_handle +#include "rendering/geometrybank.h" // gfx::geometry_handle namespace scene { class basic_section; } -// Editor-owned, editable terrain patch. +// a chunk of the heightmap terrain held in memory: its data, and what's drawn of it. // -// The engine's scene::shape_node drops its CPU-side vertices the moment it uploads them to the GPU, -// so it can't be edited or raycast after load. This class keeps the authoritative, editable data -// (a regular grid heightmap) on the CPU, generates a shape_node purely for rendering, and answers -// height/raycast queries directly from the heightmap (fast and exact). Sculpting updates the -// heightmap and pushes the new vertex positions into the shape's existing geometry chunk. +// the data is shared with the worker threads making its meshes, so it's never changed in place: an edit gets a copy +// of its own when the current data is in use elsewhere. what's drawn is managed by the terrain streamer class editor_terrain { public: - editor_terrain() = default; + explicit editor_terrain(heightmap::chunk_ptr Data) : m_data(std::move(Data)) {} - // builds an NxN-cell grid centred on Center, each quad CellSize metres across, using the - // material fetched from TextureName (empty => untextured). when Sampler is supplied it provides - // the starting height at each grid vertex (returns false to fall back to Center.y) - used to - // capture existing terrain. returns false on failure. - using height_sampler = std::function; - bool create(glm::dvec3 const &Center, int Cells, float CellSize, std::string const &TextureName, - height_sampler const &Sampler = {}); + heightmap::chunk_data const &data() const { return *m_data; } + heightmap::chunk_ptr const &shared() const { return m_data; } + // the data, to be changed. marks the chunk as edited; Shape: the heights changed, Paint: the materials changed + heightmap::chunk_data &edit(bool const Shape, bool const Paint); + // puts provided data in place of the current one + void replace(heightmap::chunk_ptr Data, bool const Shape, bool const Paint); - // true if (X,Z) lies within the terrain's horizontal footprint - bool contains(double X, double Z) const; - // surface height at (X,Z) (bilinear over the covering quad); only valid when contains() is true - double height_at(double X, double Z) const; - - // raises/lowers vertices within Radius of (X,Z) by Strength (metres, signed), with a smooth - // falloff; regenerates the rendered geometry (full resolution). returns true if anything changed. - bool sculpt(double X, double Z, double Radius, double Strength); - - // rebuilds the rendered mesh, collapsing regions flatter than ErrorMetres into larger quads - // (adaptive quadtree). the editable heightmap is untouched, so sculpting/raycast stay exact. - void optimize(float ErrorMetres); - // rebuilds the rendered mesh at full resolution (undoes optimize) - void unoptimize(); - - // removes the rendered shape from its section (stops drawing it); the renderer's geometry - // garbage collector reclaims the GPU memory shortly after. used by terrain streaming to unload. - void destroy(); - - // horizontal centre and extent, handy for the UI / camera framing + heightmap::chunk_key key() const { return m_data->key; } + // true if (X,Z) lies within the chunk + bool contains(double X, double Z) const { return m_data->contains(X, Z); } + // surface height at (X,Z), the way the full grid is triangulated + double height_at(double X, double Z) const { return m_data->height_at(X, Z); } + // the same, from heights taken from heights() earlier + double height_in(std::vector const &Heights, double X, double Z) const { return m_data->height_in(Heights, X, Z); } + std::vector const &heights() const { return m_data->heights; } + int cells() const { return m_data->cells; } glm::dvec3 centre() const; - float extent() const { return m_cells * m_cellsize; } - bool valid() const { return m_cells > 0; } - bool optimized() const { return m_simplify; } - // set by sculpt() when the mesh changed and is currently full-resolution; cleared by optimize(). - // used to auto-simplify only the chunks that were actually edited, once a stroke finishes. - bool dirty() const { return m_dirty; } - // set by sculpt() and never auto-cleared; used by streaming to know a chunk needs saving to disk + float extent() const { return static_cast(heightmap::chunk_size); } + bool valid() const { return m_data != nullptr && m_data->valid(); } + gfx::geometry_handle geometry() const { return m_geometry; } + // rendered triangle count, and the count of the full grid + std::size_t triangles() const { return m_triangles; } + std::size_t full_triangles() const { return static_cast(cells()) * cells() * 2; } + // changed since it was loaded or saved bool modified() const { return m_modified; } void clear_modified() { m_modified = false; } - // direct access to the editable heightmap (row-major world Y, (cells+1)^2), for save/load - std::vector const &heights() const { return m_heights; } - int cells() const { return m_cells; } - // rendered triangle count (drops after optimize) - std::size_t triangles() const { return m_vertexcount / 3; } - // full-resolution triangle count, for reference - std::size_t full_triangles() const { return static_cast(m_cells) * m_cells * 2; } - private: - int index(int Ix, int Iz) const { return Iz * (m_cells + 1) + Ix; } - glm::dvec3 vertex_position(int Ix, int Iz) const; - glm::vec3 vertex_normal(int Ix, int Iz) const; - world_vertex make_vertex(int Ix, int Iz) const; - // fills Out with the GL_TRIANGLES world-space vertex list; Simplify enables adaptive merging - void build_vertices(std::vector &Out, bool Simplify) const; - // adaptive quadtree helpers (used when Simplify is on) - bool block_flat(int X0, int Z0, int X1, int Z1, float Error) const; - void emit_block(int X0, int Z0, int X1, int Z1, float Error, std::vector &Out) const; - void emit_quad(int X0, int Z0, int X1, int Z1, std::vector &Out) const; - // rebuilds and re-uploads the rendered geometry (Replace when the count is unchanged, otherwise - // a fresh chunk whose handle is swapped into the shape) - void regenerate(bool Simplify); + friend class terrain_streamer; - int m_cells{0}; // quads per side; (m_cells+1)^2 grid vertices - float m_cellsize{1.0f}; // metres per quad - double m_x0{0.0}, m_z0{0.0}; // world position of grid corner (ix=0, iz=0) - std::vector m_heights; // per-vertex world Y, row-major (m_cells+1)^2 + heightmap::chunk_ptr m_data; + bool m_modified{false}; + std::uint64_t m_shapeversion{1}; // grows with every change of the heights + std::uint64_t m_paintversion{1}; // grows with every change of the paint + // drawn state, managed by the streamer + int m_slot{-1}; // geometry slot holding the mesh + gfx::geometry_handle m_geometry{0, 0}; + std::size_t m_triangles{0}; + int m_stride{0}; // of the mesh in the slot, 0: none yet + std::uint64_t m_builtversion{0}; // shape version the mesh was made from + std::uint64_t m_paintedversion{0}; // paint version the material was made from + bool m_neighbours{false}; // a neighbour came or changed, the edges of the mesh are to be made again + bool m_building{false}; // a mesh is being made by a worker material_handle m_material{null_handle}; - gfx::geometrybank_handle m_bank{0, 0}; // geometry bank owning the rendered chunk - gfx::geometry_handle m_geometry{0, 0}; // rendered chunk - std::size_t m_vertexcount{0}; // current chunk's vertex count (for Replace vs recreate) - glm::dvec3 m_origin{0.0}; // origin the GPU vertices are stored relative to - scene::basic_section *m_section{nullptr}; // section holding the shape, for handle swaps - - bool m_simplify{false}; // whether the rendered mesh is currently simplified - float m_simplify_error{0.5f}; // flatness tolerance used by optimize() - bool m_dirty{false}; // edited since the last optimize (full-res, awaiting simplification) - bool m_modified{false}; // edited since load/save (for streaming persistence) + scene::basic_section *m_section{nullptr}; // holding the shape, null: not drawn + glm::dvec3 m_low{0.0}, m_high{0.0}; // bounds of the drawn mesh }; diff --git a/editor/editorTerrainStreamer.cpp b/editor/editorTerrainStreamer.cpp index 6d45ef34..0c0a8161 100644 --- a/editor/editorTerrainStreamer.cpp +++ b/editor/editorTerrainStreamer.cpp @@ -10,280 +10,1797 @@ http://mozilla.org/MPL/2.0/. #include "stdafx.h" #include "editor/editorTerrainStreamer.hpp" -#include -#include -#include -#include -#include -#include +#include "model/vertex.h" +#include "rendering/renderer.h" +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +#include +#include +#include +#include -// simulation-level streamer instance (see header) terrain_streamer EditorTerrain; -void terrain_streamer::configure(int Cells, float CellSize, int Radius, float BaseHeight, std::string const &Texture) +namespace { - m_cells = std::max(1, Cells); - m_cellsize = std::max(0.1f, CellSize); - m_radius = std::max(0, Radius); - m_baseheight = BaseHeight; - m_texture = Texture; + +int constexpr worker_count{2}; +std::size_t constexpr max_loading{12}; // chunks asked from the workers at a time +std::size_t constexpr max_meshing{12}; // meshes asked from the workers at a time +double constexpr upload_budget{0.004}; // seconds of a frame for taking over finished work +int constexpr materials_per_frame{6}; +double constexpr look_ahead{8.0}; // seconds; chunks are loaded around where the camera will be by then +double constexpr look_ahead_limit{2000.0}; // metres +double constexpr region_half{scene::EU07_REGIONSIDESECTIONCOUNT * scene::EU07_SECTIONSIZE * 0.5 - heightmap::chunk_size}; +double constexpr kPi{3.14159265358979323846}; + +double steady_seconds() +{ + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); } -terrain_streamer::chunk_key terrain_streamer::key_at(double X, double Z) const +std::uint64_t geometry_key(gfx::geometry_handle const &Geometry) { - double const size = chunk_world_size(); - return {static_cast(std::floor(X / size)), static_cast(std::floor(Z / size))}; + return (static_cast(Geometry.bank) << 32) | Geometry.chunk; } -glm::dvec3 terrain_streamer::chunk_centre(int Cx, int Cz) const +bool in_region(glm::dvec2 const &Point) { - double const size = chunk_world_size(); - return glm::dvec3((Cx + 0.5) * size, static_cast(m_baseheight), (Cz + 0.5) * size); + return std::abs(Point.x) < region_half && std::abs(Point.y) < region_half; } -std::string terrain_streamer::chunk_path(int Cx, int Cz) const +glm::dvec3 chunk_centre(heightmap::chunk_key const &Key) { - return m_dir + "/chunk_" + std::to_string(Cx) + "_" + std::to_string(Cz) + ".etc"; + auto const low{heightmap::chunk_corner(Key)}; + return {low.x + heightmap::chunk_size * 0.5, 0.0, low.y + heightmap::chunk_size * 0.5}; } -bool terrain_streamer::chunk_on_disk(chunk_key const &Key) +// index range of the grid points (spacing Step from Origin, Count points) within [Low, High] +std::pair index_range(double const Origin, double const Step, int const Count, double const Low, double const High) { - auto const it = m_known.find(Key); - if (it != m_known.end()) - return (it->second & cf_exists_on_disk) != 0; - - std::error_code ec; - bool const exists = std::filesystem::exists(chunk_path(Key.first, Key.second), ec); - m_known[Key] = exists ? cf_exists_on_disk : 0; - return exists; + auto const first{std::max(0, static_cast(std::ceil((Low - Origin) / Step - 1e-9)))}; + auto const last{std::min(Count - 1, static_cast(std::floor((High - Origin) / Step + 1e-9)))}; + return {first, last}; } -// 16-bit chunk file: 'ETC1' | uint16 cells | float baseY | float step | (cells+1)^2 * uint16 -// where worldY = baseY + raw * step (per-chunk auto-scaled to fit the height range losslessly-ish) -bool terrain_streamer::load_heights(int Cx, int Cz, std::vector &Out) const +double falloff(double const Distance, double const Radius) { - std::ifstream f(chunk_path(Cx, Cz), std::ios::binary); - if (!f) - return false; + // smooth cosine falloff: full strength at the centre, zero at the rim + return 0.5 * (std::cos(kPi * Distance / Radius) + 1.0); +} - char magic[4] = {}; - f.read(magic, 4); - if (f.gcount() != 4 || magic[0] != 'E' || magic[1] != 'T' || magic[2] != 'C' || magic[3] != '1') - return false; - - std::uint16_t cells = 0; - float baseY = 0.0f, step = 0.0f; - f.read(reinterpret_cast(&cells), sizeof(cells)); - f.read(reinterpret_cast(&baseY), sizeof(baseY)); - f.read(reinterpret_cast(&step), sizeof(step)); - if (!f || static_cast(cells) != m_cells) - return false; // resolution changed since save: ignore and regenerate - - std::size_t const n = static_cast(cells + 1) * (cells + 1); - Out.resize(n); - for (std::size_t i = 0; i < n; ++i) +void erase_shape(std::vector &Shapes, gfx::geometry_handle const &Geometry) +{ + for (auto it = Shapes.begin(); it != Shapes.end(); ++it) { - std::uint16_t raw = 0; - f.read(reinterpret_cast(&raw), sizeof(raw)); - if (!f) - return false; - Out[i] = baseY + static_cast(raw) * step; + auto const &geometry{it->data().geometry}; + if (geometry.bank == Geometry.bank && geometry.chunk == Geometry.chunk) + { + Shapes.erase(it); + return; + } } +} + +} // namespace + +terrain_streamer::~terrain_streamer() +{ + stop_workers(); +} + +std::string terrain_streamer::pack_path(heightmap::pack_key const &Key) const +{ + return m_directory + heightmap::pack_file_name(Key); +} + +bool terrain_streamer::open(std::string const &Name) +{ + close(); + m_name = Name; + m_directory = "terrain/" + Name + "/"; + m_manifest = heightmap::manifest(); + m_manifestchanged = false; + auto const manifestpath{m_directory + "terrain.yaml"}; + std::error_code ec; + if (std::filesystem::exists(manifestpath, ec)) + { + std::string error; + if (false == heightmap::load_manifest(manifestpath, m_manifest, &error)) + ErrorLog("Bad file: heightmap terrain description \"" + manifestpath + "\": " + error, logtype::file); + } + else if (false == std::filesystem::exists(m_directory, ec)) + WriteLog("Heightmap terrain \"" + m_directory + "\" has no files yet", logtype::generic); + m_active = true; + m_waterchanged = true; + start_workers(); + WriteLog("Heightmap terrain: " + m_directory, logtype::generic); return true; } -void terrain_streamer::save_heights(int Cx, int Cz, editor_terrain const &Terrain) +void terrain_streamer::close() { - auto const &h = Terrain.heights(); - if (h.empty()) - return; - - float minh = h[0], maxh = h[0]; - for (float const v : h) + stop_workers(); + if (simulation::Region != nullptr) { - minh = std::min(minh, v); - maxh = std::max(maxh, v); + for (auto &entry : m_chunks) + undraw(*entry.second); + clear_water(true); } - float const step = (maxh > minh) ? (maxh - minh) / 65535.0f : 0.0f; - - std::error_code ec; - std::filesystem::create_directories(m_dir, ec); - std::ofstream f(chunk_path(Cx, Cz), std::ios::binary | std::ios::trunc); - if (!f) - return; - - char const magic[4] = {'E', 'T', 'C', '1'}; - std::uint16_t const cells = static_cast(Terrain.cells()); - f.write(magic, 4); - f.write(reinterpret_cast(&cells), sizeof(cells)); - f.write(reinterpret_cast(&minh), sizeof(minh)); - f.write(reinterpret_cast(&step), sizeof(step)); - for (float const v : h) - { - std::uint16_t const raw = (step > 0.0f) - ? static_cast(std::lround((v - minh) / step)) - : std::uint16_t{0}; - f.write(reinterpret_cast(&raw), sizeof(raw)); - } -} - -void terrain_streamer::update(glm::dvec3 const &CameraPos) -{ - if (!m_active) - return; - - chunk_key const camera = key_at(CameraPos.x, CameraPos.z); - - // load any missing chunk inside the radius (one build per frame keeps the hitch small) - bool built = false; - for (int dz = -m_radius; dz <= m_radius && !built; ++dz) - for (int dx = -m_radius; dx <= m_radius && !built; ++dx) - { - chunk_key const key{camera.first + dx, camera.second + dz}; - if (m_chunks.count(key)) - continue; - - // only stream chunks that were actually authored (exist on disk); empty space stays empty - std::vector loaded; - if (!chunk_on_disk(key) || !load_heights(key.first, key.second, loaded)) - continue; - - glm::dvec3 const centre = chunk_centre(key.first, key.second); - double const size = chunk_world_size(); - double const x0 = key.first * size, z0 = key.second * size; // chunk corner - int const cells = m_cells; - float const cs = m_cellsize; - editor_terrain::height_sampler sampler = - [&loaded, x0, z0, cells, cs](double X, double Z, double &OutY) -> bool { - int ix = static_cast(std::lround((X - x0) / cs)); - int iz = static_cast(std::lround((Z - z0) / cs)); - ix = std::clamp(ix, 0, cells); - iz = std::clamp(iz, 0, cells); - OutY = loaded[static_cast(iz) * (cells + 1) + ix]; - return true; - }; - - auto terrain = std::make_unique(); - if (terrain->create(centre, m_cells, m_cellsize, m_texture, sampler)) - { - if (m_auto_optimize) - terrain->optimize(m_simplify_error); - m_chunks.emplace(key, std::move(terrain)); - built = true; // amortise: at most one new chunk per frame - } - } - - // unload chunks that drifted outside the radius - for (auto it = m_chunks.begin(); it != m_chunks.end();) - { - int const dx = it->first.first - camera.first; - int const dz = it->first.second - camera.second; - if (std::abs(dx) > m_radius || std::abs(dz) > m_radius) - { - if (it->second) - { - // persist edits before dropping the chunk, so they survive the round-trip - if (m_persist && it->second->modified()) - { - save_heights(it->first.first, it->first.second, *it->second); - m_known[it->first] |= cf_exists_on_disk; - } - it->second->destroy(); - } - it = m_chunks.erase(it); - } - else - ++it; - } -} - -void terrain_streamer::clear() -{ - for (auto &entry : m_chunks) - if (entry.second) - { - if (m_persist && entry.second->modified()) - { - save_heights(entry.first.first, entry.first.second, *entry.second); - m_known[entry.first] |= cf_exists_on_disk; - } - entry.second->destroy(); - } + clear_water(false); m_chunks.clear(); -} - -void terrain_streamer::collect(std::vector &Out) const -{ - for (auto const &entry : m_chunks) - if (entry.second) - Out.push_back(entry.second.get()); -} - -editor_terrain *terrain_streamer::terrain_at(double X, double Z) const -{ - auto const it = m_chunks.find(key_at(X, Z)); - if (it != m_chunks.end() && it->second && it->second->contains(X, Z)) - return it->second.get(); - return nullptr; -} - -void terrain_streamer::save_chunk(int Cx, int Cz, editor_terrain const &Terrain) -{ - save_heights(Cx, Cz, Terrain); - m_known[{Cx, Cz}] |= cf_exists_on_disk; -} - -void terrain_streamer::add_chunk(int Cx, int Cz) -{ - chunk_key const key{Cx, Cz}; - if (m_chunks.count(key)) - return; // already resident - - glm::dvec3 const centre = chunk_centre(Cx, Cz); - auto terrain = std::make_unique(); - if (!terrain->create(centre, m_cells, m_cellsize, m_texture)) // flat - return; - - // persist immediately so it becomes part of the authored, streamable world - save_heights(Cx, Cz, *terrain); - m_known[key] |= cf_exists_on_disk; - - if (m_auto_optimize) - terrain->optimize(m_simplify_error); - m_chunks.emplace(key, std::move(terrain)); + m_packs.clear(); + m_loading.clear(); + m_removed.clear(); + m_palette.clear(); + m_recording = 0; + m_change = change(); + m_active = false; + m_name.clear(); + m_directory.clear(); } void terrain_streamer::reset() { - // do NOT call editor_terrain::destroy() here: on a scenery reload the sections those chunks - // referenced are already gone. just drop our bookkeeping; the old region freed the shapes. + // the sections the shapes were put in are gone with the old region, they aren't to be touched + stop_workers(); + for (auto &entry : m_chunks) + { + entry.second->m_section = nullptr; + if (entry.second->m_material != null_handle && GfxRenderer) + GfxRenderer->Terrain_Release(entry.second->m_material); + } + for (auto &slot : m_slots) + slot.used = false; m_chunks.clear(); - m_known.clear(); + m_packs.clear(); + m_loading.clear(); + m_removed.clear(); + m_palette.clear(); + clear_water(false); + m_recording = 0; + m_change = change(); m_active = false; + m_name.clear(); + m_directory.clear(); + m_hascamera = false; } -void terrain_streamer::flush() +// workers + +void terrain_streamer::start_workers() { + if (false == m_workers.empty()) + return; + m_stop = false; + for (int i = 0; i < worker_count; ++i) + m_workers.emplace_back(&terrain_streamer::worker, this); +} + +void terrain_streamer::stop_workers() +{ + { + std::lock_guard lock{m_mutex}; + m_stop = true; + m_jobs.clear(); + } + m_wakeup.notify_all(); + for (auto &worker : m_workers) + if (worker.joinable()) + worker.join(); + m_workers.clear(); + std::lock_guard lock{m_mutex}; + m_results.clear(); + m_busy = 0; + m_stop = false; for (auto &entry : m_chunks) - if (entry.second && entry.second->modified()) + entry.second->m_building = false; +} + +void terrain_streamer::submit(job Job) +{ + { + std::lock_guard lock{m_mutex}; + m_jobs.push_back(std::move(Job)); + } + m_wakeup.notify_one(); +} + +void terrain_streamer::worker() +{ + while (true) + { + job task; { - save_heights(entry.first.first, entry.first.second, *entry.second); - m_known[entry.first] |= cf_exists_on_disk; - entry.second->clear_modified(); + std::unique_lock lock{m_mutex}; + m_wakeup.wait(lock, [this]() { return m_stop || false == m_jobs.empty(); }); + if (m_stop) + return; + // the nearest work first + auto const next{std::min_element(m_jobs.begin(), m_jobs.end(), [](job const &A, job const &B) { return A.priority < B.priority; })}; + task = std::move(*next); + m_jobs.erase(next); + ++m_busy; } + result done; + done.type = task.type; + done.pack = task.pack; + done.chunk = task.chunk; + done.generation = task.generation; + done.stride = task.stride; + done.version = task.version; + switch (task.type) + { + case job::kind::pack: + { + auto const path{pack_path(task.pack)}; + std::error_code ec; + if (std::filesystem::exists(path, ec)) + { + auto file{std::make_shared()}; + std::lock_guard lock{m_filemutex}; + if (file->open(path, &done.error)) + done.file = file; + } + break; + } + case job::kind::chunk: + { + std::lock_guard lock{m_filemutex}; + done.data = task.file->read(task.chunk, &done.error); + break; + } + case job::kind::mesh: + { + auto const &neighbours{task.neighbours}; + heightmap::build_mesh(*task.data, task.stride, task.origin, + [&neighbours](double const X, double const Z, double &Height) { + for (auto const &neighbour : neighbours) + if (neighbour != nullptr && neighbour->contains(X, Z)) + { + Height = neighbour->height_at(X, Z); + return true; + } + return false; + }, + done.mesh); + break; + } + } + std::lock_guard lock{m_mutex}; + m_results.push_back(std::move(done)); + --m_busy; + } +} + +void terrain_streamer::process(result &Result) +{ + switch (Result.type) + { + case job::kind::pack: + { + auto &pack{m_packs[Result.pack]}; + if (Result.generation != pack.generation) + break; // the file was written meanwhile, and is known already + if (Result.file != nullptr) + { + pack.file = std::move(Result.file); + pack.status = pack_state::state::ready; + } + else + { + if (false == Result.error.empty()) + ErrorLog("Bad file: " + Result.error, logtype::file); + pack.status = pack_state::state::missing; + } + break; + } + case job::kind::chunk: + { + m_loading.erase(Result.chunk); + auto const pack{m_packs.find(heightmap::pack_of(Result.chunk))}; + if (pack == m_packs.end() || pack->second.generation != Result.generation) + break; // read from a file which was written anew since + if (Result.data == nullptr) + { + ErrorLog("Bad file: " + Result.error, logtype::file); + break; + } + if (m_chunks.count(Result.chunk) > 0 || m_removed.count(Result.chunk) > 0) + break; + auto chunk{std::make_unique(std::move(Result.data))}; + auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())}; + auto const corner{chunk->data().corner()}; + chunk->m_low = {corner.x, *low, corner.y}; + chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size}; + m_chunks.emplace(Result.chunk, std::move(chunk)); + touch_neighbours(Result.chunk); + break; + } + case job::kind::mesh: + { + auto const lookup{m_chunks.find(Result.chunk)}; + if (lookup == m_chunks.end()) + break; + auto &chunk{*lookup->second}; + chunk.m_building = false; + if (Result.version < chunk.m_builtversion) + break; + upload(chunk, Result.mesh, Result.stride); + chunk.m_builtversion = Result.version; + break; + } + } +} + +// drawing + +material_handle terrain_streamer::palette_material(std::uint16_t const Layer) +{ + auto const index{Layer < m_manifest.layers.size() ? Layer : std::uint16_t{0}}; + auto const lookup{m_palette.find(index)}; + if (lookup != m_palette.end()) + return lookup->second; + auto const material{m_manifest.layers.empty() ? null_handle : GfxRenderer->Fetch_Material(m_manifest.layers[index].material)}; + m_palette.emplace(index, material); + return material; +} + +void terrain_streamer::update_material(editor_terrain &Chunk) +{ + auto const &data{Chunk.data()}; + std::vector layers; + auto const count{std::max(1, data.layer_count())}; + for (std::size_t slot = 0; slot < count; ++slot) + { + auto const layer{data.layers[slot] == heightmap::no_layer ? std::uint16_t{0} : data.layers[slot]}; + auto const size{layer < m_manifest.layers.size() ? m_manifest.layers[layer].size : 0.f}; + auto const rotation{layer < m_manifest.layers.size() ? m_manifest.layers[layer].rotation : 0.f}; + layers.push_back({palette_material(layer), size, rotation}); + } + auto const corner{data.corner()}; + auto const packcorner{heightmap::pack_corner(heightmap::pack_of(data.key))}; + glm::vec3 const placement{static_cast(corner.x - packcorner.x), static_cast(corner.y - packcorner.y), static_cast(heightmap::chunk_size)}; + auto const painted{data.paint > 0 && count > 1}; + auto const material{GfxRenderer->Terrain_Material(Chunk.m_material, layers, painted ? data.paint : 0, painted ? data.weights.data() : nullptr, placement)}; + if (Chunk.m_material != null_handle && Chunk.m_material != material) + GfxRenderer->Terrain_Release(Chunk.m_material); + Chunk.m_material = material; + Chunk.m_paintedversion = Chunk.m_paintversion; +} + +void terrain_streamer::upload(editor_terrain &Chunk, heightmap::mesh &Mesh, int const Stride) +{ + if (simulation::Region == nullptr || Mesh.vertices.empty()) + return; + auto const cells{Chunk.cells()}; + auto const triangles{Mesh.indices.size() / 3}; + gfx::userdata_array nouserdata; + + // the mesh goes to a geometry slot made for its grid; a slot of the same kind is refilled in place + auto fits = [&](geometry_slot const &Slot) { return Slot.cells == cells && Slot.stride == Stride; }; + if (Chunk.m_slot < 0 || false == fits(m_slots[Chunk.m_slot])) + { + if (Chunk.m_slot >= 0) + m_slots[Chunk.m_slot].used = false; + Chunk.m_slot = -1; + for (std::size_t i = 0; i < m_slots.size(); ++i) + if (false == m_slots[i].used && fits(m_slots[i])) + { + Chunk.m_slot = static_cast(i); + break; + } + if (Chunk.m_slot < 0) + { + geometry_slot slot; + slot.bank = GfxRenderer->Create_Bank(); + slot.cells = cells; + slot.stride = Stride; + slot.geometry = GfxRenderer->Insert(Mesh.indices, Mesh.vertices, nouserdata, slot.bank, GL_TRIANGLES); + if (slot.geometry.chunk == 0) + { + // the shape can't keep the geometry it had, it's given back already + undraw(Chunk); + return; + } + slot.used = true; + Chunk.m_slot = static_cast(m_slots.size()); + m_slotofgeometry[geometry_key(slot.geometry)] = Chunk.m_slot; + m_slots.push_back(slot); + } + else + { + m_slots[Chunk.m_slot].used = true; + GfxRenderer->Replace(Mesh.vertices, nouserdata, m_slots[Chunk.m_slot].geometry, GL_TRIANGLES); + } + } + else + { + GfxRenderer->Replace(Mesh.vertices, nouserdata, m_slots[Chunk.m_slot].geometry, GL_TRIANGLES); + } + auto const &slot{m_slots[Chunk.m_slot]}; + + if (Chunk.m_material == null_handle || Chunk.m_paintedversion != Chunk.m_paintversion) + update_material(Chunk); + + // the shape gets just two bounding vertices: they give it its area, and leave no triangles behind for code which + // reads the source vertices of the section shapes; the terrain answers questions about the ground itself + auto const centre{chunk_centre(Chunk.key())}; + auto §ion{simulation::Region->section(centre)}; + // the section makes the geometry of its own shapes once, before it's drawn; done first, it leaves ours alone + section.create_geometry(); + auto const low{Mesh.low}; + auto const high{Mesh.high}; + std::vector bounds(2); + bounds[0].position = low; + bounds[1].position = high; + scene::shape_node shape; + shape.make_terrain(Chunk.m_material, std::move(bounds), section.m_area.center); + shape.geometry(slot.geometry); + + auto replaced{false}; + if (Chunk.m_section != nullptr) + { + for (auto &existing : Chunk.m_section->m_shapes) + { + auto const &geometry{existing.data().geometry}; + if (geometry.bank == Chunk.m_geometry.bank && geometry.chunk == Chunk.m_geometry.chunk) + { + existing = shape; + replaced = true; + break; + } + } + } + if (false == replaced) + section.m_shapes.emplace_back(shape); + // the section reaches far enough for the chunk not to be culled at its edges + section.m_area.radius = std::max(section.m_area.radius, static_cast(glm::length(section.m_area.center - Mesh.centre) + Mesh.radius)); + Chunk.m_section = §ion; + Chunk.m_geometry = slot.geometry; + Chunk.m_triangles = triangles; + Chunk.m_stride = Stride; + Chunk.m_low = low; + Chunk.m_high = high; +} + +void terrain_streamer::undraw(editor_terrain &Chunk) +{ + if (Chunk.m_section != nullptr && simulation::Region != nullptr) + erase_shape(Chunk.m_section->m_shapes, Chunk.m_geometry); + if (Chunk.m_slot >= 0) + m_slots[Chunk.m_slot].used = false; + if (Chunk.m_material != null_handle && GfxRenderer) + GfxRenderer->Terrain_Release(Chunk.m_material); + Chunk.m_slot = -1; + Chunk.m_material = null_handle; + Chunk.m_section = nullptr; + Chunk.m_geometry = gfx::geometry_handle{0, 0}; + Chunk.m_triangles = 0; + Chunk.m_stride = 0; + Chunk.m_builtversion = 0; + Chunk.m_paintedversion = 0; +} + +heightmap::chunk_ptr terrain_streamer::neighbour(chunk_key const &Key) const +{ + auto const lookup{m_chunks.find(Key)}; + return lookup != m_chunks.end() ? lookup->second->shared() : nullptr; +} + +void terrain_streamer::touch_neighbours(chunk_key const &Key) +{ + for (auto const &offset : {chunk_key{-1, 0}, chunk_key{1, 0}, chunk_key{0, -1}, chunk_key{0, 1}}) + { + auto const lookup{m_chunks.find({Key.first + offset.first, Key.second + offset.second})}; + if (lookup != m_chunks.end()) + lookup->second->m_neighbours = true; + } +} + +int terrain_streamer::wanted_stride(editor_terrain const &Chunk, glm::dvec3 const &Camera) const +{ + // distance from the camera to the box of the chunk + glm::dvec3 const offset{std::max({Chunk.m_low.x - Camera.x, 0.0, Camera.x - Chunk.m_high.x}), std::max({Chunk.m_low.y - Camera.y, 0.0, Camera.y - Chunk.m_high.y}), + std::max({Chunk.m_low.z - Camera.z, 0.0, Camera.z - Chunk.m_high.z})}; + auto const distance{glm::length(offset)}; + auto const spacing{Chunk.data().spacing()}; + auto const limit{heightmap::max_stride(Chunk.cells())}; + auto const stride_for = [&](double const Distance) { + auto const edge{Distance / m_detail}; + int stride{1}; + while (stride * 2 <= limit && stride * 2 * spacing <= edge) + stride *= 2; + return stride; + }; + // a margin around the distances the stride changes at, so a camera standing at one doesn't flip it to and fro + if (Chunk.m_stride > 0) + { + auto const finer{stride_for(distance * 0.85)}; + auto const coarser{stride_for(distance * 1.15)}; + if (Chunk.m_stride >= finer && Chunk.m_stride <= coarser) + return Chunk.m_stride; + } + return stride_for(distance); +} + +void terrain_streamer::request_mesh(editor_terrain &Chunk, int const Stride, double const Priority) +{ + auto const centre{chunk_centre(Chunk.key())}; + job task; + task.type = job::kind::mesh; + task.chunk = Chunk.key(); + task.data = Chunk.shared(); + auto const key{Chunk.key()}; + task.neighbours = {neighbour({key.first - 1, key.second}), neighbour({key.first + 1, key.second}), neighbour({key.first, key.second - 1}), neighbour({key.first, key.second + 1})}; + task.stride = Stride; + task.origin = simulation::Region->section(centre).m_area.center; + task.version = Chunk.m_shapeversion; + task.priority = Priority; + Chunk.m_building = true; + Chunk.m_neighbours = false; + submit(std::move(task)); +} + +void terrain_streamer::update(glm::dvec3 const &Camera) +{ + if (false == m_active || simulation::Region == nullptr) + return; + + // where the camera is heading + auto const now{steady_seconds()}; + if (m_hascamera) + { + auto const elapsed{now - m_cameratime}; + if (elapsed > 1e-3) + { + auto velocity{(Camera - m_camera) / elapsed}; + if (glm::length(velocity) > 300.0) + velocity = glm::dvec3{0.0}; // a jump, not a movement + m_velocity = glm::mix(m_velocity, velocity, std::min(1.0, elapsed * 2.0)); + } + } + m_camera = Camera; + m_cameratime = now; + m_hascamera = true; + auto ahead{m_velocity * look_ahead}; + if (glm::length(ahead) > look_ahead_limit) + ahead *= look_ahead_limit / glm::length(ahead); + auto const target{Camera + ahead}; + + if (m_waterchanged) + build_water(); + + // finished work, within a few milliseconds + while (steady_seconds() - now < upload_budget) + { + result done; + { + std::lock_guard lock{m_mutex}; + if (m_results.empty()) + break; + done = std::move(m_results.front()); + m_results.pop_front(); + } + process(done); + } + + // chunks wanted around the camera and around where it's heading + auto const range{std::max(m_radius * heightmap::chunk_size, static_cast(Global.BaseDrawRange) * std::max(1.0, static_cast(Global.fDistanceFactor))) + heightmap::chunk_size * 0.5}; + auto const radius{std::min(64, static_cast(std::ceil(range / heightmap::chunk_size)))}; + if (m_ringradius != radius) + { + m_ring.clear(); + for (int dz = -radius; dz <= radius; ++dz) + for (int dx = -radius; dx <= radius; ++dx) + if (dx * dx + dz * dz <= (radius + 1) * (radius + 1)) + m_ring.emplace_back(dx, dz); + std::sort(m_ring.begin(), m_ring.end(), [](chunk_key const &A, chunk_key const &B) { return A.first * A.first + A.second * A.second < B.first * B.first + B.second * B.second; }); + m_ringradius = radius; + } + auto const here{heightmap::chunk_at(Camera.x, Camera.z)}; + auto const there{heightmap::chunk_at(target.x, target.z)}; + std::vector centres{here}; + if (there != here) + centres.push_back(there); + for (auto const ¢re : centres) + { + for (auto const &offset : m_ring) + { + if (m_loading.size() >= max_loading) + break; + chunk_key const key{centre.first + offset.first, centre.second + offset.second}; + if (m_chunks.count(key) > 0 || m_loading.count(key) > 0 || m_removed.count(key) > 0) + continue; + auto const packkey{heightmap::pack_of(key)}; + auto &pack{m_packs[packkey]}; + auto const distance{std::sqrt(static_cast(offset.first * offset.first + offset.second * offset.second))}; + if (pack.status == pack_state::state::unknown) + { + pack.status = pack_state::state::loading; + job task; + task.type = job::kind::pack; + task.pack = packkey; + task.generation = pack.generation; + task.priority = distance; + submit(std::move(task)); + continue; + } + if (pack.status != pack_state::state::ready || false == pack.file->has(key)) + continue; + job task; + task.type = job::kind::chunk; + task.chunk = key; + task.pack = packkey; + task.file = pack.file; + task.generation = pack.generation; + task.priority = distance; + m_loading.insert(key); + submit(std::move(task)); + } + } + + // chunks gone out of range are dropped, except edited ones, which wait in memory to be saved + auto const outside = [&](chunk_key const &Key, int const Margin) { + auto const within = [&](chunk_key const &Centre) { + auto const dx{Key.first - Centre.first}, dz{Key.second - Centre.second}; + return dx * dx + dz * dz <= (radius + Margin) * (radius + Margin); + }; + return false == within(here) && false == within(there); + }; + for (auto it = m_chunks.begin(); it != m_chunks.end();) + { + auto &chunk{*it->second}; + if (outside(it->first, 2)) + { + if (chunk.m_section != nullptr || chunk.m_slot >= 0) + undraw(chunk); + if (false == chunk.modified() && false == chunk.m_building) + { + it = m_chunks.erase(it); + continue; + } + } + ++it; + } + + // meshes of the grid each chunk's distance asks for, the nearest first + std::size_t meshing{0}; + { + std::lock_guard lock{m_mutex}; + meshing = m_busy; + for (auto const &queued : m_jobs) + if (queued.type == job::kind::mesh) + ++meshing; + } + std::vector> wanted; + int materials{0}; + for (auto &entry : m_chunks) + { + auto &chunk{*entry.second}; + if (outside(entry.first, 1)) + continue; + if (false == in_region(glm::dvec2{chunk_centre(entry.first).x, chunk_centre(entry.first).z})) + continue; + // paint changes go to the material straight away + if (chunk.m_section != nullptr && chunk.m_paintedversion != chunk.m_paintversion && materials < materials_per_frame) + { + update_material(chunk); + upload_shape_material(chunk); + ++materials; + } + if (chunk.m_building) + continue; + auto const stride{wanted_stride(chunk, Camera)}; + if (chunk.m_stride != stride || chunk.m_builtversion != chunk.m_shapeversion || chunk.m_neighbours) + { + auto const centre{chunk_centre(entry.first)}; + wanted.emplace_back(glm::length(glm::dvec2{centre.x - Camera.x, centre.z - Camera.z}), &chunk); + } + } + std::sort(wanted.begin(), wanted.end(), [](auto const &A, auto const &B) { return A.first < B.first; }); + for (auto const &entry : wanted) + { + if (meshing >= max_meshing) + break; + request_mesh(*entry.second, wanted_stride(*entry.second, Camera), entry.first / heightmap::chunk_size); + ++meshing; + } +} + +void terrain_streamer::upload_shape_material(editor_terrain &Chunk) +{ + if (Chunk.m_section == nullptr) + return; + for (auto &existing : Chunk.m_section->m_shapes) + { + auto const &geometry{existing.data().geometry}; + if (geometry.bank == Chunk.m_geometry.bank && geometry.chunk == Chunk.m_geometry.chunk) + { + // the shape is made anew with the new material, the rest of it stays + std::vector bounds(2); + bounds[0].position = Chunk.m_low; + bounds[1].position = Chunk.m_high; + scene::shape_node shape; + shape.make_terrain(Chunk.m_material, std::move(bounds), existing.data().origin); + shape.geometry(Chunk.m_geometry); + existing = shape; + return; + } + } +} + +// queries + +void terrain_streamer::collect(std::vector &Out) const +{ + for (auto const &entry : m_chunks) + Out.push_back(entry.second.get()); +} + +editor_terrain *terrain_streamer::terrain_at(double X, double Z) const +{ + auto const lookup{m_chunks.find(heightmap::chunk_at(X, Z))}; + if (lookup != m_chunks.end() && lookup->second->contains(X, Z)) + return lookup->second.get(); + return nullptr; +} + +bool terrain_streamer::height_at(double X, double Z, double &Height) const +{ + auto const *chunk{terrain_at(X, Z)}; + if (chunk == nullptr) + return false; + Height = chunk->height_at(X, Z); + return true; +} + +void terrain_streamer::gather_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector> &Out) const +{ + auto const first{heightmap::chunk_at(Min.x, Min.y)}; + auto const last{heightmap::chunk_at(Max.x, Max.y)}; + for (int cz = first.second; cz <= last.second; ++cz) + for (int cx = first.first; cx <= last.first; ++cx) + { + auto const lookup{m_chunks.find({cx, cz})}; + if (lookup == m_chunks.end()) + continue; + auto const &data{lookup->second->data()}; + auto const low{data.corner()}; + auto const step{data.spacing()}; + // the cells overlapping the rectangle + auto const xs{index_range(low.x, step, data.cells, Min.x - step, Max.x)}; + auto const zs{index_range(low.y, step, data.cells, Min.y - step, Max.y)}; + auto const point = [&](int const Ix, int const Iz) { return glm::dvec3{low.x + Ix * step, data.height(Ix, Iz), low.y + Iz * step}; }; + for (int iz = zs.first; iz <= zs.second && iz < data.cells; ++iz) + for (int ix = xs.first; ix <= xs.second && ix < data.cells; ++ix) + { + auto const v00{point(ix, iz)}, v10{point(ix + 1, iz)}, v01{point(ix, iz + 1)}, v11{point(ix + 1, iz + 1)}; + Out.push_back({v00, v01, v10}); + Out.push_back({v11, v10, v01}); + } + } +} + +bool terrain_streamer::owns(gfx::geometry_handle const &Geometry) const +{ + if (Geometry.bank == 0 && Geometry.chunk == 0) + return false; + if (m_slotofgeometry.count(geometry_key(Geometry)) > 0) + return true; + for (auto const &water : m_watershapes) + if (water.geometry.bank == Geometry.bank && water.geometry.chunk == Geometry.chunk) + return true; + return false; +} + +bool terrain_streamer::exists(chunk_key const &Key) const +{ + if (m_chunks.count(Key) > 0) + return true; + if (m_removed.count(Key) > 0) + return false; + auto const pack{m_packs.find(heightmap::pack_of(Key))}; + return pack != m_packs.end() && pack->second.status == pack_state::state::ready && pack->second.file->has(Key); +} + +// editing + +editor_terrain *terrain_streamer::load_now(chunk_key const &Key) +{ + auto const lookup{m_chunks.find(Key)}; + if (lookup != m_chunks.end()) + return lookup->second.get(); + if (m_removed.count(Key) > 0) + return nullptr; + auto const packkey{heightmap::pack_of(Key)}; + auto &pack{m_packs[packkey]}; + if (pack.status != pack_state::state::ready && pack.status != pack_state::state::missing) + { + // the index is read here and now; a worker reading it meanwhile is ignored by the generation + ++pack.generation; + pack.status = pack_state::state::missing; + auto const path{pack_path(packkey)}; + std::error_code ec; + if (std::filesystem::exists(path, ec)) + { + auto file{std::make_shared()}; + std::string error; + std::lock_guard lock{m_filemutex}; + if (file->open(path, &error)) + { + pack.file = file; + pack.status = pack_state::state::ready; + } + else + ErrorLog("Bad file: " + error, logtype::file); + } + } + if (pack.status != pack_state::state::ready || false == pack.file->has(Key)) + return nullptr; + std::shared_ptr data; + std::string error; + { + std::lock_guard lock{m_filemutex}; + data = pack.file->read(Key, &error); + } + if (data == nullptr) + { + ErrorLog("Bad file: " + error, logtype::file); + return nullptr; + } + auto chunk{std::make_unique(std::move(data))}; + auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())}; + auto const corner{chunk->data().corner()}; + chunk->m_low = {corner.x, *low, corner.y}; + chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size}; + auto *result{chunk.get()}; + m_chunks.emplace(Key, std::move(chunk)); + m_loading.erase(Key); + touch_neighbours(Key); + return result; +} + +std::vector terrain_streamer::chunks_in(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Load) +{ + std::vector result; + auto const first{heightmap::chunk_at(Min.x, Min.y)}; + auto const last{heightmap::chunk_at(Max.x, Max.y)}; + for (int cz = first.second; cz <= last.second; ++cz) + for (int cx = first.first; cx <= last.first; ++cx) + { + auto const lookup{m_chunks.find({cx, cz})}; + if (lookup != m_chunks.end()) + result.push_back(lookup->second.get()); + else if (Load) + { + if (auto *chunk{load_now({cx, cz})}) + result.push_back(chunk); + } + } + return result; +} + +bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, float Height, std::uint16_t const Layer) +{ + return add_chunk(Cx, Cz, Spacing, [Height](double, double) { return Height; }, Layer); +} + +bool terrain_streamer::add_chunk(int Cx, int Cz, float Spacing, std::function const &Height, std::uint16_t const Layer) +{ + chunk_key const key{Cx, Cz}; + if (false == m_active || load_now(key) != nullptr || m_chunks.count(key) > 0) + return false; + if (false == in_region(glm::dvec2{chunk_centre(key).x, chunk_centre(key).z})) + return false; + auto data{heightmap::chunk_data::make_flat(key, Spacing, 0.f)}; + data->layers[0] = Layer < m_manifest.layers.size() ? Layer : std::uint16_t{0}; + auto const low{data->corner()}; + auto const step{data->spacing()}; + for (int iz = 0; iz <= data->cells; ++iz) + for (int ix = 0; ix <= data->cells; ++ix) + data->heights[static_cast(iz) * data->side() + ix] = Height(low.x + ix * step, low.y + iz * step); + return add_chunk(std::move(data)); +} + +bool terrain_streamer::add_chunk(std::shared_ptr Data) +{ + if (Data == nullptr || false == Data->valid()) + return false; + auto const key{Data->key}; + if (false == m_active || load_now(key) != nullptr || m_chunks.count(key) > 0) + return false; + if (false == in_region(glm::dvec2{chunk_centre(key).x, chunk_centre(key).z})) + return false; + for (auto &layer : Data->layers) + if (layer != heightmap::no_layer && layer >= m_manifest.layers.size()) + layer = 0; + auto data{std::move(Data)}; + auto const low{data->corner()}; + remember(key, nullptr); + auto chunk{std::make_unique(std::move(data))}; + chunk->m_modified = true; + auto const [lowest, highest]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())}; + chunk->m_low = {low.x, *lowest, low.y}; + chunk->m_high = {low.x + heightmap::chunk_size, *highest, low.y + heightmap::chunk_size}; + auto &added{*chunk}; + m_chunks.emplace(key, std::move(chunk)); + m_removed.erase(key); + m_loading.erase(key); + apply_modifiers(added, low, low + glm::dvec2{heightmap::chunk_size}); + touch_neighbours(key); + return true; } void terrain_streamer::remove_chunk(int Cx, int Cz) { chunk_key const key{Cx, Cz}; - auto const it = m_chunks.find(key); - if (it != m_chunks.end()) + if (recording()) { - if (it->second) - it->second->destroy(); - m_chunks.erase(it); + // the data is needed to bring the chunk back + if (auto const *chunk{load_now(key)}) + remember(*chunk); + } + auto const lookup{m_chunks.find(key)}; + if (lookup != m_chunks.end()) + { + undraw(*lookup->second); + m_chunks.erase(lookup); + } + m_removed.insert(key); + m_loading.erase(key); + touch_neighbours(key); +} + +bool terrain_streamer::spacing(int Cx, int Cz, float Spacing, int Paint) +{ + auto *chunk{load_now({Cx, Cz})}; + if (chunk == nullptr) + return false; + auto const ¤t{chunk->data()}; + auto const repaint{Paint > 0 && current.paint > 0 && Paint != current.paint}; + if (heightmap::cells_for(Spacing) == current.cells && false == repaint) + return false; + remember(*chunk); + chunk->replace(current.resampled(Spacing, Paint), true, repaint); + auto const low{chunk->data().corner()}; + apply_modifiers(*chunk, low, low + glm::dvec2{heightmap::chunk_size}); + touch_neighbours(chunk->key()); + return true; +} + +bool terrain_streamer::reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std::function const &Shaper) +{ + bool changed{false}; + for (auto *chunk : chunks_in(Min, Max, true)) + { + auto const ¤t{chunk->data()}; + auto const low{current.corner()}; + auto const step{current.spacing()}; + auto const xs{index_range(low.x, step, current.side(), Min.x, Max.x)}; + auto const zs{index_range(low.y, step, current.side(), Min.y, Max.y)}; + if (xs.first > xs.second || zs.first > zs.second) + continue; + if (current.base.empty() || false == m_touchup) + { + // a copy is shaped, and taken only if anything changed + auto heights{current.base.empty() ? current.heights : current.base}; + bool shaped{false}; + for (int iz = zs.first; iz <= zs.second; ++iz) + for (int ix = xs.first; ix <= xs.second; ++ix) + shaped |= Shaper(low.x + ix * step, low.y + iz * step, heights[static_cast(iz) * current.side() + ix]); + if (false == shaped) + continue; + remember(*chunk); + auto &data{chunk->edit(true, false)}; + (data.base.empty() ? data.heights : data.base) = std::move(heights); + if (false == data.base.empty()) + apply_modifiers(*chunk, Min, Max); + changed = true; + continue; + } + // the terrain as it's seen is shaped. where a modifier shapes the ground (or a touch-up is there already) the change + // is a touch-up over it, elsewhere it's the ground under the modifiers, which may then reach further + auto heights{current.heights}; + auto base{current.base}; + auto adjust{current.adjust.empty() ? std::vector(heights.size(), 0.f) : current.adjust}; + bool shaped{false}, touched{false}; + for (int iz = zs.first; iz <= zs.second; ++iz) + for (int ix = xs.first; ix <= xs.second; ++ix) + { + auto const index{static_cast(iz) * current.side() + ix}; + auto height{heights[index]}; + if (false == Shaper(low.x + ix * step, low.y + iz * step, height)) + continue; + shaped = true; + auto const change{height - heights[index]}; + auto const shapedbymodifier{std::abs(heights[index] - adjust[index] - base[index]) > 0.0005f}; + if (shapedbymodifier || adjust[index] != 0.f) + { + adjust[index] += change; + touched = true; + } + else + base[index] += change; + heights[index] = height; + } + if (false == shaped) + continue; + remember(*chunk); + auto &data{chunk->edit(true, false)}; + data.base = std::move(base); + if (touched) + data.adjust = std::move(adjust); + data.heights = std::move(heights); + apply_modifiers(*chunk, Min, Max); + changed = true; + } + return changed; +} + +bool terrain_streamer::restore_stitching(double X, double Z, double Radius, double Amount) +{ + if (Radius <= 0.0 || Amount <= 0.0) + return false; + glm::dvec2 const low{X - Radius, Z - Radius}, high{X + Radius, Z + Radius}; + bool changed{false}; + for (auto *chunk : chunks_in(low, high, false)) + { + auto const ¤t{chunk->data()}; + if (current.adjust.empty()) + continue; + auto const corner{current.corner()}; + auto const step{current.spacing()}; + auto const xs{index_range(corner.x, step, current.side(), low.x, high.x)}; + auto const zs{index_range(corner.y, step, current.side(), low.y, high.y)}; + auto adjust{current.adjust}; + bool restored{false}; + for (int iz = zs.first; iz <= zs.second; ++iz) + for (int ix = xs.first; ix <= xs.second; ++ix) + { + auto const vx{corner.x + ix * step}, vz{corner.y + iz * step}; + auto const distance{std::sqrt((vx - X) * (vx - X) + (vz - Z) * (vz - Z))}; + auto &value{adjust[static_cast(iz) * current.side() + ix]}; + if (distance > Radius || value == 0.f) + continue; + value *= static_cast(1.0 - std::min(1.0, Amount * falloff(distance, Radius))); + // what's below the step of the stored heights is gone + if (std::abs(value) < heightmap::height_step * 0.5) + value = 0.f; + restored = true; + } + if (false == restored) + continue; + remember(*chunk); + auto &data{chunk->edit(true, false)}; + if (std::all_of(adjust.begin(), adjust.end(), [](float const Value) { return Value == 0.f; })) + data.adjust.clear(); + else + data.adjust = std::move(adjust); + apply_modifiers(*chunk, low, high); + changed = true; + } + return changed; +} + +std::size_t terrain_streamer::touched_up() const +{ + std::size_t count{0}; + for (auto const &entry : m_chunks) + { + auto const &adjust{entry.second->data().adjust}; + count += static_cast(std::count_if(adjust.begin(), adjust.end(), [](float const Value) { return Value != 0.f; })); + } + return count; +} + +bool terrain_streamer::sculpt(double X, double Z, double Radius, double Strength) +{ + if (Radius <= 0.0) + return false; + return reshape(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, [&](double const Vx, double const Vz, float &Height) { + auto const distance{std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z))}; + if (distance > Radius) + return false; + Height += static_cast(Strength * falloff(distance, Radius)); + return true; + }); +} + +bool terrain_streamer::smooth(double X, double Z, double Radius, double Amount) +{ + if (Radius <= 0.0 || Amount <= 0.0) + return false; + glm::dvec2 const low{X - Radius, Z - Radius}, high{X + Radius, Z + Radius}; + // every chunk the brush touches is smoothed from the same ground, so the edges they share stay together + std::vector>> before; + for (auto *chunk : chunks_in(low - glm::dvec2{6.0}, high + glm::dvec2{6.0}, false)) + before.emplace_back(chunk, (chunk->data().base.empty() || m_touchup) ? chunk->heights() : chunk->data().base); + auto const ground = [&](double const Gx, double const Gz, double const Fallback) { + for (auto const &entry : before) + if (entry.first->contains(Gx, Gz)) + return entry.first->height_in(entry.second, Gx, Gz); + return Fallback; + }; + auto const finest{std::accumulate(before.begin(), before.end(), heightmap::chunk_size, [](double Value, auto const &Entry) { return std::min(Value, Entry.first->data().spacing()); })}; + return reshape(low, high, [&](double const Vx, double const Vz, float &Height) { + auto const distance{std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z))}; + if (distance > Radius) + return false; + auto const own{static_cast(Height)}; + auto const step{finest}; + // the neighbours a cell away, read from the ground as it was before the stroke, whichever chunk holds them + auto const average{(ground(Vx - step, Vz, own) + ground(Vx + step, Vz, own) + ground(Vx, Vz - step, own) + ground(Vx, Vz + step, own) + own * 4.0) / 8.0}; + auto const smoothed{own + (average - own) * std::min(1.0, Amount * falloff(distance, Radius))}; + if (std::abs(smoothed - own) < 1e-5) + return false; + Height = static_cast(smoothed); + return true; + }); +} + +bool terrain_streamer::level(double X, double Z, double Radius, double Target, double Amount, int const Mode) +{ + if (Radius <= 0.0 || Amount <= 0.0) + return false; + return reshape(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, [&](double const Vx, double const Vz, float &Height) { + auto const distance{std::sqrt((Vx - X) * (Vx - X) + (Vz - Z) * (Vz - Z))}; + if (distance > Radius) + return false; + auto const own{static_cast(Height)}; + if ((Mode == 1 && own >= Target) || (Mode == 2 && own <= Target)) + return false; + // the middle of the brush gets there, the rim follows less; what's within the step of the stored heights is there already + auto const levelled{own + (Target - own) * std::min(1.0, Amount * falloff(distance, Radius))}; + auto const result{std::abs(Target - levelled) < heightmap::height_step ? Target : levelled}; + if (std::abs(result - own) < 1e-5) + return false; + Height = static_cast(result); + return true; + }); +} + +bool terrain_streamer::paint(double X, double Z, double Radius, double Strength, std::uint16_t Layer) +{ + if (Radius <= 0.0 || Strength <= 0.0) + return false; + return paint_area(glm::dvec2{X - Radius, Z - Radius}, glm::dvec2{X + Radius, Z + Radius}, Layer, false, [&](double const Px, double const Pz) { + auto const distance{std::sqrt((Px - X) * (Px - X) + (Pz - Z) * (Pz - Z))}; + return distance > Radius ? 0.0 : Strength * falloff(distance, Radius); + }); +} + +bool terrain_streamer::fill(std::vector const &Outline, double Feather, std::uint16_t Layer, double Strength) +{ + if (Outline.size() < 3 || Strength <= 0.0) + return false; + Feather = std::max(0.0, Feather); + glm::dvec2 low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + for (auto const &point : Outline) + { + low = glm::min(low, point); + high = glm::max(high, point); + } + // the blend runs across the outline, half of it inside + auto const margin{Feather * 0.5}; + return paint_area(low - glm::dvec2{margin}, high + glm::dvec2{margin}, Layer, true, [&](double const Px, double const Pz) { + glm::dvec2 const point{Px, Pz}; + bool inside{false}; + auto nearest{std::numeric_limits::max()}; + for (std::size_t i = 0, j = Outline.size() - 1; i < Outline.size(); j = i++) + { + auto const &a{Outline[i]}; + auto const &b{Outline[j]}; + if ((a.y > Pz) != (b.y > Pz) && Px < (b.x - a.x) * (Pz - a.y) / (b.y - a.y) + a.x) + inside = !inside; + if (Feather > 0.0) + { + auto const run{b - a}; + auto const length{glm::dot(run, run)}; + auto const t{length > 1e-12 ? std::clamp(glm::dot(point - a, run) / length, 0.0, 1.0) : 0.0}; + nearest = std::min(nearest, glm::length(point - (a + run * t))); + } + } + if (Feather <= 0.0) + return inside ? Strength : 0.0; + // signed distance from the outline, inside positive, led smoothly through the blend + auto const t{std::clamp(((inside ? nearest : -nearest) + margin) / Feather, 0.0, 1.0)}; + return Strength * t * t * (3.0 - 2.0 * t); + }); +} + +bool terrain_streamer::paint_area(glm::dvec2 const &Min, glm::dvec2 const &Max, std::uint16_t const Layer, bool const Load, std::function const &Amount) +{ + bool changed{false}; + for (auto *chunk : chunks_in(Min, Max, Load)) + { + auto const ¤t{chunk->data()}; + auto const samples{current.paint > 0 ? current.paint : heightmap::default_paint_samples}; + auto const step{heightmap::chunk_size / samples}; + auto const low{current.corner()}; + auto const xs{index_range(low.x, step, samples + 1, Min.x, Max.x)}; + auto const zs{index_range(low.y, step, samples + 1, Min.y, Max.y)}; + if (xs.first > xs.second || zs.first > zs.second) + continue; + if (current.paint <= 0 && current.layers[0] == Layer) + continue; // the chunk is all of this material already + auto data{std::make_shared(current)}; + data->ensure_paint(samples); + auto slot{data->slot_of(Layer)}; + if (slot < 0) + { + data->compact_layers(); + data->ensure_paint(samples); + auto const count{data->layer_count()}; + if (count >= heightmap::max_chunk_layers) + { + WriteLog("Editor: chunk " + std::to_string(current.key.first) + "," + std::to_string(current.key.second) + " has " + std::to_string(count) + " materials already, no room for another", + logtype::generic); + continue; + } + slot = static_cast(count); + data->layers[slot] = Layer; + data->weights.resize(data->weights.size() + static_cast(samples + 1) * (samples + 1), 0); + } + auto const plane{static_cast(samples + 1) * (samples + 1)}; + auto const count{data->layer_count()}; + bool painted{false}; + for (int iz = zs.first; iz <= zs.second; ++iz) + for (int ix = xs.first; ix <= xs.second; ++ix) + { + auto const amount{std::clamp(Amount(low.x + ix * step, low.y + iz * step), 0.0, 1.0)}; + if (amount <= 0.0) + continue; + auto const sample{static_cast(iz) * (samples + 1) + ix}; + auto &own{data->weights[slot * plane + sample]}; + auto const target{static_cast(std::lround(own + (255 - own) * amount))}; + if (target == own) + continue; + // the other layers give way in proportion to what they have, the weights still adding up to 255 + auto const others{255 - static_cast(own)}; + auto const left{255 - target}; + int given{0}; + for (std::size_t other = 0; other < count; ++other) + { + if (static_cast(other) == slot) + continue; + auto &weight{data->weights[other * plane + sample]}; + auto const scaled{others > 0 ? static_cast(weight) * left / others : 0}; + weight = static_cast(scaled); + given += scaled; + } + own = static_cast(255 - given); + painted = true; + } + if (false == painted) + continue; + data->compact_layers(); + remember(*chunk); + chunk->replace(std::move(data), false, true); + changed = true; + } + return changed; +} + +std::uint16_t terrain_streamer::layer(std::string const &Material, float const Size, float const Rotation) +{ + for (std::size_t i = 0; i < m_manifest.layers.size(); ++i) + if (m_manifest.layers[i].material == Material && (Size <= 0.f || m_manifest.layers[i].size == Size) && m_manifest.layers[i].rotation == Rotation) + return static_cast(i); + remember_manifest(); + m_manifest.layers.push_back({Material, std::max(0.f, Size), Rotation}); + m_manifestchanged = true; + return static_cast(m_manifest.layers.size() - 1); +} + +void terrain_streamer::layer_size(std::uint16_t const Layer, float const Size) +{ + if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].size == Size) + return; + remember_manifest(); + m_manifest.layers[Layer].size = std::max(0.f, Size); + m_manifestchanged = true; + // the materials of the chunks using it are made again + for (auto &entry : m_chunks) + if (entry.second->data().slot_of(Layer) >= 0) + entry.second->m_paintedversion = 0; +} + +void terrain_streamer::layer_rotation(std::uint16_t const Layer, float Rotation) +{ + Rotation = static_cast(std::remainder(static_cast(Rotation), 360.0)); + if (Layer >= m_manifest.layers.size() || m_manifest.layers[Layer].rotation == Rotation) + return; + remember_manifest(); + m_manifest.layers[Layer].rotation = Rotation; + m_manifestchanged = true; + for (auto &entry : m_chunks) + if (entry.second->data().slot_of(Layer) >= 0 || (Layer == 0 && entry.second->data().layer_count() == 0)) + entry.second->m_paintedversion = 0; +} + +void terrain_streamer::layer_material(std::uint16_t const Layer, std::string const &Material) +{ + if (Layer >= m_manifest.layers.size() || Material.empty() || m_manifest.layers[Layer].material == Material) + return; + remember_manifest(); + m_manifest.layers[Layer].material = Material; + m_manifestchanged = true; + m_palette.erase(Layer); + for (auto &entry : m_chunks) + if (entry.second->data().slot_of(Layer) >= 0 || (Layer == 0 && entry.second->data().layer_count() == 0)) + entry.second->m_paintedversion = 0; +} + +void terrain_streamer::compress(bool const State) +{ + if (m_manifest.compress == State) + return; + m_manifest.compress = State; + m_manifestchanged = true; +} + +void terrain_streamer::default_spacing(float const Spacing) +{ + auto const spacing{heightmap::valid_spacing(Spacing)}; + if (m_manifest.spacing == spacing) + return; + m_manifest.spacing = spacing; + m_manifestchanged = true; +} + +void terrain_streamer::water(std::vector Water) +{ + remember_manifest(); + m_manifest.water = std::move(Water); + m_manifestchanged = true; + m_waterchanged = true; +} + +void terrain_streamer::water_material(std::string const &Material) +{ + if (Material.empty() || Material == m_manifest.water_material) + return; + remember_manifest(); + m_manifest.water_material = Material; + m_manifestchanged = true; + m_waterchanged = true; +} + +void terrain_streamer::apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max) +{ + auto const ¤t{Chunk.data()}; + auto const low{current.corner()}; + auto const high{low + glm::dvec2{heightmap::chunk_size}}; + std::vector shapes; + shapes.reserve(m_manifest.modifiers.size()); + bool reachesany{false}; + for (auto const &modifier : m_manifest.modifiers) + { + heightmap::modifier_shape shape{modifier}; + if (false == shape.reaches(low, high)) + continue; + reachesany = true; + if (shape.reaches(Min, Max)) + shapes.push_back(std::move(shape)); + } + if (false == reachesany) + { + // no modifier reaches the chunk (any more): its own heights are what's left, with the touch-ups made over the modifiers + if (false == current.base.empty()) + { + remember(Chunk); + auto &data{Chunk.edit(true, false)}; + data.heights = std::move(data.base); + if (data.adjust.size() == data.heights.size()) + for (std::size_t index = 0; index < data.heights.size(); ++index) + data.heights[index] += data.adjust[index]; + data.base.clear(); + data.adjust.clear(); + } + return; + } + remember(Chunk); + auto &data{Chunk.edit(true, false)}; + if (data.base.empty()) + { + data.base = data.heights; + data.adjust.clear(); + } + auto const touchups{data.adjust.size() == data.heights.size()}; + auto const step{data.spacing()}; + auto const xs{index_range(low.x, step, data.side(), std::max(Min.x, low.x), std::min(Max.x, high.x))}; + auto const zs{index_range(low.y, step, data.side(), std::max(Min.y, low.y), std::min(Max.y, high.y))}; + for (int iz = zs.first; iz <= zs.second; ++iz) + for (int ix = xs.first; ix <= xs.second; ++ix) + { + auto const index{static_cast(iz) * data.side() + ix}; + auto height{data.base[index]}; + auto const x{low.x + ix * step}, z{low.y + iz * step}; + for (auto const &shape : shapes) + shape.apply(x, z, height); + data.heights[index] = touchups ? height + data.adjust[index] : height; + } +} + +void terrain_streamer::apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const &Max) +{ + for (auto *chunk : chunks_in(Min, Max, true)) + apply_modifiers(*chunk, Min, Max); +} + +void terrain_streamer::modifier(heightmap::modifier Modifier) +{ + remember_manifest(); + auto bounds{Modifier.bounds()}; + auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Modifier.name; })}; + if (existing != m_manifest.modifiers.end()) + { + // the ground it changed before is led anew as well + auto const old{existing->bounds()}; + bounds.first = glm::min(bounds.first, old.first); + bounds.second = glm::max(bounds.second, old.second); + *existing = std::move(Modifier); + } + else + m_manifest.modifiers.push_back(std::move(Modifier)); + m_manifestchanged = true; + apply_modifiers(bounds.first, bounds.second); +} + +bool terrain_streamer::remove_modifier(std::string const &Name) +{ + auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })}; + if (existing == m_manifest.modifiers.end()) + return false; + remember_manifest(); + auto const bounds{existing->bounds()}; + m_manifest.modifiers.erase(existing); + m_manifestchanged = true; + apply_modifiers(bounds.first, bounds.second); + return true; +} + +bool terrain_streamer::bake_modifier(std::string const &Name) +{ + auto const existing{std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })}; + if (existing == m_manifest.modifiers.end()) + return false; + remember_manifest(); + auto const baked{*existing}; + auto const bounds{baked.bounds()}; + heightmap::modifier_shape const shape{baked}; + // the ground under the modifiers takes the shape this one gives it; the other modifiers and the touch-ups go on top + for (auto *chunk : chunks_in(bounds.first, bounds.second, true)) + { + auto const ¤t{chunk->data()}; + if (current.base.empty()) + continue; + auto const low{current.corner()}; + auto const step{current.spacing()}; + auto const xs{index_range(low.x, step, current.side(), bounds.first.x, bounds.second.x)}; + auto const zs{index_range(low.y, step, current.side(), bounds.first.y, bounds.second.y)}; + auto base{current.base}; + bool shaped{false}; + for (int iz = zs.first; iz <= zs.second; ++iz) + for (int ix = xs.first; ix <= xs.second; ++ix) + shaped |= shape.apply(low.x + ix * step, low.y + iz * step, base[static_cast(iz) * current.side() + ix]); + if (false == shaped) + continue; + remember(*chunk); + chunk->edit(true, false).base = std::move(base); + } + m_manifest.modifiers.erase(std::find_if(m_manifest.modifiers.begin(), m_manifest.modifiers.end(), [&](heightmap::modifier const &Item) { return Item.name == Name; })); + m_manifestchanged = true; + apply_modifiers(bounds.first, bounds.second); + return true; +} + +bool terrain_streamer::modified() const +{ + if (m_manifestchanged || false == m_removed.empty()) + return true; + return std::any_of(m_chunks.begin(), m_chunks.end(), [](auto const &Entry) { return Entry.second->modified(); }); +} + +// undo + +void terrain_streamer::begin_change() +{ + if (m_recording++ == 0) + m_change = change(); +} + +terrain_streamer::change terrain_streamer::end_change() +{ + if (m_recording == 0) + return change(); + if (--m_recording > 0) + return change(); // the outer change goes on + change result{std::move(m_change)}; + m_change = change(); + return result; +} + +void terrain_streamer::remember(chunk_key const &Key, heightmap::chunk_ptr const &Data) +{ + if (m_recording > 0) + m_change.chunks.emplace(Key, Data); // only the first state counts +} + +void terrain_streamer::remember_manifest() +{ + if (m_recording == 0 || m_change.manifest) + return; + m_change.manifest = true; + m_change.layers = m_manifest.layers; + m_change.water = m_manifest.water; + m_change.water_material = m_manifest.water_material; + m_change.modifiers = m_manifest.modifiers; +} + +terrain_streamer::change terrain_streamer::restore(change const &Change) +{ + change opposite; + if (false == m_active) + return opposite; + for (auto const &[key, data] : Change.chunks) + { + auto *current{load_now(key)}; + opposite.chunks.emplace(key, current != nullptr ? current->shared() : nullptr); + if (data == nullptr) + { + if (current != nullptr) + { + undraw(*current); + m_chunks.erase(key); + m_removed.insert(key); + touch_neighbours(key); + } + continue; + } + if (current != nullptr) + { + current->replace(data, true, true); + } + else + { + auto chunk{std::make_unique(data)}; + chunk->m_modified = true; + auto const [low, high]{std::minmax_element(chunk->heights().begin(), chunk->heights().end())}; + auto const corner{data->corner()}; + chunk->m_low = {corner.x, *low, corner.y}; + chunk->m_high = {corner.x + heightmap::chunk_size, *high, corner.y + heightmap::chunk_size}; + m_chunks.emplace(key, std::move(chunk)); + m_removed.erase(key); + m_loading.erase(key); + } + touch_neighbours(key); + } + if (Change.manifest) + { + opposite.manifest = true; + opposite.layers = m_manifest.layers; + opposite.water = m_manifest.water; + opposite.water_material = m_manifest.water_material; + opposite.modifiers = m_manifest.modifiers; + auto const samelayers{std::equal(Change.layers.begin(), Change.layers.end(), m_manifest.layers.begin(), m_manifest.layers.end(), + [](heightmap::layer_def const &A, heightmap::layer_def const &B) { return A.material == B.material && A.size == B.size && A.rotation == B.rotation; })}; + m_manifest.layers = Change.layers; + m_manifest.modifiers = Change.modifiers; + m_manifest.water = Change.water; + if (false == Change.water_material.empty()) + m_manifest.water_material = Change.water_material; + m_manifestchanged = true; + m_waterchanged = true; + if (false == samelayers) + { + // the materials of the chunks are made anew from the palette as it was + m_palette.clear(); + for (auto &entry : m_chunks) + entry.second->m_paintedversion = 0; + } + } + return opposite; +} + +bool terrain_streamer::save(std::string *Error) +{ + if (false == m_active) + return true; + auto const fail = [&](std::string const &Message) { + if (Error != nullptr) + *Error = Message; + ErrorLog("Heightmap terrain: " + Message); + return false; + }; + std::error_code ec; + auto const manifestpath{m_directory + "terrain.yaml"}; + if (m_manifestchanged || modified() || false == std::filesystem::exists(manifestpath, ec)) + { + std::string error; + if (false == heightmap::save_manifest(manifestpath, m_manifest, &error)) + return fail(error); + m_manifestchanged = false; + } + + // the packs holding changed chunks are written whole + std::set packs; + for (auto const &entry : m_chunks) + if (entry.second->modified()) + packs.insert(heightmap::pack_of(entry.first)); + for (auto const &key : m_removed) + packs.insert(heightmap::pack_of(key)); + + for (auto const &packkey : packs) + { + auto const path{pack_path(packkey)}; + std::vector chunks; + std::set taken; + // what's in the file now, unless it's in memory or removed + std::shared_ptr file; + if (std::filesystem::exists(path, ec)) + { + file = std::make_shared(); + std::string error; + std::lock_guard lock{m_filemutex}; + if (false == file->open(path, &error)) + return fail(error); + for (auto const &key : file->chunks()) + { + if (m_removed.count(key) > 0 || m_chunks.count(key) > 0) + continue; + auto data{file->read(key, &error)}; + if (data == nullptr) + return fail(error); + chunks.push_back(std::move(data)); + taken.insert(key); + } + } + for (auto const &entry : m_chunks) + if (heightmap::pack_of(entry.first) == packkey && taken.count(entry.first) == 0) + chunks.push_back(entry.second->shared()); + { + std::lock_guard lock{m_filemutex}; + if (chunks.empty()) + std::filesystem::remove(path, ec); + else + { + std::string error; + if (false == heightmap::write_pack(path, packkey, chunks, m_manifest.compress, &error)) + return fail(error); + } + } + // the file is known anew; what the workers read from the old one is ignored + auto &pack{m_packs[packkey]}; + ++pack.generation; + pack.file.reset(); + pack.status = pack_state::state::missing; + if (false == chunks.empty()) + { + auto reopened{std::make_shared()}; + std::string error; + std::lock_guard lock{m_filemutex}; + if (reopened->open(path, &error)) + { + pack.file = reopened; + pack.status = pack_state::state::ready; + } + } + for (auto it = m_removed.begin(); it != m_removed.end();) + it = (heightmap::pack_of(*it) == packkey ? m_removed.erase(it) : std::next(it)); + for (auto const &entry : m_chunks) + if (heightmap::pack_of(entry.first) == packkey) + entry.second->clear_modified(); + } + WriteLog("Heightmap terrain saved: " + m_directory + " (" + std::to_string(packs.size()) + " pack files written)", logtype::generic); + return true; +} + +// water + +void terrain_streamer::clear_water(bool const Touch) +{ + if (Touch && simulation::Region != nullptr) + for (auto const &water : m_watershapes) + { + if (water.cell != nullptr) + erase_shape(water.cell->m_shapestranslucent, water.geometry); + else if (water.section != nullptr) + erase_shape(water.section->m_shapes, water.geometry); + } + // the geometry stays with the renderer, to be filled anew + for (auto const &water : m_watershapes) + m_waterfree.push_back(water.geometry); + m_watershapes.clear(); +} + +void terrain_streamer::build_water() +{ + m_waterchanged = false; + clear_water(true); + if (simulation::Region == nullptr) + return; + for (auto const &body : m_manifest.water) + { + if (body.outline.size() < 3) + continue; + auto const indices{heightmap::triangulate(body.outline)}; + if (indices.empty()) + continue; + glm::dvec2 centroid{0.0}; + for (auto const &point : body.outline) + centroid += point; + centroid /= static_cast(body.outline.size()); + if (false == in_region(centroid)) + continue; + auto const material{GfxRenderer->Fetch_Material(body.material.empty() ? m_manifest.water_material : body.material)}; + auto const *materialdata{GfxRenderer->Material(material)}; + auto const repeat{body.size > 0.f ? static_cast(body.size) : (materialdata != nullptr && materialdata->GetSize().x > 0.f) ? static_cast(materialdata->GetSize().x) : 8.0}; + glm::dvec3 const centre{centroid.x, body.level, centroid.y}; + auto §ion{simulation::Region->section(centre)}; + // the section makes the geometry of its own shapes once, before it's drawn; done first, it leaves ours alone + section.create_geometry(); + // translucent water goes with the translucent geometry of the cell around its middle, drawn relative to the cell + auto const translucent{materialdata != nullptr && materialdata->is_translucent()}; + auto &cell{section.cell(centre)}; + auto const origin{translucent ? cell.m_area.center : section.m_area.center}; + gfx::vertex_array vertices; + vertices.reserve(indices.size()); + double reach{0.0}; + // the outline is turned counterclockwise in the plan, the triangles go the other way round to face up + for (std::size_t i = 0; i + 2 < indices.size(); i += 3) + for (auto const index : {indices[i + 2], indices[i + 1], indices[i]}) + { + auto const &point{body.outline[index]}; + gfx::basic_vertex vertex; + vertex.position = glm::vec3(glm::dvec3{point.x, body.level, point.y} - origin); + vertex.normal = {0.f, 1.f, 0.f}; + vertex.texture = {static_cast((point.x - centroid.x) / repeat), static_cast((centroid.y - point.y) / repeat)}; + vertex.tangent = {1.f, 0.f, 0.f, 1.f}; + vertices.push_back(vertex); + reach = std::max(reach, glm::length(point - centroid)); + } + // the geometry of water made before is filled anew, so editing the water doesn't pile up geometry + gfx::userdata_array nouserdata; + water_shape entry; + if (false == m_waterfree.empty()) + { + entry.geometry = m_waterfree.back(); + m_waterfree.pop_back(); + GfxRenderer->Replace(vertices, nouserdata, entry.geometry, GL_TRIANGLES); + } + else + { + if (m_waterbank.bank == 0 && m_waterbank.chunk == 0) + m_waterbank = GfxRenderer->Create_Bank(); + entry.geometry = GfxRenderer->Insert(vertices, nouserdata, m_waterbank, GL_TRIANGLES); + } + if (entry.geometry.chunk == 0) + continue; + // two bounding vertices give the shape its area, the triangles are in the geometry + std::vector bounds(2); + bounds[0].position = glm::dvec3{centroid.x - reach, body.level, centroid.y - reach}; + bounds[1].position = glm::dvec3{centroid.x + reach, body.level, centroid.y + reach}; + scene::shape_node shape; + shape.make_terrain(material, std::move(bounds), origin); + shape.geometry(entry.geometry); + if (translucent) + { + cell.m_shapestranslucent.emplace_back(std::move(shape)); + cell.m_area.radius = std::max(cell.m_area.radius, static_cast(glm::length(cell.m_area.center - centre) + reach)); + cell.m_active = true; + entry.cell = &cell; + } + else + { + section.m_shapes.emplace_back(std::move(shape)); + } + section.m_area.radius = std::max(section.m_area.radius, static_cast(glm::length(section.m_area.center - centre) + reach)); + entry.section = §ion; + m_watershapes.push_back(entry); } - std::error_code ec; - std::filesystem::remove(chunk_path(Cx, Cz), ec); - m_known[key] = 0; // no longer on disk } diff --git a/editor/editorTerrainStreamer.hpp b/editor/editorTerrainStreamer.hpp index ead4a3e1..d29451e7 100644 --- a/editor/editorTerrainStreamer.hpp +++ b/editor/editorTerrainStreamer.hpp @@ -9,108 +9,298 @@ http://mozilla.org/MPL/2.0/. #pragma once +#include +#include #include #include -#include +#include +#include #include +#include +#include #include #include "editor/editorTerrain.hpp" -// Streams editable terrain chunks around the camera for an effectively unbounded world. +struct world_vertex; +namespace scene { class basic_section; class basic_cell; } + +// the heightmap terrain of the scenery (scene/heightmapterrain.h), loaded around the camera. // -// Phase 1: keeps a ring of resident chunks (each an editor_terrain mesh) within a radius of the -// camera; chunks entering the radius are built, chunks leaving it are destroyed (the renderer's -// geometry GC reclaims their GPU memory). Heights are flat + a gentle procedural roll so streaming -// is observable. Phase 2 will add 16-bit on-disk chunk paging, per-chunk flag bits and persistence. +// the pack files are read and the meshes made by worker threads; the main thread only puts finished meshes on the gpu, +// within a few milliseconds of each frame. chunks are loaded around the camera and around the place the camera is heading +// to, so a moving train finds its terrain loaded. each chunk is drawn with a grid as coarse as its distance allows. +// +// the editor changes the chunks in memory; they're written to the pack files when the scenery is saved, until then the +// edited chunks stay in memory, also when they're far from the camera class terrain_streamer { public: - using chunk_key = std::pair; // (cx, cz) chunk coordinate + using chunk_key = heightmap::chunk_key; terrain_streamer() = default; + ~terrain_streamer(); - void active(bool State) { m_active = State; } + // opens the terrain kept in terrain// (the folder may not exist yet). returns: true if there's terrain to show + bool open(std::string const &Name); + // stops showing the terrain + void close(); bool active() const { return m_active; } + std::string const &name() const { return m_name; } + // the folder of the terrain, with a trailing slash + std::string const &directory() const { return m_directory; } - // per-chunk configuration (applied to chunks built from now on) - void configure(int Cells, float CellSize, int Radius, float BaseHeight, std::string const &Texture); - void simplify(bool Auto, float Error) { m_auto_optimize = Auto; m_simplify_error = Error; } - // safe to change while resident (only affects how far chunks load/unload) - void radius(int Radius) { m_radius = Radius < 0 ? 0 : Radius; } - // persist edited chunks to disk on unload, and load them back instead of regenerating - void persist(bool Enable) { m_persist = Enable; } - void directory(std::string const &Dir) - { - if (!Dir.empty() && Dir != m_dir) - { - m_dir = Dir; - m_known.clear(); // existence cache is per-folder; drop it when the folder changes - } - } - std::string const &directory() const { return m_dir; } - - // loads/unloads chunks so the resident set matches the radius around CameraPos - void update(glm::dvec3 const &CameraPos); - // drops all resident chunks (e.g. when streaming is switched off), saving edits first - void clear(); + // loads and unloads the chunks around the camera, and puts finished meshes on the gpu + void update(glm::dvec3 const &Camera); // hard reset used when a new scenery loads: drops everything WITHOUT touching scene sections // (the old region is being torn down, so its section pointers are already dangling) void reset(); - // appends raw pointers to every resident chunk (for sculpt / snap / optimize routing) - void collect(std::vector &Out) const; - // resident chunk whose footprint covers (X,Z), or nullptr - editor_terrain *terrain_at(double X, double Z) const; - - // authoring: create a flat chunk at (Cx,Cz), make it resident and persist it to disk - void add_chunk(int Cx, int Cz); - // authoring: delete the chunk at (Cx,Cz) from memory and disk - void remove_chunk(int Cx, int Cz); - // persist an externally-authored chunk (e.g. a manual grid chunk being handed over to streaming) - void save_chunk(int Cx, int Cz, editor_terrain const &Terrain); - // chunk coordinate covering (X,Z) - chunk_key key_for(double X, double Z) const { return key_at(X, Z); } - - std::size_t resident() const { return m_chunks.size(); } - float chunk_world_size() const { return m_cells * m_cellsize; } - int cells() const { return m_cells; } - float cellsize() const { return m_cellsize; } + // drawn detail: metres of distance per metre of grid spacing (larger: finer grid further away) + void detail(float const Detail) { m_detail = std::max(10.f, Detail); } + float detail() const { return m_detail; } + // smallest distance (chunks) the terrain is loaded to, whatever the draw range + void radius(int const Radius) { m_radius = std::max(0, Radius); } int radius() const { return m_radius; } - // writes every resident, edited chunk to disk (without unloading) - used on save - void flush(); + // chunks held in memory + void collect(std::vector &Out) const; + // chunk covering (X,Z), or nullptr + editor_terrain *terrain_at(double X, double Z) const; + // height of the terrain at (X,Z); false where there's no chunk in memory + bool height_at(double X, double Z, double &Height) const; + // triangles of the full grid of the chunks in memory overlapping the rectangle (x, z) + void gather_triangles(glm::dvec2 const &Min, glm::dvec2 const &Max, std::vector> &Out) const; + // true if the geometry belongs to the terrain (or its water) + bool owns(gfx::geometry_handle const &Geometry) const; + std::size_t resident() const { return m_chunks.size(); } + // chunks in the pack files and in memory, less the removed ones; the files known so far + bool exists(chunk_key const &Key) const; + + // editing; the chunks changed are written to the files by save() + heightmap::manifest const &manifest() const { return m_manifest; } + // a flat chunk at (Cx, Cz) of specified spacing and height, covered with the material of palette entry Layer; + // returns false if there's a chunk already + bool add_chunk(int Cx, int Cz, float Spacing, float Height, std::uint16_t Layer = 0); + // a chunk at (Cx, Cz), heights given for each grid point by Height (from its world position) + bool add_chunk(int Cx, int Cz, float Spacing, std::function const &Height, std::uint16_t Layer = 0); + // a chunk made elsewhere, at the place its key says; its layers are indices of the palette + bool add_chunk(std::shared_ptr Data); + void remove_chunk(int Cx, int Cz); + // changes the point spacing of the chunk at (Cx, Cz), heights resampled; Paint: paint samples along a side less one + // the chunk gets, 0 keeps the ones it has. returns: true if the chunk changed + bool spacing(int Cx, int Cz, float Spacing, int Paint = 0); + // raises (Strength > 0) or lowers the ground within Radius of (X,Z), with a smooth falloff + bool sculpt(double X, double Z, double Radius, double Strength); + // evens out the ground within Radius of (X,Z), Amount (0..1) of the way at the centre + bool smooth(double X, double Z, double Radius, double Amount); + // leads the ground within Radius of (X,Z) towards the height Target, Amount (0..1) of the way at the centre; + // Mode: 0 up and down, 1 only raises it, 2 only lowers it + bool level(double X, double Z, double Radius, double Target, double Amount, int Mode = 0); + // gives every grid point within the rectangle to Shaper, with its world position and height; Shaper returns true if it + // changed the height. where the modifiers shape the ground the change is a touch-up kept over them (see touch_up()), + // elsewhere it changes the ground under them + bool reshape(glm::dvec2 const &Min, glm::dvec2 const &Max, std::function const &Shaper); + // true: the brushes shape the terrain as it's seen, also where the modifiers (tracks) shape it, the changes kept there + // as touch-ups over the modifiers; false: the brushes shape only the ground under the modifiers, which keep their shape + void touch_up(bool const State) { m_touchup = State; } + bool touch_up() const { return m_touchup; } + // takes the touch-ups within Radius of (X,Z) back, Amount (0..1) of the way at the centre: the terrain goes back to the + // shape the modifiers give it + bool restore_stitching(double X, double Z, double Radius, double Amount); + // grid points with touch-ups over the modifiers, in the chunks in memory + std::size_t touched_up() const; + // paints the material of the palette entry Layer within Radius of (X,Z), Strength (0..1) of the way at the centre + bool paint(double X, double Z, double Radius, double Strength, std::uint16_t Layer); + // fills the area within Outline (x, z) with the material of the palette entry Layer, Strength (0..1) of the way. + // Feather: metres across the outline the material blends in over, half of them inside; 0: a sharp edge, as sharp as + // the paint samples of the chunks go. the chunks are loaded where needed + bool fill(std::vector const &Outline, double Feather, std::uint16_t Layer, double Strength = 1.0); + // palette entry of the material (repeated at Size, 0: any, turned by Rotation degrees), added to the palette if needed + std::uint16_t layer(std::string const &Material, float Size = 0.f, float Rotation = 0.f); + void layer_size(std::uint16_t Layer, float Size); + // turns the textures of the palette entry Layer by Rotation degrees, on every chunk painted with it + void layer_rotation(std::uint16_t Layer, float Rotation); + // gives the palette entry Layer another material; the chunks painted with it change with it + void layer_material(std::uint16_t Layer, std::string const &Material); + void compress(bool State); + void default_spacing(float Spacing); + // bodies of water + std::vector const &water() const { return m_manifest.water; } + void water(std::vector Water); + // material of the bodies of water which don't name their own + void water_material(std::string const &Material); + // modifiers: added or replaced by name + void modifier(heightmap::modifier Modifier); + bool remove_modifier(std::string const &Name); + // the ground takes the shape the modifier gives it for good, and the modifier is removed: from then on the + // terrain there is shaped by hand like anywhere else + bool bake_modifier(std::string const &Name); + std::vector const &modifiers() const { return m_manifest.modifiers; } + // changed since the last save + bool modified() const; + + // undo: the state of what a change touched, from before it + struct change + { + std::map chunks; // the data the chunks had; null: there was no chunk + bool manifest{false}; // the palette, the water (and its material) and the modifiers below were changed + std::vector layers; + std::vector water; + std::string water_material; + std::vector modifiers; + + bool empty() const { return chunks.empty() && false == manifest; } + }; + // from now on the state of what's changed is kept, until end_change(); nested calls join the change in progress + void begin_change(); + // returns: the state from before the changes made since begin_change() (empty if nothing changed) + change end_change(); + bool recording() const { return m_recording > 0; } + // puts the terrain back in the state recorded. returns: the state it replaced, to go forward again + change restore(change const &Change); + + // writes the description and the changed pack files. returns: true on success, Error describing the failure + bool save(std::string *Error = nullptr); private: - // per-chunk state bits cached so we don't stat the filesystem repeatedly - enum chunk_flag : uint8_t + struct pack_state { - cf_exists_on_disk = 1u << 0, // a saved 16-bit chunk file exists + enum class state + { + unknown, + loading, + ready, + missing + } status{state::unknown}; + std::shared_ptr file; + std::uint32_t generation{0}; + }; + struct job + { + enum class kind + { + pack, + chunk, + mesh + } type{kind::pack}; + heightmap::pack_key pack{0, 0}; + chunk_key chunk{0, 0}; + std::shared_ptr file; + std::uint32_t generation{0}; + heightmap::chunk_ptr data; + std::array neighbours; // -x, +x, -z, +z + int stride{1}; + glm::dvec3 origin{0.0}; + std::uint64_t version{0}; + double priority{0.0}; + }; + struct result + { + job::kind type{job::kind::pack}; + heightmap::pack_key pack{0, 0}; + chunk_key chunk{0, 0}; + std::uint32_t generation{0}; + std::shared_ptr file; + std::shared_ptr data; + heightmap::mesh mesh; + int stride{1}; + std::uint64_t version{0}; + std::string error; + }; + struct geometry_slot + { + gfx::geometrybank_handle bank{0, 0}; + gfx::geometry_handle geometry{0, 0}; + int cells{0}; + int stride{0}; + bool used{false}; + }; + struct water_shape + { + gfx::geometry_handle geometry{0, 0}; + scene::basic_section *section{nullptr}; + scene::basic_cell *cell{nullptr}; }; - chunk_key key_at(double X, double Z) const; - glm::dvec3 chunk_centre(int Cx, int Cz) const; - std::string chunk_path(int Cx, int Cz) const; - bool chunk_on_disk(chunk_key const &Key); // cached existence test - bool load_heights(int Cx, int Cz, std::vector &Out) const; // reads a 16-bit chunk file - void save_heights(int Cx, int Cz, editor_terrain const &Terrain); // writes a 16-bit chunk file + void start_workers(); + void stop_workers(); + void worker(); + void submit(job Job); + void process(result &Result); + // the chunk is drawn: its shape put in the scene, or put back with a new mesh + void upload(editor_terrain &Chunk, heightmap::mesh &Mesh, int const Stride); + void update_material(editor_terrain &Chunk); + // puts the current material of the chunk in its shape + void upload_shape_material(editor_terrain &Chunk); + // takes the chunk out of the scene, its geometry slot and material given back + void undraw(editor_terrain &Chunk); + void request_mesh(editor_terrain &Chunk, int const Stride, double const Priority); + int wanted_stride(editor_terrain const &Chunk, glm::dvec3 const &Camera) const; + heightmap::chunk_ptr neighbour(chunk_key const &Key) const; + void touch_neighbours(chunk_key const &Key); + material_handle palette_material(std::uint16_t const Layer); + // paints the material of the palette entry Layer over the paint samples within the rectangle, Amount (0..1) of the way + // at each, as Amount gives it for the place of the sample. Load: chunks not in memory are loaded + bool paint_area(glm::dvec2 const &Min, glm::dvec2 const &Max, std::uint16_t Layer, bool Load, std::function const &Amount); + // chunk loaded right away from its pack file, for editing far from the camera + editor_terrain *load_now(chunk_key const &Key); + // chunks in memory or in the files overlapping the rectangle, loaded if needed + std::vector chunks_in(glm::dvec2 const &Min, glm::dvec2 const &Max, bool const Load); + // leads the heights of the chunk from its base heights through the modifiers, within the rectangle + void apply_modifiers(editor_terrain &Chunk, glm::dvec2 const &Min, glm::dvec2 const &Max); + void apply_modifiers(glm::dvec2 const &Min, glm::dvec2 const &Max); + void build_water(); + void clear_water(bool const Touch); + std::string pack_path(heightmap::pack_key const &Key) const; + // the state of a chunk (null: no chunk) or of the description is kept for the change in progress, if it's the first time + void remember(chunk_key const &Key, heightmap::chunk_ptr const &Data); + void remember(editor_terrain const &Chunk) { remember(Chunk.key(), Chunk.shared()); } + void remember_manifest(); bool m_active{false}; - int m_cells{32}; - float m_cellsize{2.0f}; - int m_radius{2}; // chunks loaded around the camera (Chebyshev radius) - float m_baseheight{0.0f}; - std::string m_texture; - bool m_auto_optimize{true}; - float m_simplify_error{0.5f}; - bool m_persist{true}; - std::string m_dir{"editor_terrain"}; // folder for 16-bit chunk files + std::string m_name; + std::string m_directory; + heightmap::manifest m_manifest; + bool m_manifestchanged{false}; + float m_detail{120.f}; + bool m_touchup{true}; + int m_radius{2}; + std::map m_packs; std::map> m_chunks; - std::map m_known; // cached per-chunk flag bits + std::set m_loading; // asked from a worker + std::set m_removed; // removed in the editor, still in the files until saved + std::vector m_slots; + std::map m_slotofgeometry; // geometry key -> slot + std::map m_palette; + std::vector m_watershapes; + std::vector m_waterfree; // geometry of the water shown before, to be filled anew + gfx::geometrybank_handle m_waterbank{0, 0}; + bool m_waterchanged{false}; + + int m_recording{0}; + change m_change; + + glm::dvec3 m_camera{0.0}; + glm::dvec3 m_velocity{0.0}; + double m_cameratime{0.0}; + bool m_hascamera{false}; + std::vector m_ring; // offsets of the chunks around a point, the nearest first + int m_ringradius{-1}; + + // shared with the workers, guarded by m_mutex + std::mutex m_mutex; + std::condition_variable m_wakeup; + std::vector m_jobs; + std::deque m_results; + std::size_t m_busy{0}; // jobs taken by the workers and not finished yet + bool m_stop{false}; + std::vector m_workers; + // the pack files are read by the workers and written by save() + std::mutex m_filemutex; }; -// single, simulation-level streamer instance shared by the editor (authoring) and the scenery -// loader (so streamed terrain renders in every mode, including the driver). loaded from the -// `editorterrain` scenery directive and updated each frame with the active camera. +// single, simulation-level instance shared by the editor (authoring) and the scenery loader, so the terrain +// renders in every mode. opened by the `heightmap_terrain` scenery directive and updated each frame with the active camera. extern terrain_streamer EditorTerrain; diff --git a/editor/editorTrack.cpp b/editor/editorTrack.cpp new file mode 100644 index 00000000..418d908f --- /dev/null +++ b/editor/editorTrack.cpp @@ -0,0 +1,2831 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http: +*/ + +#include "stdafx.h" +#include "editor/editorTrack.hpp" +#include "editor/editorFormat.hpp" +#include "editor/editorGeometry.hpp" + +#include "world/Track.h" +#include "vehicle/DynObj.h" +#include "scene/scene.h" +#include "scene/scenelayers.h" +#include "simulation/simulation.h" +#include "editor/editorIncludeInfo.hpp" +#include "world/Event.h" +#include "world/MemCell.h" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" +#include "utilities/parser.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +using geometry::bezier; +using geometry::chord_length; +using geometry::cross; +using geometry::plan_of; +using geometry::signed_angle; +using geometry::turned; + +double const kSamePoint{geometry::same_point}; + +segment_data::point point_index(editor_track::point_kind const Kind) +{ + switch (Kind) + { + case editor_track::point_kind::start: return segment_data::point::start; + case editor_track::point_kind::control1: return segment_data::point::control1; + case editor_track::point_kind::control2: return segment_data::point::control2; + default: return segment_data::point::end; + } +} + +bool has_switch_paths(TTrack const &Track) +{ + return Track.SwitchExtension != nullptr && (Track.eType == tt_Switch || Track.eType == tt_Cross); +} + +void add_unique(std::vector &Tracks, TTrack *Track) +{ + if (Track != nullptr && std::find(Tracks.begin(), Tracks.end(), Track) == Tracks.end()) + Tracks.emplace_back(Track); +} + +glm::dvec3 path_tangent(segment_data const &Path, bool const Atend) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (false == Atend) + return control1 != glm::dvec3{} ? control1 : (end + control2) - start; + return control2 != glm::dvec3{} ? -control2 : end - (start + control1); +} + +double path_radius(segment_data const &Path, double const T) +{ + if (Path.points[segment_data::point::control1] == glm::dvec3{} && Path.points[segment_data::point::control2] == glm::dvec3{}) + return 0.0; + bezier const curve{Path}; + auto const first{curve.first(T)}; + auto const second{curve.second(T)}; + auto const speed{std::hypot(first.x, first.z)}; + auto const curvature{std::abs(first.x * second.z - first.z * second.x) / (speed * speed * speed)}; + return curvature > 1e-7 ? 1.0 / curvature : 0.0; +} + +segment_data reversed(segment_data const &Path) +{ + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; +} + +bool adjoining_direction(TTrack const *Neighbour, glm::dvec3 const &Joint, bool const Leaving, glm::dvec3 &Direction, double &Radius) +{ + if (Neighbour == nullptr) + return false; + for (auto const &path : Neighbour->m_paths) + { + for (auto const atend : {false, true}) + { + auto const &point{path.points[atend ? segment_data::point::end : segment_data::point::start]}; + if (glm::distance(point, Joint) > kSamePoint) + continue; + auto const tangent{glm::normalize(path_tangent(path, atend))}; + Direction = (Leaving != atend) ? tangent : -tangent; + Radius = path_radius(path, atend ? 1.0 : 0.0); + return true; + } + } + return false; +} + +} + +editor_track::observer *editor_track::s_observer{nullptr}; + +editor_track::state editor_track::capture(TTrack const &Track) +{ + if (s_observer != nullptr) + s_observer->track_captured(Track); + state result; + result.paths = Track.m_paths; + result.velocity = Track.fVelocity; + result.switchvelocity = Track.SwitchExtension ? Track.SwitchExtension->fVelocity : -1.f; + result.width = Track.fTrackWidth; + result.friction = Track.fFriction; + result.sounddistance = Track.fSoundDistance; + result.quality = Track.iQualityFlag; + result.damage = Track.iDamageFlag; + result.environment = static_cast(Track.eEnvironment); + result.material1 = Track.m_material1; + result.material2 = Track.m_material2; + result.material3 = Track.SwitchExtension ? Track.SwitchExtension->m_material3 : null_handle; + result.texlength = Track.fTexLength; + result.texheight = texture_height(Track); + result.texwidth = Track.fTexWidth; + result.texslope = Track.fTexSlope; + for (auto const *isolated : Track.Isolated) + result.isolated.push_back(isolated->asName); + result.overhead = Track.fOverhead; + result.sleepers = Track.m_sleeper_enabled; + result.sleeper_frequency = Track.m_sleeper_frequency; + result.sleeper_model = Track.m_sleeper_model_name; + result.sleeper_skin = Track.m_sleeper_skin_name; + result.sleeper_offset = Track.m_sleeper_offset; + result.sleeper_ballast = Track.m_sleeper_ballast_z; + for (int i = 0; i < 6; ++i) + result.events[i] = events(const_cast(Track), i); + result.name = Track.name(); + return result; +} + +std::vector> &editor_track::events(TTrack &Track, int const Index) +{ + switch (Index) + { + case 1: return Track.m_events1; + case 2: return Track.m_events2; + case 3: return Track.m_events0all; + case 4: return Track.m_events1all; + case 5: return Track.m_events2all; + default: return Track.m_events0; + } +} + +char const *editor_track::event_keyword(int const Index) +{ + char const *const keywords[] = {"event0", "event1", "event2", "eventall0", "eventall1", "eventall2"}; + return keywords[std::clamp(Index, 0, 5)]; +} + +void editor_track::bind_events(TTrack &Track, std::string &Missing) +{ + Missing.clear(); + Track.m_events = false; + for (int i = 0; i < 6; ++i) + for (auto &event : events(Track, i)) + { + // the ones bound by the name of the path have no name of their own + if (false == event.first.empty()) + event.second = simulation::Events.FindEvent(event.first); + if (event.second != nullptr) + Track.m_events = true; + else + Missing += (Missing.empty() ? "" : ", ") + event.first; + } +} + +void editor_track::isolated(TTrack &Track, std::vector const &Names) +{ + Track.Isolated.clear(); + for (auto const &name : Names) + { + if (name.empty()) + continue; + bool known{false}; + for (auto *existing{TIsolated::Root()}; existing != nullptr && false == known; existing = existing->Next()) + known = (existing->asName == name); + auto *isolated{TIsolated::Find(name)}; + if (false == known) + isolated->AssignEvents(); + Track.Isolated.push_back(isolated); + } +} + +void editor_track::overhead(TTrack &Track, float const Overhead) +{ + Track.fOverhead = Overhead; + if (Overhead > 0.f) + Track.iAction |= 0x40; + else + Track.iAction &= ~0x40; +} + +void editor_track::apply(TTrack &Track, state const &State) +{ + Track.m_paths = State.paths; + Track.fVelocity = State.velocity; + if (Track.SwitchExtension) + { + Track.SwitchExtension->fVelocity = State.switchvelocity; + Track.SwitchExtension->m_material3 = State.material3; + } + Track.fTrackWidth = State.width; + Track.fTrackWidth2 = State.width; + Track.fFriction = State.friction; + Track.fSoundDistance = State.sounddistance; + Track.iQualityFlag = State.quality; + damage(Track, State.damage); + Track.eEnvironment = static_cast(State.environment); + Track.m_material1 = State.material1; + Track.m_material2 = State.material2; + Track.fTexLength = State.texlength; + texture_height(Track, State.texheight); + Track.fTexWidth = State.texwidth; + Track.fTexSlope = State.texslope; + isolated(Track, State.isolated); + overhead(Track, State.overhead); + Track.m_sleeper_enabled = State.sleepers; + Track.m_sleeper_frequency = State.sleeper_frequency; + Track.m_sleeper_model_name = State.sleeper_model; + Track.m_sleeper_skin_name = State.sleeper_skin; + Track.m_sleeper_offset = State.sleeper_offset; + Track.m_sleeper_ballast_z = State.sleeper_ballast; + for (int i = 0; i < 6; ++i) + events(Track, i) = State.events[i]; + Track.m_events = std::any_of(State.events.begin(), State.events.end(), [](auto const &Events) { + return std::any_of(Events.begin(), Events.end(), [](auto const &Event) { return Event.second != nullptr; }); + }); + if (State.name != Track.name()) + rename(Track, State.name); +} + +namespace +{ + +std::string lower_name(std::string Name) +{ + for (auto &character : Name) + if (character >= 'A' && character <= 'Z') + character = static_cast(character - 'A' + 'a'); + return Name; +} + +int track_parameter(scene::instance_handle const Instance, std::string &File, std::vector &Values) +{ + if (false == scene::Layers.tracked(Instance)) + return -1; + auto const &included{scene::Layers.instance(Instance)}; + if (included.removed || included.dead || included.file == nullptr) + return -1; + static std::map descriptions; + auto found{descriptions.find(*included.file)}; + if (found == descriptions.end()) + { + include_info info; + std::string error; + editor_includes::load(*included.file, info, error); + found = descriptions.emplace(*included.file, std::move(info)).first; + } + auto const index{editor_includes::parameter_with_role(found->second, "track") - 1}; + if (index < 0 || false == editor_includes::parse_directive(scene::Layers.directive(Instance), File, Values) || index >= static_cast(Values.size())) + return -1; + return index; +} + +} // namespace + +bool editor_track::name_valid(TTrack const &Track, std::string &Name, std::string &Reason, TTrack **Owner) +{ + Name = lower_name(Name); + if (Name.empty() || Name == "none") + { + Reason = "The path needs a name"; + return false; + } + if (Name.find_first_of(" \t\r\n\"';") != std::string::npos || Name.find("//") != std::string::npos) + { + Reason = "The name can't hold blanks, quotes, semicolons or //"; + return false; + } + auto *owner{simulation::Paths.find(Name)}; + if (owner != nullptr && owner != &Track && false == owner->m_editorremoved) + { + if (Owner != nullptr) + *Owner = owner; + Reason = "Another path has this name"; + return false; + } + Reason.clear(); + return true; +} + +editor_track::name_uses editor_track::uses_of_name(TTrack const &Track) +{ + name_uses result; + auto const &name{Track.name()}; + if (name.empty() || name == "none") + return result; + for (auto const *cell : simulation::Memory.sequence()) + result.cells += (cell != nullptr && cell->Track == &Track) ? 1 : 0; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + std::string file; + std::vector values; + auto const index{track_parameter(handle, file, values)}; + result.includes += (index >= 0 && lower_name(values[index]) == name) ? 1 : 0; + } + static std::array const kinds{"event0", "event1", "event2", "eventall0", "eventall1", "eventall2"}; + for (auto const *event : simulation::Events.sequence()) + { + if (event == nullptr) + continue; + auto const &eventname{event->name()}; + if (std::any_of(kinds.begin(), kinds.end(), [&](char const *Kind) { return eventname == name + ':' + Kind; })) + { + result.loose.push_back(eventname); + continue; + } + if (result.includes > 0 && eventname.compare(0, name.size(), name) == 0) + continue; + auto const targets{event->target_nodes()}; + if (std::find(targets.begin(), targets.end(), &Track) != targets.end()) + result.events.push_back(eventname); + } + return result; +} + +void editor_track::rename(TTrack &Track, std::string const &Name) +{ + auto const old{Track.name()}; + if (old == Name) + return; + simulation::Paths.rename(&Track, Name); + Track.m_name = Name; + Track.mark_dirty(); + for (auto *cell : simulation::Memory.sequence()) + if (cell != nullptr && cell->Track == &Track) + { + cell->asTrackName = Name; + cell->mark_dirty(); + } + if (old.empty() || old == "none") + return; + for (scene::instance_handle handle = 1; handle <= scene::Layers.instance_count(); ++handle) + { + std::string file; + std::vector values; + auto const index{track_parameter(handle, file, values)}; + if (index < 0 || lower_name(values[index]) != old) + continue; + values[index] = Name; + scene::Layers.modify(handle, editor_includes::compose_directive(file, values)); + } +} + +bool editor_track::is_supported(TTrack const &Track) +{ + if ((Track.iCategoryFlag & 0x80) != 0) + return false; + // lanes are generated from their road, and change only along with it + if (Track.m_road != nullptr) + return false; + if (Track.eType == tt_Normal) + return Track.Segment != nullptr; + if (Track.eType == tt_Switch) + return Track.SwitchExtension != nullptr && Track.m_paths.size() >= 2; + return false; +} + +bool editor_track::can_edit_geometry(TTrack const &Track, std::string &Reason) +{ + if (false == is_supported(Track)) + { + Reason = "Editing of this path type isn't supported"; + return false; + } + if (Global.NvRenderer) + { + Reason = "Path geometry can't be rebuilt with the experimental renderer"; + return false; + } + std::string why; + if (false == scene::Layers.editable(&Track, &why)) + { + Reason = "The path can't be changed: " + why; + return false; + } + Reason.clear(); + return true; +} + +void editor_track::commit(std::vector const &Tracks) +{ + if (Tracks.empty()) + return; + + std::vector affected; + for (auto *track : Tracks) + { + add_unique(affected, track); + for (auto *neighbour : neighbours(*track)) + add_unique(affected, neighbour); + } + for (auto *track : Tracks) + { + disconnect(*track); + simulation::Region->erase_and_unregister(track); + } + for (auto *track : Tracks) + { + track->init_segments(false); + track->update_location(); + track->m_area.radius = -1.f; + simulation::Region->insert_and_register(track); + } + for (auto *track : Tracks) + join(*track); + for (auto *track : Tracks) + for (auto *neighbour : neighbours(*track)) + add_unique(affected, neighbour); + for (auto *track : affected) + update_transition(*track); + for (auto *track : affected) + rebuild_geometry(*track); + for (auto *track : Tracks) + track->mark_dirty(); + for (auto *track : affected) + for (auto *vehicle : track->Dynamics) + vehicle->Move(0.000001); + if (s_observer != nullptr) + s_observer->tracks_committed(Tracks); +} + +void editor_track::commit_parameters(TTrack &Track) +{ + Track.fTrackWidth2 = Track.fTrackWidth; + Track.init_segments(false); + + std::vector affected{&Track}; + for (auto *neighbour : neighbours(Track)) + add_unique(affected, neighbour); + for (auto *track : affected) + update_transition(*track); + for (auto *track : affected) + rebuild_geometry(*track); + Track.mark_dirty(); +} + +glm::dvec3 editor_track::point_position(TTrack const &Track, point_ref const &Point) +{ + if (Point.path < 0 || Point.path >= static_cast(Track.m_paths.size())) + return Track.location(); + + auto const &path{Track.m_paths[Point.path]}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + switch (Point.kind) + { + case point_kind::start: return start; + case point_kind::end: return end; + case point_kind::control1: + { + auto const &control{path.points[segment_data::point::control1]}; + return control == glm::dvec3{} ? start + (end - start) / 3.0 : start + control; + } + case point_kind::control2: + { + auto const &control{path.points[segment_data::point::control2]}; + return control == glm::dvec3{} ? end + (start - end) / 3.0 : end + control; + } + } + return start; +} + +void editor_track::move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position) +{ + if (Point.path < 0 || Point.path >= static_cast(Track.m_paths.size())) + return; + + auto &path{Track.m_paths[Point.path]}; + switch (Point.kind) + { + case point_kind::start: + case point_kind::end: + { + auto &point{path.points[point_index(Point.kind)]}; + auto const oldposition{point}; + point = Position; + for (auto &otherpath : Track.m_paths) + { + if (&otherpath == &path) + continue; + for (auto const index : {segment_data::point::start, segment_data::point::end}) + if (glm::distance(otherpath.points[index], oldposition) < kSamePoint) + otherpath.points[index] = Position; + } + break; + } + case point_kind::control1: + { + auto control{Position - path.points[segment_data::point::start]}; + path.points[segment_data::point::control1] = (glm::length(control) < 0.01 ? glm::dvec3{} : control); + break; + } + case point_kind::control2: + { + auto control{Position - path.points[segment_data::point::end]}; + path.points[segment_data::point::control2] = (glm::length(control) < 0.01 ? glm::dvec3{} : control); + break; + } + } +} + +void editor_track::translate(TTrack &Track, glm::dvec3 const &Offset) +{ + for (auto &path : Track.m_paths) + { + path.points[segment_data::point::start] += Offset; + path.points[segment_data::point::end] += Offset; + } +} + +void editor_track::rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle) +{ + auto const c{std::cos(Angle)}; + auto const s{std::sin(Angle)}; + auto const rotate = [&](glm::dvec3 const &Vector) { return glm::dvec3{Vector.x * c + Vector.z * s, Vector.y, -Vector.x * s + Vector.z * c}; }; + + for (auto &path : Track.m_paths) + { + for (auto const index : {segment_data::point::start, segment_data::point::end}) + path.points[index] = Pivot + rotate(path.points[index] - Pivot); + for (auto const index : {segment_data::point::control1, segment_data::point::control2}) + path.points[index] = rotate(path.points[index]); + } +} + +glm::dvec3 editor_track::pivot(TTrack const &Track) +{ + return Track.location(); +} + +std::vector editor_track::neighbours(TTrack const &Track) +{ + std::vector result; + add_unique(result, Track.trPrev); + add_unique(result, Track.trNext); + if (has_switch_paths(Track)) + { + for (int i = 0; i < 2; ++i) + { + add_unique(result, Track.SwitchExtension->pPrevs[i]); + add_unique(result, Track.SwitchExtension->pNexts[i]); + } + } + result.erase(std::remove(result.begin(), result.end(), &Track), result.end()); + return result; +} + +std::vector> editor_track::connected_points(TTrack const &Track, glm::dvec3 const &Position) +{ + std::vector> result; + for (auto *neighbour : neighbours(Track)) + { + for (int i = 0; i < static_cast(neighbour->m_paths.size()); ++i) + { + for (auto const kind : {point_kind::start, point_kind::end}) + { + if (glm::distance(neighbour->m_paths[i].points[point_index(kind)], Position) < kSamePoint) + result.emplace_back(neighbour, point_ref{i, kind}); + } + } + } + return result; +} + +namespace +{ + +// direction of the path at the end, into the path +glm::dvec2 end_direction(segment_data const &Path, editor_track::point_kind const Kind) +{ + bool const start{Kind == editor_track::point_kind::start}; + auto const &point{Path.points[start ? segment_data::point::start : segment_data::point::end]}; + auto const &control{Path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; + auto const direction{plan_of(control != glm::dvec3{} ? control : Path.points[start ? segment_data::point::end : segment_data::point::start] - point)}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{0.0, 1.0}; +} + +} // namespace + +std::vector editor_track::joints(TTrack const &Track) +{ + std::vector result; + for (int i = 0; i < static_cast(Track.m_paths.size()); ++i) + { + for (auto const kind : {point_kind::start, point_kind::end}) + { + point_ref const own{i, kind}; + for (auto const &connection : connected_points(Track, point_position(Track, own))) + { + auto *other{connection.first}; + std::string reason; + if (has_switch_paths(*other) || false == can_edit_geometry(*other, reason)) + continue; + if (std::any_of(result.begin(), result.end(), [&](joint const &Joint) { return Joint.other == other && Joint.theirs == connection.second; })) + continue; + auto const &path{other->m_paths[connection.second.path]}; + bool const start{connection.second.kind == point_kind::start}; + auto control{path.points[start ? segment_data::point::control1 : segment_data::point::control2]}; + if (control == glm::dvec3{}) + control = (path.points[start ? segment_data::point::end : segment_data::point::start] - path.points[start ? segment_data::point::start : segment_data::point::end]) / 3.0; + result.push_back({own, other, connection.second, end_direction(Track.m_paths[i], kind), control}); + } + } + } + return result; +} + +void editor_track::follow(TTrack const &Track, std::vector const &Joints) +{ + for (auto const &joint : Joints) + { + if (joint.own.path >= static_cast(Track.m_paths.size()) || joint.theirs.path >= static_cast(joint.other->m_paths.size())) + continue; + move_point(*joint.other, joint.theirs, point_position(Track, joint.own)); + auto const angle{signed_angle(joint.direction, end_direction(Track.m_paths[joint.own.path], joint.own.kind))}; + auto const moved{turned(plan_of(joint.control), angle)}; + joint.other->m_paths[joint.theirs.path].points[joint.theirs.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2] = {moved.x, joint.control.y, moved.y}; + } +} + +bool editor_track::is_connected(TTrack const &Track, point_ref const &Point) +{ + if (false == editor_track::is_end(Point.kind)) + return false; + return false == connected_points(Track, point_position(Track, Point)).empty(); +} + +editor_track::snap_target editor_track::find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude) +{ + return find_free_end(&Track, Track.iCategoryFlag & 15, Position, Radius, Exclude); +} + +editor_track::snap_target editor_track::find_free_end(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude) +{ + auto const found{free_ends(Self, Category, Position, Radius, Exclude)}; + return found.empty() ? snap_target{} : found.front(); +} + +std::vector editor_track::free_ends(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude) +{ + std::vector result; + auto const excluded = [&](TTrack const *Other) { return std::find(Exclude.begin(), Exclude.end(), Other) != Exclude.end(); }; + auto const sections{simulation::Region->sections(Position, static_cast(Radius))}; + for (auto *section : sections) + { + for (auto const &cell : section->m_cells) + { + for (auto *other : cell.m_directories.paths) + { + if (other == Self || excluded(other) || other->m_editorremoved || (other->iCategoryFlag & 15) != Category || false == is_supported(*other)) + continue; + for (int i = 0; i < static_cast(other->m_paths.size()); ++i) + { + auto const &path{other->m_paths[i]}; + for (auto const kind : {point_kind::start, point_kind::end}) + { + auto const &point{path.points[point_index(kind)]}; + auto const distance{glm::distance(point, Position)}; + if (distance > Radius) + continue; + if (std::any_of(result.begin(), result.end(), [&](snap_target const &Found) { return Found.track == other && Found.point == point_ref{i, kind}; })) + continue; + auto const connections{connected_points(*other, point)}; + if (std::any_of(connections.begin(), connections.end(), [&](auto const &Connection) { return false == excluded(Connection.first); })) + continue; + + glm::dvec3 direction; + if (kind == point_kind::start) + { + auto const &control{path.points[segment_data::point::control1]}; + direction = control != glm::dvec3{} ? control : path.points[segment_data::point::end] - point; + } + else + { + auto const &control{path.points[segment_data::point::control2]}; + direction = control != glm::dvec3{} ? control : path.points[segment_data::point::start] - point; + } + if (glm::length(direction) < 1e-6) + continue; + + snap_target target; + target.track = other; + target.point = point_ref{i, kind}; + target.position = point; + target.direction = glm::normalize(direction); + target.distance = distance; + result.push_back(target); + } + } + } + } + } + std::sort(result.begin(), result.end(), [](snap_target const &A, snap_target const &B) { return A.distance < B.distance; }); + return result; +} + +void editor_track::snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent) +{ + if (Target.track == nullptr || false == editor_track::is_end(Point.kind)) + return; + + move_point(Track, Point, Target.position); + if (false == Aligntangent) + return; + + auto &path{Track.m_paths[Point.path]}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + auto &control{path.points[Point.kind == point_kind::start ? segment_data::point::control1 : segment_data::point::control2]}; + auto const length{control != glm::dvec3{} ? glm::length(control) : glm::distance(start, end) / 3.0}; + control = -Target.direction * length; +} + +std::string editor_track::material_name(material_handle const Material) +{ + if (Material == null_handle) + return "none"; + auto name{GfxRenderer->Material(Material)->GetName()}; + if (name.find(paths::textures) == 0) + name.erase(0, std::string{paths::textures}.size()); + return name; +} + +material_handle editor_track::fetch_material(std::string const &Name) +{ + if (Name.empty() || Name == "none") + return null_handle; + auto name{Name}; + replace_slashes(name); + return GfxRenderer->Fetch_Material(name); +} + +float editor_track::texture_height(TTrack const &Track) +{ + return (Track.fTexHeight1 - Track.fTexHeightOffset) * (Track.iCategoryFlag & 4 ? -1.f : 1.f); +} + +void editor_track::texture_height(TTrack &Track, float const Height) +{ + Track.fTexHeight1 = Height * (Track.iCategoryFlag & 4 ? -1.f : 1.f) + Track.fTexHeightOffset; +} + +double editor_track::velocity(TTrack const &Track) +{ + if (Track.SwitchExtension && Track.eType == tt_Switch) + return Track.SwitchExtension->fVelocity; + return Track.fVelocity; +} + +void editor_track::velocity(TTrack &Track, double const Velocity) +{ + if (Track.SwitchExtension && Track.eType == tt_Switch) + { + Track.SwitchExtension->fVelocity = static_cast(Velocity); + if (Velocity <= -2.0) + Track.fVelocity = Track.SwitchExtension->CurrentIndex ? -Velocity : -1.0; + else + Track.fVelocity = Velocity; + return; + } + Track.fVelocity = Velocity; +} + +void editor_track::damage(TTrack &Track, int const Damage) +{ + Track.iDamageFlag = Damage; + if (Damage & 128) + Track.iAction |= 0x80; + else + Track.iAction &= ~0x80; +} + +void editor_track::disconnect(TTrack &Track) +{ + for (auto *neighbour : neighbours(Track)) + { + if (has_switch_paths(*neighbour)) + { + auto &extension{*neighbour->SwitchExtension}; + for (int i = 0; i < 2; ++i) + { + if (extension.pPrevs[i] == &Track) + extension.pPrevs[i] = nullptr; + if (extension.pNexts[i] == &Track) + extension.pNexts[i] = nullptr; + } + bind_switch_path(*neighbour, extension.CurrentIndex); + } + else + { + if (neighbour->trPrev == &Track) + neighbour->trPrev = nullptr; + if (neighbour->trNext == &Track) + neighbour->trNext = nullptr; + } + } + Track.trPrev = nullptr; + Track.trNext = nullptr; + if (has_switch_paths(Track)) + { + auto &extension{*Track.SwitchExtension}; + for (int i = 0; i < 2; ++i) + { + extension.pPrevs[i] = nullptr; + extension.pNexts[i] = nullptr; + } + } +} + +void editor_track::join(TTrack &Track) +{ + TTrack *other; + int endpointid; + switch (Track.eType) + { + case tt_Normal: + { + if (Track.trPrev == nullptr) + { + std::tie(other, endpointid) = simulation::Region->find_path(Track.Segment->FastGetPoint_0(), &Track); + connect(Track, true, other, endpointid); + } + if (Track.trNext == nullptr) + { + std::tie(other, endpointid) = simulation::Region->find_path(Track.Segment->FastGetPoint_1(), &Track); + connect(Track, false, other, endpointid); + } + break; + } + case tt_Switch: + { + for (int i = 0; i < 2; ++i) + { + auto const &segment{Track.SwitchExtension->Segments[i]}; + for (auto const &point : {segment->FastGetPoint_0(), segment->FastGetPoint_1()}) + { + std::tie(other, endpointid) = simulation::Region->find_path(point, &Track); + if (other != nullptr && other->eType == tt_Normal) + join(*other); + } + } + break; + } + default: + break; + } +} + +void editor_track::connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid) +{ + if (Other == nullptr) + return; + + switch (Endpointid) + { + case 0: + if (Prevside) + Track.ConnectPrevPrev(Other, 0); + else + Track.ConnectNextPrev(Other, 0); + break; + case 1: + if (Prevside) + Track.ConnectPrevNext(Other, 1); + else + Track.ConnectNextNext(Other, 1); + break; + case 2: + case 3: + case 4: + case 5: + { + if (false == has_switch_paths(*Other)) + break; + auto const path{(Endpointid - 2) / 2}; + auto const atend{((Endpointid - 2) % 2) != 0}; + bind_switch_path(*Other, path); + if (Prevside) + { + if (atend) + Track.ConnectPrevNext(Other, path ? 3 : 1); + else + Track.ConnectPrevPrev(Other, path ? 2 : 0); + } + else + { + if (atend) + Track.ConnectNextNext(Other, path ? 3 : 1); + else + Track.ConnectNextPrev(Other, path ? 2 : 0); + } + store_switch_path(*Other, path); + bind_switch_path(*Other, Other->SwitchExtension->CurrentIndex); + break; + } + default: + break; + } +} + +void editor_track::update_transition(TTrack &Track) +{ + Track.iTrapezoid &= ~2; + auto const *next{Track.trNext}; + if (next == nullptr || Track.iNextDirection != 0) + return; + if (Track.m_visible && next->m_visible && Track.eType == tt_Normal && next->eType == tt_Normal) + { + if (Track.fTrackWidth != next->fTrackWidth || Track.fTexHeight1 != next->fTexHeight1 || Track.fTexWidth != next->fTexWidth || Track.fTexSlope != next->fTexSlope) + Track.iTrapezoid |= 2; + } +} + +void editor_track::bind_switch_path(TTrack &Switch, int const Path) +{ + auto &extension{*Switch.SwitchExtension}; + auto const path{Path & 1}; + Switch.Segment = extension.Segments[path]; + Switch.trNext = extension.pNexts[path]; + Switch.trPrev = extension.pPrevs[path]; + Switch.iNextDirection = extension.iNextDirection[path]; + Switch.iPrevDirection = extension.iPrevDirection[path]; +} + +void editor_track::store_switch_path(TTrack &Switch, int const Path) +{ + auto &extension{*Switch.SwitchExtension}; + auto const path{Path & 1}; + extension.pNexts[path] = Switch.trNext; + extension.pPrevs[path] = Switch.trPrev; + extension.iNextDirection[path] = Switch.iNextDirection; + extension.iPrevDirection[path] = Switch.iPrevDirection; + if (Switch.eType == tt_Switch) + { + extension.pPrevs[path ^ 1] = Switch.trPrev; + extension.iPrevDirection[path ^ 1] = Switch.iPrevDirection; + } +} + +bool editor_track::find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error, bool const Switches) +{ + Chain = {}; + if (From == nullptr || To == nullptr) + { + Error = "Select the first and the last path of the fragment"; + return false; + } + auto const regular = [](TTrack const *Track) { return Track->eType == tt_Normal && is_supported(*Track); }; + if (false == regular(From) || false == regular(To)) + { + Error = "The fragment has to consist of regular paths, without switches"; + return false; + } + + Chain.tracks = {From}; + Chain.forward = {true}; + if (From != To) + { + bool found{false}; + for (auto const throughend : {true, false}) + { + Chain.tracks = {From}; + Chain.forward = {throughend}; + Chain.switches.clear(); + TTrack *previous{From}; + TTrack *current{throughend ? From->trNext : From->trPrev}; + while (current != nullptr && current != From && Chain.tracks.size() + Chain.switches.size() < 10000) + { + if (Switches && current->eType == tt_Switch && has_switch_paths(*current) && is_supported(*current)) + { + // only along the main track, the way in decides the way out + auto const &extension{*current->SwitchExtension}; + bool const atstart{extension.pPrevs[0] == previous}; + if (atstart == (extension.pNexts[0] == previous)) + break; + Chain.switches.push_back({current, atstart, Chain.tracks.size()}); + previous = current; + current = atstart ? extension.pNexts[0] : extension.pPrevs[0]; + continue; + } + if (false == regular(current)) + break; + auto const entersatstart{current->trPrev == previous}; + Chain.tracks.push_back(current); + Chain.forward.push_back(entersatstart); + if (current == To) + { + found = true; + break; + } + previous = current; + current = entersatstart ? current->trNext : current->trPrev; + } + if (found) + break; + } + if (false == found) + { + Chain = {}; + Error = Switches ? "The selected paths aren't connected along the main tracks of the switches" : "The selected paths aren't connected by a chain of regular paths (switches end the chain)"; + return false; + } + } + for (auto const &passage : Chain.switches) + { + if ((passage.track->iCategoryFlag & 15) != (From->iCategoryFlag & 15)) + { + Error = "The fragment mixes different kinds of paths"; + Chain = {}; + return false; + } + Chain.length += bezier{passage.track->m_paths.front()}.plan_length(); + } + + return complete_chain(Chain, Error); +} + +bool editor_track::complete_chain(chain &Chain, std::string &Error) +{ + auto *from{Chain.tracks.front()}; + auto *to{Chain.tracks.back()}; + auto const category{from->iCategoryFlag & 15}; + for (auto const *track : Chain.tracks) + { + if ((track->iCategoryFlag & 15) != category) + { + Error = "The fragment mixes different kinds of paths"; + Chain = {}; + return false; + } + Chain.length += track->Length(); + Chain.velocity = std::max(Chain.velocity, velocity(*track)); + auto radius{path_radius(track->m_paths.front(), 0.5)}; + if (radius == 0.0 && track->m_paths.front().radius != 0.f) + radius = std::abs(track->m_paths.front().radius); + if (radius > 0.0 && (Chain.radius == 0.0 || radius < Chain.radius)) + Chain.radius = radius; + } + + auto const &first{from->m_paths.front()}; + auto const firstforward{Chain.forward.front()}; + Chain.start = first.points[firstforward ? segment_data::point::start : segment_data::point::end]; + Chain.start_direction = firstforward ? path_tangent(first, false) : -path_tangent(first, true); + adjoining_direction(firstforward ? from->trPrev : from->trNext, Chain.start, false, Chain.start_direction, Chain.start_radius); + Chain.start_direction = glm::normalize(Chain.start_direction); + + auto const &last{to->m_paths.front()}; + auto const lastforward{Chain.forward.back()}; + Chain.end = last.points[lastforward ? segment_data::point::end : segment_data::point::start]; + Chain.end_direction = lastforward ? path_tangent(last, true) : -path_tangent(last, false); + adjoining_direction(lastforward ? to->trNext : to->trPrev, Chain.end, true, Chain.end_direction, Chain.end_radius); + Chain.end_direction = glm::normalize(Chain.end_direction); + + Error.clear(); + return true; +} + +std::vector editor_track::chain_parts(chain const &Chain) +{ + std::vector parts; + std::size_t first{0}; + auto const add = [&](std::size_t const End) { + if (End <= first) + return; + chain part; + part.tracks.assign(Chain.tracks.begin() + first, Chain.tracks.begin() + End); + part.forward.assign(Chain.forward.begin() + first, Chain.forward.begin() + End); + std::string error; + if (complete_chain(part, error)) + parts.push_back(std::move(part)); + first = End; + }; + for (auto const &passage : Chain.switches) + add(passage.before); + add(Chain.tracks.size()); + return parts; +} + +namespace +{ + +bool plan_straight(segment_data const &Path, editor_track::straight_tolerance const &Tolerance) +{ + auto const chord{plan_of(Path.points[segment_data::point::end] - Path.points[segment_data::point::start])}; + auto const length{glm::length(chord)}; + if (length < 1e-3) + return false; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + if (Path.radius != 0.f && std::abs(Path.radius) < Tolerance.radius) + return false; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + return true; + auto const direction{chord / length}; + for (auto const index : {segment_data::point::control1, segment_data::point::control2}) + if (std::abs(cross(direction, plan_of(Path.points[index]))) > Tolerance.offset) + return false; + auto const tangentstart{plan_of(path_tangent(Path, false))}; + auto const tangentend{plan_of(path_tangent(Path, true))}; + if (glm::length(tangentstart) < 1e-9 || glm::length(tangentend) < 1e-9) + return false; + auto const a{glm::normalize(tangentstart)}; + auto const b{glm::normalize(tangentend)}; + auto const turn{std::abs(std::atan2(cross(a, b), glm::dot(a, b)))}; + auto const deviation{std::max(std::abs(cross(direction, a)), std::abs(cross(direction, b)))}; + return turn * Tolerance.radius <= length && deviation * Tolerance.radius <= length; +} + +glm::dvec2 direction_at(TTrack const &Track, glm::dvec3 const &Joint, bool const Arriving, bool &Found) +{ + Found = false; + for (auto const &path : Track.m_paths) + { + for (auto const atend : {false, true}) + { + if (glm::distance(path.points[atend ? segment_data::point::end : segment_data::point::start], Joint) > kSamePoint) + continue; + auto const tangent{plan_of(path_tangent(path, atend))}; + if (glm::length(tangent) < 1e-9) + continue; + Found = true; + auto const unit{glm::normalize(tangent)}; + return (atend == Arriving) ? unit : -unit; + } + } + return {0.0, 1.0}; +} + +TTrack *neighbour_at(TTrack const &Track, int const Path, bool const Atend) +{ + if (has_switch_paths(Track)) + return Atend ? Track.SwitchExtension->pNexts[Path & 1] : Track.SwitchExtension->pPrevs[Path & 1]; // NOSONAR + return Atend ? Track.trNext : Track.trPrev; +} + +void rigid_move(TTrack &Track, glm::dvec3 const &Pivot, glm::dvec3 const &Target, double const Angle) +{ + for (auto &path : Track.m_paths) + { + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto &point{path.points[index]}; + auto const moved{plan_of(Target) + turned(plan_of(point - Pivot), Angle)}; + point = {moved.x, point.y + Target.y - Pivot.y, moved.y}; + } + for (auto const index : {segment_data::point::control1, segment_data::point::control2}) + { + auto &control{path.points[index]}; + auto const moved{turned(plan_of(control), Angle)}; + control = {moved.x, control.y, moved.y}; + } + } +} + +void follow_joint(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Angle) +{ + for (auto &path : Track.m_paths) + { + for (auto const atend : {false, true}) + { + auto &point{path.points[atend ? segment_data::point::end : segment_data::point::start]}; + if (glm::distance(point, From) > kSamePoint) + continue; + point = To; + auto &control{path.points[atend ? segment_data::point::control2 : segment_data::point::control1]}; + auto const moved{turned(plan_of(control), Angle)}; + control = {moved.x, control.y, moved.y}; + } + } +} + +} // namespace + +void editor_track::place(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Angle) +{ + rigid_move(Track, From, To, Angle); +} + +bool editor_track::is_straight(TTrack const &Track, straight_tolerance const &Tolerance) +{ + if (false == is_supported(Track) || Track.m_editorremoved) + return false; + if (false == std::any_of(Track.m_paths.begin(), Track.m_paths.end(), [&](segment_data const &Path) { return plan_straight(Path, Tolerance); })) + return false; + if (Track.eType != tt_Normal || Track.trPrev == nullptr || Track.trNext == nullptr) + return true; + auto const &path{Track.m_paths.front()}; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + auto const length{glm::length(plan_of(end - start))}; + auto const direction{glm::normalize(plan_of(end - start))}; + bool foundbefore, foundafter; + auto const before{direction_at(*Track.trPrev, start, true, foundbefore)}; + auto const after{direction_at(*Track.trNext, end, false, foundafter)}; + if (false == foundbefore || false == foundafter) + return true; + auto const kinkbefore{signed_angle(before, direction)}; + auto const kinkafter{signed_angle(direction, after)}; + if (std::abs(kinkbefore) <= Tolerance.angle || std::abs(kinkafter) <= Tolerance.angle || (kinkbefore > 0.0) != (kinkafter > 0.0)) + return true; + return length / ((std::abs(kinkbefore) + std::abs(kinkafter)) * 0.5) >= Tolerance.radius; +} + +editor_track::straight editor_track::find_straight(TTrack &Track, straight_tolerance const &Tolerance) +{ + straight result; + if (false == is_straight(Track, Tolerance)) + return result; + + int firstpath{0}; + while (false == plan_straight(Track.m_paths[firstpath], Tolerance)) + ++firstpath; + auto const &first{Track.m_paths[firstpath]}; + auto const origin{first.points[segment_data::point::start]}; + auto const direction{glm::normalize(plan_of(first.points[segment_data::point::end] - origin))}; + auto const lateral = [&](glm::dvec3 const &Point) { return std::abs(cross(direction, plan_of(Point - origin))); }; + auto const accepts = [&](TTrack const *Other, glm::dvec3 const &Joint) { + if (Other == nullptr || false == is_supported(*Other) || Other->m_editorremoved) + return -1; + for (int i = 0; i < static_cast(Other->m_paths.size()); ++i) + { + auto const &path{Other->m_paths[i]}; + if (false == plan_straight(path, Tolerance)) + continue; + auto const &start{path.points[segment_data::point::start]}; + auto const &end{path.points[segment_data::point::end]}; + if (glm::distance(start, Joint) > kSamePoint && glm::distance(end, Joint) > kSamePoint) + continue; + auto const otherdirection{glm::normalize(plan_of(end - start))}; + if (std::abs(cross(direction, otherdirection)) > Tolerance.angle) + continue; + if (lateral(start) > Tolerance.offset || lateral(end) > Tolerance.offset) + continue; + return i; + } + return -1; + }; + + std::deque> run{{&Track, firstpath}}; + for (auto const forward : {true, false}) + { + TTrack *current{&Track}; + int path{firstpath}; + bool atend{forward}; + while (true) + { + auto const &exit{current->m_paths[path].points[atend ? segment_data::point::end : segment_data::point::start]}; + auto *next{neighbour_at(*current, path, atend)}; + if (std::any_of(run.begin(), run.end(), [&](auto const &Member) { return Member.first == next; })) + break; + auto const nextpath{accepts(next, exit)}; + if (nextpath < 0) + break; + if (forward) + run.emplace_back(next, nextpath); + else + run.emplace_front(next, nextpath); + atend = glm::distance(next->m_paths[nextpath].points[segment_data::point::start], exit) <= kSamePoint; + current = next; + path = nextpath; + } + } + + double low{0.0}, high{0.0}; + glm::dvec3 lowpoint{origin}, highpoint{origin}; + for (auto const &member : run) + { + result.tracks.push_back(member.first); + result.paths.push_back(member.second); + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto const &point{member.first->m_paths[member.second].points[index]}; + auto const along{glm::dot(plan_of(point - origin), direction)}; + if (along < low) + { + low = along; + lowpoint = point; + } + if (along > high) + { + high = along; + highpoint = point; + } + } + } + result.start = lowpoint; + result.end = highpoint; + result.direction = direction; + result.length = high - low; + result.grade = result.length > 0.0 ? (highpoint.y - lowpoint.y) / result.length : 0.0; + auto azimuth{glm::degrees(std::atan2(direction.x, direction.y))}; + if (azimuth < 0.0) + azimuth += 360.0; + result.azimuth = azimuth; + return result; +} + +std::vector editor_track::find_straights(double const Minimumlength, straight_tolerance const &Tolerance) +{ + std::vector result; + std::unordered_set visited; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || visited.count(track) > 0 || false == is_straight(*track, Tolerance)) + continue; + auto line{find_straight(*track, Tolerance)}; + for (auto const *member : line.tracks) + visited.insert(member); + if (line.length >= Minimumlength) + result.push_back(std::move(line)); + } + std::sort(result.begin(), result.end(), [](straight const &A, straight const &B) { return A.length > B.length; }); + return result; +} + +bool editor_track::find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve) +{ + Curve = {}; + auto const curved = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && false == is_straight(*Other, Tolerance); }; + if (false == curved(&Track)) + return false; + + std::deque> run{{&Track, true}}; + auto const known = [&](TTrack const *Other) { return std::any_of(run.begin(), run.end(), [&](auto const &Member) { return Member.first == Other; }); }; + for (auto const throughend : {true, false}) + { + TTrack *previous{&Track}; + TTrack *current{throughend ? Track.trNext : Track.trPrev}; + while (current != nullptr && false == known(current)) + { + if (current->eType == tt_Switch && has_switch_paths(*current) && is_supported(*current) && false == current->m_editorremoved) + { + auto const &extension{*current->SwitchExtension}; + bool const atstart{extension.pPrevs[0] == previous}; + if (atstart == (extension.pNexts[0] == previous)) + break; + auto *beyond{atstart ? extension.pNexts[0] : extension.pPrevs[0]}; + if (false == curved(beyond) || known(beyond)) + break; + if (throughend) + run.emplace_back(current, atstart); + else + run.emplace_front(current, false == atstart); + previous = current; + current = beyond; + continue; + } + if (false == curved(current)) + break; + auto const entersatstart{current->trPrev == previous}; + if (throughend) + run.emplace_back(current, entersatstart); + else + run.emplace_front(current, false == entersatstart); + previous = current; + current = entersatstart ? current->trNext : current->trPrev; + } + } + return analyse_curve({run.begin(), run.end()}, Gauge, Curve); +} + +bool editor_track::analyse_curve(std::vector> const &Run, double const Gauge, curve &Curve) +{ + Curve = {}; + if (Run.empty()) + return false; + auto const &run{Run}; + std::vector turns; + std::vector lengths; + std::vector varying; + std::vector curvatures; + auto const incoming = [&](std::size_t const Index) { + auto const &path{run[Index].first->m_paths.front()}; + auto const tangent{glm::normalize(plan_of(run[Index].second ? path_tangent(path, false) : -path_tangent(path, true)))}; + return tangent; + }; + auto const outgoing = [&](std::size_t const Index) { + auto const &path{run[Index].first->m_paths.front()}; + auto const tangent{glm::normalize(plan_of(run[Index].second ? path_tangent(path, true) : -path_tangent(path, false)))}; + return tangent; + }; + auto const joint = [&](std::size_t const Index, bool const Atend) { + auto const &path{run[Index].first->m_paths.front()}; + return path.points[(run[Index].second == Atend) ? segment_data::point::end : segment_data::point::start]; + }; + std::vector kinks(run.size() + 1, 0.0); + { + auto *before{run.front().second ? run.front().first->trPrev : run.front().first->trNext}; + bool found{false}; + if (before != nullptr) + { + auto const direction{direction_at(*before, joint(0, false), true, found)}; + if (found) + kinks.front() = signed_angle(direction, incoming(0)); + } + auto *after{run.back().second ? run.back().first->trNext : run.back().first->trPrev}; + if (after != nullptr) + { + auto const direction{direction_at(*after, joint(run.size() - 1, true), false, found)}; + if (found) + kinks.back() = signed_angle(outgoing(run.size() - 1), direction); + } + for (std::size_t i = 1; i < run.size(); ++i) + kinks[i] = signed_angle(outgoing(i - 1), incoming(i)); + } + for (std::size_t i = 0; i < run.size(); ++i) + { + auto const &path{run[i].first->m_paths.front()}; + auto const length{run[i].first->eType == tt_Switch ? bezier{path}.plan_length() : run[i].first->Length()}; + auto const internal{signed_angle(incoming(i), outgoing(i))}; + turns.push_back(internal + kinks[i] * 0.5 + kinks[i + 1] * 0.5); + lengths.push_back(length); + auto const radiusstart{path_radius(path, 0.0)}; + auto const radiusend{path_radius(path, 1.0)}; + auto const curvaturestart{radiusstart > 0.0 ? 1.0 / radiusstart : 0.0}; + auto const curvatureend{radiusend > 0.0 ? 1.0 / radiusend : 0.0}; + auto const middle{path_radius(path, 0.5)}; + double curvature{middle > 0.0 ? 1.0 / middle : 0.0}; + if (curvature == 0.0 && path.radius != 0.f) + curvature = 1.0 / std::abs(path.radius); + if (curvature == 0.0 && length > 1e-3) + curvature = std::abs(turns.back()) / length; + curvatures.push_back(curvature); + varying.push_back(std::abs(curvaturestart - curvatureend) > 0.1 * std::max(curvaturestart, curvatureend)); + for (auto const roll : path.rolls) + Curve.cant = std::max(Curve.cant, Gauge * std::sin(std::abs(glm::radians(static_cast(roll))))); + } + auto const peak{*std::max_element(curvatures.begin(), curvatures.end())}; + // old sceneries make a transition of short arcs of growing radius, a longer arc of a larger radius is a part of a compound curve + constexpr double kShortestCompoundArc{30.0}; + for (std::size_t first = 0; first < run.size();) + { + auto last{first + 1}; + while (last < run.size() && false == varying[first] && false == varying[last] && std::abs(curvatures[last] - curvatures[first]) <= 0.05 * curvatures[first]) + ++last; + auto const length{std::accumulate(lengths.begin() + first, lengths.begin() + last, 0.0)}; + if (curvatures[first] < 0.9 * peak && (varying[first] || length < kShortestCompoundArc || curvatures[first] < 0.1 * peak)) + for (auto i = first; i < last; ++i) + varying[i] = true; + first = last; + } + double smallest{0.0}; + for (std::size_t i = 0; i < run.size(); ++i) + if (false == varying[i] && curvatures[i] > 0.0 && (smallest == 0.0 || 1.0 / curvatures[i] < smallest)) + smallest = 1.0 / curvatures[i]; + if (smallest == 0.0 && peak > 0.0) + smallest = 1.0 / peak; + int sign{0}; + for (auto const turn : turns) + { + Curve.turn += turn; + int const current{turn > 1e-9 ? 1 : turn < -1e-9 ? -1 : 0}; + if (current != 0 && sign != 0 && current != sign) + ++Curve.reversals; + if (current != 0) + sign = current; + } + for (std::size_t i = 0; i < run.size() && varying[i]; ++i) + Curve.transition_in += lengths[i]; + for (std::size_t i = run.size(); i > 0 && varying[i - 1]; --i) + Curve.transition_out += lengths[i - 1]; + if (Curve.transition_in + Curve.transition_out >= std::accumulate(lengths.begin(), lengths.end(), 0.0)) + Curve.transition_in = Curve.transition_out = 0.0; + Curve.radius = smallest; + std::vector arcs; + double between{0.0}; + for (std::size_t i = 0; i < run.size(); ++i) + { + if (varying[i] || curvatures[i] <= 0.0) + { + if (false == arcs.empty()) + between += lengths[i]; + continue; + } + auto const radius{1.0 / curvatures[i]}; + if (false == arcs.empty() && std::abs(arcs.back().radius - radius) < 0.05 * radius) + arcs.back().turn += (lengths[i] + between) / radius; + else + arcs.push_back({radius, lengths[i] / radius, arcs.empty() ? 0.0 : between}); + between = 0.0; + } + if (arcs.size() >= 2 && Curve.reversals == 0) + { + auto const [low, high]{std::minmax_element(arcs.begin(), arcs.end(), [](curve::arc_part const &A, curve::arc_part const &B) { return A.radius < B.radius; })}; + if (high->radius - low->radius > 0.1 * low->radius) + { + Curve.compound = true; + Curve.radius = arcs.front().radius; + Curve.arcs = arcs; + } + } + Curve.from = run.front().first; + Curve.to = run.back().first; + return true; +} + +bool editor_track::find_run(TTrack &Track, double const Limit, chain &Chain, std::string &Error) +{ + auto const category{Track.iCategoryFlag & 15}; + auto const regular = [&](TTrack const *Other) { return Other != nullptr && Other->eType == tt_Normal && is_supported(*Other) && false == Other->m_editorremoved && (Other->iCategoryFlag & 15) == category; }; + if (false == regular(&Track)) + { + Error = "A line is made of regular paths, a switch ends it"; + return false; + } + straight_tolerance const tolerance; + TTrack *ends[2]{&Track, &Track}; + std::vector visited{&Track}; + for (int side = 0; side < 2; ++side) + { + TTrack *previous{&Track}; + TTrack *current{side == 0 ? Track.trNext : Track.trPrev}; + double length{0.0}; + // past the limit the line goes on to the next straight, so it doesn't end inside a curve + while (regular(current) && std::find(visited.begin(), visited.end(), current) == visited.end() && (length < Limit || (length < 3.0 * Limit && false == is_straight(*ends[side], tolerance)))) + { + visited.push_back(current); + ends[side] = current; + length += current->Length(); + auto const entersatstart{current->trPrev == previous}; + previous = current; + current = entersatstart ? current->trNext : current->trPrev; + } + } + return find_chain(ends[1], ends[0], Chain, Error); +} + +std::vector editor_track::recognize_line(chain const &Chain, straight_tolerance const &Tolerance, double const Gauge) +{ + std::vector result; + auto const count{Chain.tracks.size()}; + auto const path = [&](std::size_t const Index) -> segment_data const & { return Chain.tracks[Index]->m_paths.front(); }; + auto const tangent_in = [&](std::size_t const Index) { return glm::normalize(plan_of(Chain.forward[Index] ? path_tangent(path(Index), false) : -path_tangent(path(Index), true))); }; + auto const tangent_out = [&](std::size_t const Index) { return glm::normalize(plan_of(Chain.forward[Index] ? path_tangent(path(Index), true) : -path_tangent(path(Index), false))); }; + auto const point_in = [&](std::size_t const Index) { return plan_of(path(Index).points[Chain.forward[Index] ? segment_data::point::start : segment_data::point::end]); }; + auto const point_out = [&](std::size_t const Index) { return plan_of(path(Index).points[Chain.forward[Index] ? segment_data::point::end : segment_data::point::start]); }; + + std::vector straight(count); + std::vector turns(count); + for (std::size_t i = 0; i < count; ++i) + { + straight[i] = is_straight(*Chain.tracks[i], Tolerance); + turns[i] = signed_angle(tangent_in(i), tangent_out(i)); + } + auto const add_curve = [&](std::size_t const First, std::size_t const Last) { + auto const a{point_in(First)}; + auto const b{point_out(Last)}; + auto const directionin{tangent_in(First)}; + auto const directionout{tangent_out(Last)}; + auto const determinant{cross(directionin, directionout)}; + if (std::abs(determinant) < 1e-6) + return; + line_vertex vertex; + vertex.position = a + directionin * (cross(b - a, directionout) / determinant); + std::vector> run; + for (auto i = First; i <= Last; ++i) + run.emplace_back(Chain.tracks[i], Chain.forward[i]); + analyse_curve(run, Gauge, vertex.shape); + result.push_back(vertex); + }; + std::size_t i{0}; + while (i < count) + { + if (straight[i]) + { + if (i + 1 < count && straight[i + 1] && std::abs(signed_angle(tangent_out(i), tangent_in(i + 1))) > 1e-3) + { + line_vertex vertex; + vertex.position = point_out(i); + vertex.kink = true; + result.push_back(vertex); + } + ++i; + continue; + } + auto const first{i}; + int sign{0}; + while (i < count && false == straight[i]) + { + int const current{turns[i] > 1e-9 ? 1 : turns[i] < -1e-9 ? -1 : 0}; + // a reverse curve: the next one starts here + if (current != 0 && sign != 0 && current != sign) + break; + if (current != 0) + sign = current; + ++i; + } + add_curve(first, i - 1); + } + return result; +} + +namespace +{ + +// shorter remainders of a straight path are absorbed by moving the joint instead of splitting the path +double split_margin(double const Length) +{ + return std::max(0.5, 0.002 * Length); +} + +} // namespace + +editor_track::straight_run editor_track::straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance) +{ + straight_run run; + if (Chain.tracks.empty()) + return run; + auto *edge{Atend ? Chain.tracks.back() : Chain.tracks.front()}; + bool const forward{Atend ? Chain.forward.back() : Chain.forward.front()}; + TTrack *current{forward == Atend ? edge->trNext : edge->trPrev}; + auto joint{Atend ? Chain.end : Chain.start}; + auto const origin{plan_of(joint)}; + auto direction{glm::normalize(plan_of(Atend ? Chain.end_direction : -Chain.start_direction))}; + while (current != nullptr && run.tracks.size() < 1000) + { + std::string reason; + if (current->eType != tt_Normal || current->m_editorremoved || false == can_edit_geometry(*current, reason)) + break; + if (std::find(Chain.tracks.begin(), Chain.tracks.end(), current) != Chain.tracks.end() || std::find(run.tracks.begin(), run.tracks.end(), current) != run.tracks.end()) + break; + auto const &path{current->m_paths.front()}; + if (false == plan_straight(path, Tolerance)) + break; + bool outward; + if (glm::distance(path.points[segment_data::point::start], joint) <= kSamePoint) + outward = true; + else if (glm::distance(path.points[segment_data::point::end], joint) <= kSamePoint) + outward = false; + else + break; + auto const &opposite{path.points[outward ? segment_data::point::end : segment_data::point::start]}; + if (run.tracks.empty()) + { + auto const own{glm::normalize(plan_of(opposite - joint))}; + if (std::abs(cross(direction, own)) > 0.01 || glm::dot(direction, own) <= 0.0) + break; + direction = own; + } + auto const offset{plan_of(opposite) - origin}; + auto const along{glm::dot(offset, direction)}; + if (along <= run.length + 1e-3 || std::abs(cross(direction, offset)) > Tolerance.offset) + break; + run.tracks.push_back(current); + run.outward.push_back(outward); + run.length = along; + joint = opposite; + current = outward ? current->trNext : current->trPrev; + } + return run; +} + +double editor_track::run_reserve(straight_run const &Run) +{ + if (Run.tracks.empty()) + return 0.0; + return std::max(0.0, Run.length - split_margin(chord_length(Run.tracks.back()->m_paths.front())) - 0.01); +} + +TTrack *editor_track::take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector> &States, std::vector &Created) +{ + auto const length{glm::length(plan_of(Cut - Joint))}; + auto *edge{Edge}; + double covered{0.0}; + for (std::size_t i = 0; i < Run.tracks.size(); ++i) + { + auto *track{Run.tracks[i]}; + auto const outward{Run.outward[i]}; + auto const &path{track->m_paths.front()}; + auto const pathlength{chord_length(path)}; + auto const margin{split_margin(pathlength)}; + auto const remaining{length - covered}; + if (remaining <= margin) + { + if (std::abs(remaining) > 1e-4) + { + States.emplace_back(track, capture(*track)); + move_point(*track, {0, outward ? point_kind::start : point_kind::end}, Cut); + commit({track}); + } + break; + } + if (remaining < pathlength - margin) + { + auto const &nearpoint{path.points[outward ? segment_data::point::start : segment_data::point::end]}; + auto const &farpoint{path.points[outward ? segment_data::point::end : segment_data::point::start]}; + auto const t{nearest_parameter(*track, nearpoint + (farpoint - nearpoint) * (remaining / pathlength))}; + auto const before{capture(*track)}; + auto *created{split_path(*track, t)}; + if (created == nullptr) + break; + States.emplace_back(track, before); + Created.push_back(created); + edge = outward ? track : created; + break; + } + edge = track; + covered += pathlength; + } + return edge; +} + +namespace +{ + +using route_node = std::tuple; + +std::vector route_next(route_node const &Node) +{ + std::vector result; + auto *track{std::get<0>(Node)}; + auto const path{std::get<1>(Node)}; + auto const forward{std::get<2>(Node)}; + auto const &exit{track->m_paths[path].points[forward ? segment_data::point::end : segment_data::point::start]}; + auto *next{neighbour_at(*track, path, forward)}; + if (next == nullptr || next == track || next->m_editorremoved || false == editor_track::is_supported(*next) || (next->iCategoryFlag & 15) != (track->iCategoryFlag & 15)) + return result; + for (int i = 0; i < static_cast(next->m_paths.size()); ++i) + { + auto const &candidate{next->m_paths[i]}; + if (glm::distance(candidate.points[segment_data::point::start], exit) <= kSamePoint) + result.emplace_back(next, i, true); + else if (glm::distance(candidate.points[segment_data::point::end], exit) <= kSamePoint) + result.emplace_back(next, i, false); + } + return result; +} + +} // namespace + +bool editor_track::find_route(TTrack *From, TTrack *To, route &Route, std::string &Error) +{ + Route = {}; + if (From == nullptr || To == nullptr || false == is_supported(*From) || false == is_supported(*To)) + { + Error = "Select the first and the last path of the route"; + return false; + } + if (From == To) + { + Route.spans.push_back({From, 0, true}); + Error.clear(); + return true; + } + std::vector nodes; + std::vector parents; + std::map visited; + for (int i = 0; i < static_cast(From->m_paths.size()); ++i) + for (auto const forward : {true, false}) + { + route_node const node{From, i, forward}; + visited.emplace(node, static_cast(nodes.size())); + nodes.push_back(node); + parents.push_back(-1); + } + int goal{-1}; + for (std::size_t index = 0; index < nodes.size() && goal < 0 && nodes.size() < 200000; ++index) + { + for (auto const &next : route_next(nodes[index])) + { + if (std::get<0>(next) == From || visited.count(next) > 0) + continue; + visited.emplace(next, static_cast(nodes.size())); + nodes.push_back(next); + parents.push_back(static_cast(index)); + if (std::get<0>(next) == To) + { + goal = static_cast(nodes.size()) - 1; + break; + } + } + } + if (goal < 0) + { + Error = "No route connects the selected paths"; + return false; + } + for (int index = goal; index >= 0; index = parents[index]) + Route.spans.push_back({std::get<0>(nodes[index]), std::get<1>(nodes[index]), std::get<2>(nodes[index])}); + std::reverse(Route.spans.begin(), Route.spans.end()); + Error.clear(); + return true; +} + +editor_track::route editor_track::run_route(TTrack &Track, double const Maximum) +{ + route result; + if (false == is_supported(Track)) + return result; + auto const walk = [&](route_node Node) { + std::vector nodes; + double length{0.0}; + while (nodes.size() < 5000 && length < Maximum) + { + auto candidates{route_next(Node)}; + if (candidates.empty()) + break; + auto chosen{candidates.front()}; + for (auto const &candidate : candidates) + if (std::get<1>(candidate) == 0) + chosen = candidate; + auto *track{std::get<0>(chosen)}; + if (track == &Track || std::any_of(nodes.begin(), nodes.end(), [&](route_node const &Other) { return std::get<0>(Other) == track; })) + break; + nodes.push_back(chosen); + length += bezier(track->m_paths[std::get<1>(chosen)]).plan_length(8); + Node = chosen; + } + return nodes; + }; + auto const ahead{walk({&Track, 0, true})}; + auto const behind{walk({&Track, 0, false})}; + for (auto node = behind.rbegin(); node != behind.rend(); ++node) + if (std::none_of(ahead.begin(), ahead.end(), [&](route_node const &Other) { return std::get<0>(Other) == std::get<0>(*node); })) + result.spans.push_back({std::get<0>(*node), std::get<1>(*node), false == std::get<2>(*node)}); + result.spans.push_back({&Track, 0, true}); + for (auto const &node : ahead) + result.spans.push_back({std::get<0>(node), std::get<1>(node), std::get<2>(node)}); + return result; +} + +editor_track::route editor_track::route_of(chain const &Chain) +{ + route result; + for (std::size_t i = 0; i < Chain.tracks.size(); ++i) + result.spans.push_back({Chain.tracks[i], 0, Chain.forward[i]}); + return result; +} + +std::vector editor_track::sample_route(route &Route, double const Step) +{ + std::vector samples; + double chainage{0.0}; + for (std::size_t i = 0; i < Route.spans.size(); ++i) + { + auto &span{Route.spans[i]}; + auto const &path{span.track->m_paths[span.path]}; + bezier const curve{path}; + auto const count{std::max(2, static_cast(std::ceil(curve.plan_length(16) / std::max(0.1, Step))))}; + span.from = chainage; + auto previous{plan_of(curve.point(span.forward ? 0.0 : 1.0))}; + for (int j = (samples.empty() ? 0 : 1); j <= count; ++j) + { + auto const t{span.forward ? static_cast(j) / count : 1.0 - static_cast(j) / count}; + auto const position{curve.point(t)}; + chainage += glm::distance(previous, plan_of(position)); + previous = plan_of(position); + auto derivative{curve.first(t)}; + if (false == span.forward) + derivative = -derivative; + auto const second{curve.second(t)}; + auto const planar{std::hypot(derivative.x, derivative.z)}; + route_sample sample; + sample.chainage = chainage; + sample.position = position; + sample.span = i; + sample.cant = 1.5 * std::sin(std::abs(glm::radians(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * t))); + if (planar > 1e-9) + { + sample.direction = plan_of(derivative) / planar; + sample.grade = derivative.y / planar; + sample.curvature = (derivative.x * second.z - derivative.z * second.x) / (planar * planar * planar); + } + samples.push_back(sample); + } + span.to = chainage; + } + Route.length = chainage; + return samples; +} + +namespace +{ + +template +auto interpolate(std::vector const &Samples, double const Chainage, Getter const &Get) -> decltype(Get(Samples.front())) +{ + if (Samples.empty()) + return {}; + auto const next{std::lower_bound(Samples.begin(), Samples.end(), Chainage, [](editor_track::route_sample const &Sample, double const Value) { return Sample.chainage < Value; })}; + if (next == Samples.begin()) + return Get(Samples.front()); + if (next == Samples.end()) + return Get(Samples.back()); + auto const &before{*std::prev(next)}; + auto const run{next->chainage - before.chainage}; + return run > 1e-9 ? Get(before) + (Get(*next) - Get(before)) * (Chainage - before.chainage) / run : Get(before); +} + +void set_heights(segment_data &Path, double const Startheight, double const Endheight, double const Startgrade, double const Endgrade) +{ + auto &start{Path.points[segment_data::point::start]}; + auto &end{Path.points[segment_data::point::end]}; + auto &control1{Path.points[segment_data::point::control1]}; + auto &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + start.y = Startheight; + end.y = Endheight; + control1.y = Startgrade * std::hypot(control1.x, control1.z); + control2.y = -Endgrade * std::hypot(control2.x, control2.z); +} + +// the path lies in the plane through Origin which rises with Grade along Direction (unit, in plan) and is level across it +void plane_heights(segment_data &Path, glm::dvec3 const &Origin, glm::dvec2 const &Direction, double const Grade) +{ + auto &start{Path.points[segment_data::point::start]}; + auto &end{Path.points[segment_data::point::end]}; + auto &control1{Path.points[segment_data::point::control1]}; + auto &control2{Path.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + control1 = (end - start) / 3.0; + control2 = (start - end) / 3.0; + } + auto const height = [&](glm::dvec3 const &Point) { return Origin.y + Grade * glm::dot(plan_of(Point - Origin), Direction); }; + auto const first{start + control1}; + auto const second{end + control2}; + start.y = height(start); + end.y = height(end); + control1.y = height(first) - start.y; + control2.y = height(second) - end.y; +} + +glm::dvec2 start_direction(segment_data const &Path) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &control{Path.points[segment_data::point::control1]}; + glm::dvec2 direction{control.x, control.z}; + if (glm::length(direction) < 1e-6) + direction = {Path.points[segment_data::point::end].x - start.x, Path.points[segment_data::point::end].z - start.z}; + return glm::length(direction) > 1e-9 ? glm::normalize(direction) : glm::dvec2{1.0, 0.0}; +} + +} // namespace + +double editor_track::sampled_elevation(std::vector const &Samples, double const Chainage) +{ + return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position.y; }); +} + +double editor_track::sampled_grade(std::vector const &Samples, double const Chainage) +{ + return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.grade; }); +} + +glm::dvec3 editor_track::sampled_position(std::vector const &Samples, double const Chainage) +{ + return interpolate(Samples, Chainage, [](route_sample const &Sample) { return Sample.position; }); +} + +bool editor_track::adjoining_grade(route const &Route, bool const Atend, double &Grade) +{ + if (Route.spans.empty()) + return false; + auto const &span{Atend ? Route.spans.back() : Route.spans.front()}; + auto const outward{Atend ? span.forward : false == span.forward}; + auto const next{route_next({span.track, span.path, outward})}; + if (next.empty()) + return false; + auto const &[track, path, forward]{next.front()}; + auto const tangent{path_tangent(track->m_paths[path], false == forward)}; + auto const plan{std::hypot(tangent.x, tangent.z)}; + if (plan < 1e-9) + return false; + auto const along{(forward ? 1.0 : -1.0) * tangent.y / plan}; + Grade = Atend ? along : -along; + return true; +} + +std::vector editor_track::apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &Created) +{ + double constexpr margin{0.25}; + auto const remember = [&](TTrack *Track) { + if (std::none_of(States.begin(), States.end(), [&](auto const &Entry) { return Entry.first == Track; }) && std::find(Created.begin(), Created.end(), Track) == Created.end()) + States.emplace_back(Track, capture(*Track)); + }; + sample_route(Route, 1.0); + std::vector spans; + for (auto const &span : Route.spans) + { + if (span.track->eType != tt_Normal) + { + spans.push_back(span); + continue; + } + std::vector cuts; + for (auto const chainage : Breaks) + if (chainage > span.from + margin && chainage < span.to - margin) + cuts.push_back(chainage); + if (cuts.empty()) + { + spans.push_back(span); + continue; + } + remember(span.track); + if (span.forward) + { + auto *piece{span.track}; + auto origin{span.from}; + spans.push_back({piece, 0, true}); + for (auto const cut : cuts) + { + auto const t{bezier(piece->m_paths.front()).parameter(cut - origin)}; + auto *created{split_path(*piece, t)}; + if (created == nullptr) + continue; + Created.push_back(created); + spans.push_back({created, 0, true}); + piece = created; + origin = cut; + } + } + else + { + std::vector order; + for (auto cut = cuts.begin(); cut != cuts.end(); ++cut) + { + auto const t{bezier(span.track->m_paths.front()).parameter(span.to - *cut)}; + auto *created{split_path(*span.track, t)}; + if (created == nullptr) + continue; + Created.push_back(created); + order.push_back(created); + } + for (auto *piece : order) + spans.push_back({piece, 0, false}); + spans.push_back({span.track, 0, false}); + } + } + Route.spans = spans; + sample_route(Route, 1.0); + + auto const on_route = [&](TTrack const *Track) { + return std::any_of(Route.spans.begin(), Route.spans.end(), [&](route_span const &Span) { return Span.track == Track; }); + }; + struct joint + { + TTrack *track; + int path; + bool atend; + TTrack *neighbour; + }; + std::vector joints; + std::vector changed; + for (auto const &span : Route.spans) + { + remember(span.track); + auto const startchainage{span.forward ? span.from : span.to}; + auto const endchainage{span.forward ? span.to : span.from}; + auto const sign{span.forward ? 1.0 : -1.0}; + auto &path{span.track->m_paths[span.path]}; + set_heights(path, Elevation(startchainage), Elevation(endchainage), Grade(startchainage) * sign, Grade(endchainage) * sign); + add_unique(changed, span.track); + if (span.track->eType == tt_Normal) + continue; + auto const grade{Grade((span.from + span.to) * 0.5) * sign}; + for (int i = 0; i < static_cast(span.track->m_paths.size()); ++i) + { + for (auto const atend : {false, true}) + { + auto *neighbour{neighbour_at(*span.track, i, atend)}; + if (neighbour != nullptr && false == on_route(neighbour) && std::none_of(joints.begin(), joints.end(), [&](joint const &Joint) { return Joint.track == span.track && Joint.neighbour == neighbour; })) + joints.push_back({span.track, i, atend, neighbour}); + } + if (i != span.path) + plane_heights(span.track->m_paths[i], path.points[segment_data::point::start], start_direction(path), grade); + } + } + commit(changed); + sample_route(Route, 1.0); + + std::vector gaps; + std::vector raised; + for (auto const &joint : joints) + { + auto const &end{joint.track->m_paths[joint.path].points[joint.atend ? segment_data::point::end : segment_data::point::start]}; + double nearest{std::numeric_limits::max()}; + double gap{0.0}; + glm::dvec3 *closest{nullptr}; + for (auto &path : joint.neighbour->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + { + auto &point{path.points[index]}; + auto const distance{std::hypot(point.x - end.x, point.z - end.z)}; + if (distance < nearest) + { + nearest = distance; + gap = point.y - end.y; + closest = &point; + } + } + if (nearest > kSamePoint || std::abs(gap) <= 0.001) + continue; + // a regular path off the route takes the height of the switch at the joint, keeping its other end + std::string reason; + bool const closed{joint.neighbour->eType == tt_Normal && can_edit_geometry(*joint.neighbour, reason)}; + if (closed) + { + remember(joint.neighbour); + closest->y = end.y; + add_unique(raised, joint.neighbour); + } + gaps.push_back({joint.track, joint.neighbour, gap, closed}); + } + commit(raised); + return gaps; +} + +void editor_track::keep_heights(chain const &Chain, std::vector &Pieces) +{ + auto existing{route_of(Chain)}; + auto const samples{sample_route(existing, 1.0)}; + if (samples.empty() || existing.length <= 1e-6) + return; + std::vector lengths; + double total{0.0}; + for (auto const &piece : Pieces) + { + lengths.push_back(bezier(piece).plan_length()); + total += lengths.back(); + } + if (total <= 1e-6) + return; + auto const scale{existing.length / total}; + double chainage{0.0}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + { + auto const from{chainage * scale}; + auto const to{(chainage + lengths[i]) * scale}; + set_heights(Pieces[i], sampled_elevation(samples, from), sampled_elevation(samples, to), sampled_grade(samples, from) * scale, sampled_grade(samples, to) * scale); + chainage += lengths[i]; + } +} + +std::vector editor_track::find_switch_templates() +{ + std::vector result; + std::map lookup; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2 || (track->iCategoryFlag & 0x80) != 0) + continue; + auto const &main{track->m_paths[0]}; + auto const origin{main.points[segment_data::point::start]}; + auto const chord{plan_of(main.points[segment_data::point::end] - origin)}; + if (glm::length(chord) < 1.0) + continue; + auto const axis{glm::normalize(chord)}; + glm::dvec2 const normal{-axis.y, axis.x}; + auto const tolocal = [&](glm::dvec3 const &Vector) { + auto const planar{plan_of(Vector)}; + return glm::dvec3{glm::dot(planar, normal), Vector.y, glm::dot(planar, axis)}; + }; + switch_template entry; + entry.source = track; + for (int i = 0; i < 2; ++i) + { + auto const &path{track->m_paths[i]}; + auto &local{entry.local[i]}; + local = path; + local.points[segment_data::point::start] = tolocal(path.points[segment_data::point::start] - origin); + local.points[segment_data::point::end] = tolocal(path.points[segment_data::point::end] - origin); + local.points[segment_data::point::control1] = tolocal(path.points[segment_data::point::control1]); + local.points[segment_data::point::control2] = tolocal(path.points[segment_data::point::control2]); + } + if (glm::length(plan_of(entry.local[1].points[segment_data::point::start])) > kSamePoint) + continue; + if (entry.local[1].points[segment_data::point::end].x < 0.0) + { + for (auto &path : entry.local) + { + for (auto &point : path.points) + point.x = -point.x; + path.rolls = {-path.rolls[0], -path.rolls[1]}; + } + } + entry.length = entry.local[0].points[segment_data::point::end].z; + auto const tangent{path_tangent(entry.local[1], true)}; + auto const angle{std::atan2(std::abs(tangent.x), tangent.z)}; + entry.ratio = angle > 1e-6 ? 1.0 / std::tan(angle) : 0.0; + entry.radius = path_radius(entry.local[1], 0.25); + auto const key{format("%.2f|%.2f|%.2f|%s|%s", entry.length, entry.local[1].points[segment_data::point::end].x, entry.local[1].points[segment_data::point::end].z, material_name(track->m_material1).c_str(), material_name(track->m_material2).c_str())}; + auto const found{lookup.find(key)}; + if (found != lookup.end()) + { + ++result[found->second].count; + continue; + } + entry.count = 1; + entry.label = format("L %.2f m 1:%.1f R %.0f m %s", entry.length, entry.ratio, entry.radius, material_name(track->m_material1).c_str()); + lookup.emplace(key, result.size()); + result.push_back(entry); + } + std::sort(result.begin(), result.end(), [](switch_template const &A, switch_template const &B) { return A.count > B.count; }); + return result; +} + +std::vector editor_track::place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade) +{ + std::vector result; + glm::dvec2 const normal{-Direction.y, Direction.x}; + auto const side{static_cast(Side)}; + auto const vector = [&](glm::dvec3 const &Local) { + auto const planar{Direction * Local.z + normal * (Local.x * side)}; + return glm::dvec3{planar.x, Local.y + Grade * Local.z, planar.y}; + }; + for (auto const &local : Template.local) + { + segment_data path{local}; + path.points[segment_data::point::start] = Origin + vector(local.points[segment_data::point::start]); + path.points[segment_data::point::end] = Origin + vector(local.points[segment_data::point::end]); + path.points[segment_data::point::control1] = vector(local.points[segment_data::point::control1]); + path.points[segment_data::point::control2] = vector(local.points[segment_data::point::control2]); + if (Side < 0) + path.rolls = {-local.rolls[0], -local.rolls[1]}; + result.push_back(path); + } + return result; +} + +std::vector editor_track::standard_switch_templates() +{ + struct definition + { + char const *name; + double radius; + double ratio; + double a; + double b; + }; + definition const definitions[] = { + {"Rz 1:9 R190", 190.0, 9.0, 10.523, 16.615}, + {"Rz 1:9 R300", 300.0, 9.0, 16.615, 16.615}, + {"Rz 1:12 R500", 500.0, 12.0, 20.797, 0.0}, + {"Rz 1:14 R760", 760.0, 14.0, 0.0, 0.0}, + {"Rz 1:18,5 R1200", 1200.0, 18.5, 32.409, 34.943}, + {"Rz 1:26,5 R2500", 2500.0, 26.5, 0.0, 0.0}, + }; + std::vector result; + for (auto const &definition : definitions) + { + auto const angle{std::atan(1.0 / definition.ratio)}; + auto const a{definition.a > 0.0 ? definition.a : definition.radius * std::tan(angle * 0.5)}; + auto const estimated{definition.b <= 0.0}; + auto const b{estimated ? 1.87 * definition.ratio : definition.b}; + auto const offset{b * std::tan(angle)}; + switch_template entry; + entry.a = a; + entry.b = b; + entry.length = a + b; + entry.ratio = definition.ratio; + entry.radius = definition.radius; + auto &main{entry.local[0]}; + main.points[segment_data::point::end] = {0.0, 0.0, a + b}; + auto &diverging{entry.local[1]}; + diverging.points[segment_data::point::end] = {offset, 0.0, a + b}; + diverging.points[segment_data::point::control1] = {0.0, 0.0, a * 2.0 / 3.0}; + diverging.points[segment_data::point::control2] = {-offset * 2.0 / 3.0, 0.0, -b * 2.0 / 3.0}; + diverging.radius = static_cast(definition.radius); + entry.label = std::string{definition.name} + " a " + std::to_string(a).substr(0, std::to_string(a).find('.') + 4) + " b " + std::to_string(b).substr(0, std::to_string(b).find('.') + 4) + (estimated ? " (b est.)" : ""); + result.push_back(entry); + } + for (auto const &slipping : {std::pair{190.0, "1:9"}, {300.0, "1:9"}, {500.0, "1:12"}}) + { + switch_template slip; + slip.double_slip = true; + slip.radius = slipping.first; + slip.label = std::string{"Rkpd "} + slipping.second + " R" + std::to_string(static_cast(slipping.first)) + " double slip (click at a crossing of two straights)"; + result.push_back(slip); + } + return result; +} + +namespace +{ + +// definition of a path in the scenery text, up to its points: type, length, parameters, environment and textures +void write_header(std::ostream &Text, char const *Type, double const Length, double const Sound, TTrack const &Style, material_handle const Second) +{ + char const *environments[] = {"flat", "mountains", "canyon", "tunnel", "bridge", "bank"}; + auto const environment{Style.eEnvironment >= e_flat && Style.eEnvironment <= e_bank ? environments[Style.eEnvironment] : "flat"}; + Text << Type << ' ' << Length << ' ' << Style.fTrackWidth << ' ' << Style.fFriction << ' ' << Sound << ' ' << Style.iQualityFlag << ' ' << (Style.iDamageFlag & 127) << ' ' << environment << ' '; + if (Style.m_visible) + Text << "vis " << editor_track::material_name(Style.m_material1) << ' ' << Style.fTexLength << ' ' << editor_track::material_name(Second) << ' ' << editor_track::texture_height(Style) << ' ' << Style.fTexWidth << ' ' << Style.fTexSlope << ' '; + else + Text << "unvis "; +} + +void write_points(std::ostream &Text, segment_data const &Path) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + auto const &end{Path.points[segment_data::point::end]}; + Text << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; +} + +void write_ending(std::ostream &Text, TTrack const &Style) +{ + if ((Style.iCategoryFlag & 15) == 1 && false == Style.m_profile1.first.empty()) + Text << "railprofile " << Style.m_profile1.first << ' '; + // a piece of the track made from another one stays in its track circuit and under the same overhead line + for (auto const *isolated : Style.Isolated) + Text << "isolated " << isolated->asName << ' '; + if (Style.fOverhead != -1.0) + Text << "overhead " << Style.fOverhead << ' '; + if (Style.m_sleeper_enabled && false == Style.m_sleeper_model_name.empty()) + Text << "sleepermodel " << Style.m_sleeper_frequency << ' ' << Style.m_sleeper_model_name << ' ' << (Style.m_sleeper_skin_name.empty() ? std::string{"none"} : Style.m_sleeper_skin_name) << ' ' << Style.m_sleeper_offset.x << ' ' << Style.m_sleeper_offset.y << ' ' << Style.m_sleeper_offset.z << ' ' << Style.m_sleeper_ballast_z << ' '; + Text << "endtrack\n"; +} + +material_handle ballast_of(TTrack const &Style) +{ + return Style.eType == tt_Switch ? (Style.SwitchExtension ? Style.SwitchExtension->m_material3 : null_handle) : Style.m_material2; +} + +} // namespace + +TTrack *editor_track::create_turntable(segment_data const &Path, TTrack const *Style, std::string const &Name) +{ + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + auto const length{glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end])}; + if (Style != nullptr && (Style->iCategoryFlag & 15) == 1 && Style->m_visible) + write_header(text, "turn", length, Style->fSoundDistance, *Style, null_handle); + else + text << "turn " << length << " 1.435 0.15 25 20 0 flat vis rail_screw_used1 4 none 0.2 0.5 1.3 "; + write_points(text, Path); + text << "endtrack\n"; + auto *track{load_text(text.str(), Style, Name)}; + track->m_paths = {Path}; + return track; +} + +TTrack *editor_track::create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style, std::string const &Name) +{ + if (Paths.size() < 2) + return nullptr; + TTrack *track{nullptr}; + if (Template.source != nullptr) + { + track = clone(*Template.source); + } + else + { + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + write_header(text, "switch", Template.length, 10.0, Style, Style.m_material1); + write_points(text, Paths[0]); + write_points(text, Paths[1]); + if (auto const trackbed{ballast_of(Style)}; trackbed != null_handle) + text << "trackbed " << material_name(trackbed) << ' '; + write_ending(text, Style); + track = load_path(text.str(), Style, Name); + } + track->m_paths.assign(Paths.begin(), Paths.begin() + 2); + return track; +} + +bool editor_track::touches(TTrack const &Track, glm::dvec3 const &Point) +{ + for (auto const &path : Track.m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + if (glm::distance(path.points[index], Point) <= kSamePoint) + return true; + return false; +} + +TTrack *editor_track::outside_neighbour(straight const &Line, bool const Atend) +{ + auto const &point{Atend ? Line.end : Line.start}; + for (std::size_t i = 0; i < Line.tracks.size(); ++i) + { + auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; + for (auto const end : {false, true}) + { + if (glm::distance(path.points[end ? segment_data::point::end : segment_data::point::start], point) > kSamePoint) + continue; + auto *neighbour{neighbour_at(*Line.tracks[i], Line.paths[i], end)}; + if (neighbour != nullptr && std::find(Line.tracks.begin(), Line.tracks.end(), neighbour) == Line.tracks.end()) + return neighbour; + } + } + return nullptr; +} + +std::vector editor_track::straight_affected(std::vector const &Lines) +{ + std::vector result; + for (auto const &line : Lines) + for (auto *track : line.tracks) + add_unique(result, track); + auto const members{result}; + auto const member = [&](TTrack const *Track) { return std::find(members.begin(), members.end(), Track) != members.end(); }; + for (auto *track : members) + { + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + for (auto const atend : {false, true}) + { + auto *neighbour{neighbour_at(*track, i, atend)}; + if (neighbour == nullptr || member(neighbour)) + continue; + add_unique(result, neighbour); + if (false == has_switch_paths(*neighbour)) + continue; + for (auto *next : neighbours(*neighbour)) + if (false == member(next)) + add_unique(result, next); + } + } + } + return result; +} + +void editor_track::move_straights(std::vector const &Lines, std::vector> const &Ends) +{ + std::vector members; + for (auto const &line : Lines) + for (auto *track : line.tracks) + add_unique(members, track); + auto const member = [&](TTrack const *Track) { return std::find(members.begin(), members.end(), Track) != members.end(); }; + for (std::size_t i = 0; i < Lines.size() && i < Ends.size(); ++i) + move_straight(Lines[i], Ends[i].first, Ends[i].second, member); +} + +void editor_track::move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function const &Member) +{ + if (Line.tracks.empty() || Line.length < 1e-6) + return; + auto const &direction{Line.direction}; + auto const newplan{plan_of(End - Start)}; + auto const newlength{glm::length(newplan)}; + if (newlength < 1e-3) + return; + auto const newdirection{newplan / newlength}; + auto const angle{std::atan2(cross(direction, newdirection), glm::dot(direction, newdirection))}; + auto const along = [&](glm::dvec3 const &Point) { return glm::dot(plan_of(Point - Line.start), direction); }; + + struct interval + { + double from, to; + bool rigid; + }; + std::vector intervals; + double rigidlength{0.0}; + for (std::size_t i = 0; i < Line.tracks.size(); ++i) + { + auto const &path{Line.tracks[i]->m_paths[Line.paths[i]]}; + auto const a{along(path.points[segment_data::point::start])}; + auto const b{along(path.points[segment_data::point::end])}; + auto const rigid{has_switch_paths(*Line.tracks[i])}; + intervals.push_back({std::min(a, b), std::max(a, b), rigid}); + if (rigid) + rigidlength += std::abs(b - a); + } + std::sort(intervals.begin(), intervals.end(), [](interval const &A, interval const &B) { return A.from < B.from; }); + auto const scale{Line.length - rigidlength > 1e-6 ? (newlength - rigidlength) / (Line.length - rigidlength) : 1.0}; + auto const remap = [&](double const Along) { + double mapped{0.0}, previous{0.0}; + for (auto const &span : intervals) + { + if (Along <= span.from) + return mapped + (Along - previous) * scale; + mapped += (span.from - previous) * scale; + previous = span.from; + auto const factor{span.rigid ? 1.0 : scale}; + if (Along <= span.to) + return mapped + (Along - previous) * factor; + mapped += (span.to - previous) * factor; + previous = span.to; + } + return mapped + (Along - previous) * scale; + }; + auto const transform = [&](glm::dvec3 const &Point) { + auto const a{along(Point)}; + auto const offset{cross(direction, plan_of(Point - Line.start))}; + auto const mapped{remap(a)}; + auto const oldheight{Line.start.y + (Line.end.y - Line.start.y) * a / Line.length}; + auto const newheight{Start.y + (End.y - Start.y) * mapped / newlength}; + auto const moved{plan_of(Start) + newdirection * mapped + glm::dvec2{-newdirection.y, newdirection.x} * offset}; + return glm::dvec3{moved.x, Point.y - oldheight + newheight, moved.y}; + }; + + auto const &member{Member}; + std::vector> joints; + for (std::size_t i = 0; i < Line.tracks.size(); ++i) + { + auto &track{*Line.tracks[i]}; + auto const before{track.m_paths}; + if (has_switch_paths(track)) + { + auto const pivot{before[Line.paths[i]].points[segment_data::point::start]}; + rigid_move(track, pivot, transform(pivot), angle); + } + else + { + for (auto &path : track.m_paths) + { + for (auto const index : {segment_data::point::start, segment_data::point::end}) + path.points[index] = transform(path.points[index]); + for (auto const index : {segment_data::point::control1, segment_data::point::control2}) + { + auto &control{path.points[index]}; + auto const moved{turned(plan_of(control), angle) * scale}; + control = {moved.x, control.y * scale, moved.y}; + } + } + } + for (std::size_t j = 0; j < before.size(); ++j) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + joints.emplace_back(before[j].points[index], track.m_paths[j].points[index]); + } + + std::vector moved; + for (auto *track : Line.tracks) + { + for (int i = 0; i < static_cast(track->m_paths.size()); ++i) + { + for (auto const atend : {false, true}) + { + auto *neighbour{neighbour_at(*track, i, atend)}; + if (neighbour == nullptr || member(neighbour) || std::find(moved.begin(), moved.end(), neighbour) != moved.end()) + continue; + moved.push_back(neighbour); + for (auto const &joint : joints) + { + bool touches{false}; + for (auto const &path : neighbour->m_paths) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + touches |= glm::distance(path.points[index], joint.first) <= kSamePoint; + if (false == touches) + continue; + if (has_switch_paths(*neighbour)) + { + auto const before{neighbour->m_paths}; + rigid_move(*neighbour, joint.first, joint.second, angle); + for (auto *next : neighbours(*neighbour)) + { + if (member(next) || next == neighbour) + continue; + for (std::size_t j = 0; j < before.size(); ++j) + for (auto const index : {segment_data::point::start, segment_data::point::end}) + follow_joint(*next, before[j].points[index], neighbour->m_paths[j].points[index], angle); + } + } + else + { + follow_joint(*neighbour, joint.first, joint.second, angle); + } + break; + } + } + } + } +} + +std::vector editor_track::relay(chain const &Chain, std::vector const &Pieces) +{ + std::vector created; + auto const count{Chain.tracks.size()}; + if (count == 0 || Pieces.size() < count) + return created; + + std::vector tracks; + std::size_t previous{count}; + for (std::size_t i = 0; i < Pieces.size(); ++i) + { + auto const index{i * count / Pieces.size()}; + TTrack *track; + bool forward; + if (index != previous) + { + track = Chain.tracks[index]; + forward = Chain.forward[index]; + } + else + { + track = clone(*Chain.tracks[index]); + created.push_back(track); + forward = true; + } + previous = index; + track->m_paths.resize(1); + track->m_paths.front() = forward ? Pieces[i] : reversed(Pieces[i]); + tracks.push_back(track); + } + commit(tracks); + return created; +} + +TTrack *editor_track::clone(TTrack const &Template) +{ + std::ostringstream text; + Template.export_as_text_(text); + auto content{text.str()}; + auto const type{content.find("track ")}; + if (type == 0) + content.erase(0, 6); + auto *track{load_path(content, Template)}; + track->m_friction = Template.m_friction; + return track; +} + +TTrack *editor_track::create_path(TTrack const &Style, segment_data const &Path) +{ + auto const category{Style.iCategoryFlag & 15}; + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + write_header(text, category == 2 ? "road" : category == 4 ? "river" : "normal", glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end]), Style.fSoundDistance, Style, ballast_of(Style)); + write_points(text, Path); + if (auto const speed{velocity(Style)}; speed > 0.0) + text << "velocity " << speed << ' '; + write_ending(text, Style); + auto *track{load_path(text.str(), Style)}; + track->m_paths = {Path}; + return track; +} + +glm::dvec3 editor_track::point_at(TTrack const &Track, double const T) +{ + auto const &path{Track.m_paths.front()}; + auto const p0{path.points[segment_data::point::start]}; + auto const p3{path.points[segment_data::point::end]}; + auto const p1{p0 + path.points[segment_data::point::control1]}; + auto const p2{p3 + path.points[segment_data::point::control2]}; + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; +} + +double editor_track::nearest_parameter(TTrack const &Track, glm::dvec3 const &Point) +{ + if (Track.m_paths.empty()) + return 0.5; + auto const distance = [&](double const T) { return geometry::plan_distance(point_at(Track, T), Point); }; + double best{0.5}; + for (int i = 0; i <= 200; ++i) + if (distance(i / 200.0) < distance(best)) + best = i / 200.0; + double step{1.0 / 200.0}; + for (int i = 0; i < 30; ++i) + { + step *= 0.5; + if (best - step > 0.0 && distance(best - step) < distance(best)) + best -= step; + else if (best + step < 1.0 && distance(best + step) < distance(best)) + best += step; + } + return best; +} + +TTrack *editor_track::split_path(TTrack &Track, double const T) +{ + if (Track.eType != tt_Normal || Track.m_paths.empty() || T <= 0.001 || T >= 0.999) + return nullptr; + auto const path{Track.m_paths.front()}; + auto const p0{path.points[segment_data::point::start]}; + auto const p3{path.points[segment_data::point::end]}; + segment_data first{path}; + segment_data second{path}; + auto const roll{static_cast(path.rolls[0] + (path.rolls[1] - path.rolls[0]) * T)}; + if (path.points[segment_data::point::control1] == glm::dvec3{} && path.points[segment_data::point::control2] == glm::dvec3{}) + { + // the parameter is the one of the cubic with the controls at the ends, as everywhere else, not a share of the length + auto const u{1.0 - T}; + auto const middle{p0 * (u * u * u + 3.0 * u * u * T) + p3 * (3.0 * u * T * T + T * T * T)}; + first.points[segment_data::point::end] = middle; + second.points[segment_data::point::start] = middle; + } + else + { + auto const p1{p0 + path.points[segment_data::point::control1]}; + auto const p2{p3 + path.points[segment_data::point::control2]}; + auto const p01{glm::mix(p0, p1, T)}; + auto const p12{glm::mix(p1, p2, T)}; + auto const p23{glm::mix(p2, p3, T)}; + auto const p012{glm::mix(p01, p12, T)}; + auto const p123{glm::mix(p12, p23, T)}; + auto const middle{glm::mix(p012, p123, T)}; + first.points[segment_data::point::control1] = p01 - p0; + first.points[segment_data::point::control2] = p012 - middle; + first.points[segment_data::point::end] = middle; + second.points[segment_data::point::start] = middle; + second.points[segment_data::point::control1] = p123 - middle; + second.points[segment_data::point::control2] = p23 - p3; + } + first.rolls[1] = roll; + second.rolls[0] = roll; + auto *created{create_path(Track, second)}; + Track.m_paths.front() = first; + // the events stay where the trains run into them: those of the runs towards the end at the start, in the original, + // those of the runs towards the start at the end, in the new piece; those of a stop in both. the ones bound + // by the name of the path stay with it + for (auto const index : {0, 3}) + for (auto const &event : events(Track, index)) + if (false == event.first.empty()) + events(*created, index).push_back(event); + for (auto const index : {1, 4}) + { + auto &own{events(Track, index)}; + for (auto const &event : own) + if (false == event.first.empty()) + events(*created, index).push_back(event); + own.erase(std::remove_if(own.begin(), own.end(), [](auto const &Event) { return false == Event.first.empty(); }), own.end()); + } + std::string missing; + bind_events(Track, missing); + bind_events(*created, missing); + if (s_observer != nullptr) + s_observer->track_split(Track, *created, first); + commit({&Track, created}); + return created; +} + +TTrack *editor_track::load_path(std::string const &Text, TTrack const &Template, std::string const &Name) +{ + return load_text(Text, &Template, Name); +} + +TTrack *editor_track::create_path(path_style const &Style, segment_data const &Path) +{ + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + auto const length{glm::distance(Path.points[segment_data::point::start], Path.points[segment_data::point::end])}; + text << "normal " << length << " 1.435 0.15 25 20 0 flat vis " << Style.rail << " 6 " << Style.ballast << " 0.2 0.5 1.1 "; + write_points(text, Path); + if (Style.velocity > 0.0) + text << "velocity " << Style.velocity << ' '; + if (false == Style.rail_profile.empty()) + text << "railprofile " << Style.rail_profile << ' '; + text << "endtrack\n"; + auto *track{load_text(text.str(), nullptr, {})}; + track->m_paths = {Path}; + return track; +} + +TTrack *editor_track::load_text(std::string const &Text, TTrack const *Template, std::string const &Name) +{ + cParser parser(Text, cParser::buffer_TEXT); + scene::node_data data; + data.type = "track"; + auto const base{false == Name.empty() ? Name : Template == nullptr || Template->name().empty() || Template->name() == "none" ? std::string{"editor_track"} : Template->name()}; + data.name = Name; + data.layer = Template != nullptr ? Template->layer() : scene::Layers.active(); + for (int i = 1; data.name.empty() || simulation::Paths.find(data.name) != nullptr; ++i) + data.name = base + "_" + std::to_string(i); + auto *track = new TTrack(data); + if (Template != nullptr) + { + track->m_rangesquaredmin = Template->m_rangesquaredmin; + track->m_rangesquaredmax = Template->m_rangesquaredmax; + } + track->Load(&parser, glm::dvec3{}); + for (auto *events : {&track->m_events0, &track->m_events1, &track->m_events2, &track->m_events0all, &track->m_events1all, &track->m_events2all}) + events->clear(); + track->m_events = false; + simulation::Paths.insert(track); + return track; +} + +void editor_track::retire(TTrack &Track) +{ + auto const adjoining{neighbours(Track)}; + disconnect(Track); + simulation::Region->erase_and_unregister(&Track); + Track.m_editorremoved = true; + for (auto *neighbour : adjoining) + { + update_transition(*neighbour); + rebuild_geometry(*neighbour); + } + if (s_observer != nullptr) + s_observer->track_retired(Track); +} + +void editor_track::rebuild_geometry(TTrack &Track) +{ + auto §ion{simulation::Region->section(Track.location())}; + Track.rebuild_geometry(section.m_geometrycreated ? section.m_geometrybank : gfx::geometrybank_handle{}); +} diff --git a/editor/editorTrack.hpp b/editor/editorTrack.hpp new file mode 100644 index 00000000..43d42762 --- /dev/null +++ b/editor/editorTrack.hpp @@ -0,0 +1,354 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http: +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "world/Segment.h" +#include "utilities/Classes.h" + +class TTrack; +class basic_event; + +class editor_track +{ + public: + enum class point_kind { start, control1, control2, end }; + static bool is_end(point_kind const Kind) { return Kind == point_kind::start || Kind == point_kind::end; } + struct point_ref + { + int path{-1}; + point_kind kind{point_kind::start}; + bool valid() const { return path >= 0; } + bool operator==(point_ref const &Other) const { return path == Other.path && kind == Other.kind; } + }; + + struct state + { + std::vector paths; + double velocity{-1.0}; + float switchvelocity{-1.f}; + float width{1.435f}; + float friction{0.15f}; + float sounddistance{-1.f}; + int quality{20}; + int damage{0}; + int environment{0}; + material_handle material1{0}; + material_handle material2{0}; + material_handle material3{0}; + float texlength{4.f}; + float texheight{0.6f}; + float texwidth{0.9f}; + float texslope{0.9f}; + std::vector isolated; + float overhead{-1.f}; + bool sleepers{false}; + float sleeper_frequency{0.6f}; + std::string sleeper_model; + std::string sleeper_skin; + glm::vec3 sleeper_offset{0.f}; + float sleeper_ballast{0.f}; + std::array>, 6> events; + std::string name; + }; + static state capture(TTrack const &Track); + static void apply(TTrack &Track, state const &State); + + static bool name_valid(TTrack const &Track, std::string &Name, std::string &Reason, TTrack **Owner = nullptr); + struct name_uses + { + int cells{0}; + int includes{0}; + std::vector events; + std::vector loose; + }; + static name_uses uses_of_name(TTrack const &Track); + static void rename(TTrack &Track, std::string const &Name); + + // lists of the events of the path, in the order of event_keyword(): event0, event1, event2, eventall0, eventall1, eventall2 + static std::vector> &events(TTrack &Track, int const Index); + static char const *event_keyword(int const Index); + // looks the events up by their names; the names of those which don't exist are given in Missing + static void bind_events(TTrack &Track, std::string &Missing); + // track circuits the path belongs to, made when they don't exist yet + static void isolated(TTrack &Track, std::vector const &Names); + // -1: normal, 0: no current, above 0: speed limit with the pantographs lowered + static void overhead(TTrack &Track, float const Overhead); + + static bool can_edit_geometry(TTrack const &Track, std::string &Reason); + static bool is_supported(TTrack const &Track); + + static void commit(std::vector const &Tracks); + static void commit_parameters(TTrack &Track); + + static glm::dvec3 point_position(TTrack const &Track, point_ref const &Point); + static void move_point(TTrack &Track, point_ref const &Point, glm::dvec3 const &Position); + static void translate(TTrack &Track, glm::dvec3 const &Offset); + static void rotate_y(TTrack &Track, glm::dvec3 const &Pivot, double const Angle); + static glm::dvec3 pivot(TTrack const &Track); + + static std::vector neighbours(TTrack const &Track); + static std::vector> connected_points(TTrack const &Track, glm::dvec3 const &Position); + static bool is_connected(TTrack const &Track, point_ref const &Point); + // end of a regular neighbouring path joined to an end of the track, as it was when taken + struct joint + { + point_ref own; + TTrack *other{nullptr}; + point_ref theirs; + glm::dvec2 direction{0.0, 1.0}; // of the track at its end, into it + glm::dvec3 control{0.0}; // of the neighbour at its end, never zero + }; + // joints the editor can change: switches and paths which can't be edited aren't taken + static std::vector joints(TTrack const &Track); + // moves the joined ends to where the ends of the track are now, turning them as the track turned there + static void follow(TTrack const &Track, std::vector const &Joints); + + struct snap_target + { + TTrack *track{nullptr}; + point_ref point; + glm::dvec3 position{0.0}; + glm::dvec3 direction{0.0}; + double distance{0.0}; + }; + static snap_target find_snap_target(TTrack const &Track, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); + // nearest end of a path of the category which nothing else joins; Self may be null + static snap_target find_free_end(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); + // all of them within the radius, nearest first + static std::vector free_ends(TTrack const *Self, int const Category, glm::dvec3 const &Position, double const Radius, std::vector const &Exclude); + static void snap_point(TTrack &Track, point_ref const &Point, snap_target const &Target, bool const Aligntangent); + + struct chain + { + std::vector tracks; + std::vector forward; + glm::dvec3 start{0.0}; + glm::dvec3 end{0.0}; + glm::dvec3 start_direction{0.0}; + glm::dvec3 end_direction{0.0}; + double start_radius{0.0}; + double end_radius{0.0}; + double length{0.0}; + double velocity{-1.0}; + double radius{0.0}; + // switches the chain runs through along their main track, each before the regular path of the index given + struct passage + { + TTrack *track{nullptr}; + bool forward{true}; + std::size_t before{0}; + }; + std::vector switches; + }; + // with Switches the chain may run through the switches along their main tracks; its ends are regular paths + static bool find_chain(TTrack *From, TTrack *To, chain &Chain, std::string &Error, bool const Switches = false); + // the regular parts of a chain between its switches, empty ones skipped + static std::vector chain_parts(chain const &Chain); + // moves the track as a whole, From going to To, turned by Angle in the plan + static void place(TTrack &Track, glm::dvec3 const &From, glm::dvec3 const &To, double const Angle); + + struct straight + { + std::vector tracks; + std::vector paths; + glm::dvec3 start{0.0}; + glm::dvec3 end{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double length{0.0}; + double grade{0.0}; + double azimuth{0.0}; + // distance of the point from the start, measured along the line in the plan + double along(glm::dvec3 const &Point) const { return (Point.x - start.x) * direction.x + (Point.z - start.z) * direction.y; } + }; + struct straight_tolerance + { + double angle{0.0002}; + double offset{0.02}; + double radius{20000.0}; + }; + static bool is_straight(TTrack const &Track, straight_tolerance const &Tolerance); + static straight find_straight(TTrack &Track, straight_tolerance const &Tolerance); + static std::vector find_straights(double const Minimumlength, straight_tolerance const &Tolerance); + static std::vector straight_affected(std::vector const &Lines); + static TTrack *outside_neighbour(straight const &Line, bool const Atend); + static bool touches(TTrack const &Track, glm::dvec3 const &Point); + struct curve + { + TTrack *from{nullptr}; + TTrack *to{nullptr}; + double turn{0.0}; + int reversals{0}; + double radius{0.0}; + double transition_in{0.0}; + double transition_out{0.0}; + double cant{0.0}; + bool compound{false}; + struct arc_part + { + double radius{0.0}; + double turn{0.0}; + double transition{0.0}; + }; + std::vector arcs; + }; + static bool find_curve(TTrack &Track, straight_tolerance const &Tolerance, double const Gauge, curve &Curve); + // curve made of the given paths, each with the flag of being run from its start + static bool analyse_curve(std::vector> const &Run, double const Gauge, curve &Curve); + // regular paths in line through the track, up to switches, free ends or about Limit metres each way + static bool find_run(TTrack &Track, double const Limit, chain &Chain, std::string &Error); + // the chain read as an alignment: a vertex at the intersection of the tangents of each curve + struct line_vertex + { + glm::dvec2 position{0.0}; + curve shape; + bool kink{false}; // the straights meet with no curve between them + }; + static std::vector recognize_line(chain const &Chain, straight_tolerance const &Tolerance, double const Gauge); + // regular straight paths continuing a chain end in line, which the chain can take over + struct straight_run + { + std::vector tracks; + std::vector outward; + double length{0.0}; + }; + static straight_run straight_beyond(chain const &Chain, bool const Atend, straight_tolerance const &Tolerance); + static double run_reserve(straight_run const &Run); + static TTrack *take_straight(straight_run const &Run, TTrack *Edge, glm::dvec3 const &Joint, glm::dvec3 const &Cut, std::vector> &States, std::vector &Created); + // sequence of paths, switches included, which a train can run along + struct route_span + { + TTrack *track{nullptr}; + int path{0}; + bool forward{true}; + double from{0.0}; // chainage, measured in the plan + double to{0.0}; + }; + struct route + { + std::vector spans; + double length{0.0}; + }; + static bool find_route(TTrack *From, TTrack *To, route &Route, std::string &Error); + // continues both ways from the path, through switches along their path 0 + static route run_route(TTrack &Track, double const Maximum); + static route route_of(chain const &Chain); + struct route_sample + { + double chainage{0.0}; + glm::dvec3 position{0.0}; + glm::dvec2 direction{0.0, 1.0}; + double grade{0.0}; + double curvature{0.0}; // signed, 1/m, positive turning left + double cant{0.0}; // m, raise of the outer rail + std::size_t span{0}; + }; + static std::vector sample_route(route &Route, double const Step); + static double sampled_elevation(std::vector const &Samples, double const Chainage); + static double sampled_grade(std::vector const &Samples, double const Chainage); + static glm::dvec3 sampled_position(std::vector const &Samples, double const Chainage); + // grade of the path which adjoins the end of the route, positive rising along the route + static bool adjoining_grade(route const &Route, bool const Atend, double &Grade); + // path off the route whose end no longer met the adjoining path in height + struct height_gap + { + TTrack *track{nullptr}; + TTrack *neighbour{nullptr}; + double gap{0.0}; // neighbour - track + bool closed{false}; // the end of the neighbour was brought to the track + }; + // sets elevations of the paths of the route; regular paths are split at the chainages in Breaks, + // switches are tilted as a whole in the plane of their grade, branches included, and the regular paths + // off the route get their ends joined to the switches brought to the height there + static std::vector apply_profile(route &Route, std::function const &Elevation, std::function const &Grade, std::vector const &Breaks, std::vector> &States, std::vector &Created); + // carries elevations of the chain over to the new pieces, in proportion of the length + static void keep_heights(chain const &Chain, std::vector &Pieces); + + struct switch_template + { + TTrack *source{nullptr}; + std::array local; + double length{0.0}; + double ratio{0.0}; + double radius{0.0}; + int count{0}; + std::string label; + double a{0.0}; + double b{0.0}; + bool double_slip{false}; + }; + static std::vector standard_switch_templates(); + static std::vector find_switch_templates(); + static TTrack *load_path(std::string const &Text, TTrack const &Template, std::string const &Name = {}); + static TTrack *create_path(TTrack const &Style, segment_data const &Path); + // parameters of a path laid where there's no other path to copy them from + struct path_style + { + double velocity{100.0}; + std::string rail{"rail_screw_used1"}; + std::string ballast{"1435mm/tpbps-new2"}; + std::string rail_profile; + }; + // the path goes to the layer receiving the items created in the editor + static TTrack *create_path(path_style const &Style, segment_data const &Path); + static TTrack *split_path(TTrack &Track, double const T); + static double nearest_parameter(TTrack const &Track, glm::dvec3 const &Point); + // point of the first path at the parameter of nearest_parameter() + static glm::dvec3 point_at(TTrack const &Track, double const T); + static std::vector place_switch(switch_template const &Template, glm::dvec3 const &Origin, glm::dvec2 const &Direction, int const Side, double const Grade); + static TTrack *create_switch(switch_template const &Template, std::vector const &Paths, TTrack const &Style, std::string const &Name = {}); + static TTrack *create_turntable(segment_data const &Path, TTrack const *Style, std::string const &Name); + static void move_straights(std::vector const &Lines, std::vector> const &Ends); + static std::vector relay(chain const &Chain, std::vector const &Pieces); + static void retire(TTrack &Track); + + static std::string material_name(material_handle const Material); + static material_handle fetch_material(std::string const &Name); + + static float texture_height(TTrack const &Track); + static void texture_height(TTrack &Track, float const Height); + static double velocity(TTrack const &Track); + static void velocity(TTrack &Track, double const Velocity); + static void damage(TTrack &Track, int const Damage); + + // told about the changes of the paths, to let the objects which belong to them follow + class observer + { + public: + virtual ~observer() = default; + virtual void track_captured(TTrack const &Track) = 0; + virtual void tracks_committed(std::vector const &Tracks) = 0; + virtual void track_retired(TTrack &Track) = 0; + // called before the commit; Original keeps the beginning of the path, given in First + virtual void track_split(TTrack &Original, TTrack &Created, segment_data const &First) = 0; + }; + static void observe(observer *Observer) { s_observer = Observer; } + + private: + static observer *s_observer; + // sets the length, the ends and their directions of the chain of the tracks given + static bool complete_chain(chain &Chain, std::string &Error); + static void disconnect(TTrack &Track); + static void join(TTrack &Track); + static void connect(TTrack &Track, bool const Prevside, TTrack *Other, int const Endpointid); + static void update_transition(TTrack &Track); + static void bind_switch_path(TTrack &Switch, int const Path); + static void store_switch_path(TTrack &Switch, int const Path); + static void rebuild_geometry(TTrack &Track); + static TTrack *clone(TTrack const &Template); + static TTrack *load_text(std::string const &Text, TTrack const *Template, std::string const &Name); + static void move_straight(straight const &Line, glm::dvec3 const &Start, glm::dvec3 const &End, std::function const &Member); +}; diff --git a/editor/icons/editor_icons.png b/editor/icons/editor_icons.png new file mode 100644 index 00000000..50878092 Binary files /dev/null and b/editor/icons/editor_icons.png differ diff --git a/editor/icons/editor_icons.py b/editor/icons/editor_icons.py new file mode 100644 index 00000000..9ef66aa1 --- /dev/null +++ b/editor/icons/editor_icons.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +# draws the icons of the scenery editor into editor_icons.png, next to this script: white shapes on a transparent ground, +# 64 x 64 pixels each, 8 in a row, in the order of editor_icons::icon (editor/editorIcons.hpp). +# the editor tints them as it draws them; the png is built into the executable by CMake, see CMakeLists.txt. +# needs Pillow: python3 editor_icons.py +import math +import os + +from PIL import Image, ImageDraw + +CELL = 64 +SUPER = 4 # drawn this many times bigger, then scaled down for smooth edges +S = CELL * SUPER +W = 18 # width of the lines, at the bigger size +COLUMNS = 8 + + +def p(x, y): + # coordinates given on a 64 x 64 grid + return (x * SUPER, y * SUPER) + + +def line(d, points, width=W): + pts = [p(*q) for q in points] + d.line(pts, fill=255, width=width, joint="curve") + for q in (pts[0], pts[-1]): + r = width / 2 + d.ellipse((q[0] - r, q[1] - r, q[0] + r, q[1] + r), fill=255) + + +def poly(d, points, outline=False, width=W): + pts = [p(*q) for q in points] + if outline: + line(d, points + [points[0]], width) + else: + d.polygon(pts, fill=255) + + +def circle(d, x, y, r, outline=False, width=W): + box = (p(x - r, y - r), p(x + r, y + r)) + if outline: + d.ellipse(box, outline=255, width=width) + else: + d.ellipse(box, fill=255) + + +def rect(d, x0, y0, x1, y1, outline=False, width=W, radius=0): + box = (p(x0, y0), p(x1, y1)) + if outline: + d.rounded_rectangle(box, radius=radius * SUPER, outline=255, width=width) + else: + d.rounded_rectangle(box, radius=radius * SUPER, fill=255) + + +def arrowhead(d, x, y, angle, size=9): + # filled head pointing at angle (degrees, 0 = right, 90 = down) + a = math.radians(angle) + tip = (x + math.cos(a) * size * 0.6, y + math.sin(a) * size * 0.6) + left = (x + math.cos(a + 2.5) * size, y + math.sin(a + 2.5) * size) + right = (x + math.cos(a - 2.5) * size, y + math.sin(a - 2.5) * size) + poly(d, [tip, left, right]) + + +def arc(d, x, y, r, start, end, width=W): + box = (p(x - r, y - r), p(x + r, y + r)) + d.arc(box, start, end, fill=255, width=width) + + +def dashed(d, a, b, dash=6, gap=4, width=W): + length = math.hypot(b[0] - a[0], b[1] - a[1]) + if length == 0: + return + ux, uy = (b[0] - a[0]) / length, (b[1] - a[1]) / length + t = 0.0 + while t < length: + e = min(t + dash, length) + line(d, [(a[0] + ux * t, a[1] + uy * t), (a[0] + ux * e, a[1] + uy * e)], width) + t = e + gap + + +# the icons, in the order of editor_icons::icon + + +def select(d): + poly(d, [(16, 8), (16, 50), (26, 40), (33, 56), (40, 53), (33, 37), (47, 37)]) + + +def insert(d): + # a box, with a plus over its corner + poly(d, [(8, 26), (28, 16), (48, 26), (28, 36)], outline=True) + line(d, [(8, 26), (8, 46), (28, 56), (48, 46), (48, 26)]) + line(d, [(28, 36), (28, 56)]) + circle(d, 48, 14, 11) + + +def brush(d): + line(d, [(52, 8), (30, 32)], W + 4) + poly(d, [(26, 30), (34, 38), (24, 52), (8, 56), (12, 42)]) + for x, y in ((44, 40), (52, 30), (50, 50)): + circle(d, x, y, 3) + + +def area_fill(d): + outline = [(8, 18), (34, 8), (56, 24), (50, 54), (16, 52)] + for i in range(len(outline)): + dashed(d, outline[i], outline[(i + 1) % len(outline)], 6, 5, W - 4) + for x, y in ((24, 38), (38, 30), (36, 44)): + poly(d, [(x, y - 9), (x + 6, y + 4), (x - 6, y + 4)]) + + +def copy_to_bank(d): + rect(d, 18, 6, 46, 26, outline=True, radius=3) + line(d, [(32, 26), (32, 40)]) + arrowhead(d, 32, 42, 90, 12) + line(d, [(8, 40), (8, 56), (56, 56), (56, 40)]) + + +def translate(d): + line(d, [(32, 10), (32, 54)]) + line(d, [(10, 32), (54, 32)]) + arrowhead(d, 32, 8, 270, 13) + arrowhead(d, 32, 56, 90, 13) + arrowhead(d, 8, 32, 180, 13) + arrowhead(d, 56, 32, 0, 13) + + +def rotate(d): + # turning clockwise, the gap at the top + arc(d, 32, 34, 21, 300, 225) + a = math.radians(225) + arrowhead(d, 32 + math.cos(a) * 21, 34 + math.sin(a) * 21, 315, 15) + circle(d, 32, 34, 4) + + +def scale(d): + rect(d, 8, 30, 34, 56, radius=2) + for a, b in (((8, 8), (56, 8)), ((56, 8), (56, 56)), ((8, 8), (8, 22)), ((42, 56), (56, 56))): + dashed(d, a, b, 6, 5, W - 6) + line(d, [(30, 34), (48, 16)]) + arrowhead(d, 50, 14, 315, 14) + + +def local_space(d): + # the axes turned with the model + o = (22, 42) + for angle, length in ((-20, 34), (-110, 30), (150, 18)): + a = math.radians(angle) + e = (o[0] + math.cos(a) * length, o[1] + math.sin(a) * length) + line(d, [o, e], W - 2) + arrowhead(d, e[0], e[1], angle, 11) + circle(d, o[0], o[1], 5) + + +def ortho(d): + # the plan seen from over it: a grid without perspective + rect(d, 8, 8, 56, 56, outline=True, width=W - 2) + for t in (24, 40): + line(d, [(t, 8), (t, 56)], W - 8) + line(d, [(8, t), (56, t)], W - 8) + + +def perspective(d): + # the grid going away + poly(d, [(20, 14), (44, 14), (58, 54), (6, 54)], outline=True, width=W - 2) + line(d, [(28, 14), (24, 54)], W - 8) + line(d, [(36, 14), (40, 54)], W - 8) + line(d, [(15, 30), (49, 30)], W - 8) + + +def sculpt(d): + poly(d, [(4, 56), (22, 26), (32, 38), (42, 22), (60, 56)]) + line(d, [(50, 18), (50, 4)], W - 4) + arrowhead(d, 50, 3, 270, 11) + + +def smooth(d): + pts = [(x, 40 - 12 * math.sin((x - 6) / 52 * math.pi)) for x in range(6, 59, 2)] + line(d, pts) + line(d, [(6, 54), (58, 54)], W - 6) + circle(d, 32, 28, 14, outline=True, width=W - 8) + + +def chunks(d): + for x, y in ((6, 6), (33, 6), (6, 33)): + rect(d, x, y, x + 25, y + 25, radius=3) + rect(d, 33, 33, 58, 58, outline=True, width=W - 6, radius=3) + line(d, [(45.5, 39), (45.5, 52)], W - 8) + line(d, [(39, 45.5), (52, 45.5)], W - 8) + + +def orthophoto(d): + rect(d, 6, 10, 58, 54, outline=True, radius=4) + poly(d, [(10, 50), (26, 28), (36, 40), (42, 34), (54, 50)]) + circle(d, 44, 21, 5) + + +def road(d): + line(d, [(24, 8), (8, 56)]) + line(d, [(40, 8), (56, 56)]) + dashed(d, (32, 8), (32, 56), 8, 6, W - 4) + + +def place(d): + # a pin standing on the place + poly(d, [(32, 58), (18, 34), (46, 34)]) + circle(d, 32, 26, 15) + + +def lanes(d): + for x in (14, 50): + line(d, [(x, 6), (x, 58)], W - 4) + dashed(d, (32, 6), (32, 58), 7, 6, W - 6) + line(d, [(23, 52), (23, 22)], W - 6) + arrowhead(d, 23, 18, 270, 10) + line(d, [(41, 12), (41, 42)], W - 6) + arrowhead(d, 41, 46, 90, 10) + + +def lay_track(d): + # two rails on sleepers, going up to the right + for t in range(0, 6): + x, y = 10 + t * 9, 54 - t * 9 + line(d, [(x - 8, y - 8), (x + 8, y + 8)], W - 8) + line(d, [(4, 50), (50, 4)], W - 4) + line(d, [(14, 60), (60, 14)], W - 4) + + +def switch(d): + line(d, [(18, 60), (18, 4)], W - 2) + line(d, [(34, 60), (34, 4)], W - 2) + pts = [(18 + 30 * (1 - math.cos(t / 20 * math.pi / 2)), 52 - t * 2.4) for t in range(0, 21)] + line(d, pts, W - 2) + pts = [(34 + 22 * (1 - math.cos(t / 20 * math.pi / 2)), 60 - t * 2.4) for t in range(0, 21)] + line(d, pts, W - 2) + + +def straight(d): + line(d, [(12, 52), (52, 12)]) + rect(d, 4, 44, 20, 60, radius=2) + rect(d, 44, 4, 60, 20, radius=2) + + +def curve(d): + arc(d, 56, 56, 44, 180, 270) + dashed(d, (56, 56), (25, 25), 5, 4, W - 8) + circle(d, 12, 56, 6) + circle(d, 56, 12, 6) + circle(d, 25, 25, 6) + + +def signal(d): + line(d, [(32, 34), (32, 60)], W - 2) + line(d, [(20, 60), (44, 60)], W - 2) + rect(d, 20, 4, 44, 38, radius=10) + + +def objects(d): + # a post by the track with a board on it + line(d, [(24, 30), (24, 60)], W - 2) + rect(d, 10, 6, 38, 30, radius=3) + line(d, [(42, 60), (60, 60)], W - 6) + line(d, [(48, 52), (60, 52)], W - 6) + + +def vehicle(d): + poly(d, [(4, 22), (50, 22), (60, 32), (60, 46), (4, 46)]) + circle(d, 16, 52, 6) + circle(d, 46, 52, 6) + line(d, [(4, 58), (60, 58)], W - 8) + + +def profile(d): + pts = [(6, 46), (22, 30), (38, 36), (58, 14)] + line(d, pts) + for q in pts: + circle(d, q[0], q[1], 5) + line(d, [(6, 58), (58, 58)], W - 8) + + +def speed(d): + arc(d, 32, 40, 24, 180, 360) + for a in (200, 240, 280, 320): + r = math.radians(a) + line(d, [(32 + math.cos(r) * 15, 40 + math.sin(r) * 15), (32 + math.cos(r) * 20, 40 + math.sin(r) * 20)], W - 8) + line(d, [(32, 40), (46, 24)], W - 2) + circle(d, 32, 40, 6) + + +def joints(d): + line(d, [(4, 26), (26, 26)]) + line(d, [(38, 26), (60, 26)]) + line(d, [(4, 40), (26, 40)]) + line(d, [(38, 44), (60, 44)]) + rect(d, 28, 14, 36, 54, outline=True, width=W - 10, radius=2) + + +def infra(d): + # a catenary mast with its arm and the wire hanging from it + line(d, [(14, 60), (14, 6)], W + 2) + line(d, [(14, 12), (54, 12)], W - 4) + line(d, [(14, 26), (46, 12)], W - 8) + line(d, [(44, 12), (44, 28)], W - 8) + pts = [(x, 30 + 0.012 * (x - 36) ** 2) for x in range(14, 61, 2)] + line(d, pts, W - 6) + + +def gauge(d): + # the outline of the structure gauge over the rails + line(d, [(12, 52), (12, 22), (22, 8), (42, 8), (52, 22), (52, 52)], W - 4) + line(d, [(4, 56), (60, 56)], W - 6) + for x in (24, 40): + rect(d, x - 3, 48, x + 3, 56) + + +def turntable(d): + # the pit with its bridge, and the tracks leading out of it + circle(d, 32, 32, 20, outline=True, width=W - 4) + line(d, [(18, 46), (46, 18)], W) + for a in (-90, -30, 30): + r = math.radians(a) + line(d, [(32 + math.cos(r) * 24, 32 + math.sin(r) * 24), (32 + math.cos(r) * 31, 32 + math.sin(r) * 31)], W - 4) + + +# the fields of work of the editor, in the row over the toolbar + + +def work_surroundings(d): + # a tree and a house by it + poly(d, [(18, 4), (32, 30), (4, 30)]) + poly(d, [(18, 14), (34, 44), (2, 44)]) + line(d, [(18, 44), (18, 60)], W - 2) + poly(d, [(36, 34), (49, 22), (62, 34)]) + rect(d, 39, 34, 59, 60) + + +def work_tracks(d): + # the track going away, rails on sleepers + line(d, [(8, 60), (26, 4)], W - 4) + line(d, [(56, 60), (38, 4)], W - 4) + for y, half in ((54, 28), (40, 22), (28, 17), (18, 13), (10, 10)): + line(d, [(32 - half, y), (32 + half, y)], W - 8 if y > 20 else W - 10) + + +def work_roads(d): + # a car seen from its side + poly(d, [(4, 44), (4, 34), (14, 30), (22, 18), (44, 18), (52, 30), (60, 34), (60, 44)]) + circle(d, 17, 46, 8) + circle(d, 47, 46, 8) + + +def work_terrain(d): + # the mesh of the ground over a hill + def at(u, v): + # u across 0..1, v from the back (0) to the front (1) + half = 18 + 12 * v + x = 32 + (u - 0.5) * 2 * half + hill = 18 * math.exp(-((u - 0.5) ** 2) / 0.07 - ((v - 0.35) ** 2) / 0.15) + return (x, 24 + 34 * v - hill) + steps = [i / 12 for i in range(13)] + for v in (0.0, 0.33, 0.66, 1.0): + line(d, [at(u, v) for u in steps], W - 6) + for u in (0.0, 0.33, 0.66, 1.0): + line(d, [at(u, v) for v in steps], W - 6) + + +ICONS = [select, insert, brush, area_fill, copy_to_bank, translate, rotate, scale, + local_space, ortho, perspective, sculpt, smooth, chunks, orthophoto, road, + place, lanes, lay_track, switch, straight, curve, signal, objects, + vehicle, profile, speed, joints, infra, gauge, turntable, work_surroundings, + work_tracks, work_roads, work_terrain] + + +def cutouts(name, d): + # the parts drawn in the colour of the ground: the plus of the insert, the hole of the pin, the lights of the signal + if name == "insert": + line(d, [(48, 8), (48, 20)], W - 8) + line(d, [(42, 14), (54, 14)], W - 8) + elif name == "place": + circle(d, 32, 26, 6) + elif name == "signal": + circle(d, 32, 14, 5) + circle(d, 32, 28, 5) + elif name == "vehicle": + rect(d, 10, 27, 20, 35) + rect(d, 26, 27, 36, 35) + rect(d, 42, 27, 50, 35) + elif name == "work_surroundings": + # the door and the gap between the crowns of the tree + rect(d, 46, 46, 52, 60) + line(d, [(9, 31), (27, 31)], 8) + elif name == "work_roads": + # the windows, and the gaps around the wheels + poly(d, [(25, 22), (31, 22), (31, 30), (19, 30)]) + poly(d, [(35, 22), (42, 22), (47, 30), (35, 30)]) + circle(d, 17, 46, 11, outline=True, width=12) + circle(d, 47, 46, 11, outline=True, width=12) + circle(d, 17, 46, 3) + circle(d, 47, 46, 3) + + +def main(): + rows = (len(ICONS) + COLUMNS - 1) // COLUMNS + atlas = Image.new("RGBA", (COLUMNS * CELL, rows * CELL), (255, 255, 255, 0)) + for index, draw in enumerate(ICONS): + mask = Image.new("L", (S, S), 0) + d = ImageDraw.Draw(mask) + draw(d) + holes = Image.new("L", (S, S), 0) + cutouts(draw.__name__, ImageDraw.Draw(holes)) + mask.paste(0, (0, 0), holes) + mask = mask.resize((CELL, CELL), Image.LANCZOS) + icon = Image.new("RGBA", (CELL, CELL), (255, 255, 255, 0)) + icon.putalpha(mask) + atlas.paste(icon, ((index % COLUMNS) * CELL, (index // COLUMNS) * CELL)) + atlas.save(os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor_icons.png"), optimize=True) + + +if __name__ == "__main__": + main() diff --git a/export_btf_standalone.cpp b/export_btf_standalone.cpp new file mode 100644 index 00000000..6d659242 --- /dev/null +++ b/export_btf_standalone.cpp @@ -0,0 +1,41 @@ +#include "stdafx.h" + +#include "scene/sceneterrain.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +// converts text terrain file to binary format. returns: 0 on success, 1 otherwise +int export_btf_standalone(std::string in, std::string out) +{ + if (in.empty()) + { + std::printf("usage: -btf -s [-o ]\n"); + return 1; + } + replace_slashes(in); + replace_slashes(out); + if (false == FileExists(in) && FileExists(Global.asCurrentSceneryPath + in)) + { + // the file can be given the way the sceneries refer to it + in = Global.asCurrentSceneryPath + in; + } + if (out.empty()) + { + // by default the binary file goes where the sceneries expect to find it, next to the text + out = in; + erase_extension(out); + out += ".btf"; + } + std::string message; + auto const result{scene::terrain_file::convert(in, out, &message)}; + // NOTE: the log service doesn't run in this mode, the outcome is reported directly + if (result) + { + std::printf("%s -> %s: %s\n", in.c_str(), out.c_str(), message.c_str()); + } + else + { + std::printf("%s: conversion failed, %s\n", in.c_str(), message.c_str()); + } + return (result ? 0 : 1); +} diff --git a/imgui/ImGuizmo.cpp b/imgui/ImGuizmo.cpp index 41caa41a..15e71499 100644 --- a/imgui/ImGuizmo.cpp +++ b/imgui/ImGuizmo.cpp @@ -24,10 +24,11 @@ // SOFTWARE. // -#include "imgui.h" +// imgui 1.86+ wants the operators requested before the first include of imgui.h #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif +#include "imgui.h" #include "imgui_internal.h" #include "ImGuizmo.h" @@ -1883,7 +1884,7 @@ namespace ImGuizmo // move if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsTranslateType(gContext.mCurrentOperation)) { - ImGui::CaptureMouseFromApp(); + ImGui::SetNextFrameWantCaptureMouse(true); const float len = fabsf(IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan)); // near plan vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; @@ -1953,7 +1954,7 @@ namespace ImGuizmo type = GetMoveType(op, &gizmoHitProportion); if (type != MT_NONE) { - ImGui::CaptureMouseFromApp(); + ImGui::SetNextFrameWantCaptureMouse(true); } if (CanActivate() && type != MT_NONE) { @@ -1998,7 +1999,7 @@ namespace ImGuizmo type = GetScaleType(op); if (type != MT_NONE) { - ImGui::CaptureMouseFromApp(); + ImGui::SetNextFrameWantCaptureMouse(true); } if (CanActivate() && type != MT_NONE) { @@ -2021,7 +2022,7 @@ namespace ImGuizmo // scale if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsScaleType(gContext.mCurrentOperation)) { - ImGui::CaptureMouseFromApp(); + ImGui::SetNextFrameWantCaptureMouse(true); const float len = IntersectRayPlane(gContext.mRayOrigin, gContext.mRayVector, gContext.mTranslationPlan); vec_t newPos = gContext.mRayOrigin + gContext.mRayVector * len; vec_t newOrigin = newPos - gContext.mRelativeOrigin * gContext.mScreenFactor; @@ -2112,7 +2113,7 @@ namespace ImGuizmo if (type != MT_NONE) { - ImGui::CaptureMouseFromApp(); + ImGui::SetNextFrameWantCaptureMouse(true); } if (type == MT_ROTATE_SCREEN) @@ -2146,7 +2147,7 @@ namespace ImGuizmo // rotation if (gContext.mbUsing && (gContext.mActualID == -1 || gContext.mActualID == gContext.mEditingID) && IsRotateType(gContext.mCurrentOperation)) { - ImGui::CaptureMouseFromApp(); + ImGui::SetNextFrameWantCaptureMouse(true); gContext.mRotationAngle = ComputeAngleOnPlan(); if (snap) { diff --git a/imgui/LICENSE.txt b/imgui/LICENSE.txt index 3b439aa4..00ae473a 100644 --- a/imgui/LICENSE.txt +++ b/imgui/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014-2019 Omar Cornut +Copyright (c) 2014-2025 Omar Cornut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/imgui/imconfig.h b/imgui/imconfig.h index 4962ce4e..a29296e7 100644 --- a/imgui/imconfig.h +++ b/imgui/imconfig.h @@ -1,96 +1,144 @@ //----------------------------------------------------------------------------- -// COMPILE-TIME OPTIONS FOR DEAR IMGUI +// DEAR IMGUI COMPILE-TIME OPTIONS // Runtime options (clipboard callbacks, enabling various features, etc.) can generally be set via the ImGuiIO structure. // You can use ImGui::SetAllocatorFunctions() before calling ImGui::CreateContext() to rewire memory allocation functions. //----------------------------------------------------------------------------- -// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/branch with your modifications to imconfig.h) -// B) or add configuration directives in your own file and compile with #define IMGUI_USER_CONFIG "myfilename.h" -// If you do so you need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include -// the imgui*.cpp files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures. +// A) You may edit imconfig.h (and not overwrite it when updating Dear ImGui, or maintain a patch/rebased branch with your modifications to it) +// B) or '#define IMGUI_USER_CONFIG "my_imgui_config.h"' in your project and then add directives in your own file without touching this template. +//----------------------------------------------------------------------------- +// You need to make sure that configuration settings are defined consistently _everywhere_ Dear ImGui is used, which include the imgui*.cpp +// files but also _any_ of your code that uses Dear ImGui. This is because some compile-time options have an affect on data structures. // Defining those options in imconfig.h will ensure every compilation unit gets to see the same data structure layouts. -// Call IMGUI_CHECKVERSION() from your .cpp files to verify that the data structures your files are using are matching the ones imgui.cpp is using. +// Call IMGUI_CHECKVERSION() from your .cpp file to verify that the data structures your files are using are matching the ones imgui.cpp is using. //----------------------------------------------------------------------------- #pragma once //---- Define assertion handler. Defaults to calling assert(). +// If your macro uses multiple statements, make sure is enclosed in a 'do { .. } while (0)' block so it can be used as a single statement. //#define IM_ASSERT(_EXPR) MyAssert(_EXPR) //#define IM_ASSERT(_EXPR) ((void)(_EXPR)) // Disable asserts //---- Define attributes of all API symbols declarations, e.g. for DLL under Windows -// Using dear imgui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. -//#define IMGUI_API __declspec( dllexport ) -//#define IMGUI_API __declspec( dllimport ) +// Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// - Windows DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() +// for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. +//#define IMGUI_API __declspec(dllexport) // MSVC Windows: DLL export +//#define IMGUI_API __declspec(dllimport) // MSVC Windows: DLL import +//#define IMGUI_API __attribute__((visibility("default"))) // GCC/Clang: override visibility when set is hidden -//---- Don't define obsolete functions/enums names. Consider enabling from time to time after updating to avoid using soon-to-be obsolete function/names. +//---- Don't define obsolete functions/enums/behaviors. Consider enabling from time to time after updating to clean your code of obsolete function/names. //#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS -//---- Don't implement demo windows functionality (ShowDemoWindow()/ShowStyleEditor()/ShowUserGuide() methods will be empty) -// It is very strongly recommended to NOT disable the demo windows during development. Please read the comments in imgui_demo.cpp. -//#define IMGUI_DISABLE_DEMO_WINDOWS -//#define IMGUI_DISABLE_METRICS_WINDOW +//---- Disable all of Dear ImGui or don't implement standard windows/tools. +// It is very strongly recommended to NOT disable the demo windows and debug tool during development. They are extremely useful in day to day work. Please read comments in imgui_demo.cpp. +//#define IMGUI_DISABLE // Disable everything: all headers and source files will be empty. +//#define IMGUI_DISABLE_DEMO_WINDOWS // Disable demo windows: ShowDemoWindow()/ShowStyleEditor() will be empty. +//#define IMGUI_DISABLE_DEBUG_TOOLS // Disable metrics/debugger and other debug tools: ShowMetricsWindow(), ShowDebugLogWindow() and ShowIDStackToolWindow() will be empty. //---- Don't implement some functions to reduce linkage requirements. -//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. -//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] Don't implement default IME handler. Won't use and link with ImmGetContext/ImmSetCompositionWindow. -//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, ime). -//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices'). -//#define IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself if you don't want to link with vsnprintf. -//#define IMGUI_DISABLE_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 wrapper so you can implement them yourself. Declare your prototypes in imconfig.h. +//#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS // [Win32] Don't implement default clipboard handler. Won't use and link with OpenClipboard/GetClipboardData/CloseClipboard etc. (user32.lib/.a, kernel32.lib/.a) +//#define IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with Visual Studio] Implement default IME handler (require imm32.lib/.a, auto-link for Visual Studio, -limm32 on command-line for MinGW) +//#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a) +//#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME). +//#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default). +//#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")). +//#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf) +//#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself. +//#define IMGUI_DISABLE_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle at all (replace them with dummies) +//#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS // Don't implement ImFileOpen/ImFileClose/ImFileRead/ImFileWrite and ImFileHandle so you can implement them yourself if you don't want to link with fopen/fclose/fread/fwrite. This will also disable the LogToTTY() function. //#define IMGUI_DISABLE_DEFAULT_ALLOCATORS // Don't implement default allocators calling malloc()/free() to avoid linking with them. You will need to call ImGui::SetAllocatorFunctions(). +//#define IMGUI_DISABLE_DEFAULT_FONT // Disable default embedded font (ProggyClean.ttf), remove ~9.5 KB from output binary. AddFontDefault() will assert. +//#define IMGUI_DISABLE_SSE // Disable use of SSE intrinsics even if available + +//---- Enable Test Engine / Automation features. +//#define IMGUI_ENABLE_TEST_ENGINE // Enable imgui_test_engine hooks. Generally set automatically by include "imgui_te_config.h", see Test Engine for details. //---- Include imgui_user.h at the end of imgui.h as a convenience +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. //#define IMGUI_INCLUDE_IMGUI_USER_H +//#define IMGUI_USER_H_FILENAME "my_folder/my_imgui_user.h" -//---- Pack colors to BGRA8 instead of RGBA8 (to avoid converting from one to another) +//---- Pack vertex colors as BGRA8 instead of RGBA8 (to avoid converting from one to another). Need dedicated backend support. //#define IMGUI_USE_BGRA_PACKED_COLOR +//---- Use legacy CRC32-adler tables (used before 1.91.6), in order to preserve old .ini data that you cannot afford to invalidate. +//#define IMGUI_USE_LEGACY_CRC32_ADLER + +//---- Use 32-bit for ImWchar (default is 16-bit) to support Unicode planes 1-16. (e.g. point beyond 0xFFFF like emoticons, dingbats, symbols, shapes, ancient languages, etc...) +//#define IMGUI_USE_WCHAR32 + //---- Avoid multiple STB libraries implementations, or redefine path/filenames to prioritize another version -// By default the embedded implementations are declared static and not available outside of imgui cpp files. +// By default the embedded implementations are declared static and not available outside of Dear ImGui sources files. //#define IMGUI_STB_TRUETYPE_FILENAME "my_folder/stb_truetype.h" //#define IMGUI_STB_RECT_PACK_FILENAME "my_folder/stb_rect_pack.h" +//#define IMGUI_STB_SPRINTF_FILENAME "my_folder/stb_sprintf.h" // only used if IMGUI_USE_STB_SPRINTF is defined. //#define IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION //#define IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +//#define IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION // only disabled if IMGUI_USE_STB_SPRINTF is defined. + +//---- Use stb_sprintf.h for a faster implementation of vsnprintf instead of the one from libc (unless IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS is defined) +// Compatibility checks of arguments and formats done by clang and GCC will be disabled in order to support the extra formats provided by stb_sprintf.h. +//#define IMGUI_USE_STB_SPRINTF + +//---- Use FreeType to build and rasterize the font atlas (instead of stb_truetype which is embedded by default in Dear ImGui) +// Requires FreeType headers to be available in the include path. Requires program to be compiled with 'misc/freetype/imgui_freetype.cpp' (in this repository) + the FreeType library (not provided). +// On Windows you may use vcpkg with 'vcpkg install freetype --triplet=x64-windows' + 'vcpkg integrate install'. +//#define IMGUI_ENABLE_FREETYPE + +//---- Use FreeType + plutosvg or lunasvg to render OpenType SVG fonts (SVGinOT) +// Only works in combination with IMGUI_ENABLE_FREETYPE. +// - plutosvg is currently easier to install, as e.g. it is part of vcpkg. It will support more fonts and may load them faster. See misc/freetype/README for instructions. +// - Both require headers to be available in the include path + program to be linked with the library code (not provided). +// - (note: lunasvg implementation is based on Freetype's rsvg-port.c which is licensed under CeCILL-C Free Software License Agreement) +//#define IMGUI_ENABLE_FREETYPE_PLUTOSVG +//#define IMGUI_ENABLE_FREETYPE_LUNASVG + +//---- Use stb_truetype to build and rasterize the font atlas (default) +// The only purpose of this define is if you want force compilation of the stb_truetype backend ALONG with the FreeType backend. +//#define IMGUI_ENABLE_STB_TRUETYPE //---- Define constructor and implicit cast operators to convert back<>forth between your math types and ImVec2/ImVec4. // This will be inlined as part of ImVec2 and ImVec4 class declarations. /* -#define IM_VEC2_CLASS_EXTRA \ - ImVec2(const MyVec2& f) { x = f.x; y = f.y; } \ +#define IM_VEC2_CLASS_EXTRA \ + constexpr ImVec2(const MyVec2& f) : x(f.x), y(f.y) {} \ operator MyVec2() const { return MyVec2(x,y); } -#define IM_VEC4_CLASS_EXTRA \ - ImVec4(const MyVec4& f) { x = f.x; y = f.y; z = f.z; w = f.w; } \ +#define IM_VEC4_CLASS_EXTRA \ + constexpr ImVec4(const MyVec4& f) : x(f.x), y(f.y), z(f.z), w(f.w) {} \ operator MyVec4() const { return MyVec4(x,y,z,w); } */ +//---- ...Or use Dear ImGui's own very basic math operators. +//#define IMGUI_DEFINE_MATH_OPERATORS -//---- Using 32-bits vertex indices (default is 16-bits) is one way to allow large meshes with more than 64K vertices. -// Your renderer back-end will need to support it (most example renderer back-ends support both 16/32-bits indices). -// Another way to allow large meshes while keeping 16-bits indices is to handle ImDrawCmd::VtxOffset in your renderer. +//---- Use 32-bit vertex indices (default is 16-bit) is one way to allow large meshes with more than 64K vertices. +// Your renderer backend will need to support it (most example renderer backends support both 16/32-bit indices). +// Another way to allow large meshes while keeping 16-bit indices is to handle ImDrawCmd::VtxOffset in your renderer. // Read about ImGuiBackendFlags_RendererHasVtxOffset for details. //#define ImDrawIdx unsigned int -//---- Override ImDrawCallback signature (will need to modify renderer back-ends accordingly) +//---- Override ImDrawCallback signature (will need to modify renderer backends accordingly) //struct ImDrawList; //struct ImDrawCmd; //typedef void (*MyImDrawCallback)(const ImDrawList* draw_list, const ImDrawCmd* cmd, void* my_renderer_user_data); //#define ImDrawCallback MyImDrawCallback -//---- Debug Tools -// Use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging. +//---- Debug Tools: Macro to break in Debugger (we provide a default implementation of this in the codebase) +// (use 'Metrics->Tools->Item Picker' to pick widgets with the mouse and break into them for easy debugging.) //#define IM_DEBUG_BREAK IM_ASSERT(0) //#define IM_DEBUG_BREAK __debugbreak() -// Have the Item Picker break in the ItemAdd() function instead of ItemHoverable() - which is earlier in the code, will catch a few extra items, allow picking items other than Hovered one. -// This adds a small runtime cost which is why it is not enabled by default. -//#define IMGUI_DEBUG_TOOL_ITEM_PICKER_EX -//---- Tip: You can add extra functions within the ImGui:: namespace, here or in your own headers files. +//---- Debug Tools: Enable slower asserts +//#define IMGUI_DEBUG_PARANOID + +//---- Tip: You can add extra functions within the ImGui:: namespace from anywhere (e.g. your own sources/header files) /* namespace ImGui { - void MyFunction(const char* name, const MyMatrix44& v); + void MyFunction(const char* name, MyMatrix44* mtx); } */ -#define IMGUI_IMPL_OPENGL_LOADER_GLAD -//#define IMGUI_IMPL_OPENGL_ES3 +// MaSzyna: the opengl3 backend uses glad, loaded by the simulator, instead of its own gl3w loader +#define IMGUI_IMPL_OPENGL_LOADER_CUSTOM diff --git a/imgui/imgui.cpp b/imgui/imgui.cpp index 93b15ddb..573efe24 100644 --- a/imgui/imgui.cpp +++ b/imgui/imgui.cpp @@ -1,18 +1,37 @@ -// dear imgui, v1.73 +// dear imgui, v1.91.9b // (main code and documentation) -// Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp for demo code. -// Newcomers, read 'Programmer guide' below for notes on how to setup Dear ImGui in your codebase. -// Get latest version at https://github.com/ocornut/imgui -// Releases change-log at https://github.com/ocornut/imgui/releases -// Technical Support for Getting Started https://discourse.dearimgui.org/c/getting-started -// Gallery (please post your screenshots/video there!): https://github.com/ocornut/imgui/issues/2529 +// Help: +// - See links below. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// - Read top of imgui.cpp for more details, links and comments. +// Resources: +// - FAQ ........................ https://dearimgui.com/faq (in repository as docs/FAQ.md) +// - Homepage ................... https://github.com/ocornut/imgui +// - Releases & changelog ....... https://github.com/ocornut/imgui/releases +// - Gallery .................... https://github.com/ocornut/imgui/issues?q=label%3Agallery (please post your screenshots/video there!) +// - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code) +// - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more) +// - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings, backends for various tech/engines) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui +// - Issues & support ........... https://github.com/ocornut/imgui/issues +// - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps) + +// For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there. + +// Copyright (c) 2014-2025 Omar Cornut // Developed by Omar Cornut and every direct or indirect contributors to the GitHub. // See LICENSE.txt for copyright and licensing details (standard MIT License). -// This library is free but I need your support to sustain development and maintenance. -// Businesses: you can support continued maintenance and development via support contracts or sponsoring, see docs/README. -// Individuals: you can support continued maintenance and development via donations or Patreon https://www.patreon.com/imgui. +// This library is free but needs your support to sustain development and maintenance. +// Businesses: you can support continued development via B2B invoiced technical support, maintenance and sponsoring contracts. +// PLEASE reach out at omar AT dearimgui DOT com. See https://github.com/ocornut/imgui/wiki/Funding +// Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. // It is recommended that you don't modify imgui.cpp! It will become difficult for you to update the library. // Note that 'ImGui::' being a namespace, you can add functions into the namespace from your own source files, without @@ -27,60 +46,57 @@ Index of this file: DOCUMENTATION - MISSION STATEMENT -- END-USER GUIDE -- PROGRAMMER GUIDE (read me!) - - Read first. - - How to update to a newer version of Dear ImGui. - - Getting started with integrating Dear ImGui in your code/engine. - - This is how a simple application may look like (2 variations). - - This is how a simple rendering function may look like. - - Using gamepad/keyboard navigation controls. +- CONTROLS GUIDE +- PROGRAMMER GUIDE + - READ FIRST + - HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + - GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + - HOW A SIMPLE APPLICATION MAY LOOK LIKE + - HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE - API BREAKING CHANGES (read me when you update!) -- FREQUENTLY ASKED QUESTIONS (FAQ), TIPS - - Where is the documentation? - - Which version should I get? - - Who uses Dear ImGui? - - Why the odd dual naming, "Dear ImGui" vs "ImGui"? - - How can I tell whether to dispatch mouse/keyboard to imgui or to my application? - - How can I display an image? What is ImTextureID, how does it works? - - Why are multiple widgets reacting when I interact with a single one? How can I have - multiple widgets with the same label or with an empty label? A primer on labels and the ID Stack... - - How can I use my own math types instead of ImVec2/ImVec4? - - How can I load a different font than the default? - - How can I easily use icons in my application? - - How can I load multiple fonts? - - How can I display and input non-latin characters such as Chinese, Japanese, Korean, Cyrillic? - - How can I interact with standard C++ types (such as std::string and std::vector)? - - How can I use the drawing facilities without a Dear ImGui window? (using ImDrawList API) - - How can I use Dear ImGui on a platform that doesn't have a mouse or a keyboard? (input share, remoting, gamepad) - - I integrated Dear ImGui in my engine and the text or lines are blurry.. - - I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around.. - - How can I help? +- FREQUENTLY ASKED QUESTIONS (FAQ) + - Read all answers online: https://www.dearimgui.com/faq, or in docs/FAQ.md (with a Markdown viewer) CODE (search for "[SECTION]" in the code to find them) +// [SECTION] INCLUDES // [SECTION] FORWARD DECLARATIONS // [SECTION] CONTEXT AND MEMORY ALLOCATORS -// [SECTION] MAIN USER FACING STRUCTURES (ImGuiStyle, ImGuiIO) -// [SECTION] MISC HELPERS/UTILITIES (Maths, String, Format, Hash, File functions) +// [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO, ImGuiPlatformIO) +// [SECTION] MISC HELPERS/UTILITIES (Geometry functions) +// [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions) +// [SECTION] MISC HELPERS/UTILITIES (File functions) // [SECTION] MISC HELPERS/UTILITIES (ImText* functions) // [SECTION] MISC HELPERS/UTILITIES (Color functions) // [SECTION] ImGuiStorage // [SECTION] ImGuiTextFilter -// [SECTION] ImGuiTextBuffer +// [SECTION] ImGuiTextBuffer, ImGuiTextIndex // [SECTION] ImGuiListClipper +// [SECTION] STYLING // [SECTION] RENDER HELPERS +// [SECTION] INITIALIZATION, SHUTDOWN // [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +// [SECTION] ID STACK +// [SECTION] INPUTS +// [SECTION] ERROR CHECKING, STATE RECOVERY +// [SECTION] ITEM SUBMISSION +// [SECTION] LAYOUT // [SECTION] SCROLLING // [SECTION] TOOLTIPS // [SECTION] POPUPS +// [SECTION] WINDOW FOCUS // [SECTION] KEYBOARD/GAMEPAD NAVIGATION // [SECTION] DRAG AND DROP // [SECTION] LOGGING/CAPTURING // [SECTION] SETTINGS +// [SECTION] LOCALIZATION +// [SECTION] VIEWPORTS, PLATFORM WINDOWS +// [SECTION] DOCKING // [SECTION] PLATFORM DEPENDENT HELPERS -// [SECTION] METRICS/DEBUG WINDOW +// [SECTION] METRICS/DEBUGGER WINDOW +// [SECTION] DEBUG LOG WINDOW +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL) */ @@ -96,92 +112,169 @@ CODE - Easy to use to create code-driven and data-driven tools. - Easy to use to create ad hoc short-lived tools and long-lived, more elaborate tools. - Easy to hack and improve. - - Minimize screen real-estate usage. - Minimize setup and maintenance. - Minimize state storage on user side. + - Minimize state synchronization. - Portable, minimize dependencies, run on target (consoles, phones, etc.). - - Efficient runtime and memory consumption (NB- we do allocate when "growing" content e.g. creating a window,. - opening a tree node for the first time, etc. but a typical frame should not allocate anything). + - Efficient runtime and memory consumption. - Designed for developers and content-creators, not the typical end-user! Some of the weaknesses includes: - - Doesn't look fancy, doesn't animate. + Designed primarily for developers and content-creators, not the typical end-user! + Some of the current weaknesses (which we aim to address in the future) includes: + + - Doesn't look fancy. - Limited layout features, intricate layouts are typically crafted in code. - END-USER GUIDE + CONTROLS GUIDE ============== - - Double-click on title bar to collapse window. - - Click upper right corner to close a window, available when 'bool* p_open' is passed to ImGui::Begin(). - - Click and drag on lower right corner to resize window (double-click to auto fit window to its contents). - - Click and drag on any empty space to move window. - - TAB/SHIFT+TAB to cycle through keyboard editable fields. - - CTRL+Click on a slider or drag box to input value as text. - - Use mouse wheel to scroll. - - Text editor: - - Hold SHIFT or use mouse to select text. - - CTRL+Left/Right to word jump. - - CTRL+Shift+Left/Right to select words. - - CTRL+A our Double-Click to select all. - - CTRL+X,CTRL+C,CTRL+V to use OS clipboard/ - - CTRL+Z,CTRL+Y to undo/redo. - - ESCAPE to revert text to its original value. - - You can apply arithmetic operators +,*,/ on numerical values. Use +- to subtract (because - would set a negative value!) - - Controls are automatically adjusted for OSX to match standard OSX text editing operations. - - General Keyboard controls: enable with ImGuiConfigFlags_NavEnableKeyboard. - - General Gamepad controls: enable with ImGuiConfigFlags_NavEnableGamepad. See suggested mappings in imgui.h ImGuiNavInput_ + download PNG/PSD at http://goo.gl/9LgVZW + - MOUSE CONTROLS + - Mouse wheel: Scroll vertically. + - SHIFT+Mouse wheel: Scroll horizontally. + - Click [X]: Close a window, available when 'bool* p_open' is passed to ImGui::Begin(). + - Click ^, Double-Click title: Collapse window. + - Drag on corner/border: Resize window (double-click to auto fit window to its contents). + - Drag on any empty space: Move window (unless io.ConfigWindowsMoveFromTitleBarOnly = true). + - Left-click outside popup: Close popup stack (right-click over underlying popup: Partially close popup stack). + + - TEXT EDITOR + - Hold SHIFT or Drag Mouse: Select text. + - CTRL+Left/Right: Word jump. + - CTRL+Shift+Left/Right: Select words. + - CTRL+A or Double-Click: Select All. + - CTRL+X, CTRL+C, CTRL+V: Use OS clipboard. + - CTRL+Z Undo. + - CTRL+Y or CTRL+Shift+Z: Redo. + - ESCAPE: Revert text to its original value. + - On OSX, controls are automatically adjusted to match standard OSX text editing 2ts and behaviors. + + - KEYBOARD CONTROLS + - Basic: + - Tab, SHIFT+Tab Cycle through text editable fields. + - CTRL+Tab, CTRL+Shift+Tab Cycle through windows. + - CTRL+Click Input text into a Slider or Drag widget. + - Extended features with `io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard`: + - Tab, SHIFT+Tab: Cycle through every items. + - Arrow keys Move through items using directional navigation. Tweak value. + - Arrow keys + Alt, Shift Tweak slower, tweak faster (when using arrow keys). + - Enter Activate item (prefer text input when possible). + - Space Activate item (prefer tweaking with arrows when possible). + - Escape Deactivate item, leave child window, close popup. + - Page Up, Page Down Previous page, next page. + - Home, End Scroll to top, scroll to bottom. + - Alt Toggle between scrolling layer and menu layer. + - CTRL+Tab then Ctrl+Arrows Move window. Hold SHIFT to resize instead of moving. + - Output when ImGuiConfigFlags_NavEnableKeyboard set, + - io.WantCaptureKeyboard flag is set when keyboard is claimed. + - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set. + - io.NavVisible: true when the navigation cursor is visible (usually goes to back false when mouse is used). + + - GAMEPAD CONTROLS + - Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. + - Particularly useful to use Dear ImGui on a console system (e.g. PlayStation, Switch, Xbox) without a mouse! + - Download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets + - Backend support: backend needs to: + - Set 'io.BackendFlags |= ImGuiBackendFlags_HasGamepad' + call io.AddKeyEvent/AddKeyAnalogEvent() with ImGuiKey_Gamepad_XXX keys. + - For analog values (0.0f to 1.0f), backend is responsible to handling a dead-zone and rescaling inputs accordingly. + Backend code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, etc.). + - If you need to share inputs between your game and the Dear ImGui interface, the easiest approach is to go all-or-nothing, + with a buttons combo to toggle the target. Please reach out if you think the game vs navigation input sharing could be improved. + + - REMOTE INPUTS SHARING & MOUSE EMULATION + - PS4/PS5 users: Consider emulating a mouse cursor with DualShock touch pad or a spare analog stick as a mouse-emulation fallback. + - Consoles/Tablet/Phone users: Consider using a Synergy 1.x server (on your PC) + run examples/libs/synergy/uSynergy.c (on your console/tablet/phone app) + in order to share your PC mouse/keyboard. + - See https://github.com/ocornut/imgui/wiki/Useful-Extensions#remoting for other remoting solutions. + - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the io.ConfigNavMoveSetMousePos flag. + Enabling io.ConfigNavMoveSetMousePos + ImGuiBackendFlags_HasSetMousePos instructs Dear ImGui to move your mouse cursor along with navigation movements. + When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantSetMousePos' to notify you that it wants the mouse cursor to be moved. + When that happens your backend NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the backends in examples/ do that. + (If you set the NavEnableSetMousePos flag but don't honor 'io.WantSetMousePos' properly, Dear ImGui will misbehave as it will see your mouse moving back & forth!) + (In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want + to set a boolean to ignore your other external mouse positions until the external source is moved again.) PROGRAMMER GUIDE ================ - READ FIRST: - - - Read the FAQ below this section! - - Your code creates the UI, if your code doesn't run the UI is gone! The UI can be highly dynamic, there are no construction - or destruction steps, less superfluous data retention on your side, less state duplication, less state synchronization, less bugs. + READ FIRST + ---------- + - Remember to check the wonderful Wiki (https://github.com/ocornut/imgui/wiki) + - Your code creates the UI every frame of your application loop, if your code doesn't run the UI is gone! + The UI can be highly dynamic, there are no construction or destruction steps, less superfluous + data retention on your side, less state duplication, less state synchronization, fewer bugs. - Call and read ImGui::ShowDemoWindow() for demo code demonstrating most features. + Or browse https://pthom.github.io/imgui_manual_online/manual/imgui_manual.html for interactive web version. - The library is designed to be built from sources. Avoid pre-compiled binaries and packaged versions. See imconfig.h to configure your build. - Dear ImGui is an implementation of the IMGUI paradigm (immediate-mode graphical user interface, a term coined by Casey Muratori). - You can learn about IMGUI principles at http://www.johno.se/book/imgui.html, http://mollyrocket.com/861 & more links docs/README.md. + You can learn about IMGUI principles at http://www.johno.se/book/imgui.html, http://mollyrocket.com/861 & more links in Wiki. - Dear ImGui is a "single pass" rasterizing implementation of the IMGUI paradigm, aimed at ease of use and high-performances. - For every application frame your UI code will be called only once. This is in contrast to e.g. Unity's own implementation of an IMGUI, + For every application frame, your UI code will be called only once. This is in contrast to e.g. Unity's implementation of an IMGUI, where the UI code is called multiple times ("multiple passes") from a single entry point. There are pros and cons to both approaches. - - Our origin are on the top-left. In axis aligned bounding boxes, Min = top-left, Max = bottom-right. - - This codebase is also optimized to yield decent performances with typical "Debug" builds settings. + - Our origin is on the top-left. In axis aligned bounding boxes, Min = top-left, Max = bottom-right. - Please make sure you have asserts enabled (IM_ASSERT redirects to assert() by default, but can be redirected). If you get an assert, read the messages and comments around the assert. - - C++: this is a very C-ish codebase: we don't rely on C++11, we don't include any C++ headers, and ImGui:: is a namespace. - - C++: ImVec2/ImVec4 do not expose math operators by default, because it is expected that you use your own math types. - See FAQ "How can I use my own math types instead of ImVec2/ImVec4?" for details about setting up imconfig.h for that. - However, imgui_internal.h can optionally export math operators for ImVec2/ImVec4, which we use in this codebase. - - C++: pay attention that ImVector<> manipulates plain-old-data and does not honor construction/destruction (avoid using it in your code!). + - This codebase aims to be highly optimized: + - A typical idle frame should never call malloc/free. + - We rely on a maximum of constant-time or O(N) algorithms. Limiting searches/scans as much as possible. + - We put particular energy in making sure performances are decent with typical "Debug" build settings as well. + Which mean we tend to avoid over-relying on "zero-cost abstraction" as they aren't zero-cost at all. + - This codebase aims to be both highly opinionated and highly flexible: + - This code works because of the things it choose to solve or not solve. + - C++: this is a pragmatic C-ish codebase: we don't use fancy C++ features, we don't include C++ headers, + and ImGui:: is a namespace. We rarely use member functions (and when we did, I am mostly regretting it now). + This is to increase compatibility, increase maintainability and facilitate use from other languages. + - C++: ImVec2/ImVec4 do not expose math operators by default, because it is expected that you use your own math types. + See FAQ "How can I use my own math types instead of ImVec2/ImVec4?" for details about setting up imconfig.h for that. + We can can optionally export math operators for ImVec2/ImVec4 using IMGUI_DEFINE_MATH_OPERATORS, which we use internally. + - C++: pay attention that ImVector<> manipulates plain-old-data and does not honor construction/destruction + (so don't use ImVector in your code or at our own risk!). + - Building: We don't use nor mandate a build system for the main library. + This is in an effort to ensure that it works in the real world aka with any esoteric build setup. + This is also because providing a build system for the main library would be of little-value. + The build problems are almost never coming from the main library but from specific backends. - HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI: - - Overwrite all the sources files except for imconfig.h (if you have made modification to your copy of imconfig.h) - - Or maintain your own branch where you have imconfig.h modified. + HOW TO UPDATE TO A NEWER VERSION OF DEAR IMGUI + ---------------------------------------------- + - Update submodule or copy/overwrite every file. + - About imconfig.h: + - You may modify your copy of imconfig.h, in this case don't overwrite it. + - or you may locally branch to modify imconfig.h and merge/rebase latest. + - or you may '#define IMGUI_USER_CONFIG "my_config_file.h"' globally from your build system to + specify a custom path for your imconfig.h file and instead not have to modify the default one. + + - Overwrite all the sources files except for imconfig.h (if you have modified your copy of imconfig.h) + - Or maintain your own branch where you have imconfig.h modified as a top-most commit which you can regularly rebase over "master". + - You can also use '#define IMGUI_USER_CONFIG "my_config_file.h" to redirect configuration to your own file. - Read the "API BREAKING CHANGES" section (below). This is where we list occasional API breaking changes. If a function/type has been renamed / or marked obsolete, try to fix the name in your code before it is permanently removed from the public API. If you have a problem with a missing function/symbols, search for its name in the code, there will likely be a comment about it. Please report any issue to the GitHub page! - - Try to keep your copy of dear imgui reasonably up to date. + - To find out usage of old API, you can add '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in your configuration file. + - Try to keep your copy of Dear ImGui reasonably up to date! - GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE: + GETTING STARTED WITH INTEGRATING DEAR IMGUI IN YOUR CODE/ENGINE + --------------------------------------------------------------- + - See https://github.com/ocornut/imgui/wiki/Getting-Started. - Run and study the examples and demo in imgui_demo.cpp to get acquainted with the library. - - Add the Dear ImGui source files to your projects or using your preferred build system. - It is recommended you build and statically link the .cpp files as part of your project and not as shared library (DLL). + - In the majority of cases you should be able to use unmodified backends files available in the backends/ folder. + - Add the Dear ImGui source files + selected backend source files to your projects or using your preferred build system. + It is recommended you build and statically link the .cpp files as part of your project and NOT as a shared library (DLL). - You can later customize the imconfig.h file to tweak some compile-time behavior, such as integrating Dear ImGui types with your own maths types. - When using Dear ImGui, your programming IDE is your friend: follow the declaration of variables, functions and types to find comments about them. - Dear ImGui never touches or knows about your GPU state. The only function that knows about GPU is the draw function that you provide. Effectively it means you can create widgets at any time in your code, regardless of considerations of being in "update" vs "render" - phases of your own application. All rendering information are stored into command-lists that you will retrieve after calling ImGui::Render(). - - Refer to the bindings and demo applications in the examples/ folder for instruction on how to setup your code. + phases of your own application. All rendering information is stored into command-lists that you will retrieve after calling ImGui::Render(). + - Refer to the backends and demo applications in the examples/ folder for instruction on how to setup your code. - If you are running over a standard OS with a common graphics API, you should be able to use unmodified imgui_impl_*** files from the examples/ folder. - HOW A SIMPLE APPLICATION MAY LOOK LIKE: - EXHIBIT 1: USING THE EXAMPLE BINDINGS (imgui_impl_XXX.cpp files from the examples/ folder). + + HOW A SIMPLE APPLICATION MAY LOOK LIKE + -------------------------------------- + EXHIBIT 1: USING THE EXAMPLE BACKENDS (= imgui_impl_XXX.cpp files from the backends/ folder). + The sub-folders in examples/ contain examples applications following this structure. // Application init: create a dear imgui context, setup some options, load fonts ImGui::CreateContext(); @@ -190,7 +283,7 @@ CODE // TODO: Fill optional fields of the io structure later. // TODO: Load TTF/OTF fonts if you don't want to use the default font. - // Initialize helper Platform and Renderer bindings (here we are using imgui_impl_win32 and imgui_impl_dx11) + // Initialize helper Platform and Renderer backends (here we are using imgui_impl_win32.cpp and imgui_impl_dx11.cpp) ImGui_ImplWin32_Init(hwnd); ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext); @@ -216,8 +309,7 @@ CODE ImGui_ImplWin32_Shutdown(); ImGui::DestroyContext(); - HOW A SIMPLE APPLICATION MAY LOOK LIKE: - EXHIBIT 2: IMPLEMENTING CUSTOM BINDING / CUSTOM ENGINE. + EXHIBIT 2: IMPLEMENTING CUSTOM BACKEND / CUSTOM ENGINE // Application init: create a dear imgui context, setup some options, load fonts ImGui::CreateContext(); @@ -229,29 +321,29 @@ CODE // Build and load the texture atlas into a texture // (In the examples/ app this is usually done within the ImGui_ImplXXX_Init() function from one of the demo Renderer) int width, height; - unsigned char* pixels = NULL; + unsigned char* pixels = nullptr; io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); - // At this point you've got the texture data and you need to upload that your your graphic system: + // At this point you've got the texture data and you need to upload that to your graphic system: // After we have created the texture, store its pointer/identifier (_in whichever format your engine uses_) in 'io.Fonts->TexID'. - // This will be passed back to your via the renderer. Basically ImTextureID == void*. Read FAQ below for details about ImTextureID. + // This will be passed back to your via the renderer. Basically ImTextureID == void*. Read FAQ for details about ImTextureID. MyTexture* texture = MyEngine::CreateTextureFromMemoryPixels(pixels, width, height, TEXTURE_TYPE_RGBA32) - io.Fonts->TexID = (void*)texture; + io.Fonts->SetTexID((void*)texture); // Application main loop while (true) { // Setup low-level inputs, e.g. on Win32: calling GetKeyboardState(), or write to those fields from your Windows message handlers, etc. - // (In the examples/ app this is usually done within the ImGui_ImplXXX_NewFrame() function from one of the demo Platform bindings) + // (In the examples/ app this is usually done within the ImGui_ImplXXX_NewFrame() function from one of the demo Platform Backends) io.DeltaTime = 1.0f/60.0f; // set the time elapsed since the previous frame (in seconds) io.DisplaySize.x = 1920.0f; // set the current display width io.DisplaySize.y = 1280.0f; // set the current display height here - io.MousePos = my_mouse_pos; // set the mouse position - io.MouseDown[0] = my_mouse_buttons[0]; // set the mouse button states - io.MouseDown[1] = my_mouse_buttons[1]; + io.AddMousePosEvent(mouse_x, mouse_y); // update mouse position + io.AddMouseButtonEvent(0, mouse_b[0]); // update mouse button states + io.AddMouseButtonEvent(1, mouse_b[1]); // update mouse button states // Call NewFrame(), after this point you can use ImGui::* functions anytime - // (So you want to try calling NewFrame() as early as you can in your mainloop to be able to use Dear ImGui everywhere) + // (So you want to try calling NewFrame() as early as you can in your main loop to be able to use Dear ImGui everywhere) ImGui::NewFrame(); // Most of your application code here @@ -271,14 +363,23 @@ CODE // Shutdown ImGui::DestroyContext(); - HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE: + To decide whether to dispatch mouse/keyboard inputs to Dear ImGui to the rest of your application, + you should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! + Please read the FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" about this. - void void MyImGuiRenderFunction(ImDrawData* draw_data) + + HOW A SIMPLE RENDERING FUNCTION MAY LOOK LIKE + --------------------------------------------- + The backends in impl_impl_XXX.cpp files contain many working implementations of a rendering function. + + void MyImGuiRenderFunction(ImDrawData* draw_data) { // TODO: Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled + // TODO: Setup texture sampling state: sample with bilinear filtering (NOT point/nearest filtering). Use 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines;' to allow point/nearest filtering. // TODO: Setup viewport covering draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize // TODO: Setup orthographic projection matrix cover draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize // TODO: Setup shader: vertex { float2 pos, float2 uv, u32 color }, fragment shader sample color from 1 texture, multiply by vertex color. + ImVec2 clip_off = draw_data->DisplayPos; for (int n = 0; n < draw_data->CmdListsCount; n++) { const ImDrawList* cmd_list = draw_data->CmdLists[n]; @@ -293,85 +394,406 @@ CODE } else { - // The texture for the draw call is specified by pcmd->TextureId. - // The vast majority of draw calls will use the Dear ImGui texture atlas, which value you have set yourself during initialization. - MyEngineBindTexture((MyTexture*)pcmd->TextureId); + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y); + ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; - // We are using scissoring to clip some objects. All low-level graphics API should supports it. + // We are using scissoring to clip some objects. All low-level graphics API should support it. // - If your engine doesn't support scissoring yet, you may ignore this at first. You will get some small glitches // (some elements visible outside their bounds) but you can fix that once everything else works! - // - Clipping coordinates are provided in imgui coordinates space (from draw_data->DisplayPos to draw_data->DisplayPos + draw_data->DisplaySize) - // In a single viewport application, draw_data->DisplayPos will always be (0,0) and draw_data->DisplaySize will always be == io.DisplaySize. - // However, in the interest of supporting multi-viewport applications in the future (see 'viewport' branch on github), - // always subtract draw_data->DisplayPos from clipping bounds to convert them to your viewport space. + // - Clipping coordinates are provided in imgui coordinates space: + // - For a given viewport, draw_data->DisplayPos == viewport->Pos and draw_data->DisplaySize == viewport->Size + // - In a single viewport application, draw_data->DisplayPos == (0,0) and draw_data->DisplaySize == io.DisplaySize, but always use GetMainViewport()->Pos/Size instead of hardcoding those values. + // - In the interest of supporting multi-viewport applications (see 'docking' branch on github), + // always subtract draw_data->DisplayPos from clipping bounds to convert them to your viewport space. // - Note that pcmd->ClipRect contains Min+Max bounds. Some graphics API may use Min+Max, other may use Min+Size (size being Max-Min) - ImVec2 pos = draw_data->DisplayPos; - MyEngineScissor((int)(pcmd->ClipRect.x - pos.x), (int)(pcmd->ClipRect.y - pos.y), (int)(pcmd->ClipRect.z - pos.x), (int)(pcmd->ClipRect.w - pos.y)); + MyEngineSetScissor(clip_min.x, clip_min.y, clip_max.x, clip_max.y); + + // The texture for the draw call is specified by pcmd->GetTexID(). + // The vast majority of draw calls will use the Dear ImGui texture atlas, which value you have set yourself during initialization. + MyEngineBindTexture((MyTexture*)pcmd->GetTexID()); // Render 'pcmd->ElemCount/3' indexed triangles. - // By default the indices ImDrawIdx are 16-bits, you can change them to 32-bits in imconfig.h if your engine doesn't support 16-bits indices. - MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer, vtx_buffer); + // By default the indices ImDrawIdx are 16-bit, you can change them to 32-bit in imconfig.h if your engine doesn't support 16-bit indices. + MyEngineDrawIndexedTriangles(pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer + pcmd->IdxOffset, vtx_buffer, pcmd->VtxOffset); } - idx_buffer += pcmd->ElemCount; } } } - - The examples/ folders contains many actual implementation of the pseudo-codes above. - - When calling NewFrame(), the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags are updated. - They tell you if Dear ImGui intends to use your inputs. When a flag is set you want to hide the corresponding inputs from the - rest of your application. In every cases you need to pass on the inputs to Dear ImGui. Refer to the FAQ for more information. - - Please read the FAQ below!. Amusingly, it is called a FAQ because people frequently run into the same issues! - - USING GAMEPAD/KEYBOARD NAVIGATION CONTROLS - - - The gamepad/keyboard navigation is fairly functional and keeps being improved. - - Gamepad support is particularly useful to use dear imgui on a console system (e.g. PS4, Switch, XB1) without a mouse! - - You can ask questions and report issues at https://github.com/ocornut/imgui/issues/787 - - The initial focus was to support game controllers, but keyboard is becoming increasingly and decently usable. - - Gamepad: - - Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad to enable. - - Backend: Set io.BackendFlags |= ImGuiBackendFlags_HasGamepad + fill the io.NavInputs[] fields before calling NewFrame(). - Note that io.NavInputs[] is cleared by EndFrame(). - - See 'enum ImGuiNavInput_' in imgui.h for a description of inputs. For each entry of io.NavInputs[], set the following values: - 0.0f= not held. 1.0f= fully held. Pass intermediate 0.0f..1.0f values for analog triggers/sticks. - - We uses a simple >0.0f test for activation testing, and won't attempt to test for a dead-zone. - Your code will probably need to transform your raw inputs (such as e.g. remapping your 0.2..0.9 raw input range to 0.0..1.0 imgui range, etc.). - - You can download PNG/PSD files depicting the gamepad controls for common controllers at: http://goo.gl/9LgVZW. - - If you need to share inputs between your game and the imgui parts, the easiest approach is to go all-or-nothing, with a buttons combo - to toggle the target. Please reach out if you think the game vs navigation input sharing could be improved. - - Keyboard: - - Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard to enable. - NewFrame() will automatically fill io.NavInputs[] based on your io.KeysDown[] + io.KeyMap[] arrays. - - When keyboard navigation is active (io.NavActive + ImGuiConfigFlags_NavEnableKeyboard), the io.WantCaptureKeyboard flag - will be set. For more advanced uses, you may want to read from: - - io.NavActive: true when a window is focused and it doesn't have the ImGuiWindowFlags_NoNavInputs flag set. - - io.NavVisible: true when the navigation cursor is visible (and usually goes false when mouse is used). - - or query focus information with e.g. IsWindowFocused(ImGuiFocusedFlags_AnyWindow), IsItemFocused() etc. functions. - Please reach out if you think the game vs navigation input sharing could be improved. - - Mouse: - - PS4 users: Consider emulating a mouse cursor with DualShock4 touch pad or a spare analog stick as a mouse-emulation fallback. - - Consoles/Tablet/Phone users: Consider using a Synergy 1.x server (on your PC) + uSynergy.c (on your console/tablet/phone app) to share your PC mouse/keyboard. - - On a TV/console system where readability may be lower or mouse inputs may be awkward, you may want to set the ImGuiConfigFlags_NavEnableSetMousePos flag. - Enabling ImGuiConfigFlags_NavEnableSetMousePos + ImGuiBackendFlags_HasSetMousePos instructs dear imgui to move your mouse cursor along with navigation movements. - When enabled, the NewFrame() function may alter 'io.MousePos' and set 'io.WantSetMousePos' to notify you that it wants the mouse cursor to be moved. - When that happens your back-end NEEDS to move the OS or underlying mouse cursor on the next frame. Some of the binding in examples/ do that. - (If you set the NavEnableSetMousePos flag but don't honor 'io.WantSetMousePos' properly, imgui will misbehave as it will see your mouse as moving back and forth!) - (In a setup when you may not have easy control over the mouse cursor, e.g. uSynergy.c doesn't expose moving remote mouse cursor, you may want - to set a boolean to ignore your other external mouse positions until the external source is moved again.) - API BREAKING CHANGES ==================== Occasionally introducing changes that are breaking the API. We try to make the breakage minor and easy to fix. Below is a change-log of API breaking changes only. If you are using one of the functions listed, expect to have to fix some code. - When you are not sure about a old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files. + When you are not sure about an old symbol or function name, try using the Search/Find function of your IDE to look for comments or references in all imgui files. You can read releases logs https://github.com/ocornut/imgui/releases for more details. +(Docking/Viewport Branch) + - 2025/XX/XX (1.XXXX) - when multi-viewports are enabled, all positions will be in your natural OS coordinates space. It means that: + - reference to hard-coded positions such as in SetNextWindowPos(ImVec2(0,0)) are probably not what you want anymore. + you may use GetMainViewport()->Pos to offset hard-coded positions, e.g. SetNextWindowPos(GetMainViewport()->Pos) + - likewise io.MousePos and GetMousePos() will use OS coordinates. + If you query mouse positions to interact with non-imgui coordinates you will need to offset them, e.g. subtract GetWindowViewport()->Pos. + + - 2025/03/05 (1.91.9) - BeginMenu(): Internals: reworked mangling of menu windows to use "###Menu_00" etc. instead of "##Menu_00", allowing them to also store the menu name before it. This shouldn't affect code unless directly accessing menu window from their mangled name. + - 2025/02/27 (1.91.9) - Image(): removed 'tint_col' and 'border_col' parameter from Image() function. Added ImageWithBg() replacement. (#8131, #8238) + - old: void Image (ImTextureID tex_id, ImVec2 image_size, ImVec2 uv0 = (0,0), ImVec2 uv1 = (1,1), ImVec4 tint_col = (1,1,1,1), ImVec4 border_col = (0,0,0,0)); + - new: void Image (ImTextureID tex_id, ImVec2 image_size, ImVec2 uv0 = (0,0), ImVec2 uv1 = (1,1)); + - new: void ImageWithBg(ImTextureID tex_id, ImVec2 image_size, ImVec2 uv0 = (0,0), ImVec2 uv1 = (1,1), ImVec4 bg_col = (0,0,0,0), ImVec4 tint_col = (1,1,1,1)); + - TL;DR: 'border_col' had misleading side-effect on layout, 'bg_col' was missing, parameter order couldn't be consistent with ImageButton(). + - new behavior always use ImGuiCol_Border color + style.ImageBorderSize / ImGuiStyleVar_ImageBorderSize. + - old behavior altered border size (and therefore layout) based on border color's alpha, which caused variety of problems + old behavior a fixed 1.0f for border size which was not tweakable. + - kept legacy signature (will obsolete), which mimics the old behavior, but uses Max(1.0f, style.ImageBorderSize) when border_col is specified. + - added ImageWithBg() function which has both 'bg_col' (which was missing) and 'tint_col'. It was impossible to add 'bg_col' to Image() with a parameter order consistent with other functions, so we decided to remove 'tint_col' and introduce ImageWithBg(). + - 2025/02/25 (1.91.9) - internals: fonts: ImFontAtlas::ConfigData[] has been renamed to ImFontAtlas::Sources[]. ImFont::ConfigData[], ConfigDataCount has been renamed to Sources[], SourcesCount. + - 2025/02/06 (1.91.9) - renamed ImFontConfig::GlyphExtraSpacing.x to ImFontConfig::GlyphExtraAdvanceX. + - 2025/01/22 (1.91.8) - removed ImGuiColorEditFlags_AlphaPreview (made value 0): it is now the default behavior. + prior to 1.91.8: alpha was made opaque in the preview by default _unless_ using ImGuiColorEditFlags_AlphaPreview. We now display the preview as transparent by default. You can use ImGuiColorEditFlags_AlphaOpaque to use old behavior. + the new flags (ImGuiColorEditFlags_AlphaOpaque, ImGuiColorEditFlags_AlphaNoBg + existing ImGuiColorEditFlags_AlphaPreviewHalf) may be combined better and allow finer controls: + - 2025/01/14 (1.91.7) - renamed ImGuiTreeNodeFlags_SpanTextWidth to ImGuiTreeNodeFlags_SpanLabelWidth for consistency with other names. Kept redirection enum (will obsolete). (#6937) + - 2024/11/27 (1.91.6) - changed CRC32 table from CRC32-adler to CRC32c polynomial in order to be compatible with the result of SSE 4.2 instructions. + As a result, old .ini data may be partially lost (docking and tables information particularly). + Because some users have crafted and storing .ini data as a way to workaround limitations of the docking API, we are providing a '#define IMGUI_USE_LEGACY_CRC32_ADLER' compile-time option to keep using old CRC32 tables if you cannot afford invalidating old .ini data. + - 2024/11/06 (1.91.5) - commented/obsoleted out pre-1.87 IO system (equivalent to using IMGUI_DISABLE_OBSOLETE_KEYIO or IMGUI_DISABLE_OBSOLETE_FUNCTIONS before) + - io.KeyMap[] and io.KeysDown[] are removed (obsoleted February 2022). + - io.NavInputs[] and ImGuiNavInput are removed (obsoleted July 2022). + - pre-1.87 backends are not supported: + - backends need to call io.AddKeyEvent(), io.AddMouseEvent() instead of writing to io.KeysDown[], io.MouseDown[] fields. + - backends need to call io.AddKeyAnalogEvent() for gamepad values instead of writing to io.NavInputs[] fields. + - for more reference: + - read 1.87 and 1.88 part of this section or read Changelog for 1.87 and 1.88. + - read https://github.com/ocornut/imgui/issues/4921 + - if you have trouble updating a very old codebase using legacy backend-specific key codes: consider updating to 1.91.4 first, then #define IMGUI_DISABLE_OBSOLETE_KEYIO, then update to latest. + - obsoleted ImGuiKey_COUNT (it is unusually error-prone/misleading since valid keys don't start at 0). probably use ImGuiKey_NamedKey_BEGIN/ImGuiKey_NamedKey_END? + - fonts: removed const qualifiers from most font functions in prevision for upcoming font improvements. + - 2024/10/18 (1.91.4) - renamed ImGuiCol_NavHighlight to ImGuiCol_NavCursor (for consistency with newly exposed and reworked features). Kept inline redirection enum (will obsolete). + - 2024/10/14 (1.91.4) - moved ImGuiConfigFlags_NavEnableSetMousePos to standalone io.ConfigNavMoveSetMousePos bool. + moved ImGuiConfigFlags_NavNoCaptureKeyboard to standalone io.ConfigNavCaptureKeyboard bool (note the inverted value!). + kept legacy names (will obsolete) + code that copies settings once the first time. Dynamically changing the old value won't work. Switch to using the new value! + - 2024/10/10 (1.91.4) - the typedef for ImTextureID now defaults to ImU64 instead of void*. (#1641) + this removes the requirement to redefine it for backends which are e.g. storing descriptor sets or other 64-bits structures when building on 32-bits archs. It therefore simplify various building scripts/helpers. + you may have compile-time issues if you were casting to 'void*' instead of 'ImTextureID' when passing your types to functions taking ImTextureID values, e.g. ImGui::Image(). + in doubt it is almost always better to do an intermediate intptr_t cast, since it allows casting any pointer/integer type without warning: + - May warn: ImGui::Image((void*)MyTextureData, ...); + - May warn: ImGui::Image((void*)(intptr_t)MyTextureData, ...); + - Won't warn: ImGui::Image((ImTextureID)(intptr_t)MyTextureData), ...); + - note that you can always define ImTextureID to be your own high-level structures (with dedicated constructors) if you like. + - 2024/10/03 (1.91.3) - drags: treat v_min==v_max as a valid clamping range when != 0.0f. Zero is a still special value due to legacy reasons, unless using ImGuiSliderFlags_ClampZeroRange. (#7968, #3361, #76) + - drags: extended behavior of ImGuiSliderFlags_AlwaysClamp to include _ClampZeroRange. It considers v_min==v_max==0.0f as a valid clamping range (aka edits not allowed). + although unlikely, it you wish to only clamp on text input but want v_min==v_max==0.0f to mean unclamped drags, you can use _ClampOnInput instead of _AlwaysClamp. (#7968, #3361, #76) + - 2024/09/10 (1.91.2) - internals: using multiple overlayed ButtonBehavior() with same ID will now have io.ConfigDebugHighlightIdConflicts=true feature emit a warning. (#8030) + it was one of the rare case where using same ID is legal. workarounds: (1) use single ButtonBehavior() call with multiple _MouseButton flags, or (2) surround the calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag() + - 2024/08/23 (1.91.1) - renamed ImGuiChildFlags_Border to ImGuiChildFlags_Borders for consistency. kept inline redirection flag. + - 2024/08/22 (1.91.1) - moved some functions from ImGuiIO to ImGuiPlatformIO structure: + - io.GetClipboardTextFn -> platform_io.Platform_GetClipboardTextFn + changed 'void* user_data' to 'ImGuiContext* ctx'. Pull your user data from platform_io.ClipboardUserData. + - io.SetClipboardTextFn -> platform_io.Platform_SetClipboardTextFn + same as above line. + - io.PlatformOpenInShellFn -> platform_io.Platform_OpenInShellFn (#7660) + - io.PlatformSetImeDataFn -> platform_io.Platform_SetImeDataFn + - io.PlatformLocaleDecimalPoint -> platform_io.Platform_LocaleDecimalPoint (#7389, #6719, #2278) + - access those via GetPlatformIO() instead of GetIO(). + some were introduced very recently and often automatically setup by core library and backends, so for those we are exceptionally not maintaining a legacy redirection symbol. + - commented the old ImageButton() signature obsoleted in 1.89 (~August 2022). As a reminder: + - old ImageButton() before 1.89 used ImTextureId as item id (created issue with e.g. multiple buttons in same scope, transient texture id values, opaque computation of ID) + - new ImageButton() since 1.89 requires an explicit 'const char* str_id' + - old ImageButton() before 1.89 had frame_padding' override argument. + - new ImageButton() since 1.89 always use style.FramePadding, which you can freely override with PushStyleVar()/PopStyleVar(). + - 2024/07/25 (1.91.0) - obsoleted GetContentRegionMax(), GetWindowContentRegionMin() and GetWindowContentRegionMax(). (see #7838 on GitHub for more info) + you should never need those functions. you can do everything with GetCursorScreenPos() and GetContentRegionAvail() in a more simple way. + - instead of: GetWindowContentRegionMax().x - GetCursorPos().x + - you can use: GetContentRegionAvail().x + - instead of: GetWindowContentRegionMax().x + GetWindowPos().x + - you can use: GetCursorScreenPos().x + GetContentRegionAvail().x // when called from left edge of window + - instead of: GetContentRegionMax() + - you can use: GetContentRegionAvail() + GetCursorScreenPos() - GetWindowPos() // right edge in local coordinates + - instead of: GetWindowContentRegionMax().x - GetWindowContentRegionMin().x + - you can use: GetContentRegionAvail() // when called from left edge of window + - 2024/07/15 (1.91.0) - renamed ImGuiSelectableFlags_DontClosePopups to ImGuiSelectableFlags_NoAutoClosePopups. (#1379, #1468, #2200, #4936, #5216, #7302, #7573) + (internals: also renamed ImGuiItemFlags_SelectableDontClosePopup into ImGuiItemFlags_AutoClosePopups with inverted behaviors) + - 2024/07/15 (1.91.0) - obsoleted PushButtonRepeat()/PopButtonRepeat() in favor of using new PushItemFlag(ImGuiItemFlags_ButtonRepeat, ...)/PopItemFlag(). + - 2024/07/02 (1.91.0) - commented out obsolete ImGuiModFlags (renamed to ImGuiKeyChord in 1.89). (#4921, #456) + - commented out obsolete ImGuiModFlags_XXX values (renamed to ImGuiMod_XXX in 1.89). (#4921, #456) + - ImGuiModFlags_Ctrl -> ImGuiMod_Ctrl, ImGuiModFlags_Shift -> ImGuiMod_Shift etc. + - 2024/07/02 (1.91.0) - IO, IME: renamed platform IME hook and added explicit context for consistency and future-proofness. + - old: io.SetPlatformImeDataFn(ImGuiViewport* viewport, ImGuiPlatformImeData* data); + - new: io.PlatformSetImeDataFn(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); + - 2024/06/21 (1.90.9) - BeginChild: added ImGuiChildFlags_NavFlattened as a replacement for the window flag ImGuiWindowFlags_NavFlattened: the feature only ever made sense for BeginChild() anyhow. + - old: BeginChild("Name", size, 0, ImGuiWindowFlags_NavFlattened); + - new: BeginChild("Name", size, ImGuiChildFlags_NavFlattened, 0); + - 2024/06/21 (1.90.9) - io: ClearInputKeys() (first exposed in 1.89.8) doesn't clear mouse data, newly added ClearInputMouse() does. + - 2024/06/20 (1.90.9) - renamed ImGuiDragDropFlags_SourceAutoExpirePayload to ImGuiDragDropFlags_PayloadAutoExpire. + - 2024/06/18 (1.90.9) - style: renamed ImGuiCol_TabActive -> ImGuiCol_TabSelected, ImGuiCol_TabUnfocused -> ImGuiCol_TabDimmed, ImGuiCol_TabUnfocusedActive -> ImGuiCol_TabDimmedSelected. + - 2024/06/10 (1.90.9) - removed old nested structure: renaming ImGuiStorage::ImGuiStoragePair type to ImGuiStoragePair (simpler for many languages). + - 2024/06/06 (1.90.8) - reordered ImGuiInputTextFlags values. This should not be breaking unless you are using generated headers that have values not matching the main library. + - 2024/06/06 (1.90.8) - removed 'ImGuiButtonFlags_MouseButtonDefault_ = ImGuiButtonFlags_MouseButtonLeft', was mostly unused and misleading. + - 2024/05/27 (1.90.7) - commented out obsolete symbols marked obsolete in 1.88 (May 2022): + - old: CaptureKeyboardFromApp(bool) + - new: SetNextFrameWantCaptureKeyboard(bool) + - old: CaptureMouseFromApp(bool) + - new: SetNextFrameWantCaptureMouse(bool) + - 2024/05/22 (1.90.7) - inputs (internals): renamed ImGuiKeyOwner_None to ImGuiKeyOwner_NoOwner, to make use more explicit and reduce confusion with the default it is a non-zero value and cannot be the default value (never made public, but disclosing as I expect a few users caught on owner-aware inputs). + - inputs (internals): renamed ImGuiInputFlags_RouteGlobalLow -> ImGuiInputFlags_RouteGlobal, ImGuiInputFlags_RouteGlobal -> ImGuiInputFlags_RouteGlobalOverFocused, ImGuiInputFlags_RouteGlobalHigh -> ImGuiInputFlags_RouteGlobalHighest. + - inputs (internals): Shortcut(), SetShortcutRouting(): swapped last two parameters order in function signatures: + - old: Shortcut(ImGuiKeyChord key_chord, ImGuiID owner_id = 0, ImGuiInputFlags flags = 0); + - new: Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0, ImGuiID owner_id = 0); + - inputs (internals): owner-aware versions of IsKeyPressed(), IsKeyChordPressed(), IsMouseClicked(): swapped last two parameters order in function signatures. + - old: IsKeyPressed(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + - new: IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id = 0); + - old: IsMouseClicked(ImGuiMouseButton button, ImGuiID owner_id, ImGuiInputFlags flags = 0); + - new: IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id = 0); + for various reasons those changes makes sense. They are being made because making some of those API public. + only past users of imgui_internal.h with the extra parameters will be affected. Added asserts for valid flags in various functions to detect _some_ misuses, BUT NOT ALL. + - 2024/05/21 (1.90.7) - docking: changed signature of DockSpaceOverViewport() to add explicit dockspace id if desired. pass 0 to use old behavior. (#7611) + - old: DockSpaceOverViewport(const ImGuiViewport* viewport = NULL, ImGuiDockNodeFlags flags = 0, ...); + - new: DockSpaceOverViewport(ImGuiID dockspace_id = 0, const ImGuiViewport* viewport = NULL, ImGuiDockNodeFlags flags = 0, ...); + - 2024/05/16 (1.90.7) - inputs: on macOS X, Cmd and Ctrl keys are now automatically swapped by io.AddKeyEvent() as this naturally align with how macOS X uses those keys. + - it shouldn't really affect you unless you had custom shortcut swapping in place for macOS X apps. + - removed ImGuiMod_Shortcut which was previously dynamically remapping to Ctrl or Cmd/Super. It is now unnecessary to specific cross-platform idiomatic shortcuts. (#2343, #4084, #5923, #456) + - 2024/05/14 (1.90.7) - backends: SDL_Renderer2 and SDL_Renderer3 backend now take a SDL_Renderer* in their RenderDrawData() functions. + - 2024/04/18 (1.90.6) - TreeNode: Fixed a layout inconsistency when using an empty/hidden label followed by a SameLine() call. (#7505, #282) + - old: TreeNode("##Hidden"); SameLine(); Text("Hello"); // <-- This was actually incorrect! BUT appeared to look ok with the default style where ItemSpacing.x == FramePadding.x * 2 (it didn't look aligned otherwise). + - new: TreeNode("##Hidden"); SameLine(0, 0); Text("Hello"); // <-- This is correct for all styles values. + with the fix, IF you were successfully using TreeNode("")+SameLine(); you will now have extra spacing between your TreeNode and the following item. + You'll need to change the SameLine() call to SameLine(0,0) to remove this extraneous spacing. This seemed like the more sensible fix that's not making things less consistent. + (Note: when using this idiom you are likely to also use ImGuiTreeNodeFlags_SpanAvailWidth). + - 2024/03/18 (1.90.5) - merged the radius_x/radius_y parameters in ImDrawList::AddEllipse(), AddEllipseFilled() and PathEllipticalArcTo() into a single ImVec2 parameter. Exceptionally, because those functions were added in 1.90, we are not adding inline redirection functions. The transition is easy and should affect few users. (#2743, #7417) + - 2024/03/08 (1.90.5) - inputs: more formally obsoleted GetKeyIndex() when IMGUI_DISABLE_OBSOLETE_FUNCTIONS is set. It has been unnecessary and a no-op since 1.87 (it returns the same value as passed when used with a 1.87+ backend using io.AddKeyEvent() function). (#4921) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) + - 2024/01/15 (1.90.2) - commented out obsolete ImGuiIO::ImeWindowHandle marked obsolete in 1.87, favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + - 2023/12/19 (1.90.1) - commented out obsolete ImGuiKey_KeyPadEnter redirection to ImGuiKey_KeypadEnter. + - 2023/11/06 (1.90.1) - removed CalcListClipping() marked obsolete in 1.86. Prefer using ImGuiListClipper which can return non-contiguous ranges. + - 2023/11/05 (1.90.1) - imgui_freetype: commented out ImGuiFreeType::BuildFontAtlas() obsoleted in 1.81. prefer using #define IMGUI_ENABLE_FREETYPE or see commented code for manual calls. + - 2023/11/05 (1.90.1) - internals,columns: commented out legacy ImGuiColumnsFlags_XXX symbols redirecting to ImGuiOldColumnsFlags_XXX, obsoleted from imgui_internal.h in 1.80. + - 2023/11/09 (1.90.0) - removed IM_OFFSETOF() macro in favor of using offsetof() available in C++11. Kept redirection define (will obsolete). + - 2023/11/07 (1.90.0) - removed BeginChildFrame()/EndChildFrame() in favor of using BeginChild() with the ImGuiChildFlags_FrameStyle flag. kept inline redirection function (will obsolete). + those functions were merely PushStyle/PopStyle helpers, the removal isn't so much motivated by needing to add the feature in BeginChild(), but by the necessity to avoid BeginChildFrame() signature mismatching BeginChild() signature and features. + - 2023/11/02 (1.90.0) - BeginChild: upgraded 'bool border = true' parameter to 'ImGuiChildFlags flags' type, added ImGuiChildFlags_Border equivalent. As with our prior "bool-to-flags" API updates, the ImGuiChildFlags_Border value is guaranteed to be == true forever to ensure a smoother transition, meaning all existing calls will still work. + - old: BeginChild("Name", size, true) + - new: BeginChild("Name", size, ImGuiChildFlags_Border) + - old: BeginChild("Name", size, false) + - new: BeginChild("Name", size) or BeginChild("Name", 0) or BeginChild("Name", size, ImGuiChildFlags_None) + **AMEND FROM THE FUTURE: from 1.91.1, 'ImGuiChildFlags_Border' is called 'ImGuiChildFlags_Borders'** + - 2023/11/02 (1.90.0) - BeginChild: added child-flag ImGuiChildFlags_AlwaysUseWindowPadding as a replacement for the window-flag ImGuiWindowFlags_AlwaysUseWindowPadding: the feature only ever made sense for BeginChild() anyhow. + - old: BeginChild("Name", size, 0, ImGuiWindowFlags_AlwaysUseWindowPadding); + - new: BeginChild("Name", size, ImGuiChildFlags_AlwaysUseWindowPadding, 0); + - 2023/09/27 (1.90.0) - io: removed io.MetricsActiveAllocations introduced in 1.63. Same as 'g.DebugMemAllocCount - g.DebugMemFreeCount' (still displayed in Metrics, unlikely to be accessed by end-user). + - 2023/09/26 (1.90.0) - debug tools: Renamed ShowStackToolWindow() ("Stack Tool") to ShowIDStackToolWindow() ("ID Stack Tool"), as earlier name was misleading. Kept inline redirection function. (#4631) + - 2023/09/15 (1.90.0) - ListBox, Combo: changed signature of "name getter" callback in old one-liner ListBox()/Combo() apis. kept inline redirection function (will obsolete). + - old: bool Combo(const char* label, int* current_item, bool (*getter)(void* user_data, int idx, const char** out_text), ...) + - new: bool Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), ...); + - old: bool ListBox(const char* label, int* current_item, bool (*getting)(void* user_data, int idx, const char** out_text), ...); + - new: bool ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), ...); + - 2023/09/08 (1.90.0) - commented out obsolete redirecting functions: + - GetWindowContentRegionWidth() -> use GetWindowContentRegionMax().x - GetWindowContentRegionMin().x. Consider that generally 'GetContentRegionAvail().x' is more useful. + - ImDrawCornerFlags_XXX -> use ImDrawFlags_RoundCornersXXX flags. Read 1.82 Changelog for details + grep commented names in sources. + - commented out runtime support for hardcoded ~0 or 0x01..0x0F rounding flags values for AddRect()/AddRectFilled()/PathRect()/AddImageRounded() -> use ImDrawFlags_RoundCornersXXX flags. Read 1.82 Changelog for details + - 2023/08/25 (1.89.9) - clipper: Renamed IncludeRangeByIndices() (also called ForceDisplayRangeByIndices() before 1.89.6) to IncludeItemsByIndex(). Kept inline redirection function. Sorry! + - 2023/07/12 (1.89.8) - ImDrawData: CmdLists now owned, changed from ImDrawList** to ImVector. Majority of users shouldn't be affected, but you cannot compare to NULL nor reassign manually anymore. Instead use AddDrawList(). (#6406, #4879, #1878) + - 2023/06/28 (1.89.7) - overlapping items: obsoleted 'SetItemAllowOverlap()' (called after item) in favor of calling 'SetNextItemAllowOverlap()' (called before item). 'SetItemAllowOverlap()' didn't and couldn't work reliably since 1.89 (2022-11-15). + - 2023/06/28 (1.89.7) - overlapping items: renamed 'ImGuiTreeNodeFlags_AllowItemOverlap' to 'ImGuiTreeNodeFlags_AllowOverlap', 'ImGuiSelectableFlags_AllowItemOverlap' to 'ImGuiSelectableFlags_AllowOverlap'. Kept redirecting enums (will obsolete). + - 2023/06/28 (1.89.7) - overlapping items: IsItemHovered() now by default return false when querying an item using AllowOverlap mode which is being overlapped. Use ImGuiHoveredFlags_AllowWhenOverlappedByItem to revert to old behavior. + - 2023/06/28 (1.89.7) - overlapping items: Selectable and TreeNode don't allow overlap when active so overlapping widgets won't appear as hovered. While this fixes a common small visual issue, it also means that calling IsItemHovered() after a non-reactive elements - e.g. Text() - overlapping an active one may fail if you don't use IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem). (#6610) + - 2023/06/20 (1.89.7) - moved io.HoverDelayShort/io.HoverDelayNormal to style.HoverDelayShort/style.HoverDelayNormal. As the fields were added in 1.89 and expected to be left unchanged by most users, or only tweaked once during app initialization, we are exceptionally accepting the breakage. + - 2023/05/30 (1.89.6) - backends: renamed "imgui_impl_sdlrenderer.cpp" to "imgui_impl_sdlrenderer2.cpp" and "imgui_impl_sdlrenderer.h" to "imgui_impl_sdlrenderer2.h". This is in prevision for the future release of SDL3. + - 2023/05/22 (1.89.6) - listbox: commented out obsolete/redirecting functions that were marked obsolete more than two years ago: + - ListBoxHeader() -> use BeginListBox() (note how two variants of ListBoxHeader() existed. Check commented versions in imgui.h for reference) + - ListBoxFooter() -> use EndListBox() + - 2023/05/15 (1.89.6) - clipper: commented out obsolete redirection constructor 'ImGuiListClipper(int items_count, float items_height = -1.0f)' that was marked obsolete in 1.79. Use default constructor + clipper.Begin(). + - 2023/05/15 (1.89.6) - clipper: renamed ImGuiListClipper::ForceDisplayRangeByIndices() to ImGuiListClipper::IncludeRangeByIndices(). + - 2023/03/14 (1.89.4) - commented out redirecting enums/functions names that were marked obsolete two years ago: + - ImGuiSliderFlags_ClampOnInput -> use ImGuiSliderFlags_AlwaysClamp + - ImGuiInputTextFlags_AlwaysInsertMode -> use ImGuiInputTextFlags_AlwaysOverwrite + - ImDrawList::AddBezierCurve() -> use ImDrawList::AddBezierCubic() + - ImDrawList::PathBezierCurveTo() -> use ImDrawList::PathBezierCubicCurveTo() + - 2023/03/09 (1.89.4) - renamed PushAllowKeyboardFocus()/PopAllowKeyboardFocus() to PushTabStop()/PopTabStop(). Kept inline redirection functions (will obsolete). + - 2023/03/09 (1.89.4) - tooltips: Added 'bool' return value to BeginTooltip() for API consistency. Please only submit contents and call EndTooltip() if BeginTooltip() returns true. In reality the function will _currently_ always return true, but further changes down the line may change this, best to clarify API sooner. + - 2023/02/15 (1.89.4) - moved the optional "courtesy maths operators" implementation from imgui_internal.h in imgui.h. + Even though we encourage using your own maths types and operators by setting up IM_VEC2_CLASS_EXTRA, + it has been frequently requested by people to use our own. We had an opt-in define which was + previously fulfilled in imgui_internal.h. It is now fulfilled in imgui.h. (#6164) + - OK: #define IMGUI_DEFINE_MATH_OPERATORS / #include "imgui.h" / #include "imgui_internal.h" + - Error: #include "imgui.h" / #define IMGUI_DEFINE_MATH_OPERATORS / #include "imgui_internal.h" + - 2023/02/07 (1.89.3) - backends: renamed "imgui_impl_sdl.cpp" to "imgui_impl_sdl2.cpp" and "imgui_impl_sdl.h" to "imgui_impl_sdl2.h". (#6146) This is in prevision for the future release of SDL3. + - 2022/10/26 (1.89) - commented out redirecting OpenPopupContextItem() which was briefly the name of OpenPopupOnItemClick() from 1.77 to 1.79. + - 2022/10/12 (1.89) - removed runtime patching of invalid "%f"/"%0.f" format strings for DragInt()/SliderInt(). This was obsoleted in 1.61 (May 2018). See 1.61 changelog for details. + - 2022/09/26 (1.89) - renamed and merged keyboard modifiers key enums and flags into a same set. Kept inline redirection enums (will obsolete). + - ImGuiKey_ModCtrl and ImGuiModFlags_Ctrl -> ImGuiMod_Ctrl + - ImGuiKey_ModShift and ImGuiModFlags_Shift -> ImGuiMod_Shift + - ImGuiKey_ModAlt and ImGuiModFlags_Alt -> ImGuiMod_Alt + - ImGuiKey_ModSuper and ImGuiModFlags_Super -> ImGuiMod_Super + the ImGuiKey_ModXXX were introduced in 1.87 and mostly used by backends. + the ImGuiModFlags_XXX have been exposed in imgui.h but not really used by any public api only by third-party extensions. + exceptionally commenting out the older ImGuiKeyModFlags_XXX names ahead of obsolescence schedule to reduce confusion and because they were not meant to be used anyway. + - 2022/09/20 (1.89) - ImGuiKey is now a typed enum, allowing ImGuiKey_XXX symbols to be named in debuggers. + this will require uses of legacy backend-dependent indices to be casted, e.g. + - with imgui_impl_glfw: IsKeyPressed(GLFW_KEY_A) -> IsKeyPressed((ImGuiKey)GLFW_KEY_A); + - with imgui_impl_win32: IsKeyPressed('A') -> IsKeyPressed((ImGuiKey)'A') + - etc. However if you are upgrading code you might well use the better, backend-agnostic IsKeyPressed(ImGuiKey_A) now! + - 2022/09/12 (1.89) - removed the bizarre legacy default argument for 'TreePush(const void* ptr = NULL)', always pass a pointer value explicitly. NULL/nullptr is ok but require cast, e.g. TreePush((void*)nullptr); + - 2022/09/05 (1.89) - commented out redirecting functions/enums names that were marked obsolete in 1.77 and 1.78 (June 2020): + - DragScalar(), DragScalarN(), DragFloat(), DragFloat2(), DragFloat3(), DragFloat4(): For old signatures ending with (..., const char* format, float power = 1.0f) -> use (..., format ImGuiSliderFlags_Logarithmic) if power != 1.0f. + - SliderScalar(), SliderScalarN(), SliderFloat(), SliderFloat2(), SliderFloat3(), SliderFloat4(): For old signatures ending with (..., const char* format, float power = 1.0f) -> use (..., format ImGuiSliderFlags_Logarithmic) if power != 1.0f. + - BeginPopupContextWindow(const char*, ImGuiMouseButton, bool) -> use BeginPopupContextWindow(const char*, ImGuiPopupFlags) + - 2022/09/02 (1.89) - obsoleted using SetCursorPos()/SetCursorScreenPos() to extend parent window/cell boundaries. + this relates to when moving the cursor position beyond current boundaries WITHOUT submitting an item. + - previously this would make the window content size ~200x200: + Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); + - instead, please submit an item: + Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + Dummy(ImVec2(0,0)) + End(); + - alternative: + Begin(...) + Dummy(ImVec2(200,200)) + End(); + - content size is now only extended when submitting an item! + - with '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' this will now be detected and assert. + - without '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' this will silently be fixed until we obsolete it. + - 2022/08/03 (1.89) - changed signature of ImageButton() function. Kept redirection function (will obsolete). + - added 'const char* str_id' parameter + removed 'int frame_padding = -1' parameter. + - old signature: bool ImageButton(ImTextureID tex_id, ImVec2 size, ImVec2 uv0 = ImVec2(0,0), ImVec2 uv1 = ImVec2(1,1), int frame_padding = -1, ImVec4 bg_col = ImVec4(0,0,0,0), ImVec4 tint_col = ImVec4(1,1,1,1)); + - used the ImTextureID value to create an ID. This was inconsistent with other functions, led to ID conflicts, and caused problems with engines using transient ImTextureID values. + - had a FramePadding override which was inconsistent with other functions and made the already-long signature even longer. + - new signature: bool ImageButton(const char* str_id, ImTextureID tex_id, ImVec2 size, ImVec2 uv0 = ImVec2(0,0), ImVec2 uv1 = ImVec2(1,1), ImVec4 bg_col = ImVec4(0,0,0,0), ImVec4 tint_col = ImVec4(1,1,1,1)); + - requires an explicit identifier. You may still use e.g. PushID() calls and then pass an empty identifier. + - always uses style.FramePadding for padding, to be consistent with other buttons. You may use PushStyleVar() to alter this. + - 2022/07/08 (1.89) - inputs: removed io.NavInputs[] and ImGuiNavInput enum (following 1.87 changes). + - Official backends from 1.87+ -> no issue. + - Official backends from 1.60 to 1.86 -> will build and convert gamepad inputs, unless IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Need updating! + - Custom backends not writing to io.NavInputs[] -> no issue. + - Custom backends writing to io.NavInputs[] -> will build and convert gamepad inputs, unless IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Need fixing! + - TL;DR: Backends should call io.AddKeyEvent()/io.AddKeyAnalogEvent() with ImGuiKey_GamepadXXX values instead of filling io.NavInput[]. + - 2022/06/15 (1.88) - renamed IMGUI_DISABLE_METRICS_WINDOW to IMGUI_DISABLE_DEBUG_TOOLS for correctness. kept support for old define (will obsolete). + - 2022/05/03 (1.88) - backends: osx: removed ImGui_ImplOSX_HandleEvent() from backend API in favor of backend automatically handling event capture. All ImGui_ImplOSX_HandleEvent() calls should be removed as they are now unnecessary. + - 2022/04/05 (1.88) - inputs: renamed ImGuiKeyModFlags to ImGuiModFlags. Kept inline redirection enums (will obsolete). This was never used in public API functions but technically present in imgui.h and ImGuiIO. + - 2022/01/20 (1.87) - inputs: reworded gamepad IO. + - Backend writing to io.NavInputs[] -> backend should call io.AddKeyEvent()/io.AddKeyAnalogEvent() with ImGuiKey_GamepadXXX values. + - 2022/01/19 (1.87) - sliders, drags: removed support for legacy arithmetic operators (+,+-,*,/) when inputting text. This doesn't break any api/code but a feature that used to be accessible by end-users (which seemingly no one used). + - 2022/01/17 (1.87) - inputs: reworked mouse IO. + - Backend writing to io.MousePos -> backend should call io.AddMousePosEvent() + - Backend writing to io.MouseDown[] -> backend should call io.AddMouseButtonEvent() + - Backend writing to io.MouseWheel -> backend should call io.AddMouseWheelEvent() + - Backend writing to io.MouseHoveredViewport -> backend should call io.AddMouseViewportEvent() [Docking branch w/ multi-viewports only] + note: for all calls to IO new functions, the Dear ImGui context should be bound/current. + read https://github.com/ocornut/imgui/issues/4921 for details. + - 2022/01/10 (1.87) - inputs: reworked keyboard IO. Removed io.KeyMap[], io.KeysDown[] in favor of calling io.AddKeyEvent(), ImGui::IsKeyDown(). Removed GetKeyIndex(), now unnecessary. All IsKeyXXX() functions now take ImGuiKey values. All features are still functional until IMGUI_DISABLE_OBSOLETE_KEYIO is defined. Read Changelog and Release Notes for details. + - IsKeyPressed(MY_NATIVE_KEY_XXX) -> use IsKeyPressed(ImGuiKey_XXX) + - IsKeyPressed(GetKeyIndex(ImGuiKey_XXX)) -> use IsKeyPressed(ImGuiKey_XXX) + - Backend writing to io.KeyMap[],io.KeysDown[] -> backend should call io.AddKeyEvent() (+ call io.SetKeyEventNativeData() if you want legacy user code to stil function with legacy key codes). + - Backend writing to io.KeyCtrl, io.KeyShift.. -> backend should call io.AddKeyEvent() with ImGuiMod_XXX values. *IF YOU PULLED CODE BETWEEN 2021/01/10 and 2021/01/27: We used to have a io.AddKeyModsEvent() function which was now replaced by io.AddKeyEvent() with ImGuiMod_XXX values.* + - one case won't work with backward compatibility: if your custom backend used ImGuiKey as mock native indices (e.g. "io.KeyMap[ImGuiKey_A] = ImGuiKey_A") because those values are now larger than the legacy KeyDown[] array. Will assert. + - inputs: added ImGuiKey_ModCtrl/ImGuiKey_ModShift/ImGuiKey_ModAlt/ImGuiKey_ModSuper values to submit keyboard modifiers using io.AddKeyEvent(), instead of writing directly to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper. + - 2022/01/05 (1.87) - inputs: renamed ImGuiKey_KeyPadEnter to ImGuiKey_KeypadEnter to align with new symbols. Kept redirection enum. + - 2022/01/05 (1.87) - removed io.ImeSetInputScreenPosFn() in favor of more flexible io.SetPlatformImeDataFn(). Removed 'void* io.ImeWindowHandle' in favor of writing to 'void* ImGuiViewport::PlatformHandleRaw'. + - 2022/01/01 (1.87) - commented out redirecting functions/enums names that were marked obsolete in 1.69, 1.70, 1.71, 1.72 (March-July 2019) + - ImGui::SetNextTreeNodeOpen() -> use ImGui::SetNextItemOpen() + - ImGui::GetContentRegionAvailWidth() -> use ImGui::GetContentRegionAvail().x + - ImGui::TreeAdvanceToLabelPos() -> use ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetTreeNodeToLabelSpacing()); + - ImFontAtlas::CustomRect -> use ImFontAtlasCustomRect + - ImGuiColorEditFlags_RGB/HSV/HEX -> use ImGuiColorEditFlags_DisplayRGB/HSV/Hex + - 2021/12/20 (1.86) - backends: removed obsolete Marmalade backend (imgui_impl_marmalade.cpp) + example. Find last supported version at https://github.com/ocornut/imgui/wiki/Bindings + - 2021/11/04 (1.86) - removed CalcListClipping() function. Prefer using ImGuiListClipper which can return non-contiguous ranges. Please open an issue if you think you really need this function. + - 2021/08/23 (1.85) - removed GetWindowContentRegionWidth() function. keep inline redirection helper. can use 'GetWindowContentRegionMax().x - GetWindowContentRegionMin().x' instead for generally 'GetContentRegionAvail().x' is more useful. + - 2021/07/26 (1.84) - commented out redirecting functions/enums names that were marked obsolete in 1.67 and 1.69 (March 2019): + - ImGui::GetOverlayDrawList() -> use ImGui::GetForegroundDrawList() + - ImFont::GlyphRangesBuilder -> use ImFontGlyphRangesBuilder + - 2021/05/19 (1.83) - backends: obsoleted direct access to ImDrawCmd::TextureId in favor of calling ImDrawCmd::GetTexID(). + - if you are using official backends from the source tree: you have nothing to do. + - if you have copied old backend code or using your own: change access to draw_cmd->TextureId to draw_cmd->GetTexID(). + - 2021/03/12 (1.82) - upgraded ImDrawList::AddRect(), AddRectFilled(), PathRect() to use ImDrawFlags instead of ImDrawCornersFlags. + - ImDrawCornerFlags_TopLeft -> use ImDrawFlags_RoundCornersTopLeft + - ImDrawCornerFlags_BotRight -> use ImDrawFlags_RoundCornersBottomRight + - ImDrawCornerFlags_None -> use ImDrawFlags_RoundCornersNone etc. + flags now sanely defaults to 0 instead of 0x0F, consistent with all other flags in the API. + breaking: the default with rounding > 0.0f is now "round all corners" vs old implicit "round no corners": + - rounding == 0.0f + flags == 0 --> meant no rounding --> unchanged (common use) + - rounding > 0.0f + flags != 0 --> meant rounding --> unchanged (common use) + - rounding == 0.0f + flags != 0 --> meant no rounding --> unchanged (unlikely use) + - rounding > 0.0f + flags == 0 --> meant no rounding --> BREAKING (unlikely use): will now round all corners --> use ImDrawFlags_RoundCornersNone or rounding == 0.0f. + this ONLY matters for hard coded use of 0 + rounding > 0.0f. Use of named ImDrawFlags_RoundCornersNone (new) or ImDrawCornerFlags_None (old) are ok. + the old ImDrawCornersFlags used awkward default values of ~0 or 0xF (4 lower bits set) to signify "round all corners" and we sometimes encouraged using them as shortcuts. + legacy path still support use of hard coded ~0 or any value from 0x1 or 0xF. They will behave the same with legacy paths enabled (will assert otherwise). + - 2021/03/11 (1.82) - removed redirecting functions/enums names that were marked obsolete in 1.66 (September 2018): + - ImGui::SetScrollHere() -> use ImGui::SetScrollHereY() + - 2021/03/11 (1.82) - clarified that ImDrawList::PathArcTo(), ImDrawList::PathArcToFast() won't render with radius < 0.0f. Previously it sorts of accidentally worked but would generally lead to counter-clockwise paths and have an effect on anti-aliasing. + - 2021/03/10 (1.82) - upgraded ImDrawList::AddPolyline() and PathStroke() "bool closed" parameter to "ImDrawFlags flags". The matching ImDrawFlags_Closed value is guaranteed to always stay == 1 in the future. + - 2021/02/22 (1.82) - (*undone in 1.84*) win32+mingw: Re-enabled IME functions by default even under MinGW. In July 2016, issue #738 had me incorrectly disable those default functions for MinGW. MinGW users should: either link with -limm32, either set their imconfig file with '#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS'. + - 2021/02/17 (1.82) - renamed rarely used style.CircleSegmentMaxError (old default = 1.60f) to style.CircleTessellationMaxError (new default = 0.30f) as the meaning of the value changed. + - 2021/02/03 (1.81) - renamed ListBoxHeader(const char* label, ImVec2 size) to BeginListBox(). Kept inline redirection function (will obsolete). + - removed ListBoxHeader(const char* label, int items_count, int height_in_items = -1) in favor of specifying size. Kept inline redirection function (will obsolete). + - renamed ListBoxFooter() to EndListBox(). Kept inline redirection function (will obsolete). + - 2021/01/26 (1.81) - removed ImGuiFreeType::BuildFontAtlas(). Kept inline redirection function. Prefer using '#define IMGUI_ENABLE_FREETYPE', but there's a runtime selection path available too. The shared extra flags parameters (very rarely used) are now stored in ImFontAtlas::FontBuilderFlags. + - renamed ImFontConfig::RasterizerFlags (used by FreeType) to ImFontConfig::FontBuilderFlags. + - renamed ImGuiFreeType::XXX flags to ImGuiFreeTypeBuilderFlags_XXX for consistency with other API. + - 2020/10/12 (1.80) - removed redirecting functions/enums that were marked obsolete in 1.63 (August 2018): + - ImGui::IsItemDeactivatedAfterChange() -> use ImGui::IsItemDeactivatedAfterEdit(). + - ImGuiCol_ModalWindowDarkening -> use ImGuiCol_ModalWindowDimBg + - ImGuiInputTextCallback -> use ImGuiTextEditCallback + - ImGuiInputTextCallbackData -> use ImGuiTextEditCallbackData + - 2020/12/21 (1.80) - renamed ImDrawList::AddBezierCurve() to AddBezierCubic(), and PathBezierCurveTo() to PathBezierCubicCurveTo(). Kept inline redirection function (will obsolete). + - 2020/12/04 (1.80) - added imgui_tables.cpp file! Manually constructed project files will need the new file added! + - 2020/11/18 (1.80) - renamed undocumented/internals ImGuiColumnsFlags_* to ImGuiOldColumnFlags_* in prevision of incoming Tables API. + - 2020/11/03 (1.80) - renamed io.ConfigWindowsMemoryCompactTimer to io.ConfigMemoryCompactTimer as the feature will apply to other data structures + - 2020/10/14 (1.80) - backends: moved all backends files (imgui_impl_XXXX.cpp, imgui_impl_XXXX.h) from examples/ to backends/. + - 2020/10/12 (1.80) - removed redirecting functions/enums that were marked obsolete in 1.60 (April 2018): + - io.RenderDrawListsFn pointer -> use ImGui::GetDrawData() value and call the render function of your backend + - ImGui::IsAnyWindowFocused() -> use ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow) + - ImGui::IsAnyWindowHovered() -> use ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow) + - ImGuiStyleVar_Count_ -> use ImGuiStyleVar_COUNT + - ImGuiMouseCursor_Count_ -> use ImGuiMouseCursor_COUNT + - removed redirecting functions names that were marked obsolete in 1.61 (May 2018): + - InputFloat (... int decimal_precision ...) -> use InputFloat (... const char* format ...) with format = "%.Xf" where X is your value for decimal_precision. + - same for InputFloat2()/InputFloat3()/InputFloat4() variants taking a `int decimal_precision` parameter. + - 2020/10/05 (1.79) - removed ImGuiListClipper: Renamed constructor parameters which created an ambiguous alternative to using the ImGuiListClipper::Begin() function, with misleading edge cases (note: imgui_memory_editor <0.40 from imgui_club/ used this old clipper API. Update your copy if needed). + - 2020/09/25 (1.79) - renamed ImGuiSliderFlags_ClampOnInput to ImGuiSliderFlags_AlwaysClamp. Kept redirection enum (will obsolete sooner because previous name was added recently). + - 2020/09/25 (1.79) - renamed style.TabMinWidthForUnselectedCloseButton to style.TabMinWidthForCloseButton. + - 2020/09/21 (1.79) - renamed OpenPopupContextItem() back to OpenPopupOnItemClick(), reverting the change from 1.77. For varieties of reason this is more self-explanatory. + - 2020/09/21 (1.79) - removed return value from OpenPopupOnItemClick() - returned true on mouse release on an item - because it is inconsistent with other popup APIs and makes others misleading. It's also and unnecessary: you can use IsWindowAppearing() after BeginPopup() for a similar result. + - 2020/09/17 (1.79) - removed ImFont::DisplayOffset in favor of ImFontConfig::GlyphOffset. DisplayOffset was applied after scaling and not very meaningful/useful outside of being needed by the default ProggyClean font. If you scaled this value after calling AddFontDefault(), this is now done automatically. It was also getting in the way of better font scaling, so let's get rid of it now! + - 2020/08/17 (1.78) - obsoleted use of the trailing 'float power=1.0f' parameter for DragFloat(), DragFloat2(), DragFloat3(), DragFloat4(), DragFloatRange2(), DragScalar(), DragScalarN(), SliderFloat(), SliderFloat2(), SliderFloat3(), SliderFloat4(), SliderScalar(), SliderScalarN(), VSliderFloat() and VSliderScalar(). + replaced the 'float power=1.0f' argument with integer-based flags defaulting to 0 (as with all our flags). + worked out a backward-compatibility scheme so hopefully most C++ codebase should not be affected. in short, when calling those functions: + - if you omitted the 'power' parameter (likely!), you are not affected. + - if you set the 'power' parameter to 1.0f (same as previous default value): 1/ your compiler may warn on float>int conversion, 2/ everything else will work. 3/ you can replace the 1.0f value with 0 to fix the warning, and be technically correct. + - if you set the 'power' parameter to >1.0f (to enable non-linear editing): 1/ your compiler may warn on float>int conversion, 2/ code will assert at runtime, 3/ in case asserts are disabled, the code will not crash and enable the _Logarithmic flag. 4/ you can replace the >1.0f value with ImGuiSliderFlags_Logarithmic to fix the warning/assert and get a _similar_ effect as previous uses of power >1.0f. + see https://github.com/ocornut/imgui/issues/3361 for all details. + kept inline redirection functions (will obsolete) apart for: DragFloatRange2(), VSliderFloat(), VSliderScalar(). For those three the 'float power=1.0f' version was removed directly as they were most unlikely ever used. + for shared code, you can version check at compile-time with `#if IMGUI_VERSION_NUM >= 17704`. + - obsoleted use of v_min > v_max in DragInt, DragFloat, DragScalar to lock edits (introduced in 1.73, was not demoed nor documented very), will be replaced by a more generic ReadOnly feature. You may use the ImGuiSliderFlags_ReadOnly internal flag in the meantime. + - 2020/06/23 (1.77) - removed BeginPopupContextWindow(const char*, int mouse_button, bool also_over_items) in favor of BeginPopupContextWindow(const char*, ImGuiPopupFlags flags) with ImGuiPopupFlags_NoOverItems. + - 2020/06/15 (1.77) - renamed OpenPopupOnItemClick() to OpenPopupContextItem(). Kept inline redirection function (will obsolete). [NOTE: THIS WAS REVERTED IN 1.79] + - 2020/06/15 (1.77) - removed CalcItemRectClosestPoint() entry point which was made obsolete and asserting in December 2017. + - 2020/04/23 (1.77) - removed unnecessary ID (first arg) of ImFontAtlas::AddCustomRectRegular(). + - 2020/01/22 (1.75) - ImDrawList::AddCircle()/AddCircleFilled() functions don't accept negative radius any more. + - 2019/12/17 (1.75) - [undid this change in 1.76] made Columns() limited to 64 columns by asserting above that limit. While the current code technically supports it, future code may not so we're putting the restriction ahead. + - 2019/12/13 (1.75) - [imgui_internal.h] changed ImRect() default constructor initializes all fields to 0.0f instead of (FLT_MAX,FLT_MAX,-FLT_MAX,-FLT_MAX). If you used ImRect::Add() to create bounding boxes by adding multiple points into it, you may need to fix your initial value. + - 2019/12/08 (1.75) - removed redirecting functions/enums that were marked obsolete in 1.53 (December 2017): + - ShowTestWindow() -> use ShowDemoWindow() + - IsRootWindowFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootWindow) + - IsRootWindowOrAnyChildFocused() -> use IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows) + - SetNextWindowContentWidth(w) -> use SetNextWindowContentSize(ImVec2(w, 0.0f) + - GetItemsLineHeightWithSpacing() -> use GetFrameHeightWithSpacing() + - ImGuiCol_ChildWindowBg -> use ImGuiCol_ChildBg + - ImGuiStyleVar_ChildWindowRounding -> use ImGuiStyleVar_ChildRounding + - ImGuiTreeNodeFlags_AllowOverlapMode -> use ImGuiTreeNodeFlags_AllowItemOverlap + - IMGUI_DISABLE_TEST_WINDOWS -> use IMGUI_DISABLE_DEMO_WINDOWS + - 2019/12/08 (1.75) - obsoleted calling ImDrawList::PrimReserve() with a negative count (which was vaguely documented and rarely if ever used). Instead, we added an explicit PrimUnreserve() API. + - 2019/12/06 (1.75) - removed implicit default parameter to IsMouseDragging(int button = 0) to be consistent with other mouse functions (none of the other functions have it). + - 2019/11/21 (1.74) - ImFontAtlas::AddCustomRectRegular() now requires an ID larger than 0x110000 (instead of 0x10000) to conform with supporting Unicode planes 1-16 in a future update. ID below 0x110000 will now assert. + - 2019/11/19 (1.74) - renamed IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS to IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS for consistency. + - 2019/11/19 (1.74) - renamed IMGUI_DISABLE_MATH_FUNCTIONS to IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS for consistency. + - 2019/10/22 (1.74) - removed redirecting functions/enums that were marked obsolete in 1.52 (October 2017): + - Begin() [old 5 args version] -> use Begin() [3 args], use SetNextWindowSize() SetNextWindowBgAlpha() if needed + - IsRootWindowOrAnyChildHovered() -> use IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows) + - AlignFirstTextHeightToWidgets() -> use AlignTextToFramePadding() + - SetNextWindowPosCenter() -> use SetNextWindowPos() with a pivot of (0.5f, 0.5f) + - ImFont::Glyph -> use ImFontGlyph + - 2019/10/14 (1.74) - inputs: Fixed a miscalculation in the keyboard/mouse "typematic" repeat delay/rate calculation, used by keys and e.g. repeating mouse buttons as well as the GetKeyPressedAmount() function. + if you were using a non-default value for io.KeyRepeatRate (previous default was 0.250), you can add +io.KeyRepeatDelay to it to compensate for the fix. + The function was triggering on: 0.0 and (delay+rate*N) where (N>=1). Fixed formula responds to (N>=0). Effectively it made io.KeyRepeatRate behave like it was set to (io.KeyRepeatRate + io.KeyRepeatDelay). + If you never altered io.KeyRepeatRate nor used GetKeyPressedAmount() this won't affect you. - 2019/07/15 (1.72) - removed TreeAdvanceToLabelPos() which is rarely used and only does SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()). Kept redirection function (will obsolete). - 2019/07/12 (1.72) - renamed ImFontAtlas::CustomRect to ImFontAtlasCustomRect. Kept redirection typedef (will obsolete). - - 2019/06/14 (1.72) - removed redirecting functions/enums names that were marked obsolete in 1.51 (June 2017): ImGuiCol_Column*, ImGuiSetCond_*, IsItemHoveredRect(), IsPosHoveringAnyWindow(), IsMouseHoveringAnyWindow(), IsMouseHoveringWindow(), IMGUI_ONCE_UPON_A_FRAME. Grep this log for details and new names. + - 2019/06/14 (1.72) - removed redirecting functions/enums names that were marked obsolete in 1.51 (June 2017): ImGuiCol_Column*, ImGuiSetCond_*, IsItemHoveredRect(), IsPosHoveringAnyWindow(), IsMouseHoveringAnyWindow(), IsMouseHoveringWindow(), IMGUI_ONCE_UPON_A_FRAME. Grep this log for details and new names, or see how they were implemented until 1.71. - 2019/06/07 (1.71) - rendering of child window outer decorations (bg color, border, scrollbars) is now performed as part of the parent window. If you have overlapping child windows in a same parent, and relied on their relative z-order to be mapped to their submission order, this will affect your rendering. This optimization is disabled if the parent window has no visual output, because it appears to be the most common situation leading to the creation of overlapping child windows. @@ -382,7 +804,7 @@ CODE - 2019/04/29 (1.70) - removed GetContentRegionAvailWidth(), use GetContentRegionAvail().x instead. Kept inline redirection function (will obsolete). - 2019/03/04 (1.69) - renamed GetOverlayDrawList() to GetForegroundDrawList(). Kept redirection function (will obsolete). - 2019/02/26 (1.69) - renamed ImGuiColorEditFlags_RGB/ImGuiColorEditFlags_HSV/ImGuiColorEditFlags_HEX to ImGuiColorEditFlags_DisplayRGB/ImGuiColorEditFlags_DisplayHSV/ImGuiColorEditFlags_DisplayHex. Kept redirection enums (will obsolete). - - 2019/02/14 (1.68) - made it illegal/assert when io.DisplayTime == 0.0f (with an exception for the first frame). If for some reason your time step calculation gives you a zero value, replace it with a dummy small value! + - 2019/02/14 (1.68) - made it illegal/assert when io.DisplayTime == 0.0f (with an exception for the first frame). If for some reason your time step calculation gives you a zero value, replace it with an arbitrarily small value! - 2019/02/01 (1.68) - removed io.DisplayVisibleMin/DisplayVisibleMax (which were marked obsolete and removed from viewport/docking branch already). - 2019/01/06 (1.67) - renamed io.InputCharacters[], marked internal as was always intended. Please don't access directly, and use AddInputCharacter() instead! - 2019/01/06 (1.67) - renamed ImFontAtlas::GlyphRangesBuilder to ImFontGlyphRangesBuilder. Kept redirection typedef (will obsolete). @@ -403,10 +825,10 @@ CODE - 2018/08/01 (1.63) - renamed io.OptCursorBlink to io.ConfigCursorBlink [-> io.ConfigInputTextCursorBlink in 1.65], io.OptMacOSXBehaviors to ConfigMacOSXBehaviors for consistency. - 2018/07/22 (1.63) - changed ImGui::GetTime() return value from float to double to avoid accumulating floating point imprecisions over time. - 2018/07/08 (1.63) - style: renamed ImGuiCol_ModalWindowDarkening to ImGuiCol_ModalWindowDimBg for consistency with other features. Kept redirection enum (will obsolete). - - 2018/06/08 (1.62) - examples: the imgui_impl_xxx files have been split to separate platform (Win32, Glfw, SDL2, etc.) from renderer (DX11, OpenGL, Vulkan, etc.). - old bindings will still work as is, however prefer using the separated bindings as they will be updated to support multi-viewports. - when adopting new bindings follow the main.cpp code of your preferred examples/ folder to know which functions to call. - in particular, note that old bindings called ImGui::NewFrame() at the end of their ImGui_ImplXXXX_NewFrame() function. + - 2018/06/08 (1.62) - examples: the imgui_impl_XXX files have been split to separate platform (Win32, GLFW, SDL2, etc.) from renderer (DX11, OpenGL, Vulkan, etc.). + old backends will still work as is, however prefer using the separated backends as they will be updated to support multi-viewports. + when adopting new backends follow the main.cpp code of your preferred examples/ folder to know which functions to call. + in particular, note that old backends called ImGui::NewFrame() at the end of their ImGui_ImplXXXX_NewFrame() function. - 2018/06/06 (1.62) - renamed GetGlyphRangesChinese() to GetGlyphRangesChineseFull() to distinguish other variants and discourage using the full set. - 2018/06/06 (1.62) - TreeNodeEx()/TreeNodeBehavior(): the ImGuiTreeNodeFlags_CollapsingHeader helper now include the ImGuiTreeNodeFlags_NoTreePushOnOpen flag. See Changelog for details. - 2018/05/03 (1.61) - DragInt(): the default compile-time format string has been changed from "%.0f" to "%d", as we are not using integers internally any more. @@ -416,7 +838,7 @@ CODE - 2018/04/28 (1.61) - obsoleted InputFloat() functions taking an optional "int decimal_precision" in favor of an equivalent and more flexible "const char* format", consistent with other functions. Kept redirection functions (will obsolete). - 2018/04/09 (1.61) - IM_DELETE() helper function added in 1.60 doesn't clear the input _pointer_ reference, more consistent with expectation and allows passing r-value. - - 2018/03/20 (1.60) - renamed io.WantMoveMouse to io.WantSetMousePos for consistency and ease of understanding (was added in 1.52, _not_ used by core and only honored by some binding ahead of merging the Nav branch). + - 2018/03/20 (1.60) - renamed io.WantMoveMouse to io.WantSetMousePos for consistency and ease of understanding (was added in 1.52, _not_ used by core and only honored by some backend ahead of merging the Nav branch). - 2018/03/12 (1.60) - removed ImGuiCol_CloseButton, ImGuiCol_CloseButtonActive, ImGuiCol_CloseButtonHovered as the closing cross uses regular button colors now. - 2018/03/08 (1.60) - changed ImFont::DisplayOffset.y to default to 0 instead of +1. Fixed rounding of Ascent/Descent to match TrueType renderer. If you were adding or subtracting to ImFont::DisplayOffset check if your fonts are correctly aligned vertically. - 2018/03/03 (1.60) - renamed ImGuiStyleVar_Count_ to ImGuiStyleVar_COUNT and ImGuiMouseCursor_Count_ to ImGuiMouseCursor_COUNT for consistency with other public enums. @@ -460,7 +882,7 @@ CODE - 2017/10/11 (1.52) - renamed AlignFirstTextHeightToWidgets() to AlignTextToFramePadding(). Kept inline redirection function (will obsolete). - 2017/09/26 (1.52) - renamed ImFont::Glyph to ImFontGlyph. Kept redirection typedef (will obsolete). - 2017/09/25 (1.52) - removed SetNextWindowPosCenter() because SetNextWindowPos() now has the optional pivot information to do the same and more. Kept redirection function (will obsolete). - - 2017/08/25 (1.52) - io.MousePos needs to be set to ImVec2(-FLT_MAX,-FLT_MAX) when mouse is unavailable/missing. Previously ImVec2(-1,-1) was enough but we now accept negative mouse coordinates. In your binding if you need to support unavailable mouse, make sure to replace "io.MousePos = ImVec2(-1,-1)" with "io.MousePos = ImVec2(-FLT_MAX,-FLT_MAX)". + - 2017/08/25 (1.52) - io.MousePos needs to be set to ImVec2(-FLT_MAX,-FLT_MAX) when mouse is unavailable/missing. Previously ImVec2(-1,-1) was enough but we now accept negative mouse coordinates. In your backend if you need to support unavailable mouse, make sure to replace "io.MousePos = ImVec2(-1,-1)" with "io.MousePos = ImVec2(-FLT_MAX,-FLT_MAX)". - 2017/08/22 (1.51) - renamed IsItemHoveredRect() to IsItemRectHovered(). Kept inline redirection function (will obsolete). -> (1.52) use IsItemHovered(ImGuiHoveredFlags_RectOnly)! - renamed IsMouseHoveringAnyWindow() to IsAnyWindowHovered() for consistency. Kept inline redirection function (will obsolete). - renamed IsMouseHoveringWindow() to IsWindowRectHovered() for consistency. Kept inline redirection function (will obsolete). @@ -473,29 +895,24 @@ CODE - 2017/08/09 (1.51) - removed ValueColor() helpers, they are equivalent to calling Text(label) + SameLine() + ColorButton(). - 2017/08/08 (1.51) - removed ColorEditMode() and ImGuiColorEditMode in favor of ImGuiColorEditFlags and parameters to the various Color*() functions. The SetColorEditOptions() allows to initialize default but the user can still change them with right-click context menu. - changed prototype of 'ColorEdit4(const char* label, float col[4], bool show_alpha = true)' to 'ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0)', where passing flags = 0x01 is a safe no-op (hello dodgy backward compatibility!). - check and run the demo window, under "Color/Picker Widgets", to understand the various new options. - - changed prototype of rarely used 'ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)' to 'ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0,0))' + - changed prototype of rarely used 'ColorButton(ImVec4 col, bool small_height = false, bool outline_border = true)' to 'ColorButton(const char* desc_id, ImVec4 col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0, 0))' - 2017/07/20 (1.51) - removed IsPosHoveringAnyWindow(ImVec2), which was partly broken and misleading. ASSERT + redirect user to io.WantCaptureMouse - 2017/05/26 (1.50) - removed ImFontConfig::MergeGlyphCenterV in favor of a more multipurpose ImFontConfig::GlyphOffset. - 2017/05/01 (1.50) - renamed ImDrawList::PathFill() (rarely used directly) to ImDrawList::PathFillConvex() for clarity. - - 2016/11/06 (1.50) - BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetId() and use it instead of passing string to BeginChild(). + - 2016/11/06 (1.50) - BeginChild(const char*) now applies the stack id to the provided label, consistently with other functions as it should always have been. It shouldn't affect you unless (extremely unlikely) you were appending multiple times to a same child from different locations of the stack id. If that's the case, generate an id with GetID() and use it instead of passing string to BeginChild(). - 2016/10/15 (1.50) - avoid 'void* user_data' parameter to io.SetClipboardTextFn/io.GetClipboardTextFn pointers. We pass io.ClipboardUserData to it. - 2016/09/25 (1.50) - style.WindowTitleAlign is now a ImVec2 (ImGuiAlign enum was removed). set to (0.5f,0.5f) for horizontal+vertical centering, (0.0f,0.0f) for upper-left, etc. - - 2016/07/30 (1.50) - SameLine(x) with x>0.0f is now relative to left of column/group if any, and not always to left of window. This was sort of always the intent and hopefully breakage should be minimal. + - 2016/07/30 (1.50) - SameLine(x) with x>0.0f is now relative to left of column/group if any, and not always to left of window. This was sort of always the intent and hopefully, breakage should be minimal. - 2016/05/12 (1.49) - title bar (using ImGuiCol_TitleBg/ImGuiCol_TitleBgActive colors) isn't rendered over a window background (ImGuiCol_WindowBg color) anymore. - If your TitleBg/TitleBgActive alpha was 1.0f or you are using the default theme it will not affect you. - If your TitleBg/TitleBgActive alpha was <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. - This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color. - ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) - { - float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)), k = title_bg_col.w / new_a; - return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); - } + If your TitleBg/TitleBgActive alpha was 1.0f or you are using the default theme it will not affect you, otherwise if <1.0f you need to tweak your custom theme to readjust for the fact that we don't draw a WindowBg background behind the title bar. + This helper function will convert an old TitleBg/TitleBgActive color into a new one with the same visual output, given the OLD color and the OLD WindowBg color: + ImVec4 ConvertTitleBgCol(const ImVec4& win_bg_col, const ImVec4& title_bg_col) { float new_a = 1.0f - ((1.0f - win_bg_col.w) * (1.0f - title_bg_col.w)), k = title_bg_col.w / new_a; return ImVec4((win_bg_col.x * win_bg_col.w + title_bg_col.x) * k, (win_bg_col.y * win_bg_col.w + title_bg_col.y) * k, (win_bg_col.z * win_bg_col.w + title_bg_col.z) * k, new_a); } If this is confusing, pick the RGB value from title bar from an old screenshot and apply this as TitleBg/TitleBgActive. Or you may just create TitleBgActive from a tweaked TitleBg color. - 2016/05/07 (1.49) - removed confusing set of GetInternalState(), GetInternalStateSize(), SetInternalState() functions. Now using CreateContext(), DestroyContext(), GetCurrentContext(), SetCurrentContext(). - 2016/05/02 (1.49) - renamed SetNextTreeNodeOpened() to SetNextTreeNodeOpen(), no redirection. - 2016/05/01 (1.49) - obsoleted old signature of CollapsingHeader(const char* label, const char* str_id = NULL, bool display_frame = true, bool default_open = false) as extra parameters were badly designed and rarely used. You can replace the "default_open = true" flag in new API with CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen). - 2016/04/26 (1.49) - changed ImDrawList::PushClipRect(ImVec4 rect) to ImDrawList::PushClipRect(Imvec2 min,ImVec2 max,bool intersect_with_current_clip_rect=false). Note that higher-level ImGui::PushClipRect() is preferable because it will clip at logic/widget level, whereas ImDrawList::PushClipRect() only affect your renderer. - - 2016/04/03 (1.48) - removed style.WindowFillAlphaDefault setting which was redundant. Bake default BG alpha inside style.Colors[ImGuiCol_WindowBg] and all other Bg color values. (ref github issue #337). + - 2016/04/03 (1.48) - removed style.WindowFillAlphaDefault setting which was redundant. Bake default BG alpha inside style.Colors[ImGuiCol_WindowBg] and all other Bg color values. (ref GitHub issue #337). - 2016/04/03 (1.48) - renamed ImGuiCol_TooltipBg to ImGuiCol_PopupBg, used by popups/menus and tooltips. popups/menus were previously using ImGuiCol_WindowBg. (ref github issue #337) - 2016/03/21 (1.48) - renamed GetWindowFont() to GetFont(), GetWindowFontSize() to GetFontSize(). Kept inline redirection function (will obsolete). - 2016/03/02 (1.48) - InputText() completion/history/always callbacks: if you modify the text buffer manually (without using DeleteChars()/InsertChars() helper) you need to maintain the BufTextLen field. added an assert. @@ -512,7 +929,7 @@ CODE you need to render your textured triangles with bilinear filtering to benefit from sub-pixel positioning of text. - 2015/07/08 (1.43) - switched rendering data to use indexed rendering. this is saving a fair amount of CPU/GPU and enables us to get anti-aliasing for a marginal cost. this necessary change will break your rendering function! the fix should be very easy. sorry for that :( - - if you are using a vanilla copy of one of the imgui_impl_XXXX.cpp provided in the example, you just need to update your copy and you can ignore the rest. + - if you are using a vanilla copy of one of the imgui_impl_XXX.cpp provided in the example, you just need to update your copy and you can ignore the rest. - the signature of the io.RenderDrawListsFn handler has changed! old: ImGui_XXXX_RenderDrawLists(ImDrawList** const cmd_lists, int cmd_lists_count) new: ImGui_XXXX_RenderDrawLists(ImDrawData* draw_data). @@ -526,7 +943,7 @@ CODE - 2015/07/02 (1.42) - renamed SetScrollPosHere() to SetScrollFromCursorPos(). Kept inline redirection function (will obsolete). - 2015/07/02 (1.42) - renamed GetScrollPosY() to GetScrollY(). Necessary to reduce confusion along with other scrolling functions, because positions (e.g. cursor position) are not equivalent to scrolling amount. - 2015/06/14 (1.41) - changed ImageButton() default bg_col parameter from (0,0,0,1) (black) to (0,0,0,0) (transparent) - makes a difference when texture have transparence - - 2015/06/14 (1.41) - changed Selectable() API from (label, selected, size) to (label, selected, flags, size). Size override should have been rarely be used. Sorry! + - 2015/06/14 (1.41) - changed Selectable() API from (label, selected, size) to (label, selected, flags, size). Size override should have been rarely used. Sorry! - 2015/05/31 (1.40) - renamed GetWindowCollapsed() to IsWindowCollapsed() for consistency. Kept inline redirection function (will obsolete). - 2015/05/31 (1.40) - renamed IsRectClipped() to IsRectVisible() for consistency. Note that return value is opposite! Kept inline redirection function (will obsolete). - 2015/05/27 (1.40) - removed the third 'repeat_if_held' parameter from Button() - sorry! it was rarely used and inconsistent. Use PushButtonRepeat(true) / PopButtonRepeat() to enable repeat on desired buttons. @@ -548,14 +965,13 @@ CODE - 2015/02/01 (1.31) - removed IO.MemReallocFn (unused) - 2015/01/19 (1.30) - renamed ImGuiStorage::GetIntPtr()/GetFloatPtr() to GetIntRef()/GetIntRef() because Ptr was conflicting with actual pointer storage functions. - 2015/01/11 (1.30) - big font/image API change! now loads TTF file. allow for multiple fonts. no need for a PNG loader. - (1.30) - removed GetDefaultFontData(). uses io.Fonts->GetTextureData*() API to retrieve uncompressed pixels. - font init: { const void* png_data; unsigned int png_size; ImGui::GetDefaultFontData(NULL, NULL, &png_data, &png_size); <..Upload texture to GPU..>; } - became: { unsigned char* pixels; int width, height; io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); <..Upload texture to GPU>; io.Fonts->TexId = YourTextureIdentifier; } - you now have more flexibility to load multiple TTF fonts and manage the texture buffer for internal needs. - it is now recommended that you sample the font texture with bilinear interpolation. - (1.30) - added texture identifier in ImDrawCmd passed to your render function (we can now render images). make sure to set io.Fonts->TexID. - (1.30) - removed IO.PixelCenterOffset (unnecessary, can be handled in user projection matrix) - (1.30) - removed ImGui::IsItemFocused() in favor of ImGui::IsItemActive() which handles all widgets + - 2015/01/11 (1.30) - removed GetDefaultFontData(). uses io.Fonts->GetTextureData*() API to retrieve uncompressed pixels. + - old: const void* png_data; unsigned int png_size; ImGui::GetDefaultFontData(NULL, NULL, &png_data, &png_size); [..Upload texture to GPU..]; + - new: unsigned char* pixels; int width, height; io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); [..Upload texture to GPU..]; io.Fonts->SetTexID(YourTexIdentifier); + you now have more flexibility to load multiple TTF fonts and manage the texture buffer for internal needs. It is now recommended that you sample the font texture with bilinear interpolation. + - 2015/01/11 (1.30) - added texture identifier in ImDrawCmd passed to your render function (we can now render images). make sure to call io.Fonts->SetTexID() + - 2015/01/11 (1.30) - removed IO.PixelCenterOffset (unnecessary, can be handled in user projection matrix) + - 2015/01/11 (1.30) - removed ImGui::IsItemFocused() in favor of ImGui::IsItemActive() which handles all widgets - 2014/12/10 (1.18) - removed SetNewWindowDefaultPos() in favor of new generic API SetNextWindowPos(pos, ImGuiSetCondition_FirstUseEver) - 2014/11/28 (1.17) - moved IO.Font*** options to inside the IO.Font-> structure (FontYOffset, FontTexUvForWhite, FontBaseScale, FontFallbackGlyph) - 2014/11/26 (1.17) - reworked syntax of IMGUI_ONCE_UPON_A_FRAME helper macro to increase compiler compatibility @@ -569,518 +985,319 @@ CODE - 2014/08/28 (1.09) - changed the behavior of IO.PixelCenterOffset following various rendering fixes - FREQUENTLY ASKED QUESTIONS (FAQ), TIPS - ====================================== + FREQUENTLY ASKED QUESTIONS (FAQ) + ================================ + + Read all answers online: + https://www.dearimgui.com/faq or https://github.com/ocornut/imgui/blob/master/docs/FAQ.md (same url) + Read all answers locally (with a text editor or ideally a Markdown viewer): + docs/FAQ.md + Some answers are copied down here to facilitate searching in code. + + Q&A: Basics + =========== Q: Where is the documentation? - A: This library is poorly documented at the moment and expects of the user to be acquainted with C/C++. - - Run the examples/ and explore them. + A: This library is poorly documented at the moment and expects the user to be acquainted with C/C++. + - Run the examples/ applications and explore them. + - Read Getting Started (https://github.com/ocornut/imgui/wiki/Getting-Started) guide. - See demo code in imgui_demo.cpp and particularly the ImGui::ShowDemoWindow() function. - The demo covers most features of Dear ImGui, so you can read the code and see its output. - See documentation and comments at the top of imgui.cpp + effectively imgui.h. - - Dozens of standalone example applications using e.g. OpenGL/DirectX are provided in the examples/ - folder to explain how to integrate Dear ImGui with your own engine/application. + - 20+ standalone example applications using e.g. OpenGL/DirectX are provided in the + examples/ folder to explain how to integrate Dear ImGui with your own engine/application. + - The Wiki (https://github.com/ocornut/imgui/wiki) has many resources and links. + - The Glossary (https://github.com/ocornut/imgui/wiki/Glossary) page also may be useful. - Your programming IDE is your friend, find the type or function declaration to find comments - associated to it. + associated with it. + Q: What is this library called? Q: Which version should I get? - A: I occasionally tag Releases (https://github.com/ocornut/imgui/releases) but it is generally safe - and recommended to sync to master/latest. The library is fairly stable and regressions tend to be - fixed fast when reported. You may also peak at the 'docking' branch which includes: - - Docking/Merging features (https://github.com/ocornut/imgui/issues/2109) - - Multi-viewport features (https://github.com/ocornut/imgui/issues/1542) - Many projects are using this branch and it is kept in sync with master regularly. + >> This library is called "Dear ImGui", please don't call it "ImGui" :) + >> See https://www.dearimgui.com/faq for details. + + Q&A: Integration + ================ + + Q: How to get started? + A: Read https://github.com/ocornut/imgui/wiki/Getting-Started. Read 'PROGRAMMER GUIDE' above. Read examples/README.txt. + + Q: How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application? + A: You should read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags! + >> See https://www.dearimgui.com/faq for a fully detailed answer. You really want to read this. + + Q. How can I enable keyboard or gamepad controls? + Q: How can I use this on a machine without mouse, keyboard or screen? (input share, remote display) + Q: I integrated Dear ImGui in my engine and little squares are showing instead of text... + Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around... + Q: I integrated Dear ImGui in my engine and some elements are displaying outside their expected windows boundaries... + >> See https://www.dearimgui.com/faq + + Q&A: Usage + ---------- + + Q: About the ID Stack system.. + - Why is my widget not reacting when I click on it? + - How can I have widgets with an empty label? + - How can I have multiple widgets with the same label? + - How can I have multiple windows with the same label? + Q: How can I display an image? What is ImTextureID, how does it work? + Q: How can I use my own math types instead of ImVec2? + Q: How can I interact with standard C++ types (such as std::string and std::vector)? + Q: How can I display custom shapes? (using low-level ImDrawList API) + >> See https://www.dearimgui.com/faq + + Q&A: Fonts, Text + ================ + + Q: How should I handle DPI in my application? + Q: How can I load a different font than the default? + Q: How can I easily use icons in my application? + Q: How can I load multiple fonts? + Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? + >> See https://www.dearimgui.com/faq and https://github.com/ocornut/imgui/blob/master/docs/FONTS.md + + Q&A: Concerns + ============= Q: Who uses Dear ImGui? - A: See "Quotes" (https://github.com/ocornut/imgui/wiki/Quotes) and - "Software using Dear ImGui" (https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui) Wiki pages - for a list of games/software which are publicly known to use dear imgui. Please add yours if you can! + Q: Can you create elaborate/serious tools with Dear ImGui? + Q: Can you reskin the look of Dear ImGui? + Q: Why using C++ (as opposed to C)? + >> See https://www.dearimgui.com/faq - Q: Why the odd dual naming, "Dear ImGui" vs "ImGui"? - A: The library started its life as "ImGui" due to the fact that I didn't give it a proper name when - when I released 1.0, and had no particular expectation that it would take off. However, the term IMGUI - (immediate-mode graphical user interface) was coined before and is being used in variety of other - situations (e.g. Unity uses it own implementation of the IMGUI paradigm). - To reduce the ambiguity without affecting existing code bases, I have decided on an alternate, - longer name "Dear ImGui" that people can use to refer to this specific library. - Please try to refer to this library as "Dear ImGui". - - Q: How can I tell whether to dispatch mouse/keyboard to Dear ImGui or to my application? - A: You can read the 'io.WantCaptureMouse', 'io.WantCaptureKeyboard' and 'io.WantTextInput' flags from the ImGuiIO structure (e.g. if (ImGui::GetIO().WantCaptureMouse) { ... } ) - - When 'io.WantCaptureMouse' is set, imgui wants to use your mouse state, and you may want to discard/hide the inputs from the rest of your application. - - When 'io.WantCaptureKeyboard' is set, imgui wants to use your keyboard state, and you may want to discard/hide the inputs from the rest of your application. - - When 'io.WantTextInput' is set to may want to notify your OS to popup an on-screen keyboard, if available (e.g. on a mobile phone, or console OS). - Note: you should always pass your mouse/keyboard inputs to imgui, even when the io.WantCaptureXXX flag are set false. - This is because imgui needs to detect that you clicked in the void to unfocus its own windows. - Note: The 'io.WantCaptureMouse' is more accurate that any attempt to "check if the mouse is hovering a window" (don't do that!). - It handle mouse dragging correctly (both dragging that started over your application or over an imgui window) and handle e.g. modal windows blocking inputs. - Those flags are updated by ImGui::NewFrame(). Preferably read the flags after calling NewFrame() if you can afford it, but reading them before is also - perfectly fine, as the bool toggle fairly rarely. If you have on a touch device, you might find use for an early call to UpdateHoveredWindowAndCaptureFlags(). - Note: Text input widget releases focus on "Return KeyDown", so the subsequent "Return KeyUp" event that your application receive will typically - have 'io.WantCaptureKeyboard=false'. Depending on your application logic it may or not be inconvenient. You might want to track which key-downs - were targeted for Dear ImGui, e.g. with an array of bool, and filter out the corresponding key-ups.) - - Q: How can I display an image? What is ImTextureID, how does it works? - A: Short explanation: - - Please read Wiki entry for examples: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples - - You may use functions such as ImGui::Image(), ImGui::ImageButton() or lower-level ImDrawList::AddImage() to emit draw calls that will use your own textures. - - Actual textures are identified in a way that is up to the user/engine. Those identifiers are stored and passed as ImTextureID (void*) value. - - Loading image files from the disk and turning them into a texture is not within the scope of Dear ImGui (for a good reason). - Please read documentations or tutorials on your graphics API to understand how to display textures on the screen before moving onward. - - Long explanation: - - Dear ImGui's job is to create "meshes", defined in a renderer-agnostic format made of draw commands and vertices. - At the end of the frame those meshes (ImDrawList) will be displayed by your rendering function. They are made up of textured polygons and the code - to render them is generally fairly short (a few dozen lines). In the examples/ folder we provide functions for popular graphics API (OpenGL, DirectX, etc.). - - Each rendering function decides on a data type to represent "textures". The concept of what is a "texture" is entirely tied to your underlying engine/graphics API. - We carry the information to identify a "texture" in the ImTextureID type. - ImTextureID is nothing more that a void*, aka 4/8 bytes worth of data: just enough to store 1 pointer or 1 integer of your choice. - Dear ImGui doesn't know or understand what you are storing in ImTextureID, it merely pass ImTextureID values until they reach your rendering function. - - In the examples/ bindings, for each graphics API binding we decided on a type that is likely to be a good representation for specifying - an image from the end-user perspective. This is what the _examples_ rendering functions are using: - - OpenGL: ImTextureID = GLuint (see ImGui_ImplOpenGL3_RenderDrawData() function in imgui_impl_opengl3.cpp) - DirectX9: ImTextureID = LPDIRECT3DTEXTURE9 (see ImGui_ImplDX9_RenderDrawData() function in imgui_impl_dx9.cpp) - DirectX11: ImTextureID = ID3D11ShaderResourceView* (see ImGui_ImplDX11_RenderDrawData() function in imgui_impl_dx11.cpp) - DirectX12: ImTextureID = D3D12_GPU_DESCRIPTOR_HANDLE (see ImGui_ImplDX12_RenderDrawData() function in imgui_impl_dx12.cpp) - - For example, in the OpenGL example binding we store raw OpenGL texture identifier (GLuint) inside ImTextureID. - Whereas in the DirectX11 example binding we store a pointer to ID3D11ShaderResourceView inside ImTextureID, which is a higher-level structure - tying together both the texture and information about its format and how to read it. - - If you have a custom engine built over e.g. OpenGL, instead of passing GLuint around you may decide to use a high-level data type to carry information about - the texture as well as how to display it (shaders, etc.). The decision of what to use as ImTextureID can always be made better knowing how your codebase - is designed. If your engine has high-level data types for "textures" and "material" then you may want to use them. - If you are starting with OpenGL or DirectX or Vulkan and haven't built much of a rendering engine over them, keeping the default ImTextureID - representation suggested by the example bindings is probably the best choice. - (Advanced users may also decide to keep a low-level type in ImTextureID, and use ImDrawList callback and pass information to their renderer) - - User code may do: - - // Cast our texture type to ImTextureID / void* - MyTexture* texture = g_CoffeeTableTexture; - ImGui::Image((void*)texture, ImVec2(texture->Width, texture->Height)); - - The renderer function called after ImGui::Render() will receive that same value that the user code passed: - - // Cast ImTextureID / void* stored in the draw command as our texture type - MyTexture* texture = (MyTexture*)pcmd->TextureId; - MyEngineBindTexture2D(texture); - - Once you understand this design you will understand that loading image files and turning them into displayable textures is not within the scope of Dear ImGui. - This is by design and is actually a good thing, because it means your code has full control over your data types and how you display them. - If you want to display an image file (e.g. PNG file) into the screen, please refer to documentation and tutorials for the graphics API you are using. - - Refer to the Wiki to find simplified examples for loading textures with OpenGL, DirectX9 and DirectX11: - - https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples - - C/C++ tip: a void* is pointer-sized storage. You may safely store any pointer or integer into it by casting your value to ImTextureID / void*, and vice-versa. - Because both end-points (user code and rendering function) are under your control, you know exactly what is stored inside the ImTextureID / void*. - Examples: - - GLuint my_tex = XXX; - void* my_void_ptr; - my_void_ptr = (void*)(intptr_t)my_tex; // cast a GLuint into a void* (we don't take its address! we literally store the value inside the pointer) - my_tex = (GLuint)(intptr_t)my_void_ptr; // cast a void* into a GLuint - - ID3D11ShaderResourceView* my_dx11_srv = XXX; - void* my_void_ptr; - my_void_ptr = (void*)my_dx11_srv; // cast a ID3D11ShaderResourceView* into an opaque void* - my_dx11_srv = (ID3D11ShaderResourceView*)my_void_ptr; // cast a void* into a ID3D11ShaderResourceView* - - Finally, you may call ImGui::ShowMetricsWindow() to explore/visualize/understand how the ImDrawList are generated. - - Q: Why are multiple widgets reacting when I interact with a single one? - Q: How can I have multiple widgets with the same label or with an empty label? - A: A primer on labels and the ID Stack... - - Dear ImGui internally need to uniquely identify UI elements. - Elements that are typically not clickable (such as calls to the Text functions) don't need an ID. - Interactive widgets (such as calls to Button buttons) need a unique ID. - Unique ID are used internally to track active widgets and occasionally associate state to widgets. - Unique ID are implicitly built from the hash of multiple elements that identify the "path" to the UI element. - - - Unique ID are often derived from a string label: - - Button("OK"); // Label = "OK", ID = hash of (..., "OK") - Button("Cancel"); // Label = "Cancel", ID = hash of (..., "Cancel") - - - ID are uniquely scoped within windows, tree nodes, etc. which all pushes to the ID stack. Having - two buttons labeled "OK" in different windows or different tree locations is fine. - We used "..." above to signify whatever was already pushed to the ID stack previously: - - Begin("MyWindow"); - Button("OK"); // Label = "OK", ID = hash of ("MyWindow", "OK") - End(); - Begin("MyOtherWindow"); - Button("OK"); // Label = "OK", ID = hash of ("MyOtherWindow", "OK") - End(); - - - If you have a same ID twice in the same location, you'll have a conflict: - - Button("OK"); - Button("OK"); // ID collision! Interacting with either button will trigger the first one. - - Fear not! this is easy to solve and there are many ways to solve it! - - - Solving ID conflict in a simple/local context: - When passing a label you can optionally specify extra ID information within string itself. - Use "##" to pass a complement to the ID that won't be visible to the end-user. - This helps solving the simple collision cases when you know e.g. at compilation time which items - are going to be created: - - Begin("MyWindow"); - Button("Play"); // Label = "Play", ID = hash of ("MyWindow", "Play") - Button("Play##foo1"); // Label = "Play", ID = hash of ("MyWindow", "Play##foo1") // Different from above - Button("Play##foo2"); // Label = "Play", ID = hash of ("MyWindow", "Play##foo2") // Different from above - End(); - - - If you want to completely hide the label, but still need an ID: - - Checkbox("##On", &b); // Label = "", ID = hash of (..., "##On") // No visible label, just a checkbox! - - - Occasionally/rarely you might want change a label while preserving a constant ID. This allows - you to animate labels. For example you may want to include varying information in a window title bar, - but windows are uniquely identified by their ID. Use "###" to pass a label that isn't part of ID: - - Button("Hello###ID"); // Label = "Hello", ID = hash of (..., "###ID") - Button("World###ID"); // Label = "World", ID = hash of (..., "###ID") // Same as above, even though the label looks different - - sprintf(buf, "My game (%f FPS)###MyGame", fps); - Begin(buf); // Variable title, ID = hash of "MyGame" - - - Solving ID conflict in a more general manner: - Use PushID() / PopID() to create scopes and manipulate the ID stack, as to avoid ID conflicts - within the same window. This is the most convenient way of distinguishing ID when iterating and - creating many UI elements programmatically. - You can push a pointer, a string or an integer value into the ID stack. - Remember that ID are formed from the concatenation of _everything_ pushed into the ID stack. - At each level of the stack we store the seed used for items at this level of the ID stack. - - Begin("Window"); - for (int i = 0; i < 100; i++) - { - PushID(i); // Push i to the id tack - Button("Click"); // Label = "Click", ID = hash of ("Window", i, "Click") - PopID(); - } - for (int i = 0; i < 100; i++) - { - MyObject* obj = Objects[i]; - PushID(obj); - Button("Click"); // Label = "Click", ID = hash of ("Window", obj pointer, "Click") - PopID(); - } - for (int i = 0; i < 100; i++) - { - MyObject* obj = Objects[i]; - PushID(obj->Name); - Button("Click"); // Label = "Click", ID = hash of ("Window", obj->Name, "Click") - PopID(); - } - End(); - - - You can stack multiple prefixes into the ID stack: - - Button("Click"); // Label = "Click", ID = hash of (..., "Click") - PushID("node"); - Button("Click"); // Label = "Click", ID = hash of (..., "node", "Click") - PushID(my_ptr); - Button("Click"); // Label = "Click", ID = hash of (..., "node", my_ptr, "Click") - PopID(); - PopID(); - - - Tree nodes implicitly creates a scope for you by calling PushID(). - - Button("Click"); // Label = "Click", ID = hash of (..., "Click") - if (TreeNode("node")) // <-- this function call will do a PushID() for you (unless instructed not to, with a special flag) - { - Button("Click"); // Label = "Click", ID = hash of (..., "node", "Click") - TreePop(); - } - - - When working with trees, ID are used to preserve the open/close state of each tree node. - Depending on your use cases you may want to use strings, indices or pointers as ID. - e.g. when following a single pointer that may change over time, using a static string as ID - will preserve your node open/closed state when the targeted object change. - e.g. when displaying a list of objects, using indices or pointers as ID will preserve the - node open/closed state differently. See what makes more sense in your situation! - - Q: How can I use my own math types instead of ImVec2/ImVec4? - A: You can edit imconfig.h and setup the IM_VEC2_CLASS_EXTRA/IM_VEC4_CLASS_EXTRA macros to add implicit type conversions. - This way you'll be able to use your own types everywhere, e.g. passing glm::vec2 to ImGui functions instead of ImVec2. - - Q: How can I load a different font than the default? - A: Use the font atlas to load the TTF/OTF file you want: - ImGuiIO& io = ImGui::GetIO(); - io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); - io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() - Default is ProggyClean.ttf, monospace, rendered at size 13, embedded in dear imgui's source code. - (Tip: monospace fonts are convenient because they allow to facilitate horizontal alignment directly at the string level.) - (Read the 'misc/fonts/README.txt' file for more details about font loading.) - - New programmers: remember that in C/C++ and most programming languages if you want to use a - backslash \ within a string literal, you need to write it double backslash "\\": - io.Fonts->AddFontFromFileTTF("MyDataFolder\MyFontFile.ttf", size_in_pixels); // WRONG (you are escape the M here!) - io.Fonts->AddFontFromFileTTF("MyDataFolder\\MyFontFile.ttf", size_in_pixels); // CORRECT - io.Fonts->AddFontFromFileTTF("MyDataFolder/MyFontFile.ttf", size_in_pixels); // ALSO CORRECT - - Q: How can I easily use icons in my application? - A: The most convenient and practical way is to merge an icon font such as FontAwesome inside you - main font. Then you can refer to icons within your strings. - You may want to see ImFontConfig::GlyphMinAdvanceX to make your icon look monospace to facilitate alignment. - (Read the 'misc/fonts/README.txt' file for more details about icons font loading.) - With some extra effort, you may use colorful icon by registering custom rectangle space inside the font atlas, - and copying your own graphics data into it. See misc/fonts/README.txt about using the AddCustomRectFontGlyph API. - - Q: How can I load multiple fonts? - A: Use the font atlas to pack them into a single texture: - (Read the 'misc/fonts/README.txt' file and the code in ImFontAtlas for more details.) - - ImGuiIO& io = ImGui::GetIO(); - ImFont* font0 = io.Fonts->AddFontDefault(); - ImFont* font1 = io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels); - ImFont* font2 = io.Fonts->AddFontFromFileTTF("myfontfile2.ttf", size_in_pixels); - io.Fonts->GetTexDataAsRGBA32() or GetTexDataAsAlpha8() - // the first loaded font gets used by default - // use ImGui::PushFont()/ImGui::PopFont() to change the font at runtime - - // Options - ImFontConfig config; - config.OversampleH = 2; - config.OversampleV = 1; - config.GlyphOffset.y -= 1.0f; // Move everything by 1 pixels up - config.GlyphExtraSpacing.x = 1.0f; // Increase spacing between characters - io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_pixels, &config); - - // Combine multiple fonts into one (e.g. for icon fonts) - static ImWchar ranges[] = { 0xf000, 0xf3ff, 0 }; - ImFontConfig config; - config.MergeMode = true; - io.Fonts->AddFontDefault(); - io.Fonts->AddFontFromFileTTF("fontawesome-webfont.ttf", 16.0f, &config, ranges); // Merge icon font - io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_pixels, NULL, &config, io.Fonts->GetGlyphRangesJapanese()); // Merge japanese glyphs - - Q: How can I display and input non-Latin characters such as Chinese, Japanese, Korean, Cyrillic? - A: When loading a font, pass custom Unicode ranges to specify the glyphs to load. - - // Add default Japanese ranges - io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, io.Fonts->GetGlyphRangesJapanese()); - - // Or create your own custom ranges (e.g. for a game you can feed your entire game script and only build the characters the game need) - ImVector ranges; - ImFontGlyphRangesBuilder builder; - builder.AddText("Hello world"); // Add a string (here "Hello world" contains 7 unique characters) - builder.AddChar(0x7262); // Add a specific character - builder.AddRanges(io.Fonts->GetGlyphRangesJapanese()); // Add one of the default ranges - builder.BuildRanges(&ranges); // Build the final result (ordered ranges with all the unique characters submitted) - io.Fonts->AddFontFromFileTTF("myfontfile.ttf", size_in_pixels, NULL, ranges.Data); - - All your strings needs to use UTF-8 encoding. In C++11 you can encode a string literal in UTF-8 - by using the u8"hello" syntax. Specifying literal in your source code using a local code page - (such as CP-923 for Japanese or CP-1251 for Cyrillic) will NOT work! - Otherwise you can convert yourself to UTF-8 or load text data from file already saved as UTF-8. - - Text input: it is up to your application to pass the right character code by calling io.AddInputCharacter(). - The applications in examples/ are doing that. - Windows: you can use the WM_CHAR or WM_UNICHAR or WM_IME_CHAR message (depending if your app is built using Unicode or MultiByte mode). - You may also use MultiByteToWideChar() or ToUnicode() to retrieve Unicode codepoints from MultiByte characters or keyboard state. - Windows: if your language is relying on an Input Method Editor (IME), you copy the HWND of your window to io.ImeWindowHandle in order for - the default implementation of io.ImeSetInputScreenPosFn() to set your Microsoft IME position correctly. - - Q: How can I interact with standard C++ types (such as std::string and std::vector)? - A: - Being highly portable (bindings for several languages, frameworks, programming style, obscure or older platforms/compilers), - and aiming for compatibility & performance suitable for every modern real-time game engines, dear imgui does not use - any of std C++ types. We use raw types (e.g. char* instead of std::string) because they adapt to more use cases. - - To use ImGui::InputText() with a std::string or any resizable string class, see misc/cpp/imgui_stdlib.h. - - To use combo boxes and list boxes with std::vector or any other data structure: the BeginCombo()/EndCombo() API - lets you iterate and submit items yourself, so does the ListBoxHeader()/ListBoxFooter() API. - Prefer using them over the old and awkward Combo()/ListBox() api. - - Generally for most high-level types you should be able to access the underlying data type. - You may write your own one-liner wrappers to facilitate user code (tip: add new functions in ImGui:: namespace from your code). - - Dear ImGui applications often need to make intensive use of strings. It is expected that many of the strings you will pass - to the API are raw literals (free in C/C++) or allocated in a manner that won't incur a large cost on your application. - Please bear in mind that using std::string on applications with large amount of UI may incur unsatisfactory performances. - Modern implementations of std::string often include small-string optimization (which is often a local buffer) but those - are not configurable and not the same across implementations. - - If you are finding your UI traversal cost to be too large, make sure your string usage is not leading to excessive amount - of heap allocations. Consider using literals, statically sized buffers and your own helper functions. A common pattern - is that you will need to build lots of strings on the fly, and their maximum length can be easily be scoped ahead. - One possible implementation of a helper to facilitate printf-style building of strings: https://github.com/ocornut/Str - This is a small helper where you can instance strings with configurable local buffers length. Many game engines will - provide similar or better string helpers. - - Q: How can I use the drawing facilities without an ImGui window? (using ImDrawList API) - A: - You can create a dummy window. Call Begin() with the NoBackground | NoDecoration | NoSavedSettings | NoInputs flags. - (The ImGuiWindowFlags_NoDecoration flag itself is a shortcut for NoTitleBar | NoResize | NoScrollbar | NoCollapse) - Then you can retrieve the ImDrawList* via GetWindowDrawList() and draw to it in any way you like. - - You can call ImGui::GetBackgroundDrawList() or ImGui::GetForegroundDrawList() and use those draw list to display - contents behind or over every other imgui windows (one bg/fg drawlist per viewport). - - You can create your own ImDrawList instance. You'll need to initialize them ImGui::GetDrawListSharedData(), or create - your own ImDrawListSharedData, and then call your rendered code with your own ImDrawList or ImDrawData data. - - Q: How can I use this without a mouse, without a keyboard or without a screen? (gamepad, input share, remote display) - A: - You can control Dear ImGui with a gamepad. Read about navigation in "Using gamepad/keyboard navigation controls". - (short version: map gamepad inputs into the io.NavInputs[] array + set io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad) - - You can share your computer mouse seamlessly with your console/tablet/phone using Synergy (https://symless.com/synergy) - This is the preferred solution for developer productivity. - In particular, the "micro-synergy-client" repository (https://github.com/symless/micro-synergy-client) has simple - and portable source code (uSynergy.c/.h) for a small embeddable client that you can use on any platform to connect - to your host computer, based on the Synergy 1.x protocol. Make sure you download the Synergy 1 server on your computer. - Console SDK also sometimes provide equivalent tooling or wrapper for Synergy-like protocols. - - You may also use a third party solution such as Remote ImGui (https://github.com/JordiRos/remoteimgui) which sends - the vertices to render over the local network, allowing you to use Dear ImGui even on a screen-less machine. - - For touch inputs, you can increase the hit box of widgets (via the style.TouchPadding setting) to accommodate - for the lack of precision of touch inputs, but it is recommended you use a mouse or gamepad to allow optimizing - for screen real-estate and precision. - - Q: I integrated Dear ImGui in my engine and the text or lines are blurry.. - A: In your Render function, try translating your projection matrix by (0.5f,0.5f) or (0.375f,0.375f). - Also make sure your orthographic projection matrix and io.DisplaySize matches your actual framebuffer dimension. - - Q: I integrated Dear ImGui in my engine and some elements are clipping or disappearing when I move windows around.. - A: You are probably mishandling the clipping rectangles in your render function. - Rectangles provided by ImGui are defined as (x1=left,y1=top,x2=right,y2=bottom) and NOT as (x1,y1,width,height). + Q&A: Community + ============== Q: How can I help? - A: - If you are experienced with Dear ImGui and C++, look at the github issues, look at the Wiki, read docs/TODO.txt - and see how you want to help and can help! - - Businesses: convince your company to fund development via support contracts/sponsoring! This is among the most useful thing you can do for dear imgui. - - Individuals: you can also become a Patron (http://www.patreon.com/imgui) or donate on PayPal! See README. - - Disclose your usage of dear imgui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. - You may post screenshot or links in the gallery threads (github.com/ocornut/imgui/issues/1902). Visuals are ideal as they inspire other programmers. - But even without visuals, disclosing your use of dear imgui help the library grow credibility, and help other teams and programmers with taking decisions. - - If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues (on github or privately). - - - tip: you can call Begin() multiple times with the same name during the same frame, it will keep appending to the same window. - this is also useful to set yourself in the context of another window (to get/set other settings) - - tip: you can create widgets without a Begin()/End() block, they will go in an implicit window called "Debug". - - tip: the ImGuiOnceUponAFrame helper will allow run the block of code only once a frame. You can use it to quickly add custom UI in the middle - of a deep nested inner loop in your code. - - tip: you can call Render() multiple times (e.g for VR renders). - - tip: call and read the ShowDemoWindow() code in imgui_demo.cpp for more example of how to use ImGui! + A: - Businesses: please reach out to "omar AT dearimgui DOT com" if you work in a place using Dear ImGui! + We can discuss ways for your company to fund development via invoiced technical support, maintenance or sponsoring contacts. + This is among the most useful thing you can do for Dear ImGui. With increased funding, we sustain and grow work on this project. + >>> See https://github.com/ocornut/imgui/wiki/Funding + - Businesses: you can also purchase licenses for the Dear ImGui Automation/Test Engine. + - If you are experienced with Dear ImGui and C++, look at the GitHub issues, look at the Wiki, and see how you want to help and can help! + - Disclose your usage of Dear ImGui via a dev blog post, a tweet, a screenshot, a mention somewhere etc. + You may post screenshot or links in the gallery threads. Visuals are ideal as they inspire other programmers. + But even without visuals, disclosing your use of dear imgui helps the library grow credibility, and help other teams and programmers with taking decisions. + - If you have issues or if you need to hack into the library, even if you don't expect any support it is useful that you share your issues (on GitHub or privately). */ +//------------------------------------------------------------------------- +// [SECTION] INCLUDES +//------------------------------------------------------------------------- + #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) #define _CRT_SECURE_NO_WARNINGS #endif -#include "imgui.h" #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_internal.h" -#include // toupper +// System includes #include // vsnprintf, sscanf, printf -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -#include // intptr_t -#else #include // intptr_t + +// [Windows] On non-Visual Studio compilers, we default to IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS unless explicitly enabled +#if defined(_WIN32) && !defined(_MSC_VER) && !defined(IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS #endif -// Debug options -#define IMGUI_DEBUG_NAV_SCORING 0 // Display navigation scoring preview when hovering items. Display last moving direction matches when holding CTRL -#define IMGUI_DEBUG_NAV_RECTS 0 // Display the reference navigation rectangle for each window -#define IMGUI_DEBUG_INI_SETTINGS 0 // Save additional comments in .ini file +// [Windows] OS specific includes (optional) +#if defined(_WIN32) && defined(IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS) && defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) && defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) && defined(IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#define IMGUI_DISABLE_WIN32_FUNCTIONS +#endif +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef __MINGW32__ +#include // _wfopen, OpenClipboard +#else +#include +#endif +#if defined(WINAPI_FAMILY) && ((defined(WINAPI_FAMILY_APP) && WINAPI_FAMILY == WINAPI_FAMILY_APP) || (defined(WINAPI_FAMILY_GAMES) && WINAPI_FAMILY == WINAPI_FAMILY_GAMES)) +// The UWP and GDK Win32 API subsets don't support clipboard nor IME functions +#define IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS +#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif +#endif + +// [Apple] OS specific includes +#if defined(__APPLE__) +#include +#endif // Visual Studio warnings #ifdef _MSC_VER -#pragma warning (disable: 4127) // condition expression is constant -#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to an 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). #endif // Clang/GCC warnings with -Weverything #if defined(__clang__) -#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning : unknown warning group '-Wformat-pedantic *' // not all warnings are known by all clang versions.. so ignoring warnings triggers new warnings on some configuration. great! -#pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. -#pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. -#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning : format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. -#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning : declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. -#pragma clang diagnostic ignored "-Wglobal-constructors" // warning : declaration requires a global destructor // similar to above, not sure what the exact difference is. -#pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // -#pragma clang diagnostic ignored "-Wformat-pedantic" // warning : format specifies type 'void *' but the argument has type 'xxxx *' // unreasonable, would lead to casting every %p arg to void*. probably enabled by -pedantic. -#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning : cast to 'void *' from smaller integer type 'int' -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning : zero as null pointer constant // some standard header variations use #define NULL 0 -#endif -#if __has_warning("-Wdouble-promotion") -#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! #endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat" // warning: format specifies type 'int' but the argument has type 'unsigned int' +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wformat-pedantic" // warning: format specifies type 'void *' but the argument has type 'xxxx *' // unreasonable, would lead to casting every %p arg to void*. probably enabled by -pedantic. +#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning: declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning: declaration requires a global destructor // similar to above, not sure what the exact difference is. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning: cast to 'void *' from smaller integer type 'int' +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label #elif defined(__GNUC__) -// We disable -Wpragmas because GCC doesn't provide an has_warning equivalent and some forks/patches may not following the warning/version association. -#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind -#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used -#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size -#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'void*', but argument 6 has type 'ImGuiWindow*' -#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function -#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value -#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked -#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when assuming that (X - c) > X is always false -#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +// We disable -Wpragmas because GCC doesn't provide a has_warning equivalent and some forks/patches may not follow the warning/version association. +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when assuming that (X - c) > X is always false +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers #endif +// Debug options +#define IMGUI_DEBUG_NAV_SCORING 0 // Display navigation scoring preview when hovering items. Hold CTRL to display for all candidates. CTRL+Arrow to change last direction. +#define IMGUI_DEBUG_NAV_RECTS 0 // Display the reference navigation rectangle for each window + // When using CTRL+TAB (or Gamepad Square+L/R) we delay the visual a little in order to reduce visual noise doing a fast switch. static const float NAV_WINDOWING_HIGHLIGHT_DELAY = 0.20f; // Time before the highlight and screen dimming starts fading in static const float NAV_WINDOWING_LIST_APPEAR_DELAY = 0.15f; // Time before the window list starts to appear - -// Window resizing from edges (when io.ConfigWindowsResizeFromEdges = true and ImGuiBackendFlags_HasMouseCursors is set in io.BackendFlags by back-end) -static const float WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS = 4.0f; // Extend outside and inside windows. Affect FindHoveredWindow(). +static const float NAV_ACTIVATE_HIGHLIGHT_TIMER = 0.10f; // Time to highlight an item activated by a shortcut. static const float WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER = 0.04f; // Reduce visual noise by only highlighting the border after a certain time. -static const float WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER = 2.00f; // Lock scrolled window (so it doesn't pick child windows that are scrolling through) for a certaint time, unless mouse moved. +static const float WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER = 0.70f; // Lock scrolled window (so it doesn't pick child windows that are scrolling through) for a certain time, unless mouse moved. + +// Tooltip offset +static const ImVec2 TOOLTIP_DEFAULT_OFFSET_MOUSE = ImVec2(16, 10); // Multiplied by g.Style.MouseCursorScale +static const ImVec2 TOOLTIP_DEFAULT_OFFSET_TOUCH = ImVec2(0, -20); // Multiplied by g.Style.MouseCursorScale +static const ImVec2 TOOLTIP_DEFAULT_PIVOT_TOUCH = ImVec2(0.5f, 1.0f); // Multiplied by g.Style.MouseCursorScale + +// Docking +static const float DOCKING_TRANSPARENT_PAYLOAD_ALPHA = 0.50f; // For use with io.ConfigDockingTransparentPayload. Apply to Viewport _or_ WindowBg in host viewport. //------------------------------------------------------------------------- // [SECTION] FORWARD DECLARATIONS //------------------------------------------------------------------------- static void SetCurrentWindow(ImGuiWindow* window); -static void FindHoveredWindow(); -static ImGuiWindow* CreateNewWindow(const char* name, ImVec2 size, ImGuiWindowFlags flags); -static void CheckStacksSize(ImGuiWindow* window, bool write); -static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window, bool snap_on_edges); +static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags); +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window); -static void AddDrawListToDrawData(ImVector* out_list, ImDrawList* draw_list); static void AddWindowToSortBuffer(ImVector* out_sorted_windows, ImGuiWindow* window); -static ImRect GetViewportRect(); - // Settings -static void* SettingsHandlerWindow_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); -static void SettingsHandlerWindow_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); -static void SettingsHandlerWindow_WriteAll(ImGuiContext* imgui_ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf); +static void WindowSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler*); +static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); +static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); +static void WindowSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler*); +static void WindowSettingsHandler_WriteAll(ImGuiContext*, ImGuiSettingsHandler*, ImGuiTextBuffer* buf); -// Platform Dependents default implementation for IO functions -static const char* GetClipboardTextFn_DefaultImpl(void* user_data); -static void SetClipboardTextFn_DefaultImpl(void* user_data, const char* text); -static void ImeSetInputScreenPosFn_DefaultImpl(int x, int y); +// Platform Dependents default implementation for ImGuiPlatformIO functions +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx); +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext* ctx, const char* text); +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext* ctx, const char* path); namespace ImGui { -static bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags flags); +// Item +static void ItemHandleShortcut(ImGuiID id); + +// Window Focus +static int FindWindowFocusIndex(ImGuiWindow* window); +static void UpdateWindowInFocusOrderList(ImGuiWindow* window, bool just_created, ImGuiWindowFlags new_flags); // Navigation static void NavUpdate(); static void NavUpdateWindowing(); +static void NavUpdateWindowingApplyFocus(ImGuiWindow* window); static void NavUpdateWindowingOverlay(); -static void NavUpdateMoveResult(); -static float NavUpdatePageUpPageDown(int allowed_dir_flags); +static void NavUpdateCancelRequest(); +static void NavUpdateCreateMoveRequest(); +static void NavUpdateCreateTabbingRequest(); +static float NavUpdatePageUpPageDown(); static inline void NavUpdateAnyRequestFlag(); -static bool NavScoreItem(ImGuiNavMoveResult* result, ImRect cand); -static void NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, ImGuiID id); +static void NavUpdateCreateWrappingRequest(); +static void NavEndFrame(); +static bool NavScoreItem(ImGuiNavItemData* result); +static void NavApplyItemToResult(ImGuiNavItemData* result); +static void NavProcessItem(); +static void NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags); +static ImGuiInputSource NavCalcPreferredRefPosSource(); static ImVec2 NavCalcPreferredRefPos(); static void NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window); static ImGuiWindow* NavRestoreLastChildNavWindow(ImGuiWindow* window); -static int FindWindowFocusIndex(ImGuiWindow* window); +static void NavRestoreLayer(ImGuiNavLayer layer); -// Misc +// Error Checking and Debug Tools +static void ErrorCheckNewFrameSanityChecks(); +static void ErrorCheckEndFrameSanityChecks(); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +static void UpdateDebugToolItemPicker(); +static void UpdateDebugToolStackQueries(); +static void UpdateDebugToolFlashStyleColor(); +#endif + +// Inputs +static void UpdateKeyboardInputs(); static void UpdateMouseInputs(); static void UpdateMouseWheel(); -static bool UpdateManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4]); -static void UpdateDebugToolItemPicker(); +static void UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt); + +// Misc +static void UpdateSettings(); +static int UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect); static void RenderWindowOuterBorders(ImGuiWindow* window); -static void RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size); +static void RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, bool handle_borders_and_resize_grips, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size); static void RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open); +static void RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col); +static void RenderDimmedBackgrounds(); +static void SetLastItemDataForWindow(ImGuiWindow* window, const ImRect& rect); +static void SetLastItemDataForChildWindowItem(ImGuiWindow* window, const ImRect& rect); + +// Viewports +const ImGuiID IMGUI_VIEWPORT_DEFAULT_ID = 0x11111111; // Using an arbitrary constant instead of e.g. ImHashStr("ViewportDefault", 0); so it's easier to spot in the debugger. The exact value doesn't matter. +static ImGuiViewportP* AddUpdateViewport(ImGuiWindow* window, ImGuiID id, const ImVec2& platform_pos, const ImVec2& size, ImGuiViewportFlags flags); +static void DestroyViewport(ImGuiViewportP* viewport); +static void UpdateViewportsNewFrame(); +static void UpdateViewportsEndFrame(); +static void WindowSelectViewport(ImGuiWindow* window); +static void WindowSyncOwnedViewport(ImGuiWindow* window, ImGuiWindow* parent_window_in_stack); +static bool UpdateTryMergeWindowIntoHostViewport(ImGuiWindow* window, ImGuiViewportP* host_viewport); +static bool UpdateTryMergeWindowIntoHostViewports(ImGuiWindow* window); +static bool GetWindowAlwaysWantOwnViewport(ImGuiWindow* window); +static int FindPlatformMonitorForPos(const ImVec2& pos); +static int FindPlatformMonitorForRect(const ImRect& r); +static void UpdateViewportPlatformMonitor(ImGuiViewportP* viewport); } @@ -1088,27 +1305,33 @@ static void RenderWindowTitleBarContents(ImGuiWindow* window, const // [SECTION] CONTEXT AND MEMORY ALLOCATORS //----------------------------------------------------------------------------- +// DLL users: +// - Heaps and globals are not shared across DLL boundaries! +// - You will need to call SetCurrentContext() + SetAllocatorFunctions() for each static/DLL boundary you are calling from. +// - Same applies for hot-reloading mechanisms that are reliant on reloading DLL (note that many hot-reloading mechanisms work without DLL). +// - Using Dear ImGui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// - Confused? In a debugger: add GImGui to your watch window and notice how its value changes depending on your current location (which DLL boundary you are in). + // Current context pointer. Implicitly used by all Dear ImGui functions. Always assumed to be != NULL. -// ImGui::CreateContext() will automatically set this pointer if it is NULL. Change to a different context by calling ImGui::SetCurrentContext(). -// 1) Important: globals are not shared across DLL boundaries! If you use DLLs or any form of hot-reloading: you will need to call -// SetCurrentContext() (with the pointer you got from CreateContext) from each unique static/DLL boundary, and after each hot-reloading. -// In your debugger, add GImGui to your watch window and notice how its value changes depending on which location you are currently stepping into. -// 2) Important: Dear ImGui functions are not thread-safe because of this pointer. -// If you want thread-safety to allow N threads to access N different contexts, you can: -// - Change this variable to use thread local storage so each thread can refer to a different context, in imconfig.h: -// struct ImGuiContext; -// extern thread_local ImGuiContext* MyImGuiTLS; -// #define GImGui MyImGuiTLS -// And then define MyImGuiTLS in one of your cpp file. Note that thread_local is a C++11 keyword, earlier C++ uses compiler-specific keyword. -// - Future development aim to make this context pointer explicit to all calls. Also read https://github.com/ocornut/imgui/issues/586 -// - If you need a finite number of contexts, you may compile and use multiple instances of the ImGui code from different namespace. +// - ImGui::CreateContext() will automatically set this pointer if it is NULL. +// Change to a different context by calling ImGui::SetCurrentContext(). +// - Important: Dear ImGui functions are not thread-safe because of this pointer. +// If you want thread-safety to allow N threads to access N different contexts: +// - Change this variable to use thread local storage so each thread can refer to a different context, in your imconfig.h: +// struct ImGuiContext; +// extern thread_local ImGuiContext* MyImGuiTLS; +// #define GImGui MyImGuiTLS +// And then define MyImGuiTLS in one of your cpp files. Note that thread_local is a C++11 keyword, earlier C++ uses compiler-specific keyword. +// - Future development aims to make this context pointer explicit to all calls. Also read https://github.com/ocornut/imgui/issues/586 +// - If you need a finite number of contexts, you may compile and use multiple instances of the ImGui code from a different namespace. +// - DLL users: read comments above. #ifndef GImGui ImGuiContext* GImGui = NULL; #endif // Memory Allocator functions. Use SetAllocatorFunctions() to change them. -// If you use DLL hotreloading you might need to call SetAllocatorFunctions() after reloading code from this file. -// Otherwise, you probably don't want to modify them mid-program, and if you use global/static e.g. ImVector<> instances you may need to keep them accessible during program destruction. +// - You probably don't want to modify that mid-program, and if you use global/static e.g. ImVector<> instances you may need to keep them accessible during program destruction. +// - DLL users: read comments above. #ifndef IMGUI_DISABLE_DEFAULT_ALLOCATORS static void* MallocWrapper(size_t size, void* user_data) { IM_UNUSED(user_data); return malloc(size); } static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_data); free(ptr); } @@ -1116,51 +1339,74 @@ static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_d static void* MallocWrapper(size_t size, void* user_data) { IM_UNUSED(user_data); IM_UNUSED(size); IM_ASSERT(0); return NULL; } static void FreeWrapper(void* ptr, void* user_data) { IM_UNUSED(user_data); IM_UNUSED(ptr); IM_ASSERT(0); } #endif - -static void* (*GImAllocatorAllocFunc)(size_t size, void* user_data) = MallocWrapper; -static void (*GImAllocatorFreeFunc)(void* ptr, void* user_data) = FreeWrapper; -static void* GImAllocatorUserData = NULL; +static ImGuiMemAllocFunc GImAllocatorAllocFunc = MallocWrapper; +static ImGuiMemFreeFunc GImAllocatorFreeFunc = FreeWrapper; +static void* GImAllocatorUserData = NULL; //----------------------------------------------------------------------------- -// [SECTION] MAIN USER FACING STRUCTURES (ImGuiStyle, ImGuiIO) +// [SECTION] USER FACING STRUCTURES (ImGuiStyle, ImGuiIO, ImGuiPlatformIO) //----------------------------------------------------------------------------- ImGuiStyle::ImGuiStyle() { - Alpha = 1.0f; // Global alpha applies to everything in ImGui - WindowPadding = ImVec2(8,8); // Padding within a window - WindowRounding = 7.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows - WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. - WindowMinSize = ImVec2(32,32); // Minimum window size - WindowTitleAlign = ImVec2(0.0f,0.5f);// Alignment for title bar text - WindowMenuButtonPosition= ImGuiDir_Left; // Position of the collapsing/docking button in the title bar (left/right). Defaults to ImGuiDir_Left. - ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows - ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. - PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows - PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. - FramePadding = ImVec2(4,3); // Padding within a framed rectangle (used by most widgets) - FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). - FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. - ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines - ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) - TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! - IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). - ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). - ScrollbarSize = 14.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar - ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar - GrabMinSize = 10.0f; // Minimum width/height of a grab box for slider/scrollbar - GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. - TabRounding = 4.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. - TabBorderSize = 0.0f; // Thickness of border around tabs. - ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. - ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. - SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text when button is larger than text. - DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area by at least this amount. Only applies to regular windows. - DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. - MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. - AntiAliasedLines = true; // Enable anti-aliasing on lines/borders. Disable if you are really short on CPU/GPU. - AntiAliasedFill = true; // Enable anti-aliasing on filled shapes (rounded rectangles, circles, etc.) - CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + Alpha = 1.0f; // Global alpha applies to everything in Dear ImGui. + DisabledAlpha = 0.60f; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. + WindowPadding = ImVec2(8,8); // Padding within a window + WindowRounding = 0.0f; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. + WindowBorderSize = 1.0f; // Thickness of border around windows. Generally set to 0.0f or 1.0f. Other values not well tested. + WindowBorderHoverPadding = 4.0f; // Hit-testing extent outside/inside resizing border. Also extend determination of hovered window. Generally meaningfully larger than WindowBorderSize to make it easy to reach borders. + WindowMinSize = ImVec2(32,32); // Minimum window size + WindowTitleAlign = ImVec2(0.0f,0.5f);// Alignment for title bar text + WindowMenuButtonPosition = ImGuiDir_Left; // Position of the collapsing/docking button in the title bar (left/right). Defaults to ImGuiDir_Left. + ChildRounding = 0.0f; // Radius of child window corners rounding. Set to 0.0f to have rectangular child windows + ChildBorderSize = 1.0f; // Thickness of border around child windows. Generally set to 0.0f or 1.0f. Other values not well tested. + PopupRounding = 0.0f; // Radius of popup window corners rounding. Set to 0.0f to have rectangular child windows + PopupBorderSize = 1.0f; // Thickness of border around popup or tooltip windows. Generally set to 0.0f or 1.0f. Other values not well tested. + FramePadding = ImVec2(4,3); // Padding within a framed rectangle (used by most widgets) + FrameRounding = 0.0f; // Radius of frame corners rounding. Set to 0.0f to have rectangular frames (used by most widgets). + FrameBorderSize = 0.0f; // Thickness of border around frames. Generally set to 0.0f or 1.0f. Other values not well tested. + ItemSpacing = ImVec2(8,4); // Horizontal and vertical spacing between widgets/lines + ItemInnerSpacing = ImVec2(4,4); // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label) + CellPadding = ImVec2(4,2); // Padding within a table cell. Cellpadding.x is locked for entire table. CellPadding.y may be altered between different rows. + TouchExtraPadding = ImVec2(0,0); // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! + IndentSpacing = 21.0f; // Horizontal spacing when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). + ColumnsMinSpacing = 6.0f; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). + ScrollbarSize = 14.0f; // Width of the vertical scrollbar, Height of the horizontal scrollbar + ScrollbarRounding = 9.0f; // Radius of grab corners rounding for scrollbar + GrabMinSize = 12.0f; // Minimum width/height of a grab box for slider/scrollbar + GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + LogSliderDeadzone = 4.0f; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + ImageBorderSize = 0.0f; // Thickness of border around tabs. + TabRounding = 5.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. + TabBorderSize = 0.0f; // Thickness of border around tabs. + TabCloseButtonMinWidthSelected = -1.0f; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. + TabCloseButtonMinWidthUnselected = 0.0f; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected. + TabBarBorderSize = 1.0f; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + TabBarOverlineSize = 1.0f; // Thickness of tab-bar overline, which highlights the selected tab-bar. + TableAngledHeadersAngle = 35.0f * (IM_PI / 180.0f); // Angle of angled headers (supported values range from -50 degrees to +50 degrees). + TableAngledHeadersTextAlign = ImVec2(0.5f,0.0f);// Alignment of angled headers within the cell + ColorButtonPosition = ImGuiDir_Right; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. + ButtonTextAlign = ImVec2(0.5f,0.5f);// Alignment of button text when button is larger than text. + SelectableTextAlign = ImVec2(0.0f,0.0f);// Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + SeparatorTextBorderSize = 3.0f; // Thickness of border in SeparatorText() + SeparatorTextAlign = ImVec2(0.0f,0.5f);// Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). + SeparatorTextPadding = ImVec2(20.0f,3.f);// Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. + DisplayWindowPadding = ImVec2(19,19); // Window position are clamped to be visible within the display area or monitors by at least this amount. Only applies to regular windows. + DisplaySafeAreaPadding = ImVec2(3,3); // If you cannot see the edge of your screen (e.g. on a TV) increase the safe area padding. Covers popups/tooltips as well regular windows. + DockingSeparatorSize = 2.0f; // Thickness of resizing border between docked windows + MouseCursorScale = 1.0f; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. + AntiAliasedLines = true; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. + AntiAliasedLinesUseTex = true; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). + AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.). + CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + CircleTessellationMaxError = 0.30f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + + // Behaviors + HoverStationaryDelay = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + HoverDelayShort = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + HoverDelayNormal = 0.40f; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + HoverFlagsForTooltipMouse = ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + HoverFlagsForTooltipNav = ImGuiHoveredFlags_NoSharedDelay | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_AllowWhenDisabled; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. // Default theme ImGui::StyleColorsDark(this); @@ -1170,47 +1416,51 @@ ImGuiStyle::ImGuiStyle() // Important: This operation is lossy because we round all sizes to integer. If you need to change your scale multiples, call this over a freshly initialized ImGuiStyle structure rather than scaling multiple times. void ImGuiStyle::ScaleAllSizes(float scale_factor) { - WindowPadding = ImFloor(WindowPadding * scale_factor); - WindowRounding = ImFloor(WindowRounding * scale_factor); - WindowMinSize = ImFloor(WindowMinSize * scale_factor); - ChildRounding = ImFloor(ChildRounding * scale_factor); - PopupRounding = ImFloor(PopupRounding * scale_factor); - FramePadding = ImFloor(FramePadding * scale_factor); - FrameRounding = ImFloor(FrameRounding * scale_factor); - ItemSpacing = ImFloor(ItemSpacing * scale_factor); - ItemInnerSpacing = ImFloor(ItemInnerSpacing * scale_factor); - TouchExtraPadding = ImFloor(TouchExtraPadding * scale_factor); - IndentSpacing = ImFloor(IndentSpacing * scale_factor); - ColumnsMinSpacing = ImFloor(ColumnsMinSpacing * scale_factor); - ScrollbarSize = ImFloor(ScrollbarSize * scale_factor); - ScrollbarRounding = ImFloor(ScrollbarRounding * scale_factor); - GrabMinSize = ImFloor(GrabMinSize * scale_factor); - GrabRounding = ImFloor(GrabRounding * scale_factor); - TabRounding = ImFloor(TabRounding * scale_factor); - DisplayWindowPadding = ImFloor(DisplayWindowPadding * scale_factor); - DisplaySafeAreaPadding = ImFloor(DisplaySafeAreaPadding * scale_factor); - MouseCursorScale = ImFloor(MouseCursorScale * scale_factor); + WindowPadding = ImTrunc(WindowPadding * scale_factor); + WindowRounding = ImTrunc(WindowRounding * scale_factor); + WindowMinSize = ImTrunc(WindowMinSize * scale_factor); + WindowBorderHoverPadding = ImTrunc(WindowBorderHoverPadding * scale_factor); + ChildRounding = ImTrunc(ChildRounding * scale_factor); + PopupRounding = ImTrunc(PopupRounding * scale_factor); + FramePadding = ImTrunc(FramePadding * scale_factor); + FrameRounding = ImTrunc(FrameRounding * scale_factor); + ItemSpacing = ImTrunc(ItemSpacing * scale_factor); + ItemInnerSpacing = ImTrunc(ItemInnerSpacing * scale_factor); + CellPadding = ImTrunc(CellPadding * scale_factor); + TouchExtraPadding = ImTrunc(TouchExtraPadding * scale_factor); + IndentSpacing = ImTrunc(IndentSpacing * scale_factor); + ColumnsMinSpacing = ImTrunc(ColumnsMinSpacing * scale_factor); + ScrollbarSize = ImTrunc(ScrollbarSize * scale_factor); + ScrollbarRounding = ImTrunc(ScrollbarRounding * scale_factor); + GrabMinSize = ImTrunc(GrabMinSize * scale_factor); + GrabRounding = ImTrunc(GrabRounding * scale_factor); + LogSliderDeadzone = ImTrunc(LogSliderDeadzone * scale_factor); + ImageBorderSize = ImTrunc(ImageBorderSize * scale_factor); + TabRounding = ImTrunc(TabRounding * scale_factor); + TabCloseButtonMinWidthSelected = (TabCloseButtonMinWidthSelected > 0.0f && TabCloseButtonMinWidthSelected != FLT_MAX) ? ImTrunc(TabCloseButtonMinWidthSelected * scale_factor) : TabCloseButtonMinWidthSelected; + TabCloseButtonMinWidthUnselected = (TabCloseButtonMinWidthUnselected > 0.0f && TabCloseButtonMinWidthUnselected != FLT_MAX) ? ImTrunc(TabCloseButtonMinWidthUnselected * scale_factor) : TabCloseButtonMinWidthUnselected; + TabBarOverlineSize = ImTrunc(TabBarOverlineSize * scale_factor); + SeparatorTextPadding = ImTrunc(SeparatorTextPadding * scale_factor); + DockingSeparatorSize = ImTrunc(DockingSeparatorSize * scale_factor); + DisplayWindowPadding = ImTrunc(DisplayWindowPadding * scale_factor); + DisplaySafeAreaPadding = ImTrunc(DisplaySafeAreaPadding * scale_factor); + MouseCursorScale = ImTrunc(MouseCursorScale * scale_factor); } ImGuiIO::ImGuiIO() { // Most fields are initialized with zero memset(this, 0, sizeof(*this)); + IM_STATIC_ASSERT(IM_ARRAYSIZE(ImGuiIO::MouseDown) == ImGuiMouseButton_COUNT && IM_ARRAYSIZE(ImGuiIO::MouseClicked) == ImGuiMouseButton_COUNT); // Settings ConfigFlags = ImGuiConfigFlags_None; BackendFlags = ImGuiBackendFlags_None; DisplaySize = ImVec2(-1.0f, -1.0f); - DeltaTime = 1.0f/60.0f; + DeltaTime = 1.0f / 60.0f; IniSavingRate = 5.0f; - IniFilename = "imgui.ini"; + IniFilename = "imgui.ini"; // Important: "imgui.ini" is relative to current working dir, most apps will want to lock this to an absolute path (e.g. same path as executables). LogFilename = "imgui_log.txt"; - MouseDoubleClickTime = 0.30f; - MouseDoubleClickMaxDist = 6.0f; - for (int i = 0; i < ImGuiKey_COUNT; i++) - KeyMap[i] = -1; - KeyRepeatDelay = 0.250f; - KeyRepeatRate = 0.050f; UserData = NULL; Fonts = NULL; @@ -1219,6 +1469,27 @@ ImGuiIO::ImGuiIO() FontAllowUserScaling = false; DisplayFramebufferScale = ImVec2(1.0f, 1.0f); + // Keyboard/Gamepad Navigation options + ConfigNavSwapGamepadButtons = false; + ConfigNavMoveSetMousePos = false; + ConfigNavCaptureKeyboard = true; + ConfigNavEscapeClearFocusItem = true; + ConfigNavEscapeClearFocusWindow = false; + ConfigNavCursorVisibleAuto = true; + ConfigNavCursorVisibleAlways = false; + + // Docking options (when ImGuiConfigFlags_DockingEnable is set) + ConfigDockingNoSplit = false; + ConfigDockingWithShift = false; + ConfigDockingAlwaysTabBar = false; + ConfigDockingTransparentPayload = false; + + // Viewport options (when ImGuiConfigFlags_ViewportsEnable is set) + ConfigViewportsNoAutoMerge = false; + ConfigViewportsNoTaskBarIcon = false; + ConfigViewportsNoDecoration = true; + ConfigViewportsNoDefaultParent = false; + // Miscellaneous options MouseDrawCursor = false; #ifdef __APPLE__ @@ -1226,62 +1497,480 @@ ImGuiIO::ImGuiIO() #else ConfigMacOSXBehaviors = false; #endif + ConfigInputTrickleEventQueue = true; ConfigInputTextCursorBlink = true; + ConfigInputTextEnterKeepActive = false; + ConfigDragClickToInputText = false; ConfigWindowsResizeFromEdges = true; ConfigWindowsMoveFromTitleBarOnly = false; - ConfigWindowsMemoryCompactTimer = 60.0f; + ConfigWindowsCopyContentsWithCtrlC = false; + ConfigScrollbarScrollByPage = true; + ConfigMemoryCompactTimer = 60.0f; + ConfigDebugIsDebuggerPresent = false; + ConfigDebugHighlightIdConflicts = true; + ConfigDebugHighlightIdConflictsShowItemPicker = true; + ConfigDebugBeginReturnValueOnce = false; + ConfigDebugBeginReturnValueLoop = false; + + ConfigErrorRecovery = true; + ConfigErrorRecoveryEnableAssert = true; + ConfigErrorRecoveryEnableDebugLog = true; + ConfigErrorRecoveryEnableTooltip = true; + + // Inputs Behaviors + MouseDoubleClickTime = 0.30f; + MouseDoubleClickMaxDist = 6.0f; + MouseDragThreshold = 6.0f; + KeyRepeatDelay = 0.275f; + KeyRepeatRate = 0.050f; // Platform Functions + // Note: Initialize() will setup default clipboard/ime handlers. BackendPlatformName = BackendRendererName = NULL; BackendPlatformUserData = BackendRendererUserData = BackendLanguageUserData = NULL; - GetClipboardTextFn = GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations - SetClipboardTextFn = SetClipboardTextFn_DefaultImpl; - ClipboardUserData = NULL; - ImeSetInputScreenPosFn = ImeSetInputScreenPosFn_DefaultImpl; - ImeWindowHandle = NULL; - -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - RenderDrawListsFn = NULL; -#endif // Input (NB: we already have memset zero the entire structure!) MousePos = ImVec2(-FLT_MAX, -FLT_MAX); MousePosPrev = ImVec2(-FLT_MAX, -FLT_MAX); - MouseDragThreshold = 6.0f; + MouseSource = ImGuiMouseSource_Mouse; for (int i = 0; i < IM_ARRAYSIZE(MouseDownDuration); i++) MouseDownDuration[i] = MouseDownDurationPrev[i] = -1.0f; - for (int i = 0; i < IM_ARRAYSIZE(KeysDownDuration); i++) KeysDownDuration[i] = KeysDownDurationPrev[i] = -1.0f; - for (int i = 0; i < IM_ARRAYSIZE(NavInputsDownDuration); i++) NavInputsDownDuration[i] = -1.0f; + for (int i = 0; i < IM_ARRAYSIZE(KeysData); i++) { KeysData[i].DownDuration = KeysData[i].DownDurationPrev = -1.0f; } + AppAcceptingEvents = true; } // Pass in translated ASCII characters for text input. // - with glfw you can get those from the callback set in glfwSetCharCallback() // - on Windows you can get those using ToAscii+keyboard state, or via the WM_CHAR message +// FIXME: Should in theory be called "AddCharacterEvent()" to be consistent with new API void ImGuiIO::AddInputCharacter(unsigned int c) { - if (c > 0 && c < 0x10000) - InputQueueCharacters.push_back((ImWchar)c); + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + if (c == 0 || !AppAcceptingEvents) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Text; + e.Source = ImGuiInputSource_Keyboard; + e.EventId = g.InputEventsNextEventId++; + e.Text.Char = c; + g.InputEventsQueue.push_back(e); +} + +// UTF16 strings use surrogate pairs to encode codepoints >= 0x10000, so +// we should save the high surrogate. +void ImGuiIO::AddInputCharacterUTF16(ImWchar16 c) +{ + if ((c == 0 && InputQueueSurrogate == 0) || !AppAcceptingEvents) + return; + + if ((c & 0xFC00) == 0xD800) // High surrogate, must save + { + if (InputQueueSurrogate != 0) + AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID); + InputQueueSurrogate = c; + return; + } + + ImWchar cp = c; + if (InputQueueSurrogate != 0) + { + if ((c & 0xFC00) != 0xDC00) // Invalid low surrogate + { + AddInputCharacter(IM_UNICODE_CODEPOINT_INVALID); + } + else + { +#if IM_UNICODE_CODEPOINT_MAX == 0xFFFF + cp = IM_UNICODE_CODEPOINT_INVALID; // Codepoint will not fit in ImWchar +#else + cp = (ImWchar)(((InputQueueSurrogate - 0xD800) << 10) + (c - 0xDC00) + 0x10000); +#endif + } + + InputQueueSurrogate = 0; + } + AddInputCharacter((unsigned)cp); } void ImGuiIO::AddInputCharactersUTF8(const char* utf8_chars) { + if (!AppAcceptingEvents) + return; while (*utf8_chars != 0) { unsigned int c = 0; utf8_chars += ImTextCharFromUtf8(&c, utf8_chars, NULL); - if (c > 0 && c < 0x10000) - InputQueueCharacters.push_back((ImWchar)c); + AddInputCharacter(c); } } +// Clear all incoming events. +void ImGuiIO::ClearEventsQueue() +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + g.InputEventsQueue.clear(); +} + +// Clear current keyboard/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons. +void ImGuiIO::ClearInputKeys() +{ + ImGuiContext& g = *Ctx; + for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key++) + { + if (ImGui::IsMouseKey((ImGuiKey)key)) + continue; + ImGuiKeyData* key_data = &g.IO.KeysData[key - ImGuiKey_NamedKey_BEGIN]; + key_data->Down = false; + key_data->DownDuration = -1.0f; + key_data->DownDurationPrev = -1.0f; + } + KeyCtrl = KeyShift = KeyAlt = KeySuper = false; + KeyMods = ImGuiMod_None; + InputQueueCharacters.resize(0); // Behavior of old ClearInputCharacters(). +} + +void ImGuiIO::ClearInputMouse() +{ + for (ImGuiKey key = ImGuiKey_Mouse_BEGIN; key < ImGuiKey_Mouse_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyData* key_data = &KeysData[key - ImGuiKey_NamedKey_BEGIN]; + key_data->Down = false; + key_data->DownDuration = -1.0f; + key_data->DownDurationPrev = -1.0f; + } + MousePos = ImVec2(-FLT_MAX, -FLT_MAX); + for (int n = 0; n < IM_ARRAYSIZE(MouseDown); n++) + { + MouseDown[n] = false; + MouseDownDuration[n] = MouseDownDurationPrev[n] = -1.0f; + } + MouseWheel = MouseWheelH = 0.0f; +} + +// Removed this as it is ambiguous/misleading and generally incorrect to use with the existence of a higher-level input queue. +// Current frame character buffer is now also cleared by ClearInputKeys(). +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS void ImGuiIO::ClearInputCharacters() { InputQueueCharacters.resize(0); } +#endif + +static ImGuiInputEvent* FindLatestInputEvent(ImGuiContext* ctx, ImGuiInputEventType type, int arg = -1) +{ + ImGuiContext& g = *ctx; + for (int n = g.InputEventsQueue.Size - 1; n >= 0; n--) + { + ImGuiInputEvent* e = &g.InputEventsQueue[n]; + if (e->Type != type) + continue; + if (type == ImGuiInputEventType_Key && e->Key.Key != arg) + continue; + if (type == ImGuiInputEventType_MouseButton && e->MouseButton.Button != arg) + continue; + return e; + } + return NULL; +} + +// Queue a new key down/up event. +// - ImGuiKey key: Translated key (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) +// - bool down: Is the key down? use false to signify a key release. +// - float analog_value: 0.0f..1.0f +// IMPORTANT: THIS FUNCTION AND OTHER "ADD" GRABS THE CONTEXT FROM OUR INSTANCE. +// WE NEED TO ENSURE THAT ALL FUNCTION CALLS ARE FULFILLING THIS, WHICH IS WHY GetKeyData() HAS AN EXPLICIT CONTEXT. +void ImGuiIO::AddKeyAnalogEvent(ImGuiKey key, bool down, float analog_value) +{ + //if (e->Down) { IMGUI_DEBUG_LOG_IO("AddKeyEvent() Key='%s' %d, NativeKeycode = %d, NativeScancode = %d\n", ImGui::GetKeyName(e->Key), e->Down, e->NativeKeycode, e->NativeScancode); } + IM_ASSERT(Ctx != NULL); + if (key == ImGuiKey_None || !AppAcceptingEvents) + return; + ImGuiContext& g = *Ctx; + IM_ASSERT(ImGui::IsNamedKeyOrMod(key)); // Backend needs to pass a valid ImGuiKey_ constant. 0..511 values are legacy native key codes which are not accepted by this API. + IM_ASSERT(ImGui::IsAliasKey(key) == false); // Backend cannot submit ImGuiKey_MouseXXX values they are automatically inferred from AddMouseXXX() events. + + // MacOS: swap Cmd(Super) and Ctrl + if (g.IO.ConfigMacOSXBehaviors) + { + if (key == ImGuiMod_Super) { key = ImGuiMod_Ctrl; } + else if (key == ImGuiMod_Ctrl) { key = ImGuiMod_Super; } + else if (key == ImGuiKey_LeftSuper) { key = ImGuiKey_LeftCtrl; } + else if (key == ImGuiKey_RightSuper){ key = ImGuiKey_RightCtrl; } + else if (key == ImGuiKey_LeftCtrl) { key = ImGuiKey_LeftSuper; } + else if (key == ImGuiKey_RightCtrl) { key = ImGuiKey_RightSuper; } + } + + // Filter duplicate (in particular: key mods and gamepad analog values are commonly spammed) + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_Key, (int)key); + const ImGuiKeyData* key_data = ImGui::GetKeyData(&g, key); + const bool latest_key_down = latest_event ? latest_event->Key.Down : key_data->Down; + const float latest_key_analog = latest_event ? latest_event->Key.AnalogValue : key_data->AnalogValue; + if (latest_key_down == down && latest_key_analog == analog_value) + return; + + // Add event + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Key; + e.Source = ImGui::IsGamepadKey(key) ? ImGuiInputSource_Gamepad : ImGuiInputSource_Keyboard; + e.EventId = g.InputEventsNextEventId++; + e.Key.Key = key; + e.Key.Down = down; + e.Key.AnalogValue = analog_value; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddKeyEvent(ImGuiKey key, bool down) +{ + if (!AppAcceptingEvents) + return; + AddKeyAnalogEvent(key, down, down ? 1.0f : 0.0f); +} + +// [Optional] Call after AddKeyEvent(). +// Specify native keycode, scancode + Specify index for legacy <1.87 IsKeyXXX() functions with native indices. +// If you are writing a backend in 2022 or don't use IsKeyXXX() with native values that are not ImGuiKey values, you can avoid calling this. +void ImGuiIO::SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index) +{ + if (key == ImGuiKey_None) + return; + IM_ASSERT(ImGui::IsNamedKey(key)); // >= 512 + IM_ASSERT(native_legacy_index == -1 || ImGui::IsLegacyKey((ImGuiKey)native_legacy_index)); // >= 0 && <= 511 + IM_UNUSED(key); // Yet unused + IM_UNUSED(native_keycode); // Yet unused + IM_UNUSED(native_scancode); // Yet unused + IM_UNUSED(native_legacy_index); // Yet unused +} + +// Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen. +void ImGuiIO::SetAppAcceptingEvents(bool accepting_events) +{ + AppAcceptingEvents = accepting_events; +} + +// Queue a mouse move event +void ImGuiIO::AddMousePosEvent(float x, float y) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + if (!AppAcceptingEvents) + return; + + // Apply same flooring as UpdateMouseInputs() + ImVec2 pos((x > -FLT_MAX) ? ImFloor(x) : x, (y > -FLT_MAX) ? ImFloor(y) : y); + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MousePos); + const ImVec2 latest_pos = latest_event ? ImVec2(latest_event->MousePos.PosX, latest_event->MousePos.PosY) : g.IO.MousePos; + if (latest_pos.x == pos.x && latest_pos.y == pos.y) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MousePos; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MousePos.PosX = pos.x; + e.MousePos.PosY = pos.y; + e.MousePos.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddMouseButtonEvent(int mouse_button, bool down) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + IM_ASSERT(mouse_button >= 0 && mouse_button < ImGuiMouseButton_COUNT); + if (!AppAcceptingEvents) + return; + + // On MacOS X: Convert Ctrl(Super)+Left click into Right-click: handle held button. + if (ConfigMacOSXBehaviors && mouse_button == 0 && MouseCtrlLeftAsRightClick) + { + // Order of both statements matterns: this event will still release mouse button 1 + mouse_button = 1; + if (!down) + MouseCtrlLeftAsRightClick = false; + } + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MouseButton, (int)mouse_button); + const bool latest_button_down = latest_event ? latest_event->MouseButton.Down : g.IO.MouseDown[mouse_button]; + if (latest_button_down == down) + return; + + // On MacOS X: Convert Ctrl(Super)+Left click into Right-click. + // - Note that this is actual physical Ctrl which is ImGuiMod_Super for us. + // - At this point we want from !down to down, so this is handling the initial press. + if (ConfigMacOSXBehaviors && mouse_button == 0 && down) + { + const ImGuiInputEvent* latest_super_event = FindLatestInputEvent(&g, ImGuiInputEventType_Key, (int)ImGuiMod_Super); + if (latest_super_event ? latest_super_event->Key.Down : g.IO.KeySuper) + { + IMGUI_DEBUG_LOG_IO("[io] Super+Left Click aliased into Right Click\n"); + MouseCtrlLeftAsRightClick = true; + AddMouseButtonEvent(1, true); // This is just quicker to write that passing through, as we need to filter duplicate again. + return; + } + } + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseButton; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MouseButton.Button = mouse_button; + e.MouseButton.Down = down; + e.MouseButton.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); +} + +// Queue a mouse wheel event (some mouse/API may only have a Y component) +void ImGuiIO::AddMouseWheelEvent(float wheel_x, float wheel_y) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + + // Filter duplicate (unlike most events, wheel values are relative and easy to filter) + if (!AppAcceptingEvents || (wheel_x == 0.0f && wheel_y == 0.0f)) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseWheel; + e.Source = ImGuiInputSource_Mouse; + e.EventId = g.InputEventsNextEventId++; + e.MouseWheel.WheelX = wheel_x; + e.MouseWheel.WheelY = wheel_y; + e.MouseWheel.MouseSource = g.InputEventsNextMouseSource; + g.InputEventsQueue.push_back(e); +} + +// This is not a real event, the data is latched in order to be stored in actual Mouse events. +// This is so that duplicate events (e.g. Windows sending extraneous WM_MOUSEMOVE) gets filtered and are not leading to actual source changes. +void ImGuiIO::AddMouseSourceEvent(ImGuiMouseSource source) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + g.InputEventsNextMouseSource = source; +} + +void ImGuiIO::AddMouseViewportEvent(ImGuiID viewport_id) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + //IM_ASSERT(g.IO.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport); + if (!AppAcceptingEvents) + return; + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_MouseViewport); + const ImGuiID latest_viewport_id = latest_event ? latest_event->MouseViewport.HoveredViewportID : g.IO.MouseHoveredViewport; + if (latest_viewport_id == viewport_id) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_MouseViewport; + e.Source = ImGuiInputSource_Mouse; + e.MouseViewport.HoveredViewportID = viewport_id; + g.InputEventsQueue.push_back(e); +} + +void ImGuiIO::AddFocusEvent(bool focused) +{ + IM_ASSERT(Ctx != NULL); + ImGuiContext& g = *Ctx; + + // Filter duplicate + const ImGuiInputEvent* latest_event = FindLatestInputEvent(&g, ImGuiInputEventType_Focus); + const bool latest_focused = latest_event ? latest_event->AppFocused.Focused : !g.IO.AppFocusLost; + if (latest_focused == focused || (ConfigDebugIgnoreFocusLoss && !focused)) + return; + + ImGuiInputEvent e; + e.Type = ImGuiInputEventType_Focus; + e.EventId = g.InputEventsNextEventId++; + e.AppFocused.Focused = focused; + g.InputEventsQueue.push_back(e); +} + +ImGuiPlatformIO::ImGuiPlatformIO() +{ + // Most fields are initialized with zero + memset(this, 0, sizeof(*this)); + Platform_LocaleDecimalPoint = '.'; +} //----------------------------------------------------------------------------- -// [SECTION] MISC HELPERS/UTILITIES (Maths, String, Format, Hash, File functions) +// [SECTION] MISC HELPERS/UTILITIES (Geometry functions) //----------------------------------------------------------------------------- +ImVec2 ImBezierCubicClosestPoint(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, int num_segments) +{ + IM_ASSERT(num_segments > 0); // Use ImBezierCubicClosestPointCasteljau() + ImVec2 p_last = p1; + ImVec2 p_closest; + float p_closest_dist2 = FLT_MAX; + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + { + ImVec2 p_current = ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist2 = ImLengthSqr(p - p_line); + if (dist2 < p_closest_dist2) + { + p_closest = p_line; + p_closest_dist2 = dist2; + } + p_last = p_current; + } + return p_closest; +} + +// Closely mimics PathBezierToCasteljau() in imgui_draw.cpp +static void ImBezierCubicClosestPointCasteljauStep(const ImVec2& p, ImVec2& p_closest, ImVec2& p_last, float& p_closest_dist2, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +{ + float dx = x4 - x1; + float dy = y4 - y1; + float d2 = ((x2 - x4) * dy - (y2 - y4) * dx); + float d3 = ((x3 - x4) * dy - (y3 - y4) * dx); + d2 = (d2 >= 0) ? d2 : -d2; + d3 = (d3 >= 0) ? d3 : -d3; + if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) + { + ImVec2 p_current(x4, y4); + ImVec2 p_line = ImLineClosestPoint(p_last, p_current, p); + float dist2 = ImLengthSqr(p - p_line); + if (dist2 < p_closest_dist2) + { + p_closest = p_line; + p_closest_dist2 = dist2; + } + p_last = p_current; + } + else if (level < 10) + { + float x12 = (x1 + x2)*0.5f, y12 = (y1 + y2)*0.5f; + float x23 = (x2 + x3)*0.5f, y23 = (y2 + y3)*0.5f; + float x34 = (x3 + x4)*0.5f, y34 = (y3 + y4)*0.5f; + float x123 = (x12 + x23)*0.5f, y123 = (y12 + y23)*0.5f; + float x234 = (x23 + x34)*0.5f, y234 = (y23 + y34)*0.5f; + float x1234 = (x123 + x234)*0.5f, y1234 = (y123 + y234)*0.5f; + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); + } +} + +// tess_tol is generally the same value you would find in ImGui::GetStyle().CurveTessellationTol +// Because those ImXXX functions are lower-level than ImGui:: we cannot access this value automatically. +ImVec2 ImBezierCubicClosestPointCasteljau(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, float tess_tol) +{ + IM_ASSERT(tess_tol > 0.0f); + ImVec2 p_last = p1; + ImVec2 p_closest; + float p_closest_dist2 = FLT_MAX; + ImBezierCubicClosestPointCasteljauStep(p, p_closest, p_last, p_closest_dist2, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, tess_tol, 0); + return p_closest; +} + ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p) { ImVec2 ap = p - a; @@ -1330,18 +2019,22 @@ ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, return proj_ca; } +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (String, Format, Hash functions) +//----------------------------------------------------------------------------- + // Consider using _stricmp/_strnicmp under Windows or strcasecmp/strncasecmp. We don't actually use either ImStricmp/ImStrnicmp in the codebase any more. int ImStricmp(const char* str1, const char* str2) { int d; - while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } + while ((d = ImToUpper(*str2) - ImToUpper(*str1)) == 0 && *str1) { str1++; str2++; } return d; } int ImStrnicmp(const char* str1, const char* str2, size_t count) { int d = 0; - while (count > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; count--; } + while (count > 0 && (d = ImToUpper(*str2) - ImToUpper(*str1)) == 0 && *str1) { str1++; str2++; count--; } return d; } @@ -1356,15 +2049,15 @@ void ImStrncpy(char* dst, const char* src, size_t count) char* ImStrdup(const char* str) { - size_t len = strlen(str); + size_t len = ImStrlen(str); void* buf = IM_ALLOC(len + 1); return (char*)memcpy(buf, (const void*)str, len + 1); } char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* src) { - size_t dst_buf_size = p_dst_size ? *p_dst_size : strlen(dst) + 1; - size_t src_size = strlen(src) + 1; + size_t dst_buf_size = p_dst_size ? *p_dst_size : ImStrlen(dst) + 1; + size_t src_size = ImStrlen(src) + 1; if (dst_buf_size < src_size) { IM_FREE(dst); @@ -1377,13 +2070,13 @@ char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* src) const char* ImStrchrRange(const char* str, const char* str_end, char c) { - const char* p = (const char*)memchr(str, (int)c, str_end - str); + const char* p = (const char*)ImMemchr(str, (int)c, str_end - str); return p; } int ImStrlenW(const ImWchar* str) { - //return (int)wcslen((const wchar_t*)str); // FIXME-OPT: Could use this when wchar_t are 16-bits + //return (int)wcslen((const wchar_t*)str); // FIXME-OPT: Could use this when wchar_t are 16-bit int n = 0; while (*str++) n++; return n; @@ -1392,12 +2085,13 @@ int ImStrlenW(const ImWchar* str) // Find end-of-line. Return pointer will point to either first \n, either str_end. const char* ImStreolRange(const char* str, const char* str_end) { - const char* p = (const char*)memchr(str, '\n', str_end - str); + const char* p = (const char*)ImMemchr(str, '\n', str_end - str); return p ? p : str_end; } -const ImWchar* ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin) // find beginning-of-line +const char* ImStrbol(const char* buf_mid_line, const char* buf_begin) // find beginning-of-line { + IM_ASSERT_PARANOID(buf_mid_line >= buf_begin && buf_mid_line <= buf_begin + ImStrlen(buf_begin)); while (buf_mid_line > buf_begin && buf_mid_line[-1] != '\n') buf_mid_line--; return buf_mid_line; @@ -1406,16 +2100,16 @@ const ImWchar* ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin) const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end) { if (!needle_end) - needle_end = needle + strlen(needle); + needle_end = needle + ImStrlen(needle); - const char un0 = (char)toupper(*needle); + const char un0 = (char)ImToUpper(*needle); while ((!haystack_end && *haystack) || (haystack_end && haystack < haystack_end)) { - if (toupper(*haystack) == un0) + if (ImToUpper(*haystack) == un0) { const char* b = needle + 1; for (const char* a = haystack + 1; b < needle_end; a++, b++) - if (toupper(*a) != toupper(*b)) + if (ImToUpper(*a) != ImToUpper(*b)) break; if (b == needle_end) return haystack; @@ -1441,16 +2135,32 @@ void ImStrTrimBlanks(char* buf) buf[p - p_start] = 0; // Zero terminate } +const char* ImStrSkipBlank(const char* str) +{ + while (str[0] == ' ' || str[0] == '\t') + str++; + return str; +} + // A) MSVC version appears to return -1 on overflow, whereas glibc appears to return total count (which may be >= buf_size). // Ideally we would test for only one of those limits at runtime depending on the behavior the vsnprintf(), but trying to deduct it at compile time sounds like a pandora can of worm. // B) When buf==NULL vsnprintf() will return the output size. -#ifndef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS +#ifndef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS -//#define IMGUI_USE_STB_SPRINTF +// We support stb_sprintf which is much faster (see: https://github.com/nothings/stb/blob/master/stb_sprintf.h) +// You may set IMGUI_USE_STB_SPRINTF to use our default wrapper, or set IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +// and setup the wrapper yourself. (FIXME-OPT: Some of our high-level operations such as ImGuiTextBuffer::appendfv() are +// designed using two-passes worst case, which probably could be improved using the stbsp_vsprintfcb() function.) #ifdef IMGUI_USE_STB_SPRINTF +#ifndef IMGUI_DISABLE_STB_SPRINTF_IMPLEMENTATION #define STB_SPRINTF_IMPLEMENTATION -#include "imstb_sprintf.h" #endif +#ifdef IMGUI_STB_SPRINTF_FILENAME +#include IMGUI_STB_SPRINTF_FILENAME +#else +#include "stb_sprintf.h" +#endif +#endif // #ifdef IMGUI_USE_STB_SPRINTF #if defined(_MSC_VER) && !defined(vsnprintf) #define vsnprintf _vsnprintf @@ -1488,13 +2198,59 @@ int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) buf[w] = 0; return w; } -#endif // #ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS +#endif // #ifdef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +void ImFormatStringToTempBuffer(const char** out_buf, const char** out_buf_end, const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + ImFormatStringToTempBufferV(out_buf, out_buf_end, fmt, args); + va_end(args); +} + +// FIXME: Should rework API toward allowing multiple in-flight temp buffers (easier and safer for caller) +// by making the caller acquire a temp buffer token, with either explicit or destructor release, e.g. +// ImGuiTempBufferToken token; +// ImFormatStringToTempBuffer(token, ...); +void ImFormatStringToTempBufferV(const char** out_buf, const char** out_buf_end, const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + if (fmt[0] == '%' && fmt[1] == 's' && fmt[2] == 0) + { + const char* buf = va_arg(args, const char*); // Skip formatting when using "%s" + if (buf == NULL) + buf = "(null)"; + *out_buf = buf; + if (out_buf_end) { *out_buf_end = buf + ImStrlen(buf); } + } + else if (fmt[0] == '%' && fmt[1] == '.' && fmt[2] == '*' && fmt[3] == 's' && fmt[4] == 0) + { + int buf_len = va_arg(args, int); // Skip formatting when using "%.*s" + const char* buf = va_arg(args, const char*); + if (buf == NULL) + { + buf = "(null)"; + buf_len = ImMin(buf_len, 6); + } + *out_buf = buf; + *out_buf_end = buf + buf_len; // Disallow not passing 'out_buf_end' here. User is expected to use it. + } + else + { + int buf_len = ImFormatStringV(g.TempBuffer.Data, g.TempBuffer.Size, fmt, args); + *out_buf = g.TempBuffer.Data; + if (out_buf_end) { *out_buf_end = g.TempBuffer.Data + buf_len; } + } +} + +#ifndef IMGUI_ENABLE_SSE4_2_CRC // CRC32 needs a 1KB lookup table (not cache friendly) // Although the code to generate the table is simple and shorter than the table itself, using a const table allows us to easily: // - avoid an unnecessary branch/memory tap, - keep the ImHashXXX functions usable by static constructors, - make it thread-safe. static const ImU32 GCrc32LookupTable[256] = { +#ifdef IMGUI_USE_LEGACY_CRC32_ADLER + // Legacy CRC32-adler table used pre 1.91.6 (before 2024/11/27). Only use if you cannot afford invalidating old .ini data. 0x00000000,0x77073096,0xEE0E612C,0x990951BA,0x076DC419,0x706AF48F,0xE963A535,0x9E6495A3,0x0EDB8832,0x79DCB8A4,0xE0D5E91E,0x97D2D988,0x09B64C2B,0x7EB17CBD,0xE7B82D07,0x90BF1D91, 0x1DB71064,0x6AB020F2,0xF3B97148,0x84BE41DE,0x1ADAD47D,0x6DDDE4EB,0xF4D4B551,0x83D385C7,0x136C9856,0x646BA8C0,0xFD62F97A,0x8A65C9EC,0x14015C4F,0x63066CD9,0xFA0F3D63,0x8D080DF5, 0x3B6E20C8,0x4C69105E,0xD56041E4,0xA2677172,0x3C03E4D1,0x4B04D447,0xD20D85FD,0xA50AB56B,0x35B5A8FA,0x42B2986C,0xDBBBC9D6,0xACBCF940,0x32D86CE3,0x45DF5C75,0xDCD60DCF,0xABD13D59, @@ -1511,19 +2267,51 @@ static const ImU32 GCrc32LookupTable[256] = 0x86D3D2D4,0xF1D4E242,0x68DDB3F8,0x1FDA836E,0x81BE16CD,0xF6B9265B,0x6FB077E1,0x18B74777,0x88085AE6,0xFF0F6A70,0x66063BCA,0x11010B5C,0x8F659EFF,0xF862AE69,0x616BFFD3,0x166CCF45, 0xA00AE278,0xD70DD2EE,0x4E048354,0x3903B3C2,0xA7672661,0xD06016F7,0x4969474D,0x3E6E77DB,0xAED16A4A,0xD9D65ADC,0x40DF0B66,0x37D83BF0,0xA9BCAE53,0xDEBB9EC5,0x47B2CF7F,0x30B5FFE9, 0xBDBDF21C,0xCABAC28A,0x53B39330,0x24B4A3A6,0xBAD03605,0xCDD70693,0x54DE5729,0x23D967BF,0xB3667A2E,0xC4614AB8,0x5D681B02,0x2A6F2B94,0xB40BBE37,0xC30C8EA1,0x5A05DF1B,0x2D02EF8D, +#else + // CRC32c table compatible with SSE 4.2 instructions + 0x00000000,0xF26B8303,0xE13B70F7,0x1350F3F4,0xC79A971F,0x35F1141C,0x26A1E7E8,0xD4CA64EB,0x8AD958CF,0x78B2DBCC,0x6BE22838,0x9989AB3B,0x4D43CFD0,0xBF284CD3,0xAC78BF27,0x5E133C24, + 0x105EC76F,0xE235446C,0xF165B798,0x030E349B,0xD7C45070,0x25AFD373,0x36FF2087,0xC494A384,0x9A879FA0,0x68EC1CA3,0x7BBCEF57,0x89D76C54,0x5D1D08BF,0xAF768BBC,0xBC267848,0x4E4DFB4B, + 0x20BD8EDE,0xD2D60DDD,0xC186FE29,0x33ED7D2A,0xE72719C1,0x154C9AC2,0x061C6936,0xF477EA35,0xAA64D611,0x580F5512,0x4B5FA6E6,0xB93425E5,0x6DFE410E,0x9F95C20D,0x8CC531F9,0x7EAEB2FA, + 0x30E349B1,0xC288CAB2,0xD1D83946,0x23B3BA45,0xF779DEAE,0x05125DAD,0x1642AE59,0xE4292D5A,0xBA3A117E,0x4851927D,0x5B016189,0xA96AE28A,0x7DA08661,0x8FCB0562,0x9C9BF696,0x6EF07595, + 0x417B1DBC,0xB3109EBF,0xA0406D4B,0x522BEE48,0x86E18AA3,0x748A09A0,0x67DAFA54,0x95B17957,0xCBA24573,0x39C9C670,0x2A993584,0xD8F2B687,0x0C38D26C,0xFE53516F,0xED03A29B,0x1F682198, + 0x5125DAD3,0xA34E59D0,0xB01EAA24,0x42752927,0x96BF4DCC,0x64D4CECF,0x77843D3B,0x85EFBE38,0xDBFC821C,0x2997011F,0x3AC7F2EB,0xC8AC71E8,0x1C661503,0xEE0D9600,0xFD5D65F4,0x0F36E6F7, + 0x61C69362,0x93AD1061,0x80FDE395,0x72966096,0xA65C047D,0x5437877E,0x4767748A,0xB50CF789,0xEB1FCBAD,0x197448AE,0x0A24BB5A,0xF84F3859,0x2C855CB2,0xDEEEDFB1,0xCDBE2C45,0x3FD5AF46, + 0x7198540D,0x83F3D70E,0x90A324FA,0x62C8A7F9,0xB602C312,0x44694011,0x5739B3E5,0xA55230E6,0xFB410CC2,0x092A8FC1,0x1A7A7C35,0xE811FF36,0x3CDB9BDD,0xCEB018DE,0xDDE0EB2A,0x2F8B6829, + 0x82F63B78,0x709DB87B,0x63CD4B8F,0x91A6C88C,0x456CAC67,0xB7072F64,0xA457DC90,0x563C5F93,0x082F63B7,0xFA44E0B4,0xE9141340,0x1B7F9043,0xCFB5F4A8,0x3DDE77AB,0x2E8E845F,0xDCE5075C, + 0x92A8FC17,0x60C37F14,0x73938CE0,0x81F80FE3,0x55326B08,0xA759E80B,0xB4091BFF,0x466298FC,0x1871A4D8,0xEA1A27DB,0xF94AD42F,0x0B21572C,0xDFEB33C7,0x2D80B0C4,0x3ED04330,0xCCBBC033, + 0xA24BB5A6,0x502036A5,0x4370C551,0xB11B4652,0x65D122B9,0x97BAA1BA,0x84EA524E,0x7681D14D,0x2892ED69,0xDAF96E6A,0xC9A99D9E,0x3BC21E9D,0xEF087A76,0x1D63F975,0x0E330A81,0xFC588982, + 0xB21572C9,0x407EF1CA,0x532E023E,0xA145813D,0x758FE5D6,0x87E466D5,0x94B49521,0x66DF1622,0x38CC2A06,0xCAA7A905,0xD9F75AF1,0x2B9CD9F2,0xFF56BD19,0x0D3D3E1A,0x1E6DCDEE,0xEC064EED, + 0xC38D26C4,0x31E6A5C7,0x22B65633,0xD0DDD530,0x0417B1DB,0xF67C32D8,0xE52CC12C,0x1747422F,0x49547E0B,0xBB3FFD08,0xA86F0EFC,0x5A048DFF,0x8ECEE914,0x7CA56A17,0x6FF599E3,0x9D9E1AE0, + 0xD3D3E1AB,0x21B862A8,0x32E8915C,0xC083125F,0x144976B4,0xE622F5B7,0xF5720643,0x07198540,0x590AB964,0xAB613A67,0xB831C993,0x4A5A4A90,0x9E902E7B,0x6CFBAD78,0x7FAB5E8C,0x8DC0DD8F, + 0xE330A81A,0x115B2B19,0x020BD8ED,0xF0605BEE,0x24AA3F05,0xD6C1BC06,0xC5914FF2,0x37FACCF1,0x69E9F0D5,0x9B8273D6,0x88D28022,0x7AB90321,0xAE7367CA,0x5C18E4C9,0x4F48173D,0xBD23943E, + 0xF36E6F75,0x0105EC76,0x12551F82,0xE03E9C81,0x34F4F86A,0xC69F7B69,0xD5CF889D,0x27A40B9E,0x79B737BA,0x8BDCB4B9,0x988C474D,0x6AE7C44E,0xBE2DA0A5,0x4C4623A6,0x5F16D052,0xAD7D5351 +#endif }; +#endif // Known size hash // It is ok to call ImHashData on a string with known length but the ### operator won't be supported. // FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. -ImU32 ImHashData(const void* data_p, size_t data_size, ImU32 seed) +ImGuiID ImHashData(const void* data_p, size_t data_size, ImGuiID seed) { ImU32 crc = ~seed; const unsigned char* data = (const unsigned char*)data_p; + const unsigned char *data_end = (const unsigned char*)data_p + data_size; +#ifndef IMGUI_ENABLE_SSE4_2_CRC const ImU32* crc32_lut = GCrc32LookupTable; - while (data_size-- != 0) + while (data < data_end) crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ *data++]; return ~crc; +#else + while (data + 4 <= data_end) + { + crc = _mm_crc32_u32(crc, *(ImU32*)data); + data += 4; + } + while (data < data_end) + crc = _mm_crc32_u8(crc, *data++); + return ~crc; +#endif } // Zero-terminated string hash, with support for ### to reset back to seed value @@ -1532,12 +2320,14 @@ ImU32 ImHashData(const void* data_p, size_t data_size, ImU32 seed) // - If we reach ### in the string we discard the hash so far and reset to the seed. // - We don't do 'current += 2; continue;' after handling ### to keep the code smaller/faster (measured ~10% diff in Debug build) // FIXME-OPT: Replace with e.g. FNV1a hash? CRC32 pretty much randomly access 1KB. Need to do proper measurements. -ImU32 ImHashStr(const char* data_p, size_t data_size, ImU32 seed) +ImGuiID ImHashStr(const char* data_p, size_t data_size, ImGuiID seed) { seed = ~seed; ImU32 crc = seed; const unsigned char* data = (const unsigned char*)data_p; +#ifndef IMGUI_ENABLE_SSE4_2_CRC const ImU32* crc32_lut = GCrc32LookupTable; +#endif if (data_size != 0) { while (data_size-- != 0) @@ -1545,7 +2335,11 @@ ImU32 ImHashStr(const char* data_p, size_t data_size, ImU32 seed) unsigned char c = *data++; if (c == '#' && data_size >= 2 && data[0] == '#' && data[1] == '#') crc = seed; +#ifndef IMGUI_ENABLE_SSE4_2_CRC crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; +#else + crc = _mm_crc32_u8(crc, c); +#endif } } else @@ -1554,64 +2348,90 @@ ImU32 ImHashStr(const char* data_p, size_t data_size, ImU32 seed) { if (c == '#' && data[0] == '#' && data[1] == '#') crc = seed; +#ifndef IMGUI_ENABLE_SSE4_2_CRC crc = (crc >> 8) ^ crc32_lut[(crc & 0xFF) ^ c]; +#else + crc = _mm_crc32_u8(crc, c); +#endif } } return ~crc; } -FILE* ImFileOpen(const char* filename, const char* mode) +//----------------------------------------------------------------------------- +// [SECTION] MISC HELPERS/UTILITIES (File functions) +//----------------------------------------------------------------------------- + +// Default file functions +#ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + +ImFileHandle ImFileOpen(const char* filename, const char* mode) { -#if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__GNUC__) - // We need a fopen() wrapper because MSVC/Windows fopen doesn't handle UTF-8 filenames. Converting both strings from UTF-8 to wchar format (using a single allocation, because we can) - const int filename_wsize = ImTextCountCharsFromUtf8(filename, NULL) + 1; - const int mode_wsize = ImTextCountCharsFromUtf8(mode, NULL) + 1; - ImVector buf; - buf.resize(filename_wsize + mode_wsize); - ImTextStrFromUtf8(&buf[0], filename_wsize, filename, NULL); - ImTextStrFromUtf8(&buf[filename_wsize], mode_wsize, mode, NULL); - return _wfopen((wchar_t*)&buf[0], (wchar_t*)&buf[filename_wsize]); +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && (defined(__MINGW32__) || (!defined(__CYGWIN__) && !defined(__GNUC__))) + // We need a fopen() wrapper because MSVC/Windows fopen doesn't handle UTF-8 filenames. + // Previously we used ImTextCountCharsFromUtf8/ImTextStrFromUtf8 here but we now need to support ImWchar16 and ImWchar32! + const int filename_wsize = ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, NULL, 0); + const int mode_wsize = ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, NULL, 0); + + // Use stack buffer if possible, otherwise heap buffer. Sizes include zero terminator. + // We don't rely on current ImGuiContext as this is implied to be a helper function which doesn't depend on it (see #7314). + wchar_t local_temp_stack[FILENAME_MAX]; + ImVector local_temp_heap; + if (filename_wsize + mode_wsize > IM_ARRAYSIZE(local_temp_stack)) + local_temp_heap.resize(filename_wsize + mode_wsize); + wchar_t* filename_wbuf = local_temp_heap.Data ? local_temp_heap.Data : local_temp_stack; + wchar_t* mode_wbuf = filename_wbuf + filename_wsize; + ::MultiByteToWideChar(CP_UTF8, 0, filename, -1, filename_wbuf, filename_wsize); + ::MultiByteToWideChar(CP_UTF8, 0, mode, -1, mode_wbuf, mode_wsize); + return ::_wfopen(filename_wbuf, mode_wbuf); #else return fopen(filename, mode); #endif } -// Load file content into memory +// We should in theory be using fseeko()/ftello() with off_t and _fseeki64()/_ftelli64() with __int64, waiting for the PR that does that in a very portable pre-C++11 zero-warnings way. +bool ImFileClose(ImFileHandle f) { return fclose(f) == 0; } +ImU64 ImFileGetSize(ImFileHandle f) { long off = 0, sz = 0; return ((off = ftell(f)) != -1 && !fseek(f, 0, SEEK_END) && (sz = ftell(f)) != -1 && !fseek(f, off, SEEK_SET)) ? (ImU64)sz : (ImU64)-1; } +ImU64 ImFileRead(void* data, ImU64 sz, ImU64 count, ImFileHandle f) { return fread(data, (size_t)sz, (size_t)count, f); } +ImU64 ImFileWrite(const void* data, ImU64 sz, ImU64 count, ImFileHandle f) { return fwrite(data, (size_t)sz, (size_t)count, f); } +#endif // #ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + +// Helper: Load file content into memory // Memory allocated with IM_ALLOC(), must be freed by user using IM_FREE() == ImGui::MemFree() -void* ImFileLoadToMemory(const char* filename, const char* file_open_mode, size_t* out_file_size, int padding_bytes) +// This can't really be used with "rt" because fseek size won't match read size. +void* ImFileLoadToMemory(const char* filename, const char* mode, size_t* out_file_size, int padding_bytes) { - IM_ASSERT(filename && file_open_mode); + IM_ASSERT(filename && mode); if (out_file_size) *out_file_size = 0; - FILE* f; - if ((f = ImFileOpen(filename, file_open_mode)) == NULL) + ImFileHandle f; + if ((f = ImFileOpen(filename, mode)) == NULL) return NULL; - long file_size_signed; - if (fseek(f, 0, SEEK_END) || (file_size_signed = ftell(f)) == -1 || fseek(f, 0, SEEK_SET)) + size_t file_size = (size_t)ImFileGetSize(f); + if (file_size == (size_t)-1) { - fclose(f); + ImFileClose(f); return NULL; } - size_t file_size = (size_t)file_size_signed; void* file_data = IM_ALLOC(file_size + padding_bytes); if (file_data == NULL) { - fclose(f); + ImFileClose(f); return NULL; } - if (fread(file_data, 1, file_size, f) != file_size) + if (ImFileRead(file_data, 1, file_size, f) != file_size) { - fclose(f); + ImFileClose(f); IM_FREE(file_data); return NULL; } if (padding_bytes > 0) memset((void*)(((char*)file_data) + file_size), 0, (size_t)padding_bytes); - fclose(f); + ImFileClose(f); if (out_file_size) *out_file_size = file_size; @@ -1622,79 +2442,72 @@ void* ImFileLoadToMemory(const char* filename, const char* file_open_mode, size_ // [SECTION] MISC HELPERS/UTILITIES (ImText* functions) //----------------------------------------------------------------------------- -// Convert UTF-8 to 32-bits character, process single character input. -// Based on stb_from_utf8() from github.com/nothings/stb/ +IM_MSVC_RUNTIME_CHECKS_OFF + +// Convert UTF-8 to 32-bit character, process single character input. +// A nearly-branchless UTF-8 decoder, based on work of Christopher Wellons (https://github.com/skeeto/branchless-utf8). // We handle UTF-8 decoding error by skipping forward. int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end) { - unsigned int c = (unsigned int)-1; - const unsigned char* str = (const unsigned char*)in_text; - if (!(*str & 0x80)) + static const char lengths[32] = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2, 3, 3, 4, 0 }; + static const int masks[] = { 0x00, 0x7f, 0x1f, 0x0f, 0x07 }; + static const uint32_t mins[] = { 0x400000, 0, 0x80, 0x800, 0x10000 }; + static const int shiftc[] = { 0, 18, 12, 6, 0 }; + static const int shifte[] = { 0, 6, 4, 2, 0 }; + int len = lengths[*(const unsigned char*)in_text >> 3]; + int wanted = len + (len ? 0 : 1); + + if (in_text_end == NULL) + in_text_end = in_text + wanted; // Max length, nulls will be taken into account. + + // Copy at most 'len' bytes, stop copying at 0 or past in_text_end. Branch predictor does a good job here, + // so it is fast even with excessive branching. + unsigned char s[4]; + s[0] = in_text + 0 < in_text_end ? in_text[0] : 0; + s[1] = in_text + 1 < in_text_end ? in_text[1] : 0; + s[2] = in_text + 2 < in_text_end ? in_text[2] : 0; + s[3] = in_text + 3 < in_text_end ? in_text[3] : 0; + + // Assume a four-byte character and load four bytes. Unused bits are shifted out. + *out_char = (uint32_t)(s[0] & masks[len]) << 18; + *out_char |= (uint32_t)(s[1] & 0x3f) << 12; + *out_char |= (uint32_t)(s[2] & 0x3f) << 6; + *out_char |= (uint32_t)(s[3] & 0x3f) << 0; + *out_char >>= shiftc[len]; + + // Accumulate the various error conditions. + int e = 0; + e = (*out_char < mins[len]) << 6; // non-canonical encoding + e |= ((*out_char >> 11) == 0x1b) << 7; // surrogate half? + e |= (*out_char > IM_UNICODE_CODEPOINT_MAX) << 8; // out of range we can store in ImWchar (FIXME: May evolve) + e |= (s[1] & 0xc0) >> 2; + e |= (s[2] & 0xc0) >> 4; + e |= (s[3] ) >> 6; + e ^= 0x2a; // top two bits of each tail byte correct? + e >>= shifte[len]; + + if (e) { - c = (unsigned int)(*str++); - *out_char = c; - return 1; + // No bytes are consumed when *in_text == 0 || in_text == in_text_end. + // One byte is consumed in case of invalid first byte of in_text. + // All available bytes (at most `len` bytes) are consumed on incomplete/invalid second to last bytes. + // Invalid or incomplete input may consume less bytes than wanted, therefore every byte has to be inspected in s. + wanted = ImMin(wanted, !!s[0] + !!s[1] + !!s[2] + !!s[3]); + *out_char = IM_UNICODE_CODEPOINT_INVALID; } - if ((*str & 0xe0) == 0xc0) - { - *out_char = 0xFFFD; // will be invalid but not end of string - if (in_text_end && in_text_end - (const char*)str < 2) return 1; - if (*str < 0xc2) return 2; - c = (unsigned int)((*str++ & 0x1f) << 6); - if ((*str & 0xc0) != 0x80) return 2; - c += (*str++ & 0x3f); - *out_char = c; - return 2; - } - if ((*str & 0xf0) == 0xe0) - { - *out_char = 0xFFFD; // will be invalid but not end of string - if (in_text_end && in_text_end - (const char*)str < 3) return 1; - if (*str == 0xe0 && (str[1] < 0xa0 || str[1] > 0xbf)) return 3; - if (*str == 0xed && str[1] > 0x9f) return 3; // str[1] < 0x80 is checked below - c = (unsigned int)((*str++ & 0x0f) << 12); - if ((*str & 0xc0) != 0x80) return 3; - c += (unsigned int)((*str++ & 0x3f) << 6); - if ((*str & 0xc0) != 0x80) return 3; - c += (*str++ & 0x3f); - *out_char = c; - return 3; - } - if ((*str & 0xf8) == 0xf0) - { - *out_char = 0xFFFD; // will be invalid but not end of string - if (in_text_end && in_text_end - (const char*)str < 4) return 1; - if (*str > 0xf4) return 4; - if (*str == 0xf0 && (str[1] < 0x90 || str[1] > 0xbf)) return 4; - if (*str == 0xf4 && str[1] > 0x8f) return 4; // str[1] < 0x80 is checked below - c = (unsigned int)((*str++ & 0x07) << 18); - if ((*str & 0xc0) != 0x80) return 4; - c += (unsigned int)((*str++ & 0x3f) << 12); - if ((*str & 0xc0) != 0x80) return 4; - c += (unsigned int)((*str++ & 0x3f) << 6); - if ((*str & 0xc0) != 0x80) return 4; - c += (*str++ & 0x3f); - // utf-8 encodings of values used in surrogate pairs are invalid - if ((c & 0xFFFFF800) == 0xD800) return 4; - *out_char = c; - return 4; - } - *out_char = 0; - return 0; + + return wanted; } int ImTextStrFromUtf8(ImWchar* buf, int buf_size, const char* in_text, const char* in_text_end, const char** in_text_remaining) { ImWchar* buf_out = buf; ImWchar* buf_end = buf + buf_size; - while (buf_out < buf_end-1 && (!in_text_end || in_text < in_text_end) && *in_text) + while (buf_out < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) { unsigned int c; in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); - if (c == 0) - break; - if (c < 0x10000) // FIXME: Losing characters that don't fit in 2 bytes - *buf_out++ = (ImWchar)c; + *buf_out++ = (ImWchar)c; } *buf_out = 0; if (in_text_remaining) @@ -1709,16 +2522,13 @@ int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end) { unsigned int c; in_text += ImTextCharFromUtf8(&c, in_text, in_text_end); - if (c == 0) - break; - if (c < 0x10000) - char_count++; + char_count++; } return char_count; } // Based on stb_to_utf8() from github.com/nothings/stb/ -static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) +static inline int ImTextCharToUtf8_inline(char* buf, int buf_size, unsigned int c) { if (c < 0x80) { @@ -1732,11 +2542,15 @@ static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) buf[1] = (char)(0x80 + (c & 0x3f)); return 2; } - if (c >= 0xdc00 && c < 0xe000) + if (c < 0x10000) { - return 0; + if (buf_size < 3) return 0; + buf[0] = (char)(0xe0 + (c >> 12)); + buf[1] = (char)(0x80 + ((c >> 6) & 0x3f)); + buf[2] = (char)(0x80 + ((c ) & 0x3f)); + return 3; } - if (c >= 0xd800 && c < 0xdc00) + if (c <= 0x10FFFF) { if (buf_size < 4) return 0; buf[0] = (char)(0xf0 + (c >> 18)); @@ -1745,46 +2559,47 @@ static inline int ImTextCharToUtf8(char* buf, int buf_size, unsigned int c) buf[3] = (char)(0x80 + ((c ) & 0x3f)); return 4; } - //else if (c < 0x10000) - { - if (buf_size < 3) return 0; - buf[0] = (char)(0xe0 + (c >> 12)); - buf[1] = (char)(0x80 + ((c>> 6) & 0x3f)); - buf[2] = (char)(0x80 + ((c ) & 0x3f)); - return 3; - } + // Invalid code point, the max unicode is 0x10FFFF + return 0; +} + +const char* ImTextCharToUtf8(char out_buf[5], unsigned int c) +{ + int count = ImTextCharToUtf8_inline(out_buf, 5, c); + out_buf[count] = 0; + return out_buf; } // Not optimal but we very rarely use this function. int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end) { - unsigned int dummy = 0; - return ImTextCharFromUtf8(&dummy, in_text, in_text_end); + unsigned int unused = 0; + return ImTextCharFromUtf8(&unused, in_text, in_text_end); } static inline int ImTextCountUtf8BytesFromChar(unsigned int c) { if (c < 0x80) return 1; if (c < 0x800) return 2; - if (c >= 0xdc00 && c < 0xe000) return 0; - if (c >= 0xd800 && c < 0xdc00) return 4; + if (c < 0x10000) return 3; + if (c <= 0x10FFFF) return 4; return 3; } -int ImTextStrToUtf8(char* buf, int buf_size, const ImWchar* in_text, const ImWchar* in_text_end) +int ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end) { - char* buf_out = buf; - const char* buf_end = buf + buf_size; - while (buf_out < buf_end-1 && (!in_text_end || in_text < in_text_end) && *in_text) + char* buf_p = out_buf; + const char* buf_end = out_buf + out_buf_size; + while (buf_p < buf_end - 1 && (!in_text_end || in_text < in_text_end) && *in_text) { unsigned int c = (unsigned int)(*in_text++); if (c < 0x80) - *buf_out++ = (char)c; + *buf_p++ = (char)c; else - buf_out += ImTextCharToUtf8(buf_out, (int)(buf_end-buf_out-1), c); + buf_p += ImTextCharToUtf8_inline(buf_p, (int)(buf_end - buf_p - 1), c); } - *buf_out = 0; - return (int)(buf_out - buf); + *buf_p = 0; + return (int)(buf_p - out_buf); } int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end) @@ -1801,14 +2616,50 @@ int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_e return bytes_count; } +const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr) +{ + while (in_text_curr > in_text_start) + { + in_text_curr--; + if ((*in_text_curr & 0xC0) != 0x80) + return in_text_curr; + } + return in_text_start; +} + +int ImTextCountLines(const char* in_text, const char* in_text_end) +{ + if (in_text_end == NULL) + in_text_end = in_text + ImStrlen(in_text); // FIXME-OPT: Not optimal approach, discourage use for now. + int count = 0; + while (in_text < in_text_end) + { + const char* line_end = (const char*)ImMemchr(in_text, '\n', in_text_end - in_text); + in_text = line_end ? line_end + 1 : in_text_end; + count++; + } + return count; +} + +IM_MSVC_RUNTIME_CHECKS_RESTORE + //----------------------------------------------------------------------------- -// [SECTION] MISC HELPERS/UTILTIES (Color functions) +// [SECTION] MISC HELPERS/UTILITIES (Color functions) // Note: The Convert functions are early design which are not consistent with other API. //----------------------------------------------------------------------------- +IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b) +{ + float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; + int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t); + int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t); + int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t); + return IM_COL32(r, g, b, 0xFF); +} + ImVec4 ImGui::ColorConvertU32ToFloat4(ImU32 in) { - float s = 1.0f/255.0f; + float s = 1.0f / 255.0f; return ImVec4( ((in >> IM_COL32_R_SHIFT) & 0xFF) * s, ((in >> IM_COL32_G_SHIFT) & 0xFF) * s, @@ -1859,7 +2710,7 @@ void ImGui::ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& return; } - h = ImFmod(h, 1.0f) / (60.0f/360.0f); + h = ImFmod(h, 1.0f) / (60.0f / 360.0f); int i = (int)h; float f = h - (float)i; float p = v * (1.0f - s); @@ -1877,56 +2728,22 @@ void ImGui::ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& } } -ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) -{ - ImGuiStyle& style = GImGui->Style; - ImVec4 c = style.Colors[idx]; - c.w *= style.Alpha * alpha_mul; - return ColorConvertFloat4ToU32(c); -} - -ImU32 ImGui::GetColorU32(const ImVec4& col) -{ - ImGuiStyle& style = GImGui->Style; - ImVec4 c = col; - c.w *= style.Alpha; - return ColorConvertFloat4ToU32(c); -} - -const ImVec4& ImGui::GetStyleColorVec4(ImGuiCol idx) -{ - ImGuiStyle& style = GImGui->Style; - return style.Colors[idx]; -} - -ImU32 ImGui::GetColorU32(ImU32 col) -{ - float style_alpha = GImGui->Style.Alpha; - if (style_alpha >= 1.0f) - return col; - ImU32 a = (col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT; - a = (ImU32)(a * style_alpha); // We don't need to clamp 0..255 because Style.Alpha is in 0..1 range. - return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); -} - //----------------------------------------------------------------------------- // [SECTION] ImGuiStorage // Helper: Key->value storage //----------------------------------------------------------------------------- // std::lower_bound but without the bullshit -static ImGuiStorage::ImGuiStoragePair* LowerBound(ImVector& data, ImGuiID key) +ImGuiStoragePair* ImLowerBound(ImGuiStoragePair* in_begin, ImGuiStoragePair* in_end, ImGuiID key) { - ImGuiStorage::ImGuiStoragePair* first = data.Data; - ImGuiStorage::ImGuiStoragePair* last = data.Data + data.Size; - size_t count = (size_t)(last - first); - while (count > 0) + ImGuiStoragePair* in_p = in_begin; + for (size_t count = (size_t)(in_end - in_p); count > 0; ) { size_t count2 = count >> 1; - ImGuiStorage::ImGuiStoragePair* mid = first + count2; + ImGuiStoragePair* mid = in_p + count2; if (mid->key < key) { - first = ++mid; + in_p = ++mid; count -= count2 + 1; } else @@ -1934,30 +2751,28 @@ static ImGuiStorage::ImGuiStoragePair* LowerBound(ImVectorkey; + ImGuiID rhs_v = ((const ImGuiStoragePair*)rhs)->key; + return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0); } // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. void ImGuiStorage::BuildSortByKey() { - struct StaticFunc - { - static int IMGUI_CDECL PairCompareByID(const void* lhs, const void* rhs) - { - // We can't just do a subtraction because qsort uses signed integers and subtracting our ID doesn't play well with that. - if (((const ImGuiStoragePair*)lhs)->key > ((const ImGuiStoragePair*)rhs)->key) return +1; - if (((const ImGuiStoragePair*)lhs)->key < ((const ImGuiStoragePair*)rhs)->key) return -1; - return 0; - } - }; - if (Data.Size > 1) - ImQsort(Data.Data, (size_t)Data.Size, sizeof(ImGuiStoragePair), StaticFunc::PairCompareByID); + ImQsort(Data.Data, (size_t)Data.Size, sizeof(ImGuiStoragePair), PairComparerByID); } int ImGuiStorage::GetInt(ImGuiID key, int default_val) const { - ImGuiStoragePair* it = LowerBound(const_cast&>(Data), key); - if (it == Data.end() || it->key != key) + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) return default_val; return it->val_i; } @@ -1969,16 +2784,16 @@ bool ImGuiStorage::GetBool(ImGuiID key, bool default_val) const float ImGuiStorage::GetFloat(ImGuiID key, float default_val) const { - ImGuiStoragePair* it = LowerBound(const_cast&>(Data), key); - if (it == Data.end() || it->key != key) + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) return default_val; return it->val_f; } void* ImGuiStorage::GetVoidPtr(ImGuiID key) const { - ImGuiStoragePair* it = LowerBound(const_cast&>(Data), key); - if (it == Data.end() || it->key != key) + ImGuiStoragePair* it = ImLowerBound(const_cast(Data.Data), const_cast(Data.Data + Data.Size), key); + if (it == Data.Data + Data.Size || it->key != key) return NULL; return it->val_p; } @@ -1986,8 +2801,8 @@ void* ImGuiStorage::GetVoidPtr(ImGuiID key) const // References are only valid until a new value is added to the storage. Calling a Set***() function or a Get***Ref() function invalidates the pointer. int* ImGuiStorage::GetIntRef(ImGuiID key, int default_val) { - ImGuiStoragePair* it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) it = Data.insert(it, ImGuiStoragePair(key, default_val)); return &it->val_i; } @@ -1999,16 +2814,16 @@ bool* ImGuiStorage::GetBoolRef(ImGuiID key, bool default_val) float* ImGuiStorage::GetFloatRef(ImGuiID key, float default_val) { - ImGuiStoragePair* it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) it = Data.insert(it, ImGuiStoragePair(key, default_val)); return &it->val_f; } void** ImGuiStorage::GetVoidPtrRef(ImGuiID key, void* default_val) { - ImGuiStoragePair* it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) it = Data.insert(it, ImGuiStoragePair(key, default_val)); return &it->val_p; } @@ -2016,13 +2831,11 @@ void** ImGuiStorage::GetVoidPtrRef(ImGuiID key, void* default_val) // FIXME-OPT: Need a way to reuse the result of lower_bound when doing GetInt()/SetInt() - not too bad because it only happens on explicit interaction (maximum one a frame) void ImGuiStorage::SetInt(ImGuiID key, int val) { - ImGuiStoragePair* it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) Data.insert(it, ImGuiStoragePair(key, val)); - return; - } - it->val_i = val; + else + it->val_i = val; } void ImGuiStorage::SetBool(ImGuiID key, bool val) @@ -2032,24 +2845,20 @@ void ImGuiStorage::SetBool(ImGuiID key, bool val) void ImGuiStorage::SetFloat(ImGuiID key, float val) { - ImGuiStoragePair* it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) Data.insert(it, ImGuiStoragePair(key, val)); - return; - } - it->val_f = val; + else + it->val_f = val; } void ImGuiStorage::SetVoidPtr(ImGuiID key, void* val) { - ImGuiStoragePair* it = LowerBound(Data, key); - if (it == Data.end() || it->key != key) - { + ImGuiStoragePair* it = ImLowerBound(Data.Data, Data.Data + Data.Size, key); + if (it == Data.Data + Data.Size || it->key != key) Data.insert(it, ImGuiStoragePair(key, val)); - return; - } - it->val_p = val; + else + it->val_p = val; } void ImGuiStorage::SetAllInt(int v) @@ -2057,24 +2866,22 @@ void ImGuiStorage::SetAllInt(int v) for (int i = 0; i < Data.Size; i++) Data[i].val_i = v; } +IM_MSVC_RUNTIME_CHECKS_RESTORE //----------------------------------------------------------------------------- // [SECTION] ImGuiTextFilter //----------------------------------------------------------------------------- // Helper: Parse and apply text filters. In format "aaaaa[,bbbb][,ccccc]" -ImGuiTextFilter::ImGuiTextFilter(const char* default_filter) +ImGuiTextFilter::ImGuiTextFilter(const char* default_filter) //-V1077 { + InputBuf[0] = 0; + CountGrep = 0; if (default_filter) { ImStrncpy(InputBuf, default_filter, IM_ARRAYSIZE(InputBuf)); Build(); } - else - { - InputBuf[0] = 0; - CountGrep = 0; - } } bool ImGuiTextFilter::Draw(const char* label, float width) @@ -2108,36 +2915,34 @@ void ImGuiTextFilter::ImGuiTextRange::split(char separator, ImVector f.b && ImCharIsBlankA(f.e[-1])) f.e--; if (f.empty()) continue; - if (Filters[i].b[0] != '-') + if (f.b[0] != '-') CountGrep += 1; } } bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const { - if (Filters.empty()) + if (Filters.Size == 0) return true; if (text == NULL) - text = ""; + text = text_end = ""; - for (int i = 0; i != Filters.Size; i++) + for (const ImGuiTextRange& f : Filters) { - const ImGuiTextRange& f = Filters[i]; - if (f.empty()) + if (f.b == f.e) continue; if (f.b[0] == '-') { @@ -2161,7 +2966,7 @@ bool ImGuiTextFilter::PassFilter(const char* text, const char* text_end) const } //----------------------------------------------------------------------------- -// [SECTION] ImGuiTextBuffer +// [SECTION] ImGuiTextBuffer, ImGuiTextIndex //----------------------------------------------------------------------------- // On some platform vsnprintf() takes va_list by reference and modifies it. @@ -2178,7 +2983,7 @@ char ImGuiTextBuffer::EmptyString[1] = { 0 }; void ImGuiTextBuffer::append(const char* str, const char* str_end) { - int len = str_end ? (int)(str_end - str) : (int)strlen(str); + int len = str_end ? (int)(str_end - str) : (int)ImStrlen(str); // Add zero-terminator the first time const int write_off = (Buf.Size != 0) ? Buf.Size : 1; @@ -2229,145 +3034,617 @@ void ImGuiTextBuffer::appendfv(const char* fmt, va_list args) va_end(args_copy); } -//----------------------------------------------------------------------------- -// [SECTION] ImGuiListClipper -// This is currently not as flexible/powerful as it should be and really confusing/spaghetti, mostly because we changed -// the API mid-way through development and support two ways to using the clipper, needs some rework (see TODO) -//----------------------------------------------------------------------------- - -// Helper to calculate coarse clipping of large list of evenly sized items. -// NB: Prefer using the ImGuiListClipper higher-level helper if you can! Read comments and instructions there on how those use this sort of pattern. -// NB: 'items_count' is only used to clamp the result, if you don't know your count you can use INT_MAX -void ImGui::CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end) +void ImGuiTextIndex::append(const char* base, int old_size, int new_size) { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (g.LogEnabled) - { - // If logging is active, do not perform any clipping - *out_items_display_start = 0; - *out_items_display_end = items_count; + IM_ASSERT(old_size >= 0 && new_size >= old_size && new_size >= EndOffset); + if (old_size == new_size) return; - } - if (window->SkipItems) - { - *out_items_display_start = *out_items_display_end = 0; - return; - } - - // We create the union of the ClipRect and the NavScoringRect which at worst should be 1 page away from ClipRect - ImRect unclipped_rect = window->ClipRect; - if (g.NavMoveRequest) - unclipped_rect.Add(g.NavScoringRectScreen); - - const ImVec2 pos = window->DC.CursorPos; - int start = (int)((unclipped_rect.Min.y - pos.y) / items_height); - int end = (int)((unclipped_rect.Max.y - pos.y) / items_height); - - // When performing a navigation request, ensure we have one item extra in the direction we are moving to - if (g.NavMoveRequest && g.NavMoveClipDir == ImGuiDir_Up) - start--; - if (g.NavMoveRequest && g.NavMoveClipDir == ImGuiDir_Down) - end++; - - start = ImClamp(start, 0, items_count); - end = ImClamp(end + 1, start, items_count); - *out_items_display_start = start; - *out_items_display_end = end; + if (EndOffset == 0 || base[EndOffset - 1] == '\n') + LineOffsets.push_back(EndOffset); + const char* base_end = base + new_size; + for (const char* p = base + old_size; (p = (const char*)ImMemchr(p, '\n', base_end - p)) != 0; ) + if (++p < base_end) // Don't push a trailing offset on last \n + LineOffsets.push_back((int)(intptr_t)(p - base)); + EndOffset = ImMax(EndOffset, new_size); } -static void SetCursorPosYAndSetupDummyPrevLine(float pos_y, float line_height) +//----------------------------------------------------------------------------- +// [SECTION] ImGuiListClipper +//----------------------------------------------------------------------------- + +// FIXME-TABLE: This prevents us from using ImGuiListClipper _inside_ a table cell. +// The problem we have is that without a Begin/End scheme for rows using the clipper is ambiguous. +static bool GetSkipItemForListClipping() +{ + ImGuiContext& g = *GImGui; + return (g.CurrentTable ? g.CurrentTable->HostSkipItems : g.CurrentWindow->SkipItems); +} + +static void ImGuiListClipper_SortAndFuseRanges(ImVector& ranges, int offset = 0) +{ + if (ranges.Size - offset <= 1) + return; + + // Helper to order ranges and fuse them together if possible (bubble sort is fine as we are only sorting 2-3 entries) + for (int sort_end = ranges.Size - offset - 1; sort_end > 0; --sort_end) + for (int i = offset; i < sort_end + offset; ++i) + if (ranges[i].Min > ranges[i + 1].Min) + ImSwap(ranges[i], ranges[i + 1]); + + // Now fuse ranges together as much as possible. + for (int i = 1 + offset; i < ranges.Size; i++) + { + IM_ASSERT(!ranges[i].PosToIndexConvert && !ranges[i - 1].PosToIndexConvert); + if (ranges[i - 1].Max < ranges[i].Min) + continue; + ranges[i - 1].Min = ImMin(ranges[i - 1].Min, ranges[i].Min); + ranges[i - 1].Max = ImMax(ranges[i - 1].Max, ranges[i].Max); + ranges.erase(ranges.Data + i); + i--; + } +} + +static void ImGuiListClipper_SeekCursorAndSetupPrevLine(float pos_y, float line_height) { // Set cursor position and a few other things so that SetScrollHereY() and Columns() can work when seeking cursor. // FIXME: It is problematic that we have to do that here, because custom/equivalent end-user code would stumble on the same issue. - // The clipper should probably have a 4th step to display the last item in a regular manner. + // The clipper should probably have a final step to display the last item in a regular manner, maybe with an opt-out flag for data sets which may have costly seek? ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; + float off_y = pos_y - window->DC.CursorPos.y; window->DC.CursorPos.y = pos_y; - window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, pos_y); + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, pos_y - g.Style.ItemSpacing.y); window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y - line_height; // Setting those fields so that SetScrollHereY() can properly function after the end of our clipper usage. window->DC.PrevLineSize.y = (line_height - g.Style.ItemSpacing.y); // If we end up needing more accurate data (to e.g. use SameLine) we may as well make the clipper have a fourth step to let user process and display the last item in their list. - if (ImGuiColumns* columns = window->DC.CurrentColumns) + if (ImGuiOldColumns* columns = window->DC.CurrentColumns) columns->LineMinY = window->DC.CursorPos.y; // Setting this so that cell Y position are set properly + if (ImGuiTable* table = g.CurrentTable) + { + if (table->IsInsideRow) + ImGui::TableEndRow(table); + table->RowPosY2 = window->DC.CursorPos.y; + const int row_increase = (int)((off_y / line_height) + 0.5f); + //table->CurrentRow += row_increase; // Can't do without fixing TableEndRow() + table->RowBgColorCounter += row_increase; + } } -// Use case A: Begin() called from constructor with items_height<0, then called again from Sync() in StepNo 1 -// Use case B: Begin() called from constructor with items_height>0 -// FIXME-LEGACY: Ideally we should remove the Begin/End functions but they are part of the legacy API we still support. This is why some of the code in Step() calling Begin() and reassign some fields, spaghetti style. -void ImGuiListClipper::Begin(int count, float items_height) +ImGuiListClipper::ImGuiListClipper() { - ImGuiContext& g = *GImGui; + memset(this, 0, sizeof(*this)); +} + +ImGuiListClipper::~ImGuiListClipper() +{ + End(); +} + +void ImGuiListClipper::Begin(int items_count, float items_height) +{ + if (Ctx == NULL) + Ctx = ImGui::GetCurrentContext(); + + ImGuiContext& g = *Ctx; ImGuiWindow* window = g.CurrentWindow; + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Begin(%d,%.2f) in '%s'\n", items_count, items_height, window->Name); + + if (ImGuiTable* table = g.CurrentTable) + if (table->IsInsideRow) + ImGui::TableEndRow(table); StartPosY = window->DC.CursorPos.y; ItemsHeight = items_height; - ItemsCount = count; - StepNo = 0; - DisplayEnd = DisplayStart = -1; - if (ItemsHeight > 0.0f) - { - ImGui::CalcListClipping(ItemsCount, ItemsHeight, &DisplayStart, &DisplayEnd); // calculate how many to clip/display - if (DisplayStart > 0) - SetCursorPosYAndSetupDummyPrevLine(StartPosY + DisplayStart * ItemsHeight, ItemsHeight); // advance cursor - StepNo = 2; - } + ItemsCount = items_count; + DisplayStart = -1; + DisplayEnd = 0; + + // Acquire temporary buffer + if (++g.ClipperTempDataStacked > g.ClipperTempData.Size) + g.ClipperTempData.resize(g.ClipperTempDataStacked, ImGuiListClipperData()); + ImGuiListClipperData* data = &g.ClipperTempData[g.ClipperTempDataStacked - 1]; + data->Reset(this); + data->LossynessOffset = window->DC.CursorStartPosLossyness.y; + TempData = data; + StartSeekOffsetY = data->LossynessOffset; } void ImGuiListClipper::End() { - if (ItemsCount < 0) - return; - // In theory here we should assert that ImGui::GetCursorPosY() == StartPosY + DisplayEnd * ItemsHeight, but it feels saner to just seek at the end and not assert/crash the user. - if (ItemsCount < INT_MAX) - SetCursorPosYAndSetupDummyPrevLine(StartPosY + ItemsCount * ItemsHeight, ItemsHeight); // advance cursor + if (ImGuiListClipperData* data = (ImGuiListClipperData*)TempData) + { + // In theory here we should assert that we are already at the right position, but it seems saner to just seek at the end and not assert/crash the user. + ImGuiContext& g = *Ctx; + IMGUI_DEBUG_LOG_CLIPPER("Clipper: End() in '%s'\n", g.CurrentWindow->Name); + if (ItemsCount >= 0 && ItemsCount < INT_MAX && DisplayStart >= 0) + SeekCursorForItem(ItemsCount); + + // Restore temporary buffer and fix back pointers which may be invalidated when nesting + IM_ASSERT(data->ListClipper == this); + data->StepNo = data->Ranges.Size; + if (--g.ClipperTempDataStacked > 0) + { + data = &g.ClipperTempData[g.ClipperTempDataStacked - 1]; + data->ListClipper->TempData = data; + } + TempData = NULL; + } ItemsCount = -1; - StepNo = 3; +} + +void ImGuiListClipper::IncludeItemsByIndex(int item_begin, int item_end) +{ + ImGuiListClipperData* data = (ImGuiListClipperData*)TempData; + IM_ASSERT(DisplayStart < 0); // Only allowed after Begin() and if there has not been a specified range yet. + IM_ASSERT(item_begin <= item_end); + if (item_begin < item_end) + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(item_begin, item_end)); +} + +// This is already called while stepping. +// The ONLY reason you may want to call this is if you passed INT_MAX to ImGuiListClipper::Begin() because you couldn't step item count beforehand. +void ImGuiListClipper::SeekCursorForItem(int item_n) +{ + // - Perform the add and multiply with double to allow seeking through larger ranges. + // - StartPosY starts from ItemsFrozen, by adding SeekOffsetY we generally cancel that out (SeekOffsetY == LossynessOffset - ItemsFrozen * ItemsHeight). + // - The reason we store SeekOffsetY instead of inferring it, is because we want to allow user to perform Seek after the last step, where ImGuiListClipperData is already done. + float pos_y = (float)((double)StartPosY + StartSeekOffsetY + (double)item_n * ItemsHeight); + ImGuiListClipper_SeekCursorAndSetupPrevLine(pos_y, ItemsHeight); +} + +static bool ImGuiListClipper_StepInternal(ImGuiListClipper* clipper) +{ + ImGuiContext& g = *clipper->Ctx; + ImGuiWindow* window = g.CurrentWindow; + ImGuiListClipperData* data = (ImGuiListClipperData*)clipper->TempData; + IM_ASSERT(data != NULL && "Called ImGuiListClipper::Step() too many times, or before ImGuiListClipper::Begin() ?"); + + ImGuiTable* table = g.CurrentTable; + if (table && table->IsInsideRow) + ImGui::TableEndRow(table); + + // No items + if (clipper->ItemsCount == 0 || GetSkipItemForListClipping()) + return false; + + // While we are in frozen row state, keep displaying items one by one, unclipped + // FIXME: Could be stored as a table-agnostic state. + if (data->StepNo == 0 && table != NULL && !table->IsUnfrozenRows) + { + clipper->DisplayStart = data->ItemsFrozen; + clipper->DisplayEnd = ImMin(data->ItemsFrozen + 1, clipper->ItemsCount); + if (clipper->DisplayStart < clipper->DisplayEnd) + data->ItemsFrozen++; + return true; + } + + // Step 0: Let you process the first element (regardless of it being visible or not, so we can measure the element height) + bool calc_clipping = false; + if (data->StepNo == 0) + { + clipper->StartPosY = window->DC.CursorPos.y; + if (clipper->ItemsHeight <= 0.0f) + { + // Submit the first item (or range) so we can measure its height (generally the first range is 0..1) + data->Ranges.push_front(ImGuiListClipperRange::FromIndices(data->ItemsFrozen, data->ItemsFrozen + 1)); + clipper->DisplayStart = ImMax(data->Ranges[0].Min, data->ItemsFrozen); + clipper->DisplayEnd = ImMin(data->Ranges[0].Max, clipper->ItemsCount); + data->StepNo = 1; + return true; + } + calc_clipping = true; // If on the first step with known item height, calculate clipping. + } + + // Step 1: Let the clipper infer height from first range + if (clipper->ItemsHeight <= 0.0f) + { + IM_ASSERT(data->StepNo == 1); + if (table) + IM_ASSERT(table->RowPosY1 == clipper->StartPosY && table->RowPosY2 == window->DC.CursorPos.y); + + clipper->ItemsHeight = (window->DC.CursorPos.y - clipper->StartPosY) / (float)(clipper->DisplayEnd - clipper->DisplayStart); + bool affected_by_floating_point_precision = ImIsFloatAboveGuaranteedIntegerPrecision(clipper->StartPosY) || ImIsFloatAboveGuaranteedIntegerPrecision(window->DC.CursorPos.y); + if (affected_by_floating_point_precision) + clipper->ItemsHeight = window->DC.PrevLineSize.y + g.Style.ItemSpacing.y; // FIXME: Technically wouldn't allow multi-line entries. + if (clipper->ItemsHeight == 0.0f && clipper->ItemsCount == INT_MAX) // Accept that no item have been submitted if in indeterminate mode. + return false; + IM_ASSERT(clipper->ItemsHeight > 0.0f && "Unable to calculate item height! First item hasn't moved the cursor vertically!"); + calc_clipping = true; // If item height had to be calculated, calculate clipping afterwards. + } + + // Step 0 or 1: Calculate the actual ranges of visible elements. + const int already_submitted = clipper->DisplayEnd; + if (calc_clipping) + { + // Record seek offset, this is so ImGuiListClipper::Seek() can be called after ImGuiListClipperData is done + clipper->StartSeekOffsetY = (double)data->LossynessOffset - data->ItemsFrozen * (double)clipper->ItemsHeight; + + if (g.LogEnabled) + { + // If logging is active, do not perform any clipping + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(0, clipper->ItemsCount)); + } + else + { + // Add range selected to be included for navigation + const bool is_nav_request = (g.NavMoveScoringItems && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (is_nav_request) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(g.NavScoringNoClipRect.Min.y, g.NavScoringNoClipRect.Max.y, 0, 0)); + if (is_nav_request && (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && g.NavTabbingDir == -1) + data->Ranges.push_back(ImGuiListClipperRange::FromIndices(clipper->ItemsCount - 1, clipper->ItemsCount)); + + // Add focused/active item + ImRect nav_rect_abs = ImGui::WindowRectRelToAbs(window, window->NavRectRel[0]); + if (g.NavId != 0 && window->NavLastIds[0] == g.NavId) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(nav_rect_abs.Min.y, nav_rect_abs.Max.y, 0, 0)); + + // Add visible range + float min_y = window->ClipRect.Min.y; + float max_y = window->ClipRect.Max.y; + + // Add box selection range + ImGuiBoxSelectState* bs = &g.BoxSelectState; + if (bs->IsActive && bs->Window == window) + { + // FIXME: Selectable() use of half-ItemSpacing isn't consistent in matter of layout, as ItemAdd(bb) stray above ItemSize()'s CursorPos. + // RangeSelect's BoxSelect relies on comparing overlap of previous and current rectangle and is sensitive to that. + // As a workaround we currently half ItemSpacing worth on each side. + min_y -= g.Style.ItemSpacing.y; + max_y += g.Style.ItemSpacing.y; + + // Box-select on 2D area requires different clipping. + if (bs->UnclipMode) + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(bs->UnclipRect.Min.y, bs->UnclipRect.Max.y, 0, 0)); + } + + const int off_min = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Up) ? -1 : 0; + const int off_max = (is_nav_request && g.NavMoveClipDir == ImGuiDir_Down) ? 1 : 0; + data->Ranges.push_back(ImGuiListClipperRange::FromPositions(min_y, max_y, off_min, off_max)); + } + + // Convert position ranges to item index ranges + // - Very important: when a starting position is after our maximum item, we set Min to (ItemsCount - 1). This allows us to handle most forms of wrapping. + // - Due to how Selectable extra padding they tend to be "unaligned" with exact unit in the item list, + // which with the flooring/ceiling tend to lead to 2 items instead of one being submitted. + for (ImGuiListClipperRange& range : data->Ranges) + if (range.PosToIndexConvert) + { + int m1 = (int)(((double)range.Min - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight); + int m2 = (int)((((double)range.Max - window->DC.CursorPos.y - data->LossynessOffset) / clipper->ItemsHeight) + 0.999999f); + range.Min = ImClamp(already_submitted + m1 + range.PosToIndexOffsetMin, already_submitted, clipper->ItemsCount - 1); + range.Max = ImClamp(already_submitted + m2 + range.PosToIndexOffsetMax, range.Min + 1, clipper->ItemsCount); + range.PosToIndexConvert = false; + } + ImGuiListClipper_SortAndFuseRanges(data->Ranges, data->StepNo); + } + + // Step 0+ (if item height is given in advance) or 1+: Display the next range in line. + while (data->StepNo < data->Ranges.Size) + { + clipper->DisplayStart = ImMax(data->Ranges[data->StepNo].Min, already_submitted); + clipper->DisplayEnd = ImMin(data->Ranges[data->StepNo].Max, clipper->ItemsCount); + data->StepNo++; + if (clipper->DisplayStart >= clipper->DisplayEnd) + continue; + if (clipper->DisplayStart > already_submitted) + clipper->SeekCursorForItem(clipper->DisplayStart); + return true; + } + + // After the last step: Let the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), + // Advance the cursor to the end of the list and then returns 'false' to end the loop. + if (clipper->ItemsCount < INT_MAX) + clipper->SeekCursorForItem(clipper->ItemsCount); + + return false; } bool ImGuiListClipper::Step() { - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - - if (ItemsCount == 0 || window->SkipItems) + ImGuiContext& g = *Ctx; + bool need_items_height = (ItemsHeight <= 0.0f); + bool ret = ImGuiListClipper_StepInternal(this); + if (ret && (DisplayStart >= DisplayEnd)) + ret = false; + if (g.CurrentTable && g.CurrentTable->IsUnfrozenRows == false) + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): inside frozen table row.\n"); + if (need_items_height && ItemsHeight > 0.0f) + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): computed ItemsHeight: %.2f.\n", ItemsHeight); + if (ret) { - ItemsCount = -1; - return false; + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): display %d to %d.\n", DisplayStart, DisplayEnd); } - if (StepNo == 0) // Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height. + else { - DisplayStart = 0; - DisplayEnd = 1; - StartPosY = window->DC.CursorPos.y; - StepNo = 1; - return true; - } - if (StepNo == 1) // Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element. - { - if (ItemsCount == 1) { ItemsCount = -1; return false; } - float items_height = window->DC.CursorPos.y - StartPosY; - IM_ASSERT(items_height > 0.0f); // If this triggers, it means Item 0 hasn't moved the cursor vertically - Begin(ItemsCount - 1, items_height); - DisplayStart++; - DisplayEnd++; - StepNo = 3; - return true; - } - if (StepNo == 2) // Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user still call Step(). Does nothing and switch to Step 3. - { - IM_ASSERT(DisplayStart >= 0 && DisplayEnd >= 0); - StepNo = 3; - return true; - } - if (StepNo == 3) // Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop. + IMGUI_DEBUG_LOG_CLIPPER("Clipper: Step(): End.\n"); End(); - return false; + } + return ret; +} + +//----------------------------------------------------------------------------- +// [SECTION] STYLING +//----------------------------------------------------------------------------- + +ImGuiStyle& ImGui::GetStyle() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); + return GImGui->Style; +} + +ImU32 ImGui::GetColorU32(ImGuiCol idx, float alpha_mul) +{ + ImGuiStyle& style = GImGui->Style; + ImVec4 c = style.Colors[idx]; + c.w *= style.Alpha * alpha_mul; + return ColorConvertFloat4ToU32(c); +} + +ImU32 ImGui::GetColorU32(const ImVec4& col) +{ + ImGuiStyle& style = GImGui->Style; + ImVec4 c = col; + c.w *= style.Alpha; + return ColorConvertFloat4ToU32(c); +} + +const ImVec4& ImGui::GetStyleColorVec4(ImGuiCol idx) +{ + ImGuiStyle& style = GImGui->Style; + return style.Colors[idx]; +} + +ImU32 ImGui::GetColorU32(ImU32 col, float alpha_mul) +{ + ImGuiStyle& style = GImGui->Style; + alpha_mul *= style.Alpha; + if (alpha_mul >= 1.0f) + return col; + ImU32 a = (col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT; + a = (ImU32)(a * alpha_mul); // We don't need to clamp 0..255 because alpha is in 0..1 range. + return (col & ~IM_COL32_A_MASK) | (a << IM_COL32_A_SHIFT); +} + +// FIXME: This may incur a round-trip (if the end user got their data from a float4) but eventually we aim to store the in-flight colors as ImU32 +void ImGui::PushStyleColor(ImGuiCol idx, ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiColorMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorStack.push_back(backup); + if (g.DebugFlashStyleColorIdx != idx) + g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); +} + +void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) +{ + ImGuiContext& g = *GImGui; + ImGuiColorMod backup; + backup.Col = idx; + backup.BackupValue = g.Style.Colors[idx]; + g.ColorStack.push_back(backup); + if (g.DebugFlashStyleColorIdx != idx) + g.Style.Colors[idx] = col; +} + +void ImGui::PopStyleColor(int count) +{ + ImGuiContext& g = *GImGui; + if (g.ColorStack.Size < count) + { + IM_ASSERT_USER_ERROR(0, "Calling PopStyleColor() too many times!"); + count = g.ColorStack.Size; + } + while (count > 0) + { + ImGuiColorMod& backup = g.ColorStack.back(); + g.Style.Colors[backup.Col] = backup.BackupValue; + g.ColorStack.pop_back(); + count--; + } +} + +static const ImGuiCol GWindowDockStyleColors[ImGuiWindowDockStyleCol_COUNT] = +{ + ImGuiCol_Text, ImGuiCol_TabHovered, ImGuiCol_Tab, ImGuiCol_TabSelected, ImGuiCol_TabSelectedOverline, ImGuiCol_TabDimmed, ImGuiCol_TabDimmedSelected, ImGuiCol_TabDimmedSelectedOverline, +}; + +static const ImGuiStyleVarInfo GStyleVarsInfo[] = +{ + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, DisabledAlpha) }, // ImGuiStyleVar_DisabledAlpha + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, CellPadding) }, // ImGuiStyleVar_CellPadding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ImageBorderSize) }, // ImGuiStyleVar_ImageBorderSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBorderSize) }, // ImGuiStyleVar_TabBorderSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBarBorderSize) }, // ImGuiStyleVar_TabBarBorderSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TabBarOverlineSize) }, // ImGuiStyleVar_TabBarOverlineSize + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersAngle)}, // ImGuiStyleVar_TableAngledHeadersAngle + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, TableAngledHeadersTextAlign)},// ImGuiStyleVar_TableAngledHeadersTextAlign + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorTextBorderSize)}, // ImGuiStyleVar_SeparatorTextBorderSize + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorTextAlign) }, // ImGuiStyleVar_SeparatorTextAlign + { 2, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, SeparatorTextPadding) }, // ImGuiStyleVar_SeparatorTextPadding + { 1, ImGuiDataType_Float, (ImU32)offsetof(ImGuiStyle, DockingSeparatorSize) }, // ImGuiStyleVar_DockingSeparatorSize +}; + +const ImGuiStyleVarInfo* ImGui::GetStyleVarInfo(ImGuiStyleVar idx) +{ + IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_COUNT); + IM_STATIC_ASSERT(IM_ARRAYSIZE(GStyleVarsInfo) == ImGuiStyleVar_COUNT); + return &GStyleVarsInfo[idx]; +} + +void ImGui::PushStyleVar(ImGuiStyleVar idx, float val) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->DataType != ImGuiDataType_Float || var_info->Count != 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + float* pvar = (float*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; +} + +void ImGui::PushStyleVarX(ImGuiStyleVar idx, float val_x) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->DataType != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + pvar->x = val_x; +} + +void ImGui::PushStyleVarY(ImGuiStyleVar idx, float val_y) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->DataType != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + pvar->y = val_y; +} + +void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) +{ + ImGuiContext& g = *GImGui; + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); + if (var_info->DataType != ImGuiDataType_Float || var_info->Count != 2) + { + IM_ASSERT_USER_ERROR(0, "Calling PushStyleVar() variant with wrong type!"); + return; + } + ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); + g.StyleVarStack.push_back(ImGuiStyleMod(idx, *pvar)); + *pvar = val; +} + +void ImGui::PopStyleVar(int count) +{ + ImGuiContext& g = *GImGui; + if (g.StyleVarStack.Size < count) + { + IM_ASSERT_USER_ERROR(0, "Calling PopStyleVar() too many times!"); + count = g.StyleVarStack.Size; + } + while (count > 0) + { + // We avoid a generic memcpy(data, &backup.Backup.., GDataTypeSize[info->Type] * info->Count), the overhead in Debug is not worth it. + ImGuiStyleMod& backup = g.StyleVarStack.back(); + const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(backup.VarIdx); + void* data = var_info->GetVarPtr(&g.Style); + if (var_info->DataType == ImGuiDataType_Float && var_info->Count == 1) { ((float*)data)[0] = backup.BackupFloat[0]; } + else if (var_info->DataType == ImGuiDataType_Float && var_info->Count == 2) { ((float*)data)[0] = backup.BackupFloat[0]; ((float*)data)[1] = backup.BackupFloat[1]; } + g.StyleVarStack.pop_back(); + count--; + } +} + +const char* ImGui::GetStyleColorName(ImGuiCol idx) +{ + // Create switch-case from enum with regexp: ImGuiCol_{.*}, --> case ImGuiCol_\1: return "\1"; + switch (idx) + { + case ImGuiCol_Text: return "Text"; + case ImGuiCol_TextDisabled: return "TextDisabled"; + case ImGuiCol_WindowBg: return "WindowBg"; + case ImGuiCol_ChildBg: return "ChildBg"; + case ImGuiCol_PopupBg: return "PopupBg"; + case ImGuiCol_Border: return "Border"; + case ImGuiCol_BorderShadow: return "BorderShadow"; + case ImGuiCol_FrameBg: return "FrameBg"; + case ImGuiCol_FrameBgHovered: return "FrameBgHovered"; + case ImGuiCol_FrameBgActive: return "FrameBgActive"; + case ImGuiCol_TitleBg: return "TitleBg"; + case ImGuiCol_TitleBgActive: return "TitleBgActive"; + case ImGuiCol_TitleBgCollapsed: return "TitleBgCollapsed"; + case ImGuiCol_MenuBarBg: return "MenuBarBg"; + case ImGuiCol_ScrollbarBg: return "ScrollbarBg"; + case ImGuiCol_ScrollbarGrab: return "ScrollbarGrab"; + case ImGuiCol_ScrollbarGrabHovered: return "ScrollbarGrabHovered"; + case ImGuiCol_ScrollbarGrabActive: return "ScrollbarGrabActive"; + case ImGuiCol_CheckMark: return "CheckMark"; + case ImGuiCol_SliderGrab: return "SliderGrab"; + case ImGuiCol_SliderGrabActive: return "SliderGrabActive"; + case ImGuiCol_Button: return "Button"; + case ImGuiCol_ButtonHovered: return "ButtonHovered"; + case ImGuiCol_ButtonActive: return "ButtonActive"; + case ImGuiCol_Header: return "Header"; + case ImGuiCol_HeaderHovered: return "HeaderHovered"; + case ImGuiCol_HeaderActive: return "HeaderActive"; + case ImGuiCol_Separator: return "Separator"; + case ImGuiCol_SeparatorHovered: return "SeparatorHovered"; + case ImGuiCol_SeparatorActive: return "SeparatorActive"; + case ImGuiCol_ResizeGrip: return "ResizeGrip"; + case ImGuiCol_ResizeGripHovered: return "ResizeGripHovered"; + case ImGuiCol_ResizeGripActive: return "ResizeGripActive"; + case ImGuiCol_TabHovered: return "TabHovered"; + case ImGuiCol_Tab: return "Tab"; + case ImGuiCol_TabSelected: return "TabSelected"; + case ImGuiCol_TabSelectedOverline: return "TabSelectedOverline"; + case ImGuiCol_TabDimmed: return "TabDimmed"; + case ImGuiCol_TabDimmedSelected: return "TabDimmedSelected"; + case ImGuiCol_TabDimmedSelectedOverline: return "TabDimmedSelectedOverline"; + case ImGuiCol_DockingPreview: return "DockingPreview"; + case ImGuiCol_DockingEmptyBg: return "DockingEmptyBg"; + case ImGuiCol_PlotLines: return "PlotLines"; + case ImGuiCol_PlotLinesHovered: return "PlotLinesHovered"; + case ImGuiCol_PlotHistogram: return "PlotHistogram"; + case ImGuiCol_PlotHistogramHovered: return "PlotHistogramHovered"; + case ImGuiCol_TableHeaderBg: return "TableHeaderBg"; + case ImGuiCol_TableBorderStrong: return "TableBorderStrong"; + case ImGuiCol_TableBorderLight: return "TableBorderLight"; + case ImGuiCol_TableRowBg: return "TableRowBg"; + case ImGuiCol_TableRowBgAlt: return "TableRowBgAlt"; + case ImGuiCol_TextLink: return "TextLink"; + case ImGuiCol_TextSelectedBg: return "TextSelectedBg"; + case ImGuiCol_DragDropTarget: return "DragDropTarget"; + case ImGuiCol_NavCursor: return "NavCursor"; + case ImGuiCol_NavWindowingHighlight: return "NavWindowingHighlight"; + case ImGuiCol_NavWindowingDimBg: return "NavWindowingDimBg"; + case ImGuiCol_ModalWindowDimBg: return "ModalWindowDimBg"; + } + IM_ASSERT(0); + return "Unknown"; } //----------------------------------------------------------------------------- // [SECTION] RENDER HELPERS -// Those (internal) functions are currently quite a legacy mess - their signature and behavior will change. -// Also see imgui_draw.cpp for some more which have been reworked to not rely on ImGui:: state. +// Some of those (internal) functions are currently quite a legacy mess - their signature and behavior will change, +// we need a nicer separation between low-level functions and high-level functions relying on the ImGui context. +// Also see imgui_draw.cpp for some more which have been reworked to not rely on ImGui:: context. //----------------------------------------------------------------------------- const char* ImGui::FindRenderedTextEnd(const char* text, const char* text_end) @@ -2397,7 +3674,7 @@ void ImGui::RenderText(ImVec2 pos, const char* text, const char* text_end, bool else { if (!text_end) - text_end = text + strlen(text); // FIXME-OPT + text_end = text + ImStrlen(text); // FIXME-OPT text_display_end = text_end; } @@ -2415,7 +3692,7 @@ void ImGui::RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end ImGuiWindow* window = g.CurrentWindow; if (!text_end) - text_end = text + strlen(text); // FIXME-OPT + text_end = text + ImStrlen(text); // FIXME-OPT if (text != text_end) { @@ -2427,6 +3704,9 @@ void ImGui::RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end // Default clip_rect uses (pos_min,pos_max) // Handle clipping on CPU immediately (vs typically let the GPU clip the triangles that are overlapping the clipping rectangle edges) +// FIXME-OPT: Since we have or calculate text_size we could coarse clip whole block immediately, especally for text above draw_list->DrawList. +// Effectively as this is called from widget doing their own coarse clipping it's not very valuable presently. Next time function will take +// better advantage of the render function taking size into account for coarse clipping. void ImGui::RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_display_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect) { // Perform CPU side clipping for single clipped element to avoid using scissor state @@ -2470,7 +3750,6 @@ void ImGui::RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, cons LogRenderedText(&pos_min, text, text_display_end); } - // Another overly complex function until we reorganize everything into a nice all-in-one helper. // This is made more complex because we have dissociated the layout rectangle (pos_min..pos_max) which define _where_ the ellipsis is, from actual clipping of text and limit of the ellipsis display. // This is because in the context of tabs we selectively hide part of the text when the Close Button appears, but we don't want the ellipsis to move. @@ -2492,32 +3771,14 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con // min max ellipsis_max // <-> this is generally some padding value - const ImFont* font = draw_list->_Data->Font; + ImFont* font = draw_list->_Data->Font; const float font_size = draw_list->_Data->FontSize; + const float font_scale = draw_list->_Data->FontScale; const char* text_end_ellipsis = NULL; + const float ellipsis_width = font->EllipsisWidth * font_scale; - ImWchar ellipsis_char = font->EllipsisChar; - int ellipsis_char_count = 1; - if (ellipsis_char == (ImWchar)-1) - { - ellipsis_char = (ImWchar)'.'; - ellipsis_char_count = 3; - } - const ImFontGlyph* glyph = font->FindGlyph(ellipsis_char); - - float ellipsis_glyph_width = glyph->X1; // Width of the glyph with no padding on either side - float ellipsis_total_width = ellipsis_glyph_width; // Full width of entire ellipsis - - if (ellipsis_char_count > 1) - { - // Full ellipsis size without free spacing after it. - const float spacing_between_dots = 1.0f * (draw_list->_Data->FontSize / font->FontSize); - ellipsis_glyph_width = glyph->X1 - glyph->X0 + spacing_between_dots; - ellipsis_total_width = ellipsis_glyph_width * (float)ellipsis_char_count - spacing_between_dots; - } - // We can now claim the space between pos_max.x and ellipsis_max.x - const float text_avail_width = ImMax((ImMax(pos_max.x, ellipsis_max_x) - ellipsis_total_width) - pos_min.x, 1.0f); + const float text_avail_width = ImMax((ImMax(pos_max.x, ellipsis_max_x) - ellipsis_width) - pos_min.x, 1.0f); float text_size_clipped_x = font->CalcTextSizeA(font_size, text_avail_width, 0.0f, text, text_end_full, &text_end_ellipsis).x; if (text == text_end_ellipsis && text_end_ellipsis < text_end_full) { @@ -2534,13 +3795,10 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con // Render text, render ellipsis RenderTextClippedEx(draw_list, pos_min, ImVec2(clip_max_x, pos_max.y), text, text_end_ellipsis, &text_size, ImVec2(0.0f, 0.0f)); - float ellipsis_x = pos_min.x + text_size_clipped_x; - if (ellipsis_x + ellipsis_total_width <= ellipsis_max_x) - for (int i = 0; i < ellipsis_char_count; i++) - { - font->RenderChar(draw_list, font_size, ImVec2(ellipsis_x, pos_min.y), GetColorU32(ImGuiCol_Text), ellipsis_char); - ellipsis_x += ellipsis_glyph_width; - } + ImVec2 ellipsis_pos = ImTrunc(ImVec2(pos_min.x + text_size_clipped_x, pos_min.y)); + if (ellipsis_pos.x + ellipsis_width <= ellipsis_max_x) + for (int i = 0; i < font->EllipsisCharCount; i++, ellipsis_pos.x += font->EllipsisCharStep * font_scale) + font->RenderChar(draw_list, font_size, ellipsis_pos, GetColorU32(ImGuiCol_Text), font->EllipsisChar); } else { @@ -2552,16 +3810,16 @@ void ImGui::RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, con } // Render a rectangle shaped with optional rounding and borders -void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border, float rounding) +void ImGui::RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders, float rounding) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; window->DrawList->AddRectFilled(p_min, p_max, fill_col, rounding); const float border_size = g.Style.FrameBorderSize; - if (border && border_size > 0.0f) + if (borders && border_size > 0.0f) { - window->DrawList->AddRect(p_min+ImVec2(1,1), p_max+ImVec2(1,1), GetColorU32(ImGuiCol_BorderShadow), rounding, ImDrawCornerFlags_All, border_size); - window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, ImDrawCornerFlags_All, border_size); + window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); } } @@ -2572,669 +3830,83 @@ void ImGui::RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding) const float border_size = g.Style.FrameBorderSize; if (border_size > 0.0f) { - window->DrawList->AddRect(p_min+ImVec2(1,1), p_max+ImVec2(1,1), GetColorU32(ImGuiCol_BorderShadow), rounding, ImDrawCornerFlags_All, border_size); - window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, ImDrawCornerFlags_All, border_size); + window->DrawList->AddRect(p_min + ImVec2(1, 1), p_max + ImVec2(1, 1), GetColorU32(ImGuiCol_BorderShadow), rounding, 0, border_size); + window->DrawList->AddRect(p_min, p_max, GetColorU32(ImGuiCol_Border), rounding, 0, border_size); } } -// Render an arrow aimed to be aligned with text (p_min is a position in the same space text would be positioned). To e.g. denote expanded/collapsed state -void ImGui::RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale) -{ - const float h = draw_list->_Data->FontSize * 1.00f; - float r = h * 0.40f * scale; - ImVec2 center = pos + ImVec2(h * 0.50f, h * 0.50f * scale); - - ImVec2 a, b, c; - switch (dir) - { - case ImGuiDir_Up: - case ImGuiDir_Down: - if (dir == ImGuiDir_Up) r = -r; - a = ImVec2(+0.000f,+0.750f) * r; - b = ImVec2(-0.866f,-0.750f) * r; - c = ImVec2(+0.866f,-0.750f) * r; - break; - case ImGuiDir_Left: - case ImGuiDir_Right: - if (dir == ImGuiDir_Left) r = -r; - a = ImVec2(+0.750f,+0.000f) * r; - b = ImVec2(-0.750f,+0.866f) * r; - c = ImVec2(-0.750f,-0.866f) * r; - break; - case ImGuiDir_None: - case ImGuiDir_COUNT: - IM_ASSERT(0); - break; - } - draw_list->AddTriangleFilled(center + a, center + b, center + c, col); -} - -void ImGui::RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col) -{ - draw_list->AddCircleFilled(pos, draw_list->_Data->FontSize * 0.20f, col, 8); -} - -void ImGui::RenderCheckMark(ImVec2 pos, ImU32 col, float sz) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - - float thickness = ImMax(sz / 5.0f, 1.0f); - sz -= thickness*0.5f; - pos += ImVec2(thickness*0.25f, thickness*0.25f); - - float third = sz / 3.0f; - float bx = pos.x + third; - float by = pos.y + sz - third*0.5f; - window->DrawList->PathLineTo(ImVec2(bx - third, by - third)); - window->DrawList->PathLineTo(ImVec2(bx, by)); - window->DrawList->PathLineTo(ImVec2(bx + third*2, by - third*2)); - window->DrawList->PathStroke(col, false, thickness); -} - -void ImGui::RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags) +void ImGui::RenderNavCursor(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFlags flags) { ImGuiContext& g = *GImGui; if (id != g.NavId) return; - if (g.NavDisableHighlight && !(flags & ImGuiNavHighlightFlags_AlwaysDraw)) + if (!g.NavCursorVisible && !(flags & ImGuiNavRenderCursorFlags_AlwaysDraw)) + return; + if (id == g.LastItemData.ID && (g.LastItemData.ItemFlags & ImGuiItemFlags_NoNav)) return; ImGuiWindow* window = g.CurrentWindow; if (window->DC.NavHideHighlightOneFrame) return; - float rounding = (flags & ImGuiNavHighlightFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; + float rounding = (flags & ImGuiNavRenderCursorFlags_NoRounding) ? 0.0f : g.Style.FrameRounding; ImRect display_rect = bb; display_rect.ClipWith(window->ClipRect); - if (flags & ImGuiNavHighlightFlags_TypeDefault) + const float thickness = 2.0f; + if (flags & ImGuiNavRenderCursorFlags_Compact) { - const float THICKNESS = 2.0f; - const float DISTANCE = 3.0f + THICKNESS * 0.5f; - display_rect.Expand(ImVec2(DISTANCE,DISTANCE)); + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavCursor), rounding, 0, thickness); + } + else + { + const float distance = 3.0f + thickness * 0.5f; + display_rect.Expand(ImVec2(distance, distance)); bool fully_visible = window->ClipRect.Contains(display_rect); if (!fully_visible) window->DrawList->PushClipRect(display_rect.Min, display_rect.Max); - window->DrawList->AddRect(display_rect.Min + ImVec2(THICKNESS*0.5f,THICKNESS*0.5f), display_rect.Max - ImVec2(THICKNESS*0.5f,THICKNESS*0.5f), GetColorU32(ImGuiCol_NavHighlight), rounding, ImDrawCornerFlags_All, THICKNESS); + window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavCursor), rounding, 0, thickness); if (!fully_visible) window->DrawList->PopClipRect(); } - if (flags & ImGuiNavHighlightFlags_TypeThin) +} + +void ImGui::RenderMouseCursor(ImVec2 base_pos, float base_scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow) +{ + ImGuiContext& g = *GImGui; + if (mouse_cursor <= ImGuiMouseCursor_None || mouse_cursor >= ImGuiMouseCursor_COUNT) // We intentionally accept out of bound values. + mouse_cursor = ImGuiMouseCursor_Arrow; + ImFontAtlas* font_atlas = g.DrawListSharedData.Font->ContainerAtlas; + for (ImGuiViewportP* viewport : g.Viewports) { - window->DrawList->AddRect(display_rect.Min, display_rect.Max, GetColorU32(ImGuiCol_NavHighlight), rounding, ~0, 1.0f); + // We scale cursor with current viewport/monitor, however Windows 10 for its own hardware cursor seems to be using a different scale factor. + ImVec2 offset, size, uv[4]; + if (!ImFontAtlasGetMouseCursorTexData(font_atlas, mouse_cursor, &offset, &size, &uv[0], &uv[2])) + continue; + const ImVec2 pos = base_pos - offset; + const float scale = base_scale * viewport->DpiScale; + if (!viewport->GetMainRect().Overlaps(ImRect(pos, pos + ImVec2(size.x + 2, size.y + 2) * scale))) + continue; + ImDrawList* draw_list = GetForegroundDrawList(viewport); + ImTextureID tex_id = font_atlas->TexID; + draw_list->PushTextureID(tex_id); + draw_list->AddImage(tex_id, pos + ImVec2(1, 0) * scale, pos + (ImVec2(1, 0) + size) * scale, uv[2], uv[3], col_shadow); + draw_list->AddImage(tex_id, pos + ImVec2(2, 0) * scale, pos + (ImVec2(2, 0) + size) * scale, uv[2], uv[3], col_shadow); + draw_list->AddImage(tex_id, pos, pos + size * scale, uv[2], uv[3], col_border); + draw_list->AddImage(tex_id, pos, pos + size * scale, uv[0], uv[1], col_fill); + if (mouse_cursor == ImGuiMouseCursor_Wait || mouse_cursor == ImGuiMouseCursor_Progress) + { + float a_min = ImFmod((float)g.Time * 5.0f, 2.0f * IM_PI); + float a_max = a_min + IM_PI * 1.65f; + draw_list->PathArcTo(pos + ImVec2(14, -1) * scale, 6.0f * scale, a_min, a_max); + draw_list->PathStroke(col_fill, ImDrawFlags_None, 3.0f * scale); + } + draw_list->PopTextureID(); } } //----------------------------------------------------------------------------- -// [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +// [SECTION] INITIALIZATION, SHUTDOWN //----------------------------------------------------------------------------- -// ImGuiWindow is mostly a dumb struct. It merely has a constructor and a few helper methods -ImGuiWindow::ImGuiWindow(ImGuiContext* context, const char* name) - : DrawListInst(&context->DrawListSharedData) -{ - Name = ImStrdup(name); - ID = ImHashStr(name); - IDStack.push_back(ID); - Flags = ImGuiWindowFlags_None; - Pos = ImVec2(0.0f, 0.0f); - Size = SizeFull = ImVec2(0.0f, 0.0f); - ContentSize = ContentSizeExplicit = ImVec2(0.0f, 0.0f); - WindowPadding = ImVec2(0.0f, 0.0f); - WindowRounding = 0.0f; - WindowBorderSize = 0.0f; - NameBufLen = (int)strlen(name) + 1; - MoveId = GetID("#MOVE"); - ChildId = 0; - Scroll = ImVec2(0.0f, 0.0f); - ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); - ScrollTargetCenterRatio = ImVec2(0.5f, 0.5f); - ScrollbarSizes = ImVec2(0.0f, 0.0f); - ScrollbarX = ScrollbarY = false; - Active = WasActive = false; - WriteAccessed = false; - Collapsed = false; - WantCollapseToggle = false; - SkipItems = false; - Appearing = false; - Hidden = false; - HasCloseButton = false; - ResizeBorderHeld = -1; - BeginCount = 0; - BeginOrderWithinParent = -1; - BeginOrderWithinContext = -1; - PopupId = 0; - AutoFitFramesX = AutoFitFramesY = -1; - AutoFitChildAxises = 0x00; - AutoFitOnlyGrows = false; - AutoPosLastDirection = ImGuiDir_None; - HiddenFramesCanSkipItems = HiddenFramesCannotSkipItems = 0; - SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; - SetWindowPosVal = SetWindowPosPivot = ImVec2(FLT_MAX, FLT_MAX); - - LastFrameActive = -1; - LastTimeActive = -1.0f; - ItemWidthDefault = 0.0f; - FontWindowScale = 1.0f; - SettingsIdx = -1; - - DrawList = &DrawListInst; - DrawList->_OwnerName = Name; - ParentWindow = NULL; - RootWindow = NULL; - RootWindowForTitleBarHighlight = NULL; - RootWindowForNav = NULL; - - NavLastIds[0] = NavLastIds[1] = 0; - NavRectRel[0] = NavRectRel[1] = ImRect(); - NavLastChildNavWindow = NULL; - - MemoryCompacted = false; - MemoryDrawListIdxCapacity = MemoryDrawListVtxCapacity = 0; -} - -ImGuiWindow::~ImGuiWindow() -{ - IM_ASSERT(DrawList == &DrawListInst); - IM_DELETE(Name); - for (int i = 0; i != ColumnsStorage.Size; i++) - ColumnsStorage[i].~ImGuiColumns(); -} - -ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) -{ - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); - ImGui::KeepAliveID(id); - return id; -} - -ImGuiID ImGuiWindow::GetID(const void* ptr) -{ - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); - ImGui::KeepAliveID(id); - return id; -} - -ImGuiID ImGuiWindow::GetID(int n) -{ - ImGuiID seed = IDStack.back(); - ImGuiID id = ImHashData(&n, sizeof(n), seed); - ImGui::KeepAliveID(id); - return id; -} - -ImGuiID ImGuiWindow::GetIDNoKeepAlive(const char* str, const char* str_end) -{ - ImGuiID seed = IDStack.back(); - return ImHashStr(str, str_end ? (str_end - str) : 0, seed); -} - -ImGuiID ImGuiWindow::GetIDNoKeepAlive(const void* ptr) -{ - ImGuiID seed = IDStack.back(); - return ImHashData(&ptr, sizeof(void*), seed); -} - -ImGuiID ImGuiWindow::GetIDNoKeepAlive(int n) -{ - ImGuiID seed = IDStack.back(); - return ImHashData(&n, sizeof(n), seed); -} - -// This is only used in rare/specific situations to manufacture an ID out of nowhere. -ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) -{ - ImGuiID seed = IDStack.back(); - const int r_rel[4] = { (int)(r_abs.Min.x - Pos.x), (int)(r_abs.Min.y - Pos.y), (int)(r_abs.Max.x - Pos.x), (int)(r_abs.Max.y - Pos.y) }; - ImGuiID id = ImHashData(&r_rel, sizeof(r_rel), seed); - ImGui::KeepAliveID(id); - return id; -} - -static void SetCurrentWindow(ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - g.CurrentWindow = window; - if (window) - g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); -} - -// Free up/compact internal window buffers, we can use this when a window becomes unused. -// This is currently unused by the library, but you may call this yourself for easy GC. -// Not freed: -// - ImGuiWindow, ImGuiWindowSettings, Name -// - StateStorage, ColumnsStorage (may hold useful data) -// This should have no noticeable visual effect. When the window reappear however, expect new allocation/buffer growth/copy cost. -void ImGui::GcCompactTransientWindowBuffers(ImGuiWindow* window) -{ - window->MemoryCompacted = true; - window->MemoryDrawListIdxCapacity = window->DrawList->IdxBuffer.Capacity; - window->MemoryDrawListVtxCapacity = window->DrawList->VtxBuffer.Capacity; - window->IDStack.clear(); - window->DrawList->ClearFreeMemory(); - window->DC.ChildWindows.clear(); - window->DC.ItemFlagsStack.clear(); - window->DC.ItemWidthStack.clear(); - window->DC.TextWrapPosStack.clear(); - window->DC.GroupStack.clear(); -} - -void ImGui::GcAwakeTransientWindowBuffers(ImGuiWindow* window) -{ - // We stored capacity of the ImDrawList buffer to reduce growth-caused allocation/copy when awakening. - // The other buffers tends to amortize much faster. - window->MemoryCompacted = false; - window->DrawList->IdxBuffer.reserve(window->MemoryDrawListIdxCapacity); - window->DrawList->VtxBuffer.reserve(window->MemoryDrawListVtxCapacity); - window->MemoryDrawListIdxCapacity = window->MemoryDrawListVtxCapacity = 0; -} - -void ImGui::SetNavID(ImGuiID id, int nav_layer) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(g.NavWindow); - IM_ASSERT(nav_layer == 0 || nav_layer == 1); - g.NavId = id; - g.NavWindow->NavLastIds[nav_layer] = id; -} - -void ImGui::SetNavIDWithRectRel(ImGuiID id, int nav_layer, const ImRect& rect_rel) -{ - ImGuiContext& g = *GImGui; - SetNavID(id, nav_layer); - g.NavWindow->NavRectRel[nav_layer] = rect_rel; - g.NavMousePosDirty = true; - g.NavDisableHighlight = false; - g.NavDisableMouseHover = true; -} - -void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - g.ActiveIdIsJustActivated = (g.ActiveId != id); - if (g.ActiveIdIsJustActivated) - { - g.ActiveIdTimer = 0.0f; - g.ActiveIdHasBeenPressedBefore = false; - g.ActiveIdHasBeenEditedBefore = false; - if (id != 0) - { - g.LastActiveId = id; - g.LastActiveIdTimer = 0.0f; - } - } - g.ActiveId = id; - g.ActiveIdAllowNavDirFlags = 0; - g.ActiveIdBlockNavInputFlags = 0; - g.ActiveIdAllowOverlap = false; - g.ActiveIdWindow = window; - g.ActiveIdHasBeenEditedThisFrame = false; - if (id) - { - g.ActiveIdIsAlive = id; - g.ActiveIdSource = (g.NavActivateId == id || g.NavInputId == id || g.NavJustTabbedId == id || g.NavJustMovedToId == id) ? ImGuiInputSource_Nav : ImGuiInputSource_Mouse; - } -} - -// FIXME-NAV: The existence of SetNavID/SetNavIDWithRectRel/SetFocusID is incredibly messy and confusing and needs some explanation or refactoring. -void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(id != 0); - - // Assume that SetFocusID() is called in the context where its NavLayer is the current layer, which is the case everywhere we call it. - const ImGuiNavLayer nav_layer = window->DC.NavLayerCurrent; - if (g.NavWindow != window) - g.NavInitRequest = false; - g.NavId = id; - g.NavWindow = window; - g.NavLayer = nav_layer; - window->NavLastIds[nav_layer] = id; - if (window->DC.LastItemId == id) - window->NavRectRel[nav_layer] = ImRect(window->DC.LastItemRect.Min - window->Pos, window->DC.LastItemRect.Max - window->Pos); - - if (g.ActiveIdSource == ImGuiInputSource_Nav) - g.NavDisableMouseHover = true; - else - g.NavDisableHighlight = true; -} - -void ImGui::ClearActiveID() -{ - SetActiveID(0, NULL); -} - -void ImGui::SetHoveredID(ImGuiID id) -{ - ImGuiContext& g = *GImGui; - g.HoveredId = id; - g.HoveredIdAllowOverlap = false; - if (id != 0 && g.HoveredIdPreviousFrame != id) - g.HoveredIdTimer = g.HoveredIdNotActiveTimer = 0.0f; -} - -ImGuiID ImGui::GetHoveredID() -{ - ImGuiContext& g = *GImGui; - return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; -} - -void ImGui::KeepAliveID(ImGuiID id) -{ - ImGuiContext& g = *GImGui; - if (g.ActiveId == id) - g.ActiveIdIsAlive = id; - if (g.ActiveIdPreviousFrame == id) - g.ActiveIdPreviousFrameIsAlive = true; -} - -void ImGui::MarkItemEdited(ImGuiID id) -{ - // This marking is solely to be able to provide info for IsItemDeactivatedAfterEdit(). - // ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need need to fill the data. - ImGuiContext& g = *GImGui; - IM_ASSERT(g.ActiveId == id || g.ActiveId == 0 || g.DragDropActive); - IM_UNUSED(id); // Avoid unused variable warnings when asserts are compiled out. - //IM_ASSERT(g.CurrentWindow->DC.LastItemId == id); - g.ActiveIdHasBeenEditedThisFrame = true; - g.ActiveIdHasBeenEditedBefore = true; - g.CurrentWindow->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_Edited; -} - -static inline bool IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) -{ - // An active popup disable hovering on other windows (apart from its own children) - // FIXME-OPT: This could be cached/stored within the window. - ImGuiContext& g = *GImGui; - if (g.NavWindow) - if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindow) - if (focused_root_window->WasActive && focused_root_window != window->RootWindow) - { - // For the purpose of those flags we differentiate "standard popup" from "modal popup" - // NB: The order of those two tests is important because Modal windows are also Popups. - if (focused_root_window->Flags & ImGuiWindowFlags_Modal) - return false; - if ((focused_root_window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiHoveredFlags_AllowWhenBlockedByPopup)) - return false; - } - - return true; -} - -// Advance cursor given item size for layout. -void ImGui::ItemSize(const ImVec2& size, float text_offset_y) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (window->SkipItems) - return; - - // Always align ourselves on pixel boundaries - const float line_height = ImMax(window->DC.CurrLineSize.y, size.y); - const float text_base_offset = ImMax(window->DC.CurrLineTextBaseOffset, text_offset_y); - //if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] - window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x + size.x; - window->DC.CursorPosPrevLine.y = window->DC.CursorPos.y; - window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); - window->DC.CursorPos.y = (float)(int)(window->DC.CursorPos.y + line_height + g.Style.ItemSpacing.y); - window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); - window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); - //if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] - - window->DC.PrevLineSize.y = line_height; - window->DC.PrevLineTextBaseOffset = text_base_offset; - window->DC.CurrLineSize.y = window->DC.CurrLineTextBaseOffset = 0.0f; - - // Horizontal layout mode - if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) - SameLine(); -} - -void ImGui::ItemSize(const ImRect& bb, float text_offset_y) -{ - ItemSize(bb.GetSize(), text_offset_y); -} - -// Declare item bounding box for clipping and interaction. -// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface -// declare their minimum size requirement to ItemSize() and then use a larger region for drawing/interaction, which is passed to ItemAdd(). -bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - - if (id != 0) - { - // Navigation processing runs prior to clipping early-out - // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget - // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests - // unfortunately, but it is still limited to one window. It may not scale very well for windows with ten of - // thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. - // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able - // to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick). - // We intentionally don't check if g.NavWindow != NULL because g.NavAnyRequest should only be set when it is non null. - // If we crash on a NULL g.NavWindow we need to fix the bug elsewhere. - window->DC.NavLayerActiveMaskNext |= window->DC.NavLayerCurrentMask; - if (g.NavId == id || g.NavAnyRequest) - if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) - if (window == g.NavWindow || ((window->Flags | g.NavWindow->Flags) & ImGuiWindowFlags_NavFlattened)) - NavProcessItem(window, nav_bb_arg ? *nav_bb_arg : bb, id); - - // [DEBUG] Item Picker tool, when enabling the "extended" version we perform the check in ItemAdd() -#ifdef IMGUI_DEBUG_TOOL_ITEM_PICKER_EX - if (id == g.DebugItemPickerBreakID) - { - IM_DEBUG_BREAK(); - g.DebugItemPickerBreakID = 0; - } -#endif - } - - window->DC.LastItemId = id; - window->DC.LastItemRect = bb; - window->DC.LastItemStatusFlags = ImGuiItemStatusFlags_None; - g.NextItemData.Flags = ImGuiNextItemDataFlags_None; - -#ifdef IMGUI_ENABLE_TEST_ENGINE - if (id != 0) - IMGUI_TEST_ENGINE_ITEM_ADD(nav_bb_arg ? *nav_bb_arg : bb, id); -#endif - - // Clipping test - const bool is_clipped = IsClippedEx(bb, id, false); - if (is_clipped) - return false; - //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] - - // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) - if (IsMouseHoveringRect(bb.Min, bb.Max)) - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HoveredRect; - return true; -} - -// This is roughly matching the behavior of internal-facing ItemHoverable() -// - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() -// - this should work even for non-interactive items that have no ID, so we cannot use LastItemId -bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (g.NavDisableMouseHover && !g.NavDisableHighlight) - return IsItemFocused(); - - // Test for bounding box overlap, as updated as ItemAdd() - if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect)) - return false; - IM_ASSERT((flags & (ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows)) == 0); // Flags not supported by this function - - // Test if we are hovering the right window (our window could be behind another window) - // [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable to use IsItemHovered() after EndChild() itself. - // Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was the test that has been running for a long while. - //if (g.HoveredWindow != window) - // return false; - if (g.HoveredRootWindow != window->RootWindow && !(flags & ImGuiHoveredFlags_AllowWhenOverlapped)) - return false; - - // Test if another item is active (e.g. being dragged) - if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) - if (g.ActiveId != 0 && g.ActiveId != window->DC.LastItemId && !g.ActiveIdAllowOverlap && g.ActiveId != window->MoveId) - return false; - - // Test if interactions on this window are blocked by an active popup or modal. - // The ImGuiHoveredFlags_AllowWhenBlockedByPopup flag will be tested here. - if (!IsWindowContentHoverable(window, flags)) - return false; - - // Test if the item is disabled - if ((window->DC.ItemFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) - return false; - - // Special handling for the dummy item after Begin() which represent the title bar or tab. - // When the window is collapsed (SkipItems==true) that last item will never be overwritten so we need to detect the case. - if (window->DC.LastItemId == window->MoveId && window->WriteAccessed) - return false; - return true; -} - -// Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). -bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id) -{ - ImGuiContext& g = *GImGui; - if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) - return false; - - ImGuiWindow* window = g.CurrentWindow; - if (g.HoveredWindow != window) - return false; - if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) - return false; - if (!IsMouseHoveringRect(bb.Min, bb.Max)) - return false; - if (g.NavDisableMouseHover || !IsWindowContentHoverable(window, ImGuiHoveredFlags_None)) - return false; - if (window->DC.ItemFlags & ImGuiItemFlags_Disabled) - return false; - - SetHoveredID(id); - - // [DEBUG] Item Picker tool! - // We perform the check here because SetHoveredID() is not frequently called (1~ time a frame), making - // the cost of this tool near-zero. We can get slightly better call-stack and support picking non-hovered - // items if we perform the test in ItemAdd(), but that would incur a small runtime cost. - // #define IMGUI_DEBUG_TOOL_ITEM_PICKER_EX in imconfig.h if you want this check to also be performed in ItemAdd(). - if (g.DebugItemPickerActive && g.HoveredIdPreviousFrame == id) - GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); - if (g.DebugItemPickerBreakID == id) - IM_DEBUG_BREAK(); - - return true; -} - -bool ImGui::IsClippedEx(const ImRect& bb, ImGuiID id, bool clip_even_when_logged) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (!bb.Overlaps(window->ClipRect)) - if (id == 0 || id != g.ActiveId) - if (clip_even_when_logged || !g.LogEnabled) - return true; - return false; -} - -// Process TAB/Shift+TAB. Be mindful that this function may _clear_ the ActiveID when tabbing out. -bool ImGui::FocusableItemRegister(ImGuiWindow* window, ImGuiID id) -{ - ImGuiContext& g = *GImGui; - - // Increment counters - const bool is_tab_stop = (window->DC.ItemFlags & (ImGuiItemFlags_NoTabStop | ImGuiItemFlags_Disabled)) == 0; - window->DC.FocusCounterAll++; - if (is_tab_stop) - window->DC.FocusCounterTab++; - - // Process TAB/Shift-TAB to tab *OUT* of the currently focused item. - // (Note that we can always TAB out of a widget that doesn't allow tabbing in) - if (g.ActiveId == id && g.FocusTabPressed && !(g.ActiveIdBlockNavInputFlags & (1 << ImGuiNavInput_KeyTab_)) && g.FocusRequestNextWindow == NULL) - { - g.FocusRequestNextWindow = window; - g.FocusRequestNextCounterTab = window->DC.FocusCounterTab + (g.IO.KeyShift ? (is_tab_stop ? -1 : 0) : +1); // Modulo on index will be applied at the end of frame once we've got the total counter of items. - } - - // Handle focus requests - if (g.FocusRequestCurrWindow == window) - { - if (window->DC.FocusCounterAll == g.FocusRequestCurrCounterAll) - return true; - if (is_tab_stop && window->DC.FocusCounterTab == g.FocusRequestCurrCounterTab) - { - g.NavJustTabbedId = id; - return true; - } - - // If another item is about to be focused, we clear our own active id - if (g.ActiveId == id) - ClearActiveID(); - } - - return false; -} - -void ImGui::FocusableItemUnregister(ImGuiWindow* window) -{ - window->DC.FocusCounterAll--; - window->DC.FocusCounterTab--; -} - -float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) -{ - if (wrap_pos_x < 0.0f) - return 0.0f; - - ImGuiWindow* window = GImGui->CurrentWindow; - if (wrap_pos_x == 0.0f) - wrap_pos_x = window->WorkRect.Max.x; - else if (wrap_pos_x > 0.0f) - wrap_pos_x += window->Pos.x - window->Scroll.x; // wrap_pos_x is provided is window local space - - return ImMax(wrap_pos_x - pos.x, 1.0f); -} - -// IM_ALLOC() == ImGui::MemAlloc() -void* ImGui::MemAlloc(size_t size) -{ - if (ImGuiContext* ctx = GImGui) - ctx->IO.MetricsActiveAllocations++; - return GImAllocatorAllocFunc(size, GImAllocatorUserData); -} - -// IM_FREE() == ImGui::MemFree() -void ImGui::MemFree(void* ptr) -{ - if (ptr) - if (ImGuiContext* ctx = GImGui) - ctx->IO.MetricsActiveAllocations--; - return GImAllocatorFreeFunc(ptr, GImAllocatorUserData); -} - -const char* ImGui::GetClipboardText() -{ - ImGuiContext& g = *GImGui; - return g.IO.GetClipboardTextFn ? g.IO.GetClipboardTextFn(g.IO.ClipboardUserData) : ""; -} - -void ImGui::SetClipboardText(const char* text) -{ - ImGuiContext& g = *GImGui; - if (g.IO.SetClipboardTextFn) - g.IO.SetClipboardTextFn(g.IO.ClipboardUserData, text); -} - -const char* ImGui::GetVersion() -{ - return IMGUI_VERSION; -} - // Internal state access - if you want to share Dear ImGui state between modules (e.g. DLL) or allocate it yourself // Note that we still point to some static data and members (such as GFontAtlas), so the state instance you end up using will point to the static data within its module ImGuiContext* ImGui::GetCurrentContext() @@ -3251,67 +3923,1064 @@ void ImGui::SetCurrentContext(ImGuiContext* ctx) #endif } -// Helper function to verify ABI compatibility between caller code and compiled version of Dear ImGui. -// Verify that the type sizes are matching between the calling file's compilation unit and imgui.cpp's compilation unit -// If the user has inconsistent compilation settings, imgui configuration #define, packing pragma, etc. your user code -// may see different structures than what imgui.cpp sees, which is problematic. -// We usually require settings to be in imconfig.h to make sure that they are accessible to all compilation units involved with Dear ImGui. -bool ImGui::DebugCheckVersionAndDataLayout(const char* version, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_vert, size_t sz_idx) -{ - bool error = false; - if (strcmp(version, IMGUI_VERSION)!=0) { error = true; IM_ASSERT(strcmp(version,IMGUI_VERSION)==0 && "Mismatched version string!"); } - if (sz_io != sizeof(ImGuiIO)) { error = true; IM_ASSERT(sz_io == sizeof(ImGuiIO) && "Mismatched struct layout!"); } - if (sz_style != sizeof(ImGuiStyle)) { error = true; IM_ASSERT(sz_style == sizeof(ImGuiStyle) && "Mismatched struct layout!"); } - if (sz_vec2 != sizeof(ImVec2)) { error = true; IM_ASSERT(sz_vec2 == sizeof(ImVec2) && "Mismatched struct layout!"); } - if (sz_vec4 != sizeof(ImVec4)) { error = true; IM_ASSERT(sz_vec4 == sizeof(ImVec4) && "Mismatched struct layout!"); } - if (sz_vert != sizeof(ImDrawVert)) { error = true; IM_ASSERT(sz_vert == sizeof(ImDrawVert) && "Mismatched struct layout!"); } - if (sz_idx != sizeof(ImDrawIdx)) { error = true; IM_ASSERT(sz_idx == sizeof(ImDrawIdx) && "Mismatched struct layout!"); } - return !error; -} - -void ImGui::SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data) +void ImGui::SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data) { GImAllocatorAllocFunc = alloc_func; GImAllocatorFreeFunc = free_func; GImAllocatorUserData = user_data; } +// This is provided to facilitate copying allocators from one static/DLL boundary to another (e.g. retrieve default allocator of your executable address space) +void ImGui::GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data) +{ + *p_alloc_func = GImAllocatorAllocFunc; + *p_free_func = GImAllocatorFreeFunc; + *p_user_data = GImAllocatorUserData; +} + ImGuiContext* ImGui::CreateContext(ImFontAtlas* shared_font_atlas) { + ImGuiContext* prev_ctx = GetCurrentContext(); ImGuiContext* ctx = IM_NEW(ImGuiContext)(shared_font_atlas); - if (GImGui == NULL) - SetCurrentContext(ctx); - Initialize(ctx); + SetCurrentContext(ctx); + Initialize(); + if (prev_ctx != NULL) + SetCurrentContext(prev_ctx); // Restore previous context if any, else keep new one. return ctx; } void ImGui::DestroyContext(ImGuiContext* ctx) { - if (ctx == NULL) - ctx = GImGui; - Shutdown(ctx); - if (GImGui == ctx) - SetCurrentContext(NULL); + ImGuiContext* prev_ctx = GetCurrentContext(); + if (ctx == NULL) //-V1051 + ctx = prev_ctx; + SetCurrentContext(ctx); + Shutdown(); + SetCurrentContext((prev_ctx != ctx) ? prev_ctx : NULL); IM_DELETE(ctx); } +// IMPORTANT: interactive elements requires a fixed ###xxx suffix, it must be same in ALL languages to allow for automation. +static const ImGuiLocEntry GLocalizationEntriesEnUS[] = +{ + { ImGuiLocKey_VersionStr, "Dear ImGui " IMGUI_VERSION " (" IM_STRINGIFY(IMGUI_VERSION_NUM) ")" }, + { ImGuiLocKey_TableSizeOne, "Size column to fit###SizeOne" }, + { ImGuiLocKey_TableSizeAllFit, "Size all columns to fit###SizeAll" }, + { ImGuiLocKey_TableSizeAllDefault, "Size all columns to default###SizeAll" }, + { ImGuiLocKey_TableResetOrder, "Reset order###ResetOrder" }, + { ImGuiLocKey_WindowingMainMenuBar, "(Main menu bar)" }, + { ImGuiLocKey_WindowingPopup, "(Popup)" }, + { ImGuiLocKey_WindowingUntitled, "(Untitled)" }, + { ImGuiLocKey_OpenLink_s, "Open '%s'" }, + { ImGuiLocKey_CopyLink, "Copy Link###CopyLink" }, + { ImGuiLocKey_DockingHideTabBar, "Hide tab bar###HideTabBar" }, + { ImGuiLocKey_DockingHoldShiftToDock, "Hold SHIFT to enable Docking window." }, + { ImGuiLocKey_DockingDragToUndockOrMoveNode,"Click and drag to move or undock whole node." }, +}; + +ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) +{ + IO.Ctx = this; + InputTextState.Ctx = this; + + Initialized = false; + ConfigFlagsCurrFrame = ConfigFlagsLastFrame = ImGuiConfigFlags_None; + FontAtlasOwnedByContext = shared_font_atlas ? false : true; + Font = NULL; + FontSize = FontBaseSize = FontScale = CurrentDpiScale = 0.0f; + IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); + Time = 0.0f; + FrameCount = 0; + FrameCountEnded = FrameCountPlatformEnded = FrameCountRendered = -1; + WithinEndChildID = 0; + WithinFrameScope = WithinFrameScopeWithImplicitWindow = false; + GcCompactAll = false; + TestEngineHookItems = false; + TestEngine = NULL; + memset(ContextName, 0, sizeof(ContextName)); + + InputEventsNextMouseSource = ImGuiMouseSource_Mouse; + InputEventsNextEventId = 1; + + WindowsActiveCount = 0; + WindowsBorderHoverPadding = 0.0f; + CurrentWindow = NULL; + HoveredWindow = NULL; + HoveredWindowUnderMovingWindow = NULL; + HoveredWindowBeforeClear = NULL; + MovingWindow = NULL; + WheelingWindow = NULL; + WheelingWindowStartFrame = WheelingWindowScrolledFrame = -1; + WheelingWindowReleaseTimer = 0.0f; + + DebugDrawIdConflicts = 0; + DebugHookIdInfo = 0; + HoveredId = HoveredIdPreviousFrame = 0; + HoveredIdPreviousFrameItemCount = 0; + HoveredIdAllowOverlap = false; + HoveredIdIsDisabled = false; + HoveredIdTimer = HoveredIdNotActiveTimer = 0.0f; + ItemUnclipByLog = false; + ActiveId = 0; + ActiveIdIsAlive = 0; + ActiveIdTimer = 0.0f; + ActiveIdIsJustActivated = false; + ActiveIdAllowOverlap = false; + ActiveIdNoClearOnFocusLoss = false; + ActiveIdHasBeenPressedBefore = false; + ActiveIdHasBeenEditedBefore = false; + ActiveIdHasBeenEditedThisFrame = false; + ActiveIdFromShortcut = false; + ActiveIdClickOffset = ImVec2(-1, -1); + ActiveIdWindow = NULL; + ActiveIdSource = ImGuiInputSource_None; + ActiveIdMouseButton = -1; + ActiveIdPreviousFrame = 0; + memset(&DeactivatedItemData, 0, sizeof(DeactivatedItemData)); + memset(&ActiveIdValueOnActivation, 0, sizeof(ActiveIdValueOnActivation)); + LastActiveId = 0; + LastActiveIdTimer = 0.0f; + + LastKeyboardKeyPressTime = LastKeyModsChangeTime = LastKeyModsChangeFromNoneTime = -1.0; + + ActiveIdUsingNavDirMask = 0x00; + ActiveIdUsingAllKeyboardKeys = false; + + CurrentFocusScopeId = 0; + CurrentItemFlags = ImGuiItemFlags_None; + DebugShowGroupRects = false; + + CurrentViewport = NULL; + MouseViewport = MouseLastHoveredViewport = NULL; + PlatformLastFocusedViewportId = 0; + ViewportCreatedCount = PlatformWindowsCreatedCount = 0; + ViewportFocusedStampCount = 0; + + NavCursorVisible = false; + NavHighlightItemUnderNav = false; + NavMousePosDirty = false; + NavIdIsAlive = false; + NavId = 0; + NavWindow = NULL; + NavFocusScopeId = NavActivateId = NavActivateDownId = NavActivatePressedId = 0; + NavLayer = ImGuiNavLayer_Main; + NavNextActivateId = 0; + NavActivateFlags = NavNextActivateFlags = ImGuiActivateFlags_None; + NavHighlightActivatedId = 0; + NavHighlightActivatedTimer = 0.0f; + NavInputSource = ImGuiInputSource_Keyboard; + NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + NavCursorHideFrames = 0; + + NavAnyRequest = false; + NavInitRequest = false; + NavInitRequestFromMove = false; + NavMoveSubmitted = false; + NavMoveScoringItems = false; + NavMoveForwardToNextFrame = false; + NavMoveFlags = ImGuiNavMoveFlags_None; + NavMoveScrollFlags = ImGuiScrollFlags_None; + NavMoveKeyMods = ImGuiMod_None; + NavMoveDir = NavMoveDirForDebug = NavMoveClipDir = ImGuiDir_None; + NavScoringDebugCount = 0; + NavTabbingDir = 0; + NavTabbingCounter = 0; + + NavJustMovedFromFocusScopeId = NavJustMovedToId = NavJustMovedToFocusScopeId = 0; + NavJustMovedToKeyMods = ImGuiMod_None; + NavJustMovedToIsTabbing = false; + NavJustMovedToHasSelectionData = false; + + // All platforms use Ctrl+Tab but Ctrl<>Super are swapped on Mac... + // FIXME: Because this value is stored, it annoyingly interfere with toggling io.ConfigMacOSXBehaviors updating this.. + ConfigNavWindowingKeyNext = IO.ConfigMacOSXBehaviors ? (ImGuiMod_Super | ImGuiKey_Tab) : (ImGuiMod_Ctrl | ImGuiKey_Tab); + ConfigNavWindowingKeyPrev = IO.ConfigMacOSXBehaviors ? (ImGuiMod_Super | ImGuiMod_Shift | ImGuiKey_Tab) : (ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Tab); + NavWindowingTarget = NavWindowingTargetAnim = NavWindowingListWindow = NULL; + NavWindowingTimer = NavWindowingHighlightAlpha = 0.0f; + NavWindowingToggleLayer = false; + NavWindowingToggleKey = ImGuiKey_None; + + DimBgRatio = 0.0f; + + DragDropActive = DragDropWithinSource = DragDropWithinTarget = false; + DragDropSourceFlags = ImGuiDragDropFlags_None; + DragDropSourceFrameCount = -1; + DragDropMouseButton = -1; + DragDropTargetId = 0; + DragDropAcceptFlags = ImGuiDragDropFlags_None; + DragDropAcceptIdCurrRectSurface = 0.0f; + DragDropAcceptIdPrev = DragDropAcceptIdCurr = 0; + DragDropAcceptFrameCount = -1; + DragDropHoldJustPressedId = 0; + memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); + + ClipperTempDataStacked = 0; + + CurrentTable = NULL; + TablesTempDataStacked = 0; + CurrentTabBar = NULL; + CurrentMultiSelect = NULL; + MultiSelectTempDataStacked = 0; + + HoverItemDelayId = HoverItemDelayIdPreviousFrame = HoverItemUnlockedStationaryId = HoverWindowUnlockedStationaryId = 0; + HoverItemDelayTimer = HoverItemDelayClearTimer = 0.0f; + + MouseCursor = ImGuiMouseCursor_Arrow; + MouseStationaryTimer = 0.0f; + + TempInputId = 0; + memset(&DataTypeZeroValue, 0, sizeof(DataTypeZeroValue)); + BeginMenuDepth = BeginComboDepth = 0; + ColorEditOptions = ImGuiColorEditFlags_DefaultOptions_; + ColorEditCurrentID = ColorEditSavedID = 0; + ColorEditSavedHue = ColorEditSavedSat = 0.0f; + ColorEditSavedColor = 0; + WindowResizeRelativeMode = false; + ScrollbarSeekMode = 0; + ScrollbarClickDeltaToGrabCenter = 0.0f; + SliderGrabClickOffset = 0.0f; + SliderCurrentAccum = 0.0f; + SliderCurrentAccumDirty = false; + DragCurrentAccumDirty = false; + DragCurrentAccum = 0.0f; + DragSpeedDefaultRatio = 1.0f / 100.0f; + DisabledAlphaBackup = 0.0f; + DisabledStackSize = 0; + TooltipOverrideCount = 0; + TooltipPreviousWindow = NULL; + + PlatformImeData.InputPos = ImVec2(0.0f, 0.0f); + PlatformImeDataPrev.InputPos = ImVec2(-1.0f, -1.0f); // Different to ensure initial submission + PlatformImeViewport = 0; + + DockNodeWindowMenuHandler = NULL; + + SettingsLoaded = false; + SettingsDirtyTimer = 0.0f; + HookIdNext = 0; + + memset(LocalizationTable, 0, sizeof(LocalizationTable)); + + LogEnabled = false; + LogFlags = ImGuiLogFlags_None; + LogWindow = NULL; + LogNextPrefix = LogNextSuffix = NULL; + LogFile = NULL; + LogLinePosY = FLT_MAX; + LogLineFirstItem = false; + LogDepthRef = 0; + LogDepthToExpand = LogDepthToExpandDefault = 2; + + ErrorCallback = NULL; + ErrorCallbackUserData = NULL; + ErrorFirst = true; + ErrorCountCurrentFrame = 0; + StackSizesInBeginForCurrentWindow = NULL; + + DebugDrawIdConflictsCount = 0; + DebugLogFlags = ImGuiDebugLogFlags_EventError | ImGuiDebugLogFlags_OutputToTTY; + DebugLocateId = 0; + DebugLogSkippedErrors = 0; + DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; + DebugLogAutoDisableFrames = 0; + DebugLocateFrames = 0; + DebugBeginReturnValueCullDepth = -1; + DebugItemPickerActive = false; + DebugItemPickerMouseButton = ImGuiMouseButton_Left; + DebugItemPickerBreakId = 0; + DebugFlashStyleColorTime = 0.0f; + DebugFlashStyleColorIdx = ImGuiCol_COUNT; + DebugHoveredDockNode = NULL; + + // Same as DebugBreakClearData(). Those fields are scattered in their respective subsystem to stay in hot-data locations + DebugBreakInWindow = 0; + DebugBreakInTable = 0; + DebugBreakInLocateId = false; + DebugBreakKeyChord = ImGuiKey_Pause; + DebugBreakInShortcutRouting = ImGuiKey_None; + + memset(FramerateSecPerFrame, 0, sizeof(FramerateSecPerFrame)); + FramerateSecPerFrameIdx = FramerateSecPerFrameCount = 0; + FramerateSecPerFrameAccum = 0.0f; + WantCaptureMouseNextFrame = WantCaptureKeyboardNextFrame = WantTextInputNextFrame = -1; + memset(TempKeychordName, 0, sizeof(TempKeychordName)); +} + +void ImGui::Initialize() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(!g.Initialized && !g.SettingsLoaded); + + // Add .ini handle for ImGuiWindow and ImGuiTable types + { + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Window"; + ini_handler.TypeHash = ImHashStr("Window"); + ini_handler.ClearAllFn = WindowSettingsHandler_ClearAll; + ini_handler.ReadOpenFn = WindowSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = WindowSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = WindowSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = WindowSettingsHandler_WriteAll; + AddSettingsHandler(&ini_handler); + } + TableSettingsAddSettingsHandler(); + + // Setup default localization table + LocalizeRegisterEntries(GLocalizationEntriesEnUS, IM_ARRAYSIZE(GLocalizationEntriesEnUS)); + + // Setup default ImGuiPlatformIO clipboard/IME handlers. + g.PlatformIO.Platform_GetClipboardTextFn = Platform_GetClipboardTextFn_DefaultImpl; // Platform dependent default implementations + g.PlatformIO.Platform_SetClipboardTextFn = Platform_SetClipboardTextFn_DefaultImpl; + g.PlatformIO.Platform_OpenInShellFn = Platform_OpenInShellFn_DefaultImpl; + g.PlatformIO.Platform_SetImeDataFn = Platform_SetImeDataFn_DefaultImpl; + + // Create default viewport + ImGuiViewportP* viewport = IM_NEW(ImGuiViewportP)(); + viewport->ID = IMGUI_VIEWPORT_DEFAULT_ID; + viewport->Idx = 0; + viewport->PlatformWindowCreated = true; + viewport->Flags = ImGuiViewportFlags_OwnedByApp; + g.Viewports.push_back(viewport); + g.TempBuffer.resize(1024 * 3 + 1, 0); + g.ViewportCreatedCount++; + g.PlatformIO.Viewports.push_back(g.Viewports[0]); + + // Build KeysMayBeCharInput[] lookup table (1 bool per named key) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + if ((key >= ImGuiKey_0 && key <= ImGuiKey_9) || (key >= ImGuiKey_A && key <= ImGuiKey_Z) || (key >= ImGuiKey_Keypad0 && key <= ImGuiKey_Keypad9) + || key == ImGuiKey_Tab || key == ImGuiKey_Space || key == ImGuiKey_Apostrophe || key == ImGuiKey_Comma || key == ImGuiKey_Minus || key == ImGuiKey_Period + || key == ImGuiKey_Slash || key == ImGuiKey_Semicolon || key == ImGuiKey_Equal || key == ImGuiKey_LeftBracket || key == ImGuiKey_RightBracket || key == ImGuiKey_GraveAccent + || key == ImGuiKey_KeypadDecimal || key == ImGuiKey_KeypadDivide || key == ImGuiKey_KeypadMultiply || key == ImGuiKey_KeypadSubtract || key == ImGuiKey_KeypadAdd || key == ImGuiKey_KeypadEqual) + g.KeysMayBeCharInput.SetBit(key); + +#ifdef IMGUI_HAS_DOCK + // Initialize Docking + DockContextInitialize(&g); +#endif + + g.Initialized = true; +} + +// This function is merely here to free heap allocations. +void ImGui::Shutdown() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT_USER_ERROR(g.IO.BackendPlatformUserData == NULL, "Forgot to shutdown Platform backend?"); + IM_ASSERT_USER_ERROR(g.IO.BackendRendererUserData == NULL, "Forgot to shutdown Renderer backend?"); + + // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) + if (g.IO.Fonts && g.FontAtlasOwnedByContext) + { + g.IO.Fonts->Locked = false; + IM_DELETE(g.IO.Fonts); + } + g.IO.Fonts = NULL; + g.DrawListSharedData.TempBuffer.clear(); + + // Cleanup of other data are conditional on actually having initialized Dear ImGui. + if (!g.Initialized) + return; + + // Save settings (unless we haven't attempted to load them: CreateContext/DestroyContext without a call to NewFrame shouldn't save an empty file) + if (g.SettingsLoaded && g.IO.IniFilename != NULL) + SaveIniSettingsToDisk(g.IO.IniFilename); + + // Destroy platform windows + DestroyPlatformWindows(); + + // Shutdown extensions + DockContextShutdown(&g); + + CallContextHooks(&g, ImGuiContextHookType_Shutdown); + + // Clear everything else + g.Windows.clear_delete(); + g.WindowsFocusOrder.clear(); + g.WindowsTempSortBuffer.clear(); + g.CurrentWindow = NULL; + g.CurrentWindowStack.clear(); + g.WindowsById.Clear(); + g.NavWindow = NULL; + g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL; + g.ActiveIdWindow = NULL; + g.MovingWindow = NULL; + + g.KeysRoutingTable.Clear(); + + g.ColorStack.clear(); + g.StyleVarStack.clear(); + g.FontStack.clear(); + g.OpenPopupStack.clear(); + g.BeginPopupStack.clear(); + g.TreeNodeStack.clear(); + + g.CurrentViewport = g.MouseViewport = g.MouseLastHoveredViewport = NULL; + g.Viewports.clear_delete(); + + g.TabBars.Clear(); + g.CurrentTabBarStack.clear(); + g.ShrinkWidthBuffer.clear(); + + g.ClipperTempData.clear_destruct(); + + g.Tables.Clear(); + g.TablesTempData.clear_destruct(); + g.DrawChannelsTempMergeBuffer.clear(); + + g.MultiSelectStorage.Clear(); + g.MultiSelectTempData.clear_destruct(); + + g.ClipboardHandlerData.clear(); + g.MenusIdSubmittedThisFrame.clear(); + g.InputTextState.ClearFreeMemory(); + g.InputTextDeactivatedState.ClearFreeMemory(); + + g.SettingsWindows.clear(); + g.SettingsHandlers.clear(); + + if (g.LogFile) + { +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + if (g.LogFile != stdout) +#endif + ImFileClose(g.LogFile); + g.LogFile = NULL; + } + g.LogBuffer.clear(); + g.DebugLogBuf.clear(); + g.DebugLogIndex.clear(); + + g.Initialized = false; +} + +// No specific ordering/dependency support, will see as needed +ImGuiID ImGui::AddContextHook(ImGuiContext* ctx, const ImGuiContextHook* hook) +{ + ImGuiContext& g = *ctx; + IM_ASSERT(hook->Callback != NULL && hook->HookId == 0 && hook->Type != ImGuiContextHookType_PendingRemoval_); + g.Hooks.push_back(*hook); + g.Hooks.back().HookId = ++g.HookIdNext; + return g.HookIdNext; +} + +// Deferred removal, avoiding issue with changing vector while iterating it +void ImGui::RemoveContextHook(ImGuiContext* ctx, ImGuiID hook_id) +{ + ImGuiContext& g = *ctx; + IM_ASSERT(hook_id != 0); + for (ImGuiContextHook& hook : g.Hooks) + if (hook.HookId == hook_id) + hook.Type = ImGuiContextHookType_PendingRemoval_; +} + +// Call context hooks (used by e.g. test engine) +// We assume a small number of hooks so all stored in same array +void ImGui::CallContextHooks(ImGuiContext* ctx, ImGuiContextHookType hook_type) +{ + ImGuiContext& g = *ctx; + for (ImGuiContextHook& hook : g.Hooks) + if (hook.Type == hook_type) + hook.Callback(&g, &hook); +} + +//----------------------------------------------------------------------------- +// [SECTION] MAIN CODE (most of the code! lots of stuff, needs tidying up!) +//----------------------------------------------------------------------------- + +// ImGuiWindow is mostly a dumb struct. It merely has a constructor and a few helper methods +ImGuiWindow::ImGuiWindow(ImGuiContext* ctx, const char* name) : DrawListInst(NULL) +{ + memset(this, 0, sizeof(*this)); + Ctx = ctx; + Name = ImStrdup(name); + NameBufLen = (int)ImStrlen(name) + 1; + ID = ImHashStr(name); + IDStack.push_back(ID); + ViewportAllowPlatformMonitorExtend = -1; + ViewportPos = ImVec2(FLT_MAX, FLT_MAX); + MoveId = GetID("#MOVE"); + TabId = GetID("#TAB"); + ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + ScrollTargetCenterRatio = ImVec2(0.5f, 0.5f); + AutoFitFramesX = AutoFitFramesY = -1; + AutoPosLastDirection = ImGuiDir_None; + SetWindowPosAllowFlags = SetWindowSizeAllowFlags = SetWindowCollapsedAllowFlags = SetWindowDockAllowFlags = 0; + SetWindowPosVal = SetWindowPosPivot = ImVec2(FLT_MAX, FLT_MAX); + LastFrameActive = -1; + LastFrameJustFocused = -1; + LastTimeActive = -1.0f; + FontRefSize = 0.0f; + FontWindowScale = FontWindowScaleParents = FontDpiScale = 1.0f; + SettingsOffset = -1; + DockOrder = -1; + DrawList = &DrawListInst; + DrawList->_OwnerName = Name; + DrawList->_Data = &Ctx->DrawListSharedData; + NavPreferredScoringPosRel[0] = NavPreferredScoringPosRel[1] = ImVec2(FLT_MAX, FLT_MAX); + IM_PLACEMENT_NEW(&WindowClass) ImGuiWindowClass(); +} + +ImGuiWindow::~ImGuiWindow() +{ + IM_ASSERT(DrawList == &DrawListInst); + IM_DELETE(Name); + ColumnsStorage.clear_destruct(); +} + +static void SetCurrentWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + g.CurrentWindow = window; + g.StackSizesInBeginForCurrentWindow = g.CurrentWindow ? &g.CurrentWindowStack.back().StackSizesInBegin : NULL; + g.CurrentTable = window && window->DC.CurrentTableIdx != -1 ? g.Tables.GetByIndex(window->DC.CurrentTableIdx) : NULL; + if (window) + { + g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + g.FontScale = g.DrawListSharedData.FontScale = g.FontSize / g.Font->FontSize; + ImGui::NavUpdateCurrentWindowIsScrollPushableX(); + } +} + +void ImGui::GcCompactTransientMiscBuffers() +{ + ImGuiContext& g = *GImGui; + g.ItemFlagsStack.clear(); + g.GroupStack.clear(); + g.MultiSelectTempDataStacked = 0; + g.MultiSelectTempData.clear_destruct(); + TableGcCompactSettings(); +} + +// Free up/compact internal window buffers, we can use this when a window becomes unused. +// Not freed: +// - ImGuiWindow, ImGuiWindowSettings, Name, StateStorage, ColumnsStorage (may hold useful data) +// This should have no noticeable visual effect. When the window reappear however, expect new allocation/buffer growth/copy cost. +void ImGui::GcCompactTransientWindowBuffers(ImGuiWindow* window) +{ + window->MemoryCompacted = true; + window->MemoryDrawListIdxCapacity = window->DrawList->IdxBuffer.Capacity; + window->MemoryDrawListVtxCapacity = window->DrawList->VtxBuffer.Capacity; + window->IDStack.clear(); + window->DrawList->_ClearFreeMemory(); + window->DC.ChildWindows.clear(); + window->DC.ItemWidthStack.clear(); + window->DC.TextWrapPosStack.clear(); +} + +void ImGui::GcAwakeTransientWindowBuffers(ImGuiWindow* window) +{ + // We stored capacity of the ImDrawList buffer to reduce growth-caused allocation/copy when awakening. + // The other buffers tends to amortize much faster. + window->MemoryCompacted = false; + window->DrawList->IdxBuffer.reserve(window->MemoryDrawListIdxCapacity); + window->DrawList->VtxBuffer.reserve(window->MemoryDrawListVtxCapacity); + window->MemoryDrawListIdxCapacity = window->MemoryDrawListVtxCapacity = 0; +} + +void ImGui::SetActiveID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + // Clear previous active id + if (g.ActiveId != 0) + { + // While most behaved code would make an effort to not steal active id during window move/drag operations, + // we at least need to be resilient to it. Canceling the move is rather aggressive and users of 'master' branch + // may prefer the weird ill-defined half working situation ('docking' did assert), so may need to rework that. + if (g.MovingWindow != NULL && g.ActiveId == g.MovingWindow->MoveId) + { + IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() cancel MovingWindow\n"); + g.MovingWindow = NULL; + } + + // Store deactivate data + ImGuiDeactivatedItemData* deactivated_data = &g.DeactivatedItemData; + deactivated_data->ID = g.ActiveId; + deactivated_data->ElapseFrame = (g.LastItemData.ID == g.ActiveId) ? g.FrameCount : g.FrameCount + 1; // FIXME: OK to use LastItemData? + deactivated_data->HasBeenEditedBefore = g.ActiveIdHasBeenEditedBefore; + deactivated_data->IsAlive = (g.ActiveIdIsAlive == g.ActiveId); + + // This could be written in a more general way (e.g associate a hook to ActiveId), + // but since this is currently quite an exception we'll leave it as is. + // One common scenario leading to this is: pressing Key ->NavMoveRequestApplyResult() -> ClearActiveID() + if (g.InputTextState.ID == g.ActiveId) + InputTextDeactivateHook(g.ActiveId); + } + + // Set active id + g.ActiveIdIsJustActivated = (g.ActiveId != id); + if (g.ActiveIdIsJustActivated) + { + IMGUI_DEBUG_LOG_ACTIVEID("SetActiveID() old:0x%08X (window \"%s\") -> new:0x%08X (window \"%s\")\n", g.ActiveId, g.ActiveIdWindow ? g.ActiveIdWindow->Name : "", id, window ? window->Name : ""); + g.ActiveIdTimer = 0.0f; + g.ActiveIdHasBeenPressedBefore = false; + g.ActiveIdHasBeenEditedBefore = false; + g.ActiveIdMouseButton = -1; + if (id != 0) + { + g.LastActiveId = id; + g.LastActiveIdTimer = 0.0f; + } + } + g.ActiveId = id; + g.ActiveIdAllowOverlap = false; + g.ActiveIdNoClearOnFocusLoss = false; + g.ActiveIdWindow = window; + g.ActiveIdHasBeenEditedThisFrame = false; + g.ActiveIdFromShortcut = false; + if (id) + { + g.ActiveIdIsAlive = id; + g.ActiveIdSource = (g.NavActivateId == id || g.NavJustMovedToId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + IM_ASSERT(g.ActiveIdSource != ImGuiInputSource_None); + } + + // Clear declaration of inputs claimed by the widget + // (Please note that this is WIP and not all keys/inputs are thoroughly declared by all widgets yet) + g.ActiveIdUsingNavDirMask = 0x00; + g.ActiveIdUsingAllKeyboardKeys = false; +} + +void ImGui::ClearActiveID() +{ + SetActiveID(0, NULL); // g.ActiveId = 0; +} + +void ImGui::SetHoveredID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.HoveredId = id; + g.HoveredIdAllowOverlap = false; + if (id != 0 && g.HoveredIdPreviousFrame != id) + g.HoveredIdTimer = g.HoveredIdNotActiveTimer = 0.0f; +} + +ImGuiID ImGui::GetHoveredID() +{ + ImGuiContext& g = *GImGui; + return g.HoveredId ? g.HoveredId : g.HoveredIdPreviousFrame; +} + +void ImGui::MarkItemEdited(ImGuiID id) +{ + // This marking is to be able to provide info for IsItemDeactivatedAfterEdit(). + // ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data. + ImGuiContext& g = *GImGui; + if (g.LastItemData.ItemFlags & ImGuiItemFlags_NoMarkEdited) + return; + if (g.ActiveId == id || g.ActiveId == 0) + { + // FIXME: Can't we fully rely on LastItemData yet? + g.ActiveIdHasBeenEditedThisFrame = true; + g.ActiveIdHasBeenEditedBefore = true; + if (g.DeactivatedItemData.ID == id) + g.DeactivatedItemData.HasBeenEditedBefore = true; + } + + // We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343) + // We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714) + IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive)); + + //IM_ASSERT(g.CurrentWindow->DC.LastItemId == id); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; +} + +bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags) +{ + // An active popup disable hovering on other windows (apart from its own children) + // FIXME-OPT: This could be cached/stored within the window. + ImGuiContext& g = *GImGui; + if (g.NavWindow) + if (ImGuiWindow* focused_root_window = g.NavWindow->RootWindowDockTree) + if (focused_root_window->WasActive && focused_root_window != window->RootWindowDockTree) + { + // For the purpose of those flags we differentiate "standard popup" from "modal popup" + // NB: The 'else' is important because Modal windows are also Popups. + bool want_inhibit = false; + if (focused_root_window->Flags & ImGuiWindowFlags_Modal) + want_inhibit = true; + else if ((focused_root_window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + want_inhibit = true; + + // Inhibit hover unless the window is within the stack of our modal/popup + if (want_inhibit) + if (!IsWindowWithinBeginStackOf(window->RootWindow, focused_root_window)) + return false; + } + + // Filter by viewport + if (window->Viewport != g.MouseViewport) + if (g.MovingWindow == NULL || window->RootWindowDockTree != g.MovingWindow->RootWindowDockTree) + return false; + + return true; +} + +static inline float CalcDelayFromHoveredFlags(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + if (flags & ImGuiHoveredFlags_DelayNormal) + return g.Style.HoverDelayNormal; + if (flags & ImGuiHoveredFlags_DelayShort) + return g.Style.HoverDelayShort; + return 0.0f; +} + +static ImGuiHoveredFlags ApplyHoverFlagsForTooltip(ImGuiHoveredFlags user_flags, ImGuiHoveredFlags shared_flags) +{ + // Allow instance flags to override shared flags + if (user_flags & (ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal)) + shared_flags &= ~(ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal); + return user_flags | shared_flags; +} + +// This is roughly matching the behavior of internal-facing ItemHoverable() +// - we allow hovering to be true when ActiveId==window->MoveID, so that clicking on non-interactive items such as a Text() item still returns true with IsItemHovered() +// - this should work even for non-interactive items that have no ID, so we cannot use LastItemId +bool ImGui::IsItemHovered(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT_USER_ERROR((flags & ~ImGuiHoveredFlags_AllowedMaskForIsItemHovered) == 0, "Invalid flags for IsItemHovered()!"); + + if (g.NavHighlightItemUnderNav && g.NavCursorVisible && !(flags & ImGuiHoveredFlags_NoNavOverride)) + { + if (!IsItemFocused()) + return false; + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipNav); + } + else + { + // Test for bounding box overlap, as updated as ItemAdd() + ImGuiItemStatusFlags status_flags = g.LastItemData.StatusFlags; + if (!(status_flags & ImGuiItemStatusFlags_HoveredRect)) + return false; + + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse); + + // Done with rectangle culling so we can perform heavier checks now + // Test if we are hovering the right window (our window could be behind another window) + // [2021/03/02] Reworked / reverted the revert, finally. Note we want e.g. BeginGroup/ItemAdd/EndGroup to work as well. (#3851) + // [2017/10/16] Reverted commit 344d48be3 and testing RootWindow instead. I believe it is correct to NOT test for RootWindow but this leaves us unable + // to use IsItemHovered() after EndChild() itself. Until a solution is found I believe reverting to the test from 2017/09/27 is safe since this was + // the test that has been running for a long while. + if (g.HoveredWindow != window && (status_flags & ImGuiItemStatusFlags_HoveredWindow) == 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlappedByWindow) == 0) + return false; + + // Test if another item is active (e.g. being dragged) + const ImGuiID id = g.LastItemData.ID; + if ((flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) == 0) + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + if (g.ActiveId != window->MoveId && g.ActiveId != window->TabId) + return false; + + // Test if interactions on this window are blocked by an active popup or modal. + // The ImGuiHoveredFlags_AllowWhenBlockedByPopup flag will be tested here. + if (!IsWindowContentHoverable(window, flags) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_NoWindowHoverableCheck)) + return false; + + // Test if the item is disabled + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_Disabled) && !(flags & ImGuiHoveredFlags_AllowWhenDisabled)) + return false; + + // Special handling for calling after Begin() which represent the title bar or tab. + // When the window is skipped/collapsed (SkipItems==true) that last item (always ->MoveId submitted by Begin) + // will never be overwritten so we need to detect the case. + if (id == window->MoveId && window->WriteAccessed) + return false; + + // Test if using AllowOverlap and overlapped + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap) && id != 0) + if ((flags & ImGuiHoveredFlags_AllowWhenOverlappedByItem) == 0) + if (g.HoveredIdPreviousFrame != g.LastItemData.ID) + return false; + } + + // Handle hover delay + // (some ideas: https://www.nngroup.com/articles/timing-exposing-content) + const float delay = CalcDelayFromHoveredFlags(flags); + if (delay > 0.0f || (flags & ImGuiHoveredFlags_Stationary)) + { + ImGuiID hover_delay_id = (g.LastItemData.ID != 0) ? g.LastItemData.ID : window->GetIDFromPos(g.LastItemData.Rect.Min); + if ((flags & ImGuiHoveredFlags_NoSharedDelay) && (g.HoverItemDelayIdPreviousFrame != hover_delay_id)) + g.HoverItemDelayTimer = 0.0f; + g.HoverItemDelayId = hover_delay_id; + + // When changing hovered item we requires a bit of stationary delay before activating hover timer, + // but once unlocked on a given item we also moving. + //if (g.HoverDelayTimer >= delay && (g.HoverDelayTimer - g.IO.DeltaTime < delay || g.MouseStationaryTimer - g.IO.DeltaTime < g.Style.HoverStationaryDelay)) { IMGUI_DEBUG_LOG("HoverDelayTimer = %f/%f, MouseStationaryTimer = %f\n", g.HoverDelayTimer, delay, g.MouseStationaryTimer); } + if ((flags & ImGuiHoveredFlags_Stationary) != 0 && g.HoverItemUnlockedStationaryId != hover_delay_id) + return false; + + if (g.HoverItemDelayTimer < delay) + return false; + } + + return true; +} + +// Internal facing ItemHoverable() used when submitting widgets. Differs slightly from IsItemHovered(). +// (this does not rely on LastItemData it can be called from a ButtonBehavior() call not following an ItemAdd() call) +// FIXME-LEGACY: the 'ImGuiItemFlags item_flags' parameter was added on 2023-06-28. +// If you used this in your legacy/custom widgets code: +// - Commonly: if your ItemHoverable() call comes after an ItemAdd() call: pass 'item_flags = g.LastItemData.ItemFlags'. +// - Rare: otherwise you may pass 'item_flags = 0' (ImGuiItemFlags_None) unless you want to benefit from special behavior handled by ItemHoverable. +bool ImGui::ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Detect ID conflicts +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0 && g.HoveredIdPreviousFrame == id && (item_flags & ImGuiItemFlags_AllowDuplicateId) == 0) + { + g.HoveredIdPreviousFrameItemCount++; + if (g.DebugDrawIdConflicts == id) + window->DrawList->AddRect(bb.Min - ImVec2(1,1), bb.Max + ImVec2(1,1), IM_COL32(255, 0, 0, 255), 0.0f, ImDrawFlags_None, 2.0f); + } +#endif + + if (g.HoveredWindow != window) + return false; + if (!IsMouseHoveringRect(bb.Min, bb.Max)) + return false; + + if (g.HoveredId != 0 && g.HoveredId != id && !g.HoveredIdAllowOverlap) + return false; + if (g.ActiveId != 0 && g.ActiveId != id && !g.ActiveIdAllowOverlap) + if (!g.ActiveIdFromShortcut) + return false; + + // Done with rectangle culling so we can perform heavier checks now. + if (!(item_flags & ImGuiItemFlags_NoWindowHoverableCheck) && !IsWindowContentHoverable(window, ImGuiHoveredFlags_None)) + { + g.HoveredIdIsDisabled = true; + return false; + } + + // We exceptionally allow this function to be called with id==0 to allow using it for easy high-level + // hover test in widgets code. We could also decide to split this function is two. + if (id != 0) + { + // Drag source doesn't report as hovered + if (g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover)) + return false; + + SetHoveredID(id); + + // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. + // This allows using patterns where a later submitted widget overlaps a previous one. Generally perceived as a front-to-back hit-test. + if (item_flags & ImGuiItemFlags_AllowOverlap) + { + g.HoveredIdAllowOverlap = true; + if (g.HoveredIdPreviousFrame != id) + return false; + } + + // Display shortcut (only works with mouse) + // (ImGuiItemStatusFlags_HasShortcut in LastItemData denotes we want a tooltip) + if (id == g.LastItemData.ID && (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasShortcut) && g.ActiveId != id) + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal)) + SetTooltip("%s", GetKeyChordName(g.LastItemData.Shortcut)); + } + + // When disabled we'll return false but still set HoveredId + if (item_flags & ImGuiItemFlags_Disabled) + { + // Release active id if turning disabled + if (g.ActiveId == id && id != 0) + ClearActiveID(); + g.HoveredIdIsDisabled = true; + return false; + } + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0) + { + // [DEBUG] Item Picker tool! + // We perform the check here because reaching is path is rare (1~ time a frame), + // making the cost of this tool near-zero! We could get better call-stack and support picking non-hovered + // items if we performed the test in ItemAdd(), but that would incur a bigger runtime cost. + if (g.DebugItemPickerActive && g.HoveredIdPreviousFrame == id) + GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); + if (g.DebugItemPickerBreakId == id) + IM_DEBUG_BREAK(); + } +#endif + + if (g.NavHighlightItemUnderNav && (item_flags & ImGuiItemFlags_NoNavDisableMouseHover) == 0) + return false; + + return true; +} + +// FIXME: This is inlined/duplicated in ItemAdd() +// FIXME: The id != 0 path is not used by our codebase, may get rid of it? +bool ImGui::IsClippedEx(const ImRect& bb, ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!bb.Overlaps(window->ClipRect)) + if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId && id != g.NavActivateId)) + if (!g.ItemUnclipByLog) + return true; + return false; +} + +// This is also inlined in ItemAdd() +// Note: if ImGuiItemStatusFlags_HasDisplayRect is set, user needs to set g.LastItemData.DisplayRect. +void ImGui::SetLastItemData(ImGuiID item_id, ImGuiItemFlags item_flags, ImGuiItemStatusFlags status_flags, const ImRect& item_rect) +{ + ImGuiContext& g = *GImGui; + g.LastItemData.ID = item_id; + g.LastItemData.ItemFlags = item_flags; + g.LastItemData.StatusFlags = status_flags; + g.LastItemData.Rect = g.LastItemData.NavRect = item_rect; +} + +static void ImGui::SetLastItemDataForWindow(ImGuiWindow* window, const ImRect& rect) +{ + ImGuiContext& g = *GImGui; + if (window->DockIsActive) + SetLastItemData(window->MoveId, g.CurrentItemFlags, window->DC.DockTabItemStatusFlags, window->DC.DockTabItemRect); + else + SetLastItemData(window->MoveId, g.CurrentItemFlags, window->DC.WindowItemStatusFlags, rect); +} + +static void ImGui::SetLastItemDataForChildWindowItem(ImGuiWindow* window, const ImRect& rect) +{ + ImGuiContext& g = *GImGui; + SetLastItemData(window->ChildId, g.CurrentItemFlags, window->DC.ChildItemStatusFlags, rect); +} + +float ImGui::CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x) +{ + if (wrap_pos_x < 0.0f) + return 0.0f; + + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (wrap_pos_x == 0.0f) + { + // We could decide to setup a default wrapping max point for auto-resizing windows, + // or have auto-wrap (with unspecified wrapping pos) behave as a ContentSize extending function? + //if (window->Hidden && (window->Flags & ImGuiWindowFlags_AlwaysAutoResize)) + // wrap_pos_x = ImMax(window->WorkRect.Min.x + g.FontSize * 10.0f, window->WorkRect.Max.x); + //else + wrap_pos_x = window->WorkRect.Max.x; + } + else if (wrap_pos_x > 0.0f) + { + wrap_pos_x += window->Pos.x - window->Scroll.x; // wrap_pos_x is provided is window local space + } + + return ImMax(wrap_pos_x - pos.x, 1.0f); +} + +// IM_ALLOC() == ImGui::MemAlloc() +void* ImGui::MemAlloc(size_t size) +{ + void* ptr = (*GImAllocatorAllocFunc)(size, GImAllocatorUserData); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ImGuiContext* ctx = GImGui) + DebugAllocHook(&ctx->DebugAllocInfo, ctx->FrameCount, ptr, size); +#endif + return ptr; +} + +// IM_FREE() == ImGui::MemFree() +void ImGui::MemFree(void* ptr) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ptr != NULL) + if (ImGuiContext* ctx = GImGui) + DebugAllocHook(&ctx->DebugAllocInfo, ctx->FrameCount, ptr, (size_t)-1); +#endif + return (*GImAllocatorFreeFunc)(ptr, GImAllocatorUserData); +} + +// We record the number of allocation in recent frames, as a way to audit/sanitize our guiding principles of "no allocations on idle/repeating frames" +void ImGui::DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size) +{ + ImGuiDebugAllocEntry* entry = &info->LastEntriesBuf[info->LastEntriesIdx]; + IM_UNUSED(ptr); + if (entry->FrameCount != frame_count) + { + info->LastEntriesIdx = (info->LastEntriesIdx + 1) % IM_ARRAYSIZE(info->LastEntriesBuf); + entry = &info->LastEntriesBuf[info->LastEntriesIdx]; + entry->FrameCount = frame_count; + entry->AllocCount = entry->FreeCount = 0; + } + if (size != (size_t)-1) + { + //printf("[%05d] MemAlloc(%d) -> 0x%p\n", frame_count, (int)size, ptr); + entry->AllocCount++; + info->TotalAllocCount++; + } + else + { + //printf("[%05d] MemFree(0x%p)\n", frame_count, ptr); + entry->FreeCount++; + info->TotalFreeCount++; + } +} + +const char* ImGui::GetClipboardText() +{ + ImGuiContext& g = *GImGui; + return g.PlatformIO.Platform_GetClipboardTextFn ? g.PlatformIO.Platform_GetClipboardTextFn(&g) : ""; +} + +void ImGui::SetClipboardText(const char* text) +{ + ImGuiContext& g = *GImGui; + if (g.PlatformIO.Platform_SetClipboardTextFn != NULL) + g.PlatformIO.Platform_SetClipboardTextFn(&g, text); +} + +const char* ImGui::GetVersion() +{ + return IMGUI_VERSION; +} + ImGuiIO& ImGui::GetIO() { IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); return GImGui->IO; } -ImGuiStyle& ImGui::GetStyle() +// This variant exists to facilitate backends experimenting with multi-threaded parallel context. (#8069, #6293, #5856) +ImGuiIO& ImGui::GetIO(ImGuiContext* ctx) { - IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); - return GImGui->Style; + IM_ASSERT(ctx != NULL); + return ctx->IO; } -// Same value as passed to the old io.RenderDrawListsFn function. Valid after Render() and until the next call to NewFrame() +ImGuiPlatformIO& ImGui::GetPlatformIO() +{ + IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext()?"); + return GImGui->PlatformIO; +} + +// This variant exists to facilitate backends experimenting with multi-threaded parallel context. (#8069, #6293, #5856) +ImGuiPlatformIO& ImGui::GetPlatformIO(ImGuiContext* ctx) +{ + IM_ASSERT(ctx != NULL); + return ctx->PlatformIO; +} + +// Pass this to your backend rendering function! Valid after Render() and until the next call to NewFrame() ImDrawData* ImGui::GetDrawData() { ImGuiContext& g = *GImGui; - return g.DrawData.Valid ? &g.DrawData : NULL; + ImGuiViewportP* viewport = g.Viewports[0]; + return viewport->DrawDataP.Valid ? &viewport->DrawDataP : NULL; } double ImGui::GetTime() @@ -3324,14 +4993,42 @@ int ImGui::GetFrameCount() return GImGui->FrameCount; } -ImDrawList* ImGui::GetBackgroundDrawList() +static ImDrawList* GetViewportBgFgDrawList(ImGuiViewportP* viewport, size_t drawlist_no, const char* drawlist_name) { - return &GImGui->BackgroundDrawList; + // Create the draw list on demand, because they are not frequently used for all viewports + ImGuiContext& g = *GImGui; + IM_ASSERT(drawlist_no < IM_ARRAYSIZE(viewport->BgFgDrawLists)); + ImDrawList* draw_list = viewport->BgFgDrawLists[drawlist_no]; + if (draw_list == NULL) + { + draw_list = IM_NEW(ImDrawList)(&g.DrawListSharedData); + draw_list->_OwnerName = drawlist_name; + viewport->BgFgDrawLists[drawlist_no] = draw_list; + } + + // Our ImDrawList system requires that there is always a command + if (viewport->BgFgDrawListsLastFrame[drawlist_no] != g.FrameCount) + { + draw_list->_ResetForNewFrame(); + draw_list->PushTextureID(g.IO.Fonts->TexID); + draw_list->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size, false); + viewport->BgFgDrawListsLastFrame[drawlist_no] = g.FrameCount; + } + return draw_list; } -ImDrawList* ImGui::GetForegroundDrawList() +ImDrawList* ImGui::GetBackgroundDrawList(ImGuiViewport* viewport) { - return &GImGui->ForegroundDrawList; + if (viewport == NULL) + viewport = GImGui->CurrentWindow->Viewport; + return GetViewportBgFgDrawList((ImGuiViewportP*)viewport, 0, "##Background"); +} + +ImDrawList* ImGui::GetForegroundDrawList(ImGuiViewport* viewport) +{ + if (viewport == NULL) + viewport = GImGui->CurrentWindow->Viewport; + return GetViewportBgFgDrawList((ImGuiViewportP*)viewport, 1, "##Foreground"); } ImDrawListSharedData* ImGui::GetDrawListSharedData() @@ -3347,18 +5044,50 @@ void ImGui::StartMouseMovingWindow(ImGuiWindow* window) ImGuiContext& g = *GImGui; FocusWindow(window); SetActiveID(window->MoveId, window); - g.NavDisableHighlight = true; - g.ActiveIdClickOffset = g.IO.MousePos - window->RootWindow->Pos; + if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; + g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - window->RootWindowDockTree->Pos; + g.ActiveIdNoClearOnFocusLoss = true; + SetActiveIdUsingAllKeyboardKeys(); bool can_move_window = true; - if ((window->Flags & ImGuiWindowFlags_NoMove) || (window->RootWindow->Flags & ImGuiWindowFlags_NoMove)) + if ((window->Flags & ImGuiWindowFlags_NoMove) || (window->RootWindowDockTree->Flags & ImGuiWindowFlags_NoMove)) can_move_window = false; + if (ImGuiDockNode* node = window->DockNodeAsHost) + if (node->VisibleWindow && (node->VisibleWindow->Flags & ImGuiWindowFlags_NoMove)) + can_move_window = false; if (can_move_window) g.MovingWindow = window; } +// We use 'undock == false' when dragging from title bar to allow moving groups of floating nodes without undocking them. +void ImGui::StartMouseMovingWindowOrNode(ImGuiWindow* window, ImGuiDockNode* node, bool undock) +{ + ImGuiContext& g = *GImGui; + bool can_undock_node = false; + if (undock && node != NULL && node->VisibleWindow && (node->VisibleWindow->Flags & ImGuiWindowFlags_NoMove) == 0 && (node->MergedFlags & ImGuiDockNodeFlags_NoUndocking) == 0) + { + // Can undock if: + // - part of a hierarchy with more than one visible node (if only one is visible, we'll just move the root window) + // - part of a dockspace node hierarchy: so we can undock the last single visible node too. Undocking from a fixed/central node will create a new node and copy windows. + ImGuiDockNode* root_node = DockNodeGetRootNode(node); + if (root_node->OnlyNodeWithWindows != node || root_node->CentralNode != NULL) // -V1051 PVS-Studio thinks node should be root_node and is wrong about that. + can_undock_node = true; + } + + const bool clicked = IsMouseClicked(0); + const bool dragging = IsMouseDragging(0); + if (can_undock_node && dragging) + DockContextQueueUndockNode(&g, node); // Will lead to DockNodeStartMouseMovingWindow() -> StartMouseMovingWindow() being called next frame + else if (!can_undock_node && (clicked || dragging) && g.MovingWindow != window) + StartMouseMovingWindow(window); +} + // Handle mouse moving window // Note: moving window with the navigation keys (Square + d-pad / CTRL+TAB + Arrows) are processed in NavUpdateWindowing() +// FIXME: We don't have strong guarantee that g.MovingWindow stay synced with g.ActiveId == g.MovingWindow->MoveId. +// This is currently enforced by the fact that BeginDragDropSource() is setting all g.ActiveIdUsingXXXX flags to inhibit navigation inputs, +// but if we should more thoroughly test cases where g.ActiveId or g.MovingWindow gets changed and not the other. void ImGui::UpdateMouseMovingWindowNewFrame() { ImGuiContext& g = *GImGui; @@ -3367,22 +5096,45 @@ void ImGui::UpdateMouseMovingWindowNewFrame() // We actually want to move the root window. g.MovingWindow == window we clicked on (could be a child window). // We track it to preserve Focus and so that generally ActiveIdWindow == MovingWindow and ActiveId == MovingWindow->MoveId for consistency. KeepAliveID(g.ActiveId); - IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindow); - ImGuiWindow* moving_window = g.MovingWindow->RootWindow; - if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos)) + IM_ASSERT(g.MovingWindow && g.MovingWindow->RootWindowDockTree); + ImGuiWindow* moving_window = g.MovingWindow->RootWindowDockTree; + + // When a window stop being submitted while being dragged, it may will its viewport until next Begin() + const bool window_disappared = (!moving_window->WasActive && !moving_window->Active); + if (g.IO.MouseDown[0] && IsMousePosValid(&g.IO.MousePos) && !window_disappared) { ImVec2 pos = g.IO.MousePos - g.ActiveIdClickOffset; if (moving_window->Pos.x != pos.x || moving_window->Pos.y != pos.y) { - MarkIniSettingsDirty(moving_window); SetWindowPos(moving_window, pos, ImGuiCond_Always); + if (moving_window->Viewport && moving_window->ViewportOwned) // Synchronize viewport immediately because some overlays may relies on clipping rectangle before we Begin() into the window. + { + moving_window->Viewport->Pos = pos; + moving_window->Viewport->UpdateWorkRect(); + } } FocusWindow(g.MovingWindow); } else { - ClearActiveID(); + if (!window_disappared) + { + // Try to merge the window back into the main viewport. + // This works because MouseViewport should be != MovingWindow->Viewport on release (as per code in UpdateViewports) + if (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) + UpdateTryMergeWindowIntoHostViewport(moving_window, g.MouseViewport); + + // Restore the mouse viewport so that we don't hover the viewport _under_ the moved window during the frame we released the mouse button. + if (moving_window->Viewport && !IsDragDropPayloadBeingAccepted()) + g.MouseViewport = moving_window->Viewport; + + // Clear the NoInput window flag set by the Viewport system + if (moving_window->Viewport) + moving_window->Viewport->Flags &= ~ImGuiViewportFlags_NoInputs; + } + g.MovingWindow = NULL; + ClearActiveID(); } } else @@ -3397,291 +5149,198 @@ void ImGui::UpdateMouseMovingWindowNewFrame() } } -// Initiate moving window, handle left-click and right-click focus +// Initiate focusing and moving window when clicking on empty space or title bar. +// Initiate focusing window when clicking on a disabled item. +// Handle left-click and right-click focus. void ImGui::UpdateMouseMovingWindowEndFrame() { - // Initiate moving window ImGuiContext& g = *GImGui; - if (g.ActiveId != 0 || g.HoveredId != 0) + if (g.ActiveId != 0 || (g.HoveredId != 0 && !g.HoveredIdIsDisabled)) return; // Unless we just made a window/popup appear if (g.NavWindow && g.NavWindow->Appearing) return; - // Click to focus window and start moving (after we're done with all our widgets) + // Click on empty space to focus window and start moving + // (after we're done with all our widgets, so e.g. clicking on docking tab-bar which have set HoveredId already and not get us here!) if (g.IO.MouseClicked[0]) { - if (g.HoveredRootWindow != NULL) + // Handle the edge case of a popup being closed while clicking in its empty space. + // If we try to focus it, FocusWindow() > ClosePopupsOverWindow() will accidentally close any parent popups because they are not linked together any more. + ImGuiWindow* root_window = g.HoveredWindow ? g.HoveredWindow->RootWindow : NULL; + const bool is_closed_popup = root_window && (root_window->Flags & ImGuiWindowFlags_Popup) && !IsPopupOpen(root_window->PopupId, ImGuiPopupFlags_AnyPopupLevel); + + if (root_window != NULL && !is_closed_popup) { - StartMouseMovingWindow(g.HoveredWindow); - if (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(g.HoveredRootWindow->Flags & ImGuiWindowFlags_NoTitleBar)) - if (!g.HoveredRootWindow->TitleBarRect().Contains(g.IO.MouseClickedPos[0])) - g.MovingWindow = NULL; + StartMouseMovingWindow(g.HoveredWindow); //-V595 + + // Cancel moving if clicked outside of title bar + if (g.IO.ConfigWindowsMoveFromTitleBarOnly) + if (!(root_window->Flags & ImGuiWindowFlags_NoTitleBar) || root_window->DockIsActive) + if (!root_window->TitleBarRect().Contains(g.IO.MouseClickedPos[0])) + g.MovingWindow = NULL; + + // Cancel moving if clicked over an item which was disabled or inhibited by popups + // (when g.HoveredIdIsDisabled == true && g.HoveredId == 0 we are inhibited by popups, when g.HoveredIdIsDisabled == true && g.HoveredId != 0 we are over a disabled item)0 already) + if (g.HoveredIdIsDisabled) + g.MovingWindow = NULL; } - else if (g.NavWindow != NULL && GetTopMostPopupModal() == NULL) + else if (root_window == NULL && g.NavWindow != NULL) { // Clicking on void disable focus - FocusWindow(NULL); + FocusWindow(NULL, ImGuiFocusRequestFlags_UnlessBelowModal); } } // With right mouse button we close popups without changing focus based on where the mouse is aimed // Instead, focus will be restored to the window under the bottom-most closed popup. // (The left mouse button path calls FocusWindow on the hovered window, which will lead NewFrame->ClosePopupsOverWindow to trigger) - if (g.IO.MouseClicked[1]) + if (g.IO.MouseClicked[1] && g.HoveredId == 0) { // Find the top-most window between HoveredWindow and the top-most Modal Window. // This is where we can trim the popup stack. ImGuiWindow* modal = GetTopMostPopupModal(); - bool hovered_window_above_modal = false; - if (modal == NULL) - hovered_window_above_modal = true; - for (int i = g.Windows.Size - 1; i >= 0 && hovered_window_above_modal == false; i--) - { - ImGuiWindow* window = g.Windows[i]; - if (window == modal) - break; - if (window == g.HoveredWindow) - hovered_window_above_modal = true; - } + bool hovered_window_above_modal = g.HoveredWindow && (modal == NULL || IsWindowAbove(g.HoveredWindow, modal)); ClosePopupsOverWindow(hovered_window_above_modal ? g.HoveredWindow : modal, true); } } +// This is called during NewFrame()->UpdateViewportsNewFrame() only. +// Need to keep in sync with SetWindowPos() +static void TranslateWindow(ImGuiWindow* window, const ImVec2& delta) +{ + window->Pos += delta; + window->ClipRect.Translate(delta); + window->OuterRectClipped.Translate(delta); + window->InnerRect.Translate(delta); + window->DC.CursorPos += delta; + window->DC.CursorStartPos += delta; + window->DC.CursorMaxPos += delta; + window->DC.IdealMaxPos += delta; +} + +static void ScaleWindow(ImGuiWindow* window, float scale) +{ + ImVec2 origin = window->Viewport->Pos; + window->Pos = ImFloor((window->Pos - origin) * scale + origin); + window->Size = ImTrunc(window->Size * scale); + window->SizeFull = ImTrunc(window->SizeFull * scale); + window->ContentSize = ImTrunc(window->ContentSize * scale); +} + static bool IsWindowActiveAndVisible(ImGuiWindow* window) { return (window->Active) && (!window->Hidden); } -static void ImGui::UpdateMouseInputs() -{ - ImGuiContext& g = *GImGui; - - // Round mouse position to avoid spreading non-rounded position (e.g. UpdateManualResize doesn't support them well) - if (IsMousePosValid(&g.IO.MousePos)) - g.IO.MousePos = g.LastValidMousePos = ImFloor(g.IO.MousePos); - - // If mouse just appeared or disappeared (usually denoted by -FLT_MAX components) we cancel out movement in MouseDelta - if (IsMousePosValid(&g.IO.MousePos) && IsMousePosValid(&g.IO.MousePosPrev)) - g.IO.MouseDelta = g.IO.MousePos - g.IO.MousePosPrev; - else - g.IO.MouseDelta = ImVec2(0.0f, 0.0f); - if (g.IO.MouseDelta.x != 0.0f || g.IO.MouseDelta.y != 0.0f) - g.NavDisableMouseHover = false; - - g.IO.MousePosPrev = g.IO.MousePos; - for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++) - { - g.IO.MouseClicked[i] = g.IO.MouseDown[i] && g.IO.MouseDownDuration[i] < 0.0f; - g.IO.MouseReleased[i] = !g.IO.MouseDown[i] && g.IO.MouseDownDuration[i] >= 0.0f; - g.IO.MouseDownDurationPrev[i] = g.IO.MouseDownDuration[i]; - g.IO.MouseDownDuration[i] = g.IO.MouseDown[i] ? (g.IO.MouseDownDuration[i] < 0.0f ? 0.0f : g.IO.MouseDownDuration[i] + g.IO.DeltaTime) : -1.0f; - g.IO.MouseDoubleClicked[i] = false; - if (g.IO.MouseClicked[i]) - { - if ((float)(g.Time - g.IO.MouseClickedTime[i]) < g.IO.MouseDoubleClickTime) - { - ImVec2 delta_from_click_pos = IsMousePosValid(&g.IO.MousePos) ? (g.IO.MousePos - g.IO.MouseClickedPos[i]) : ImVec2(0.0f, 0.0f); - if (ImLengthSqr(delta_from_click_pos) < g.IO.MouseDoubleClickMaxDist * g.IO.MouseDoubleClickMaxDist) - g.IO.MouseDoubleClicked[i] = true; - g.IO.MouseClickedTime[i] = -FLT_MAX; // so the third click isn't turned into a double-click - } - else - { - g.IO.MouseClickedTime[i] = g.Time; - } - g.IO.MouseClickedPos[i] = g.IO.MousePos; - g.IO.MouseDownWasDoubleClick[i] = g.IO.MouseDoubleClicked[i]; - g.IO.MouseDragMaxDistanceAbs[i] = ImVec2(0.0f, 0.0f); - g.IO.MouseDragMaxDistanceSqr[i] = 0.0f; - } - else if (g.IO.MouseDown[i]) - { - // Maintain the maximum distance we reaching from the initial click position, which is used with dragging threshold - ImVec2 delta_from_click_pos = IsMousePosValid(&g.IO.MousePos) ? (g.IO.MousePos - g.IO.MouseClickedPos[i]) : ImVec2(0.0f, 0.0f); - g.IO.MouseDragMaxDistanceSqr[i] = ImMax(g.IO.MouseDragMaxDistanceSqr[i], ImLengthSqr(delta_from_click_pos)); - g.IO.MouseDragMaxDistanceAbs[i].x = ImMax(g.IO.MouseDragMaxDistanceAbs[i].x, delta_from_click_pos.x < 0.0f ? -delta_from_click_pos.x : delta_from_click_pos.x); - g.IO.MouseDragMaxDistanceAbs[i].y = ImMax(g.IO.MouseDragMaxDistanceAbs[i].y, delta_from_click_pos.y < 0.0f ? -delta_from_click_pos.y : delta_from_click_pos.y); - } - if (!g.IO.MouseDown[i] && !g.IO.MouseReleased[i]) - g.IO.MouseDownWasDoubleClick[i] = false; - if (g.IO.MouseClicked[i]) // Clicking any mouse button reactivate mouse hovering which may have been deactivated by gamepad/keyboard navigation - g.NavDisableMouseHover = false; - } -} - -static void StartLockWheelingWindow(ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - if (g.WheelingWindow == window) - return; - g.WheelingWindow = window; - g.WheelingWindowRefMousePos = g.IO.MousePos; - g.WheelingWindowTimer = WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER; -} - -void ImGui::UpdateMouseWheel() -{ - ImGuiContext& g = *GImGui; - - // Reset the locked window if we move the mouse or after the timer elapses - if (g.WheelingWindow != NULL) - { - g.WheelingWindowTimer -= g.IO.DeltaTime; - if (IsMousePosValid() && ImLengthSqr(g.IO.MousePos - g.WheelingWindowRefMousePos) > g.IO.MouseDragThreshold * g.IO.MouseDragThreshold) - g.WheelingWindowTimer = 0.0f; - if (g.WheelingWindowTimer <= 0.0f) - { - g.WheelingWindow = NULL; - g.WheelingWindowTimer = 0.0f; - } - } - - if (g.IO.MouseWheel == 0.0f && g.IO.MouseWheelH == 0.0f) - return; - - ImGuiWindow* window = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow; - if (!window || window->Collapsed) - return; - - // Zoom / Scale window - // FIXME-OBSOLETE: This is an old feature, it still works but pretty much nobody is using it and may be best redesigned. - if (g.IO.MouseWheel != 0.0f && g.IO.KeyCtrl && g.IO.FontAllowUserScaling) - { - StartLockWheelingWindow(window); - const float new_font_scale = ImClamp(window->FontWindowScale + g.IO.MouseWheel * 0.10f, 0.50f, 2.50f); - const float scale = new_font_scale / window->FontWindowScale; - window->FontWindowScale = new_font_scale; - if (!(window->Flags & ImGuiWindowFlags_ChildWindow)) - { - const ImVec2 offset = window->Size * (1.0f - scale) * (g.IO.MousePos - window->Pos) / window->Size; - SetWindowPos(window, window->Pos + offset, 0); - window->Size = ImFloor(window->Size * scale); - window->SizeFull = ImFloor(window->SizeFull * scale); - } - return; - } - - // Mouse wheel scrolling - // If a child window has the ImGuiWindowFlags_NoScrollWithMouse flag, we give a chance to scroll its parent - - // Vertical Mouse Wheel scrolling - const float wheel_y = (g.IO.MouseWheel != 0.0f && !g.IO.KeyShift) ? g.IO.MouseWheel : 0.0f; - if (wheel_y != 0.0f && !g.IO.KeyCtrl) - { - StartLockWheelingWindow(window); - while ((window->Flags & ImGuiWindowFlags_ChildWindow) && ((window->ScrollMax.y == 0.0f) || ((window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)))) - window = window->ParentWindow; - if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)) - { - float max_step = window->InnerRect.GetHeight() * 0.67f; - float scroll_step = ImFloor(ImMin(5 * window->CalcFontSize(), max_step)); - SetScrollY(window, window->Scroll.y - wheel_y * scroll_step); - } - } - - // Horizontal Mouse Wheel scrolling, or Vertical Mouse Wheel w/ Shift held - const float wheel_x = (g.IO.MouseWheelH != 0.0f && !g.IO.KeyShift) ? g.IO.MouseWheelH : (g.IO.MouseWheel != 0.0f && g.IO.KeyShift) ? g.IO.MouseWheel : 0.0f; - if (wheel_x != 0.0f && !g.IO.KeyCtrl) - { - StartLockWheelingWindow(window); - while ((window->Flags & ImGuiWindowFlags_ChildWindow) && ((window->ScrollMax.x == 0.0f) || ((window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)))) - window = window->ParentWindow; - if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)) - { - float max_step = window->InnerRect.GetWidth() * 0.67f; - float scroll_step = ImFloor(ImMin(2 * window->CalcFontSize(), max_step)); - SetScrollX(window, window->Scroll.x - wheel_x * scroll_step); - } - } -} - // The reason this is exposed in imgui_internal.h is: on touch-based system that don't have hovering, we want to dispatch inputs to the right target (imgui vs imgui+app) void ImGui::UpdateHoveredWindowAndCaptureFlags() { ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // FIXME-DPI: This storage was added on 2021/03/31 for test engine, but if we want to multiply WINDOWS_HOVER_PADDING + // by DpiScale, we need to make this window-agnostic anyhow, maybe need storing inside ImGuiWindow. + g.WindowsBorderHoverPadding = ImMax(ImMax(g.Style.TouchExtraPadding.x, g.Style.TouchExtraPadding.y), g.Style.WindowBorderHoverPadding); // Find the window hovered by mouse: // - Child windows can extend beyond the limit of their parent so we need to derive HoveredRootWindow from HoveredWindow. // - When moving a window we can skip the search, which also conveniently bypasses the fact that window->WindowRectClipped is lagging as this point of the frame. // - We also support the moved window toggling the NoInputs flag after moving has started in order to be able to detect windows below it, which is useful for e.g. docking mechanisms. - FindHoveredWindow(); + bool clear_hovered_windows = false; + FindHoveredWindowEx(g.IO.MousePos, false, &g.HoveredWindow, &g.HoveredWindowUnderMovingWindow); + IM_ASSERT(g.HoveredWindow == NULL || g.HoveredWindow == g.MovingWindow || g.HoveredWindow->Viewport == g.MouseViewport); + g.HoveredWindowBeforeClear = g.HoveredWindow; - // Modal windows prevents cursor from hovering behind them. + // Modal windows prevents mouse from hovering behind them. ImGuiWindow* modal_window = GetTopMostPopupModal(); - if (modal_window) - if (g.HoveredRootWindow && !IsWindowChildOf(g.HoveredRootWindow, modal_window)) - g.HoveredRootWindow = g.HoveredWindow = NULL; + if (modal_window && g.HoveredWindow && !IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, modal_window)) // FIXME-MERGE: RootWindowDockTree ? + clear_hovered_windows = true; - // Disabled mouse? - if (g.IO.ConfigFlags & ImGuiConfigFlags_NoMouse) - g.HoveredWindow = g.HoveredRootWindow = NULL; + // Disabled mouse hovering (we don't currently clear MousePos, we could) + if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) + clear_hovered_windows = true; - // We track click ownership. When clicked outside of a window the click is owned by the application and won't report hovering nor request capture even while dragging over our windows afterward. - int mouse_earliest_button_down = -1; + // We track click ownership. When clicked outside of a window the click is owned by the application and + // won't report hovering nor request capture even while dragging over our windows afterward. + const bool has_open_popup = (g.OpenPopupStack.Size > 0); + const bool has_open_modal = (modal_window != NULL); + int mouse_earliest_down = -1; bool mouse_any_down = false; - for (int i = 0; i < IM_ARRAYSIZE(g.IO.MouseDown); i++) + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) { - if (g.IO.MouseClicked[i]) - g.IO.MouseDownOwned[i] = (g.HoveredWindow != NULL) || (!g.OpenPopupStack.empty()); - mouse_any_down |= g.IO.MouseDown[i]; - if (g.IO.MouseDown[i]) - if (mouse_earliest_button_down == -1 || g.IO.MouseClickedTime[i] < g.IO.MouseClickedTime[mouse_earliest_button_down]) - mouse_earliest_button_down = i; + if (io.MouseClicked[i]) + { + io.MouseDownOwned[i] = (g.HoveredWindow != NULL) || has_open_popup; + io.MouseDownOwnedUnlessPopupClose[i] = (g.HoveredWindow != NULL) || has_open_modal; + } + mouse_any_down |= io.MouseDown[i]; + if (io.MouseDown[i] || io.MouseReleased[i]) // Increase release frame for our evaluation of earliest button (#1392) + if (mouse_earliest_down == -1 || io.MouseClickedTime[i] < io.MouseClickedTime[mouse_earliest_down]) + mouse_earliest_down = i; } - const bool mouse_avail_to_imgui = (mouse_earliest_button_down == -1) || g.IO.MouseDownOwned[mouse_earliest_button_down]; + const bool mouse_avail = (mouse_earliest_down == -1) || io.MouseDownOwned[mouse_earliest_down]; + const bool mouse_avail_unless_popup_close = (mouse_earliest_down == -1) || io.MouseDownOwnedUnlessPopupClose[mouse_earliest_down]; // If mouse was first clicked outside of ImGui bounds we also cancel out hovering. // FIXME: For patterns of drag and drop across OS windows, we may need to rework/remove this test (first committed 311c0ca9 on 2015/02) const bool mouse_dragging_extern_payload = g.DragDropActive && (g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) != 0; - if (!mouse_avail_to_imgui && !mouse_dragging_extern_payload) - g.HoveredWindow = g.HoveredRootWindow = NULL; + if (!mouse_avail && !mouse_dragging_extern_payload) + clear_hovered_windows = true; - // Update io.WantCaptureMouse for the user application (true = dispatch mouse info to imgui, false = dispatch mouse info to Dear ImGui + app) + if (clear_hovered_windows) + g.HoveredWindow = g.HoveredWindowUnderMovingWindow = NULL; + + // Update io.WantCaptureMouse for the user application (true = dispatch mouse info to Dear ImGui only, false = dispatch mouse to Dear ImGui + underlying app) + // Update io.WantCaptureMouseAllowPopupClose (experimental) to give a chance for app to react to popup closure with a drag if (g.WantCaptureMouseNextFrame != -1) - g.IO.WantCaptureMouse = (g.WantCaptureMouseNextFrame != 0); + { + io.WantCaptureMouse = io.WantCaptureMouseUnlessPopupClose = (g.WantCaptureMouseNextFrame != 0); + } else - g.IO.WantCaptureMouse = (mouse_avail_to_imgui && (g.HoveredWindow != NULL || mouse_any_down)) || (!g.OpenPopupStack.empty()); + { + io.WantCaptureMouse = (mouse_avail && (g.HoveredWindow != NULL || mouse_any_down)) || has_open_popup; + io.WantCaptureMouseUnlessPopupClose = (mouse_avail_unless_popup_close && (g.HoveredWindow != NULL || mouse_any_down)) || has_open_modal; + } - // Update io.WantCaptureKeyboard for the user application (true = dispatch keyboard info to imgui, false = dispatch keyboard info to Dear ImGui + app) - if (g.WantCaptureKeyboardNextFrame != -1) - g.IO.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); - else - g.IO.WantCaptureKeyboard = (g.ActiveId != 0) || (modal_window != NULL); - if (g.IO.NavActive && (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) && !(g.IO.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard)) - g.IO.WantCaptureKeyboard = true; + // Update io.WantCaptureKeyboard for the user application (true = dispatch keyboard info to Dear ImGui only, false = dispatch keyboard info to Dear ImGui + underlying app) + io.WantCaptureKeyboard = false; + if ((io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) == 0) + { + if ((g.ActiveId != 0) || (modal_window != NULL)) + io.WantCaptureKeyboard = true; + else if (io.NavActive && (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) && io.ConfigNavCaptureKeyboard) + io.WantCaptureKeyboard = true; + } + if (g.WantCaptureKeyboardNextFrame != -1) // Manual override + io.WantCaptureKeyboard = (g.WantCaptureKeyboardNextFrame != 0); // Update io.WantTextInput flag, this is to allow systems without a keyboard (e.g. mobile, hand-held) to show a software keyboard if possible - g.IO.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : false; + io.WantTextInput = (g.WantTextInputNextFrame != -1) ? (g.WantTextInputNextFrame != 0) : false; } -static void NewFrameSanityChecks() +// Called once a frame. Followed by SetCurrentFont() which sets up the remaining data. +// FIXME-VIEWPORT: the concept of a single ClipRectFullscreen is not ideal! +static void SetupDrawListSharedData() { ImGuiContext& g = *GImGui; - - // Check user data - // (We pass an error message in the assert expression to make it visible to programmers who are not using a debugger, as most assert handlers display their argument) - IM_ASSERT(g.Initialized); - IM_ASSERT((g.IO.DeltaTime > 0.0f || g.FrameCount == 0) && "Need a positive DeltaTime!"); - IM_ASSERT((g.FrameCount == 0 || g.FrameCountEnded == g.FrameCount) && "Forgot to call Render() or EndFrame() at the end of the previous frame?"); - IM_ASSERT(g.IO.DisplaySize.x >= 0.0f && g.IO.DisplaySize.y >= 0.0f && "Invalid DisplaySize value!"); - IM_ASSERT(g.IO.Fonts->Fonts.Size > 0 && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8() ?"); - IM_ASSERT(g.IO.Fonts->Fonts[0]->IsLoaded() && "Font Atlas not built. Did you call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8() ?"); - IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting!"); - IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting. Alpha cannot be negative (allows us to avoid a few clamps in color computations)!"); - IM_ASSERT(g.Style.WindowMinSize.x >= 1.0f && g.Style.WindowMinSize.y >= 1.0f && "Invalid style setting."); - IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right); - for (int n = 0; n < ImGuiKey_COUNT; n++) - IM_ASSERT(g.IO.KeyMap[n] >= -1 && g.IO.KeyMap[n] < IM_ARRAYSIZE(g.IO.KeysDown) && "io.KeyMap[] contains an out of bound value (need to be 0..512, or -1 for unmapped key)"); - - // Perform simple check: required key mapping (we intentionally do NOT check all keys to not pressure user into setting up everything, but Space is required and was only recently added in 1.60 WIP) - if (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) - IM_ASSERT(g.IO.KeyMap[ImGuiKey_Space] != -1 && "ImGuiKey_Space is not mapped, required for keyboard navigation."); - - // Perform simple check: the beta io.ConfigWindowsResizeFromEdges option requires back-end to honor mouse cursor changes and set the ImGuiBackendFlags_HasMouseCursors flag accordingly. - if (g.IO.ConfigWindowsResizeFromEdges && !(g.IO.BackendFlags & ImGuiBackendFlags_HasMouseCursors)) - g.IO.ConfigWindowsResizeFromEdges = false; + ImRect virtual_space(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (ImGuiViewportP* viewport : g.Viewports) + virtual_space.Add(viewport->GetMainRect()); + g.DrawListSharedData.ClipRectFullscreen = virtual_space.ToVec4(); + g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; + g.DrawListSharedData.SetCircleTessellationMaxError(g.Style.CircleTessellationMaxError); + g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; + if (g.Style.AntiAliasedLines) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; + if (g.Style.AntiAliasedLinesUseTex && !(g.IO.Fonts->Flags & ImFontAtlasFlags_NoBakedLines)) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLinesUseTex; + if (g.Style.AntiAliasedFill) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedFill; + if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) + g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; + g.DrawListSharedData.InitialFringeScale = 1.0f; // FIXME-DPI: Change this for some DPI scaling experiments. } void ImGui::NewFrame() @@ -3689,72 +5348,68 @@ void ImGui::NewFrame() IM_ASSERT(GImGui != NULL && "No current context. Did you call ImGui::CreateContext() and ImGui::SetCurrentContext() ?"); ImGuiContext& g = *GImGui; -#ifdef IMGUI_ENABLE_TEST_ENGINE - ImGuiTestEngineHook_PreNewFrame(&g); -#endif + // Remove pending delete hooks before frame start. + // This deferred removal avoid issues of removal while iterating the hook vector + for (int n = g.Hooks.Size - 1; n >= 0; n--) + if (g.Hooks[n].Type == ImGuiContextHookType_PendingRemoval_) + g.Hooks.erase(&g.Hooks[n]); + + CallContextHooks(&g, ImGuiContextHookType_NewFramePre); // Check and assert for various common IO and Configuration mistakes - NewFrameSanityChecks(); + g.ConfigFlagsLastFrame = g.ConfigFlagsCurrFrame; + ErrorCheckNewFrameSanityChecks(); + g.ConfigFlagsCurrFrame = g.IO.ConfigFlags; - // Load settings on first frame (if not explicitly loaded manually before) - if (!g.SettingsLoaded) - { - IM_ASSERT(g.SettingsWindows.empty()); - if (g.IO.IniFilename) - LoadIniSettingsFromDisk(g.IO.IniFilename); - g.SettingsLoaded = true; - } - - // Save settings (with a delay after the last modification, so we don't spam disk too much) - if (g.SettingsDirtyTimer > 0.0f) - { - g.SettingsDirtyTimer -= g.IO.DeltaTime; - if (g.SettingsDirtyTimer <= 0.0f) - { - if (g.IO.IniFilename != NULL) - SaveIniSettingsToDisk(g.IO.IniFilename); - else - g.IO.WantSaveIniSettings = true; // Let user know they can call SaveIniSettingsToMemory(). user will need to clear io.WantSaveIniSettings themselves. - g.SettingsDirtyTimer = 0.0f; - } - } + // Load settings on first frame, save settings when modified (after a delay) + UpdateSettings(); g.Time += g.IO.DeltaTime; - g.FrameScopeActive = true; + g.WithinFrameScope = true; g.FrameCount += 1; g.TooltipOverrideCount = 0; g.WindowsActiveCount = 0; + g.MenusIdSubmittedThisFrame.resize(0); + + // Calculate frame-rate for the user, as a purely luxurious feature + g.FramerateSecPerFrameAccum += g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx]; + g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime; + g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % IM_ARRAYSIZE(g.FramerateSecPerFrame); + g.FramerateSecPerFrameCount = ImMin(g.FramerateSecPerFrameCount + 1, IM_ARRAYSIZE(g.FramerateSecPerFrame)); + g.IO.Framerate = (g.FramerateSecPerFrameAccum > 0.0f) ? (1.0f / (g.FramerateSecPerFrameAccum / (float)g.FramerateSecPerFrameCount)) : FLT_MAX; + + // Process input queue (trickle as many events as possible), turn events into writes to IO structure + g.InputEventsTrail.resize(0); + UpdateInputEvents(g.IO.ConfigInputTrickleEventQueue); + + // Update viewports (after processing input queue, so io.MouseHoveredViewport is set) + UpdateViewportsNewFrame(); // Setup current font and draw list shared data + // FIXME-VIEWPORT: the concept of a single ClipRectFullscreen is not ideal! g.IO.Fonts->Locked = true; + SetupDrawListSharedData(); SetCurrentFont(GetDefaultFont()); IM_ASSERT(g.Font->IsLoaded()); - g.DrawListSharedData.ClipRectFullscreen = ImVec4(0.0f, 0.0f, g.IO.DisplaySize.x, g.IO.DisplaySize.y); - g.DrawListSharedData.CurveTessellationTol = g.Style.CurveTessellationTol; - g.DrawListSharedData.InitialFlags = ImDrawListFlags_None; - if (g.Style.AntiAliasedLines) - g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedLines; - if (g.Style.AntiAliasedFill) - g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AntiAliasedFill; - if (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) - g.DrawListSharedData.InitialFlags |= ImDrawListFlags_AllowVtxOffset; - - g.BackgroundDrawList.Clear(); - g.BackgroundDrawList.PushTextureID(g.IO.Fonts->TexID); - g.BackgroundDrawList.PushClipRectFullScreen(); - - g.ForegroundDrawList.Clear(); - g.ForegroundDrawList.PushTextureID(g.IO.Fonts->TexID); - g.ForegroundDrawList.PushClipRectFullScreen(); // Mark rendering data as invalid to prevent user who may have a handle on it to use it. - g.DrawData.Clear(); + for (ImGuiViewportP* viewport : g.Viewports) + { + viewport->DrawData = NULL; + viewport->DrawDataP.Valid = false; + } // Drag and drop keep the source ID alive so even if the source disappear our state is consistent if (g.DragDropActive && g.DragDropPayload.SourceId == g.ActiveId) KeepAliveID(g.DragDropPayload.SourceId); - // Clear reference to active widget if the widget isn't alive anymore + // [DEBUG] + if (!g.IO.ConfigDebugHighlightIdConflicts || !g.IO.KeyCtrl) // Count is locked while holding CTRL + g.DebugDrawIdConflicts = 0; + if (g.IO.ConfigDebugHighlightIdConflicts && g.HoveredIdPreviousFrameItemCount > 1) + g.DebugDrawIdConflicts = g.HoveredIdPreviousFrame; + + // Update HoveredId data if (!g.HoveredIdPreviousFrame) g.HoveredIdTimer = 0.0f; if (!g.HoveredIdPreviousFrame || (g.HoveredId && g.ActiveId == g.HoveredId)) @@ -3764,48 +5419,118 @@ void ImGui::NewFrame() if (g.HoveredId && g.ActiveId != g.HoveredId) g.HoveredIdNotActiveTimer += g.IO.DeltaTime; g.HoveredIdPreviousFrame = g.HoveredId; + g.HoveredIdPreviousFrameItemCount = 0; g.HoveredId = 0; g.HoveredIdAllowOverlap = false; - if (g.ActiveIdIsAlive != g.ActiveId && g.ActiveIdPreviousFrame == g.ActiveId && g.ActiveId != 0) + g.HoveredIdIsDisabled = false; + + // Clear ActiveID if the item is not alive anymore. + // In 1.87, the common most call to KeepAliveID() was moved from GetID() to ItemAdd(). + // As a result, custom widget using ButtonBehavior() _without_ ItemAdd() need to call KeepAliveID() themselves. + if (g.ActiveId != 0 && g.ActiveIdIsAlive != g.ActiveId && g.ActiveIdPreviousFrame == g.ActiveId) + { + IMGUI_DEBUG_LOG_ACTIVEID("NewFrame(): ClearActiveID() because it isn't marked alive anymore!\n"); ClearActiveID(); + } + + // Update ActiveId data (clear reference to active widget if the widget isn't alive anymore) if (g.ActiveId) g.ActiveIdTimer += g.IO.DeltaTime; g.LastActiveIdTimer += g.IO.DeltaTime; g.ActiveIdPreviousFrame = g.ActiveId; - g.ActiveIdPreviousFrameWindow = g.ActiveIdWindow; - g.ActiveIdPreviousFrameHasBeenEditedBefore = g.ActiveIdHasBeenEditedBefore; g.ActiveIdIsAlive = 0; g.ActiveIdHasBeenEditedThisFrame = false; - g.ActiveIdPreviousFrameIsAlive = false; g.ActiveIdIsJustActivated = false; - if (g.TempInputTextId != 0 && g.ActiveId != g.TempInputTextId) - g.TempInputTextId = 0; + if (g.TempInputId != 0 && g.ActiveId != g.TempInputId) + g.TempInputId = 0; + if (g.ActiveId == 0) + { + g.ActiveIdUsingNavDirMask = 0x00; + g.ActiveIdUsingAllKeyboardKeys = false; + } + if (g.DeactivatedItemData.ElapseFrame < g.FrameCount) + g.DeactivatedItemData.ID = 0; + g.DeactivatedItemData.IsAlive = false; + + // Record when we have been stationary as this state is preserved while over same item. + // FIXME: The way this is expressed means user cannot alter HoverStationaryDelay during the frame to use varying values. + // To allow this we should store HoverItemMaxStationaryTime+ID and perform the >= check in IsItemHovered() function. + if (g.HoverItemDelayId != 0 && g.MouseStationaryTimer >= g.Style.HoverStationaryDelay) + g.HoverItemUnlockedStationaryId = g.HoverItemDelayId; + else if (g.HoverItemDelayId == 0) + g.HoverItemUnlockedStationaryId = 0; + if (g.HoveredWindow != NULL && g.MouseStationaryTimer >= g.Style.HoverStationaryDelay) + g.HoverWindowUnlockedStationaryId = g.HoveredWindow->ID; + else if (g.HoveredWindow == NULL) + g.HoverWindowUnlockedStationaryId = 0; + + // Update hover delay for IsItemHovered() with delays and tooltips + g.HoverItemDelayIdPreviousFrame = g.HoverItemDelayId; + if (g.HoverItemDelayId != 0) + { + g.HoverItemDelayTimer += g.IO.DeltaTime; + g.HoverItemDelayClearTimer = 0.0f; + g.HoverItemDelayId = 0; + } + else if (g.HoverItemDelayTimer > 0.0f) + { + // This gives a little bit of leeway before clearing the hover timer, allowing mouse to cross gaps + // We could expose 0.25f as style.HoverClearDelay but I am not sure of the logic yet, this is particularly subtle. + g.HoverItemDelayClearTimer += g.IO.DeltaTime; + if (g.HoverItemDelayClearTimer >= ImMax(0.25f, g.IO.DeltaTime * 2.0f)) // ~7 frames at 30 Hz + allow for low framerate + g.HoverItemDelayTimer = g.HoverItemDelayClearTimer = 0.0f; // May want a decaying timer, in which case need to clamp at max first, based on max of caller last requested timer. + } // Drag and drop g.DragDropAcceptIdPrev = g.DragDropAcceptIdCurr; g.DragDropAcceptIdCurr = 0; g.DragDropAcceptIdCurrRectSurface = FLT_MAX; - g.DragDropWithinSourceOrTarget = false; + g.DragDropWithinSource = false; + g.DragDropWithinTarget = false; + g.DragDropHoldJustPressedId = 0; + g.TooltipPreviousWindow = NULL; + + // Close popups on focus lost (currently wip/opt-in) + //if (g.IO.AppFocusLost) + // ClosePopupsExceptModals(); // Update keyboard input state - memcpy(g.IO.KeysDownDurationPrev, g.IO.KeysDownDuration, sizeof(g.IO.KeysDownDuration)); - for (int i = 0; i < IM_ARRAYSIZE(g.IO.KeysDown); i++) - g.IO.KeysDownDuration[i] = g.IO.KeysDown[i] ? (g.IO.KeysDownDuration[i] < 0.0f ? 0.0f : g.IO.KeysDownDuration[i] + g.IO.DeltaTime) : -1.0f; + UpdateKeyboardInputs(); - // Update gamepad/keyboard directional navigation + //IM_ASSERT(g.IO.KeyCtrl == IsKeyDown(ImGuiKey_LeftCtrl) || IsKeyDown(ImGuiKey_RightCtrl)); + //IM_ASSERT(g.IO.KeyShift == IsKeyDown(ImGuiKey_LeftShift) || IsKeyDown(ImGuiKey_RightShift)); + //IM_ASSERT(g.IO.KeyAlt == IsKeyDown(ImGuiKey_LeftAlt) || IsKeyDown(ImGuiKey_RightAlt)); + //IM_ASSERT(g.IO.KeySuper == IsKeyDown(ImGuiKey_LeftSuper) || IsKeyDown(ImGuiKey_RightSuper)); + + // Update keyboard/gamepad navigation NavUpdate(); // Update mouse input state UpdateMouseInputs(); - // Calculate frame-rate for the user, as a purely luxurious feature - g.FramerateSecPerFrameAccum += g.IO.DeltaTime - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx]; - g.FramerateSecPerFrame[g.FramerateSecPerFrameIdx] = g.IO.DeltaTime; - g.FramerateSecPerFrameIdx = (g.FramerateSecPerFrameIdx + 1) % IM_ARRAYSIZE(g.FramerateSecPerFrame); - g.IO.Framerate = (g.FramerateSecPerFrameAccum > 0.0f) ? (1.0f / (g.FramerateSecPerFrameAccum / (float)IM_ARRAYSIZE(g.FramerateSecPerFrame))) : FLT_MAX; + // Undocking + // (needs to be before UpdateMouseMovingWindowNewFrame so the window is already offset and following the mouse on the detaching frame) + DockContextNewFrameUpdateUndocking(&g); + + // Mark all windows as not visible and compact unused memory. + IM_ASSERT(g.WindowsFocusOrder.Size <= g.Windows.Size); + const float memory_compact_start_time = (g.GcCompactAll || g.IO.ConfigMemoryCompactTimer < 0.0f) ? FLT_MAX : (float)g.Time - g.IO.ConfigMemoryCompactTimer; + for (ImGuiWindow* window : g.Windows) + { + window->WasActive = window->Active; + window->Active = false; + window->WriteAccessed = false; + window->BeginCountPreviousFrame = window->BeginCount; + window->BeginCount = 0; + + // Garbage collect transient buffers of recently unused windows + if (!window->WasActive && !window->MemoryCompacted && window->LastTimeActive < memory_compact_start_time) + GcCompactTransientWindowBuffers(window); + } // Find hovered window // (needs to be before UpdateMouseMovingWindowNewFrame so we fill g.HoveredWindowUnderMovingWindow on the mouse release frame) + // (currently needs to be done after the WasActive=Active loop and FindHoveredWindowEx uses ->Active) UpdateHoveredWindowAndCaptureFlags(); // Handle user moving window with mouse (at the beginning of the frame to avoid input lag or sheering) @@ -3819,192 +5544,81 @@ void ImGui::NewFrame() g.MouseCursor = ImGuiMouseCursor_Arrow; g.WantCaptureMouseNextFrame = g.WantCaptureKeyboardNextFrame = g.WantTextInputNextFrame = -1; - g.PlatformImePos = ImVec2(1.0f, 1.0f); // OS Input Method Editor showing on top-left of our window by default + + // Platform IME data: reset for the frame + g.PlatformImeDataPrev = g.PlatformImeData; + g.PlatformImeData.WantVisible = false; // Mouse wheel scrolling, scale UpdateMouseWheel(); - // Pressing TAB activate widget focus - g.FocusTabPressed = (g.NavWindow && g.NavWindow->Active && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) && !g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_Tab)); - if (g.ActiveId == 0 && g.FocusTabPressed) - { - // Note that SetKeyboardFocusHere() sets the Next fields mid-frame. To be consistent we also - // manipulate the Next fields even, even though they will be turned into Curr fields by the code below. - g.FocusRequestNextWindow = g.NavWindow; - g.FocusRequestNextCounterAll = INT_MAX; - if (g.NavId != 0 && g.NavIdTabCounter != INT_MAX) - g.FocusRequestNextCounterTab = g.NavIdTabCounter + 1 + (g.IO.KeyShift ? -1 : 1); - else - g.FocusRequestNextCounterTab = g.IO.KeyShift ? -1 : 0; - } - - // Turn queued focus request into current one - g.FocusRequestCurrWindow = NULL; - g.FocusRequestCurrCounterAll = g.FocusRequestCurrCounterTab = INT_MAX; - if (g.FocusRequestNextWindow != NULL) - { - ImGuiWindow* window = g.FocusRequestNextWindow; - g.FocusRequestCurrWindow = window; - if (g.FocusRequestNextCounterAll != INT_MAX && window->DC.FocusCounterAll != -1) - g.FocusRequestCurrCounterAll = ImModPositive(g.FocusRequestNextCounterAll, window->DC.FocusCounterAll + 1); - if (g.FocusRequestNextCounterTab != INT_MAX && window->DC.FocusCounterTab != -1) - g.FocusRequestCurrCounterTab = ImModPositive(g.FocusRequestNextCounterTab, window->DC.FocusCounterTab + 1); - g.FocusRequestNextWindow = NULL; - g.FocusRequestNextCounterAll = g.FocusRequestNextCounterTab = INT_MAX; - } - - g.NavIdTabCounter = INT_MAX; - - // Mark all windows as not visible and compact unused memory. - IM_ASSERT(g.WindowsFocusOrder.Size == g.Windows.Size); - const float memory_compact_start_time = (g.IO.ConfigWindowsMemoryCompactTimer >= 0.0f) ? (float)g.Time - g.IO.ConfigWindowsMemoryCompactTimer : FLT_MAX; - for (int i = 0; i != g.Windows.Size; i++) - { - ImGuiWindow* window = g.Windows[i]; - window->WasActive = window->Active; - window->BeginCount = 0; - window->Active = false; - window->WriteAccessed = false; - - // Garbage collect (this is totally functional but we may need decide if the side-effects are desirable) - if (!window->WasActive && !window->MemoryCompacted && window->LastTimeActive < memory_compact_start_time) - GcCompactTransientWindowBuffers(window); - } + // Garbage collect transient buffers of recently unused tables + for (int i = 0; i < g.TablesLastTimeActive.Size; i++) + if (g.TablesLastTimeActive[i] >= 0.0f && g.TablesLastTimeActive[i] < memory_compact_start_time) + TableGcCompactTransientBuffers(g.Tables.GetByIndex(i)); + for (ImGuiTableTempData& table_temp_data : g.TablesTempData) + if (table_temp_data.LastTimeActive >= 0.0f && table_temp_data.LastTimeActive < memory_compact_start_time) + TableGcCompactTransientBuffers(&table_temp_data); + if (g.GcCompactAll) + GcCompactTransientMiscBuffers(); + g.GcCompactAll = false; // Closing the focused window restore focus to the first active root window in descending z-order if (g.NavWindow && !g.NavWindow->WasActive) - FocusTopMostWindowUnderOne(NULL, NULL); + FocusTopMostWindowUnderOne(NULL, NULL, NULL, ImGuiFocusRequestFlags_RestoreFocusedChild); // No window should be open at the beginning of the frame. // But in order to allow the user to call NewFrame() multiple times without calling Render(), we are doing an explicit clear. g.CurrentWindowStack.resize(0); g.BeginPopupStack.resize(0); - ClosePopupsOverWindow(g.NavWindow, false); + g.ItemFlagsStack.resize(0); + g.ItemFlagsStack.push_back(ImGuiItemFlags_AutoClosePopups); // Default flags + g.CurrentItemFlags = g.ItemFlagsStack.back(); + g.GroupStack.resize(0); - // [DEBUG] Item picker tool - start with DebugStartItemPicker() - useful to visually select an item and break into its call-stack. + // Docking + DockContextNewFrameUpdateDocking(&g); + + // [DEBUG] Update debug features +#ifndef IMGUI_DISABLE_DEBUG_TOOLS UpdateDebugToolItemPicker(); + UpdateDebugToolStackQueries(); + UpdateDebugToolFlashStyleColor(); + if (g.DebugLocateFrames > 0 && --g.DebugLocateFrames == 0) + { + g.DebugLocateId = 0; + g.DebugBreakInLocateId = false; + } + if (g.DebugLogAutoDisableFrames > 0 && --g.DebugLogAutoDisableFrames == 0) + { + DebugLog("(Debug Log: Auto-disabled some ImGuiDebugLogFlags after 2 frames)\n"); + g.DebugLogFlags &= ~g.DebugLogAutoDisableFlags; + g.DebugLogAutoDisableFlags = ImGuiDebugLogFlags_None; + } +#endif // Create implicit/fallback window - which we will only render it if the user has added something to it. // We don't use "Debug" to avoid colliding with user trying to create a "Debug" window with custom flags. - // This fallback is particularly important as it avoid ImGui:: calls from crashing. - SetNextWindowSize(ImVec2(400,400), ImGuiCond_FirstUseEver); + // This fallback is particularly important as it prevents ImGui:: calls from crashing. + g.WithinFrameScopeWithImplicitWindow = true; + SetNextWindowSize(ImVec2(400, 400), ImGuiCond_FirstUseEver); Begin("Debug##Default"); - g.FrameScopePushedImplicitWindow = true; + IM_ASSERT(g.CurrentWindow->IsFallbackWindow == true); -#ifdef IMGUI_ENABLE_TEST_ENGINE - ImGuiTestEngineHook_PostNewFrame(&g); + // Store stack sizes + g.ErrorCountCurrentFrame = 0; + ErrorRecoveryStoreState(&g.StackSizesInNewFrame); + + // [DEBUG] When io.ConfigDebugBeginReturnValue is set, we make Begin()/BeginChild() return false at different level of the window-stack, + // allowing to validate correct Begin/End behavior in user code. +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.IO.ConfigDebugBeginReturnValueLoop) + g.DebugBeginReturnValueCullDepth = (g.DebugBeginReturnValueCullDepth == -1) ? 0 : ((g.DebugBeginReturnValueCullDepth + ((g.FrameCount % 4) == 0 ? 1 : 0)) % 10); + else + g.DebugBeginReturnValueCullDepth = -1; #endif -} -// [DEBUG] Item picker tool - start with DebugStartItemPicker() - useful to visually select an item and break into its call-stack. -void ImGui::UpdateDebugToolItemPicker() -{ - ImGuiContext& g = *GImGui; - g.DebugItemPickerBreakID = 0; - if (g.DebugItemPickerActive) - { - const ImGuiID hovered_id = g.HoveredIdPreviousFrame; - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - if (ImGui::IsKeyPressedMap(ImGuiKey_Escape)) - g.DebugItemPickerActive = false; - if (ImGui::IsMouseClicked(0) && hovered_id) - { - g.DebugItemPickerBreakID = hovered_id; - g.DebugItemPickerActive = false; - } - ImGui::SetNextWindowBgAlpha(0.60f); - ImGui::BeginTooltip(); - ImGui::Text("HoveredId: 0x%08X", hovered_id); - ImGui::Text("Press ESC to abort picking."); - ImGui::TextColored(GetStyleColorVec4(hovered_id ? ImGuiCol_Text : ImGuiCol_TextDisabled), "Click to break in debugger!"); - ImGui::EndTooltip(); - } -} - -void ImGui::Initialize(ImGuiContext* context) -{ - ImGuiContext& g = *context; - IM_ASSERT(!g.Initialized && !g.SettingsLoaded); - - // Add .ini handle for ImGuiWindow type - ImGuiSettingsHandler ini_handler; - ini_handler.TypeName = "Window"; - ini_handler.TypeHash = ImHashStr("Window"); - ini_handler.ReadOpenFn = SettingsHandlerWindow_ReadOpen; - ini_handler.ReadLineFn = SettingsHandlerWindow_ReadLine; - ini_handler.WriteAllFn = SettingsHandlerWindow_WriteAll; - g.SettingsHandlers.push_back(ini_handler); - - g.Initialized = true; -} - -// This function is merely here to free heap allocations. -void ImGui::Shutdown(ImGuiContext* context) -{ - // The fonts atlas can be used prior to calling NewFrame(), so we clear it even if g.Initialized is FALSE (which would happen if we never called NewFrame) - ImGuiContext& g = *context; - if (g.IO.Fonts && g.FontAtlasOwnedByContext) - { - g.IO.Fonts->Locked = false; - IM_DELETE(g.IO.Fonts); - } - g.IO.Fonts = NULL; - - // Cleanup of other data are conditional on actually having initialized Dear ImGui. - if (!g.Initialized) - return; - - // Save settings (unless we haven't attempted to load them: CreateContext/DestroyContext without a call to NewFrame shouldn't save an empty file) - if (g.SettingsLoaded && g.IO.IniFilename != NULL) - { - ImGuiContext* backup_context = GImGui; - SetCurrentContext(context); - SaveIniSettingsToDisk(g.IO.IniFilename); - SetCurrentContext(backup_context); - } - - // Clear everything else - for (int i = 0; i < g.Windows.Size; i++) - IM_DELETE(g.Windows[i]); - g.Windows.clear(); - g.WindowsFocusOrder.clear(); - g.WindowsSortBuffer.clear(); - g.CurrentWindow = NULL; - g.CurrentWindowStack.clear(); - g.WindowsById.Clear(); - g.NavWindow = NULL; - g.HoveredWindow = g.HoveredRootWindow = NULL; - g.ActiveIdWindow = g.ActiveIdPreviousFrameWindow = NULL; - g.MovingWindow = NULL; - g.ColorModifiers.clear(); - g.StyleModifiers.clear(); - g.FontStack.clear(); - g.OpenPopupStack.clear(); - g.BeginPopupStack.clear(); - g.DrawDataBuilder.ClearFreeMemory(); - g.BackgroundDrawList.ClearFreeMemory(); - g.ForegroundDrawList.ClearFreeMemory(); - - g.TabBars.Clear(); - g.CurrentTabBarStack.clear(); - g.ShrinkWidthBuffer.clear(); - - g.PrivateClipboard.clear(); - g.InputTextState.ClearFreeMemory(); - - for (int i = 0; i < g.SettingsWindows.Size; i++) - IM_DELETE(g.SettingsWindows[i].Name); - g.SettingsWindows.clear(); - g.SettingsHandlers.clear(); - - if (g.LogFile && g.LogFile != stdout) - { - fclose(g.LogFile); - g.LogFile = NULL; - } - g.LogBuffer.clear(); - - g.Initialized = false; + CallContextHooks(&g, ImGuiContextHookType_NewFramePost); } // FIXME: Add a more explicit sort order in the window structure. @@ -4025,8 +5639,7 @@ static void AddWindowToSortBuffer(ImVector* out_sorted_windows, Im if (window->Active) { int count = window->DC.ChildWindows.Size; - if (count > 1) - ImQsort(window->DC.ChildWindows.Data, (size_t)count, sizeof(ImGuiWindow*), ChildWindowComparer); + ImQsort(window->DC.ChildWindows.Data, (size_t)count, sizeof(ImGuiWindow*), ChildWindowComparer); for (int i = 0; i < count; i++) { ImGuiWindow* child = window->DC.ChildWindows[i]; @@ -4036,107 +5649,84 @@ static void AddWindowToSortBuffer(ImVector* out_sorted_windows, Im } } -static void AddDrawListToDrawData(ImVector* out_list, ImDrawList* draw_list) -{ - if (draw_list->CmdBuffer.empty()) - return; - - // Remove trailing command if unused - ImDrawCmd& last_cmd = draw_list->CmdBuffer.back(); - if (last_cmd.ElemCount == 0 && last_cmd.UserCallback == NULL) - { - draw_list->CmdBuffer.pop_back(); - if (draw_list->CmdBuffer.empty()) - return; - } - - // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. - // May trigger for you if you are using PrimXXX functions incorrectly. - IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); - IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); - if (!(draw_list->Flags & ImDrawListFlags_AllowVtxOffset)) - IM_ASSERT((int)draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); - - // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) - // If this assert triggers because you are drawing lots of stuff manually: - // - First, make sure you are coarse clipping yourself and not trying to draw many things outside visible bounds. - // Be mindful that the ImDrawList API doesn't filter vertices. Use the Metrics window to inspect draw list contents. - // - If you want large meshes with more than 64K vertices, you can either: - // (A) Handle the ImDrawCmd::VtxOffset value in your renderer back-end, and set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset'. - // Most example back-ends already support this from 1.71. Pre-1.71 back-ends won't. - // Some graphics API such as GL ES 1/2 don't have a way to offset the starting vertex so it is not supported for them. - // (B) Or handle 32-bits indices in your renderer back-end, and uncomment '#define ImDrawIdx unsigned int' line in imconfig.h. - // Most example back-ends already support this. For example, the OpenGL example code detect index size at compile-time: - // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); - // Your own engine or render API may use different parameters or function calls to specify index sizes. - // 2 and 4 bytes indices are generally supported by most graphics API. - // - If for some reason neither of those solutions works for you, a workaround is to call BeginChild()/EndChild() before reaching - // the 64K limit to split your draw commands in multiple draw lists. - if (sizeof(ImDrawIdx) == 2) - IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); - - out_list->push_back(draw_list); -} - -static void AddWindowToDrawData(ImVector* out_render_list, ImGuiWindow* window) +static void AddWindowToDrawData(ImGuiWindow* window, int layer) { ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = window->Viewport; + IM_ASSERT(viewport != NULL); g.IO.MetricsRenderWindows++; - AddDrawListToDrawData(out_render_list, window->DrawList); - for (int i = 0; i < window->DC.ChildWindows.Size; i++) - { - ImGuiWindow* child = window->DC.ChildWindows[i]; - if (IsWindowActiveAndVisible(child)) // clipped children may have been marked not active - AddWindowToDrawData(out_render_list, child); - } + if (window->DrawList->_Splitter._Count > 1) + window->DrawList->ChannelsMerge(); // Merge if user forgot to merge back. Also required in Docking branch for ImGuiWindowFlags_DockNodeHost windows. + ImGui::AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[layer], window->DrawList); + for (ImGuiWindow* child : window->DC.ChildWindows) + if (IsWindowActiveAndVisible(child)) // Clipped children may have been marked not active + AddWindowToDrawData(child, layer); +} + +static inline int GetWindowDisplayLayer(ImGuiWindow* window) +{ + return (window->Flags & ImGuiWindowFlags_Tooltip) ? 1 : 0; } // Layer is locked for the root window, however child windows may use a different viewport (e.g. extruding menu) -static void AddRootWindowToDrawData(ImGuiWindow* window) +static inline void AddRootWindowToDrawData(ImGuiWindow* window) { - ImGuiContext& g = *GImGui; - if (window->Flags & ImGuiWindowFlags_Tooltip) - AddWindowToDrawData(&g.DrawDataBuilder.Layers[1], window); - else - AddWindowToDrawData(&g.DrawDataBuilder.Layers[0], window); + AddWindowToDrawData(window, GetWindowDisplayLayer(window)); } -void ImDrawDataBuilder::FlattenIntoSingleLayer() +static void FlattenDrawDataIntoSingleLayer(ImDrawDataBuilder* builder) { - int n = Layers[0].Size; - int size = n; - for (int i = 1; i < IM_ARRAYSIZE(Layers); i++) - size += Layers[i].Size; - Layers[0].resize(size); - for (int layer_n = 1; layer_n < IM_ARRAYSIZE(Layers); layer_n++) + int n = builder->Layers[0]->Size; + int full_size = n; + for (int i = 1; i < IM_ARRAYSIZE(builder->Layers); i++) + full_size += builder->Layers[i]->Size; + builder->Layers[0]->resize(full_size); + for (int layer_n = 1; layer_n < IM_ARRAYSIZE(builder->Layers); layer_n++) { - ImVector& layer = Layers[layer_n]; - if (layer.empty()) + ImVector* layer = builder->Layers[layer_n]; + if (layer->empty()) continue; - memcpy(&Layers[0][n], &layer[0], layer.Size * sizeof(ImDrawList*)); - n += layer.Size; - layer.resize(0); + memcpy(builder->Layers[0]->Data + n, layer->Data, layer->Size * sizeof(ImDrawList*)); + n += layer->Size; + layer->resize(0); } } -static void SetupDrawData(ImVector* draw_lists, ImDrawData* draw_data) +static void InitViewportDrawData(ImGuiViewportP* viewport) { ImGuiIO& io = ImGui::GetIO(); + ImDrawData* draw_data = &viewport->DrawDataP; + + viewport->DrawData = draw_data; // Make publicly accessible + viewport->DrawDataBuilder.Layers[0] = &draw_data->CmdLists; + viewport->DrawDataBuilder.Layers[1] = &viewport->DrawDataBuilder.LayerData1; + viewport->DrawDataBuilder.Layers[0]->resize(0); + viewport->DrawDataBuilder.Layers[1]->resize(0); + + // When minimized, we report draw_data->DisplaySize as zero to be consistent with non-viewport mode, + // and to allow applications/backends to easily skip rendering. + // FIXME: Note that we however do NOT attempt to report "zero drawlist / vertices" into the ImDrawData structure. + // This is because the work has been done already, and its wasted! We should fix that and add optimizations for + // it earlier in the pipeline, rather than pretend to hide the data at the end of the pipeline. + const bool is_minimized = (viewport->Flags & ImGuiViewportFlags_IsMinimized) != 0; + draw_data->Valid = true; - draw_data->CmdLists = (draw_lists->Size > 0) ? draw_lists->Data : NULL; - draw_data->CmdListsCount = draw_lists->Size; + draw_data->CmdListsCount = 0; draw_data->TotalVtxCount = draw_data->TotalIdxCount = 0; - draw_data->DisplayPos = ImVec2(0.0f, 0.0f); - draw_data->DisplaySize = io.DisplaySize; - draw_data->FramebufferScale = io.DisplayFramebufferScale; - for (int n = 0; n < draw_lists->Size; n++) - { - draw_data->TotalVtxCount += draw_lists->Data[n]->VtxBuffer.Size; - draw_data->TotalIdxCount += draw_lists->Data[n]->IdxBuffer.Size; - } + draw_data->DisplayPos = viewport->Pos; + draw_data->DisplaySize = is_minimized ? ImVec2(0.0f, 0.0f) : viewport->Size; + draw_data->FramebufferScale = io.DisplayFramebufferScale; // FIXME-VIEWPORT: This may vary on a per-monitor/viewport basis? + draw_data->OwnerViewport = viewport; } -// When using this function it is sane to ensure that float are perfectly rounded to integer values, to that e.g. (int)(max.x-min.x) in user's render produce correct result. +// Push a clipping rectangle for both ImGui logic (hit-testing etc.) and low-level ImDrawList rendering. +// - When using this function it is sane to ensure that float are perfectly rounded to integer values, +// so that e.g. (int)(max.x-min.x) in user's render produce correct result. +// - If the code here changes, may need to update code of functions like NextColumn() and PushColumnClipRect(): +// some frequently called functions which to modify both channels and clipping simultaneously tend to use the +// more specialized SetWindowClipRectBeforeSetChannel() to avoid extraneous updates of underlying ImDrawCmds. +// - This is analoguous to PushFont()/PopFont() in the sense that are a mixing a global stack and a window stack, +// which in the case of ClipRect is not so problematic but tends to be more restrictive for fonts. void ImGui::PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect) { ImGuiWindow* window = GetCurrentWindow(); @@ -4151,98 +5741,235 @@ void ImGui::PopClipRect() window->ClipRect = window->DrawList->_ClipRectStack.back(); } +static ImGuiWindow* FindFrontMostVisibleChildWindow(ImGuiWindow* window) +{ + for (int n = window->DC.ChildWindows.Size - 1; n >= 0; n--) + if (IsWindowActiveAndVisible(window->DC.ChildWindows[n])) + return FindFrontMostVisibleChildWindow(window->DC.ChildWindows[n]); + return window; +} + +static void ImGui::RenderDimmedBackgroundBehindWindow(ImGuiWindow* window, ImU32 col) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + ImGuiViewportP* viewport = window->Viewport; + ImRect viewport_rect = viewport->GetMainRect(); + + // Draw behind window by moving the draw command at the FRONT of the draw list + { + // Draw list have been trimmed already, hence the explicit recreation of a draw command if missing. + // FIXME: This is creating complication, might be simpler if we could inject a drawlist in drawdata at a given position and not attempt to manipulate ImDrawCmd order. + ImDrawList* draw_list = window->RootWindowDockTree->DrawList; + draw_list->ChannelsMerge(); + if (draw_list->CmdBuffer.Size == 0) + draw_list->AddDrawCmd(); + draw_list->PushClipRect(viewport_rect.Min - ImVec2(1, 1), viewport_rect.Max + ImVec2(1, 1), false); // FIXME: Need to stricty ensure ImDrawCmd are not merged (ElemCount==6 checks below will verify that) + draw_list->AddRectFilled(viewport_rect.Min, viewport_rect.Max, col); + ImDrawCmd cmd = draw_list->CmdBuffer.back(); + IM_ASSERT(cmd.ElemCount == 6); + draw_list->CmdBuffer.pop_back(); + draw_list->CmdBuffer.push_front(cmd); + draw_list->AddDrawCmd(); // We need to create a command as CmdBuffer.back().IdxOffset won't be correct if we append to same command. + draw_list->PopClipRect(); + } + + // Draw over sibling docking nodes in a same docking tree + if (window->RootWindow->DockIsActive) + { + ImDrawList* draw_list = FindFrontMostVisibleChildWindow(window->RootWindowDockTree)->DrawList; + draw_list->ChannelsMerge(); + if (draw_list->CmdBuffer.Size == 0) + draw_list->AddDrawCmd(); + draw_list->PushClipRect(viewport_rect.Min, viewport_rect.Max, false); + RenderRectFilledWithHole(draw_list, window->RootWindowDockTree->Rect(), window->RootWindow->Rect(), col, 0.0f);// window->RootWindowDockTree->WindowRounding); + draw_list->PopClipRect(); + } +} + +ImGuiWindow* ImGui::FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* parent_window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* bottom_most_visible_window = parent_window; + for (int i = FindWindowDisplayIndex(parent_window); i >= 0; i--) + { + ImGuiWindow* window = g.Windows[i]; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + continue; + if (!IsWindowWithinBeginStackOf(window, parent_window)) + break; + if (IsWindowActiveAndVisible(window) && GetWindowDisplayLayer(window) <= GetWindowDisplayLayer(parent_window)) + bottom_most_visible_window = window; + } + return bottom_most_visible_window; +} + +// Important: AddWindowToDrawData() has not been called yet, meaning DockNodeHost windows needs a DrawList->ChannelsMerge() before usage. +// We call ChannelsMerge() lazily here at it is faster that doing a full iteration of g.Windows[] prior to calling RenderDimmedBackgrounds(). +static void ImGui::RenderDimmedBackgrounds() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* modal_window = GetTopMostAndVisiblePopupModal(); + if (g.DimBgRatio <= 0.0f && g.NavWindowingHighlightAlpha <= 0.0f) + return; + const bool dim_bg_for_modal = (modal_window != NULL); + const bool dim_bg_for_window_list = (g.NavWindowingTargetAnim != NULL && g.NavWindowingTargetAnim->Active); + if (!dim_bg_for_modal && !dim_bg_for_window_list) + return; + + ImGuiViewport* viewports_already_dimmed[2] = { NULL, NULL }; + if (dim_bg_for_modal) + { + // Draw dimming behind modal or a begin stack child, whichever comes first in draw order. + ImGuiWindow* dim_behind_window = FindBottomMostVisibleWindowWithinBeginStack(modal_window); + RenderDimmedBackgroundBehindWindow(dim_behind_window, GetColorU32(modal_window->DC.ModalDimBgColor, g.DimBgRatio)); + viewports_already_dimmed[0] = modal_window->Viewport; + } + else if (dim_bg_for_window_list) + { + // Draw dimming behind CTRL+Tab target window and behind CTRL+Tab UI window + RenderDimmedBackgroundBehindWindow(g.NavWindowingTargetAnim, GetColorU32(ImGuiCol_NavWindowingDimBg, g.DimBgRatio)); + if (g.NavWindowingListWindow != NULL && g.NavWindowingListWindow->Viewport && g.NavWindowingListWindow->Viewport != g.NavWindowingTargetAnim->Viewport) + RenderDimmedBackgroundBehindWindow(g.NavWindowingListWindow, GetColorU32(ImGuiCol_NavWindowingDimBg, g.DimBgRatio)); + viewports_already_dimmed[0] = g.NavWindowingTargetAnim->Viewport; + viewports_already_dimmed[1] = g.NavWindowingListWindow ? g.NavWindowingListWindow->Viewport : NULL; + + // Draw border around CTRL+Tab target window + ImGuiWindow* window = g.NavWindowingTargetAnim; + ImGuiViewport* viewport = window->Viewport; + float distance = g.FontSize; + ImRect bb = window->Rect(); + bb.Expand(distance); + if (bb.GetWidth() >= viewport->Size.x && bb.GetHeight() >= viewport->Size.y) + bb.Expand(-distance - 1.0f); // If a window fits the entire viewport, adjust its highlight inward + window->DrawList->ChannelsMerge(); + if (window->DrawList->CmdBuffer.Size == 0) + window->DrawList->AddDrawCmd(); + window->DrawList->PushClipRect(viewport->Pos, viewport->Pos + viewport->Size); + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), window->WindowRounding, 0, 3.0f); + window->DrawList->PopClipRect(); + } + + // Draw dimming background on _other_ viewports than the ones our windows are in + for (ImGuiViewportP* viewport : g.Viewports) + { + if (viewport == viewports_already_dimmed[0] || viewport == viewports_already_dimmed[1]) + continue; + if (modal_window && viewport->Window && IsWindowAbove(viewport->Window, modal_window)) + continue; + ImDrawList* draw_list = GetForegroundDrawList(viewport); + const ImU32 dim_bg_col = GetColorU32(dim_bg_for_modal ? ImGuiCol_ModalWindowDimBg : ImGuiCol_NavWindowingDimBg, g.DimBgRatio); + draw_list->AddRectFilled(viewport->Pos, viewport->Pos + viewport->Size, dim_bg_col); + } +} + // This is normally called by Render(). You may want to call it directly if you want to avoid calling Render() but the gain will be very minimal. void ImGui::EndFrame() { ImGuiContext& g = *GImGui; IM_ASSERT(g.Initialized); - if (g.FrameCountEnded == g.FrameCount) // Don't process EndFrame() multiple times. + + // Don't process EndFrame() multiple times. + if (g.FrameCountEnded == g.FrameCount) return; - IM_ASSERT(g.FrameScopeActive && "Forgot to call ImGui::NewFrame()?"); + IM_ASSERT(g.WithinFrameScope && "Forgot to call ImGui::NewFrame()?"); - // Notify OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME) - if (g.IO.ImeSetInputScreenPosFn && (g.PlatformImeLastPos.x == FLT_MAX || ImLengthSqr(g.PlatformImeLastPos - g.PlatformImePos) > 0.0001f)) - { - g.IO.ImeSetInputScreenPosFn((int)g.PlatformImePos.x, (int)g.PlatformImePos.y); - g.PlatformImeLastPos = g.PlatformImePos; - } + CallContextHooks(&g, ImGuiContextHookType_EndFramePre); - // Report when there is a mismatch of Begin/BeginChild vs End/EndChild calls. Important: Remember that the Begin/BeginChild API requires you - // to always call End/EndChild even if Begin/BeginChild returns false! (this is unfortunately inconsistent with most other Begin* API). - if (g.CurrentWindowStack.Size != 1) + // [EXPERIMENTAL] Recover from errors + if (g.IO.ConfigErrorRecovery) + ErrorRecoveryTryToRecoverState(&g.StackSizesInNewFrame); + ErrorCheckEndFrameSanityChecks(); + ErrorCheckEndFrameFinalizeErrorTooltip(); + + // Notify Platform/OS when our Input Method Editor cursor has moved (e.g. CJK inputs using Microsoft IME) + ImGuiPlatformImeData* ime_data = &g.PlatformImeData; + if (g.PlatformIO.Platform_SetImeDataFn != NULL && memcmp(ime_data, &g.PlatformImeDataPrev, sizeof(ImGuiPlatformImeData)) != 0) { - if (g.CurrentWindowStack.Size > 1) - { - IM_ASSERT(g.CurrentWindowStack.Size == 1 && "Mismatched Begin/BeginChild vs End/EndChild calls: did you forget to call End/EndChild?"); - while (g.CurrentWindowStack.Size > 1) // FIXME-ERRORHANDLING - End(); - } - else - { - IM_ASSERT(g.CurrentWindowStack.Size == 1 && "Mismatched Begin/BeginChild vs End/EndChild calls: did you call End/EndChild too much?"); - } + ImGuiViewport* viewport = FindViewportByID(g.PlatformImeViewport); + IMGUI_DEBUG_LOG_IO("[io] Calling Platform_SetImeDataFn(): WantVisible: %d, InputPos (%.2f,%.2f)\n", ime_data->WantVisible, ime_data->InputPos.x, ime_data->InputPos.y); + if (viewport == NULL) + viewport = GetMainViewport(); + g.PlatformIO.Platform_SetImeDataFn(&g, viewport, ime_data); } // Hide implicit/fallback "Debug" window if it hasn't been used - g.FrameScopePushedImplicitWindow = false; + g.WithinFrameScopeWithImplicitWindow = false; if (g.CurrentWindow && !g.CurrentWindow->WriteAccessed) g.CurrentWindow->Active = false; End(); - // Show CTRL+TAB list window - if (g.NavWindowingTarget != NULL) - NavUpdateWindowingOverlay(); + // Update navigation: CTRL+Tab, wrap-around requests + NavEndFrame(); + + // Update docking + DockContextEndFrame(&g); + + SetCurrentViewport(NULL, NULL); // Drag and Drop: Elapse payload (if delivered, or if source stops being submitted) if (g.DragDropActive) { bool is_delivered = g.DragDropPayload.Delivery; - bool is_elapsed = (g.DragDropPayload.DataFrameCount + 1 < g.FrameCount) && ((g.DragDropSourceFlags & ImGuiDragDropFlags_SourceAutoExpirePayload) || !IsMouseDown(g.DragDropMouseButton)); + bool is_elapsed = (g.DragDropSourceFrameCount + 1 < g.FrameCount) && ((g.DragDropSourceFlags & ImGuiDragDropFlags_PayloadAutoExpire) || g.DragDropMouseButton == -1 || !IsMouseDown(g.DragDropMouseButton)); if (is_delivered || is_elapsed) ClearDragDrop(); } - // Drag and Drop: Fallback for source tooltip. This is not ideal but better than nothing. - if (g.DragDropActive && g.DragDropSourceFrameCount < g.FrameCount) + // Drag and Drop: Fallback for missing source tooltip. This is not ideal but better than nothing. + // If you want to handle source item disappearing: instead of submitting your description tooltip + // in the BeginDragDropSource() block of the dragged item, you can submit them from a safe single spot + // (e.g. end of your item loop, or before EndFrame) by reading payload data. + // In the typical case, the contents of drag tooltip should be possible to infer solely from payload data. + if (g.DragDropActive && g.DragDropSourceFrameCount + 1 < g.FrameCount && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) { - g.DragDropWithinSourceOrTarget = true; + g.DragDropWithinSource = true; SetTooltip("..."); - g.DragDropWithinSourceOrTarget = false; + g.DragDropWithinSource = false; } // End frame - g.FrameScopeActive = false; + g.WithinFrameScope = false; g.FrameCountEnded = g.FrameCount; // Initiate moving window + handle left-click and right-click focus UpdateMouseMovingWindowEndFrame(); + // Update user-facing viewport list (g.Viewports -> g.PlatformIO.Viewports after filtering out some) + UpdateViewportsEndFrame(); + // Sort the window list so that all child windows are after their parent - // We cannot do that on FocusWindow() because childs may not exist yet - g.WindowsSortBuffer.resize(0); - g.WindowsSortBuffer.reserve(g.Windows.Size); - for (int i = 0; i != g.Windows.Size; i++) + // We cannot do that on FocusWindow() because children may not exist yet + g.WindowsTempSortBuffer.resize(0); + g.WindowsTempSortBuffer.reserve(g.Windows.Size); + for (ImGuiWindow* window : g.Windows) { - ImGuiWindow* window = g.Windows[i]; if (window->Active && (window->Flags & ImGuiWindowFlags_ChildWindow)) // if a child is active its parent will add it continue; - AddWindowToSortBuffer(&g.WindowsSortBuffer, window); + AddWindowToSortBuffer(&g.WindowsTempSortBuffer, window); } // This usually assert if there is a mismatch between the ImGuiWindowFlags_ChildWindow / ParentWindow values and DC.ChildWindows[] in parents, aka we've done something wrong. - IM_ASSERT(g.Windows.Size == g.WindowsSortBuffer.Size); - g.Windows.swap(g.WindowsSortBuffer); + IM_ASSERT(g.Windows.Size == g.WindowsTempSortBuffer.Size); + g.Windows.swap(g.WindowsTempSortBuffer); g.IO.MetricsActiveWindows = g.WindowsActiveCount; // Unlock font atlas g.IO.Fonts->Locked = false; // Clear Input data for next frame + g.IO.MousePosPrev = g.IO.MousePos; + g.IO.AppFocusLost = false; g.IO.MouseWheel = g.IO.MouseWheelH = 0.0f; g.IO.InputQueueCharacters.resize(0); - memset(g.IO.NavInputs, 0, sizeof(g.IO.NavInputs)); + + CallContextHooks(&g, ImGuiContextHookType_EndFramePost); } +// Prepare the data for rendering so you can call GetDrawData() +// (As with anything within the ImGui:: namspace this doesn't touch your GPU or graphics API at all: +// it is the role of the ImGui_ImplXXXX_RenderDrawData() function provided by the renderer backend) void ImGui::Render() { ImGuiContext& g = *GImGui; @@ -4250,49 +5977,67 @@ void ImGui::Render() if (g.FrameCountEnded != g.FrameCount) EndFrame(); + if (g.FrameCountRendered == g.FrameCount) + return; g.FrameCountRendered = g.FrameCount; - // Gather ImDrawList to render (for each active window) - g.IO.MetricsRenderVertices = g.IO.MetricsRenderIndices = g.IO.MetricsRenderWindows = 0; - g.DrawDataBuilder.Clear(); - if (!g.BackgroundDrawList.VtxBuffer.empty()) - AddDrawListToDrawData(&g.DrawDataBuilder.Layers[0], &g.BackgroundDrawList); + g.IO.MetricsRenderWindows = 0; + CallContextHooks(&g, ImGuiContextHookType_RenderPre); - ImGuiWindow* windows_to_render_top_most[2]; - windows_to_render_top_most[0] = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget->RootWindow : NULL; - windows_to_render_top_most[1] = g.NavWindowingTarget ? g.NavWindowingList : NULL; - for (int n = 0; n != g.Windows.Size; n++) + // Add background ImDrawList (for each active viewport) + for (ImGuiViewportP* viewport : g.Viewports) { - ImGuiWindow* window = g.Windows[n]; + InitViewportDrawData(viewport); + if (viewport->BgFgDrawLists[0] != NULL) + AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[0], GetBackgroundDrawList(viewport)); + } + + // Draw modal/window whitening backgrounds + RenderDimmedBackgrounds(); + + // Add ImDrawList to render + ImGuiWindow* windows_to_render_top_most[2]; + windows_to_render_top_most[0] = (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) ? g.NavWindowingTarget->RootWindowDockTree : NULL; + windows_to_render_top_most[1] = (g.NavWindowingTarget ? g.NavWindowingListWindow : NULL); + for (ImGuiWindow* window : g.Windows) + { + IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" if (IsWindowActiveAndVisible(window) && (window->Flags & ImGuiWindowFlags_ChildWindow) == 0 && window != windows_to_render_top_most[0] && window != windows_to_render_top_most[1]) AddRootWindowToDrawData(window); } for (int n = 0; n < IM_ARRAYSIZE(windows_to_render_top_most); n++) if (windows_to_render_top_most[n] && IsWindowActiveAndVisible(windows_to_render_top_most[n])) // NavWindowingTarget is always temporarily displayed as the top-most window AddRootWindowToDrawData(windows_to_render_top_most[n]); - g.DrawDataBuilder.FlattenIntoSingleLayer(); - // Draw software mouse cursor if requested - if (g.IO.MouseDrawCursor) - RenderMouseCursor(&g.ForegroundDrawList, g.IO.MousePos, g.Style.MouseCursorScale, g.MouseCursor, IM_COL32_WHITE, IM_COL32_BLACK, IM_COL32(0, 0, 0, 48)); + // Draw software mouse cursor if requested by io.MouseDrawCursor flag + if (g.IO.MouseDrawCursor && g.MouseCursor != ImGuiMouseCursor_None) + RenderMouseCursor(g.IO.MousePos, g.Style.MouseCursorScale, g.MouseCursor, IM_COL32_WHITE, IM_COL32_BLACK, IM_COL32(0, 0, 0, 48)); - if (!g.ForegroundDrawList.VtxBuffer.empty()) - AddDrawListToDrawData(&g.DrawDataBuilder.Layers[0], &g.ForegroundDrawList); + // Setup ImDrawData structures for end-user + g.IO.MetricsRenderVertices = g.IO.MetricsRenderIndices = 0; + for (ImGuiViewportP* viewport : g.Viewports) + { + FlattenDrawDataIntoSingleLayer(&viewport->DrawDataBuilder); - // Setup ImDrawData structure for end-user - SetupDrawData(&g.DrawDataBuilder.Layers[0], &g.DrawData); - g.IO.MetricsRenderVertices = g.DrawData.TotalVtxCount; - g.IO.MetricsRenderIndices = g.DrawData.TotalIdxCount; + // Add foreground ImDrawList (for each active viewport) + if (viewport->BgFgDrawLists[1] != NULL) + AddDrawListToDrawDataEx(&viewport->DrawDataP, viewport->DrawDataBuilder.Layers[0], GetForegroundDrawList(viewport)); - // (Legacy) Call the Render callback function. The current prefer way is to let the user retrieve GetDrawData() and call the render function themselves. -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - if (g.DrawData.CmdListsCount > 0 && g.IO.RenderDrawListsFn != NULL) - g.IO.RenderDrawListsFn(&g.DrawData); -#endif + // We call _PopUnusedDrawCmd() last thing, as RenderDimmedBackgrounds() rely on a valid command being there (especially in docking branch). + ImDrawData* draw_data = &viewport->DrawDataP; + IM_ASSERT(draw_data->CmdLists.Size == draw_data->CmdListsCount); + for (ImDrawList* draw_list : draw_data->CmdLists) + draw_list->_PopUnusedDrawCmd(); + + g.IO.MetricsRenderVertices += draw_data->TotalVtxCount; + g.IO.MetricsRenderIndices += draw_data->TotalIdxCount; + } + + CallContextHooks(&g, ImGuiContextHookType_RenderPost); } // Calculate text size. Text can be multi-line. Optionally ignore text after a ## marker. -// CalcTextSize("") should return ImVec2(0.0f, GImGui->FontSize) +// CalcTextSize("") should return ImVec2(0.0f, g.FontSize) ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_text_after_double_hash, float wrap_width) { ImGuiContext& g = *GImGui; @@ -4310,277 +6055,95 @@ ImVec2 ImGui::CalcTextSize(const char* text, const char* text_end, bool hide_tex ImVec2 text_size = font->CalcTextSizeA(font_size, FLT_MAX, wrap_width, text, text_display_end, NULL); // Round - text_size.x = (float)(int)(text_size.x + 0.95f); + // FIXME: This has been here since Dec 2015 (7b0bf230) but down the line we want this out. + // FIXME: Investigate using ceilf or e.g. + // - https://git.musl-libc.org/cgit/musl/tree/src/math/ceilf.c + // - https://embarkstudios.github.io/rust-gpu/api/src/libm/math/ceilf.rs.html + text_size.x = IM_TRUNC(text_size.x + 0.99999f); return text_size; } // Find window given position, search front-to-back -// FIXME: Note that we have an inconsequential lag here: OuterRectClipped is updated in Begin(), so windows moved programatically -// with SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is -// called, aka before the next Begin(). Moving window isn't affected. -static void FindHoveredWindow() +// - Typically write output back to g.HoveredWindow and g.HoveredWindowUnderMovingWindow. +// - FIXME: Note that we have an inconsequential lag here: OuterRectClipped is updated in Begin(), so windows moved programmatically +// with SetWindowPos() and not SetNextWindowPos() will have that rectangle lagging by a frame at the time FindHoveredWindow() is +// called, aka before the next Begin(). Moving window isn't affected. +// - The 'find_first_and_in_any_viewport = true' mode is only used by TestEngine. It is simpler to maintain here. +void ImGui::FindHoveredWindowEx(const ImVec2& pos, bool find_first_and_in_any_viewport, ImGuiWindow** out_hovered_window, ImGuiWindow** out_hovered_window_under_moving_window) { ImGuiContext& g = *GImGui; - ImGuiWindow* hovered_window = NULL; - if (g.MovingWindow && !(g.MovingWindow->Flags & ImGuiWindowFlags_NoMouseInputs)) - hovered_window = g.MovingWindow; + ImGuiWindow* hovered_window_under_moving_window = NULL; + + // Special handling for the window being moved: Ignore the mouse viewport check (because it may reset/lose its viewport during the undocking frame) + ImGuiViewportP* backup_moving_window_viewport = NULL; + if (find_first_and_in_any_viewport == false && g.MovingWindow) + { + backup_moving_window_viewport = g.MovingWindow->Viewport; + g.MovingWindow->Viewport = g.MouseViewport; + if (!(g.MovingWindow->Flags & ImGuiWindowFlags_NoMouseInputs)) + hovered_window = g.MovingWindow; + } ImVec2 padding_regular = g.Style.TouchExtraPadding; - ImVec2 padding_for_resize_from_edges = g.IO.ConfigWindowsResizeFromEdges ? ImMax(g.Style.TouchExtraPadding, ImVec2(WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS, WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS)) : padding_regular; + ImVec2 padding_for_resize = ImMax(g.Style.TouchExtraPadding, ImVec2(g.Style.WindowBorderHoverPadding, g.Style.WindowBorderHoverPadding)); for (int i = g.Windows.Size - 1; i >= 0; i--) { ImGuiWindow* window = g.Windows[i]; - if (!window->Active || window->Hidden) + IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. + if (!window->WasActive || window->Hidden) continue; if (window->Flags & ImGuiWindowFlags_NoMouseInputs) continue; - - // Using the clipped AABB, a child window will typically be clipped by its parent (not always) - ImRect bb(window->OuterRectClipped); - if (window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) - bb.Expand(padding_regular); - else - bb.Expand(padding_for_resize_from_edges); - if (!bb.Contains(g.IO.MousePos)) + IM_ASSERT(window->Viewport); + if (window->Viewport != g.MouseViewport) continue; - // Those seemingly unnecessary extra tests are because the code here is a little different in viewport/docking branches. - if (hovered_window == NULL) + // Using the clipped AABB, a child window will typically be clipped by its parent (not always) + ImVec2 hit_padding = (window->Flags & (ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize)) ? padding_regular : padding_for_resize; + if (!window->OuterRectClipped.ContainsWithPad(pos, hit_padding)) + continue; + + // Support for one rectangular hole in any given window + // FIXME: Consider generalizing hit-testing override (with more generic data, callback, etc.) (#1512) + if (window->HitTestHoleSize.x != 0) + { + ImVec2 hole_pos(window->Pos.x + (float)window->HitTestHoleOffset.x, window->Pos.y + (float)window->HitTestHoleOffset.y); + ImVec2 hole_size((float)window->HitTestHoleSize.x, (float)window->HitTestHoleSize.y); + if (ImRect(hole_pos, hole_pos + hole_size).Contains(pos)) + continue; + } + + if (find_first_and_in_any_viewport) + { hovered_window = window; - if (hovered_window) break; + } + else + { + if (hovered_window == NULL) + hovered_window = window; + IM_MSVC_WARNING_SUPPRESS(28182); // [Static Analyzer] Dereferencing NULL pointer. + if (hovered_window_under_moving_window == NULL && (!g.MovingWindow || window->RootWindowDockTree != g.MovingWindow->RootWindowDockTree)) + hovered_window_under_moving_window = window; + if (hovered_window && hovered_window_under_moving_window) + break; + } } - g.HoveredWindow = hovered_window; - g.HoveredRootWindow = g.HoveredWindow ? g.HoveredWindow->RootWindow : NULL; - -} - -// Test if mouse cursor is hovering given rectangle -// NB- Rectangle is clipped by our current clip setting -// NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) -bool ImGui::IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip) -{ - ImGuiContext& g = *GImGui; - - // Clip - ImRect rect_clipped(r_min, r_max); - if (clip) - rect_clipped.ClipWith(g.CurrentWindow->ClipRect); - - // Expand for touch input - const ImRect rect_for_touch(rect_clipped.Min - g.Style.TouchExtraPadding, rect_clipped.Max + g.Style.TouchExtraPadding); - if (!rect_for_touch.Contains(g.IO.MousePos)) - return false; - return true; -} - -int ImGui::GetKeyIndex(ImGuiKey imgui_key) -{ - IM_ASSERT(imgui_key >= 0 && imgui_key < ImGuiKey_COUNT); - ImGuiContext& g = *GImGui; - return g.IO.KeyMap[imgui_key]; -} - -// Note that imgui doesn't know the semantic of each entry of io.KeysDown[]. Use your own indices/enums according to how your back-end/engine stored them into io.KeysDown[]! -bool ImGui::IsKeyDown(int user_key_index) -{ - if (user_key_index < 0) - return false; - ImGuiContext& g = *GImGui; - IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(g.IO.KeysDown)); - return g.IO.KeysDown[user_key_index]; -} - -int ImGui::CalcTypematicPressedRepeatAmount(float t, float t_prev, float repeat_delay, float repeat_rate) -{ - if (t == 0.0f) - return 1; - if (t <= repeat_delay || repeat_rate <= 0.0f) - return 0; - const int count = (int)((t - repeat_delay) / repeat_rate) - (int)((t_prev - repeat_delay) / repeat_rate); - return (count > 0) ? count : 0; -} - -int ImGui::GetKeyPressedAmount(int key_index, float repeat_delay, float repeat_rate) -{ - ImGuiContext& g = *GImGui; - if (key_index < 0) - return 0; - IM_ASSERT(key_index >= 0 && key_index < IM_ARRAYSIZE(g.IO.KeysDown)); - const float t = g.IO.KeysDownDuration[key_index]; - return CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, repeat_delay, repeat_rate); -} - -bool ImGui::IsKeyPressed(int user_key_index, bool repeat) -{ - ImGuiContext& g = *GImGui; - if (user_key_index < 0) - return false; - IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(g.IO.KeysDown)); - const float t = g.IO.KeysDownDuration[user_key_index]; - if (t == 0.0f) - return true; - if (repeat && t > g.IO.KeyRepeatDelay) - return GetKeyPressedAmount(user_key_index, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0; - return false; -} - -bool ImGui::IsKeyReleased(int user_key_index) -{ - ImGuiContext& g = *GImGui; - if (user_key_index < 0) return false; - IM_ASSERT(user_key_index >= 0 && user_key_index < IM_ARRAYSIZE(g.IO.KeysDown)); - return g.IO.KeysDownDurationPrev[user_key_index] >= 0.0f && !g.IO.KeysDown[user_key_index]; -} - -bool ImGui::IsMouseDown(int button) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseDown[button]; -} - -bool ImGui::IsAnyMouseDown() -{ - ImGuiContext& g = *GImGui; - for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++) - if (g.IO.MouseDown[n]) - return true; - return false; -} - -bool ImGui::IsMouseClicked(int button, bool repeat) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - const float t = g.IO.MouseDownDuration[button]; - if (t == 0.0f) - return true; - - if (repeat && t > g.IO.KeyRepeatDelay) - { - // FIXME: 2019/05/03: Our old repeat code was wrong here and led to doubling the repeat rate, which made it an ok rate for repeat on mouse hold. - int amount = CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate * 0.5f); - if (amount > 0) - return true; - } - - return false; -} - -bool ImGui::IsMouseReleased(int button) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseReleased[button]; -} - -bool ImGui::IsMouseDoubleClicked(int button) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - return g.IO.MouseDoubleClicked[button]; -} - -// [Internal] This doesn't test if the button is pressed -bool ImGui::IsMouseDragPastThreshold(int button, float lock_threshold) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - if (lock_threshold < 0.0f) - lock_threshold = g.IO.MouseDragThreshold; - return g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold; -} - -bool ImGui::IsMouseDragging(int button, float lock_threshold) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - if (!g.IO.MouseDown[button]) - return false; - return IsMouseDragPastThreshold(button, lock_threshold); -} - -ImVec2 ImGui::GetMousePos() -{ - return GImGui->IO.MousePos; -} - -// NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! -ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() -{ - ImGuiContext& g = *GImGui; - if (g.BeginPopupStack.Size > 0) - return g.OpenPopupStack[g.BeginPopupStack.Size-1].OpenMousePos; - return g.IO.MousePos; -} - -// We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position. -bool ImGui::IsMousePosValid(const ImVec2* mouse_pos) -{ - // The assert is only to silence a false-positive in XCode Static Analysis. - // Because GImGui is not dereferenced in every code path, the static analyzer assume that it may be NULL (which it doesn't for other functions). - IM_ASSERT(GImGui != NULL); - const float MOUSE_INVALID = -256000.0f; - ImVec2 p = mouse_pos ? *mouse_pos : GImGui->IO.MousePos; - return p.x >= MOUSE_INVALID && p.y >= MOUSE_INVALID; -} - -// Return the delta from the initial clicking position while the mouse button is clicked or was just released. -// This is locked and return 0.0f until the mouse moves past a distance threshold at least once. -// NB: This is only valid if IsMousePosValid(). Back-ends in theory should always keep mouse position valid when dragging even outside the client window. -ImVec2 ImGui::GetMouseDragDelta(int button, float lock_threshold) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - if (lock_threshold < 0.0f) - lock_threshold = g.IO.MouseDragThreshold; - if (g.IO.MouseDown[button] || g.IO.MouseReleased[button]) - if (g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold) - if (IsMousePosValid(&g.IO.MousePos) && IsMousePosValid(&g.IO.MouseClickedPos[button])) - return g.IO.MousePos - g.IO.MouseClickedPos[button]; - return ImVec2(0.0f, 0.0f); -} - -void ImGui::ResetMouseDragDelta(int button) -{ - ImGuiContext& g = *GImGui; - IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); - // NB: We don't need to reset g.IO.MouseDragMaxDistanceSqr - g.IO.MouseClickedPos[button] = g.IO.MousePos; -} - -ImGuiMouseCursor ImGui::GetMouseCursor() -{ - return GImGui->MouseCursor; -} - -void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) -{ - GImGui->MouseCursor = cursor_type; -} - -void ImGui::CaptureKeyboardFromApp(bool capture) -{ - GImGui->WantCaptureKeyboardNextFrame = capture ? 1 : 0; -} - -void ImGui::CaptureMouseFromApp(bool capture) -{ - GImGui->WantCaptureMouseNextFrame = capture ? 1 : 0; + *out_hovered_window = hovered_window; + if (out_hovered_window_under_moving_window != NULL) + *out_hovered_window_under_moving_window = hovered_window_under_moving_window; + if (find_first_and_in_any_viewport == false && g.MovingWindow) + g.MovingWindow->Viewport = backup_moving_window_viewport; } bool ImGui::IsItemActive() { ImGuiContext& g = *GImGui; if (g.ActiveId) - { - ImGuiWindow* window = g.CurrentWindow; - return g.ActiveId == window->DC.LastItemId; - } + return g.ActiveId == g.LastItemData.ID; return false; } @@ -4588,50 +6151,69 @@ bool ImGui::IsItemActivated() { ImGuiContext& g = *GImGui; if (g.ActiveId) - { - ImGuiWindow* window = g.CurrentWindow; - if (g.ActiveId == window->DC.LastItemId && g.ActiveIdPreviousFrame != window->DC.LastItemId) + if (g.ActiveId == g.LastItemData.ID && g.ActiveIdPreviousFrame != g.LastItemData.ID) return true; - } return false; } bool ImGui::IsItemDeactivated() { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HasDeactivated) - return (window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_Deactivated) != 0; - return (g.ActiveIdPreviousFrame == window->DC.LastItemId && g.ActiveIdPreviousFrame != 0 && g.ActiveId != window->DC.LastItemId); + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDeactivated) + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Deactivated) != 0; + return (g.DeactivatedItemData.ID == g.LastItemData.ID && g.LastItemData.ID != 0 && g.DeactivatedItemData.ElapseFrame >= g.FrameCount); } bool ImGui::IsItemDeactivatedAfterEdit() { ImGuiContext& g = *GImGui; - return IsItemDeactivated() && (g.ActiveIdPreviousFrameHasBeenEditedBefore || (g.ActiveId == 0 && g.ActiveIdHasBeenEditedBefore)); + return IsItemDeactivated() && g.DeactivatedItemData.HasBeenEditedBefore; } +// == (GetItemID() == GetFocusID() && GetFocusID() != 0) bool ImGui::IsItemFocused() { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - - if (g.NavId == 0 || g.NavDisableHighlight || g.NavId != window->DC.LastItemId) + if (g.NavId != g.LastItemData.ID || g.NavId == 0) return false; + + // Special handling for the dummy item after Begin() which represent the title bar or tab. + // When the window is collapsed (SkipItems==true) that last item will never be overwritten so we need to detect the case. + ImGuiWindow* window = g.CurrentWindow; + if (g.LastItemData.ID == window->ID && window->WriteAccessed) + return false; + return true; } -bool ImGui::IsItemClicked(int mouse_button) +// Important: this can be useful but it is NOT equivalent to the behavior of e.g.Button()! +// Most widgets have specific reactions based on mouse-up/down state, mouse position etc. +bool ImGui::IsItemClicked(ImGuiMouseButton mouse_button) { return IsMouseClicked(mouse_button) && IsItemHovered(ImGuiHoveredFlags_None); } +bool ImGui::IsItemToggledOpen() +{ + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledOpen) ? true : false; +} + +// Call after a Selectable() or TreeNode() involved in multi-selection. +// Useful if you need the per-item information before reaching EndMultiSelect(), e.g. for rendering purpose. +// This is only meant to be called inside a BeginMultiSelect()/EndMultiSelect() block. +// (Outside of multi-select, it would be misleading/ambiguous to report this signal, as widgets +// return e.g. a pressed event and user code is in charge of altering selection in ways we cannot predict.) bool ImGui::IsItemToggledSelection() { ImGuiContext& g = *GImGui; - return (g.CurrentWindow->DC.LastItemStatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; + IM_ASSERT(g.CurrentMultiSelect != NULL); // Can only be used inside a BeginMultiSelect()/EndMultiSelect() + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_ToggledSelection) ? true : false; } +// IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, +// you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! +// Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. bool ImGui::IsAnyItemHovered() { ImGuiContext& g = *GImGui; @@ -4647,89 +6229,200 @@ bool ImGui::IsAnyItemActive() bool ImGui::IsAnyItemFocused() { ImGuiContext& g = *GImGui; - return g.NavId != 0 && !g.NavDisableHighlight; + return g.NavId != 0 && g.NavCursorVisible; } bool ImGui::IsItemVisible() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->ClipRect.Overlaps(window->DC.LastItemRect); + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) != 0; } bool ImGui::IsItemEdited() { - ImGuiWindow* window = GetCurrentWindowRead(); - return (window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_Edited) != 0; + ImGuiContext& g = *GImGui; + return (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Edited) != 0; } +// Allow next item to be overlapped by subsequent items. +// This works by requiring HoveredId to match for two subsequent frames, +// so if a following items overwrite it our interactions will naturally be disabled. +void ImGui::SetNextItemAllowOverlap() +{ + ImGuiContext& g = *GImGui; + g.NextItemData.ItemFlags |= ImGuiItemFlags_AllowOverlap; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS // Allow last item to be overlapped by a subsequent item. Both may be activated during the same frame before the later one takes priority. +// FIXME-LEGACY: Use SetNextItemAllowOverlap() *before* your item instead. void ImGui::SetItemAllowOverlap() { ImGuiContext& g = *GImGui; - if (g.HoveredId == g.CurrentWindow->DC.LastItemId) + ImGuiID id = g.LastItemData.ID; + if (g.HoveredId == id) g.HoveredIdAllowOverlap = true; - if (g.ActiveId == g.CurrentWindow->DC.LastItemId) + if (g.ActiveId == id) // Before we made this obsolete, most calls to SetItemAllowOverlap() used to avoid this path by testing g.ActiveId != id. g.ActiveIdAllowOverlap = true; } +#endif + +// This is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations. +// FIXME: It might be undesirable that this will likely disable KeyOwner-aware shortcuts systems. Consider a more fine-tuned version if needed? +void ImGui::SetActiveIdUsingAllKeyboardKeys() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ActiveId != 0); + g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_COUNT) - 1; + g.ActiveIdUsingAllKeyboardKeys = true; + NavMoveRequestCancel(); +} + +ImGuiID ImGui::GetItemID() +{ + ImGuiContext& g = *GImGui; + return g.LastItemData.ID; +} ImVec2 ImGui::GetItemRectMin() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.LastItemRect.Min; + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.Min; } ImVec2 ImGui::GetItemRectMax() { - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.LastItemRect.Max; + ImGuiContext& g = *GImGui; + return g.LastItemData.Rect.Max; } ImVec2 ImGui::GetItemRectSize() -{ - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.LastItemRect.GetSize(); -} - -static ImRect GetViewportRect() { ImGuiContext& g = *GImGui; - return ImRect(0.0f, 0.0f, g.IO.DisplaySize.x, g.IO.DisplaySize.y); + return g.LastItemData.Rect.GetSize(); } -static bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags flags) +// Prior to v1.90 2023/10/16, the BeginChild() function took a 'bool border = false' parameter instead of 'ImGuiChildFlags child_flags = 0'. +// ImGuiChildFlags_Borders is defined as always == 1 in order to allow old code passing 'true'. Read comments in imgui.h for details! +bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) +{ + ImGuiID id = GetCurrentWindow()->GetID(str_id); + return BeginChildEx(str_id, id, size_arg, child_flags, window_flags); +} + +bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) +{ + return BeginChildEx(NULL, id, size_arg, child_flags, window_flags); +} + +bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags) { ImGuiContext& g = *GImGui; ImGuiWindow* parent_window = g.CurrentWindow; + IM_ASSERT(id != 0); - flags |= ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_ChildWindow; - flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + // Sanity check as it is likely that some user will accidentally pass ImGuiWindowFlags into the ImGuiChildFlags argument. + const ImGuiChildFlags ImGuiChildFlags_SupportedMask_ = ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding | ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY | ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize | ImGuiChildFlags_FrameStyle | ImGuiChildFlags_NavFlattened; + IM_UNUSED(ImGuiChildFlags_SupportedMask_); + IM_ASSERT((child_flags & ~ImGuiChildFlags_SupportedMask_) == 0 && "Illegal ImGuiChildFlags value. Did you pass ImGuiWindowFlags values instead of ImGuiChildFlags?"); + IM_ASSERT((window_flags & ImGuiWindowFlags_AlwaysAutoResize) == 0 && "Cannot specify ImGuiWindowFlags_AlwaysAutoResize for BeginChild(). Use ImGuiChildFlags_AlwaysAutoResize!"); + if (child_flags & ImGuiChildFlags_AlwaysAutoResize) + { + IM_ASSERT((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0 && "Cannot use ImGuiChildFlags_ResizeX or ImGuiChildFlags_ResizeY with ImGuiChildFlags_AlwaysAutoResize!"); + IM_ASSERT((child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY)) != 0 && "Must use ImGuiChildFlags_AutoResizeX or ImGuiChildFlags_AutoResizeY with ImGuiChildFlags_AlwaysAutoResize!"); + } +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (window_flags & ImGuiWindowFlags_AlwaysUseWindowPadding) + child_flags |= ImGuiChildFlags_AlwaysUseWindowPadding; + if (window_flags & ImGuiWindowFlags_NavFlattened) + child_flags |= ImGuiChildFlags_NavFlattened; +#endif + if (child_flags & ImGuiChildFlags_AutoResizeX) + child_flags &= ~ImGuiChildFlags_ResizeX; + if (child_flags & ImGuiChildFlags_AutoResizeY) + child_flags &= ~ImGuiChildFlags_ResizeY; - // Size - const ImVec2 content_avail = GetContentRegionAvail(); - ImVec2 size = ImFloor(size_arg); - const int auto_fit_axises = ((size.x == 0.0f) ? (1 << ImGuiAxis_X) : 0x00) | ((size.y == 0.0f) ? (1 << ImGuiAxis_Y) : 0x00); - if (size.x <= 0.0f) - size.x = ImMax(content_avail.x + size.x, 4.0f); // Arbitrary minimum child size (0.0f causing too much issues) - if (size.y <= 0.0f) - size.y = ImMax(content_avail.y + size.y, 4.0f); + // Set window flags + window_flags |= ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDocking; + window_flags |= (parent_window->Flags & ImGuiWindowFlags_NoMove); // Inherit the NoMove flag + if (child_flags & (ImGuiChildFlags_AutoResizeX | ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_AlwaysAutoResize)) + window_flags |= ImGuiWindowFlags_AlwaysAutoResize; + if ((child_flags & (ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY)) == 0) + window_flags |= ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; + + // Special framed style + if (child_flags & ImGuiChildFlags_FrameStyle) + { + PushStyleColor(ImGuiCol_ChildBg, g.Style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, g.Style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, g.Style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.FramePadding); + child_flags |= ImGuiChildFlags_Borders | ImGuiChildFlags_AlwaysUseWindowPadding; + window_flags |= ImGuiWindowFlags_NoMove; + } + + // Forward size + // Important: Begin() has special processing to switch condition to ImGuiCond_FirstUseEver for a given axis when ImGuiChildFlags_ResizeXXX is set. + // (the alternative would to store conditional flags per axis, which is possible but more code) + const ImVec2 size_avail = GetContentRegionAvail(); + const ImVec2 size_default((child_flags & ImGuiChildFlags_AutoResizeX) ? 0.0f : size_avail.x, (child_flags & ImGuiChildFlags_AutoResizeY) ? 0.0f : size_avail.y); + ImVec2 size = CalcItemSize(size_arg, size_default.x, size_default.y); + + // A SetNextWindowSize() call always has priority (#8020) + // (since the code in Begin() never supported SizeVal==0.0f aka auto-resize via SetNextWindowSize() call, we don't support it here for now) + // FIXME: We only support ImGuiCond_Always in this path. Supporting other paths would requires to obtain window pointer. + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) != 0 && (g.NextWindowData.SizeCond & ImGuiCond_Always) != 0) + { + if (g.NextWindowData.SizeVal.x > 0.0f) + { + size.x = g.NextWindowData.SizeVal.x; + child_flags &= ~ImGuiChildFlags_ResizeX; + } + if (g.NextWindowData.SizeVal.y > 0.0f) + { + size.y = g.NextWindowData.SizeVal.y; + child_flags &= ~ImGuiChildFlags_ResizeY; + } + } SetNextWindowSize(size); - // Build up name. If you need to append to a same child from multiple location in the ID stack, use BeginChild(ImGuiID id) with a stable value. - char title[256]; - if (name) - ImFormatString(title, IM_ARRAYSIZE(title), "%s/%s_%08X", parent_window->Name, name, id); + // Forward child flags (we allow prior settings to merge but it'll only work for adding flags) + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasChildFlags) + g.NextWindowData.ChildFlags |= child_flags; else - ImFormatString(title, IM_ARRAYSIZE(title), "%s/%08X", parent_window->Name, id); + g.NextWindowData.ChildFlags = child_flags; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasChildFlags; + // Build up name. If you need to append to a same child from multiple location in the ID stack, use BeginChild(ImGuiID id) with a stable value. + // FIXME: 2023/11/14: commented out shorted version. We had an issue with multiple ### in child window path names, which the trailing hash helped workaround. + // e.g. "ParentName###ParentIdentifier/ChildName###ChildIdentifier" would get hashed incorrectly by ImHashStr(), trailing _%08X somehow fixes it. + const char* temp_window_name; + /*if (name && parent_window->IDStack.back() == parent_window->ID) + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%s", parent_window->Name, name); // May omit ID if in root of ID stack + else*/ + if (name) + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%s_%08X", parent_window->Name, name, id); + else + ImFormatStringToTempBuffer(&temp_window_name, NULL, "%s/%08X", parent_window->Name, id); + + // Set style const float backup_border_size = g.Style.ChildBorderSize; - if (!border) + if ((child_flags & ImGuiChildFlags_Borders) == 0) g.Style.ChildBorderSize = 0.0f; - bool ret = Begin(title, NULL, flags); + + // Begin into window + const bool ret = Begin(temp_window_name, NULL, window_flags); + + // Restore style g.Style.ChildBorderSize = backup_border_size; + if (child_flags & ImGuiChildFlags_FrameStyle) + { + PopStyleVar(3); + PopStyleColor(); + } ImGuiWindow* child_window = g.CurrentWindow; child_window->ChildId = id; - child_window->AutoFitChildAxises = (ImS8)auto_fit_axises; // Set the cursor to handle case where the user called SetNextWindowPos()+BeginChild() manually. // While this is not really documented/defined, it seems that the expected thing to do. @@ -4737,101 +6430,69 @@ static bool ImGui::BeginChildEx(const char* name, ImGuiID id, const ImVec2& size parent_window->DC.CursorPos = child_window->Pos; // Process navigation-in immediately so NavInit can run on first frame - if (g.NavActivateId == id && !(flags & ImGuiWindowFlags_NavFlattened) && (child_window->DC.NavLayerActiveMask != 0 || child_window->DC.NavHasScroll)) + // Can enter a child if (A) it has navigable items or (B) it can be scrolled. + const ImGuiID temp_id_for_activation = ImHashStr("##Child", 0, id); + if (g.ActiveId == temp_id_for_activation) + ClearActiveID(); + if (g.NavActivateId == id && !(child_flags & ImGuiChildFlags_NavFlattened) && (child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY)) { FocusWindow(child_window); NavInitWindow(child_window, false); - SetActiveID(id+1, child_window); // Steal ActiveId with a dummy id so that key-press won't activate child item - g.ActiveIdSource = ImGuiInputSource_Nav; + SetActiveID(temp_id_for_activation, child_window); // Steal ActiveId with another arbitrary id so that key-press won't activate child item + g.ActiveIdSource = g.NavInputSource; } return ret; } -bool ImGui::BeginChild(const char* str_id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) -{ - ImGuiWindow* window = GetCurrentWindow(); - return BeginChildEx(str_id, window->GetID(str_id), size_arg, border, extra_flags); -} - -bool ImGui::BeginChild(ImGuiID id, const ImVec2& size_arg, bool border, ImGuiWindowFlags extra_flags) -{ - IM_ASSERT(id != 0); - return BeginChildEx(NULL, id, size_arg, border, extra_flags); -} - void ImGui::EndChild() { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; + ImGuiWindow* child_window = g.CurrentWindow; - IM_ASSERT(window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() callss - if (window->BeginCount > 1) - { - End(); - } - else - { - ImVec2 sz = window->Size; - if (window->AutoFitChildAxises & (1 << ImGuiAxis_X)) // Arbitrary minimum zero-ish child size of 4.0f causes less trouble than a 0.0f - sz.x = ImMax(4.0f, sz.x); - if (window->AutoFitChildAxises & (1 << ImGuiAxis_Y)) - sz.y = ImMax(4.0f, sz.y); - End(); + const ImGuiID backup_within_end_child_id = g.WithinEndChildID; + IM_ASSERT(child_window->Flags & ImGuiWindowFlags_ChildWindow); // Mismatched BeginChild()/EndChild() calls + g.WithinEndChildID = child_window->ID; + ImVec2 child_size = child_window->Size; + End(); + if (child_window->BeginCount == 1) + { ImGuiWindow* parent_window = g.CurrentWindow; - ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + sz); - ItemSize(sz); - if ((window->DC.NavLayerActiveMask != 0 || window->DC.NavHasScroll) && !(window->Flags & ImGuiWindowFlags_NavFlattened)) + ImRect bb(parent_window->DC.CursorPos, parent_window->DC.CursorPos + child_size); + ItemSize(child_size); + const bool nav_flattened = (child_window->ChildFlags & ImGuiChildFlags_NavFlattened) != 0; + if ((child_window->DC.NavLayersActiveMask != 0 || child_window->DC.NavWindowHasScrollY) && !nav_flattened) { - ItemAdd(bb, window->ChildId); - RenderNavHighlight(bb, window->ChildId); + ItemAdd(bb, child_window->ChildId); + RenderNavCursor(bb, child_window->ChildId); - // When browsing a window that has no activable items (scroll only) we keep a highlight on the child - if (window->DC.NavLayerActiveMask == 0 && window == g.NavWindow) - RenderNavHighlight(ImRect(bb.Min - ImVec2(2,2), bb.Max + ImVec2(2,2)), g.NavId, ImGuiNavHighlightFlags_TypeThin); + // When browsing a window that has no activable items (scroll only) we keep a highlight on the child (pass g.NavId to trick into always displaying) + if (child_window->DC.NavLayersActiveMask == 0 && child_window == g.NavWindow) + RenderNavCursor(ImRect(bb.Min - ImVec2(2, 2), bb.Max + ImVec2(2, 2)), g.NavId, ImGuiNavRenderCursorFlags_Compact); } else { // Not navigable into - ItemAdd(bb, 0); + // - This is a bit of a fringe use case, mostly useful for undecorated, non-scrolling contents childs, or empty childs. + // - We could later decide to not apply this path if ImGuiChildFlags_FrameStyle or ImGuiChildFlags_Borders is set. + ItemAdd(bb, child_window->ChildId, NULL, ImGuiItemFlags_NoNav); + + // But when flattened we directly reach items, adjust active layer mask accordingly + if (nav_flattened) + parent_window->DC.NavLayersActiveMaskNext |= child_window->DC.NavLayersActiveMaskNext; } + if (g.HoveredWindow == child_window) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + child_window->DC.ChildItemStatusFlags = g.LastItemData.StatusFlags; + //SetLastItemDataForChildWindowItem(child_window, child_window->Rect()); // Not needed, effectively done by ItemAdd() + } + else + { + SetLastItemDataForChildWindowItem(child_window, child_window->Rect()); } -} -// Helper to create a child window / scrolling region that looks like a normal widget frame. -bool ImGui::BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags extra_flags) -{ - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); - PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); - PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); - PushStyleVar(ImGuiStyleVar_WindowPadding, style.FramePadding); - bool ret = BeginChild(id, size, true, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysUseWindowPadding | extra_flags); - PopStyleVar(3); - PopStyleColor(); - return ret; -} - -void ImGui::EndChildFrame() -{ - EndChild(); -} - -// Save and compare stack sizes on Begin()/End() to detect usage errors -static void CheckStacksSize(ImGuiWindow* window, bool write) -{ - // NOT checking: DC.ItemWidth, DC.AllowKeyboardFocus, DC.ButtonRepeat, DC.TextWrapPos (per window) to allow user to conveniently push once and not pop (they are cleared on Begin) - ImGuiContext& g = *GImGui; - short* p_backup = &window->DC.StackSizesBackup[0]; - { int current = window->IDStack.Size; if (write) *p_backup = (short)current; else IM_ASSERT(*p_backup == current && "PushID/PopID or TreeNode/TreePop Mismatch!"); p_backup++; } // Too few or too many PopID()/TreePop() - { int current = window->DC.GroupStack.Size; if (write) *p_backup = (short)current; else IM_ASSERT(*p_backup == current && "BeginGroup/EndGroup Mismatch!"); p_backup++; } // Too few or too many EndGroup() - { int current = g.BeginPopupStack.Size; if (write) *p_backup = (short)current; else IM_ASSERT(*p_backup == current && "BeginMenu/EndMenu or BeginPopup/EndPopup Mismatch"); p_backup++;}// Too few or too many EndMenu()/EndPopup() - // For color, style and font stacks there is an incentive to use Push/Begin/Pop/.../End patterns, so we relax our checks a little to allow them. - { int current = g.ColorModifiers.Size; if (write) *p_backup = (short)current; else IM_ASSERT(*p_backup >= current && "PushStyleColor/PopStyleColor Mismatch!"); p_backup++; } // Too few or too many PopStyleColor() - { int current = g.StyleModifiers.Size; if (write) *p_backup = (short)current; else IM_ASSERT(*p_backup >= current && "PushStyleVar/PopStyleVar Mismatch!"); p_backup++; } // Too few or too many PopStyleVar() - { int current = g.FontStack.Size; if (write) *p_backup = (short)current; else IM_ASSERT(*p_backup >= current && "PushFont/PopFont Mismatch!"); p_backup++; } // Too few or too many PopFont() - IM_ASSERT(p_backup == window->DC.StackSizesBackup + IM_ARRAYSIZE(window->DC.StackSizesBackup)); + g.WithinEndChildID = backup_within_end_child_id; + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return } static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, bool enabled) @@ -4839,6 +6500,7 @@ static void SetWindowConditionAllowFlags(ImGuiWindow* window, ImGuiCond flags, b window->SetWindowPosAllowFlags = enabled ? (window->SetWindowPosAllowFlags | flags) : (window->SetWindowPosAllowFlags & ~flags); window->SetWindowSizeAllowFlags = enabled ? (window->SetWindowSizeAllowFlags | flags) : (window->SetWindowSizeAllowFlags & ~flags); window->SetWindowCollapsedAllowFlags = enabled ? (window->SetWindowCollapsedAllowFlags | flags) : (window->SetWindowCollapsedAllowFlags & ~flags); + window->SetWindowDockAllowFlags = enabled ? (window->SetWindowDockAllowFlags | flags) : (window->SetWindowDockAllowFlags & ~flags); } ImGuiWindow* ImGui::FindWindowByID(ImGuiID id) @@ -4853,35 +6515,41 @@ ImGuiWindow* ImGui::FindWindowByName(const char* name) return FindWindowByID(id); } -static ImGuiWindow* CreateNewWindow(const char* name, ImVec2 size, ImGuiWindowFlags flags) +static void ApplyWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* settings) { - ImGuiContext& g = *GImGui; - //IMGUI_DEBUG_LOG("CreateNewWindow '%s', flags = 0x%08X\n", name, flags); + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + window->ViewportPos = main_viewport->Pos; + if (settings->ViewportId) + { + window->ViewportId = settings->ViewportId; + window->ViewportPos = ImVec2(settings->ViewportPos.x, settings->ViewportPos.y); + } + window->Pos = ImTrunc(ImVec2(settings->Pos.x + window->ViewportPos.x, settings->Pos.y + window->ViewportPos.y)); + if (settings->Size.x > 0 && settings->Size.y > 0) + window->Size = window->SizeFull = ImTrunc(ImVec2(settings->Size.x, settings->Size.y)); + window->Collapsed = settings->Collapsed; + window->DockId = settings->DockId; + window->DockOrder = settings->DockOrder; +} - // Create window the first time - ImGuiWindow* window = IM_NEW(ImGuiWindow)(&g, name); - window->Flags = flags; - g.WindowsById.SetVoidPtr(window->ID, window); +static void InitOrLoadWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* settings) +{ + // Initial window state with e.g. default/arbitrary window position + // Use SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + window->Pos = main_viewport->Pos + ImVec2(60, 60); + window->Size = window->SizeFull = ImVec2(0, 0); + window->ViewportPos = main_viewport->Pos; + window->SetWindowPosAllowFlags = window->SetWindowSizeAllowFlags = window->SetWindowCollapsedAllowFlags = window->SetWindowDockAllowFlags = ImGuiCond_Always | ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing; - // Default/arbitrary window position. Use SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. - window->Pos = ImVec2(60, 60); + if (settings != NULL) + { + SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); + ApplyWindowSettings(window, settings); + } + window->DC.CursorStartPos = window->DC.CursorMaxPos = window->DC.IdealMaxPos = window->Pos; // So first call to CalcWindowContentSizes() doesn't return crazy values - // User can disable loading and saving of settings. Tooltip and child windows also don't store settings. - if (!(flags & ImGuiWindowFlags_NoSavedSettings)) - if (ImGuiWindowSettings* settings = ImGui::FindWindowSettings(window->ID)) - { - // Retrieve settings from .ini file - window->SettingsIdx = g.SettingsWindows.index_from_ptr(settings); - SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); - window->Pos = ImVec2(settings->Pos.x, settings->Pos.y); - window->Collapsed = settings->Collapsed; - if (settings->Size.x > 0 && settings->Size.y > 0) - size = ImVec2(settings->Size.x, settings->Size.y); - } - window->Size = window->SizeFull = ImFloor(size); - window->DC.CursorStartPos = window->DC.CursorMaxPos = window->Pos; // So first call to CalcContentSize() doesn't return crazy values - - if ((flags & ImGuiWindowFlags_AlwaysAutoResize) != 0) + if ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0) { window->AutoFitFramesX = window->AutoFitFramesY = 2; window->AutoFitOnlyGrows = false; @@ -4894,21 +6562,73 @@ static ImGuiWindow* CreateNewWindow(const char* name, ImVec2 size, ImGuiWindowFl window->AutoFitFramesY = 2; window->AutoFitOnlyGrows = (window->AutoFitFramesX > 0) || (window->AutoFitFramesY > 0); } +} + +static ImGuiWindow* CreateNewWindow(const char* name, ImGuiWindowFlags flags) +{ + // Create window the first time + //IMGUI_DEBUG_LOG("CreateNewWindow '%s', flags = 0x%08X\n", name, flags); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = IM_NEW(ImGuiWindow)(&g, name); + window->Flags = flags; + g.WindowsById.SetVoidPtr(window->ID, window); + + ImGuiWindowSettings* settings = NULL; + if (!(flags & ImGuiWindowFlags_NoSavedSettings)) + if ((settings = ImGui::FindWindowSettingsByWindow(window)) != 0) + window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); + + InitOrLoadWindowSettings(window, settings); - g.WindowsFocusOrder.push_back(window); if (flags & ImGuiWindowFlags_NoBringToFrontOnFocus) g.Windows.push_front(window); // Quite slow but rare and only once else g.Windows.push_back(window); + return window; } -static ImVec2 CalcWindowSizeAfterConstraint(ImGuiWindow* window, ImVec2 new_size) +static ImGuiWindow* GetWindowForTitleDisplay(ImGuiWindow* window) +{ + return window->DockNodeAsHost ? window->DockNodeAsHost->VisibleWindow : window; +} + +static ImGuiWindow* GetWindowForTitleAndMenuHeight(ImGuiWindow* window) +{ + return (window->DockNodeAsHost && window->DockNodeAsHost->VisibleWindow) ? window->DockNodeAsHost->VisibleWindow : window; +} + +static inline ImVec2 CalcWindowMinSize(ImGuiWindow* window) +{ + // We give windows non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups) + // FIXME: Essentially we want to restrict manual resizing to WindowMinSize+Decoration, and allow api resizing to be smaller. + // Perhaps should tend further a neater test for this. + ImGuiContext& g = *GImGui; + ImVec2 size_min; + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_Popup)) + { + size_min.x = (window->ChildFlags & ImGuiChildFlags_ResizeX) ? g.Style.WindowMinSize.x : 4.0f; + size_min.y = (window->ChildFlags & ImGuiChildFlags_ResizeY) ? g.Style.WindowMinSize.y : 4.0f; + } + else + { + size_min.x = ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) == 0) ? g.Style.WindowMinSize.x : 4.0f; + size_min.y = ((window->Flags & ImGuiWindowFlags_AlwaysAutoResize) == 0) ? g.Style.WindowMinSize.y : 4.0f; + } + + // Reduce artifacts with very small windows + ImGuiWindow* window_for_height = GetWindowForTitleAndMenuHeight(window); + size_min.y = ImMax(size_min.y, window_for_height->TitleBarHeight + window_for_height->MenuBarHeight + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); + return size_min; +} + +static ImVec2 CalcWindowSizeAfterConstraint(ImGuiWindow* window, const ImVec2& size_desired) { ImGuiContext& g = *GImGui; - if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint) + ImVec2 new_size = size_desired; + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint) { - // Using -1,-1 on either X/Y axis to preserve the current size. + // See comments in SetNextWindowSizeConstraints() for details about setting size_min an size_max. ImRect cr = g.NextWindowData.SizeConstraintRect; new_size.x = (cr.Min.x >= 0 && cr.Max.x >= 0) ? ImClamp(new_size.x, cr.Min.x, cr.Max.x) : window->SizeFull.x; new_size.y = (cr.Min.y >= 0 && cr.Max.y >= 0) ? ImClamp(new_size.y, cr.Min.y, cr.Max.y) : window->SizeFull.y; @@ -4922,40 +6642,43 @@ static ImVec2 CalcWindowSizeAfterConstraint(ImGuiWindow* window, ImVec2 new_size g.NextWindowData.SizeCallback(&data); new_size = data.DesiredSize; } - new_size.x = ImFloor(new_size.x); - new_size.y = ImFloor(new_size.y); + new_size.x = IM_TRUNC(new_size.x); + new_size.y = IM_TRUNC(new_size.y); } // Minimum size - if (!(window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_AlwaysAutoResize))) - { - new_size = ImMax(new_size, g.Style.WindowMinSize); - new_size.y = ImMax(new_size.y, window->TitleBarHeight() + window->MenuBarHeight() + ImMax(0.0f, g.Style.WindowRounding - 1.0f)); // Reduce artifacts with very small windows - } - return new_size; + ImVec2 size_min = CalcWindowMinSize(window); + return ImMax(new_size, size_min); } -static ImVec2 CalcWindowContentSize(ImGuiWindow* window) +static void CalcWindowContentSizes(ImGuiWindow* window, ImVec2* content_size_current, ImVec2* content_size_ideal) { - if (window->Collapsed) - if (window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) - return window->ContentSize; - if (window->Hidden && window->HiddenFramesCannotSkipItems == 0 && window->HiddenFramesCanSkipItems > 0) - return window->ContentSize; + bool preserve_old_content_sizes = false; + if (window->Collapsed && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) + preserve_old_content_sizes = true; + else if (window->Hidden && window->HiddenFramesCannotSkipItems == 0 && window->HiddenFramesCanSkipItems > 0) + preserve_old_content_sizes = true; + if (preserve_old_content_sizes) + { + *content_size_current = window->ContentSize; + *content_size_ideal = window->ContentSizeIdeal; + return; + } - ImVec2 sz; - sz.x = (float)(int)((window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x); - sz.y = (float)(int)((window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y); - return sz; + content_size_current->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(window->DC.CursorMaxPos.x - window->DC.CursorStartPos.x); + content_size_current->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(window->DC.CursorMaxPos.y - window->DC.CursorStartPos.y); + content_size_ideal->x = (window->ContentSizeExplicit.x != 0.0f) ? window->ContentSizeExplicit.x : IM_TRUNC(ImMax(window->DC.CursorMaxPos.x, window->DC.IdealMaxPos.x) - window->DC.CursorStartPos.x); + content_size_ideal->y = (window->ContentSizeExplicit.y != 0.0f) ? window->ContentSizeExplicit.y : IM_TRUNC(ImMax(window->DC.CursorMaxPos.y, window->DC.IdealMaxPos.y) - window->DC.CursorStartPos.y); } static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_contents) { ImGuiContext& g = *GImGui; ImGuiStyle& style = g.Style; - ImVec2 size_decorations = ImVec2(0.0f, window->TitleBarHeight() + window->MenuBarHeight()); + const float decoration_w_without_scrollbars = window->DecoOuterSizeX1 + window->DecoOuterSizeX2 - window->ScrollbarSizes.x; + const float decoration_h_without_scrollbars = window->DecoOuterSizeY1 + window->DecoOuterSizeY2 - window->ScrollbarSizes.y; ImVec2 size_pad = window->WindowPadding * 2.0f; - ImVec2 size_desired = size_contents + size_pad + size_decorations; + ImVec2 size_desired = size_contents + size_pad + ImVec2(decoration_w_without_scrollbars, decoration_h_without_scrollbars); if (window->Flags & ImGuiWindowFlags_Tooltip) { // Tooltip always resize @@ -4964,18 +6687,34 @@ static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_cont else { // Maximum window size is determined by the viewport size or monitor size - const bool is_popup = (window->Flags & ImGuiWindowFlags_Popup) != 0; - const bool is_menu = (window->Flags & ImGuiWindowFlags_ChildMenu) != 0; - ImVec2 size_min = style.WindowMinSize; - if (is_popup || is_menu) // Popups and menus bypass style.WindowMinSize by default, but we give then a non-zero minimum size to facilitate understanding problematic cases (e.g. empty popups) - size_min = ImMin(size_min, ImVec2(4.0f, 4.0f)); - ImVec2 size_auto_fit = ImClamp(size_desired, size_min, ImMax(size_min, g.IO.DisplaySize - style.DisplaySafeAreaPadding * 2.0f)); + ImVec2 size_min = CalcWindowMinSize(window); + ImVec2 size_max = ImVec2(FLT_MAX, FLT_MAX); + + // Child windows are layed within their parent (unless they are also popups/menus) and thus have no restriction + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || (window->Flags & ImGuiWindowFlags_Popup) != 0) + { + if (!window->ViewportOwned) + size_max = ImGui::GetMainViewport()->WorkSize - style.DisplaySafeAreaPadding * 2.0f; + const int monitor_idx = window->ViewportAllowPlatformMonitorExtend; + if (monitor_idx >= 0 && monitor_idx < g.PlatformIO.Monitors.Size) + size_max = g.PlatformIO.Monitors[monitor_idx].WorkSize - style.DisplaySafeAreaPadding * 2.0f; + } + + ImVec2 size_auto_fit = ImClamp(size_desired, size_min, ImMax(size_min, size_max)); + + // FIXME: CalcWindowAutoFitSize() doesn't take into account that only one axis may be auto-fit when calculating scrollbars, + // we may need to compute/store three variants of size_auto_fit, for x/y/xy. + // Here we implement a workaround for child windows only, but a full solution would apply to normal windows as well: + if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && !(window->ChildFlags & ImGuiChildFlags_ResizeY)) + size_auto_fit.y = window->SizeFull.y; + else if (!(window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->ChildFlags & ImGuiChildFlags_ResizeY)) + size_auto_fit.x = window->SizeFull.x; // When the window cannot fit all contents (either because of constraints, either because screen is too small), // we are growing the size on the other axis to compensate for expected scrollbar. FIXME: Might turn bigger than ViewportSize-WindowPadding. ImVec2 size_auto_fit_after_constraint = CalcWindowSizeAfterConstraint(window, size_auto_fit); - bool will_have_scrollbar_x = (size_auto_fit_after_constraint.x - size_pad.x - size_decorations.x < size_contents.x && !(window->Flags & ImGuiWindowFlags_NoScrollbar) && (window->Flags & ImGuiWindowFlags_HorizontalScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar); - bool will_have_scrollbar_y = (size_auto_fit_after_constraint.y - size_pad.y - size_decorations.y < size_contents.y && !(window->Flags & ImGuiWindowFlags_NoScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysVerticalScrollbar); + bool will_have_scrollbar_x = (size_auto_fit_after_constraint.x - size_pad.x - decoration_w_without_scrollbars < size_contents.x && !(window->Flags & ImGuiWindowFlags_NoScrollbar) && (window->Flags & ImGuiWindowFlags_HorizontalScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar); + bool will_have_scrollbar_y = (size_auto_fit_after_constraint.y - size_pad.y - decoration_h_without_scrollbars < size_contents.y && !(window->Flags & ImGuiWindowFlags_NoScrollbar)) || (window->Flags & ImGuiWindowFlags_AlwaysVerticalScrollbar); if (will_have_scrollbar_x) size_auto_fit.y += style.ScrollbarSize; if (will_have_scrollbar_y) @@ -4984,19 +6723,21 @@ static ImVec2 CalcWindowAutoFitSize(ImGuiWindow* window, const ImVec2& size_cont } } -ImVec2 ImGui::CalcWindowExpectedSize(ImGuiWindow* window) +ImVec2 ImGui::CalcWindowNextAutoFitSize(ImGuiWindow* window) { - ImVec2 size_contents = CalcWindowContentSize(window); - ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, size_contents); + ImVec2 size_contents_current; + ImVec2 size_contents_ideal; + CalcWindowContentSizes(window, &size_contents_current, &size_contents_ideal); + ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, size_contents_ideal); ImVec2 size_final = CalcWindowSizeAfterConstraint(window, size_auto_fit); return size_final; } -static ImGuiCol GetWindowBgColorIdxFromFlags(ImGuiWindowFlags flags) +static ImGuiCol GetWindowBgColorIdx(ImGuiWindow* window) { - if (flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) + if (window->Flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) return ImGuiCol_PopupBg; - if (flags & ImGuiWindowFlags_ChildWindow) + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !window->DockIsActive) return ImGuiCol_ChildBg; return ImGuiCol_WindowBg; } @@ -5015,217 +6756,368 @@ static void CalcResizePosSizeFromAnyCorner(ImGuiWindow* window, const ImVec2& co *out_size = size_constrained; } +// Data for resizing from resize grip / corner struct ImGuiResizeGripDef { ImVec2 CornerPosN; ImVec2 InnerDir; int AngleMin12, AngleMax12; }; - static const ImGuiResizeGripDef resize_grip_def[4] = { - { ImVec2(1,1), ImVec2(-1,-1), 0, 3 }, // Lower right - { ImVec2(0,1), ImVec2(+1,-1), 3, 6 }, // Lower left - { ImVec2(0,0), ImVec2(+1,+1), 6, 9 }, // Upper left - { ImVec2(1,0), ImVec2(-1,+1), 9,12 }, // Upper right + { ImVec2(1, 1), ImVec2(-1, -1), 0, 3 }, // Lower-right + { ImVec2(0, 1), ImVec2(+1, -1), 3, 6 }, // Lower-left + { ImVec2(0, 0), ImVec2(+1, +1), 6, 9 }, // Upper-left (Unused) + { ImVec2(1, 0), ImVec2(-1, +1), 9, 12 } // Upper-right (Unused) +}; + +// Data for resizing from borders +struct ImGuiResizeBorderDef +{ + ImVec2 InnerDir; // Normal toward inside + ImVec2 SegmentN1, SegmentN2; // End positions, normalized (0,0: upper left) + float OuterAngle; // Angle toward outside +}; +static const ImGuiResizeBorderDef resize_border_def[4] = +{ + { ImVec2(+1, 0), ImVec2(0, 1), ImVec2(0, 0), IM_PI * 1.00f }, // Left + { ImVec2(-1, 0), ImVec2(1, 0), ImVec2(1, 1), IM_PI * 0.00f }, // Right + { ImVec2(0, +1), ImVec2(0, 0), ImVec2(1, 0), IM_PI * 1.50f }, // Up + { ImVec2(0, -1), ImVec2(1, 1), ImVec2(0, 1), IM_PI * 0.50f } // Down }; static ImRect GetResizeBorderRect(ImGuiWindow* window, int border_n, float perp_padding, float thickness) { ImRect rect = window->Rect(); - if (thickness == 0.0f) rect.Max -= ImVec2(1,1); - if (border_n == 0) return ImRect(rect.Min.x + perp_padding, rect.Min.y - thickness, rect.Max.x - perp_padding, rect.Min.y + thickness); // Top - if (border_n == 1) return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x + thickness, rect.Max.y - perp_padding); // Right - if (border_n == 2) return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y + thickness); // Bottom - if (border_n == 3) return ImRect(rect.Min.x - thickness, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); // Left + if (thickness == 0.0f) + rect.Max -= ImVec2(1, 1); + if (border_n == ImGuiDir_Left) { return ImRect(rect.Min.x - thickness, rect.Min.y + perp_padding, rect.Min.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Right) { return ImRect(rect.Max.x - thickness, rect.Min.y + perp_padding, rect.Max.x + thickness, rect.Max.y - perp_padding); } + if (border_n == ImGuiDir_Up) { return ImRect(rect.Min.x + perp_padding, rect.Min.y - thickness, rect.Max.x - perp_padding, rect.Min.y + thickness); } + if (border_n == ImGuiDir_Down) { return ImRect(rect.Min.x + perp_padding, rect.Max.y - thickness, rect.Max.x - perp_padding, rect.Max.y + thickness); } IM_ASSERT(0); return ImRect(); } +// 0..3: corners (Lower-right, Lower-left, Unused, Unused) +ImGuiID ImGui::GetWindowResizeCornerID(ImGuiWindow* window, int n) +{ + IM_ASSERT(n >= 0 && n < 4); + ImGuiID id = window->DockIsActive ? window->DockNode->HostWindow->ID : window->ID; + id = ImHashStr("#RESIZE", 0, id); + id = ImHashData(&n, sizeof(int), id); + return id; +} + +// Borders (Left, Right, Up, Down) +ImGuiID ImGui::GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir) +{ + IM_ASSERT(dir >= 0 && dir < 4); + int n = (int)dir + 4; + ImGuiID id = window->DockIsActive ? window->DockNode->HostWindow->ID : window->ID; + id = ImHashStr("#RESIZE", 0, id); + id = ImHashData(&n, sizeof(int), id); + return id; +} + // Handle resize for: Resize Grips, Borders, Gamepad -// Return true when using auto-fit (double click on resize grip) -static bool ImGui::UpdateManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4]) +// Return true when using auto-fit (double-click on resize grip) +static int ImGui::UpdateWindowManualResize(ImGuiWindow* window, const ImVec2& size_auto_fit, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect) { ImGuiContext& g = *GImGui; ImGuiWindowFlags flags = window->Flags; if ((flags & ImGuiWindowFlags_NoResize) || (flags & ImGuiWindowFlags_AlwaysAutoResize) || window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) return false; - if (window->WasActive == false) // Early out to avoid running this code for e.g. an hidden implicit/fallback Debug window. + if (window->WasActive == false) // Early out to avoid running this code for e.g. a hidden implicit/fallback Debug window. return false; - bool ret_auto_fit = false; - const int resize_border_count = g.IO.ConfigWindowsResizeFromEdges ? 4 : 0; - const float grip_draw_size = (float)(int)ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f); - const float grip_hover_inner_size = (float)(int)(grip_draw_size * 0.75f); - const float grip_hover_outer_size = g.IO.ConfigWindowsResizeFromEdges ? WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS : 0.0f; + int ret_auto_fit_mask = 0x00; + const float grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + const float grip_hover_inner_size = (resize_grip_count > 0) ? IM_TRUNC(grip_draw_size * 0.75f) : 0.0f; + const float grip_hover_outer_size = g.WindowsBorderHoverPadding; + + ImRect clamp_rect = visibility_rect; + const bool window_move_from_title_bar = g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar); + if (window_move_from_title_bar) + clamp_rect.Min.y -= window->TitleBarHeight; ImVec2 pos_target(FLT_MAX, FLT_MAX); ImVec2 size_target(FLT_MAX, FLT_MAX); + // Clip mouse interaction rectangles within the viewport rectangle (in practice the narrowing is going to happen most of the time). + // - Not narrowing would mostly benefit the situation where OS windows _without_ decoration have a threshold for hovering when outside their limits. + // This is however not the case with current backends under Win32, but a custom borderless window implementation would benefit from it. + // - When decoration are enabled we typically benefit from that distance, but then our resize elements would be conflicting with OS resize elements, so we also narrow. + // - Note that we are unable to tell if the platform setup allows hovering with a distance threshold (on Win32, decorated window have such threshold). + // We only clip interaction so we overwrite window->ClipRect, cannot call PushClipRect() yet as DrawList is not yet setup. + const bool clip_with_viewport_rect = !(g.IO.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport) || (g.IO.MouseHoveredViewport != window->ViewportId) || !(window->Viewport->Flags & ImGuiViewportFlags_NoDecoration); + if (clip_with_viewport_rect) + window->ClipRect = window->Viewport->GetMainRect(); + // Resize grips and borders are on layer 1 window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; - window->DC.NavLayerCurrentMask = (1 << ImGuiNavLayer_Menu); // Manual resize grips PushID("#RESIZE"); for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) { - const ImGuiResizeGripDef& grip = resize_grip_def[resize_grip_n]; - const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPosN); + const ImGuiResizeGripDef& def = resize_grip_def[resize_grip_n]; + const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, def.CornerPosN); // Using the FlattenChilds button flag we make the resize button accessible even if we are hovering over a child window - ImRect resize_rect(corner - grip.InnerDir * grip_hover_outer_size, corner + grip.InnerDir * grip_hover_inner_size); + bool hovered, held; + ImRect resize_rect(corner - def.InnerDir * grip_hover_outer_size, corner + def.InnerDir * grip_hover_inner_size); if (resize_rect.Min.x > resize_rect.Max.x) ImSwap(resize_rect.Min.x, resize_rect.Max.x); if (resize_rect.Min.y > resize_rect.Max.y) ImSwap(resize_rect.Min.y, resize_rect.Max.y); - bool hovered, held; - ButtonBehavior(resize_rect, window->GetID((void*)(intptr_t)resize_grip_n), &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + ImGuiID resize_grip_id = window->GetID(resize_grip_n); // == GetWindowResizeCornerID() + ItemAdd(resize_rect, resize_grip_id, NULL, ImGuiItemFlags_NoNav); + ButtonBehavior(resize_rect, resize_grip_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); //GetForegroundDrawList(window)->AddRect(resize_rect.Min, resize_rect.Max, IM_COL32(255, 255, 0, 255)); if (hovered || held) - g.MouseCursor = (resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE; + SetMouseCursor((resize_grip_n & 1) ? ImGuiMouseCursor_ResizeNESW : ImGuiMouseCursor_ResizeNWSE); - if (held && g.IO.MouseDoubleClicked[0] && resize_grip_n == 0) + if (held && g.IO.MouseDoubleClicked[0]) { - // Manual auto-fit when double-clicking + // Auto-fit when double-clicking size_target = CalcWindowSizeAfterConstraint(window, size_auto_fit); - ret_auto_fit = true; + ret_auto_fit_mask = 0x03; // Both axes ClearActiveID(); } else if (held) { // Resize from any of the four corners // We don't use an incremental MouseDelta but rather compute an absolute target size based on mouse position - ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + ImLerp(grip.InnerDir * grip_hover_outer_size, grip.InnerDir * -grip_hover_inner_size, grip.CornerPosN); // Corner of the window corresponding to our corner grip - CalcResizePosSizeFromAnyCorner(window, corner_target, grip.CornerPosN, &pos_target, &size_target); + ImVec2 clamp_min = ImVec2(def.CornerPosN.x == 1.0f ? clamp_rect.Min.x : -FLT_MAX, (def.CornerPosN.y == 1.0f || (def.CornerPosN.y == 0.0f && window_move_from_title_bar)) ? clamp_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max = ImVec2(def.CornerPosN.x == 0.0f ? clamp_rect.Max.x : +FLT_MAX, def.CornerPosN.y == 0.0f ? clamp_rect.Max.y : +FLT_MAX); + ImVec2 corner_target = g.IO.MousePos - g.ActiveIdClickOffset + ImLerp(def.InnerDir * grip_hover_outer_size, def.InnerDir * -grip_hover_inner_size, def.CornerPosN); // Corner of the window corresponding to our corner grip + corner_target = ImClamp(corner_target, clamp_min, clamp_max); + CalcResizePosSizeFromAnyCorner(window, corner_target, def.CornerPosN, &pos_target, &size_target); } + + // Only lower-left grip is visible before hovering/activating if (resize_grip_n == 0 || held || hovered) resize_grip_col[resize_grip_n] = GetColorU32(held ? ImGuiCol_ResizeGripActive : hovered ? ImGuiCol_ResizeGripHovered : ImGuiCol_ResizeGrip); } - for (int border_n = 0; border_n < resize_border_count; border_n++) + + int resize_border_mask = 0x00; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + resize_border_mask |= ((window->ChildFlags & ImGuiChildFlags_ResizeX) ? 0x02 : 0) | ((window->ChildFlags & ImGuiChildFlags_ResizeY) ? 0x08 : 0); + else + resize_border_mask = g.IO.ConfigWindowsResizeFromEdges ? 0x0F : 0x00; + for (int border_n = 0; border_n < 4; border_n++) { + if ((resize_border_mask & (1 << border_n)) == 0) + continue; + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const ImGuiAxis axis = (border_n == ImGuiDir_Left || border_n == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + bool hovered, held; - ImRect border_rect = GetResizeBorderRect(window, border_n, grip_hover_inner_size, WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS); - ButtonBehavior(border_rect, window->GetID((void*)(intptr_t)(border_n + 4)), &hovered, &held, ImGuiButtonFlags_FlattenChildren); - //GetForegroundDrawLists(window)->AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255)); - if ((hovered && g.HoveredIdTimer > WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER) || held) + ImRect border_rect = GetResizeBorderRect(window, border_n, grip_hover_inner_size, g.WindowsBorderHoverPadding); + ImGuiID border_id = window->GetID(border_n + 4); // == GetWindowResizeBorderID() + ItemAdd(border_rect, border_id, NULL, ImGuiItemFlags_NoNav); + ButtonBehavior(border_rect, border_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_NoNavFocus); + //GetForegroundDrawList(window)->AddRect(border_rect.Min, border_rect.Max, IM_COL32(255, 255, 0, 255)); + if (hovered && g.HoveredIdTimer <= WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER) + hovered = false; + if (hovered || held) + SetMouseCursor((axis == ImGuiAxis_X) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS); + if (held && g.IO.MouseDoubleClicked[0]) { - g.MouseCursor = (border_n & 1) ? ImGuiMouseCursor_ResizeEW : ImGuiMouseCursor_ResizeNS; - if (held) - *border_held = border_n; + // Double-clicking bottom or right border auto-fit on this axis + // FIXME: CalcWindowAutoFitSize() doesn't take into account that only one side may be auto-fit when calculating scrollbars. + // FIXME: Support top and right borders: rework CalcResizePosSizeFromAnyCorner() to be reusable in both cases. + if (border_n == 1 || border_n == 3) // Right and bottom border + { + size_target[axis] = CalcWindowSizeAfterConstraint(window, size_auto_fit)[axis]; + ret_auto_fit_mask |= (1 << axis); + hovered = held = false; // So border doesn't show highlighted at new position + } + ClearActiveID(); } - if (held) + else if (held) { + // Switch to relative resizing mode when border geometry moved (e.g. resizing a child altering parent scroll), in order to avoid resizing feedback loop. + // Currently only using relative mode on resizable child windows, as the problem to solve is more likely noticeable for them, but could apply for all windows eventually. + // FIXME: May want to generalize this idiom at lower-level, so more widgets can use it! + const bool just_scrolled_manually_while_resizing = (g.WheelingWindow != NULL && g.WheelingWindowScrolledFrame == g.FrameCount && IsWindowChildOf(window, g.WheelingWindow, false, true)); + if (g.ActiveIdIsJustActivated || just_scrolled_manually_while_resizing) + { + g.WindowResizeBorderExpectedRect = border_rect; + g.WindowResizeRelativeMode = false; + } + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && memcmp(&g.WindowResizeBorderExpectedRect, &border_rect, sizeof(ImRect)) != 0) + g.WindowResizeRelativeMode = true; + + const ImVec2 border_curr = (window->Pos + ImMin(def.SegmentN1, def.SegmentN2) * window->Size); + const float border_target_rel_mode_for_axis = border_curr[axis] + g.IO.MouseDelta[axis]; + const float border_target_abs_mode_for_axis = g.IO.MousePos[axis] - g.ActiveIdClickOffset[axis] + g.WindowsBorderHoverPadding; // Match ButtonBehavior() padding above. + + // Use absolute mode position ImVec2 border_target = window->Pos; - ImVec2 border_posn; - if (border_n == 0) { border_posn = ImVec2(0, 0); border_target.y = (g.IO.MousePos.y - g.ActiveIdClickOffset.y + WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS); } // Top - if (border_n == 1) { border_posn = ImVec2(1, 0); border_target.x = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS); } // Right - if (border_n == 2) { border_posn = ImVec2(0, 1); border_target.y = (g.IO.MousePos.y - g.ActiveIdClickOffset.y + WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS); } // Bottom - if (border_n == 3) { border_posn = ImVec2(0, 0); border_target.x = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + WINDOWS_RESIZE_FROM_EDGES_HALF_THICKNESS); } // Left - CalcResizePosSizeFromAnyCorner(window, border_target, border_posn, &pos_target, &size_target); + border_target[axis] = border_target_abs_mode_for_axis; + + // Use relative mode target for child window, ignore resize when moving back toward the ideal absolute position. + bool ignore_resize = false; + if (g.WindowResizeRelativeMode) + { + //GetForegroundDrawList()->AddText(GetMainViewport()->WorkPos, IM_COL32_WHITE, "Relative Mode"); + border_target[axis] = border_target_rel_mode_for_axis; + if (g.IO.MouseDelta[axis] == 0.0f || (g.IO.MouseDelta[axis] > 0.0f) == (border_target_rel_mode_for_axis > border_target_abs_mode_for_axis)) + ignore_resize = true; + } + + // Clamp, apply + ImVec2 clamp_min(border_n == ImGuiDir_Right ? clamp_rect.Min.x : -FLT_MAX, border_n == ImGuiDir_Down || (border_n == ImGuiDir_Up && window_move_from_title_bar) ? clamp_rect.Min.y : -FLT_MAX); + ImVec2 clamp_max(border_n == ImGuiDir_Left ? clamp_rect.Max.x : +FLT_MAX, border_n == ImGuiDir_Up ? clamp_rect.Max.y : +FLT_MAX); + border_target = ImClamp(border_target, clamp_min, clamp_max); + if (flags & ImGuiWindowFlags_ChildWindow) // Clamp resizing of childs within parent + { + ImGuiWindow* parent_window = window->ParentWindow; + ImGuiWindowFlags parent_flags = parent_window->Flags; + ImRect border_limit_rect = parent_window->InnerRect; + border_limit_rect.Expand(ImVec2(-ImMax(parent_window->WindowPadding.x, parent_window->WindowBorderSize), -ImMax(parent_window->WindowPadding.y, parent_window->WindowBorderSize))); + if ((axis == ImGuiAxis_X) && ((parent_flags & (ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar)) == 0 || (parent_flags & ImGuiWindowFlags_NoScrollbar))) + border_target.x = ImClamp(border_target.x, border_limit_rect.Min.x, border_limit_rect.Max.x); + if ((axis == ImGuiAxis_Y) && (parent_flags & ImGuiWindowFlags_NoScrollbar)) + border_target.y = ImClamp(border_target.y, border_limit_rect.Min.y, border_limit_rect.Max.y); + } + if (!ignore_resize) + CalcResizePosSizeFromAnyCorner(window, border_target, ImMin(def.SegmentN1, def.SegmentN2), &pos_target, &size_target); } + if (hovered) + *border_hovered = border_n; + if (held) + *border_held = border_n; } PopID(); + // Restore nav layer + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + // Navigation resize (keyboard/gamepad) - if (g.NavWindowingTarget && g.NavWindowingTarget->RootWindow == window) + // FIXME: This cannot be moved to NavUpdateWindowing() because CalcWindowSizeAfterConstraint() need to callback into user. + // Not even sure the callback works here. + if (g.NavWindowingTarget && g.NavWindowingTarget->RootWindowDockTree == window) { - ImVec2 nav_resize_delta; - if (g.NavInputSource == ImGuiInputSource_NavKeyboard && g.IO.KeyShift) - nav_resize_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard, ImGuiInputReadMode_Down); - if (g.NavInputSource == ImGuiInputSource_NavGamepad) - nav_resize_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_Down); - if (nav_resize_delta.x != 0.0f || nav_resize_delta.y != 0.0f) + ImVec2 nav_resize_dir; + if (g.NavInputSource == ImGuiInputSource_Keyboard && g.IO.KeyShift) + nav_resize_dir = GetKeyMagnitude2d(ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow); + if (g.NavInputSource == ImGuiInputSource_Gamepad) + nav_resize_dir = GetKeyMagnitude2d(ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadRight, ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadDpadDown); + if (nav_resize_dir.x != 0.0f || nav_resize_dir.y != 0.0f) { const float NAV_RESIZE_SPEED = 600.0f; - nav_resize_delta *= ImFloor(NAV_RESIZE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y)); + const float resize_step = NAV_RESIZE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y); + g.NavWindowingAccumDeltaSize += nav_resize_dir * resize_step; + g.NavWindowingAccumDeltaSize = ImMax(g.NavWindowingAccumDeltaSize, clamp_rect.Min - window->Pos - window->Size); // We need Pos+Size >= clmap_rect.Min, so Size >= clmap_rect.Min - Pos, so size_delta >= clmap_rect.Min - window->Pos - window->Size g.NavWindowingToggleLayer = false; - g.NavDisableMouseHover = true; + g.NavHighlightItemUnderNav = true; resize_grip_col[0] = GetColorU32(ImGuiCol_ResizeGripActive); - // FIXME-NAV: Should store and accumulate into a separate size buffer to handle sizing constraints properly, right now a constraint will make us stuck. - size_target = CalcWindowSizeAfterConstraint(window, window->SizeFull + nav_resize_delta); + ImVec2 accum_floored = ImTrunc(g.NavWindowingAccumDeltaSize); + if (accum_floored.x != 0.0f || accum_floored.y != 0.0f) + { + // FIXME-NAV: Should store and accumulate into a separate size buffer to handle sizing constraints properly, right now a constraint will make us stuck. + size_target = CalcWindowSizeAfterConstraint(window, window->SizeFull + accum_floored); + g.NavWindowingAccumDeltaSize -= accum_floored; + } } } // Apply back modified position/size to window - if (size_target.x != FLT_MAX) - { - window->SizeFull = size_target; + const ImVec2 curr_pos = window->Pos; + const ImVec2 curr_size = window->SizeFull; + if (size_target.x != FLT_MAX && (window->Size.x != size_target.x || window->SizeFull.x != size_target.x)) + window->Size.x = window->SizeFull.x = size_target.x; + if (size_target.y != FLT_MAX && (window->Size.y != size_target.y || window->SizeFull.y != size_target.y)) + window->Size.y = window->SizeFull.y = size_target.y; + if (pos_target.x != FLT_MAX && window->Pos.x != ImTrunc(pos_target.x)) + window->Pos.x = ImTrunc(pos_target.x); + if (pos_target.y != FLT_MAX && window->Pos.y != ImTrunc(pos_target.y)) + window->Pos.y = ImTrunc(pos_target.y); + if (curr_pos.x != window->Pos.x || curr_pos.y != window->Pos.y || curr_size.x != window->SizeFull.x || curr_size.y != window->SizeFull.y) MarkIniSettingsDirty(window); - } - if (pos_target.x != FLT_MAX) - { - window->Pos = ImFloor(pos_target); - MarkIniSettingsDirty(window); - } - // Resize nav layer - window->DC.NavLayerCurrent = ImGuiNavLayer_Main; - window->DC.NavLayerCurrentMask = (1 << ImGuiNavLayer_Main); + // Recalculate next expected border expected coordinates + if (*border_held != -1) + g.WindowResizeBorderExpectedRect = GetResizeBorderRect(window, *border_held, grip_hover_inner_size, g.WindowsBorderHoverPadding); - window->Size = window->SizeFull; - return ret_auto_fit; + return ret_auto_fit_mask; } -static inline void ClampWindowRect(ImGuiWindow* window, const ImRect& rect, const ImVec2& padding) +static inline void ClampWindowPos(ImGuiWindow* window, const ImRect& visibility_rect) { ImGuiContext& g = *GImGui; - ImVec2 size_for_clamping = (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) ? ImVec2(window->Size.x, window->TitleBarHeight()) : window->Size; - window->Pos = ImMin(rect.Max - padding, ImMax(window->Pos + size_for_clamping, rect.Min + padding) - size_for_clamping); + ImVec2 size_for_clamping = window->Size; + if (g.IO.ConfigWindowsMoveFromTitleBarOnly && window->DockNodeAsHost) + size_for_clamping.y = ImGui::GetFrameHeight(); // Not using window->TitleBarHeight() as DockNodeAsHost will report 0.0f here. + else if (g.IO.ConfigWindowsMoveFromTitleBarOnly && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + size_for_clamping.y = window->TitleBarHeight; + window->Pos = ImClamp(window->Pos, visibility_rect.Min - size_for_clamping, visibility_rect.Max); +} + +static void RenderWindowOuterSingleBorder(ImGuiWindow* window, int border_n, ImU32 border_col, float border_size) +{ + const ImGuiResizeBorderDef& def = resize_border_def[border_n]; + const float rounding = window->WindowRounding; + const ImRect border_r = GetResizeBorderRect(window, border_n, rounding, 0.0f); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI * 0.25f, def.OuterAngle); + window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.SegmentN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI * 0.25f); + window->DrawList->PathStroke(border_col, ImDrawFlags_None, border_size); } static void ImGui::RenderWindowOuterBorders(ImGuiWindow* window) { ImGuiContext& g = *GImGui; - float rounding = window->WindowRounding; - float border_size = window->WindowBorderSize; - if (border_size > 0.0f && !(window->Flags & ImGuiWindowFlags_NoBackground)) - window->DrawList->AddRect(window->Pos, window->Pos + window->Size, GetColorU32(ImGuiCol_Border), rounding, ImDrawCornerFlags_All, border_size); - - int border_held = window->ResizeBorderHeld; - if (border_held != -1) + const float border_size = window->WindowBorderSize; + const ImU32 border_col = GetColorU32(ImGuiCol_Border); + if (border_size > 0.0f && (window->Flags & ImGuiWindowFlags_NoBackground) == 0) + window->DrawList->AddRect(window->Pos, window->Pos + window->Size, border_col, window->WindowRounding, 0, window->WindowBorderSize); + else if (border_size > 0.0f) { - struct ImGuiResizeBorderDef - { - ImVec2 InnerDir; - ImVec2 CornerPosN1, CornerPosN2; - float OuterAngle; - }; - static const ImGuiResizeBorderDef resize_border_def[4] = - { - { ImVec2(0,+1), ImVec2(0,0), ImVec2(1,0), IM_PI*1.50f }, // Top - { ImVec2(-1,0), ImVec2(1,0), ImVec2(1,1), IM_PI*0.00f }, // Right - { ImVec2(0,-1), ImVec2(1,1), ImVec2(0,1), IM_PI*0.50f }, // Bottom - { ImVec2(+1,0), ImVec2(0,1), ImVec2(0,0), IM_PI*1.00f } // Left - }; - const ImGuiResizeBorderDef& def = resize_border_def[border_held]; - ImRect border_r = GetResizeBorderRect(window, border_held, rounding, 0.0f); - window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.CornerPosN1) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle - IM_PI*0.25f, def.OuterAngle); - window->DrawList->PathArcTo(ImLerp(border_r.Min, border_r.Max, def.CornerPosN2) + ImVec2(0.5f, 0.5f) + def.InnerDir * rounding, rounding, def.OuterAngle, def.OuterAngle + IM_PI*0.25f); - window->DrawList->PathStroke(GetColorU32(ImGuiCol_SeparatorActive), false, ImMax(2.0f, border_size)); // Thicker than usual + if (window->ChildFlags & ImGuiChildFlags_ResizeX) // Similar code as 'resize_border_mask' computation in UpdateWindowManualResize() but we specifically only always draw explicit child resize border. + RenderWindowOuterSingleBorder(window, 1, border_col, border_size); + if (window->ChildFlags & ImGuiChildFlags_ResizeY) + RenderWindowOuterSingleBorder(window, 3, border_col, border_size); } - if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + if (window->ResizeBorderHovered != -1 || window->ResizeBorderHeld != -1) { - float y = window->Pos.y + window->TitleBarHeight() - 1; - window->DrawList->AddLine(ImVec2(window->Pos.x + border_size, y), ImVec2(window->Pos.x + window->Size.x - border_size, y), GetColorU32(ImGuiCol_Border), g.Style.FrameBorderSize); + const int border_n = (window->ResizeBorderHeld != -1) ? window->ResizeBorderHeld : window->ResizeBorderHovered; + const ImU32 border_col_resizing = GetColorU32((window->ResizeBorderHeld != -1) ? ImGuiCol_SeparatorActive : ImGuiCol_SeparatorHovered); + RenderWindowOuterSingleBorder(window, border_n, border_col_resizing, ImMax(2.0f, window->WindowBorderSize)); // Thicker than usual + } + if (g.Style.FrameBorderSize > 0 && !(window->Flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive) + { + float y = window->Pos.y + window->TitleBarHeight - 1; + window->DrawList->AddLine(ImVec2(window->Pos.x + border_size * 0.5f, y), ImVec2(window->Pos.x + window->Size.x - border_size * 0.5f, y), border_col, g.Style.FrameBorderSize); } } -void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size) +// Draw background and borders +// Draw and handle scrollbars +void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, bool handle_borders_and_resize_grips, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size) { ImGuiContext& g = *GImGui; ImGuiStyle& style = g.Style; ImGuiWindowFlags flags = window->Flags; + // Ensure that Scrollbar() doesn't read last frame's SkipItems + IM_ASSERT(window->BeginCount == 0); + window->SkipItems = false; + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + // Draw window + handle manual resize - // As we highlight the title bar when want_focus is set, multiple reappearing windows will have have their title bar highlighted on their reappearing frame. + // As we highlight the title bar when want_focus is set, multiple reappearing windows will have their title bar highlighted on their reappearing frame. const float window_rounding = window->WindowRounding; const float window_border_size = window->WindowBorderSize; if (window->Collapsed) { // Title bar only - float backup_border_size = style.FrameBorderSize; + const float backup_border_size = style.FrameBorderSize; g.Style.FrameBorderSize = window->WindowBorderSize; - ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && !g.NavDisableHighlight) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); + ImU32 title_bar_col = GetColorU32((title_bar_is_highlight && g.NavCursorVisible) ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBgCollapsed); + if (window->ViewportOwned) + title_bar_col |= IM_COL32_A_MASK; // No alpha (we don't support is_docking_transparent_payload here because simpler and less meaningful, but could with a bit of code shuffle/reuse) RenderFrame(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, true, window_rounding); g.Style.FrameBorderSize = backup_border_size; } @@ -5234,20 +7126,59 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar // Window background if (!(flags & ImGuiWindowFlags_NoBackground)) { - ImU32 bg_col = GetColorU32(GetWindowBgColorIdxFromFlags(flags)); - float alpha = 1.0f; - if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasBgAlpha) - alpha = g.NextWindowData.BgAlphaVal; - if (alpha != 1.0f) - bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(alpha) << IM_COL32_A_SHIFT); - window->DrawList->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight()), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Bot); + bool is_docking_transparent_payload = false; + if (g.DragDropActive && (g.FrameCount - g.DragDropAcceptFrameCount) <= 1 && g.IO.ConfigDockingTransparentPayload) + if (g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW) && *(ImGuiWindow**)g.DragDropPayload.Data == window) + is_docking_transparent_payload = true; + + ImU32 bg_col = GetColorU32(GetWindowBgColorIdx(window)); + if (window->ViewportOwned) + { + bg_col |= IM_COL32_A_MASK; // No alpha + if (is_docking_transparent_payload) + window->Viewport->Alpha *= DOCKING_TRANSPARENT_PAYLOAD_ALPHA; + } + else + { + // Adjust alpha. For docking + bool override_alpha = false; + float alpha = 1.0f; + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasBgAlpha) + { + alpha = g.NextWindowData.BgAlphaVal; + override_alpha = true; + } + if (is_docking_transparent_payload) + { + alpha *= DOCKING_TRANSPARENT_PAYLOAD_ALPHA; // FIXME-DOCK: Should that be an override? + override_alpha = true; + } + if (override_alpha) + bg_col = (bg_col & ~IM_COL32_A_MASK) | (IM_F32_TO_INT8_SAT(alpha) << IM_COL32_A_SHIFT); + } + + // Render, for docked windows and host windows we ensure bg goes before decorations + if (window->DockIsActive) + window->DockNode->LastBgColor = bg_col; + ImDrawList* bg_draw_list = window->DockIsActive ? window->DockNode->HostWindow->DrawList : window->DrawList; + if (window->DockIsActive || (flags & ImGuiWindowFlags_DockNodeHost)) + bg_draw_list->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG); + bg_draw_list->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? 0 : ImDrawFlags_RoundCornersBottom); + if (window->DockIsActive || (flags & ImGuiWindowFlags_DockNodeHost)) + bg_draw_list->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_FG); } + if (window->DockIsActive) + window->DockNode->IsBgDrawnThisFrame = true; // Title bar - if (!(flags & ImGuiWindowFlags_NoTitleBar)) + // (when docked, DockNode are drawing their own title bar. Individual windows however do NOT set the _NoTitleBar flag, + // in order for their pos/size to be matching their undocking state.) + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive) { ImU32 title_bar_col = GetColorU32(title_bar_is_highlight ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); - window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawCornerFlags_Top); + if (window->ViewportOwned) + title_bar_col |= IM_COL32_A_MASK; // No alpha + window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, window_rounding, ImDrawFlags_RoundCornersTop); } // Menu bar @@ -5255,9 +7186,30 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar { ImRect menu_bar_rect = window->MenuBarRect(); menu_bar_rect.ClipWith(window->Rect()); // Soft clipping, in particular child window don't have minimum size covering the menu bar so this is useful for them. - window->DrawList->AddRectFilled(menu_bar_rect.Min + ImVec2(window_border_size, 0), menu_bar_rect.Max - ImVec2(window_border_size, 0), GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawCornerFlags_Top); + window->DrawList->AddRectFilled(menu_bar_rect.Min, menu_bar_rect.Max, GetColorU32(ImGuiCol_MenuBarBg), (flags & ImGuiWindowFlags_NoTitleBar) ? window_rounding : 0.0f, ImDrawFlags_RoundCornersTop); if (style.FrameBorderSize > 0.0f && menu_bar_rect.Max.y < window->Pos.y + window->Size.y) - window->DrawList->AddLine(menu_bar_rect.GetBL(), menu_bar_rect.GetBR(), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + window->DrawList->AddLine(menu_bar_rect.GetBL() + ImVec2(window_border_size * 0.5f, 0.0f), menu_bar_rect.GetBR() - ImVec2(window_border_size * 0.5f, 0.0f), GetColorU32(ImGuiCol_Border), style.FrameBorderSize); + } + + // Docking: Unhide tab bar (small triangle in the corner), drag from small triangle to quickly undock + ImGuiDockNode* node = window->DockNode; + if (window->DockIsActive && node->IsHiddenTabBar() && !node->IsNoTabBar()) + { + float unhide_sz_draw = ImTrunc(g.FontSize * 0.70f); + float unhide_sz_hit = ImTrunc(g.FontSize * 0.55f); + ImVec2 p = node->Pos; + ImRect r(p, p + ImVec2(unhide_sz_hit, unhide_sz_hit)); + ImGuiID unhide_id = window->GetID("#UNHIDE"); + KeepAliveID(unhide_id); + bool hovered, held; + if (ButtonBehavior(r, unhide_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren)) + node->WantHiddenTabBarToggle = true; + else if (held && IsMouseDragging(0)) + StartMouseMovingWindowOrNode(window, node, true); // Undock from tab-bar triangle = same as window/collapse menu button + + // FIXME-DOCK: Ideally we'd use ImGuiCol_TitleBgActive/ImGuiCol_TitleBg here, but neither is guaranteed to be visible enough at this sort of size.. + ImU32 col = GetColorU32(((held && hovered) || (node->IsFocused && !hovered)) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + window->DrawList->AddTriangleFilled(p, p + ImVec2(unhide_sz_draw, 0.0f), p + ImVec2(0.0f, unhide_sz_draw), col); } // Scrollbars @@ -5267,24 +7219,31 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar Scrollbar(ImGuiAxis_Y); // Render resize grips (after their input handling so we don't have a frame of latency) - if (!(flags & ImGuiWindowFlags_NoResize)) + if (handle_borders_and_resize_grips && !(flags & ImGuiWindowFlags_NoResize)) { for (int resize_grip_n = 0; resize_grip_n < resize_grip_count; resize_grip_n++) { + const ImU32 col = resize_grip_col[resize_grip_n]; + if ((col & IM_COL32_A_MASK) == 0) + continue; const ImGuiResizeGripDef& grip = resize_grip_def[resize_grip_n]; const ImVec2 corner = ImLerp(window->Pos, window->Pos + window->Size, grip.CornerPosN); - window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(window_border_size, resize_grip_draw_size) : ImVec2(resize_grip_draw_size, window_border_size))); - window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(resize_grip_draw_size, window_border_size) : ImVec2(window_border_size, resize_grip_draw_size))); - window->DrawList->PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + window_border_size), corner.y + grip.InnerDir.y * (window_rounding + window_border_size)), window_rounding, grip.AngleMin12, grip.AngleMax12); - window->DrawList->PathFillConvex(resize_grip_col[resize_grip_n]); + const float border_inner = IM_ROUND(window_border_size * 0.5f); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(border_inner, resize_grip_draw_size) : ImVec2(resize_grip_draw_size, border_inner))); + window->DrawList->PathLineTo(corner + grip.InnerDir * ((resize_grip_n & 1) ? ImVec2(resize_grip_draw_size, border_inner) : ImVec2(border_inner, resize_grip_draw_size))); + window->DrawList->PathArcToFast(ImVec2(corner.x + grip.InnerDir.x * (window_rounding + border_inner), corner.y + grip.InnerDir.y * (window_rounding + border_inner)), window_rounding, grip.AngleMin12, grip.AngleMax12); + window->DrawList->PathFillConvex(col); } } - // Borders - RenderWindowOuterBorders(window); + // Borders (for dock node host they will be rendered over after the tab bar) + if (handle_borders_and_resize_grips && !window->DockNodeAsHost) + RenderWindowOuterBorders(window); } + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; } +// When inside a dock node, this is handled in DockNodeCalcTabBarLayout() instead. // Render title text, collapse button, close button void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open) { @@ -5296,10 +7255,10 @@ void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& titl const bool has_collapse_button = !(flags & ImGuiWindowFlags_NoCollapse) && (style.WindowMenuButtonPosition != ImGuiDir_None); // Close & Collapse button are on the Menu NavLayer and don't default focus (unless there's nothing else on that layer) - const ImGuiItemFlags item_flags_backup = window->DC.ItemFlags; - window->DC.ItemFlags |= ImGuiItemFlags_NoNavDefaultFocus; + // FIXME-NAV: Might want (or not?) to set the equivalent of ImGuiButtonFlags_NoNavFocus so that mouse clicks on standard title bar items don't necessarily set nav/keyboard ref? + const ImGuiItemFlags item_flags_backup = g.CurrentItemFlags; + g.CurrentItemFlags |= ImGuiItemFlags_NoNavDefaultFocus; window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; - window->DC.NavLayerCurrentMask = (1 << ImGuiNavLayer_Menu); // Layout buttons // FIXME: Would be nice to generalize the subtleties expressed here into reusable code. @@ -5310,23 +7269,23 @@ void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& titl ImVec2 collapse_button_pos; if (has_close_button) { - pad_r += button_sz; - close_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - style.FramePadding.x, title_bar_rect.Min.y); + close_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y); + pad_r += button_sz + style.ItemInnerSpacing.x; } if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Right) { - pad_r += button_sz; - collapse_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - style.FramePadding.x, title_bar_rect.Min.y); + collapse_button_pos = ImVec2(title_bar_rect.Max.x - pad_r - button_sz, title_bar_rect.Min.y + style.FramePadding.y); + pad_r += button_sz + style.ItemInnerSpacing.x; } if (has_collapse_button && style.WindowMenuButtonPosition == ImGuiDir_Left) { - collapse_button_pos = ImVec2(title_bar_rect.Min.x + pad_l - style.FramePadding.x, title_bar_rect.Min.y); - pad_l += button_sz; + collapse_button_pos = ImVec2(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y + style.FramePadding.y); + pad_l += button_sz + style.ItemInnerSpacing.x; } // Collapse button (submitting first so it gets priority when choosing a navigation init fallback) if (has_collapse_button) - if (CollapseButton(window->GetID("#COLLAPSE"), collapse_button_pos)) + if (CollapseButton(window->GetID("#COLLAPSE"), collapse_button_pos, NULL)) window->WantCollapseToggle = true; // Defer actual collapsing to next frame as we are too far in the Begin() function // Close button @@ -5335,17 +7294,15 @@ void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& titl *p_open = false; window->DC.NavLayerCurrent = ImGuiNavLayer_Main; - window->DC.NavLayerCurrentMask = (1 << ImGuiNavLayer_Main); - window->DC.ItemFlags = item_flags_backup; + g.CurrentItemFlags = item_flags_backup; // Title bar text (with: horizontal alignment, avoiding collapse/close button, optional "unsaved document" marker) // FIXME: Refactor text alignment facilities along with RenderText helpers, this is WAY too much messy code.. - const char* UNSAVED_DOCUMENT_MARKER = "*"; - const float marker_size_x = (flags & ImGuiWindowFlags_UnsavedDocument) ? CalcTextSize(UNSAVED_DOCUMENT_MARKER, NULL, false).x : 0.0f; + const float marker_size_x = (flags & ImGuiWindowFlags_UnsavedDocument) ? button_sz * 0.80f : 0.0f; const ImVec2 text_size = CalcTextSize(name, NULL, true) + ImVec2(marker_size_x, 0.0f); // As a nice touch we try to ensure that centered title text doesn't get affected by visibility of Close/Collapse button, - // while uncentered title text will still reach edges correct. + // while uncentered title text will still reach edges correctly. if (pad_l > style.FramePadding.x) pad_l += g.Style.ItemInnerSpacing.x; if (pad_r > style.FramePadding.x) @@ -5359,32 +7316,81 @@ void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& titl } ImRect layout_r(title_bar_rect.Min.x + pad_l, title_bar_rect.Min.y, title_bar_rect.Max.x - pad_r, title_bar_rect.Max.y); - ImRect clip_r(layout_r.Min.x, layout_r.Min.y, layout_r.Max.x + g.Style.ItemInnerSpacing.x, layout_r.Max.y); - //if (g.IO.KeyCtrl) window->DrawList->AddRect(layout_r.Min, layout_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] - RenderTextClipped(layout_r.Min, layout_r.Max, name, NULL, &text_size, style.WindowTitleAlign, &clip_r); + ImRect clip_r(layout_r.Min.x, layout_r.Min.y, ImMin(layout_r.Max.x + g.Style.ItemInnerSpacing.x, title_bar_rect.Max.x), layout_r.Max.y); if (flags & ImGuiWindowFlags_UnsavedDocument) { - ImVec2 marker_pos = ImVec2(ImMax(layout_r.Min.x, layout_r.Min.x + (layout_r.GetWidth() - text_size.x) * style.WindowTitleAlign.x) + text_size.x, layout_r.Min.y) + ImVec2(2 - marker_size_x, 0.0f); - ImVec2 off = ImVec2(0.0f, (float)(int)(-g.FontSize * 0.25f)); - RenderTextClipped(marker_pos + off, layout_r.Max + off, UNSAVED_DOCUMENT_MARKER, NULL, NULL, ImVec2(0, style.WindowTitleAlign.y), &clip_r); + ImVec2 marker_pos; + marker_pos.x = ImClamp(layout_r.Min.x + (layout_r.GetWidth() - text_size.x) * style.WindowTitleAlign.x + text_size.x, layout_r.Min.x, layout_r.Max.x); + marker_pos.y = (layout_r.Min.y + layout_r.Max.y) * 0.5f; + if (marker_pos.x > layout_r.Min.x) + { + RenderBullet(window->DrawList, marker_pos, GetColorU32(ImGuiCol_Text)); + clip_r.Max.x = ImMin(clip_r.Max.x, marker_pos.x - (int)(marker_size_x * 0.5f)); + } } + //if (g.IO.KeyShift) window->DrawList->AddRect(layout_r.Min, layout_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] + //if (g.IO.KeyCtrl) window->DrawList->AddRect(clip_r.Min, clip_r.Max, IM_COL32(255, 128, 0, 255)); // [DEBUG] + RenderTextClipped(layout_r.Min, layout_r.Max, name, NULL, &text_size, style.WindowTitleAlign, &clip_r); } void ImGui::UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window) { window->ParentWindow = parent_window; - window->RootWindow = window->RootWindowForTitleBarHighlight = window->RootWindowForNav = window; + window->RootWindow = window->RootWindowPopupTree = window->RootWindowDockTree = window->RootWindowForTitleBarHighlight = window->RootWindowForNav = window; if (parent_window && (flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Tooltip)) - window->RootWindow = parent_window->RootWindow; - if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) + { + window->RootWindowDockTree = parent_window->RootWindowDockTree; + if (!window->DockIsActive && !(parent_window->Flags & ImGuiWindowFlags_DockNodeHost)) + window->RootWindow = parent_window->RootWindow; + } + if (parent_window && (flags & ImGuiWindowFlags_Popup)) + window->RootWindowPopupTree = parent_window->RootWindowPopupTree; + if (parent_window && !(flags & ImGuiWindowFlags_Modal) && (flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup))) // FIXME: simply use _NoTitleBar ? window->RootWindowForTitleBarHighlight = parent_window->RootWindowForTitleBarHighlight; - while (window->RootWindowForNav->Flags & ImGuiWindowFlags_NavFlattened) + while (window->RootWindowForNav->ChildFlags & ImGuiChildFlags_NavFlattened) { IM_ASSERT(window->RootWindowForNav->ParentWindow != NULL); window->RootWindowForNav = window->RootWindowForNav->ParentWindow; } } +// [EXPERIMENTAL] Called by Begin(). NextWindowData is valid at this point. +// This is designed as a toy/test-bed for +void ImGui::UpdateWindowSkipRefresh(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + window->SkipRefresh = false; + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasRefreshPolicy) == 0) + return; + if (g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_TryToAvoidRefresh) + { + // FIXME-IDLE: Tests for e.g. mouse clicks or keyboard while focused. + if (window->Appearing) // If currently appearing + return; + if (window->Hidden) // If was hidden (previous frame) + return; + if ((g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_RefreshOnHover) && g.HoveredWindow) + if (window->RootWindow == g.HoveredWindow->RootWindow || IsWindowWithinBeginStackOf(g.HoveredWindow->RootWindow, window)) + return; + if ((g.NextWindowData.RefreshFlagsVal & ImGuiWindowRefreshFlags_RefreshOnFocus) && g.NavWindow) + if (window->RootWindow == g.NavWindow->RootWindow || IsWindowWithinBeginStackOf(g.NavWindow->RootWindow, window)) + return; + window->DrawList = NULL; + window->SkipRefresh = true; + } +} + +static void SetWindowActiveForSkipRefresh(ImGuiWindow* window) +{ + window->Active = true; + for (ImGuiWindow* child : window->DC.ChildWindows) + if (!child->Hidden) + { + child->Active = child->SkipRefresh = true; + SetWindowActiveForSkipRefresh(child); + } +} + // Push a new Dear ImGui window to add widgets to. // - A default window called "Debug" is automatically stacked at the beginning of every frame so you can use widgets without explicitly calling a Begin/End pair. // - Begin/End can be called multiple times during the frame with the same window name to append content. @@ -5397,45 +7403,47 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; IM_ASSERT(name != NULL && name[0] != '\0'); // Window name required - IM_ASSERT(g.FrameScopeActive); // Forgot to call ImGui::NewFrame() + IM_ASSERT(g.WithinFrameScope); // Forgot to call ImGui::NewFrame() IM_ASSERT(g.FrameCountEnded != g.FrameCount); // Called ImGui::Render() or ImGui::EndFrame() and haven't called ImGui::NewFrame() again yet // Find or create ImGuiWindow* window = FindWindowByName(name); const bool window_just_created = (window == NULL); if (window_just_created) - { - ImVec2 size_on_first_use = (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) ? g.NextWindowData.SizeVal : ImVec2(0.0f, 0.0f); // Any condition flag will do since we are creating a new window here. - window = CreateNewWindow(name, size_on_first_use, flags); - } + window = CreateNewWindow(name, flags); + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInWindow == window->ID) + IM_DEBUG_BREAK(); // Automatically disable manual moving/resizing when NoInputs is set if ((flags & ImGuiWindowFlags_NoInputs) == ImGuiWindowFlags_NoInputs) flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize; - if (flags & ImGuiWindowFlags_NavFlattened) - IM_ASSERT(flags & ImGuiWindowFlags_ChildWindow); - const int current_frame = g.FrameCount; const bool first_begin_of_the_frame = (window->LastFrameActive != current_frame); + window->IsFallbackWindow = (g.CurrentWindowStack.Size == 0 && g.WithinFrameScopeWithImplicitWindow); - // Update the Appearing flag - bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on - const bool window_just_appearing_after_hidden_for_resize = (window->HiddenFramesCannotSkipItems > 0); + // Update the Appearing flag (note: the BeginDocked() path may also set this to true later) + bool window_just_activated_by_user = (window->LastFrameActive < current_frame - 1); // Not using !WasActive because the implicit "Debug" window would always toggle off->on if (flags & ImGuiWindowFlags_Popup) { ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; window_just_activated_by_user |= (window->PopupId != popup_ref.PopupId); // We recycle popups so treat window as activated if popup id changed window_just_activated_by_user |= (window != popup_ref.Window); } - window->Appearing = (window_just_activated_by_user || window_just_appearing_after_hidden_for_resize); - if (window->Appearing) - SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); // Update Flags, LastFrameActive, BeginOrderXXX fields + const bool window_was_appearing = window->Appearing; if (first_begin_of_the_frame) { + UpdateWindowInFocusOrderList(window, window_just_created, flags); + window->Appearing = window_just_activated_by_user; + if (window->Appearing) + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); + window->FlagsPreviousFrame = window->Flags; window->Flags = (ImGuiWindowFlags)flags; + window->ChildFlags = (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasChildFlags) ? g.NextWindowData.ChildFlags : 0; window->LastFrameActive = current_frame; window->LastTimeActive = (float)g.Time; window->BeginOrderWithinParent = 0; @@ -5446,8 +7454,42 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) flags = window->Flags; } + // Docking + // (NB: during the frame dock nodes are created, it is possible that (window->DockIsActive == false) even though (window->DockNode->Windows.Size > 1) + IM_ASSERT(window->DockNode == NULL || window->DockNodeAsHost == NULL); // Cannot be both + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasDock) + SetWindowDock(window, g.NextWindowData.DockId, g.NextWindowData.DockCond); + if (first_begin_of_the_frame) + { + bool has_dock_node = (window->DockId != 0 || window->DockNode != NULL); + bool new_auto_dock_node = !has_dock_node && GetWindowAlwaysWantOwnTabBar(window); + bool dock_node_was_visible = window->DockNodeIsVisible; + bool dock_tab_was_visible = window->DockTabIsVisible; + if (has_dock_node || new_auto_dock_node) + { + BeginDocked(window, p_open); + flags = window->Flags; + if (window->DockIsActive) + { + IM_ASSERT(window->DockNode != NULL); + g.NextWindowData.HasFlags &= ~ImGuiNextWindowDataFlags_HasSizeConstraint; // Docking currently override constraints + } + + // Amend the Appearing flag + if (window->DockTabIsVisible && !dock_tab_was_visible && dock_node_was_visible && !window->Appearing && !window_was_appearing) + { + window->Appearing = true; + SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, true); + } + } + else + { + window->DockIsActive = window->DockNodeIsVisible = window->DockTabIsVisible = false; + } + } + // Parent window is latched only on the first call to Begin() of the frame, so further append-calls can be done from a different window stack - ImGuiWindow* parent_window_in_stack = g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back(); + ImGuiWindow* parent_window_in_stack = (window->DockIsActive && window->DockNode->HostWindow) ? window->DockNode->HostWindow : g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back().Window; ImGuiWindow* parent_window = first_begin_of_the_frame ? ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Popup)) ? parent_window_in_stack : NULL) : window->ParentWindow; IM_ASSERT(parent_window != NULL || !(flags & ImGuiWindowFlags_ChildWindow)); @@ -5456,25 +7498,62 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->IDStack.push_back(window->ID); // Add to stack - // We intentionally set g.CurrentWindow to NULL to prevent usage until when the viewport is set, then will call SetCurrentWindow() - g.CurrentWindowStack.push_back(window); - g.CurrentWindow = NULL; - CheckStacksSize(window, true); + g.CurrentWindow = window; + g.CurrentWindowStack.resize(g.CurrentWindowStack.Size + 1); + ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back(); + window_stack_data.Window = window; + window_stack_data.ParentLastItemDataBackup = g.LastItemData; + window_stack_data.DisabledOverrideReenable = (flags & ImGuiWindowFlags_Tooltip) && (g.CurrentItemFlags & ImGuiItemFlags_Disabled); + window_stack_data.DisabledOverrideReenableAlphaBackup = 0.0f; + ErrorRecoveryStoreState(&window_stack_data.StackSizesInBegin); + g.StackSizesInBeginForCurrentWindow = &window_stack_data.StackSizesInBegin; + if (flags & ImGuiWindowFlags_ChildMenu) + g.BeginMenuDepth++; + + // Update ->RootWindow and others pointers (before any possible call to FocusWindow) + if (first_begin_of_the_frame) + { + UpdateWindowParentAndRootLinks(window, flags, parent_window); + window->ParentWindowInBeginStack = parent_window_in_stack; + + // Focus route + // There's little point to expose a flag to set this: because the interesting cases won't be using parent_window_in_stack, + // Use for e.g. linking a tool window in a standalone viewport to a document window, regardless of their Begin() stack parenting. (#6798) + window->ParentWindowForFocusRoute = (window->RootWindow != window) ? parent_window_in_stack : NULL; + if (window->ParentWindowForFocusRoute == NULL && window->DockNode != NULL) + if (window->DockNode->MergedFlags & ImGuiDockNodeFlags_DockedWindowsInFocusRoute) + window->ParentWindowForFocusRoute = window->DockNode->HostWindow; + + // Override with SetNextWindowClass() field or direct call to SetWindowParentWindowForFocusRoute() + if (window->WindowClass.FocusRouteParentWindowId != 0) + { + window->ParentWindowForFocusRoute = FindWindowByID(window->WindowClass.FocusRouteParentWindowId); + IM_ASSERT(window->ParentWindowForFocusRoute != 0); // Invalid value for FocusRouteParentWindowId. + } + + // Inherit SetWindowFontScale() from parent until we fix this system... + window->FontWindowScaleParents = parent_window ? parent_window->FontWindowScaleParents * parent_window->FontWindowScale : 1.0f; + } + + // Add to focus scope stack + PushFocusScope((window->ChildFlags & ImGuiChildFlags_NavFlattened) ? g.CurrentFocusScopeId : window->ID); + window->NavRootFocusScopeId = g.CurrentFocusScopeId; + + // Add to popup stacks: update OpenPopupStack[] data, push to BeginPopupStack[] if (flags & ImGuiWindowFlags_Popup) { ImGuiPopupData& popup_ref = g.OpenPopupStack[g.BeginPopupStack.Size]; popup_ref.Window = window; + popup_ref.ParentNavLayer = parent_window_in_stack->DC.NavLayerCurrent; g.BeginPopupStack.push_back(popup_ref); window->PopupId = popup_ref.PopupId; } - if (window_just_appearing_after_hidden_for_resize && !(flags & ImGuiWindowFlags_ChildWindow)) - window->NavLastIds[0] = 0; - // Process SetNextWindow***() calls + // (FIXME: Consider splitting the HasXXX flags into X/Y components bool window_pos_set_by_api = false; bool window_size_x_set_by_api = false, window_size_y_set_by_api = false; - if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) { window_pos_set_by_api = (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) != 0; if (window_pos_set_by_api && ImLengthSqr(g.NextWindowData.PosPivotVal) > 0.00001f) @@ -5490,34 +7569,69 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) SetWindowPos(window, g.NextWindowData.PosVal, g.NextWindowData.PosCond); } } - if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSize) + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) { window_size_x_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.x > 0.0f); window_size_y_set_by_api = (window->SetWindowSizeAllowFlags & g.NextWindowData.SizeCond) != 0 && (g.NextWindowData.SizeVal.y > 0.0f); + if ((window->ChildFlags & ImGuiChildFlags_ResizeX) && (window->SetWindowSizeAllowFlags & ImGuiCond_FirstUseEver) == 0) // Axis-specific conditions for BeginChild() + g.NextWindowData.SizeVal.x = window->SizeFull.x; + if ((window->ChildFlags & ImGuiChildFlags_ResizeY) && (window->SetWindowSizeAllowFlags & ImGuiCond_FirstUseEver) == 0) + g.NextWindowData.SizeVal.y = window->SizeFull.y; SetWindowSize(window, g.NextWindowData.SizeVal, g.NextWindowData.SizeCond); } - if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasContentSize) + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasScroll) + { + if (g.NextWindowData.ScrollVal.x >= 0.0f) + { + window->ScrollTarget.x = g.NextWindowData.ScrollVal.x; + window->ScrollTargetCenterRatio.x = 0.0f; + } + if (g.NextWindowData.ScrollVal.y >= 0.0f) + { + window->ScrollTarget.y = g.NextWindowData.ScrollVal.y; + window->ScrollTargetCenterRatio.y = 0.0f; + } + } + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasContentSize) window->ContentSizeExplicit = g.NextWindowData.ContentSizeVal; else if (first_begin_of_the_frame) window->ContentSizeExplicit = ImVec2(0.0f, 0.0f); - if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasCollapsed) + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasWindowClass) + window->WindowClass = g.NextWindowData.WindowClass; + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasCollapsed) SetWindowCollapsed(window, g.NextWindowData.CollapsedVal, g.NextWindowData.CollapsedCond); - if (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasFocus) + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasFocus) FocusWindow(window); if (window->Appearing) SetWindowConditionAllowFlags(window, ImGuiCond_Appearing, false); + // [EXPERIMENTAL] Skip Refresh mode + UpdateWindowSkipRefresh(window); + + // Nested root windows (typically tooltips) override disabled state + if (window_stack_data.DisabledOverrideReenable && window->RootWindow == window) + BeginDisabledOverrideReenable(); + + // We intentionally set g.CurrentWindow to NULL to prevent usage until when the viewport is set, then will call SetCurrentWindow() + g.CurrentWindow = NULL; + // When reusing window again multiple times a frame, just append content (don't need to setup again) - if (first_begin_of_the_frame) + if (first_begin_of_the_frame && !window->SkipRefresh) { // Initialize const bool window_is_child_tooltip = (flags & ImGuiWindowFlags_ChildWindow) && (flags & ImGuiWindowFlags_Tooltip); // FIXME-WIP: Undocumented behavior of Child+Tooltip for pinned tooltip (#1345) - UpdateWindowParentAndRootLinks(window, flags, parent_window); - + const bool window_just_appearing_after_hidden_for_resize = (window->HiddenFramesCannotSkipItems > 0); window->Active = true; window->HasCloseButton = (p_open != NULL); - window->ClipRect = ImVec4(-FLT_MAX,-FLT_MAX,+FLT_MAX,+FLT_MAX); + window->ClipRect = ImVec4(-FLT_MAX, -FLT_MAX, +FLT_MAX, +FLT_MAX); window->IDStack.resize(1); + window->DrawList->_ResetForNewFrame(); + window->DC.CurrentTableIdx = -1; + if (flags & ImGuiWindowFlags_DockNodeHost) + { + window->DrawList->ChannelsSplit(2); + window->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_FG); // Render decorations on channel 1 as we will render the backgrounds manually later + } // Restore buffer capacity when woken from a compacted state, to avoid if (window->MemoryCompacted) @@ -5526,7 +7640,11 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // Update stored window name when it changes (which can _only_ happen with the "###" operator, so the ID would stay unchanged). // The title bar always display the 'name' parameter, so we only update the string storage if it needs to be visible to the end-user elsewhere. bool window_title_visible_elsewhere = false; - if (g.NavWindowingList != NULL && (window->Flags & ImGuiWindowFlags_NoNavFocus) == 0) // Window titles visible when using CTRL+TAB + if ((window->Viewport && window->Viewport->Window == window) || (window->DockIsActive)) + window_title_visible_elsewhere = true; + else if (g.NavWindowingListWindow != NULL && (flags & ImGuiWindowFlags_NoNavFocus) == 0) // Window titles visible when using CTRL+TAB + window_title_visible_elsewhere = true; + else if (flags & ImGuiWindowFlags_ChildMenu) window_title_visible_elsewhere = true; if (window_title_visible_elsewhere && !window_just_created && strcmp(name, window->Name) != 0) { @@ -5538,11 +7656,17 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // UPDATE CONTENTS SIZE, UPDATE HIDDEN STATUS // Update contents size from last frame for auto-fitting (or use explicit size) - window->ContentSize = CalcWindowContentSize(window); + CalcWindowContentSizes(window, &window->ContentSize, &window->ContentSizeIdeal); + + // FIXME: These flags are decremented before they are used. This means that in order to have these fields produce their intended behaviors + // for one frame we must set them to at least 2, which is counter-intuitive. HiddenFramesCannotSkipItems is a more complicated case because + // it has a single usage before this code block and may be set below before it is finally checked. if (window->HiddenFramesCanSkipItems > 0) window->HiddenFramesCanSkipItems--; if (window->HiddenFramesCannotSkipItems > 0) window->HiddenFramesCannotSkipItems--; + if (window->HiddenFramesForRenderOnly > 0) + window->HiddenFramesForRenderOnly--; // Hide new windows for one frame until they calculate their size if (window_just_created && (!window_size_x_set_by_api || !window_size_y_set_by_api)) @@ -5559,38 +7683,62 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->Size.x = window->SizeFull.x = 0.f; if (!window_size_y_set_by_api) window->Size.y = window->SizeFull.y = 0.f; - window->ContentSize = ImVec2(0.f, 0.f); + window->ContentSize = window->ContentSizeIdeal = ImVec2(0.f, 0.f); } } - // FIXME-VIEWPORT: In the docking/viewport branch, this is the point where we select the current viewport (which may affect the style) + // SELECT VIEWPORT + // We need to do this before using any style/font sizes, as viewport with a different DPI may affect font sizes. + + WindowSelectViewport(window); + SetCurrentViewport(window, window->Viewport); + window->FontDpiScale = (g.IO.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleFonts) ? window->Viewport->DpiScale : 1.0f; SetCurrentWindow(window); + flags = window->Flags; // LOCK BORDER SIZE AND PADDING FOR THE FRAME (so that altering them doesn't cause inconsistencies) + // We read Style data after the call to UpdateSelectWindowViewport() which might be swapping the style. - if (flags & ImGuiWindowFlags_ChildWindow) + if (!window->DockIsActive && (flags & ImGuiWindowFlags_ChildWindow)) window->WindowBorderSize = style.ChildBorderSize; else window->WindowBorderSize = ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupBorderSize : style.WindowBorderSize; window->WindowPadding = style.WindowPadding; - if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & (ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_Popup)) && window->WindowBorderSize == 0.0f) + if (!window->DockIsActive && (flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !(window->ChildFlags & ImGuiChildFlags_AlwaysUseWindowPadding) && window->WindowBorderSize == 0.0f) window->WindowPadding = ImVec2(0.0f, (flags & ImGuiWindowFlags_MenuBar) ? style.WindowPadding.y : 0.0f); + + // Lock menu offset so size calculation can use it as menu-bar windows need a minimum size. window->DC.MenuBarOffset.x = ImMax(ImMax(window->WindowPadding.x, style.ItemSpacing.x), g.NextWindowData.MenuBarOffsetMinVal.x); window->DC.MenuBarOffset.y = g.NextWindowData.MenuBarOffsetMinVal.y; + window->TitleBarHeight = (flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : g.FontSize + g.Style.FramePadding.y * 2.0f; + window->MenuBarHeight = (flags & ImGuiWindowFlags_MenuBar) ? window->DC.MenuBarOffset.y + g.FontSize + g.Style.FramePadding.y * 2.0f : 0.0f; + window->FontRefSize = g.FontSize; // Lock this to discourage calling window->CalcFontSize() outside of current window. + + // Depending on condition we use previous or current window size to compare against contents size to decide if a scrollbar should be visible. + // Those flags will be altered further down in the function depending on more conditions. + bool use_current_size_for_scrollbar_x = window_just_created; + bool use_current_size_for_scrollbar_y = window_just_created; + if (window_size_x_set_by_api && window->ContentSizeExplicit.x != 0.0f) + use_current_size_for_scrollbar_x = true; + if (window_size_y_set_by_api && window->ContentSizeExplicit.y != 0.0f) // #7252 + use_current_size_for_scrollbar_y = true; // Collapse window by double-clicking on title bar // At this point we don't have a clipping rectangle setup yet, so we can use the title bar area for hit detection and drawing - if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse)) + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !(flags & ImGuiWindowFlags_NoCollapse) && !window->DockIsActive) { - // We don't use a regular button+id to test for double-click on title bar (mostly due to legacy reason, could be fixed), so verify that we don't have items over the title bar. + // We don't use a regular button+id to test for double-click on title bar (mostly due to legacy reason, could be fixed), + // so verify that we don't have items over the title bar. ImRect title_bar_rect = window->TitleBarRect(); - if (g.HoveredWindow == window && g.HoveredId == 0 && g.HoveredIdPreviousFrame == 0 && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max) && g.IO.MouseDoubleClicked[0]) - window->WantCollapseToggle = true; + if (g.HoveredWindow == window && g.HoveredId == 0 && g.HoveredIdPreviousFrame == 0 && g.ActiveId == 0 && IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max)) + if (g.IO.MouseClickedCount[0] == 2 && GetKeyOwner(ImGuiKey_MouseLeft) == ImGuiKeyOwner_NoOwner) + window->WantCollapseToggle = true; if (window->WantCollapseToggle) { window->Collapsed = !window->Collapsed; + if (!window->Collapsed) + use_current_size_for_scrollbar_y = true; MarkIniSettingsDirty(window); - FocusWindow(window); } } else @@ -5601,10 +7749,17 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // SIZE + // Outer Decoration Sizes + // (we need to clear ScrollbarSize immediately as CalcWindowAutoFitSize() needs it and can be called from other locations). + const ImVec2 scrollbar_sizes_from_last_frame = window->ScrollbarSizes; + window->DecoOuterSizeX1 = 0.0f; + window->DecoOuterSizeX2 = 0.0f; + window->DecoOuterSizeY1 = window->TitleBarHeight + window->MenuBarHeight; + window->DecoOuterSizeY2 = 0.0f; + window->ScrollbarSizes = ImVec2(0.0f, 0.0f); + // Calculate auto-fit size, handle automatic resize - const ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, window->ContentSize); - bool use_current_size_for_scrollbar_x = window_just_created; - bool use_current_size_for_scrollbar_y = window_just_created; + const ImVec2 size_auto_fit = CalcWindowAutoFitSize(window, window->ContentSizeIdeal); if ((flags & ImGuiWindowFlags_AlwaysAutoResize) && !window->Collapsed) { // Using SetNextWindowSize() overrides ImGuiWindowFlags_AlwaysAutoResize, so it can be used on tooltips/popups, etc. @@ -5641,16 +7796,13 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->SizeFull = CalcWindowSizeAfterConstraint(window, window->SizeFull); window->Size = window->Collapsed && !(flags & ImGuiWindowFlags_ChildWindow) ? window->TitleBarRect().GetSize() : window->SizeFull; - // Decoration size - const float decoration_up_height = window->TitleBarHeight() + window->MenuBarHeight(); - // POSITION // Popup latch its initial position, will position itself when it appears next frame if (window_just_activated_by_user) { window->AutoPosLastDirection = ImGuiDir_None; - if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api) + if ((flags & ImGuiWindowFlags_Popup) != 0 && !(flags & ImGuiWindowFlags_Modal) && !window_pos_set_by_api) // FIXME: BeginPopup() could use SetNextWindowPos() window->Pos = g.BeginPopupStack.back().OpenPopupPos; } @@ -5666,7 +7818,7 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) const bool window_pos_with_pivot = (window->SetWindowPosVal.x != FLT_MAX && window->HiddenFramesCannotSkipItems == 0); if (window_pos_with_pivot) - SetWindowPos(window, window->SetWindowPosVal - window->SizeFull * window->SetWindowPosPivot, 0); // Position given a pivot (e.g. for centering) + SetWindowPos(window, window->SetWindowPosVal - window->Size * window->SetWindowPosPivot, 0); // Position given a pivot (e.g. for centering) else if ((flags & ImGuiWindowFlags_ChildMenu) != 0) window->Pos = FindBestWindowPosForPopup(window); else if ((flags & ImGuiWindowFlags_Popup) != 0 && !window_pos_set_by_api && window_just_appearing_after_hidden_for_resize) @@ -5674,22 +7826,70 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) else if ((flags & ImGuiWindowFlags_Tooltip) != 0 && !window_pos_set_by_api && !window_is_child_tooltip) window->Pos = FindBestWindowPosForPopup(window); - // Clamp position/size so window stays visible within its viewport or monitor - - // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. - ImRect viewport_rect(GetViewportRect()); - if (!window_pos_set_by_api && !(flags & ImGuiWindowFlags_ChildWindow) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) - { - if (g.IO.DisplaySize.x > 0.0f && g.IO.DisplaySize.y > 0.0f) // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. + // Late create viewport if we don't fit within our current host viewport. + if (window->ViewportAllowPlatformMonitorExtend >= 0 && !window->ViewportOwned && !(window->Viewport->Flags & ImGuiViewportFlags_IsMinimized)) + if (!window->Viewport->GetMainRect().Contains(window->Rect())) { - ImVec2 clamp_padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); - ClampWindowRect(window, viewport_rect, clamp_padding); + // This is based on the assumption that the DPI will be known ahead (same as the DPI of the selection done in UpdateSelectWindowViewport) + //ImGuiViewport* old_viewport = window->Viewport; + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_NoFocusOnAppearing); + + // FIXME-DPI + //IM_ASSERT(old_viewport->DpiScale == window->Viewport->DpiScale); // FIXME-DPI: Something went wrong + SetCurrentViewport(window, window->Viewport); + window->FontDpiScale = (g.IO.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleFonts) ? window->Viewport->DpiScale : 1.0f; + SetCurrentWindow(window); + } + + if (window->ViewportOwned) + WindowSyncOwnedViewport(window, parent_window_in_stack); + + // Calculate the range of allowed position for that window (to be movable and visible past safe area padding) + // When clamping to stay visible, we will enforce that window->Pos stays inside of visibility_rect. + ImRect viewport_rect(window->Viewport->GetMainRect()); + ImRect viewport_work_rect(window->Viewport->GetWorkRect()); + ImVec2 visibility_padding = ImMax(style.DisplayWindowPadding, style.DisplaySafeAreaPadding); + ImRect visibility_rect(viewport_work_rect.Min + visibility_padding, viewport_work_rect.Max - visibility_padding); + + // Clamp position/size so window stays visible within its viewport or monitor + // Ignore zero-sized display explicitly to avoid losing positions if a window manager reports zero-sized window when initializing or minimizing. + // FIXME: Similar to code in GetWindowAllowedExtentRect() + if (!window_pos_set_by_api && !(flags & ImGuiWindowFlags_ChildWindow)) + { + if (!window->ViewportOwned && viewport_rect.GetWidth() > 0 && viewport_rect.GetHeight() > 0.0f) + { + ClampWindowPos(window, visibility_rect); + } + else if (window->ViewportOwned && g.PlatformIO.Monitors.Size > 0) + { + if (g.MovingWindow != NULL && window->RootWindowDockTree == g.MovingWindow->RootWindowDockTree) + { + // While moving windows we allow them to straddle monitors (#7299, #3071) + visibility_rect = g.PlatformMonitorsFullWorkRect; + } + else + { + // When not moving ensure visible in its monitor + // Lost windows (e.g. a monitor disconnected) will naturally moved to the fallback/dummy monitor aka the main viewport. + const ImGuiPlatformMonitor* monitor = GetViewportPlatformMonitor(window->Viewport); + visibility_rect = ImRect(monitor->WorkPos, monitor->WorkPos + monitor->WorkSize); + } + visibility_rect.Expand(-visibility_padding); + ClampWindowPos(window, visibility_rect); } } - window->Pos = ImFloor(window->Pos); + window->Pos = ImTrunc(window->Pos); // Lock window rounding for the frame (so that altering them doesn't cause inconsistencies) - window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; + // Large values tend to lead to variety of artifacts and are not recommended. + if (window->ViewportOwned || window->DockIsActive) + window->WindowRounding = 0.0f; + else + window->WindowRounding = (flags & ImGuiWindowFlags_ChildWindow) ? style.ChildRounding : ((flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiWindowFlags_Modal)) ? style.PopupRounding : style.WindowRounding; + + // For windows with title bar or menu bar, we clamp to FrameHeight(FontSize + FramePadding.y * 2.0f) to completely hide artifacts. + //if ((window->Flags & ImGuiWindowFlags_MenuBar) || !(window->Flags & ImGuiWindowFlags_NoTitleBar)) + // window->WindowRounding = ImMin(window->WindowRounding, g.FontSize + style.FramePadding.y * 2.0f); // Apply window focus (new and reactivated windows are moved to front) bool want_focus = false; @@ -5697,42 +7897,99 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) { if (flags & ImGuiWindowFlags_Popup) want_focus = true; - else if ((flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_Tooltip)) == 0) + else if ((window->DockIsActive || (flags & ImGuiWindowFlags_ChildWindow) == 0) && !(flags & ImGuiWindowFlags_Tooltip)) want_focus = true; } + // [Test Engine] Register whole window in the item system (before submitting further decorations) +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (g.TestEngineHookItems) + { + IM_ASSERT(window->IDStack.Size == 1); + window->IDStack.Size = 0; // As window->IDStack[0] == window->ID here, make sure TestEngine doesn't erroneously see window as parent of itself. + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + IMGUI_TEST_ENGINE_ITEM_ADD(window->ID, window->Rect(), NULL); + IMGUI_TEST_ENGINE_ITEM_INFO(window->ID, window->Name, (g.HoveredWindow == window) ? ImGuiItemStatusFlags_HoveredRect : 0); + window->IDStack.Size = 1; + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + + } +#endif + + // Decide if we are going to handle borders and resize grips + const bool handle_borders_and_resize_grips = (window->DockNodeAsHost || !window->DockIsActive); + // Handle manual resize: Resize Grips, Borders, Gamepad - int border_held = -1; - ImU32 resize_grip_col[4] = { 0 }; - const int resize_grip_count = g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // 4 - const float resize_grip_draw_size = (float)(int)ImMax(g.FontSize * 1.35f, window->WindowRounding + 1.0f + g.FontSize * 0.2f); - if (!window->Collapsed) - if (UpdateManualResize(window, size_auto_fit, &border_held, resize_grip_count, &resize_grip_col[0])) - use_current_size_for_scrollbar_x = use_current_size_for_scrollbar_y = true; + int border_hovered = -1, border_held = -1; + ImU32 resize_grip_col[4] = {}; + const int resize_grip_count = ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup)) ? 0 : g.IO.ConfigWindowsResizeFromEdges ? 2 : 1; // Allow resize from lower-left if we have the mouse cursor feedback for it. + const float resize_grip_draw_size = IM_TRUNC(ImMax(g.FontSize * 1.10f, window->WindowRounding + 1.0f + g.FontSize * 0.2f)); + if (handle_borders_and_resize_grips && !window->Collapsed) + if (int auto_fit_mask = UpdateWindowManualResize(window, size_auto_fit, &border_hovered, &border_held, resize_grip_count, &resize_grip_col[0], visibility_rect)) + { + if (auto_fit_mask & (1 << ImGuiAxis_X)) + use_current_size_for_scrollbar_x = true; + if (auto_fit_mask & (1 << ImGuiAxis_Y)) + use_current_size_for_scrollbar_y = true; + } + window->ResizeBorderHovered = (signed char)border_hovered; window->ResizeBorderHeld = (signed char)border_held; + // Synchronize window --> viewport again and one last time (clamping and manual resize may have affected either) + if (window->ViewportOwned) + { + if (!window->Viewport->PlatformRequestMove) + window->Viewport->Pos = window->Pos; + if (!window->Viewport->PlatformRequestResize) + window->Viewport->Size = window->Size; + window->Viewport->UpdateWorkRect(); + viewport_rect = window->Viewport->GetMainRect(); + } + + // Save last known viewport position within the window itself (so it can be saved in .ini file and restored) + window->ViewportPos = window->Viewport->Pos; + // SCROLLBAR VISIBILITY // Update scrollbar visibility (based on the Size that was effective during last frame or the auto-resized Size). if (!window->Collapsed) { // When reading the current size we need to read it after size constraints have been applied. - // When we use InnerRect here we are intentionally reading last frame size, same for ScrollbarSizes values before we set them again. - ImVec2 avail_size_from_current_frame = ImVec2(window->SizeFull.x, window->SizeFull.y - decoration_up_height); - ImVec2 avail_size_from_last_frame = window->InnerRect.GetSize() + window->ScrollbarSizes; + // Intentionally use previous frame values for InnerRect and ScrollbarSizes. + // And when we use window->DecorationUp here it doesn't have ScrollbarSizes.y applied yet. + ImVec2 avail_size_from_current_frame = ImVec2(window->SizeFull.x, window->SizeFull.y - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2)); + ImVec2 avail_size_from_last_frame = window->InnerRect.GetSize() + scrollbar_sizes_from_last_frame; ImVec2 needed_size_from_last_frame = window_just_created ? ImVec2(0, 0) : window->ContentSize + window->WindowPadding * 2.0f; float size_x_for_scrollbars = use_current_size_for_scrollbar_x ? avail_size_from_current_frame.x : avail_size_from_last_frame.x; float size_y_for_scrollbars = use_current_size_for_scrollbar_y ? avail_size_from_current_frame.y : avail_size_from_last_frame.y; + bool scrollbar_x_prev = window->ScrollbarX; //bool scrollbar_y_from_last_frame = window->ScrollbarY; // FIXME: May want to use that in the ScrollbarX expression? How many pros vs cons? window->ScrollbarY = (flags & ImGuiWindowFlags_AlwaysVerticalScrollbar) || ((needed_size_from_last_frame.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar)); window->ScrollbarX = (flags & ImGuiWindowFlags_AlwaysHorizontalScrollbar) || ((needed_size_from_last_frame.x > size_x_for_scrollbars - (window->ScrollbarY ? style.ScrollbarSize : 0.0f)) && !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar)); + + // Track when ScrollbarX visibility keeps toggling, which is a sign of a feedback loop, and stabilize by enforcing visibility (#3285, #8488) + // (Feedback loops of this sort can manifest in various situations, but combining horizontal + vertical scrollbar + using a clipper with varying width items is one frequent cause. + // The better solution is to, either (1) enforce visibility by using ImGuiWindowFlags_AlwaysHorizontalScrollbar or (2) declare stable contents width with SetNextWindowContentSize(), if you can compute it) + window->ScrollbarXStabilizeToggledHistory <<= 1; + window->ScrollbarXStabilizeToggledHistory |= (scrollbar_x_prev != window->ScrollbarX) ? 0x01 : 0x00; + const bool scrollbar_x_stabilize = (window->ScrollbarXStabilizeToggledHistory != 0) && ImCountSetBits(window->ScrollbarXStabilizeToggledHistory) >= 4; // 4 == half of bits in our U8 history. + if (scrollbar_x_stabilize) + window->ScrollbarX = true; + //if (scrollbar_x_stabilize && !window->ScrollbarXStabilizeEnabled) + // IMGUI_DEBUG_LOG("[scroll] Stabilize ScrollbarX for Window '%s'\n", window->Name); + window->ScrollbarXStabilizeEnabled = scrollbar_x_stabilize; + if (window->ScrollbarX && !window->ScrollbarY) - window->ScrollbarY = (needed_size_from_last_frame.y > size_y_for_scrollbars) && !(flags & ImGuiWindowFlags_NoScrollbar); + window->ScrollbarY = (needed_size_from_last_frame.y > size_y_for_scrollbars - style.ScrollbarSize) && !(flags & ImGuiWindowFlags_NoScrollbar); window->ScrollbarSizes = ImVec2(window->ScrollbarY ? style.ScrollbarSize : 0.0f, window->ScrollbarX ? style.ScrollbarSize : 0.0f); + + // Amend the partially filled window->DecorationXXX values. + window->DecoOuterSizeX2 += window->ScrollbarSizes.x; + window->DecoOuterSizeY2 += window->ScrollbarSizes.y; } // UPDATE RECTANGLES (1- THOSE NOT AFFECTED BY SCROLLING) - // Update various regions. Variables they depends on should be set above in this function. + // Update various regions. Variables they depend on should be set above in this function. // We set this up after processing the resize grip so that our rectangles doesn't lag by a frame. // Outer rectangle @@ -5744,38 +8001,46 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) const ImRect outer_rect = window->Rect(); const ImRect title_bar_rect = window->TitleBarRect(); window->OuterRectClipped = outer_rect; + if (window->DockIsActive) + window->OuterRectClipped.Min.y += window->TitleBarHeight; window->OuterRectClipped.ClipWith(host_rect); // Inner rectangle // Not affected by window border size. Used by: // - InnerClipRect - // - ScrollToBringRectIntoView() + // - ScrollToRectEx() // - NavUpdatePageUpPageDown() // - Scrollbar() - window->InnerRect.Min.x = window->Pos.x; - window->InnerRect.Min.y = window->Pos.y + decoration_up_height; - window->InnerRect.Max.x = window->Pos.x + window->Size.x - window->ScrollbarSizes.x; - window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->ScrollbarSizes.y; + window->InnerRect.Min.x = window->Pos.x + window->DecoOuterSizeX1; + window->InnerRect.Min.y = window->Pos.y + window->DecoOuterSizeY1; + window->InnerRect.Max.x = window->Pos.x + window->Size.x - window->DecoOuterSizeX2; + window->InnerRect.Max.y = window->Pos.y + window->Size.y - window->DecoOuterSizeY2; // Inner clipping rectangle. - // Will extend a little bit outside the normal work region. - // This is to allow e.g. Selectable or CollapsingHeader or some separators to cover that space. - // Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. + // - Extend a outside of normal work region up to borders. + // - This is to allow e.g. Selectable or CollapsingHeader or some separators to cover that space. + // - It also makes clipped items be more noticeable. + // - And is consistent on both axis (prior to 2024/05/03 ClipRect used WindowPadding.x * 0.5f on left and right edge), see #3312 + // - Force round operator last to ensure that e.g. (int)(max.x-min.x) in user's render code produce correct result. // Note that if our window is collapsed we will end up with an inverted (~null) clipping rectangle which is the correct behavior. // Affected by window/frame border size. Used by: // - Begin() initial clip rect float top_border_size = (((flags & ImGuiWindowFlags_MenuBar) || !(flags & ImGuiWindowFlags_NoTitleBar)) ? style.FrameBorderSize : window->WindowBorderSize); - window->InnerClipRect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + ImMax(ImFloor(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); - window->InnerClipRect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y + top_border_size); - window->InnerClipRect.Max.x = ImFloor(0.5f + window->InnerRect.Max.x - ImMax(ImFloor(window->WindowPadding.x * 0.5f), window->WindowBorderSize)); - window->InnerClipRect.Max.y = ImFloor(0.5f + window->InnerRect.Max.y - window->WindowBorderSize); + + // Try to match the fact that our border is drawn centered over the window rectangle, rather than inner. + // This is why we do a *0.5f here. We don't currently even technically support large values for WindowBorderSize, + // see e.g #7887 #7888, but may do after we move the window border to become an inner border (and then we can remove the 0.5f here). + window->InnerClipRect.Min.x = ImFloor(0.5f + window->InnerRect.Min.x + window->WindowBorderSize * 0.5f); + window->InnerClipRect.Min.y = ImFloor(0.5f + window->InnerRect.Min.y + top_border_size * 0.5f); + window->InnerClipRect.Max.x = ImFloor(window->InnerRect.Max.x - window->WindowBorderSize * 0.5f); + window->InnerClipRect.Max.y = ImFloor(window->InnerRect.Max.y - window->WindowBorderSize * 0.5f); window->InnerClipRect.ClipWithFull(host_rect); // Default item width. Make it proportional to window size if window manually resizes if (window->Size.x > 0.0f && !(flags & ImGuiWindowFlags_Tooltip) && !(flags & ImGuiWindowFlags_AlwaysAutoResize)) - window->ItemWidthDefault = (float)(int)(window->Size.x * 0.65f); + window->ItemWidthDefault = ImTrunc(window->Size.x * 0.65f); else - window->ItemWidthDefault = (float)(int)(g.FontSize * 16.0f); + window->ItemWidthDefault = ImTrunc(g.FontSize * 16.0f); // SCROLLING @@ -5786,66 +8051,44 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) window->ScrollMax.y = ImMax(0.0f, window->ContentSize.y + window->WindowPadding.y * 2.0f - window->InnerRect.GetHeight()); // Apply scrolling - window->Scroll = CalcNextScrollFromScrollTargetAndClamp(window, true); + window->Scroll = CalcNextScrollFromScrollTargetAndClamp(window); window->ScrollTarget = ImVec2(FLT_MAX, FLT_MAX); + window->DecoInnerSizeX1 = window->DecoInnerSizeY1 = 0.0f; // DRAWING // Setup draw list and outer clipping rectangle - window->DrawList->Clear(); + IM_ASSERT(window->DrawList->CmdBuffer.Size == 1 && window->DrawList->CmdBuffer[0].ElemCount == 0); window->DrawList->PushTextureID(g.Font->ContainerAtlas->TexID); PushClipRect(host_rect.Min, host_rect.Max, false); - // Draw modal window background (darkens what is behind them, all viewports) - const bool dim_bg_for_modal = (flags & ImGuiWindowFlags_Modal) && window == GetTopMostPopupModal() && window->HiddenFramesCannotSkipItems <= 0; - const bool dim_bg_for_window_list = g.NavWindowingTargetAnim && (window == g.NavWindowingTargetAnim->RootWindow); - if (dim_bg_for_modal || dim_bg_for_window_list) - { - const ImU32 dim_bg_col = GetColorU32(dim_bg_for_modal ? ImGuiCol_ModalWindowDimBg : ImGuiCol_NavWindowingDimBg, g.DimBgRatio); - window->DrawList->AddRectFilled(viewport_rect.Min, viewport_rect.Max, dim_bg_col); - } - - // Draw navigation selection/windowing rectangle background - if (dim_bg_for_window_list && window == g.NavWindowingTargetAnim) - { - ImRect bb = window->Rect(); - bb.Expand(g.FontSize); - if (!bb.Contains(viewport_rect)) // Avoid drawing if the window covers all the viewport anyway - window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha * 0.25f), g.Style.WindowRounding); - } - - // Since 1.71, child window can render their decoration (bg color, border, scrollbars, etc.) within their parent to save a draw call. + // Child windows can render their decoration (bg color, border, scrollbars, etc.) within their parent to save a draw call (since 1.71) // When using overlapping child windows, this will break the assumption that child z-order is mapped to submission order. - // We disable this when the parent window has zero vertices, which is a common pattern leading to laying out multiple overlapping child. - // We also disabled this when we have dimming overlay behind this specific one child. - // FIXME: More code may rely on explicit sorting of overlapping child window and would need to disable this somehow. Please get in contact if you are affected. - bool render_decorations_in_parent = false; - if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) - if (window->DrawList->CmdBuffer.back().ElemCount == 0 && parent_window->DrawList->VtxBuffer.Size > 0) - render_decorations_in_parent = true; - if (render_decorations_in_parent) - window->DrawList = parent_window->DrawList; - - // Handle title bar, scrollbar, resize grips and resize borders - const ImGuiWindow* window_to_highlight = g.NavWindowingTarget ? g.NavWindowingTarget : g.NavWindow; - const bool title_bar_is_highlight = want_focus || (window_to_highlight && window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight); - RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, resize_grip_count, resize_grip_col, resize_grip_draw_size); - - if (render_decorations_in_parent) - window->DrawList = &window->DrawListInst; - - // Draw navigation selection/windowing rectangle border - if (g.NavWindowingTargetAnim == window) + // FIXME: User code may rely on explicit sorting of overlapping child window and would need to disable this somehow. Please get in contact if you are affected (github #4493) + const bool is_undocked_or_docked_visible = !window->DockIsActive || window->DockTabIsVisible; + if (is_undocked_or_docked_visible) { - float rounding = ImMax(window->WindowRounding, g.Style.WindowRounding); - ImRect bb = window->Rect(); - bb.Expand(g.FontSize); - if (bb.Contains(viewport_rect)) // If a window fits the entire viewport, adjust its highlight inward + bool render_decorations_in_parent = false; + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_Popup) && !window_is_child_tooltip) { - bb.Expand(-g.FontSize - 1.0f); - rounding = window->WindowRounding; + // - We test overlap with the previous child window only (testing all would end up being O(log N) not a good investment here) + // - We disable this when the parent window has zero vertices, which is a common pattern leading to laying out multiple overlapping childs + ImGuiWindow* previous_child = parent_window->DC.ChildWindows.Size >= 2 ? parent_window->DC.ChildWindows[parent_window->DC.ChildWindows.Size - 2] : NULL; + bool previous_child_overlapping = previous_child ? previous_child->Rect().Overlaps(window->Rect()) : false; + bool parent_is_empty = (parent_window->DrawList->VtxBuffer.Size == 0); + if (window->DrawList->CmdBuffer.back().ElemCount == 0 && !parent_is_empty && !previous_child_overlapping) + render_decorations_in_parent = true; } - window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_NavWindowingHighlight, g.NavWindowingHighlightAlpha), rounding, ~0, 3.0f); + if (render_decorations_in_parent) + window->DrawList = parent_window->DrawList; + + // Handle title bar, scrollbar, resize grips and resize borders + const ImGuiWindow* window_to_highlight = g.NavWindowingTarget ? g.NavWindowingTarget : g.NavWindow; + const bool title_bar_is_highlight = want_focus || (window_to_highlight && (window->RootWindowForTitleBarHighlight == window_to_highlight->RootWindowForTitleBarHighlight || (window->DockNode && window->DockNode == window_to_highlight->DockNode))); + RenderWindowDecorations(window, title_bar_rect, title_bar_is_highlight, handle_borders_and_resize_grips, resize_grip_count, resize_grip_col, resize_grip_draw_size); + + if (render_decorations_in_parent) + window->DrawList = &window->DrawListInst; } // UPDATE RECTANGLES (2- THOSE AFFECTED BY SCROLLING) @@ -5857,379 +8100,320 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags) // - BeginTabBar() for right-most edge const bool allow_scrollbar_x = !(flags & ImGuiWindowFlags_NoScrollbar) && (flags & ImGuiWindowFlags_HorizontalScrollbar); const bool allow_scrollbar_y = !(flags & ImGuiWindowFlags_NoScrollbar); - const float work_rect_size_x = (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : ImMax(allow_scrollbar_x ? window->ContentSize.x : 0.0f, window->Size.x - window->WindowPadding.x * 2.0f - window->ScrollbarSizes.x)); - const float work_rect_size_y = (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : ImMax(allow_scrollbar_y ? window->ContentSize.y : 0.0f, window->Size.y - window->WindowPadding.y * 2.0f - decoration_up_height - window->ScrollbarSizes.y)); - window->WorkRect.Min.x = ImFloor(window->InnerRect.Min.x - window->Scroll.x + ImMax(window->WindowPadding.x, window->WindowBorderSize)); - window->WorkRect.Min.y = ImFloor(window->InnerRect.Min.y - window->Scroll.y + ImMax(window->WindowPadding.y, window->WindowBorderSize)); + const float work_rect_size_x = (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : ImMax(allow_scrollbar_x ? window->ContentSize.x : 0.0f, window->Size.x - window->WindowPadding.x * 2.0f - (window->DecoOuterSizeX1 + window->DecoOuterSizeX2))); + const float work_rect_size_y = (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : ImMax(allow_scrollbar_y ? window->ContentSize.y : 0.0f, window->Size.y - window->WindowPadding.y * 2.0f - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2))); + window->WorkRect.Min.x = ImTrunc(window->InnerRect.Min.x - window->Scroll.x + ImMax(window->WindowPadding.x, window->WindowBorderSize)); + window->WorkRect.Min.y = ImTrunc(window->InnerRect.Min.y - window->Scroll.y + ImMax(window->WindowPadding.y, window->WindowBorderSize)); window->WorkRect.Max.x = window->WorkRect.Min.x + work_rect_size_x; window->WorkRect.Max.y = window->WorkRect.Min.y + work_rect_size_y; + window->ParentWorkRect = window->WorkRect; - // [LEGACY] Contents Region - // FIXME-OBSOLETE: window->ContentsRegionRect.Max is currently very misleading / partly faulty, but some BeginChild() patterns relies on it. + // [LEGACY] Content Region + // FIXME-OBSOLETE: window->ContentRegionRect.Max is currently very misleading / partly faulty, but some BeginChild() patterns relies on it. + // Unless explicit content size is specified by user, this currently represent the region leading to no scrolling. // Used by: // - Mouse wheel scrolling + many other things - window->ContentsRegionRect.Min.x = window->Pos.x - window->Scroll.x + window->WindowPadding.x; - window->ContentsRegionRect.Min.y = window->Pos.y - window->Scroll.y + window->WindowPadding.y + decoration_up_height; - window->ContentsRegionRect.Max.x = window->ContentsRegionRect.Min.x + (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : (window->Size.x - window->WindowPadding.x * 2.0f - window->ScrollbarSizes.x)); - window->ContentsRegionRect.Max.y = window->ContentsRegionRect.Min.y + (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : (window->Size.y - window->WindowPadding.y * 2.0f - decoration_up_height - window->ScrollbarSizes.y)); + window->ContentRegionRect.Min.x = window->Pos.x - window->Scroll.x + window->WindowPadding.x + window->DecoOuterSizeX1; + window->ContentRegionRect.Min.y = window->Pos.y - window->Scroll.y + window->WindowPadding.y + window->DecoOuterSizeY1; + window->ContentRegionRect.Max.x = window->ContentRegionRect.Min.x + (window->ContentSizeExplicit.x != 0.0f ? window->ContentSizeExplicit.x : (window->Size.x - window->WindowPadding.x * 2.0f - (window->DecoOuterSizeX1 + window->DecoOuterSizeX2))); + window->ContentRegionRect.Max.y = window->ContentRegionRect.Min.y + (window->ContentSizeExplicit.y != 0.0f ? window->ContentSizeExplicit.y : (window->Size.y - window->WindowPadding.y * 2.0f - (window->DecoOuterSizeY1 + window->DecoOuterSizeY2))); // Setup drawing context // (NB: That term "drawing context / DC" lost its meaning a long time ago. Initially was meant to hold transient data only. Nowadays difference between window-> and window->DC-> is dubious.) - window->DC.Indent.x = 0.0f + window->WindowPadding.x - window->Scroll.x; + window->DC.Indent.x = window->DecoOuterSizeX1 + window->WindowPadding.x - window->Scroll.x; window->DC.GroupOffset.x = 0.0f; window->DC.ColumnsOffset.x = 0.0f; - window->DC.CursorStartPos = window->Pos + ImVec2(window->DC.Indent.x + window->DC.ColumnsOffset.x, decoration_up_height + window->WindowPadding.y - window->Scroll.y); + + // Record the loss of precision of CursorStartPos which can happen due to really large scrolling amount. + // This is used by clipper to compensate and fix the most common use case of large scroll area. Easy and cheap, next best thing compared to switching everything to double or ImU64. + double start_pos_highp_x = (double)window->Pos.x + window->WindowPadding.x - (double)window->Scroll.x + window->DecoOuterSizeX1 + window->DC.ColumnsOffset.x; + double start_pos_highp_y = (double)window->Pos.y + window->WindowPadding.y - (double)window->Scroll.y + window->DecoOuterSizeY1; + window->DC.CursorStartPos = ImVec2((float)start_pos_highp_x, (float)start_pos_highp_y); + window->DC.CursorStartPosLossyness = ImVec2((float)(start_pos_highp_x - window->DC.CursorStartPos.x), (float)(start_pos_highp_y - window->DC.CursorStartPos.y)); window->DC.CursorPos = window->DC.CursorStartPos; window->DC.CursorPosPrevLine = window->DC.CursorPos; window->DC.CursorMaxPos = window->DC.CursorStartPos; + window->DC.IdealMaxPos = window->DC.CursorStartPos; window->DC.CurrLineSize = window->DC.PrevLineSize = ImVec2(0.0f, 0.0f); window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset = 0.0f; + window->DC.IsSameLine = window->DC.IsSetPos = false; + + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + window->DC.NavLayersActiveMask = window->DC.NavLayersActiveMaskNext; + window->DC.NavLayersActiveMaskNext = 0x00; + window->DC.NavIsScrollPushableX = true; window->DC.NavHideHighlightOneFrame = false; - window->DC.NavHasScroll = (window->ScrollMax.y > 0.0f); - window->DC.NavLayerActiveMask = window->DC.NavLayerActiveMaskNext; - window->DC.NavLayerActiveMaskNext = 0x00; + window->DC.NavWindowHasScrollY = (window->ScrollMax.y > 0.0f); + window->DC.MenuBarAppending = false; + window->DC.MenuColumns.Update(style.ItemSpacing.x, window_just_activated_by_user); + window->DC.TreeDepth = 0; + window->DC.TreeHasStackDataDepthMask = 0x00; window->DC.ChildWindows.resize(0); + window->DC.StateStorage = &window->StateStorage; + window->DC.CurrentColumns = NULL; window->DC.LayoutType = ImGuiLayoutType_Vertical; window->DC.ParentLayoutType = parent_window ? parent_window->DC.LayoutType : ImGuiLayoutType_Vertical; - window->DC.FocusCounterAll = window->DC.FocusCounterTab = -1; - window->DC.ItemFlags = parent_window ? parent_window->DC.ItemFlags : ImGuiItemFlags_Default_; + window->DC.ItemWidth = window->ItemWidthDefault; window->DC.TextWrapPos = -1.0f; // disabled - window->DC.ItemFlagsStack.resize(0); window->DC.ItemWidthStack.resize(0); window->DC.TextWrapPosStack.resize(0); - window->DC.CurrentColumns = NULL; - window->DC.TreeDepth = 0; - window->DC.TreeMayJumpToParentOnPopMask = 0x00; - window->DC.StateStorage = &window->StateStorage; - window->DC.GroupStack.resize(0); - window->MenuColumns.Update(3, style.ItemSpacing.x, window_just_activated_by_user); - - if ((flags & ImGuiWindowFlags_ChildWindow) && (window->DC.ItemFlags != parent_window->DC.ItemFlags)) - { - window->DC.ItemFlags = parent_window->DC.ItemFlags; - window->DC.ItemFlagsStack.push_back(window->DC.ItemFlags); - } + if (flags & ImGuiWindowFlags_Modal) + window->DC.ModalDimBgColor = ColorConvertFloat4ToU32(GetStyleColorVec4(ImGuiCol_ModalWindowDimBg)); if (window->AutoFitFramesX > 0) window->AutoFitFramesX--; if (window->AutoFitFramesY > 0) window->AutoFitFramesY--; + // Clear SetNextWindowXXX data (can aim to move this higher in the function) + g.NextWindowData.ClearFlags(); + // Apply focus (we need to call FocusWindow() AFTER setting DC.CursorStartPos so our initial navigation reference rectangle can start around there) + // We ImGuiFocusRequestFlags_UnlessBelowModal to: + // - Avoid focusing a window that is created outside of a modal. This will prevent active modal from being closed. + // - Position window behind the modal that is not a begin-parent of this window. if (want_focus) + FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal); + if (want_focus && window == g.NavWindow) + NavInitWindow(window, false); // <-- this is in the way for us to be able to defer and sort reappearing FocusWindow() calls + + // Close requested by platform window (apply to all windows in this viewport) + if (p_open != NULL && window->Viewport->PlatformRequestClose && window->Viewport != GetMainViewport()) { - FocusWindow(window); - NavInitWindow(window, false); + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Window '%s' closed by PlatformRequestClose\n", window->Name); + *p_open = false; + g.NavWindowingToggleLayer = false; // Assume user mapped PlatformRequestClose on ALT-F4 so we disable ALT for menu toggle. False positive not an issue. // FIXME-NAV: Try removing. } - // Title bar - if (!(flags & ImGuiWindowFlags_NoTitleBar)) - RenderWindowTitleBarContents(window, title_bar_rect, name, p_open); + // Pressing CTRL+C copy window content into the clipboard + // [EXPERIMENTAL] Breaks on nested Begin/End pairs. We need to work that out and add better logging scope. + // [EXPERIMENTAL] Text outputs has many issues. + if (g.IO.ConfigWindowsCopyContentsWithCtrlC) + if (g.NavWindow && g.NavWindow->RootWindow == window && g.ActiveId == 0 && Shortcut(ImGuiMod_Ctrl | ImGuiKey_C)) + LogToClipboard(0); - // Pressing CTRL+C while holding on a window copy its content to the clipboard - // This works but 1. doesn't handle multiple Begin/End pairs, 2. recursing into another Begin/End pair - so we need to work that out and add better logging scope. - // Maybe we can support CTRL+C on every element? - /* - if (g.ActiveId == move_id) - if (g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_C)) - LogToClipboard(); - */ + // Title bar + if (!(flags & ImGuiWindowFlags_NoTitleBar) && !window->DockIsActive) + RenderWindowTitleBarContents(window, ImRect(title_bar_rect.Min.x + window->WindowBorderSize, title_bar_rect.Min.y, title_bar_rect.Max.x - window->WindowBorderSize, title_bar_rect.Max.y), name, p_open); + else if (!(flags & ImGuiWindowFlags_NoTitleBar) && window->DockIsActive) + LogText("%.*s\n", (int)(FindRenderedTextEnd(window->Name) - window->Name), window->Name); + + // Clear hit test shape every frame + window->HitTestHoleSize.x = window->HitTestHoleSize.y = 0; + + if (flags & ImGuiWindowFlags_Tooltip) + g.TooltipPreviousWindow = window; + + if (g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable) + { + // Docking: Dragging a dockable window (or any of its child) turns it into a drag and drop source. + // We need to do this _before_ we overwrite window->DC.LastItemId below because BeginDockableDragDropSource() also overwrites it. + if (g.MovingWindow == window && (window->RootWindowDockTree->Flags & ImGuiWindowFlags_NoDocking) == 0) + BeginDockableDragDropSource(window); + + // Docking: Any dockable window can act as a target. For dock node hosts we call BeginDockableDragDropTarget() in DockNodeUpdate() instead. + if (g.DragDropActive && !(flags & ImGuiWindowFlags_NoDocking)) + if (g.MovingWindow == NULL || g.MovingWindow->RootWindowDockTree != window) + if ((window == window->RootWindowDockTree) && !(window->Flags & ImGuiWindowFlags_DockNodeHost)) + BeginDockableDragDropTarget(window); + } // We fill last item data based on Title Bar/Tab, in order for IsItemHovered() and IsItemActive() to be usable after Begin(). // This is useful to allow creating context menus on title bar only, etc. - window->DC.LastItemId = window->MoveId; - window->DC.LastItemStatusFlags = IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0; - window->DC.LastItemRect = title_bar_rect; + window->DC.WindowItemStatusFlags = ImGuiItemStatusFlags_None; + window->DC.WindowItemStatusFlags |= IsMouseHoveringRect(title_bar_rect.Min, title_bar_rect.Max, false) ? ImGuiItemStatusFlags_HoveredRect : 0; + SetLastItemDataForWindow(window, title_bar_rect); + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.DebugLocateId != 0 && (window->ID == g.DebugLocateId || window->MoveId == g.DebugLocateId)) + DebugLocateItemResolveWithLastItem(); +#endif + + // [Test Engine] Register title bar / tab with MoveId. #ifdef IMGUI_ENABLE_TEST_ENGINE if (!(window->Flags & ImGuiWindowFlags_NoTitleBar)) - IMGUI_TEST_ENGINE_ITEM_ADD(window->DC.LastItemRect, window->DC.LastItemId); + { + window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + IMGUI_TEST_ENGINE_ITEM_ADD(g.LastItemData.ID, g.LastItemData.Rect, &g.LastItemData); + window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + } #endif } else { + // Skip refresh always mark active + if (window->SkipRefresh) + SetWindowActiveForSkipRefresh(window); + // Append + SetCurrentViewport(window, window->Viewport); SetCurrentWindow(window); + g.NextWindowData.ClearFlags(); + SetLastItemDataForWindow(window, window->TitleBarRect()); } - PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); + if (!(flags & ImGuiWindowFlags_DockNodeHost) && !window->SkipRefresh) + PushClipRect(window->InnerClipRect.Min, window->InnerClipRect.Max, true); // Clear 'accessed' flag last thing (After PushClipRect which will set the flag. We want the flag to stay false when the default "Debug" window is unused) - if (first_begin_of_the_frame) - window->WriteAccessed = false; - + window->WriteAccessed = false; window->BeginCount++; - g.NextWindowData.ClearFlags(); - if (flags & ImGuiWindowFlags_ChildWindow) + // Update visibility + if (first_begin_of_the_frame && !window->SkipRefresh) { - // Child window can be out of sight and have "negative" clip windows. - // Mark them as collapsed so commands are skipped earlier (we can't manually collapse them because they have no title bar). - IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0); - if (!(flags & ImGuiWindowFlags_AlwaysAutoResize) && window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0) - if (window->OuterRectClipped.Min.x >= window->OuterRectClipped.Max.x || window->OuterRectClipped.Min.y >= window->OuterRectClipped.Max.y) + // When we are about to select this tab (which will only be visible on the _next frame_), flag it with a non-zero HiddenFramesCannotSkipItems. + // This will have the important effect of actually returning true in Begin() and not setting SkipItems, allowing an earlier submission of the window contents. + // This is analogous to regular windows being hidden from one frame. + // It is especially important as e.g. nested TabBars would otherwise generate flicker in the form of one empty frame, or focus requests won't be processed. + if (window->DockIsActive && !window->DockTabIsVisible) + { + if (window->LastFrameJustFocused == g.FrameCount) + window->HiddenFramesCannotSkipItems = 1; + else window->HiddenFramesCanSkipItems = 1; + } - // Hide along with parent or if parent is collapsed - if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCanSkipItems > 0)) + if ((flags & ImGuiWindowFlags_ChildWindow) && !(flags & ImGuiWindowFlags_ChildMenu)) + { + // Child window can be out of sight and have "negative" clip windows. + // Mark them as collapsed so commands are skipped earlier (we can't manually collapse them because they have no title bar). + IM_ASSERT((flags & ImGuiWindowFlags_NoTitleBar) != 0 || window->DockIsActive); + const bool nav_request = (window->ChildFlags & ImGuiChildFlags_NavFlattened) && (g.NavAnyRequest && g.NavWindow && g.NavWindow->RootWindowForNav == window->RootWindowForNav); + if (!g.LogEnabled && !nav_request) + if (window->OuterRectClipped.Min.x >= window->OuterRectClipped.Max.x || window->OuterRectClipped.Min.y >= window->OuterRectClipped.Max.y) + { + if (window->AutoFitFramesX > 0 || window->AutoFitFramesY > 0) + window->HiddenFramesCannotSkipItems = 1; + else + window->HiddenFramesCanSkipItems = 1; + } + + // Hide along with parent or if parent is collapsed + if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCanSkipItems > 0)) + window->HiddenFramesCanSkipItems = 1; + if (parent_window && parent_window->HiddenFramesCannotSkipItems > 0) + window->HiddenFramesCannotSkipItems = 1; + } + + // Don't render if style alpha is 0.0 at the time of Begin(). This is arbitrary and inconsistent but has been there for a long while (may remove at some point) + if (style.Alpha <= 0.0f) window->HiddenFramesCanSkipItems = 1; - if (parent_window && (parent_window->Collapsed || parent_window->HiddenFramesCannotSkipItems > 0)) - window->HiddenFramesCannotSkipItems = 1; + + // Update the Hidden flag + bool hidden_regular = (window->HiddenFramesCanSkipItems > 0) || (window->HiddenFramesCannotSkipItems > 0); + window->Hidden = hidden_regular || (window->HiddenFramesForRenderOnly > 0); + + // Disable inputs for requested number of frames + if (window->DisableInputsFrames > 0) + { + window->DisableInputsFrames--; + window->Flags |= ImGuiWindowFlags_NoInputs; + } + + // Update the SkipItems flag, used to early out of all items functions (no layout required) + bool skip_items = false; + if (window->Collapsed || !window->Active || hidden_regular) + if (window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && window->HiddenFramesCannotSkipItems <= 0) + skip_items = true; + window->SkipItems = skip_items; + + // Restore NavLayersActiveMaskNext to previous value when not visible, so a CTRL+Tab back can use a safe value. + if (window->SkipItems) + window->DC.NavLayersActiveMaskNext = window->DC.NavLayersActiveMask; + + // Sanity check: there are two spots which can set Appearing = true + // - when 'window_just_activated_by_user' is set -> HiddenFramesCannotSkipItems is set -> SkipItems always false + // - in BeginDocked() path when DockNodeIsVisible == DockTabIsVisible == true -> hidden _should_ be all zero // FIXME: Not formally proven, hence the assert. + if (window->SkipItems && !window->Appearing) + IM_ASSERT(window->Appearing == false); // Please report on GitHub if this triggers: https://github.com/ocornut/imgui/issues/4177 + } + else if (first_begin_of_the_frame) + { + // Skip refresh mode + window->SkipItems = true; } - // Don't render if style alpha is 0.0 at the time of Begin(). This is arbitrary and inconsistent but has been there for a long while (may remove at some point) - if (style.Alpha <= 0.0f) - window->HiddenFramesCanSkipItems = 1; + // [DEBUG] io.ConfigDebugBeginReturnValue override return value to test Begin/End and BeginChild/EndChild behaviors. + // (The implicit fallback window is NOT automatically ended allowing it to always be able to receive commands without crashing) +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (!window->IsFallbackWindow) + if ((g.IO.ConfigDebugBeginReturnValueOnce && window_just_created) || (g.IO.ConfigDebugBeginReturnValueLoop && g.DebugBeginReturnValueCullDepth == g.CurrentWindowStack.Size)) + { + if (window->AutoFitFramesX > 0) { window->AutoFitFramesX++; } + if (window->AutoFitFramesY > 0) { window->AutoFitFramesY++; } + return false; + } +#endif - // Update the Hidden flag - window->Hidden = (window->HiddenFramesCanSkipItems > 0) || (window->HiddenFramesCannotSkipItems > 0); - - // Update the SkipItems flag, used to early out of all items functions (no layout required) - bool skip_items = false; - if (window->Collapsed || !window->Active || window->Hidden) - if (window->AutoFitFramesX <= 0 && window->AutoFitFramesY <= 0 && window->HiddenFramesCannotSkipItems <= 0) - skip_items = true; - window->SkipItems = skip_items; - - return !skip_items; + return !window->SkipItems; } -// Old Begin() API with 5 parameters, avoid calling this version directly! Use SetNextWindowSize()/SetNextWindowBgAlpha() + Begin() instead. -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS -bool ImGui::Begin(const char* name, bool* p_open, const ImVec2& size_first_use, float bg_alpha_override, ImGuiWindowFlags flags) -{ - // Old API feature: we could pass the initial window size as a parameter. This was misleading because it only had an effect if the window didn't have data in the .ini file. - if (size_first_use.x != 0.0f || size_first_use.y != 0.0f) - SetNextWindowSize(size_first_use, ImGuiCond_FirstUseEver); - - // Old API feature: override the window background alpha with a parameter. - if (bg_alpha_override >= 0.0f) - SetNextWindowBgAlpha(bg_alpha_override); - - return Begin(name, p_open, flags); -} -#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS - void ImGui::End() { ImGuiContext& g = *GImGui; - - if (g.CurrentWindowStack.Size <= 1 && g.FrameScopePushedImplicitWindow) - { - IM_ASSERT(g.CurrentWindowStack.Size > 1 && "Calling End() too many times!"); - return; // FIXME-ERRORHANDLING - } - IM_ASSERT(g.CurrentWindowStack.Size > 0); - ImGuiWindow* window = g.CurrentWindow; + // Error checking: verify that user hasn't called End() too many times! + if (g.CurrentWindowStack.Size <= 1 && g.WithinFrameScopeWithImplicitWindow) + { + IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size > 1, "Calling End() too many times!"); + return; + } + ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back(); + + // Error checking: verify that user doesn't directly call End() on a child window. + if ((window->Flags & ImGuiWindowFlags_ChildWindow) && !(window->Flags & ImGuiWindowFlags_DockNodeHost) && !window->DockIsActive) + IM_ASSERT_USER_ERROR(g.WithinEndChildID == window->ID, "Must call EndChild() and not End()!"); + + // Close anything that is open if (window->DC.CurrentColumns) EndColumns(); - PopClipRect(); // Inner window clip rectangle + if (!(window->Flags & ImGuiWindowFlags_DockNodeHost) && !window->SkipRefresh) // Pop inner window clip rectangle + PopClipRect(); + PopFocusScope(); + if (window_stack_data.DisabledOverrideReenable && window->RootWindow == window) + EndDisabledOverrideReenable(); + + if (window->SkipRefresh) + { + IM_ASSERT(window->DrawList == NULL); + window->DrawList = &window->DrawListInst; + } // Stop logging - if (!(window->Flags & ImGuiWindowFlags_ChildWindow)) // FIXME: add more options for scope of logging + if (g.LogWindow == window) // FIXME: add more options for scope of logging LogFinish(); + if (window->DC.IsSetPos) + ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + + // Docking: report contents sizes to parent to allow for auto-resize + if (window->DockNode && window->DockTabIsVisible) + if (ImGuiWindow* host_window = window->DockNode->HostWindow) // FIXME-DOCK + host_window->DC.CursorMaxPos = window->DC.CursorMaxPos + window->WindowPadding - host_window->WindowPadding; + // Pop from window stack - g.CurrentWindowStack.pop_back(); + g.LastItemData = window_stack_data.ParentLastItemDataBackup; + if (window->Flags & ImGuiWindowFlags_ChildMenu) + g.BeginMenuDepth--; if (window->Flags & ImGuiWindowFlags_Popup) g.BeginPopupStack.pop_back(); - CheckStacksSize(window, false); - SetCurrentWindow(g.CurrentWindowStack.empty() ? NULL : g.CurrentWindowStack.back()); -} - -void ImGui::BringWindowToFocusFront(ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - if (g.WindowsFocusOrder.back() == window) - return; - for (int i = g.WindowsFocusOrder.Size - 2; i >= 0; i--) // We can ignore the top-most window - if (g.WindowsFocusOrder[i] == window) - { - memmove(&g.WindowsFocusOrder[i], &g.WindowsFocusOrder[i + 1], (size_t)(g.WindowsFocusOrder.Size - i - 1) * sizeof(ImGuiWindow*)); - g.WindowsFocusOrder[g.WindowsFocusOrder.Size - 1] = window; - break; - } -} - -void ImGui::BringWindowToDisplayFront(ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* current_front_window = g.Windows.back(); - if (current_front_window == window || current_front_window->RootWindow == window) - return; - for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the top-most window - if (g.Windows[i] == window) - { - memmove(&g.Windows[i], &g.Windows[i + 1], (size_t)(g.Windows.Size - i - 1) * sizeof(ImGuiWindow*)); - g.Windows[g.Windows.Size - 1] = window; - break; - } -} - -void ImGui::BringWindowToDisplayBack(ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - if (g.Windows[0] == window) - return; - for (int i = 0; i < g.Windows.Size; i++) - if (g.Windows[i] == window) - { - memmove(&g.Windows[1], &g.Windows[0], (size_t)i * sizeof(ImGuiWindow*)); - g.Windows[0] = window; - break; - } -} - -// Moving window to front of display and set focus (which happens to be back of our sorted list) -void ImGui::FocusWindow(ImGuiWindow* window) -{ - ImGuiContext& g = *GImGui; - - if (g.NavWindow != window) - { - g.NavWindow = window; - if (window && g.NavDisableMouseHover) - g.NavMousePosDirty = true; - g.NavInitRequest = false; - g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId - g.NavIdIsAlive = false; - g.NavLayer = ImGuiNavLayer_Main; - //IMGUI_DEBUG_LOG("FocusWindow(\"%s\")\n", window ? window->Name : NULL); - } - - // Close popups if any - ClosePopupsOverWindow(window, false); - - // Passing NULL allow to disable keyboard focus - if (!window) - return; - - // Move the root window to the top of the pile - if (window->RootWindow) - window = window->RootWindow; - - // Steal focus on active widgets - if (window->Flags & ImGuiWindowFlags_Popup) // FIXME: This statement should be unnecessary. Need further testing before removing it.. - if (g.ActiveId != 0 && g.ActiveIdWindow && g.ActiveIdWindow->RootWindow != window) - ClearActiveID(); - - // Bring to front - BringWindowToFocusFront(window); - if (!(window->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus)) - BringWindowToDisplayFront(window); -} - -void ImGui::FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window) -{ - ImGuiContext& g = *GImGui; - - int start_idx = g.WindowsFocusOrder.Size - 1; - if (under_this_window != NULL) - { - int under_this_window_idx = FindWindowFocusIndex(under_this_window); - if (under_this_window_idx != -1) - start_idx = under_this_window_idx - 1; - } - for (int i = start_idx; i >= 0; i--) - { - // We may later decide to test for different NoXXXInputs based on the active navigation input (mouse vs nav) but that may feel more confusing to the user. - ImGuiWindow* window = g.WindowsFocusOrder[i]; - if (window != ignore_window && window->WasActive && !(window->Flags & ImGuiWindowFlags_ChildWindow)) - if ((window->Flags & (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) != (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) - { - ImGuiWindow* focus_window = NavRestoreLastChildNavWindow(window); - FocusWindow(focus_window); - return; - } - } - FocusWindow(NULL); -} - -void ImGui::SetNextItemWidth(float item_width) -{ - ImGuiContext& g = *GImGui; - g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasWidth; - g.NextItemData.Width = item_width; -} - -void ImGui::PushItemWidth(float item_width) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - window->DC.ItemWidth = (item_width == 0.0f ? window->ItemWidthDefault : item_width); - window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); - g.NextItemData.Flags &= ~ImGuiNextItemDataFlags_HasWidth; -} - -void ImGui::PushMultiItemsWidths(int components, float w_full) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - const ImGuiStyle& style = g.Style; - const float w_item_one = ImMax(1.0f, (float)(int)((w_full - (style.ItemInnerSpacing.x) * (components-1)) / (float)components)); - const float w_item_last = ImMax(1.0f, (float)(int)(w_full - (w_item_one + style.ItemInnerSpacing.x) * (components-1))); - window->DC.ItemWidthStack.push_back(w_item_last); - for (int i = 0; i < components-1; i++) - window->DC.ItemWidthStack.push_back(w_item_one); - window->DC.ItemWidth = window->DC.ItemWidthStack.back(); - g.NextItemData.Flags &= ~ImGuiNextItemDataFlags_HasWidth; -} - -void ImGui::PopItemWidth() -{ - ImGuiWindow* window = GetCurrentWindow(); - window->DC.ItemWidthStack.pop_back(); - window->DC.ItemWidth = window->DC.ItemWidthStack.empty() ? window->ItemWidthDefault : window->DC.ItemWidthStack.back(); -} - -// Calculate default item width given value passed to PushItemWidth() or SetNextItemWidth(). -// The SetNextItemWidth() data is generally cleared/consumed by ItemAdd() or NextItemData.ClearFlags() -float ImGui::CalcItemWidth() -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - float w; - if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasWidth) - w = g.NextItemData.Width; - else - w = window->DC.ItemWidth; - if (w < 0.0f) - { - float region_max_x = GetContentRegionMaxAbs().x; - w = ImMax(1.0f, region_max_x - window->DC.CursorPos.x + w); - } - w = (float)(int)w; - return w; -} - -// [Internal] Calculate full item size given user provided 'size' parameter and default width/height. Default width is often == CalcItemWidth(). -// Those two functions CalcItemWidth vs CalcItemSize are awkwardly named because they are not fully symmetrical. -// Note that only CalcItemWidth() is publicly exposed. -// The 4.0f here may be changed to match CalcItemWidth() and/or BeginChild() (right now we have a mismatch which is harmless but undesirable) -ImVec2 ImGui::CalcItemSize(ImVec2 size, float default_w, float default_h) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - - ImVec2 region_max; - if (size.x < 0.0f || size.y < 0.0f) - region_max = GetContentRegionMaxAbs(); - - if (size.x == 0.0f) - size.x = default_w; - else if (size.x < 0.0f) - size.x = ImMax(4.0f, region_max.x - window->DC.CursorPos.x + size.x); - - if (size.y == 0.0f) - size.y = default_h; - else if (size.y < 0.0f) - size.y = ImMax(4.0f, region_max.y - window->DC.CursorPos.y + size.y); - - return size; + + // Error handling, state recovery + if (g.IO.ConfigErrorRecovery) + ErrorRecoveryTryToRecoverWindowState(&window_stack_data.StackSizesInBegin); + + g.CurrentWindowStack.pop_back(); + SetCurrentWindow(g.CurrentWindowStack.Size == 0 ? NULL : g.CurrentWindowStack.back().Window); + if (g.CurrentWindow) + SetCurrentViewport(g.CurrentWindow, g.CurrentWindow->Viewport); } +// Important: this alone doesn't alter current ImDrawList state. This is called by PushFont/PopFont only. void ImGui::SetCurrentFont(ImFont* font) { ImGuiContext& g = *GImGui; @@ -6238,261 +8422,188 @@ void ImGui::SetCurrentFont(ImFont* font) g.Font = font; g.FontBaseSize = ImMax(1.0f, g.IO.FontGlobalScale * g.Font->FontSize * g.Font->Scale); g.FontSize = g.CurrentWindow ? g.CurrentWindow->CalcFontSize() : 0.0f; + g.FontScale = g.FontSize / g.Font->FontSize; ImFontAtlas* atlas = g.Font->ContainerAtlas; g.DrawListSharedData.TexUvWhitePixel = atlas->TexUvWhitePixel; + g.DrawListSharedData.TexUvLines = atlas->TexUvLines; g.DrawListSharedData.Font = g.Font; g.DrawListSharedData.FontSize = g.FontSize; + g.DrawListSharedData.FontScale = g.FontScale; } +// Use ImDrawList::_SetTextureID(), making our shared g.FontStack[] authorative against window-local ImDrawList. +// - Whereas ImDrawList::PushTextureID()/PopTextureID() is not to be used across Begin() calls. +// - Note that we don't propagate current texture id when e.g. Begin()-ing into a new window, we never really did... +// - Some code paths never really fully worked with multiple atlas textures. +// - The right-ish solution may be to remove _SetTextureID() and make AddText/RenderText lazily call PushTextureID()/PopTextureID() +// the same way AddImage() does, but then all other primitives would also need to? I don't think we should tackle this problem +// because we have a concrete need and a test bed for multiple atlas textures. void ImGui::PushFont(ImFont* font) { ImGuiContext& g = *GImGui; - if (!font) + if (font == NULL) font = GetDefaultFont(); - SetCurrentFont(font); g.FontStack.push_back(font); - g.CurrentWindow->DrawList->PushTextureID(font->ContainerAtlas->TexID); + SetCurrentFont(font); + g.CurrentWindow->DrawList->_SetTextureID(font->ContainerAtlas->TexID); } void ImGui::PopFont() { ImGuiContext& g = *GImGui; - g.CurrentWindow->DrawList->PopTextureID(); + if (g.FontStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopFont() too many times!"); + return; + } g.FontStack.pop_back(); - SetCurrentFont(g.FontStack.empty() ? GetDefaultFont() : g.FontStack.back()); + ImFont* font = g.FontStack.Size == 0 ? GetDefaultFont() : g.FontStack.back(); + SetCurrentFont(font); + g.CurrentWindow->DrawList->_SetTextureID(font->ContainerAtlas->TexID); } void ImGui::PushItemFlag(ImGuiItemFlags option, bool enabled) { - ImGuiWindow* window = GetCurrentWindow(); + ImGuiContext& g = *GImGui; + ImGuiItemFlags item_flags = g.CurrentItemFlags; + IM_ASSERT(item_flags == g.ItemFlagsStack.back()); if (enabled) - window->DC.ItemFlags |= option; + item_flags |= option; else - window->DC.ItemFlags &= ~option; - window->DC.ItemFlagsStack.push_back(window->DC.ItemFlags); + item_flags &= ~option; + g.CurrentItemFlags = item_flags; + g.ItemFlagsStack.push_back(item_flags); } void ImGui::PopItemFlag() { - ImGuiWindow* window = GetCurrentWindow(); - window->DC.ItemFlagsStack.pop_back(); - window->DC.ItemFlags = window->DC.ItemFlagsStack.empty() ? ImGuiItemFlags_Default_ : window->DC.ItemFlagsStack.back(); + ImGuiContext& g = *GImGui; + if (g.ItemFlagsStack.Size <= 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PopItemFlag() too many times!"); + return; + } + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); } -// FIXME: Look into renaming this once we have settled the new Focus/Activation/TabStop system. -void ImGui::PushAllowKeyboardFocus(bool allow_keyboard_focus) +// BeginDisabled()/EndDisabled() +// - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) +// - Visually this is currently altering alpha, but it is expected that in a future styling system this would work differently. +// - Feedback welcome at https://github.com/ocornut/imgui/issues/211 +// - BeginDisabled(false)/EndDisabled() essentially does nothing but is provided to facilitate use of boolean expressions. +// (as a micro-optimization: if you have tens of thousands of BeginDisabled(false)/EndDisabled() pairs, you might want to reformulate your code to avoid making those calls) +// - Note: mixing up BeginDisabled() and PushItemFlag(ImGuiItemFlags_Disabled) is currently NOT SUPPORTED. +void ImGui::BeginDisabled(bool disabled) { - PushItemFlag(ImGuiItemFlags_NoTabStop, !allow_keyboard_focus); + ImGuiContext& g = *GImGui; + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (!was_disabled && disabled) + { + g.DisabledAlphaBackup = g.Style.Alpha; + g.Style.Alpha *= g.Style.DisabledAlpha; // PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * g.Style.DisabledAlpha); + } + if (was_disabled || disabled) + g.CurrentItemFlags |= ImGuiItemFlags_Disabled; + g.ItemFlagsStack.push_back(g.CurrentItemFlags); // FIXME-OPT: can we simply skip this and use DisabledStackSize? + g.DisabledStackSize++; } -void ImGui::PopAllowKeyboardFocus() +void ImGui::EndDisabled() { - PopItemFlag(); + ImGuiContext& g = *GImGui; + if (g.DisabledStackSize <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling EndDisabled() too many times!"); + return; + } + g.DisabledStackSize--; + bool was_disabled = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + //PopItemFlag(); + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); + if (was_disabled && (g.CurrentItemFlags & ImGuiItemFlags_Disabled) == 0) + g.Style.Alpha = g.DisabledAlphaBackup; //PopStyleVar(); } -void ImGui::PushButtonRepeat(bool repeat) +// Could have been called BeginDisabledDisable() but it didn't want to be award nominated for most awkward function name. +// Ideally we would use a shared e.g. BeginDisabled()->BeginDisabledEx() but earlier needs to be optimal. +// The whole code for this is awkward, will reevaluate if we find a way to implement SetNextItemDisabled(). +void ImGui::BeginDisabledOverrideReenable() { - PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentItemFlags & ImGuiItemFlags_Disabled); + g.CurrentWindowStack.back().DisabledOverrideReenableAlphaBackup = g.Style.Alpha; + g.Style.Alpha = g.DisabledAlphaBackup; + g.CurrentItemFlags &= ~ImGuiItemFlags_Disabled; + g.ItemFlagsStack.push_back(g.CurrentItemFlags); + g.DisabledStackSize++; } -void ImGui::PopButtonRepeat() +void ImGui::EndDisabledOverrideReenable() { - PopItemFlag(); + ImGuiContext& g = *GImGui; + g.DisabledStackSize--; + IM_ASSERT(g.DisabledStackSize > 0); + g.ItemFlagsStack.pop_back(); + g.CurrentItemFlags = g.ItemFlagsStack.back(); + g.Style.Alpha = g.CurrentWindowStack.back().DisabledOverrideReenableAlphaBackup; } void ImGui::PushTextWrapPos(float wrap_pos_x) { - ImGuiWindow* window = GetCurrentWindow(); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.TextWrapPosStack.push_back(window->DC.TextWrapPos); window->DC.TextWrapPos = wrap_pos_x; - window->DC.TextWrapPosStack.push_back(wrap_pos_x); } void ImGui::PopTextWrapPos() { - ImGuiWindow* window = GetCurrentWindow(); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.TextWrapPosStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopTextWrapPos() too many times!"); + return; + } + window->DC.TextWrapPos = window->DC.TextWrapPosStack.back(); window->DC.TextWrapPosStack.pop_back(); - window->DC.TextWrapPos = window->DC.TextWrapPosStack.empty() ? -1.0f : window->DC.TextWrapPosStack.back(); } -// FIXME: This may incur a round-trip (if the end user got their data from a float4) but eventually we aim to store the in-flight colors as ImU32 -void ImGui::PushStyleColor(ImGuiCol idx, ImU32 col) +static ImGuiWindow* GetCombinedRootWindow(ImGuiWindow* window, bool popup_hierarchy, bool dock_hierarchy) { - ImGuiContext& g = *GImGui; - ImGuiColorMod backup; - backup.Col = idx; - backup.BackupValue = g.Style.Colors[idx]; - g.ColorModifiers.push_back(backup); - g.Style.Colors[idx] = ColorConvertU32ToFloat4(col); -} - -void ImGui::PushStyleColor(ImGuiCol idx, const ImVec4& col) -{ - ImGuiContext& g = *GImGui; - ImGuiColorMod backup; - backup.Col = idx; - backup.BackupValue = g.Style.Colors[idx]; - g.ColorModifiers.push_back(backup); - g.Style.Colors[idx] = col; -} - -void ImGui::PopStyleColor(int count) -{ - ImGuiContext& g = *GImGui; - while (count > 0) + ImGuiWindow* last_window = NULL; + while (last_window != window) { - ImGuiColorMod& backup = g.ColorModifiers.back(); - g.Style.Colors[backup.Col] = backup.BackupValue; - g.ColorModifiers.pop_back(); - count--; - } + last_window = window; + window = window->RootWindow; + if (popup_hierarchy) + window = window->RootWindowPopupTree; + if (dock_hierarchy) + window = window->RootWindowDockTree; + } + return window; } -struct ImGuiStyleVarInfo +bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy, bool dock_hierarchy) { - ImGuiDataType Type; - ImU32 Count; - ImU32 Offset; - void* GetVarPtr(ImGuiStyle* style) const { return (void*)((unsigned char*)style + Offset); } -}; - -static const ImGuiStyleVarInfo GStyleVarInfo[] = -{ - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, Alpha) }, // ImGuiStyleVar_Alpha - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowPadding) }, // ImGuiStyleVar_WindowPadding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowRounding) }, // ImGuiStyleVar_WindowRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowBorderSize) }, // ImGuiStyleVar_WindowBorderSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowMinSize) }, // ImGuiStyleVar_WindowMinSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, WindowTitleAlign) }, // ImGuiStyleVar_WindowTitleAlign - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ChildRounding) }, // ImGuiStyleVar_ChildRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ChildBorderSize) }, // ImGuiStyleVar_ChildBorderSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, PopupRounding) }, // ImGuiStyleVar_PopupRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, PopupBorderSize) }, // ImGuiStyleVar_PopupBorderSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, FramePadding) }, // ImGuiStyleVar_FramePadding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameRounding) }, // ImGuiStyleVar_FrameRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, FrameBorderSize) }, // ImGuiStyleVar_FrameBorderSize - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemSpacing) }, // ImGuiStyleVar_ItemSpacing - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ItemInnerSpacing) }, // ImGuiStyleVar_ItemInnerSpacing - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, IndentSpacing) }, // ImGuiStyleVar_IndentSpacing - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarSize) }, // ImGuiStyleVar_ScrollbarSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, ScrollbarRounding) }, // ImGuiStyleVar_ScrollbarRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabMinSize) }, // ImGuiStyleVar_GrabMinSize - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, GrabRounding) }, // ImGuiStyleVar_GrabRounding - { ImGuiDataType_Float, 1, (ImU32)IM_OFFSETOF(ImGuiStyle, TabRounding) }, // ImGuiStyleVar_TabRounding - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, ButtonTextAlign) }, // ImGuiStyleVar_ButtonTextAlign - { ImGuiDataType_Float, 2, (ImU32)IM_OFFSETOF(ImGuiStyle, SelectableTextAlign) }, // ImGuiStyleVar_SelectableTextAlign -}; - -static const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx) -{ - IM_ASSERT(idx >= 0 && idx < ImGuiStyleVar_COUNT); - IM_ASSERT(IM_ARRAYSIZE(GStyleVarInfo) == ImGuiStyleVar_COUNT); - return &GStyleVarInfo[idx]; -} - -void ImGui::PushStyleVar(ImGuiStyleVar idx, float val) -{ - const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); - if (var_info->Type == ImGuiDataType_Float && var_info->Count == 1) + ImGuiWindow* window_root = GetCombinedRootWindow(window, popup_hierarchy, dock_hierarchy); + if (window_root == potential_parent) + return true; + while (window != NULL) { - ImGuiContext& g = *GImGui; - float* pvar = (float*)var_info->GetVarPtr(&g.Style); - g.StyleModifiers.push_back(ImGuiStyleMod(idx, *pvar)); - *pvar = val; - return; + if (window == potential_parent) + return true; + if (window == window_root) // end of chain + return false; + window = window->ParentWindow; } - IM_ASSERT(0 && "Called PushStyleVar() float variant but variable is not a float!"); + return false; } -void ImGui::PushStyleVar(ImGuiStyleVar idx, const ImVec2& val) -{ - const ImGuiStyleVarInfo* var_info = GetStyleVarInfo(idx); - if (var_info->Type == ImGuiDataType_Float && var_info->Count == 2) - { - ImGuiContext& g = *GImGui; - ImVec2* pvar = (ImVec2*)var_info->GetVarPtr(&g.Style); - g.StyleModifiers.push_back(ImGuiStyleMod(idx, *pvar)); - *pvar = val; - return; - } - IM_ASSERT(0 && "Called PushStyleVar() ImVec2 variant but variable is not a ImVec2!"); -} - -void ImGui::PopStyleVar(int count) -{ - ImGuiContext& g = *GImGui; - while (count > 0) - { - // We avoid a generic memcpy(data, &backup.Backup.., GDataTypeSize[info->Type] * info->Count), the overhead in Debug is not worth it. - ImGuiStyleMod& backup = g.StyleModifiers.back(); - const ImGuiStyleVarInfo* info = GetStyleVarInfo(backup.VarIdx); - void* data = info->GetVarPtr(&g.Style); - if (info->Type == ImGuiDataType_Float && info->Count == 1) { ((float*)data)[0] = backup.BackupFloat[0]; } - else if (info->Type == ImGuiDataType_Float && info->Count == 2) { ((float*)data)[0] = backup.BackupFloat[0]; ((float*)data)[1] = backup.BackupFloat[1]; } - g.StyleModifiers.pop_back(); - count--; - } -} - -const char* ImGui::GetStyleColorName(ImGuiCol idx) -{ - // Create switch-case from enum with regexp: ImGuiCol_{.*}, --> case ImGuiCol_\1: return "\1"; - switch (idx) - { - case ImGuiCol_Text: return "Text"; - case ImGuiCol_TextDisabled: return "TextDisabled"; - case ImGuiCol_WindowBg: return "WindowBg"; - case ImGuiCol_ChildBg: return "ChildBg"; - case ImGuiCol_PopupBg: return "PopupBg"; - case ImGuiCol_Border: return "Border"; - case ImGuiCol_BorderShadow: return "BorderShadow"; - case ImGuiCol_FrameBg: return "FrameBg"; - case ImGuiCol_FrameBgHovered: return "FrameBgHovered"; - case ImGuiCol_FrameBgActive: return "FrameBgActive"; - case ImGuiCol_TitleBg: return "TitleBg"; - case ImGuiCol_TitleBgActive: return "TitleBgActive"; - case ImGuiCol_TitleBgCollapsed: return "TitleBgCollapsed"; - case ImGuiCol_MenuBarBg: return "MenuBarBg"; - case ImGuiCol_ScrollbarBg: return "ScrollbarBg"; - case ImGuiCol_ScrollbarGrab: return "ScrollbarGrab"; - case ImGuiCol_ScrollbarGrabHovered: return "ScrollbarGrabHovered"; - case ImGuiCol_ScrollbarGrabActive: return "ScrollbarGrabActive"; - case ImGuiCol_CheckMark: return "CheckMark"; - case ImGuiCol_SliderGrab: return "SliderGrab"; - case ImGuiCol_SliderGrabActive: return "SliderGrabActive"; - case ImGuiCol_Button: return "Button"; - case ImGuiCol_ButtonHovered: return "ButtonHovered"; - case ImGuiCol_ButtonActive: return "ButtonActive"; - case ImGuiCol_Header: return "Header"; - case ImGuiCol_HeaderHovered: return "HeaderHovered"; - case ImGuiCol_HeaderActive: return "HeaderActive"; - case ImGuiCol_Separator: return "Separator"; - case ImGuiCol_SeparatorHovered: return "SeparatorHovered"; - case ImGuiCol_SeparatorActive: return "SeparatorActive"; - case ImGuiCol_ResizeGrip: return "ResizeGrip"; - case ImGuiCol_ResizeGripHovered: return "ResizeGripHovered"; - case ImGuiCol_ResizeGripActive: return "ResizeGripActive"; - case ImGuiCol_Tab: return "Tab"; - case ImGuiCol_TabHovered: return "TabHovered"; - case ImGuiCol_TabActive: return "TabActive"; - case ImGuiCol_TabUnfocused: return "TabUnfocused"; - case ImGuiCol_TabUnfocusedActive: return "TabUnfocusedActive"; - case ImGuiCol_PlotLines: return "PlotLines"; - case ImGuiCol_PlotLinesHovered: return "PlotLinesHovered"; - case ImGuiCol_PlotHistogram: return "PlotHistogram"; - case ImGuiCol_PlotHistogramHovered: return "PlotHistogramHovered"; - case ImGuiCol_TextSelectedBg: return "TextSelectedBg"; - case ImGuiCol_DragDropTarget: return "DragDropTarget"; - case ImGuiCol_NavHighlight: return "NavHighlight"; - case ImGuiCol_NavWindowingHighlight: return "NavWindowingHighlight"; - case ImGuiCol_NavWindowingDimBg: return "NavWindowingDimBg"; - case ImGuiCol_ModalWindowDimBg: return "ModalWindowDimBg"; - } - IM_ASSERT(0); - return "Unknown"; -} - -bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent) +bool ImGui::IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent) { if (window->RootWindow == potential_parent) return true; @@ -6500,79 +8611,91 @@ bool ImGui::IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent) { if (window == potential_parent) return true; - window = window->ParentWindow; + window = window->ParentWindowInBeginStack; } return false; } -bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) +bool ImGui::IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below) { - IM_ASSERT((flags & ImGuiHoveredFlags_AllowWhenOverlapped) == 0); // Flags not supported by this function ImGuiContext& g = *GImGui; - if (flags & ImGuiHoveredFlags_AnyWindow) + // It would be saner to ensure that display layer is always reflected in the g.Windows[] order, which would likely requires altering all manipulations of that array + const int display_layer_delta = GetWindowDisplayLayer(potential_above) - GetWindowDisplayLayer(potential_below); + if (display_layer_delta != 0) + return display_layer_delta > 0; + + for (int i = g.Windows.Size - 1; i >= 0; i--) { - if (g.HoveredWindow == NULL) + ImGuiWindow* candidate_window = g.Windows[i]; + if (candidate_window == potential_above) + return true; + if (candidate_window == potential_below) return false; } - else + return false; +} + +// Is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags_ for options. +// IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, +// you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! +// Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. +bool ImGui::IsWindowHovered(ImGuiHoveredFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT_USER_ERROR((flags & ~ImGuiHoveredFlags_AllowedMaskForIsWindowHovered) == 0, "Invalid flags for IsWindowHovered()!"); + + ImGuiWindow* ref_window = g.HoveredWindow; + ImGuiWindow* cur_window = g.CurrentWindow; + if (ref_window == NULL) + return false; + + if ((flags & ImGuiHoveredFlags_AnyWindow) == 0) { - switch (flags & (ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows)) - { - case ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows: - if (g.HoveredRootWindow != g.CurrentWindow->RootWindow) - return false; - break; - case ImGuiHoveredFlags_RootWindow: - if (g.HoveredWindow != g.CurrentWindow->RootWindow) - return false; - break; - case ImGuiHoveredFlags_ChildWindows: - if (g.HoveredWindow == NULL || !IsWindowChildOf(g.HoveredWindow, g.CurrentWindow)) - return false; - break; - default: - if (g.HoveredWindow != g.CurrentWindow) - return false; - break; - } + IM_ASSERT(cur_window); // Not inside a Begin()/End() + const bool popup_hierarchy = (flags & ImGuiHoveredFlags_NoPopupHierarchy) == 0; + const bool dock_hierarchy = (flags & ImGuiHoveredFlags_DockHierarchy) != 0; + if (flags & ImGuiHoveredFlags_RootWindow) + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy, dock_hierarchy); + + bool result; + if (flags & ImGuiHoveredFlags_ChildWindows) + result = IsWindowChildOf(ref_window, cur_window, popup_hierarchy, dock_hierarchy); + else + result = (ref_window == cur_window); + if (!result) + return false; } - if (!IsWindowContentHoverable(g.HoveredWindow, flags)) + if (!IsWindowContentHoverable(ref_window, flags)) return false; if (!(flags & ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) - if (g.ActiveId != 0 && !g.ActiveIdAllowOverlap && g.ActiveId != g.HoveredWindow->MoveId) + if (g.ActiveId != 0 && !g.ActiveIdAllowOverlap && g.ActiveId != ref_window->MoveId) return false; + + // When changing hovered window we requires a bit of stationary delay before activating hover timer. + // FIXME: We don't support delay other than stationary one for now, other delay would need a way + // to fulfill the possibility that multiple IsWindowHovered() with varying flag could return true + // for different windows of the hierarchy. Possibly need a Hash(Current+Flags) ==> (Timer) cache. + // We can implement this for _Stationary because the data is linked to HoveredWindow rather than CurrentWindow. + if (flags & ImGuiHoveredFlags_ForTooltip) + flags = ApplyHoverFlagsForTooltip(flags, g.Style.HoverFlagsForTooltipMouse); + if ((flags & ImGuiHoveredFlags_Stationary) != 0 && g.HoverWindowUnlockedStationaryId != ref_window->ID) + return false; + return true; } -bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) +ImGuiID ImGui::GetWindowDockID() { ImGuiContext& g = *GImGui; - - if (flags & ImGuiFocusedFlags_AnyWindow) - return g.NavWindow != NULL; - - IM_ASSERT(g.CurrentWindow); // Not inside a Begin()/End() - switch (flags & (ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows)) - { - case ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows: - return g.NavWindow && g.NavWindow->RootWindow == g.CurrentWindow->RootWindow; - case ImGuiFocusedFlags_RootWindow: - return g.NavWindow == g.CurrentWindow->RootWindow; - case ImGuiFocusedFlags_ChildWindows: - return g.NavWindow && IsWindowChildOf(g.NavWindow, g.CurrentWindow); - default: - return g.NavWindow == g.CurrentWindow; - } + return g.CurrentWindow->DockId; } -// Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) -// Note that NoNavFocus makes the window not reachable with CTRL+TAB but it can still be focused with mouse or programmaticaly. -// If you want a window to never be focused, you may use the e.g. NoInputs flag. -bool ImGui::IsWindowNavFocusable(ImGuiWindow* window) +bool ImGui::IsWindowDocked() { - return window->Active && window == window->RootWindow && !(window->Flags & ImGuiWindowFlags_NoNavFocus); + ImGuiContext& g = *GImGui; + return g.CurrentWindow->DockIsActive; } float ImGui::GetWindowWidth() @@ -6606,10 +8729,15 @@ void ImGui::SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond) // Set const ImVec2 old_pos = window->Pos; - window->Pos = ImFloor(pos); + window->Pos = ImTrunc(pos); ImVec2 offset = window->Pos - old_pos; + if (offset.x == 0.0f && offset.y == 0.0f) + return; + MarkIniSettingsDirty(window); + // FIXME: share code with TranslateWindow(), need to confirm whether the 3 rect modified by TranslateWindow() are desirable here. window->DC.CursorPos += offset; // As we happen to move the window while it is being appended to (which is a bad idea - will smear) let's at least offset the cursor window->DC.CursorMaxPos += offset; // And more importantly we need to offset CursorMaxPos/CursorStartPos this so ContentSize calculation doesn't get affected. + window->DC.IdealMaxPos += offset; window->DC.CursorStartPos += offset; } @@ -6640,27 +8768,24 @@ void ImGui::SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond con IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. window->SetWindowSizeAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + // Enable auto-fit (not done in BeginChild() path unless appearing or combined with ImGuiChildFlags_AlwaysAutoResize) + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || window->Appearing || (window->ChildFlags & ImGuiChildFlags_AlwaysAutoResize) != 0) + window->AutoFitFramesX = (size.x <= 0.0f) ? 2 : 0; + if ((window->Flags & ImGuiWindowFlags_ChildWindow) == 0 || window->Appearing || (window->ChildFlags & ImGuiChildFlags_AlwaysAutoResize) != 0) + window->AutoFitFramesY = (size.y <= 0.0f) ? 2 : 0; + // Set - if (size.x > 0.0f) - { - window->AutoFitFramesX = 0; - window->SizeFull.x = ImFloor(size.x); - } - else - { - window->AutoFitFramesX = 2; + ImVec2 old_size = window->SizeFull; + if (size.x <= 0.0f) window->AutoFitOnlyGrows = false; - } - if (size.y > 0.0f) - { - window->AutoFitFramesY = 0; - window->SizeFull.y = ImFloor(size.y); - } else - { - window->AutoFitFramesY = 2; + window->SizeFull.x = IM_TRUNC(size.x); + if (size.y <= 0.0f) window->AutoFitOnlyGrows = false; - } + else + window->SizeFull.y = IM_TRUNC(size.y); + if (old_size.x != window->SizeFull.x || old_size.y != window->SizeFull.y) + MarkIniSettingsDirty(window); } void ImGui::SetWindowSize(const ImVec2& size, ImGuiCond cond) @@ -6685,6 +8810,19 @@ void ImGui::SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond co window->Collapsed = collapsed; } +void ImGui::SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size) +{ + IM_ASSERT(window->HitTestHoleSize.x == 0); // We don't support multiple holes/hit test filters + window->HitTestHoleSize = ImVec2ih(size); + window->HitTestHoleOffset = ImVec2ih(pos - window->Pos); +} + +void ImGui::SetWindowHiddenAndSkipItemsForCurrentFrame(ImGuiWindow* window) +{ + window->Hidden = window->SkipItems = true; + window->HiddenFramesCanSkipItems = 1; +} + void ImGui::SetWindowCollapsed(bool collapsed, ImGuiCond cond) { SetWindowCollapsed(GImGui->CurrentWindow, collapsed, cond); @@ -6708,47 +8846,34 @@ void ImGui::SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond) SetWindowCollapsed(window, collapsed, cond); } -void ImGui::SetWindowFocus() -{ - FocusWindow(GImGui->CurrentWindow); -} - -void ImGui::SetWindowFocus(const char* name) -{ - if (name) - { - if (ImGuiWindow* window = FindWindowByName(name)) - FocusWindow(window); - } - else - { - FocusWindow(NULL); - } -} - void ImGui::SetNextWindowPos(const ImVec2& pos, ImGuiCond cond, const ImVec2& pivot) { ImGuiContext& g = *GImGui; IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasPos; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasPos; g.NextWindowData.PosVal = pos; g.NextWindowData.PosPivotVal = pivot; g.NextWindowData.PosCond = cond ? cond : ImGuiCond_Always; + g.NextWindowData.PosUndock = true; } void ImGui::SetNextWindowSize(const ImVec2& size, ImGuiCond cond) { ImGuiContext& g = *GImGui; IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasSize; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasSize; g.NextWindowData.SizeVal = size; g.NextWindowData.SizeCond = cond ? cond : ImGuiCond_Always; } +// For each axis: +// - Use 0.0f as min or FLT_MAX as max if you don't want limits, e.g. size_min = (500.0f, 0.0f), size_max = (FLT_MAX, FLT_MAX) sets a minimum width. +// - Use -1 for both min and max of same axis to preserve current size which itself is a constraint. +// - See "Demo->Examples->Constrained-resizing window" for examples. void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback, void* custom_callback_user_data) { ImGuiContext& g = *GImGui; - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasSizeConstraint; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasSizeConstraint; g.NextWindowData.SizeConstraintRect = ImRect(size_min, size_max); g.NextWindowData.SizeCallback = custom_callback; g.NextWindowData.SizeCallbackUserData = custom_callback_user_data; @@ -6759,101 +8884,62 @@ void ImGui::SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& s void ImGui::SetNextWindowContentSize(const ImVec2& size) { ImGuiContext& g = *GImGui; - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasContentSize; - g.NextWindowData.ContentSizeVal = size; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasContentSize; + g.NextWindowData.ContentSizeVal = ImTrunc(size); +} + +void ImGui::SetNextWindowScroll(const ImVec2& scroll) +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasScroll; + g.NextWindowData.ScrollVal = scroll; } void ImGui::SetNextWindowCollapsed(bool collapsed, ImGuiCond cond) { ImGuiContext& g = *GImGui; IM_ASSERT(cond == 0 || ImIsPowerOfTwo(cond)); // Make sure the user doesn't attempt to combine multiple condition flags. - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasCollapsed; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasCollapsed; g.NextWindowData.CollapsedVal = collapsed; g.NextWindowData.CollapsedCond = cond ? cond : ImGuiCond_Always; } -void ImGui::SetNextWindowFocus() -{ - ImGuiContext& g = *GImGui; - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasFocus; -} - void ImGui::SetNextWindowBgAlpha(float alpha) { ImGuiContext& g = *GImGui; - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasBgAlpha; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasBgAlpha; g.NextWindowData.BgAlphaVal = alpha; } -// FIXME: This is in window space (not screen space!). We should try to obsolete all those functions. -ImVec2 ImGui::GetContentRegionMax() +void ImGui::SetNextWindowViewport(ImGuiID id) { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - ImVec2 mx = window->ContentsRegionRect.Max - window->Pos; - if (window->DC.CurrentColumns) - mx.x = window->WorkRect.Max.x - window->Pos.x; - return mx; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasViewport; + g.NextWindowData.ViewportId = id; } -// [Internal] Absolute coordinate. Saner. This is not exposed until we finishing refactoring work rect features. -ImVec2 ImGui::GetContentRegionMaxAbs() +void ImGui::SetNextWindowDockID(ImGuiID id, ImGuiCond cond) { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - ImVec2 mx = window->ContentsRegionRect.Max; - if (window->DC.CurrentColumns) - mx.x = window->WorkRect.Max.x; - return mx; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasDock; + g.NextWindowData.DockCond = cond ? cond : ImGuiCond_Always; + g.NextWindowData.DockId = id; } -ImVec2 ImGui::GetContentRegionAvail() -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return GetContentRegionMaxAbs() - window->DC.CursorPos; -} - -// In window space (not screen space!) -ImVec2 ImGui::GetWindowContentRegionMin() -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->ContentsRegionRect.Min - window->Pos; -} - -ImVec2 ImGui::GetWindowContentRegionMax() -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->ContentsRegionRect.Max - window->Pos; -} - -float ImGui::GetWindowContentRegionWidth() -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->ContentsRegionRect.GetWidth(); -} - -float ImGui::GetTextLineHeight() +void ImGui::SetNextWindowClass(const ImGuiWindowClass* window_class) { ImGuiContext& g = *GImGui; - return g.FontSize; + IM_ASSERT((window_class->ViewportFlagsOverrideSet & window_class->ViewportFlagsOverrideClear) == 0); // Cannot set both set and clear for the same bit + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasWindowClass; + g.NextWindowData.WindowClass = *window_class; } -float ImGui::GetTextLineHeightWithSpacing() +// This is experimental and meant to be a toy for exploring a future/wider range of features. +void ImGui::SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags) { ImGuiContext& g = *GImGui; - return g.FontSize + g.Style.ItemSpacing.y; -} - -float ImGui::GetFrameHeight() -{ - ImGuiContext& g = *GImGui; - return g.FontSize + g.Style.FramePadding.y * 2.0f; -} - -float ImGui::GetFrameHeightWithSpacing() -{ - ImGuiContext& g = *GImGui; - return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasRefreshPolicy; + g.NextWindowData.RefreshFlagsVal = flags; } ImDrawList* ImGui::GetWindowDrawList() @@ -6862,6 +8948,19 @@ ImDrawList* ImGui::GetWindowDrawList() return window->DrawList; } +float ImGui::GetWindowDpiScale() +{ + ImGuiContext& g = *GImGui; + return g.CurrentDpiScale; +} + +ImGuiViewport* ImGui::GetWindowViewport() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentViewport != NULL && g.CurrentViewport == g.CurrentWindow->Viewport); + return g.CurrentViewport; +} + ImFont* ImGui::GetFont() { return GImGui->Font; @@ -6879,10 +8978,2466 @@ ImVec2 ImGui::GetFontTexUvWhitePixel() void ImGui::SetWindowFontScale(float scale) { + IM_ASSERT(scale > 0.0f); ImGuiContext& g = *GImGui; ImGuiWindow* window = GetCurrentWindow(); window->FontWindowScale = scale; g.FontSize = g.DrawListSharedData.FontSize = window->CalcFontSize(); + g.FontScale = g.DrawListSharedData.FontScale = g.FontSize / g.Font->FontSize; +} + +void ImGui::PushFocusScope(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiFocusScopeData data; + data.ID = id; + data.WindowID = g.CurrentWindow->ID; + g.FocusScopeStack.push_back(data); + g.CurrentFocusScopeId = id; +} + +void ImGui::PopFocusScope() +{ + ImGuiContext& g = *GImGui; + if (g.FocusScopeStack.Size <= g.StackSizesInBeginForCurrentWindow->SizeOfFocusScopeStack) + { + IM_ASSERT_USER_ERROR(0, "Calling PopFocusScope() too many times!"); + return; + } + g.FocusScopeStack.pop_back(); + g.CurrentFocusScopeId = g.FocusScopeStack.Size ? g.FocusScopeStack.back().ID : 0; +} + +void ImGui::SetNavFocusScope(ImGuiID focus_scope_id) +{ + ImGuiContext& g = *GImGui; + g.NavFocusScopeId = focus_scope_id; + g.NavFocusRoute.resize(0); // Invalidate + if (focus_scope_id == 0) + return; + IM_ASSERT(g.NavWindow != NULL); + + // Store current path (in reverse order) + if (focus_scope_id == g.CurrentFocusScopeId) + { + // Top of focus stack contains local focus scopes inside current window + for (int n = g.FocusScopeStack.Size - 1; n >= 0 && g.FocusScopeStack.Data[n].WindowID == g.CurrentWindow->ID; n--) + g.NavFocusRoute.push_back(g.FocusScopeStack.Data[n]); + } + else if (focus_scope_id == g.NavWindow->NavRootFocusScopeId) + g.NavFocusRoute.push_back({ focus_scope_id, g.NavWindow->ID }); + else + return; + + // Then follow on manually set ParentWindowForFocusRoute field (#6798) + for (ImGuiWindow* window = g.NavWindow->ParentWindowForFocusRoute; window != NULL; window = window->ParentWindowForFocusRoute) + g.NavFocusRoute.push_back({ window->NavRootFocusScopeId, window->ID }); + IM_ASSERT(g.NavFocusRoute.Size < 100); // Maximum depth is technically 251 as per CalcRoutingScore(): 254 - 3 +} + +// Focus = move navigation cursor, set scrolling, set focus window. +void ImGui::FocusItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IMGUI_DEBUG_LOG_FOCUS("FocusItem(0x%08x) in window \"%s\"\n", g.LastItemData.ID, window->Name); + if (g.DragDropActive || g.MovingWindow != NULL) // FIXME: Opt-in flags for this? + { + IMGUI_DEBUG_LOG_FOCUS("FocusItem() ignored while DragDropActive!\n"); + return; + } + + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_FocusApi | ImGuiNavMoveFlags_NoSetNavCursorVisible | ImGuiNavMoveFlags_NoSelect; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + SetNavWindow(window); + NavMoveRequestSubmit(ImGuiDir_None, ImGuiDir_Up, move_flags, scroll_flags); + NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); +} + +void ImGui::ActivateItemByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavNextActivateId = id; + g.NavNextActivateFlags = ImGuiActivateFlags_None; +} + +// Note: this will likely be called ActivateItem() once we rework our Focus/Activation system! +// But ActivateItem() should function without altering scroll/focus? +void ImGui::SetKeyboardFocusHere(int offset) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(offset >= -1); // -1 is allowed but not below + IMGUI_DEBUG_LOG_FOCUS("SetKeyboardFocusHere(%d) in window \"%s\"\n", offset, window->Name); + + // It makes sense in the vast majority of cases to never interrupt a drag and drop. + // When we refactor this function into ActivateItem() we may want to make this an option. + // MovingWindow is protected from most user inputs using SetActiveIdUsingNavAndKeys(), but + // is also automatically dropped in the event g.ActiveId is stolen. + if (g.DragDropActive || g.MovingWindow != NULL) + { + IMGUI_DEBUG_LOG_FOCUS("SetKeyboardFocusHere() ignored while DragDropActive!\n"); + return; + } + + SetNavWindow(window); + + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_Activate | ImGuiNavMoveFlags_FocusApi | ImGuiNavMoveFlags_NoSetNavCursorVisible; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + NavMoveRequestSubmit(ImGuiDir_None, offset < 0 ? ImGuiDir_Up : ImGuiDir_Down, move_flags, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + if (offset == -1) + { + NavMoveRequestResolveWithLastItem(&g.NavMoveResultLocal); + } + else + { + g.NavTabbingDir = 1; + g.NavTabbingCounter = offset + 1; + } +} + +void ImGui::SetItemDefaultFocus() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (!window->Appearing) + return; + if (g.NavWindow != window->RootWindowForNav || (!g.NavInitRequest && g.NavInitResult.ID == 0) || g.NavLayer != window->DC.NavLayerCurrent) + return; + + g.NavInitRequest = false; + NavApplyItemToResult(&g.NavInitResult); + NavUpdateAnyRequestFlag(); + + // Scroll could be done in NavInitRequestApplyResult() via an opt-in flag (we however don't want regular init requests to scroll) + if (!window->ClipRect.Contains(g.LastItemData.Rect)) + ScrollToRectEx(window, g.LastItemData.Rect, ImGuiScrollFlags_None); +} + +void ImGui::SetStateStorage(ImGuiStorage* tree) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + window->DC.StateStorage = tree ? tree : &window->StateStorage; +} + +ImGuiStorage* ImGui::GetStateStorage() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->DC.StateStorage; +} + +bool ImGui::IsRectVisible(const ImVec2& size) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); +} + +bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); +} + +//----------------------------------------------------------------------------- +// [SECTION] ID STACK +//----------------------------------------------------------------------------- + +// This is one of the very rare legacy case where we use ImGuiWindow methods, +// it should ideally be flattened at some point but it's been used a lots by widgets. +IM_MSVC_RUNTIME_CHECKS_OFF +ImGuiID ImGuiWindow::GetID(const char* str, const char* str_end) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); +#endif + return id; +} + +ImGuiID ImGuiWindow::GetID(const void* ptr) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&ptr, sizeof(void*), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_Pointer, ptr, NULL); +#endif + return id; +} + +ImGuiID ImGuiWindow::GetID(int n) +{ + ImGuiID seed = IDStack.back(); + ImGuiID id = ImHashData(&n, sizeof(n), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *Ctx; + if (g.DebugHookIdInfo == id) + ImGui::DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); +#endif + return id; +} + +// This is only used in rare/specific situations to manufacture an ID out of nowhere. +// FIXME: Consider instead storing last non-zero ID + count of successive zero-ID, and combine those? +ImGuiID ImGuiWindow::GetIDFromPos(const ImVec2& p_abs) +{ + ImGuiID seed = IDStack.back(); + ImVec2 p_rel = ImGui::WindowPosAbsToRel(this, p_abs); + ImGuiID id = ImHashData(&p_rel, sizeof(p_rel), seed); + return id; +} + +// " +ImGuiID ImGuiWindow::GetIDFromRectangle(const ImRect& r_abs) +{ + ImGuiID seed = IDStack.back(); + ImRect r_rel = ImGui::WindowRectAbsToRel(this, r_abs); + ImGuiID id = ImHashData(&r_rel, sizeof(r_rel), seed); + return id; +} + +void ImGui::PushID(const char* str_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(str_id); + window->IDStack.push_back(id); +} + +void ImGui::PushID(const char* str_id_begin, const char* str_id_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(str_id_begin, str_id_end); + window->IDStack.push_back(id); +} + +void ImGui::PushID(const void* ptr_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(ptr_id); + window->IDStack.push_back(id); +} + +void ImGui::PushID(int int_id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetID(int_id); + window->IDStack.push_back(id); +} + +// Push a given id value ignoring the ID stack as a seed. +void ImGui::PushOverrideID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_ID, NULL, NULL); +#endif + window->IDStack.push_back(id); +} + +// Helper to avoid a common series of PushOverrideID -> GetID() -> PopID() call +// (note that when using this pattern, ID Stack Tool will tend to not display the intermediate stack level. +// for that to work we would need to do PushOverrideID() -> ItemAdd() -> PopID() which would alter widget code a little more) +ImGuiID ImGui::GetIDWithSeed(const char* str, const char* str_end, ImGuiID seed) +{ + ImGuiID id = ImHashStr(str, str_end ? (str_end - str) : 0, seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_String, str, str_end); +#endif + return id; +} + +ImGuiID ImGui::GetIDWithSeed(int n, ImGuiID seed) +{ + ImGuiID id = ImHashData(&n, sizeof(n), seed); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugHookIdInfo == id) + DebugHookIdInfo(id, ImGuiDataType_S32, (void*)(intptr_t)n, NULL); +#endif + return id; +} + +void ImGui::PopID() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + if (window->IDStack.Size <= 1) + { + IM_ASSERT_USER_ERROR(0, "Calling PopID() too many times!"); + return; + } + window->IDStack.pop_back(); +} + +ImGuiID ImGui::GetID(const char* str_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(str_id); +} + +ImGuiID ImGui::GetID(const char* str_id_begin, const char* str_id_end) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(str_id_begin, str_id_end); +} + +ImGuiID ImGui::GetID(const void* ptr_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(ptr_id); +} + +ImGuiID ImGui::GetID(int int_id) +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->GetID(int_id); +} +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] INPUTS +//----------------------------------------------------------------------------- +// - GetModForLRModKey() [Internal] +// - FixupKeyChord() [Internal] +// - GetKeyData() [Internal] +// - GetKeyIndex() [Internal] +// - GetKeyName() +// - GetKeyChordName() [Internal] +// - CalcTypematicRepeatAmount() [Internal] +// - GetTypematicRepeatRate() [Internal] +// - GetKeyPressedAmount() [Internal] +// - GetKeyMagnitude2d() [Internal] +//----------------------------------------------------------------------------- +// - UpdateKeyRoutingTable() [Internal] +// - GetRoutingIdFromOwnerId() [Internal] +// - GetShortcutRoutingData() [Internal] +// - CalcRoutingScore() [Internal] +// - SetShortcutRouting() [Internal] +// - TestShortcutRouting() [Internal] +//----------------------------------------------------------------------------- +// - IsKeyDown() +// - IsKeyPressed() +// - IsKeyReleased() +//----------------------------------------------------------------------------- +// - IsMouseDown() +// - IsMouseClicked() +// - IsMouseReleased() +// - IsMouseDoubleClicked() +// - GetMouseClickedCount() +// - IsMouseHoveringRect() [Internal] +// - IsMouseDragPastThreshold() [Internal] +// - IsMouseDragging() +// - GetMousePos() +// - SetMousePos() [Internal] +// - GetMousePosOnOpeningCurrentPopup() +// - IsMousePosValid() +// - IsAnyMouseDown() +// - GetMouseDragDelta() +// - ResetMouseDragDelta() +// - GetMouseCursor() +// - SetMouseCursor() +//----------------------------------------------------------------------------- +// - UpdateAliasKey() +// - GetMergedModsFromKeys() +// - UpdateKeyboardInputs() +// - UpdateMouseInputs() +//----------------------------------------------------------------------------- +// - LockWheelingWindow [Internal] +// - FindBestWheelingWindow [Internal] +// - UpdateMouseWheel() [Internal] +//----------------------------------------------------------------------------- +// - SetNextFrameWantCaptureKeyboard() +// - SetNextFrameWantCaptureMouse() +//----------------------------------------------------------------------------- +// - GetInputSourceName() [Internal] +// - DebugPrintInputEvent() [Internal] +// - UpdateInputEvents() [Internal] +//----------------------------------------------------------------------------- +// - GetKeyOwner() [Internal] +// - TestKeyOwner() [Internal] +// - SetKeyOwner() [Internal] +// - SetItemKeyOwner() [Internal] +// - Shortcut() [Internal] +//----------------------------------------------------------------------------- + +static ImGuiKeyChord GetModForLRModKey(ImGuiKey key) +{ + if (key == ImGuiKey_LeftCtrl || key == ImGuiKey_RightCtrl) + return ImGuiMod_Ctrl; + if (key == ImGuiKey_LeftShift || key == ImGuiKey_RightShift) + return ImGuiMod_Shift; + if (key == ImGuiKey_LeftAlt || key == ImGuiKey_RightAlt) + return ImGuiMod_Alt; + if (key == ImGuiKey_LeftSuper || key == ImGuiKey_RightSuper) + return ImGuiMod_Super; + return ImGuiMod_None; +} + +ImGuiKeyChord ImGui::FixupKeyChord(ImGuiKeyChord key_chord) +{ + // Add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified. + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (IsLRModKey(key)) + key_chord |= GetModForLRModKey(key); + return key_chord; +} + +ImGuiKeyData* ImGui::GetKeyData(ImGuiContext* ctx, ImGuiKey key) +{ + ImGuiContext& g = *ctx; + + // Special storage location for mods + if (key & ImGuiMod_Mask_) + key = ConvertSingleModFlagToKey(key); + + IM_ASSERT(IsNamedKey(key) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend & user code."); + return &g.IO.KeysData[key - ImGuiKey_NamedKey_BEGIN]; +} + +// Those names are provided for debugging purpose and are not meant to be saved persistently nor compared. +static const char* const GKeyNames[] = +{ + "Tab", "LeftArrow", "RightArrow", "UpArrow", "DownArrow", "PageUp", "PageDown", + "Home", "End", "Insert", "Delete", "Backspace", "Space", "Enter", "Escape", + "LeftCtrl", "LeftShift", "LeftAlt", "LeftSuper", "RightCtrl", "RightShift", "RightAlt", "RightSuper", "Menu", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F", "G", "H", + "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12", + "F13", "F14", "F15", "F16", "F17", "F18", "F19", "F20", "F21", "F22", "F23", "F24", + "Apostrophe", "Comma", "Minus", "Period", "Slash", "Semicolon", "Equal", "LeftBracket", + "Backslash", "RightBracket", "GraveAccent", "CapsLock", "ScrollLock", "NumLock", "PrintScreen", + "Pause", "Keypad0", "Keypad1", "Keypad2", "Keypad3", "Keypad4", "Keypad5", "Keypad6", + "Keypad7", "Keypad8", "Keypad9", "KeypadDecimal", "KeypadDivide", "KeypadMultiply", + "KeypadSubtract", "KeypadAdd", "KeypadEnter", "KeypadEqual", + "AppBack", "AppForward", "Oem102", + "GamepadStart", "GamepadBack", + "GamepadFaceLeft", "GamepadFaceRight", "GamepadFaceUp", "GamepadFaceDown", + "GamepadDpadLeft", "GamepadDpadRight", "GamepadDpadUp", "GamepadDpadDown", + "GamepadL1", "GamepadR1", "GamepadL2", "GamepadR2", "GamepadL3", "GamepadR3", + "GamepadLStickLeft", "GamepadLStickRight", "GamepadLStickUp", "GamepadLStickDown", + "GamepadRStickLeft", "GamepadRStickRight", "GamepadRStickUp", "GamepadRStickDown", + "MouseLeft", "MouseRight", "MouseMiddle", "MouseX1", "MouseX2", "MouseWheelX", "MouseWheelY", + "ModCtrl", "ModShift", "ModAlt", "ModSuper", // ReservedForModXXX are showing the ModXXX names. +}; +IM_STATIC_ASSERT(ImGuiKey_NamedKey_COUNT == IM_ARRAYSIZE(GKeyNames)); + +const char* ImGui::GetKeyName(ImGuiKey key) +{ + if (key == ImGuiKey_None) + return "None"; + IM_ASSERT(IsNamedKeyOrMod(key) && "Support for user key indices was dropped in favor of ImGuiKey. Please update backend and user code."); + if (key & ImGuiMod_Mask_) + key = ConvertSingleModFlagToKey(key); + if (!IsNamedKey(key)) + return "Unknown"; + + return GKeyNames[key - ImGuiKey_NamedKey_BEGIN]; +} + +// Return untranslated names: on macOS, Cmd key will show as Ctrl, Ctrl key will show as super. +// Lifetime of return value: valid until next call to same function. +const char* ImGui::GetKeyChordName(ImGuiKeyChord key_chord) +{ + ImGuiContext& g = *GImGui; + + const ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (IsLRModKey(key)) + key_chord &= ~GetModForLRModKey(key); // Return "Ctrl+LeftShift" instead of "Ctrl+Shift+LeftShift" + ImFormatString(g.TempKeychordName, IM_ARRAYSIZE(g.TempKeychordName), "%s%s%s%s%s", + (key_chord & ImGuiMod_Ctrl) ? "Ctrl+" : "", + (key_chord & ImGuiMod_Shift) ? "Shift+" : "", + (key_chord & ImGuiMod_Alt) ? "Alt+" : "", + (key_chord & ImGuiMod_Super) ? "Super+" : "", + (key != ImGuiKey_None || key_chord == ImGuiKey_None) ? GetKeyName(key) : ""); + size_t len; + if (key == ImGuiKey_None && key_chord != 0) + if ((len = ImStrlen(g.TempKeychordName)) != 0) // Remove trailing '+' + g.TempKeychordName[len - 1] = 0; + return g.TempKeychordName; +} + +// t0 = previous time (e.g.: g.Time - g.IO.DeltaTime) +// t1 = current time (e.g.: g.Time) +// An event is triggered at: +// t = 0.0f t = repeat_delay, t = repeat_delay + repeat_rate*N +int ImGui::CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate) +{ + if (t1 == 0.0f) + return 1; + if (t0 >= t1) + return 0; + if (repeat_rate <= 0.0f) + return (t0 < repeat_delay) && (t1 >= repeat_delay); + const int count_t0 = (t0 < repeat_delay) ? -1 : (int)((t0 - repeat_delay) / repeat_rate); + const int count_t1 = (t1 < repeat_delay) ? -1 : (int)((t1 - repeat_delay) / repeat_rate); + const int count = count_t1 - count_t0; + return count; +} + +void ImGui::GetTypematicRepeatRate(ImGuiInputFlags flags, float* repeat_delay, float* repeat_rate) +{ + ImGuiContext& g = *GImGui; + switch (flags & ImGuiInputFlags_RepeatRateMask_) + { + case ImGuiInputFlags_RepeatRateNavMove: *repeat_delay = g.IO.KeyRepeatDelay * 0.72f; *repeat_rate = g.IO.KeyRepeatRate * 0.80f; return; + case ImGuiInputFlags_RepeatRateNavTweak: *repeat_delay = g.IO.KeyRepeatDelay * 0.72f; *repeat_rate = g.IO.KeyRepeatRate * 0.30f; return; + case ImGuiInputFlags_RepeatRateDefault: default: *repeat_delay = g.IO.KeyRepeatDelay * 1.00f; *repeat_rate = g.IO.KeyRepeatRate * 1.00f; return; + } +} + +// Return value representing the number of presses in the last time period, for the given repeat rate +// (most often returns 0 or 1. The result is generally only >1 when RepeatRate is smaller than DeltaTime, aka large DeltaTime or fast RepeatRate) +int ImGui::GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float repeat_rate) +{ + ImGuiContext& g = *GImGui; + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return 0; + const float t = key_data->DownDuration; + return CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, repeat_delay, repeat_rate); +} + +// Return 2D vector representing the combination of four cardinal direction, with analog value support (for e.g. ImGuiKey_GamepadLStick* values). +ImVec2 ImGui::GetKeyMagnitude2d(ImGuiKey key_left, ImGuiKey key_right, ImGuiKey key_up, ImGuiKey key_down) +{ + return ImVec2( + GetKeyData(key_right)->AnalogValue - GetKeyData(key_left)->AnalogValue, + GetKeyData(key_down)->AnalogValue - GetKeyData(key_up)->AnalogValue); +} + +// Rewrite routing data buffers to strip old entries + sort by key to make queries not touch scattered data. +// Entries D,A,B,B,A,C,B --> A,A,B,B,B,C,D +// Index A:1 B:2 C:5 D:0 --> A:0 B:2 C:5 D:6 +// See 'Metrics->Key Owners & Shortcut Routing' to visualize the result of that operation. +static void ImGui::UpdateKeyRoutingTable(ImGuiKeyRoutingTable* rt) +{ + ImGuiContext& g = *GImGui; + rt->EntriesNext.resize(0); + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + const int new_routing_start_idx = rt->EntriesNext.Size; + ImGuiKeyRoutingData* routing_entry; + for (int old_routing_idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; old_routing_idx != -1; old_routing_idx = routing_entry->NextEntryIndex) + { + routing_entry = &rt->Entries[old_routing_idx]; + routing_entry->RoutingCurrScore = routing_entry->RoutingNextScore; + routing_entry->RoutingCurr = routing_entry->RoutingNext; // Update entry + routing_entry->RoutingNext = ImGuiKeyOwner_NoOwner; + routing_entry->RoutingNextScore = 255; + if (routing_entry->RoutingCurr == ImGuiKeyOwner_NoOwner) + continue; + rt->EntriesNext.push_back(*routing_entry); // Write alive ones into new buffer + + // Apply routing to owner if there's no owner already (RoutingCurr == None at this point) + // This is the result of previous frame's SetShortcutRouting() call. + if (routing_entry->Mods == g.IO.KeyMods) + { + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_data->OwnerCurr == ImGuiKeyOwner_NoOwner) + { + owner_data->OwnerCurr = routing_entry->RoutingCurr; + //IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X) via Routing\n", GetKeyName(key), routing_entry->RoutingCurr); + } + } + } + + // Rewrite linked-list + rt->Index[key - ImGuiKey_NamedKey_BEGIN] = (ImGuiKeyRoutingIndex)(new_routing_start_idx < rt->EntriesNext.Size ? new_routing_start_idx : -1); + for (int n = new_routing_start_idx; n < rt->EntriesNext.Size; n++) + rt->EntriesNext[n].NextEntryIndex = (ImGuiKeyRoutingIndex)((n + 1 < rt->EntriesNext.Size) ? n + 1 : -1); + } + rt->Entries.swap(rt->EntriesNext); // Swap new and old indexes +} + +// owner_id may be None/Any, but routing_id needs to be always be set, so we default to GetCurrentFocusScope(). +static inline ImGuiID GetRoutingIdFromOwnerId(ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + return (owner_id != ImGuiKeyOwner_NoOwner && owner_id != ImGuiKeyOwner_Any) ? owner_id : g.CurrentFocusScopeId; +} + +ImGuiKeyRoutingData* ImGui::GetShortcutRoutingData(ImGuiKeyChord key_chord) +{ + // Majority of shortcuts will be Key + any number of Mods + // We accept _Single_ mod with ImGuiKey_None. + // - Shortcut(ImGuiKey_S | ImGuiMod_Ctrl); // Legal + // - Shortcut(ImGuiKey_S | ImGuiMod_Ctrl | ImGuiMod_Shift); // Legal + // - Shortcut(ImGuiMod_Ctrl); // Legal + // - Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift); // Not legal + ImGuiContext& g = *GImGui; + ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable; + ImGuiKeyRoutingData* routing_data; + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey(mods); + IM_ASSERT(IsNamedKey(key)); + + // Get (in the majority of case, the linked list will have one element so this should be 2 reads. + // Subsequent elements will be contiguous in memory as list is sorted/rebuilt in NewFrame). + for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; idx = routing_data->NextEntryIndex) + { + routing_data = &rt->Entries[idx]; + if (routing_data->Mods == mods) + return routing_data; + } + + // Add to linked-list + ImGuiKeyRoutingIndex routing_data_idx = (ImGuiKeyRoutingIndex)rt->Entries.Size; + rt->Entries.push_back(ImGuiKeyRoutingData()); + routing_data = &rt->Entries[routing_data_idx]; + routing_data->Mods = (ImU16)mods; + routing_data->NextEntryIndex = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; // Setup linked list + rt->Index[key - ImGuiKey_NamedKey_BEGIN] = routing_data_idx; + return routing_data; +} + +// Current score encoding (lower is highest priority): +// - 0: ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverActive +// - 1: ImGuiInputFlags_ActiveItem or ImGuiInputFlags_RouteFocused (if item active) +// - 2: ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused +// - 3+: ImGuiInputFlags_RouteFocused (if window in focus-stack) +// - 254: ImGuiInputFlags_RouteGlobal +// - 255: never route +// 'flags' should include an explicit routing policy +static int CalcRoutingScore(ImGuiID focus_scope_id, ImGuiID owner_id, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + if (flags & ImGuiInputFlags_RouteFocused) + { + // ActiveID gets top priority + // (we don't check g.ActiveIdUsingAllKeys here. Routing is applied but if input ownership is tested later it may discard it) + if (owner_id != 0 && g.ActiveId == owner_id) + return 1; + + // Score based on distance to focused window (lower is better) + // Assuming both windows are submitting a routing request, + // - When Window....... is focused -> Window scores 3 (best), Window/ChildB scores 255 (no match) + // - When Window/ChildB is focused -> Window scores 4, Window/ChildB scores 3 (best) + // Assuming only WindowA is submitting a routing request, + // - When Window/ChildB is focused -> Window scores 4 (best), Window/ChildB doesn't have a score. + // This essentially follow the window->ParentWindowForFocusRoute chain. + if (focus_scope_id == 0) + return 255; + for (int index_in_focus_path = 0; index_in_focus_path < g.NavFocusRoute.Size; index_in_focus_path++) + if (g.NavFocusRoute.Data[index_in_focus_path].ID == focus_scope_id) + return 3 + index_in_focus_path; + return 255; + } + else if (flags & ImGuiInputFlags_RouteActive) + { + if (owner_id != 0 && g.ActiveId == owner_id) + return 1; + return 255; + } + else if (flags & ImGuiInputFlags_RouteGlobal) + { + if (flags & ImGuiInputFlags_RouteOverActive) + return 0; + if (flags & ImGuiInputFlags_RouteOverFocused) + return 2; + return 254; + } + IM_ASSERT(0); + return 0; +} + +// - We need this to filter some Shortcut() routes when an item e.g. an InputText() is active +// e.g. ImGuiKey_G won't be considered a shortcut when item is active, but ImGuiMod|ImGuiKey_G can be. +// - This is also used by UpdateInputEvents() to avoid trickling in the most common case of e.g. pressing ImGuiKey_G also emitting a G character. +static bool IsKeyChordPotentiallyCharInput(ImGuiKeyChord key_chord) +{ + // Mimic 'ignore_char_inputs' logic in InputText() + ImGuiContext& g = *GImGui; + + // When the right mods are pressed it cannot be a char input so we won't filter the shortcut out. + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + const bool ignore_char_inputs = ((mods & ImGuiMod_Ctrl) && !(mods & ImGuiMod_Alt)) || (g.IO.ConfigMacOSXBehaviors && (mods & ImGuiMod_Ctrl)); + if (ignore_char_inputs) + return false; + + // Return true for A-Z, 0-9 and other keys associated to char inputs. Other keys such as F1-F12 won't be filtered. + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + return false; + return g.KeysMayBeCharInput.TestBit(key); +} + +// Request a desired route for an input chord (key + mods). +// Return true if the route is available this frame. +// - Routes and key ownership are attributed at the beginning of next frame based on best score and mod state. +// (Conceptually this does a "Submit for next frame" + "Test for current frame". +// As such, it could be called TrySetXXX or SubmitXXX, or the Submit and Test operations should be separate.) +bool ImGui::SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + if ((flags & ImGuiInputFlags_RouteTypeMask_) == 0) + flags |= ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive; // IMPORTANT: This is the default for SetShortcutRouting() but NOT Shortcut() + else + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiInputFlags_RouteTypeMask_)); // Check that only 1 routing flag is used + IM_ASSERT(owner_id != ImGuiKeyOwner_Any && owner_id != ImGuiKeyOwner_NoOwner); + if (flags & (ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive | ImGuiInputFlags_RouteUnlessBgFocused)) + IM_ASSERT(flags & ImGuiInputFlags_RouteGlobal); + + // Add ImGuiMod_XXXX when a corresponding ImGuiKey_LeftXXX/ImGuiKey_RightXXX is specified. + key_chord = FixupKeyChord(key_chord); + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInShortcutRouting == key_chord) + IM_DEBUG_BREAK(); + + if (flags & ImGuiInputFlags_RouteUnlessBgFocused) + if (g.NavWindow == NULL) + return false; + + // Note how ImGuiInputFlags_RouteAlways won't set routing and thus won't set owner. May want to rework this? + if (flags & ImGuiInputFlags_RouteAlways) + { + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> always, no register\n", GetKeyChordName(key_chord), flags, owner_id); + return true; + } + + // Specific culling when there's an active item. + if (g.ActiveId != 0 && g.ActiveId != owner_id) + { + if (flags & ImGuiInputFlags_RouteActive) + return false; + + // Cull shortcuts with no modifiers when it could generate a character. + // e.g. Shortcut(ImGuiKey_G) also generates 'g' character, should not trigger when InputText() is active. + // but Shortcut(Ctrl+G) should generally trigger when InputText() is active. + // TL;DR: lettered shortcut with no mods or with only Alt mod will not trigger while an item reading text input is active. + // (We cannot filter based on io.InputQueueCharacters[] contents because of trickling and key<>chars submission order are undefined) + if (g.IO.WantTextInput && IsKeyChordPotentiallyCharInput(key_chord)) + { + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> filtered as potential char input\n", GetKeyChordName(key_chord), flags, owner_id); + return false; + } + + // ActiveIdUsingAllKeyboardKeys trumps all for ActiveId + if ((flags & ImGuiInputFlags_RouteOverActive) == 0 && g.ActiveIdUsingAllKeyboardKeys) + { + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey((ImGuiKey)(key_chord & ImGuiMod_Mask_)); + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return false; + } + } + + // Where do we evaluate route for? + ImGuiID focus_scope_id = g.CurrentFocusScopeId; + if (flags & ImGuiInputFlags_RouteFromRootWindow) + focus_scope_id = g.CurrentWindow->RootWindow->ID; // See PushFocusScope() call in Begin() + + const int score = CalcRoutingScore(focus_scope_id, owner_id, flags); + IMGUI_DEBUG_LOG_INPUTROUTING("SetShortcutRouting(%s, flags=%04X, owner_id=0x%08X) -> score %d\n", GetKeyChordName(key_chord), flags, owner_id, score); + if (score == 255) + return false; + + // Submit routing for NEXT frame (assuming score is sufficient) + // FIXME: Could expose a way to use a "serve last" policy for same score resolution (using <= instead of <). + ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord); + //const bool set_route = (flags & ImGuiInputFlags_ServeLast) ? (score <= routing_data->RoutingNextScore) : (score < routing_data->RoutingNextScore); + if (score < routing_data->RoutingNextScore) + { + routing_data->RoutingNext = owner_id; + routing_data->RoutingNextScore = (ImU8)score; + } + + // Return routing state for CURRENT frame + if (routing_data->RoutingCurr == owner_id) + IMGUI_DEBUG_LOG_INPUTROUTING("--> granting current route\n"); + return routing_data->RoutingCurr == owner_id; +} + +// Currently unused by core (but used by tests) +// Note: this cannot be turned into GetShortcutRouting() because we do the owner_id->routing_id translation, name would be more misleading. +bool ImGui::TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id) +{ + const ImGuiID routing_id = GetRoutingIdFromOwnerId(owner_id); + key_chord = FixupKeyChord(key_chord); + ImGuiKeyRoutingData* routing_data = GetShortcutRoutingData(key_chord); // FIXME: Could avoid creating entry. + return routing_data->RoutingCurr == routing_id; +} + +// Note that Dear ImGui doesn't know the meaning/semantic of ImGuiKey from 0..511: they are legacy native keycodes. +// Consider transitioning from 'IsKeyDown(MY_ENGINE_KEY_A)' (<1.87) to IsKeyDown(ImGuiKey_A) (>= 1.87) +bool ImGui::IsKeyDown(ImGuiKey key) +{ + return IsKeyDown(key, ImGuiKeyOwner_Any); +} + +bool ImGui::IsKeyDown(ImGuiKey key, ImGuiID owner_id) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; +} + +bool ImGui::IsKeyPressed(ImGuiKey key, bool repeat) +{ + return IsKeyPressed(key, repeat ? ImGuiInputFlags_Repeat : ImGuiInputFlags_None, ImGuiKeyOwner_Any); +} + +// Important: unlike legacy IsKeyPressed(ImGuiKey, bool repeat=true) which DEFAULT to repeat, this requires EXPLICIT repeat. +bool ImGui::IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + if (!key_data->Down) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return false; + const float t = key_data->DownDuration; + if (t < 0.0f) + return false; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsKeyPressed) == 0); // Passing flags not supported by this function! + if (flags & (ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RepeatUntilMask_)) // Setting any _RepeatXXX option enables _Repeat + flags |= ImGuiInputFlags_Repeat; + + bool pressed = (t == 0.0f); + if (!pressed && (flags & ImGuiInputFlags_Repeat) != 0) + { + float repeat_delay, repeat_rate; + GetTypematicRepeatRate(flags, &repeat_delay, &repeat_rate); + pressed = (t > repeat_delay) && GetKeyPressedAmount(key, repeat_delay, repeat_rate) > 0; + if (pressed && (flags & ImGuiInputFlags_RepeatUntilMask_)) + { + // Slightly bias 'key_pressed_time' as DownDuration is an accumulation of DeltaTime which we compare to an absolute time value. + // Ideally we'd replace DownDuration with KeyPressedTime but it would break user's code. + ImGuiContext& g = *GImGui; + double key_pressed_time = g.Time - t + 0.00001f; + if ((flags & ImGuiInputFlags_RepeatUntilKeyModsChange) && (g.LastKeyModsChangeTime > key_pressed_time)) + pressed = false; + if ((flags & ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone) && (g.LastKeyModsChangeFromNoneTime > key_pressed_time)) + pressed = false; + if ((flags & ImGuiInputFlags_RepeatUntilOtherKeyPress) && (g.LastKeyboardKeyPressTime > key_pressed_time)) + pressed = false; + } + } + if (!pressed) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; +} + +bool ImGui::IsKeyReleased(ImGuiKey key) +{ + return IsKeyReleased(key, ImGuiKeyOwner_Any); +} + +bool ImGui::IsKeyReleased(ImGuiKey key, ImGuiID owner_id) +{ + const ImGuiKeyData* key_data = GetKeyData(key); + if (key_data->DownDurationPrev < 0.0f || key_data->Down) + return false; + if (!TestKeyOwner(key, owner_id)) + return false; + return true; +} + +bool ImGui::IsMouseDown(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button] && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); // should be same as IsKeyDown(MouseButtonToKey(button), ImGuiKeyOwner_Any), but this allows legacy code hijacking the io.Mousedown[] array. +} + +bool ImGui::IsMouseDown(ImGuiMouseButton button, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseDown[button] && TestKeyOwner(MouseButtonToKey(button), owner_id); // Should be same as IsKeyDown(MouseButtonToKey(button), owner_id), but this allows legacy code hijacking the io.Mousedown[] array. +} + +bool ImGui::IsMouseClicked(ImGuiMouseButton button, bool repeat) +{ + return IsMouseClicked(button, repeat ? ImGuiInputFlags_Repeat : ImGuiInputFlags_None, ImGuiKeyOwner_Any); +} + +bool ImGui::IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (!g.IO.MouseDown[button]) // In theory this should already be encoded as (DownDuration < 0.0f), but testing this facilitates eating mechanism (until we finish work on key ownership) + return false; + const float t = g.IO.MouseDownDuration[button]; + if (t < 0.0f) + return false; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByIsMouseClicked) == 0); // Passing flags not supported by this function! // FIXME: Could support RepeatRate and RepeatUntil flags here. + + const bool repeat = (flags & ImGuiInputFlags_Repeat) != 0; + const bool pressed = (t == 0.0f) || (repeat && t > g.IO.KeyRepeatDelay && CalcTypematicRepeatAmount(t - g.IO.DeltaTime, t, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0); + if (!pressed) + return false; + + if (!TestKeyOwner(MouseButtonToKey(button), owner_id)) + return false; + + return true; +} + +bool ImGui::IsMouseReleased(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button] && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); // Should be same as IsKeyReleased(MouseButtonToKey(button), ImGuiKeyOwner_Any) +} + +bool ImGui::IsMouseReleased(ImGuiMouseButton button, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseReleased[button] && TestKeyOwner(MouseButtonToKey(button), owner_id); // Should be same as IsKeyReleased(MouseButtonToKey(button), owner_id) +} + +// Use if you absolutely need to distinguish single-click from double-click by introducing a delay. +// Generally use with 'delay >= io.MouseDoubleClickTime' + combined with a 'io.MouseClickedLastCount == 1' test. +// This is a very rarely used UI idiom, but some apps use this: e.g. MS Explorer single click on an icon to rename. +bool ImGui::IsMouseReleasedWithDelay(ImGuiMouseButton button, float delay) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + const float time_since_release = (float)(g.Time - g.IO.MouseReleasedTime[button]); + return !IsMouseDown(button) && (time_since_release - g.IO.DeltaTime < delay) && (time_since_release >= delay); +} + +bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button] == 2 && TestKeyOwner(MouseButtonToKey(button), ImGuiKeyOwner_Any); +} + +bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button] == 2 && TestKeyOwner(MouseButtonToKey(button), owner_id); +} + +int ImGui::GetMouseClickedCount(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + return g.IO.MouseClickedCount[button]; +} + +// Test if mouse cursor is hovering given rectangle +// NB- Rectangle is clipped by our current clip setting +// NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) +bool ImGui::IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip) +{ + ImGuiContext& g = *GImGui; + + // Clip + ImRect rect_clipped(r_min, r_max); + if (clip) + rect_clipped.ClipWith(g.CurrentWindow->ClipRect); + + // Hit testing, expanded for touch input + if (!rect_clipped.ContainsWithPad(g.IO.MousePos, g.Style.TouchExtraPadding)) + return false; + if (!g.MouseViewport->GetMainRect().Overlaps(rect_clipped)) + return false; + return true; +} + +// Return if a mouse click/drag went past the given threshold. Valid to call during the MouseReleased frame. +// [Internal] This doesn't test if the button is pressed +bool ImGui::IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + return g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold; +} + +bool ImGui::IsMouseDragging(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (!g.IO.MouseDown[button]) + return false; + return IsMouseDragPastThreshold(button, lock_threshold); +} + +ImVec2 ImGui::GetMousePos() +{ + ImGuiContext& g = *GImGui; + return g.IO.MousePos; +} + +// This is called TeleportMousePos() and not SetMousePos() to emphasis that setting MousePosPrev will effectively clear mouse delta as well. +// It is expected you only call this if (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos) is set and supported by backend. +void ImGui::TeleportMousePos(const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + g.IO.MousePos = g.IO.MousePosPrev = pos; + g.IO.MouseDelta = ImVec2(0.0f, 0.0f); + g.IO.WantSetMousePos = true; + //IMGUI_DEBUG_LOG_IO("TeleportMousePos: (%.1f,%.1f)\n", io.MousePos.x, io.MousePos.y); +} + +// NB: prefer to call right after BeginPopup(). At the time Selectable/MenuItem is activated, the popup is already closed! +ImVec2 ImGui::GetMousePosOnOpeningCurrentPopup() +{ + ImGuiContext& g = *GImGui; + if (g.BeginPopupStack.Size > 0) + return g.OpenPopupStack[g.BeginPopupStack.Size - 1].OpenMousePos; + return g.IO.MousePos; +} + +// We typically use ImVec2(-FLT_MAX,-FLT_MAX) to denote an invalid mouse position. +bool ImGui::IsMousePosValid(const ImVec2* mouse_pos) +{ + // The assert is only to silence a false-positive in XCode Static Analysis. + // Because GImGui is not dereferenced in every code path, the static analyzer assume that it may be NULL (which it doesn't for other functions). + IM_ASSERT(GImGui != NULL); + const float MOUSE_INVALID = -256000.0f; + ImVec2 p = mouse_pos ? *mouse_pos : GImGui->IO.MousePos; + return p.x >= MOUSE_INVALID && p.y >= MOUSE_INVALID; +} + +// [WILL OBSOLETE] This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. +bool ImGui::IsAnyMouseDown() +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++) + if (g.IO.MouseDown[n]) + return true; + return false; +} + +// Return the delta from the initial clicking position while the mouse button is clicked or was just released. +// This is locked and return 0.0f until the mouse moves past a distance threshold at least once. +// NB: This is only valid if IsMousePosValid(). backends in theory should always keep mouse position valid when dragging even outside the client window. +ImVec2 ImGui::GetMouseDragDelta(ImGuiMouseButton button, float lock_threshold) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + if (lock_threshold < 0.0f) + lock_threshold = g.IO.MouseDragThreshold; + if (g.IO.MouseDown[button] || g.IO.MouseReleased[button]) + if (g.IO.MouseDragMaxDistanceSqr[button] >= lock_threshold * lock_threshold) + if (IsMousePosValid(&g.IO.MousePos) && IsMousePosValid(&g.IO.MouseClickedPos[button])) + return g.IO.MousePos - g.IO.MouseClickedPos[button]; + return ImVec2(0.0f, 0.0f); +} + +void ImGui::ResetMouseDragDelta(ImGuiMouseButton button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(button >= 0 && button < IM_ARRAYSIZE(g.IO.MouseDown)); + // NB: We don't need to reset g.IO.MouseDragMaxDistanceSqr + g.IO.MouseClickedPos[button] = g.IO.MousePos; +} + +// Get desired mouse cursor shape. +// Important: this is meant to be used by a platform backend, it is reset in ImGui::NewFrame(), +// updated during the frame, and locked in EndFrame()/Render(). +// If you use software rendering by setting io.MouseDrawCursor then Dear ImGui will render those for you +ImGuiMouseCursor ImGui::GetMouseCursor() +{ + ImGuiContext& g = *GImGui; + return g.MouseCursor; +} + +// We intentionally accept values of ImGuiMouseCursor that are outside our bounds, in case users needs to hack-in a custom cursor value. +// Custom cursors may be handled by custom backends. If you are using a standard backend and want to hack in a custom cursor, you may +// handle it before the backend _NewFrame() call and temporarily set ImGuiConfigFlags_NoMouseCursorChange during the backend _NewFrame() call. +void ImGui::SetMouseCursor(ImGuiMouseCursor cursor_type) +{ + ImGuiContext& g = *GImGui; + g.MouseCursor = cursor_type; +} + +static void UpdateAliasKey(ImGuiKey key, bool v, float analog_value) +{ + IM_ASSERT(ImGui::IsAliasKey(key)); + ImGuiKeyData* key_data = ImGui::GetKeyData(key); + key_data->Down = v; + key_data->AnalogValue = analog_value; +} + +// [Internal] Do not use directly +static ImGuiKeyChord GetMergedModsFromKeys() +{ + ImGuiKeyChord mods = 0; + if (ImGui::IsKeyDown(ImGuiMod_Ctrl)) { mods |= ImGuiMod_Ctrl; } + if (ImGui::IsKeyDown(ImGuiMod_Shift)) { mods |= ImGuiMod_Shift; } + if (ImGui::IsKeyDown(ImGuiMod_Alt)) { mods |= ImGuiMod_Alt; } + if (ImGui::IsKeyDown(ImGuiMod_Super)) { mods |= ImGuiMod_Super; } + return mods; +} + +static void ImGui::UpdateKeyboardInputs() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + io.ClearInputKeys(); + + // Update aliases + for (int n = 0; n < ImGuiMouseButton_COUNT; n++) + UpdateAliasKey(MouseButtonToKey(n), io.MouseDown[n], io.MouseDown[n] ? 1.0f : 0.0f); + UpdateAliasKey(ImGuiKey_MouseWheelX, io.MouseWheelH != 0.0f, io.MouseWheelH); + UpdateAliasKey(ImGuiKey_MouseWheelY, io.MouseWheel != 0.0f, io.MouseWheel); + + // Synchronize io.KeyMods and io.KeyCtrl/io.KeyShift/etc. values. + // - New backends (1.87+): send io.AddKeyEvent(ImGuiMod_XXX) -> -> (here) deriving io.KeyMods + io.KeyXXX from key array. + // - Legacy backends: set io.KeyXXX bools -> (above) set key array from io.KeyXXX -> (here) deriving io.KeyMods + io.KeyXXX from key array. + // So with legacy backends the 4 values will do a unnecessary back-and-forth but it makes the code simpler and future facing. + const ImGuiKeyChord prev_key_mods = io.KeyMods; + io.KeyMods = GetMergedModsFromKeys(); + io.KeyCtrl = (io.KeyMods & ImGuiMod_Ctrl) != 0; + io.KeyShift = (io.KeyMods & ImGuiMod_Shift) != 0; + io.KeyAlt = (io.KeyMods & ImGuiMod_Alt) != 0; + io.KeySuper = (io.KeyMods & ImGuiMod_Super) != 0; + if (prev_key_mods != io.KeyMods) + g.LastKeyModsChangeTime = g.Time; + if (prev_key_mods != io.KeyMods && prev_key_mods == 0) + g.LastKeyModsChangeFromNoneTime = g.Time; + + // Clear gamepad data if disabled + if ((io.BackendFlags & ImGuiBackendFlags_HasGamepad) == 0) + for (int key = ImGuiKey_Gamepad_BEGIN; key < ImGuiKey_Gamepad_END; key++) + { + io.KeysData[key - ImGuiKey_NamedKey_BEGIN].Down = false; + io.KeysData[key - ImGuiKey_NamedKey_BEGIN].AnalogValue = 0.0f; + } + + // Update keys + for (int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key++) + { + ImGuiKeyData* key_data = &io.KeysData[key - ImGuiKey_NamedKey_BEGIN]; + key_data->DownDurationPrev = key_data->DownDuration; + key_data->DownDuration = key_data->Down ? (key_data->DownDuration < 0.0f ? 0.0f : key_data->DownDuration + io.DeltaTime) : -1.0f; + if (key_data->DownDuration == 0.0f) + { + if (IsKeyboardKey((ImGuiKey)key)) + g.LastKeyboardKeyPressTime = g.Time; + else if (key == ImGuiKey_ReservedForModCtrl || key == ImGuiKey_ReservedForModShift || key == ImGuiKey_ReservedForModAlt || key == ImGuiKey_ReservedForModSuper) + g.LastKeyboardKeyPressTime = g.Time; + } + } + + // Update keys/input owner (named keys only): one entry per key + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyData* key_data = &io.KeysData[key - ImGuiKey_NamedKey_BEGIN]; + ImGuiKeyOwnerData* owner_data = &g.KeysOwnerData[key - ImGuiKey_NamedKey_BEGIN]; + owner_data->OwnerCurr = owner_data->OwnerNext; + if (!key_data->Down) // Important: ownership is released on the frame after a release. Ensure a 'MouseDown -> CloseWindow -> MouseUp' chain doesn't lead to someone else seeing the MouseUp. + owner_data->OwnerNext = ImGuiKeyOwner_NoOwner; + owner_data->LockThisFrame = owner_data->LockUntilRelease = owner_data->LockUntilRelease && key_data->Down; // Clear LockUntilRelease when key is not Down anymore + } + + // Update key routing (for e.g. shortcuts) + UpdateKeyRoutingTable(&g.KeysRoutingTable); +} + +static void ImGui::UpdateMouseInputs() +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Mouse Wheel swapping flag + // As a standard behavior holding SHIFT while using Vertical Mouse Wheel triggers Horizontal scroll instead + // - We avoid doing it on OSX as it the OS input layer handles this already. + // - FIXME: However this means when running on OSX over Emscripten, Shift+WheelY will incur two swapping (1 in OS, 1 here), canceling the feature. + // - FIXME: When we can distinguish e.g. touchpad scroll events from mouse ones, we'll set this accordingly based on input source. + io.MouseWheelRequestAxisSwap = io.KeyShift && !io.ConfigMacOSXBehaviors; + + // Round mouse position to avoid spreading non-rounded position (e.g. UpdateManualResize doesn't support them well) + if (IsMousePosValid(&io.MousePos)) + io.MousePos = g.MouseLastValidPos = ImFloor(io.MousePos); + + // If mouse just appeared or disappeared (usually denoted by -FLT_MAX components) we cancel out movement in MouseDelta + if (IsMousePosValid(&io.MousePos) && IsMousePosValid(&io.MousePosPrev)) + io.MouseDelta = io.MousePos - io.MousePosPrev; + else + io.MouseDelta = ImVec2(0.0f, 0.0f); + + // Update stationary timer. + // FIXME: May need to rework again to have some tolerance for occasional small movement, while being functional on high-framerates. + const float mouse_stationary_threshold = (io.MouseSource == ImGuiMouseSource_Mouse) ? 2.0f : 3.0f; // Slightly higher threshold for ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen, may need rework. + const bool mouse_stationary = (ImLengthSqr(io.MouseDelta) <= mouse_stationary_threshold * mouse_stationary_threshold); + g.MouseStationaryTimer = mouse_stationary ? (g.MouseStationaryTimer + io.DeltaTime) : 0.0f; + //IMGUI_DEBUG_LOG("%.4f\n", g.MouseStationaryTimer); + + // If mouse moved we re-enable mouse hovering in case it was disabled by keyboard/gamepad. In theory should use a >0.0f threshold but would need to reset in everywhere we set this to true. + if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) + g.NavHighlightItemUnderNav = false; + + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + { + io.MouseClicked[i] = io.MouseDown[i] && io.MouseDownDuration[i] < 0.0f; + io.MouseClickedCount[i] = 0; // Will be filled below + io.MouseReleased[i] = !io.MouseDown[i] && io.MouseDownDuration[i] >= 0.0f; + if (io.MouseReleased[i]) + io.MouseReleasedTime[i] = g.Time; + io.MouseDownDurationPrev[i] = io.MouseDownDuration[i]; + io.MouseDownDuration[i] = io.MouseDown[i] ? (io.MouseDownDuration[i] < 0.0f ? 0.0f : io.MouseDownDuration[i] + io.DeltaTime) : -1.0f; + if (io.MouseClicked[i]) + { + bool is_repeated_click = false; + if ((float)(g.Time - io.MouseClickedTime[i]) < io.MouseDoubleClickTime) + { + ImVec2 delta_from_click_pos = IsMousePosValid(&io.MousePos) ? (io.MousePos - io.MouseClickedPos[i]) : ImVec2(0.0f, 0.0f); + if (ImLengthSqr(delta_from_click_pos) < io.MouseDoubleClickMaxDist * io.MouseDoubleClickMaxDist) + is_repeated_click = true; + } + if (is_repeated_click) + io.MouseClickedLastCount[i]++; + else + io.MouseClickedLastCount[i] = 1; + io.MouseClickedTime[i] = g.Time; + io.MouseClickedPos[i] = io.MousePos; + io.MouseClickedCount[i] = io.MouseClickedLastCount[i]; + io.MouseDragMaxDistanceAbs[i] = ImVec2(0.0f, 0.0f); + io.MouseDragMaxDistanceSqr[i] = 0.0f; + } + else if (io.MouseDown[i]) + { + // Maintain the maximum distance we reaching from the initial click position, which is used with dragging threshold + ImVec2 delta_from_click_pos = IsMousePosValid(&io.MousePos) ? (io.MousePos - io.MouseClickedPos[i]) : ImVec2(0.0f, 0.0f); + io.MouseDragMaxDistanceSqr[i] = ImMax(io.MouseDragMaxDistanceSqr[i], ImLengthSqr(delta_from_click_pos)); + io.MouseDragMaxDistanceAbs[i].x = ImMax(io.MouseDragMaxDistanceAbs[i].x, delta_from_click_pos.x < 0.0f ? -delta_from_click_pos.x : delta_from_click_pos.x); + io.MouseDragMaxDistanceAbs[i].y = ImMax(io.MouseDragMaxDistanceAbs[i].y, delta_from_click_pos.y < 0.0f ? -delta_from_click_pos.y : delta_from_click_pos.y); + } + + // We provide io.MouseDoubleClicked[] as a legacy service + io.MouseDoubleClicked[i] = (io.MouseClickedCount[i] == 2); + + // Clicking any mouse button reactivate mouse hovering which may have been deactivated by keyboard/gamepad navigation + if (io.MouseClicked[i]) + g.NavHighlightItemUnderNav = false; + } +} + +static void LockWheelingWindow(ImGuiWindow* window, float wheel_amount) +{ + ImGuiContext& g = *GImGui; + if (window) + g.WheelingWindowReleaseTimer = ImMin(g.WheelingWindowReleaseTimer + ImAbs(wheel_amount) * WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER, WINDOWS_MOUSE_WHEEL_SCROLL_LOCK_TIMER); + else + g.WheelingWindowReleaseTimer = 0.0f; + if (g.WheelingWindow == window) + return; + IMGUI_DEBUG_LOG_IO("[io] LockWheelingWindow() \"%s\"\n", window ? window->Name : "NULL"); + g.WheelingWindow = window; + g.WheelingWindowRefMousePos = g.IO.MousePos; + if (window == NULL) + { + g.WheelingWindowStartFrame = -1; + g.WheelingAxisAvg = ImVec2(0.0f, 0.0f); + } +} + +static ImGuiWindow* FindBestWheelingWindow(const ImVec2& wheel) +{ + // For each axis, find window in the hierarchy that may want to use scrolling + ImGuiContext& g = *GImGui; + ImGuiWindow* windows[2] = { NULL, NULL }; + for (int axis = 0; axis < 2; axis++) + if (wheel[axis] != 0.0f) + for (ImGuiWindow* window = windows[axis] = g.HoveredWindow; window->Flags & ImGuiWindowFlags_ChildWindow; window = windows[axis] = window->ParentWindow) + { + // Bubble up into parent window if: + // - a child window doesn't allow any scrolling. + // - a child window has the ImGuiWindowFlags_NoScrollWithMouse flag. + //// - a child window doesn't need scrolling because it is already at the edge for the direction we are going in (FIXME-WIP) + const bool has_scrolling = (window->ScrollMax[axis] != 0.0f); + const bool inputs_disabled = (window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs); + //const bool scrolling_past_limits = (wheel_v < 0.0f) ? (window->Scroll[axis] <= 0.0f) : (window->Scroll[axis] >= window->ScrollMax[axis]); + if (has_scrolling && !inputs_disabled) // && !scrolling_past_limits) + break; // select this window + } + if (windows[0] == NULL && windows[1] == NULL) + return NULL; + + // If there's only one window or only one axis then there's no ambiguity + if (windows[0] == windows[1] || windows[0] == NULL || windows[1] == NULL) + return windows[1] ? windows[1] : windows[0]; + + // If candidate are different windows we need to decide which one to prioritize + // - First frame: only find a winner if one axis is zero. + // - Subsequent frames: only find a winner when one is more than the other. + if (g.WheelingWindowStartFrame == -1) + g.WheelingWindowStartFrame = g.FrameCount; + if ((g.WheelingWindowStartFrame == g.FrameCount && wheel.x != 0.0f && wheel.y != 0.0f) || (g.WheelingAxisAvg.x == g.WheelingAxisAvg.y)) + { + g.WheelingWindowWheelRemainder = wheel; + return NULL; + } + return (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? windows[0] : windows[1]; +} + +// Called by NewFrame() +void ImGui::UpdateMouseWheel() +{ + // Reset the locked window if we move the mouse or after the timer elapses. + // FIXME: Ideally we could refactor to have one timer for "changing window w/ same axis" and a shorter timer for "changing window or axis w/ other axis" (#3795) + ImGuiContext& g = *GImGui; + if (g.WheelingWindow != NULL) + { + g.WheelingWindowReleaseTimer -= g.IO.DeltaTime; + if (IsMousePosValid() && ImLengthSqr(g.IO.MousePos - g.WheelingWindowRefMousePos) > g.IO.MouseDragThreshold * g.IO.MouseDragThreshold) + g.WheelingWindowReleaseTimer = 0.0f; + if (g.WheelingWindowReleaseTimer <= 0.0f) + LockWheelingWindow(NULL, 0.0f); + } + + ImVec2 wheel; + wheel.x = TestKeyOwner(ImGuiKey_MouseWheelX, ImGuiKeyOwner_NoOwner) ? g.IO.MouseWheelH : 0.0f; + wheel.y = TestKeyOwner(ImGuiKey_MouseWheelY, ImGuiKeyOwner_NoOwner) ? g.IO.MouseWheel : 0.0f; + + //IMGUI_DEBUG_LOG("MouseWheel X:%.3f Y:%.3f\n", wheel_x, wheel_y); + ImGuiWindow* mouse_window = g.WheelingWindow ? g.WheelingWindow : g.HoveredWindow; + if (!mouse_window || mouse_window->Collapsed) + return; + + // Zoom / Scale window + // FIXME-OBSOLETE: This is an old feature, it still works but pretty much nobody is using it and may be best redesigned. + if (wheel.y != 0.0f && g.IO.KeyCtrl && g.IO.FontAllowUserScaling) + { + LockWheelingWindow(mouse_window, wheel.y); + ImGuiWindow* window = mouse_window; + const float new_font_scale = ImClamp(window->FontWindowScale + g.IO.MouseWheel * 0.10f, 0.50f, 2.50f); + const float scale = new_font_scale / window->FontWindowScale; + window->FontWindowScale = new_font_scale; + if (window == window->RootWindow) + { + const ImVec2 offset = window->Size * (1.0f - scale) * (g.IO.MousePos - window->Pos) / window->Size; + SetWindowPos(window, window->Pos + offset, 0); + window->Size = ImTrunc(window->Size * scale); + window->SizeFull = ImTrunc(window->SizeFull * scale); + } + return; + } + if (g.IO.KeyCtrl) + return; + + // Mouse wheel scrolling + // Read about io.MouseWheelRequestAxisSwap and its issue on Mac+Emscripten in UpdateMouseInputs() + if (g.IO.MouseWheelRequestAxisSwap) + wheel = ImVec2(wheel.y, 0.0f); + + // Maintain a rough average of moving magnitude on both axes + // FIXME: should by based on wall clock time rather than frame-counter + g.WheelingAxisAvg.x = ImExponentialMovingAverage(g.WheelingAxisAvg.x, ImAbs(wheel.x), 30); + g.WheelingAxisAvg.y = ImExponentialMovingAverage(g.WheelingAxisAvg.y, ImAbs(wheel.y), 30); + + // In the rare situation where FindBestWheelingWindow() had to defer first frame of wheeling due to ambiguous main axis, reinject it now. + wheel += g.WheelingWindowWheelRemainder; + g.WheelingWindowWheelRemainder = ImVec2(0.0f, 0.0f); + if (wheel.x == 0.0f && wheel.y == 0.0f) + return; + + // Mouse wheel scrolling: find target and apply + // - don't renew lock if axis doesn't apply on the window. + // - select a main axis when both axes are being moved. + if (ImGuiWindow* window = (g.WheelingWindow ? g.WheelingWindow : FindBestWheelingWindow(wheel))) + if (!(window->Flags & ImGuiWindowFlags_NoScrollWithMouse) && !(window->Flags & ImGuiWindowFlags_NoMouseInputs)) + { + bool do_scroll[2] = { wheel.x != 0.0f && window->ScrollMax.x != 0.0f, wheel.y != 0.0f && window->ScrollMax.y != 0.0f }; + if (do_scroll[ImGuiAxis_X] && do_scroll[ImGuiAxis_Y]) + do_scroll[(g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? ImGuiAxis_Y : ImGuiAxis_X] = false; + if (do_scroll[ImGuiAxis_X]) + { + LockWheelingWindow(window, wheel.x); + float max_step = window->InnerRect.GetWidth() * 0.67f; + float scroll_step = ImTrunc(ImMin(2 * window->FontRefSize, max_step)); + SetScrollX(window, window->Scroll.x - wheel.x * scroll_step); + g.WheelingWindowScrolledFrame = g.FrameCount; + } + if (do_scroll[ImGuiAxis_Y]) + { + LockWheelingWindow(window, wheel.y); + float max_step = window->InnerRect.GetHeight() * 0.67f; + float scroll_step = ImTrunc(ImMin(5 * window->FontRefSize, max_step)); + SetScrollY(window, window->Scroll.y - wheel.y * scroll_step); + g.WheelingWindowScrolledFrame = g.FrameCount; + } + } +} + +void ImGui::SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard) +{ + ImGuiContext& g = *GImGui; + g.WantCaptureKeyboardNextFrame = want_capture_keyboard ? 1 : 0; +} + +void ImGui::SetNextFrameWantCaptureMouse(bool want_capture_mouse) +{ + ImGuiContext& g = *GImGui; + g.WantCaptureMouseNextFrame = want_capture_mouse ? 1 : 0; +} + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +static const char* GetInputSourceName(ImGuiInputSource source) +{ + const char* input_source_names[] = { "None", "Mouse", "Keyboard", "Gamepad" }; + IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_COUNT && source >= 0 && source < ImGuiInputSource_COUNT); + return input_source_names[source]; +} +static const char* GetMouseSourceName(ImGuiMouseSource source) +{ + const char* mouse_source_names[] = { "Mouse", "TouchScreen", "Pen" }; + IM_ASSERT(IM_ARRAYSIZE(mouse_source_names) == ImGuiMouseSource_COUNT && source >= 0 && source < ImGuiMouseSource_COUNT); + return mouse_source_names[source]; +} +static void DebugPrintInputEvent(const char* prefix, const ImGuiInputEvent* e) +{ + ImGuiContext& g = *GImGui; + if (e->Type == ImGuiInputEventType_MousePos) { if (e->MousePos.PosX == -FLT_MAX && e->MousePos.PosY == -FLT_MAX) IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (-FLT_MAX, -FLT_MAX)\n", prefix); else IMGUI_DEBUG_LOG_IO("[io] %s: MousePos (%.1f, %.1f) (%s)\n", prefix, e->MousePos.PosX, e->MousePos.PosY, GetMouseSourceName(e->MousePos.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseButton) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseButton %d %s (%s)\n", prefix, e->MouseButton.Button, e->MouseButton.Down ? "Down" : "Up", GetMouseSourceName(e->MouseButton.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseWheel) { IMGUI_DEBUG_LOG_IO("[io] %s: MouseWheel (%.3f, %.3f) (%s)\n", prefix, e->MouseWheel.WheelX, e->MouseWheel.WheelY, GetMouseSourceName(e->MouseWheel.MouseSource)); return; } + if (e->Type == ImGuiInputEventType_MouseViewport){IMGUI_DEBUG_LOG_IO("[io] %s: MouseViewport (0x%08X)\n", prefix, e->MouseViewport.HoveredViewportID); return; } + if (e->Type == ImGuiInputEventType_Key) { IMGUI_DEBUG_LOG_IO("[io] %s: Key \"%s\" %s\n", prefix, ImGui::GetKeyName(e->Key.Key), e->Key.Down ? "Down" : "Up"); return; } + if (e->Type == ImGuiInputEventType_Text) { IMGUI_DEBUG_LOG_IO("[io] %s: Text: %c (U+%08X)\n", prefix, e->Text.Char, e->Text.Char); return; } + if (e->Type == ImGuiInputEventType_Focus) { IMGUI_DEBUG_LOG_IO("[io] %s: AppFocused %d\n", prefix, e->AppFocused.Focused); return; } +} +#endif + +// Process input queue +// We always call this with the value of 'bool g.IO.ConfigInputTrickleEventQueue'. +// - trickle_fast_inputs = false : process all events, turn into flattened input state (e.g. successive down/up/down/up will be lost) +// - trickle_fast_inputs = true : process as many events as possible (successive down/up/down/up will be trickled over several frames so nothing is lost) (new feature in 1.87) +void ImGui::UpdateInputEvents(bool trickle_fast_inputs) +{ + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + + // Only trickle chars<>key when working with InputText() + // FIXME: InputText() could parse event trail? + // FIXME: Could specialize chars<>keys trickling rules for control keys (those not typically associated to characters) + const bool trickle_interleaved_nonchar_keys_and_text = (trickle_fast_inputs && g.WantTextInputNextFrame == 1); + + bool mouse_moved = false, mouse_wheeled = false, key_changed = false, key_changed_nonchar = false, text_inputted = false; + int mouse_button_changed = 0x00; + ImBitArray key_changed_mask; + + int event_n = 0; + for (; event_n < g.InputEventsQueue.Size; event_n++) + { + ImGuiInputEvent* e = &g.InputEventsQueue[event_n]; + if (e->Type == ImGuiInputEventType_MousePos) + { + if (g.IO.WantSetMousePos) + continue; + // Trickling Rule: Stop processing queued events if we already handled a mouse button change + ImVec2 event_pos(e->MousePos.PosX, e->MousePos.PosY); + if (trickle_fast_inputs && (mouse_button_changed != 0 || mouse_wheeled || key_changed || text_inputted)) + break; + io.MousePos = event_pos; + io.MouseSource = e->MousePos.MouseSource; + mouse_moved = true; + } + else if (e->Type == ImGuiInputEventType_MouseButton) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the same button + const ImGuiMouseButton button = e->MouseButton.Button; + IM_ASSERT(button >= 0 && button < ImGuiMouseButton_COUNT); + if (trickle_fast_inputs && ((mouse_button_changed & (1 << button)) || mouse_wheeled)) + break; + if (trickle_fast_inputs && e->MouseButton.MouseSource == ImGuiMouseSource_TouchScreen && mouse_moved) // #2702: TouchScreen have no initial hover. + break; + io.MouseDown[button] = e->MouseButton.Down; + io.MouseSource = e->MouseButton.MouseSource; + mouse_button_changed |= (1 << button); + } + else if (e->Type == ImGuiInputEventType_MouseWheel) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the event + if (trickle_fast_inputs && (mouse_moved || mouse_button_changed != 0)) + break; + io.MouseWheelH += e->MouseWheel.WheelX; + io.MouseWheel += e->MouseWheel.WheelY; + io.MouseSource = e->MouseWheel.MouseSource; + mouse_wheeled = true; + } + else if (e->Type == ImGuiInputEventType_MouseViewport) + { + io.MouseHoveredViewport = e->MouseViewport.HoveredViewportID; + } + else if (e->Type == ImGuiInputEventType_Key) + { + // Trickling Rule: Stop processing queued events if we got multiple action on the same button + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + continue; + ImGuiKey key = e->Key.Key; + IM_ASSERT(key != ImGuiKey_None); + ImGuiKeyData* key_data = GetKeyData(key); + const int key_data_index = (int)(key_data - g.IO.KeysData); + if (trickle_fast_inputs && key_data->Down != e->Key.Down && (key_changed_mask.TestBit(key_data_index) || mouse_button_changed != 0)) + break; + + const bool key_is_potentially_for_char_input = IsKeyChordPotentiallyCharInput(GetMergedModsFromKeys() | key); + if (trickle_interleaved_nonchar_keys_and_text && (text_inputted && !key_is_potentially_for_char_input)) + break; + + key_data->Down = e->Key.Down; + key_data->AnalogValue = e->Key.AnalogValue; + key_changed = true; + key_changed_mask.SetBit(key_data_index); + if (trickle_interleaved_nonchar_keys_and_text && !key_is_potentially_for_char_input) + key_changed_nonchar = true; + } + else if (e->Type == ImGuiInputEventType_Text) + { + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) + continue; + // Trickling Rule: Stop processing queued events if keys/mouse have been interacted with + if (trickle_fast_inputs && (mouse_button_changed != 0 || mouse_moved || mouse_wheeled)) + break; + if (trickle_interleaved_nonchar_keys_and_text && key_changed_nonchar) + break; + unsigned int c = e->Text.Char; + io.InputQueueCharacters.push_back(c <= IM_UNICODE_CODEPOINT_MAX ? (ImWchar)c : IM_UNICODE_CODEPOINT_INVALID); + if (trickle_interleaved_nonchar_keys_and_text) + text_inputted = true; + } + else if (e->Type == ImGuiInputEventType_Focus) + { + // We intentionally overwrite this and process in NewFrame(), in order to give a chance + // to multi-viewports backends to queue AddFocusEvent(false) + AddFocusEvent(true) in same frame. + const bool focus_lost = !e->AppFocused.Focused; + io.AppFocusLost = focus_lost; + } + else + { + IM_ASSERT(0 && "Unknown event!"); + } + } + + // Record trail (for domain-specific applications wanting to access a precise trail) + //if (event_n != 0) IMGUI_DEBUG_LOG_IO("Processed: %d / Remaining: %d\n", event_n, g.InputEventsQueue.Size - event_n); + for (int n = 0; n < event_n; n++) + g.InputEventsTrail.push_back(g.InputEventsQueue[n]); + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (event_n != 0 && (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO)) + for (int n = 0; n < g.InputEventsQueue.Size; n++) + DebugPrintInputEvent(n < event_n ? "Processed" : "Remaining", &g.InputEventsQueue[n]); +#endif + + // Remaining events will be processed on the next frame + if (event_n == g.InputEventsQueue.Size) + g.InputEventsQueue.resize(0); + else + g.InputEventsQueue.erase(g.InputEventsQueue.Data, g.InputEventsQueue.Data + event_n); + + // Clear buttons state when focus is lost + // - this is useful so e.g. releasing Alt after focus loss on Alt-Tab doesn't trigger the Alt menu toggle. + // - we clear in EndFrame() and not now in order allow application/user code polling this flag + // (e.g. custom backend may want to clear additional data, custom widgets may want to react with a "canceling" event). + if (g.IO.AppFocusLost) + { + g.IO.ClearInputKeys(); + g.IO.ClearInputMouse(); + } +} + +ImGuiID ImGui::GetKeyOwner(ImGuiKey key) +{ + if (!IsNamedKeyOrMod(key)) + return ImGuiKeyOwner_NoOwner; + + ImGuiContext& g = *GImGui; + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + ImGuiID owner_id = owner_data->OwnerCurr; + + if (g.ActiveIdUsingAllKeyboardKeys && owner_id != g.ActiveId && owner_id != ImGuiKeyOwner_Any) + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return ImGuiKeyOwner_NoOwner; + + return owner_id; +} + +// TestKeyOwner(..., ID) : (owner == None || owner == ID) +// TestKeyOwner(..., None) : (owner == None) +// TestKeyOwner(..., Any) : no owner test +// All paths are also testing for key not being locked, for the rare cases that key have been locked with using ImGuiInputFlags_LockXXX flags. +bool ImGui::TestKeyOwner(ImGuiKey key, ImGuiID owner_id) +{ + if (!IsNamedKeyOrMod(key)) + return true; + + ImGuiContext& g = *GImGui; + if (g.ActiveIdUsingAllKeyboardKeys && owner_id != g.ActiveId && owner_id != ImGuiKeyOwner_Any) + if (key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END) + return false; + + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_id == ImGuiKeyOwner_Any) + return (owner_data->LockThisFrame == false); + + // Note: SetKeyOwner() sets OwnerCurr. It is not strictly required for most mouse routing overlap (because of ActiveId/HoveredId + // are acting as filter before this has a chance to filter), but sane as soon as user tries to look into things. + // Setting OwnerCurr in SetKeyOwner() is more consistent than testing OwnerNext here: would be inconsistent with getter and other functions. + if (owner_data->OwnerCurr != owner_id) + { + if (owner_data->LockThisFrame) + return false; + if (owner_data->OwnerCurr != ImGuiKeyOwner_NoOwner) + return false; + } + + return true; +} + +// _LockXXX flags are useful to lock keys away from code which is not input-owner aware. +// When using _LockXXX flags, you can use ImGuiKeyOwner_Any to lock keys from everyone. +// - SetKeyOwner(..., None) : clears owner +// - SetKeyOwner(..., Any, !Lock) : illegal (assert) +// - SetKeyOwner(..., Any or None, Lock) : set lock +void ImGui::SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(IsNamedKeyOrMod(key) && (owner_id != ImGuiKeyOwner_Any || (flags & (ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease)))); // Can only use _Any with _LockXXX flags (to eat a key away without an ID to retrieve it) + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetKeyOwner) == 0); // Passing flags not supported by this function! + //IMGUI_DEBUG_LOG("SetKeyOwner(%s, owner_id=0x%08X, flags=%08X)\n", GetKeyName(key), owner_id, flags); + + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + owner_data->OwnerCurr = owner_data->OwnerNext = owner_id; + + // We cannot lock by default as it would likely break lots of legacy code. + // In the case of using LockUntilRelease while key is not down we still lock during the frame (no key_data->Down test) + owner_data->LockUntilRelease = (flags & ImGuiInputFlags_LockUntilRelease) != 0; + owner_data->LockThisFrame = (flags & ImGuiInputFlags_LockThisFrame) != 0 || (owner_data->LockUntilRelease); +} + +// Rarely used helper +void ImGui::SetKeyOwnersForKeyChord(ImGuiKeyChord key_chord, ImGuiID owner_id, ImGuiInputFlags flags) +{ + if (key_chord & ImGuiMod_Ctrl) { SetKeyOwner(ImGuiMod_Ctrl, owner_id, flags); } + if (key_chord & ImGuiMod_Shift) { SetKeyOwner(ImGuiMod_Shift, owner_id, flags); } + if (key_chord & ImGuiMod_Alt) { SetKeyOwner(ImGuiMod_Alt, owner_id, flags); } + if (key_chord & ImGuiMod_Super) { SetKeyOwner(ImGuiMod_Super, owner_id, flags); } + if (key_chord & ~ImGuiMod_Mask_) { SetKeyOwner((ImGuiKey)(key_chord & ~ImGuiMod_Mask_), owner_id, flags); } +} + +// This is more or less equivalent to: +// if (IsItemHovered() || IsItemActive()) +// SetKeyOwner(key, GetItemID()); +// Extensive uses of that (e.g. many calls for a single item) may want to manually perform the tests once and then call SetKeyOwner() multiple times. +// More advanced usage scenarios may want to call SetKeyOwner() manually based on different condition. +// Worth noting is that only one item can be hovered and only one item can be active, therefore this usage pattern doesn't need to bother with routing and priority. +void ImGui::SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = g.LastItemData.ID; + if (id == 0 || (g.HoveredId != id && g.ActiveId != id)) + return; + if ((flags & ImGuiInputFlags_CondMask_) == 0) + flags |= ImGuiInputFlags_CondDefault_; + if ((g.HoveredId == id && (flags & ImGuiInputFlags_CondHovered)) || (g.ActiveId == id && (flags & ImGuiInputFlags_CondActive))) + { + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetItemKeyOwner) == 0); // Passing flags not supported by this function! + SetKeyOwner(key, id, flags & ~ImGuiInputFlags_CondMask_); + } +} + +void ImGui::SetItemKeyOwner(ImGuiKey key) +{ + SetItemKeyOwner(key, ImGuiInputFlags_None); +} + +// This is the only public API until we expose owner_id versions of the API as replacements. +bool ImGui::IsKeyChordPressed(ImGuiKeyChord key_chord) +{ + return IsKeyChordPressed(key_chord, ImGuiInputFlags_None, ImGuiKeyOwner_Any); +} + +// This is equivalent to comparing KeyMods + doing a IsKeyPressed() +bool ImGui::IsKeyChordPressed(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + key_chord = FixupKeyChord(key_chord); + ImGuiKey mods = (ImGuiKey)(key_chord & ImGuiMod_Mask_); + if (g.IO.KeyMods != mods) + return false; + + // Special storage location for mods + ImGuiKey key = (ImGuiKey)(key_chord & ~ImGuiMod_Mask_); + if (key == ImGuiKey_None) + key = ConvertSingleModFlagToKey(mods); + if (!IsKeyPressed(key, (flags & ImGuiInputFlags_RepeatMask_), owner_id)) + return false; + return true; +} + +void ImGui::SetNextItemShortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasShortcut; + g.NextItemData.Shortcut = key_chord; + g.NextItemData.ShortcutFlags = flags; +} + +// Called from within ItemAdd: at this point we can read from NextItemData and write to LastItemData +void ImGui::ItemHandleShortcut(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiInputFlags flags = g.NextItemData.ShortcutFlags; + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedBySetNextItemShortcut) == 0); // Passing flags not supported by SetNextItemShortcut()! + + if (g.LastItemData.ItemFlags & ImGuiItemFlags_Disabled) + return; + if (flags & ImGuiInputFlags_Tooltip) + { + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasShortcut; + g.LastItemData.Shortcut = g.NextItemData.Shortcut; + } + if (!Shortcut(g.NextItemData.Shortcut, flags & ImGuiInputFlags_SupportedByShortcut, id) || g.NavActivateId != 0) + return; + + // FIXME: Generalize Activation queue? + g.NavActivateId = id; // Will effectively disable clipping. + g.NavActivateFlags = ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_FromShortcut; + //if (g.ActiveId == 0 || g.ActiveId == id) + g.NavActivateDownId = g.NavActivatePressedId = id; + NavHighlightActivated(id); +} + +bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags) +{ + return Shortcut(key_chord, flags, ImGuiKeyOwner_Any); +} + +bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id) +{ + ImGuiContext& g = *GImGui; + //IMGUI_DEBUG_LOG("Shortcut(%s, flags=%X, owner_id=0x%08X)\n", GetKeyChordName(key_chord, g.TempBuffer.Data, g.TempBuffer.Size), flags, owner_id); + + // When using (owner_id == 0/Any): SetShortcutRouting() will use CurrentFocusScopeId and filter with this, so IsKeyPressed() is fine with he 0/Any. + if ((flags & ImGuiInputFlags_RouteTypeMask_) == 0) + flags |= ImGuiInputFlags_RouteFocused; + + // Using 'owner_id == ImGuiKeyOwner_Any/0': auto-assign an owner based on current focus scope (each window has its focus scope by default) + // Effectively makes Shortcut() always input-owner aware. + if (owner_id == ImGuiKeyOwner_Any || owner_id == ImGuiKeyOwner_NoOwner) + owner_id = GetRoutingIdFromOwnerId(owner_id); + + if (g.CurrentItemFlags & ImGuiItemFlags_Disabled) + return false; + + // Submit route + if (!SetShortcutRouting(key_chord, flags, owner_id)) + return false; + + // Default repeat behavior for Shortcut() + // So e.g. pressing Ctrl+W and releasing Ctrl while holding W will not trigger the W shortcut. + if ((flags & ImGuiInputFlags_Repeat) != 0 && (flags & ImGuiInputFlags_RepeatUntilMask_) == 0) + flags |= ImGuiInputFlags_RepeatUntilKeyModsChange; + + if (!IsKeyChordPressed(key_chord, flags, owner_id)) + return false; + + // Claim mods during the press + SetKeyOwnersForKeyChord(key_chord & ImGuiMod_Mask_, owner_id); + + IM_ASSERT((flags & ~ImGuiInputFlags_SupportedByShortcut) == 0); // Passing flags not supported by this function! + return true; +} + +//----------------------------------------------------------------------------- +// [SECTION] ERROR CHECKING, STATE RECOVERY +//----------------------------------------------------------------------------- +// - DebugCheckVersionAndDataLayout() (called via IMGUI_CHECKVERSION() macros) +// - ErrorCheckUsingSetCursorPosToExtendParentBoundaries() +// - ErrorCheckNewFrameSanityChecks() +// - ErrorCheckEndFrameSanityChecks() +// - ErrorRecoveryStoreState() +// - ErrorRecoveryTryToRecoverState() +// - ErrorRecoveryTryToRecoverWindowState() +// - ErrorLog() +//----------------------------------------------------------------------------- + +// Verify ABI compatibility between caller code and compiled version of Dear ImGui. This helps detects some build issues. +// Called by IMGUI_CHECKVERSION(). +// Verify that the type sizes are matching between the calling file's compilation unit and imgui.cpp's compilation unit +// If this triggers you have mismatched headers and compiled code versions. +// - It could be because of a build issue (using new headers with old compiled code) +// - It could be because of mismatched configuration #define, compilation settings, packing pragma etc. +// THE CONFIGURATION SETTINGS MENTIONED IN imconfig.h MUST BE SET FOR ALL COMPILATION UNITS INVOLVED WITH DEAR IMGUI. +// Which is why it is required you put them in your imconfig file (and NOT only before including imgui.h). +// Otherwise it is possible that different compilation units would see different structure layout. +// If you don't want to modify imconfig.h you can use the IMGUI_USER_CONFIG define to change filename. +bool ImGui::DebugCheckVersionAndDataLayout(const char* version, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_vert, size_t sz_idx) +{ + bool error = false; + if (strcmp(version, IMGUI_VERSION) != 0) { error = true; IM_ASSERT(strcmp(version, IMGUI_VERSION) == 0 && "Mismatched version string!"); } + if (sz_io != sizeof(ImGuiIO)) { error = true; IM_ASSERT(sz_io == sizeof(ImGuiIO) && "Mismatched struct layout!"); } + if (sz_style != sizeof(ImGuiStyle)) { error = true; IM_ASSERT(sz_style == sizeof(ImGuiStyle) && "Mismatched struct layout!"); } + if (sz_vec2 != sizeof(ImVec2)) { error = true; IM_ASSERT(sz_vec2 == sizeof(ImVec2) && "Mismatched struct layout!"); } + if (sz_vec4 != sizeof(ImVec4)) { error = true; IM_ASSERT(sz_vec4 == sizeof(ImVec4) && "Mismatched struct layout!"); } + if (sz_vert != sizeof(ImDrawVert)) { error = true; IM_ASSERT(sz_vert == sizeof(ImDrawVert) && "Mismatched struct layout!"); } + if (sz_idx != sizeof(ImDrawIdx)) { error = true; IM_ASSERT(sz_idx == sizeof(ImDrawIdx) && "Mismatched struct layout!"); } + return !error; +} + +// Until 1.89 (IMGUI_VERSION_NUM < 18814) it was legal to use SetCursorPos() to extend the boundary of a parent (e.g. window or table cell) +// This is causing issues and ambiguity and we need to retire that. +// See https://github.com/ocornut/imgui/issues/5548 for more details. +// [Scenario 1] +// Previously this would make the window content size ~200x200: +// Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + End(); // NOT OK +// Instead, please submit an item: +// Begin(...) + SetCursorScreenPos(GetCursorScreenPos() + ImVec2(200,200)) + Dummy(ImVec2(0,0)) + End(); // OK +// Alternative: +// Begin(...) + Dummy(ImVec2(200,200)) + End(); // OK +// [Scenario 2] +// For reference this is one of the issue what we aim to fix with this change: +// BeginGroup() + SomeItem("foobar") + SetCursorScreenPos(GetCursorScreenPos()) + EndGroup() +// The previous logic made SetCursorScreenPos(GetCursorScreenPos()) have a side-effect! It would erroneously incorporate ItemSpacing.y after the item into content size, making the group taller! +// While this code is a little twisted, no-one would expect SetXXX(GetXXX()) to have a side-effect. Using vertical alignment patterns could trigger this issue. +void ImGui::ErrorCheckUsingSetCursorPosToExtendParentBoundaries() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window->DC.IsSetPos); + window->DC.IsSetPos = false; +#ifdef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + if (window->DC.CursorPos.x <= window->DC.CursorMaxPos.x && window->DC.CursorPos.y <= window->DC.CursorMaxPos.y) + return; + if (window->SkipItems) + return; + IM_ASSERT(0 && "Code uses SetCursorPos()/SetCursorScreenPos() to extend window/parent boundaries. Please submit an item e.g. Dummy() to validate extent."); +#else + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); +#endif +} + +static void ImGui::ErrorCheckNewFrameSanityChecks() +{ + ImGuiContext& g = *GImGui; + + // Check user IM_ASSERT macro + // (IF YOU GET A WARNING OR COMPILE ERROR HERE: it means your assert macro is incorrectly defined! + // If your macro uses multiple statements, it NEEDS to be surrounded by a 'do { ... } while (0)' block. + // This is a common C/C++ idiom to allow multiple statements macros to be used in control flow blocks.) + // #define IM_ASSERT(EXPR) if (SomeCode(EXPR)) SomeMoreCode(); // Wrong! + // #define IM_ASSERT(EXPR) do { if (SomeCode(EXPR)) SomeMoreCode(); } while (0) // Correct! + if (true) IM_ASSERT(1); else IM_ASSERT(0); + + // Emscripten backends are often imprecise in their submission of DeltaTime. (#6114, #3644) + // Ideally the Emscripten app/backend should aim to fix or smooth this value and avoid feeding zero, but we tolerate it. +#ifdef __EMSCRIPTEN__ + if (g.IO.DeltaTime <= 0.0f && g.FrameCount > 0) + g.IO.DeltaTime = 0.00001f; +#endif + + // Check user data + // (We pass an error message in the assert expression to make it visible to programmers who are not using a debugger, as most assert handlers display their argument) + IM_ASSERT(g.Initialized); + IM_ASSERT((g.IO.DeltaTime > 0.0f || g.FrameCount == 0) && "Need a positive DeltaTime!"); + IM_ASSERT((g.FrameCount == 0 || g.FrameCountEnded == g.FrameCount) && "Forgot to call Render() or EndFrame() at the end of the previous frame?"); + IM_ASSERT(g.IO.DisplaySize.x >= 0.0f && g.IO.DisplaySize.y >= 0.0f && "Invalid DisplaySize value!"); + IM_ASSERT(g.IO.Fonts->IsBuilt() && "Font Atlas not built! Make sure you called ImGui_ImplXXXX_NewFrame() function for renderer backend, which should call io.Fonts->GetTexDataAsRGBA32() / GetTexDataAsAlpha8()"); + IM_ASSERT(g.Style.CurveTessellationTol > 0.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.CircleTessellationMaxError > 0.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.Alpha >= 0.0f && g.Style.Alpha <= 1.0f && "Invalid style setting!"); // Allows us to avoid a few clamps in color computations + IM_ASSERT(g.Style.WindowMinSize.x >= 1.0f && g.Style.WindowMinSize.y >= 1.0f && "Invalid style setting!"); + IM_ASSERT(g.Style.WindowBorderHoverPadding > 0.0f && "Invalid style setting!"); // Required otherwise cannot resize from borders. + IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right); + IM_ASSERT(g.Style.ColorButtonPosition == ImGuiDir_Left || g.Style.ColorButtonPosition == ImGuiDir_Right); + + // Error handling: we do not accept 100% silent recovery! Please contact me if you feel this is getting in your way. + if (g.IO.ConfigErrorRecovery) + IM_ASSERT(g.IO.ConfigErrorRecoveryEnableAssert || g.IO.ConfigErrorRecoveryEnableDebugLog || g.IO.ConfigErrorRecoveryEnableTooltip || g.ErrorCallback != NULL); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // Remap legacy names + if (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) + { + g.IO.ConfigNavMoveSetMousePos = true; + g.IO.ConfigFlags &= ~ImGuiConfigFlags_NavEnableSetMousePos; + } + if (g.IO.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard) + { + g.IO.ConfigNavCaptureKeyboard = false; + g.IO.ConfigFlags &= ~ImGuiConfigFlags_NavNoCaptureKeyboard; + } + + // Remap legacy clipboard handlers (OBSOLETED in 1.91.1, August 2024) + if (g.IO.GetClipboardTextFn != NULL && (g.PlatformIO.Platform_GetClipboardTextFn == NULL || g.PlatformIO.Platform_GetClipboardTextFn == Platform_GetClipboardTextFn_DefaultImpl)) + g.PlatformIO.Platform_GetClipboardTextFn = [](ImGuiContext* ctx) { return ctx->IO.GetClipboardTextFn(ctx->IO.ClipboardUserData); }; + if (g.IO.SetClipboardTextFn != NULL && (g.PlatformIO.Platform_SetClipboardTextFn == NULL || g.PlatformIO.Platform_SetClipboardTextFn == Platform_SetClipboardTextFn_DefaultImpl)) + g.PlatformIO.Platform_SetClipboardTextFn = [](ImGuiContext* ctx, const char* text) { return ctx->IO.SetClipboardTextFn(ctx->IO.ClipboardUserData, text); }; +#endif + + // Perform simple check: error if Docking or Viewport are enabled _exactly_ on frame 1 (instead of frame 0 or later), which is a common error leading to loss of .ini data. + if (g.FrameCount == 1 && (g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable) && (g.ConfigFlagsLastFrame & ImGuiConfigFlags_DockingEnable) == 0) + IM_ASSERT(0 && "Please set DockingEnable before the first call to NewFrame()! Otherwise you will lose your .ini settings!"); + if (g.FrameCount == 1 && (g.IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) && (g.ConfigFlagsLastFrame & ImGuiConfigFlags_ViewportsEnable) == 0) + IM_ASSERT(0 && "Please set ViewportsEnable before the first call to NewFrame()! Otherwise you will lose your .ini settings!"); + + // Perform simple checks: multi-viewport and platform windows support + if (g.IO.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + if ((g.IO.BackendFlags & ImGuiBackendFlags_PlatformHasViewports) && (g.IO.BackendFlags & ImGuiBackendFlags_RendererHasViewports)) + { + IM_ASSERT((g.FrameCount == 0 || g.FrameCount == g.FrameCountPlatformEnded) && "Forgot to call UpdatePlatformWindows() in main loop after EndFrame()? Check examples/ applications for reference."); + IM_ASSERT(g.PlatformIO.Platform_CreateWindow != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_DestroyWindow != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_GetWindowPos != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_SetWindowPos != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_GetWindowSize != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Platform_SetWindowSize != NULL && "Platform init didn't install handlers?"); + IM_ASSERT(g.PlatformIO.Monitors.Size > 0 && "Platform init didn't setup Monitors list?"); + IM_ASSERT((g.Viewports[0]->PlatformUserData != NULL || g.Viewports[0]->PlatformHandle != NULL) && "Platform init didn't setup main viewport."); + if (g.IO.ConfigDockingTransparentPayload && (g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + IM_ASSERT(g.PlatformIO.Platform_SetWindowAlpha != NULL && "Platform_SetWindowAlpha handler is required to use io.ConfigDockingTransparent!"); + } + else + { + // Disable feature, our backends do not support it + g.IO.ConfigFlags &= ~ImGuiConfigFlags_ViewportsEnable; + } + + // Perform simple checks on platform monitor data + compute a total bounding box for quick early outs + for (ImGuiPlatformMonitor& mon : g.PlatformIO.Monitors) + { + IM_UNUSED(mon); + IM_ASSERT(mon.MainSize.x > 0.0f && mon.MainSize.y > 0.0f && "Monitor main bounds not setup properly."); + IM_ASSERT(ImRect(mon.MainPos, mon.MainPos + mon.MainSize).Contains(ImRect(mon.WorkPos, mon.WorkPos + mon.WorkSize)) && "Monitor work bounds not setup properly. If you don't have work area information, just copy MainPos/MainSize into them."); + IM_ASSERT(mon.DpiScale > 0.0f && mon.DpiScale < 99.0f && "Monitor DpiScale is invalid."); // Typical correct values would be between 1.0f and 4.0f + } + } +} + +static void ImGui::ErrorCheckEndFrameSanityChecks() +{ + // Verify that io.KeyXXX fields haven't been tampered with. Key mods should not be modified between NewFrame() and EndFrame() + // One possible reason leading to this assert is that your backends update inputs _AFTER_ NewFrame(). + // It is known that when some modal native windows called mid-frame takes focus away, some backends such as GLFW will + // send key release events mid-frame. This would normally trigger this assertion and lead to sheared inputs. + // We silently accommodate for this case by ignoring the case where all io.KeyXXX modifiers were released (aka key_mod_flags == 0), + // while still correctly asserting on mid-frame key press events. + ImGuiContext& g = *GImGui; + const ImGuiKeyChord key_mods = GetMergedModsFromKeys(); + IM_UNUSED(g); + IM_UNUSED(key_mods); + IM_ASSERT((key_mods == 0 || g.IO.KeyMods == key_mods) && "Mismatching io.KeyCtrl/io.KeyShift/io.KeyAlt/io.KeySuper vs io.KeyMods"); + IM_UNUSED(key_mods); + + IM_ASSERT(g.CurrentWindowStack.Size == 1); + IM_ASSERT(g.CurrentWindowStack[0].Window->IsFallbackWindow); +} + +// Save current stack sizes. Called e.g. by NewFrame() and by Begin() but may be called for manual recovery. +void ImGui::ErrorRecoveryStoreState(ImGuiErrorRecoveryState* state_out) +{ + ImGuiContext& g = *GImGui; + state_out->SizeOfWindowStack = (short)g.CurrentWindowStack.Size; + state_out->SizeOfIDStack = (short)g.CurrentWindow->IDStack.Size; + state_out->SizeOfTreeStack = (short)g.CurrentWindow->DC.TreeDepth; // NOT g.TreeNodeStack.Size which is a partial stack! + state_out->SizeOfColorStack = (short)g.ColorStack.Size; + state_out->SizeOfStyleVarStack = (short)g.StyleVarStack.Size; + state_out->SizeOfFontStack = (short)g.FontStack.Size; + state_out->SizeOfFocusScopeStack = (short)g.FocusScopeStack.Size; + state_out->SizeOfGroupStack = (short)g.GroupStack.Size; + state_out->SizeOfItemFlagsStack = (short)g.ItemFlagsStack.Size; + state_out->SizeOfBeginPopupStack = (short)g.BeginPopupStack.Size; + state_out->SizeOfDisabledStack = (short)g.DisabledStackSize; +} + +// Chosen name "Try to recover" over e.g. "Restore" to suggest this is not a 100% guaranteed recovery. +// Called by e.g. EndFrame() but may be called for manual recovery. +// Attempt to recover full window stack. +void ImGui::ErrorRecoveryTryToRecoverState(const ImGuiErrorRecoveryState* state_in) +{ + // PVS-Studio V1044 is "Loop break conditions do not depend on the number of iterations" + ImGuiContext& g = *GImGui; + while (g.CurrentWindowStack.Size > state_in->SizeOfWindowStack) //-V1044 + { + // Recap: + // - Begin()/BeginChild() return false to indicate the window is collapsed or fully clipped. + // - Always call a matching End() for each Begin() call, regardless of its return value! + // - Begin/End and BeginChild/EndChild logic is KNOWN TO BE INCONSISTENT WITH ALL OTHER BEGIN/END FUNCTIONS. + // - We will fix that in a future major update. + ImGuiWindow* window = g.CurrentWindow; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + { + if (g.CurrentTable != NULL && g.CurrentTable->InnerWindow == g.CurrentWindow) + { + IM_ASSERT_USER_ERROR(0, "Missing EndTable()"); + EndTable(); + } + else + { + IM_ASSERT_USER_ERROR(0, "Missing EndChild()"); + EndChild(); + } + } + else + { + IM_ASSERT_USER_ERROR(0, "Missing End()"); + End(); + } + } + if (g.CurrentWindowStack.Size == state_in->SizeOfWindowStack) + ErrorRecoveryTryToRecoverWindowState(state_in); +} + +// Called by e.g. End() but may be called for manual recovery. +// Read '// Error Handling [BETA]' block in imgui_internal.h for details. +// Attempt to recover from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls. +void ImGui::ErrorRecoveryTryToRecoverWindowState(const ImGuiErrorRecoveryState* state_in) +{ + ImGuiContext& g = *GImGui; + + while (g.CurrentTable != NULL && g.CurrentTable->InnerWindow == g.CurrentWindow) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndTable()"); + EndTable(); + } + + ImGuiWindow* window = g.CurrentWindow; + + // FIXME: Can't recover from inside BeginTabItem/EndTabItem yet. + while (g.CurrentTabBar != NULL && g.CurrentTabBar->Window == window) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndTabBar()"); + EndTabBar(); + } + while (g.CurrentMultiSelect != NULL && g.CurrentMultiSelect->Storage->Window == window) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndMultiSelect()"); + EndMultiSelect(); + } + if (window->DC.MenuBarAppending) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndMenuBar()"); + EndMenuBar(); + } + while (window->DC.TreeDepth > state_in->SizeOfTreeStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing TreePop()"); + TreePop(); + } + while (g.GroupStack.Size > state_in->SizeOfGroupStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndGroup()"); + EndGroup(); + } + IM_ASSERT(g.GroupStack.Size == state_in->SizeOfGroupStack); + while (window->IDStack.Size > state_in->SizeOfIDStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopID()"); + PopID(); + } + while (g.DisabledStackSize > state_in->SizeOfDisabledStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing EndDisabled()"); + if (g.CurrentItemFlags & ImGuiItemFlags_Disabled) + EndDisabled(); + else + { + EndDisabledOverrideReenable(); + g.CurrentWindowStack.back().DisabledOverrideReenable = false; + } + } + IM_ASSERT(g.DisabledStackSize == state_in->SizeOfDisabledStack); + while (g.ColorStack.Size > state_in->SizeOfColorStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopStyleColor()"); + PopStyleColor(); + } + while (g.ItemFlagsStack.Size > state_in->SizeOfItemFlagsStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopItemFlag()"); + PopItemFlag(); + } + while (g.StyleVarStack.Size > state_in->SizeOfStyleVarStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopStyleVar()"); + PopStyleVar(); + } + while (g.FontStack.Size > state_in->SizeOfFontStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopFont()"); + PopFont(); + } + while (g.FocusScopeStack.Size > state_in->SizeOfFocusScopeStack) //-V1044 + { + IM_ASSERT_USER_ERROR(0, "Missing PopFocusScope()"); + PopFocusScope(); + } + //IM_ASSERT(g.FocusScopeStack.Size == state_in->SizeOfFocusScopeStack); +} + +bool ImGui::ErrorLog(const char* msg) +{ + ImGuiContext& g = *GImGui; + + // Output to debug log +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiWindow* window = g.CurrentWindow; + + if (g.IO.ConfigErrorRecoveryEnableDebugLog) + { + if (g.ErrorFirst) + IMGUI_DEBUG_LOG_ERROR("[imgui-error] (current settings: Assert=%d, Log=%d, Tooltip=%d)\n", + g.IO.ConfigErrorRecoveryEnableAssert, g.IO.ConfigErrorRecoveryEnableDebugLog, g.IO.ConfigErrorRecoveryEnableTooltip); + IMGUI_DEBUG_LOG_ERROR("[imgui-error] In window '%s': %s\n", window ? window->Name : "NULL", msg); + } + g.ErrorFirst = false; + + // Output to tooltip + if (g.IO.ConfigErrorRecoveryEnableTooltip) + { + if (g.WithinFrameScope && BeginErrorTooltip()) + { + if (g.ErrorCountCurrentFrame < 20) + { + Text("In window '%s': %s", window ? window->Name : "NULL", msg); + if (window && (!window->IsFallbackWindow || window->WasActive)) + GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 0, 0, 255)); + } + if (g.ErrorCountCurrentFrame == 20) + Text("(and more errors)"); + // EndFrame() will amend debug buttons to this window, after all errors have been submitted. + EndErrorTooltip(); + } + g.ErrorCountCurrentFrame++; + } +#endif + + // Output to callback + if (g.ErrorCallback != NULL) + g.ErrorCallback(&g, g.ErrorCallbackUserData, msg); + + // Return whether we should assert + return g.IO.ConfigErrorRecoveryEnableAssert; +} + +void ImGui::ErrorCheckEndFrameFinalizeErrorTooltip() +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + if (g.DebugDrawIdConflicts != 0 && g.IO.KeyCtrl == false) + g.DebugDrawIdConflictsCount = g.HoveredIdPreviousFrameItemCount; + if (g.DebugDrawIdConflicts != 0 && g.DebugItemPickerActive == false && BeginErrorTooltip()) + { + Text("Programmer error: %d visible items with conflicting ID!", g.DebugDrawIdConflictsCount); + BulletText("Code should use PushID()/PopID() in loops, or append \"##xx\" to same-label identifiers!"); + BulletText("Empty label e.g. Button(\"\") == same ID as parent widget/node. Use Button(\"##xx\") instead!"); + //BulletText("Code intending to use duplicate ID may use e.g. PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag()"); // Not making this too visible for fear of it being abused. + BulletText("Set io.ConfigDebugHighlightIdConflicts=false to disable this warning in non-programmers builds."); + Separator(); + if (g.IO.ConfigDebugHighlightIdConflictsShowItemPicker) + { + Text("(Hold CTRL to: use "); + SameLine(0.0f, 0.0f); + if (SmallButton("Item Picker")) + DebugStartItemPicker(); + SameLine(0.0f, 0.0f); + Text(" to break in item call-stack, or "); + } + else + { + Text("(Hold CTRL to "); + } + SameLine(0.0f, 0.0f); + if (SmallButton("Open FAQ->About ID Stack System") && g.PlatformIO.Platform_OpenInShellFn != NULL) + g.PlatformIO.Platform_OpenInShellFn(&g, "https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#qa-usage"); + SameLine(0.0f, 0.0f); + Text(")"); + EndErrorTooltip(); + } + + if (g.ErrorCountCurrentFrame > 0 && BeginErrorTooltip()) // Amend at end of frame + { + Separator(); + Text("(Hold CTRL to:"); + SameLine(); + if (SmallButton("Enable Asserts")) + g.IO.ConfigErrorRecoveryEnableAssert = true; + //SameLine(); + //if (SmallButton("Hide Error Tooltips")) + // g.IO.ConfigErrorRecoveryEnableTooltip = false; // Too dangerous + SameLine(0, 0); + Text(")"); + EndErrorTooltip(); + } +#endif +} + +// Pseudo-tooltip. Follow mouse until CTRL is held. When CTRL is held we lock position, allowing to click it. +bool ImGui::BeginErrorTooltip() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = FindWindowByName("##Tooltip_Error"); + const bool use_locked_pos = (g.IO.KeyCtrl && window && window->WasActive); + PushStyleColor(ImGuiCol_PopupBg, ImLerp(g.Style.Colors[ImGuiCol_PopupBg], ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 0.15f)); + if (use_locked_pos) + SetNextWindowPos(g.ErrorTooltipLockedPos); + bool is_visible = Begin("##Tooltip_Error", NULL, ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); + PopStyleColor(); + if (is_visible && g.CurrentWindow->BeginCount == 1) + { + SeparatorText("MESSAGE FROM DEAR IMGUI"); + BringWindowToDisplayFront(g.CurrentWindow); + BringWindowToFocusFront(g.CurrentWindow); + g.ErrorTooltipLockedPos = GetWindowPos(); + } + else if (!is_visible) + { + End(); + } + return is_visible; +} + +void ImGui::EndErrorTooltip() +{ + End(); +} + +//----------------------------------------------------------------------------- +// [SECTION] ITEM SUBMISSION +//----------------------------------------------------------------------------- +// - KeepAliveID() +// - ItemAdd() +//----------------------------------------------------------------------------- + +// Code not using ItemAdd() may need to call this manually otherwise ActiveId will be cleared. In IMGUI_VERSION_NUM < 18717 this was called by GetID(). +void ImGui::KeepAliveID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + if (g.ActiveId == id) + g.ActiveIdIsAlive = id; + if (g.DeactivatedItemData.ID == id) + g.DeactivatedItemData.IsAlive = true; +} + +// Declare item bounding box for clipping and interaction. +// Note that the size can be different than the one provided to ItemSize(). Typically, widgets that spread over available surface +// declare their minimum size requirement to ItemSize() and provide a larger region to ItemAdd() which is used drawing/interaction. +// THIS IS IN THE PERFORMANCE CRITICAL PATH (UNTIL THE CLIPPING TEST AND EARLY-RETURN) +IM_MSVC_RUNTIME_CHECKS_OFF +bool ImGui::ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb_arg, ImGuiItemFlags extra_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Set item data + // (DisplayRect is left untouched, made valid when ImGuiItemStatusFlags_HasDisplayRect is set) + g.LastItemData.ID = id; + g.LastItemData.Rect = bb; + g.LastItemData.NavRect = nav_bb_arg ? *nav_bb_arg : bb; + g.LastItemData.ItemFlags = g.CurrentItemFlags | g.NextItemData.ItemFlags | extra_flags; + g.LastItemData.StatusFlags = ImGuiItemStatusFlags_None; + // Note: we don't copy 'g.NextItemData.SelectionUserData' to an hypothetical g.LastItemData.SelectionUserData: since the former is not cleared. + + if (id != 0) + { + KeepAliveID(id); + + // Directional navigation processing + // Runs prior to clipping early-out + // (a) So that NavInitRequest can be honored, for newly opened windows to select a default widget + // (b) So that we can scroll up/down past clipped items. This adds a small O(N) cost to regular navigation requests + // unfortunately, but it is still limited to one window. It may not scale very well for windows with ten of + // thousands of item, but at least NavMoveRequest is only set on user interaction, aka maximum once a frame. + // We could early out with "if (is_clipped && !g.NavInitRequest) return false;" but when we wouldn't be able + // to reach unclipped widgets. This would work if user had explicit scrolling control (e.g. mapped on a stick). + // We intentionally don't check if g.NavWindow != NULL because g.NavAnyRequest should only be set when it is non null. + // If we crash on a NULL g.NavWindow we need to fix the bug elsewhere. + if (!(g.LastItemData.ItemFlags & ImGuiItemFlags_NoNav)) + { + // FIMXE-NAV: investigate changing the window tests into a simple 'if (g.NavFocusScopeId == g.CurrentFocusScopeId)' test. + window->DC.NavLayersActiveMaskNext |= (1 << window->DC.NavLayerCurrent); + if (g.NavId == id || g.NavAnyRequest) + if (g.NavWindow->RootWindowForNav == window->RootWindowForNav) + if (window == g.NavWindow || ((window->ChildFlags | g.NavWindow->ChildFlags) & ImGuiChildFlags_NavFlattened)) + NavProcessItem(); + } + + if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasShortcut) + ItemHandleShortcut(id); + } + + // Lightweight clear of SetNextItemXXX data. + g.NextItemData.HasFlags = ImGuiNextItemDataFlags_None; + g.NextItemData.ItemFlags = ImGuiItemFlags_None; + +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (id != 0) + IMGUI_TEST_ENGINE_ITEM_ADD(id, g.LastItemData.NavRect, &g.LastItemData); +#endif + + // Clipping test + // (this is an inline copy of IsClippedEx() so we can reuse the is_rect_visible value, otherwise we'd do 'if (IsClippedEx(bb, id)) return false') + // g.NavActivateId is not necessarily == g.NavId, in the case of remote activation (e.g. shortcuts) + const bool is_rect_visible = bb.Overlaps(window->ClipRect); + if (!is_rect_visible) + if (id == 0 || (id != g.ActiveId && id != g.ActiveIdPreviousFrame && id != g.NavId && id != g.NavActivateId)) + if (!g.ItemUnclipByLog) + return false; + + // [DEBUG] +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (id != 0) + { + if (id == g.DebugLocateId) + DebugLocateItemResolveWithLastItem(); + + // [DEBUG] People keep stumbling on this problem and using "" as identifier in the root of a window instead of "##something". + // Empty identifier are valid and useful in a small amount of cases, but 99.9% of the time you want to use "##something". + // READ THE FAQ: https://dearimgui.com/faq + IM_ASSERT(id != window->ID && "Cannot have an empty ID at the root of a window. If you need an empty label, use ## and read the FAQ about how the ID Stack works!"); + } + //if (g.IO.KeyAlt) window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255,255,0,120)); // [DEBUG] + //if ((g.LastItemData.ItemFlags & ImGuiItemFlags_NoNav) == 0) + // window->DrawList->AddRect(g.LastItemData.NavRect.Min, g.LastItemData.NavRect.Max, IM_COL32(255,255,0,255)); // [DEBUG] +#endif + + if (id != 0 && g.DeactivatedItemData.ID == id) + g.DeactivatedItemData.ElapseFrame = g.FrameCount; + + // We need to calculate this now to take account of the current clipping rectangle (as items like Selectable may change them) + if (is_rect_visible) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Visible; + if (IsMouseHoveringRect(bb.Min, bb.Max)) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; + return true; +} +IM_MSVC_RUNTIME_CHECKS_RESTORE + +//----------------------------------------------------------------------------- +// [SECTION] LAYOUT +//----------------------------------------------------------------------------- +// - ItemSize() +// - SameLine() +// - GetCursorScreenPos() +// - SetCursorScreenPos() +// - GetCursorPos(), GetCursorPosX(), GetCursorPosY() +// - SetCursorPos(), SetCursorPosX(), SetCursorPosY() +// - GetCursorStartPos() +// - Indent() +// - Unindent() +// - SetNextItemWidth() +// - PushItemWidth() +// - PushMultiItemsWidths() +// - PopItemWidth() +// - CalcItemWidth() +// - CalcItemSize() +// - GetTextLineHeight() +// - GetTextLineHeightWithSpacing() +// - GetFrameHeight() +// - GetFrameHeightWithSpacing() +// - GetContentRegionMax() +// - GetContentRegionAvail(), +// - BeginGroup() +// - EndGroup() +// Also see in imgui_widgets: tab bars, and in imgui_tables: tables, columns. +//----------------------------------------------------------------------------- + +// Advance cursor given item size for layout. +// Register minimum needed size so it can extend the bounding box used for auto-fit calculation. +// See comments in ItemAdd() about how/why the size provided to ItemSize() vs ItemAdd() may often different. +// THIS IS IN THE PERFORMANCE CRITICAL PATH. +IM_MSVC_RUNTIME_CHECKS_OFF +void ImGui::ItemSize(const ImVec2& size, float text_baseline_y) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + // We increase the height in this function to accommodate for baseline offset. + // In theory we should be offsetting the starting position (window->DC.CursorPos), that will be the topic of a larger refactor, + // but since ItemSize() is not yet an API that moves the cursor (to handle e.g. wrapping) enlarging the height has the same effect. + const float offset_to_match_baseline_y = (text_baseline_y >= 0) ? ImMax(0.0f, window->DC.CurrLineTextBaseOffset - text_baseline_y) : 0.0f; + + const float line_y1 = window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y; + const float line_height = ImMax(window->DC.CurrLineSize.y, /*ImMax(*/window->DC.CursorPos.y - line_y1/*, 0.0f)*/ + size.y + offset_to_match_baseline_y); + + // Always align ourselves on pixel boundaries + //if (g.IO.KeyAlt) window->DrawList->AddRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(size.x, line_height), IM_COL32(255,0,0,200)); // [DEBUG] + window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x + size.x; + window->DC.CursorPosPrevLine.y = line_y1; + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); // Next line + window->DC.CursorPos.y = IM_TRUNC(line_y1 + line_height + g.Style.ItemSpacing.y); // Next line + window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPosPrevLine.x); + window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y - g.Style.ItemSpacing.y); + //if (g.IO.KeyAlt) window->DrawList->AddCircle(window->DC.CursorMaxPos, 3.0f, IM_COL32(255,0,0,255), 4); // [DEBUG] + + window->DC.PrevLineSize.y = line_height; + window->DC.CurrLineSize.y = 0.0f; + window->DC.PrevLineTextBaseOffset = ImMax(window->DC.CurrLineTextBaseOffset, text_baseline_y); + window->DC.CurrLineTextBaseOffset = 0.0f; + window->DC.IsSameLine = window->DC.IsSetPos = false; + + // Horizontal layout mode + if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) + SameLine(); +} +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Gets back to previous line and continue with horizontal layout +// offset_from_start_x == 0 : follow right after previous item +// offset_from_start_x != 0 : align to specified x position (relative to window/group left) +// spacing_w < 0 : use default spacing if offset_from_start_x == 0, no spacing if offset_from_start_x != 0 +// spacing_w >= 0 : enforce spacing amount +void ImGui::SameLine(float offset_from_start_x, float spacing_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + if (offset_from_start_x != 0.0f) + { + if (spacing_w < 0.0f) + spacing_w = 0.0f; + window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + offset_from_start_x + spacing_w + window->DC.GroupOffset.x + window->DC.ColumnsOffset.x; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + else + { + if (spacing_w < 0.0f) + spacing_w = g.Style.ItemSpacing.x; + window->DC.CursorPos.x = window->DC.CursorPosPrevLine.x + spacing_w; + window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; + } + window->DC.CurrLineSize = window->DC.PrevLineSize; + window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; + window->DC.IsSameLine = true; +} + +ImVec2 ImGui::GetCursorScreenPos() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CursorPos; +} + +void ImGui::SetCursorScreenPos(const ImVec2& pos) +{ + ImGuiWindow* window = GetCurrentWindow(); + window->DC.CursorPos = pos; + //window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + window->DC.IsSetPos = true; } // User generally sees positions in window coordinates. Internally we store CursorPos in absolute screen coordinates because it is more convenient. @@ -6909,21 +11464,24 @@ void ImGui::SetCursorPos(const ImVec2& local_pos) { ImGuiWindow* window = GetCurrentWindow(); window->DC.CursorPos = window->Pos - window->Scroll + local_pos; - window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + //window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); + window->DC.IsSetPos = true; } void ImGui::SetCursorPosX(float x) { ImGuiWindow* window = GetCurrentWindow(); window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + x; - window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPos.x); + //window->DC.CursorMaxPos.x = ImMax(window->DC.CursorMaxPos.x, window->DC.CursorPos.x); + window->DC.IsSetPos = true; } void ImGui::SetCursorPosY(float y) { ImGuiWindow* window = GetCurrentWindow(); window->DC.CursorPos.y = window->Pos.y - window->Scroll.y + y; - window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y); + //window->DC.CursorMaxPos.y = ImMax(window->DC.CursorMaxPos.y, window->DC.CursorPos.y); + window->DC.IsSetPos = true; } ImVec2 ImGui::GetCursorStartPos() @@ -6932,239 +11490,6 @@ ImVec2 ImGui::GetCursorStartPos() return window->DC.CursorStartPos - window->Pos; } -ImVec2 ImGui::GetCursorScreenPos() -{ - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.CursorPos; -} - -void ImGui::SetCursorScreenPos(const ImVec2& pos) -{ - ImGuiWindow* window = GetCurrentWindow(); - window->DC.CursorPos = pos; - window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, window->DC.CursorPos); -} - -void ImGui::ActivateItem(ImGuiID id) -{ - ImGuiContext& g = *GImGui; - g.NavNextActivateId = id; -} - -void ImGui::SetKeyboardFocusHere(int offset) -{ - IM_ASSERT(offset >= -1); // -1 is allowed but not below - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - g.FocusRequestNextWindow = window; - g.FocusRequestNextCounterAll = window->DC.FocusCounterAll + 1 + offset; - g.FocusRequestNextCounterTab = INT_MAX; -} - -void ImGui::SetItemDefaultFocus() -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - if (!window->Appearing) - return; - if (g.NavWindow == window->RootWindowForNav && (g.NavInitRequest || g.NavInitResultId != 0) && g.NavLayer == g.NavWindow->DC.NavLayerCurrent) - { - g.NavInitRequest = false; - g.NavInitResultId = g.NavWindow->DC.LastItemId; - g.NavInitResultRectRel = ImRect(g.NavWindow->DC.LastItemRect.Min - g.NavWindow->Pos, g.NavWindow->DC.LastItemRect.Max - g.NavWindow->Pos); - NavUpdateAnyRequestFlag(); - if (!IsItemVisible()) - SetScrollHereY(); - } -} - -void ImGui::SetStateStorage(ImGuiStorage* tree) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - window->DC.StateStorage = tree ? tree : &window->StateStorage; -} - -ImGuiStorage* ImGui::GetStateStorage() -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->DC.StateStorage; -} - -void ImGui::PushID(const char* str_id) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - window->IDStack.push_back(window->GetIDNoKeepAlive(str_id)); -} - -void ImGui::PushID(const char* str_id_begin, const char* str_id_end) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - window->IDStack.push_back(window->GetIDNoKeepAlive(str_id_begin, str_id_end)); -} - -void ImGui::PushID(const void* ptr_id) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - window->IDStack.push_back(window->GetIDNoKeepAlive(ptr_id)); -} - -void ImGui::PushID(int int_id) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - window->IDStack.push_back(window->GetIDNoKeepAlive(int_id)); -} - -// Push a given id value ignoring the ID stack as a seed. -void ImGui::PushOverrideID(ImGuiID id) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - window->IDStack.push_back(id); -} - -void ImGui::PopID() -{ - ImGuiWindow* window = GImGui->CurrentWindow; - window->IDStack.pop_back(); -} - -ImGuiID ImGui::GetID(const char* str_id) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->GetID(str_id); -} - -ImGuiID ImGui::GetID(const char* str_id_begin, const char* str_id_end) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->GetID(str_id_begin, str_id_end); -} - -ImGuiID ImGui::GetID(const void* ptr_id) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->GetID(ptr_id); -} - -bool ImGui::IsRectVisible(const ImVec2& size) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->ClipRect.Overlaps(ImRect(window->DC.CursorPos, window->DC.CursorPos + size)); -} - -bool ImGui::IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - return window->ClipRect.Overlaps(ImRect(rect_min, rect_max)); -} - -// Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) -void ImGui::BeginGroup() -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = GetCurrentWindow(); - - window->DC.GroupStack.resize(window->DC.GroupStack.Size + 1); - ImGuiGroupData& group_data = window->DC.GroupStack.back(); - group_data.BackupCursorPos = window->DC.CursorPos; - group_data.BackupCursorMaxPos = window->DC.CursorMaxPos; - group_data.BackupIndent = window->DC.Indent; - group_data.BackupGroupOffset = window->DC.GroupOffset; - group_data.BackupCurrLineSize = window->DC.CurrLineSize; - group_data.BackupCurrLineTextBaseOffset = window->DC.CurrLineTextBaseOffset; - group_data.BackupActiveIdIsAlive = g.ActiveIdIsAlive; - group_data.BackupActiveIdPreviousFrameIsAlive = g.ActiveIdPreviousFrameIsAlive; - group_data.EmitItem = true; - - window->DC.GroupOffset.x = window->DC.CursorPos.x - window->Pos.x - window->DC.ColumnsOffset.x; - window->DC.Indent = window->DC.GroupOffset; - window->DC.CursorMaxPos = window->DC.CursorPos; - window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); - if (g.LogEnabled) - g.LogLinePosY = -FLT_MAX; // To enforce Log carriage return -} - -void ImGui::EndGroup() -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = GetCurrentWindow(); - IM_ASSERT(!window->DC.GroupStack.empty()); // Mismatched BeginGroup()/EndGroup() calls - - ImGuiGroupData& group_data = window->DC.GroupStack.back(); - - ImRect group_bb(group_data.BackupCursorPos, ImMax(window->DC.CursorMaxPos, group_data.BackupCursorPos)); - - window->DC.CursorPos = group_data.BackupCursorPos; - window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, window->DC.CursorMaxPos); - window->DC.Indent = group_data.BackupIndent; - window->DC.GroupOffset = group_data.BackupGroupOffset; - window->DC.CurrLineSize = group_data.BackupCurrLineSize; - window->DC.CurrLineTextBaseOffset = group_data.BackupCurrLineTextBaseOffset; - if (g.LogEnabled) - g.LogLinePosY = -FLT_MAX; // To enforce Log carriage return - - if (!group_data.EmitItem) - { - window->DC.GroupStack.pop_back(); - return; - } - - window->DC.CurrLineTextBaseOffset = ImMax(window->DC.PrevLineTextBaseOffset, group_data.BackupCurrLineTextBaseOffset); // FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now. - ItemSize(group_bb.GetSize(), 0.0f); - ItemAdd(group_bb, 0); - - // If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive(), IsItemDeactivated() etc. will be functional on the entire group. - // It would be be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but would put a little more burden on individual widgets. - // Also if you grep for LastItemId you'll notice it is only used in that context. - // (The tests not symmetrical because ActiveIdIsAlive is an ID itself, in order to be able to handle ActiveId being overwritten during the frame.) - const bool group_contains_curr_active_id = (group_data.BackupActiveIdIsAlive != g.ActiveId) && (g.ActiveIdIsAlive == g.ActiveId) && g.ActiveId; - const bool group_contains_prev_active_id = !group_data.BackupActiveIdPreviousFrameIsAlive && g.ActiveIdPreviousFrameIsAlive; - if (group_contains_curr_active_id) - window->DC.LastItemId = g.ActiveId; - else if (group_contains_prev_active_id) - window->DC.LastItemId = g.ActiveIdPreviousFrame; - window->DC.LastItemRect = group_bb; - - // Forward Edited flag - if (group_contains_curr_active_id && g.ActiveIdHasBeenEditedThisFrame) - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_Edited; - - // Forward Deactivated flag - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HasDeactivated; - if (group_contains_prev_active_id && g.ActiveId != g.ActiveIdPreviousFrame) - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_Deactivated; - - window->DC.GroupStack.pop_back(); - //window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] -} - -// Gets back to previous line and continue with horizontal layout -// offset_from_start_x == 0 : follow right after previous item -// offset_from_start_x != 0 : align to specified x position (relative to window/group left) -// spacing_w < 0 : use default spacing if pos_x == 0, no spacing if pos_x != 0 -// spacing_w >= 0 : enforce spacing amount -void ImGui::SameLine(float offset_from_start_x, float spacing_w) -{ - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems) - return; - - ImGuiContext& g = *GImGui; - if (offset_from_start_x != 0.0f) - { - if (spacing_w < 0.0f) spacing_w = 0.0f; - window->DC.CursorPos.x = window->Pos.x - window->Scroll.x + offset_from_start_x + spacing_w + window->DC.GroupOffset.x + window->DC.ColumnsOffset.x; - window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; - } - else - { - if (spacing_w < 0.0f) spacing_w = g.Style.ItemSpacing.x; - window->DC.CursorPos.x = window->DC.CursorPosPrevLine.x + spacing_w; - window->DC.CursorPos.y = window->DC.CursorPosPrevLine.y; - } - window->DC.CurrLineSize = window->DC.PrevLineSize; - window->DC.CurrLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; -} - void ImGui::Indent(float indent_w) { ImGuiContext& g = *GImGui; @@ -7181,72 +11506,375 @@ void ImGui::Unindent(float indent_w) window->DC.CursorPos.x = window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x; } +// Affect large frame+labels widgets only. +void ImGui::SetNextItemWidth(float item_width) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasWidth; + g.NextItemData.Width = item_width; +} + +// FIXME: Remove the == 0.0f behavior? +void ImGui::PushItemWidth(float item_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width + window->DC.ItemWidth = (item_width == 0.0f ? window->ItemWidthDefault : item_width); + g.NextItemData.HasFlags &= ~ImGuiNextItemDataFlags_HasWidth; +} + +void ImGui::PushMultiItemsWidths(int components, float w_full) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(components > 0); + const ImGuiStyle& style = g.Style; + window->DC.ItemWidthStack.push_back(window->DC.ItemWidth); // Backup current width + float w_items = w_full - style.ItemInnerSpacing.x * (components - 1); + float prev_split = w_items; + for (int i = components - 1; i > 0; i--) + { + float next_split = IM_TRUNC(w_items * i / components); + window->DC.ItemWidthStack.push_back(ImMax(prev_split - next_split, 1.0f)); + prev_split = next_split; + } + window->DC.ItemWidth = ImMax(prev_split, 1.0f); + g.NextItemData.HasFlags &= ~ImGuiNextItemDataFlags_HasWidth; +} + +void ImGui::PopItemWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->DC.ItemWidthStack.Size <= 0) + { + IM_ASSERT_USER_ERROR(0, "Calling PopItemWidth() too many times!"); + return; + } + window->DC.ItemWidth = window->DC.ItemWidthStack.back(); + window->DC.ItemWidthStack.pop_back(); +} + +// Calculate default item width given value passed to PushItemWidth() or SetNextItemWidth(). +// The SetNextItemWidth() data is generally cleared/consumed by ItemAdd() or NextItemData.ClearFlags() +float ImGui::CalcItemWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float w; + if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth) + w = g.NextItemData.Width; + else + w = window->DC.ItemWidth; + if (w < 0.0f) + { + float region_avail_x = GetContentRegionAvail().x; + w = ImMax(1.0f, region_avail_x + w); + } + w = IM_TRUNC(w); + return w; +} + +// [Internal] Calculate full item size given user provided 'size' parameter and default width/height. Default width is often == CalcItemWidth(). +// Those two functions CalcItemWidth vs CalcItemSize are awkwardly named because they are not fully symmetrical. +// Note that only CalcItemWidth() is publicly exposed. +// The 4.0f here may be changed to match CalcItemWidth() and/or BeginChild() (right now we have a mismatch which is harmless but undesirable) +ImVec2 ImGui::CalcItemSize(ImVec2 size, float default_w, float default_h) +{ + ImVec2 avail; + if (size.x < 0.0f || size.y < 0.0f) + avail = GetContentRegionAvail(); + + if (size.x == 0.0f) + size.x = default_w; + else if (size.x < 0.0f) + size.x = ImMax(4.0f, avail.x + size.x); // <-- size.x is negative here so we are subtracting + + if (size.y == 0.0f) + size.y = default_h; + else if (size.y < 0.0f) + size.y = ImMax(4.0f, avail.y + size.y); // <-- size.y is negative here so we are subtracting + + return size; +} + +float ImGui::GetTextLineHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize; +} + +float ImGui::GetTextLineHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.ItemSpacing.y; +} + +float ImGui::GetFrameHeight() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f; +} + +float ImGui::GetFrameHeightWithSpacing() +{ + ImGuiContext& g = *GImGui; + return g.FontSize + g.Style.FramePadding.y * 2.0f + g.Style.ItemSpacing.y; +} + +ImVec2 ImGui::GetContentRegionAvail() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 mx = (window->DC.CurrentColumns || g.CurrentTable) ? window->WorkRect.Max : window->ContentRegionRect.Max; + return mx - window->DC.CursorPos; +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// You should never need those functions. Always use GetCursorScreenPos() and GetContentRegionAvail()! +// They are bizarre local-coordinates which don't play well with scrolling. +ImVec2 ImGui::GetContentRegionMax() +{ + return GetContentRegionAvail() + GetCursorScreenPos() - GetWindowPos(); +} + +ImVec2 ImGui::GetWindowContentRegionMin() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ContentRegionRect.Min - window->Pos; +} + +ImVec2 ImGui::GetWindowContentRegionMax() +{ + ImGuiWindow* window = GImGui->CurrentWindow; + return window->ContentRegionRect.Max - window->Pos; +} +#endif + +// Lock horizontal starting position + capture group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) +// Groups are currently a mishmash of functionalities which should perhaps be clarified and separated. +// FIXME-OPT: Could we safely early out on ->SkipItems? +void ImGui::BeginGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + g.GroupStack.resize(g.GroupStack.Size + 1); + ImGuiGroupData& group_data = g.GroupStack.back(); + group_data.WindowID = window->ID; + group_data.BackupCursorPos = window->DC.CursorPos; + group_data.BackupCursorPosPrevLine = window->DC.CursorPosPrevLine; + group_data.BackupCursorMaxPos = window->DC.CursorMaxPos; + group_data.BackupIndent = window->DC.Indent; + group_data.BackupGroupOffset = window->DC.GroupOffset; + group_data.BackupCurrLineSize = window->DC.CurrLineSize; + group_data.BackupCurrLineTextBaseOffset = window->DC.CurrLineTextBaseOffset; + group_data.BackupActiveIdIsAlive = g.ActiveIdIsAlive; + group_data.BackupHoveredIdIsAlive = g.HoveredId != 0; + group_data.BackupIsSameLine = window->DC.IsSameLine; + group_data.BackupDeactivatedIdIsAlive = g.DeactivatedItemData.IsAlive; + group_data.EmitItem = true; + + window->DC.GroupOffset.x = window->DC.CursorPos.x - window->Pos.x - window->DC.ColumnsOffset.x; + window->DC.Indent = window->DC.GroupOffset; + window->DC.CursorMaxPos = window->DC.CursorPos; + window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + if (g.LogEnabled) + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return +} + +void ImGui::EndGroup() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(g.GroupStack.Size > 0); // Mismatched BeginGroup()/EndGroup() calls + + ImGuiGroupData& group_data = g.GroupStack.back(); + IM_ASSERT(group_data.WindowID == window->ID); // EndGroup() in wrong window? + + if (window->DC.IsSetPos) + ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + + // Include LastItemData.Rect.Max as a workaround for e.g. EndTable() undershooting with CursorMaxPos report. (#7543) + ImRect group_bb(group_data.BackupCursorPos, ImMax(ImMax(window->DC.CursorMaxPos, g.LastItemData.Rect.Max), group_data.BackupCursorPos)); + window->DC.CursorPos = group_data.BackupCursorPos; + window->DC.CursorPosPrevLine = group_data.BackupCursorPosPrevLine; + window->DC.CursorMaxPos = ImMax(group_data.BackupCursorMaxPos, group_bb.Max); + window->DC.Indent = group_data.BackupIndent; + window->DC.GroupOffset = group_data.BackupGroupOffset; + window->DC.CurrLineSize = group_data.BackupCurrLineSize; + window->DC.CurrLineTextBaseOffset = group_data.BackupCurrLineTextBaseOffset; + window->DC.IsSameLine = group_data.BackupIsSameLine; + if (g.LogEnabled) + g.LogLinePosY = -FLT_MAX; // To enforce a carriage return + + if (!group_data.EmitItem) + { + g.GroupStack.pop_back(); + return; + } + + window->DC.CurrLineTextBaseOffset = ImMax(window->DC.PrevLineTextBaseOffset, group_data.BackupCurrLineTextBaseOffset); // FIXME: Incorrect, we should grab the base offset from the *first line* of the group but it is hard to obtain now. + ItemSize(group_bb.GetSize()); + ItemAdd(group_bb, 0, NULL, ImGuiItemFlags_NoTabStop); + + // If the current ActiveId was declared within the boundary of our group, we copy it to LastItemId so IsItemActive(), IsItemDeactivated() etc. will be functional on the entire group. + // It would be neater if we replaced window.DC.LastItemId by e.g. 'bool LastItemIsActive', but would put a little more burden on individual widgets. + // Also if you grep for LastItemId you'll notice it is only used in that context. + // (The two tests not the same because ActiveIdIsAlive is an ID itself, in order to be able to handle ActiveId being overwritten during the frame.) + const bool group_contains_curr_active_id = (group_data.BackupActiveIdIsAlive != g.ActiveId) && (g.ActiveIdIsAlive == g.ActiveId) && g.ActiveId; + const bool group_contains_deactivated_id = (group_data.BackupDeactivatedIdIsAlive == false) && (g.DeactivatedItemData.IsAlive == true); + if (group_contains_curr_active_id) + g.LastItemData.ID = g.ActiveId; + else if (group_contains_deactivated_id) + g.LastItemData.ID = g.DeactivatedItemData.ID; + g.LastItemData.Rect = group_bb; + + // Forward Hovered flag + const bool group_contains_curr_hovered_id = (group_data.BackupHoveredIdIsAlive == false) && g.HoveredId != 0; + if (group_contains_curr_hovered_id) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + + // Forward Edited flag + if (group_contains_curr_active_id && g.ActiveIdHasBeenEditedThisFrame) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Edited; + + // Forward Deactivated flag + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDeactivated; + if (group_contains_deactivated_id) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_Deactivated; + + g.GroupStack.pop_back(); + if (g.DebugShowGroupRects) + window->DrawList->AddRect(group_bb.Min, group_bb.Max, IM_COL32(255,0,255,255)); // [Debug] +} + //----------------------------------------------------------------------------- // [SECTION] SCROLLING //----------------------------------------------------------------------------- -static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window, bool snap_on_edges) +// Helper to snap on edges when aiming at an item very close to the edge, +// So the difference between WindowPadding and ItemSpacing will be in the visible area after scrolling. +// When we refactor the scrolling API this may be configurable with a flag? +// Note that the effect for this won't be visible on X axis with default Style settings as WindowPadding.x == ItemSpacing.x by default. +static float CalcScrollEdgeSnap(float target, float snap_min, float snap_max, float snap_threshold, float center_ratio) +{ + if (target <= snap_min + snap_threshold) + return ImLerp(snap_min, target, center_ratio); + if (target >= snap_max - snap_threshold) + return ImLerp(target, snap_max, center_ratio); + return target; +} + +static ImVec2 CalcNextScrollFromScrollTargetAndClamp(ImGuiWindow* window) { - ImGuiContext& g = *GImGui; ImVec2 scroll = window->Scroll; - if (window->ScrollTarget.x < FLT_MAX) + ImVec2 decoration_size(window->DecoOuterSizeX1 + window->DecoInnerSizeX1 + window->DecoOuterSizeX2, window->DecoOuterSizeY1 + window->DecoInnerSizeY1 + window->DecoOuterSizeY2); + for (int axis = 0; axis < 2; axis++) { - float cr_x = window->ScrollTargetCenterRatio.x; - float target_x = window->ScrollTarget.x; - if (snap_on_edges && cr_x <= 0.0f && target_x <= window->WindowPadding.x) - target_x = 0.0f; - else if (snap_on_edges && cr_x >= 1.0f && target_x >= window->ContentSize.x + window->WindowPadding.x + g.Style.ItemSpacing.x) - target_x = window->ContentSize.x + window->WindowPadding.x * 2.0f; - scroll.x = target_x - cr_x * (window->SizeFull.x - window->ScrollbarSizes.x); - } - if (window->ScrollTarget.y < FLT_MAX) - { - // 'snap_on_edges' allows for a discontinuity at the edge of scrolling limits to take account of WindowPadding so that scrolling to make the last item visible scroll far enough to see the padding. - float decoration_up_height = window->TitleBarHeight() + window->MenuBarHeight(); - float cr_y = window->ScrollTargetCenterRatio.y; - float target_y = window->ScrollTarget.y; - if (snap_on_edges && cr_y <= 0.0f && target_y <= window->WindowPadding.y) - target_y = 0.0f; - if (snap_on_edges && cr_y >= 1.0f && target_y >= window->ContentSize.y + window->WindowPadding.y + g.Style.ItemSpacing.y) - target_y = window->ContentSize.y + window->WindowPadding.y * 2.0f; - scroll.y = target_y - cr_y * (window->SizeFull.y - window->ScrollbarSizes.y - decoration_up_height); - } - scroll = ImMax(scroll, ImVec2(0.0f, 0.0f)); - if (!window->Collapsed && !window->SkipItems) - { - scroll.x = ImMin(scroll.x, window->ScrollMax.x); - scroll.y = ImMin(scroll.y, window->ScrollMax.y); + if (window->ScrollTarget[axis] < FLT_MAX) + { + float center_ratio = window->ScrollTargetCenterRatio[axis]; + float scroll_target = window->ScrollTarget[axis]; + if (window->ScrollTargetEdgeSnapDist[axis] > 0.0f) + { + float snap_min = 0.0f; + float snap_max = window->ScrollMax[axis] + window->SizeFull[axis] - decoration_size[axis]; + scroll_target = CalcScrollEdgeSnap(scroll_target, snap_min, snap_max, window->ScrollTargetEdgeSnapDist[axis], center_ratio); + } + scroll[axis] = scroll_target - center_ratio * (window->SizeFull[axis] - decoration_size[axis]); + } + scroll[axis] = IM_ROUND(ImMax(scroll[axis], 0.0f)); + if (!window->Collapsed && !window->SkipItems) + scroll[axis] = ImMin(scroll[axis], window->ScrollMax[axis]); } return scroll; } -// Scroll to keep newly navigated item fully into view -ImVec2 ImGui::ScrollToBringRectIntoView(ImGuiWindow* window, const ImRect& item_rect) +void ImGui::ScrollToItem(ImGuiScrollFlags flags) { ImGuiContext& g = *GImGui; - ImRect window_rect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1)); - //GetForegroundDrawList(window)->AddRect(window_rect.Min, window_rect.Max, IM_COL32_WHITE); // [DEBUG] + ImGuiWindow* window = g.CurrentWindow; + ScrollToRectEx(window, g.LastItemData.NavRect, flags); +} - ImVec2 delta_scroll; - if (!window_rect.Contains(item_rect)) +void ImGui::ScrollToRect(ImGuiWindow* window, const ImRect& item_rect, ImGuiScrollFlags flags) +{ + ScrollToRectEx(window, item_rect, flags); +} + +// Scroll to keep newly navigated item fully into view +ImVec2 ImGui::ScrollToRectEx(ImGuiWindow* window, const ImRect& item_rect, ImGuiScrollFlags flags) +{ + ImGuiContext& g = *GImGui; + ImRect scroll_rect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1)); + scroll_rect.Min.x = ImMin(scroll_rect.Min.x + window->DecoInnerSizeX1, scroll_rect.Max.x); + scroll_rect.Min.y = ImMin(scroll_rect.Min.y + window->DecoInnerSizeY1, scroll_rect.Max.y); + //GetForegroundDrawList(window)->AddRect(item_rect.Min, item_rect.Max, IM_COL32(255,0,0,255), 0.0f, 0, 5.0f); // [DEBUG] + //GetForegroundDrawList(window)->AddRect(scroll_rect.Min, scroll_rect.Max, IM_COL32_WHITE); // [DEBUG] + + // Check that only one behavior is selected per axis + IM_ASSERT((flags & ImGuiScrollFlags_MaskX_) == 0 || ImIsPowerOfTwo(flags & ImGuiScrollFlags_MaskX_)); + IM_ASSERT((flags & ImGuiScrollFlags_MaskY_) == 0 || ImIsPowerOfTwo(flags & ImGuiScrollFlags_MaskY_)); + + // Defaults + ImGuiScrollFlags in_flags = flags; + if ((flags & ImGuiScrollFlags_MaskX_) == 0 && window->ScrollbarX) + flags |= ImGuiScrollFlags_KeepVisibleEdgeX; + if ((flags & ImGuiScrollFlags_MaskY_) == 0) + flags |= window->Appearing ? ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeY; + + const bool fully_visible_x = item_rect.Min.x >= scroll_rect.Min.x && item_rect.Max.x <= scroll_rect.Max.x; + const bool fully_visible_y = item_rect.Min.y >= scroll_rect.Min.y && item_rect.Max.y <= scroll_rect.Max.y; + const bool can_be_fully_visible_x = (item_rect.GetWidth() + g.Style.ItemSpacing.x * 2.0f) <= scroll_rect.GetWidth() || (window->AutoFitFramesX > 0) || (window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0; + const bool can_be_fully_visible_y = (item_rect.GetHeight() + g.Style.ItemSpacing.y * 2.0f) <= scroll_rect.GetHeight() || (window->AutoFitFramesY > 0) || (window->Flags & ImGuiWindowFlags_AlwaysAutoResize) != 0; + + if ((flags & ImGuiScrollFlags_KeepVisibleEdgeX) && !fully_visible_x) { - if (window->ScrollbarX && item_rect.Min.x < window_rect.Min.x) - SetScrollFromPosX(window, item_rect.Min.x - window->Pos.x + g.Style.ItemSpacing.x, 0.0f); - else if (window->ScrollbarX && item_rect.Max.x >= window_rect.Max.x) - SetScrollFromPosX(window, item_rect.Max.x - window->Pos.x + g.Style.ItemSpacing.x, 1.0f); - if (item_rect.Min.y < window_rect.Min.y) - SetScrollFromPosY(window, item_rect.Min.y - window->Pos.y - g.Style.ItemSpacing.y, 0.0f); - else if (item_rect.Max.y >= window_rect.Max.y) - SetScrollFromPosY(window, item_rect.Max.y - window->Pos.y + g.Style.ItemSpacing.y, 1.0f); - - ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window, false); - delta_scroll = next_scroll - window->Scroll; + if (item_rect.Min.x < scroll_rect.Min.x || !can_be_fully_visible_x) + SetScrollFromPosX(window, item_rect.Min.x - g.Style.ItemSpacing.x - window->Pos.x, 0.0f); + else if (item_rect.Max.x >= scroll_rect.Max.x) + SetScrollFromPosX(window, item_rect.Max.x + g.Style.ItemSpacing.x - window->Pos.x, 1.0f); + } + else if (((flags & ImGuiScrollFlags_KeepVisibleCenterX) && !fully_visible_x) || (flags & ImGuiScrollFlags_AlwaysCenterX)) + { + if (can_be_fully_visible_x) + SetScrollFromPosX(window, ImTrunc((item_rect.Min.x + item_rect.Max.x) * 0.5f) - window->Pos.x, 0.5f); + else + SetScrollFromPosX(window, item_rect.Min.x - window->Pos.x, 0.0f); } + if ((flags & ImGuiScrollFlags_KeepVisibleEdgeY) && !fully_visible_y) + { + if (item_rect.Min.y < scroll_rect.Min.y || !can_be_fully_visible_y) + SetScrollFromPosY(window, item_rect.Min.y - g.Style.ItemSpacing.y - window->Pos.y, 0.0f); + else if (item_rect.Max.y >= scroll_rect.Max.y) + SetScrollFromPosY(window, item_rect.Max.y + g.Style.ItemSpacing.y - window->Pos.y, 1.0f); + } + else if (((flags & ImGuiScrollFlags_KeepVisibleCenterY) && !fully_visible_y) || (flags & ImGuiScrollFlags_AlwaysCenterY)) + { + if (can_be_fully_visible_y) + SetScrollFromPosY(window, ImTrunc((item_rect.Min.y + item_rect.Max.y) * 0.5f) - window->Pos.y, 0.5f); + else + SetScrollFromPosY(window, item_rect.Min.y - window->Pos.y, 0.0f); + } + + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + ImVec2 delta_scroll = next_scroll - window->Scroll; + // Also scroll parent window to keep us into view if necessary - if (window->Flags & ImGuiWindowFlags_ChildWindow) - delta_scroll += ScrollToBringRectIntoView(window->ParentWindow, ImRect(item_rect.Min - delta_scroll, item_rect.Max - delta_scroll)); + if (!(flags & ImGuiScrollFlags_NoScrollParent) && (window->Flags & ImGuiWindowFlags_ChildWindow)) + { + // FIXME-SCROLL: May be an option? + if ((in_flags & (ImGuiScrollFlags_AlwaysCenterX | ImGuiScrollFlags_KeepVisibleCenterX)) != 0) + in_flags = (in_flags & ~ImGuiScrollFlags_MaskX_) | ImGuiScrollFlags_KeepVisibleEdgeX; + if ((in_flags & (ImGuiScrollFlags_AlwaysCenterY | ImGuiScrollFlags_KeepVisibleCenterY)) != 0) + in_flags = (in_flags & ~ImGuiScrollFlags_MaskY_) | ImGuiScrollFlags_KeepVisibleEdgeY; + delta_scroll += ScrollToRectEx(window->ParentWindow, ImRect(item_rect.Min - delta_scroll, item_rect.Max - delta_scroll), in_flags); + } return delta_scroll; } @@ -7275,49 +11903,56 @@ float ImGui::GetScrollMaxY() return window->ScrollMax.y; } -void ImGui::SetScrollX(float scroll_x) +void ImGui::SetScrollX(ImGuiWindow* window, float scroll_x) { - ImGuiWindow* window = GetCurrentWindow(); window->ScrollTarget.x = scroll_x; window->ScrollTargetCenterRatio.x = 0.0f; + window->ScrollTargetEdgeSnapDist.x = 0.0f; +} + +void ImGui::SetScrollY(ImGuiWindow* window, float scroll_y) +{ + window->ScrollTarget.y = scroll_y; + window->ScrollTargetCenterRatio.y = 0.0f; + window->ScrollTargetEdgeSnapDist.y = 0.0f; +} + +void ImGui::SetScrollX(float scroll_x) +{ + ImGuiContext& g = *GImGui; + SetScrollX(g.CurrentWindow, scroll_x); } void ImGui::SetScrollY(float scroll_y) { - ImGuiWindow* window = GetCurrentWindow(); - window->ScrollTarget.y = scroll_y; - window->ScrollTargetCenterRatio.y = 0.0f; + ImGuiContext& g = *GImGui; + SetScrollY(g.CurrentWindow, scroll_y); } -void ImGui::SetScrollX(ImGuiWindow* window, float new_scroll_x) -{ - window->ScrollTarget.x = new_scroll_x; - window->ScrollTargetCenterRatio.x = 0.0f; -} - -void ImGui::SetScrollY(ImGuiWindow* window, float new_scroll_y) -{ - window->ScrollTarget.y = new_scroll_y; - window->ScrollTargetCenterRatio.y = 0.0f; -} - - +// Note that a local position will vary depending on initial scroll value, +// This is a little bit confusing so bear with us: +// - local_pos = (absolution_pos - window->Pos) +// - So local_x/local_y are 0.0f for a position at the upper-left corner of a window, +// and generally local_x/local_y are >(padding+decoration) && <(size-padding-decoration) when in the visible area. +// - They mostly exist because of legacy API. +// Following the rules above, when trying to work with scrolling code, consider that: +// - SetScrollFromPosY(0.0f) == SetScrollY(0.0f + scroll.y) == has no effect! +// - SetScrollFromPosY(-scroll.y) == SetScrollY(-scroll.y + scroll.y) == SetScrollY(0.0f) == reset scroll. Of course writing SetScrollY(0.0f) directly then makes more sense +// We store a target position so centering and clamping can occur on the next frame when we are guaranteed to have a known window size void ImGui::SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio) { - // We store a target position so centering can occur on the next frame when we are guaranteed to have a known window size IM_ASSERT(center_x_ratio >= 0.0f && center_x_ratio <= 1.0f); - window->ScrollTarget.x = (float)(int)(local_x + window->Scroll.x); + window->ScrollTarget.x = IM_TRUNC(local_x - window->DecoOuterSizeX1 - window->DecoInnerSizeX1 + window->Scroll.x); // Convert local position to scroll offset window->ScrollTargetCenterRatio.x = center_x_ratio; + window->ScrollTargetEdgeSnapDist.x = 0.0f; } void ImGui::SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio) { - // We store a target position so centering can occur on the next frame when we are guaranteed to have a known window size IM_ASSERT(center_y_ratio >= 0.0f && center_y_ratio <= 1.0f); - const float decoration_up_height = window->TitleBarHeight() + window->MenuBarHeight(); - local_y -= decoration_up_height; - window->ScrollTarget.y = (float)(int)(local_y + window->Scroll.y); + window->ScrollTarget.y = IM_TRUNC(local_y - window->DecoOuterSizeY1 - window->DecoInnerSizeY1 + window->Scroll.y); // Convert local position to scroll offset window->ScrollTargetCenterRatio.y = center_y_ratio; + window->ScrollTargetEdgeSnapDist.y = 0.0f; } void ImGui::SetScrollFromPosX(float local_x, float center_x_ratio) @@ -7337,10 +11972,12 @@ void ImGui::SetScrollHereX(float center_x_ratio) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - float target_x = window->DC.LastItemRect.Min.x - window->Pos.x; // Left of last item, in window space - float last_item_width = window->DC.LastItemRect.GetWidth(); - target_x += (last_item_width * center_x_ratio) + (g.Style.ItemSpacing.x * (center_x_ratio - 0.5f) * 2.0f); // Precisely aim before, in the middle or after the last item. - SetScrollFromPosX(target_x, center_x_ratio); + float spacing_x = ImMax(window->WindowPadding.x, g.Style.ItemSpacing.x); + float target_pos_x = ImLerp(g.LastItemData.Rect.Min.x - spacing_x, g.LastItemData.Rect.Max.x + spacing_x, center_x_ratio); + SetScrollFromPosX(window, target_pos_x - window->Pos.x, center_x_ratio); // Convert from absolute to local pos + + // Tweak: snap on edges when aiming at an item very close to the edge + window->ScrollTargetEdgeSnapDist.x = ImMax(0.0f, window->WindowPadding.x - spacing_x); } // center_y_ratio: 0.0f top of last item, 0.5f vertical center of last item, 1.0f bottom of last item. @@ -7348,53 +11985,75 @@ void ImGui::SetScrollHereY(float center_y_ratio) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - float target_y = window->DC.CursorPosPrevLine.y - window->Pos.y; // Top of last item, in window space - target_y += (window->DC.PrevLineSize.y * center_y_ratio) + (g.Style.ItemSpacing.y * (center_y_ratio - 0.5f) * 2.0f); // Precisely aim above, in the middle or below the last line. - SetScrollFromPosY(target_y, center_y_ratio); + float spacing_y = ImMax(window->WindowPadding.y, g.Style.ItemSpacing.y); + float target_pos_y = ImLerp(window->DC.CursorPosPrevLine.y - spacing_y, window->DC.CursorPosPrevLine.y + window->DC.PrevLineSize.y + spacing_y, center_y_ratio); + SetScrollFromPosY(window, target_pos_y - window->Pos.y, center_y_ratio); // Convert from absolute to local pos + + // Tweak: snap on edges when aiming at an item very close to the edge + window->ScrollTargetEdgeSnapDist.y = ImMax(0.0f, window->WindowPadding.y - spacing_y); } //----------------------------------------------------------------------------- // [SECTION] TOOLTIPS //----------------------------------------------------------------------------- -void ImGui::BeginTooltip() +bool ImGui::BeginTooltip() { - ImGuiContext& g = *GImGui; - if (g.DragDropWithinSourceOrTarget) - { - // The default tooltip position is a little offset to give space to see the context menu (it's also clamped within the current viewport/monitor) - // In the context of a dragging tooltip we try to reduce that offset and we enforce following the cursor. - // Whatever we do we want to call SetNextWindowPos() to enforce a tooltip position and disable clipping the tooltip without our display area, like regular tooltip do. - //ImVec2 tooltip_pos = g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding; - ImVec2 tooltip_pos = g.IO.MousePos + ImVec2(16 * g.Style.MouseCursorScale, 8 * g.Style.MouseCursorScale); - SetNextWindowPos(tooltip_pos); - SetNextWindowBgAlpha(g.Style.Colors[ImGuiCol_PopupBg].w * 0.60f); - //PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * 0.60f); // This would be nice but e.g ColorButton with checkboard has issue with transparent colors :( - BeginTooltipEx(0, true); - } - else - { - BeginTooltipEx(0, false); - } + return BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); } -// Not exposed publicly as BeginTooltip() because bool parameters are evil. Let's see if other needs arise first. -void ImGui::BeginTooltipEx(ImGuiWindowFlags extra_flags, bool override_previous_tooltip) +bool ImGui::BeginItemTooltip() +{ + if (!IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + return false; + return BeginTooltipEx(ImGuiTooltipFlags_None, ImGuiWindowFlags_None); +} + +bool ImGui::BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags) { ImGuiContext& g = *GImGui; - char window_name[16]; - ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", g.TooltipOverrideCount); - if (override_previous_tooltip) - if (ImGuiWindow* window = FindWindowByName(window_name)) - if (window->Active) - { - // Hide previous tooltip from being displayed. We can't easily "reset" the content of a window so we create a new one. - window->Hidden = true; - window->HiddenFramesCanSkipItems = 1; - ImFormatString(window_name, IM_ARRAYSIZE(window_name), "##Tooltip_%02d", ++g.TooltipOverrideCount); - } - ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip|ImGuiWindowFlags_NoInputs|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoMove|ImGuiWindowFlags_NoResize|ImGuiWindowFlags_NoSavedSettings|ImGuiWindowFlags_AlwaysAutoResize; - Begin(window_name, NULL, flags | extra_flags); + + const bool is_dragdrop_tooltip = g.DragDropWithinSource || g.DragDropWithinTarget; + if (is_dragdrop_tooltip) + { + // Drag and Drop tooltips are positioning differently than other tooltips: + // - offset visibility to increase visibility around mouse. + // - never clamp within outer viewport boundary. + // We call SetNextWindowPos() to enforce position and disable clamping. + // See FindBestWindowPosForPopup() for positionning logic of other tooltips (not drag and drop ones). + //ImVec2 tooltip_pos = g.IO.MousePos - g.ActiveIdClickOffset - g.Style.WindowPadding; + const bool is_touchscreen = (g.IO.MouseSource == ImGuiMouseSource_TouchScreen); + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) == 0) + { + ImVec2 tooltip_pos = is_touchscreen ? (g.IO.MousePos + TOOLTIP_DEFAULT_OFFSET_TOUCH * g.Style.MouseCursorScale) : (g.IO.MousePos + TOOLTIP_DEFAULT_OFFSET_MOUSE * g.Style.MouseCursorScale); + ImVec2 tooltip_pivot = is_touchscreen ? TOOLTIP_DEFAULT_PIVOT_TOUCH : ImVec2(0.0f, 0.0f); + SetNextWindowPos(tooltip_pos, ImGuiCond_None, tooltip_pivot); + } + + SetNextWindowBgAlpha(g.Style.Colors[ImGuiCol_PopupBg].w * 0.60f); + //PushStyleVar(ImGuiStyleVar_Alpha, g.Style.Alpha * 0.60f); // This would be nice but e.g ColorButton with checkboard has issue with transparent colors :( + tooltip_flags |= ImGuiTooltipFlags_OverridePrevious; + } + + const char* window_name_template = is_dragdrop_tooltip ? "##Tooltip_DragDrop_%02d" : "##Tooltip_%02d"; + char window_name[32]; + ImFormatString(window_name, IM_ARRAYSIZE(window_name), window_name_template, g.TooltipOverrideCount); + if ((tooltip_flags & ImGuiTooltipFlags_OverridePrevious) && g.TooltipPreviousWindow != NULL && g.TooltipPreviousWindow->Active) + { + // Hide previous tooltip from being displayed. We can't easily "reset" the content of a window so we create a new one. + //IMGUI_DEBUG_LOG("[tooltip] '%s' already active, using +1 for this frame\n", window_name); + SetWindowHiddenAndSkipItemsForCurrentFrame(g.TooltipPreviousWindow); + ImFormatString(window_name, IM_ARRAYSIZE(window_name), window_name_template, ++g.TooltipOverrideCount); + } + + ImGuiWindowFlags flags = ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking; + Begin(window_name, NULL, flags | extra_window_flags); + // 2023-03-09: Added bool return value to the API, but currently always returning true. + // If this ever returns false we need to update BeginDragDropSource() accordingly. + //if (!ret) + // End(); + //return ret; + return true; } void ImGui::EndTooltip() @@ -7403,17 +12062,6 @@ void ImGui::EndTooltip() End(); } -void ImGui::SetTooltipV(const char* fmt, va_list args) -{ - ImGuiContext& g = *GImGui; - if (g.DragDropWithinSourceOrTarget) - BeginTooltip(); - else - BeginTooltipEx(0, true); - TextV(fmt, args); - EndTooltip(); -} - void ImGui::SetTooltip(const char* fmt, ...) { va_list args; @@ -7422,75 +12070,197 @@ void ImGui::SetTooltip(const char* fmt, ...) va_end(args); } +void ImGui::SetTooltipV(const char* fmt, va_list args) +{ + if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) + return; + TextV(fmt, args); + EndTooltip(); +} + +// Shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav'. +// Defaults to == ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort when using the mouse. +void ImGui::SetItemTooltip(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + SetTooltipV(fmt, args); + va_end(args); +} + +void ImGui::SetItemTooltipV(const char* fmt, va_list args) +{ + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + SetTooltipV(fmt, args); +} + + //----------------------------------------------------------------------------- // [SECTION] POPUPS //----------------------------------------------------------------------------- -bool ImGui::IsPopupOpen(ImGuiID id) +// Supported flags: ImGuiPopupFlags_AnyPopupId, ImGuiPopupFlags_AnyPopupLevel +bool ImGui::IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags) { ImGuiContext& g = *GImGui; - return g.OpenPopupStack.Size > g.BeginPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].PopupId == id; + if (popup_flags & ImGuiPopupFlags_AnyPopupId) + { + // Return true if any popup is open at the current BeginPopup() level of the popup stack + // This may be used to e.g. test for another popups already opened to handle popups priorities at the same level. + IM_ASSERT(id == 0); + if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) + return g.OpenPopupStack.Size > 0; + else + return g.OpenPopupStack.Size > g.BeginPopupStack.Size; + } + else + { + if (popup_flags & ImGuiPopupFlags_AnyPopupLevel) + { + // Return true if the popup is open anywhere in the popup stack + for (ImGuiPopupData& popup_data : g.OpenPopupStack) + if (popup_data.PopupId == id) + return true; + return false; + } + else + { + // Return true if the popup is open at the current BeginPopup() level of the popup stack (this is the most-common query) + return g.OpenPopupStack.Size > g.BeginPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].PopupId == id; + } + } } -bool ImGui::IsPopupOpen(const char* str_id) +bool ImGui::IsPopupOpen(const char* str_id, ImGuiPopupFlags popup_flags) { ImGuiContext& g = *GImGui; - return g.OpenPopupStack.Size > g.BeginPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].PopupId == g.CurrentWindow->GetID(str_id); + ImGuiID id = (popup_flags & ImGuiPopupFlags_AnyPopupId) ? 0 : g.CurrentWindow->GetID(str_id); + if ((popup_flags & ImGuiPopupFlags_AnyPopupLevel) && id != 0) + IM_ASSERT(0 && "Cannot use IsPopupOpen() with a string id and ImGuiPopupFlags_AnyPopupLevel."); // But non-string version is legal and used internally + return IsPopupOpen(id, popup_flags); } +// Also see FindBlockingModal(NULL) ImGuiWindow* ImGui::GetTopMostPopupModal() { ImGuiContext& g = *GImGui; - for (int n = g.OpenPopupStack.Size-1; n >= 0; n--) + for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) if (popup->Flags & ImGuiWindowFlags_Modal) return popup; return NULL; } -void ImGui::OpenPopup(const char* str_id) +// See Demo->Stacked Modal to confirm what this is for. +ImGuiWindow* ImGui::GetTopMostAndVisiblePopupModal() { ImGuiContext& g = *GImGui; - OpenPopupEx(g.CurrentWindow->GetID(str_id)); + for (int n = g.OpenPopupStack.Size - 1; n >= 0; n--) + if (ImGuiWindow* popup = g.OpenPopupStack.Data[n].Window) + if ((popup->Flags & ImGuiWindowFlags_Modal) && IsWindowActiveAndVisible(popup)) + return popup; + return NULL; +} + + +// When a modal popup is open, newly created windows that want focus (i.e. are not popups and do not specify ImGuiWindowFlags_NoFocusOnAppearing) +// should be positioned behind that modal window, unless the window was created inside the modal begin-stack. +// In case of multiple stacked modals newly created window honors begin stack order and does not go below its own modal parent. +// - WindowA // FindBlockingModal() returns Modal1 +// - WindowB // .. returns Modal1 +// - Modal1 // .. returns Modal2 +// - WindowC // .. returns Modal2 +// - WindowD // .. returns Modal2 +// - Modal2 // .. returns Modal2 +// - WindowE // .. returns NULL +// Notes: +// - FindBlockingModal(NULL) == NULL is generally equivalent to GetTopMostPopupModal() == NULL. +// Only difference is here we check for ->Active/WasActive but it may be unnecessary. +ImGuiWindow* ImGui::FindBlockingModal(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.OpenPopupStack.Size <= 0) + return NULL; + + // Find a modal that has common parent with specified window. Specified window should be positioned behind that modal. + for (ImGuiPopupData& popup_data : g.OpenPopupStack) + { + ImGuiWindow* popup_window = popup_data.Window; + if (popup_window == NULL || !(popup_window->Flags & ImGuiWindowFlags_Modal)) + continue; + if (!popup_window->Active && !popup_window->WasActive) // Check WasActive, because this code may run before popup renders on current frame, also check Active to handle newly created windows. + continue; + if (window == NULL) // FindBlockingModal(NULL) test for if FocusWindow(NULL) is naturally possible via a mouse click. + return popup_window; + if (IsWindowWithinBeginStackOf(window, popup_window)) // Window may be over modal + continue; + return popup_window; // Place window right below first block modal + } + return NULL; +} + +void ImGui::OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiID id = g.CurrentWindow->GetID(str_id); + IMGUI_DEBUG_LOG_POPUP("[popup] OpenPopup(\"%s\" -> 0x%08X)\n", str_id, id); + OpenPopupEx(id, popup_flags); +} + +void ImGui::OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags) +{ + OpenPopupEx(id, popup_flags); } // Mark popup as open (toggle toward open state). // Popups are closed when user click outside, or activate a pressable item, or CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. // Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). // One open popup per level of the popup hierarchy (NB: when assigning we reset the Window member of ImGuiPopupRef to NULL) -void ImGui::OpenPopupEx(ImGuiID id) +void ImGui::OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags) { ImGuiContext& g = *GImGui; ImGuiWindow* parent_window = g.CurrentWindow; - int current_stack_size = g.BeginPopupStack.Size; + const int current_stack_size = g.BeginPopupStack.Size; + + if (popup_flags & ImGuiPopupFlags_NoOpenOverExistingPopup) + if (IsPopupOpen((ImGuiID)0, ImGuiPopupFlags_AnyPopupId)) + return; + ImGuiPopupData popup_ref; // Tagged as new ref as Window will be set back to NULL if we write this into OpenPopupStack. popup_ref.PopupId = id; popup_ref.Window = NULL; - popup_ref.SourceWindow = g.NavWindow; + popup_ref.RestoreNavWindow = g.NavWindow; // When popup closes focus may be restored to NavWindow (depend on window type). popup_ref.OpenFrameCount = g.FrameCount; popup_ref.OpenParentId = parent_window->IDStack.back(); popup_ref.OpenPopupPos = NavCalcPreferredRefPos(); popup_ref.OpenMousePos = IsMousePosValid(&g.IO.MousePos) ? g.IO.MousePos : popup_ref.OpenPopupPos; - //IMGUI_DEBUG_LOG("OpenPopupEx(0x%08X)\n", g.FrameCount, id); + IMGUI_DEBUG_LOG_POPUP("[popup] OpenPopupEx(0x%08X)\n", id); if (g.OpenPopupStack.Size < current_stack_size + 1) { g.OpenPopupStack.push_back(popup_ref); } else { - // Gently handle the user mistakenly calling OpenPopup() every frame. It is a programming mistake! However, if we were to run the regular code path, the ui - // would become completely unusable because the popup will always be in hidden-while-calculating-size state _while_ claiming focus. Which would be a very confusing - // situation for the programmer. Instead, we silently allow the popup to proceed, it will keep reappearing and the programming error will be more obvious to understand. - if (g.OpenPopupStack[current_stack_size].PopupId == id && g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) + // Gently handle the user mistakenly calling OpenPopup() every frames: it is likely a programming mistake! + // However, if we were to run the regular code path, the ui would become completely unusable because the popup will always be + // in hidden-while-calculating-size state _while_ claiming focus. Which is extremely confusing situation for the programmer. + // Instead, for successive frames calls to OpenPopup(), we silently avoid reopening even if ImGuiPopupFlags_NoReopen is not specified. + bool keep_existing = false; + if (g.OpenPopupStack[current_stack_size].PopupId == id) + if ((g.OpenPopupStack[current_stack_size].OpenFrameCount == g.FrameCount - 1) || (popup_flags & ImGuiPopupFlags_NoReopen)) + keep_existing = true; + if (keep_existing) { + // No reopen g.OpenPopupStack[current_stack_size].OpenFrameCount = popup_ref.OpenFrameCount; } else { - // Close child popups if any, then flag popup for open/reopen - g.OpenPopupStack.resize(current_stack_size + 1); - g.OpenPopupStack[current_stack_size] = popup_ref; + // Reopen: close child popups if any, then flag popup for open/reopen (set position, focus, init navigation) + ClosePopupToLevel(current_stack_size, true); + g.OpenPopupStack.push_back(popup_ref); } // When reopening a popup we first refocus its parent, otherwise if its parent is itself a popup it would get closed by ClosePopupsOverWindow(). @@ -7500,27 +12270,16 @@ void ImGui::OpenPopupEx(ImGuiID id) } } -bool ImGui::OpenPopupOnItemClick(const char* str_id, int mouse_button) -{ - ImGuiWindow* window = GImGui->CurrentWindow; - if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) - { - ImGuiID id = str_id ? window->GetID(str_id) : window->DC.LastItemId; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! - IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) - OpenPopupEx(id); - return true; - } - return false; -} - +// When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it. +// This function closes any popups that are over 'ref_window'. void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup) { ImGuiContext& g = *GImGui; - if (g.OpenPopupStack.empty()) + if (g.OpenPopupStack.Size == 0) return; - // When popups are stacked, clicking on a lower level popups puts focus back to it and close popups above it. // Don't close our own child popup windows. + //IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\") restore_under=%d\n", ref_window ? ref_window->Name : "", restore_focus_to_window_under_popup); int popup_count_to_keep = 0; if (ref_window) { @@ -7531,47 +12290,72 @@ void ImGui::ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to if (!popup.Window) continue; IM_ASSERT((popup.Window->Flags & ImGuiWindowFlags_Popup) != 0); - if (popup.Window->Flags & ImGuiWindowFlags_ChildWindow) - continue; - // Trim the stack when popups are not direct descendant of the reference window (the reference window is often the NavWindow) - bool popup_or_descendent_is_ref_window = false; - for (int m = popup_count_to_keep; m < g.OpenPopupStack.Size && !popup_or_descendent_is_ref_window; m++) - if (ImGuiWindow* popup_window = g.OpenPopupStack[m].Window) - if (popup_window->RootWindow == ref_window->RootWindow) - popup_or_descendent_is_ref_window = true; - if (!popup_or_descendent_is_ref_window) + // Trim the stack unless the popup is a direct parent of the reference window (the reference window is often the NavWindow) + // - Clicking/Focusing Window2 won't close Popup1: + // Window -> Popup1 -> Window2(Ref) + // - Clicking/focusing Popup1 will close Popup2 and Popup3: + // Window -> Popup1(Ref) -> Popup2 -> Popup3 + // - Each popups may contain child windows, which is why we compare ->RootWindowDockTree! + // Window -> Popup1 -> Popup1_Child -> Popup2 -> Popup2_Child + // We step through every popup from bottom to top to validate their position relative to reference window. + bool ref_window_is_descendent_of_popup = false; + for (int n = popup_count_to_keep; n < g.OpenPopupStack.Size; n++) + if (ImGuiWindow* popup_window = g.OpenPopupStack[n].Window) + //if (popup_window->RootWindowDockTree == ref_window->RootWindowDockTree) // FIXME-MERGE + if (IsWindowWithinBeginStackOf(ref_window, popup_window)) + { + ref_window_is_descendent_of_popup = true; + break; + } + if (!ref_window_is_descendent_of_popup) break; } } if (popup_count_to_keep < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the statement below { - //IMGUI_DEBUG_LOG("ClosePopupsOverWindow(%s) -> ClosePopupToLevel(%d)\n", ref_window->Name, popup_count_to_keep); + IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupsOverWindow(\"%s\")\n", ref_window ? ref_window->Name : ""); ClosePopupToLevel(popup_count_to_keep, restore_focus_to_window_under_popup); } } +void ImGui::ClosePopupsExceptModals() +{ + ImGuiContext& g = *GImGui; + + int popup_count_to_keep; + for (popup_count_to_keep = g.OpenPopupStack.Size; popup_count_to_keep > 0; popup_count_to_keep--) + { + ImGuiWindow* window = g.OpenPopupStack[popup_count_to_keep - 1].Window; + if (!window || (window->Flags & ImGuiWindowFlags_Modal)) + break; + } + if (popup_count_to_keep < g.OpenPopupStack.Size) // This test is not required but it allows to set a convenient breakpoint on the statement below + ClosePopupToLevel(popup_count_to_keep, true); +} + void ImGui::ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup) { ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_POPUP("[popup] ClosePopupToLevel(%d), restore_under=%d\n", remaining, restore_focus_to_window_under_popup); IM_ASSERT(remaining >= 0 && remaining < g.OpenPopupStack.Size); - ImGuiWindow* focus_window = g.OpenPopupStack[remaining].SourceWindow; - ImGuiWindow* popup_window = g.OpenPopupStack[remaining].Window; + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) + for (int n = remaining; n < g.OpenPopupStack.Size; n++) + IMGUI_DEBUG_LOG_POPUP("[popup] - Closing PopupID 0x%08X Window \"%s\"\n", g.OpenPopupStack[n].PopupId, g.OpenPopupStack[n].Window ? g.OpenPopupStack[n].Window->Name : NULL); + + // Trim open popup stack + ImGuiPopupData prev_popup = g.OpenPopupStack[remaining]; g.OpenPopupStack.resize(remaining); - if (restore_focus_to_window_under_popup) + // Restore focus (unless popup window was not yet submitted, and didn't have a chance to take focus anyhow. See #7325 for an edge case) + if (restore_focus_to_window_under_popup && prev_popup.Window) { - if (focus_window && !focus_window->WasActive && popup_window) - { - // Fallback - FocusTopMostWindowUnderOne(popup_window, NULL); - } + ImGuiWindow* popup_window = prev_popup.Window; + ImGuiWindow* focus_window = (popup_window->Flags & ImGuiWindowFlags_ChildMenu) ? popup_window->ParentWindow : prev_popup.RestoreNavWindow; + if (focus_window && !focus_window->WasActive) + FocusTopMostWindowUnderOne(popup_window, NULL, NULL, ImGuiFocusRequestFlags_RestoreFocusedChild); // Fallback else - { - if (g.NavLayer == 0 && focus_window) - focus_window = NavRestoreLastChildNavWindow(focus_window); - FocusWindow(focus_window); - } + FocusWindow(focus_window, (g.NavLayer == ImGuiNavLayer_Main) ? ImGuiFocusRequestFlags_RestoreFocusedChild : ImGuiFocusRequestFlags_None); } } @@ -7590,13 +12374,13 @@ void ImGui::CloseCurrentPopup() ImGuiWindow* parent_popup_window = g.OpenPopupStack[popup_idx - 1].Window; bool close_parent = false; if (popup_window && (popup_window->Flags & ImGuiWindowFlags_ChildMenu)) - if (parent_popup_window == NULL || !(parent_popup_window->Flags & ImGuiWindowFlags_Modal)) + if (parent_popup_window && !(parent_popup_window->Flags & ImGuiWindowFlags_MenuBar)) close_parent = true; if (!close_parent) break; popup_idx--; } - //IMGUI_DEBUG_LOG("CloseCurrentPopup %d -> %d\n", g.BeginPopupStack.Size - 1, popup_idx); + IMGUI_DEBUG_LOG_POPUP("[popup] CloseCurrentPopup %d -> %d\n", g.BeginPopupStack.Size - 1, popup_idx); ClosePopupToLevel(popup_idx, true); // A common pattern is to close a popup when selecting a menu item/selectable that will open another window. @@ -7606,25 +12390,43 @@ void ImGui::CloseCurrentPopup() window->DC.NavHideHighlightOneFrame = true; } -bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags) +// Attention! BeginPopup() adds default flags when calling BeginPopupEx()! +bool ImGui::BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags) { ImGuiContext& g = *GImGui; - if (!IsPopupOpen(id)) + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) { g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values return false; } char name[20]; - if (extra_flags & ImGuiWindowFlags_ChildMenu) - ImFormatString(name, IM_ARRAYSIZE(name), "##Menu_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth - else - ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // Not recycling, so we can close/open during the same frame + IM_ASSERT((extra_window_flags & ImGuiWindowFlags_ChildMenu) == 0); // Use BeginPopupMenuEx() + ImFormatString(name, IM_ARRAYSIZE(name), "##Popup_%08x", id); // No recycling, so we can close/open during the same frame - bool is_open = Begin(name, NULL, extra_flags | ImGuiWindowFlags_Popup); + bool is_open = Begin(name, NULL, extra_window_flags | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoDocking); if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) EndPopup(); + //g.CurrentWindow->FocusRouteParentWindow = g.CurrentWindow->ParentWindowInBeginStack; + return is_open; +} +bool ImGui::BeginPopupMenuEx(ImGuiID id, const char* label, ImGuiWindowFlags extra_window_flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + return false; + } + + char name[128]; + IM_ASSERT(extra_window_flags & ImGuiWindowFlags_ChildMenu); + ImFormatString(name, IM_ARRAYSIZE(name), "%s###Menu_%02d", label, g.BeginMenuDepth); // Recycle windows based on depth + bool is_open = Begin(name, NULL, extra_window_flags | ImGuiWindowFlags_Popup); + if (!is_open) // NB: Begin can return false when the popup is completely clipped (e.g. zero size display) + EndPopup(); + //g.CurrentWindow->FocusRouteParentWindow = g.CurrentWindow->ParentWindowInBeginStack; return is_open; } @@ -7637,28 +12439,37 @@ bool ImGui::BeginPopup(const char* str_id, ImGuiWindowFlags flags) return false; } flags |= ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings; - return BeginPopupEx(g.CurrentWindow->GetID(str_id), flags); + ImGuiID id = g.CurrentWindow->GetID(str_id); + return BeginPopupEx(id, flags); } // If 'p_open' is specified for a modal popup window, the popup will have a regular close button which will close the popup. -// Note that popup visibility status is owned by Dear ImGui (and manipulated with e.g. OpenPopup) so the actual value of *p_open is meaningless here. +// Note that popup visibility status is owned by Dear ImGui (and manipulated with e.g. OpenPopup). +// - *p_open set back to false in BeginPopupModal() when popup is not open. +// - if you set *p_open to false before calling BeginPopupModal(), it will close the popup. bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; const ImGuiID id = window->GetID(name); - if (!IsPopupOpen(id)) + if (!IsPopupOpen(id, ImGuiPopupFlags_None)) { g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values + if (p_open && *p_open) + *p_open = false; return false; } - // Center modal windows by default + // Center modal windows by default for increased visibility + // (this won't really last as settings will kick in, and is mostly for backward compatibility. user may do the same themselves) // FIXME: Should test for (PosCond & window->SetWindowPosAllowFlags) with the upcoming window. - if ((g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasPos) == 0) - SetNextWindowPos(g.IO.DisplaySize * 0.5f, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) == 0) + { + const ImGuiViewport* viewport = window->WasActive ? window->Viewport : GetMainViewport(); // FIXME-VIEWPORT: What may be our reference viewport? + SetNextWindowPos(viewport->GetCenter(), ImGuiCond_FirstUseEver, ImVec2(0.5f, 0.5f)); + } - flags |= ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoSavedSettings; + flags |= ImGuiWindowFlags_Popup | ImGuiWindowFlags_Modal | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoDocking; const bool is_open = Begin(name, p_open, flags); if (!is_open || (p_open && !*p_open)) // NB: is_open can be 'false' when the popup is completely clipped (e.g. zero size display) { @@ -7673,53 +12484,100 @@ bool ImGui::BeginPopupModal(const char* name, bool* p_open, ImGuiWindowFlags fla void ImGui::EndPopup() { ImGuiContext& g = *GImGui; - IM_ASSERT(g.CurrentWindow->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginPopup()/EndPopup() calls + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginPopup()/EndPopup() calls IM_ASSERT(g.BeginPopupStack.Size > 0); - // Make all menus and popups wrap around for now, may need to expose that policy. - NavMoveRequestTryWrapping(g.CurrentWindow, ImGuiNavMoveFlags_LoopY); + // Make all menus and popups wrap around for now, may need to expose that policy (e.g. focus scope could include wrap/loop policy flags used by new move requests) + if (g.NavWindow == window) + NavMoveRequestTryWrapping(window, ImGuiNavMoveFlags_LoopY); + // Child-popups don't need to be laid out + const ImGuiID backup_within_end_child_id = g.WithinEndChildID; + if (window->Flags & ImGuiWindowFlags_ChildWindow) + g.WithinEndChildID = window->ID; End(); + g.WithinEndChildID = backup_within_end_child_id; +} + +// Helper to open a popup if mouse button is released over the item +// - This is essentially the same as BeginPopupContextItem() but without the trailing BeginPopup() +void ImGui::OpenPopupOnItemClick(const char* str_id, ImGuiPopupFlags popup_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); + if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + { + ImGuiID id = str_id ? window->GetID(str_id) : g.LastItemData.ID; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + OpenPopupEx(id, popup_flags); + } } // This is a helper to handle the simplest case of associating one named popup to one given widget. -// You may want to handle this on user side if you have specific needs (e.g. tweaking IsItemHovered() parameters). -// You can pass a NULL str_id to use the identifier of the last item. -bool ImGui::BeginPopupContextItem(const char* str_id, int mouse_button) +// - To create a popup associated to the last item, you generally want to pass a NULL value to str_id. +// - To create a popup with a specific identifier, pass it in str_id. +// - This is useful when using using BeginPopupContextItem() on an item which doesn't have an identifier, e.g. a Text() call. +// - This is useful when multiple code locations may want to manipulate/open the same popup, given an explicit id. +// - You may want to handle the whole on user side if you have specific needs (e.g. tweaking IsItemHovered() parameters). +// This is essentially the same as: +// id = str_id ? GetID(str_id) : GetItemID(); +// OpenPopupOnItemClick(str_id, ImGuiPopupFlags_MouseButtonRight); +// return BeginPopup(id); +// Which is essentially the same as: +// id = str_id ? GetID(str_id) : GetItemID(); +// if (IsItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) +// OpenPopup(id); +// return BeginPopup(id); +// The main difference being that this is tweaked to avoid computing the ID twice. +bool ImGui::BeginPopupContextItem(const char* str_id, ImGuiPopupFlags popup_flags) { - ImGuiWindow* window = GImGui->CurrentWindow; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; if (window->SkipItems) return false; - ImGuiID id = str_id ? window->GetID(str_id) : window->DC.LastItemId; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! - IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + ImGuiID id = str_id ? window->GetID(str_id) : g.LastItemData.ID; // If user hasn't passed an ID, we can use the LastItemID. Using LastItemID as a Popup ID won't conflict! + IM_ASSERT(id != 0); // You cannot pass a NULL str_id if the last item has no identifier (e.g. a Text() item) + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); if (IsMouseReleased(mouse_button) && IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) - OpenPopupEx(id); - return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings); + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); } -bool ImGui::BeginPopupContextWindow(const char* str_id, int mouse_button, bool also_over_items) +bool ImGui::BeginPopupContextWindow(const char* str_id, ImGuiPopupFlags popup_flags) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; if (!str_id) str_id = "window_context"; - ImGuiID id = GImGui->CurrentWindow->GetID(str_id); + ImGuiID id = window->GetID(str_id); + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); if (IsMouseReleased(mouse_button) && IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup)) - if (also_over_items || !IsAnyItemHovered()) - OpenPopupEx(id); - return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings); + if (!(popup_flags & ImGuiPopupFlags_NoOpenOverItems) || !IsAnyItemHovered()) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); } -bool ImGui::BeginPopupContextVoid(const char* str_id, int mouse_button) +bool ImGui::BeginPopupContextVoid(const char* str_id, ImGuiPopupFlags popup_flags) { + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; if (!str_id) str_id = "void_context"; - ImGuiID id = GImGui->CurrentWindow->GetID(str_id); + ImGuiID id = window->GetID(str_id); + int mouse_button = (popup_flags & ImGuiPopupFlags_MouseButtonMask_); if (IsMouseReleased(mouse_button) && !IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) - OpenPopupEx(id); - return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize|ImGuiWindowFlags_NoTitleBar|ImGuiWindowFlags_NoSavedSettings); + if (GetTopMostPopupModal() == NULL) + OpenPopupEx(id, popup_flags); + return BeginPopupEx(id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings); } // r_avoid = the rectangle to avoid (e.g. for tooltip it is a rectangle around the mouse cursor which we want to avoid. for popups it's a small point around the cursor.) // r_outer = the visible area rectangle, minus safe area padding. If our popup size won't fit because of safe area padding we ignore it. +// (r_outer is usually equivalent to the viewport rectangle minus padding, but when multi-viewports are enabled and monitor +// information are available, it may represent the entire platform monitor from the frame of reference of the current viewport. +// this allows us to have tooltips/popups displayed out of the parent viewport.) ImVec2 ImGui::FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy) { ImVec2 base_pos_clamped = ImClamp(ref_pos, r_outer.Min, r_outer.Max - size); @@ -7747,37 +12605,71 @@ ImVec2 ImGui::FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& s } } - // Default popup policy - const ImGuiDir dir_prefered_order[ImGuiDir_COUNT] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; - for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_COUNT; n++) + // Tooltip and Default popup policy + // (Always first try the direction we used on the last frame, if any) + if (policy == ImGuiPopupPositionPolicy_Tooltip || policy == ImGuiPopupPositionPolicy_Default) { - const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; - if (n != -1 && dir == *last_dir) // Already tried this direction? - continue; - float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); - float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); - if (avail_w < size.x || avail_h < size.y) - continue; - ImVec2 pos; - pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; - pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; - *last_dir = dir; - return pos; + const ImGuiDir dir_prefered_order[ImGuiDir_COUNT] = { ImGuiDir_Right, ImGuiDir_Down, ImGuiDir_Up, ImGuiDir_Left }; + for (int n = (*last_dir != ImGuiDir_None) ? -1 : 0; n < ImGuiDir_COUNT; n++) + { + const ImGuiDir dir = (n == -1) ? *last_dir : dir_prefered_order[n]; + if (n != -1 && dir == *last_dir) // Already tried this direction? + continue; + + const float avail_w = (dir == ImGuiDir_Left ? r_avoid.Min.x : r_outer.Max.x) - (dir == ImGuiDir_Right ? r_avoid.Max.x : r_outer.Min.x); + const float avail_h = (dir == ImGuiDir_Up ? r_avoid.Min.y : r_outer.Max.y) - (dir == ImGuiDir_Down ? r_avoid.Max.y : r_outer.Min.y); + + // If there's not enough room on one axis, there's no point in positioning on a side on this axis (e.g. when not enough width, use a top/bottom position to maximize available width) + if (avail_w < size.x && (dir == ImGuiDir_Left || dir == ImGuiDir_Right)) + continue; + if (avail_h < size.y && (dir == ImGuiDir_Up || dir == ImGuiDir_Down)) + continue; + + ImVec2 pos; + pos.x = (dir == ImGuiDir_Left) ? r_avoid.Min.x - size.x : (dir == ImGuiDir_Right) ? r_avoid.Max.x : base_pos_clamped.x; + pos.y = (dir == ImGuiDir_Up) ? r_avoid.Min.y - size.y : (dir == ImGuiDir_Down) ? r_avoid.Max.y : base_pos_clamped.y; + + // Clamp top-left corner of popup + pos.x = ImMax(pos.x, r_outer.Min.x); + pos.y = ImMax(pos.y, r_outer.Min.y); + + *last_dir = dir; + return pos; + } } - // Fallback, try to keep within display + // Fallback when not enough room: *last_dir = ImGuiDir_None; + + // For tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. + if (policy == ImGuiPopupPositionPolicy_Tooltip) + return ref_pos + ImVec2(2, 2); + + // Otherwise try to keep within display ImVec2 pos = ref_pos; pos.x = ImMax(ImMin(pos.x + size.x, r_outer.Max.x) - size.x, r_outer.Min.x); pos.y = ImMax(ImMin(pos.y + size.y, r_outer.Max.y) - size.y, r_outer.Min.y); return pos; } -ImRect ImGui::GetWindowAllowedExtentRect(ImGuiWindow* window) +// Note that this is used for popups, which can overlap the non work-area of individual viewports. +ImRect ImGui::GetPopupAllowedExtentRect(ImGuiWindow* window) { - IM_UNUSED(window); - ImVec2 padding = GImGui->Style.DisplaySafeAreaPadding; - ImRect r_screen = GetViewportRect(); + ImGuiContext& g = *GImGui; + ImRect r_screen; + if (window->ViewportAllowPlatformMonitorExtend >= 0) + { + // Extent with be in the frame of reference of the given viewport (so Min is likely to be negative here) + const ImGuiPlatformMonitor& monitor = g.PlatformIO.Monitors[window->ViewportAllowPlatformMonitorExtend]; + r_screen.Min = monitor.WorkPos; + r_screen.Max = monitor.WorkPos + monitor.WorkSize; + } + else + { + // Use the full viewport area (not work area) for popups + r_screen = window->Viewport->GetMainRect(); + } + ImVec2 padding = g.Style.DisplaySafeAreaPadding; r_screen.Expand(ImVec2((r_screen.GetWidth() > padding.x * 2) ? -padding.x : 0.0f, (r_screen.GetHeight() > padding.y * 2) ? -padding.y : 0.0f)); return r_screen; } @@ -7786,110 +12678,479 @@ ImVec2 ImGui::FindBestWindowPosForPopup(ImGuiWindow* window) { ImGuiContext& g = *GImGui; - ImRect r_outer = GetWindowAllowedExtentRect(window); + ImRect r_outer = GetPopupAllowedExtentRect(window); if (window->Flags & ImGuiWindowFlags_ChildMenu) { // Child menus typically request _any_ position within the parent menu item, and then we move the new menu outside the parent bounds. // This is how we end up with child menus appearing (most-commonly) on the right of the parent menu. - IM_ASSERT(g.CurrentWindow == window); - ImGuiWindow* parent_window = g.CurrentWindowStack[g.CurrentWindowStack.Size - 2]; + ImGuiWindow* parent_window = window->ParentWindow; float horizontal_overlap = g.Style.ItemInnerSpacing.x; // We want some overlap to convey the relative depth of each menu (currently the amount of overlap is hard-coded to style.ItemSpacing.x). ImRect r_avoid; if (parent_window->DC.MenuBarAppending) - r_avoid = ImRect(-FLT_MAX, parent_window->Pos.y + parent_window->TitleBarHeight(), FLT_MAX, parent_window->Pos.y + parent_window->TitleBarHeight() + parent_window->MenuBarHeight()); + r_avoid = ImRect(-FLT_MAX, parent_window->ClipRect.Min.y, FLT_MAX, parent_window->ClipRect.Max.y); // Avoid parent menu-bar. If we wanted multi-line menu-bar, we may instead want to have the calling window setup e.g. a NextWindowData.PosConstraintAvoidRect field else r_avoid = ImRect(parent_window->Pos.x + horizontal_overlap, -FLT_MAX, parent_window->Pos.x + parent_window->Size.x - horizontal_overlap - parent_window->ScrollbarSizes.x, FLT_MAX); - return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid); + return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Default); } if (window->Flags & ImGuiWindowFlags_Popup) { - ImRect r_avoid = ImRect(window->Pos.x - 1, window->Pos.y - 1, window->Pos.x + 1, window->Pos.y + 1); - return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid); + return FindBestWindowPosForPopupEx(window->Pos, window->Size, &window->AutoPosLastDirection, r_outer, ImRect(window->Pos, window->Pos), ImGuiPopupPositionPolicy_Default); // Ideally we'd disable r_avoid here } if (window->Flags & ImGuiWindowFlags_Tooltip) { - // Position tooltip (always follows mouse) - float sc = g.Style.MouseCursorScale; - ImVec2 ref_pos = NavCalcPreferredRefPos(); + // Position tooltip (always follows mouse + clamp within outer boundaries) + // FIXME: + // - Too many paths. One problem is that FindBestWindowPosForPopupEx() doesn't allow passing a suggested position (so touch screen path doesn't use it by default). + // - Drag and drop tooltips are not using this path either: BeginTooltipEx() manually sets their position. + // - Require some tidying up. In theory we could handle both cases in same location, but requires a bit of shuffling + // as drag and drop tooltips are calling SetNextWindowPos() leading to 'window_pos_set_by_api' being set in Begin(). + IM_ASSERT(g.CurrentWindow == window); + const float scale = g.Style.MouseCursorScale; + const ImVec2 ref_pos = NavCalcPreferredRefPos(); + + if (g.IO.MouseSource == ImGuiMouseSource_TouchScreen && NavCalcPreferredRefPosSource() == ImGuiInputSource_Mouse) + { + ImVec2 tooltip_pos = ref_pos + TOOLTIP_DEFAULT_OFFSET_TOUCH * scale - (TOOLTIP_DEFAULT_PIVOT_TOUCH * window->Size); + if (r_outer.Contains(ImRect(tooltip_pos, tooltip_pos + window->Size))) + return tooltip_pos; + } + + ImVec2 tooltip_pos = ref_pos + TOOLTIP_DEFAULT_OFFSET_MOUSE * scale; ImRect r_avoid; - if (!g.NavDisableHighlight && g.NavDisableMouseHover && !(g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos)) + if (g.NavCursorVisible && g.NavHighlightItemUnderNav && !g.IO.ConfigNavMoveSetMousePos) r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 16, ref_pos.y + 8); else - r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * sc, ref_pos.y + 24 * sc); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. - ImVec2 pos = FindBestWindowPosForPopupEx(ref_pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid); - if (window->AutoPosLastDirection == ImGuiDir_None) - pos = ref_pos + ImVec2(2, 2); // If there's not enough room, for tooltip we prefer avoiding the cursor at all cost even if it means that part of the tooltip won't be visible. - return pos; + r_avoid = ImRect(ref_pos.x - 16, ref_pos.y - 8, ref_pos.x + 24 * scale, ref_pos.y + 24 * scale); // FIXME: Hard-coded based on mouse cursor shape expectation. Exact dimension not very important. + //GetForegroundDrawList()->AddRect(r_avoid.Min, r_avoid.Max, IM_COL32(255, 0, 255, 255)); + + return FindBestWindowPosForPopupEx(tooltip_pos, window->Size, &window->AutoPosLastDirection, r_outer, r_avoid, ImGuiPopupPositionPolicy_Tooltip); } IM_ASSERT(0); return window->Pos; } +//----------------------------------------------------------------------------- +// [SECTION] WINDOW FOCUS +//---------------------------------------------------------------------------- +// - SetWindowFocus() +// - SetNextWindowFocus() +// - IsWindowFocused() +// - UpdateWindowInFocusOrderList() [Internal] +// - BringWindowToFocusFront() [Internal] +// - BringWindowToDisplayFront() [Internal] +// - BringWindowToDisplayBack() [Internal] +// - BringWindowToDisplayBehind() [Internal] +// - FindWindowDisplayIndex() [Internal] +// - FocusWindow() [Internal] +// - FocusTopMostWindowUnderOne() [Internal] +//----------------------------------------------------------------------------- + +void ImGui::SetWindowFocus() +{ + FocusWindow(GImGui->CurrentWindow); +} + +void ImGui::SetWindowFocus(const char* name) +{ + if (name) + { + if (ImGuiWindow* window = FindWindowByName(name)) + FocusWindow(window); + } + else + { + FocusWindow(NULL); + } +} + +void ImGui::SetNextWindowFocus() +{ + ImGuiContext& g = *GImGui; + g.NextWindowData.HasFlags |= ImGuiNextWindowDataFlags_HasFocus; +} + +// Similar to IsWindowHovered() +bool ImGui::IsWindowFocused(ImGuiFocusedFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* ref_window = g.NavWindow; + ImGuiWindow* cur_window = g.CurrentWindow; + + if (ref_window == NULL) + return false; + if (flags & ImGuiFocusedFlags_AnyWindow) + return true; + + IM_ASSERT(cur_window); // Not inside a Begin()/End() + const bool popup_hierarchy = (flags & ImGuiFocusedFlags_NoPopupHierarchy) == 0; + const bool dock_hierarchy = (flags & ImGuiFocusedFlags_DockHierarchy) != 0; + if (flags & ImGuiHoveredFlags_RootWindow) + cur_window = GetCombinedRootWindow(cur_window, popup_hierarchy, dock_hierarchy); + + if (flags & ImGuiHoveredFlags_ChildWindows) + return IsWindowChildOf(ref_window, cur_window, popup_hierarchy, dock_hierarchy); + else + return (ref_window == cur_window); +} + +static int ImGui::FindWindowFocusIndex(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(g); + int order = window->FocusOrder; + IM_ASSERT(window->RootWindow == window); // No child window (not testing _ChildWindow because of docking) + IM_ASSERT(g.WindowsFocusOrder[order] == window); + return order; +} + +static void ImGui::UpdateWindowInFocusOrderList(ImGuiWindow* window, bool just_created, ImGuiWindowFlags new_flags) +{ + ImGuiContext& g = *GImGui; + + const bool new_is_explicit_child = (new_flags & ImGuiWindowFlags_ChildWindow) != 0 && ((new_flags & ImGuiWindowFlags_Popup) == 0 || (new_flags & ImGuiWindowFlags_ChildMenu) != 0); + const bool child_flag_changed = new_is_explicit_child != window->IsExplicitChild; + if ((just_created || child_flag_changed) && !new_is_explicit_child) + { + IM_ASSERT(!g.WindowsFocusOrder.contains(window)); + g.WindowsFocusOrder.push_back(window); + window->FocusOrder = (short)(g.WindowsFocusOrder.Size - 1); + } + else if (!just_created && child_flag_changed && new_is_explicit_child) + { + IM_ASSERT(g.WindowsFocusOrder[window->FocusOrder] == window); + for (int n = window->FocusOrder + 1; n < g.WindowsFocusOrder.Size; n++) + g.WindowsFocusOrder[n]->FocusOrder--; + g.WindowsFocusOrder.erase(g.WindowsFocusOrder.Data + window->FocusOrder); + window->FocusOrder = -1; + } + window->IsExplicitChild = new_is_explicit_child; +} + +void ImGui::BringWindowToFocusFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window == window->RootWindow); + + const int cur_order = window->FocusOrder; + IM_ASSERT(g.WindowsFocusOrder[cur_order] == window); + if (g.WindowsFocusOrder.back() == window) + return; + + const int new_order = g.WindowsFocusOrder.Size - 1; + for (int n = cur_order; n < new_order; n++) + { + g.WindowsFocusOrder[n] = g.WindowsFocusOrder[n + 1]; + g.WindowsFocusOrder[n]->FocusOrder--; + IM_ASSERT(g.WindowsFocusOrder[n]->FocusOrder == n); + } + g.WindowsFocusOrder[new_order] = window; + window->FocusOrder = (short)new_order; +} + +// Note technically focus related but rather adjacent and close to BringWindowToFocusFront() +void ImGui::BringWindowToDisplayFront(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* current_front_window = g.Windows.back(); + if (current_front_window == window || current_front_window->RootWindowDockTree == window) // Cheap early out (could be better) + return; + for (int i = g.Windows.Size - 2; i >= 0; i--) // We can ignore the top-most window + if (g.Windows[i] == window) + { + memmove(&g.Windows[i], &g.Windows[i + 1], (size_t)(g.Windows.Size - i - 1) * sizeof(ImGuiWindow*)); + g.Windows[g.Windows.Size - 1] = window; + break; + } +} + +void ImGui::BringWindowToDisplayBack(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.Windows[0] == window) + return; + for (int i = 0; i < g.Windows.Size; i++) + if (g.Windows[i] == window) + { + memmove(&g.Windows[1], &g.Windows[0], (size_t)i * sizeof(ImGuiWindow*)); + g.Windows[0] = window; + break; + } +} + +void ImGui::BringWindowToDisplayBehind(ImGuiWindow* window, ImGuiWindow* behind_window) +{ + IM_ASSERT(window != NULL && behind_window != NULL); + ImGuiContext& g = *GImGui; + window = window->RootWindow; + behind_window = behind_window->RootWindow; + int pos_wnd = FindWindowDisplayIndex(window); + int pos_beh = FindWindowDisplayIndex(behind_window); + if (pos_wnd < pos_beh) + { + size_t copy_bytes = (pos_beh - pos_wnd - 1) * sizeof(ImGuiWindow*); + memmove(&g.Windows.Data[pos_wnd], &g.Windows.Data[pos_wnd + 1], copy_bytes); + g.Windows[pos_beh - 1] = window; + } + else + { + size_t copy_bytes = (pos_wnd - pos_beh) * sizeof(ImGuiWindow*); + memmove(&g.Windows.Data[pos_beh + 1], &g.Windows.Data[pos_beh], copy_bytes); + g.Windows[pos_beh] = window; + } +} + +int ImGui::FindWindowDisplayIndex(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + return g.Windows.index_from_ptr(g.Windows.find(window)); +} + +// Moving window to front of display and set focus (which happens to be back of our sorted list) +void ImGui::FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags) +{ + ImGuiContext& g = *GImGui; + + // Modal check? + if ((flags & ImGuiFocusRequestFlags_UnlessBelowModal) && (g.NavWindow != window)) // Early out in common case. + if (ImGuiWindow* blocking_modal = FindBlockingModal(window)) + { + // This block would typically be reached in two situations: + // - API call to FocusWindow() with a window under a modal and ImGuiFocusRequestFlags_UnlessBelowModal flag. + // - User clicking on void or anything behind a modal while a modal is open (window == NULL) + IMGUI_DEBUG_LOG_FOCUS("[focus] FocusWindow(\"%s\", UnlessBelowModal): prevented by \"%s\".\n", window ? window->Name : "", blocking_modal->Name); + if (window && window == window->RootWindow && (window->Flags & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) + BringWindowToDisplayBehind(window, blocking_modal); // Still bring right under modal. (FIXME: Could move in focus list too?) + ClosePopupsOverWindow(GetTopMostPopupModal(), false); // Note how we need to use GetTopMostPopupModal() aad NOT blocking_modal, to handle nested modals + return; + } + + // Find last focused child (if any) and focus it instead. + if ((flags & ImGuiFocusRequestFlags_RestoreFocusedChild) && window != NULL) + window = NavRestoreLastChildNavWindow(window); + + // Apply focus + if (g.NavWindow != window) + { + SetNavWindow(window); + if (window && g.NavHighlightItemUnderNav) + g.NavMousePosDirty = true; + g.NavId = window ? window->NavLastIds[0] : 0; // Restore NavId + g.NavLayer = ImGuiNavLayer_Main; + SetNavFocusScope(window ? window->NavRootFocusScopeId : 0); + g.NavIdIsAlive = false; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + + // Close popups if any + ClosePopupsOverWindow(window, false); + } + + // Move the root window to the top of the pile + IM_ASSERT(window == NULL || window->RootWindowDockTree != NULL); + ImGuiWindow* focus_front_window = window ? window->RootWindow : NULL; + ImGuiWindow* display_front_window = window ? window->RootWindowDockTree : NULL; + ImGuiDockNode* dock_node = window ? window->DockNode : NULL; + bool active_id_window_is_dock_node_host = (g.ActiveIdWindow && dock_node && dock_node->HostWindow == g.ActiveIdWindow); + + // Steal active widgets. Some of the cases it triggers includes: + // - Focus a window while an InputText in another window is active, if focus happens before the old InputText can run. + // - When using Nav to activate menu items (due to timing of activating on press->new window appears->losing ActiveId) + // - Using dock host items (tab, collapse button) can trigger this before we redirect the ActiveIdWindow toward the child window. + if (g.ActiveId != 0 && g.ActiveIdWindow && g.ActiveIdWindow->RootWindow != focus_front_window) + if (!g.ActiveIdNoClearOnFocusLoss && !active_id_window_is_dock_node_host) + ClearActiveID(); + + // Passing NULL allow to disable keyboard focus + if (!window) + return; + window->LastFrameJustFocused = g.FrameCount; + + // Select in dock node + // For #2304 we avoid applying focus immediately before the tabbar is visible. + //if (dock_node && dock_node->TabBar) + // dock_node->TabBar->SelectedTabId = dock_node->TabBar->NextSelectedTabId = window->TabId; + + // Bring to front + BringWindowToFocusFront(focus_front_window); + if (((window->Flags | focus_front_window->Flags | display_front_window->Flags) & ImGuiWindowFlags_NoBringToFrontOnFocus) == 0) + BringWindowToDisplayFront(display_front_window); +} + +void ImGui::FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window, ImGuiViewport* filter_viewport, ImGuiFocusRequestFlags flags) +{ + ImGuiContext& g = *GImGui; + int start_idx = g.WindowsFocusOrder.Size - 1; + if (under_this_window != NULL) + { + // Aim at root window behind us, if we are in a child window that's our own root (see #4640) + int offset = -1; + while (under_this_window->Flags & ImGuiWindowFlags_ChildWindow) + { + under_this_window = under_this_window->ParentWindow; + offset = 0; + } + start_idx = FindWindowFocusIndex(under_this_window) + offset; + } + for (int i = start_idx; i >= 0; i--) + { + // We may later decide to test for different NoXXXInputs based on the active navigation input (mouse vs nav) but that may feel more confusing to the user. + ImGuiWindow* window = g.WindowsFocusOrder[i]; + if (window == ignore_window || !window->WasActive) + continue; + if (filter_viewport != NULL && window->Viewport != filter_viewport) + continue; + if ((window->Flags & (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) != (ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs)) + { + // FIXME-DOCK: When ImGuiFocusRequestFlags_RestoreFocusedChild is set... + // This is failing (lagging by one frame) for docked windows. + // If A and B are docked into window and B disappear, at the NewFrame() call site window->NavLastChildNavWindow will still point to B. + // We might leverage the tab order implicitly stored in window->DockNodeAsHost->TabBar (essentially the 'most_recently_selected_tab' code in tab bar will do that but on next update) + // to tell which is the "previous" window. Or we may leverage 'LastFrameFocused/LastFrameJustFocused' and have this function handle child window itself? + FocusWindow(window, flags); + return; + } + } + FocusWindow(NULL, flags); +} //----------------------------------------------------------------------------- // [SECTION] KEYBOARD/GAMEPAD NAVIGATION //----------------------------------------------------------------------------- -ImGuiDir ImGetDirQuadrantFromDelta(float dx, float dy) +// FIXME-NAV: The existence of SetNavID vs SetFocusID vs FocusWindow() needs to be clarified/reworked. +// In our terminology those should be interchangeable, yet right now this is super confusing. +// Those two functions are merely a legacy artifact, so at minimum naming should be clarified. + +void ImGui::SetNavCursorVisible(bool visible) +{ + ImGuiContext& g = *GImGui; + if (g.IO.ConfigNavCursorVisibleAlways) + visible = true; + g.NavCursorVisible = visible; +} + +// (was called NavRestoreHighlightAfterMove() before 1.91.4) +void ImGui::SetNavCursorVisibleAfterMove() +{ + ImGuiContext& g = *GImGui; + if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = true; + g.NavHighlightItemUnderNav = g.NavMousePosDirty = true; +} + +void ImGui::SetNavWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.NavWindow != window) + { + IMGUI_DEBUG_LOG_FOCUS("[focus] SetNavWindow(\"%s\")\n", window ? window->Name : ""); + g.NavWindow = window; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + } + g.NavInitRequest = g.NavMoveSubmitted = g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavHighlightActivated(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + g.NavHighlightActivatedId = id; + g.NavHighlightActivatedTimer = NAV_ACTIVATE_HIGHLIGHT_TIMER; +} + +void ImGui::NavClearPreferredPosForAxis(ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer][axis] = FLT_MAX; +} + +void ImGui::SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow != NULL); + IM_ASSERT(nav_layer == ImGuiNavLayer_Main || nav_layer == ImGuiNavLayer_Menu); + g.NavId = id; + g.NavLayer = nav_layer; + SetNavFocusScope(focus_scope_id); + g.NavWindow->NavLastIds[nav_layer] = id; + g.NavWindow->NavRectRel[nav_layer] = rect_rel; + + // Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it) + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); +} + +void ImGui::SetFocusID(ImGuiID id, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(id != 0); + + if (g.NavWindow != window) + SetNavWindow(window); + + // Assume that SetFocusID() is called in the context where its window->DC.NavLayerCurrent and g.CurrentFocusScopeId are valid. + // Note that window may be != g.CurrentWindow (e.g. SetFocusID call in InputTextEx for multi-line text) + const ImGuiNavLayer nav_layer = window->DC.NavLayerCurrent; + g.NavId = id; + g.NavLayer = nav_layer; + SetNavFocusScope(g.CurrentFocusScopeId); + window->NavLastIds[nav_layer] = id; + if (g.LastItemData.ID == id) + window->NavRectRel[nav_layer] = WindowRectAbsToRel(window, g.LastItemData.NavRect); + + if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) + g.NavHighlightItemUnderNav = true; + else if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; + + // Clear preferred scoring position (NavMoveRequestApplyResult() will tend to restore it) + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); +} + +static ImGuiDir ImGetDirQuadrantFromDelta(float dx, float dy) { if (ImFabs(dx) > ImFabs(dy)) return (dx > 0.0f) ? ImGuiDir_Right : ImGuiDir_Left; return (dy > 0.0f) ? ImGuiDir_Down : ImGuiDir_Up; } -static float inline NavScoreItemDistInterval(float a0, float a1, float b0, float b1) +static float inline NavScoreItemDistInterval(float cand_min, float cand_max, float curr_min, float curr_max) { - if (a1 < b0) - return a1 - b0; - if (b1 < a0) - return a0 - b1; + if (cand_max < curr_min) + return cand_max - curr_min; + if (curr_max < cand_min) + return cand_min - curr_max; return 0.0f; } -static void inline NavClampRectToVisibleAreaForMoveDir(ImGuiDir move_dir, ImRect& r, const ImRect& clip_rect) -{ - if (move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) - { - r.Min.y = ImClamp(r.Min.y, clip_rect.Min.y, clip_rect.Max.y); - r.Max.y = ImClamp(r.Max.y, clip_rect.Min.y, clip_rect.Max.y); - } - else - { - r.Min.x = ImClamp(r.Min.x, clip_rect.Min.x, clip_rect.Max.x); - r.Max.x = ImClamp(r.Max.x, clip_rect.Min.x, clip_rect.Max.x); - } -} - -// Scoring function for directional navigation. Based on https://gist.github.com/rygorous/6981057 -static bool ImGui::NavScoreItem(ImGuiNavMoveResult* result, ImRect cand) +// Scoring function for keyboard/gamepad directional navigation. Based on https://gist.github.com/rygorous/6981057 +static bool ImGui::NavScoreItem(ImGuiNavItemData* result) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; if (g.NavLayer != window->DC.NavLayerCurrent) return false; - const ImRect& curr = g.NavScoringRectScreen; // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width) - g.NavScoringCount++; + // FIXME: Those are not good variables names + ImRect cand = g.LastItemData.NavRect; // Current item nav rectangle + const ImRect curr = g.NavScoringRect; // Current modified source rect (NB: we've applied Max.x = Min.x in NavUpdate() to inhibit the effect of having varied item width) + g.NavScoringDebugCount++; // When entering through a NavFlattened border, we consider child window items as fully clipped for scoring if (window->ParentWindow == g.NavWindow) { - IM_ASSERT((window->Flags | g.NavWindow->Flags) & ImGuiWindowFlags_NavFlattened); + IM_ASSERT((window->ChildFlags | g.NavWindow->ChildFlags) & ImGuiChildFlags_NavFlattened); if (!window->ClipRect.Overlaps(cand)) return false; cand.ClipWithFull(window->ClipRect); // This allows the scored item to not overlap other candidates in the parent window } - // We perform scoring on items bounding box clipped by the current clipping rectangle on the other axis (clipping on our movement axis would give us equal scores for all clipped items) - // For example, this ensure that items in one column are not reached when moving vertically from items in another column. - NavClampRectToVisibleAreaForMoveDir(g.NavMoveClipDir, cand, window->ClipRect); - // Compute distance between boxes // FIXME-NAV: Introducing biases for vertical navigation, needs to be removed. float dbx = NavScoreItemDistInterval(cand.Min.x, cand.Max.x, curr.Min.x, curr.Max.x); float dby = NavScoreItemDistInterval(ImLerp(cand.Min.y, cand.Max.y, 0.2f), ImLerp(cand.Min.y, cand.Max.y, 0.8f), ImLerp(curr.Min.y, curr.Max.y, 0.2f), ImLerp(curr.Min.y, curr.Max.y, 0.8f)); // Scale down on Y to keep using box-distance for vertically touching items if (dby != 0.0f && dbx != 0.0f) - dbx = (dbx/1000.0f) + ((dbx > 0.0f) ? +1.0f : -1.0f); + dbx = (dbx / 1000.0f) + ((dbx > 0.0f) ? +1.0f : -1.0f); float dist_box = ImFabs(dbx) + ImFabs(dby); // Compute distance between centers (this is off by a factor of 2, but we only compare center distances with each other so it doesn't matter) @@ -7903,6 +13164,9 @@ static bool ImGui::NavScoreItem(ImGuiNavMoveResult* result, ImRect cand) if (dbx != 0.0f || dby != 0.0f) { // For non-overlapping boxes, use distance between boxes + // FIXME-NAV: Quadrant may be incorrect because of (1) dbx bias and (2) curr.Max.y bias applied by NavBiasScoringRect() where typically curr.Max.y==curr.Min.y + // One typical case where this happens, with style.WindowMenuButtonPosition == ImGuiDir_Right, pressing Left to navigate from Close to Collapse tends to fail. + // Also see #6344. Calling ImGetDirQuadrantFromDelta() with unbiased values may be good but side-effects are plenty. dax = dbx; day = dby; dist_axial = dist_box; @@ -7919,36 +13183,45 @@ static bool ImGui::NavScoreItem(ImGuiNavMoveResult* result, ImRect cand) else { // Degenerate case: two overlapping buttons with same center, break ties arbitrarily (note that LastItemId here is really the _previous_ item order, but it doesn't matter) - quadrant = (window->DC.LastItemId < g.NavId) ? ImGuiDir_Left : ImGuiDir_Right; + quadrant = (g.LastItemData.ID < g.NavId) ? ImGuiDir_Left : ImGuiDir_Right; } + const ImGuiDir move_dir = g.NavMoveDir; #if IMGUI_DEBUG_NAV_SCORING - char buf[128]; - if (IsMouseHoveringRect(cand.Min, cand.Max)) + char buf[200]; + if (g.IO.KeyCtrl) // Hold CTRL to preview score in matching quadrant. CTRL+Arrow to rotate. { - ImFormatString(buf, IM_ARRAYSIZE(buf), "dbox (%.2f,%.2f->%.4f)\ndcen (%.2f,%.2f->%.4f)\nd (%.2f,%.2f->%.4f)\nnav %c, quadrant %c", dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "WENS"[g.NavMoveDir], "WENS"[quadrant]); - ImDrawList* draw_list = GetForegroundDrawList(window); - draw_list->AddRect(curr.Min, curr.Max, IM_COL32(255,200,0,100)); - draw_list->AddRect(cand.Min, cand.Max, IM_COL32(255,255,0,200)); - draw_list->AddRectFilled(cand.Max - ImVec2(4,4), cand.Max + CalcTextSize(buf) + ImVec2(4,4), IM_COL32(40,0,0,150)); - draw_list->AddText(g.IO.FontDefault, 13.0f, cand.Max, ~0U, buf); - } - else if (g.IO.KeyCtrl) // Hold to preview score in matching quadrant. Press C to rotate. - { - if (IsKeyPressedMap(ImGuiKey_C)) { g.NavMoveDirLast = (ImGuiDir)((g.NavMoveDirLast + 1) & 3); g.IO.KeysDownDuration[g.IO.KeyMap[ImGuiKey_C]] = 0.01f; } - if (quadrant == g.NavMoveDir) + if (quadrant == move_dir) { ImFormatString(buf, IM_ARRAYSIZE(buf), "%.0f/%.0f", dist_box, dist_center); ImDrawList* draw_list = GetForegroundDrawList(window); - draw_list->AddRectFilled(cand.Min, cand.Max, IM_COL32(255, 0, 0, 200)); - draw_list->AddText(g.IO.FontDefault, 13.0f, cand.Min, IM_COL32(255, 255, 255, 255), buf); + draw_list->AddRectFilled(cand.Min, cand.Max, IM_COL32(255, 0, 0, 80)); + draw_list->AddRectFilled(cand.Min, cand.Min + CalcTextSize(buf), IM_COL32(255, 0, 0, 200)); + draw_list->AddText(cand.Min, IM_COL32(255, 255, 255, 255), buf); } } - #endif + const bool debug_hovering = IsMouseHoveringRect(cand.Min, cand.Max); + const bool debug_tty = (g.IO.KeyCtrl && IsKeyPressed(ImGuiKey_Space)); + if (debug_hovering || debug_tty) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), + "d-box (%7.3f,%7.3f) -> %7.3f\nd-center (%7.3f,%7.3f) -> %7.3f\nd-axial (%7.3f,%7.3f) -> %7.3f\nnav %c, quadrant %c", + dbx, dby, dist_box, dcx, dcy, dist_center, dax, day, dist_axial, "-WENS"[move_dir+1], "-WENS"[quadrant+1]); + if (debug_hovering) + { + ImDrawList* draw_list = GetForegroundDrawList(window); + draw_list->AddRect(curr.Min, curr.Max, IM_COL32(255, 200, 0, 100)); + draw_list->AddRect(cand.Min, cand.Max, IM_COL32(255, 255, 0, 200)); + draw_list->AddRectFilled(cand.Max - ImVec2(4, 4), cand.Max + CalcTextSize(buf) + ImVec2(4, 4), IM_COL32(40, 0, 0, 200)); + draw_list->AddText(cand.Max, ~0U, buf); + } + if (debug_tty) { IMGUI_DEBUG_LOG_NAV("id 0x%08X\n%s\n", g.LastItemData.ID, buf); } + } +#endif - // Is it in the quadrant we're interesting in moving to? + // Is it in the quadrant we're interested in moving to? bool new_best = false; - if (quadrant == g.NavMoveDir) + if (quadrant == move_dir) { // Does it beat the current best candidate? if (dist_box < result->DistBox) @@ -7970,7 +13243,7 @@ static bool ImGui::NavScoreItem(ImGuiNavMoveResult* result, ImRect cand) // Still tied! we need to be extra-careful to make sure everything gets linked properly. We consistently break ties by symbolically moving "later" items // (with higher index) to the right/downwards by an infinitesimal amount since we the current "best" button already (so it must have a lower index), // this is fairly easy. This rule ensures that all buttons with dx==dy==0 will end up being linked in order of appearance along the x axis. - if (((g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? dby : dbx) < 0.0f) // moving bj to the right/down decreases distance + if (((move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) ? dby : dbx) < 0.0f) // moving bj to the right/down decreases distance new_best = true; } } @@ -7982,8 +13255,8 @@ static bool ImGui::NavScoreItem(ImGuiNavMoveResult* result, ImRect cand) // 2017/09/29: FIXME: This now currently only enabled inside menu bars, ideally we'd disable it everywhere. Menus in particular need to catch failure. For general navigation it feels awkward. // Disabling it may lead to disconnected graphs when nodes are very spaced out on different axis. Perhaps consider offering this as an option? if (result->DistBox == FLT_MAX && dist_axial < result->DistAxial) // Check axial match - if (g.NavLayer == 1 && !(g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) - if ((g.NavMoveDir == ImGuiDir_Left && dax < 0.0f) || (g.NavMoveDir == ImGuiDir_Right && dax > 0.0f) || (g.NavMoveDir == ImGuiDir_Up && day < 0.0f) || (g.NavMoveDir == ImGuiDir_Down && day > 0.0f)) + if (g.NavLayer == ImGuiNavLayer_Menu && !(g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) + if ((move_dir == ImGuiDir_Left && dax < 0.0f) || (move_dir == ImGuiDir_Right && dax > 0.0f) || (move_dir == ImGuiDir_Up && day < 0.0f) || (move_dir == ImGuiDir_Down && day > 0.0f)) { result->DistAxial = dist_axial; new_best = true; @@ -7992,26 +13265,58 @@ static bool ImGui::NavScoreItem(ImGuiNavMoveResult* result, ImRect cand) return new_best; } -// We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above) -static void ImGui::NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, const ImGuiID id) +static void ImGui::NavApplyItemToResult(ImGuiNavItemData* result) { ImGuiContext& g = *GImGui; - //if (!g.IO.NavActive) // [2017/10/06] Removed this possibly redundant test but I am not sure of all the side-effects yet. Some of the feature here will need to work regardless of using a _NoNavInputs flag. - // return; + ImGuiWindow* window = g.CurrentWindow; + result->Window = window; + result->ID = g.LastItemData.ID; + result->FocusScopeId = g.CurrentFocusScopeId; + result->ItemFlags = g.LastItemData.ItemFlags; + result->RectRel = WindowRectAbsToRel(window, g.LastItemData.NavRect); + if (result->ItemFlags & ImGuiItemFlags_HasSelectionUserData) + { + IM_ASSERT(g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid); + result->SelectionUserData = g.NextItemData.SelectionUserData; // INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData. + } +} - const ImGuiItemFlags item_flags = window->DC.ItemFlags; - const ImRect nav_bb_rel(nav_bb.Min - window->Pos, nav_bb.Max - window->Pos); +// True when current work location may be scrolled horizontally when moving left / right. +// This is generally always true UNLESS within a column. We don't have a vertical equivalent. +void ImGui::NavUpdateCurrentWindowIsScrollPushableX() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + window->DC.NavIsScrollPushableX = (g.CurrentTable == NULL && window->DC.CurrentColumns == NULL); +} + +// We get there when either NavId == id, or when g.NavAnyRequest is set (which is updated by NavUpdateAnyRequestFlag above) +// This is called after LastItemData is set, but NextItemData is also still valid. +static void ImGui::NavProcessItem() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = g.LastItemData.ID; + const ImGuiItemFlags item_flags = g.LastItemData.ItemFlags; + + // When inside a container that isn't scrollable with Left<>Right, clip NavRect accordingly (#2221) + if (window->DC.NavIsScrollPushableX == false) + { + g.LastItemData.NavRect.Min.x = ImClamp(g.LastItemData.NavRect.Min.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + g.LastItemData.NavRect.Max.x = ImClamp(g.LastItemData.NavRect.Max.x, window->ClipRect.Min.x, window->ClipRect.Max.x); + } + const ImRect nav_bb = g.LastItemData.NavRect; // Process Init Request - if (g.NavInitRequest && g.NavLayer == window->DC.NavLayerCurrent) + if (g.NavInitRequest && g.NavLayer == window->DC.NavLayerCurrent && (item_flags & ImGuiItemFlags_Disabled) == 0) { // Even if 'ImGuiItemFlags_NoNavDefaultFocus' is on (typically collapse/close button) we record the first ResultId so they can be used as a fallback - if (!(item_flags & ImGuiItemFlags_NoNavDefaultFocus) || g.NavInitResultId == 0) + const bool candidate_for_nav_default_focus = (item_flags & ImGuiItemFlags_NoNavDefaultFocus) == 0; + if (candidate_for_nav_default_focus || g.NavInitResult.ID == 0) { - g.NavInitResultId = id; - g.NavInitResultRectRel = nav_bb_rel; + NavApplyItemToResult(&g.NavInitResult); } - if (!(item_flags & ImGuiItemFlags_NoNavDefaultFocus)) + if (candidate_for_nav_default_focus) { g.NavInitRequest = false; // Found a match, clear request NavUpdateAnyRequestFlag(); @@ -8019,144 +13324,252 @@ static void ImGui::NavProcessItem(ImGuiWindow* window, const ImRect& nav_bb, con } // Process Move Request (scoring for navigation) - // FIXME-NAV: Consider policy for double scoring (scoring from NavScoringRectScreen + scoring from a rect wrapped according to current wrapping policy) - if ((g.NavId != id || (g.NavMoveRequestFlags & ImGuiNavMoveFlags_AllowCurrentNavId)) && !(item_flags & (ImGuiItemFlags_Disabled|ImGuiItemFlags_NoNav))) + // FIXME-NAV: Consider policy for double scoring (scoring from NavScoringRect + scoring from a rect wrapped according to current wrapping policy) + if (g.NavMoveScoringItems && (item_flags & ImGuiItemFlags_Disabled) == 0) { - ImGuiNavMoveResult* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; -#if IMGUI_DEBUG_NAV_SCORING - // [DEBUG] Score all items in NavWindow at all times - if (!g.NavMoveRequest) - g.NavMoveDir = g.NavMoveDirLast; - bool new_best = NavScoreItem(result, nav_bb) && g.NavMoveRequest; -#else - bool new_best = g.NavMoveRequest && NavScoreItem(result, nav_bb); -#endif - if (new_best) + if ((g.NavMoveFlags & ImGuiNavMoveFlags_FocusApi) || (window->Flags & ImGuiWindowFlags_NoNavInputs) == 0) { - result->ID = id; - result->SelectScopeId = g.MultiSelectScopeId; - result->Window = window; - result->RectRel = nav_bb_rel; - } + const bool is_tabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) != 0; + if (is_tabbing) + { + NavProcessItemForTabbingRequest(id, item_flags, g.NavMoveFlags); + } + else if (g.NavId != id || (g.NavMoveFlags & ImGuiNavMoveFlags_AllowCurrentNavId)) + { + ImGuiNavItemData* result = (window == g.NavWindow) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; + if (NavScoreItem(result)) + NavApplyItemToResult(result); - const float VISIBLE_RATIO = 0.70f; - if ((g.NavMoveRequestFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) && window->ClipRect.Overlaps(nav_bb)) - if (ImClamp(nav_bb.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y) - ImClamp(nav_bb.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO) - if (NavScoreItem(&g.NavMoveResultLocalVisibleSet, nav_bb)) - { - result = &g.NavMoveResultLocalVisibleSet; - result->ID = id; - result->SelectScopeId = g.MultiSelectScopeId; - result->Window = window; - result->RectRel = nav_bb_rel; - } + // Features like PageUp/PageDown need to maintain a separate score for the visible set of items. + const float VISIBLE_RATIO = 0.70f; + if ((g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) && window->ClipRect.Overlaps(nav_bb)) + if (ImClamp(nav_bb.Max.y, window->ClipRect.Min.y, window->ClipRect.Max.y) - ImClamp(nav_bb.Min.y, window->ClipRect.Min.y, window->ClipRect.Max.y) >= (nav_bb.Max.y - nav_bb.Min.y) * VISIBLE_RATIO) + if (NavScoreItem(&g.NavMoveResultLocalVisible)) + NavApplyItemToResult(&g.NavMoveResultLocalVisible); + } + } } - // Update window-relative bounding box of navigated item + // Update information for currently focused/navigated item if (g.NavId == id) { - g.NavWindow = window; // Always refresh g.NavWindow, because some operations such as FocusItem() don't have a window. + if (g.NavWindow != window) + SetNavWindow(window); // Always refresh g.NavWindow, because some operations such as FocusItem() may not have a window. g.NavLayer = window->DC.NavLayerCurrent; + SetNavFocusScope(g.CurrentFocusScopeId); // Will set g.NavFocusScopeId AND store g.NavFocusScopePath + g.NavFocusScopeId = g.CurrentFocusScopeId; g.NavIdIsAlive = true; - g.NavIdTabCounter = window->DC.FocusCounterTab; - window->NavRectRel[window->DC.NavLayerCurrent] = nav_bb_rel; // Store item bounding box (relative to window position) + if (g.LastItemData.ItemFlags & ImGuiItemFlags_HasSelectionUserData) + { + IM_ASSERT(g.NextItemData.SelectionUserData != ImGuiSelectionUserData_Invalid); + g.NavLastValidSelectionUserData = g.NextItemData.SelectionUserData; // INTENTIONAL: At this point this field is not cleared in NextItemData. Avoid unnecessary copy to LastItemData. + } + window->NavRectRel[window->DC.NavLayerCurrent] = WindowRectAbsToRel(window, nav_bb); // Store item bounding box (relative to window position) + } +} + +// Handle "scoring" of an item for a tabbing/focusing request initiated by NavUpdateCreateTabbingRequest(). +// Note that SetKeyboardFocusHere() API calls are considered tabbing requests! +// - Case 1: no nav/active id: set result to first eligible item, stop storing. +// - Case 2: tab forward: on ref id set counter, on counter elapse store result +// - Case 3: tab forward wrap: set result to first eligible item (preemptively), on ref id set counter, on next frame if counter hasn't elapsed store result. // FIXME-TABBING: Could be done as a next-frame forwarded request +// - Case 4: tab backward: store all results, on ref id pick prev, stop storing +// - Case 5: tab backward wrap: store all results, on ref id if no result keep storing until last // FIXME-TABBING: Could be done as next-frame forwarded requested +void ImGui::NavProcessItemForTabbingRequest(ImGuiID id, ImGuiItemFlags item_flags, ImGuiNavMoveFlags move_flags) +{ + ImGuiContext& g = *GImGui; + + if ((move_flags & ImGuiNavMoveFlags_FocusApi) == 0) + { + if (g.NavLayer != g.CurrentWindow->DC.NavLayerCurrent) + return; + if (g.NavFocusScopeId != g.CurrentFocusScopeId) + return; + } + + // - Can always land on an item when using API call. + // - Tabbing with _NavEnableKeyboard (space/enter/arrows): goes through every item. + // - Tabbing without _NavEnableKeyboard: goes through inputable items only. + bool can_stop; + if (move_flags & ImGuiNavMoveFlags_FocusApi) + can_stop = true; + else + can_stop = (item_flags & ImGuiItemFlags_NoTabStop) == 0 && ((g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) || (item_flags & ImGuiItemFlags_Inputable)); + + // Always store in NavMoveResultLocal (unlike directional request which uses NavMoveResultOther on sibling/flattened windows) + ImGuiNavItemData* result = &g.NavMoveResultLocal; + if (g.NavTabbingDir == +1) + { + // Tab Forward or SetKeyboardFocusHere() with >= 0 + if (can_stop && g.NavTabbingResultFirst.ID == 0) + NavApplyItemToResult(&g.NavTabbingResultFirst); + if (can_stop && g.NavTabbingCounter > 0 && --g.NavTabbingCounter == 0) + NavMoveRequestResolveWithLastItem(result); + else if (g.NavId == id) + g.NavTabbingCounter = 1; + } + else if (g.NavTabbingDir == -1) + { + // Tab Backward + if (g.NavId == id) + { + if (result->ID) + { + g.NavMoveScoringItems = false; + NavUpdateAnyRequestFlag(); + } + } + else if (can_stop) + { + // Keep applying until reaching NavId + NavApplyItemToResult(result); + } + } + else if (g.NavTabbingDir == 0) + { + if (can_stop && g.NavId == id) + NavMoveRequestResolveWithLastItem(result); + if (can_stop && g.NavTabbingResultFirst.ID == 0) // Tab init + NavApplyItemToResult(&g.NavTabbingResultFirst); } } bool ImGui::NavMoveRequestButNoResultYet() { ImGuiContext& g = *GImGui; - return g.NavMoveRequest && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; + return g.NavMoveScoringItems && g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0; +} + +// FIXME: ScoringRect is not set +void ImGui::NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.NavWindow != NULL); + //IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestSubmit: dir %c, window \"%s\"\n", "-WENS"[move_dir + 1], g.NavWindow->Name); + + if (move_flags & ImGuiNavMoveFlags_IsTabbing) + move_flags |= ImGuiNavMoveFlags_AllowCurrentNavId; + + g.NavMoveSubmitted = g.NavMoveScoringItems = true; + g.NavMoveDir = move_dir; + g.NavMoveDirForDebug = move_dir; + g.NavMoveClipDir = clip_dir; + g.NavMoveFlags = move_flags; + g.NavMoveScrollFlags = scroll_flags; + g.NavMoveForwardToNextFrame = false; + g.NavMoveKeyMods = (move_flags & ImGuiNavMoveFlags_FocusApi) ? 0 : g.IO.KeyMods; + g.NavMoveResultLocal.Clear(); + g.NavMoveResultLocalVisible.Clear(); + g.NavMoveResultOther.Clear(); + g.NavTabbingCounter = 0; + g.NavTabbingResultFirst.Clear(); + NavUpdateAnyRequestFlag(); +} + +void ImGui::NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result) +{ + ImGuiContext& g = *GImGui; + g.NavMoveScoringItems = false; // Ensure request doesn't need more processing + NavApplyItemToResult(result); + NavUpdateAnyRequestFlag(); +} + +// Called by TreePop() to implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere +void ImGui::NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiTreeNodeStackData* tree_node_data) +{ + ImGuiContext& g = *GImGui; + g.NavMoveScoringItems = false; + g.LastItemData.ID = tree_node_data->ID; + g.LastItemData.ItemFlags = tree_node_data->ItemFlags & ~ImGuiItemFlags_HasSelectionUserData; // Losing SelectionUserData, recovered next-frame (cheaper). + g.LastItemData.NavRect = tree_node_data->NavRect; + NavApplyItemToResult(result); // Result this instead of implementing a NavApplyPastTreeNodeToResult() + NavClearPreferredPosForAxis(ImGuiAxis_Y); + NavUpdateAnyRequestFlag(); } void ImGui::NavMoveRequestCancel() { ImGuiContext& g = *GImGui; - g.NavMoveRequest = false; + g.NavMoveSubmitted = g.NavMoveScoringItems = false; NavUpdateAnyRequestFlag(); } -void ImGui::NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, const ImRect& bb_rel, ImGuiNavMoveFlags move_flags) +// Forward will reuse the move request again on the next frame (generally with modifications done to it) +void ImGui::NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags) { ImGuiContext& g = *GImGui; - IM_ASSERT(g.NavMoveRequestForward == ImGuiNavForward_None); + IM_ASSERT(g.NavMoveForwardToNextFrame == false); NavMoveRequestCancel(); + g.NavMoveForwardToNextFrame = true; g.NavMoveDir = move_dir; g.NavMoveClipDir = clip_dir; - g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued; - g.NavMoveRequestFlags = move_flags; - g.NavWindow->NavRectRel[g.NavLayer] = bb_rel; + g.NavMoveFlags = move_flags | ImGuiNavMoveFlags_Forwarded; + g.NavMoveScrollFlags = scroll_flags; } -void ImGui::NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags move_flags) +// Navigation wrap-around logic is delayed to the end of the frame because this operation is only valid after entire +// popup is assembled and in case of appended popups it is not clear which EndPopup() call is final. +void ImGui::NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags wrap_flags) { ImGuiContext& g = *GImGui; - if (g.NavWindow != window || !NavMoveRequestButNoResultYet() || g.NavMoveRequestForward != ImGuiNavForward_None || g.NavLayer != 0) - return; - IM_ASSERT(move_flags != 0); // No points calling this with no wrapping - ImRect bb_rel = window->NavRectRel[0]; + IM_ASSERT((wrap_flags & ImGuiNavMoveFlags_WrapMask_ ) != 0 && (wrap_flags & ~ImGuiNavMoveFlags_WrapMask_) == 0); // Call with _WrapX, _WrapY, _LoopX, _LoopY - ImGuiDir clip_dir = g.NavMoveDir; - if (g.NavMoveDir == ImGuiDir_Left && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) - { - bb_rel.Min.x = bb_rel.Max.x = ImMax(window->SizeFull.x, window->ContentSize.x + window->WindowPadding.x * 2.0f) - window->Scroll.x; - if (move_flags & ImGuiNavMoveFlags_WrapX) { bb_rel.TranslateY(-bb_rel.GetHeight()); clip_dir = ImGuiDir_Up; } - NavMoveRequestForward(g.NavMoveDir, clip_dir, bb_rel, move_flags); - } - if (g.NavMoveDir == ImGuiDir_Right && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) - { - bb_rel.Min.x = bb_rel.Max.x = -window->Scroll.x; - if (move_flags & ImGuiNavMoveFlags_WrapX) { bb_rel.TranslateY(+bb_rel.GetHeight()); clip_dir = ImGuiDir_Down; } - NavMoveRequestForward(g.NavMoveDir, clip_dir, bb_rel, move_flags); - } - if (g.NavMoveDir == ImGuiDir_Up && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) - { - bb_rel.Min.y = bb_rel.Max.y = ImMax(window->SizeFull.y, window->ContentSize.y + window->WindowPadding.y * 2.0f) - window->Scroll.y; - if (move_flags & ImGuiNavMoveFlags_WrapY) { bb_rel.TranslateX(-bb_rel.GetWidth()); clip_dir = ImGuiDir_Left; } - NavMoveRequestForward(g.NavMoveDir, clip_dir, bb_rel, move_flags); - } - if (g.NavMoveDir == ImGuiDir_Down && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) - { - bb_rel.Min.y = bb_rel.Max.y = -window->Scroll.y; - if (move_flags & ImGuiNavMoveFlags_WrapY) { bb_rel.TranslateX(+bb_rel.GetWidth()); clip_dir = ImGuiDir_Right; } - NavMoveRequestForward(g.NavMoveDir, clip_dir, bb_rel, move_flags); - } + // In theory we should test for NavMoveRequestButNoResultYet() but there's no point doing it: + // as NavEndFrame() will do the same test. It will end up calling NavUpdateCreateWrappingRequest(). + if (g.NavWindow == window && g.NavMoveScoringItems && g.NavLayer == ImGuiNavLayer_Main) + g.NavMoveFlags = (g.NavMoveFlags & ~ImGuiNavMoveFlags_WrapMask_) | wrap_flags; } // FIXME: This could be replaced by updating a frame number in each window when (window == NavWindow) and (NavLayer == 0). // This way we could find the last focused window among our children. It would be much less confusing this way? static void ImGui::NavSaveLastChildNavWindowIntoParent(ImGuiWindow* nav_window) { - ImGuiWindow* parent_window = nav_window; - while (parent_window && (parent_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 && (parent_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) - parent_window = parent_window->ParentWindow; - if (parent_window && parent_window != nav_window) - parent_window->NavLastChildNavWindow = nav_window; + ImGuiWindow* parent = nav_window; + while (parent && parent->RootWindow != parent && (parent->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) + parent = parent->ParentWindow; + if (parent && parent != nav_window) + parent->NavLastChildNavWindow = nav_window; } // Restore the last focused child. // Call when we are expected to land on the Main Layer (0) after FocusWindow() static ImGuiWindow* ImGui::NavRestoreLastChildNavWindow(ImGuiWindow* window) { - return window->NavLastChildNavWindow ? window->NavLastChildNavWindow : window; + if (window->NavLastChildNavWindow && window->NavLastChildNavWindow->WasActive) + return window->NavLastChildNavWindow; + if (window->DockNodeAsHost && window->DockNodeAsHost->TabBar) + if (ImGuiTabItem* tab = TabBarFindMostRecentlySelectedTabForActiveWindow(window->DockNodeAsHost->TabBar)) + return tab->Window; + return window; } -static void NavRestoreLayer(ImGuiNavLayer layer) +void ImGui::NavRestoreLayer(ImGuiNavLayer layer) { ImGuiContext& g = *GImGui; - g.NavLayer = layer; - if (layer == 0) - g.NavWindow = ImGui::NavRestoreLastChildNavWindow(g.NavWindow); - if (layer == 0 && g.NavWindow->NavLastIds[0] != 0) - ImGui::SetNavIDWithRectRel(g.NavWindow->NavLastIds[0], layer, g.NavWindow->NavRectRel[0]); + if (layer == ImGuiNavLayer_Main) + { + ImGuiWindow* prev_nav_window = g.NavWindow; + g.NavWindow = NavRestoreLastChildNavWindow(g.NavWindow); // FIXME-NAV: Should clear ongoing nav requests? + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + if (prev_nav_window) + IMGUI_DEBUG_LOG_FOCUS("[focus] NavRestoreLayer: from \"%s\" to SetNavWindow(\"%s\")\n", prev_nav_window->Name, g.NavWindow->Name); + } + ImGuiWindow* window = g.NavWindow; + if (window->NavLastIds[layer] != 0) + { + SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); + } else - ImGui::NavInitWindow(g.NavWindow, true); + { + g.NavLayer = layer; + NavInitWindow(window, true); + } } static inline void ImGui::NavUpdateAnyRequestFlag() { ImGuiContext& g = *GImGui; - g.NavAnyRequest = g.NavMoveRequest || g.NavInitRequest || (IMGUI_DEBUG_NAV_SCORING && g.NavWindow != NULL); + g.NavAnyRequest = g.NavMoveScoringItems || g.NavInitRequest || (IMGUI_DEBUG_NAV_SCORING && g.NavWindow != NULL); if (g.NavAnyRequest) IM_ASSERT(g.NavWindow != NULL); } @@ -8164,384 +13577,515 @@ static inline void ImGui::NavUpdateAnyRequestFlag() // This needs to be called before we submit any widget (aka in or before Begin) void ImGui::NavInitWindow(ImGuiWindow* window, bool force_reinit) { + // FIXME: ChildWindow test here is wrong for docking ImGuiContext& g = *GImGui; IM_ASSERT(window == g.NavWindow); + + if (window->Flags & ImGuiWindowFlags_NoNavInputs) + { + g.NavId = 0; + SetNavFocusScope(window->NavRootFocusScopeId); + return; + } + bool init_for_nav = false; - if (!(window->Flags & ImGuiWindowFlags_NoNavInputs)) - if (!(window->Flags & ImGuiWindowFlags_ChildWindow) || (window->Flags & ImGuiWindowFlags_Popup) || (window->NavLastIds[0] == 0) || force_reinit) - init_for_nav = true; + if (window == window->RootWindow || (window->Flags & ImGuiWindowFlags_Popup) || (window->NavLastIds[0] == 0) || force_reinit) + init_for_nav = true; + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from NavInitWindow(), init_for_nav=%d, window=\"%s\", layer=%d\n", init_for_nav, window->Name, g.NavLayer); if (init_for_nav) { - SetNavID(0, g.NavLayer); + SetNavID(0, g.NavLayer, window->NavRootFocusScopeId, ImRect()); g.NavInitRequest = true; g.NavInitRequestFromMove = false; - g.NavInitResultId = 0; - g.NavInitResultRectRel = ImRect(); + g.NavInitResult.ID = 0; NavUpdateAnyRequestFlag(); } else { g.NavId = window->NavLastIds[0]; + SetNavFocusScope(window->NavRootFocusScopeId); } } +static ImGuiInputSource ImGui::NavCalcPreferredRefPosSource() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + const bool activated_shortcut = g.ActiveId != 0 && g.ActiveIdFromShortcut && g.ActiveId == g.LastItemData.ID; + + // Testing for !activated_shortcut here could in theory be removed if we decided that activating a remote shortcut altered one of the g.NavDisableXXX flag. + if ((!g.NavCursorVisible || !g.NavHighlightItemUnderNav || !window) && !activated_shortcut) + return ImGuiInputSource_Mouse; + else + return ImGuiInputSource_Keyboard; // or Nav in general +} + static ImVec2 ImGui::NavCalcPreferredRefPos() { ImGuiContext& g = *GImGui; - if (g.NavDisableHighlight || !g.NavDisableMouseHover || !g.NavWindow) + ImGuiWindow* window = g.NavWindow; + ImGuiInputSource source = NavCalcPreferredRefPosSource(); + + const bool activated_shortcut = g.ActiveId != 0 && g.ActiveIdFromShortcut && g.ActiveId == g.LastItemData.ID; + + // Testing for !activated_shortcut here could in theory be removed if we decided that activating a remote shortcut altered one of the g.NavDisableXXX flag. + if (source == ImGuiInputSource_Mouse) { // Mouse (we need a fallback in case the mouse becomes invalid after being used) - if (IsMousePosValid(&g.IO.MousePos)) - return g.IO.MousePos; - return g.LastValidMousePos; + // The +1.0f offset when stored by OpenPopupEx() allows reopening this or another popup (same or another mouse button) while not moving the mouse, it is pretty standard. + // In theory we could move that +1.0f offset in OpenPopupEx() + ImVec2 p = IsMousePosValid(&g.IO.MousePos) ? g.IO.MousePos : g.MouseLastValidPos; + return ImVec2(p.x + 1.0f, p.y); } else { - // When navigation is active and mouse is disabled, decide on an arbitrary position around the bottom left of the currently navigated item. - const ImRect& rect_rel = g.NavWindow->NavRectRel[g.NavLayer]; - ImVec2 pos = g.NavWindow->Pos + ImVec2(rect_rel.Min.x + ImMin(g.Style.FramePadding.x * 4, rect_rel.GetWidth()), rect_rel.Max.y - ImMin(g.Style.FramePadding.y, rect_rel.GetHeight())); - ImRect visible_rect = GetViewportRect(); - return ImFloor(ImClamp(pos, visible_rect.Min, visible_rect.Max)); // ImFloor() is important because non-integer mouse position application in back-end might be lossy and result in undesirable non-zero delta. + // When navigation is active and mouse is disabled, pick a position around the bottom left of the currently navigated item + ImRect ref_rect; + if (activated_shortcut) + ref_rect = g.LastItemData.NavRect; + else + ref_rect = WindowRectRelToAbs(window, window->NavRectRel[g.NavLayer]); + + // Take account of upcoming scrolling (maybe set mouse pos should be done in EndFrame?) + if (window->LastFrameActive != g.FrameCount && (window->ScrollTarget.x != FLT_MAX || window->ScrollTarget.y != FLT_MAX)) + { + ImVec2 next_scroll = CalcNextScrollFromScrollTargetAndClamp(window); + ref_rect.Translate(window->Scroll - next_scroll); + } + ImVec2 pos = ImVec2(ref_rect.Min.x + ImMin(g.Style.FramePadding.x * 4, ref_rect.GetWidth()), ref_rect.Max.y - ImMin(g.Style.FramePadding.y, ref_rect.GetHeight())); + ImGuiViewport* viewport = window->Viewport; + return ImTrunc(ImClamp(pos, viewport->Pos, viewport->Pos + viewport->Size)); // ImTrunc() is important because non-integer mouse position application in backend might be lossy and result in undesirable non-zero delta. } } -float ImGui::GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode) +float ImGui::GetNavTweakPressedAmount(ImGuiAxis axis) { ImGuiContext& g = *GImGui; - if (mode == ImGuiInputReadMode_Down) - return g.IO.NavInputs[n]; // Instant, read analog input (0.0f..1.0f, as provided by user) + float repeat_delay, repeat_rate; + GetTypematicRepeatRate(ImGuiInputFlags_RepeatRateNavTweak, &repeat_delay, &repeat_rate); - const float t = g.IO.NavInputsDownDuration[n]; - if (t < 0.0f && mode == ImGuiInputReadMode_Released) // Return 1.0f when just released, no repeat, ignore analog input. - return (g.IO.NavInputsDownDurationPrev[n] >= 0.0f ? 1.0f : 0.0f); - if (t < 0.0f) - return 0.0f; - if (mode == ImGuiInputReadMode_Pressed) // Return 1.0f when just pressed, no repeat, ignore analog input. - return (t == 0.0f) ? 1.0f : 0.0f; - if (mode == ImGuiInputReadMode_Repeat) - return (float)CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 0.80f, g.IO.KeyRepeatRate * 0.80f); - if (mode == ImGuiInputReadMode_RepeatSlow) - return (float)CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 1.00f, g.IO.KeyRepeatRate * 2.00f); - if (mode == ImGuiInputReadMode_RepeatFast) - return (float)CalcTypematicPressedRepeatAmount(t, t - g.IO.DeltaTime, g.IO.KeyRepeatDelay * 0.80f, g.IO.KeyRepeatRate * 0.30f); - return 0.0f; -} - -ImVec2 ImGui::GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor, float fast_factor) -{ - ImVec2 delta(0.0f, 0.0f); - if (dir_sources & ImGuiNavDirSourceFlags_Keyboard) - delta += ImVec2(GetNavInputAmount(ImGuiNavInput_KeyRight_, mode) - GetNavInputAmount(ImGuiNavInput_KeyLeft_, mode), GetNavInputAmount(ImGuiNavInput_KeyDown_, mode) - GetNavInputAmount(ImGuiNavInput_KeyUp_, mode)); - if (dir_sources & ImGuiNavDirSourceFlags_PadDPad) - delta += ImVec2(GetNavInputAmount(ImGuiNavInput_DpadRight, mode) - GetNavInputAmount(ImGuiNavInput_DpadLeft, mode), GetNavInputAmount(ImGuiNavInput_DpadDown, mode) - GetNavInputAmount(ImGuiNavInput_DpadUp, mode)); - if (dir_sources & ImGuiNavDirSourceFlags_PadLStick) - delta += ImVec2(GetNavInputAmount(ImGuiNavInput_LStickRight, mode) - GetNavInputAmount(ImGuiNavInput_LStickLeft, mode), GetNavInputAmount(ImGuiNavInput_LStickDown, mode) - GetNavInputAmount(ImGuiNavInput_LStickUp, mode)); - if (slow_factor != 0.0f && IsNavInputDown(ImGuiNavInput_TweakSlow)) - delta *= slow_factor; - if (fast_factor != 0.0f && IsNavInputDown(ImGuiNavInput_TweakFast)) - delta *= fast_factor; - return delta; + ImGuiKey key_less, key_more; + if (g.NavInputSource == ImGuiInputSource_Gamepad) + { + key_less = (axis == ImGuiAxis_X) ? ImGuiKey_GamepadDpadLeft : ImGuiKey_GamepadDpadUp; + key_more = (axis == ImGuiAxis_X) ? ImGuiKey_GamepadDpadRight : ImGuiKey_GamepadDpadDown; + } + else + { + key_less = (axis == ImGuiAxis_X) ? ImGuiKey_LeftArrow : ImGuiKey_UpArrow; + key_more = (axis == ImGuiAxis_X) ? ImGuiKey_RightArrow : ImGuiKey_DownArrow; + } + float amount = (float)GetKeyPressedAmount(key_more, repeat_delay, repeat_rate) - (float)GetKeyPressedAmount(key_less, repeat_delay, repeat_rate); + if (amount != 0.0f && IsKeyDown(key_less) && IsKeyDown(key_more)) // Cancel when opposite directions are held, regardless of repeat phase + amount = 0.0f; + return amount; } static void ImGui::NavUpdate() { ImGuiContext& g = *GImGui; - g.IO.WantSetMousePos = false; -#if 0 - if (g.NavScoringCount > 0) IMGUI_DEBUG_LOG("NavScoringCount %d for '%s' layer %d (Init:%d, Move:%d)\n", g.FrameCount, g.NavScoringCount, g.NavWindow ? g.NavWindow->Name : "NULL", g.NavLayer, g.NavInitRequest || g.NavInitResultId != 0, g.NavMoveRequest); -#endif + ImGuiIO& io = g.IO; - // Set input source as Gamepad when buttons are pressed before we map Keyboard (some features differs when used with Gamepad vs Keyboard) - bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; - bool nav_gamepad_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (g.IO.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + io.WantSetMousePos = false; + //if (g.NavScoringDebugCount > 0) IMGUI_DEBUG_LOG_NAV("[nav] NavScoringDebugCount %d for '%s' layer %d (Init:%d, Move:%d)\n", g.NavScoringDebugCount, g.NavWindow ? g.NavWindow->Name : "NULL", g.NavLayer, g.NavInitRequest || g.NavInitResultId != 0, g.NavMoveRequest); + + // Set input source based on which keys are last pressed (as some features differs when used with Gamepad vs Keyboard) + // FIXME-NAV: Now that keys are separated maybe we can get rid of NavInputSource? + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const ImGuiKey nav_gamepad_keys_to_change_source[] = { ImGuiKey_GamepadFaceRight, ImGuiKey_GamepadFaceLeft, ImGuiKey_GamepadFaceUp, ImGuiKey_GamepadFaceDown, ImGuiKey_GamepadDpadRight, ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadUp, ImGuiKey_GamepadDpadDown }; if (nav_gamepad_active) - if (g.IO.NavInputs[ImGuiNavInput_Activate] > 0.0f || g.IO.NavInputs[ImGuiNavInput_Input] > 0.0f || g.IO.NavInputs[ImGuiNavInput_Cancel] > 0.0f || g.IO.NavInputs[ImGuiNavInput_Menu] > 0.0f) - g.NavInputSource = ImGuiInputSource_NavGamepad; - - // Update Keyboard->Nav inputs mapping + for (ImGuiKey key : nav_gamepad_keys_to_change_source) + if (IsKeyDown(key)) + g.NavInputSource = ImGuiInputSource_Gamepad; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + const ImGuiKey nav_keyboard_keys_to_change_source[] = { ImGuiKey_Space, ImGuiKey_Enter, ImGuiKey_Escape, ImGuiKey_RightArrow, ImGuiKey_LeftArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow }; if (nav_keyboard_active) - { - #define NAV_MAP_KEY(_KEY, _NAV_INPUT) do { if (IsKeyDown(g.IO.KeyMap[_KEY])) { g.IO.NavInputs[_NAV_INPUT] = 1.0f; g.NavInputSource = ImGuiInputSource_NavKeyboard; } } while (0) - NAV_MAP_KEY(ImGuiKey_Space, ImGuiNavInput_Activate ); - NAV_MAP_KEY(ImGuiKey_Enter, ImGuiNavInput_Input ); - NAV_MAP_KEY(ImGuiKey_Escape, ImGuiNavInput_Cancel ); - NAV_MAP_KEY(ImGuiKey_LeftArrow, ImGuiNavInput_KeyLeft_ ); - NAV_MAP_KEY(ImGuiKey_RightArrow,ImGuiNavInput_KeyRight_); - NAV_MAP_KEY(ImGuiKey_UpArrow, ImGuiNavInput_KeyUp_ ); - NAV_MAP_KEY(ImGuiKey_DownArrow, ImGuiNavInput_KeyDown_ ); - NAV_MAP_KEY(ImGuiKey_Tab, ImGuiNavInput_KeyTab_ ); - if (g.IO.KeyCtrl) - g.IO.NavInputs[ImGuiNavInput_TweakSlow] = 1.0f; - if (g.IO.KeyShift) - g.IO.NavInputs[ImGuiNavInput_TweakFast] = 1.0f; - if (g.IO.KeyAlt && !g.IO.KeyCtrl) // AltGR is Alt+Ctrl, also even on keyboards without AltGR we don't want Alt+Ctrl to open menu. - g.IO.NavInputs[ImGuiNavInput_KeyMenu_] = 1.0f; - #undef NAV_MAP_KEY - } - memcpy(g.IO.NavInputsDownDurationPrev, g.IO.NavInputsDownDuration, sizeof(g.IO.NavInputsDownDuration)); - for (int i = 0; i < IM_ARRAYSIZE(g.IO.NavInputs); i++) - g.IO.NavInputsDownDuration[i] = (g.IO.NavInputs[i] > 0.0f) ? (g.IO.NavInputsDownDuration[i] < 0.0f ? 0.0f : g.IO.NavInputsDownDuration[i] + g.IO.DeltaTime) : -1.0f; + for (ImGuiKey key : nav_keyboard_keys_to_change_source) + if (IsKeyDown(key)) + g.NavInputSource = ImGuiInputSource_Keyboard; // Process navigation init request (select first/default focus) - // In very rare cases g.NavWindow may be null (e.g. clearing focus after requesting an init request, which does happen when releasing Alt while clicking on void) - if (g.NavInitResultId != 0 && (!g.NavDisableHighlight || g.NavInitRequestFromMove) && g.NavWindow) - { - // Apply result from previous navigation init request (will typically select the first item, unless SetItemDefaultFocus() has been called) - if (g.NavInitRequestFromMove) - SetNavIDWithRectRel(g.NavInitResultId, g.NavLayer, g.NavInitResultRectRel); - else - SetNavID(g.NavInitResultId, g.NavLayer); - g.NavWindow->NavRectRel[g.NavLayer] = g.NavInitResultRectRel; - } + g.NavJustMovedToId = 0; + g.NavJustMovedToFocusScopeId = g.NavJustMovedFromFocusScopeId = 0; + if (g.NavInitResult.ID != 0) + NavInitRequestApplyResult(); g.NavInitRequest = false; g.NavInitRequestFromMove = false; - g.NavInitResultId = 0; - g.NavJustMovedToId = 0; + g.NavInitResult.ID = 0; // Process navigation move request - if (g.NavMoveRequest) - NavUpdateMoveResult(); + if (g.NavMoveSubmitted) + NavMoveRequestApplyResult(); + g.NavTabbingCounter = 0; + g.NavMoveSubmitted = g.NavMoveScoringItems = false; + if (g.NavCursorHideFrames > 0) + if (--g.NavCursorHideFrames == 0) + g.NavCursorVisible = true; - // When a forwarded move request failed, we restore the highlight that we disabled during the forward frame - if (g.NavMoveRequestForward == ImGuiNavForward_ForwardActive) - { - IM_ASSERT(g.NavMoveRequest); - if (g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0) - g.NavDisableHighlight = false; - g.NavMoveRequestForward = ImGuiNavForward_None; - } - - // Apply application mouse position movement, after we had a chance to process move request result. + // Schedule mouse position update (will be done at the bottom of this function, after 1) processing all move requests and 2) updating scrolling) + bool set_mouse_pos = false; if (g.NavMousePosDirty && g.NavIdIsAlive) - { - // Set mouse position given our knowledge of the navigated item position from last frame - if ((g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) && (g.IO.BackendFlags & ImGuiBackendFlags_HasSetMousePos)) - { - if (!g.NavDisableHighlight && g.NavDisableMouseHover && g.NavWindow) - { - g.IO.MousePos = g.IO.MousePosPrev = NavCalcPreferredRefPos(); - g.IO.WantSetMousePos = true; - } - } - g.NavMousePosDirty = false; - } - g.NavIdIsAlive = false; - g.NavJustTabbedId = 0; - IM_ASSERT(g.NavLayer == 0 || g.NavLayer == 1); + if (g.NavCursorVisible && g.NavHighlightItemUnderNav && g.NavWindow) + set_mouse_pos = true; + g.NavMousePosDirty = false; + IM_ASSERT(g.NavLayer == ImGuiNavLayer_Main || g.NavLayer == ImGuiNavLayer_Menu); - // Store our return window (for returning from Layer 1 to Layer 0) and clear it as soon as we step back in our own Layer 0 + // Store our return window (for returning from Menu Layer to Main Layer) and clear it as soon as we step back in our own Layer 0 if (g.NavWindow) NavSaveLastChildNavWindowIntoParent(g.NavWindow); - if (g.NavWindow && g.NavWindow->NavLastChildNavWindow != NULL && g.NavLayer == 0) + if (g.NavWindow && g.NavWindow->NavLastChildNavWindow != NULL && g.NavLayer == ImGuiNavLayer_Main) g.NavWindow->NavLastChildNavWindow = NULL; // Update CTRL+TAB and Windowing features (hold Square to move/resize/etc.) NavUpdateWindowing(); // Set output flags for user application - g.IO.NavActive = (nav_keyboard_active || nav_gamepad_active) && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs); - g.IO.NavVisible = (g.IO.NavActive && g.NavId != 0 && !g.NavDisableHighlight) || (g.NavWindowingTarget != NULL); + io.NavActive = (nav_keyboard_active || nav_gamepad_active) && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs); + io.NavVisible = (io.NavActive && g.NavId != 0 && g.NavCursorVisible) || (g.NavWindowingTarget != NULL); // Process NavCancel input (to close a popup, get back to parent, clear focus) - if (IsNavInputPressed(ImGuiNavInput_Cancel, ImGuiInputReadMode_Pressed)) - { - if (g.ActiveId != 0) - { - if (!(g.ActiveIdBlockNavInputFlags & (1 << ImGuiNavInput_Cancel))) - ClearActiveID(); - } - else if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow) && !(g.NavWindow->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->ParentWindow) - { - // Exit child window - ImGuiWindow* child_window = g.NavWindow; - ImGuiWindow* parent_window = g.NavWindow->ParentWindow; - IM_ASSERT(child_window->ChildId != 0); - FocusWindow(parent_window); - SetNavID(child_window->ChildId, 0); - g.NavIdIsAlive = false; - if (g.NavDisableMouseHover) - g.NavMousePosDirty = true; - } - else if (g.OpenPopupStack.Size > 0) - { - // Close open popup/menu - if (!(g.OpenPopupStack.back().Window->Flags & ImGuiWindowFlags_Modal)) - ClosePopupToLevel(g.OpenPopupStack.Size - 1, true); - } - else if (g.NavLayer != 0) - { - // Leave the "menu" layer - NavRestoreLayer(ImGuiNavLayer_Main); - } - else - { - // Clear NavLastId for popups but keep it for regular child window so we can leave one and come back where we were - if (g.NavWindow && ((g.NavWindow->Flags & ImGuiWindowFlags_Popup) || !(g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow))) - g.NavWindow->NavLastIds[0] = 0; - g.NavId = 0; - } - } + NavUpdateCancelRequest(); // Process manual activation request - g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavInputId = 0; - if (g.NavId != 0 && !g.NavDisableHighlight && !g.NavWindowingTarget && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = 0; + g.NavActivateFlags = ImGuiActivateFlags_None; + if (g.NavId != 0 && g.NavCursorVisible && !g.NavWindowingTarget && g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) { - bool activate_down = IsNavInputDown(ImGuiNavInput_Activate); - bool activate_pressed = activate_down && IsNavInputPressed(ImGuiNavInput_Activate, ImGuiInputReadMode_Pressed); + const bool activate_down = (nav_keyboard_active && IsKeyDown(ImGuiKey_Space, ImGuiKeyOwner_NoOwner)) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadActivate, ImGuiKeyOwner_NoOwner)); + const bool activate_pressed = activate_down && ((nav_keyboard_active && IsKeyPressed(ImGuiKey_Space, 0, ImGuiKeyOwner_NoOwner)) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadActivate, 0, ImGuiKeyOwner_NoOwner))); + const bool input_down = (nav_keyboard_active && (IsKeyDown(ImGuiKey_Enter, ImGuiKeyOwner_NoOwner) || IsKeyDown(ImGuiKey_KeypadEnter, ImGuiKeyOwner_NoOwner))) || (nav_gamepad_active && IsKeyDown(ImGuiKey_NavGamepadInput, ImGuiKeyOwner_NoOwner)); + const bool input_pressed = input_down && ((nav_keyboard_active && (IsKeyPressed(ImGuiKey_Enter, 0, ImGuiKeyOwner_NoOwner) || IsKeyPressed(ImGuiKey_KeypadEnter, 0, ImGuiKeyOwner_NoOwner))) || (nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadInput, 0, ImGuiKeyOwner_NoOwner))); if (g.ActiveId == 0 && activate_pressed) + { g.NavActivateId = g.NavId; - if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && activate_down) + g.NavActivateFlags = ImGuiActivateFlags_PreferTweak; + } + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && input_pressed) + { + g.NavActivateId = g.NavId; + g.NavActivateFlags = ImGuiActivateFlags_PreferInput; + } + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && (activate_down || input_down)) g.NavActivateDownId = g.NavId; - if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && activate_pressed) + if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && (activate_pressed || input_pressed)) + { g.NavActivatePressedId = g.NavId; - if ((g.ActiveId == 0 || g.ActiveId == g.NavId) && IsNavInputPressed(ImGuiNavInput_Input, ImGuiInputReadMode_Pressed)) - g.NavInputId = g.NavId; + NavHighlightActivated(g.NavId); + } } if (g.NavWindow && (g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) - g.NavDisableHighlight = true; + g.NavCursorVisible = false; + else if (g.IO.ConfigNavCursorVisibleAlways && g.NavCursorHideFrames == 0) + g.NavCursorVisible = true; if (g.NavActivateId != 0) IM_ASSERT(g.NavActivateDownId == g.NavActivateId); - g.NavMoveRequest = false; + + // Highlight + if (g.NavHighlightActivatedTimer > 0.0f) + g.NavHighlightActivatedTimer = ImMax(0.0f, g.NavHighlightActivatedTimer - io.DeltaTime); + if (g.NavHighlightActivatedTimer == 0.0f) + g.NavHighlightActivatedId = 0; // Process programmatic activation request + // FIXME-NAV: Those should eventually be queued (unlike focus they don't cancel each others) if (g.NavNextActivateId != 0) - g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavInputId = g.NavNextActivateId; + { + g.NavActivateId = g.NavActivateDownId = g.NavActivatePressedId = g.NavNextActivateId; + g.NavActivateFlags = g.NavNextActivateFlags; + } g.NavNextActivateId = 0; - // Initiate directional inputs request - const int allowed_dir_flags = (g.ActiveId == 0) ? ~0 : g.ActiveIdAllowNavDirFlags; - if (g.NavMoveRequestForward == ImGuiNavForward_None) - { - g.NavMoveDir = ImGuiDir_None; - g.NavMoveRequestFlags = ImGuiNavMoveFlags_None; - if (g.NavWindow && !g.NavWindowingTarget && allowed_dir_flags && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) - { - if ((allowed_dir_flags & (1 << ImGuiDir_Left)) && IsNavInputPressedAnyOfTwo(ImGuiNavInput_DpadLeft, ImGuiNavInput_KeyLeft_, ImGuiInputReadMode_Repeat)) { g.NavMoveDir = ImGuiDir_Left; } - if ((allowed_dir_flags & (1 << ImGuiDir_Right)) && IsNavInputPressedAnyOfTwo(ImGuiNavInput_DpadRight,ImGuiNavInput_KeyRight_,ImGuiInputReadMode_Repeat)) { g.NavMoveDir = ImGuiDir_Right; } - if ((allowed_dir_flags & (1 << ImGuiDir_Up)) && IsNavInputPressedAnyOfTwo(ImGuiNavInput_DpadUp, ImGuiNavInput_KeyUp_, ImGuiInputReadMode_Repeat)) { g.NavMoveDir = ImGuiDir_Up; } - if ((allowed_dir_flags & (1 << ImGuiDir_Down)) && IsNavInputPressedAnyOfTwo(ImGuiNavInput_DpadDown, ImGuiNavInput_KeyDown_, ImGuiInputReadMode_Repeat)) { g.NavMoveDir = ImGuiDir_Down; } - } - g.NavMoveClipDir = g.NavMoveDir; - } - else - { - // Forwarding previous request (which has been modified, e.g. wrap around menus rewrite the requests with a starting rectangle at the other side of the window) - // (Preserve g.NavMoveRequestFlags, g.NavMoveClipDir which were set by the NavMoveRequestForward() function) - IM_ASSERT(g.NavMoveDir != ImGuiDir_None && g.NavMoveClipDir != ImGuiDir_None); - IM_ASSERT(g.NavMoveRequestForward == ImGuiNavForward_ForwardQueued); - g.NavMoveRequestForward = ImGuiNavForward_ForwardActive; - } - - // Update PageUp/PageDown/Home/End scroll - // FIXME-NAV: Consider enabling those keys even without the master ImGuiConfigFlags_NavEnableKeyboard flag? - float nav_scoring_rect_offset_y = 0.0f; - if (nav_keyboard_active) - nav_scoring_rect_offset_y = NavUpdatePageUpPageDown(allowed_dir_flags); - - // If we initiate a movement request and have no current NavId, we initiate a InitDefautRequest that will be used as a fallback if the direction fails to find a match - if (g.NavMoveDir != ImGuiDir_None) - { - g.NavMoveRequest = true; - g.NavMoveDirLast = g.NavMoveDir; - } - if (g.NavMoveRequest && g.NavId == 0) - { - g.NavInitRequest = g.NavInitRequestFromMove = true; - g.NavInitResultId = 0; - g.NavDisableHighlight = false; - } + // Process move requests + NavUpdateCreateMoveRequest(); + if (g.NavMoveDir == ImGuiDir_None) + NavUpdateCreateTabbingRequest(); NavUpdateAnyRequestFlag(); + g.NavIdIsAlive = false; // Scrolling if (g.NavWindow && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) && !g.NavWindowingTarget) { // *Fallback* manual-scroll with Nav directional keys when window has no navigable item ImGuiWindow* window = g.NavWindow; - const float scroll_speed = ImFloor(window->CalcFontSize() * 100 * g.IO.DeltaTime + 0.5f); // We need round the scrolling speed because sub-pixel scroll isn't reliably supported. - if (window->DC.NavLayerActiveMask == 0x00 && window->DC.NavHasScroll && g.NavMoveRequest) + const float scroll_speed = IM_ROUND(window->FontRefSize * 100 * io.DeltaTime); // We need round the scrolling speed because sub-pixel scroll isn't reliably supported. + const ImGuiDir move_dir = g.NavMoveDir; + if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavWindowHasScrollY && move_dir != ImGuiDir_None) { - if (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) - SetScrollX(window, ImFloor(window->Scroll.x + ((g.NavMoveDir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); - if (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) - SetScrollY(window, ImFloor(window->Scroll.y + ((g.NavMoveDir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); + if (move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) + SetScrollX(window, ImTrunc(window->Scroll.x + ((move_dir == ImGuiDir_Left) ? -1.0f : +1.0f) * scroll_speed)); + if (move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) + SetScrollY(window, ImTrunc(window->Scroll.y + ((move_dir == ImGuiDir_Up) ? -1.0f : +1.0f) * scroll_speed)); } - // *Normal* Manual scroll with NavScrollXXX keys + // *Normal* Manual scroll with LStick // Next movement request will clamp the NavId reference rectangle to the visible area, so navigation will resume within those bounds. - ImVec2 scroll_dir = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadLStick, ImGuiInputReadMode_Down, 1.0f/10.0f, 10.0f); - if (scroll_dir.x != 0.0f && window->ScrollbarX) + if (nav_gamepad_active) { - SetScrollX(window, ImFloor(window->Scroll.x + scroll_dir.x * scroll_speed)); - g.NavMoveFromClampedRefRect = true; - } - if (scroll_dir.y != 0.0f) - { - SetScrollY(window, ImFloor(window->Scroll.y + scroll_dir.y * scroll_speed)); - g.NavMoveFromClampedRefRect = true; + const ImVec2 scroll_dir = GetKeyMagnitude2d(ImGuiKey_GamepadLStickLeft, ImGuiKey_GamepadLStickRight, ImGuiKey_GamepadLStickUp, ImGuiKey_GamepadLStickDown); + const float tweak_factor = IsKeyDown(ImGuiKey_NavGamepadTweakSlow) ? 1.0f / 10.0f : IsKeyDown(ImGuiKey_NavGamepadTweakFast) ? 10.0f : 1.0f; + if (scroll_dir.x != 0.0f && window->ScrollbarX) + SetScrollX(window, ImTrunc(window->Scroll.x + scroll_dir.x * scroll_speed * tweak_factor)); + if (scroll_dir.y != 0.0f) + SetScrollY(window, ImTrunc(window->Scroll.y + scroll_dir.y * scroll_speed * tweak_factor)); } } - // Reset search results - g.NavMoveResultLocal.Clear(); - g.NavMoveResultLocalVisibleSet.Clear(); - g.NavMoveResultOther.Clear(); - - // When we have manually scrolled (without using navigation) and NavId becomes out of bounds, we project its bounding box to the visible area to restart navigation within visible items - if (g.NavMoveRequest && g.NavMoveFromClampedRefRect && g.NavLayer == 0) + // Always prioritize mouse highlight if navigation is disabled + if (!nav_keyboard_active && !nav_gamepad_active) { - ImGuiWindow* window = g.NavWindow; - ImRect window_rect_rel(window->InnerRect.Min - window->Pos - ImVec2(1,1), window->InnerRect.Max - window->Pos + ImVec2(1,1)); - if (!window_rect_rel.Contains(window->NavRectRel[g.NavLayer])) - { - float pad = window->CalcFontSize() * 0.5f; - window_rect_rel.Expand(ImVec2(-ImMin(window_rect_rel.GetWidth(), pad), -ImMin(window_rect_rel.GetHeight(), pad))); // Terrible approximation for the intent of starting navigation from first fully visible item - window->NavRectRel[g.NavLayer].ClipWith(window_rect_rel); - g.NavId = 0; - } - g.NavMoveFromClampedRefRect = false; + g.NavCursorVisible = false; + g.NavHighlightItemUnderNav = set_mouse_pos = false; } - // For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items) - ImRect nav_rect_rel = (g.NavWindow && !g.NavWindow->NavRectRel[g.NavLayer].IsInverted()) ? g.NavWindow->NavRectRel[g.NavLayer] : ImRect(0,0,0,0); - g.NavScoringRectScreen = g.NavWindow ? ImRect(g.NavWindow->Pos + nav_rect_rel.Min, g.NavWindow->Pos + nav_rect_rel.Max) : GetViewportRect(); - g.NavScoringRectScreen.TranslateY(nav_scoring_rect_offset_y); - g.NavScoringRectScreen.Min.x = ImMin(g.NavScoringRectScreen.Min.x + 1.0f, g.NavScoringRectScreen.Max.x); - g.NavScoringRectScreen.Max.x = g.NavScoringRectScreen.Min.x; - IM_ASSERT(!g.NavScoringRectScreen.IsInverted()); // Ensure if we have a finite, non-inverted bounding box here will allows us to remove extraneous ImFabs() calls in NavScoreItem(). - //GetForegroundDrawList()->AddRect(g.NavScoringRectScreen.Min, g.NavScoringRectScreen.Max, IM_COL32(255,200,0,255)); // [DEBUG] - g.NavScoringCount = 0; + // Update mouse position if requested + // (This will take into account the possibility that a Scroll was queued in the window to offset our absolute mouse position before scroll has been applied) + if (set_mouse_pos && io.ConfigNavMoveSetMousePos && (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos)) + TeleportMousePos(NavCalcPreferredRefPos()); + + // [DEBUG] + g.NavScoringDebugCount = 0; #if IMGUI_DEBUG_NAV_RECTS - if (g.NavWindow) + if (ImGuiWindow* debug_window = g.NavWindow) { - ImDrawList* draw_list = GetForegroundDrawList(g.NavWindow); - if (1) { for (int layer = 0; layer < 2; layer++) draw_list->AddRect(g.NavWindow->Pos + g.NavWindow->NavRectRel[layer].Min, g.NavWindow->Pos + g.NavWindow->NavRectRel[layer].Max, IM_COL32(255,200,0,255)); } // [DEBUG] - if (1) { ImU32 col = (!g.NavWindow->Hidden) ? IM_COL32(255,0,255,255) : IM_COL32(255,0,0,255); ImVec2 p = NavCalcPreferredRefPos(); char buf[32]; ImFormatString(buf, 32, "%d", g.NavLayer); draw_list->AddCircleFilled(p, 3.0f, col); draw_list->AddText(NULL, 13.0f, p + ImVec2(8,-4), col, buf); } + ImDrawList* draw_list = GetForegroundDrawList(debug_window); + int layer = g.NavLayer; /* for (int layer = 0; layer < 2; layer++)*/ { ImRect r = WindowRectRelToAbs(debug_window, debug_window->NavRectRel[layer]); draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 200, 0, 255)); } + //if (1) { ImU32 col = (!debug_window->Hidden) ? IM_COL32(255,0,255,255) : IM_COL32(255,0,0,255); ImVec2 p = NavCalcPreferredRefPos(); char buf[32]; ImFormatString(buf, 32, "%d", g.NavLayer); draw_list->AddCircleFilled(p, 3.0f, col); draw_list->AddText(NULL, 13.0f, p + ImVec2(8,-4), col, buf); } } #endif } -// Apply result from previous frame navigation directional move request -static void ImGui::NavUpdateMoveResult() +void ImGui::NavInitRequestApplyResult() +{ + // In very rare cases g.NavWindow may be null (e.g. clearing focus after requesting an init request, which does happen when releasing Alt while clicking on void) + ImGuiContext& g = *GImGui; + if (!g.NavWindow) + return; + + ImGuiNavItemData* result = &g.NavInitResult; + if (g.NavId != result->ID) + { + g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId; + g.NavJustMovedToId = result->ID; + g.NavJustMovedToFocusScopeId = result->FocusScopeId; + g.NavJustMovedToKeyMods = 0; + g.NavJustMovedToIsTabbing = false; + g.NavJustMovedToHasSelectionData = (result->ItemFlags & ImGuiItemFlags_HasSelectionUserData) != 0; + } + + // Apply result from previous navigation init request (will typically select the first item, unless SetItemDefaultFocus() has been called) + // FIXME-NAV: On _NavFlattened windows, g.NavWindow will only be updated during subsequent frame. Not a problem currently. + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: ApplyResult: NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); + SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); + g.NavIdIsAlive = true; // Mark as alive from previous frame as we got a result + if (result->SelectionUserData != ImGuiSelectionUserData_Invalid) + g.NavLastValidSelectionUserData = result->SelectionUserData; + if (g.NavInitRequestFromMove) + SetNavCursorVisibleAfterMove(); +} + +// Bias scoring rect ahead of scoring + update preferred pos (if missing) using source position +static void NavBiasScoringRect(ImRect& r, ImVec2& preferred_pos_rel, ImGuiDir move_dir, ImGuiNavMoveFlags move_flags) +{ + // Bias initial rect + ImGuiContext& g = *GImGui; + const ImVec2 rel_to_abs_offset = g.NavWindow->DC.CursorStartPos; + + // Initialize bias on departure if we don't have any. So mouse-click + arrow will record bias. + // - We default to L/U bias, so moving down from a large source item into several columns will land on left-most column. + // - But each successful move sets new bias on one axis, only cleared when using mouse. + if ((move_flags & ImGuiNavMoveFlags_Forwarded) == 0) + { + if (preferred_pos_rel.x == FLT_MAX) + preferred_pos_rel.x = ImMin(r.Min.x + 1.0f, r.Max.x) - rel_to_abs_offset.x; + if (preferred_pos_rel.y == FLT_MAX) + preferred_pos_rel.y = r.GetCenter().y - rel_to_abs_offset.y; + } + + // Apply general bias on the other axis + if ((move_dir == ImGuiDir_Up || move_dir == ImGuiDir_Down) && preferred_pos_rel.x != FLT_MAX) + r.Min.x = r.Max.x = preferred_pos_rel.x + rel_to_abs_offset.x; + else if ((move_dir == ImGuiDir_Left || move_dir == ImGuiDir_Right) && preferred_pos_rel.y != FLT_MAX) + r.Min.y = r.Max.y = preferred_pos_rel.y + rel_to_abs_offset.y; +} + +void ImGui::NavUpdateCreateMoveRequest() { ImGuiContext& g = *GImGui; - if (g.NavMoveResultLocal.ID == 0 && g.NavMoveResultOther.ID == 0) + ImGuiIO& io = g.IO; + ImGuiWindow* window = g.NavWindow; + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + + if (g.NavMoveForwardToNextFrame && window != NULL) { - // In a situation when there is no results but NavId != 0, re-enable the Navigation highlight (because g.NavId is not considered as a possible result) - if (g.NavId != 0) + // Forwarding previous request (which has been modified, e.g. wrap around menus rewrite the requests with a starting rectangle at the other side of the window) + // (preserve most state, which were already set by the NavMoveRequestForward() function) + IM_ASSERT(g.NavMoveDir != ImGuiDir_None && g.NavMoveClipDir != ImGuiDir_None); + IM_ASSERT(g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded); + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequestForward %d\n", g.NavMoveDir); + } + else + { + // Initiate directional inputs request + g.NavMoveDir = ImGuiDir_None; + g.NavMoveFlags = ImGuiNavMoveFlags_None; + g.NavMoveScrollFlags = ImGuiScrollFlags_None; + if (window && !g.NavWindowingTarget && !(window->Flags & ImGuiWindowFlags_NoNavInputs)) { - g.NavDisableHighlight = false; - g.NavDisableMouseHover = true; + const ImGuiInputFlags repeat_mode = ImGuiInputFlags_Repeat | (ImGuiInputFlags)ImGuiInputFlags_RepeatRateNavMove; + if (!IsActiveIdUsingNavDir(ImGuiDir_Left) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadLeft, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_LeftArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Left; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Right) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadRight, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_RightArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Right; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Up) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadUp, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_UpArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Up; } + if (!IsActiveIdUsingNavDir(ImGuiDir_Down) && ((nav_gamepad_active && IsKeyPressed(ImGuiKey_GamepadDpadDown, repeat_mode, ImGuiKeyOwner_NoOwner)) || (nav_keyboard_active && IsKeyPressed(ImGuiKey_DownArrow, repeat_mode, ImGuiKeyOwner_NoOwner)))) { g.NavMoveDir = ImGuiDir_Down; } } + g.NavMoveClipDir = g.NavMoveDir; + g.NavScoringNoClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + } + + // Update PageUp/PageDown/Home/End scroll + // FIXME-NAV: Consider enabling those keys even without the master ImGuiConfigFlags_NavEnableKeyboard flag? + float scoring_rect_offset_y = 0.0f; + if (window && g.NavMoveDir == ImGuiDir_None && nav_keyboard_active) + scoring_rect_offset_y = NavUpdatePageUpPageDown(); + if (scoring_rect_offset_y != 0.0f) + { + g.NavScoringNoClipRect = window->InnerRect; + g.NavScoringNoClipRect.TranslateY(scoring_rect_offset_y); + } + + // [DEBUG] Always send a request when holding CTRL. Hold CTRL + Arrow change the direction. +#if IMGUI_DEBUG_NAV_SCORING + //if (io.KeyCtrl && IsKeyPressed(ImGuiKey_C)) + // g.NavMoveDirForDebug = (ImGuiDir)((g.NavMoveDirForDebug + 1) & 3); + if (io.KeyCtrl) + { + if (g.NavMoveDir == ImGuiDir_None) + g.NavMoveDir = g.NavMoveDirForDebug; + g.NavMoveClipDir = g.NavMoveDir; + g.NavMoveFlags |= ImGuiNavMoveFlags_DebugNoResult; + } +#endif + + // Submit + g.NavMoveForwardToNextFrame = false; + if (g.NavMoveDir != ImGuiDir_None) + NavMoveRequestSubmit(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); + + // Moving with no reference triggers an init request (will be used as a fallback if the direction fails to find a match) + if (g.NavMoveSubmitted && g.NavId == 0) + { + IMGUI_DEBUG_LOG_NAV("[nav] NavInitRequest: from move, window \"%s\", layer=%d\n", window ? window->Name : "", g.NavLayer); + g.NavInitRequest = g.NavInitRequestFromMove = true; + g.NavInitResult.ID = 0; + if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = true; + } + + // When using gamepad, we project the reference nav bounding box into window visible area. + // This is to allow resuming navigation inside the visible area after doing a large amount of scrolling, + // since with gamepad all movements are relative (can't focus a visible object like we can with the mouse). + if (g.NavMoveSubmitted && g.NavInputSource == ImGuiInputSource_Gamepad && g.NavLayer == ImGuiNavLayer_Main && window != NULL)// && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded)) + { + bool clamp_x = (g.NavMoveFlags & (ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_WrapX)) == 0; + bool clamp_y = (g.NavMoveFlags & (ImGuiNavMoveFlags_LoopY | ImGuiNavMoveFlags_WrapY)) == 0; + ImRect inner_rect_rel = WindowRectAbsToRel(window, ImRect(window->InnerRect.Min - ImVec2(1, 1), window->InnerRect.Max + ImVec2(1, 1))); + + // Take account of changing scroll to handle triggering a new move request on a scrolling frame. (#6171) + // Otherwise 'inner_rect_rel' would be off on the move result frame. + inner_rect_rel.Translate(CalcNextScrollFromScrollTargetAndClamp(window) - window->Scroll); + + if ((clamp_x || clamp_y) && !inner_rect_rel.Contains(window->NavRectRel[g.NavLayer])) + { + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: clamp NavRectRel for gamepad move\n"); + float pad_x = ImMin(inner_rect_rel.GetWidth(), window->FontRefSize * 0.5f); + float pad_y = ImMin(inner_rect_rel.GetHeight(), window->FontRefSize * 0.5f); // Terrible approximation for the intent of starting navigation from first fully visible item + inner_rect_rel.Min.x = clamp_x ? (inner_rect_rel.Min.x + pad_x) : -FLT_MAX; + inner_rect_rel.Max.x = clamp_x ? (inner_rect_rel.Max.x - pad_x) : +FLT_MAX; + inner_rect_rel.Min.y = clamp_y ? (inner_rect_rel.Min.y + pad_y) : -FLT_MAX; + inner_rect_rel.Max.y = clamp_y ? (inner_rect_rel.Max.y - pad_y) : +FLT_MAX; + window->NavRectRel[g.NavLayer].ClipWithFull(inner_rect_rel); + g.NavId = 0; + } + } + + // For scoring we use a single segment on the left side our current item bounding box (not touching the edge to avoid box overlap with zero-spaced items) + ImRect scoring_rect; + if (window != NULL) + { + ImRect nav_rect_rel = !window->NavRectRel[g.NavLayer].IsInverted() ? window->NavRectRel[g.NavLayer] : ImRect(0, 0, 0, 0); + scoring_rect = WindowRectRelToAbs(window, nav_rect_rel); + scoring_rect.TranslateY(scoring_rect_offset_y); + if (g.NavMoveSubmitted) + NavBiasScoringRect(scoring_rect, window->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer], g.NavMoveDir, g.NavMoveFlags); + IM_ASSERT(!scoring_rect.IsInverted()); // Ensure we have a non-inverted bounding box here will allow us to remove extraneous ImFabs() calls in NavScoreItem(). + //GetForegroundDrawList()->AddRect(scoring_rect.Min, scoring_rect.Max, IM_COL32(255,200,0,255)); // [DEBUG] + //if (!g.NavScoringNoClipRect.IsInverted()) { GetForegroundDrawList()->AddRect(g.NavScoringNoClipRect.Min, g.NavScoringNoClipRect.Max, IM_COL32(255, 200, 0, 255)); } // [DEBUG] + } + g.NavScoringRect = scoring_rect; + g.NavScoringNoClipRect.Add(scoring_rect); +} + +void ImGui::NavUpdateCreateTabbingRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + IM_ASSERT(g.NavMoveDir == ImGuiDir_None); + if (window == NULL || g.NavWindowingTarget != NULL || (window->Flags & ImGuiWindowFlags_NoNavInputs)) + return; + + const bool tab_pressed = IsKeyPressed(ImGuiKey_Tab, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner) && !g.IO.KeyCtrl && !g.IO.KeyAlt; + if (!tab_pressed) + return; + + // Initiate tabbing request + // (this is ALWAYS ENABLED, regardless of ImGuiConfigFlags_NavEnableKeyboard flag!) + // See NavProcessItemForTabbingRequest() for a description of the various forward/backward tabbing cases with and without wrapping. + const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + if (nav_keyboard_active) + g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.NavCursorVisible == false && g.ActiveId == 0) ? 0 : +1; + else + g.NavTabbingDir = g.IO.KeyShift ? -1 : (g.ActiveId == 0) ? 0 : +1; + ImGuiNavMoveFlags move_flags = ImGuiNavMoveFlags_IsTabbing | ImGuiNavMoveFlags_Activate; + ImGuiScrollFlags scroll_flags = window->Appearing ? ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_AlwaysCenterY : ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleEdgeY; + ImGuiDir clip_dir = (g.NavTabbingDir < 0) ? ImGuiDir_Up : ImGuiDir_Down; + NavMoveRequestSubmit(ImGuiDir_None, clip_dir, move_flags, scroll_flags); // FIXME-NAV: Once we refactor tabbing, add LegacyApi flag to not activate non-inputable. + g.NavTabbingCounter = -1; +} + +// Apply result from previous frame navigation directional move request. Always called from NavUpdate() +void ImGui::NavMoveRequestApplyResult() +{ + ImGuiContext& g = *GImGui; +#if IMGUI_DEBUG_NAV_SCORING + if (g.NavMoveFlags & ImGuiNavMoveFlags_DebugNoResult) // [DEBUG] Scoring all items in NavWindow at all times + return; +#endif + + // Select which result to use + ImGuiNavItemData* result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : (g.NavMoveResultOther.ID != 0) ? &g.NavMoveResultOther : NULL; + + // Tabbing forward wrap + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && result == NULL) + if ((g.NavTabbingCounter == 1 || g.NavTabbingDir == 0) && g.NavTabbingResultFirst.ID) + result = &g.NavTabbingResultFirst; + + // In a situation when there are no results but NavId != 0, re-enable the Navigation highlight (because g.NavId is not considered as a possible result) + const ImGuiAxis axis = (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + if (result == NULL) + { + if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) + g.NavMoveFlags |= ImGuiNavMoveFlags_NoSetNavCursorVisible; + if (g.NavId != 0 && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSetNavCursorVisible) == 0) + SetNavCursorVisibleAfterMove(); + NavClearPreferredPosForAxis(axis); // On a failed move, clear preferred pos for this axis. + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveSubmitted but not led to a result!\n"); return; } - // Select which result to use - ImGuiNavMoveResult* result = (g.NavMoveResultLocal.ID != 0) ? &g.NavMoveResultLocal : &g.NavMoveResultOther; - // PageUp/PageDown behavior first jumps to the bottom/top mostly visible item, _otherwise_ use the result from the previous/next page. - if (g.NavMoveRequestFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) - if (g.NavMoveResultLocalVisibleSet.ID != 0 && g.NavMoveResultLocalVisibleSet.ID != g.NavId) - result = &g.NavMoveResultLocalVisibleSet; + if (g.NavMoveFlags & ImGuiNavMoveFlags_AlsoScoreVisibleSet) + if (g.NavMoveResultLocalVisible.ID != 0 && g.NavMoveResultLocalVisible.ID != g.NavId) + result = &g.NavMoveResultLocalVisible; // Maybe entering a flattened child from the outside? In this case solve the tie using the regular scoring rules. if (result != &g.NavMoveResultOther && g.NavMoveResultOther.ID != 0 && g.NavMoveResultOther.Window->ParentWindow == g.NavWindow) @@ -8550,117 +14094,290 @@ static void ImGui::NavUpdateMoveResult() IM_ASSERT(g.NavWindow && result->Window); // Scroll to keep newly navigated item fully into view. - if (g.NavLayer == 0) + if (g.NavLayer == ImGuiNavLayer_Main) { - ImVec2 delta_scroll; - if (g.NavMoveRequestFlags & ImGuiNavMoveFlags_ScrollToEdge) + ImRect rect_abs = WindowRectRelToAbs(result->Window, result->RectRel); + ScrollToRectEx(result->Window, rect_abs, g.NavMoveScrollFlags); + + if (g.NavMoveFlags & ImGuiNavMoveFlags_ScrollToEdgeY) { + // FIXME: Should remove this? Or make more precise: use ScrollToRectEx() with edge? float scroll_target = (g.NavMoveDir == ImGuiDir_Up) ? result->Window->ScrollMax.y : 0.0f; - delta_scroll.y = result->Window->Scroll.y - scroll_target; SetScrollY(result->Window, scroll_target); } - else - { - ImRect rect_abs = ImRect(result->RectRel.Min + result->Window->Pos, result->RectRel.Max + result->Window->Pos); - delta_scroll = ScrollToBringRectIntoView(result->Window, rect_abs); - } - - // Offset our result position so mouse position can be applied immediately after in NavUpdate() - result->RectRel.TranslateX(-delta_scroll.x); - result->RectRel.TranslateY(-delta_scroll.y); } - ClearActiveID(); - g.NavWindow = result->Window; - if (g.NavId != result->ID) + if (g.NavWindow != result->Window) { - // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) - g.NavJustMovedToId = result->ID; - g.NavJustMovedToMultiSelectScopeId = result->SelectScopeId; + IMGUI_DEBUG_LOG_FOCUS("[focus] NavMoveRequest: SetNavWindow(\"%s\")\n", result->Window->Name); + g.NavWindow = result->Window; + g.NavLastValidSelectionUserData = ImGuiSelectionUserData_Invalid; + } + + // Clear active id unless requested not to + // FIXME: ImGuiNavMoveFlags_NoClearActiveId is currently unused as we don't have a clear strategy to preserve active id after interaction, + // so this is mostly provided as a gateway for further experiments (see #1418, #2890) + if (g.ActiveId != result->ID && (g.NavMoveFlags & ImGuiNavMoveFlags_NoClearActiveId) == 0) + ClearActiveID(); + + // Don't set NavJustMovedToId if just landed on the same spot (which may happen with ImGuiNavMoveFlags_AllowCurrentNavId) + // PageUp/PageDown however sets always set NavJustMovedTo (vs Home/End which doesn't) mimicking Windows behavior. + if ((g.NavId != result->ID || (g.NavMoveFlags & ImGuiNavMoveFlags_IsPageMove)) && (g.NavMoveFlags & ImGuiNavMoveFlags_NoSelect) == 0) + { + g.NavJustMovedFromFocusScopeId = g.NavFocusScopeId; + g.NavJustMovedToId = result->ID; + g.NavJustMovedToFocusScopeId = result->FocusScopeId; + g.NavJustMovedToKeyMods = g.NavMoveKeyMods; + g.NavJustMovedToIsTabbing = (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) != 0; + g.NavJustMovedToHasSelectionData = (result->ItemFlags & ImGuiItemFlags_HasSelectionUserData) != 0; + //IMGUI_DEBUG_LOG_NAV("[nav] NavJustMovedFromFocusScopeId = 0x%08X, NavJustMovedToFocusScopeId = 0x%08X\n", g.NavJustMovedFromFocusScopeId, g.NavJustMovedToFocusScopeId); + } + + // Apply new NavID/Focus + IMGUI_DEBUG_LOG_NAV("[nav] NavMoveRequest: result NavID 0x%08X in Layer %d Window \"%s\"\n", result->ID, g.NavLayer, g.NavWindow->Name); + ImVec2 preferred_scoring_pos_rel = g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer]; + SetNavID(result->ID, g.NavLayer, result->FocusScopeId, result->RectRel); + if (result->SelectionUserData != ImGuiSelectionUserData_Invalid) + g.NavLastValidSelectionUserData = result->SelectionUserData; + + // Restore last preferred position for current axis + // (storing in RootWindowForNav-> as the info is desirable at the beginning of a Move Request. In theory all storage should use RootWindowForNav..) + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) == 0) + { + preferred_scoring_pos_rel[axis] = result->RectRel.GetCenter()[axis]; + g.NavWindow->RootWindowForNav->NavPreferredScoringPosRel[g.NavLayer] = preferred_scoring_pos_rel; + } + + // Tabbing: Activates Inputable, otherwise only Focus + if ((g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) && (result->ItemFlags & ImGuiItemFlags_Inputable) == 0) + g.NavMoveFlags &= ~ImGuiNavMoveFlags_Activate; + + // Activate + if (g.NavMoveFlags & ImGuiNavMoveFlags_Activate) + { + g.NavNextActivateId = result->ID; + g.NavNextActivateFlags = ImGuiActivateFlags_None; + if (g.NavMoveFlags & ImGuiNavMoveFlags_IsTabbing) + g.NavNextActivateFlags |= ImGuiActivateFlags_PreferInput | ImGuiActivateFlags_TryToPreserveState | ImGuiActivateFlags_FromTabbing; + } + + // Make nav cursor visible + if ((g.NavMoveFlags & ImGuiNavMoveFlags_NoSetNavCursorVisible) == 0) + SetNavCursorVisibleAfterMove(); +} + +// Process Escape/NavCancel input (to close a popup, get back to parent, clear focus) +// FIXME: In order to support e.g. Escape to clear a selection we'll need: +// - either to store the equivalent of ActiveIdUsingKeyInputMask for a FocusScope and test for it. +// - either to move most/all of those tests to the epilogue/end functions of the scope they are dealing with (e.g. exit child window in EndChild()) or in EndFrame(), to allow an earlier intercept +static void ImGui::NavUpdateCancelRequest() +{ + ImGuiContext& g = *GImGui; + const bool nav_gamepad_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (g.IO.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + if (!(nav_keyboard_active && IsKeyPressed(ImGuiKey_Escape, 0, ImGuiKeyOwner_NoOwner)) && !(nav_gamepad_active && IsKeyPressed(ImGuiKey_NavGamepadCancel, 0, ImGuiKeyOwner_NoOwner))) + return; + + IMGUI_DEBUG_LOG_NAV("[nav] NavUpdateCancelRequest()\n"); + if (g.ActiveId != 0) + { + ClearActiveID(); + } + else if (g.NavLayer != ImGuiNavLayer_Main) + { + // Leave the "menu" layer + NavRestoreLayer(ImGuiNavLayer_Main); + SetNavCursorVisibleAfterMove(); + } + else if (g.NavWindow && g.NavWindow != g.NavWindow->RootWindow && !(g.NavWindow->RootWindowForNav->Flags & ImGuiWindowFlags_Popup) && g.NavWindow->RootWindowForNav->ParentWindow) + { + // Exit child window + ImGuiWindow* child_window = g.NavWindow->RootWindowForNav; + ImGuiWindow* parent_window = child_window->ParentWindow; + IM_ASSERT(child_window->ChildId != 0); + FocusWindow(parent_window); + SetNavID(child_window->ChildId, ImGuiNavLayer_Main, 0, WindowRectAbsToRel(parent_window, child_window->Rect())); + SetNavCursorVisibleAfterMove(); + } + else if (g.OpenPopupStack.Size > 0 && g.OpenPopupStack.back().Window != NULL && !(g.OpenPopupStack.back().Window->Flags & ImGuiWindowFlags_Modal)) + { + // Close open popup/menu + ClosePopupToLevel(g.OpenPopupStack.Size - 1, true); + } + else + { + // Clear NavLastId for popups but keep it for regular child window so we can leave one and come back where we were + // FIXME-NAV: This should happen on window appearing. + if (g.IO.ConfigNavEscapeClearFocusItem || g.IO.ConfigNavEscapeClearFocusWindow) + if (g.NavWindow && ((g.NavWindow->Flags & ImGuiWindowFlags_Popup)))// || !(g.NavWindow->Flags & ImGuiWindowFlags_ChildWindow))) + g.NavWindow->NavLastIds[0] = 0; + + // Clear nav focus + if (g.IO.ConfigNavEscapeClearFocusItem || g.IO.ConfigNavEscapeClearFocusWindow) + g.NavId = 0; + if (g.IO.ConfigNavEscapeClearFocusWindow) + FocusWindow(NULL); } - SetNavIDWithRectRel(result->ID, g.NavLayer, result->RectRel); - g.NavMoveFromClampedRefRect = false; } // Handle PageUp/PageDown/Home/End keys -static float ImGui::NavUpdatePageUpPageDown(int allowed_dir_flags) +// Called from NavUpdateCreateMoveRequest() which will use our output to create a move request +// FIXME-NAV: This doesn't work properly with NavFlattened siblings as we use NavWindow rectangle for reference +// FIXME-NAV: how to get Home/End to aim at the beginning/end of a 2D grid? +static float ImGui::NavUpdatePageUpPageDown() { ImGuiContext& g = *GImGui; - if (g.NavMoveDir != ImGuiDir_None || g.NavWindow == NULL) - return 0.0f; - if ((g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs) || g.NavWindowingTarget != NULL || g.NavLayer != 0) + ImGuiWindow* window = g.NavWindow; + if ((window->Flags & ImGuiWindowFlags_NoNavInputs) || g.NavWindowingTarget != NULL) return 0.0f; - ImGuiWindow* window = g.NavWindow; - const bool page_up_held = IsKeyDown(g.IO.KeyMap[ImGuiKey_PageUp]) && (allowed_dir_flags & (1 << ImGuiDir_Up)); - const bool page_down_held = IsKeyDown(g.IO.KeyMap[ImGuiKey_PageDown]) && (allowed_dir_flags & (1 << ImGuiDir_Down)); - const bool home_pressed = IsKeyPressed(g.IO.KeyMap[ImGuiKey_Home]) && (allowed_dir_flags & (1 << ImGuiDir_Up)); - const bool end_pressed = IsKeyPressed(g.IO.KeyMap[ImGuiKey_End]) && (allowed_dir_flags & (1 << ImGuiDir_Down)); - if (page_up_held != page_down_held || home_pressed != end_pressed) // If either (not both) are pressed + const bool page_up_held = IsKeyDown(ImGuiKey_PageUp, ImGuiKeyOwner_NoOwner); + const bool page_down_held = IsKeyDown(ImGuiKey_PageDown, ImGuiKeyOwner_NoOwner); + const bool home_pressed = IsKeyPressed(ImGuiKey_Home, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner); + const bool end_pressed = IsKeyPressed(ImGuiKey_End, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner); + if (page_up_held == page_down_held && home_pressed == end_pressed) // Proceed if either (not both) are pressed, otherwise early out + return 0.0f; + + if (g.NavLayer != ImGuiNavLayer_Main) + NavRestoreLayer(ImGuiNavLayer_Main); + + if (window->DC.NavLayersActiveMask == 0x00 && window->DC.NavWindowHasScrollY) { - if (window->DC.NavLayerActiveMask == 0x00 && window->DC.NavHasScroll) + // Fallback manual-scroll when window has no navigable item + if (IsKeyPressed(ImGuiKey_PageUp, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner)) + SetScrollY(window, window->Scroll.y - window->InnerRect.GetHeight()); + else if (IsKeyPressed(ImGuiKey_PageDown, ImGuiInputFlags_Repeat, ImGuiKeyOwner_NoOwner)) + SetScrollY(window, window->Scroll.y + window->InnerRect.GetHeight()); + else if (home_pressed) + SetScrollY(window, 0.0f); + else if (end_pressed) + SetScrollY(window, window->ScrollMax.y); + } + else + { + ImRect& nav_rect_rel = window->NavRectRel[g.NavLayer]; + const float page_offset_y = ImMax(0.0f, window->InnerRect.GetHeight() - window->FontRefSize * 1.0f + nav_rect_rel.GetHeight()); + float nav_scoring_rect_offset_y = 0.0f; + if (IsKeyPressed(ImGuiKey_PageUp, true)) { - // Fallback manual-scroll when window has no navigable item - if (IsKeyPressed(g.IO.KeyMap[ImGuiKey_PageUp], true)) - SetScrollY(window, window->Scroll.y - window->InnerRect.GetHeight()); - else if (IsKeyPressed(g.IO.KeyMap[ImGuiKey_PageDown], true)) - SetScrollY(window, window->Scroll.y + window->InnerRect.GetHeight()); - else if (home_pressed) - SetScrollY(window, 0.0f); - else if (end_pressed) - SetScrollY(window, window->ScrollMax.y); + nav_scoring_rect_offset_y = -page_offset_y; + g.NavMoveDir = ImGuiDir_Down; // Because our scoring rect is offset up, we request the down direction (so we can always land on the last item) + g.NavMoveClipDir = ImGuiDir_Up; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet | ImGuiNavMoveFlags_IsPageMove; } - else + else if (IsKeyPressed(ImGuiKey_PageDown, true)) { - ImRect& nav_rect_rel = window->NavRectRel[g.NavLayer]; - const float page_offset_y = ImMax(0.0f, window->InnerRect.GetHeight() - window->CalcFontSize() * 1.0f + nav_rect_rel.GetHeight()); - float nav_scoring_rect_offset_y = 0.0f; - if (IsKeyPressed(g.IO.KeyMap[ImGuiKey_PageUp], true)) - { - nav_scoring_rect_offset_y = -page_offset_y; - g.NavMoveDir = ImGuiDir_Down; // Because our scoring rect is offset up, we request the down direction (so we can always land on the last item) - g.NavMoveClipDir = ImGuiDir_Up; - g.NavMoveRequestFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet; - } - else if (IsKeyPressed(g.IO.KeyMap[ImGuiKey_PageDown], true)) - { - nav_scoring_rect_offset_y = +page_offset_y; - g.NavMoveDir = ImGuiDir_Up; // Because our scoring rect is offset down, we request the up direction (so we can always land on the last item) - g.NavMoveClipDir = ImGuiDir_Down; - g.NavMoveRequestFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet; - } - else if (home_pressed) - { - // FIXME-NAV: handling of Home/End is assuming that the top/bottom most item will be visible with Scroll.y == 0/ScrollMax.y - // Scrolling will be handled via the ImGuiNavMoveFlags_ScrollToEdge flag, we don't scroll immediately to avoid scrolling happening before nav result. - // Preserve current horizontal position if we have any. - nav_rect_rel.Min.y = nav_rect_rel.Max.y = -window->Scroll.y; - if (nav_rect_rel.IsInverted()) - nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; - g.NavMoveDir = ImGuiDir_Down; - g.NavMoveRequestFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdge; - } - else if (end_pressed) - { - nav_rect_rel.Min.y = nav_rect_rel.Max.y = window->ScrollMax.y + window->SizeFull.y - window->Scroll.y; - if (nav_rect_rel.IsInverted()) - nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; - g.NavMoveDir = ImGuiDir_Up; - g.NavMoveRequestFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdge; - } - return nav_scoring_rect_offset_y; + nav_scoring_rect_offset_y = +page_offset_y; + g.NavMoveDir = ImGuiDir_Up; // Because our scoring rect is offset down, we request the up direction (so we can always land on the last item) + g.NavMoveClipDir = ImGuiDir_Down; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_AlsoScoreVisibleSet | ImGuiNavMoveFlags_IsPageMove; } + else if (home_pressed) + { + // FIXME-NAV: handling of Home/End is assuming that the top/bottom most item will be visible with Scroll.y == 0/ScrollMax.y + // Scrolling will be handled via the ImGuiNavMoveFlags_ScrollToEdgeY flag, we don't scroll immediately to avoid scrolling happening before nav result. + // Preserve current horizontal position if we have any. + nav_rect_rel.Min.y = nav_rect_rel.Max.y = 0.0f; + if (nav_rect_rel.IsInverted()) + nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; + g.NavMoveDir = ImGuiDir_Down; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdgeY; + // FIXME-NAV: MoveClipDir left to _None, intentional? + } + else if (end_pressed) + { + nav_rect_rel.Min.y = nav_rect_rel.Max.y = window->ContentSize.y; + if (nav_rect_rel.IsInverted()) + nav_rect_rel.Min.x = nav_rect_rel.Max.x = 0.0f; + g.NavMoveDir = ImGuiDir_Up; + g.NavMoveFlags = ImGuiNavMoveFlags_AllowCurrentNavId | ImGuiNavMoveFlags_ScrollToEdgeY; + // FIXME-NAV: MoveClipDir left to _None, intentional? + } + return nav_scoring_rect_offset_y; } return 0.0f; } -static int ImGui::FindWindowFocusIndex(ImGuiWindow* window) // FIXME-OPT O(N) +static void ImGui::NavEndFrame() { ImGuiContext& g = *GImGui; - for (int i = g.WindowsFocusOrder.Size-1; i >= 0; i--) - if (g.WindowsFocusOrder[i] == window) - return i; - return -1; + + // Show CTRL+TAB list window + if (g.NavWindowingTarget != NULL) + NavUpdateWindowingOverlay(); + + // Perform wrap-around in menus + // FIXME-NAV: Wrap may need to apply a weight bias on the other axis. e.g. 4x4 grid with 2 last items missing on last item won't handle LoopY/WrapY correctly. + // FIXME-NAV: Wrap (not Loop) support could be handled by the scoring function and then WrapX would function without an extra frame. + if (g.NavWindow && NavMoveRequestButNoResultYet() && (g.NavMoveFlags & ImGuiNavMoveFlags_WrapMask_) && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) + NavUpdateCreateWrappingRequest(); +} + +static void ImGui::NavUpdateCreateWrappingRequest() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.NavWindow; + + bool do_forward = false; + ImRect bb_rel = window->NavRectRel[g.NavLayer]; + ImGuiDir clip_dir = g.NavMoveDir; + + const ImGuiNavMoveFlags move_flags = g.NavMoveFlags; + //const ImGuiAxis move_axis = (g.NavMoveDir == ImGuiDir_Up || g.NavMoveDir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + if (g.NavMoveDir == ImGuiDir_Left && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) + { + bb_rel.Min.x = bb_rel.Max.x = window->ContentSize.x + window->WindowPadding.x; + if (move_flags & ImGuiNavMoveFlags_WrapX) + { + bb_rel.TranslateY(-bb_rel.GetHeight()); // Previous row + clip_dir = ImGuiDir_Up; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Right && (move_flags & (ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_LoopX))) + { + bb_rel.Min.x = bb_rel.Max.x = -window->WindowPadding.x; + if (move_flags & ImGuiNavMoveFlags_WrapX) + { + bb_rel.TranslateY(+bb_rel.GetHeight()); // Next row + clip_dir = ImGuiDir_Down; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Up && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) + { + bb_rel.Min.y = bb_rel.Max.y = window->ContentSize.y + window->WindowPadding.y; + if (move_flags & ImGuiNavMoveFlags_WrapY) + { + bb_rel.TranslateX(-bb_rel.GetWidth()); // Previous column + clip_dir = ImGuiDir_Left; + } + do_forward = true; + } + if (g.NavMoveDir == ImGuiDir_Down && (move_flags & (ImGuiNavMoveFlags_WrapY | ImGuiNavMoveFlags_LoopY))) + { + bb_rel.Min.y = bb_rel.Max.y = -window->WindowPadding.y; + if (move_flags & ImGuiNavMoveFlags_WrapY) + { + bb_rel.TranslateX(+bb_rel.GetWidth()); // Next column + clip_dir = ImGuiDir_Right; + } + do_forward = true; + } + if (!do_forward) + return; + window->NavRectRel[g.NavLayer] = bb_rel; + NavClearPreferredPosForAxis(ImGuiAxis_X); + NavClearPreferredPosForAxis(ImGuiAxis_Y); + NavMoveRequestForward(g.NavMoveDir, clip_dir, move_flags, g.NavMoveScrollFlags); +} + +// Can we focus this window with CTRL+TAB (or PadMenu + PadFocusPrev/PadFocusNext) +// Note that NoNavFocus makes the window not reachable with CTRL+TAB but it can still be focused with mouse or programmatically. +// If you want a window to never be focused, you may use the e.g. NoInputs flag. +bool ImGui::IsWindowNavFocusable(ImGuiWindow* window) +{ + return window->WasActive && window == window->RootWindow && !(window->Flags & ImGuiWindowFlags_NoNavFocus); } static ImGuiWindow* FindWindowNavFocusable(int i_start, int i_stop, int dir) // FIXME-OPT O(N) @@ -8672,7 +14389,7 @@ static ImGuiWindow* FindWindowNavFocusable(int i_start, int i_stop, int dir) // return NULL; } -static void NavUpdateWindowingHighlightWindow(int focus_change_dir) +static void NavUpdateWindowingTarget(int focus_change_dir) { ImGuiContext& g = *GImGui; IM_ASSERT(g.NavWindowingTarget); @@ -8684,63 +14401,114 @@ static void NavUpdateWindowingHighlightWindow(int focus_change_dir) if (!window_target) window_target = FindWindowNavFocusable((focus_change_dir < 0) ? (g.WindowsFocusOrder.Size - 1) : 0, i_current, focus_change_dir); if (window_target) // Don't reset windowing target if there's a single window in the list + { g.NavWindowingTarget = g.NavWindowingTargetAnim = window_target; + g.NavWindowingAccumDeltaPos = g.NavWindowingAccumDeltaSize = ImVec2(0.0f, 0.0f); + } g.NavWindowingToggleLayer = false; } +// Apply focus and close overlay +static void ImGui::NavUpdateWindowingApplyFocus(ImGuiWindow* apply_focus_window) +{ + // FIXME: Many actions here could be part of a higher-level/reused function. Why aren't they in FocusWindow() ? + // Investigate for each of them: ClearActiveID(), NavRestoreHighlightAfterMove(), NavRestoreLastChildNavWindow(), ClosePopupsOverWindow(), NavInitWindow() + ImGuiContext& g = *GImGui; + if (g.NavWindow == NULL || apply_focus_window != g.NavWindow->RootWindow) + { + ImGuiViewport* previous_viewport = g.NavWindow ? g.NavWindow->Viewport : NULL; + ClearActiveID(); + SetNavCursorVisibleAfterMove(); + ClosePopupsOverWindow(apply_focus_window, false); + FocusWindow(apply_focus_window, ImGuiFocusRequestFlags_RestoreFocusedChild); + apply_focus_window = g.NavWindow; + if (apply_focus_window->NavLastIds[0] == 0) + NavInitWindow(apply_focus_window, false); + + // If the window has ONLY a menu layer (no main layer), select it directly + // Use NavLayersActiveMaskNext since windows didn't have a chance to be Begin()-ed on this frame, + // so CTRL+Tab where the keys are only held for 1 frame will be able to use correct layers mask since + // the target window as already been previewed once. + // FIXME-NAV: This should be done in NavInit.. or in FocusWindow... However in both of those cases, + // we won't have a guarantee that windows has been visible before and therefore NavLayersActiveMask* + // won't be valid. + if (apply_focus_window->DC.NavLayersActiveMaskNext == (1 << ImGuiNavLayer_Menu)) + g.NavLayer = ImGuiNavLayer_Menu; + + // Request OS level focus + if (apply_focus_window->Viewport != previous_viewport && g.PlatformIO.Platform_SetWindowFocus) + g.PlatformIO.Platform_SetWindowFocus(apply_focus_window->Viewport); + } + g.NavWindowingTarget = NULL; +} + // Windowing management mode // Keyboard: CTRL+Tab (change focus/move/resize), Alt (toggle menu layer) // Gamepad: Hold Menu/Square (change focus/move/resize), Tap Menu/Square (toggle menu layer) static void ImGui::NavUpdateWindowing() { ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + ImGuiWindow* apply_focus_window = NULL; bool apply_toggle_layer = false; ImGuiWindow* modal_window = GetTopMostPopupModal(); - if (modal_window != NULL) - { + bool allow_windowing = (modal_window == NULL); // FIXME: This prevent CTRL+TAB from being usable with windows that are inside the Begin-stack of that modal. + if (!allow_windowing) g.NavWindowingTarget = NULL; - return; - } // Fade out if (g.NavWindowingTargetAnim && g.NavWindowingTarget == NULL) { - g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha - g.IO.DeltaTime * 10.0f, 0.0f); + g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha - io.DeltaTime * 10.0f, 0.0f); if (g.DimBgRatio <= 0.0f && g.NavWindowingHighlightAlpha <= 0.0f) g.NavWindowingTargetAnim = NULL; } - // Start CTRL-TAB or Square+L/R window selection - bool start_windowing_with_gamepad = !g.NavWindowingTarget && IsNavInputPressed(ImGuiNavInput_Menu, ImGuiInputReadMode_Pressed); - bool start_windowing_with_keyboard = !g.NavWindowingTarget && g.IO.KeyCtrl && IsKeyPressedMap(ImGuiKey_Tab) && (g.IO.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard); + // Start CTRL+Tab or Square+L/R window selection + // (g.ConfigNavWindowingKeyNext/g.ConfigNavWindowingKeyPrev defaults are ImGuiMod_Ctrl|ImGuiKey_Tab and ImGuiMod_Ctrl|ImGuiMod_Shift|ImGuiKey_Tab) + const ImGuiID owner_id = ImHashStr("##NavUpdateWindowing"); + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool nav_keyboard_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) != 0; + const bool keyboard_next_window = allow_windowing && g.ConfigNavWindowingKeyNext && Shortcut(g.ConfigNavWindowingKeyNext, ImGuiInputFlags_Repeat | ImGuiInputFlags_RouteAlways, owner_id); + const bool keyboard_prev_window = allow_windowing && g.ConfigNavWindowingKeyPrev && Shortcut(g.ConfigNavWindowingKeyPrev, ImGuiInputFlags_Repeat | ImGuiInputFlags_RouteAlways, owner_id); + const bool start_windowing_with_gamepad = allow_windowing && nav_gamepad_active && !g.NavWindowingTarget && IsKeyPressed(ImGuiKey_NavGamepadMenu, ImGuiInputFlags_None); + const bool start_windowing_with_keyboard = allow_windowing && !g.NavWindowingTarget && (keyboard_next_window || keyboard_prev_window); // Note: enabled even without NavEnableKeyboard! + bool just_started_windowing_from_null_focus = false; if (start_windowing_with_gamepad || start_windowing_with_keyboard) if (ImGuiWindow* window = g.NavWindow ? g.NavWindow : FindWindowNavFocusable(g.WindowsFocusOrder.Size - 1, -INT_MAX, -1)) { - g.NavWindowingTarget = g.NavWindowingTargetAnim = window; + g.NavWindowingTarget = g.NavWindowingTargetAnim = window->RootWindow; // Current location g.NavWindowingTimer = g.NavWindowingHighlightAlpha = 0.0f; - g.NavWindowingToggleLayer = start_windowing_with_keyboard ? false : true; - g.NavInputSource = start_windowing_with_keyboard ? ImGuiInputSource_NavKeyboard : ImGuiInputSource_NavGamepad; + g.NavWindowingAccumDeltaPos = g.NavWindowingAccumDeltaSize = ImVec2(0.0f, 0.0f); + g.NavWindowingToggleLayer = start_windowing_with_gamepad ? true : false; // Gamepad starts toggling layer + g.NavInputSource = start_windowing_with_keyboard ? ImGuiInputSource_Keyboard : ImGuiInputSource_Gamepad; + if (g.NavWindow == NULL) + just_started_windowing_from_null_focus = true; + + // Manually register ownership of our mods. Using a global route in the Shortcut() calls instead would probably be correct but may have more side-effects. + if (keyboard_next_window || keyboard_prev_window) + SetKeyOwnersForKeyChord((g.ConfigNavWindowingKeyNext | g.ConfigNavWindowingKeyPrev) & ImGuiMod_Mask_, owner_id); } // Gamepad update - g.NavWindowingTimer += g.IO.DeltaTime; - if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_NavGamepad) + g.NavWindowingTimer += io.DeltaTime; + if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Gamepad) { // Highlight only appears after a brief time holding the button, so that a fast tap on PadMenu (to toggle NavLayer) doesn't add visual noise g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); // Select window to focus - const int focus_change_dir = (int)IsNavInputPressed(ImGuiNavInput_FocusPrev, ImGuiInputReadMode_RepeatSlow) - (int)IsNavInputPressed(ImGuiNavInput_FocusNext, ImGuiInputReadMode_RepeatSlow); - if (focus_change_dir != 0) + const int focus_change_dir = (int)IsKeyPressed(ImGuiKey_GamepadL1) - (int)IsKeyPressed(ImGuiKey_GamepadR1); + if (focus_change_dir != 0 && !just_started_windowing_from_null_focus) { - NavUpdateWindowingHighlightWindow(focus_change_dir); + NavUpdateWindowingTarget(focus_change_dir); g.NavWindowingHighlightAlpha = 1.0f; } // Single press toggles NavLayer, long press with L/R apply actual focus on release (until then the window was merely rendered top-most) - if (!IsNavInputDown(ImGuiNavInput_Menu)) + if (!IsKeyDown(ImGuiKey_NavGamepadMenu)) { g.NavWindowingToggleLayer &= (g.NavWindowingHighlightAlpha < 1.0f); // Once button was held long enough we don't consider it a tap-to-toggle-layer press anymore. if (g.NavWindowingToggleLayer && g.NavWindow) @@ -8752,68 +14520,91 @@ static void ImGui::NavUpdateWindowing() } // Keyboard: Focus - if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_NavKeyboard) + if (g.NavWindowingTarget && g.NavInputSource == ImGuiInputSource_Keyboard) { // Visuals only appears after a brief time after pressing TAB the first time, so that a fast CTRL+TAB doesn't add visual noise + ImGuiKeyChord shared_mods = ((g.ConfigNavWindowingKeyNext ? g.ConfigNavWindowingKeyNext : ImGuiMod_Mask_) & (g.ConfigNavWindowingKeyPrev ? g.ConfigNavWindowingKeyPrev : ImGuiMod_Mask_)) & ImGuiMod_Mask_; + IM_ASSERT(shared_mods != 0); // Next/Prev shortcut currently needs a shared modifier to "hold", otherwise Prev actions would keep cycling between two windows. g.NavWindowingHighlightAlpha = ImMax(g.NavWindowingHighlightAlpha, ImSaturate((g.NavWindowingTimer - NAV_WINDOWING_HIGHLIGHT_DELAY) / 0.05f)); // 1.0f - if (IsKeyPressedMap(ImGuiKey_Tab, true)) - NavUpdateWindowingHighlightWindow(g.IO.KeyShift ? +1 : -1); - if (!g.IO.KeyCtrl) + if ((keyboard_next_window || keyboard_prev_window) && !just_started_windowing_from_null_focus) + NavUpdateWindowingTarget(keyboard_next_window ? -1 : +1); + else if ((io.KeyMods & shared_mods) != shared_mods) apply_focus_window = g.NavWindowingTarget; } // Keyboard: Press and Release ALT to toggle menu layer - // FIXME: We lack an explicit IO variable for "is the imgui window focused", so compare mouse validity to detect the common case of back-end clearing releases all keys on ALT-TAB - if (IsNavInputPressed(ImGuiNavInput_KeyMenu_, ImGuiInputReadMode_Pressed)) - g.NavWindowingToggleLayer = true; - if ((g.ActiveId == 0 || g.ActiveIdAllowOverlap) && g.NavWindowingToggleLayer && IsNavInputPressed(ImGuiNavInput_KeyMenu_, ImGuiInputReadMode_Released)) - if (IsMousePosValid(&g.IO.MousePos) == IsMousePosValid(&g.IO.MousePosPrev)) - apply_toggle_layer = true; + const ImGuiKey windowing_toggle_keys[] = { ImGuiKey_LeftAlt, ImGuiKey_RightAlt }; + bool windowing_toggle_layer_start = false; + if (g.NavWindow != NULL && !(g.NavWindow->Flags & ImGuiWindowFlags_NoNavInputs)) + for (ImGuiKey windowing_toggle_key : windowing_toggle_keys) + if (nav_keyboard_active && IsKeyPressed(windowing_toggle_key, 0, ImGuiKeyOwner_NoOwner)) + { + windowing_toggle_layer_start = true; + g.NavWindowingToggleLayer = true; + g.NavWindowingToggleKey = windowing_toggle_key; + g.NavInputSource = ImGuiInputSource_Keyboard; + break; + } + if (g.NavWindowingToggleLayer && g.NavInputSource == ImGuiInputSource_Keyboard) + { + // We cancel toggling nav layer when any text has been typed (generally while holding Alt). (See #370) + // We cancel toggling nav layer when other modifiers are pressed. (See #4439) + // - AltGR is Alt+Ctrl on some layout but we can't reliably detect it (not all backends/systems/layout emit it as Alt+Ctrl). + // We cancel toggling nav layer if an owner has claimed the key. + if (io.InputQueueCharacters.Size > 0 || io.KeyCtrl || io.KeyShift || io.KeySuper) + g.NavWindowingToggleLayer = false; + else if (windowing_toggle_layer_start == false && g.LastKeyboardKeyPressTime == g.Time) + g.NavWindowingToggleLayer = false; + else if (TestKeyOwner(g.NavWindowingToggleKey, ImGuiKeyOwner_NoOwner) == false || TestKeyOwner(ImGuiMod_Alt, ImGuiKeyOwner_NoOwner) == false) + g.NavWindowingToggleLayer = false; + + // Apply layer toggle on Alt release + // Important: as before version <18314 we lacked an explicit IO event for focus gain/loss, we also compare mouse validity to detect old backends clearing mouse pos on focus loss. + if (IsKeyReleased(g.NavWindowingToggleKey) && g.NavWindowingToggleLayer) + if (g.ActiveId == 0 || g.ActiveIdAllowOverlap) + if (IsMousePosValid(&io.MousePos) == IsMousePosValid(&io.MousePosPrev)) + apply_toggle_layer = true; + if (!IsKeyDown(g.NavWindowingToggleKey)) + g.NavWindowingToggleLayer = false; + } // Move window if (g.NavWindowingTarget && !(g.NavWindowingTarget->Flags & ImGuiWindowFlags_NoMove)) { - ImVec2 move_delta; - if (g.NavInputSource == ImGuiInputSource_NavKeyboard && !g.IO.KeyShift) - move_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard, ImGuiInputReadMode_Down); - if (g.NavInputSource == ImGuiInputSource_NavGamepad) - move_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_PadLStick, ImGuiInputReadMode_Down); - if (move_delta.x != 0.0f || move_delta.y != 0.0f) + ImVec2 nav_move_dir; + if (g.NavInputSource == ImGuiInputSource_Keyboard && !io.KeyShift) + nav_move_dir = GetKeyMagnitude2d(ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_UpArrow, ImGuiKey_DownArrow); + if (g.NavInputSource == ImGuiInputSource_Gamepad) + nav_move_dir = GetKeyMagnitude2d(ImGuiKey_GamepadLStickLeft, ImGuiKey_GamepadLStickRight, ImGuiKey_GamepadLStickUp, ImGuiKey_GamepadLStickDown); + if (nav_move_dir.x != 0.0f || nav_move_dir.y != 0.0f) { const float NAV_MOVE_SPEED = 800.0f; - const float move_speed = ImFloor(NAV_MOVE_SPEED * g.IO.DeltaTime * ImMin(g.IO.DisplayFramebufferScale.x, g.IO.DisplayFramebufferScale.y)); // FIXME: Doesn't code variable framerate very well - SetWindowPos(g.NavWindowingTarget->RootWindow, g.NavWindowingTarget->RootWindow->Pos + move_delta * move_speed, ImGuiCond_Always); - g.NavDisableMouseHover = true; - MarkIniSettingsDirty(g.NavWindowingTarget); + const float move_step = NAV_MOVE_SPEED * io.DeltaTime * ImMin(io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y); + g.NavWindowingAccumDeltaPos += nav_move_dir * move_step; + g.NavHighlightItemUnderNav = true; + ImVec2 accum_floored = ImTrunc(g.NavWindowingAccumDeltaPos); + if (accum_floored.x != 0.0f || accum_floored.y != 0.0f) + { + ImGuiWindow* moving_window = g.NavWindowingTarget->RootWindowDockTree; + SetWindowPos(moving_window, moving_window->Pos + accum_floored, ImGuiCond_Always); + g.NavWindowingAccumDeltaPos -= accum_floored; + } } } // Apply final focus - if (apply_focus_window && (g.NavWindow == NULL || apply_focus_window != g.NavWindow->RootWindow)) - { - ClearActiveID(); - g.NavDisableHighlight = false; - g.NavDisableMouseHover = true; - apply_focus_window = NavRestoreLastChildNavWindow(apply_focus_window); - ClosePopupsOverWindow(apply_focus_window, false); - FocusWindow(apply_focus_window); - if (apply_focus_window->NavLastIds[0] == 0) - NavInitWindow(apply_focus_window, false); - - // If the window only has a menu layer, select it directly - if (apply_focus_window->DC.NavLayerActiveMask == (1 << ImGuiNavLayer_Menu)) - g.NavLayer = ImGuiNavLayer_Menu; - } if (apply_focus_window) - g.NavWindowingTarget = NULL; + NavUpdateWindowingApplyFocus(apply_focus_window); // Apply menu/layer toggle if (apply_toggle_layer && g.NavWindow) { + ClearActiveID(); + // Move to parent menu if necessary ImGuiWindow* new_nav_window = g.NavWindow; while (new_nav_window->ParentWindow - && (new_nav_window->DC.NavLayerActiveMask & (1 << ImGuiNavLayer_Menu)) == 0 + && (new_nav_window->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) == 0 && (new_nav_window->Flags & ImGuiWindowFlags_ChildWindow) != 0 && (new_nav_window->Flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_ChildMenu)) == 0) new_nav_window = new_nav_window->ParentWindow; @@ -8823,12 +14614,18 @@ static void ImGui::NavUpdateWindowing() FocusWindow(new_nav_window); new_nav_window->NavLastChildNavWindow = old_nav_window; } - g.NavDisableHighlight = false; - g.NavDisableMouseHover = true; - // When entering a regular menu bar with the Alt key, we always reinitialize the navigation ID. - const ImGuiNavLayer new_nav_layer = (g.NavWindow->DC.NavLayerActiveMask & (1 << ImGuiNavLayer_Menu)) ? (ImGuiNavLayer)((int)g.NavLayer ^ 1) : ImGuiNavLayer_Main; - NavRestoreLayer(new_nav_layer); + // Toggle layer + const ImGuiNavLayer new_nav_layer = (g.NavWindow->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) ? (ImGuiNavLayer)((int)g.NavLayer ^ 1) : ImGuiNavLayer_Main; + if (new_nav_layer != g.NavLayer) + { + // Reinitialize navigation when entering menu bar with the Alt key (FIXME: could be a properly of the layer?) + const bool preserve_layer_1_nav_id = (new_nav_window->DockNodeAsHost != NULL); + if (new_nav_layer == ImGuiNavLayer_Menu && !preserve_layer_1_nav_id) + g.NavWindow->NavLastIds[new_nav_layer] = 0; + NavRestoreLayer(new_nav_layer); + SetNavCursorVisibleAfterMove(); + } } } @@ -8836,10 +14633,12 @@ static void ImGui::NavUpdateWindowing() static const char* GetFallbackWindowNameForWindowingList(ImGuiWindow* window) { if (window->Flags & ImGuiWindowFlags_Popup) - return "(Popup)"; + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingPopup); if ((window->Flags & ImGuiWindowFlags_MenuBar) && strcmp(window->Name, "##MainMenuBar") == 0) - return "(Main menu bar)"; - return "(Untitled)"; + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingMainMenuBar); + if (window->DockNodeAsHost) + return "(Dock node)"; // Not normally shown to user. + return ImGui::LocalizeGetMsg(ImGuiLocKey_WindowingUntitled); } // Overlay displayed when using CTRL+TAB. Called by EndFrame(). @@ -8851,15 +14650,19 @@ void ImGui::NavUpdateWindowingOverlay() if (g.NavWindowingTimer < NAV_WINDOWING_LIST_APPEAR_DELAY) return; - if (g.NavWindowingList == NULL) - g.NavWindowingList = FindWindowByName("###NavWindowingList"); - SetNextWindowSizeConstraints(ImVec2(g.IO.DisplaySize.x * 0.20f, g.IO.DisplaySize.y * 0.20f), ImVec2(FLT_MAX, FLT_MAX)); - SetNextWindowPos(g.IO.DisplaySize * 0.5f, ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + if (g.NavWindowingListWindow == NULL) + g.NavWindowingListWindow = FindWindowByName("##NavWindowingOverlay"); + const ImGuiViewport* viewport = /*g.NavWindow ? g.NavWindow->Viewport :*/ GetMainViewport(); + SetNextWindowSizeConstraints(ImVec2(viewport->Size.x * 0.20f, viewport->Size.y * 0.20f), ImVec2(FLT_MAX, FLT_MAX)); + SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); PushStyleVar(ImGuiStyleVar_WindowPadding, g.Style.WindowPadding * 2.0f); - Begin("###NavWindowingList", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); + Begin("##NavWindowingOverlay", NULL, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings); + if (g.ContextName[0] != 0) + SeparatorText(g.ContextName); for (int n = g.WindowsFocusOrder.Size - 1; n >= 0; n--) { ImGuiWindow* window = g.WindowsFocusOrder[n]; + IM_ASSERT(window != NULL); // Fix static analyzers if (!IsWindowNavFocusable(window)) continue; const char* label = window->Name; @@ -8871,14 +14674,21 @@ void ImGui::NavUpdateWindowingOverlay() PopStyleVar(); } - //----------------------------------------------------------------------------- // [SECTION] DRAG AND DROP //----------------------------------------------------------------------------- +bool ImGui::IsDragDropActive() +{ + ImGuiContext& g = *GImGui; + return g.DragDropActive; +} + void ImGui::ClearDragDrop() { ImGuiContext& g = *GImGui; + if (g.DragDropActive) + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] ClearDragDrop()\n"); g.DragDropActive = false; g.DragDropPayload.Clear(); g.DragDropAcceptFlags = ImGuiDragDropFlags_None; @@ -8890,45 +14700,71 @@ void ImGui::ClearDragDrop() memset(&g.DragDropPayloadBufLocal, 0, sizeof(g.DragDropPayloadBufLocal)); } -// Call when current ID is active. +bool ImGui::BeginTooltipHidden() +{ + ImGuiContext& g = *GImGui; + bool ret = Begin("##Tooltip_Hidden", NULL, ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize); + SetWindowHiddenAndSkipItemsForCurrentFrame(g.CurrentWindow); + return ret; +} + // When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource() +// If the item has an identifier: +// - This assume/require the item to be activated (typically via ButtonBehavior). +// - Therefore if you want to use this with a mouse button other than left mouse button, it is up to the item itself to activate with another button. +// - We then pull and use the mouse button that was used to activate the item and use it to carry on the drag. +// If the item has no identifier: +// - Currently always assume left mouse button. bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; + // FIXME-DRAGDROP: While in the common-most "drag from non-zero active id" case we can tell the mouse button, + // in both SourceExtern and id==0 cases we may requires something else (explicit flags or some heuristic). + ImGuiMouseButton mouse_button = ImGuiMouseButton_Left; + bool source_drag_active = false; ImGuiID source_id = 0; ImGuiID source_parent_id = 0; - int mouse_button = 0; - if (!(flags & ImGuiDragDropFlags_SourceExtern)) + if ((flags & ImGuiDragDropFlags_SourceExtern) == 0) { - source_id = window->DC.LastItemId; - if (source_id != 0 && g.ActiveId != source_id) // Early out for most common case - return false; - if (g.IO.MouseDown[mouse_button] == false) - return false; - - if (source_id == 0) + source_id = g.LastItemData.ID; + if (source_id != 0) { + // Common path: items with ID + if (g.ActiveId != source_id) + return false; + if (g.ActiveIdMouseButton != -1) + mouse_button = g.ActiveIdMouseButton; + if (g.IO.MouseDown[mouse_button] == false || window->SkipItems) + return false; + g.ActiveIdAllowOverlap = false; + } + else + { + // Uncommon path: items without ID + if (g.IO.MouseDown[mouse_button] == false || window->SkipItems) + return false; + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) == 0 && (g.ActiveId == 0 || g.ActiveIdWindow != window)) + return false; + // If you want to use BeginDragDropSource() on an item with no unique identifier for interaction, such as Text() or Image(), you need to: - // A) Read the explanation below, B) Use the ImGuiDragDropFlags_SourceAllowNullID flag, C) Swallow your programmer pride. + // A) Read the explanation below, B) Use the ImGuiDragDropFlags_SourceAllowNullID flag. if (!(flags & ImGuiDragDropFlags_SourceAllowNullID)) { IM_ASSERT(0); return false; } - // Early out - if ((window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) == 0 && (g.ActiveId == 0 || g.ActiveIdWindow != window)) - return false; - - // Magic fallback (=somehow reprehensible) to handle items with no assigned ID, e.g. Text(), Image() + // Magic fallback to handle items with no assigned ID, e.g. Text(), Image() // We build a throwaway ID based on current ID stack + relative AABB of items in window. - // THE IDENTIFIER WON'T SURVIVE ANY REPOSITIONING OF THE WIDGET, so if your widget moves your dragging operation will be canceled. + // THE IDENTIFIER WON'T SURVIVE ANY REPOSITIONING/RESIZINGG OF THE WIDGET, so if your widget moves your dragging operation will be canceled. // We don't need to maintain/call ClearActiveID() as releasing the button will early out this function and trigger !ActiveIdIsAlive. - source_id = window->DC.LastItemId = window->GetIDFromRectangle(window->DC.LastItemRect); - bool is_hovered = ItemHoverable(window->DC.LastItemRect, source_id); + // Rely on keeping other window->LastItemXXX fields intact. + source_id = g.LastItemData.ID = window->GetIDFromRectangle(g.LastItemData.Rect); + KeepAliveID(source_id); + bool is_hovered = ItemHoverable(g.LastItemData.Rect, source_id, g.LastItemData.ItemFlags); if (is_hovered && g.IO.MouseClicked[mouse_button]) { SetActiveID(source_id, window); @@ -8937,64 +14773,72 @@ bool ImGui::BeginDragDropSource(ImGuiDragDropFlags flags) if (g.ActiveId == source_id) // Allow the underlying widget to display/return hovered during the mouse release frame, else we would get a flicker. g.ActiveIdAllowOverlap = is_hovered; } - else - { - g.ActiveIdAllowOverlap = false; - } if (g.ActiveId != source_id) return false; source_parent_id = window->IDStack.back(); source_drag_active = IsMouseDragging(mouse_button); + + // Disable navigation and key inputs while dragging + cancel existing request if any + SetActiveIdUsingAllKeyboardKeys(); } else { + // When ImGuiDragDropFlags_SourceExtern is set: window = NULL; source_id = ImHashStr("#SourceExtern"); source_drag_active = true; + mouse_button = g.IO.MouseDown[0] ? 0 : -1; + KeepAliveID(source_id); + SetActiveID(source_id, NULL); } - if (source_drag_active) + IM_ASSERT(g.DragDropWithinTarget == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() + if (!source_drag_active) + return false; + + // Activate drag and drop + if (!g.DragDropActive) { - if (!g.DragDropActive) - { - IM_ASSERT(source_id != 0); - ClearDragDrop(); - ImGuiPayload& payload = g.DragDropPayload; - payload.SourceId = source_id; - payload.SourceParentId = source_parent_id; - g.DragDropActive = true; - g.DragDropSourceFlags = flags; - g.DragDropMouseButton = mouse_button; - } - g.DragDropSourceFrameCount = g.FrameCount; - g.DragDropWithinSourceOrTarget = true; - - if (!(flags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) - { - // Target can request the Source to not display its tooltip (we use a dedicated flag to make this request explicit) - // We unfortunately can't just modify the source flags and skip the call to BeginTooltip, as caller may be emitting contents. - BeginTooltip(); - if (g.DragDropAcceptIdPrev && (g.DragDropAcceptFlags & ImGuiDragDropFlags_AcceptNoPreviewTooltip)) - { - ImGuiWindow* tooltip_window = g.CurrentWindow; - tooltip_window->SkipItems = true; - tooltip_window->HiddenFramesCanSkipItems = 1; - } - } - - if (!(flags & ImGuiDragDropFlags_SourceNoDisableHover) && !(flags & ImGuiDragDropFlags_SourceExtern)) - window->DC.LastItemStatusFlags &= ~ImGuiItemStatusFlags_HoveredRect; - - return true; + IM_ASSERT(source_id != 0); + ClearDragDrop(); + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] BeginDragDropSource() DragDropActive = true, source_id = 0x%08X%s\n", + source_id, (flags & ImGuiDragDropFlags_SourceExtern) ? " (EXTERN)" : ""); + ImGuiPayload& payload = g.DragDropPayload; + payload.SourceId = source_id; + payload.SourceParentId = source_parent_id; + g.DragDropActive = true; + g.DragDropSourceFlags = flags; + g.DragDropMouseButton = mouse_button; + if (payload.SourceId == g.ActiveId) + g.ActiveIdNoClearOnFocusLoss = true; } - return false; + g.DragDropSourceFrameCount = g.FrameCount; + g.DragDropWithinSource = true; + + if (!(flags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) + { + // Target can request the Source to not display its tooltip (we use a dedicated flag to make this request explicit) + // We unfortunately can't just modify the source flags and skip the call to BeginTooltip, as caller may be emitting contents. + bool ret; + if (g.DragDropAcceptIdPrev && (g.DragDropAcceptFlags & ImGuiDragDropFlags_AcceptNoPreviewTooltip)) + ret = BeginTooltipHidden(); + else + ret = BeginTooltip(); + IM_ASSERT(ret); // FIXME-NEWBEGIN: If this ever becomes false, we need to Begin("##Hidden", NULL, ImGuiWindowFlags_NoSavedSettings) + SetWindowHiddendAndSkipItemsForCurrentFrame(). + IM_UNUSED(ret); + } + + if (!(flags & ImGuiDragDropFlags_SourceNoDisableHover) && !(flags & ImGuiDragDropFlags_SourceExtern)) + g.LastItemData.StatusFlags &= ~ImGuiItemStatusFlags_HoveredRect; + + return true; } void ImGui::EndDragDropSource() { ImGuiContext& g = *GImGui; IM_ASSERT(g.DragDropActive); - IM_ASSERT(g.DragDropWithinSourceOrTarget && "Not after a BeginDragDropSource()?"); + IM_ASSERT(g.DragDropWithinSource && "Not after a BeginDragDropSource()?"); if (!(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoPreviewTooltip)) EndTooltip(); @@ -9002,7 +14846,7 @@ void ImGui::EndDragDropSource() // Discard the drag if have not called SetDragDropPayload() if (g.DragDropPayload.DataFrameCount == -1) ClearDragDrop(); - g.DragDropWithinSourceOrTarget = false; + g.DragDropWithinSource = false; } // Use 'cond' to choose to submit payload on drag start or every frame @@ -9014,10 +14858,10 @@ bool ImGui::SetDragDropPayload(const char* type, const void* data, size_t data_s cond = ImGuiCond_Always; IM_ASSERT(type != NULL); - IM_ASSERT(strlen(type) < IM_ARRAYSIZE(payload.DataType) && "Payload type can be at most 32 characters long"); + IM_ASSERT(ImStrlen(type) < IM_ARRAYSIZE(payload.DataType) && "Payload type can be at most 32 characters long"); IM_ASSERT((data != NULL && data_size > 0) || (data == NULL && data_size == 0)); IM_ASSERT(cond == ImGuiCond_Always || cond == ImGuiCond_Once); - IM_ASSERT(payload.SourceId != 0); // Not called between BeginDragDropSource() and EndDragDropSource() + IM_ASSERT(payload.SourceId != 0); // Not called between BeginDragDropSource() and EndDragDropSource() if (cond == ImGuiCond_Always || payload.DataFrameCount == -1) { @@ -9046,6 +14890,7 @@ bool ImGui::SetDragDropPayload(const char* type, const void* data, size_t data_s } payload.DataFrameCount = g.FrameCount; + // Return whether the payload has been accepted return (g.DragDropAcceptFrameCount == g.FrameCount) || (g.DragDropAcceptFrameCount == g.FrameCount - 1); } @@ -9056,7 +14901,8 @@ bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) return false; ImGuiWindow* window = g.CurrentWindow; - if (g.HoveredWindow == NULL || window->RootWindow != g.HoveredWindow->RootWindow) + ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; + if (hovered_window == NULL || window->RootWindowDockTree != hovered_window->RootWindowDockTree) return false; IM_ASSERT(id != 0); if (!IsMouseHoveringRect(bb.Min, bb.Max) || (id == g.DragDropPayload.SourceId)) @@ -9064,15 +14910,16 @@ bool ImGui::BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id) if (window->SkipItems) return false; - IM_ASSERT(g.DragDropWithinSourceOrTarget == false); + IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() g.DragDropTargetRect = bb; + g.DragDropTargetClipRect = window->ClipRect; // May want to be overridden by user depending on use case? g.DragDropTargetId = id; - g.DragDropWithinSourceOrTarget = true; + g.DragDropWithinTarget = true; return true; } // We don't use BeginDragDropTargetCustom() and duplicate its code because: -// 1) we use LastItemRectHoveredRect which handles items that pushes a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. +// 1) we use LastItemData's ImGuiItemStatusFlags_HoveredRect which handles items that push a temporarily clip rectangle in their code. Calling BeginDragDropTargetCustom(LastItemRect) would not handle them. // 2) and it's faster. as this code may be very frequently called, we want to early out as fast as we can. // Also note how the HoveredWindow test is positioned differently in both functions (in both functions we optimize for the cheapest early out case) bool ImGui::BeginDragDropTarget() @@ -9082,22 +14929,27 @@ bool ImGui::BeginDragDropTarget() return false; ImGuiWindow* window = g.CurrentWindow; - if (!(window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect)) + if (!(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect)) return false; - if (g.HoveredWindow == NULL || window->RootWindow != g.HoveredWindow->RootWindow) + ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow; + if (hovered_window == NULL || window->RootWindowDockTree != hovered_window->RootWindowDockTree || window->SkipItems) return false; - const ImRect& display_rect = (window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HasDisplayRect) ? window->DC.LastItemDisplayRect : window->DC.LastItemRect; - ImGuiID id = window->DC.LastItemId; + const ImRect& display_rect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasDisplayRect) ? g.LastItemData.DisplayRect : g.LastItemData.Rect; + ImGuiID id = g.LastItemData.ID; if (id == 0) + { id = window->GetIDFromRectangle(display_rect); + KeepAliveID(id); + } if (g.DragDropPayload.SourceId == id) return false; - IM_ASSERT(g.DragDropWithinSourceOrTarget == false); + IM_ASSERT(g.DragDropWithinTarget == false && g.DragDropWithinSource == false); // Can't nest BeginDragDropSource() and BeginDragDropTarget() g.DragDropTargetRect = display_rect; + g.DragDropTargetClipRect = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HasClipRect) ? g.LastItemData.ClipRect : window->ClipRect; g.DragDropTargetId = id; - g.DragDropWithinSourceOrTarget = true; + g.DragDropWithinTarget = true; return true; } @@ -9110,7 +14962,6 @@ bool ImGui::IsDragDropPayloadBeingAccepted() const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags) { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; ImGuiPayload& payload = g.DragDropPayload; IM_ASSERT(g.DragDropActive); // Not called between BeginDragDropTarget() and EndDragDropTarget() ? IM_ASSERT(payload.DataFrameCount != -1); // Forgot to call EndDragDropTarget() ? @@ -9122,49 +14973,66 @@ const ImGuiPayload* ImGui::AcceptDragDropPayload(const char* type, ImGuiDragDrop const bool was_accepted_previously = (g.DragDropAcceptIdPrev == g.DragDropTargetId); ImRect r = g.DragDropTargetRect; float r_surface = r.GetWidth() * r.GetHeight(); - if (r_surface < g.DragDropAcceptIdCurrRectSurface) - { - g.DragDropAcceptFlags = flags; - g.DragDropAcceptIdCurr = g.DragDropTargetId; - g.DragDropAcceptIdCurrRectSurface = r_surface; - } + if (r_surface > g.DragDropAcceptIdCurrRectSurface) + return NULL; + + g.DragDropAcceptFlags = flags; + g.DragDropAcceptIdCurr = g.DragDropTargetId; + g.DragDropAcceptIdCurrRectSurface = r_surface; + //IMGUI_DEBUG_LOG("AcceptDragDropPayload(): %08X: accept\n", g.DragDropTargetId); // Render default drop visuals payload.Preview = was_accepted_previously; - flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that lives for 1 frame) + flags |= (g.DragDropSourceFlags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect); // Source can also inhibit the preview (useful for external sources that live for 1 frame) if (!(flags & ImGuiDragDropFlags_AcceptNoDrawDefaultRect) && payload.Preview) - { - // FIXME-DRAG: Settle on a proper default visuals for drop target. - r.Expand(3.5f); - bool push_clip_rect = !window->ClipRect.Contains(r); - if (push_clip_rect) window->DrawList->PushClipRect(r.Min-ImVec2(1,1), r.Max+ImVec2(1,1)); - window->DrawList->AddRect(r.Min, r.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, ~0, 2.0f); - if (push_clip_rect) window->DrawList->PopClipRect(); - } + RenderDragDropTargetRect(r, g.DragDropTargetClipRect); g.DragDropAcceptFrameCount = g.FrameCount; - payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting os window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() + if ((g.DragDropSourceFlags & ImGuiDragDropFlags_SourceExtern) && g.DragDropMouseButton == -1) + payload.Delivery = was_accepted_previously && (g.DragDropSourceFrameCount < g.FrameCount); + else + payload.Delivery = was_accepted_previously && !IsMouseDown(g.DragDropMouseButton); // For extern drag sources affecting OS window focus, it's easier to just test !IsMouseDown() instead of IsMouseReleased() if (!payload.Delivery && !(flags & ImGuiDragDropFlags_AcceptBeforeDelivery)) return NULL; + if (payload.Delivery) + IMGUI_DEBUG_LOG_ACTIVEID("[dragdrop] AcceptDragDropPayload(): 0x%08X: payload delivery\n", g.DragDropTargetId); return &payload; } +// FIXME-STYLE FIXME-DRAGDROP: Settle on a proper default visuals for drop target. +void ImGui::RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImRect bb_display = bb; + bb_display.ClipWith(item_clip_rect); // Clip THEN expand so we have a way to visualize that target is not entirely visible. + bb_display.Expand(3.5f); + bool push_clip_rect = !window->ClipRect.Contains(bb_display); + if (push_clip_rect) + window->DrawList->PushClipRectFullScreen(); + window->DrawList->AddRect(bb_display.Min, bb_display.Max, GetColorU32(ImGuiCol_DragDropTarget), 0.0f, 0, 2.0f); + if (push_clip_rect) + window->DrawList->PopClipRect(); +} + const ImGuiPayload* ImGui::GetDragDropPayload() { ImGuiContext& g = *GImGui; - return g.DragDropActive ? &g.DragDropPayload : NULL; + return (g.DragDropActive && g.DragDropPayload.DataFrameCount != -1) ? &g.DragDropPayload : NULL; } -// We don't really use/need this now, but added it for the sake of consistency and because we might need it later. void ImGui::EndDragDropTarget() { ImGuiContext& g = *GImGui; IM_ASSERT(g.DragDropActive); - IM_ASSERT(g.DragDropWithinSourceOrTarget); - g.DragDropWithinSourceOrTarget = false; -} + IM_ASSERT(g.DragDropWithinTarget); + g.DragDropWithinTarget = false; + // Clear drag and drop state payload right after delivery + if (g.DragDropPayload.Delivery) + ClearDragDrop(); +} //----------------------------------------------------------------------------- // [SECTION] LOGGING/CAPTURING @@ -9174,6 +15042,20 @@ void ImGui::EndDragDropTarget() //----------------------------------------------------------------------------- // Pass text data straight to log (without being displayed) +static inline void LogTextV(ImGuiContext& g, const char* fmt, va_list args) +{ + if (g.LogFile) + { + g.LogBuffer.Buf.resize(0); + g.LogBuffer.appendfv(fmt, args); + ImFileWrite(g.LogBuffer.c_str(), sizeof(char), (ImU64)g.LogBuffer.size(), g.LogFile); + } + else + { + g.LogBuffer.appendfv(fmt, args); + } +} + void ImGui::LogText(const char* fmt, ...) { ImGuiContext& g = *GImGui; @@ -9182,88 +15064,117 @@ void ImGui::LogText(const char* fmt, ...) va_list args; va_start(args, fmt); - if (g.LogFile) - vfprintf(g.LogFile, fmt, args); - else - g.LogBuffer.appendfv(fmt, args); + LogTextV(g, fmt, args); va_end(args); } +void ImGui::LogTextV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + if (!g.LogEnabled) + return; + + LogTextV(g, fmt, args); +} + // Internal version that takes a position to decide on newline placement and pad items according to their depth. // We split text into individual lines to add current tree level padding +// FIXME: This code is a little complicated perhaps, considering simplifying the whole system. void ImGui::LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; + const char* prefix = g.LogNextPrefix; + const char* suffix = g.LogNextSuffix; + g.LogNextPrefix = g.LogNextSuffix = NULL; + if (!text_end) text_end = FindRenderedTextEnd(text, text_end); - const bool log_new_line = ref_pos && (ref_pos->y > g.LogLinePosY + 1); + const bool log_new_line = ref_pos && (ref_pos->y > g.LogLinePosY + g.Style.FramePadding.y + 1); if (ref_pos) g.LogLinePosY = ref_pos->y; if (log_new_line) + { + LogText(IM_NEWLINE); g.LogLineFirstItem = true; + } - const char* text_remaining = text; - if (g.LogDepthRef > window->DC.TreeDepth) // Re-adjust padding if we have popped out of our starting depth + if (prefix) + LogRenderedText(ref_pos, prefix, prefix + ImStrlen(prefix)); // Calculate end ourself to ensure "##" are included here. + + // Re-adjust padding if we have popped out of our starting depth + if (g.LogDepthRef > window->DC.TreeDepth) g.LogDepthRef = window->DC.TreeDepth; const int tree_depth = (window->DC.TreeDepth - g.LogDepthRef); + + const char* text_remaining = text; for (;;) { - // Split the string. Each new line (after a '\n') is followed by spacing corresponding to the current depth of our log entry. - // We don't add a trailing \n to allow a subsequent item on the same line to be captured. + // Split the string. Each new line (after a '\n') is followed by indentation corresponding to the current depth of our log entry. + // We don't add a trailing \n yet to allow a subsequent item on the same line to be captured. const char* line_start = text_remaining; const char* line_end = ImStreolRange(line_start, text_end); - const bool is_first_line = (line_start == text); const bool is_last_line = (line_end == text_end); - if (!is_last_line || (line_start != line_end)) + if (line_start != line_end || !is_last_line) { - const int char_count = (int)(line_end - line_start); - if (log_new_line || !is_first_line) - LogText(IM_NEWLINE "%*s%.*s", tree_depth * 4, "", char_count, line_start); - else if (g.LogLineFirstItem) - LogText("%*s%.*s", tree_depth * 4, "", char_count, line_start); - else - LogText(" %.*s", char_count, line_start); + const int line_length = (int)(line_end - line_start); + const int indentation = g.LogLineFirstItem ? tree_depth * 4 : 1; + LogText("%*s%.*s", indentation, "", line_length, line_start); g.LogLineFirstItem = false; + if (*line_end == '\n') + { + LogText(IM_NEWLINE); + g.LogLineFirstItem = true; + } } - else if (log_new_line) - { - // An empty "" string at a different Y position should output a carriage return. - LogText(IM_NEWLINE); - break; - } - if (is_last_line) break; text_remaining = line_end + 1; } + + if (suffix) + LogRenderedText(ref_pos, suffix, suffix + ImStrlen(suffix)); } // Start logging/capturing text output -void ImGui::LogBegin(ImGuiLogType type, int auto_open_depth) +void ImGui::LogBegin(ImGuiLogFlags flags, int auto_open_depth) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; IM_ASSERT(g.LogEnabled == false); - IM_ASSERT(g.LogFile == NULL); - IM_ASSERT(g.LogBuffer.empty()); - g.LogEnabled = true; - g.LogType = type; + IM_ASSERT(g.LogFile == NULL && g.LogBuffer.empty()); + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiLogFlags_OutputMask_)); // Check that only 1 type flag is used + + g.LogEnabled = g.ItemUnclipByLog = true; + g.LogFlags = flags; + g.LogWindow = window; + g.LogNextPrefix = g.LogNextSuffix = NULL; g.LogDepthRef = window->DC.TreeDepth; g.LogDepthToExpand = ((auto_open_depth >= 0) ? auto_open_depth : g.LogDepthToExpandDefault); g.LogLinePosY = FLT_MAX; g.LogLineFirstItem = true; } +// Important: doesn't copy underlying data, use carefully (prefix/suffix must be in scope at the time of the next LogRenderedText) +void ImGui::LogSetNextTextDecoration(const char* prefix, const char* suffix) +{ + ImGuiContext& g = *GImGui; + g.LogNextPrefix = prefix; + g.LogNextSuffix = suffix; +} + void ImGui::LogToTTY(int auto_open_depth) { ImGuiContext& g = *GImGui; if (g.LogEnabled) return; - LogBegin(ImGuiLogType_TTY, auto_open_depth); + IM_UNUSED(auto_open_depth); +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS + LogBegin(ImGuiLogFlags_OutputTTY, auto_open_depth); g.LogFile = stdout; +#endif } // Start logging/capturing text output to given file @@ -9280,14 +15191,14 @@ void ImGui::LogToFile(int auto_open_depth, const char* filename) filename = g.IO.LogFilename; if (!filename || !filename[0]) return; - FILE* f = ImFileOpen(filename, "ab"); - if (f == NULL) + ImFileHandle f = ImFileOpen(filename, "ab"); + if (!f) { IM_ASSERT(0); return; } - LogBegin(ImGuiLogType_File, auto_open_depth); + LogBegin(ImGuiLogFlags_OutputFile, auto_open_depth); g.LogFile = f; } @@ -9297,7 +15208,7 @@ void ImGui::LogToClipboard(int auto_open_depth) ImGuiContext& g = *GImGui; if (g.LogEnabled) return; - LogBegin(ImGuiLogType_Clipboard, auto_open_depth); + LogBegin(ImGuiLogFlags_OutputClipboard, auto_open_depth); } void ImGui::LogToBuffer(int auto_open_depth) @@ -9305,7 +15216,7 @@ void ImGui::LogToBuffer(int auto_open_depth) ImGuiContext& g = *GImGui; if (g.LogEnabled) return; - LogBegin(ImGuiLogType_Buffer, auto_open_depth); + LogBegin(ImGuiLogFlags_OutputBuffer, auto_open_depth); } void ImGui::LogFinish() @@ -9315,27 +15226,29 @@ void ImGui::LogFinish() return; LogText(IM_NEWLINE); - switch (g.LogType) + switch (g.LogFlags & ImGuiLogFlags_OutputMask_) { - case ImGuiLogType_TTY: + case ImGuiLogFlags_OutputTTY: +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS fflush(g.LogFile); +#endif break; - case ImGuiLogType_File: - fclose(g.LogFile); + case ImGuiLogFlags_OutputFile: + ImFileClose(g.LogFile); break; - case ImGuiLogType_Buffer: + case ImGuiLogFlags_OutputBuffer: break; - case ImGuiLogType_Clipboard: + case ImGuiLogFlags_OutputClipboard: if (!g.LogBuffer.empty()) SetClipboardText(g.LogBuffer.begin()); break; - case ImGuiLogType_None: + default: IM_ASSERT(0); break; } - g.LogEnabled = false; - g.LogType = ImGuiLogType_None; + g.LogEnabled = g.ItemUnclipByLog = false; + g.LogFlags = ImGuiLogFlags_None; g.LogFile = NULL; g.LogBuffer.clear(); } @@ -9347,13 +15260,17 @@ void ImGui::LogButtons() ImGuiContext& g = *GImGui; PushID("LogButtons"); +#ifndef IMGUI_DISABLE_TTY_FUNCTIONS const bool log_to_tty = Button("Log To TTY"); SameLine(); +#else + const bool log_to_tty = false; +#endif const bool log_to_file = Button("Log To File"); SameLine(); const bool log_to_clipboard = Button("Log To Clipboard"); SameLine(); - PushAllowKeyboardFocus(false); + PushItemFlag(ImGuiItemFlags_NoTabStop, true); SetNextItemWidth(80.0f); SliderInt("Default Depth", &g.LogDepthToExpandDefault, 0, 9, NULL); - PopAllowKeyboardFocus(); + PopItemFlag(); PopID(); // Start logging at the end of the function so that the buttons don't appear in the log @@ -9368,6 +15285,49 @@ void ImGui::LogButtons() //----------------------------------------------------------------------------- // [SECTION] SETTINGS //----------------------------------------------------------------------------- +// - UpdateSettings() [Internal] +// - MarkIniSettingsDirty() [Internal] +// - FindSettingsHandler() [Internal] +// - ClearIniSettings() [Internal] +// - LoadIniSettingsFromDisk() +// - LoadIniSettingsFromMemory() +// - SaveIniSettingsToDisk() +// - SaveIniSettingsToMemory() +//----------------------------------------------------------------------------- +// - CreateNewWindowSettings() [Internal] +// - FindWindowSettingsByID() [Internal] +// - FindWindowSettingsByWindow() [Internal] +// - ClearWindowSettings() [Internal] +// - WindowSettingsHandler_***() [Internal] +//----------------------------------------------------------------------------- + +// Called by NewFrame() +void ImGui::UpdateSettings() +{ + // Load settings on first frame (if not explicitly loaded manually before) + ImGuiContext& g = *GImGui; + if (!g.SettingsLoaded) + { + IM_ASSERT(g.SettingsWindows.empty()); + if (g.IO.IniFilename) + LoadIniSettingsFromDisk(g.IO.IniFilename); + g.SettingsLoaded = true; + } + + // Save settings (with a delay after the last modification, so we don't spam disk too much) + if (g.SettingsDirtyTimer > 0.0f) + { + g.SettingsDirtyTimer -= g.IO.DeltaTime; + if (g.SettingsDirtyTimer <= 0.0f) + { + if (g.IO.IniFilename != NULL) + SaveIniSettingsToDisk(g.IO.IniFilename); + else + g.IO.WantSaveIniSettings = true; // Let user know they can call SaveIniSettingsToMemory(). user will need to clear io.WantSaveIniSettings themselves. + g.SettingsDirtyTimer = 0.0f; + } + } +} void ImGui::MarkIniSettingsDirty() { @@ -9384,36 +15344,38 @@ void ImGui::MarkIniSettingsDirty(ImGuiWindow* window) g.SettingsDirtyTimer = g.IO.IniSavingRate; } -ImGuiWindowSettings* ImGui::CreateNewWindowSettings(const char* name) +void ImGui::AddSettingsHandler(const ImGuiSettingsHandler* handler) { ImGuiContext& g = *GImGui; - g.SettingsWindows.push_back(ImGuiWindowSettings()); - ImGuiWindowSettings* settings = &g.SettingsWindows.back(); -#if !IMGUI_DEBUG_INI_SETTINGS - // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() - // Preserve the full string when IMGUI_DEBUG_INI_SETTINGS is set to make .ini inspection easier. - if (const char* p = strstr(name, "###")) - name = p; -#endif - settings->Name = ImStrdup(name); - settings->ID = ImHashStr(name); - return settings; + IM_ASSERT(FindSettingsHandler(handler->TypeName) == NULL); + g.SettingsHandlers.push_back(*handler); } -ImGuiWindowSettings* ImGui::FindWindowSettings(ImGuiID id) +void ImGui::RemoveSettingsHandler(const char* type_name) { ImGuiContext& g = *GImGui; - for (int i = 0; i != g.SettingsWindows.Size; i++) - if (g.SettingsWindows[i].ID == id) - return &g.SettingsWindows[i]; + if (ImGuiSettingsHandler* handler = FindSettingsHandler(type_name)) + g.SettingsHandlers.erase(handler); +} + +ImGuiSettingsHandler* ImGui::FindSettingsHandler(const char* type_name) +{ + ImGuiContext& g = *GImGui; + const ImGuiID type_hash = ImHashStr(type_name); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.TypeHash == type_hash) + return &handler; return NULL; } -ImGuiWindowSettings* ImGui::FindOrCreateWindowSettings(const char* name) +// Clear all settings (windows, tables, docking etc.) +void ImGui::ClearIniSettings() { - if (ImGuiWindowSettings* settings = FindWindowSettings(ImHashStr(name))) - return settings; - return CreateNewWindowSettings(name); + ImGuiContext& g = *GImGui; + g.SettingsIniData.clear(); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ClearAllFn != NULL) + handler.ClearAllFn(&g, &handler); } void ImGui::LoadIniSettingsFromDisk(const char* ini_filename) @@ -9422,35 +15384,35 @@ void ImGui::LoadIniSettingsFromDisk(const char* ini_filename) char* file_data = (char*)ImFileLoadToMemory(ini_filename, "rb", &file_data_size); if (!file_data) return; - LoadIniSettingsFromMemory(file_data, (size_t)file_data_size); + if (file_data_size > 0) + LoadIniSettingsFromMemory(file_data, (size_t)file_data_size); IM_FREE(file_data); } -ImGuiSettingsHandler* ImGui::FindSettingsHandler(const char* type_name) -{ - ImGuiContext& g = *GImGui; - const ImGuiID type_hash = ImHashStr(type_name); - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - if (g.SettingsHandlers[handler_n].TypeHash == type_hash) - return &g.SettingsHandlers[handler_n]; - return NULL; -} - // Zero-tolerance, no error reporting, cheap .ini parsing +// Set ini_size==0 to let us use strlen(ini_data). Do not call this function with a 0 if your buffer is actually empty! void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) { ImGuiContext& g = *GImGui; IM_ASSERT(g.Initialized); - IM_ASSERT(g.SettingsLoaded == false && g.FrameCount == 0); + //IM_ASSERT(!g.WithinFrameScope && "Cannot be called between NewFrame() and EndFrame()"); + //IM_ASSERT(g.SettingsLoaded == false && g.FrameCount == 0); // For user convenience, we allow passing a non zero-terminated string (hence the ini_size parameter). // For our convenience and to make the code simpler, we'll also write zero-terminators within the buffer. So let's create a writable copy.. if (ini_size == 0) - ini_size = strlen(ini_data); - char* buf = (char*)IM_ALLOC(ini_size + 1); - char* buf_end = buf + ini_size; + ini_size = ImStrlen(ini_data); + g.SettingsIniData.Buf.resize((int)ini_size + 1); + char* const buf = g.SettingsIniData.Buf.Data; + char* const buf_end = buf + ini_size; memcpy(buf, ini_data, ini_size); - buf[ini_size] = 0; + buf_end[0] = 0; + + // Call pre-read handlers + // Some types will clear their data (e.g. dock information) some types will allow merge/override (window) + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ReadInitFn != NULL) + handler.ReadInitFn(&g, &handler); void* entry_data = NULL; ImGuiSettingsHandler* entry_handler = NULL; @@ -9473,18 +15435,12 @@ void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) line_end[-1] = 0; const char* name_end = line_end - 1; const char* type_start = line + 1; - char* type_end = (char*)(intptr_t)ImStrchrRange(type_start, name_end, ']'); + char* type_end = (char*)(void*)ImStrchrRange(type_start, name_end, ']'); const char* name_start = type_end ? ImStrchrRange(type_end + 1, name_end, '[') : NULL; if (!type_end || !name_start) - { - name_start = type_start; // Import legacy entries that have no type - type_start = "Window"; - } - else - { - *type_end = 0; // Overwrite first ']' - name_start++; // Skip second '[' - } + continue; + *type_end = 0; // Overwrite first ']' + name_start++; // Skip second '[' entry_handler = FindSettingsHandler(type_start); entry_data = entry_handler ? entry_handler->ReadOpenFn(&g, entry_handler, name_start) : NULL; } @@ -9494,8 +15450,15 @@ void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) entry_handler->ReadLineFn(&g, entry_handler, entry_data, line); } } - IM_FREE(buf); g.SettingsLoaded = true; + + // [DEBUG] Restore untouched copy so it can be browsed in Metrics (not strictly necessary) + memcpy(buf, ini_data, ini_size); + + // Call post-read handlers + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.ApplyAllFn != NULL) + handler.ApplyAllFn(&g, &handler); } void ImGui::SaveIniSettingsToDisk(const char* ini_filename) @@ -9507,11 +15470,11 @@ void ImGui::SaveIniSettingsToDisk(const char* ini_filename) size_t ini_data_size = 0; const char* ini_data = SaveIniSettingsToMemory(&ini_data_size); - FILE* f = ImFileOpen(ini_filename, "wt"); + ImFileHandle f = ImFileOpen(ini_filename, "wt"); if (!f) return; - fwrite(ini_data, sizeof(char), ini_data_size, f); - fclose(f); + ImFileWrite(ini_data, sizeof(char), ini_data_size, f); + ImFileClose(f); } // Call registered handlers (e.g. SettingsHandlerWindow_WriteAll() + custom handlers) to write their stuff into a text buffer @@ -9521,113 +15484,5195 @@ const char* ImGui::SaveIniSettingsToMemory(size_t* out_size) g.SettingsDirtyTimer = 0.0f; g.SettingsIniData.Buf.resize(0); g.SettingsIniData.Buf.push_back(0); - for (int handler_n = 0; handler_n < g.SettingsHandlers.Size; handler_n++) - { - ImGuiSettingsHandler* handler = &g.SettingsHandlers[handler_n]; - handler->WriteAllFn(&g, handler, &g.SettingsIniData); - } + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + handler.WriteAllFn(&g, &handler, &g.SettingsIniData); if (out_size) *out_size = (size_t)g.SettingsIniData.size(); return g.SettingsIniData.c_str(); } -static void* SettingsHandlerWindow_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +ImGuiWindowSettings* ImGui::CreateNewWindowSettings(const char* name) { - ImGuiWindowSettings* settings = ImGui::FindWindowSettings(ImHashStr(name)); - if (!settings) + ImGuiContext& g = *GImGui; + + if (g.IO.ConfigDebugIniSettings == false) + { + // Skip to the "###" marker if any. We don't skip past to match the behavior of GetID() + // Preserve the full string when ConfigDebugVerboseIniSettings is set to make .ini inspection easier. + if (const char* p = strstr(name, "###")) + name = p; + } + const size_t name_len = ImStrlen(name); + + // Allocate chunk + const size_t chunk_size = sizeof(ImGuiWindowSettings) + name_len + 1; + ImGuiWindowSettings* settings = g.SettingsWindows.alloc_chunk(chunk_size); + IM_PLACEMENT_NEW(settings) ImGuiWindowSettings(); + settings->ID = ImHashStr(name, name_len); + memcpy(settings->GetName(), name, name_len + 1); // Store with zero terminator + + return settings; +} + +// We don't provide a FindWindowSettingsByName() because Docking system doesn't always hold on names. +// This is called once per window .ini entry + once per newly instantiated window. +ImGuiWindowSettings* ImGui::FindWindowSettingsByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->ID == id && !settings->WantDelete) + return settings; + return NULL; +} + +// This is faster if you are holding on a Window already as we don't need to perform a search. +ImGuiWindowSettings* ImGui::FindWindowSettingsByWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (window->SettingsOffset != -1) + return g.SettingsWindows.ptr_from_offset(window->SettingsOffset); + return FindWindowSettingsByID(window->ID); +} + +// This will revert window to its initial state, including enabling the ImGuiCond_FirstUseEver/ImGuiCond_Once conditions once more. +void ImGui::ClearWindowSettings(const char* name) +{ + //IMGUI_DEBUG_LOG("ClearWindowSettings('%s')\n", name); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = FindWindowByName(name); + if (window != NULL) + { + window->Flags |= ImGuiWindowFlags_NoSavedSettings; + InitOrLoadWindowSettings(window, NULL); + if (window->DockId != 0) + DockContextProcessUndockWindow(&g, window, true); + } + if (ImGuiWindowSettings* settings = window ? FindWindowSettingsByWindow(window) : FindWindowSettingsByID(ImHashStr(name))) + settings->WantDelete = true; +} + +static void WindowSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (ImGuiWindow* window : g.Windows) + window->SettingsOffset = -1; + g.SettingsWindows.clear(); +} + +static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiID id = ImHashStr(name); + ImGuiWindowSettings* settings = ImGui::FindWindowSettingsByID(id); + if (settings) + *settings = ImGuiWindowSettings(); // Clear existing if recycling previous entry + else settings = ImGui::CreateNewWindowSettings(name); + settings->ID = id; + settings->WantApply = true; return (void*)settings; } -static void SettingsHandlerWindow_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) { ImGuiWindowSettings* settings = (ImGuiWindowSettings*)entry; int x, y; int i; - if (sscanf(line, "Pos=%i,%i", &x, &y) == 2) settings->Pos = ImVec2ih((short)x, (short)y); - else if (sscanf(line, "Size=%i,%i", &x, &y) == 2) settings->Size = ImVec2ih((short)x, (short)y); - else if (sscanf(line, "Collapsed=%d", &i) == 1) settings->Collapsed = (i != 0); + ImU32 u1; + if (sscanf(line, "Pos=%i,%i", &x, &y) == 2) { settings->Pos = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Size=%i,%i", &x, &y) == 2) { settings->Size = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "ViewportId=0x%08X", &u1) == 1) { settings->ViewportId = u1; } + else if (sscanf(line, "ViewportPos=%i,%i", &x, &y) == 2){ settings->ViewportPos = ImVec2ih((short)x, (short)y); } + else if (sscanf(line, "Collapsed=%d", &i) == 1) { settings->Collapsed = (i != 0); } + else if (sscanf(line, "IsChild=%d", &i) == 1) { settings->IsChild = (i != 0); } + else if (sscanf(line, "DockId=0x%X,%d", &u1, &i) == 2) { settings->DockId = u1; settings->DockOrder = (short)i; } + else if (sscanf(line, "DockId=0x%X", &u1) == 1) { settings->DockId = u1; settings->DockOrder = -1; } + else if (sscanf(line, "ClassId=0x%X", &u1) == 1) { settings->ClassId = u1; } } -static void SettingsHandlerWindow_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +// Apply to existing windows (if any) +static void WindowSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->WantApply) + { + if (ImGuiWindow* window = ImGui::FindWindowByID(settings->ID)) + ApplyWindowSettings(window, settings); + settings->WantApply = false; + } +} + +static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) { // Gather data from windows that were active during this session // (if a window wasn't opened in this session we preserve its settings) ImGuiContext& g = *ctx; - for (int i = 0; i != g.Windows.Size; i++) + for (ImGuiWindow* window : g.Windows) { - ImGuiWindow* window = g.Windows[i]; if (window->Flags & ImGuiWindowFlags_NoSavedSettings) continue; - ImGuiWindowSettings* settings = (window->SettingsIdx != -1) ? &g.SettingsWindows[window->SettingsIdx] : ImGui::FindWindowSettings(window->ID); + ImGuiWindowSettings* settings = ImGui::FindWindowSettingsByWindow(window); if (!settings) { settings = ImGui::CreateNewWindowSettings(window->Name); - window->SettingsIdx = g.SettingsWindows.index_from_ptr(settings); + window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); } IM_ASSERT(settings->ID == window->ID); - settings->Pos = ImVec2ih((short)window->Pos.x, (short)window->Pos.y); - settings->Size = ImVec2ih((short)window->SizeFull.x, (short)window->SizeFull.y); + settings->Pos = ImVec2ih(window->Pos - window->ViewportPos); + settings->Size = ImVec2ih(window->SizeFull); + settings->ViewportId = window->ViewportId; + settings->ViewportPos = ImVec2ih(window->ViewportPos); + IM_ASSERT(window->DockNode == NULL || window->DockNode->ID == window->DockId); + settings->DockId = window->DockId; + settings->ClassId = window->WindowClass.ClassId; + settings->DockOrder = window->DockOrder; settings->Collapsed = window->Collapsed; + settings->IsChild = (window->RootWindow != window); // Cannot rely on ImGuiWindowFlags_ChildWindow here as docked windows have this set. + settings->WantDelete = false; } // Write to text buffer - buf->reserve(buf->size() + g.SettingsWindows.Size * 96); // ballpark reserve - for (int i = 0; i != g.SettingsWindows.Size; i++) + buf->reserve(buf->size() + g.SettingsWindows.size() * 6); // ballpark reserve + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) { - const ImGuiWindowSettings* settings = &g.SettingsWindows[i]; - buf->appendf("[%s][%s]\n", handler->TypeName, settings->Name); - buf->appendf("Pos=%d,%d\n", settings->Pos.x, settings->Pos.y); - buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); - buf->appendf("Collapsed=%d\n", settings->Collapsed); - buf->appendf("\n"); + if (settings->WantDelete) + continue; + const char* settings_name = settings->GetName(); + buf->appendf("[%s][%s]\n", handler->TypeName, settings_name); + if (settings->IsChild) + { + buf->appendf("IsChild=1\n"); + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + } + else + { + if (settings->ViewportId != 0 && settings->ViewportId != ImGui::IMGUI_VIEWPORT_DEFAULT_ID) + { + buf->appendf("ViewportPos=%d,%d\n", settings->ViewportPos.x, settings->ViewportPos.y); + buf->appendf("ViewportId=0x%08X\n", settings->ViewportId); + } + if (settings->Pos.x != 0 || settings->Pos.y != 0 || settings->ViewportId == ImGui::IMGUI_VIEWPORT_DEFAULT_ID) + buf->appendf("Pos=%d,%d\n", settings->Pos.x, settings->Pos.y); + if (settings->Size.x != 0 || settings->Size.y != 0) + buf->appendf("Size=%d,%d\n", settings->Size.x, settings->Size.y); + buf->appendf("Collapsed=%d\n", settings->Collapsed); + if (settings->DockId != 0) + { + //buf->appendf("TabId=0x%08X\n", ImHashStr("#TAB", 4, settings->ID)); // window->TabId: this is not read back but writing it makes "debugging" the .ini data easier. + if (settings->DockOrder == -1) + buf->appendf("DockId=0x%08X\n", settings->DockId); + else + buf->appendf("DockId=0x%08X,%d\n", settings->DockId, settings->DockOrder); + if (settings->ClassId != 0) + buf->appendf("ClassId=0x%08X\n", settings->ClassId); + } + } + buf->append("\n"); } } +//----------------------------------------------------------------------------- +// [SECTION] LOCALIZATION +//----------------------------------------------------------------------------- + +void ImGui::LocalizeRegisterEntries(const ImGuiLocEntry* entries, int count) +{ + ImGuiContext& g = *GImGui; + for (int n = 0; n < count; n++) + g.LocalizationTable[entries[n].Key] = entries[n].Text; +} //----------------------------------------------------------------------------- // [SECTION] VIEWPORTS, PLATFORM WINDOWS //----------------------------------------------------------------------------- +// - GetMainViewport() +// - FindViewportByID() +// - FindViewportByPlatformHandle() +// - SetCurrentViewport() [Internal] +// - SetWindowViewport() [Internal] +// - GetWindowAlwaysWantOwnViewport() [Internal] +// - UpdateTryMergeWindowIntoHostViewport() [Internal] +// - UpdateTryMergeWindowIntoHostViewports() [Internal] +// - TranslateWindowsInViewport() [Internal] +// - ScaleWindowsInViewport() [Internal] +// - FindHoveredViewportFromPlatformWindowStack() [Internal] +// - UpdateViewportsNewFrame() [Internal] +// - UpdateViewportsEndFrame() [Internal] +// - AddUpdateViewport() [Internal] +// - WindowSelectViewport() [Internal] +// - WindowSyncOwnedViewport() [Internal] +// - UpdatePlatformWindows() +// - RenderPlatformWindowsDefault() +// - FindPlatformMonitorForPos() [Internal] +// - FindPlatformMonitorForRect() [Internal] +// - UpdateViewportPlatformMonitor() [Internal] +// - DestroyPlatformWindow() [Internal] +// - DestroyPlatformWindows() +//----------------------------------------------------------------------------- -// (this section is filled in the 'docking' branch) +ImGuiViewport* ImGui::GetMainViewport() +{ + ImGuiContext& g = *GImGui; + return g.Viewports[0]; +} + +// FIXME: This leaks access to viewports not listed in PlatformIO.Viewports[]. Problematic? (#4236) +ImGuiViewport* ImGui::FindViewportByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + for (ImGuiViewportP* viewport : g.Viewports) + if (viewport->ID == id) + return viewport; + return NULL; +} + +ImGuiViewport* ImGui::FindViewportByPlatformHandle(void* platform_handle) +{ + ImGuiContext& g = *GImGui; + for (ImGuiViewportP* viewport : g.Viewports) + if (viewport->PlatformHandle == platform_handle) + return viewport; + return NULL; +} + +void ImGui::SetCurrentViewport(ImGuiWindow* current_window, ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + (void)current_window; + + if (viewport) + viewport->LastFrameActive = g.FrameCount; + if (g.CurrentViewport == viewport) + return; + g.CurrentDpiScale = viewport ? viewport->DpiScale : 1.0f; + g.CurrentViewport = viewport; + IM_ASSERT(g.CurrentDpiScale > 0.0f && g.CurrentDpiScale < 99.0f); // Typical correct values would be between 1.0f and 4.0f + //IMGUI_DEBUG_LOG_VIEWPORT("[viewport] SetCurrentViewport changed '%s' 0x%08X\n", current_window ? current_window->Name : NULL, viewport ? viewport->ID : 0); + + // Notify platform layer of viewport changes + // FIXME-DPI: This is only currently used for experimenting with handling of multiple DPI + if (g.CurrentViewport && g.PlatformIO.Platform_OnChangedViewport) + g.PlatformIO.Platform_OnChangedViewport(g.CurrentViewport); +} + +void ImGui::SetWindowViewport(ImGuiWindow* window, ImGuiViewportP* viewport) +{ + // Abandon viewport + if (window->ViewportOwned && window->Viewport->Window == window) + window->Viewport->Size = ImVec2(0.0f, 0.0f); + + window->Viewport = viewport; + window->ViewportId = viewport->ID; + window->ViewportOwned = (viewport->Window == window); +} + +static bool ImGui::GetWindowAlwaysWantOwnViewport(ImGuiWindow* window) +{ + // Tooltips and menus are not automatically forced into their own viewport when the NoMerge flag is set, however the multiplication of viewports makes them more likely to protrude and create their own. + ImGuiContext& g = *GImGui; + if (g.IO.ConfigViewportsNoAutoMerge || (window->WindowClass.ViewportFlagsOverrideSet & ImGuiViewportFlags_NoAutoMerge)) + if (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) + if (!window->DockIsActive) + if ((window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_Tooltip)) == 0) + if ((window->Flags & ImGuiWindowFlags_Popup) == 0 || (window->Flags & ImGuiWindowFlags_Modal) != 0) + return true; + return false; +} + +static bool ImGui::UpdateTryMergeWindowIntoHostViewport(ImGuiWindow* window, ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + if (window->Viewport == viewport) + return false; + if ((viewport->Flags & ImGuiViewportFlags_CanHostOtherWindows) == 0) + return false; + if ((viewport->Flags & ImGuiViewportFlags_IsMinimized) != 0) + return false; + if (!viewport->GetMainRect().Contains(window->Rect())) + return false; + if (GetWindowAlwaysWantOwnViewport(window)) + return false; + + // FIXME: Can't use g.WindowsFocusOrder[] for root windows only as we care about Z order. If we maintained a DisplayOrder along with FocusOrder we could.. + for (ImGuiWindow* window_behind : g.Windows) + { + if (window_behind == window) + break; + if (window_behind->WasActive && window_behind->ViewportOwned && !(window_behind->Flags & ImGuiWindowFlags_ChildWindow)) + if (window_behind->Viewport->GetMainRect().Overlaps(window->Rect())) + return false; + } + + // Move to the existing viewport, Move child/hosted windows as well (FIXME-OPT: iterate child) + ImGuiViewportP* old_viewport = window->Viewport; + if (window->ViewportOwned) + for (int n = 0; n < g.Windows.Size; n++) + if (g.Windows[n]->Viewport == old_viewport) + SetWindowViewport(g.Windows[n], viewport); + SetWindowViewport(window, viewport); + BringWindowToDisplayFront(window); + + return true; +} + +// FIXME: handle 0 to N host viewports +static bool ImGui::UpdateTryMergeWindowIntoHostViewports(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + return UpdateTryMergeWindowIntoHostViewport(window, g.Viewports[0]); +} + +// Translate Dear ImGui windows when a Host Viewport has been moved +// (This additionally keeps windows at the same place when ImGuiConfigFlags_ViewportsEnable is toggled!) +void ImGui::TranslateWindowsInViewport(ImGuiViewportP* viewport, const ImVec2& old_pos, const ImVec2& new_pos, const ImVec2& old_size, const ImVec2& new_size) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(viewport->Window == NULL && (viewport->Flags & ImGuiViewportFlags_CanHostOtherWindows)); + + // 1) We test if ImGuiConfigFlags_ViewportsEnable was just toggled, which allows us to conveniently + // translate imgui windows from OS-window-local to absolute coordinates or vice-versa. + // 2) If it's not going to fit into the new size, keep it at same absolute position. + // One problem with this is that most Win32 applications doesn't update their render while dragging, + // and so the window will appear to teleport when releasing the mouse. + const bool translate_all_windows = (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) != (g.ConfigFlagsLastFrame & ImGuiConfigFlags_ViewportsEnable); + ImRect test_still_fit_rect(old_pos, old_pos + viewport->Size); + ImVec2 delta_pos = new_pos - old_pos; + for (ImGuiWindow* window : g.Windows) // FIXME-OPT + if (translate_all_windows || (window->Viewport == viewport && (old_size == new_size || test_still_fit_rect.Contains(window->Rect())))) + TranslateWindow(window, delta_pos); +} + +// Scale all windows (position, size). Use when e.g. changing DPI. (This is a lossy operation!) +void ImGui::ScaleWindowsInViewport(ImGuiViewportP* viewport, float scale) +{ + ImGuiContext& g = *GImGui; + if (viewport->Window) + { + ScaleWindow(viewport->Window, scale); + } + else + { + for (ImGuiWindow* window : g.Windows) + if (window->Viewport == viewport) + ScaleWindow(window, scale); + } +} + +// If the backend doesn't set MouseLastHoveredViewport or doesn't honor ImGuiViewportFlags_NoInputs, we do a search ourselves. +// A) It won't take account of the possibility that non-imgui windows may be in-between our dragged window and our target window. +// B) It requires Platform_GetWindowFocus to be implemented by backend. +ImGuiViewportP* ImGui::FindHoveredViewportFromPlatformWindowStack(const ImVec2& mouse_platform_pos) +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* best_candidate = NULL; + for (ImGuiViewportP* viewport : g.Viewports) + if (!(viewport->Flags & (ImGuiViewportFlags_NoInputs | ImGuiViewportFlags_IsMinimized)) && viewport->GetMainRect().Contains(mouse_platform_pos)) + if (best_candidate == NULL || best_candidate->LastFocusedStampCount < viewport->LastFocusedStampCount) + best_candidate = viewport; + return best_candidate; +} + +// Update viewports and monitor infos +// Note that this is running even if 'ImGuiConfigFlags_ViewportsEnable' is not set, in order to clear unused viewports (if any) and update monitor info. +static void ImGui::UpdateViewportsNewFrame() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.PlatformIO.Viewports.Size <= g.Viewports.Size); + + // Update Minimized status (we need it first in order to decide if we'll apply Pos/Size of the main viewport) + // Update Focused status + const bool viewports_enabled = (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) != 0; + if (viewports_enabled) + { + ImGuiViewportP* focused_viewport = NULL; + for (ImGuiViewportP* viewport : g.Viewports) + { + const bool platform_funcs_available = viewport->PlatformWindowCreated; + if (g.PlatformIO.Platform_GetWindowMinimized && platform_funcs_available) + { + bool is_minimized = g.PlatformIO.Platform_GetWindowMinimized(viewport); + if (is_minimized) + viewport->Flags |= ImGuiViewportFlags_IsMinimized; + else + viewport->Flags &= ~ImGuiViewportFlags_IsMinimized; + } + + // Update our implicit z-order knowledge of platform windows, which is used when the backend cannot provide io.MouseHoveredViewport. + // When setting Platform_GetWindowFocus, it is expected that the platform backend can handle calls without crashing if it doesn't have data stored. + if (g.PlatformIO.Platform_GetWindowFocus && platform_funcs_available) + { + bool is_focused = g.PlatformIO.Platform_GetWindowFocus(viewport); + if (is_focused) + viewport->Flags |= ImGuiViewportFlags_IsFocused; + else + viewport->Flags &= ~ImGuiViewportFlags_IsFocused; + if (is_focused) + focused_viewport = viewport; + } + } + + // Focused viewport has changed? + if (focused_viewport && g.PlatformLastFocusedViewportId != focused_viewport->ID) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Focused viewport changed %08X -> %08X, attempting to apply our focus.\n", g.PlatformLastFocusedViewportId, focused_viewport->ID); + const ImGuiViewport* prev_focused_viewport = FindViewportByID(g.PlatformLastFocusedViewportId); + const bool prev_focused_has_been_destroyed = (prev_focused_viewport == NULL) || (prev_focused_viewport->PlatformWindowCreated == false); + + // Store a tag so we can infer z-order easily from all our windows + // We compare PlatformLastFocusedViewportId so newly created viewports with _NoFocusOnAppearing flag + // will keep the front most stamp instead of losing it back to their parent viewport. + if (focused_viewport->LastFocusedStampCount != g.ViewportFocusedStampCount) + focused_viewport->LastFocusedStampCount = ++g.ViewportFocusedStampCount; + g.PlatformLastFocusedViewportId = focused_viewport->ID; + + // Focus associated dear imgui window + // - if focus didn't happen with a click within imgui boundaries, e.g. Clicking platform title bar. (#6299) + // - if focus didn't happen because we destroyed another window (#6462) + // FIXME: perhaps 'FocusTopMostWindowUnderOne()' can handle the 'focused_window->Window != NULL' case as well. + const bool apply_imgui_focus_on_focused_viewport = !IsAnyMouseDown() && !prev_focused_has_been_destroyed; + if (apply_imgui_focus_on_focused_viewport) + { + focused_viewport->LastFocusedHadNavWindow |= (g.NavWindow != NULL) && (g.NavWindow->Viewport == focused_viewport); // Update so a window changing viewport won't lose focus. + ImGuiFocusRequestFlags focus_request_flags = ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild; + if (focused_viewport->Window != NULL) + FocusWindow(focused_viewport->Window, focus_request_flags); + else if (focused_viewport->LastFocusedHadNavWindow) + FocusTopMostWindowUnderOne(NULL, NULL, focused_viewport, focus_request_flags); // Focus top most in viewport + else + FocusWindow(NULL, focus_request_flags); // No window had focus last time viewport was focused + } + } + if (focused_viewport) + focused_viewport->LastFocusedHadNavWindow = (g.NavWindow != NULL) && (g.NavWindow->Viewport == focused_viewport); + } + + // Create/update main viewport with current platform position. + // FIXME-VIEWPORT: Size is driven by backend/user code for backward-compatibility but we should aim to make this more consistent. + ImGuiViewportP* main_viewport = g.Viewports[0]; + IM_ASSERT(main_viewport->ID == IMGUI_VIEWPORT_DEFAULT_ID); + IM_ASSERT(main_viewport->Window == NULL); + ImVec2 main_viewport_pos = viewports_enabled ? g.PlatformIO.Platform_GetWindowPos(main_viewport) : ImVec2(0.0f, 0.0f); + ImVec2 main_viewport_size = g.IO.DisplaySize; + if (viewports_enabled && (main_viewport->Flags & ImGuiViewportFlags_IsMinimized)) + { + main_viewport_pos = main_viewport->Pos; // Preserve last pos/size when minimized (FIXME: We don't do the same for Size outside of the viewport path) + main_viewport_size = main_viewport->Size; + } + AddUpdateViewport(NULL, IMGUI_VIEWPORT_DEFAULT_ID, main_viewport_pos, main_viewport_size, ImGuiViewportFlags_OwnedByApp | ImGuiViewportFlags_CanHostOtherWindows); + + g.CurrentDpiScale = 0.0f; + g.CurrentViewport = NULL; + g.MouseViewport = NULL; + for (int n = 0; n < g.Viewports.Size; n++) + { + ImGuiViewportP* viewport = g.Viewports[n]; + viewport->Idx = n; + + // Erase unused viewports + if (n > 0 && viewport->LastFrameActive < g.FrameCount - 2) + { + DestroyViewport(viewport); + n--; + continue; + } + + const bool platform_funcs_available = viewport->PlatformWindowCreated; + if (viewports_enabled) + { + // Update Position and Size (from Platform Window to ImGui) if requested. + // We do it early in the frame instead of waiting for UpdatePlatformWindows() to avoid a frame of lag when moving/resizing using OS facilities. + if (!(viewport->Flags & ImGuiViewportFlags_IsMinimized) && platform_funcs_available) + { + // Viewport->WorkPos and WorkSize will be updated below + if (viewport->PlatformRequestMove) + viewport->Pos = viewport->LastPlatformPos = g.PlatformIO.Platform_GetWindowPos(viewport); + if (viewport->PlatformRequestResize) + viewport->Size = viewport->LastPlatformSize = g.PlatformIO.Platform_GetWindowSize(viewport); + } + } + + // Update/copy monitor info + UpdateViewportPlatformMonitor(viewport); + + // Lock down space taken by menu bars and status bars + query initial insets from backend + // Setup initial value for functions like BeginMainMenuBar(), DockSpaceOverViewport() etc. + viewport->WorkInsetMin = viewport->BuildWorkInsetMin; + viewport->WorkInsetMax = viewport->BuildWorkInsetMax; + viewport->BuildWorkInsetMin = viewport->BuildWorkInsetMax = ImVec2(0.0f, 0.0f); + if (g.PlatformIO.Platform_GetWindowWorkAreaInsets != NULL && platform_funcs_available) + { + ImVec4 insets = g.PlatformIO.Platform_GetWindowWorkAreaInsets(viewport); + IM_ASSERT(insets.x >= 0.0f && insets.y >= 0.0f && insets.z >= 0.0f && insets.w >= 0.0f); + viewport->BuildWorkInsetMin = ImVec2(insets.x, insets.y); + viewport->BuildWorkInsetMax = ImVec2(insets.z, insets.w); + } + viewport->UpdateWorkRect(); + + // Reset alpha every frame. Users of transparency (docking) needs to request a lower alpha back. + viewport->Alpha = 1.0f; + + // Translate Dear ImGui windows when a Host Viewport has been moved + // (This additionally keeps windows at the same place when ImGuiConfigFlags_ViewportsEnable is toggled!) + const ImVec2 viewport_delta_pos = viewport->Pos - viewport->LastPos; + if ((viewport->Flags & ImGuiViewportFlags_CanHostOtherWindows) && (viewport_delta_pos.x != 0.0f || viewport_delta_pos.y != 0.0f)) + TranslateWindowsInViewport(viewport, viewport->LastPos, viewport->Pos, viewport->LastSize, viewport->Size); + + // Update DPI scale + float new_dpi_scale; + if (g.PlatformIO.Platform_GetWindowDpiScale && platform_funcs_available) + new_dpi_scale = g.PlatformIO.Platform_GetWindowDpiScale(viewport); + else if (viewport->PlatformMonitor != -1) + new_dpi_scale = g.PlatformIO.Monitors[viewport->PlatformMonitor].DpiScale; + else + new_dpi_scale = (viewport->DpiScale != 0.0f) ? viewport->DpiScale : 1.0f; + IM_ASSERT(new_dpi_scale > 0.0f && new_dpi_scale < 99.0f); // Typical correct values would be between 1.0f and 4.0f + if (viewport->DpiScale != 0.0f && new_dpi_scale != viewport->DpiScale) + { + float scale_factor = new_dpi_scale / viewport->DpiScale; + if (g.IO.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleViewports) + ScaleWindowsInViewport(viewport, scale_factor); + //if (viewport == GetMainViewport()) + // g.PlatformInterface.SetWindowSize(viewport, viewport->Size * scale_factor); + + // Scale our window moving pivot so that the window will rescale roughly around the mouse position. + // FIXME-VIEWPORT: This currently creates a resizing feedback loop when a window is straddling a DPI transition border. + // (Minor: since our sizes do not perfectly linearly scale, deferring the click offset scale until we know the actual window scale ratio may get us slightly more precise mouse positioning.) + //if (g.MovingWindow != NULL && g.MovingWindow->Viewport == viewport) + // g.ActiveIdClickOffset = ImTrunc(g.ActiveIdClickOffset * scale_factor); + } + viewport->DpiScale = new_dpi_scale; + } + + // Update fallback monitor + g.PlatformMonitorsFullWorkRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + if (g.PlatformIO.Monitors.Size == 0) + { + ImGuiPlatformMonitor* monitor = &g.FallbackMonitor; + monitor->MainPos = main_viewport->Pos; + monitor->MainSize = main_viewport->Size; + monitor->WorkPos = main_viewport->WorkPos; + monitor->WorkSize = main_viewport->WorkSize; + monitor->DpiScale = main_viewport->DpiScale; + g.PlatformMonitorsFullWorkRect.Add(monitor->WorkPos); + g.PlatformMonitorsFullWorkRect.Add(monitor->WorkPos + monitor->WorkSize); + } + else + { + g.FallbackMonitor = g.PlatformIO.Monitors[0]; + } + for (ImGuiPlatformMonitor& monitor : g.PlatformIO.Monitors) + { + g.PlatformMonitorsFullWorkRect.Add(monitor.WorkPos); + g.PlatformMonitorsFullWorkRect.Add(monitor.WorkPos + monitor.WorkSize); + } + + if (!viewports_enabled) + { + g.MouseViewport = main_viewport; + return; + } + + // Mouse handling: decide on the actual mouse viewport for this frame between the active/focused viewport and the hovered viewport. + // Note that 'viewport_hovered' should skip over any viewport that has the ImGuiViewportFlags_NoInputs flags set. + ImGuiViewportP* viewport_hovered = NULL; + if (g.IO.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport) + { + viewport_hovered = g.IO.MouseHoveredViewport ? (ImGuiViewportP*)FindViewportByID(g.IO.MouseHoveredViewport) : NULL; + if (viewport_hovered && (viewport_hovered->Flags & ImGuiViewportFlags_NoInputs)) + viewport_hovered = FindHoveredViewportFromPlatformWindowStack(g.IO.MousePos); // Backend failed to handle _NoInputs viewport: revert to our fallback. + } + else + { + // If the backend doesn't know how to honor ImGuiViewportFlags_NoInputs, we do a search ourselves. Note that this search: + // A) won't take account of the possibility that non-imgui windows may be in-between our dragged window and our target window. + // B) won't take account of how the backend apply parent<>child relationship to secondary viewports, which affects their Z order. + // C) uses LastFrameAsRefViewport as a flawed replacement for the last time a window was focused (we could/should fix that by introducing Focus functions in PlatformIO) + viewport_hovered = FindHoveredViewportFromPlatformWindowStack(g.IO.MousePos); + } + if (viewport_hovered != NULL) + g.MouseLastHoveredViewport = viewport_hovered; + else if (g.MouseLastHoveredViewport == NULL) + g.MouseLastHoveredViewport = g.Viewports[0]; + + // Update mouse reference viewport + // (when moving a window we aim at its viewport, but this will be overwritten below if we go in drag and drop mode) + // (MovingViewport->Viewport will be NULL in the rare situation where the window disappared while moving, set UpdateMouseMovingWindowNewFrame() for details) + if (g.MovingWindow && g.MovingWindow->Viewport) + g.MouseViewport = g.MovingWindow->Viewport; + else + g.MouseViewport = g.MouseLastHoveredViewport; + + // When dragging something, always refer to the last hovered viewport. + // - when releasing a moving window we will revert to aiming behind (at viewport_hovered) + // - when we are between viewports, our dragged preview will tend to show in the last viewport _even_ if we don't have tooltips in their viewports (when lacking monitor info) + // - consider the case of holding on a menu item to browse child menus: even thou a mouse button is held, there's no active id because menu items only react on mouse release. + // FIXME-VIEWPORT: This is essentially broken, when ImGuiBackendFlags_HasMouseHoveredViewport is set we want to trust when viewport_hovered==NULL and use that. + const bool is_mouse_dragging_with_an_expected_destination = g.DragDropActive; + if (is_mouse_dragging_with_an_expected_destination && viewport_hovered == NULL) + viewport_hovered = g.MouseLastHoveredViewport; + if (is_mouse_dragging_with_an_expected_destination || g.ActiveId == 0 || !IsAnyMouseDown()) + if (viewport_hovered != NULL && viewport_hovered != g.MouseViewport && !(viewport_hovered->Flags & ImGuiViewportFlags_NoInputs)) + g.MouseViewport = viewport_hovered; + + IM_ASSERT(g.MouseViewport != NULL); +} + +// Update user-facing viewport list (g.Viewports -> g.PlatformIO.Viewports after filtering out some) +static void ImGui::UpdateViewportsEndFrame() +{ + ImGuiContext& g = *GImGui; + g.PlatformIO.Viewports.resize(0); + for (int i = 0; i < g.Viewports.Size; i++) + { + ImGuiViewportP* viewport = g.Viewports[i]; + viewport->LastPos = viewport->Pos; + viewport->LastSize = viewport->Size; + if (viewport->LastFrameActive < g.FrameCount || viewport->Size.x <= 0.0f || viewport->Size.y <= 0.0f) + if (i > 0) // Always include main viewport in the list + continue; + if (viewport->Window && !IsWindowActiveAndVisible(viewport->Window)) + continue; + if (i > 0) + IM_ASSERT(viewport->Window != NULL); + g.PlatformIO.Viewports.push_back(viewport); + } + g.Viewports[0]->ClearRequestFlags(); // Clear main viewport flags because UpdatePlatformWindows() won't do it and may not even be called +} + +// FIXME: We should ideally refactor the system to call this every frame (we currently don't) +ImGuiViewportP* ImGui::AddUpdateViewport(ImGuiWindow* window, ImGuiID id, const ImVec2& pos, const ImVec2& size, ImGuiViewportFlags flags) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(id != 0); + + flags |= ImGuiViewportFlags_IsPlatformWindow; + if (window != NULL) + { + if (g.MovingWindow && g.MovingWindow->RootWindowDockTree == window) + flags |= ImGuiViewportFlags_NoInputs | ImGuiViewportFlags_NoFocusOnAppearing; + if ((window->Flags & ImGuiWindowFlags_NoMouseInputs) && (window->Flags & ImGuiWindowFlags_NoNavInputs)) + flags |= ImGuiViewportFlags_NoInputs; + if (window->Flags & ImGuiWindowFlags_NoFocusOnAppearing) + flags |= ImGuiViewportFlags_NoFocusOnAppearing; + } + + ImGuiViewportP* viewport = (ImGuiViewportP*)FindViewportByID(id); + if (viewport) + { + // Always update for main viewport as we are already pulling correct platform pos/size (see #4900) + ImVec2 prev_pos = viewport->Pos; + ImVec2 prev_size = viewport->Size; + if (!viewport->PlatformRequestMove || viewport->ID == IMGUI_VIEWPORT_DEFAULT_ID) + viewport->Pos = pos; + if (!viewport->PlatformRequestResize || viewport->ID == IMGUI_VIEWPORT_DEFAULT_ID) + viewport->Size = size; + viewport->Flags = flags | (viewport->Flags & (ImGuiViewportFlags_IsMinimized | ImGuiViewportFlags_IsFocused)); // Preserve existing flags + if (prev_pos != viewport->Pos || prev_size != viewport->Size) + UpdateViewportPlatformMonitor(viewport); + } + else + { + // New viewport + viewport = IM_NEW(ImGuiViewportP)(); + viewport->ID = id; + viewport->Idx = g.Viewports.Size; + viewport->Pos = viewport->LastPos = pos; + viewport->Size = viewport->LastSize = size; + viewport->Flags = flags; + UpdateViewportPlatformMonitor(viewport); + g.Viewports.push_back(viewport); + g.ViewportCreatedCount++; + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Add Viewport %08X '%s'\n", id, window ? window->Name : ""); + + // We normally setup for all viewports in NewFrame() but here need to handle the mid-frame creation of a new viewport. + // We need to extend the fullscreen clip rect so the OverlayDrawList clip is correct for that the first frame + g.DrawListSharedData.ClipRectFullscreen.x = ImMin(g.DrawListSharedData.ClipRectFullscreen.x, viewport->Pos.x); + g.DrawListSharedData.ClipRectFullscreen.y = ImMin(g.DrawListSharedData.ClipRectFullscreen.y, viewport->Pos.y); + g.DrawListSharedData.ClipRectFullscreen.z = ImMax(g.DrawListSharedData.ClipRectFullscreen.z, viewport->Pos.x + viewport->Size.x); + g.DrawListSharedData.ClipRectFullscreen.w = ImMax(g.DrawListSharedData.ClipRectFullscreen.w, viewport->Pos.y + viewport->Size.y); + + // Store initial DpiScale before the OS platform window creation, based on expected monitor data. + // This is so we can select an appropriate font size on the first frame of our window lifetime + if (viewport->PlatformMonitor != -1) + viewport->DpiScale = g.PlatformIO.Monitors[viewport->PlatformMonitor].DpiScale; + else + viewport->DpiScale = 1.0f; + } + + viewport->Window = window; + viewport->LastFrameActive = g.FrameCount; + viewport->UpdateWorkRect(); + IM_ASSERT(window == NULL || viewport->ID == window->ID); + + if (window != NULL) + window->ViewportOwned = true; + + return viewport; +} + +static void ImGui::DestroyViewport(ImGuiViewportP* viewport) +{ + // Clear references to this viewport in windows (window->ViewportId becomes the master data) + ImGuiContext& g = *GImGui; + for (ImGuiWindow* window : g.Windows) + { + if (window->Viewport != viewport) + continue; + window->Viewport = NULL; + window->ViewportOwned = false; + } + if (viewport == g.MouseLastHoveredViewport) + g.MouseLastHoveredViewport = NULL; + + // Destroy + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Delete Viewport %08X '%s'\n", viewport->ID, viewport->Window ? viewport->Window->Name : "n/a"); + DestroyPlatformWindow(viewport); // In most circumstances the platform window will already be destroyed here. + IM_ASSERT(g.PlatformIO.Viewports.contains(viewport) == false); + IM_ASSERT(g.Viewports[viewport->Idx] == viewport); + g.Viewports.erase(g.Viewports.Data + viewport->Idx); + IM_DELETE(viewport); +} + +// FIXME-VIEWPORT: This is all super messy and ought to be clarified or rewritten. +static void ImGui::WindowSelectViewport(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiWindowFlags flags = window->Flags; + window->ViewportAllowPlatformMonitorExtend = -1; + + // Restore main viewport if multi-viewport is not supported by the backend + ImGuiViewportP* main_viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + if (!(g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable)) + { + SetWindowViewport(window, main_viewport); + return; + } + window->ViewportOwned = false; + + // Appearing popups reset their viewport so they can inherit again + if ((flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && window->Appearing) + { + window->Viewport = NULL; + window->ViewportId = 0; + } + + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasViewport) == 0) + { + // By default inherit from parent window + if (window->Viewport == NULL && window->ParentWindow && (!window->ParentWindow->IsFallbackWindow || window->ParentWindow->WasActive)) + window->Viewport = window->ParentWindow->Viewport; + + // Attempt to restore saved viewport id (= window that hasn't been activated yet), try to restore the viewport based on saved 'window->ViewportPos' restored from .ini file + if (window->Viewport == NULL && window->ViewportId != 0) + { + window->Viewport = (ImGuiViewportP*)FindViewportByID(window->ViewportId); + if (window->Viewport == NULL && window->ViewportPos.x != FLT_MAX && window->ViewportPos.y != FLT_MAX) + window->Viewport = AddUpdateViewport(window, window->ID, window->ViewportPos, window->Size, ImGuiViewportFlags_None); + } + } + + bool lock_viewport = false; + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasViewport) + { + // Code explicitly request a viewport + window->Viewport = (ImGuiViewportP*)FindViewportByID(g.NextWindowData.ViewportId); + window->ViewportId = g.NextWindowData.ViewportId; // Store ID even if Viewport isn't resolved yet. + if (window->Viewport && (window->Flags & ImGuiWindowFlags_DockNodeHost) != 0 && window->Viewport->Window != NULL) + { + window->Viewport->Window = window; + window->Viewport->ID = window->ViewportId = window->ID; // Overwrite ID (always owned by node) + } + lock_viewport = true; + } + else if ((flags & ImGuiWindowFlags_ChildWindow) || (flags & ImGuiWindowFlags_ChildMenu)) + { + // Always inherit viewport from parent window + if (window->DockNode && window->DockNode->HostWindow) + IM_ASSERT(window->DockNode->HostWindow->Viewport == window->ParentWindow->Viewport); + window->Viewport = window->ParentWindow->Viewport; + } + else if (window->DockNode && window->DockNode->HostWindow) + { + // This covers the "always inherit viewport from parent window" case for when a window reattach to a node that was just created mid-frame + window->Viewport = window->DockNode->HostWindow->Viewport; + } + else if (flags & ImGuiWindowFlags_Tooltip) + { + window->Viewport = g.MouseViewport; + } + else if (GetWindowAlwaysWantOwnViewport(window)) + { + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_None); + } + else if (g.MovingWindow && g.MovingWindow->RootWindowDockTree == window && IsMousePosValid()) + { + if (window->Viewport != NULL && window->Viewport->Window == window) + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_None); + } + else + { + // Merge into host viewport? + // We cannot test window->ViewportOwned as it set lower in the function. + // Testing (g.ActiveId == 0 || g.ActiveIdAllowOverlap) to avoid merging during a short-term widget interaction. Main intent was to avoid during resize (see #4212) + bool try_to_merge_into_host_viewport = (window->Viewport && window == window->Viewport->Window && (g.ActiveId == 0 || g.ActiveIdAllowOverlap)); + if (try_to_merge_into_host_viewport) + UpdateTryMergeWindowIntoHostViewports(window); + } + + // Fallback: merge in default viewport if z-order matches, otherwise create a new viewport + if (window->Viewport == NULL) + if (!UpdateTryMergeWindowIntoHostViewport(window, main_viewport)) + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_None); + + // Mark window as allowed to protrude outside of its viewport and into the current monitor + if (!lock_viewport) + { + if (flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) + { + // We need to take account of the possibility that mouse may become invalid. + // Popups/Tooltip always set ViewportAllowPlatformMonitorExtend so GetWindowAllowedExtentRect() will return full monitor bounds. + ImVec2 mouse_ref = (flags & ImGuiWindowFlags_Tooltip) ? g.IO.MousePos : g.BeginPopupStack.back().OpenMousePos; + bool use_mouse_ref = (!g.NavCursorVisible || !g.NavHighlightItemUnderNav || !g.NavWindow); + bool mouse_valid = IsMousePosValid(&mouse_ref); + if ((window->Appearing || (flags & (ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_ChildMenu))) && (!use_mouse_ref || mouse_valid)) + window->ViewportAllowPlatformMonitorExtend = FindPlatformMonitorForPos((use_mouse_ref && mouse_valid) ? mouse_ref : NavCalcPreferredRefPos()); + else + window->ViewportAllowPlatformMonitorExtend = window->Viewport->PlatformMonitor; + } + else if (window->Viewport && window != window->Viewport->Window && window->Viewport->Window && !(flags & ImGuiWindowFlags_ChildWindow) && window->DockNode == NULL) + { + // When called from Begin() we don't have access to a proper version of the Hidden flag yet, so we replicate this code. + const bool will_be_visible = (window->DockIsActive && !window->DockTabIsVisible) ? false : true; + if ((window->Flags & ImGuiWindowFlags_DockNodeHost) && window->Viewport->LastFrameActive < g.FrameCount && will_be_visible) + { + // Steal/transfer ownership + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Window '%s' steal Viewport %08X from Window '%s'\n", window->Name, window->Viewport->ID, window->Viewport->Window->Name); + window->Viewport->Window = window; + window->Viewport->ID = window->ID; + window->Viewport->LastNameHash = 0; + } + else if (!UpdateTryMergeWindowIntoHostViewports(window)) // Merge? + { + // New viewport + window->Viewport = AddUpdateViewport(window, window->ID, window->Pos, window->Size, ImGuiViewportFlags_NoFocusOnAppearing); + } + } + else if (window->ViewportAllowPlatformMonitorExtend < 0 && (flags & ImGuiWindowFlags_ChildWindow) == 0) + { + // Regular (non-child, non-popup) windows by default are also allowed to protrude + // Child windows are kept contained within their parent. + window->ViewportAllowPlatformMonitorExtend = window->Viewport->PlatformMonitor; + } + } + + // Update flags + window->ViewportOwned = (window == window->Viewport->Window); + window->ViewportId = window->Viewport->ID; + + // If the OS window has a title bar, hide our imgui title bar + //if (window->ViewportOwned && !(window->Viewport->Flags & ImGuiViewportFlags_NoDecoration)) + // window->Flags |= ImGuiWindowFlags_NoTitleBar; +} + +void ImGui::WindowSyncOwnedViewport(ImGuiWindow* window, ImGuiWindow* parent_window_in_stack) +{ + ImGuiContext& g = *GImGui; + + bool viewport_rect_changed = false; + + // Synchronize window --> viewport in most situations + // Synchronize viewport -> window in case the platform window has been moved or resized from the OS/WM + if (window->Viewport->PlatformRequestMove) + { + window->Pos = window->Viewport->Pos; + MarkIniSettingsDirty(window); + } + else if (memcmp(&window->Viewport->Pos, &window->Pos, sizeof(window->Pos)) != 0) + { + viewport_rect_changed = true; + window->Viewport->Pos = window->Pos; + } + + if (window->Viewport->PlatformRequestResize) + { + window->Size = window->SizeFull = window->Viewport->Size; + MarkIniSettingsDirty(window); + } + else if (memcmp(&window->Viewport->Size, &window->Size, sizeof(window->Size)) != 0) + { + viewport_rect_changed = true; + window->Viewport->Size = window->Size; + } + window->Viewport->UpdateWorkRect(); + + // The viewport may have changed monitor since the global update in UpdateViewportsNewFrame() + // Either a SetNextWindowPos() call in the current frame or a SetWindowPos() call in the previous frame may have this effect. + if (viewport_rect_changed) + UpdateViewportPlatformMonitor(window->Viewport); + + // Update common viewport flags + const ImGuiViewportFlags viewport_flags_to_clear = ImGuiViewportFlags_TopMost | ImGuiViewportFlags_NoTaskBarIcon | ImGuiViewportFlags_NoDecoration | ImGuiViewportFlags_NoRendererClear; + ImGuiViewportFlags viewport_flags = window->Viewport->Flags & ~viewport_flags_to_clear; + ImGuiWindowFlags window_flags = window->Flags; + const bool is_modal = (window_flags & ImGuiWindowFlags_Modal) != 0; + const bool is_short_lived_floating_window = (window_flags & (ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_Tooltip | ImGuiWindowFlags_Popup)) != 0; + if (window_flags & ImGuiWindowFlags_Tooltip) + viewport_flags |= ImGuiViewportFlags_TopMost; + if ((g.IO.ConfigViewportsNoTaskBarIcon || is_short_lived_floating_window) && !is_modal) + viewport_flags |= ImGuiViewportFlags_NoTaskBarIcon; + if (g.IO.ConfigViewportsNoDecoration || is_short_lived_floating_window) + viewport_flags |= ImGuiViewportFlags_NoDecoration; + + // Not correct to set modal as topmost because: + // - Because other popups can be stacked above a modal (e.g. combo box in a modal) + // - ImGuiViewportFlags_TopMost is currently handled different in backends: in Win32 it is "appear top most" whereas in GLFW and SDL it is "stay topmost" + //if (flags & ImGuiWindowFlags_Modal) + // viewport_flags |= ImGuiViewportFlags_TopMost; + + // For popups and menus that may be protruding out of their parent viewport, we enable _NoFocusOnClick so that clicking on them + // won't steal the OS focus away from their parent window (which may be reflected in OS the title bar decoration). + // Setting _NoFocusOnClick would technically prevent us from bringing back to front in case they are being covered by an OS window from a different app, + // but it shouldn't be much of a problem considering those are already popups that are closed when clicking elsewhere. + if (is_short_lived_floating_window && !is_modal) + viewport_flags |= ImGuiViewportFlags_NoFocusOnAppearing | ImGuiViewportFlags_NoFocusOnClick; + + // We can overwrite viewport flags using ImGuiWindowClass (advanced users) + if (window->WindowClass.ViewportFlagsOverrideSet) + viewport_flags |= window->WindowClass.ViewportFlagsOverrideSet; + if (window->WindowClass.ViewportFlagsOverrideClear) + viewport_flags &= ~window->WindowClass.ViewportFlagsOverrideClear; + + // We can also tell the backend that clearing the platform window won't be necessary, + // as our window background is filling the viewport and we have disabled BgAlpha. + // FIXME: Work on support for per-viewport transparency (#2766) + if (!(window_flags & ImGuiWindowFlags_NoBackground)) + viewport_flags |= ImGuiViewportFlags_NoRendererClear; + + window->Viewport->Flags = viewport_flags; + + // Update parent viewport ID + // (the !IsFallbackWindow test mimic the one done in WindowSelectViewport()) + if (window->WindowClass.ParentViewportId != (ImGuiID)-1) + window->Viewport->ParentViewportId = window->WindowClass.ParentViewportId; + else if ((window_flags & (ImGuiWindowFlags_Popup | ImGuiWindowFlags_Tooltip)) && parent_window_in_stack && (!parent_window_in_stack->IsFallbackWindow || parent_window_in_stack->WasActive)) + window->Viewport->ParentViewportId = parent_window_in_stack->Viewport->ID; + else + window->Viewport->ParentViewportId = g.IO.ConfigViewportsNoDefaultParent ? 0 : IMGUI_VIEWPORT_DEFAULT_ID; +} + +// Called by user at the end of the main loop, after EndFrame() +// This will handle the creation/update of all OS windows via function defined in the ImGuiPlatformIO api. +void ImGui::UpdatePlatformWindows() +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.FrameCountEnded == g.FrameCount && "Forgot to call Render() or EndFrame() before UpdatePlatformWindows()?"); + IM_ASSERT(g.FrameCountPlatformEnded < g.FrameCount); + g.FrameCountPlatformEnded = g.FrameCount; + if (!(g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable)) + return; + + // Create/resize/destroy platform windows to match each active viewport. + // Skip the main viewport (index 0), which is always fully handled by the application! + for (int i = 1; i < g.Viewports.Size; i++) + { + ImGuiViewportP* viewport = g.Viewports[i]; + + // Destroy platform window if the viewport hasn't been submitted or if it is hosting a hidden window + // (the implicit/fallback Debug##Default window will be registering its viewport then be disabled, causing a dummy DestroyPlatformWindow to be made each frame) + bool destroy_platform_window = false; + destroy_platform_window |= (viewport->LastFrameActive < g.FrameCount - 1); + destroy_platform_window |= (viewport->Window && !IsWindowActiveAndVisible(viewport->Window)); + if (destroy_platform_window) + { + DestroyPlatformWindow(viewport); + continue; + } + + // New windows that appears directly in a new viewport won't always have a size on their first frame + if (viewport->LastFrameActive < g.FrameCount || viewport->Size.x <= 0 || viewport->Size.y <= 0) + continue; + + // Create window + const bool is_new_platform_window = (viewport->PlatformWindowCreated == false); + if (is_new_platform_window) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Create Platform Window %08X '%s'\n", viewport->ID, viewport->Window ? viewport->Window->Name : "n/a"); + g.PlatformIO.Platform_CreateWindow(viewport); + if (g.PlatformIO.Renderer_CreateWindow != NULL) + g.PlatformIO.Renderer_CreateWindow(viewport); + g.PlatformWindowsCreatedCount++; + viewport->LastNameHash = 0; + viewport->LastPlatformPos = viewport->LastPlatformSize = ImVec2(FLT_MAX, FLT_MAX); // By clearing those we'll enforce a call to Platform_SetWindowPos/Size below, before Platform_ShowWindow (FIXME: Is that necessary?) + viewport->LastRendererSize = viewport->Size; // We don't need to call Renderer_SetWindowSize() as it is expected Renderer_CreateWindow() already did it. + viewport->PlatformWindowCreated = true; + } + + // Apply Position and Size (from ImGui to Platform/Renderer backends) + if ((viewport->LastPlatformPos.x != viewport->Pos.x || viewport->LastPlatformPos.y != viewport->Pos.y) && !viewport->PlatformRequestMove) + g.PlatformIO.Platform_SetWindowPos(viewport, viewport->Pos); + if ((viewport->LastPlatformSize.x != viewport->Size.x || viewport->LastPlatformSize.y != viewport->Size.y) && !viewport->PlatformRequestResize) + g.PlatformIO.Platform_SetWindowSize(viewport, viewport->Size); + if ((viewport->LastRendererSize.x != viewport->Size.x || viewport->LastRendererSize.y != viewport->Size.y) && g.PlatformIO.Renderer_SetWindowSize) + g.PlatformIO.Renderer_SetWindowSize(viewport, viewport->Size); + viewport->LastPlatformPos = viewport->Pos; + viewport->LastPlatformSize = viewport->LastRendererSize = viewport->Size; + + // Update title bar (if it changed) + if (ImGuiWindow* window_for_title = GetWindowForTitleDisplay(viewport->Window)) + { + const char* title_begin = window_for_title->Name; + char* title_end = (char*)(intptr_t)FindRenderedTextEnd(title_begin); + const ImGuiID title_hash = ImHashStr(title_begin, title_end - title_begin); + if (viewport->LastNameHash != title_hash) + { + char title_end_backup_c = *title_end; + *title_end = 0; // Cut existing buffer short instead of doing an alloc/free, no small gain. + g.PlatformIO.Platform_SetWindowTitle(viewport, title_begin); + *title_end = title_end_backup_c; + viewport->LastNameHash = title_hash; + } + } + + // Update alpha (if it changed) + if (viewport->LastAlpha != viewport->Alpha && g.PlatformIO.Platform_SetWindowAlpha) + g.PlatformIO.Platform_SetWindowAlpha(viewport, viewport->Alpha); + viewport->LastAlpha = viewport->Alpha; + + // Optional, general purpose call to allow the backend to perform general book-keeping even if things haven't changed. + if (g.PlatformIO.Platform_UpdateWindow) + g.PlatformIO.Platform_UpdateWindow(viewport); + + if (is_new_platform_window) + { + // On startup ensure new platform window don't steal focus (give it a few frames, as nested contents may lead to viewport being created a few frames late) + if (g.FrameCount < 3) + viewport->Flags |= ImGuiViewportFlags_NoFocusOnAppearing; + + // Show window + g.PlatformIO.Platform_ShowWindow(viewport); + + // Even without focus, we assume the window becomes front-most. + // This is useful for our platform z-order heuristic when io.MouseHoveredViewport is not available. + if (viewport->LastFocusedStampCount != g.ViewportFocusedStampCount) + viewport->LastFocusedStampCount = ++g.ViewportFocusedStampCount; + } + + // Clear request flags + viewport->ClearRequestFlags(); + } +} + +// This is a default/basic function for performing the rendering/swap of multiple Platform Windows. +// Custom renderers may prefer to not call this function at all, and instead iterate the publicly exposed platform data and handle rendering/sync themselves. +// The Render/Swap functions stored in ImGuiPlatformIO are merely here to allow for this helper to exist, but you can do it yourself: +// +// ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); +// for (int i = 1; i < platform_io.Viewports.Size; i++) +// if ((platform_io.Viewports[i]->Flags & ImGuiViewportFlags_Minimized) == 0) +// MyRenderFunction(platform_io.Viewports[i], my_args); +// for (int i = 1; i < platform_io.Viewports.Size; i++) +// if ((platform_io.Viewports[i]->Flags & ImGuiViewportFlags_Minimized) == 0) +// MySwapBufferFunction(platform_io.Viewports[i], my_args); +// +void ImGui::RenderPlatformWindowsDefault(void* platform_render_arg, void* renderer_render_arg) +{ + // Skip the main viewport (index 0), which is always fully handled by the application! + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + for (int i = 1; i < platform_io.Viewports.Size; i++) + { + ImGuiViewport* viewport = platform_io.Viewports[i]; + if (viewport->Flags & ImGuiViewportFlags_IsMinimized) + continue; + if (platform_io.Platform_RenderWindow) platform_io.Platform_RenderWindow(viewport, platform_render_arg); + if (platform_io.Renderer_RenderWindow) platform_io.Renderer_RenderWindow(viewport, renderer_render_arg); + } + for (int i = 1; i < platform_io.Viewports.Size; i++) + { + ImGuiViewport* viewport = platform_io.Viewports[i]; + if (viewport->Flags & ImGuiViewportFlags_IsMinimized) + continue; + if (platform_io.Platform_SwapBuffers) platform_io.Platform_SwapBuffers(viewport, platform_render_arg); + if (platform_io.Renderer_SwapBuffers) platform_io.Renderer_SwapBuffers(viewport, renderer_render_arg); + } +} + +static int ImGui::FindPlatformMonitorForPos(const ImVec2& pos) +{ + ImGuiContext& g = *GImGui; + for (int monitor_n = 0; monitor_n < g.PlatformIO.Monitors.Size; monitor_n++) + { + const ImGuiPlatformMonitor& monitor = g.PlatformIO.Monitors[monitor_n]; + if (ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize).Contains(pos)) + return monitor_n; + } + return -1; +} + +// Search for the monitor with the largest intersection area with the given rectangle +// We generally try to avoid searching loops but the monitor count should be very small here +// FIXME-OPT: We could test the last monitor used for that viewport first, and early +static int ImGui::FindPlatformMonitorForRect(const ImRect& rect) +{ + ImGuiContext& g = *GImGui; + + const int monitor_count = g.PlatformIO.Monitors.Size; + if (monitor_count <= 1) + return monitor_count - 1; + + // Use a minimum threshold of 1.0f so a zero-sized rect won't false positive, and will still find the correct monitor given its position. + // This is necessary for tooltips which always resize down to zero at first. + const float surface_threshold = ImMax(rect.GetWidth() * rect.GetHeight() * 0.5f, 1.0f); + int best_monitor_n = 0; // Default to the first monitor as fallback + float best_monitor_surface = 0.001f; + + for (int monitor_n = 0; monitor_n < g.PlatformIO.Monitors.Size && best_monitor_surface < surface_threshold; monitor_n++) + { + const ImGuiPlatformMonitor& monitor = g.PlatformIO.Monitors[monitor_n]; + const ImRect monitor_rect = ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize); + if (monitor_rect.Contains(rect)) + return monitor_n; + ImRect overlapping_rect = rect; + overlapping_rect.ClipWithFull(monitor_rect); + float overlapping_surface = overlapping_rect.GetWidth() * overlapping_rect.GetHeight(); + if (overlapping_surface < best_monitor_surface) + continue; + best_monitor_surface = overlapping_surface; + best_monitor_n = monitor_n; + } + return best_monitor_n; +} + +// Update monitor from viewport rectangle (we'll use this info to clamp windows and save windows lost in a removed monitor) +static void ImGui::UpdateViewportPlatformMonitor(ImGuiViewportP* viewport) +{ + viewport->PlatformMonitor = (short)FindPlatformMonitorForRect(viewport->GetMainRect()); +} + +// Return value is always != NULL, but don't hold on it across frames. +const ImGuiPlatformMonitor* ImGui::GetViewportPlatformMonitor(ImGuiViewport* viewport_p) +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)viewport_p; + int monitor_idx = viewport->PlatformMonitor; + if (monitor_idx >= 0 && monitor_idx < g.PlatformIO.Monitors.Size) + return &g.PlatformIO.Monitors[monitor_idx]; + return &g.FallbackMonitor; +} + +void ImGui::DestroyPlatformWindow(ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + if (viewport->PlatformWindowCreated) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Destroy Platform Window %08X '%s'\n", viewport->ID, viewport->Window ? viewport->Window->Name : "n/a"); + if (g.PlatformIO.Renderer_DestroyWindow) + g.PlatformIO.Renderer_DestroyWindow(viewport); + if (g.PlatformIO.Platform_DestroyWindow) + g.PlatformIO.Platform_DestroyWindow(viewport); + IM_ASSERT(viewport->RendererUserData == NULL && viewport->PlatformUserData == NULL); + + // Don't clear PlatformWindowCreated for the main viewport, as we initially set that up to true in Initialize() + // The righter way may be to leave it to the backend to set this flag all-together, and made the flag public. + if (viewport->ID != IMGUI_VIEWPORT_DEFAULT_ID) + viewport->PlatformWindowCreated = false; + } + else + { + IM_ASSERT(viewport->RendererUserData == NULL && viewport->PlatformUserData == NULL && viewport->PlatformHandle == NULL); + } + viewport->RendererUserData = viewport->PlatformUserData = viewport->PlatformHandle = NULL; + viewport->ClearRequestFlags(); +} + +void ImGui::DestroyPlatformWindows() +{ + // We call the destroy window on every viewport (including the main viewport, index 0) to give a chance to the backend + // to clear any data they may have stored in e.g. PlatformUserData, RendererUserData. + // It is convenient for the platform backend code to store something in the main viewport, in order for e.g. the mouse handling + // code to operator a consistent manner. + // It is expected that the backend can handle calls to Renderer_DestroyWindow/Platform_DestroyWindow without + // crashing if it doesn't have data stored. + ImGuiContext& g = *GImGui; + for (ImGuiViewportP* viewport : g.Viewports) + DestroyPlatformWindow(viewport); +} //----------------------------------------------------------------------------- // [SECTION] DOCKING //----------------------------------------------------------------------------- +// Docking: Internal Types +// Docking: Forward Declarations +// Docking: ImGuiDockContext +// Docking: ImGuiDockContext Docking/Undocking functions +// Docking: ImGuiDockNode +// Docking: ImGuiDockNode Tree manipulation functions +// Docking: Public Functions (SetWindowDock, DockSpace, DockSpaceOverViewport) +// Docking: Builder Functions +// Docking: Begin/End Support Functions (called from Begin/End) +// Docking: Settings +//----------------------------------------------------------------------------- -// (this section is filled in the 'docking' branch) +//----------------------------------------------------------------------------- +// Typical Docking call flow: (root level is generally public API): +//----------------------------------------------------------------------------- +// - NewFrame() new dear imgui frame +// | DockContextNewFrameUpdateUndocking() - process queued undocking requests +// | - DockContextProcessUndockWindow() - process one window undocking request +// | - DockContextProcessUndockNode() - process one whole node undocking request +// | DockContextNewFrameUpdateUndocking() - process queue docking requests, create floating dock nodes +// | - update g.HoveredDockNode - [debug] update node hovered by mouse +// | - DockContextProcessDock() - process one docking request +// | - DockNodeUpdate() +// | - DockNodeUpdateForRootNode() +// | - DockNodeUpdateFlagsAndCollapse() +// | - DockNodeFindInfo() +// | - destroy unused node or tab bar +// | - create dock node host window +// | - Begin() etc. +// | - DockNodeStartMouseMovingWindow() +// | - DockNodeTreeUpdatePosSize() +// | - DockNodeTreeUpdateSplitter() +// | - draw node background +// | - DockNodeUpdateTabBar() - create/update tab bar for a docking node +// | - DockNodeAddTabBar() +// | - DockNodeWindowMenuUpdate() +// | - DockNodeCalcTabBarLayout() +// | - BeginTabBarEx() +// | - TabItemEx() calls +// | - EndTabBar() +// | - BeginDockableDragDropTarget() +// | - DockNodeUpdate() - recurse into child nodes... +//----------------------------------------------------------------------------- +// - DockSpace() user submit a dockspace into a window +// | Begin(Child) - create a child window +// | DockNodeUpdate() - call main dock node update function +// | End(Child) +// | ItemSize() +//----------------------------------------------------------------------------- +// - Begin() +// | BeginDocked() +// | BeginDockableDragDropSource() +// | BeginDockableDragDropTarget() +// | - DockNodePreviewDockRender() +//----------------------------------------------------------------------------- +// - EndFrame() +// | DockContextEndFrame() +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Docking: Internal Types +//----------------------------------------------------------------------------- +// - ImGuiDockRequestType +// - ImGuiDockRequest +// - ImGuiDockPreviewData +// - ImGuiDockNodeSettings +// - ImGuiDockContext +//----------------------------------------------------------------------------- + +enum ImGuiDockRequestType +{ + ImGuiDockRequestType_None = 0, + ImGuiDockRequestType_Dock, + ImGuiDockRequestType_Undock, + ImGuiDockRequestType_Split // Split is the same as Dock but without a DockPayload +}; + +struct ImGuiDockRequest +{ + ImGuiDockRequestType Type; + ImGuiWindow* DockTargetWindow; // Destination/Target Window to dock into (may be a loose window or a DockNode, might be NULL in which case DockTargetNode cannot be NULL) + ImGuiDockNode* DockTargetNode; // Destination/Target Node to dock into + ImGuiWindow* DockPayload; // Source/Payload window to dock (may be a loose window or a DockNode), [Optional] + ImGuiDir DockSplitDir; + float DockSplitRatio; + bool DockSplitOuter; + ImGuiWindow* UndockTargetWindow; + ImGuiDockNode* UndockTargetNode; + + ImGuiDockRequest() + { + Type = ImGuiDockRequestType_None; + DockTargetWindow = DockPayload = UndockTargetWindow = NULL; + DockTargetNode = UndockTargetNode = NULL; + DockSplitDir = ImGuiDir_None; + DockSplitRatio = 0.5f; + DockSplitOuter = false; + } +}; + +struct ImGuiDockPreviewData +{ + ImGuiDockNode FutureNode; + bool IsDropAllowed; + bool IsCenterAvailable; + bool IsSidesAvailable; // Hold your breath, grammar freaks.. + bool IsSplitDirExplicit; // Set when hovered the drop rect (vs. implicit SplitDir==None when hovered the window) + ImGuiDockNode* SplitNode; + ImGuiDir SplitDir; + float SplitRatio; + ImRect DropRectsDraw[ImGuiDir_COUNT + 1]; // May be slightly different from hit-testing drop rects used in DockNodeCalcDropRects() + + ImGuiDockPreviewData() : FutureNode(0) { IsDropAllowed = IsCenterAvailable = IsSidesAvailable = IsSplitDirExplicit = false; SplitNode = NULL; SplitDir = ImGuiDir_None; SplitRatio = 0.f; for (int n = 0; n < IM_ARRAYSIZE(DropRectsDraw); n++) DropRectsDraw[n] = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); } +}; + +// Persistent Settings data, stored contiguously in SettingsNodes (sizeof() ~32 bytes) +struct ImGuiDockNodeSettings +{ + ImGuiID ID; + ImGuiID ParentNodeId; + ImGuiID ParentWindowId; + ImGuiID SelectedTabId; + signed char SplitAxis; + char Depth; + ImGuiDockNodeFlags Flags; // NB: We save individual flags one by one in ascii format (ImGuiDockNodeFlags_SavedFlagsMask_) + ImVec2ih Pos; + ImVec2ih Size; + ImVec2ih SizeRef; + ImGuiDockNodeSettings() { memset(this, 0, sizeof(*this)); SplitAxis = ImGuiAxis_None; } +}; + +//----------------------------------------------------------------------------- +// Docking: Forward Declarations +//----------------------------------------------------------------------------- + +namespace ImGui +{ + // ImGuiDockContext + static ImGuiDockNode* DockContextAddNode(ImGuiContext* ctx, ImGuiID id); + static void DockContextRemoveNode(ImGuiContext* ctx, ImGuiDockNode* node, bool merge_sibling_into_parent_node); + static void DockContextQueueNotifyRemovedNode(ImGuiContext* ctx, ImGuiDockNode* node); + static void DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req); + static void DockContextPruneUnusedSettingsNodes(ImGuiContext* ctx); + static ImGuiDockNode* DockContextBindNodeToWindow(ImGuiContext* ctx, ImGuiWindow* window); + static void DockContextBuildNodesFromSettings(ImGuiContext* ctx, ImGuiDockNodeSettings* node_settings_array, int node_settings_count); + static void DockContextBuildAddWindowsToNodes(ImGuiContext* ctx, ImGuiID root_id); // Use root_id==0 to add all + + // ImGuiDockNode + static void DockNodeAddWindow(ImGuiDockNode* node, ImGuiWindow* window, bool add_to_tab_bar); + static void DockNodeMoveWindows(ImGuiDockNode* dst_node, ImGuiDockNode* src_node); + static void DockNodeMoveChildNodes(ImGuiDockNode* dst_node, ImGuiDockNode* src_node); + static ImGuiWindow* DockNodeFindWindowByID(ImGuiDockNode* node, ImGuiID id); + static void DockNodeApplyPosSizeToWindows(ImGuiDockNode* node); + static void DockNodeRemoveWindow(ImGuiDockNode* node, ImGuiWindow* window, ImGuiID save_dock_id); + static void DockNodeHideHostWindow(ImGuiDockNode* node); + static void DockNodeUpdate(ImGuiDockNode* node); + static void DockNodeUpdateForRootNode(ImGuiDockNode* node); + static void DockNodeUpdateFlagsAndCollapse(ImGuiDockNode* node); + static void DockNodeUpdateHasCentralNodeChild(ImGuiDockNode* node); + static void DockNodeUpdateTabBar(ImGuiDockNode* node, ImGuiWindow* host_window); + static void DockNodeAddTabBar(ImGuiDockNode* node); + static void DockNodeRemoveTabBar(ImGuiDockNode* node); + static void DockNodeWindowMenuUpdate(ImGuiDockNode* node, ImGuiTabBar* tab_bar); + static void DockNodeUpdateVisibleFlag(ImGuiDockNode* node); + static void DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window); + static bool DockNodeIsDropAllowed(ImGuiWindow* host_window, ImGuiWindow* payload_window); + static void DockNodePreviewDockSetup(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDockPreviewData* preview_data, bool is_explicit_target, bool is_outer_docking); + static void DockNodePreviewDockRender(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, const ImGuiDockPreviewData* preview_data); + static void DockNodeCalcTabBarLayout(const ImGuiDockNode* node, ImRect* out_title_rect, ImRect* out_tab_bar_rect, ImVec2* out_window_menu_button_pos, ImVec2* out_close_button_pos); + static void DockNodeCalcSplitRects(ImVec2& pos_old, ImVec2& size_old, ImVec2& pos_new, ImVec2& size_new, ImGuiDir dir, ImVec2 size_new_desired); + static bool DockNodeCalcDropRectsAndTestMousePos(const ImRect& parent, ImGuiDir dir, ImRect& out_draw, bool outer_docking, ImVec2* test_mouse_pos); + static const char* DockNodeGetHostWindowTitle(ImGuiDockNode* node, char* buf, int buf_size) { ImFormatString(buf, buf_size, "##DockNode_%02X", node->ID); return buf; } + static int DockNodeGetTabOrder(ImGuiWindow* window); + + // ImGuiDockNode tree manipulations + static void DockNodeTreeSplit(ImGuiContext* ctx, ImGuiDockNode* parent_node, ImGuiAxis split_axis, int split_first_child, float split_ratio, ImGuiDockNode* new_node); + static void DockNodeTreeMerge(ImGuiContext* ctx, ImGuiDockNode* parent_node, ImGuiDockNode* merge_lead_child); + static void DockNodeTreeUpdatePosSize(ImGuiDockNode* node, ImVec2 pos, ImVec2 size, ImGuiDockNode* only_write_to_single_node = NULL); + static void DockNodeTreeUpdateSplitter(ImGuiDockNode* node); + static ImGuiDockNode* DockNodeTreeFindVisibleNodeByPos(ImGuiDockNode* node, ImVec2 pos); + static ImGuiDockNode* DockNodeTreeFindFallbackLeafNode(ImGuiDockNode* node); + + // Settings + static void DockSettingsRenameNodeReferences(ImGuiID old_node_id, ImGuiID new_node_id); + static void DockSettingsRemoveNodeReferences(ImGuiID* node_ids, int node_ids_count); + static ImGuiDockNodeSettings* DockSettingsFindNodeSettings(ImGuiContext* ctx, ImGuiID node_id); + static void DockSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler*); + static void DockSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler*); + static void* DockSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); + static void DockSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); + static void DockSettingsHandler_WriteAll(ImGuiContext* imgui_ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf); +} + +//----------------------------------------------------------------------------- +// Docking: ImGuiDockContext +//----------------------------------------------------------------------------- +// The lifetime model is different from the one of regular windows: we always create a ImGuiDockNode for each ImGuiDockNodeSettings, +// or we always hold the entire docking node tree. Nodes are frequently hidden, e.g. if the window(s) or child nodes they host are not active. +// At boot time only, we run a simple GC to remove nodes that have no references. +// Because dock node settings (which are small, contiguous structures) are always mirrored by their corresponding dock nodes (more complete structures), +// we can also very easily recreate the nodes from scratch given the settings data (this is what DockContextRebuild() does). +// This is convenient as docking reconfiguration can be implemented by mostly poking at the simpler settings data. +//----------------------------------------------------------------------------- +// - DockContextInitialize() +// - DockContextShutdown() +// - DockContextClearNodes() +// - DockContextRebuildNodes() +// - DockContextNewFrameUpdateUndocking() +// - DockContextNewFrameUpdateDocking() +// - DockContextEndFrame() +// - DockContextFindNodeByID() +// - DockContextBindNodeToWindow() +// - DockContextGenNodeID() +// - DockContextAddNode() +// - DockContextRemoveNode() +// - ImGuiDockContextPruneNodeData +// - DockContextPruneUnusedSettingsNodes() +// - DockContextBuildNodesFromSettings() +// - DockContextBuildAddWindowsToNodes() +//----------------------------------------------------------------------------- + +void ImGui::DockContextInitialize(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + + // Add .ini handle for persistent docking data + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Docking"; + ini_handler.TypeHash = ImHashStr("Docking"); + ini_handler.ClearAllFn = DockSettingsHandler_ClearAll; + ini_handler.ReadInitFn = DockSettingsHandler_ClearAll; // Also clear on read + ini_handler.ReadOpenFn = DockSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = DockSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = DockSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = DockSettingsHandler_WriteAll; + g.SettingsHandlers.push_back(ini_handler); + + g.DockNodeWindowMenuHandler = &DockNodeWindowMenuHandler_Default; +} + +void ImGui::DockContextShutdown(ImGuiContext* ctx) +{ + ImGuiDockContext* dc = &ctx->DockContext; + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + IM_DELETE(node); +} + +void ImGui::DockContextClearNodes(ImGuiContext* ctx, ImGuiID root_id, bool clear_settings_refs) +{ + IM_UNUSED(ctx); + IM_ASSERT(ctx == GImGui); + DockBuilderRemoveNodeDockedWindows(root_id, clear_settings_refs); + DockBuilderRemoveNodeChildNodes(root_id); +} + +// [DEBUG] This function also acts as a defacto test to make sure we can rebuild from scratch without a glitch +// (Different from DockSettingsHandler_ClearAll() + DockSettingsHandler_ApplyAll() because this reuses current settings!) +void ImGui::DockContextRebuildNodes(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextRebuildNodes\n"); + SaveIniSettingsToMemory(); + ImGuiID root_id = 0; // Rebuild all + DockContextClearNodes(ctx, root_id, false); + DockContextBuildNodesFromSettings(ctx, dc->NodesSettings.Data, dc->NodesSettings.Size); + DockContextBuildAddWindowsToNodes(ctx, root_id); +} + +// Docking context update function, called by NewFrame() +void ImGui::DockContextNewFrameUpdateUndocking(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + if (!(g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + { + if (dc->Nodes.Data.Size > 0 || dc->Requests.Size > 0) + DockContextClearNodes(ctx, 0, true); + return; + } + + // Setting NoSplit at runtime merges all nodes + if (g.IO.ConfigDockingNoSplit) + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (node->IsRootNode() && node->IsSplitNode()) + { + DockBuilderRemoveNodeChildNodes(node->ID); + //dc->WantFullRebuild = true; + } + + // Process full rebuild +#if 0 + if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_C))) + dc->WantFullRebuild = true; +#endif + if (dc->WantFullRebuild) + { + DockContextRebuildNodes(ctx); + dc->WantFullRebuild = false; + } + + // Process Undocking requests (we need to process them _before_ the UpdateMouseMovingWindowNewFrame call in NewFrame) + for (ImGuiDockRequest& req : dc->Requests) + { + if (req.Type == ImGuiDockRequestType_Undock && req.UndockTargetWindow) + DockContextProcessUndockWindow(ctx, req.UndockTargetWindow); + else if (req.Type == ImGuiDockRequestType_Undock && req.UndockTargetNode) + DockContextProcessUndockNode(ctx, req.UndockTargetNode); + } +} + +// Docking context update function, called by NewFrame() +void ImGui::DockContextNewFrameUpdateDocking(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + if (!(g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + return; + + // [DEBUG] Store hovered dock node. + // We could in theory use DockNodeTreeFindVisibleNodeByPos() on the root host dock node, but using ->DockNode is a good shortcut. + // Note this is mostly a debug thing and isn't actually used for docking target, because docking involve more detailed filtering. + g.DebugHoveredDockNode = NULL; + if (ImGuiWindow* hovered_window = g.HoveredWindowUnderMovingWindow) + { + if (hovered_window->DockNodeAsHost) + g.DebugHoveredDockNode = DockNodeTreeFindVisibleNodeByPos(hovered_window->DockNodeAsHost, g.IO.MousePos); + else if (hovered_window->RootWindow->DockNode) + g.DebugHoveredDockNode = hovered_window->RootWindow->DockNode; + } + + // Process Docking requests + for (ImGuiDockRequest& req : dc->Requests) + if (req.Type == ImGuiDockRequestType_Dock) + DockContextProcessDock(ctx, &req); + dc->Requests.resize(0); + + // Create windows for each automatic docking nodes + // We can have NULL pointers when we delete nodes, but because ID are recycled this should amortize nicely (and our node count will never be very high) + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (node->IsFloatingNode()) + DockNodeUpdate(node); +} + +void ImGui::DockContextEndFrame(ImGuiContext* ctx) +{ + // Draw backgrounds of node missing their window + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &g.DockContext; + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (node->LastFrameActive == g.FrameCount && node->IsVisible && node->HostWindow && node->IsLeafNode() && !node->IsBgDrawnThisFrame) + { + ImRect bg_rect(node->Pos + ImVec2(0.0f, GetFrameHeight()), node->Pos + node->Size); + ImDrawFlags bg_rounding_flags = CalcRoundingFlagsForRectInRect(bg_rect, node->HostWindow->Rect(), g.Style.DockingSeparatorSize); + node->HostWindow->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG); + node->HostWindow->DrawList->AddRectFilled(bg_rect.Min, bg_rect.Max, node->LastBgColor, node->HostWindow->WindowRounding, bg_rounding_flags); + } +} + +ImGuiDockNode* ImGui::DockContextFindNodeByID(ImGuiContext* ctx, ImGuiID id) +{ + return (ImGuiDockNode*)ctx->DockContext.Nodes.GetVoidPtr(id); +} + +ImGuiID ImGui::DockContextGenNodeID(ImGuiContext* ctx) +{ + // Generate an ID for new node (the exact ID value doesn't matter as long as it is not already used) + // FIXME-OPT FIXME-DOCK: This is suboptimal, even if the node count is small enough not to be a worry.0 + // We should poke in ctx->Nodes to find a suitable ID faster. Even more so trivial that ctx->Nodes lookup is already sorted. + ImGuiID id = 0x0001; + while (DockContextFindNodeByID(ctx, id) != NULL) + id++; + return id; +} + +static ImGuiDockNode* ImGui::DockContextAddNode(ImGuiContext* ctx, ImGuiID id) +{ + // Generate an ID for the new node (the exact ID value doesn't matter as long as it is not already used) and add the first window. + ImGuiContext& g = *ctx; + if (id == 0) + id = DockContextGenNodeID(ctx); + else + IM_ASSERT(DockContextFindNodeByID(ctx, id) == NULL); + + // We don't set node->LastFrameAlive on construction. Nodes are always created at all time to reflect .ini settings! + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextAddNode 0x%08X\n", id); + ImGuiDockNode* node = IM_NEW(ImGuiDockNode)(id); + ctx->DockContext.Nodes.SetVoidPtr(node->ID, node); + return node; +} + +static void ImGui::DockContextRemoveNode(ImGuiContext* ctx, ImGuiDockNode* node, bool merge_sibling_into_parent_node) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextRemoveNode 0x%08X\n", node->ID); + IM_ASSERT(DockContextFindNodeByID(ctx, node->ID) == node); + IM_ASSERT(node->ChildNodes[0] == NULL && node->ChildNodes[1] == NULL); + IM_ASSERT(node->Windows.Size == 0); + + if (node->HostWindow) + node->HostWindow->DockNodeAsHost = NULL; + + ImGuiDockNode* parent_node = node->ParentNode; + const bool merge = (merge_sibling_into_parent_node && parent_node != NULL); + if (merge) + { + IM_ASSERT(parent_node->ChildNodes[0] == node || parent_node->ChildNodes[1] == node); + ImGuiDockNode* sibling_node = (parent_node->ChildNodes[0] == node ? parent_node->ChildNodes[1] : parent_node->ChildNodes[0]); + DockNodeTreeMerge(&g, parent_node, sibling_node); + } + else + { + for (int n = 0; parent_node && n < IM_ARRAYSIZE(parent_node->ChildNodes); n++) + if (parent_node->ChildNodes[n] == node) + node->ParentNode->ChildNodes[n] = NULL; + dc->Nodes.SetVoidPtr(node->ID, NULL); + IM_DELETE(node); + } +} + +static int IMGUI_CDECL DockNodeComparerDepthMostFirst(const void* lhs, const void* rhs) +{ + const ImGuiDockNode* a = *(const ImGuiDockNode* const*)lhs; + const ImGuiDockNode* b = *(const ImGuiDockNode* const*)rhs; + return ImGui::DockNodeGetDepth(b) - ImGui::DockNodeGetDepth(a); +} + +// Pre C++0x doesn't allow us to use a function-local type (without linkage) as template parameter, so we moved this here. +struct ImGuiDockContextPruneNodeData +{ + int CountWindows, CountChildWindows, CountChildNodes; + ImGuiID RootId; + ImGuiDockContextPruneNodeData() { CountWindows = CountChildWindows = CountChildNodes = 0; RootId = 0; } +}; + +// Garbage collect unused nodes (run once at init time) +static void ImGui::DockContextPruneUnusedSettingsNodes(ImGuiContext* ctx) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + IM_ASSERT(g.Windows.Size == 0); + + ImPool pool; + pool.Reserve(dc->NodesSettings.Size); + + // Count child nodes and compute RootID + for (int settings_n = 0; settings_n < dc->NodesSettings.Size; settings_n++) + { + ImGuiDockNodeSettings* settings = &dc->NodesSettings[settings_n]; + ImGuiDockContextPruneNodeData* parent_data = settings->ParentNodeId ? pool.GetByKey(settings->ParentNodeId) : 0; + pool.GetOrAddByKey(settings->ID)->RootId = parent_data ? parent_data->RootId : settings->ID; + if (settings->ParentNodeId) + pool.GetOrAddByKey(settings->ParentNodeId)->CountChildNodes++; + } + + // Count reference to dock ids from dockspaces + // We track the 'auto-DockNode <- manual-Window <- manual-DockSpace' in order to avoid 'auto-DockNode' being ditched by DockContextPruneUnusedSettingsNodes() + for (int settings_n = 0; settings_n < dc->NodesSettings.Size; settings_n++) + { + ImGuiDockNodeSettings* settings = &dc->NodesSettings[settings_n]; + if (settings->ParentWindowId != 0) + if (ImGuiWindowSettings* window_settings = FindWindowSettingsByID(settings->ParentWindowId)) + if (window_settings->DockId) + if (ImGuiDockContextPruneNodeData* data = pool.GetByKey(window_settings->DockId)) + data->CountChildNodes++; + } + + // Count reference to dock ids from window settings + // We guard against the possibility of an invalid .ini file (RootID may point to a missing node) + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (ImGuiID dock_id = settings->DockId) + if (ImGuiDockContextPruneNodeData* data = pool.GetByKey(dock_id)) + { + data->CountWindows++; + if (ImGuiDockContextPruneNodeData* data_root = (data->RootId == dock_id) ? data : pool.GetByKey(data->RootId)) + data_root->CountChildWindows++; + } + + // Prune + for (int settings_n = 0; settings_n < dc->NodesSettings.Size; settings_n++) + { + ImGuiDockNodeSettings* settings = &dc->NodesSettings[settings_n]; + ImGuiDockContextPruneNodeData* data = pool.GetByKey(settings->ID); + if (data->CountWindows > 1) + continue; + ImGuiDockContextPruneNodeData* data_root = (data->RootId == settings->ID) ? data : pool.GetByKey(data->RootId); + + bool remove = false; + remove |= (data->CountWindows == 1 && settings->ParentNodeId == 0 && data->CountChildNodes == 0 && !(settings->Flags & ImGuiDockNodeFlags_CentralNode)); // Floating root node with only 1 window + remove |= (data->CountWindows == 0 && settings->ParentNodeId == 0 && data->CountChildNodes == 0); // Leaf nodes with 0 window + remove |= (data_root->CountChildWindows == 0); + if (remove) + { + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextPruneUnusedSettingsNodes: Prune 0x%08X\n", settings->ID); + DockSettingsRemoveNodeReferences(&settings->ID, 1); + settings->ID = 0; + } + } +} + +static void ImGui::DockContextBuildNodesFromSettings(ImGuiContext* ctx, ImGuiDockNodeSettings* node_settings_array, int node_settings_count) +{ + // Build nodes + for (int node_n = 0; node_n < node_settings_count; node_n++) + { + ImGuiDockNodeSettings* settings = &node_settings_array[node_n]; + if (settings->ID == 0) + continue; + ImGuiDockNode* node = DockContextAddNode(ctx, settings->ID); + node->ParentNode = settings->ParentNodeId ? DockContextFindNodeByID(ctx, settings->ParentNodeId) : NULL; + node->Pos = ImVec2(settings->Pos.x, settings->Pos.y); + node->Size = ImVec2(settings->Size.x, settings->Size.y); + node->SizeRef = ImVec2(settings->SizeRef.x, settings->SizeRef.y); + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_DockNode; + if (node->ParentNode && node->ParentNode->ChildNodes[0] == NULL) + node->ParentNode->ChildNodes[0] = node; + else if (node->ParentNode && node->ParentNode->ChildNodes[1] == NULL) + node->ParentNode->ChildNodes[1] = node; + node->SelectedTabId = settings->SelectedTabId; + node->SplitAxis = (ImGuiAxis)settings->SplitAxis; + node->SetLocalFlags(settings->Flags & ImGuiDockNodeFlags_SavedFlagsMask_); + + // Bind host window immediately if it already exist (in case of a rebuild) + // This is useful as the RootWindowForTitleBarHighlight links necessary to highlight the currently focused node requires node->HostWindow to be set. + char host_window_title[20]; + ImGuiDockNode* root_node = DockNodeGetRootNode(node); + node->HostWindow = FindWindowByName(DockNodeGetHostWindowTitle(root_node, host_window_title, IM_ARRAYSIZE(host_window_title))); + } +} + +void ImGui::DockContextBuildAddWindowsToNodes(ImGuiContext* ctx, ImGuiID root_id) +{ + // Rebind all windows to nodes (they can also lazily rebind but we'll have a visible glitch during the first frame) + ImGuiContext& g = *ctx; + for (ImGuiWindow* window : g.Windows) + { + if (window->DockId == 0 || window->LastFrameActive < g.FrameCount - 1) + continue; + if (window->DockNode != NULL) + continue; + + ImGuiDockNode* node = DockContextFindNodeByID(ctx, window->DockId); + IM_ASSERT(node != NULL); // This should have been called after DockContextBuildNodesFromSettings() + if (root_id == 0 || DockNodeGetRootNode(node)->ID == root_id) + DockNodeAddWindow(node, window, true); + } +} + +//----------------------------------------------------------------------------- +// Docking: ImGuiDockContext Docking/Undocking functions +//----------------------------------------------------------------------------- +// - DockContextQueueDock() +// - DockContextQueueUndockWindow() +// - DockContextQueueUndockNode() +// - DockContextQueueNotifyRemovedNode() +// - DockContextProcessDock() +// - DockContextProcessUndockWindow() +// - DockContextProcessUndockNode() +// - DockContextCalcDropPosForDocking() +//----------------------------------------------------------------------------- + +void ImGui::DockContextQueueDock(ImGuiContext* ctx, ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload, ImGuiDir split_dir, float split_ratio, bool split_outer) +{ + IM_ASSERT(target != payload); + ImGuiDockRequest req; + req.Type = ImGuiDockRequestType_Dock; + req.DockTargetWindow = target; + req.DockTargetNode = target_node; + req.DockPayload = payload; + req.DockSplitDir = split_dir; + req.DockSplitRatio = split_ratio; + req.DockSplitOuter = split_outer; + ctx->DockContext.Requests.push_back(req); +} + +void ImGui::DockContextQueueUndockWindow(ImGuiContext* ctx, ImGuiWindow* window) +{ + ImGuiDockRequest req; + req.Type = ImGuiDockRequestType_Undock; + req.UndockTargetWindow = window; + ctx->DockContext.Requests.push_back(req); +} + +void ImGui::DockContextQueueUndockNode(ImGuiContext* ctx, ImGuiDockNode* node) +{ + ImGuiDockRequest req; + req.Type = ImGuiDockRequestType_Undock; + req.UndockTargetNode = node; + ctx->DockContext.Requests.push_back(req); +} + +void ImGui::DockContextQueueNotifyRemovedNode(ImGuiContext* ctx, ImGuiDockNode* node) +{ + ImGuiDockContext* dc = &ctx->DockContext; + for (ImGuiDockRequest& req : dc->Requests) + if (req.DockTargetNode == node) + req.Type = ImGuiDockRequestType_None; +} + +void ImGui::DockContextProcessDock(ImGuiContext* ctx, ImGuiDockRequest* req) +{ + IM_ASSERT((req->Type == ImGuiDockRequestType_Dock && req->DockPayload != NULL) || (req->Type == ImGuiDockRequestType_Split && req->DockPayload == NULL)); + IM_ASSERT(req->DockTargetWindow != NULL || req->DockTargetNode != NULL); + + ImGuiContext& g = *ctx; + IM_UNUSED(g); + + ImGuiWindow* payload_window = req->DockPayload; // Optional + ImGuiWindow* target_window = req->DockTargetWindow; + ImGuiDockNode* node = req->DockTargetNode; + if (payload_window) + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessDock node 0x%08X target '%s' dock window '%s', split_dir %d\n", node ? node->ID : 0, target_window ? target_window->Name : "NULL", payload_window->Name, req->DockSplitDir); + else + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessDock node 0x%08X, split_dir %d\n", node ? node->ID : 0, req->DockSplitDir); + + // Decide which Tab will be selected at the end of the operation + ImGuiID next_selected_id = 0; + ImGuiDockNode* payload_node = NULL; + if (payload_window) + { + payload_node = payload_window->DockNodeAsHost; + payload_window->DockNodeAsHost = NULL; // Important to clear this as the node will have its life as a child which might be merged/deleted later. + if (payload_node && payload_node->IsLeafNode()) + next_selected_id = payload_node->TabBar->NextSelectedTabId ? payload_node->TabBar->NextSelectedTabId : payload_node->TabBar->SelectedTabId; + if (payload_node == NULL) + next_selected_id = payload_window->TabId; + } + + // FIXME-DOCK: When we are trying to dock an existing single-window node into a loose window, transfer Node ID as well + // When processing an interactive split, usually LastFrameAlive will be < g.FrameCount. But DockBuilder operations can make it ==. + if (node) + IM_ASSERT(node->LastFrameAlive <= g.FrameCount); + if (node && target_window && node == target_window->DockNodeAsHost) + IM_ASSERT(node->Windows.Size > 0 || node->IsSplitNode() || node->IsCentralNode()); + + // Create new node and add existing window to it + if (node == NULL) + { + node = DockContextAddNode(ctx, 0); + node->Pos = target_window->Pos; + node->Size = target_window->Size; + if (target_window->DockNodeAsHost == NULL) + { + DockNodeAddWindow(node, target_window, true); + node->TabBar->Tabs[0].Flags &= ~ImGuiTabItemFlags_Unsorted; + target_window->DockIsActive = true; + } + } + + ImGuiDir split_dir = req->DockSplitDir; + if (split_dir != ImGuiDir_None) + { + // Split into two, one side will be our payload node unless we are dropping a loose window + const ImGuiAxis split_axis = (split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + const int split_inheritor_child_idx = (split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Up) ? 1 : 0; // Current contents will be moved to the opposite side + const float split_ratio = req->DockSplitRatio; + DockNodeTreeSplit(ctx, node, split_axis, split_inheritor_child_idx, split_ratio, payload_node); // payload_node may be NULL here! + ImGuiDockNode* new_node = node->ChildNodes[split_inheritor_child_idx ^ 1]; + new_node->HostWindow = node->HostWindow; + node = new_node; + } + node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_HiddenTabBar); + + if (node != payload_node) + { + // Create tab bar before we call DockNodeMoveWindows (which would attempt to move the old tab-bar, which would lead us to payload tabs wrongly appearing before target tabs!) + if (node->Windows.Size > 0 && node->TabBar == NULL) + { + DockNodeAddTabBar(node); + for (int n = 0; n < node->Windows.Size; n++) + TabBarAddTab(node->TabBar, ImGuiTabItemFlags_None, node->Windows[n]); + } + + if (payload_node != NULL) + { + // Transfer full payload node (with 1+ child windows or child nodes) + if (payload_node->IsSplitNode()) + { + if (node->Windows.Size > 0) + { + // We can dock a split payload into a node that already has windows _only_ if our payload is a node tree with a single visible node. + // In this situation, we move the windows of the target node into the currently visible node of the payload. + // This allows us to preserve some of the underlying dock tree settings nicely. + IM_ASSERT(payload_node->OnlyNodeWithWindows != NULL); // The docking should have been blocked by DockNodePreviewDockSetup() early on and never submitted. + ImGuiDockNode* visible_node = payload_node->OnlyNodeWithWindows; + if (visible_node->TabBar) + IM_ASSERT(visible_node->TabBar->Tabs.Size > 0); + DockNodeMoveWindows(node, visible_node); + DockNodeMoveWindows(visible_node, node); + DockSettingsRenameNodeReferences(node->ID, visible_node->ID); + } + if (node->IsCentralNode()) + { + // Central node property needs to be moved to a leaf node, pick the last focused one. + // FIXME-DOCK: If we had to transfer other flags here, what would the policy be? + ImGuiDockNode* last_focused_node = DockContextFindNodeByID(ctx, payload_node->LastFocusedNodeId); + IM_ASSERT(last_focused_node != NULL); + ImGuiDockNode* last_focused_root_node = DockNodeGetRootNode(last_focused_node); + IM_ASSERT(last_focused_root_node == DockNodeGetRootNode(payload_node)); + last_focused_node->SetLocalFlags(last_focused_node->LocalFlags | ImGuiDockNodeFlags_CentralNode); + node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_CentralNode); + last_focused_root_node->CentralNode = last_focused_node; + } + + IM_ASSERT(node->Windows.Size == 0); + DockNodeMoveChildNodes(node, payload_node); + } + else + { + const ImGuiID payload_dock_id = payload_node->ID; + DockNodeMoveWindows(node, payload_node); + DockSettingsRenameNodeReferences(payload_dock_id, node->ID); + } + DockContextRemoveNode(ctx, payload_node, true); + } + else if (payload_window) + { + // Transfer single window + const ImGuiID payload_dock_id = payload_window->DockId; + node->VisibleWindow = payload_window; + DockNodeAddWindow(node, payload_window, true); + if (payload_dock_id != 0) + DockSettingsRenameNodeReferences(payload_dock_id, node->ID); + } + } + else + { + // When docking a floating single window node we want to reevaluate auto-hiding of the tab bar + node->WantHiddenTabBarUpdate = true; + } + + // Update selection immediately + if (ImGuiTabBar* tab_bar = node->TabBar) + tab_bar->NextSelectedTabId = next_selected_id; + MarkIniSettingsDirty(); +} + +// Problem: +// Undocking a large (~full screen) window would leave it so large that the bottom right sizing corner would more +// than likely be off the screen and the window would be hard to resize to fit on screen. This can be particularly problematic +// with 'ConfigWindowsMoveFromTitleBarOnly=true' and/or with 'ConfigWindowsResizeFromEdges=false' as well (the later can be +// due to missing ImGuiBackendFlags_HasMouseCursors backend flag). +// Solution: +// When undocking a window we currently force its maximum size to 90% of the host viewport or monitor. +// Reevaluate this when we implement preserving docked/undocked size ("docking_wip/undocked_size" branch). +static ImVec2 FixLargeWindowsWhenUndocking(const ImVec2& size, ImGuiViewport* ref_viewport) +{ + if (ref_viewport == NULL) + return size; + + ImGuiContext& g = *GImGui; + ImVec2 max_size = ImTrunc(ref_viewport->WorkSize * 0.90f); + if (g.ConfigFlagsCurrFrame & ImGuiConfigFlags_ViewportsEnable) + { + const ImGuiPlatformMonitor* monitor = ImGui::GetViewportPlatformMonitor(ref_viewport); + max_size = ImTrunc(monitor->WorkSize * 0.90f); + } + return ImMin(size, max_size); +} + +void ImGui::DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* window, bool clear_persistent_docking_ref) +{ + ImGuiContext& g = *ctx; + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessUndockWindow window '%s', clear_persistent_docking_ref = %d\n", window->Name, clear_persistent_docking_ref); + if (window->DockNode) + DockNodeRemoveWindow(window->DockNode, window, clear_persistent_docking_ref ? 0 : window->DockId); + else + window->DockId = 0; + window->Collapsed = false; + window->DockIsActive = false; + window->DockNodeIsVisible = window->DockTabIsVisible = false; + window->Size = window->SizeFull = FixLargeWindowsWhenUndocking(window->SizeFull, window->Viewport); + + MarkIniSettingsDirty(); +} + +void ImGui::DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node) +{ + ImGuiContext& g = *ctx; + IMGUI_DEBUG_LOG_DOCKING("[docking] DockContextProcessUndockNode node %08X\n", node->ID); + IM_ASSERT(node->IsLeafNode()); + IM_ASSERT(node->Windows.Size >= 1); + + if (node->IsRootNode() || node->IsCentralNode()) + { + // In the case of a root node or central node, the node will have to stay in place. Create a new node to receive the payload. + ImGuiDockNode* new_node = DockContextAddNode(ctx, 0); + new_node->Pos = node->Pos; + new_node->Size = node->Size; + new_node->SizeRef = node->SizeRef; + DockNodeMoveWindows(new_node, node); + DockSettingsRenameNodeReferences(node->ID, new_node->ID); + node = new_node; + } + else + { + // Otherwise extract our node and merge our sibling back into the parent node. + IM_ASSERT(node->ParentNode->ChildNodes[0] == node || node->ParentNode->ChildNodes[1] == node); + int index_in_parent = (node->ParentNode->ChildNodes[0] == node) ? 0 : 1; + node->ParentNode->ChildNodes[index_in_parent] = NULL; + DockNodeTreeMerge(ctx, node->ParentNode, node->ParentNode->ChildNodes[index_in_parent ^ 1]); + node->ParentNode->AuthorityForViewport = ImGuiDataAuthority_Window; // The node that stays in place keeps the viewport, so our newly dragged out node will create a new viewport + node->ParentNode = NULL; + } + for (ImGuiWindow* window : node->Windows) + { + window->Flags &= ~ImGuiWindowFlags_ChildWindow; + if (window->ParentWindow) + window->ParentWindow->DC.ChildWindows.find_erase(window); + UpdateWindowParentAndRootLinks(window, window->Flags, NULL); + } + node->AuthorityForPos = node->AuthorityForSize = ImGuiDataAuthority_DockNode; + node->Size = FixLargeWindowsWhenUndocking(node->Size, node->Windows[0]->Viewport); + node->WantMouseMove = true; + MarkIniSettingsDirty(); +} + +// This is mostly used for automation. +bool ImGui::DockContextCalcDropPosForDocking(ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDir split_dir, bool split_outer, ImVec2* out_pos) +{ + if (target != NULL && target_node == NULL) + target_node = target->DockNode; + + // In DockNodePreviewDockSetup() for a root central node instead of showing both "inner" and "outer" drop rects + // (which would be functionally identical) we only show the outer one. Reflect this here. + if (target_node && target_node->ParentNode == NULL && target_node->IsCentralNode() && split_dir != ImGuiDir_None) + split_outer = true; + ImGuiDockPreviewData split_data; + DockNodePreviewDockSetup(target, target_node, payload_window, payload_node, &split_data, false, split_outer); + if (split_data.DropRectsDraw[split_dir+1].IsInverted()) + return false; + *out_pos = split_data.DropRectsDraw[split_dir+1].GetCenter(); + return true; +} + +//----------------------------------------------------------------------------- +// Docking: ImGuiDockNode +//----------------------------------------------------------------------------- +// - DockNodeGetTabOrder() +// - DockNodeAddWindow() +// - DockNodeRemoveWindow() +// - DockNodeMoveChildNodes() +// - DockNodeMoveWindows() +// - DockNodeApplyPosSizeToWindows() +// - DockNodeHideHostWindow() +// - ImGuiDockNodeFindInfoResults +// - DockNodeFindInfo() +// - DockNodeFindWindowByID() +// - DockNodeUpdateFlagsAndCollapse() +// - DockNodeUpdateHasCentralNodeFlag() +// - DockNodeUpdateVisibleFlag() +// - DockNodeStartMouseMovingWindow() +// - DockNodeUpdate() +// - DockNodeUpdateWindowMenu() +// - DockNodeBeginAmendTabBar() +// - DockNodeEndAmendTabBar() +// - DockNodeUpdateTabBar() +// - DockNodeAddTabBar() +// - DockNodeRemoveTabBar() +// - DockNodeIsDropAllowedOne() +// - DockNodeIsDropAllowed() +// - DockNodeCalcTabBarLayout() +// - DockNodeCalcSplitRects() +// - DockNodeCalcDropRectsAndTestMousePos() +// - DockNodePreviewDockSetup() +// - DockNodePreviewDockRender() +//----------------------------------------------------------------------------- + +ImGuiDockNode::ImGuiDockNode(ImGuiID id) +{ + ID = id; + SharedFlags = LocalFlags = LocalFlagsInWindows = MergedFlags = ImGuiDockNodeFlags_None; + ParentNode = ChildNodes[0] = ChildNodes[1] = NULL; + TabBar = NULL; + SplitAxis = ImGuiAxis_None; + + State = ImGuiDockNodeState_Unknown; + LastBgColor = IM_COL32_WHITE; + HostWindow = VisibleWindow = NULL; + CentralNode = OnlyNodeWithWindows = NULL; + CountNodeWithWindows = 0; + LastFrameAlive = LastFrameActive = LastFrameFocused = -1; + LastFocusedNodeId = 0; + SelectedTabId = 0; + WantCloseTabId = 0; + RefViewportId = 0; + AuthorityForPos = AuthorityForSize = ImGuiDataAuthority_DockNode; + AuthorityForViewport = ImGuiDataAuthority_Auto; + IsVisible = true; + IsFocused = HasCloseButton = HasWindowMenuButton = HasCentralNodeChild = false; + IsBgDrawnThisFrame = false; + WantCloseAll = WantLockSizeOnce = WantMouseMove = WantHiddenTabBarUpdate = WantHiddenTabBarToggle = false; +} + +ImGuiDockNode::~ImGuiDockNode() +{ + IM_DELETE(TabBar); + TabBar = NULL; + ChildNodes[0] = ChildNodes[1] = NULL; +} + +int ImGui::DockNodeGetTabOrder(ImGuiWindow* window) +{ + ImGuiTabBar* tab_bar = window->DockNode->TabBar; + if (tab_bar == NULL) + return -1; + ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, window->TabId); + return tab ? TabBarGetTabOrder(tab_bar, tab) : -1; +} + +static void DockNodeHideWindowDuringHostWindowCreation(ImGuiWindow* window) +{ + window->Hidden = true; + window->HiddenFramesCanSkipItems = window->Active ? 1 : 2; +} + +static void ImGui::DockNodeAddWindow(ImGuiDockNode* node, ImGuiWindow* window, bool add_to_tab_bar) +{ + ImGuiContext& g = *GImGui; (void)g; + if (window->DockNode) + { + // Can overwrite an existing window->DockNode (e.g. pointing to a disabled DockSpace node) + IM_ASSERT(window->DockNode->ID != node->ID); + DockNodeRemoveWindow(window->DockNode, window, 0); + } + IM_ASSERT(window->DockNode == NULL || window->DockNodeAsHost == NULL); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockNodeAddWindow node 0x%08X window '%s'\n", node->ID, window->Name); + + // If more than 2 windows appeared on the same frame leading to the creation of a new hosting window, + // we'll hide windows until the host window is ready. Hide the 1st window after its been output (so it is not visible for one frame). + // We will call DockNodeHideWindowDuringHostWindowCreation() on ourselves in Begin() + if (node->HostWindow == NULL && node->Windows.Size == 1 && node->Windows[0]->WasActive == false) + DockNodeHideWindowDuringHostWindowCreation(node->Windows[0]); + + node->Windows.push_back(window); + node->WantHiddenTabBarUpdate = true; + window->DockNode = node; + window->DockId = node->ID; + window->DockIsActive = (node->Windows.Size > 1); + window->DockTabWantClose = false; + + // When reactivating a node with one or two loose window, the window pos/size/viewport are authoritative over the node storage. + // In particular it is important we init the viewport from the first window so we don't create two viewports and drop one. + if (node->HostWindow == NULL && node->IsFloatingNode()) + { + if (node->AuthorityForPos == ImGuiDataAuthority_Auto) + node->AuthorityForPos = ImGuiDataAuthority_Window; + if (node->AuthorityForSize == ImGuiDataAuthority_Auto) + node->AuthorityForSize = ImGuiDataAuthority_Window; + if (node->AuthorityForViewport == ImGuiDataAuthority_Auto) + node->AuthorityForViewport = ImGuiDataAuthority_Window; + } + + // Add to tab bar if requested + if (add_to_tab_bar) + { + if (node->TabBar == NULL) + { + DockNodeAddTabBar(node); + node->TabBar->SelectedTabId = node->TabBar->NextSelectedTabId = node->SelectedTabId; + + // Add existing windows + for (int n = 0; n < node->Windows.Size - 1; n++) + TabBarAddTab(node->TabBar, ImGuiTabItemFlags_None, node->Windows[n]); + } + TabBarAddTab(node->TabBar, ImGuiTabItemFlags_Unsorted, window); + } + + DockNodeUpdateVisibleFlag(node); + + // Update this without waiting for the next time we Begin() in the window, so our host window will have the proper title bar color on its first frame. + if (node->HostWindow) + UpdateWindowParentAndRootLinks(window, window->Flags | ImGuiWindowFlags_ChildWindow, node->HostWindow); +} + +static void ImGui::DockNodeRemoveWindow(ImGuiDockNode* node, ImGuiWindow* window, ImGuiID save_dock_id) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(window->DockNode == node); + //IM_ASSERT(window->RootWindowDockTree == node->HostWindow); + //IM_ASSERT(window->LastFrameActive < g.FrameCount); // We may call this from Begin() + IM_ASSERT(save_dock_id == 0 || save_dock_id == node->ID); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockNodeRemoveWindow node 0x%08X window '%s'\n", node->ID, window->Name); + + window->DockNode = NULL; + window->DockIsActive = window->DockTabWantClose = false; + window->DockId = save_dock_id; + window->Flags &= ~ImGuiWindowFlags_ChildWindow; + if (window->ParentWindow) + window->ParentWindow->DC.ChildWindows.find_erase(window); + UpdateWindowParentAndRootLinks(window, window->Flags, NULL); // Update immediately + + if (node->HostWindow && node->HostWindow->ViewportOwned) + { + // When undocking from a user interaction this will always run in NewFrame() and have not much effect. + // But mid-frame, if we clear viewport we need to mark window as hidden as well. + window->Viewport = NULL; + window->ViewportId = 0; + window->ViewportOwned = false; + window->Hidden = true; + } + + // Remove window + bool erased = false; + for (int n = 0; n < node->Windows.Size; n++) + if (node->Windows[n] == window) + { + node->Windows.erase(node->Windows.Data + n); + erased = true; + break; + } + if (!erased) + IM_ASSERT(erased); + if (node->VisibleWindow == window) + node->VisibleWindow = NULL; + + // Remove tab and possibly tab bar + node->WantHiddenTabBarUpdate = true; + if (node->TabBar) + { + TabBarRemoveTab(node->TabBar, window->TabId); + const int tab_count_threshold_for_tab_bar = node->IsCentralNode() ? 1 : 2; + if (node->Windows.Size < tab_count_threshold_for_tab_bar) + DockNodeRemoveTabBar(node); + } + + if (node->Windows.Size == 0 && !node->IsCentralNode() && !node->IsDockSpace() && window->DockId != node->ID) + { + // Automatic dock node delete themselves if they are not holding at least one tab + DockContextRemoveNode(&g, node, true); + return; + } + + if (node->Windows.Size == 1 && !node->IsCentralNode() && node->HostWindow) + { + ImGuiWindow* remaining_window = node->Windows[0]; + // Note: we used to transport viewport ownership here. + remaining_window->Collapsed = node->HostWindow->Collapsed; + } + + // Update visibility immediately is required so the DockNodeUpdateRemoveInactiveChilds() processing can reflect changes up the tree + DockNodeUpdateVisibleFlag(node); +} + +static void ImGui::DockNodeMoveChildNodes(ImGuiDockNode* dst_node, ImGuiDockNode* src_node) +{ + IM_ASSERT(dst_node->Windows.Size == 0); + dst_node->ChildNodes[0] = src_node->ChildNodes[0]; + dst_node->ChildNodes[1] = src_node->ChildNodes[1]; + if (dst_node->ChildNodes[0]) + dst_node->ChildNodes[0]->ParentNode = dst_node; + if (dst_node->ChildNodes[1]) + dst_node->ChildNodes[1]->ParentNode = dst_node; + dst_node->SplitAxis = src_node->SplitAxis; + dst_node->SizeRef = src_node->SizeRef; + src_node->ChildNodes[0] = src_node->ChildNodes[1] = NULL; +} + +static void ImGui::DockNodeMoveWindows(ImGuiDockNode* dst_node, ImGuiDockNode* src_node) +{ + // Insert tabs in the same orders as currently ordered (node->Windows isn't ordered) + IM_ASSERT(src_node && dst_node && dst_node != src_node); + ImGuiTabBar* src_tab_bar = src_node->TabBar; + if (src_tab_bar != NULL) + IM_ASSERT(src_node->Windows.Size <= src_node->TabBar->Tabs.Size); + + // If the dst_node is empty we can just move the entire tab bar (to preserve selection, scrolling, etc.) + bool move_tab_bar = (src_tab_bar != NULL) && (dst_node->TabBar == NULL); + if (move_tab_bar) + { + dst_node->TabBar = src_node->TabBar; + src_node->TabBar = NULL; + } + + // Tab order is not important here, it is preserved by sorting in DockNodeUpdateTabBar(). + for (ImGuiWindow* window : src_node->Windows) + { + window->DockNode = NULL; + window->DockIsActive = false; + DockNodeAddWindow(dst_node, window, !move_tab_bar); + } + src_node->Windows.clear(); + + if (!move_tab_bar && src_node->TabBar) + { + if (dst_node->TabBar) + dst_node->TabBar->SelectedTabId = src_node->TabBar->SelectedTabId; + DockNodeRemoveTabBar(src_node); + } +} + +static void ImGui::DockNodeApplyPosSizeToWindows(ImGuiDockNode* node) +{ + for (ImGuiWindow* window : node->Windows) + { + SetWindowPos(window, node->Pos, ImGuiCond_Always); // We don't assign directly to Pos because it can break the calculation of SizeContents on next frame + SetWindowSize(window, node->Size, ImGuiCond_Always); + } +} + +static void ImGui::DockNodeHideHostWindow(ImGuiDockNode* node) +{ + if (node->HostWindow) + { + if (node->HostWindow->DockNodeAsHost == node) + node->HostWindow->DockNodeAsHost = NULL; + node->HostWindow = NULL; + } + + if (node->Windows.Size == 1) + { + node->VisibleWindow = node->Windows[0]; + node->Windows[0]->DockIsActive = false; + } + + if (node->TabBar) + DockNodeRemoveTabBar(node); +} + +// Search function called once by root node in DockNodeUpdate() +struct ImGuiDockNodeTreeInfo +{ + ImGuiDockNode* CentralNode; + ImGuiDockNode* FirstNodeWithWindows; + int CountNodesWithWindows; + //ImGuiWindowClass WindowClassForMerges; + + ImGuiDockNodeTreeInfo() { memset(this, 0, sizeof(*this)); } +}; + +static void DockNodeFindInfo(ImGuiDockNode* node, ImGuiDockNodeTreeInfo* info) +{ + if (node->Windows.Size > 0) + { + if (info->FirstNodeWithWindows == NULL) + info->FirstNodeWithWindows = node; + info->CountNodesWithWindows++; + } + if (node->IsCentralNode()) + { + IM_ASSERT(info->CentralNode == NULL); // Should be only one + IM_ASSERT(node->IsLeafNode() && "If you get this assert: please submit .ini file + repro of actions leading to this."); + info->CentralNode = node; + } + if (info->CountNodesWithWindows > 1 && info->CentralNode != NULL) + return; + if (node->ChildNodes[0]) + DockNodeFindInfo(node->ChildNodes[0], info); + if (node->ChildNodes[1]) + DockNodeFindInfo(node->ChildNodes[1], info); +} + +static ImGuiWindow* ImGui::DockNodeFindWindowByID(ImGuiDockNode* node, ImGuiID id) +{ + IM_ASSERT(id != 0); + for (ImGuiWindow* window : node->Windows) + if (window->ID == id) + return window; + return NULL; +} + +// - Remove inactive windows/nodes. +// - Update visibility flag. +static void ImGui::DockNodeUpdateFlagsAndCollapse(ImGuiDockNode* node) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(node->ParentNode == NULL || node->ParentNode->ChildNodes[0] == node || node->ParentNode->ChildNodes[1] == node); + + // Inherit most flags + if (node->ParentNode) + node->SharedFlags = node->ParentNode->SharedFlags & ImGuiDockNodeFlags_SharedFlagsInheritMask_; + + // Recurse into children + // There is the possibility that one of our child becoming empty will delete itself and moving its sibling contents into 'node'. + // If 'node->ChildNode[0]' delete itself, then 'node->ChildNode[1]->Windows' will be moved into 'node' + // If 'node->ChildNode[1]' delete itself, then 'node->ChildNode[0]->Windows' will be moved into 'node' and the "remove inactive windows" loop will have run twice on those windows (harmless) + node->HasCentralNodeChild = false; + if (node->ChildNodes[0]) + DockNodeUpdateFlagsAndCollapse(node->ChildNodes[0]); + if (node->ChildNodes[1]) + DockNodeUpdateFlagsAndCollapse(node->ChildNodes[1]); + + // Remove inactive windows, collapse nodes + // Merge node flags overrides stored in windows + node->LocalFlagsInWindows = ImGuiDockNodeFlags_None; + for (int window_n = 0; window_n < node->Windows.Size; window_n++) + { + ImGuiWindow* window = node->Windows[window_n]; + IM_ASSERT(window->DockNode == node); + + bool node_was_active = (node->LastFrameActive + 1 == g.FrameCount); + bool remove = false; + remove |= node_was_active && (window->LastFrameActive + 1 < g.FrameCount); + remove |= node_was_active && (node->WantCloseAll || node->WantCloseTabId == window->TabId) && window->HasCloseButton && !(window->Flags & ImGuiWindowFlags_UnsavedDocument); // Submit all _expected_ closure from last frame + remove |= (window->DockTabWantClose); + if (remove) + { + window->DockTabWantClose = false; + if (node->Windows.Size == 1 && !node->IsCentralNode()) + { + DockNodeHideHostWindow(node); + node->State = ImGuiDockNodeState_HostWindowHiddenBecauseSingleWindow; + DockNodeRemoveWindow(node, window, node->ID); // Will delete the node so it'll be invalid on return + return; + } + DockNodeRemoveWindow(node, window, node->ID); + window_n--; + continue; + } + + // FIXME-DOCKING: Missing policies for conflict resolution, hence the "Experimental" tag on this. + //node->LocalFlagsInWindow &= ~window->WindowClass.DockNodeFlagsOverrideClear; + node->LocalFlagsInWindows |= window->WindowClass.DockNodeFlagsOverrideSet; + } + node->UpdateMergedFlags(); + + // Auto-hide tab bar option + ImGuiDockNodeFlags node_flags = node->MergedFlags; + if (node->WantHiddenTabBarUpdate && node->Windows.Size == 1 && (node_flags & ImGuiDockNodeFlags_AutoHideTabBar) && !node->IsHiddenTabBar()) + node->WantHiddenTabBarToggle = true; + node->WantHiddenTabBarUpdate = false; + + // Cancel toggling if we know our tab bar is enforced to be hidden at all times + if (node->WantHiddenTabBarToggle && node->VisibleWindow && (node->VisibleWindow->WindowClass.DockNodeFlagsOverrideSet & ImGuiDockNodeFlags_HiddenTabBar)) + node->WantHiddenTabBarToggle = false; + + // Apply toggles at a single point of the frame (here!) + if (node->Windows.Size > 1) + node->SetLocalFlags(node->LocalFlags & ~ImGuiDockNodeFlags_HiddenTabBar); + else if (node->WantHiddenTabBarToggle) + node->SetLocalFlags(node->LocalFlags ^ ImGuiDockNodeFlags_HiddenTabBar); + node->WantHiddenTabBarToggle = false; + + DockNodeUpdateVisibleFlag(node); +} + +// This is rarely called as DockNodeUpdateForRootNode() generally does it most frames. +static void ImGui::DockNodeUpdateHasCentralNodeChild(ImGuiDockNode* node) +{ + node->HasCentralNodeChild = false; + if (node->ChildNodes[0]) + DockNodeUpdateHasCentralNodeChild(node->ChildNodes[0]); + if (node->ChildNodes[1]) + DockNodeUpdateHasCentralNodeChild(node->ChildNodes[1]); + if (node->IsRootNode()) + { + ImGuiDockNode* mark_node = node->CentralNode; + while (mark_node) + { + mark_node->HasCentralNodeChild = true; + mark_node = mark_node->ParentNode; + } + } +} + +static void ImGui::DockNodeUpdateVisibleFlag(ImGuiDockNode* node) +{ + // Update visibility flag + bool is_visible = (node->ParentNode == NULL) ? node->IsDockSpace() : node->IsCentralNode(); + is_visible |= (node->Windows.Size > 0); + is_visible |= (node->ChildNodes[0] && node->ChildNodes[0]->IsVisible); + is_visible |= (node->ChildNodes[1] && node->ChildNodes[1]->IsVisible); + node->IsVisible = is_visible; +} + +static void ImGui::DockNodeStartMouseMovingWindow(ImGuiDockNode* node, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(node->WantMouseMove == true); + StartMouseMovingWindow(window); + g.ActiveIdClickOffset = g.IO.MouseClickedPos[0] - node->Pos; + g.MovingWindow = window; // If we are docked into a non moveable root window, StartMouseMovingWindow() won't set g.MovingWindow. Override that decision. + node->WantMouseMove = false; +} + +// Update CentralNode, OnlyNodeWithWindows, LastFocusedNodeID. Copy window class. +static void ImGui::DockNodeUpdateForRootNode(ImGuiDockNode* node) +{ + DockNodeUpdateFlagsAndCollapse(node); + + // - Setup central node pointers + // - Find if there's only a single visible window in the hierarchy (in which case we need to display a regular title bar -> FIXME-DOCK: that last part is not done yet!) + // Cannot merge this with DockNodeUpdateFlagsAndCollapse() because FirstNodeWithWindows is found after window removal and child collapsing + ImGuiDockNodeTreeInfo info; + DockNodeFindInfo(node, &info); + node->CentralNode = info.CentralNode; + node->OnlyNodeWithWindows = (info.CountNodesWithWindows == 1) ? info.FirstNodeWithWindows : NULL; + node->CountNodeWithWindows = info.CountNodesWithWindows; + if (node->LastFocusedNodeId == 0 && info.FirstNodeWithWindows != NULL) + node->LastFocusedNodeId = info.FirstNodeWithWindows->ID; + + // Copy the window class from of our first window so it can be used for proper dock filtering. + // When node has mixed windows, prioritize the class with the most constraint (DockingAllowUnclassed = false) as the reference to copy. + // FIXME-DOCK: We don't recurse properly, this code could be reworked to work from DockNodeUpdateScanRec. + if (ImGuiDockNode* first_node_with_windows = info.FirstNodeWithWindows) + { + node->WindowClass = first_node_with_windows->Windows[0]->WindowClass; + for (int n = 1; n < first_node_with_windows->Windows.Size; n++) + if (first_node_with_windows->Windows[n]->WindowClass.DockingAllowUnclassed == false) + { + node->WindowClass = first_node_with_windows->Windows[n]->WindowClass; + break; + } + } + + ImGuiDockNode* mark_node = node->CentralNode; + while (mark_node) + { + mark_node->HasCentralNodeChild = true; + mark_node = mark_node->ParentNode; + } +} + +static void DockNodeSetupHostWindow(ImGuiDockNode* node, ImGuiWindow* host_window) +{ + // Remove ourselves from any previous different host window + // This can happen if a user mistakenly does (see #4295 for details): + // - N+0: DockBuilderAddNode(id, 0) // missing ImGuiDockNodeFlags_DockSpace + // - N+1: NewFrame() // will create floating host window for that node + // - N+1: DockSpace(id) // requalify node as dockspace, moving host window + if (node->HostWindow && node->HostWindow != host_window && node->HostWindow->DockNodeAsHost == node) + node->HostWindow->DockNodeAsHost = NULL; + + host_window->DockNodeAsHost = node; + node->HostWindow = host_window; +} + +static void ImGui::DockNodeUpdate(ImGuiDockNode* node) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(node->LastFrameActive != g.FrameCount); + node->LastFrameAlive = g.FrameCount; + node->IsBgDrawnThisFrame = false; + + node->CentralNode = node->OnlyNodeWithWindows = NULL; + if (node->IsRootNode()) + DockNodeUpdateForRootNode(node); + + // Remove tab bar if not needed + if (node->TabBar && node->IsNoTabBar()) + DockNodeRemoveTabBar(node); + + // Early out for hidden root dock nodes (when all DockId references are in inactive windows, or there is only 1 floating window holding on the DockId) + bool want_to_hide_host_window = false; + if (node->IsFloatingNode()) + { + if (node->Windows.Size <= 1 && node->IsLeafNode()) + if (!g.IO.ConfigDockingAlwaysTabBar && (node->Windows.Size == 0 || !node->Windows[0]->WindowClass.DockingAlwaysTabBar)) + want_to_hide_host_window = true; + if (node->CountNodeWithWindows == 0) + want_to_hide_host_window = true; + } + if (want_to_hide_host_window) + { + if (node->Windows.Size == 1) + { + // Floating window pos/size is authoritative + ImGuiWindow* single_window = node->Windows[0]; + node->Pos = single_window->Pos; + node->Size = single_window->SizeFull; + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_Window; + + // Transfer focus immediately so when we revert to a regular window it is immediately selected + if (node->HostWindow && g.NavWindow == node->HostWindow) + FocusWindow(single_window); + if (node->HostWindow) + { + IMGUI_DEBUG_LOG_VIEWPORT("[viewport] Node %08X transfer Viewport %08X->%08X to Window '%s'\n", node->ID, node->HostWindow->Viewport->ID, single_window->ID, single_window->Name); + single_window->Viewport = node->HostWindow->Viewport; + single_window->ViewportId = node->HostWindow->ViewportId; + if (node->HostWindow->ViewportOwned) + { + single_window->Viewport->ID = single_window->ID; + single_window->Viewport->Window = single_window; + single_window->ViewportOwned = true; + } + } + node->RefViewportId = single_window->ViewportId; + } + + DockNodeHideHostWindow(node); + node->State = ImGuiDockNodeState_HostWindowHiddenBecauseSingleWindow; + node->WantCloseAll = false; + node->WantCloseTabId = 0; + node->HasCloseButton = node->HasWindowMenuButton = false; + node->LastFrameActive = g.FrameCount; + + if (node->WantMouseMove && node->Windows.Size == 1) + DockNodeStartMouseMovingWindow(node, node->Windows[0]); + return; + } + + // In some circumstance we will defer creating the host window (so everything will be kept hidden), + // while the expected visible window is resizing itself. + // This is important for first-time (no ini settings restored) single window when io.ConfigDockingAlwaysTabBar is enabled, + // otherwise the node ends up using the minimum window size. Effectively those windows will take an extra frame to show up: + // N+0: Begin(): window created (with no known size), node is created + // N+1: DockNodeUpdate(): node skip creating host window / Begin(): window size applied, not visible + // N+2: DockNodeUpdate(): node can create host window / Begin(): window becomes visible + // We could remove this frame if we could reliably calculate the expected window size during node update, before the Begin() code. + // It would require a generalization of CalcWindowExpectedSize(), probably extracting code away from Begin(). + // In reality it isn't very important as user quickly ends up with size data in .ini file. + if (node->IsVisible && node->HostWindow == NULL && node->IsFloatingNode() && node->IsLeafNode()) + { + IM_ASSERT(node->Windows.Size > 0); + ImGuiWindow* ref_window = NULL; + if (node->SelectedTabId != 0) // Note that we prune single-window-node settings on .ini loading, so this is generally 0 for them! + ref_window = DockNodeFindWindowByID(node, node->SelectedTabId); + if (ref_window == NULL) + ref_window = node->Windows[0]; + if (ref_window->AutoFitFramesX > 0 || ref_window->AutoFitFramesY > 0) + { + node->State = ImGuiDockNodeState_HostWindowHiddenBecauseWindowsAreResizing; + return; + } + } + + const ImGuiDockNodeFlags node_flags = node->MergedFlags; + + // Decide if the node will have a close button and a window menu button + node->HasWindowMenuButton = (node->Windows.Size > 0) && (node_flags & ImGuiDockNodeFlags_NoWindowMenuButton) == 0; + node->HasCloseButton = false; + for (ImGuiWindow* window : node->Windows) + { + // FIXME-DOCK: Setting DockIsActive here means that for single active window in a leaf node, DockIsActive will be cleared until the next Begin() call. + node->HasCloseButton |= window->HasCloseButton; + window->DockIsActive = (node->Windows.Size > 1); + } + if (node_flags & ImGuiDockNodeFlags_NoCloseButton) + node->HasCloseButton = false; + + // Bind or create host window + ImGuiWindow* host_window = NULL; + bool beginned_into_host_window = false; + if (node->IsDockSpace()) + { + // [Explicit root dockspace node] + IM_ASSERT(node->HostWindow); + host_window = node->HostWindow; + } + else + { + // [Automatic root or child nodes] + if (node->IsRootNode() && node->IsVisible) + { + ImGuiWindow* ref_window = (node->Windows.Size > 0) ? node->Windows[0] : NULL; + + // Sync Pos + if (node->AuthorityForPos == ImGuiDataAuthority_Window && ref_window) + SetNextWindowPos(ref_window->Pos); + else if (node->AuthorityForPos == ImGuiDataAuthority_DockNode) + SetNextWindowPos(node->Pos); + + // Sync Size + if (node->AuthorityForSize == ImGuiDataAuthority_Window && ref_window) + SetNextWindowSize(ref_window->SizeFull); + else if (node->AuthorityForSize == ImGuiDataAuthority_DockNode) + SetNextWindowSize(node->Size); + + // Sync Collapsed + if (node->AuthorityForSize == ImGuiDataAuthority_Window && ref_window) + SetNextWindowCollapsed(ref_window->Collapsed); + + // Sync Viewport + if (node->AuthorityForViewport == ImGuiDataAuthority_Window && ref_window) + SetNextWindowViewport(ref_window->ViewportId); + else if (node->AuthorityForViewport == ImGuiDataAuthority_Window && node->RefViewportId != 0) + SetNextWindowViewport(node->RefViewportId); + + SetNextWindowClass(&node->WindowClass); + + // Begin into the host window + char window_label[20]; + DockNodeGetHostWindowTitle(node, window_label, IM_ARRAYSIZE(window_label)); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse | ImGuiWindowFlags_DockNodeHost; + window_flags |= ImGuiWindowFlags_NoFocusOnAppearing; + window_flags |= ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoCollapse; + window_flags |= ImGuiWindowFlags_NoTitleBar; + + SetNextWindowBgAlpha(0.0f); // Don't set ImGuiWindowFlags_NoBackground because it disables borders + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + Begin(window_label, NULL, window_flags); + PopStyleVar(); + beginned_into_host_window = true; + + host_window = g.CurrentWindow; + DockNodeSetupHostWindow(node, host_window); + host_window->DC.CursorPos = host_window->Pos; + node->Pos = host_window->Pos; + node->Size = host_window->Size; + + // We set ImGuiWindowFlags_NoFocusOnAppearing because we don't want the host window to take full focus (e.g. steal NavWindow) + // But we still it bring it to the front of display. There's no way to choose this precise behavior via window flags. + // One simple case to ponder if: window A has a toggle to create windows B/C/D. Dock B/C/D together, clear the toggle and enable it again. + // When reappearing B/C/D will request focus and be moved to the top of the display pile, but they are not linked to the dock host window + // during the frame they appear. The dock host window would keep its old display order, and the sorting in EndFrame would move B/C/D back + // after the dock host window, losing their top-most status. + if (node->HostWindow->Appearing) + BringWindowToDisplayFront(node->HostWindow); + + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_Auto; + } + else if (node->ParentNode) + { + node->HostWindow = host_window = node->ParentNode->HostWindow; + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_Auto; + } + if (node->WantMouseMove && node->HostWindow) + DockNodeStartMouseMovingWindow(node, node->HostWindow); + } + node->RefViewportId = 0; // Clear when we have a host window + + // Update focused node (the one whose title bar is highlight) within a node tree + if (node->IsSplitNode()) + IM_ASSERT(node->TabBar == NULL); + if (node->IsRootNode()) + if (ImGuiWindow* p_window = g.NavWindow ? g.NavWindow->RootWindow : NULL) + while (p_window != NULL && p_window->DockNode != NULL) + { + ImGuiDockNode* p_node = DockNodeGetRootNode(p_window->DockNode); + if (p_node == node) + { + node->LastFocusedNodeId = p_window->DockNode->ID; // Note: not using root node ID! + break; + } + p_window = p_node->HostWindow ? p_node->HostWindow->RootWindow : NULL; + } + + // Register a hit-test hole in the window unless we are currently dragging a window that is compatible with our dockspace + ImGuiDockNode* central_node = node->CentralNode; + const bool central_node_hole = node->IsRootNode() && host_window && (node_flags & ImGuiDockNodeFlags_PassthruCentralNode) != 0 && central_node != NULL && central_node->IsEmpty(); + bool central_node_hole_register_hit_test_hole = central_node_hole; + if (central_node_hole) + if (const ImGuiPayload* payload = ImGui::GetDragDropPayload()) + if (payload->IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW) && DockNodeIsDropAllowed(host_window, *(ImGuiWindow**)payload->Data)) + central_node_hole_register_hit_test_hole = false; + if (central_node_hole_register_hit_test_hole) + { + // We add a little padding to match the "resize from edges" behavior and allow grabbing the splitter easily. + // (But we only add it if there's something else on the other side of the hole, otherwise for e.g. fullscreen + // covering passthru node we'd have a gap on the edge not covered by the hole) + IM_ASSERT(node->IsDockSpace()); // We cannot pass this flag without the DockSpace() api. Testing this because we also setup the hole in host_window->ParentNode + ImGuiDockNode* root_node = DockNodeGetRootNode(central_node); + ImRect root_rect(root_node->Pos, root_node->Pos + root_node->Size); + ImRect hole_rect(central_node->Pos, central_node->Pos + central_node->Size); + if (hole_rect.Min.x > root_rect.Min.x) { hole_rect.Min.x += g.WindowsBorderHoverPadding; } + if (hole_rect.Max.x < root_rect.Max.x) { hole_rect.Max.x -= g.WindowsBorderHoverPadding; } + if (hole_rect.Min.y > root_rect.Min.y) { hole_rect.Min.y += g.WindowsBorderHoverPadding; } + if (hole_rect.Max.y < root_rect.Max.y) { hole_rect.Max.y -= g.WindowsBorderHoverPadding; } + //GetForegroundDrawList()->AddRect(hole_rect.Min, hole_rect.Max, IM_COL32(255, 0, 0, 255)); + if (central_node_hole && !hole_rect.IsInverted()) + { + SetWindowHitTestHole(host_window, hole_rect.Min, hole_rect.Max - hole_rect.Min); + if (host_window->ParentWindow) + SetWindowHitTestHole(host_window->ParentWindow, hole_rect.Min, hole_rect.Max - hole_rect.Min); + } + } + + // Update position/size, process and draw resizing splitters + if (node->IsRootNode() && host_window) + { + DockNodeTreeUpdatePosSize(node, host_window->Pos, host_window->Size); + PushStyleColor(ImGuiCol_Separator, g.Style.Colors[ImGuiCol_Border]); + PushStyleColor(ImGuiCol_SeparatorActive, g.Style.Colors[ImGuiCol_ResizeGripActive]); + PushStyleColor(ImGuiCol_SeparatorHovered, g.Style.Colors[ImGuiCol_ResizeGripHovered]); + DockNodeTreeUpdateSplitter(node); + PopStyleColor(3); + } + + // Draw empty node background (currently can only be the Central Node) + if (host_window && node->IsEmpty() && node->IsVisible) + { + host_window->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG); + node->LastBgColor = (node_flags & ImGuiDockNodeFlags_PassthruCentralNode) ? 0 : GetColorU32(ImGuiCol_DockingEmptyBg); + if (node->LastBgColor != 0) + host_window->DrawList->AddRectFilled(node->Pos, node->Pos + node->Size, node->LastBgColor); + node->IsBgDrawnThisFrame = true; + } + + // Draw whole dockspace background if ImGuiDockNodeFlags_PassthruCentralNode if set. + // We need to draw a background at the root level if requested by ImGuiDockNodeFlags_PassthruCentralNode, but we will only know the correct pos/size + // _after_ processing the resizing splitters. So we are using the DrawList channel splitting facility to submit drawing primitives out of order! + const bool render_dockspace_bg = node->IsRootNode() && host_window && (node_flags & ImGuiDockNodeFlags_PassthruCentralNode) != 0; + if (render_dockspace_bg && node->IsVisible) + { + host_window->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_BG); + if (central_node_hole) + RenderRectFilledWithHole(host_window->DrawList, node->Rect(), central_node->Rect(), GetColorU32(ImGuiCol_WindowBg), 0.0f); + else + host_window->DrawList->AddRectFilled(node->Pos, node->Pos + node->Size, GetColorU32(ImGuiCol_WindowBg), 0.0f); + } + + // Draw and populate Tab Bar + if (host_window) + host_window->DrawList->ChannelsSetCurrent(DOCKING_HOST_DRAW_CHANNEL_FG); + if (host_window && node->Windows.Size > 0) + { + DockNodeUpdateTabBar(node, host_window); + } + else + { + node->WantCloseAll = false; + node->WantCloseTabId = 0; + node->IsFocused = false; + } + if (node->TabBar && node->TabBar->SelectedTabId) + node->SelectedTabId = node->TabBar->SelectedTabId; + else if (node->Windows.Size > 0) + node->SelectedTabId = node->Windows[0]->TabId; + + // Draw payload drop target + if (host_window && node->IsVisible) + if (node->IsRootNode() && (g.MovingWindow == NULL || g.MovingWindow->RootWindowDockTree != host_window)) + BeginDockableDragDropTarget(host_window); + + // We update this after DockNodeUpdateTabBar() + node->LastFrameActive = g.FrameCount; + + // Recurse into children + // FIXME-DOCK FIXME-OPT: Should not need to recurse into children + if (host_window) + { + if (node->ChildNodes[0]) + DockNodeUpdate(node->ChildNodes[0]); + if (node->ChildNodes[1]) + DockNodeUpdate(node->ChildNodes[1]); + + // Render outer borders last (after the tab bar) + if (node->IsRootNode()) + RenderWindowOuterBorders(host_window); + } + + // End host window + if (beginned_into_host_window) //-V1020 + End(); +} + +// Compare TabItem nodes given the last known DockOrder (will persist in .ini file as hint), used to sort tabs when multiple tabs are added on the same frame. +static int IMGUI_CDECL TabItemComparerByDockOrder(const void* lhs, const void* rhs) +{ + ImGuiWindow* a = ((const ImGuiTabItem*)lhs)->Window; + ImGuiWindow* b = ((const ImGuiTabItem*)rhs)->Window; + if (int d = ((a->DockOrder == -1) ? INT_MAX : a->DockOrder) - ((b->DockOrder == -1) ? INT_MAX : b->DockOrder)) + return d; + return (a->BeginOrderWithinContext - b->BeginOrderWithinContext); +} + +// Default handler for g.DockNodeWindowMenuHandler(): display the list of windows for a given dock-node. +// This is exceptionally stored in a function pointer to also user applications to tweak this menu (undocumented) +// Custom overrides may want to decorate, group, sort entries. +// Please note those are internal structures: if you copy this expect occasional breakage. +// (if you don't need to modify the "Tabs.Size == 1" behavior/path it is recommend you call this function in your handler) +void ImGui::DockNodeWindowMenuHandler_Default(ImGuiContext* ctx, ImGuiDockNode* node, ImGuiTabBar* tab_bar) +{ + IM_UNUSED(ctx); + if (tab_bar->Tabs.Size == 1) + { + // "Hide tab bar" option. Being one of our rare user-facing string we pull it from a table. + if (MenuItem(LocalizeGetMsg(ImGuiLocKey_DockingHideTabBar), NULL, node->IsHiddenTabBar())) + node->WantHiddenTabBarToggle = true; + } + else + { + // Display a selectable list of windows in this docking node + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + if (tab->Flags & ImGuiTabItemFlags_Button) + continue; + if (Selectable(TabBarGetTabName(tab_bar, tab), tab->ID == tab_bar->SelectedTabId)) + TabBarQueueFocus(tab_bar, tab); + SameLine(); + Text(" "); + } + } +} + +static void ImGui::DockNodeWindowMenuUpdate(ImGuiDockNode* node, ImGuiTabBar* tab_bar) +{ + // Try to position the menu so it is more likely to stays within the same viewport + ImGuiContext& g = *GImGui; + if (g.Style.WindowMenuButtonPosition == ImGuiDir_Left) + SetNextWindowPos(ImVec2(node->Pos.x, node->Pos.y + GetFrameHeight()), ImGuiCond_Always, ImVec2(0.0f, 0.0f)); + else + SetNextWindowPos(ImVec2(node->Pos.x + node->Size.x, node->Pos.y + GetFrameHeight()), ImGuiCond_Always, ImVec2(1.0f, 0.0f)); + if (BeginPopup("#WindowMenu")) + { + node->IsFocused = true; + g.DockNodeWindowMenuHandler(&g, node, tab_bar); + EndPopup(); + } +} + +// User helper to append/amend into a dock node tab bar. Most commonly used to add e.g. a "+" button. +bool ImGui::DockNodeBeginAmendTabBar(ImGuiDockNode* node) +{ + if (node->TabBar == NULL || node->HostWindow == NULL) + return false; + if (node->MergedFlags & ImGuiDockNodeFlags_KeepAliveOnly) + return false; + if (node->TabBar->ID == 0) + return false; + Begin(node->HostWindow->Name); + PushOverrideID(node->ID); + bool ret = BeginTabBarEx(node->TabBar, node->TabBar->BarRect, node->TabBar->Flags); + IM_UNUSED(ret); + IM_ASSERT(ret); + return true; +} + +void ImGui::DockNodeEndAmendTabBar() +{ + EndTabBar(); + PopID(); + End(); +} + +static bool IsDockNodeTitleBarHighlighted(ImGuiDockNode* node, ImGuiDockNode* root_node) +{ + // CTRL+Tab highlight (only highlighting leaf node, not whole hierarchy) + ImGuiContext& g = *GImGui; + if (g.NavWindowingTarget) + return (g.NavWindowingTarget->DockNode == node); + + // FIXME-DOCKING: May want alternative to treat central node void differently? e.g. if (g.NavWindow == host_window) + if (g.NavWindow && root_node->LastFocusedNodeId == node->ID) + { + // FIXME: This could all be backed in RootWindowForTitleBarHighlight? Probably need to reorganize for both dock nodes + other RootWindowForTitleBarHighlight users (not-node) + ImGuiWindow* parent_window = g.NavWindow->RootWindow; + while (parent_window->Flags & ImGuiWindowFlags_ChildMenu) + parent_window = parent_window->ParentWindow->RootWindow; + ImGuiDockNode* start_parent_node = parent_window->DockNodeAsHost ? parent_window->DockNodeAsHost : parent_window->DockNode; + for (ImGuiDockNode* parent_node = start_parent_node; parent_node != NULL; parent_node = parent_node->HostWindow ? parent_node->HostWindow->RootWindow->DockNode : NULL) + if ((parent_node = ImGui::DockNodeGetRootNode(parent_node)) == root_node) + return true; + } + return false; +} + +// Submit the tab bar corresponding to a dock node and various housekeeping details. +static void ImGui::DockNodeUpdateTabBar(ImGuiDockNode* node, ImGuiWindow* host_window) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + + const bool node_was_active = (node->LastFrameActive + 1 == g.FrameCount); + const bool closed_all = node->WantCloseAll && node_was_active; + const ImGuiID closed_one = node->WantCloseTabId && node_was_active; + node->WantCloseAll = false; + node->WantCloseTabId = 0; + + // Decide if we should use a focused title bar color + bool is_focused = false; + ImGuiDockNode* root_node = DockNodeGetRootNode(node); + if (IsDockNodeTitleBarHighlighted(node, root_node)) + is_focused = true; + + // Hidden tab bar will show a triangle on the upper-left (in Begin) + if (node->IsHiddenTabBar() || node->IsNoTabBar()) + { + node->VisibleWindow = (node->Windows.Size > 0) ? node->Windows[0] : NULL; + node->IsFocused = is_focused; + if (is_focused) + node->LastFrameFocused = g.FrameCount; + if (node->VisibleWindow) + { + // Notify root of visible window (used to display title in OS task bar) + if (is_focused || root_node->VisibleWindow == NULL) + root_node->VisibleWindow = node->VisibleWindow; + if (node->TabBar) + node->TabBar->VisibleTabId = node->VisibleWindow->TabId; + } + return; + } + + // Move ourselves to the Menu layer (so we can be accessed by tapping Alt) + undo SkipItems flag in order to draw over the title bar even if the window is collapsed + bool backup_skip_item = host_window->SkipItems; + if (!node->IsDockSpace()) + { + host_window->SkipItems = false; + host_window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; + } + + // Use PushOverrideID() instead of PushID() to use the node id _without_ the host window ID. + // This is to facilitate computing those ID from the outside, and will affect more or less only the ID of the collapse button, popup and tabs, + // as docked windows themselves will override the stack with their own root ID. + PushOverrideID(node->ID); + ImGuiTabBar* tab_bar = node->TabBar; + bool tab_bar_is_recreated = (tab_bar == NULL); // Tab bar are automatically destroyed when a node gets hidden + if (tab_bar == NULL) + { + DockNodeAddTabBar(node); + tab_bar = node->TabBar; + } + + ImGuiID focus_tab_id = 0; + node->IsFocused = is_focused; + + const ImGuiDockNodeFlags node_flags = node->MergedFlags; + const bool has_window_menu_button = (node_flags & ImGuiDockNodeFlags_NoWindowMenuButton) == 0 && (style.WindowMenuButtonPosition != ImGuiDir_None); + + // In a dock node, the Collapse Button turns into the Window Menu button. + // FIXME-DOCK FIXME-OPT: Could we recycle popups id across multiple dock nodes? + if (has_window_menu_button && IsPopupOpen("#WindowMenu")) + { + ImGuiID next_selected_tab_id = tab_bar->NextSelectedTabId; + DockNodeWindowMenuUpdate(node, tab_bar); + if (tab_bar->NextSelectedTabId != 0 && tab_bar->NextSelectedTabId != next_selected_tab_id) + focus_tab_id = tab_bar->NextSelectedTabId; + is_focused |= node->IsFocused; + } + + // Layout + ImRect title_bar_rect, tab_bar_rect; + ImVec2 window_menu_button_pos; + ImVec2 close_button_pos; + DockNodeCalcTabBarLayout(node, &title_bar_rect, &tab_bar_rect, &window_menu_button_pos, &close_button_pos); + + // Submit new tabs, they will be added as Unsorted and sorted below based on relative DockOrder value. + const int tabs_count_old = tab_bar->Tabs.Size; + for (int window_n = 0; window_n < node->Windows.Size; window_n++) + { + ImGuiWindow* window = node->Windows[window_n]; + if (TabBarFindTabByID(tab_bar, window->TabId) == NULL) + TabBarAddTab(tab_bar, ImGuiTabItemFlags_Unsorted, window); + } + + // Title bar + if (is_focused) + node->LastFrameFocused = g.FrameCount; + ImU32 title_bar_col = GetColorU32(host_window->Collapsed ? ImGuiCol_TitleBgCollapsed : is_focused ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg); + ImDrawFlags rounding_flags = CalcRoundingFlagsForRectInRect(title_bar_rect, host_window->Rect(), g.Style.DockingSeparatorSize); + host_window->DrawList->AddRectFilled(title_bar_rect.Min, title_bar_rect.Max, title_bar_col, host_window->WindowRounding, rounding_flags); + + // Docking/Collapse button + if (has_window_menu_button) + { + if (CollapseButton(host_window->GetID("#COLLAPSE"), window_menu_button_pos, node)) // == DockNodeGetWindowMenuButtonId(node) + OpenPopup("#WindowMenu"); + if (IsItemActive()) + focus_tab_id = tab_bar->SelectedTabId; + if (IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal) && g.HoveredIdTimer > 0.5f) + SetTooltip("%s", LocalizeGetMsg(ImGuiLocKey_DockingDragToUndockOrMoveNode)); + } + + // If multiple tabs are appearing on the same frame, sort them based on their persistent DockOrder value + int tabs_unsorted_start = tab_bar->Tabs.Size; + for (int tab_n = tab_bar->Tabs.Size - 1; tab_n >= 0 && (tab_bar->Tabs[tab_n].Flags & ImGuiTabItemFlags_Unsorted); tab_n--) + { + // FIXME-DOCK: Consider only clearing the flag after the tab has been alive for a few consecutive frames, allowing late comers to not break sorting? + tab_bar->Tabs[tab_n].Flags &= ~ImGuiTabItemFlags_Unsorted; + tabs_unsorted_start = tab_n; + } + if (tab_bar->Tabs.Size > tabs_unsorted_start) + { + IMGUI_DEBUG_LOG_DOCKING("[docking] In node 0x%08X: %d new appearing tabs:%s\n", node->ID, tab_bar->Tabs.Size - tabs_unsorted_start, (tab_bar->Tabs.Size > tabs_unsorted_start + 1) ? " (will sort)" : ""); + for (int tab_n = tabs_unsorted_start; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + IM_UNUSED(tab); + IMGUI_DEBUG_LOG_DOCKING("[docking] - Tab 0x%08X '%s' Order %d\n", tab->ID, TabBarGetTabName(tab_bar, tab), tab->Window ? tab->Window->DockOrder : -1); + } + IMGUI_DEBUG_LOG_DOCKING("[docking] SelectedTabId = 0x%08X, NavWindow->TabId = 0x%08X\n", node->SelectedTabId, g.NavWindow ? g.NavWindow->TabId : -1); + if (tab_bar->Tabs.Size > tabs_unsorted_start + 1) + ImQsort(tab_bar->Tabs.Data + tabs_unsorted_start, tab_bar->Tabs.Size - tabs_unsorted_start, sizeof(ImGuiTabItem), TabItemComparerByDockOrder); + } + + // Apply NavWindow focus back to the tab bar + if (g.NavWindow && g.NavWindow->RootWindow->DockNode == node) + tab_bar->SelectedTabId = g.NavWindow->RootWindow->TabId; + + // Selected newly added tabs, or persistent tab ID if the tab bar was just recreated + if (tab_bar_is_recreated && TabBarFindTabByID(tab_bar, node->SelectedTabId) != NULL) + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = node->SelectedTabId; + else if (tab_bar->Tabs.Size > tabs_count_old) + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = tab_bar->Tabs.back().Window->TabId; + + // Begin tab bar + ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_AutoSelectNewTabs; // | ImGuiTabBarFlags_NoTabListScrollingButtons); + tab_bar_flags |= ImGuiTabBarFlags_SaveSettings | ImGuiTabBarFlags_DockNode;// | ImGuiTabBarFlags_FittingPolicyScroll; + tab_bar_flags |= ImGuiTabBarFlags_DrawSelectedOverline; + if (!host_window->Collapsed && is_focused) + tab_bar_flags |= ImGuiTabBarFlags_IsFocused; + tab_bar->ID = GetID("#TabBar"); + tab_bar->SeparatorMinX = node->Pos.x + host_window->WindowBorderSize; // Separator cover the whole node width + tab_bar->SeparatorMaxX = node->Pos.x + node->Size.x - host_window->WindowBorderSize; + BeginTabBarEx(tab_bar, tab_bar_rect, tab_bar_flags); + //host_window->DrawList->AddRect(tab_bar_rect.Min, tab_bar_rect.Max, IM_COL32(255,0,255,255)); + + // Backup style colors + ImVec4 backup_style_cols[ImGuiWindowDockStyleCol_COUNT]; + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + backup_style_cols[color_n] = g.Style.Colors[GWindowDockStyleColors[color_n]]; + + // Submit actual tabs + node->VisibleWindow = NULL; + for (int window_n = 0; window_n < node->Windows.Size; window_n++) + { + ImGuiWindow* window = node->Windows[window_n]; + if ((closed_all || closed_one == window->TabId) && window->HasCloseButton && !(window->Flags & ImGuiWindowFlags_UnsavedDocument)) + continue; + if (window->LastFrameActive + 1 >= g.FrameCount || !node_was_active) + { + ImGuiTabItemFlags tab_item_flags = 0; + tab_item_flags |= window->WindowClass.TabItemFlagsOverrideSet; + if (window->Flags & ImGuiWindowFlags_UnsavedDocument) + tab_item_flags |= ImGuiTabItemFlags_UnsavedDocument; + if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) + tab_item_flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; + + // Apply stored style overrides for the window + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + g.Style.Colors[GWindowDockStyleColors[color_n]] = ColorConvertU32ToFloat4(window->DockStyle.Colors[color_n]); + + // Note that TabItemEx() calls TabBarCalcTabID() so our tab item ID will ignore the current ID stack (rightly so) + bool tab_open = true; + TabItemEx(tab_bar, window->Name, window->HasCloseButton ? &tab_open : NULL, tab_item_flags, window); + if (!tab_open) + node->WantCloseTabId = window->TabId; + if (tab_bar->VisibleTabId == window->TabId) + node->VisibleWindow = window; + + // Store last item data so it can be queried with IsItemXXX functions after the user Begin() call + window->DC.DockTabItemStatusFlags = g.LastItemData.StatusFlags; + window->DC.DockTabItemRect = g.LastItemData.Rect; + + // Update navigation ID on menu layer + if (g.NavWindow && g.NavWindow->RootWindow == window && (window->DC.NavLayersActiveMask & (1 << ImGuiNavLayer_Menu)) == 0) + host_window->NavLastIds[1] = window->TabId; + } + } + + // Restore style colors + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + g.Style.Colors[GWindowDockStyleColors[color_n]] = backup_style_cols[color_n]; + + // Notify root of visible window (used to display title in OS task bar) + if (node->VisibleWindow) + if (is_focused || root_node->VisibleWindow == NULL) + root_node->VisibleWindow = node->VisibleWindow; + + // Close button (after VisibleWindow was updated) + // Note that VisibleWindow may have been overrided by CTRL+Tabbing, so VisibleWindow->TabId may be != from tab_bar->SelectedTabId + const bool close_button_is_enabled = node->HasCloseButton && node->VisibleWindow && node->VisibleWindow->HasCloseButton; + const bool close_button_is_visible = node->HasCloseButton; + //const bool close_button_is_visible = close_button_is_enabled; // Most people would expect this behavior of not even showing the button (leaving a hole since we can't claim that space as other windows in the tba bar have one) + if (close_button_is_visible) + { + if (!close_button_is_enabled) + { + PushItemFlag(ImGuiItemFlags_Disabled, true); + PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_Text] * ImVec4(1.0f,1.0f,1.0f,0.4f)); + } + if (CloseButton(host_window->GetID("#CLOSE"), close_button_pos)) + { + node->WantCloseAll = true; + for (int n = 0; n < tab_bar->Tabs.Size; n++) + TabBarCloseTab(tab_bar, &tab_bar->Tabs[n]); + } + //if (IsItemActive()) + // focus_tab_id = tab_bar->SelectedTabId; + if (!close_button_is_enabled) + { + PopStyleColor(); + PopItemFlag(); + } + } + + // When clicking on the title bar outside of tabs, we still focus the selected tab for that node + // FIXME: TabItems submitted earlier use AllowItemOverlap so we manually perform a more specific test for now (hovered || held) in order to not cover them. + ImGuiID title_bar_id = host_window->GetID("#TITLEBAR"); + if (g.HoveredId == 0 || g.HoveredId == title_bar_id || g.ActiveId == title_bar_id) + { + // AllowOverlap mode required for appending into dock node tab bar, + // otherwise dragging window will steal HoveredId and amended tabs cannot get them. + bool held; + KeepAliveID(title_bar_id); + ButtonBehavior(title_bar_rect, title_bar_id, NULL, &held, ImGuiButtonFlags_AllowOverlap); + if (g.HoveredId == title_bar_id) + { + g.LastItemData.ID = title_bar_id; + } + if (held) + { + if (IsMouseClicked(0)) + focus_tab_id = tab_bar->SelectedTabId; + + // Forward moving request to selected window + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId)) + StartMouseMovingWindowOrNode(tab->Window ? tab->Window : node->HostWindow, node, false); // Undock from tab bar empty space + } + } + + // Forward focus from host node to selected window + //if (is_focused && g.NavWindow == host_window && !g.NavWindowingTarget) + // focus_tab_id = tab_bar->SelectedTabId; + + // When clicked on a tab we requested focus to the docked child + // This overrides the value set by "forward focus from host node to selected window". + if (tab_bar->NextSelectedTabId) + focus_tab_id = tab_bar->NextSelectedTabId; + + // Apply navigation focus + if (focus_tab_id != 0) + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, focus_tab_id)) + if (tab->Window) + { + FocusWindow(tab->Window); + NavInitWindow(tab->Window, false); + } + + EndTabBar(); + PopID(); + + // Restore SkipItems flag + if (!node->IsDockSpace()) + { + host_window->DC.NavLayerCurrent = ImGuiNavLayer_Main; + host_window->SkipItems = backup_skip_item; + } +} + +static void ImGui::DockNodeAddTabBar(ImGuiDockNode* node) +{ + IM_ASSERT(node->TabBar == NULL); + node->TabBar = IM_NEW(ImGuiTabBar); +} + +static void ImGui::DockNodeRemoveTabBar(ImGuiDockNode* node) +{ + if (node->TabBar == NULL) + return; + IM_DELETE(node->TabBar); + node->TabBar = NULL; +} + +static bool DockNodeIsDropAllowedOne(ImGuiWindow* payload, ImGuiWindow* host_window) +{ + if (host_window->DockNodeAsHost && host_window->DockNodeAsHost->IsDockSpace() && payload->BeginOrderWithinContext < host_window->BeginOrderWithinContext) + return false; + + ImGuiWindowClass* host_class = host_window->DockNodeAsHost ? &host_window->DockNodeAsHost->WindowClass : &host_window->WindowClass; + ImGuiWindowClass* payload_class = &payload->WindowClass; + if (host_class->ClassId != payload_class->ClassId) + { + bool pass = false; + if (host_class->ClassId != 0 && host_class->DockingAllowUnclassed && payload_class->ClassId == 0) + pass = true; + if (payload_class->ClassId != 0 && payload_class->DockingAllowUnclassed && host_class->ClassId == 0) + pass = true; + if (!pass) + return false; + } + + // Prevent docking any window created above a popup + // Technically we should support it (e.g. in the case of a long-lived modal window that had fancy docking features), + // by e.g. adding a 'if (!ImGui::IsWindowWithinBeginStackOf(host_window, popup_window))' test. + // But it would requires more work on our end because the dock host windows is technically created in NewFrame() + // and our ->ParentXXX and ->RootXXX pointers inside windows are currently mislading or lacking. + ImGuiContext& g = *GImGui; + for (int i = g.OpenPopupStack.Size - 1; i >= 0; i--) + if (ImGuiWindow* popup_window = g.OpenPopupStack[i].Window) + if (ImGui::IsWindowWithinBeginStackOf(payload, popup_window)) // Payload is created from within a popup begin stack. + return false; + + return true; +} + +static bool ImGui::DockNodeIsDropAllowed(ImGuiWindow* host_window, ImGuiWindow* root_payload) +{ + if (root_payload->DockNodeAsHost && root_payload->DockNodeAsHost->IsSplitNode()) // FIXME-DOCK: Missing filtering + return true; + + const int payload_count = root_payload->DockNodeAsHost ? root_payload->DockNodeAsHost->Windows.Size : 1; + for (int payload_n = 0; payload_n < payload_count; payload_n++) + { + ImGuiWindow* payload = root_payload->DockNodeAsHost ? root_payload->DockNodeAsHost->Windows[payload_n] : root_payload; + if (DockNodeIsDropAllowedOne(payload, host_window)) + return true; + } + return false; +} + +// window menu button == collapse button when not in a dock node. +// FIXME: This is similar to RenderWindowTitleBarContents(), may want to share code. +static void ImGui::DockNodeCalcTabBarLayout(const ImGuiDockNode* node, ImRect* out_title_rect, ImRect* out_tab_bar_rect, ImVec2* out_window_menu_button_pos, ImVec2* out_close_button_pos) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + + ImRect r = ImRect(node->Pos.x, node->Pos.y, node->Pos.x + node->Size.x, node->Pos.y + g.FontSize + g.Style.FramePadding.y * 2.0f); + if (out_title_rect) { *out_title_rect = r; } + + r.Min.x += style.WindowBorderSize; + r.Max.x -= style.WindowBorderSize; + + float button_sz = g.FontSize; + r.Min.x += style.FramePadding.x; + r.Max.x -= style.FramePadding.x; + ImVec2 window_menu_button_pos = ImVec2(r.Min.x, r.Min.y + style.FramePadding.y); + if (node->HasCloseButton) + { + if (out_close_button_pos) *out_close_button_pos = ImVec2(r.Max.x - button_sz, r.Min.y + style.FramePadding.y); + r.Max.x -= button_sz + style.ItemInnerSpacing.x; + } + if (node->HasWindowMenuButton && style.WindowMenuButtonPosition == ImGuiDir_Left) + { + r.Min.x += button_sz + style.ItemInnerSpacing.x; + } + else if (node->HasWindowMenuButton && style.WindowMenuButtonPosition == ImGuiDir_Right) + { + window_menu_button_pos = ImVec2(r.Max.x - button_sz, r.Min.y + style.FramePadding.y); + r.Max.x -= button_sz + style.ItemInnerSpacing.x; + } + if (out_tab_bar_rect) { *out_tab_bar_rect = r; } + if (out_window_menu_button_pos) { *out_window_menu_button_pos = window_menu_button_pos; } +} + +void ImGui::DockNodeCalcSplitRects(ImVec2& pos_old, ImVec2& size_old, ImVec2& pos_new, ImVec2& size_new, ImGuiDir dir, ImVec2 size_new_desired) +{ + ImGuiContext& g = *GImGui; + const float dock_spacing = g.Style.ItemInnerSpacing.x; + const ImGuiAxis axis = (dir == ImGuiDir_Left || dir == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + pos_new[axis ^ 1] = pos_old[axis ^ 1]; + size_new[axis ^ 1] = size_old[axis ^ 1]; + + // Distribute size on given axis (with a desired size or equally) + const float w_avail = size_old[axis] - dock_spacing; + if (size_new_desired[axis] > 0.0f && size_new_desired[axis] <= w_avail * 0.5f) + { + size_new[axis] = size_new_desired[axis]; + size_old[axis] = IM_TRUNC(w_avail - size_new[axis]); + } + else + { + size_new[axis] = IM_TRUNC(w_avail * 0.5f); + size_old[axis] = IM_TRUNC(w_avail - size_new[axis]); + } + + // Position each node + if (dir == ImGuiDir_Right || dir == ImGuiDir_Down) + { + pos_new[axis] = pos_old[axis] + size_old[axis] + dock_spacing; + } + else if (dir == ImGuiDir_Left || dir == ImGuiDir_Up) + { + pos_new[axis] = pos_old[axis]; + pos_old[axis] = pos_new[axis] + size_new[axis] + dock_spacing; + } +} + +// Retrieve the drop rectangles for a given direction or for the center + perform hit testing. +bool ImGui::DockNodeCalcDropRectsAndTestMousePos(const ImRect& parent, ImGuiDir dir, ImRect& out_r, bool outer_docking, ImVec2* test_mouse_pos) +{ + ImGuiContext& g = *GImGui; + + const float parent_smaller_axis = ImMin(parent.GetWidth(), parent.GetHeight()); + const float hs_for_central_nodes = ImMin(g.FontSize * 1.5f, ImMax(g.FontSize * 0.5f, parent_smaller_axis / 8.0f)); + float hs_w; // Half-size, longer axis + float hs_h; // Half-size, smaller axis + ImVec2 off; // Distance from edge or center + if (outer_docking) + { + //hs_w = ImTrunc(ImClamp(parent_smaller_axis - hs_for_central_nodes * 4.0f, g.FontSize * 0.5f, g.FontSize * 8.0f)); + //hs_h = ImTrunc(hs_w * 0.15f); + //off = ImVec2(ImTrunc(parent.GetWidth() * 0.5f - GetFrameHeightWithSpacing() * 1.4f - hs_h), ImTrunc(parent.GetHeight() * 0.5f - GetFrameHeightWithSpacing() * 1.4f - hs_h)); + hs_w = ImTrunc(hs_for_central_nodes * 1.50f); + hs_h = ImTrunc(hs_for_central_nodes * 0.80f); + off = ImTrunc(ImVec2(parent.GetWidth() * 0.5f - hs_h, parent.GetHeight() * 0.5f - hs_h)); + } + else + { + hs_w = ImTrunc(hs_for_central_nodes); + hs_h = ImTrunc(hs_for_central_nodes * 0.90f); + off = ImTrunc(ImVec2(hs_w * 2.40f, hs_w * 2.40f)); + } + + ImVec2 c = ImTrunc(parent.GetCenter()); + if (dir == ImGuiDir_None) { out_r = ImRect(c.x - hs_w, c.y - hs_w, c.x + hs_w, c.y + hs_w); } + else if (dir == ImGuiDir_Up) { out_r = ImRect(c.x - hs_w, c.y - off.y - hs_h, c.x + hs_w, c.y - off.y + hs_h); } + else if (dir == ImGuiDir_Down) { out_r = ImRect(c.x - hs_w, c.y + off.y - hs_h, c.x + hs_w, c.y + off.y + hs_h); } + else if (dir == ImGuiDir_Left) { out_r = ImRect(c.x - off.x - hs_h, c.y - hs_w, c.x - off.x + hs_h, c.y + hs_w); } + else if (dir == ImGuiDir_Right) { out_r = ImRect(c.x + off.x - hs_h, c.y - hs_w, c.x + off.x + hs_h, c.y + hs_w); } + + if (test_mouse_pos == NULL) + return false; + + ImRect hit_r = out_r; + if (!outer_docking) + { + // Custom hit testing for the 5-way selection, designed to reduce flickering when moving diagonally between sides + hit_r.Expand(ImTrunc(hs_w * 0.30f)); + ImVec2 mouse_delta = (*test_mouse_pos - c); + float mouse_delta_len2 = ImLengthSqr(mouse_delta); + float r_threshold_center = hs_w * 1.4f; + float r_threshold_sides = hs_w * (1.4f + 1.2f); + if (mouse_delta_len2 < r_threshold_center * r_threshold_center) + return (dir == ImGuiDir_None); + if (mouse_delta_len2 < r_threshold_sides * r_threshold_sides) + return (dir == ImGetDirQuadrantFromDelta(mouse_delta.x, mouse_delta.y)); + } + return hit_r.Contains(*test_mouse_pos); +} + +// host_node may be NULL if the window doesn't have a DockNode already. +// FIXME-DOCK: This is misnamed since it's also doing the filtering. +static void ImGui::DockNodePreviewDockSetup(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDockPreviewData* data, bool is_explicit_target, bool is_outer_docking) +{ + ImGuiContext& g = *GImGui; + + // There is an edge case when docking into a dockspace which only has inactive nodes. + // In this case DockNodeTreeFindNodeByPos() will have selected a leaf node which is inactive. + // Because the inactive leaf node doesn't have proper pos/size yet, we'll use the root node as reference. + if (payload_node == NULL) + payload_node = payload_window->DockNodeAsHost; + ImGuiDockNode* ref_node_for_rect = (host_node && !host_node->IsVisible) ? DockNodeGetRootNode(host_node) : host_node; + if (ref_node_for_rect) + IM_ASSERT(ref_node_for_rect->IsVisible == true); + + // Filter, figure out where we are allowed to dock + ImGuiDockNodeFlags src_node_flags = payload_node ? payload_node->MergedFlags : payload_window->WindowClass.DockNodeFlagsOverrideSet; + ImGuiDockNodeFlags dst_node_flags = host_node ? host_node->MergedFlags : host_window->WindowClass.DockNodeFlagsOverrideSet; + data->IsCenterAvailable = true; + if (is_outer_docking) + data->IsCenterAvailable = false; + else if (dst_node_flags & ImGuiDockNodeFlags_NoDockingOverMe) + data->IsCenterAvailable = false; + else if (host_node && (dst_node_flags & ImGuiDockNodeFlags_NoDockingOverCentralNode) && host_node->IsCentralNode()) + data->IsCenterAvailable = false; + else if ((!host_node || !host_node->IsEmpty()) && payload_node && payload_node->IsSplitNode() && (payload_node->OnlyNodeWithWindows == NULL)) // Is _visibly_ split? + data->IsCenterAvailable = false; + else if ((src_node_flags & ImGuiDockNodeFlags_NoDockingOverOther) && (!host_node || !host_node->IsEmpty())) + data->IsCenterAvailable = false; + else if ((src_node_flags & ImGuiDockNodeFlags_NoDockingOverEmpty) && host_node && host_node->IsEmpty()) + data->IsCenterAvailable = false; + + data->IsSidesAvailable = true; + if ((dst_node_flags & ImGuiDockNodeFlags_NoDockingSplit) || g.IO.ConfigDockingNoSplit) + data->IsSidesAvailable = false; + else if (!is_outer_docking && host_node && host_node->ParentNode == NULL && host_node->IsCentralNode()) + data->IsSidesAvailable = false; + else if (src_node_flags & ImGuiDockNodeFlags_NoDockingSplitOther) + data->IsSidesAvailable = false; + + // Build a tentative future node (reuse same structure because it is practical. Shape will be readjusted when previewing a split) + data->FutureNode.HasCloseButton = (host_node ? host_node->HasCloseButton : host_window->HasCloseButton) || (payload_window->HasCloseButton); + data->FutureNode.HasWindowMenuButton = host_node ? true : ((host_window->Flags & ImGuiWindowFlags_NoCollapse) == 0); + data->FutureNode.Pos = ref_node_for_rect ? ref_node_for_rect->Pos : host_window->Pos; + data->FutureNode.Size = ref_node_for_rect ? ref_node_for_rect->Size : host_window->Size; + + // Calculate drop shapes geometry for allowed splitting directions + IM_ASSERT(ImGuiDir_None == -1); + data->SplitNode = host_node; + data->SplitDir = ImGuiDir_None; + data->IsSplitDirExplicit = false; + if (!host_window->Collapsed) + for (int dir = ImGuiDir_None; dir < ImGuiDir_COUNT; dir++) + { + if (dir == ImGuiDir_None && !data->IsCenterAvailable) + continue; + if (dir != ImGuiDir_None && !data->IsSidesAvailable) + continue; + if (DockNodeCalcDropRectsAndTestMousePos(data->FutureNode.Rect(), (ImGuiDir)dir, data->DropRectsDraw[dir+1], is_outer_docking, &g.IO.MousePos)) + { + data->SplitDir = (ImGuiDir)dir; + data->IsSplitDirExplicit = true; + } + } + + // When docking without holding Shift, we only allow and preview docking when hovering over a drop rect or over the title bar + data->IsDropAllowed = (data->SplitDir != ImGuiDir_None) || (data->IsCenterAvailable); + if (!is_explicit_target && !data->IsSplitDirExplicit && !g.IO.ConfigDockingWithShift) + data->IsDropAllowed = false; + + // Calculate split area + data->SplitRatio = 0.0f; + if (data->SplitDir != ImGuiDir_None) + { + ImGuiDir split_dir = data->SplitDir; + ImGuiAxis split_axis = (split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Right) ? ImGuiAxis_X : ImGuiAxis_Y; + ImVec2 pos_new, pos_old = data->FutureNode.Pos; + ImVec2 size_new, size_old = data->FutureNode.Size; + DockNodeCalcSplitRects(pos_old, size_old, pos_new, size_new, split_dir, payload_window->Size); + + // Calculate split ratio so we can pass it down the docking request + float split_ratio = ImSaturate(size_new[split_axis] / data->FutureNode.Size[split_axis]); + data->FutureNode.Pos = pos_new; + data->FutureNode.Size = size_new; + data->SplitRatio = (split_dir == ImGuiDir_Right || split_dir == ImGuiDir_Down) ? (1.0f - split_ratio) : (split_ratio); + } +} + +static void ImGui::DockNodePreviewDockRender(ImGuiWindow* host_window, ImGuiDockNode* host_node, ImGuiWindow* root_payload, const ImGuiDockPreviewData* data) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.CurrentWindow == host_window); // Because we rely on font size to calculate tab sizes + + // With this option, we only display the preview on the target viewport, and the payload viewport is made transparent. + // To compensate for the single layer obstructed by the payload, we'll increase the alpha of the preview nodes. + const bool is_transparent_payload = g.IO.ConfigDockingTransparentPayload; + + // In case the two windows involved are on different viewports, we will draw the overlay on each of them. + int overlay_draw_lists_count = 0; + ImDrawList* overlay_draw_lists[2]; + overlay_draw_lists[overlay_draw_lists_count++] = GetForegroundDrawList(host_window->Viewport); + if (host_window->Viewport != root_payload->Viewport && !is_transparent_payload) + overlay_draw_lists[overlay_draw_lists_count++] = GetForegroundDrawList(root_payload->Viewport); + + // Draw main preview rectangle + const ImU32 overlay_col_main = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 0.60f : 0.40f); + const ImU32 overlay_col_drop = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 0.90f : 0.70f); + const ImU32 overlay_col_drop_hovered = GetColorU32(ImGuiCol_DockingPreview, is_transparent_payload ? 1.20f : 1.00f); + const ImU32 overlay_col_lines = GetColorU32(ImGuiCol_NavWindowingHighlight, is_transparent_payload ? 0.80f : 0.60f); + + // Display area preview + const bool can_preview_tabs = (root_payload->DockNodeAsHost == NULL || root_payload->DockNodeAsHost->Windows.Size > 0); + if (data->IsDropAllowed) + { + ImRect overlay_rect = data->FutureNode.Rect(); + if (data->SplitDir == ImGuiDir_None && can_preview_tabs) + overlay_rect.Min.y += GetFrameHeight(); + if (data->SplitDir != ImGuiDir_None || data->IsCenterAvailable) + for (int overlay_n = 0; overlay_n < overlay_draw_lists_count; overlay_n++) + overlay_draw_lists[overlay_n]->AddRectFilled(overlay_rect.Min, overlay_rect.Max, overlay_col_main, host_window->WindowRounding, CalcRoundingFlagsForRectInRect(overlay_rect, host_window->Rect(), g.Style.DockingSeparatorSize)); + } + + // Display tab shape/label preview unless we are splitting node (it generally makes the situation harder to read) + if (data->IsDropAllowed && can_preview_tabs && data->SplitDir == ImGuiDir_None && data->IsCenterAvailable) + { + // Compute target tab bar geometry so we can locate our preview tabs + ImRect tab_bar_rect; + DockNodeCalcTabBarLayout(&data->FutureNode, NULL, &tab_bar_rect, NULL, NULL); + ImVec2 tab_pos = tab_bar_rect.Min; + if (host_node && host_node->TabBar) + { + if (!host_node->IsHiddenTabBar() && !host_node->IsNoTabBar()) + tab_pos.x += host_node->TabBar->WidthAllTabs + g.Style.ItemInnerSpacing.x; // We don't use OffsetNewTab because when using non-persistent-order tab bar it is incremented with each Tab submission. + else + tab_pos.x += g.Style.ItemInnerSpacing.x + TabItemCalcSize(host_node->Windows[0]).x; + } + else if (!(host_window->Flags & ImGuiWindowFlags_DockNodeHost)) + { + tab_pos.x += g.Style.ItemInnerSpacing.x + TabItemCalcSize(host_window).x; // Account for slight offset which will be added when changing from title bar to tab bar + } + + // Draw tab shape/label preview (payload may be a loose window or a host window carrying multiple tabbed windows) + if (root_payload->DockNodeAsHost) + IM_ASSERT(root_payload->DockNodeAsHost->Windows.Size <= root_payload->DockNodeAsHost->TabBar->Tabs.Size); + ImGuiTabBar* tab_bar_with_payload = root_payload->DockNodeAsHost ? root_payload->DockNodeAsHost->TabBar : NULL; + const int payload_count = tab_bar_with_payload ? tab_bar_with_payload->Tabs.Size : 1; + for (int payload_n = 0; payload_n < payload_count; payload_n++) + { + // DockNode's TabBar may have non-window Tabs manually appended by user + ImGuiWindow* payload_window = tab_bar_with_payload ? tab_bar_with_payload->Tabs[payload_n].Window : root_payload; + if (tab_bar_with_payload && payload_window == NULL) + continue; + if (!DockNodeIsDropAllowedOne(payload_window, host_window)) + continue; + + // Calculate the tab bounding box for each payload window + ImVec2 tab_size = TabItemCalcSize(payload_window); + ImRect tab_bb(tab_pos.x, tab_pos.y, tab_pos.x + tab_size.x, tab_pos.y + tab_size.y); + tab_pos.x += tab_size.x + g.Style.ItemInnerSpacing.x; + const ImU32 overlay_col_text = GetColorU32(payload_window->DockStyle.Colors[ImGuiWindowDockStyleCol_Text]); + const ImU32 overlay_col_tabs = GetColorU32(payload_window->DockStyle.Colors[ImGuiWindowDockStyleCol_TabSelected]); + PushStyleColor(ImGuiCol_Text, overlay_col_text); + for (int overlay_n = 0; overlay_n < overlay_draw_lists_count; overlay_n++) + { + ImGuiTabItemFlags tab_flags = (payload_window->Flags & ImGuiWindowFlags_UnsavedDocument) ? ImGuiTabItemFlags_UnsavedDocument : 0; + if (!tab_bar_rect.Contains(tab_bb)) + overlay_draw_lists[overlay_n]->PushClipRect(tab_bar_rect.Min, tab_bar_rect.Max); + TabItemBackground(overlay_draw_lists[overlay_n], tab_bb, tab_flags, overlay_col_tabs); + TabItemLabelAndCloseButton(overlay_draw_lists[overlay_n], tab_bb, tab_flags, g.Style.FramePadding, payload_window->Name, 0, 0, false, NULL, NULL); + if (!tab_bar_rect.Contains(tab_bb)) + overlay_draw_lists[overlay_n]->PopClipRect(); + } + PopStyleColor(); + } + } + + // Display drop boxes + const float overlay_rounding = ImMax(3.0f, g.Style.FrameRounding); + for (int dir = ImGuiDir_None; dir < ImGuiDir_COUNT; dir++) + { + if (!data->DropRectsDraw[dir + 1].IsInverted()) + { + ImRect draw_r = data->DropRectsDraw[dir + 1]; + ImRect draw_r_in = draw_r; + draw_r_in.Expand(-2.0f); + ImU32 overlay_col = (data->SplitDir == (ImGuiDir)dir && data->IsSplitDirExplicit) ? overlay_col_drop_hovered : overlay_col_drop; + for (int overlay_n = 0; overlay_n < overlay_draw_lists_count; overlay_n++) + { + ImVec2 center = ImFloor(draw_r_in.GetCenter()); + overlay_draw_lists[overlay_n]->AddRectFilled(draw_r.Min, draw_r.Max, overlay_col, overlay_rounding); + overlay_draw_lists[overlay_n]->AddRect(draw_r_in.Min, draw_r_in.Max, overlay_col_lines, overlay_rounding); + if (dir == ImGuiDir_Left || dir == ImGuiDir_Right) + overlay_draw_lists[overlay_n]->AddLine(ImVec2(center.x, draw_r_in.Min.y), ImVec2(center.x, draw_r_in.Max.y), overlay_col_lines); + if (dir == ImGuiDir_Up || dir == ImGuiDir_Down) + overlay_draw_lists[overlay_n]->AddLine(ImVec2(draw_r_in.Min.x, center.y), ImVec2(draw_r_in.Max.x, center.y), overlay_col_lines); + } + } + + // Stop after ImGuiDir_None + if ((host_node && (host_node->MergedFlags & ImGuiDockNodeFlags_NoDockingSplit)) || g.IO.ConfigDockingNoSplit) + return; + } +} + +//----------------------------------------------------------------------------- +// Docking: ImGuiDockNode Tree manipulation functions +//----------------------------------------------------------------------------- +// - DockNodeTreeSplit() +// - DockNodeTreeMerge() +// - DockNodeTreeUpdatePosSize() +// - DockNodeTreeUpdateSplitterFindTouchingNode() +// - DockNodeTreeUpdateSplitter() +// - DockNodeTreeFindFallbackLeafNode() +// - DockNodeTreeFindNodeByPos() +//----------------------------------------------------------------------------- + +void ImGui::DockNodeTreeSplit(ImGuiContext* ctx, ImGuiDockNode* parent_node, ImGuiAxis split_axis, int split_inheritor_child_idx, float split_ratio, ImGuiDockNode* new_node) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(split_axis != ImGuiAxis_None); + + ImGuiDockNode* child_0 = (new_node && split_inheritor_child_idx != 0) ? new_node : DockContextAddNode(ctx, 0); + child_0->ParentNode = parent_node; + + ImGuiDockNode* child_1 = (new_node && split_inheritor_child_idx != 1) ? new_node : DockContextAddNode(ctx, 0); + child_1->ParentNode = parent_node; + + ImGuiDockNode* child_inheritor = (split_inheritor_child_idx == 0) ? child_0 : child_1; + DockNodeMoveChildNodes(child_inheritor, parent_node); + parent_node->ChildNodes[0] = child_0; + parent_node->ChildNodes[1] = child_1; + parent_node->ChildNodes[split_inheritor_child_idx]->VisibleWindow = parent_node->VisibleWindow; + parent_node->SplitAxis = split_axis; + parent_node->VisibleWindow = NULL; + parent_node->AuthorityForPos = parent_node->AuthorityForSize = ImGuiDataAuthority_DockNode; + + float size_avail = (parent_node->Size[split_axis] - g.Style.DockingSeparatorSize); + size_avail = ImMax(size_avail, g.Style.WindowMinSize[split_axis] * 2.0f); + IM_ASSERT(size_avail > 0.0f); // If you created a node manually with DockBuilderAddNode(), you need to also call DockBuilderSetNodeSize() before splitting. + child_0->SizeRef = child_1->SizeRef = parent_node->Size; + child_0->SizeRef[split_axis] = ImTrunc(size_avail * split_ratio); + child_1->SizeRef[split_axis] = ImTrunc(size_avail - child_0->SizeRef[split_axis]); + + DockNodeMoveWindows(parent_node->ChildNodes[split_inheritor_child_idx], parent_node); + DockSettingsRenameNodeReferences(parent_node->ID, parent_node->ChildNodes[split_inheritor_child_idx]->ID); + DockNodeUpdateHasCentralNodeChild(DockNodeGetRootNode(parent_node)); + DockNodeTreeUpdatePosSize(parent_node, parent_node->Pos, parent_node->Size); + + // Flags transfer (e.g. this is where we transfer the ImGuiDockNodeFlags_CentralNode property) + child_0->SharedFlags = parent_node->SharedFlags & ImGuiDockNodeFlags_SharedFlagsInheritMask_; + child_1->SharedFlags = parent_node->SharedFlags & ImGuiDockNodeFlags_SharedFlagsInheritMask_; + child_inheritor->LocalFlags = parent_node->LocalFlags & ImGuiDockNodeFlags_LocalFlagsTransferMask_; + parent_node->LocalFlags &= ~ImGuiDockNodeFlags_LocalFlagsTransferMask_; + child_0->UpdateMergedFlags(); + child_1->UpdateMergedFlags(); + parent_node->UpdateMergedFlags(); + if (child_inheritor->IsCentralNode()) + DockNodeGetRootNode(parent_node)->CentralNode = child_inheritor; +} + +void ImGui::DockNodeTreeMerge(ImGuiContext* ctx, ImGuiDockNode* parent_node, ImGuiDockNode* merge_lead_child) +{ + // When called from DockContextProcessUndockNode() it is possible that one of the child is NULL. + ImGuiContext& g = *GImGui; + ImGuiDockNode* child_0 = parent_node->ChildNodes[0]; + ImGuiDockNode* child_1 = parent_node->ChildNodes[1]; + IM_ASSERT(child_0 || child_1); + IM_ASSERT(merge_lead_child == child_0 || merge_lead_child == child_1); + if ((child_0 && child_0->Windows.Size > 0) || (child_1 && child_1->Windows.Size > 0)) + { + IM_ASSERT(parent_node->TabBar == NULL); + IM_ASSERT(parent_node->Windows.Size == 0); + } + IMGUI_DEBUG_LOG_DOCKING("[docking] DockNodeTreeMerge: 0x%08X + 0x%08X back into parent 0x%08X\n", child_0 ? child_0->ID : 0, child_1 ? child_1->ID : 0, parent_node->ID); + + ImVec2 backup_last_explicit_size = parent_node->SizeRef; + DockNodeMoveChildNodes(parent_node, merge_lead_child); + if (child_0) + { + DockNodeMoveWindows(parent_node, child_0); // Generally only 1 of the 2 child node will have windows + DockSettingsRenameNodeReferences(child_0->ID, parent_node->ID); + } + if (child_1) + { + DockNodeMoveWindows(parent_node, child_1); + DockSettingsRenameNodeReferences(child_1->ID, parent_node->ID); + } + DockNodeApplyPosSizeToWindows(parent_node); + parent_node->AuthorityForPos = parent_node->AuthorityForSize = parent_node->AuthorityForViewport = ImGuiDataAuthority_Auto; + parent_node->VisibleWindow = merge_lead_child->VisibleWindow; + parent_node->SizeRef = backup_last_explicit_size; + + // Flags transfer + parent_node->LocalFlags &= ~ImGuiDockNodeFlags_LocalFlagsTransferMask_; // Preserve Dockspace flag + parent_node->LocalFlags |= (child_0 ? child_0->LocalFlags : 0) & ImGuiDockNodeFlags_LocalFlagsTransferMask_; + parent_node->LocalFlags |= (child_1 ? child_1->LocalFlags : 0) & ImGuiDockNodeFlags_LocalFlagsTransferMask_; + parent_node->LocalFlagsInWindows = (child_0 ? child_0->LocalFlagsInWindows : 0) | (child_1 ? child_1->LocalFlagsInWindows : 0); // FIXME: Would be more consistent to update from actual windows + parent_node->UpdateMergedFlags(); + + if (child_0) + { + ctx->DockContext.Nodes.SetVoidPtr(child_0->ID, NULL); + IM_DELETE(child_0); + } + if (child_1) + { + ctx->DockContext.Nodes.SetVoidPtr(child_1->ID, NULL); + IM_DELETE(child_1); + } +} + +// Update Pos/Size for a node hierarchy (don't affect child Windows yet) +// (Depth-first, Pre-Order) +void ImGui::DockNodeTreeUpdatePosSize(ImGuiDockNode* node, ImVec2 pos, ImVec2 size, ImGuiDockNode* only_write_to_single_node) +{ + // During the regular dock node update we write to all nodes. + // 'only_write_to_single_node' is only set when turning a node visible mid-frame and we need its size right-away. + ImGuiContext& g = *GImGui; + const bool write_to_node = only_write_to_single_node == NULL || only_write_to_single_node == node; + if (write_to_node) + { + node->Pos = pos; + node->Size = size; + } + + if (node->IsLeafNode()) + return; + + ImGuiDockNode* child_0 = node->ChildNodes[0]; + ImGuiDockNode* child_1 = node->ChildNodes[1]; + ImVec2 child_0_pos = pos, child_1_pos = pos; + ImVec2 child_0_size = size, child_1_size = size; + + const bool child_0_is_toward_single_node = (only_write_to_single_node != NULL && DockNodeIsInHierarchyOf(only_write_to_single_node, child_0)); + const bool child_1_is_toward_single_node = (only_write_to_single_node != NULL && DockNodeIsInHierarchyOf(only_write_to_single_node, child_1)); + const bool child_0_is_or_will_be_visible = child_0->IsVisible || child_0_is_toward_single_node; + const bool child_1_is_or_will_be_visible = child_1->IsVisible || child_1_is_toward_single_node; + + if (child_0_is_or_will_be_visible && child_1_is_or_will_be_visible) + { + const float spacing = g.Style.DockingSeparatorSize; + const ImGuiAxis axis = (ImGuiAxis)node->SplitAxis; + const float size_avail = ImMax(size[axis] - spacing, 0.0f); + + // Size allocation policy + // 1) The first 0..WindowMinSize[axis]*2 are allocated evenly to both windows. + const float size_min_each = ImTrunc(ImMin(size_avail, g.Style.WindowMinSize[axis] * 2.0f) * 0.5f); + + // FIXME: Blocks 2) and 3) are essentially doing nearly the same thing. + // Difference are: write-back to SizeRef; application of a minimum size; rounding before ImTrunc() + // Clarify and rework differences between Size & SizeRef and purpose of WantLockSizeOnce + + // 2) Process locked absolute size (during a splitter resize we preserve the child of nodes not touching the splitter edge) + if (child_0->WantLockSizeOnce && !child_1->WantLockSizeOnce) + { + child_0_size[axis] = child_0->SizeRef[axis] = ImMin(size_avail - 1.0f, child_0->Size[axis]); + child_1_size[axis] = child_1->SizeRef[axis] = (size_avail - child_0_size[axis]); + IM_ASSERT(child_0->SizeRef[axis] > 0.0f && child_1->SizeRef[axis] > 0.0f); + } + else if (child_1->WantLockSizeOnce && !child_0->WantLockSizeOnce) + { + child_1_size[axis] = child_1->SizeRef[axis] = ImMin(size_avail - 1.0f, child_1->Size[axis]); + child_0_size[axis] = child_0->SizeRef[axis] = (size_avail - child_1_size[axis]); + IM_ASSERT(child_0->SizeRef[axis] > 0.0f && child_1->SizeRef[axis] > 0.0f); + } + else if (child_0->WantLockSizeOnce && child_1->WantLockSizeOnce) + { + // FIXME-DOCK: We cannot honor the requested size, so apply ratio. + // Currently this path will only be taken if code programmatically sets WantLockSizeOnce + float split_ratio = child_0_size[axis] / (child_0_size[axis] + child_1_size[axis]); + child_0_size[axis] = child_0->SizeRef[axis] = ImTrunc(size_avail * split_ratio); + child_1_size[axis] = child_1->SizeRef[axis] = (size_avail - child_0_size[axis]); + IM_ASSERT(child_0->SizeRef[axis] > 0.0f && child_1->SizeRef[axis] > 0.0f); + } + + // 3) If one window is the central node (~ use remaining space, should be made explicit!), use explicit size from the other, and remainder for the central node + else if (child_0->SizeRef[axis] != 0.0f && child_1->HasCentralNodeChild) + { + child_0_size[axis] = ImMin(size_avail - size_min_each, child_0->SizeRef[axis]); + child_1_size[axis] = (size_avail - child_0_size[axis]); + } + else if (child_1->SizeRef[axis] != 0.0f && child_0->HasCentralNodeChild) + { + child_1_size[axis] = ImMin(size_avail - size_min_each, child_1->SizeRef[axis]); + child_0_size[axis] = (size_avail - child_1_size[axis]); + } + else + { + // 4) Otherwise distribute according to the relative ratio of each SizeRef value + float split_ratio = child_0->SizeRef[axis] / (child_0->SizeRef[axis] + child_1->SizeRef[axis]); + child_0_size[axis] = ImMax(size_min_each, ImTrunc(size_avail * split_ratio + 0.5f)); + child_1_size[axis] = (size_avail - child_0_size[axis]); + } + + child_1_pos[axis] += spacing + child_0_size[axis]; + } + + if (only_write_to_single_node == NULL) + child_0->WantLockSizeOnce = child_1->WantLockSizeOnce = false; + + const bool child_0_recurse = only_write_to_single_node ? child_0_is_toward_single_node : child_0->IsVisible; + const bool child_1_recurse = only_write_to_single_node ? child_1_is_toward_single_node : child_1->IsVisible; + if (child_0_recurse) + DockNodeTreeUpdatePosSize(child_0, child_0_pos, child_0_size); + if (child_1_recurse) + DockNodeTreeUpdatePosSize(child_1, child_1_pos, child_1_size); +} + +static void DockNodeTreeUpdateSplitterFindTouchingNode(ImGuiDockNode* node, ImGuiAxis axis, int side, ImVector* touching_nodes) +{ + if (node->IsLeafNode()) + { + touching_nodes->push_back(node); + return; + } + if (node->ChildNodes[0]->IsVisible) + if (node->SplitAxis != axis || side == 0 || !node->ChildNodes[1]->IsVisible) + DockNodeTreeUpdateSplitterFindTouchingNode(node->ChildNodes[0], axis, side, touching_nodes); + if (node->ChildNodes[1]->IsVisible) + if (node->SplitAxis != axis || side == 1 || !node->ChildNodes[0]->IsVisible) + DockNodeTreeUpdateSplitterFindTouchingNode(node->ChildNodes[1], axis, side, touching_nodes); +} + +// (Depth-First, Pre-Order) +void ImGui::DockNodeTreeUpdateSplitter(ImGuiDockNode* node) +{ + if (node->IsLeafNode()) + return; + + ImGuiContext& g = *GImGui; + + ImGuiDockNode* child_0 = node->ChildNodes[0]; + ImGuiDockNode* child_1 = node->ChildNodes[1]; + if (child_0->IsVisible && child_1->IsVisible) + { + // Bounding box of the splitter cover the space between both nodes (w = Spacing, h = Size[xy^1] for when splitting horizontally) + const ImGuiAxis axis = (ImGuiAxis)node->SplitAxis; + IM_ASSERT(axis != ImGuiAxis_None); + ImRect bb; + bb.Min = child_0->Pos; + bb.Max = child_1->Pos; + bb.Min[axis] += child_0->Size[axis]; + bb.Max[axis ^ 1] += child_1->Size[axis ^ 1]; + //if (g.IO.KeyCtrl) GetForegroundDrawList(g.CurrentWindow->Viewport)->AddRect(bb.Min, bb.Max, IM_COL32(255,0,255,255)); + + const ImGuiDockNodeFlags merged_flags = child_0->MergedFlags | child_1->MergedFlags; // Merged flags for BOTH childs + const ImGuiDockNodeFlags no_resize_axis_flag = (axis == ImGuiAxis_X) ? ImGuiDockNodeFlags_NoResizeX : ImGuiDockNodeFlags_NoResizeY; + if ((merged_flags & ImGuiDockNodeFlags_NoResize) || (merged_flags & no_resize_axis_flag)) + { + ImGuiWindow* window = g.CurrentWindow; + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator), g.Style.FrameRounding); + } + else + { + //bb.Min[axis] += 1; // Display a little inward so highlight doesn't connect with nearby tabs on the neighbor node. + //bb.Max[axis] -= 1; + PushID(node->ID); + + // Find resizing limits by gathering list of nodes that are touching the splitter line. + ImVector touching_nodes[2]; + float min_size = g.Style.WindowMinSize[axis]; + float resize_limits[2]; + resize_limits[0] = node->ChildNodes[0]->Pos[axis] + min_size; + resize_limits[1] = node->ChildNodes[1]->Pos[axis] + node->ChildNodes[1]->Size[axis] - min_size; + + ImGuiID splitter_id = GetID("##Splitter"); + if (g.ActiveId == splitter_id) // Only process when splitter is active + { + DockNodeTreeUpdateSplitterFindTouchingNode(child_0, axis, 1, &touching_nodes[0]); + DockNodeTreeUpdateSplitterFindTouchingNode(child_1, axis, 0, &touching_nodes[1]); + for (int touching_node_n = 0; touching_node_n < touching_nodes[0].Size; touching_node_n++) + resize_limits[0] = ImMax(resize_limits[0], touching_nodes[0][touching_node_n]->Rect().Min[axis] + min_size); + for (int touching_node_n = 0; touching_node_n < touching_nodes[1].Size; touching_node_n++) + resize_limits[1] = ImMin(resize_limits[1], touching_nodes[1][touching_node_n]->Rect().Max[axis] - min_size); + + // [DEBUG] Render touching nodes & limits + /* + ImDrawList* draw_list = node->HostWindow ? GetForegroundDrawList(node->HostWindow) : GetForegroundDrawList(GetMainViewport()); + for (int n = 0; n < 2; n++) + { + for (int touching_node_n = 0; touching_node_n < touching_nodes[n].Size; touching_node_n++) + draw_list->AddRect(touching_nodes[n][touching_node_n]->Pos, touching_nodes[n][touching_node_n]->Pos + touching_nodes[n][touching_node_n]->Size, IM_COL32(0, 255, 0, 255)); + if (axis == ImGuiAxis_X) + draw_list->AddLine(ImVec2(resize_limits[n], node->ChildNodes[n]->Pos.y), ImVec2(resize_limits[n], node->ChildNodes[n]->Pos.y + node->ChildNodes[n]->Size.y), IM_COL32(255, 0, 255, 255), 3.0f); + else + draw_list->AddLine(ImVec2(node->ChildNodes[n]->Pos.x, resize_limits[n]), ImVec2(node->ChildNodes[n]->Pos.x + node->ChildNodes[n]->Size.x, resize_limits[n]), IM_COL32(255, 0, 255, 255), 3.0f); + } + */ + } + + // Use a short delay before highlighting the splitter (and changing the mouse cursor) in order for regular mouse movement to not highlight many splitters + float cur_size_0 = child_0->Size[axis]; + float cur_size_1 = child_1->Size[axis]; + float min_size_0 = resize_limits[0] - child_0->Pos[axis]; + float min_size_1 = child_1->Pos[axis] + child_1->Size[axis] - resize_limits[1]; + ImU32 bg_col = GetColorU32(ImGuiCol_WindowBg); + if (SplitterBehavior(bb, GetID("##Splitter"), axis, &cur_size_0, &cur_size_1, min_size_0, min_size_1, g.WindowsBorderHoverPadding, WINDOWS_RESIZE_FROM_EDGES_FEEDBACK_TIMER, bg_col)) + { + if (touching_nodes[0].Size > 0 && touching_nodes[1].Size > 0) + { + child_0->Size[axis] = child_0->SizeRef[axis] = cur_size_0; + child_1->Pos[axis] -= cur_size_1 - child_1->Size[axis]; + child_1->Size[axis] = child_1->SizeRef[axis] = cur_size_1; + + // Lock the size of every node that is a sibling of the node we are touching + // This might be less desirable if we can merge sibling of a same axis into the same parental level. + for (int side_n = 0; side_n < 2; side_n++) + for (int touching_node_n = 0; touching_node_n < touching_nodes[side_n].Size; touching_node_n++) + { + ImGuiDockNode* touching_node = touching_nodes[side_n][touching_node_n]; + //ImDrawList* draw_list = node->HostWindow ? GetForegroundDrawList(node->HostWindow) : GetForegroundDrawList(GetMainViewport()); + //draw_list->AddRect(touching_node->Pos, touching_node->Pos + touching_node->Size, IM_COL32(255, 128, 0, 255)); + while (touching_node->ParentNode != node) + { + if (touching_node->ParentNode->SplitAxis == axis) + { + // Mark other node so its size will be preserved during the upcoming call to DockNodeTreeUpdatePosSize(). + ImGuiDockNode* node_to_preserve = touching_node->ParentNode->ChildNodes[side_n]; + node_to_preserve->WantLockSizeOnce = true; + //draw_list->AddRect(touching_node->Pos, touching_node->Rect().Max, IM_COL32(255, 0, 0, 255)); + //draw_list->AddRectFilled(node_to_preserve->Pos, node_to_preserve->Rect().Max, IM_COL32(0, 255, 0, 100)); + } + touching_node = touching_node->ParentNode; + } + } + + DockNodeTreeUpdatePosSize(child_0, child_0->Pos, child_0->Size); + DockNodeTreeUpdatePosSize(child_1, child_1->Pos, child_1->Size); + MarkIniSettingsDirty(); + } + } + PopID(); + } + } + + if (child_0->IsVisible) + DockNodeTreeUpdateSplitter(child_0); + if (child_1->IsVisible) + DockNodeTreeUpdateSplitter(child_1); +} + +ImGuiDockNode* ImGui::DockNodeTreeFindFallbackLeafNode(ImGuiDockNode* node) +{ + if (node->IsLeafNode()) + return node; + if (ImGuiDockNode* leaf_node = DockNodeTreeFindFallbackLeafNode(node->ChildNodes[0])) + return leaf_node; + if (ImGuiDockNode* leaf_node = DockNodeTreeFindFallbackLeafNode(node->ChildNodes[1])) + return leaf_node; + return NULL; +} + +ImGuiDockNode* ImGui::DockNodeTreeFindVisibleNodeByPos(ImGuiDockNode* node, ImVec2 pos) +{ + if (!node->IsVisible) + return NULL; + + const float dock_spacing = 0.0f;// g.Style.ItemInnerSpacing.x; // FIXME: Relation to DOCKING_SPLITTER_SIZE? + ImRect r(node->Pos, node->Pos + node->Size); + r.Expand(dock_spacing * 0.5f); + bool inside = r.Contains(pos); + if (!inside) + return NULL; + + if (node->IsLeafNode()) + return node; + if (ImGuiDockNode* hovered_node = DockNodeTreeFindVisibleNodeByPos(node->ChildNodes[0], pos)) + return hovered_node; + if (ImGuiDockNode* hovered_node = DockNodeTreeFindVisibleNodeByPos(node->ChildNodes[1], pos)) + return hovered_node; + + // This means we are hovering over the splitter/spacing of a parent node + return node; +} + +//----------------------------------------------------------------------------- +// Docking: Public Functions (SetWindowDock, DockSpace, DockSpaceOverViewport) +//----------------------------------------------------------------------------- +// - SetWindowDock() [Internal] +// - DockSpace() +// - DockSpaceOverViewport() +//----------------------------------------------------------------------------- + +// [Internal] Called via SetNextWindowDockID() +void ImGui::SetWindowDock(ImGuiWindow* window, ImGuiID dock_id, ImGuiCond cond) +{ + // Test condition (NB: bit 0 is always true) and clear flags for next time + if (cond && (window->SetWindowDockAllowFlags & cond) == 0) + return; + window->SetWindowDockAllowFlags &= ~(ImGuiCond_Once | ImGuiCond_FirstUseEver | ImGuiCond_Appearing); + + if (window->DockId == dock_id) + return; + + // If the user attempt to set a dock id that is a split node, we'll dig within to find a suitable docking spot + ImGuiContext& g = *GImGui; + if (ImGuiDockNode* new_node = DockContextFindNodeByID(&g, dock_id)) + if (new_node->IsSplitNode()) + { + // Policy: Find central node or latest focused node. We first move back to our root node. + new_node = DockNodeGetRootNode(new_node); + if (new_node->CentralNode) + { + IM_ASSERT(new_node->CentralNode->IsCentralNode()); + dock_id = new_node->CentralNode->ID; + } + else + { + dock_id = new_node->LastFocusedNodeId; + } + } + + if (window->DockId == dock_id) + return; + + if (window->DockNode) + DockNodeRemoveWindow(window->DockNode, window, 0); + window->DockId = dock_id; +} + +// Create an explicit dockspace node within an existing window. Also expose dock node flags and creates a CentralNode by default. +// The Central Node is always displayed even when empty and shrink/extend according to the requested size of its neighbors. +// DockSpace() needs to be submitted _before_ any window they can host. If you use a dockspace, submit it early in your app. +// When ImGuiDockNodeFlags_KeepAliveOnly is set, nothing is submitted in the current window (function may be called from any location). +ImGuiID ImGui::DockSpace(ImGuiID dockspace_id, const ImVec2& size_arg, ImGuiDockNodeFlags flags, const ImGuiWindowClass* window_class) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindowRead(); + if (!(g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + return 0; + + // Early out if parent window is hidden/collapsed + // This is faster but also DockNodeUpdateTabBar() relies on TabBarLayout() running (which won't if SkipItems=true) to set NextSelectedTabId = 0). See #2960. + // If for whichever reason this is causing problem we would need to ensure that DockNodeUpdateTabBar() ends up clearing NextSelectedTabId even if SkipItems=true. + if (window->SkipItems) + flags |= ImGuiDockNodeFlags_KeepAliveOnly; + if ((flags & ImGuiDockNodeFlags_KeepAliveOnly) == 0) + window = GetCurrentWindow(); // call to set window->WriteAccessed = true; + + IM_ASSERT((flags & ImGuiDockNodeFlags_DockSpace) == 0); // Flag is automatically set by DockSpace() as LocalFlags, not SharedFlags! + IM_ASSERT((flags & ImGuiDockNodeFlags_CentralNode) == 0); // Flag is automatically set by DockSpace() as LocalFlags, not SharedFlags! (#8145) + + IM_ASSERT(dockspace_id != 0); + ImGuiDockNode* node = DockContextFindNodeByID(&g, dockspace_id); + if (node == NULL) + { + IMGUI_DEBUG_LOG_DOCKING("[docking] DockSpace: dockspace node 0x%08X created\n", dockspace_id); + node = DockContextAddNode(&g, dockspace_id); + node->SetLocalFlags(ImGuiDockNodeFlags_CentralNode); + } + if (window_class && window_class->ClassId != node->WindowClass.ClassId) + IMGUI_DEBUG_LOG_DOCKING("[docking] DockSpace: dockspace node 0x%08X: setup WindowClass 0x%08X -> 0x%08X\n", dockspace_id, node->WindowClass.ClassId, window_class->ClassId); + node->SharedFlags = flags; + node->WindowClass = window_class ? *window_class : ImGuiWindowClass(); + + // When a DockSpace transitioned form implicit to explicit this may be called a second time + // It is possible that the node has already been claimed by a docked window which appeared before the DockSpace() node, so we overwrite IsDockSpace again. + if (node->LastFrameActive == g.FrameCount && !(flags & ImGuiDockNodeFlags_KeepAliveOnly)) + { + IM_ASSERT(node->IsDockSpace() == false && "Cannot call DockSpace() twice a frame with the same ID"); + node->SetLocalFlags(node->LocalFlags | ImGuiDockNodeFlags_DockSpace); + return dockspace_id; + } + node->SetLocalFlags(node->LocalFlags | ImGuiDockNodeFlags_DockSpace); + + // Keep alive mode, this is allow windows docked into this node so stay docked even if they are not visible + if (flags & ImGuiDockNodeFlags_KeepAliveOnly) + { + node->LastFrameAlive = g.FrameCount; + return dockspace_id; + } + + const ImVec2 content_avail = GetContentRegionAvail(); + ImVec2 size = ImTrunc(size_arg); + if (size.x <= 0.0f) + size.x = ImMax(content_avail.x + size.x, 4.0f); // Arbitrary minimum child size (0.0f causing too much issues) + if (size.y <= 0.0f) + size.y = ImMax(content_avail.y + size.y, 4.0f); + IM_ASSERT(size.x > 0.0f && size.y > 0.0f); + + node->Pos = window->DC.CursorPos; + node->Size = node->SizeRef = size; + SetNextWindowPos(node->Pos); + SetNextWindowSize(node->Size); + g.NextWindowData.PosUndock = false; + + // FIXME-DOCK: Why do we need a child window to host a dockspace, could we host it in the existing window? + // FIXME-DOCK: What is the reason for not simply calling BeginChild()? (OK to have a reason but should be commented) + ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_DockNodeHost; + window_flags |= ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar; + window_flags |= ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse; + window_flags |= ImGuiWindowFlags_NoBackground; + + char title[256]; + ImFormatString(title, IM_ARRAYSIZE(title), "%s/DockSpace_%08X", window->Name, dockspace_id); + + PushStyleVar(ImGuiStyleVar_ChildBorderSize, 0.0f); + Begin(title, NULL, window_flags); + PopStyleVar(); + + ImGuiWindow* host_window = g.CurrentWindow; + DockNodeSetupHostWindow(node, host_window); + host_window->ChildId = window->GetID(title); + node->OnlyNodeWithWindows = NULL; + + IM_ASSERT(node->IsRootNode()); + + // We need to handle the rare case were a central node is missing. + // This can happen if the node was first created manually with DockBuilderAddNode() but _without_ the ImGuiDockNodeFlags_Dockspace. + // Doing it correctly would set the _CentralNode flags, which would then propagate according to subsequent split. + // It would also be ambiguous to attempt to assign a central node while there are split nodes, so we wait until there's a single node remaining. + // The specific sub-property of _CentralNode we are interested in recovering here is the "Don't delete when empty" property, + // as it doesn't make sense for an empty dockspace to not have this property. + if (node->IsLeafNode() && !node->IsCentralNode()) + node->SetLocalFlags(node->LocalFlags | ImGuiDockNodeFlags_CentralNode); + + // Update the node + DockNodeUpdate(node); + + End(); + + ImRect bb(node->Pos, node->Pos + size); + ItemSize(size); + ItemAdd(bb, dockspace_id, NULL, ImGuiItemFlags_NoNav); // Not a nav point (could be, would need to draw the nav rect and replicate/refactor activation from BeginChild(), but seems like CTRL+Tab works better here?) + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && IsWindowChildOf(g.HoveredWindow, host_window, false, true)) // To fullfill IsItemHovered(), similar to EndChild() + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + + return dockspace_id; +} + +// Tips: Use with ImGuiDockNodeFlags_PassthruCentralNode! +// The limitation with this call is that your window won't have a local menu bar, but you can also use BeginMainMenuBar(). +// Even though we could pass window flags, it would also require the user to be able to call BeginMenuBar() somehow meaning we can't Begin/End in a single function. +// If you really want a menu bar inside the same window as the one hosting the dockspace, you will need to copy this code somewhere and tweak it. +ImGuiID ImGui::DockSpaceOverViewport(ImGuiID dockspace_id, const ImGuiViewport* viewport, ImGuiDockNodeFlags dockspace_flags, const ImGuiWindowClass* window_class) +{ + if (viewport == NULL) + viewport = GetMainViewport(); + + // Submit a window filling the entire viewport + SetNextWindowPos(viewport->WorkPos); + SetNextWindowSize(viewport->WorkSize); + SetNextWindowViewport(viewport->ID); + + ImGuiWindowFlags host_window_flags = 0; + host_window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking; + host_window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; + if (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode) + host_window_flags |= ImGuiWindowFlags_NoBackground; + + // FIXME-OPT: When using ImGuiDockNodeFlags_KeepAliveOnly with DockSpaceOverViewport() we might be able to spare submitting the window, + // since DockSpace() with that flag doesn't need a window. We'd only need to compute the default ID accordingly. + if (dockspace_flags & ImGuiDockNodeFlags_KeepAliveOnly) + host_window_flags |= ImGuiWindowFlags_NoMouseInputs; + + char label[32]; + ImFormatString(label, IM_ARRAYSIZE(label), "WindowOverViewport_%08X", viewport->ID); + + PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + Begin(label, NULL, host_window_flags); + PopStyleVar(3); + + // Submit the dockspace + if (dockspace_id == 0) + dockspace_id = GetID("DockSpace"); + DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags, window_class); + + End(); + + return dockspace_id; +} + +//----------------------------------------------------------------------------- +// Docking: Builder Functions +//----------------------------------------------------------------------------- +// Very early end-user API to manipulate dock nodes. +// Only available in imgui_internal.h. Expect this API to change/break! +// It is expected that those functions are all called _before_ the dockspace node submission. +//----------------------------------------------------------------------------- +// - DockBuilderDockWindow() +// - DockBuilderGetNode() +// - DockBuilderSetNodePos() +// - DockBuilderSetNodeSize() +// - DockBuilderAddNode() +// - DockBuilderRemoveNode() +// - DockBuilderRemoveNodeChildNodes() +// - DockBuilderRemoveNodeDockedWindows() +// - DockBuilderSplitNode() +// - DockBuilderCopyNodeRec() +// - DockBuilderCopyNode() +// - DockBuilderCopyWindowSettings() +// - DockBuilderCopyDockSpace() +// - DockBuilderFinish() +//----------------------------------------------------------------------------- + +void ImGui::DockBuilderDockWindow(const char* window_name, ImGuiID node_id) +{ + // We don't preserve relative order of multiple docked windows (by clearing DockOrder back to -1) + ImGuiContext& g = *GImGui; IM_UNUSED(g); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockBuilderDockWindow '%s' to node 0x%08X\n", window_name, node_id); + ImGuiID window_id = ImHashStr(window_name); + if (ImGuiWindow* window = FindWindowByID(window_id)) + { + // Apply to created window + ImGuiID prev_node_id = window->DockId; + SetWindowDock(window, node_id, ImGuiCond_Always); + if (window->DockId != prev_node_id) + window->DockOrder = -1; + } + else + { + // Apply to settings + ImGuiWindowSettings* settings = FindWindowSettingsByID(window_id); + if (settings == NULL) + settings = CreateNewWindowSettings(window_name); + if (settings->DockId != node_id) + settings->DockOrder = -1; + settings->DockId = node_id; + } +} + +ImGuiDockNode* ImGui::DockBuilderGetNode(ImGuiID node_id) +{ + ImGuiContext& g = *GImGui; + return DockContextFindNodeByID(&g, node_id); +} + +void ImGui::DockBuilderSetNodePos(ImGuiID node_id, ImVec2 pos) +{ + ImGuiContext& g = *GImGui; + ImGuiDockNode* node = DockContextFindNodeByID(&g, node_id); + if (node == NULL) + return; + node->Pos = pos; + node->AuthorityForPos = ImGuiDataAuthority_DockNode; +} + +void ImGui::DockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size) +{ + ImGuiContext& g = *GImGui; + ImGuiDockNode* node = DockContextFindNodeByID(&g, node_id); + if (node == NULL) + return; + IM_ASSERT(size.x > 0.0f && size.y > 0.0f); + node->Size = node->SizeRef = size; + node->AuthorityForSize = ImGuiDataAuthority_DockNode; +} + +// Make sure to use the ImGuiDockNodeFlags_DockSpace flag to create a dockspace node! Otherwise this will create a floating node! +// - Floating node: you can then call DockBuilderSetNodePos()/DockBuilderSetNodeSize() to position and size the floating node. +// - Dockspace node: calling DockBuilderSetNodePos() is unnecessary. +// - If you intend to split a node immediately after creation using DockBuilderSplitNode(), make sure to call DockBuilderSetNodeSize() beforehand! +// For various reason, the splitting code currently needs a base size otherwise space may not be allocated as precisely as you would expect. +// - Use (id == 0) to let the system allocate a node identifier. +// - Existing node with a same id will be removed. +ImGuiID ImGui::DockBuilderAddNode(ImGuiID node_id, ImGuiDockNodeFlags flags) +{ + ImGuiContext& g = *GImGui; IM_UNUSED(g); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockBuilderAddNode 0x%08X flags=%08X\n", node_id, flags); + + if (node_id != 0) + DockBuilderRemoveNode(node_id); + + ImGuiDockNode* node = NULL; + if (flags & ImGuiDockNodeFlags_DockSpace) + { + DockSpace(node_id, ImVec2(0, 0), (flags & ~ImGuiDockNodeFlags_DockSpace) | ImGuiDockNodeFlags_KeepAliveOnly); + node = DockContextFindNodeByID(&g, node_id); + } + else + { + node = DockContextAddNode(&g, node_id); + node->SetLocalFlags(flags); + } + node->LastFrameAlive = g.FrameCount; // Set this otherwise BeginDocked will undock during the same frame. + return node->ID; +} + +void ImGui::DockBuilderRemoveNode(ImGuiID node_id) +{ + ImGuiContext& g = *GImGui; IM_UNUSED(g); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockBuilderRemoveNode 0x%08X\n", node_id); + + ImGuiDockNode* node = DockContextFindNodeByID(&g, node_id); + if (node == NULL) + return; + DockBuilderRemoveNodeDockedWindows(node_id, true); + DockBuilderRemoveNodeChildNodes(node_id); + // Node may have moved or deleted if e.g. any merge happened + node = DockContextFindNodeByID(&g, node_id); + if (node == NULL) + return; + if (node->IsCentralNode() && node->ParentNode) + node->ParentNode->SetLocalFlags(node->ParentNode->LocalFlags | ImGuiDockNodeFlags_CentralNode); + DockContextRemoveNode(&g, node, true); +} + +// root_id = 0 to remove all, root_id != 0 to remove child of given node. +void ImGui::DockBuilderRemoveNodeChildNodes(ImGuiID root_id) +{ + ImGuiContext& g = *GImGui; + ImGuiDockContext* dc = &g.DockContext; + + ImGuiDockNode* root_node = root_id ? DockContextFindNodeByID(&g, root_id) : NULL; + if (root_id && root_node == NULL) + return; + bool has_central_node = false; + + ImGuiDataAuthority backup_root_node_authority_for_pos = root_node ? root_node->AuthorityForPos : ImGuiDataAuthority_Auto; + ImGuiDataAuthority backup_root_node_authority_for_size = root_node ? root_node->AuthorityForSize : ImGuiDataAuthority_Auto; + + // Process active windows + ImVector nodes_to_remove; + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + { + bool want_removal = (root_id == 0) || (node->ID != root_id && DockNodeGetRootNode(node)->ID == root_id); + if (want_removal) + { + if (node->IsCentralNode()) + has_central_node = true; + if (root_id != 0) + DockContextQueueNotifyRemovedNode(&g, node); + if (root_node) + { + DockNodeMoveWindows(root_node, node); + DockSettingsRenameNodeReferences(node->ID, root_node->ID); + } + nodes_to_remove.push_back(node); + } + } + + // DockNodeMoveWindows->DockNodeAddWindow will normally set those when reaching two windows (which is only adequate during interactive merge) + // Make sure we don't lose our current pos/size. (FIXME-DOCK: Consider tidying up that code in DockNodeAddWindow instead) + if (root_node) + { + root_node->AuthorityForPos = backup_root_node_authority_for_pos; + root_node->AuthorityForSize = backup_root_node_authority_for_size; + } + + // Apply to settings + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (ImGuiID window_settings_dock_id = settings->DockId) + for (int n = 0; n < nodes_to_remove.Size; n++) + if (nodes_to_remove[n]->ID == window_settings_dock_id) + { + settings->DockId = root_id; + break; + } + + // Not really efficient, but easier to destroy a whole hierarchy considering DockContextRemoveNode is attempting to merge nodes + if (nodes_to_remove.Size > 1) + ImQsort(nodes_to_remove.Data, nodes_to_remove.Size, sizeof(ImGuiDockNode*), DockNodeComparerDepthMostFirst); + for (int n = 0; n < nodes_to_remove.Size; n++) + DockContextRemoveNode(&g, nodes_to_remove[n], false); + + if (root_id == 0) + { + dc->Nodes.Clear(); + dc->Requests.clear(); + } + else if (has_central_node) + { + root_node->CentralNode = root_node; + root_node->SetLocalFlags(root_node->LocalFlags | ImGuiDockNodeFlags_CentralNode); + } +} + +void ImGui::DockBuilderRemoveNodeDockedWindows(ImGuiID root_id, bool clear_settings_refs) +{ + // Clear references in settings + ImGuiContext& g = *GImGui; + if (clear_settings_refs) + { + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + { + bool want_removal = (root_id == 0) || (settings->DockId == root_id); + if (!want_removal && settings->DockId != 0) + if (ImGuiDockNode* node = DockContextFindNodeByID(&g, settings->DockId)) + if (DockNodeGetRootNode(node)->ID == root_id) + want_removal = true; + if (want_removal) + settings->DockId = 0; + } + } + + // Clear references in windows + for (int n = 0; n < g.Windows.Size; n++) + { + ImGuiWindow* window = g.Windows[n]; + bool want_removal = (root_id == 0) || (window->DockNode && DockNodeGetRootNode(window->DockNode)->ID == root_id) || (window->DockNodeAsHost && window->DockNodeAsHost->ID == root_id); + if (want_removal) + { + const ImGuiID backup_dock_id = window->DockId; + IM_UNUSED(backup_dock_id); + DockContextProcessUndockWindow(&g, window, clear_settings_refs); + if (!clear_settings_refs) + IM_ASSERT(window->DockId == backup_dock_id); + } + } +} + +// If 'out_id_at_dir' or 'out_id_at_opposite_dir' are non NULL, the function will write out the ID of the two new nodes created. +// Return value is ID of the node at the specified direction, so same as (*out_id_at_dir) if that pointer is set. +// FIXME-DOCK: We are not exposing nor using split_outer. +ImGuiID ImGui::DockBuilderSplitNode(ImGuiID id, ImGuiDir split_dir, float size_ratio_for_node_at_dir, ImGuiID* out_id_at_dir, ImGuiID* out_id_at_opposite_dir) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(split_dir != ImGuiDir_None); + IMGUI_DEBUG_LOG_DOCKING("[docking] DockBuilderSplitNode: node 0x%08X, split_dir %d\n", id, split_dir); + + ImGuiDockNode* node = DockContextFindNodeByID(&g, id); + if (node == NULL) + { + IM_ASSERT(node != NULL); + return 0; + } + + ImGuiDockRequest req; + req.Type = ImGuiDockRequestType_Split; + req.DockTargetWindow = NULL; + req.DockTargetNode = node; + req.DockPayload = NULL; + req.DockSplitDir = split_dir; + req.DockSplitRatio = ImSaturate((split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Up) ? size_ratio_for_node_at_dir : 1.0f - size_ratio_for_node_at_dir); + req.DockSplitOuter = false; + DockContextProcessDock(&g, &req); + + ImGuiID id_at_dir = node->ChildNodes[(split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Up) ? 0 : 1]->ID; + ImGuiID id_at_opposite_dir = node->ChildNodes[(split_dir == ImGuiDir_Left || split_dir == ImGuiDir_Up) ? 1 : 0]->ID; + if (out_id_at_dir) + *out_id_at_dir = id_at_dir; + if (out_id_at_opposite_dir) + *out_id_at_opposite_dir = id_at_opposite_dir; + return id_at_dir; +} + +static ImGuiDockNode* DockBuilderCopyNodeRec(ImGuiDockNode* src_node, ImGuiID dst_node_id_if_known, ImVector* out_node_remap_pairs) +{ + ImGuiContext& g = *GImGui; + ImGuiDockNode* dst_node = ImGui::DockContextAddNode(&g, dst_node_id_if_known); + dst_node->SharedFlags = src_node->SharedFlags; + dst_node->LocalFlags = src_node->LocalFlags; + dst_node->LocalFlagsInWindows = ImGuiDockNodeFlags_None; + dst_node->Pos = src_node->Pos; + dst_node->Size = src_node->Size; + dst_node->SizeRef = src_node->SizeRef; + dst_node->SplitAxis = src_node->SplitAxis; + dst_node->UpdateMergedFlags(); + + out_node_remap_pairs->push_back(src_node->ID); + out_node_remap_pairs->push_back(dst_node->ID); + + for (int child_n = 0; child_n < IM_ARRAYSIZE(src_node->ChildNodes); child_n++) + if (src_node->ChildNodes[child_n]) + { + dst_node->ChildNodes[child_n] = DockBuilderCopyNodeRec(src_node->ChildNodes[child_n], 0, out_node_remap_pairs); + dst_node->ChildNodes[child_n]->ParentNode = dst_node; + } + + IMGUI_DEBUG_LOG_DOCKING("[docking] Fork node %08X -> %08X (%d childs)\n", src_node->ID, dst_node->ID, dst_node->IsSplitNode() ? 2 : 0); + return dst_node; +} + +void ImGui::DockBuilderCopyNode(ImGuiID src_node_id, ImGuiID dst_node_id, ImVector* out_node_remap_pairs) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(src_node_id != 0); + IM_ASSERT(dst_node_id != 0); + IM_ASSERT(out_node_remap_pairs != NULL); + + DockBuilderRemoveNode(dst_node_id); + + ImGuiDockNode* src_node = DockContextFindNodeByID(&g, src_node_id); + IM_ASSERT(src_node != NULL); + + out_node_remap_pairs->clear(); + DockBuilderCopyNodeRec(src_node, dst_node_id, out_node_remap_pairs); + + IM_ASSERT((out_node_remap_pairs->Size % 2) == 0); +} + +void ImGui::DockBuilderCopyWindowSettings(const char* src_name, const char* dst_name) +{ + ImGuiWindow* src_window = FindWindowByName(src_name); + if (src_window == NULL) + return; + if (ImGuiWindow* dst_window = FindWindowByName(dst_name)) + { + dst_window->Pos = src_window->Pos; + dst_window->Size = src_window->Size; + dst_window->SizeFull = src_window->SizeFull; + dst_window->Collapsed = src_window->Collapsed; + } + else + { + ImGuiWindowSettings* dst_settings = FindWindowSettingsByID(ImHashStr(dst_name)); + if (!dst_settings) + dst_settings = CreateNewWindowSettings(dst_name); + ImVec2ih window_pos_2ih = ImVec2ih(src_window->Pos); + if (src_window->ViewportId != 0 && src_window->ViewportId != IMGUI_VIEWPORT_DEFAULT_ID) + { + dst_settings->ViewportPos = window_pos_2ih; + dst_settings->ViewportId = src_window->ViewportId; + dst_settings->Pos = ImVec2ih(0, 0); + } + else + { + dst_settings->Pos = window_pos_2ih; + } + dst_settings->Size = ImVec2ih(src_window->SizeFull); + dst_settings->Collapsed = src_window->Collapsed; + } +} + +// FIXME: Will probably want to change this signature, in particular how the window remapping pairs are passed. +void ImGui::DockBuilderCopyDockSpace(ImGuiID src_dockspace_id, ImGuiID dst_dockspace_id, ImVector* in_window_remap_pairs) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(src_dockspace_id != 0); + IM_ASSERT(dst_dockspace_id != 0); + IM_ASSERT(in_window_remap_pairs != NULL); + IM_ASSERT((in_window_remap_pairs->Size % 2) == 0); + + // Duplicate entire dock + // FIXME: When overwriting dst_dockspace_id, windows that aren't part of our dockspace window class but that are docked in a same node will be split apart, + // whereas we could attempt to at least keep them together in a new, same floating node. + ImVector node_remap_pairs; + DockBuilderCopyNode(src_dockspace_id, dst_dockspace_id, &node_remap_pairs); + + // Attempt to transition all the upcoming windows associated to dst_dockspace_id into the newly created hierarchy of dock nodes + // (The windows associated to src_dockspace_id are staying in place) + ImVector src_windows; + for (int remap_window_n = 0; remap_window_n < in_window_remap_pairs->Size; remap_window_n += 2) + { + const char* src_window_name = (*in_window_remap_pairs)[remap_window_n]; + const char* dst_window_name = (*in_window_remap_pairs)[remap_window_n + 1]; + ImGuiID src_window_id = ImHashStr(src_window_name); + src_windows.push_back(src_window_id); + + // Search in the remapping tables + ImGuiID src_dock_id = 0; + if (ImGuiWindow* src_window = FindWindowByID(src_window_id)) + src_dock_id = src_window->DockId; + else if (ImGuiWindowSettings* src_window_settings = FindWindowSettingsByID(src_window_id)) + src_dock_id = src_window_settings->DockId; + ImGuiID dst_dock_id = 0; + for (int dock_remap_n = 0; dock_remap_n < node_remap_pairs.Size; dock_remap_n += 2) + if (node_remap_pairs[dock_remap_n] == src_dock_id) + { + dst_dock_id = node_remap_pairs[dock_remap_n + 1]; + //node_remap_pairs[dock_remap_n] = node_remap_pairs[dock_remap_n + 1] = 0; // Clear + break; + } + + if (dst_dock_id != 0) + { + // Docked windows gets redocked into the new node hierarchy. + IMGUI_DEBUG_LOG_DOCKING("[docking] Remap live window '%s' 0x%08X -> '%s' 0x%08X\n", src_window_name, src_dock_id, dst_window_name, dst_dock_id); + DockBuilderDockWindow(dst_window_name, dst_dock_id); + } + else + { + // Floating windows gets their settings transferred (regardless of whether the new window already exist or not) + // When this is leading to a Copy and not a Move, we would get two overlapping floating windows. Could we possibly dock them together? + IMGUI_DEBUG_LOG_DOCKING("[docking] Remap window settings '%s' -> '%s'\n", src_window_name, dst_window_name); + DockBuilderCopyWindowSettings(src_window_name, dst_window_name); + } + } + + // Anything else in the source nodes of 'node_remap_pairs' are windows that are not included in the remapping list. + // Find those windows and move to them to the cloned dock node. This may be optional? + // Dock those are a second step as undocking would invalidate source dock nodes. + struct DockRemainingWindowTask { ImGuiWindow* Window; ImGuiID DockId; DockRemainingWindowTask(ImGuiWindow* window, ImGuiID dock_id) { Window = window; DockId = dock_id; } }; + ImVector dock_remaining_windows; + for (int dock_remap_n = 0; dock_remap_n < node_remap_pairs.Size; dock_remap_n += 2) + if (ImGuiID src_dock_id = node_remap_pairs[dock_remap_n]) + { + ImGuiID dst_dock_id = node_remap_pairs[dock_remap_n + 1]; + ImGuiDockNode* node = DockBuilderGetNode(src_dock_id); + for (int window_n = 0; window_n < node->Windows.Size; window_n++) + { + ImGuiWindow* window = node->Windows[window_n]; + if (src_windows.contains(window->ID)) + continue; + + // Docked windows gets redocked into the new node hierarchy. + IMGUI_DEBUG_LOG_DOCKING("[docking] Remap window '%s' %08X -> %08X\n", window->Name, src_dock_id, dst_dock_id); + dock_remaining_windows.push_back(DockRemainingWindowTask(window, dst_dock_id)); + } + } + for (const DockRemainingWindowTask& task : dock_remaining_windows) + DockBuilderDockWindow(task.Window->Name, task.DockId); +} + +// FIXME-DOCK: This is awkward because in series of split user is likely to loose access to its root node. +void ImGui::DockBuilderFinish(ImGuiID root_id) +{ + ImGuiContext& g = *GImGui; + //DockContextRebuild(&g); + DockContextBuildAddWindowsToNodes(&g, root_id); +} + +//----------------------------------------------------------------------------- +// Docking: Begin/End Support Functions (called from Begin/End) +//----------------------------------------------------------------------------- +// - GetWindowAlwaysWantOwnTabBar() +// - DockContextBindNodeToWindow() +// - BeginDocked() +// - BeginDockableDragDropSource() +// - BeginDockableDragDropTarget() +//----------------------------------------------------------------------------- + +bool ImGui::GetWindowAlwaysWantOwnTabBar(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (g.IO.ConfigDockingAlwaysTabBar || window->WindowClass.DockingAlwaysTabBar) + if ((window->Flags & (ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoDocking)) == 0) + if (!window->IsFallbackWindow) // We don't support AlwaysTabBar on the fallback/implicit window to avoid unused dock-node overhead/noise + return true; + return false; +} + +static ImGuiDockNode* ImGui::DockContextBindNodeToWindow(ImGuiContext* ctx, ImGuiWindow* window) +{ + ImGuiContext& g = *ctx; + ImGuiDockNode* node = DockContextFindNodeByID(ctx, window->DockId); + IM_ASSERT(window->DockNode == NULL); + + // We should not be docking into a split node (SetWindowDock should avoid this) + if (node && node->IsSplitNode()) + { + DockContextProcessUndockWindow(ctx, window); + return NULL; + } + + // Create node + if (node == NULL) + { + node = DockContextAddNode(ctx, window->DockId); + node->AuthorityForPos = node->AuthorityForSize = node->AuthorityForViewport = ImGuiDataAuthority_Window; + node->LastFrameAlive = g.FrameCount; + } + + // If the node just turned visible and is part of a hierarchy, it doesn't have a Size assigned by DockNodeTreeUpdatePosSize() yet, + // so we're forcing a Pos/Size update from the first ancestor that is already visible (often it will be the root node). + // If we don't do this, the window will be assigned a zero-size on its first frame, which won't ideally warm up the layout. + // This is a little wonky because we don't normally update the Pos/Size of visible node mid-frame. + if (!node->IsVisible) + { + ImGuiDockNode* ancestor_node = node; + while (!ancestor_node->IsVisible && ancestor_node->ParentNode) + ancestor_node = ancestor_node->ParentNode; + IM_ASSERT(ancestor_node->Size.x > 0.0f && ancestor_node->Size.y > 0.0f); + DockNodeUpdateHasCentralNodeChild(DockNodeGetRootNode(ancestor_node)); + DockNodeTreeUpdatePosSize(ancestor_node, ancestor_node->Pos, ancestor_node->Size, node); + } + + // Add window to node + bool node_was_visible = node->IsVisible; + DockNodeAddWindow(node, window, true); + node->IsVisible = node_was_visible; // Don't mark visible right away (so DockContextEndFrame() doesn't render it, maybe other side effects? will see) + IM_ASSERT(node == window->DockNode); + return node; +} + +static void StoreDockStyleForWindow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + for (int color_n = 0; color_n < ImGuiWindowDockStyleCol_COUNT; color_n++) + window->DockStyle.Colors[color_n] = ImGui::ColorConvertFloat4ToU32(g.Style.Colors[GWindowDockStyleColors[color_n]]); +} + +void ImGui::BeginDocked(ImGuiWindow* window, bool* p_open) +{ + ImGuiContext& g = *GImGui; + + // Clear fields ahead so most early-out paths don't have to do it + window->DockIsActive = window->DockNodeIsVisible = window->DockTabIsVisible = false; + + const bool auto_dock_node = GetWindowAlwaysWantOwnTabBar(window); + if (auto_dock_node) + { + if (window->DockId == 0) + { + IM_ASSERT(window->DockNode == NULL); + window->DockId = DockContextGenNodeID(&g); + } + } + else + { + // Calling SetNextWindowPos() undock windows by default (by setting PosUndock) + bool want_undock = false; + want_undock |= (window->Flags & ImGuiWindowFlags_NoDocking) != 0; + want_undock |= (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasPos) && (window->SetWindowPosAllowFlags & g.NextWindowData.PosCond) && g.NextWindowData.PosUndock; + if (want_undock) + { + DockContextProcessUndockWindow(&g, window); + return; + } + } + + // Bind to our dock node + ImGuiDockNode* node = window->DockNode; + if (node != NULL) + IM_ASSERT(window->DockId == node->ID); + if (window->DockId != 0 && node == NULL) + { + node = DockContextBindNodeToWindow(&g, window); + if (node == NULL) + return; + } + +#if 0 + // Undock if the ImGuiDockNodeFlags_NoDockingInCentralNode got set + if (node->IsCentralNode && (node->Flags & ImGuiDockNodeFlags_NoDockingInCentralNode)) + { + DockContextProcessUndockWindow(ctx, window); + return; + } +#endif + + // Undock if our dockspace node disappeared + // Note how we are testing for LastFrameAlive and NOT LastFrameActive. A DockSpace node can be maintained alive while being inactive with ImGuiDockNodeFlags_KeepAliveOnly. + if (node->LastFrameAlive < g.FrameCount) + { + // If the window has been orphaned, transition the docknode to an implicit node processed in DockContextNewFrameUpdateDocking() + ImGuiDockNode* root_node = DockNodeGetRootNode(node); + if (root_node->LastFrameAlive < g.FrameCount) + DockContextProcessUndockWindow(&g, window); + else + window->DockIsActive = true; + return; + } + + // Store style overrides + StoreDockStyleForWindow(window); + + // Fast path return. It is common for windows to hold on a persistent DockId but be the only visible window, + // and never create neither a host window neither a tab bar. + // FIXME-DOCK: replace ->HostWindow NULL compare with something more explicit (~was initially intended as a first frame test) + if (node->HostWindow == NULL) + { + if (node->State == ImGuiDockNodeState_HostWindowHiddenBecauseWindowsAreResizing) + window->DockIsActive = true; + if (node->Windows.Size > 1 && window->Appearing) // Only hide appearing window + DockNodeHideWindowDuringHostWindowCreation(window); + return; + } + + // We can have zero-sized nodes (e.g. children of a small-size dockspace) + IM_ASSERT(node->HostWindow); + IM_ASSERT(node->IsLeafNode()); + IM_ASSERT(node->Size.x >= 0.0f && node->Size.y >= 0.0f); + node->State = ImGuiDockNodeState_HostWindowVisible; + + // Undock if we are submitted earlier than the host window + if (!(node->MergedFlags & ImGuiDockNodeFlags_KeepAliveOnly) && window->BeginOrderWithinContext < node->HostWindow->BeginOrderWithinContext) + { + DockContextProcessUndockWindow(&g, window); + return; + } + + // Position/Size window + SetNextWindowPos(node->Pos); + SetNextWindowSize(node->Size); + g.NextWindowData.PosUndock = false; // Cancel implicit undocking of SetNextWindowPos() + window->DockIsActive = true; + window->DockNodeIsVisible = true; + window->DockTabIsVisible = false; + if (node->MergedFlags & ImGuiDockNodeFlags_KeepAliveOnly) + return; + + // When the window is selected we mark it as visible. + if (node->VisibleWindow == window) + window->DockTabIsVisible = true; + + // Update window flag + IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) == 0); + window->Flags |= ImGuiWindowFlags_ChildWindow | ImGuiWindowFlags_NoResize; + window->ChildFlags |= ImGuiChildFlags_AlwaysUseWindowPadding; + if (node->IsHiddenTabBar() || node->IsNoTabBar()) + window->Flags |= ImGuiWindowFlags_NoTitleBar; + else + window->Flags &= ~ImGuiWindowFlags_NoTitleBar; // Clear the NoTitleBar flag in case the user set it: confusingly enough we need a title bar height so we are correctly offset, but it won't be displayed! + + // Save new dock order only if the window has been visible once already + // This allows multiple windows to be created in the same frame and have their respective dock orders preserved. + if (node->TabBar && window->WasActive) + window->DockOrder = (short)DockNodeGetTabOrder(window); + + if ((node->WantCloseAll || node->WantCloseTabId == window->TabId) && p_open != NULL) + *p_open = false; + + // Update ChildId to allow returning from Child to Parent with Escape + ImGuiWindow* parent_window = window->DockNode->HostWindow; + window->ChildId = parent_window->GetID(window->Name); +} + +void ImGui::BeginDockableDragDropSource(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ActiveId == window->MoveId); + IM_ASSERT(g.MovingWindow == window); + IM_ASSERT(g.CurrentWindow == window); + + // 0: Hold SHIFT to disable docking, 1: Hold SHIFT to enable docking. + if (g.IO.ConfigDockingWithShift != g.IO.KeyShift) + { + // When ConfigDockingWithShift is set, display a tooltip to increase UI affordance. + // We cannot set for HoveredWindowUnderMovingWindow != NULL here, as it is only valid/useful when drag and drop is already active + // (because of the 'is_mouse_dragging_with_an_expected_destination' logic in UpdateViewportsNewFrame() function) + IM_ASSERT(g.NextWindowData.HasFlags == 0); + if (g.IO.ConfigDockingWithShift && g.MouseStationaryTimer >= 1.0f && g.ActiveId >= 1.0f) + SetTooltip("%s", LocalizeGetMsg(ImGuiLocKey_DockingHoldShiftToDock)); + return; + } + + g.LastItemData.ID = window->MoveId; + window = window->RootWindowDockTree; + IM_ASSERT((window->Flags & ImGuiWindowFlags_NoDocking) == 0); + bool is_drag_docking = (g.IO.ConfigDockingWithShift) || ImRect(0, 0, window->SizeFull.x, GetFrameHeight()).Contains(g.ActiveIdClickOffset); // FIXME-DOCKING: Need to make this stateful and explicit + ImGuiDragDropFlags drag_drop_flags = ImGuiDragDropFlags_SourceNoPreviewTooltip | ImGuiDragDropFlags_SourceNoHoldToOpenOthers | ImGuiDragDropFlags_PayloadAutoExpire | ImGuiDragDropFlags_PayloadNoCrossContext | ImGuiDragDropFlags_PayloadNoCrossProcess; + if (is_drag_docking && BeginDragDropSource(drag_drop_flags)) + { + SetDragDropPayload(IMGUI_PAYLOAD_TYPE_WINDOW, &window, sizeof(window)); + EndDragDropSource(); + StoreDockStyleForWindow(window); // Store style overrides while dragging (even when not docked) because docking preview may need it. + } +} + +void ImGui::BeginDockableDragDropTarget(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + + //IM_ASSERT(window->RootWindowDockTree == window); // May also be a DockSpace + IM_ASSERT((window->Flags & ImGuiWindowFlags_NoDocking) == 0); + if (!g.DragDropActive) + return; + //GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!BeginDragDropTargetCustom(window->Rect(), window->ID)) + return; + + // Peek into the payload before calling AcceptDragDropPayload() so we can handle overlapping dock nodes with filtering + // (this is a little unusual pattern, normally most code would call AcceptDragDropPayload directly) + const ImGuiPayload* payload = &g.DragDropPayload; + if (!payload->IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW) || !DockNodeIsDropAllowed(window, *(ImGuiWindow**)payload->Data)) + { + EndDragDropTarget(); + return; + } + + ImGuiWindow* payload_window = *(ImGuiWindow**)payload->Data; + if (AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_WINDOW, ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect)) + { + // Select target node + // (Important: we cannot use g.HoveredDockNode here! Because each of our target node have filters based on payload, each candidate drop target will do its own evaluation) + bool dock_into_floating_window = false; + ImGuiDockNode* node = NULL; + if (window->DockNodeAsHost) + { + // Cannot assume that node will != NULL even though we passed the rectangle test: it depends on padding/spacing handled by DockNodeTreeFindVisibleNodeByPos(). + node = DockNodeTreeFindVisibleNodeByPos(window->DockNodeAsHost, g.IO.MousePos); + + // There is an edge case when docking into a dockspace which only has _inactive_ nodes (because none of the windows are active) + // In this case we need to fallback into any leaf mode, possibly the central node. + // FIXME-20181220: We should not have to test for IsLeafNode() here but we have another bug to fix first. + if (node && node->IsDockSpace() && node->IsRootNode()) + node = (node->CentralNode && node->IsLeafNode()) ? node->CentralNode : DockNodeTreeFindFallbackLeafNode(node); + } + else + { + if (window->DockNode) + node = window->DockNode; + else + dock_into_floating_window = true; // Dock into a regular window + } + + const ImRect explicit_target_rect = (node && node->TabBar && !node->IsHiddenTabBar() && !node->IsNoTabBar()) ? node->TabBar->BarRect : ImRect(window->Pos, window->Pos + ImVec2(window->Size.x, GetFrameHeight())); + const bool is_explicit_target = g.IO.ConfigDockingWithShift || IsMouseHoveringRect(explicit_target_rect.Min, explicit_target_rect.Max); + + // Preview docking request and find out split direction/ratio + //const bool do_preview = true; // Ignore testing for payload->IsPreview() which removes one frame of delay, but breaks overlapping drop targets within the same window. + const bool do_preview = payload->IsPreview() || payload->IsDelivery(); + if (do_preview && (node != NULL || dock_into_floating_window)) + { + // If we have a non-leaf node it means we are hovering the border of a parent node, in which case only outer markers will appear. + ImGuiDockPreviewData split_inner; + ImGuiDockPreviewData split_outer; + ImGuiDockPreviewData* split_data = &split_inner; + if (node && (node->ParentNode || node->IsCentralNode() || !node->IsLeafNode())) + if (ImGuiDockNode* root_node = DockNodeGetRootNode(node)) + { + DockNodePreviewDockSetup(window, root_node, payload_window, NULL, &split_outer, is_explicit_target, true); + if (split_outer.IsSplitDirExplicit) + split_data = &split_outer; + } + if (!node || node->IsLeafNode()) + DockNodePreviewDockSetup(window, node, payload_window, NULL, &split_inner, is_explicit_target, false); + if (split_data == &split_outer) + split_inner.IsDropAllowed = false; + + // Draw inner then outer, so that previewed tab (in inner data) will be behind the outer drop boxes + DockNodePreviewDockRender(window, node, payload_window, &split_inner); + DockNodePreviewDockRender(window, node, payload_window, &split_outer); + + // Queue docking request + if (split_data->IsDropAllowed && payload->IsDelivery()) + DockContextQueueDock(&g, window, split_data->SplitNode, payload_window, split_data->SplitDir, split_data->SplitRatio, split_data == &split_outer); + } + } + EndDragDropTarget(); +} + +//----------------------------------------------------------------------------- +// Docking: Settings +//----------------------------------------------------------------------------- +// - DockSettingsRenameNodeReferences() +// - DockSettingsRemoveNodeReferences() +// - DockSettingsFindNodeSettings() +// - DockSettingsHandler_ApplyAll() +// - DockSettingsHandler_ReadOpen() +// - DockSettingsHandler_ReadLine() +// - DockSettingsHandler_DockNodeToSettings() +// - DockSettingsHandler_WriteAll() +//----------------------------------------------------------------------------- + +static void ImGui::DockSettingsRenameNodeReferences(ImGuiID old_node_id, ImGuiID new_node_id) +{ + ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_DOCKING("[docking] DockSettingsRenameNodeReferences: from 0x%08X -> to 0x%08X\n", old_node_id, new_node_id); + for (int window_n = 0; window_n < g.Windows.Size; window_n++) + { + ImGuiWindow* window = g.Windows[window_n]; + if (window->DockId == old_node_id && window->DockNode == NULL) + window->DockId = new_node_id; + } + //// FIXME-OPT: We could remove this loop by storing the index in the map + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->DockId == old_node_id) + settings->DockId = new_node_id; +} + +// Remove references stored in ImGuiWindowSettings to the given ImGuiDockNodeSettings +static void ImGui::DockSettingsRemoveNodeReferences(ImGuiID* node_ids, int node_ids_count) +{ + ImGuiContext& g = *GImGui; + int found = 0; + //// FIXME-OPT: We could remove this loop by storing the index in the map + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + for (int node_n = 0; node_n < node_ids_count; node_n++) + if (settings->DockId == node_ids[node_n]) + { + settings->DockId = 0; + settings->DockOrder = -1; + if (++found < node_ids_count) + break; + return; + } +} + +static ImGuiDockNodeSettings* ImGui::DockSettingsFindNodeSettings(ImGuiContext* ctx, ImGuiID id) +{ + // FIXME-OPT + ImGuiDockContext* dc = &ctx->DockContext; + for (int n = 0; n < dc->NodesSettings.Size; n++) + if (dc->NodesSettings[n].ID == id) + return &dc->NodesSettings[n]; + return NULL; +} + +// Clear settings data +static void ImGui::DockSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiDockContext* dc = &ctx->DockContext; + dc->NodesSettings.clear(); + DockContextClearNodes(ctx, 0, true); +} + +// Recreate nodes based on settings data +static void ImGui::DockSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + // Prune settings at boot time only + ImGuiDockContext* dc = &ctx->DockContext; + if (ctx->Windows.Size == 0) + DockContextPruneUnusedSettingsNodes(ctx); + DockContextBuildNodesFromSettings(ctx, dc->NodesSettings.Data, dc->NodesSettings.Size); + DockContextBuildAddWindowsToNodes(ctx, 0); +} + +static void* ImGui::DockSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + if (strcmp(name, "Data") != 0) + return NULL; + return (void*)1; +} + +static void ImGui::DockSettingsHandler_ReadLine(ImGuiContext* ctx, ImGuiSettingsHandler*, void*, const char* line) +{ + char c = 0; + int x = 0, y = 0; + int r = 0; + + // Parsing, e.g. + // " DockNode ID=0x00000001 Pos=383,193 Size=201,322 Split=Y,0.506 " + // " DockNode ID=0x00000002 Parent=0x00000001 " + // Important: this code expect currently fields in a fixed order. + ImGuiDockNodeSettings node; + line = ImStrSkipBlank(line); + if (strncmp(line, "DockNode", 8) == 0) { line = ImStrSkipBlank(line + strlen("DockNode")); } + else if (strncmp(line, "DockSpace", 9) == 0) { line = ImStrSkipBlank(line + strlen("DockSpace")); node.Flags |= ImGuiDockNodeFlags_DockSpace; } + else return; + if (sscanf(line, "ID=0x%08X%n", &node.ID, &r) == 1) { line += r; } else return; + if (sscanf(line, " Parent=0x%08X%n", &node.ParentNodeId, &r) == 1) { line += r; if (node.ParentNodeId == 0) return; } + if (sscanf(line, " Window=0x%08X%n", &node.ParentWindowId, &r) ==1) { line += r; if (node.ParentWindowId == 0) return; } + if (node.ParentNodeId == 0) + { + if (sscanf(line, " Pos=%i,%i%n", &x, &y, &r) == 2) { line += r; node.Pos = ImVec2ih((short)x, (short)y); } else return; + if (sscanf(line, " Size=%i,%i%n", &x, &y, &r) == 2) { line += r; node.Size = ImVec2ih((short)x, (short)y); } else return; + } + else + { + if (sscanf(line, " SizeRef=%i,%i%n", &x, &y, &r) == 2) { line += r; node.SizeRef = ImVec2ih((short)x, (short)y); } + } + if (sscanf(line, " Split=%c%n", &c, &r) == 1) { line += r; if (c == 'X') node.SplitAxis = ImGuiAxis_X; else if (c == 'Y') node.SplitAxis = ImGuiAxis_Y; } + if (sscanf(line, " NoResize=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_NoResize; } + if (sscanf(line, " CentralNode=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_CentralNode; } + if (sscanf(line, " NoTabBar=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_NoTabBar; } + if (sscanf(line, " HiddenTabBar=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_HiddenTabBar; } + if (sscanf(line, " NoWindowMenuButton=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_NoWindowMenuButton; } + if (sscanf(line, " NoCloseButton=%d%n", &x, &r) == 1) { line += r; if (x != 0) node.Flags |= ImGuiDockNodeFlags_NoCloseButton; } + if (sscanf(line, " Selected=0x%08X%n", &node.SelectedTabId,&r) == 1) { line += r; } + if (node.ParentNodeId != 0) + if (ImGuiDockNodeSettings* parent_settings = DockSettingsFindNodeSettings(ctx, node.ParentNodeId)) + node.Depth = parent_settings->Depth + 1; + ctx->DockContext.NodesSettings.push_back(node); +} + +static void DockSettingsHandler_DockNodeToSettings(ImGuiDockContext* dc, ImGuiDockNode* node, int depth) +{ + ImGuiDockNodeSettings node_settings; + IM_ASSERT(depth < (1 << (sizeof(node_settings.Depth) << 3))); + node_settings.ID = node->ID; + node_settings.ParentNodeId = node->ParentNode ? node->ParentNode->ID : 0; + node_settings.ParentWindowId = (node->IsDockSpace() && node->HostWindow && node->HostWindow->ParentWindow) ? node->HostWindow->ParentWindow->ID : 0; + node_settings.SelectedTabId = node->SelectedTabId; + node_settings.SplitAxis = (signed char)(node->IsSplitNode() ? node->SplitAxis : ImGuiAxis_None); + node_settings.Depth = (char)depth; + node_settings.Flags = (node->LocalFlags & ImGuiDockNodeFlags_SavedFlagsMask_); + node_settings.Pos = ImVec2ih(node->Pos); + node_settings.Size = ImVec2ih(node->Size); + node_settings.SizeRef = ImVec2ih(node->SizeRef); + dc->NodesSettings.push_back(node_settings); + if (node->ChildNodes[0]) + DockSettingsHandler_DockNodeToSettings(dc, node->ChildNodes[0], depth + 1); + if (node->ChildNodes[1]) + DockSettingsHandler_DockNodeToSettings(dc, node->ChildNodes[1], depth + 1); +} + +static void ImGui::DockSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + ImGuiContext& g = *ctx; + ImGuiDockContext* dc = &ctx->DockContext; + if (!(g.IO.ConfigFlags & ImGuiConfigFlags_DockingEnable)) + return; + + // Gather settings data + // (unlike our windows settings, because nodes are always built we can do a full rewrite of the SettingsNode buffer) + dc->NodesSettings.resize(0); + dc->NodesSettings.reserve(dc->Nodes.Data.Size); + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (node->IsRootNode()) + DockSettingsHandler_DockNodeToSettings(dc, node, 0); + + int max_depth = 0; + for (int node_n = 0; node_n < dc->NodesSettings.Size; node_n++) + max_depth = ImMax((int)dc->NodesSettings[node_n].Depth, max_depth); + + // Write to text buffer + buf->appendf("[%s][Data]\n", handler->TypeName); + for (int node_n = 0; node_n < dc->NodesSettings.Size; node_n++) + { + const int line_start_pos = buf->size(); (void)line_start_pos; + const ImGuiDockNodeSettings* node_settings = &dc->NodesSettings[node_n]; + buf->appendf("%*s%s%*s", node_settings->Depth * 2, "", (node_settings->Flags & ImGuiDockNodeFlags_DockSpace) ? "DockSpace" : "DockNode ", (max_depth - node_settings->Depth) * 2, ""); // Text align nodes to facilitate looking at .ini file + buf->appendf(" ID=0x%08X", node_settings->ID); + if (node_settings->ParentNodeId) + { + buf->appendf(" Parent=0x%08X SizeRef=%d,%d", node_settings->ParentNodeId, node_settings->SizeRef.x, node_settings->SizeRef.y); + } + else + { + if (node_settings->ParentWindowId) + buf->appendf(" Window=0x%08X", node_settings->ParentWindowId); + buf->appendf(" Pos=%d,%d Size=%d,%d", node_settings->Pos.x, node_settings->Pos.y, node_settings->Size.x, node_settings->Size.y); + } + if (node_settings->SplitAxis != ImGuiAxis_None) + buf->appendf(" Split=%c", (node_settings->SplitAxis == ImGuiAxis_X) ? 'X' : 'Y'); + if (node_settings->Flags & ImGuiDockNodeFlags_NoResize) + buf->appendf(" NoResize=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_CentralNode) + buf->appendf(" CentralNode=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_NoTabBar) + buf->appendf(" NoTabBar=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_HiddenTabBar) + buf->appendf(" HiddenTabBar=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_NoWindowMenuButton) + buf->appendf(" NoWindowMenuButton=1"); + if (node_settings->Flags & ImGuiDockNodeFlags_NoCloseButton) + buf->appendf(" NoCloseButton=1"); + if (node_settings->SelectedTabId) + buf->appendf(" Selected=0x%08X", node_settings->SelectedTabId); + + // [DEBUG] Include comments in the .ini file to ease debugging (this makes saving slower!) + if (g.IO.ConfigDebugIniSettings) + if (ImGuiDockNode* node = DockContextFindNodeByID(ctx, node_settings->ID)) + { + buf->appendf("%*s", ImMax(2, (line_start_pos + 92) - buf->size()), ""); // Align everything + if (node->IsDockSpace() && node->HostWindow && node->HostWindow->ParentWindow) + buf->appendf(" ; in '%s'", node->HostWindow->ParentWindow->Name); + // Iterate settings so we can give info about windows that didn't exist during the session. + int contains_window = 0; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->DockId == node_settings->ID) + { + if (contains_window++ == 0) + buf->appendf(" ; contains "); + buf->appendf("'%s' ", settings->GetName()); + } + } + + buf->appendf("\n"); + } + buf->appendf("\n"); +} //----------------------------------------------------------------------------- // [SECTION] PLATFORM DEPENDENT HELPERS //----------------------------------------------------------------------------- - -#if defined(_WIN32) && !defined(_WINDOWS_) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && (!defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) || !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS)) -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#ifndef __MINGW32__ -#include -#else -#include -#endif -#elif defined(__APPLE__) -#include -#endif +// - Default clipboard handlers +// - Default shell function handlers +// - Default IME handlers +//----------------------------------------------------------------------------- #if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_CLIPBOARD_FUNCTIONS) #ifdef _MSC_VER #pragma comment(lib, "user32") +#pragma comment(lib, "kernel32") #endif // Win32 clipboard implementation -static const char* GetClipboardTextFn_DefaultImpl(void*) +// We use g.ClipboardHandlerData for temporary storage to ensure it is freed on Shutdown() +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) { - static ImVector buf_local; - buf_local.clear(); + ImGuiContext& g = *ctx; + g.ClipboardHandlerData.clear(); if (!::OpenClipboard(NULL)) return NULL; HANDLE wbuf_handle = ::GetClipboardData(CF_UNICODETEXT); @@ -9636,30 +20681,30 @@ static const char* GetClipboardTextFn_DefaultImpl(void*) ::CloseClipboard(); return NULL; } - if (ImWchar* wbuf_global = (ImWchar*)::GlobalLock(wbuf_handle)) + if (const WCHAR* wbuf_global = (const WCHAR*)::GlobalLock(wbuf_handle)) { - int buf_len = ImTextCountUtf8BytesFromStr(wbuf_global, NULL) + 1; - buf_local.resize(buf_len); - ImTextStrToUtf8(buf_local.Data, buf_len, wbuf_global, NULL); + int buf_len = ::WideCharToMultiByte(CP_UTF8, 0, wbuf_global, -1, NULL, 0, NULL, NULL); + g.ClipboardHandlerData.resize(buf_len); + ::WideCharToMultiByte(CP_UTF8, 0, wbuf_global, -1, g.ClipboardHandlerData.Data, buf_len, NULL, NULL); } ::GlobalUnlock(wbuf_handle); ::CloseClipboard(); - return buf_local.Data; + return g.ClipboardHandlerData.Data; } -static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext*, const char* text) { if (!::OpenClipboard(NULL)) return; - const int wbuf_length = ImTextCountCharsFromUtf8(text, NULL) + 1; - HGLOBAL wbuf_handle = ::GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)wbuf_length * sizeof(ImWchar)); + const int wbuf_length = ::MultiByteToWideChar(CP_UTF8, 0, text, -1, NULL, 0); + HGLOBAL wbuf_handle = ::GlobalAlloc(GMEM_MOVEABLE, (SIZE_T)wbuf_length * sizeof(WCHAR)); if (wbuf_handle == NULL) { ::CloseClipboard(); return; } - ImWchar* wbuf_global = (ImWchar*)::GlobalLock(wbuf_handle); - ImTextStrFromUtf8(wbuf_global, wbuf_length, text, NULL); + WCHAR* wbuf_global = (WCHAR*)::GlobalLock(wbuf_handle); + ::MultiByteToWideChar(CP_UTF8, 0, text, -1, wbuf_global, wbuf_length); ::GlobalUnlock(wbuf_handle); ::EmptyClipboard(); if (::SetClipboardData(CF_UNICODETEXT, wbuf_handle) == NULL) @@ -9674,12 +20719,12 @@ static PasteboardRef main_clipboard = 0; // OSX clipboard implementation // If you enable this you will need to add '-framework ApplicationServices' to your linker command-line! -static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext*, const char* text) { if (!main_clipboard) PasteboardCreate(kPasteboardClipboard, &main_clipboard); PasteboardClear(main_clipboard); - CFDataRef cf_data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)text, strlen(text)); + CFDataRef cf_data = CFDataCreate(kCFAllocatorDefault, (const UInt8*)text, ImStrlen(text)); if (cf_data) { PasteboardPutItemFlavor(main_clipboard, (PasteboardItemID)1, CFSTR("public.utf8-plain-text"), cf_data, 0); @@ -9687,8 +20732,9 @@ static void SetClipboardTextFn_DefaultImpl(void*, const char* text) } } -static const char* GetClipboardTextFn_DefaultImpl(void*) +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) { + ImGuiContext& g = *ctx; if (!main_clipboard) PasteboardCreate(kPasteboardClipboard, &main_clipboard); PasteboardSynchronize(main_clipboard); @@ -9706,13 +20752,13 @@ static const char* GetClipboardTextFn_DefaultImpl(void*) CFDataRef cf_data; if (PasteboardCopyItemFlavorData(main_clipboard, item_id, CFSTR("public.utf8-plain-text"), &cf_data) == noErr) { - static ImVector clipboard_text; + g.ClipboardHandlerData.clear(); int length = (int)CFDataGetLength(cf_data); - clipboard_text.resize(length + 1); - CFDataGetBytes(cf_data, CFRangeMake(0, length), (UInt8*)clipboard_text.Data); - clipboard_text[length] = 0; + g.ClipboardHandlerData.resize(length + 1); + CFDataGetBytes(cf_data, CFRangeMake(0, length), (UInt8*)g.ClipboardHandlerData.Data); + g.ClipboardHandlerData[length] = 0; CFRelease(cf_data); - return clipboard_text.Data; + return g.ClipboardHandlerData.Data; } } } @@ -9722,93 +20768,439 @@ static const char* GetClipboardTextFn_DefaultImpl(void*) #else // Local Dear ImGui-only clipboard implementation, if user hasn't defined better clipboard handlers. -static const char* GetClipboardTextFn_DefaultImpl(void*) +static const char* Platform_GetClipboardTextFn_DefaultImpl(ImGuiContext* ctx) { - ImGuiContext& g = *GImGui; - return g.PrivateClipboard.empty() ? NULL : g.PrivateClipboard.begin(); + ImGuiContext& g = *ctx; + return g.ClipboardHandlerData.empty() ? NULL : g.ClipboardHandlerData.begin(); } -static void SetClipboardTextFn_DefaultImpl(void*, const char* text) +static void Platform_SetClipboardTextFn_DefaultImpl(ImGuiContext* ctx, const char* text) { - ImGuiContext& g = *GImGui; - g.PrivateClipboard.clear(); - const char* text_end = text + strlen(text); - g.PrivateClipboard.resize((int)(text_end - text) + 1); - memcpy(&g.PrivateClipboard[0], text, (size_t)(text_end - text)); - g.PrivateClipboard[(int)(text_end - text)] = 0; + ImGuiContext& g = *ctx; + g.ClipboardHandlerData.clear(); + const char* text_end = text + ImStrlen(text); + g.ClipboardHandlerData.resize((int)(text_end - text) + 1); + memcpy(&g.ClipboardHandlerData[0], text, (size_t)(text_end - text)); + g.ClipboardHandlerData[(int)(text_end - text)] = 0; } +#endif // Default clipboard handlers + +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#if defined(__APPLE__) && TARGET_OS_IPHONE +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS #endif +#if defined(__3DS__) +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif +#if defined(_WIN32) && defined(IMGUI_DISABLE_WIN32_FUNCTIONS) +#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#endif +#endif // #ifndef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS + +#ifndef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS +#ifdef _WIN32 +#include // ShellExecuteA() +#ifdef _MSC_VER +#pragma comment(lib, "shell32") +#endif +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char* path) +{ + const int path_wsize = ::MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0); + ImVector path_wbuf; + path_wbuf.resize(path_wsize); + ::MultiByteToWideChar(CP_UTF8, 0, path, -1, path_wbuf.Data, path_wsize); + return (INT_PTR)::ShellExecuteW(NULL, L"open", path_wbuf.Data, NULL, NULL, SW_SHOWDEFAULT) > 32; +} +#else +#include +#include +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char* path) +{ +#if defined(__APPLE__) + const char* args[] { "open", "--", path, NULL }; +#else + const char* args[] { "xdg-open", path, NULL }; +#endif + pid_t pid = fork(); + if (pid < 0) + return false; + if (!pid) + { + execvp(args[0], const_cast(args)); + exit(-1); + } + else + { + int status; + waitpid(pid, &status, 0); + return WEXITSTATUS(status) == 0; + } +} +#endif +#else +static bool Platform_OpenInShellFn_DefaultImpl(ImGuiContext*, const char*) { return false; } +#endif // Default shell handlers + +//----------------------------------------------------------------------------- // Win32 API IME support (for Asian languages, etc.) -#if defined(_WIN32) && !defined(__GNUC__) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) +#if defined(_WIN32) && !defined(IMGUI_DISABLE_WIN32_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) #include #ifdef _MSC_VER #pragma comment(lib, "imm32") #endif -static void ImeSetInputScreenPosFn_DefaultImpl(int x, int y) +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext*, ImGuiViewport* viewport, ImGuiPlatformImeData* data) { // Notify OS Input Method Editor of text input position - ImGuiIO& io = ImGui::GetIO(); - if (HWND hwnd = (HWND)io.ImeWindowHandle) - if (HIMC himc = ::ImmGetContext(hwnd)) - { - COMPOSITIONFORM cf; - cf.ptCurrentPos.x = x; - cf.ptCurrentPos.y = y; - cf.dwStyle = CFS_FORCE_POSITION; - ::ImmSetCompositionWindow(himc, &cf); - ::ImmReleaseContext(hwnd, himc); - } + HWND hwnd = (HWND)viewport->PlatformHandleRaw; + if (hwnd == 0) + return; + + //::ImmAssociateContextEx(hwnd, NULL, data->WantVisible ? IACE_DEFAULT : 0); + if (HIMC himc = ::ImmGetContext(hwnd)) + { + COMPOSITIONFORM composition_form = {}; + composition_form.ptCurrentPos.x = (LONG)(data->InputPos.x - viewport->Pos.x); + composition_form.ptCurrentPos.y = (LONG)(data->InputPos.y - viewport->Pos.y); + composition_form.dwStyle = CFS_FORCE_POSITION; + ::ImmSetCompositionWindow(himc, &composition_form); + CANDIDATEFORM candidate_form = {}; + candidate_form.dwStyle = CFS_CANDIDATEPOS; + candidate_form.ptCurrentPos.x = (LONG)(data->InputPos.x - viewport->Pos.x); + candidate_form.ptCurrentPos.y = (LONG)(data->InputPos.y - viewport->Pos.y); + ::ImmSetCandidateWindow(himc, &candidate_form); + ::ImmReleaseContext(hwnd, himc); + } } #else -static void ImeSetInputScreenPosFn_DefaultImpl(int, int) {} +static void Platform_SetImeDataFn_DefaultImpl(ImGuiContext*, ImGuiViewport*, ImGuiPlatformImeData*) {} -#endif +#endif // Default IME handlers //----------------------------------------------------------------------------- -// [SECTION] METRICS/DEBUG WINDOW +// [SECTION] METRICS/DEBUGGER WINDOW +//----------------------------------------------------------------------------- +// - DebugRenderViewportThumbnail() [Internal] +// - RenderViewportsThumbnails() [Internal] +// - DebugTextEncoding() +// - MetricsHelpMarker() [Internal] +// - ShowFontAtlas() [Internal] +// - ShowMetricsWindow() +// - DebugNodeColumns() [Internal] +// - DebugNodeDockNode() [Internal] +// - DebugNodeDrawList() [Internal] +// - DebugNodeDrawCmdShowMeshAndBoundingBox() [Internal] +// - DebugNodeFont() [Internal] +// - DebugNodeFontGlyph() [Internal] +// - DebugNodeStorage() [Internal] +// - DebugNodeTabBar() [Internal] +// - DebugNodeViewport() [Internal] +// - DebugNodeWindow() [Internal] +// - DebugNodeWindowSettings() [Internal] +// - DebugNodeWindowsList() [Internal] +// - DebugNodeWindowsListByBeginStackParent() [Internal] //----------------------------------------------------------------------------- -#ifndef IMGUI_DISABLE_METRICS_WINDOW +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + +void ImGui::DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + ImVec2 scale = bb.GetSize() / viewport->Size; + ImVec2 off = bb.Min - viewport->Pos * scale; + float alpha_mul = (viewport->Flags & ImGuiViewportFlags_IsMinimized) ? 0.30f : 1.00f; + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul * 0.40f)); + for (ImGuiWindow* thumb_window : g.Windows) + { + if (!thumb_window->WasActive || (thumb_window->Flags & ImGuiWindowFlags_ChildWindow)) + continue; + if (thumb_window->Viewport != viewport) + continue; + + ImRect thumb_r = thumb_window->Rect(); + ImRect title_r = thumb_window->TitleBarRect(); + thumb_r = ImRect(ImTrunc(off + thumb_r.Min * scale), ImTrunc(off + thumb_r.Max * scale)); + title_r = ImRect(ImTrunc(off + title_r.Min * scale), ImTrunc(off + ImVec2(title_r.Max.x, title_r.Min.y + title_r.GetHeight() * 3.0f) * scale)); // Exaggerate title bar height + thumb_r.ClipWithFull(bb); + title_r.ClipWithFull(bb); + const bool window_is_focused = (g.NavWindow && thumb_window->RootWindowForTitleBarHighlight == g.NavWindow->RootWindowForTitleBarHighlight); + window->DrawList->AddRectFilled(thumb_r.Min, thumb_r.Max, GetColorU32(ImGuiCol_WindowBg, alpha_mul)); + window->DrawList->AddRectFilled(title_r.Min, title_r.Max, GetColorU32(window_is_focused ? ImGuiCol_TitleBgActive : ImGuiCol_TitleBg, alpha_mul)); + window->DrawList->AddRect(thumb_r.Min, thumb_r.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); + window->DrawList->AddText(g.Font, g.FontSize * 1.0f, title_r.Min, GetColorU32(ImGuiCol_Text, alpha_mul), thumb_window->Name, FindRenderedTextEnd(thumb_window->Name)); + } + draw_list->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border, alpha_mul)); + if (viewport->ID == g.DebugMetricsConfig.HighlightViewportID) + window->DrawList->AddRect(bb.Min, bb.Max, IM_COL32(255, 255, 0, 255)); +} + +static void RenderViewportsThumbnails() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + // Draw monitor and calculate their boundaries + float SCALE = 1.0f / 8.0f; + ImRect bb_full(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + for (ImGuiPlatformMonitor& monitor : g.PlatformIO.Monitors) + bb_full.Add(ImRect(monitor.MainPos, monitor.MainPos + monitor.MainSize)); + ImVec2 p = window->DC.CursorPos; + ImVec2 off = p - bb_full.Min * SCALE; + for (ImGuiPlatformMonitor& monitor : g.PlatformIO.Monitors) + { + ImRect monitor_draw_bb(off + (monitor.MainPos) * SCALE, off + (monitor.MainPos + monitor.MainSize) * SCALE); + window->DrawList->AddRect(monitor_draw_bb.Min, monitor_draw_bb.Max, (g.DebugMetricsConfig.HighlightMonitorIdx == g.PlatformIO.Monitors.index_from_ptr(&monitor)) ? IM_COL32(255, 255, 0, 255) : ImGui::GetColorU32(ImGuiCol_Border), 4.0f); + window->DrawList->AddRectFilled(monitor_draw_bb.Min, monitor_draw_bb.Max, ImGui::GetColorU32(ImGuiCol_Border, 0.10f), 4.0f); + } + + // Draw viewports + for (ImGuiViewportP* viewport : g.Viewports) + { + ImRect viewport_draw_bb(off + (viewport->Pos) * SCALE, off + (viewport->Pos + viewport->Size) * SCALE); + ImGui::DebugRenderViewportThumbnail(window->DrawList, viewport, viewport_draw_bb); + } + ImGui::Dummy(bb_full.GetSize() * SCALE); +} + +static int IMGUI_CDECL ViewportComparerByLastFocusedStampCount(const void* lhs, const void* rhs) +{ + const ImGuiViewportP* a = *(const ImGuiViewportP* const*)lhs; + const ImGuiViewportP* b = *(const ImGuiViewportP* const*)rhs; + return b->LastFocusedStampCount - a->LastFocusedStampCount; +} + +// Draw an arbitrary US keyboard layout to visualize translated keys +void ImGui::DebugRenderKeyboardPreview(ImDrawList* draw_list) +{ + const float scale = ImGui::GetFontSize() / 13.0f; + const ImVec2 key_size = ImVec2(35.0f, 35.0f) * scale; + const float key_rounding = 3.0f * scale; + const ImVec2 key_face_size = ImVec2(25.0f, 25.0f) * scale; + const ImVec2 key_face_pos = ImVec2(5.0f, 3.0f) * scale; + const float key_face_rounding = 2.0f * scale; + const ImVec2 key_label_pos = ImVec2(7.0f, 4.0f) * scale; + const ImVec2 key_step = ImVec2(key_size.x - 1.0f, key_size.y - 1.0f); + const float key_row_offset = 9.0f * scale; + + ImVec2 board_min = GetCursorScreenPos(); + ImVec2 board_max = ImVec2(board_min.x + 3 * key_step.x + 2 * key_row_offset + 10.0f, board_min.y + 3 * key_step.y + 10.0f); + ImVec2 start_pos = ImVec2(board_min.x + 5.0f - key_step.x, board_min.y); + + struct KeyLayoutData { int Row, Col; const char* Label; ImGuiKey Key; }; + const KeyLayoutData keys_to_display[] = + { + { 0, 0, "", ImGuiKey_Tab }, { 0, 1, "Q", ImGuiKey_Q }, { 0, 2, "W", ImGuiKey_W }, { 0, 3, "E", ImGuiKey_E }, { 0, 4, "R", ImGuiKey_R }, + { 1, 0, "", ImGuiKey_CapsLock }, { 1, 1, "A", ImGuiKey_A }, { 1, 2, "S", ImGuiKey_S }, { 1, 3, "D", ImGuiKey_D }, { 1, 4, "F", ImGuiKey_F }, + { 2, 0, "", ImGuiKey_LeftShift },{ 2, 1, "Z", ImGuiKey_Z }, { 2, 2, "X", ImGuiKey_X }, { 2, 3, "C", ImGuiKey_C }, { 2, 4, "V", ImGuiKey_V } + }; + + // Elements rendered manually via ImDrawList API are not clipped automatically. + // While not strictly necessary, here IsItemVisible() is used to avoid rendering these shapes when they are out of view. + Dummy(board_max - board_min); + if (!IsItemVisible()) + return; + draw_list->PushClipRect(board_min, board_max, true); + for (int n = 0; n < IM_ARRAYSIZE(keys_to_display); n++) + { + const KeyLayoutData* key_data = &keys_to_display[n]; + ImVec2 key_min = ImVec2(start_pos.x + key_data->Col * key_step.x + key_data->Row * key_row_offset, start_pos.y + key_data->Row * key_step.y); + ImVec2 key_max = key_min + key_size; + draw_list->AddRectFilled(key_min, key_max, IM_COL32(204, 204, 204, 255), key_rounding); + draw_list->AddRect(key_min, key_max, IM_COL32(24, 24, 24, 255), key_rounding); + ImVec2 face_min = ImVec2(key_min.x + key_face_pos.x, key_min.y + key_face_pos.y); + ImVec2 face_max = ImVec2(face_min.x + key_face_size.x, face_min.y + key_face_size.y); + draw_list->AddRect(face_min, face_max, IM_COL32(193, 193, 193, 255), key_face_rounding, ImDrawFlags_None, 2.0f); + draw_list->AddRectFilled(face_min, face_max, IM_COL32(252, 252, 252, 255), key_face_rounding); + ImVec2 label_min = ImVec2(key_min.x + key_label_pos.x, key_min.y + key_label_pos.y); + draw_list->AddText(label_min, IM_COL32(64, 64, 64, 255), key_data->Label); + if (IsKeyDown(key_data->Key)) + draw_list->AddRectFilled(key_min, key_max, IM_COL32(255, 0, 0, 128), key_rounding); + } + draw_list->PopClipRect(); +} + +// Helper tool to diagnose between text encoding issues and font loading issues. Pass your UTF-8 string and verify that there are correct. +void ImGui::DebugTextEncoding(const char* str) +{ + Text("Text: \"%s\"", str); + if (!BeginTable("##DebugTextEncoding", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable)) + return; + TableSetupColumn("Offset"); + TableSetupColumn("UTF-8"); + TableSetupColumn("Glyph"); + TableSetupColumn("Codepoint"); + TableHeadersRow(); + for (const char* p = str; *p != 0; ) + { + unsigned int c; + const int c_utf8_len = ImTextCharFromUtf8(&c, p, NULL); + TableNextColumn(); + Text("%d", (int)(p - str)); + TableNextColumn(); + for (int byte_index = 0; byte_index < c_utf8_len; byte_index++) + { + if (byte_index > 0) + SameLine(); + Text("0x%02X", (int)(unsigned char)p[byte_index]); + } + TableNextColumn(); + if (GetFont()->FindGlyphNoFallback((ImWchar)c)) + TextUnformatted(p, p + c_utf8_len); + else + TextUnformatted((c == IM_UNICODE_CODEPOINT_INVALID) ? "[invalid]" : "[missing]"); + TableNextColumn(); + Text("U+%04X", (int)c); + p += c_utf8_len; + } + EndTable(); +} + +static void DebugFlashStyleColorStop() +{ + ImGuiContext& g = *GImGui; + if (g.DebugFlashStyleColorIdx != ImGuiCol_COUNT) + g.Style.Colors[g.DebugFlashStyleColorIdx] = g.DebugFlashStyleColorBackup; + g.DebugFlashStyleColorIdx = ImGuiCol_COUNT; +} + +// Flash a given style color for some + inhibit modifications of this color via PushStyleColor() calls. +void ImGui::DebugFlashStyleColor(ImGuiCol idx) +{ + ImGuiContext& g = *GImGui; + DebugFlashStyleColorStop(); + g.DebugFlashStyleColorTime = 0.5f; + g.DebugFlashStyleColorIdx = idx; + g.DebugFlashStyleColorBackup = g.Style.Colors[idx]; +} + +void ImGui::UpdateDebugToolFlashStyleColor() +{ + ImGuiContext& g = *GImGui; + if (g.DebugFlashStyleColorTime <= 0.0f) + return; + ColorConvertHSVtoRGB(ImCos(g.DebugFlashStyleColorTime * 6.0f) * 0.5f + 0.5f, 0.5f, 0.5f, g.Style.Colors[g.DebugFlashStyleColorIdx].x, g.Style.Colors[g.DebugFlashStyleColorIdx].y, g.Style.Colors[g.DebugFlashStyleColorIdx].z); + g.Style.Colors[g.DebugFlashStyleColorIdx].w = 1.0f; + if ((g.DebugFlashStyleColorTime -= g.IO.DeltaTime) <= 0.0f) + DebugFlashStyleColorStop(); +} + +static const char* FormatTextureIDForDebugDisplay(char* buf, int buf_size, ImTextureID tex_id) +{ + union { void* ptr; int integer; } tex_id_opaque; + memcpy(&tex_id_opaque, &tex_id, ImMin(sizeof(void*), sizeof(tex_id))); + if (sizeof(tex_id) >= sizeof(void*)) + ImFormatString(buf, buf_size, "0x%p", tex_id_opaque.ptr); + else + ImFormatString(buf, buf_size, "0x%04X", tex_id_opaque.integer); + return buf; +} + +// Avoid naming collision with imgui_demo.cpp's HelpMarker() for unity builds. +static void MetricsHelpMarker(const char* desc) +{ + ImGui::TextDisabled("(?)"); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); + ImGui::TextUnformatted(desc); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +// [DEBUG] List fonts in a font atlas and display its texture +void ImGui::ShowFontAtlas(ImFontAtlas* atlas) +{ + for (ImFont* font : atlas->Fonts) + { + PushID(font); + DebugNodeFont(font); + PopID(); + } + if (TreeNode("Font Atlas", "Font Atlas (%dx%d pixels)", atlas->TexWidth, atlas->TexHeight)) + { + ImGuiContext& g = *GImGui; + PushStyleVar(ImGuiStyleVar_ImageBorderSize, ImMax(1.0f, g.Style.ImageBorderSize)); + ImageWithBg(atlas->TexID, ImVec2((float)atlas->TexWidth, (float)atlas->TexHeight), ImVec2(0.0f, 0.0f), ImVec2(1.0f, 1.0f), ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + PopStyleVar(); + TreePop(); + } +} + void ImGui::ShowMetricsWindow(bool* p_open) { - if (!ImGui::Begin("Dear ImGui Metrics", p_open)) + ImGuiContext& g = *GImGui; + ImGuiIO& io = g.IO; + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + if (cfg->ShowDebugLog) + ShowDebugLogWindow(&cfg->ShowDebugLog); + if (cfg->ShowIDStackTool) + ShowIDStackToolWindow(&cfg->ShowIDStackTool); + + if (!Begin("Dear ImGui Metrics/Debugger", p_open) || GetCurrentWindow()->BeginCount > 1) { - ImGui::End(); + End(); return; } - // State - enum { WRT_OuterRect, WRT_OuterRectClipped, WRT_InnerRect, WRT_InnerClipRect, WRT_WorkRect, WRT_Contents, WRT_ContentsRegionRect, WRT_Count }; // Windows Rect Type - const char* wrt_rects_names[WRT_Count] = { "OuterRect", "OuterRectClipped", "InnerRect", "InnerClipRect", "WorkRect", "Contents", "ContentsRegionRect" }; - static bool show_windows_rects = false; - static int show_windows_rect_type = WRT_WorkRect; - static bool show_windows_begin_order = false; - static bool show_drawcmd_clip_rects = true; + // [DEBUG] Clear debug breaks hooks after exactly one cycle. + DebugBreakClearData(); // Basic info - ImGuiContext& g = *GImGui; - ImGuiIO& io = ImGui::GetIO(); - ImGui::Text("Dear ImGui %s", ImGui::GetVersion()); - ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); - ImGui::Text("%d vertices, %d indices (%d triangles)", io.MetricsRenderVertices, io.MetricsRenderIndices, io.MetricsRenderIndices / 3); - ImGui::Text("%d active windows (%d visible)", io.MetricsActiveWindows, io.MetricsRenderWindows); - ImGui::Text("%d active allocations", io.MetricsActiveAllocations); - ImGui::Separator(); + Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); + if (g.ContextName[0] != 0) + { + SameLine(); + Text("(Context Name: \"%s\")", g.ContextName); + } + Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + Text("%d vertices, %d indices (%d triangles)", io.MetricsRenderVertices, io.MetricsRenderIndices, io.MetricsRenderIndices / 3); + Text("%d visible windows, %d current allocations", io.MetricsRenderWindows, g.DebugAllocInfo.TotalAllocCount - g.DebugAllocInfo.TotalFreeCount); + //SameLine(); if (SmallButton("GC")) { g.GcCompactAll = true; } + + Separator(); + + // Debugging enums + enum { WRT_OuterRect, WRT_OuterRectClipped, WRT_InnerRect, WRT_InnerClipRect, WRT_WorkRect, WRT_Content, WRT_ContentIdeal, WRT_ContentRegionRect, WRT_Count }; // Windows Rect Type + const char* wrt_rects_names[WRT_Count] = { "OuterRect", "OuterRectClipped", "InnerRect", "InnerClipRect", "WorkRect", "Content", "ContentIdeal", "ContentRegionRect" }; + enum { TRT_OuterRect, TRT_InnerRect, TRT_WorkRect, TRT_HostClipRect, TRT_InnerClipRect, TRT_BackgroundClipRect, TRT_ColumnsRect, TRT_ColumnsWorkRect, TRT_ColumnsClipRect, TRT_ColumnsContentHeadersUsed, TRT_ColumnsContentHeadersIdeal, TRT_ColumnsContentFrozen, TRT_ColumnsContentUnfrozen, TRT_Count }; // Tables Rect Type + const char* trt_rects_names[TRT_Count] = { "OuterRect", "InnerRect", "WorkRect", "HostClipRect", "InnerClipRect", "BackgroundClipRect", "ColumnsRect", "ColumnsWorkRect", "ColumnsClipRect", "ColumnsContentHeadersUsed", "ColumnsContentHeadersIdeal", "ColumnsContentFrozen", "ColumnsContentUnfrozen" }; + if (cfg->ShowWindowsRectsType < 0) + cfg->ShowWindowsRectsType = WRT_WorkRect; + if (cfg->ShowTablesRectsType < 0) + cfg->ShowTablesRectsType = TRT_WorkRect; - // Helper functions to display common structures: - // - NodeDrawList - // - NodeColumns - // - NodeWindow - // - NodeWindows - // - NodeTabBar struct Funcs { + static ImRect GetTableRect(ImGuiTable* table, int rect_type, int n) + { + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); // Always using last submitted instance + if (rect_type == TRT_OuterRect) { return table->OuterRect; } + else if (rect_type == TRT_InnerRect) { return table->InnerRect; } + else if (rect_type == TRT_WorkRect) { return table->WorkRect; } + else if (rect_type == TRT_HostClipRect) { return table->HostClipRect; } + else if (rect_type == TRT_InnerClipRect) { return table->InnerClipRect; } + else if (rect_type == TRT_BackgroundClipRect) { return table->BgClipRect; } + else if (rect_type == TRT_ColumnsRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->MinX, table->InnerClipRect.Min.y, c->MaxX, table->InnerClipRect.Min.y + table_instance->LastOuterHeight); } + else if (rect_type == TRT_ColumnsWorkRect) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->WorkRect.Min.y, c->WorkMaxX, table->WorkRect.Max.y); } + else if (rect_type == TRT_ColumnsClipRect) { ImGuiTableColumn* c = &table->Columns[n]; return c->ClipRect; } + else if (rect_type == TRT_ColumnsContentHeadersUsed){ ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersUsed, table->InnerClipRect.Min.y + table_instance->LastTopHeadersRowHeight); } // Note: y1/y2 not always accurate + else if (rect_type == TRT_ColumnsContentHeadersIdeal){ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXHeadersIdeal, table->InnerClipRect.Min.y + table_instance->LastTopHeadersRowHeight); } + else if (rect_type == TRT_ColumnsContentFrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y, c->ContentMaxXFrozen, table->InnerClipRect.Min.y + table_instance->LastFrozenHeight); } + else if (rect_type == TRT_ColumnsContentUnfrozen) { ImGuiTableColumn* c = &table->Columns[n]; return ImRect(c->WorkMinX, table->InnerClipRect.Min.y + table_instance->LastFrozenHeight, c->ContentMaxXUnfrozen, table->InnerClipRect.Max.y); } + IM_ASSERT(0); + return ImRect(); + } + static ImRect GetWindowRect(ImGuiWindow* window, int rect_type) { if (rect_type == WRT_OuterRect) { return window->Rect(); } @@ -9816,286 +21208,1713 @@ void ImGui::ShowMetricsWindow(bool* p_open) else if (rect_type == WRT_InnerRect) { return window->InnerRect; } else if (rect_type == WRT_InnerClipRect) { return window->InnerClipRect; } else if (rect_type == WRT_WorkRect) { return window->WorkRect; } - else if (rect_type == WRT_Contents) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSize); } - else if (rect_type == WRT_ContentsRegionRect) { return window->ContentsRegionRect; } + else if (rect_type == WRT_Content) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSize); } + else if (rect_type == WRT_ContentIdeal) { ImVec2 min = window->InnerRect.Min - window->Scroll + window->WindowPadding; return ImRect(min, min + window->ContentSizeIdeal); } + else if (rect_type == WRT_ContentRegionRect) { return window->ContentRegionRect; } IM_ASSERT(0); return ImRect(); } - - static void NodeDrawList(ImGuiWindow* window, ImDrawList* draw_list, const char* label) - { - bool node_open = ImGui::TreeNode(draw_list, "%s: '%s' %d vtx, %d indices, %d cmds", label, draw_list->_OwnerName ? draw_list->_OwnerName : "", draw_list->VtxBuffer.Size, draw_list->IdxBuffer.Size, draw_list->CmdBuffer.Size); - if (draw_list == ImGui::GetWindowDrawList()) - { - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f,0.4f,0.4f,1.0f), "CURRENTLY APPENDING"); // Can't display stats for active draw list! (we don't have the data double-buffered) - if (node_open) ImGui::TreePop(); - return; - } - - ImDrawList* fg_draw_list = GetForegroundDrawList(window); // Render additional visuals into the top-most draw list - if (window && IsItemHovered()) - fg_draw_list->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); - if (!node_open) - return; - - if (window && !window->WasActive) - ImGui::Text("(Note: owning Window is inactive: DrawList is not being rendered!)"); - - int elem_offset = 0; - for (const ImDrawCmd* pcmd = draw_list->CmdBuffer.begin(); pcmd < draw_list->CmdBuffer.end(); elem_offset += pcmd->ElemCount, pcmd++) - { - if (pcmd->UserCallback == NULL && pcmd->ElemCount == 0) - continue; - if (pcmd->UserCallback) - { - ImGui::BulletText("Callback %p, user_data %p", pcmd->UserCallback, pcmd->UserCallbackData); - continue; - } - ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; - char buf[300]; - ImFormatString(buf, IM_ARRAYSIZE(buf), "Draw %4d triangles, tex 0x%p, clip_rect (%4.0f,%4.0f)-(%4.0f,%4.0f)", - pcmd->ElemCount/3, (void*)(intptr_t)pcmd->TextureId, pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); - bool pcmd_node_open = ImGui::TreeNode((void*)(pcmd - draw_list->CmdBuffer.begin()), "%s", buf); - if (show_drawcmd_clip_rects && fg_draw_list && ImGui::IsItemHovered()) - { - ImRect clip_rect = pcmd->ClipRect; - ImRect vtxs_rect; - for (int i = elem_offset; i < elem_offset + (int)pcmd->ElemCount; i++) - vtxs_rect.Add(draw_list->VtxBuffer[idx_buffer ? idx_buffer[i] : i].pos); - clip_rect.Floor(); fg_draw_list->AddRect(clip_rect.Min, clip_rect.Max, IM_COL32(255,0,255,255)); - vtxs_rect.Floor(); fg_draw_list->AddRect(vtxs_rect.Min, vtxs_rect.Max, IM_COL32(255,255,0,255)); - } - if (!pcmd_node_open) - continue; - - // Display individual triangles/vertices. Hover on to get the corresponding triangle highlighted. - ImGui::Text("ElemCount: %d, ElemCount/3: %d, VtxOffset: +%d, IdxOffset: +%d", pcmd->ElemCount, pcmd->ElemCount/3, pcmd->VtxOffset, pcmd->IdxOffset); - ImGuiListClipper clipper(pcmd->ElemCount/3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. - while (clipper.Step()) - for (int prim = clipper.DisplayStart, idx_i = elem_offset + clipper.DisplayStart*3; prim < clipper.DisplayEnd; prim++) - { - char *buf_p = buf, *buf_end = buf + IM_ARRAYSIZE(buf); - ImVec2 triangles_pos[3]; - for (int n = 0; n < 3; n++, idx_i++) - { - int vtx_i = idx_buffer ? idx_buffer[idx_i] : idx_i; - ImDrawVert& v = draw_list->VtxBuffer[vtx_i]; - triangles_pos[n] = v.pos; - buf_p += ImFormatString(buf_p, buf_end - buf_p, "%s %04d: pos (%8.2f,%8.2f), uv (%.6f,%.6f), col %08X\n", - (n == 0) ? "elem" : " ", idx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col); - } - ImGui::Selectable(buf, false); - if (fg_draw_list && ImGui::IsItemHovered()) - { - ImDrawListFlags backup_flags = fg_draw_list->Flags; - fg_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines at is more readable for very large and thin triangles. - fg_draw_list->AddPolyline(triangles_pos, 3, IM_COL32(255,255,0,255), true, 1.0f); - fg_draw_list->Flags = backup_flags; - } - } - ImGui::TreePop(); - } - ImGui::TreePop(); - } - - static void NodeColumns(const ImGuiColumns* columns) - { - if (!ImGui::TreeNode((void*)(uintptr_t)columns->ID, "Columns Id: 0x%08X, Count: %d, Flags: 0x%04X", columns->ID, columns->Count, columns->Flags)) - return; - ImGui::BulletText("Width: %.1f (MinX: %.1f, MaxX: %.1f)", columns->OffMaxX - columns->OffMinX, columns->OffMinX, columns->OffMaxX); - for (int column_n = 0; column_n < columns->Columns.Size; column_n++) - ImGui::BulletText("Column %02d: OffsetNorm %.3f (= %.1f px)", column_n, columns->Columns[column_n].OffsetNorm, GetColumnOffsetFromNorm(columns, columns->Columns[column_n].OffsetNorm)); - ImGui::TreePop(); - } - - static void NodeWindows(ImVector& windows, const char* label) - { - if (!ImGui::TreeNode(label, "%s (%d)", label, windows.Size)) - return; - for (int i = 0; i < windows.Size; i++) - Funcs::NodeWindow(windows[i], "Window"); - ImGui::TreePop(); - } - - static void NodeWindow(ImGuiWindow* window, const char* label) - { - if (window == NULL) - { - ImGui::BulletText("%s: NULL", label); - return; - } - if (!ImGui::TreeNode(window, "%s '%s', %d @ 0x%p", label, window->Name, (window->Active || window->WasActive), window)) - return; - ImGuiWindowFlags flags = window->Flags; - NodeDrawList(window, window->DrawList, "DrawList"); - ImGui::BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), ContentSize (%.1f,%.1f)", window->Pos.x, window->Pos.y, window->Size.x, window->Size.y, window->ContentSize.x, window->ContentSize.y); - ImGui::BulletText("Flags: 0x%08X (%s%s%s%s%s%s%s%s%s..)", flags, - (flags & ImGuiWindowFlags_ChildWindow) ? "Child " : "", (flags & ImGuiWindowFlags_Tooltip) ? "Tooltip " : "", (flags & ImGuiWindowFlags_Popup) ? "Popup " : "", - (flags & ImGuiWindowFlags_Modal) ? "Modal " : "", (flags & ImGuiWindowFlags_ChildMenu) ? "ChildMenu " : "", (flags & ImGuiWindowFlags_NoSavedSettings) ? "NoSavedSettings " : "", - (flags & ImGuiWindowFlags_NoMouseInputs)? "NoMouseInputs":"", (flags & ImGuiWindowFlags_NoNavInputs) ? "NoNavInputs" : "", (flags & ImGuiWindowFlags_AlwaysAutoResize) ? "AlwaysAutoResize" : ""); - ImGui::BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f)", window->Scroll.x, window->ScrollMax.x, window->Scroll.y, window->ScrollMax.y); - ImGui::BulletText("Active: %d/%d, WriteAccessed: %d, BeginOrderWithinContext: %d", window->Active, window->WasActive, window->WriteAccessed, (window->Active || window->WasActive) ? window->BeginOrderWithinContext : -1); - ImGui::BulletText("Appearing: %d, Hidden: %d (CanSkip %d Cannot %d), SkipItems: %d", window->Appearing, window->Hidden, window->HiddenFramesCanSkipItems, window->HiddenFramesCannotSkipItems, window->SkipItems); - ImGui::BulletText("NavLastIds: 0x%08X,0x%08X, NavLayerActiveMask: %X", window->NavLastIds[0], window->NavLastIds[1], window->DC.NavLayerActiveMask); - ImGui::BulletText("NavLastChildNavWindow: %s", window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); - if (!window->NavRectRel[0].IsInverted()) - ImGui::BulletText("NavRectRel[0]: (%.1f,%.1f)(%.1f,%.1f)", window->NavRectRel[0].Min.x, window->NavRectRel[0].Min.y, window->NavRectRel[0].Max.x, window->NavRectRel[0].Max.y); - else - ImGui::BulletText("NavRectRel[0]: "); - if (window->RootWindow != window) NodeWindow(window->RootWindow, "RootWindow"); - if (window->ParentWindow != NULL) NodeWindow(window->ParentWindow, "ParentWindow"); - if (window->DC.ChildWindows.Size > 0) NodeWindows(window->DC.ChildWindows, "ChildWindows"); - if (window->ColumnsStorage.Size > 0 && ImGui::TreeNode("Columns", "Columns sets (%d)", window->ColumnsStorage.Size)) - { - for (int n = 0; n < window->ColumnsStorage.Size; n++) - NodeColumns(&window->ColumnsStorage[n]); - ImGui::TreePop(); - } - ImGui::BulletText("Storage: %d bytes", window->StateStorage.Data.size_in_bytes()); - ImGui::TreePop(); - } - - static void NodeTabBar(ImGuiTabBar* tab_bar) - { - // Standalone tab bars (not associated to docking/windows functionality) currently hold no discernible strings. - char buf[256]; - char* p = buf; - const char* buf_end = buf + IM_ARRAYSIZE(buf); - ImFormatString(p, buf_end - p, "TabBar (%d tabs)%s", tab_bar->Tabs.Size, (tab_bar->PrevFrameVisible < ImGui::GetFrameCount() - 2) ? " *Inactive*" : ""); - if (ImGui::TreeNode(tab_bar, "%s", buf)) - { - for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) - { - const ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; - ImGui::PushID(tab); - if (ImGui::SmallButton("<")) { TabBarQueueChangeTabOrder(tab_bar, tab, -1); } ImGui::SameLine(0, 2); - if (ImGui::SmallButton(">")) { TabBarQueueChangeTabOrder(tab_bar, tab, +1); } ImGui::SameLine(); - ImGui::Text("%02d%c Tab 0x%08X", tab_n, (tab->ID == tab_bar->SelectedTabId) ? '*' : ' ', tab->ID); - ImGui::PopID(); - } - ImGui::TreePop(); - } - } }; - Funcs::NodeWindows(g.Windows, "Windows"); - if (ImGui::TreeNode("DrawList", "Active DrawLists (%d)", g.DrawDataBuilder.Layers[0].Size)) - { - for (int i = 0; i < g.DrawDataBuilder.Layers[0].Size; i++) - Funcs::NodeDrawList(NULL, g.DrawDataBuilder.Layers[0][i], "DrawList"); - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Popups", "Popups (%d)", g.OpenPopupStack.Size)) - { - for (int i = 0; i < g.OpenPopupStack.Size; i++) - { - ImGuiWindow* window = g.OpenPopupStack[i].Window; - ImGui::BulletText("PopupID: %08x, Window: '%s'%s%s", g.OpenPopupStack[i].PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? " ChildWindow" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? " ChildMenu" : ""); - } - ImGui::TreePop(); - } - - if (ImGui::TreeNode("TabBars", "Tab Bars (%d)", g.TabBars.Data.Size)) - { - for (int n = 0; n < g.TabBars.Data.Size; n++) - Funcs::NodeTabBar(g.TabBars.GetByIndex(n)); - ImGui::TreePop(); - } - -#if 0 - if (ImGui::TreeNode("Docking")) - { - ImGui::TreePop(); - } -#endif - -#if 0 - if (ImGui::TreeNode("Tables", "Tables (%d)", g.Tables.Data.Size)) - { - ImGui::TreePop(); - } -#endif - - if (ImGui::TreeNode("Internal state")) - { - const char* input_source_names[] = { "None", "Mouse", "Nav", "NavKeyboard", "NavGamepad" }; IM_ASSERT(IM_ARRAYSIZE(input_source_names) == ImGuiInputSource_COUNT); - ImGui::Text("HoveredWindow: '%s'", g.HoveredWindow ? g.HoveredWindow->Name : "NULL"); - ImGui::Text("HoveredRootWindow: '%s'", g.HoveredRootWindow ? g.HoveredRootWindow->Name : "NULL"); - ImGui::Text("HoveredId: 0x%08X/0x%08X (%.2f sec), AllowOverlap: %d", g.HoveredId, g.HoveredIdPreviousFrame, g.HoveredIdTimer, g.HoveredIdAllowOverlap); // Data is "in-flight" so depending on when the Metrics window is called we may see current frame information or not - ImGui::Text("ActiveId: 0x%08X/0x%08X (%.2f sec), AllowOverlap: %d, Source: %s", g.ActiveId, g.ActiveIdPreviousFrame, g.ActiveIdTimer, g.ActiveIdAllowOverlap, input_source_names[g.ActiveIdSource]); - ImGui::Text("ActiveIdWindow: '%s'", g.ActiveIdWindow ? g.ActiveIdWindow->Name : "NULL"); - ImGui::Text("MovingWindow: '%s'", g.MovingWindow ? g.MovingWindow->Name : "NULL"); - ImGui::Text("NavWindow: '%s'", g.NavWindow ? g.NavWindow->Name : "NULL"); - ImGui::Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer); - ImGui::Text("NavInputSource: %s", input_source_names[g.NavInputSource]); - ImGui::Text("NavActive: %d, NavVisible: %d", g.IO.NavActive, g.IO.NavVisible); - ImGui::Text("NavActivateId: 0x%08X, NavInputId: 0x%08X", g.NavActivateId, g.NavInputId); - ImGui::Text("NavDisableHighlight: %d, NavDisableMouseHover: %d", g.NavDisableHighlight, g.NavDisableMouseHover); - ImGui::Text("NavWindowingTarget: '%s'", g.NavWindowingTarget ? g.NavWindowingTarget->Name : "NULL"); - ImGui::Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\" (%d bytes)", g.DragDropActive, g.DragDropPayload.SourceId, g.DragDropPayload.DataType, g.DragDropPayload.DataSize); - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Tools")) + // Tools + if (TreeNode("Tools")) { + // Debug Break features // The Item Picker tool is super useful to visually select an item and break into the call-stack of where it was submitted. - if (ImGui::Button("Item Picker..")) - ImGui::DebugStartItemPicker(); + SeparatorTextEx(0, "Debug breaks", NULL, CalcTextSize("(?)").x + g.Style.SeparatorTextPadding.x); + SameLine(); + MetricsHelpMarker("Will call the IM_DEBUG_BREAK() macro to break in debugger.\nWarning: If you don't have a debugger attached, this will probably crash."); + if (Checkbox("Show Item Picker", &g.DebugItemPickerActive) && g.DebugItemPickerActive) + DebugStartItemPicker(); + Checkbox("Show \"Debug Break\" buttons in other sections (io.ConfigDebugIsDebuggerPresent)", &g.IO.ConfigDebugIsDebuggerPresent); - ImGui::Checkbox("Show windows begin order", &show_windows_begin_order); - ImGui::Checkbox("Show windows rectangles", &show_windows_rects); - ImGui::SameLine(); - ImGui::SetNextItemWidth(ImGui::GetFontSize() * 12); - show_windows_rects |= ImGui::Combo("##show_windows_rect_type", &show_windows_rect_type, wrt_rects_names, WRT_Count); - if (show_windows_rects && g.NavWindow) + SeparatorText("Visualize"); + + Checkbox("Show Debug Log", &cfg->ShowDebugLog); + SameLine(); + MetricsHelpMarker("You can also call ImGui::ShowDebugLogWindow() from your code."); + + Checkbox("Show ID Stack Tool", &cfg->ShowIDStackTool); + SameLine(); + MetricsHelpMarker("You can also call ImGui::ShowIDStackToolWindow() from your code."); + + Checkbox("Show windows begin order", &cfg->ShowWindowsBeginOrder); + Checkbox("Show windows rectangles", &cfg->ShowWindowsRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowWindowsRects |= Combo("##show_windows_rect_type", &cfg->ShowWindowsRectsType, wrt_rects_names, WRT_Count, WRT_Count); + if (cfg->ShowWindowsRects && g.NavWindow != NULL) { - ImGui::BulletText("'%s':", g.NavWindow->Name); - ImGui::Indent(); + BulletText("'%s':", g.NavWindow->Name); + Indent(); for (int rect_n = 0; rect_n < WRT_Count; rect_n++) { ImRect r = Funcs::GetWindowRect(g.NavWindow, rect_n); - ImGui::Text("(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), wrt_rects_names[rect_n]); + Text("(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), wrt_rects_names[rect_n]); } - ImGui::Unindent(); + Unindent(); } - ImGui::Checkbox("Show clipping rectangle when hovering ImDrawCmd node", &show_drawcmd_clip_rects); - ImGui::TreePop(); + + Checkbox("Show tables rectangles", &cfg->ShowTablesRects); + SameLine(); + SetNextItemWidth(GetFontSize() * 12); + cfg->ShowTablesRects |= Combo("##show_table_rects_type", &cfg->ShowTablesRectsType, trt_rects_names, TRT_Count, TRT_Count); + if (cfg->ShowTablesRects && g.NavWindow != NULL) + { + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + { + ImGuiTable* table = g.Tables.TryGetMapData(table_n); + if (table == NULL || table->LastFrameActive < g.FrameCount - 1 || (table->OuterWindow != g.NavWindow && table->InnerWindow != g.NavWindow)) + continue; + + BulletText("Table 0x%08X (%d columns, in '%s')", table->ID, table->ColumnsCount, table->OuterWindow->Name); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min - ImVec2(1, 1), table->OuterRect.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + Indent(); + char buf[128]; + for (int rect_n = 0; rect_n < TRT_Count; rect_n++) + { + if (rect_n >= TRT_ColumnsRect) + { + if (rect_n != TRT_ColumnsRect && rect_n != TRT_ColumnsClipRect) + continue; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, rect_n, column_n); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) Col %d %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), column_n, trt_rects_names[rect_n]); + Selectable(buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, rect_n, -1); + ImFormatString(buf, IM_ARRAYSIZE(buf), "(%6.1f,%6.1f) (%6.1f,%6.1f) Size (%6.1f,%6.1f) %s", r.Min.x, r.Min.y, r.Max.x, r.Max.y, r.GetWidth(), r.GetHeight(), trt_rects_names[rect_n]); + Selectable(buf); + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(r.Min - ImVec2(1, 1), r.Max + ImVec2(1, 1), IM_COL32(255, 255, 0, 255), 0.0f, 0, 2.0f); + } + } + Unindent(); + } + } + Checkbox("Show groups rectangles", &g.DebugShowGroupRects); // Storing in context as this is used by group code and prefers to be in hot-data + + SeparatorText("Validate"); + + Checkbox("Debug Begin/BeginChild return value", &io.ConfigDebugBeginReturnValueLoop); + SameLine(); + MetricsHelpMarker("Some calls to Begin()/BeginChild() will return false.\n\nWill cycle through window depths then repeat. Windows should be flickering while running."); + + Checkbox("UTF-8 Encoding viewer", &cfg->ShowTextEncodingViewer); + SameLine(); + MetricsHelpMarker("You can also call ImGui::DebugTextEncoding() from your code with a given string to test that your UTF-8 encoding settings are correct."); + if (cfg->ShowTextEncodingViewer) + { + static char buf[64] = ""; + SetNextItemWidth(-FLT_MIN); + InputText("##DebugTextEncodingBuf", buf, IM_ARRAYSIZE(buf)); + if (buf[0] != 0) + DebugTextEncoding(buf); + } + + TreePop(); } - // Tool: Display windows Rectangles and Begin Order - if (show_windows_rects || show_windows_begin_order) + // Windows + if (TreeNode("Windows", "Windows (%d)", g.Windows.Size)) { - for (int n = 0; n < g.Windows.Size; n++) + //SetNextItemOpen(true, ImGuiCond_Once); + DebugNodeWindowsList(&g.Windows, "By display order"); + DebugNodeWindowsList(&g.WindowsFocusOrder, "By focus order (root windows)"); + if (TreeNode("By submission order (begin stack)")) + { + // Here we display windows in their submitted order/hierarchy, however note that the Begin stack doesn't constitute a Parent<>Child relationship! + ImVector& temp_buffer = g.WindowsTempSortBuffer; + temp_buffer.resize(0); + for (ImGuiWindow* window : g.Windows) + if (window->LastFrameActive + 1 >= g.FrameCount) + temp_buffer.push_back(window); + struct Func { static int IMGUI_CDECL WindowComparerByBeginOrder(const void* lhs, const void* rhs) { return ((int)(*(const ImGuiWindow* const *)lhs)->BeginOrderWithinContext - (*(const ImGuiWindow* const*)rhs)->BeginOrderWithinContext); } }; + ImQsort(temp_buffer.Data, (size_t)temp_buffer.Size, sizeof(ImGuiWindow*), Func::WindowComparerByBeginOrder); + DebugNodeWindowsListByBeginStackParent(temp_buffer.Data, temp_buffer.Size, NULL); + TreePop(); + } + + TreePop(); + } + + // DrawLists + int drawlist_count = 0; + for (ImGuiViewportP* viewport : g.Viewports) + drawlist_count += viewport->DrawDataP.CmdLists.Size; + if (TreeNode("DrawLists", "DrawLists (%d)", drawlist_count)) + { + Checkbox("Show ImDrawCmd mesh when hovering", &cfg->ShowDrawCmdMesh); + Checkbox("Show ImDrawCmd bounding boxes when hovering", &cfg->ShowDrawCmdBoundingBoxes); + for (ImGuiViewportP* viewport : g.Viewports) + { + bool viewport_has_drawlist = false; + for (ImDrawList* draw_list : viewport->DrawDataP.CmdLists) + { + if (!viewport_has_drawlist) + Text("Active DrawLists in Viewport #%d, ID: 0x%08X", viewport->Idx, viewport->ID); + viewport_has_drawlist = true; + DebugNodeDrawList(NULL, viewport, draw_list, "DrawList"); + } + } + TreePop(); + } + + // Viewports + if (TreeNode("Viewports", "Viewports (%d)", g.Viewports.Size)) + { + cfg->HighlightMonitorIdx = -1; + bool open = TreeNode("Monitors", "Monitors (%d)", g.PlatformIO.Monitors.Size); + SameLine(); + MetricsHelpMarker("Dear ImGui uses monitor data:\n- to query DPI settings on a per monitor basis\n- to position popup/tooltips so they don't straddle monitors."); + if (open) + { + for (int i = 0; i < g.PlatformIO.Monitors.Size; i++) + { + DebugNodePlatformMonitor(&g.PlatformIO.Monitors[i], "Monitor", i); + if (IsItemHovered()) + cfg->HighlightMonitorIdx = i; + } + DebugNodePlatformMonitor(&g.FallbackMonitor, "Fallback", 0); + TreePop(); + } + + SetNextItemOpen(true, ImGuiCond_Once); + if (TreeNode("Windows Minimap")) + { + RenderViewportsThumbnails(); + TreePop(); + } + cfg->HighlightViewportID = 0; + + BulletText("MouseViewport: 0x%08X (UserHovered 0x%08X, LastHovered 0x%08X)", g.MouseViewport ? g.MouseViewport->ID : 0, g.IO.MouseHoveredViewport, g.MouseLastHoveredViewport ? g.MouseLastHoveredViewport->ID : 0); + if (TreeNode("Inferred Z order (front-to-back)")) + { + static ImVector viewports; + viewports.resize(g.Viewports.Size); + memcpy(viewports.Data, g.Viewports.Data, g.Viewports.size_in_bytes()); + if (viewports.Size > 1) + ImQsort(viewports.Data, viewports.Size, sizeof(ImGuiViewport*), ViewportComparerByLastFocusedStampCount); + for (ImGuiViewportP* viewport : viewports) + { + BulletText("Viewport #%d, ID: 0x%08X, LastFocused = %08d, PlatformFocused = %s, Window: \"%s\"", + viewport->Idx, viewport->ID, viewport->LastFocusedStampCount, + (g.PlatformIO.Platform_GetWindowFocus && viewport->PlatformWindowCreated) ? (g.PlatformIO.Platform_GetWindowFocus(viewport) ? "1" : "0") : "N/A", + viewport->Window ? viewport->Window->Name : "N/A"); + if (IsItemHovered()) + cfg->HighlightViewportID = viewport->ID; + } + TreePop(); + } + + for (ImGuiViewportP* viewport : g.Viewports) + DebugNodeViewport(viewport); + TreePop(); + } + + // Details for Popups + if (TreeNode("Popups", "Popups (%d)", g.OpenPopupStack.Size)) + { + for (const ImGuiPopupData& popup_data : g.OpenPopupStack) + { + // As it's difficult to interact with tree nodes while popups are open, we display everything inline. + ImGuiWindow* window = popup_data.Window; + BulletText("PopupID: %08x, Window: '%s' (%s%s), RestoreNavWindow '%s', ParentWindow '%s'", + popup_data.PopupId, window ? window->Name : "NULL", window && (window->Flags & ImGuiWindowFlags_ChildWindow) ? "Child;" : "", window && (window->Flags & ImGuiWindowFlags_ChildMenu) ? "Menu;" : "", + popup_data.RestoreNavWindow ? popup_data.RestoreNavWindow->Name : "NULL", window && window->ParentWindow ? window->ParentWindow->Name : "NULL"); + } + TreePop(); + } + + // Details for TabBars + if (TreeNode("TabBars", "Tab Bars (%d)", g.TabBars.GetAliveCount())) + { + for (int n = 0; n < g.TabBars.GetMapSize(); n++) + if (ImGuiTabBar* tab_bar = g.TabBars.TryGetMapData(n)) + { + PushID(tab_bar); + DebugNodeTabBar(tab_bar, "TabBar"); + PopID(); + } + TreePop(); + } + + // Details for Tables + if (TreeNode("Tables", "Tables (%d)", g.Tables.GetAliveCount())) + { + for (int n = 0; n < g.Tables.GetMapSize(); n++) + if (ImGuiTable* table = g.Tables.TryGetMapData(n)) + DebugNodeTable(table); + TreePop(); + } + + // Details for Fonts + ImFontAtlas* atlas = g.IO.Fonts; + if (TreeNode("Fonts", "Fonts (%d)", atlas->Fonts.Size)) + { + ShowFontAtlas(atlas); + TreePop(); + } + + // Details for InputText + if (TreeNode("InputText")) + { + DebugNodeInputTextState(&g.InputTextState); + TreePop(); + } + + // Details for TypingSelect + if (TreeNode("TypingSelect", "TypingSelect (%d)", g.TypingSelectState.SearchBuffer[0] != 0 ? 1 : 0)) + { + DebugNodeTypingSelectState(&g.TypingSelectState); + TreePop(); + } + + // Details for MultiSelect + if (TreeNode("MultiSelect", "MultiSelect (%d)", g.MultiSelectStorage.GetAliveCount())) + { + ImGuiBoxSelectState* bs = &g.BoxSelectState; + BulletText("BoxSelect ID=0x%08X, Starting = %d, Active %d", bs->ID, bs->IsStarting, bs->IsActive); + for (int n = 0; n < g.MultiSelectStorage.GetMapSize(); n++) + if (ImGuiMultiSelectState* state = g.MultiSelectStorage.TryGetMapData(n)) + DebugNodeMultiSelectState(state); + TreePop(); + } + + // Details for Docking +#ifdef IMGUI_HAS_DOCK + if (TreeNode("Docking")) + { + static bool root_nodes_only = true; + ImGuiDockContext* dc = &g.DockContext; + Checkbox("List root nodes", &root_nodes_only); + Checkbox("Ctrl shows window dock info", &cfg->ShowDockingNodes); + if (SmallButton("Clear nodes")) { DockContextClearNodes(&g, 0, true); } + SameLine(); + if (SmallButton("Rebuild all")) { dc->WantFullRebuild = true; } + for (int n = 0; n < dc->Nodes.Data.Size; n++) + if (ImGuiDockNode* node = (ImGuiDockNode*)dc->Nodes.Data[n].val_p) + if (!root_nodes_only || node->IsRootNode()) + DebugNodeDockNode(node, "Node"); + TreePop(); + } +#endif // #ifdef IMGUI_HAS_DOCK + + // Settings + if (TreeNode("Settings")) + { + if (SmallButton("Clear")) + ClearIniSettings(); + SameLine(); + if (SmallButton("Save to memory")) + SaveIniSettingsToMemory(); + SameLine(); + if (SmallButton("Save to disk")) + SaveIniSettingsToDisk(g.IO.IniFilename); + SameLine(); + if (g.IO.IniFilename) + Text("\"%s\"", g.IO.IniFilename); + else + TextUnformatted(""); + Checkbox("io.ConfigDebugIniSettings", &io.ConfigDebugIniSettings); + Text("SettingsDirtyTimer %.2f", g.SettingsDirtyTimer); + if (TreeNode("SettingsHandlers", "Settings handlers: (%d)", g.SettingsHandlers.Size)) + { + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + BulletText("\"%s\"", handler.TypeName); + TreePop(); + } + if (TreeNode("SettingsWindows", "Settings packed data: Windows: %d bytes", g.SettingsWindows.size())) + { + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + DebugNodeWindowSettings(settings); + TreePop(); + } + + if (TreeNode("SettingsTables", "Settings packed data: Tables: %d bytes", g.SettingsTables.size())) + { + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + DebugNodeTableSettings(settings); + TreePop(); + } + +#ifdef IMGUI_HAS_DOCK + if (TreeNode("SettingsDocking", "Settings packed data: Docking")) + { + ImGuiDockContext* dc = &g.DockContext; + Text("In SettingsWindows:"); + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + if (settings->DockId != 0) + BulletText("Window '%s' -> DockId %08X DockOrder=%d", settings->GetName(), settings->DockId, settings->DockOrder); + Text("In SettingsNodes:"); + for (int n = 0; n < dc->NodesSettings.Size; n++) + { + ImGuiDockNodeSettings* settings = &dc->NodesSettings[n]; + const char* selected_tab_name = NULL; + if (settings->SelectedTabId) + { + if (ImGuiWindow* window = FindWindowByID(settings->SelectedTabId)) + selected_tab_name = window->Name; + else if (ImGuiWindowSettings* window_settings = FindWindowSettingsByID(settings->SelectedTabId)) + selected_tab_name = window_settings->GetName(); + } + BulletText("Node %08X, Parent %08X, SelectedTab %08X ('%s')", settings->ID, settings->ParentNodeId, settings->SelectedTabId, selected_tab_name ? selected_tab_name : settings->SelectedTabId ? "N/A" : ""); + } + TreePop(); + } +#endif // #ifdef IMGUI_HAS_DOCK + + if (TreeNode("SettingsIniData", "Settings unpacked data (.ini): %d bytes", g.SettingsIniData.size())) + { + InputTextMultiline("##Ini", (char*)(void*)g.SettingsIniData.c_str(), g.SettingsIniData.Buf.Size, ImVec2(-FLT_MIN, GetTextLineHeight() * 20), ImGuiInputTextFlags_ReadOnly); + TreePop(); + } + TreePop(); + } + + // Settings + if (TreeNode("Memory allocations")) + { + ImGuiDebugAllocInfo* info = &g.DebugAllocInfo; + Text("%d current allocations", info->TotalAllocCount - info->TotalFreeCount); + if (SmallButton("GC now")) { g.GcCompactAll = true; } + Text("Recent frames with allocations:"); + int buf_size = IM_ARRAYSIZE(info->LastEntriesBuf); + for (int n = buf_size - 1; n >= 0; n--) + { + ImGuiDebugAllocEntry* entry = &info->LastEntriesBuf[(info->LastEntriesIdx - n + buf_size) % buf_size]; + BulletText("Frame %06d: %+3d ( %2d alloc, %2d free )", entry->FrameCount, entry->AllocCount - entry->FreeCount, entry->AllocCount, entry->FreeCount); + if (n == 0) + { + SameLine(); + Text("<- %d frames ago", g.FrameCount - entry->FrameCount); + } + } + TreePop(); + } + + if (TreeNode("Inputs")) + { + Text("KEYBOARD/GAMEPAD/MOUSE KEYS"); + { + // User code should never have to go through such hoops! You can generally iterate between ImGuiKey_NamedKey_BEGIN and ImGuiKey_NamedKey_END. + Indent(); + Text("Keys down:"); for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { if (!IsKeyDown(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); SameLine(); Text("(%.02f)", GetKeyData(key)->DownDuration); } + Text("Keys pressed:"); for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { if (!IsKeyPressed(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); } + Text("Keys released:"); for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { if (!IsKeyReleased(key)) continue; SameLine(); Text(IsNamedKey(key) ? "\"%s\"" : "\"%s\" %d", GetKeyName(key), key); } + Text("Keys mods: %s%s%s%s", io.KeyCtrl ? "CTRL " : "", io.KeyShift ? "SHIFT " : "", io.KeyAlt ? "ALT " : "", io.KeySuper ? "SUPER " : ""); + Text("Chars queue:"); for (int i = 0; i < io.InputQueueCharacters.Size; i++) { ImWchar c = io.InputQueueCharacters[i]; SameLine(); Text("\'%c\' (0x%04X)", (c > ' ' && c <= 255) ? (char)c : '?', c); } // FIXME: We should convert 'c' to UTF-8 here but the functions are not public. + DebugRenderKeyboardPreview(GetWindowDrawList()); + Unindent(); + } + + Text("MOUSE STATE"); + { + Indent(); + if (IsMousePosValid()) + Text("Mouse pos: (%g, %g)", io.MousePos.x, io.MousePos.y); + else + Text("Mouse pos: "); + Text("Mouse delta: (%g, %g)", io.MouseDelta.x, io.MouseDelta.y); + int count = IM_ARRAYSIZE(io.MouseDown); + Text("Mouse down:"); for (int i = 0; i < count; i++) if (IsMouseDown(i)) { SameLine(); Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); } + Text("Mouse clicked:"); for (int i = 0; i < count; i++) if (IsMouseClicked(i)) { SameLine(); Text("b%d (%d)", i, io.MouseClickedCount[i]); } + Text("Mouse released:"); for (int i = 0; i < count; i++) if (IsMouseReleased(i)) { SameLine(); Text("b%d", i); } + Text("Mouse wheel: %.1f", io.MouseWheel); + Text("MouseStationaryTimer: %.2f", g.MouseStationaryTimer); + Text("Mouse source: %s", GetMouseSourceName(io.MouseSource)); + Text("Pen Pressure: %.1f", io.PenPressure); // Note: currently unused + Unindent(); + } + + Text("MOUSE WHEELING"); + { + Indent(); + Text("WheelingWindow: '%s'", g.WheelingWindow ? g.WheelingWindow->Name : "NULL"); + Text("WheelingWindowReleaseTimer: %.2f", g.WheelingWindowReleaseTimer); + Text("WheelingAxisAvg[] = { %.3f, %.3f }, Main Axis: %s", g.WheelingAxisAvg.x, g.WheelingAxisAvg.y, (g.WheelingAxisAvg.x > g.WheelingAxisAvg.y) ? "X" : (g.WheelingAxisAvg.x < g.WheelingAxisAvg.y) ? "Y" : ""); + Unindent(); + } + + Text("KEY OWNERS"); + { + Indent(); + if (BeginChild("##owners", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_NoSavedSettings)) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyOwnerData* owner_data = GetKeyOwnerData(&g, key); + if (owner_data->OwnerCurr == ImGuiKeyOwner_NoOwner) + continue; + Text("%s: 0x%08X%s", GetKeyName(key), owner_data->OwnerCurr, + owner_data->LockUntilRelease ? " LockUntilRelease" : owner_data->LockThisFrame ? " LockThisFrame" : ""); + DebugLocateItemOnHover(owner_data->OwnerCurr); + } + EndChild(); + Unindent(); + } + Text("SHORTCUT ROUTING"); + SameLine(); + MetricsHelpMarker("Declared shortcut routes automatically set key owner when mods matches."); + { + Indent(); + if (BeginChild("##routes", ImVec2(-FLT_MIN, GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY, ImGuiWindowFlags_NoSavedSettings)) + for (ImGuiKey key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) + { + ImGuiKeyRoutingTable* rt = &g.KeysRoutingTable; + for (ImGuiKeyRoutingIndex idx = rt->Index[key - ImGuiKey_NamedKey_BEGIN]; idx != -1; ) + { + ImGuiKeyRoutingData* routing_data = &rt->Entries[idx]; + ImGuiKeyChord key_chord = key | routing_data->Mods; + Text("%s: 0x%08X (scored %d)", GetKeyChordName(key_chord), routing_data->RoutingCurr, routing_data->RoutingCurrScore); + DebugLocateItemOnHover(routing_data->RoutingCurr); + if (g.IO.ConfigDebugIsDebuggerPresent) + { + SameLine(); + if (DebugBreakButton("**DebugBreak**", "in SetShortcutRouting() for this KeyChord")) + g.DebugBreakInShortcutRouting = key_chord; + } + idx = routing_data->NextEntryIndex; + } + } + EndChild(); + Text("(ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: 0x%X)", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask); + Unindent(); + } + TreePop(); + } + + if (TreeNode("Internal state")) + { + Text("WINDOWING"); + Indent(); + Text("HoveredWindow: '%s'", g.HoveredWindow ? g.HoveredWindow->Name : "NULL"); + Text("HoveredWindow->Root: '%s'", g.HoveredWindow ? g.HoveredWindow->RootWindowDockTree->Name : "NULL"); + Text("HoveredWindowUnderMovingWindow: '%s'", g.HoveredWindowUnderMovingWindow ? g.HoveredWindowUnderMovingWindow->Name : "NULL"); + Text("HoveredDockNode: 0x%08X", g.DebugHoveredDockNode ? g.DebugHoveredDockNode->ID : 0); + Text("MovingWindow: '%s'", g.MovingWindow ? g.MovingWindow->Name : "NULL"); + Text("MouseViewport: 0x%08X (UserHovered 0x%08X, LastHovered 0x%08X)", g.MouseViewport->ID, g.IO.MouseHoveredViewport, g.MouseLastHoveredViewport ? g.MouseLastHoveredViewport->ID : 0); + Unindent(); + + Text("ITEMS"); + Indent(); + Text("ActiveId: 0x%08X/0x%08X (%.2f sec), AllowOverlap: %d, Source: %s", g.ActiveId, g.ActiveIdPreviousFrame, g.ActiveIdTimer, g.ActiveIdAllowOverlap, GetInputSourceName(g.ActiveIdSource)); + DebugLocateItemOnHover(g.ActiveId); + Text("ActiveIdWindow: '%s'", g.ActiveIdWindow ? g.ActiveIdWindow->Name : "NULL"); + Text("ActiveIdUsing: AllKeyboardKeys: %d, NavDirMask: %X", g.ActiveIdUsingAllKeyboardKeys, g.ActiveIdUsingNavDirMask); + Text("HoveredId: 0x%08X (%.2f sec), AllowOverlap: %d", g.HoveredIdPreviousFrame, g.HoveredIdTimer, g.HoveredIdAllowOverlap); // Not displaying g.HoveredId as it is update mid-frame + Text("HoverItemDelayId: 0x%08X, Timer: %.2f, ClearTimer: %.2f", g.HoverItemDelayId, g.HoverItemDelayTimer, g.HoverItemDelayClearTimer); + Text("DragDrop: %d, SourceId = 0x%08X, Payload \"%s\" (%d bytes)", g.DragDropActive, g.DragDropPayload.SourceId, g.DragDropPayload.DataType, g.DragDropPayload.DataSize); + DebugLocateItemOnHover(g.DragDropPayload.SourceId); + Unindent(); + + Text("NAV,FOCUS"); + Indent(); + Text("NavWindow: '%s'", g.NavWindow ? g.NavWindow->Name : "NULL"); + Text("NavId: 0x%08X, NavLayer: %d", g.NavId, g.NavLayer); + DebugLocateItemOnHover(g.NavId); + Text("NavInputSource: %s", GetInputSourceName(g.NavInputSource)); + Text("NavLastValidSelectionUserData = %" IM_PRId64 " (0x%" IM_PRIX64 ")", g.NavLastValidSelectionUserData, g.NavLastValidSelectionUserData); + Text("NavActive: %d, NavVisible: %d", g.IO.NavActive, g.IO.NavVisible); + Text("NavActivateId/DownId/PressedId: %08X/%08X/%08X", g.NavActivateId, g.NavActivateDownId, g.NavActivatePressedId); + Text("NavActivateFlags: %04X", g.NavActivateFlags); + Text("NavCursorVisible: %d, NavHighlightItemUnderNav: %d", g.NavCursorVisible, g.NavHighlightItemUnderNav); + Text("NavFocusScopeId = 0x%08X", g.NavFocusScopeId); + Text("NavFocusRoute[] = "); + for (int path_n = g.NavFocusRoute.Size - 1; path_n >= 0; path_n--) + { + const ImGuiFocusScopeData& focus_scope = g.NavFocusRoute[path_n]; + SameLine(0.0f, 0.0f); + Text("0x%08X/", focus_scope.ID); + SetItemTooltip("In window \"%s\"", FindWindowByID(focus_scope.WindowID)->Name); + } + Text("NavWindowingTarget: '%s'", g.NavWindowingTarget ? g.NavWindowingTarget->Name : "NULL"); + Unindent(); + + TreePop(); + } + + // Overlay: Display windows Rectangles and Begin Order + if (cfg->ShowWindowsRects || cfg->ShowWindowsBeginOrder) + { + for (ImGuiWindow* window : g.Windows) { - ImGuiWindow* window = g.Windows[n]; if (!window->WasActive) continue; ImDrawList* draw_list = GetForegroundDrawList(window); - if (show_windows_rects) + if (cfg->ShowWindowsRects) { - ImRect r = Funcs::GetWindowRect(window, show_windows_rect_type); + ImRect r = Funcs::GetWindowRect(window, cfg->ShowWindowsRectsType); draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); } - if (show_windows_begin_order && !(window->Flags & ImGuiWindowFlags_ChildWindow)) + if (cfg->ShowWindowsBeginOrder && !(window->Flags & ImGuiWindowFlags_ChildWindow)) { char buf[32]; ImFormatString(buf, IM_ARRAYSIZE(buf), "%d", window->BeginOrderWithinContext); - float font_size = ImGui::GetFontSize(); + float font_size = GetFontSize(); draw_list->AddRectFilled(window->Pos, window->Pos + ImVec2(font_size, font_size), IM_COL32(200, 100, 100, 255)); draw_list->AddText(window->Pos, IM_COL32(255, 255, 255, 255), buf); } } } - ImGui::End(); + + // Overlay: Display Tables Rectangles + if (cfg->ShowTablesRects) + { + for (int table_n = 0; table_n < g.Tables.GetMapSize(); table_n++) + { + ImGuiTable* table = g.Tables.TryGetMapData(table_n); + if (table == NULL || table->LastFrameActive < g.FrameCount - 1) + continue; + ImDrawList* draw_list = GetForegroundDrawList(table->OuterWindow); + if (cfg->ShowTablesRectsType >= TRT_ColumnsRect) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, column_n); + ImU32 col = (table->HoveredColumnBody == column_n) ? IM_COL32(255, 255, 128, 255) : IM_COL32(255, 0, 128, 255); + float thickness = (table->HoveredColumnBody == column_n) ? 3.0f : 1.0f; + draw_list->AddRect(r.Min, r.Max, col, 0.0f, 0, thickness); + } + } + else + { + ImRect r = Funcs::GetTableRect(table, cfg->ShowTablesRectsType, -1); + draw_list->AddRect(r.Min, r.Max, IM_COL32(255, 0, 128, 255)); + } + } + } + +#ifdef IMGUI_HAS_DOCK + // Overlay: Display Docking info + if (cfg->ShowDockingNodes && g.IO.KeyCtrl && g.DebugHoveredDockNode) + { + char buf[64] = ""; + char* p = buf; + ImGuiDockNode* node = g.DebugHoveredDockNode; + ImDrawList* overlay_draw_list = node->HostWindow ? GetForegroundDrawList(node->HostWindow) : GetForegroundDrawList(GetMainViewport()); + p += ImFormatString(p, buf + IM_ARRAYSIZE(buf) - p, "DockId: %X%s\n", node->ID, node->IsCentralNode() ? " *CentralNode*" : ""); + p += ImFormatString(p, buf + IM_ARRAYSIZE(buf) - p, "WindowClass: %08X\n", node->WindowClass.ClassId); + p += ImFormatString(p, buf + IM_ARRAYSIZE(buf) - p, "Size: (%.0f, %.0f)\n", node->Size.x, node->Size.y); + p += ImFormatString(p, buf + IM_ARRAYSIZE(buf) - p, "SizeRef: (%.0f, %.0f)\n", node->SizeRef.x, node->SizeRef.y); + int depth = DockNodeGetDepth(node); + overlay_draw_list->AddRect(node->Pos + ImVec2(3, 3) * (float)depth, node->Pos + node->Size - ImVec2(3, 3) * (float)depth, IM_COL32(200, 100, 100, 255)); + ImVec2 pos = node->Pos + ImVec2(3, 3) * (float)depth; + overlay_draw_list->AddRectFilled(pos - ImVec2(1, 1), pos + CalcTextSize(buf) + ImVec2(1, 1), IM_COL32(200, 100, 100, 255)); + overlay_draw_list->AddText(NULL, 0.0f, pos, IM_COL32(255, 255, 255, 255), buf); + } +#endif // #ifdef IMGUI_HAS_DOCK + + End(); +} + +void ImGui::DebugBreakClearData() +{ + // Those fields are scattered in their respective subsystem to stay in hot-data locations + ImGuiContext& g = *GImGui; + g.DebugBreakInWindow = 0; + g.DebugBreakInTable = 0; + g.DebugBreakInShortcutRouting = ImGuiKey_None; +} + +void ImGui::DebugBreakButtonTooltip(bool keyboard_only, const char* description_of_location) +{ + if (!BeginItemTooltip()) + return; + Text("To call IM_DEBUG_BREAK() %s:", description_of_location); + Separator(); + TextUnformatted(keyboard_only ? "- Press 'Pause/Break' on keyboard." : "- Press 'Pause/Break' on keyboard.\n- or Click (may alter focus/active id).\n- or navigate using keyboard and press space."); + Separator(); + TextUnformatted("Choose one way that doesn't interfere with what you are trying to debug!\nYou need a debugger attached or this will crash!"); + EndTooltip(); +} + +// Special button that doesn't take focus, doesn't take input owner, and can be activated without a click etc. +// In order to reduce interferences with the contents we are trying to debug into. +bool ImGui::DebugBreakButton(const char* label, const char* description_of_location) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + ImVec2 pos = window->DC.CursorPos + ImVec2(0.0f, window->DC.CurrLineTextBaseOffset); + ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x * 2.0f, label_size.y); + + const ImRect bb(pos, pos + size); + ItemSize(size, 0.0f); + if (!ItemAdd(bb, id)) + return false; + + // WE DO NOT USE ButtonEx() or ButtonBehavior() in order to reduce our side-effects. + bool hovered = ItemHoverable(bb, id, g.CurrentItemFlags); + bool pressed = hovered && (IsKeyChordPressed(g.DebugBreakKeyChord) || IsMouseClicked(0) || g.NavActivateId == id); + DebugBreakButtonTooltip(false, description_of_location); + + ImVec4 col4f = GetStyleColorVec4(hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + ImVec4 hsv; + ColorConvertRGBtoHSV(col4f.x, col4f.y, col4f.z, hsv.x, hsv.y, hsv.z); + ColorConvertHSVtoRGB(hsv.x + 0.20f, hsv.y, hsv.z, col4f.x, col4f.y, col4f.z); + + RenderNavCursor(bb, id); + RenderFrame(bb.Min, bb.Max, GetColorU32(col4f), true, g.Style.FrameRounding); + RenderTextClipped(bb.Min, bb.Max, label, NULL, &label_size, g.Style.ButtonTextAlign, &bb); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +// [DEBUG] Display contents of Columns +void ImGui::DebugNodeColumns(ImGuiOldColumns* columns) +{ + if (!TreeNode((void*)(uintptr_t)columns->ID, "Columns Id: 0x%08X, Count: %d, Flags: 0x%04X", columns->ID, columns->Count, columns->Flags)) + return; + BulletText("Width: %.1f (MinX: %.1f, MaxX: %.1f)", columns->OffMaxX - columns->OffMinX, columns->OffMinX, columns->OffMaxX); + for (ImGuiOldColumnData& column : columns->Columns) + BulletText("Column %02d: OffsetNorm %.3f (= %.1f px)", (int)columns->Columns.index_from_ptr(&column), column.OffsetNorm, GetColumnOffsetFromNorm(columns, column.OffsetNorm)); + TreePop(); +} + +static void DebugNodeDockNodeFlags(ImGuiDockNodeFlags* p_flags, const char* label, bool enabled) +{ + using namespace ImGui; + PushID(label); + PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + Text("%s:", label); + if (!enabled) + BeginDisabled(); + CheckboxFlags("NoResize", p_flags, ImGuiDockNodeFlags_NoResize); + CheckboxFlags("NoResizeX", p_flags, ImGuiDockNodeFlags_NoResizeX); + CheckboxFlags("NoResizeY",p_flags, ImGuiDockNodeFlags_NoResizeY); + CheckboxFlags("NoTabBar", p_flags, ImGuiDockNodeFlags_NoTabBar); + CheckboxFlags("HiddenTabBar", p_flags, ImGuiDockNodeFlags_HiddenTabBar); + CheckboxFlags("NoWindowMenuButton", p_flags, ImGuiDockNodeFlags_NoWindowMenuButton); + CheckboxFlags("NoCloseButton", p_flags, ImGuiDockNodeFlags_NoCloseButton); + CheckboxFlags("DockedWindowsInFocusRoute", p_flags, ImGuiDockNodeFlags_DockedWindowsInFocusRoute); + CheckboxFlags("NoDocking", p_flags, ImGuiDockNodeFlags_NoDocking); // Multiple flags + CheckboxFlags("NoDockingSplit", p_flags, ImGuiDockNodeFlags_NoDockingSplit); + CheckboxFlags("NoDockingSplitOther", p_flags, ImGuiDockNodeFlags_NoDockingSplitOther); + CheckboxFlags("NoDockingOver", p_flags, ImGuiDockNodeFlags_NoDockingOverMe); + CheckboxFlags("NoDockingOverOther", p_flags, ImGuiDockNodeFlags_NoDockingOverOther); + CheckboxFlags("NoDockingOverEmpty", p_flags, ImGuiDockNodeFlags_NoDockingOverEmpty); + CheckboxFlags("NoUndocking", p_flags, ImGuiDockNodeFlags_NoUndocking); + if (!enabled) + EndDisabled(); + PopStyleVar(); + PopID(); +} + +// [DEBUG] Display contents of ImDockNode +void ImGui::DebugNodeDockNode(ImGuiDockNode* node, const char* label) +{ + ImGuiContext& g = *GImGui; + const bool is_alive = (g.FrameCount - node->LastFrameAlive < 2); // Submitted with ImGuiDockNodeFlags_KeepAliveOnly + const bool is_active = (g.FrameCount - node->LastFrameActive < 2); // Submitted + if (!is_alive) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open; + ImGuiTreeNodeFlags tree_node_flags = node->IsFocused ? ImGuiTreeNodeFlags_Selected : ImGuiTreeNodeFlags_None; + if (node->Windows.Size > 0) + open = TreeNodeEx((void*)(intptr_t)node->ID, tree_node_flags, "%s 0x%04X%s: %d windows (vis: '%s')", label, node->ID, node->IsVisible ? "" : " (hidden)", node->Windows.Size, node->VisibleWindow ? node->VisibleWindow->Name : "NULL"); + else + open = TreeNodeEx((void*)(intptr_t)node->ID, tree_node_flags, "%s 0x%04X%s: %s (vis: '%s')", label, node->ID, node->IsVisible ? "" : " (hidden)", (node->SplitAxis == ImGuiAxis_X) ? "horizontal split" : (node->SplitAxis == ImGuiAxis_Y) ? "vertical split" : "empty", node->VisibleWindow ? node->VisibleWindow->Name : "NULL"); + if (!is_alive) { PopStyleColor(); } + if (is_active && IsItemHovered()) + if (ImGuiWindow* window = node->HostWindow ? node->HostWindow : node->VisibleWindow) + GetForegroundDrawList(window)->AddRect(node->Pos, node->Pos + node->Size, IM_COL32(255, 255, 0, 255)); + if (open) + { + IM_ASSERT(node->ChildNodes[0] == NULL || node->ChildNodes[0]->ParentNode == node); + IM_ASSERT(node->ChildNodes[1] == NULL || node->ChildNodes[1]->ParentNode == node); + BulletText("Pos (%.0f,%.0f), Size (%.0f, %.0f) Ref (%.0f, %.0f)", + node->Pos.x, node->Pos.y, node->Size.x, node->Size.y, node->SizeRef.x, node->SizeRef.y); + DebugNodeWindow(node->HostWindow, "HostWindow"); + DebugNodeWindow(node->VisibleWindow, "VisibleWindow"); + BulletText("SelectedTabID: 0x%08X, LastFocusedNodeID: 0x%08X", node->SelectedTabId, node->LastFocusedNodeId); + BulletText("Misc:%s%s%s%s%s%s%s", + node->IsDockSpace() ? " IsDockSpace" : "", + node->IsCentralNode() ? " IsCentralNode" : "", + is_alive ? " IsAlive" : "", is_active ? " IsActive" : "", node->IsFocused ? " IsFocused" : "", + node->WantLockSizeOnce ? " WantLockSizeOnce" : "", + node->HasCentralNodeChild ? " HasCentralNodeChild" : ""); + if (TreeNode("flags", "Flags Merged: 0x%04X, Local: 0x%04X, InWindows: 0x%04X, Shared: 0x%04X", node->MergedFlags, node->LocalFlags, node->LocalFlagsInWindows, node->SharedFlags)) + { + if (BeginTable("flags", 4)) + { + TableNextColumn(); DebugNodeDockNodeFlags(&node->MergedFlags, "MergedFlags", false); + TableNextColumn(); DebugNodeDockNodeFlags(&node->LocalFlags, "LocalFlags", true); + TableNextColumn(); DebugNodeDockNodeFlags(&node->LocalFlagsInWindows, "LocalFlagsInWindows", false); + TableNextColumn(); DebugNodeDockNodeFlags(&node->SharedFlags, "SharedFlags", true); + EndTable(); + } + TreePop(); + } + if (node->ParentNode) + DebugNodeDockNode(node->ParentNode, "ParentNode"); + if (node->ChildNodes[0]) + DebugNodeDockNode(node->ChildNodes[0], "Child[0]"); + if (node->ChildNodes[1]) + DebugNodeDockNode(node->ChildNodes[1], "Child[1]"); + if (node->TabBar) + DebugNodeTabBar(node->TabBar, "TabBar"); + DebugNodeWindowsList(&node->Windows, "Windows"); + + TreePop(); + } +} + +// [DEBUG] Display contents of ImDrawList +// Note that both 'window' and 'viewport' may be NULL here. Viewport is generally null of destroyed popups which previously owned a viewport. +void ImGui::DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label) +{ + ImGuiContext& g = *GImGui; + ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; + int cmd_count = draw_list->CmdBuffer.Size; + if (cmd_count > 0 && draw_list->CmdBuffer.back().ElemCount == 0 && draw_list->CmdBuffer.back().UserCallback == NULL) + cmd_count--; + bool node_open = TreeNode(draw_list, "%s: '%s' %d vtx, %d indices, %d cmds", label, draw_list->_OwnerName ? draw_list->_OwnerName : "", draw_list->VtxBuffer.Size, draw_list->IdxBuffer.Size, cmd_count); + if (draw_list == GetWindowDrawList()) + { + SameLine(); + TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "CURRENTLY APPENDING"); // Can't display stats for active draw list! (we don't have the data double-buffered) + if (node_open) + TreePop(); + return; + } + + ImDrawList* fg_draw_list = viewport ? GetForegroundDrawList(viewport) : NULL; // Render additional visuals into the top-most draw list + if (window && IsItemHovered() && fg_draw_list) + fg_draw_list->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!node_open) + return; + + if (window && !window->WasActive) + TextDisabled("Warning: owning Window is inactive. This DrawList is not being rendered!"); + + for (const ImDrawCmd* pcmd = draw_list->CmdBuffer.Data; pcmd < draw_list->CmdBuffer.Data + cmd_count; pcmd++) + { + if (pcmd->UserCallback) + { + BulletText("Callback %p, user_data %p", pcmd->UserCallback, pcmd->UserCallbackData); + continue; + } + + char texid_desc[20]; + FormatTextureIDForDebugDisplay(texid_desc, IM_ARRAYSIZE(texid_desc), pcmd->TextureId); + char buf[300]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "DrawCmd:%5d tris, Tex %s, ClipRect (%4.0f,%4.0f)-(%4.0f,%4.0f)", + pcmd->ElemCount / 3, texid_desc, pcmd->ClipRect.x, pcmd->ClipRect.y, pcmd->ClipRect.z, pcmd->ClipRect.w); + bool pcmd_node_open = TreeNode((void*)(pcmd - draw_list->CmdBuffer.begin()), "%s", buf); + if (IsItemHovered() && (cfg->ShowDrawCmdMesh || cfg->ShowDrawCmdBoundingBoxes) && fg_draw_list) + DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, cfg->ShowDrawCmdMesh, cfg->ShowDrawCmdBoundingBoxes); + if (!pcmd_node_open) + continue; + + // Calculate approximate coverage area (touched pixel count) + // This will be in pixels squared as long there's no post-scaling happening to the renderer output. + const ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; + const ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data + pcmd->VtxOffset; + float total_area = 0.0f; + for (unsigned int idx_n = pcmd->IdxOffset; idx_n < pcmd->IdxOffset + pcmd->ElemCount; ) + { + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_n++) + triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos; + total_area += ImTriangleArea(triangle[0], triangle[1], triangle[2]); + } + + // Display vertex information summary. Hover to get all triangles drawn in wire-frame + ImFormatString(buf, IM_ARRAYSIZE(buf), "Mesh: ElemCount: %d, VtxOffset: +%d, IdxOffset: +%d, Area: ~%0.f px", pcmd->ElemCount, pcmd->VtxOffset, pcmd->IdxOffset, total_area); + Selectable(buf); + if (IsItemHovered() && fg_draw_list) + DebugNodeDrawCmdShowMeshAndBoundingBox(fg_draw_list, draw_list, pcmd, true, false); + + // Display individual triangles/vertices. Hover on to get the corresponding triangle highlighted. + ImGuiListClipper clipper; + clipper.Begin(pcmd->ElemCount / 3); // Manually coarse clip our print out of individual vertices to save CPU, only items that may be visible. + while (clipper.Step()) + for (int prim = clipper.DisplayStart, idx_i = pcmd->IdxOffset + clipper.DisplayStart * 3; prim < clipper.DisplayEnd; prim++) + { + char* buf_p = buf, * buf_end = buf + IM_ARRAYSIZE(buf); + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_i++) + { + const ImDrawVert& v = vtx_buffer[idx_buffer ? idx_buffer[idx_i] : idx_i]; + triangle[n] = v.pos; + buf_p += ImFormatString(buf_p, buf_end - buf_p, "%s %04d: pos (%8.2f,%8.2f), uv (%.6f,%.6f), col %08X\n", + (n == 0) ? "Vert:" : " ", idx_i, v.pos.x, v.pos.y, v.uv.x, v.uv.y, v.col); + } + + Selectable(buf, false); + if (fg_draw_list && IsItemHovered()) + { + ImDrawListFlags backup_flags = fg_draw_list->Flags; + fg_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. + fg_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); + fg_draw_list->Flags = backup_flags; + } + } + TreePop(); + } + TreePop(); +} + +// [DEBUG] Display mesh/aabb of a ImDrawCmd +void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb) +{ + IM_ASSERT(show_mesh || show_aabb); + + // Draw wire-frame version of all triangles + ImRect clip_rect = draw_cmd->ClipRect; + ImRect vtxs_rect(FLT_MAX, FLT_MAX, -FLT_MAX, -FLT_MAX); + ImDrawListFlags backup_flags = out_draw_list->Flags; + out_draw_list->Flags &= ~ImDrawListFlags_AntiAliasedLines; // Disable AA on triangle outlines is more readable for very large and thin triangles. + for (unsigned int idx_n = draw_cmd->IdxOffset, idx_end = draw_cmd->IdxOffset + draw_cmd->ElemCount; idx_n < idx_end; ) + { + ImDrawIdx* idx_buffer = (draw_list->IdxBuffer.Size > 0) ? draw_list->IdxBuffer.Data : NULL; // We don't hold on those pointers past iterations as ->AddPolyline() may invalidate them if out_draw_list==draw_list + ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data + draw_cmd->VtxOffset; + + ImVec2 triangle[3]; + for (int n = 0; n < 3; n++, idx_n++) + vtxs_rect.Add((triangle[n] = vtx_buffer[idx_buffer ? idx_buffer[idx_n] : idx_n].pos)); + if (show_mesh) + out_draw_list->AddPolyline(triangle, 3, IM_COL32(255, 255, 0, 255), ImDrawFlags_Closed, 1.0f); // In yellow: mesh triangles + } + // Draw bounding boxes + if (show_aabb) + { + out_draw_list->AddRect(ImTrunc(clip_rect.Min), ImTrunc(clip_rect.Max), IM_COL32(255, 0, 255, 255)); // In pink: clipping rectangle submitted to GPU + out_draw_list->AddRect(ImTrunc(vtxs_rect.Min), ImTrunc(vtxs_rect.Max), IM_COL32(0, 255, 255, 255)); // In cyan: bounding box of triangles + } + out_draw_list->Flags = backup_flags; +} + +// [DEBUG] Display details for a single font, called by ShowStyleEditor(). +void ImGui::DebugNodeFont(ImFont* font) +{ + bool opened = TreeNode(font, "Font: \"%s\": %.2f px, %d glyphs, %d sources(s)", + font->Sources ? font->Sources[0].Name : "", font->FontSize, font->Glyphs.Size, font->SourcesCount); + + // Display preview text + if (!opened) + Indent(); + Indent(); + PushFont(font); + Text("The quick brown fox jumps over the lazy dog"); + PopFont(); + if (!opened) + { + Unindent(); + Unindent(); + return; + } + if (SmallButton("Set as default")) + GetIO().FontDefault = font; + + // Display details + SetNextItemWidth(GetFontSize() * 8); + DragFloat("Font scale", &font->Scale, 0.005f, 0.3f, 2.0f, "%.1f"); + SameLine(); MetricsHelpMarker( + "Note that the default embedded font is NOT meant to be scaled.\n\n" + "Font are currently rendered into bitmaps at a given size at the time of building the atlas. " + "You may oversample them to get some flexibility with scaling. " + "You can also render at multiple sizes and select which one to use at runtime.\n\n" + "(Glimmer of hope: the atlas system will be rewritten in the future to make scaling more flexible.)"); + Text("Ascent: %f, Descent: %f, Height: %f", font->Ascent, font->Descent, font->Ascent - font->Descent); + char c_str[5]; + Text("Fallback character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->FallbackChar), font->FallbackChar); + Text("Ellipsis character: '%s' (U+%04X)", ImTextCharToUtf8(c_str, font->EllipsisChar), font->EllipsisChar); + const int surface_sqrt = (int)ImSqrt((float)font->MetricsTotalSurface); + Text("Texture Area: about %d px ~%dx%d px", font->MetricsTotalSurface, surface_sqrt, surface_sqrt); + for (int config_i = 0; config_i < font->SourcesCount; config_i++) + if (font->Sources) + { + const ImFontConfig* src = &font->Sources[config_i]; + int oversample_h, oversample_v; + ImFontAtlasBuildGetOversampleFactors(src, &oversample_h, &oversample_v); + BulletText("Input %d: \'%s\', Oversample: (%d=>%d,%d=>%d), PixelSnapH: %d, Offset: (%.1f,%.1f)", + config_i, src->Name, src->OversampleH, oversample_h, src->OversampleV, oversample_v, src->PixelSnapH, src->GlyphOffset.x, src->GlyphOffset.y); + } + + // Display all glyphs of the fonts in separate pages of 256 characters + { + if (TreeNode("Glyphs", "Glyphs (%d)", font->Glyphs.Size)) + { + ImDrawList* draw_list = GetWindowDrawList(); + const ImU32 glyph_col = GetColorU32(ImGuiCol_Text); + const float cell_size = font->FontSize * 1; + const float cell_spacing = GetStyle().ItemSpacing.y; + for (unsigned int base = 0; base <= IM_UNICODE_CODEPOINT_MAX; base += 256) + { + // Skip ahead if a large bunch of glyphs are not present in the font (test in chunks of 4k) + // This is only a small optimization to reduce the number of iterations when IM_UNICODE_MAX_CODEPOINT + // is large // (if ImWchar==ImWchar32 we will do at least about 272 queries here) + if (!(base & 8191) && font->IsGlyphRangeUnused(base, base + 8191)) + { + base += 8192 - 256; + continue; + } + + int count = 0; + for (unsigned int n = 0; n < 256; n++) + if (font->FindGlyphNoFallback((ImWchar)(base + n))) + count++; + if (count <= 0) + continue; + if (!TreeNode((void*)(intptr_t)base, "U+%04X..U+%04X (%d %s)", base, base + 255, count, count > 1 ? "glyphs" : "glyph")) + continue; + + // Draw a 16x16 grid of glyphs + ImVec2 base_pos = GetCursorScreenPos(); + for (unsigned int n = 0; n < 256; n++) + { + // We use ImFont::RenderChar as a shortcut because we don't have UTF-8 conversion functions + // available here and thus cannot easily generate a zero-terminated UTF-8 encoded string. + ImVec2 cell_p1(base_pos.x + (n % 16) * (cell_size + cell_spacing), base_pos.y + (n / 16) * (cell_size + cell_spacing)); + ImVec2 cell_p2(cell_p1.x + cell_size, cell_p1.y + cell_size); + const ImFontGlyph* glyph = font->FindGlyphNoFallback((ImWchar)(base + n)); + draw_list->AddRect(cell_p1, cell_p2, glyph ? IM_COL32(255, 255, 255, 100) : IM_COL32(255, 255, 255, 50)); + if (!glyph) + continue; + font->RenderChar(draw_list, cell_size, cell_p1, glyph_col, (ImWchar)(base + n)); + if (IsMouseHoveringRect(cell_p1, cell_p2) && BeginTooltip()) + { + DebugNodeFontGlyph(font, glyph); + EndTooltip(); + } + } + Dummy(ImVec2((cell_size + cell_spacing) * 16, (cell_size + cell_spacing) * 16)); + TreePop(); + } + TreePop(); + } + } + TreePop(); + Unindent(); +} + +void ImGui::DebugNodeFontGlyph(ImFont*, const ImFontGlyph* glyph) +{ + Text("Codepoint: U+%04X", glyph->Codepoint); + Separator(); + Text("Visible: %d", glyph->Visible); + Text("AdvanceX: %.1f", glyph->AdvanceX); + Text("Pos: (%.2f,%.2f)->(%.2f,%.2f)", glyph->X0, glyph->Y0, glyph->X1, glyph->Y1); + Text("UV: (%.3f,%.3f)->(%.3f,%.3f)", glyph->U0, glyph->V0, glyph->U1, glyph->V1); +} + +// [DEBUG] Display contents of ImGuiStorage +void ImGui::DebugNodeStorage(ImGuiStorage* storage, const char* label) +{ + if (!TreeNode(label, "%s: %d entries, %d bytes", label, storage->Data.Size, storage->Data.size_in_bytes())) + return; + for (const ImGuiStoragePair& p : storage->Data) + { + BulletText("Key 0x%08X Value { i: %d }", p.key, p.val_i); // Important: we currently don't store a type, real value may not be integer. + DebugLocateItemOnHover(p.key); + } + TreePop(); +} + +// [DEBUG] Display contents of ImGuiTabBar +void ImGui::DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label) +{ + // Standalone tab bars (not associated to docking/windows functionality) currently hold no discernible strings. + char buf[256]; + char* p = buf; + const char* buf_end = buf + IM_ARRAYSIZE(buf); + const bool is_active = (tab_bar->PrevFrameVisible >= GetFrameCount() - 2); + p += ImFormatString(p, buf_end - p, "%s 0x%08X (%d tabs)%s {", label, tab_bar->ID, tab_bar->Tabs.Size, is_active ? "" : " *Inactive*"); + for (int tab_n = 0; tab_n < ImMin(tab_bar->Tabs.Size, 3); tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + p += ImFormatString(p, buf_end - p, "%s'%s'", tab_n > 0 ? ", " : "", TabBarGetTabName(tab_bar, tab)); + } + p += ImFormatString(p, buf_end - p, (tab_bar->Tabs.Size > 3) ? " ... }" : " } "); + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode(label, "%s", buf); + if (!is_active) { PopStyleColor(); } + if (is_active && IsItemHovered()) + { + ImDrawList* draw_list = GetForegroundDrawList(); + draw_list->AddRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, IM_COL32(255, 255, 0, 255)); + draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMinX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); + draw_list->AddLine(ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Min.y), ImVec2(tab_bar->ScrollingRectMaxX, tab_bar->BarRect.Max.y), IM_COL32(0, 255, 0, 255)); + } + if (open) + { + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + PushID(tab); + if (SmallButton("<")) { TabBarQueueReorder(tab_bar, tab, -1); } SameLine(0, 2); + if (SmallButton(">")) { TabBarQueueReorder(tab_bar, tab, +1); } SameLine(); + Text("%02d%c Tab 0x%08X '%s' Offset: %.2f, Width: %.2f/%.2f", + tab_n, (tab->ID == tab_bar->SelectedTabId) ? '*' : ' ', tab->ID, TabBarGetTabName(tab_bar, tab), tab->Offset, tab->Width, tab->ContentWidth); + PopID(); + } + TreePop(); + } +} + +void ImGui::DebugNodeViewport(ImGuiViewportP* viewport) +{ + ImGuiContext& g = *GImGui; + SetNextItemOpen(true, ImGuiCond_Once); + bool open = TreeNode((void*)(intptr_t)viewport->ID, "Viewport #%d, ID: 0x%08X, Parent: 0x%08X, Window: \"%s\"", viewport->Idx, viewport->ID, viewport->ParentViewportId, viewport->Window ? viewport->Window->Name : "N/A"); + if (IsItemHovered()) + g.DebugMetricsConfig.HighlightViewportID = viewport->ID; + if (open) + { + ImGuiWindowFlags flags = viewport->Flags; + BulletText("Main Pos: (%.0f,%.0f), Size: (%.0f,%.0f)\nWorkArea Inset Left: %.0f Top: %.0f, Right: %.0f, Bottom: %.0f\nMonitor: %d, DpiScale: %.0f%%", + viewport->Pos.x, viewport->Pos.y, viewport->Size.x, viewport->Size.y, + viewport->WorkInsetMin.x, viewport->WorkInsetMin.y, viewport->WorkInsetMax.x, viewport->WorkInsetMax.y, + viewport->PlatformMonitor, viewport->DpiScale * 100.0f); + if (viewport->Idx > 0) { SameLine(); if (SmallButton("Reset Pos")) { viewport->Pos = ImVec2(200, 200); viewport->UpdateWorkRect(); if (viewport->Window) viewport->Window->Pos = viewport->Pos; } } + BulletText("Flags: 0x%04X =%s%s%s%s%s%s%s%s%s%s%s%s%s", viewport->Flags, + //(flags & ImGuiViewportFlags_IsPlatformWindow) ? " IsPlatformWindow" : "", // Omitting because it is the standard + (flags & ImGuiViewportFlags_IsPlatformMonitor) ? " IsPlatformMonitor" : "", + (flags & ImGuiViewportFlags_IsMinimized) ? " IsMinimized" : "", + (flags & ImGuiViewportFlags_IsFocused) ? " IsFocused" : "", + (flags & ImGuiViewportFlags_OwnedByApp) ? " OwnedByApp" : "", + (flags & ImGuiViewportFlags_NoDecoration) ? " NoDecoration" : "", + (flags & ImGuiViewportFlags_NoTaskBarIcon) ? " NoTaskBarIcon" : "", + (flags & ImGuiViewportFlags_NoFocusOnAppearing) ? " NoFocusOnAppearing" : "", + (flags & ImGuiViewportFlags_NoFocusOnClick) ? " NoFocusOnClick" : "", + (flags & ImGuiViewportFlags_NoInputs) ? " NoInputs" : "", + (flags & ImGuiViewportFlags_NoRendererClear) ? " NoRendererClear" : "", + (flags & ImGuiViewportFlags_NoAutoMerge) ? " NoAutoMerge" : "", + (flags & ImGuiViewportFlags_TopMost) ? " TopMost" : "", + (flags & ImGuiViewportFlags_CanHostOtherWindows) ? " CanHostOtherWindows" : ""); + for (ImDrawList* draw_list : viewport->DrawDataP.CmdLists) + DebugNodeDrawList(NULL, viewport, draw_list, "DrawList"); + TreePop(); + } +} + +void ImGui::DebugNodePlatformMonitor(ImGuiPlatformMonitor* monitor, const char* label, int idx) +{ + BulletText("%s %d: DPI %.0f%%\n MainMin (%.0f,%.0f), MainMax (%.0f,%.0f), MainSize (%.0f,%.0f)\n WorkMin (%.0f,%.0f), WorkMax (%.0f,%.0f), WorkSize (%.0f,%.0f)", + label, idx, monitor->DpiScale * 100.0f, + monitor->MainPos.x, monitor->MainPos.y, monitor->MainPos.x + monitor->MainSize.x, monitor->MainPos.y + monitor->MainSize.y, monitor->MainSize.x, monitor->MainSize.y, + monitor->WorkPos.x, monitor->WorkPos.y, monitor->WorkPos.x + monitor->WorkSize.x, monitor->WorkPos.y + monitor->WorkSize.y, monitor->WorkSize.x, monitor->WorkSize.y); +} + +void ImGui::DebugNodeWindow(ImGuiWindow* window, const char* label) +{ + if (window == NULL) + { + BulletText("%s: NULL", label); + return; + } + + ImGuiContext& g = *GImGui; + const bool is_active = window->WasActive; + ImGuiTreeNodeFlags tree_node_flags = (window == g.NavWindow) ? ImGuiTreeNodeFlags_Selected : ImGuiTreeNodeFlags_None; + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + const bool open = TreeNodeEx(label, tree_node_flags, "%s '%s'%s", label, window->Name, is_active ? "" : " *Inactive*"); + if (!is_active) { PopStyleColor(); } + if (IsItemHovered() && is_active) + GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 255, 0, 255)); + if (!open) + return; + + if (window->MemoryCompacted) + TextDisabled("Note: some memory buffers have been compacted/freed."); + + if (g.IO.ConfigDebugIsDebuggerPresent && DebugBreakButton("**DebugBreak**", "in Begin()")) + g.DebugBreakInWindow = window->ID; + + ImGuiWindowFlags flags = window->Flags; + DebugNodeDrawList(window, window->Viewport, window->DrawList, "DrawList"); + BulletText("Pos: (%.1f,%.1f), Size: (%.1f,%.1f), ContentSize (%.1f,%.1f) Ideal (%.1f,%.1f)", window->Pos.x, window->Pos.y, window->Size.x, window->Size.y, window->ContentSize.x, window->ContentSize.y, window->ContentSizeIdeal.x, window->ContentSizeIdeal.y); + BulletText("Flags: 0x%08X (%s%s%s%s%s%s%s%s%s..)", flags, + (flags & ImGuiWindowFlags_ChildWindow) ? "Child " : "", (flags & ImGuiWindowFlags_Tooltip) ? "Tooltip " : "", (flags & ImGuiWindowFlags_Popup) ? "Popup " : "", + (flags & ImGuiWindowFlags_Modal) ? "Modal " : "", (flags & ImGuiWindowFlags_ChildMenu) ? "ChildMenu " : "", (flags & ImGuiWindowFlags_NoSavedSettings) ? "NoSavedSettings " : "", + (flags & ImGuiWindowFlags_NoMouseInputs)? "NoMouseInputs":"", (flags & ImGuiWindowFlags_NoNavInputs) ? "NoNavInputs" : "", (flags & ImGuiWindowFlags_AlwaysAutoResize) ? "AlwaysAutoResize" : ""); + if (flags & ImGuiWindowFlags_ChildWindow) + BulletText("ChildFlags: 0x%08X (%s%s%s%s..)", window->ChildFlags, + (window->ChildFlags & ImGuiChildFlags_Borders) ? "Borders " : "", + (window->ChildFlags & ImGuiChildFlags_ResizeX) ? "ResizeX " : "", + (window->ChildFlags & ImGuiChildFlags_ResizeY) ? "ResizeY " : "", + (window->ChildFlags & ImGuiChildFlags_NavFlattened) ? "NavFlattened " : ""); + BulletText("WindowClassId: 0x%08X", window->WindowClass.ClassId); + BulletText("Scroll: (%.2f/%.2f,%.2f/%.2f) Scrollbar:%s%s", window->Scroll.x, window->ScrollMax.x, window->Scroll.y, window->ScrollMax.y, window->ScrollbarX ? "X" : "", window->ScrollbarY ? "Y" : ""); + BulletText("Active: %d/%d, WriteAccessed: %d, BeginOrderWithinContext: %d", window->Active, window->WasActive, window->WriteAccessed, (window->Active || window->WasActive) ? window->BeginOrderWithinContext : -1); + BulletText("Appearing: %d, Hidden: %d (CanSkip %d Cannot %d), SkipItems: %d", window->Appearing, window->Hidden, window->HiddenFramesCanSkipItems, window->HiddenFramesCannotSkipItems, window->SkipItems); + for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) + { + ImRect r = window->NavRectRel[layer]; + if (r.Min.x >= r.Max.x && r.Min.y >= r.Max.y) + BulletText("NavLastIds[%d]: 0x%08X", layer, window->NavLastIds[layer]); + else + BulletText("NavLastIds[%d]: 0x%08X at +(%.1f,%.1f)(%.1f,%.1f)", layer, window->NavLastIds[layer], r.Min.x, r.Min.y, r.Max.x, r.Max.y); + DebugLocateItemOnHover(window->NavLastIds[layer]); + } + const ImVec2* pr = window->NavPreferredScoringPosRel; + for (int layer = 0; layer < ImGuiNavLayer_COUNT; layer++) + BulletText("NavPreferredScoringPosRel[%d] = {%.1f,%.1f)", layer, (pr[layer].x == FLT_MAX ? -99999.0f : pr[layer].x), (pr[layer].y == FLT_MAX ? -99999.0f : pr[layer].y)); // Display as 99999.0f so it looks neater. + BulletText("NavLayersActiveMask: %X, NavLastChildNavWindow: %s", window->DC.NavLayersActiveMask, window->NavLastChildNavWindow ? window->NavLastChildNavWindow->Name : "NULL"); + + BulletText("Viewport: %d%s, ViewportId: 0x%08X, ViewportPos: (%.1f,%.1f)", window->Viewport ? window->Viewport->Idx : -1, window->ViewportOwned ? " (Owned)" : "", window->ViewportId, window->ViewportPos.x, window->ViewportPos.y); + BulletText("ViewportMonitor: %d", window->Viewport ? window->Viewport->PlatformMonitor : -1); + BulletText("DockId: 0x%04X, DockOrder: %d, Act: %d, Vis: %d", window->DockId, window->DockOrder, window->DockIsActive, window->DockTabIsVisible); + if (window->DockNode || window->DockNodeAsHost) + DebugNodeDockNode(window->DockNodeAsHost ? window->DockNodeAsHost : window->DockNode, window->DockNodeAsHost ? "DockNodeAsHost" : "DockNode"); + + if (window->RootWindow != window) { DebugNodeWindow(window->RootWindow, "RootWindow"); } + if (window->RootWindowDockTree != window->RootWindow) { DebugNodeWindow(window->RootWindowDockTree, "RootWindowDockTree"); } + if (window->ParentWindow != NULL) { DebugNodeWindow(window->ParentWindow, "ParentWindow"); } + if (window->ParentWindowForFocusRoute != NULL) { DebugNodeWindow(window->ParentWindowForFocusRoute, "ParentWindowForFocusRoute"); } + if (window->DC.ChildWindows.Size > 0) { DebugNodeWindowsList(&window->DC.ChildWindows, "ChildWindows"); } + if (window->ColumnsStorage.Size > 0 && TreeNode("Columns", "Columns sets (%d)", window->ColumnsStorage.Size)) + { + for (ImGuiOldColumns& columns : window->ColumnsStorage) + DebugNodeColumns(&columns); + TreePop(); + } + DebugNodeStorage(&window->StateStorage, "Storage"); + TreePop(); +} + +void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings* settings) +{ + if (settings->WantDelete) + BeginDisabled(); + Text("0x%08X \"%s\" Pos (%d,%d) Size (%d,%d) Collapsed=%d", + settings->ID, settings->GetName(), settings->Pos.x, settings->Pos.y, settings->Size.x, settings->Size.y, settings->Collapsed); + if (settings->WantDelete) + EndDisabled(); +} + +void ImGui::DebugNodeWindowsList(ImVector* windows, const char* label) +{ + if (!TreeNode(label, "%s (%d)", label, windows->Size)) + return; + for (int i = windows->Size - 1; i >= 0; i--) // Iterate front to back + { + PushID((*windows)[i]); + DebugNodeWindow((*windows)[i], "Window"); + PopID(); + } + TreePop(); +} + +// FIXME-OPT: This is technically suboptimal, but it is simpler this way. +void ImGui::DebugNodeWindowsListByBeginStackParent(ImGuiWindow** windows, int windows_size, ImGuiWindow* parent_in_begin_stack) +{ + for (int i = 0; i < windows_size; i++) + { + ImGuiWindow* window = windows[i]; + if (window->ParentWindowInBeginStack != parent_in_begin_stack) + continue; + char buf[20]; + ImFormatString(buf, IM_ARRAYSIZE(buf), "[%04d] Window", window->BeginOrderWithinContext); + //BulletText("[%04d] Window '%s'", window->BeginOrderWithinContext, window->Name); + DebugNodeWindow(window, buf); + Indent(); + DebugNodeWindowsListByBeginStackParent(windows + i + 1, windows_size - i - 1, window); + Unindent(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DEBUG LOG WINDOW +//----------------------------------------------------------------------------- + +void ImGui::DebugLog(const char* fmt, ...) +{ + va_list args; + va_start(args, fmt); + DebugLogV(fmt, args); + va_end(args); +} + +void ImGui::DebugLogV(const char* fmt, va_list args) +{ + ImGuiContext& g = *GImGui; + const int old_size = g.DebugLogBuf.size(); + if (g.ContextName[0] != 0) + g.DebugLogBuf.appendf("[%s] [%05d] ", g.ContextName, g.FrameCount); + else + g.DebugLogBuf.appendf("[%05d] ", g.FrameCount); + g.DebugLogBuf.appendfv(fmt, args); + g.DebugLogIndex.append(g.DebugLogBuf.c_str(), old_size, g.DebugLogBuf.size()); + if (g.DebugLogFlags & ImGuiDebugLogFlags_OutputToTTY) + IMGUI_DEBUG_PRINTF("%s", g.DebugLogBuf.begin() + old_size); +#ifdef IMGUI_ENABLE_TEST_ENGINE + // IMGUI_TEST_ENGINE_LOG() adds a trailing \n automatically + const int new_size = g.DebugLogBuf.size(); + const bool trailing_carriage_return = (g.DebugLogBuf[new_size - 1] == '\n'); + if (g.DebugLogFlags & ImGuiDebugLogFlags_OutputToTestEngine) + IMGUI_TEST_ENGINE_LOG("%.*s", new_size - old_size - (trailing_carriage_return ? 1 : 0), g.DebugLogBuf.begin() + old_size); +#endif +} + +// FIXME-LAYOUT: To be done automatically via layout mode once we rework ItemSize/ItemAdd into ItemLayout. +static void SameLineOrWrap(const ImVec2& size) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 pos(window->DC.CursorPosPrevLine.x + g.Style.ItemSpacing.x, window->DC.CursorPosPrevLine.y); + if (window->WorkRect.Contains(ImRect(pos, pos + size))) + ImGui::SameLine(); +} + +static void ShowDebugLogFlag(const char* name, ImGuiDebugLogFlags flags) +{ + ImGuiContext& g = *GImGui; + ImVec2 size(ImGui::GetFrameHeight() + g.Style.ItemInnerSpacing.x + ImGui::CalcTextSize(name).x, ImGui::GetFrameHeight()); + SameLineOrWrap(size); // FIXME-LAYOUT: To be done automatically once we rework ItemSize/ItemAdd into ItemLayout. + + bool highlight_errors = (flags == ImGuiDebugLogFlags_EventError && g.DebugLogSkippedErrors > 0); + if (highlight_errors) + ImGui::PushStyleColor(ImGuiCol_Text, ImLerp(g.Style.Colors[ImGuiCol_Text], ImVec4(1.0f, 0.0f, 0.0f, 1.0f), 0.30f)); + if (ImGui::CheckboxFlags(name, &g.DebugLogFlags, flags) && g.IO.KeyShift && (g.DebugLogFlags & flags) != 0) + { + g.DebugLogAutoDisableFrames = 2; + g.DebugLogAutoDisableFlags |= flags; + } + if (highlight_errors) + { + ImGui::PopStyleColor(); + ImGui::SetItemTooltip("%d past errors skipped.", g.DebugLogSkippedErrors); + } + else + { + ImGui::SetItemTooltip("Hold SHIFT when clicking to enable for 2 frames only (useful for spammy log entries)"); + } +} + +void ImGui::ShowDebugLogWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0) + SetNextWindowSize(ImVec2(0.0f, GetFontSize() * 12.0f), ImGuiCond_FirstUseEver); + if (!Begin("Dear ImGui Debug Log", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + ImGuiDebugLogFlags all_enable_flags = ImGuiDebugLogFlags_EventMask_ & ~ImGuiDebugLogFlags_EventInputRouting; + CheckboxFlags("All", &g.DebugLogFlags, all_enable_flags); + SetItemTooltip("(except InputRouting which is spammy)"); + + ShowDebugLogFlag("Errors", ImGuiDebugLogFlags_EventError); + ShowDebugLogFlag("ActiveId", ImGuiDebugLogFlags_EventActiveId); + ShowDebugLogFlag("Clipper", ImGuiDebugLogFlags_EventClipper); + ShowDebugLogFlag("Docking", ImGuiDebugLogFlags_EventDocking); + ShowDebugLogFlag("Focus", ImGuiDebugLogFlags_EventFocus); + ShowDebugLogFlag("IO", ImGuiDebugLogFlags_EventIO); + //ShowDebugLogFlag("Font", ImGuiDebugLogFlags_EventFont); + ShowDebugLogFlag("Nav", ImGuiDebugLogFlags_EventNav); + ShowDebugLogFlag("Popup", ImGuiDebugLogFlags_EventPopup); + ShowDebugLogFlag("Selection", ImGuiDebugLogFlags_EventSelection); + ShowDebugLogFlag("Viewport", ImGuiDebugLogFlags_EventViewport); + ShowDebugLogFlag("InputRouting", ImGuiDebugLogFlags_EventInputRouting); + + if (SmallButton("Clear")) + { + g.DebugLogBuf.clear(); + g.DebugLogIndex.clear(); + g.DebugLogSkippedErrors = 0; + } + SameLine(); + if (SmallButton("Copy")) + SetClipboardText(g.DebugLogBuf.c_str()); + SameLine(); + if (SmallButton("Configure Outputs..")) + OpenPopup("Outputs"); + if (BeginPopup("Outputs")) + { + CheckboxFlags("OutputToTTY", &g.DebugLogFlags, ImGuiDebugLogFlags_OutputToTTY); +#ifndef IMGUI_ENABLE_TEST_ENGINE + BeginDisabled(); +#endif + CheckboxFlags("OutputToTestEngine", &g.DebugLogFlags, ImGuiDebugLogFlags_OutputToTestEngine); +#ifndef IMGUI_ENABLE_TEST_ENGINE + EndDisabled(); +#endif + EndPopup(); + } + + BeginChild("##log", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar); + + const ImGuiDebugLogFlags backup_log_flags = g.DebugLogFlags; + g.DebugLogFlags &= ~ImGuiDebugLogFlags_EventClipper; + + ImGuiListClipper clipper; + clipper.Begin(g.DebugLogIndex.size()); + while (clipper.Step()) + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + DebugTextUnformattedWithLocateItem(g.DebugLogIndex.get_line_begin(g.DebugLogBuf.c_str(), line_no), g.DebugLogIndex.get_line_end(g.DebugLogBuf.c_str(), line_no)); + g.DebugLogFlags = backup_log_flags; + if (GetScrollY() >= GetScrollMaxY()) + SetScrollHereY(1.0f); + EndChild(); + + End(); +} + +// Display line, search for 0xXXXXXXXX identifiers and call DebugLocateItemOnHover() when hovered. +void ImGui::DebugTextUnformattedWithLocateItem(const char* line_begin, const char* line_end) +{ + TextUnformatted(line_begin, line_end); + if (!IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem)) + return; + ImGuiContext& g = *GImGui; + ImRect text_rect = g.LastItemData.Rect; + for (const char* p = line_begin; p <= line_end - 10; p++) + { + ImGuiID id = 0; + if (p[0] != '0' || (p[1] != 'x' && p[1] != 'X') || sscanf(p + 2, "%X", &id) != 1 || ImCharIsXdigitA(p[10])) + continue; + ImVec2 p0 = CalcTextSize(line_begin, p); + ImVec2 p1 = CalcTextSize(p, p + 10); + g.LastItemData.Rect = ImRect(text_rect.Min + ImVec2(p0.x, 0.0f), text_rect.Min + ImVec2(p0.x + p1.x, p1.y)); + if (IsMouseHoveringRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, true)) + DebugLocateItemOnHover(id); + p += 10; + } +} + +//----------------------------------------------------------------------------- +// [SECTION] OTHER DEBUG TOOLS (ITEM PICKER, ID STACK TOOL) +//----------------------------------------------------------------------------- + +// Draw a small cross at current CursorPos in current window's DrawList +void ImGui::DebugDrawCursorPos(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImVec2 pos = window->DC.CursorPos; + window->DrawList->AddLine(ImVec2(pos.x, pos.y - 3.0f), ImVec2(pos.x, pos.y + 4.0f), col, 1.0f); + window->DrawList->AddLine(ImVec2(pos.x - 3.0f, pos.y), ImVec2(pos.x + 4.0f, pos.y), col, 1.0f); +} + +// Draw a 10px wide rectangle around CurposPos.x using Line Y1/Y2 in current window's DrawList +void ImGui::DebugDrawLineExtents(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + float curr_x = window->DC.CursorPos.x; + float line_y1 = (window->DC.IsSameLine ? window->DC.CursorPosPrevLine.y : window->DC.CursorPos.y); + float line_y2 = line_y1 + (window->DC.IsSameLine ? window->DC.PrevLineSize.y : window->DC.CurrLineSize.y); + window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y1), ImVec2(curr_x + 5.0f, line_y1), col, 1.0f); + window->DrawList->AddLine(ImVec2(curr_x - 0.5f, line_y1), ImVec2(curr_x - 0.5f, line_y2), col, 1.0f); + window->DrawList->AddLine(ImVec2(curr_x - 5.0f, line_y2), ImVec2(curr_x + 5.0f, line_y2), col, 1.0f); +} + +// Draw last item rect in ForegroundDrawList (so it is always visible) +void ImGui::DebugDrawItemRect(ImU32 col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + GetForegroundDrawList(window)->AddRect(g.LastItemData.Rect.Min, g.LastItemData.Rect.Max, col); +} + +// [DEBUG] Locate item position/rectangle given an ID. +static const ImU32 DEBUG_LOCATE_ITEM_COLOR = IM_COL32(0, 255, 0, 255); // Green + +void ImGui::DebugLocateItem(ImGuiID target_id) +{ + ImGuiContext& g = *GImGui; + g.DebugLocateId = target_id; + g.DebugLocateFrames = 2; + g.DebugBreakInLocateId = false; +} + +// FIXME: Doesn't work over through a modal window, because they clear HoveredWindow. +void ImGui::DebugLocateItemOnHover(ImGuiID target_id) +{ + if (target_id == 0 || !IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenBlockedByPopup)) + return; + ImGuiContext& g = *GImGui; + DebugLocateItem(target_id); + GetForegroundDrawList(g.CurrentWindow)->AddRect(g.LastItemData.Rect.Min - ImVec2(3.0f, 3.0f), g.LastItemData.Rect.Max + ImVec2(3.0f, 3.0f), DEBUG_LOCATE_ITEM_COLOR); + + // Can't easily use a context menu here because it will mess with focus, active id etc. + if (g.IO.ConfigDebugIsDebuggerPresent && g.MouseStationaryTimer > 1.0f) + { + DebugBreakButtonTooltip(false, "in ItemAdd()"); + if (IsKeyChordPressed(g.DebugBreakKeyChord)) + g.DebugBreakInLocateId = true; + } +} + +void ImGui::DebugLocateItemResolveWithLastItem() +{ + ImGuiContext& g = *GImGui; + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInLocateId) + IM_DEBUG_BREAK(); + + ImGuiLastItemData item_data = g.LastItemData; + g.DebugLocateId = 0; + ImDrawList* draw_list = GetForegroundDrawList(g.CurrentWindow); + ImRect r = item_data.Rect; + r.Expand(3.0f); + ImVec2 p1 = g.IO.MousePos; + ImVec2 p2 = ImVec2((p1.x < r.Min.x) ? r.Min.x : (p1.x > r.Max.x) ? r.Max.x : p1.x, (p1.y < r.Min.y) ? r.Min.y : (p1.y > r.Max.y) ? r.Max.y : p1.y); + draw_list->AddRect(r.Min, r.Max, DEBUG_LOCATE_ITEM_COLOR); + draw_list->AddLine(p1, p2, DEBUG_LOCATE_ITEM_COLOR); +} + +void ImGui::DebugStartItemPicker() +{ + ImGuiContext& g = *GImGui; + g.DebugItemPickerActive = true; +} + +// [DEBUG] Item picker tool - start with DebugStartItemPicker() - useful to visually select an item and break into its call-stack. +void ImGui::UpdateDebugToolItemPicker() +{ + ImGuiContext& g = *GImGui; + g.DebugItemPickerBreakId = 0; + if (!g.DebugItemPickerActive) + return; + + const ImGuiID hovered_id = g.HoveredIdPreviousFrame; + SetMouseCursor(ImGuiMouseCursor_Hand); + if (IsKeyPressed(ImGuiKey_Escape)) + g.DebugItemPickerActive = false; + const bool change_mapping = g.IO.KeyMods == (ImGuiMod_Ctrl | ImGuiMod_Shift); + if (!change_mapping && IsMouseClicked(g.DebugItemPickerMouseButton) && hovered_id) + { + g.DebugItemPickerBreakId = hovered_id; + g.DebugItemPickerActive = false; + } + for (int mouse_button = 0; mouse_button < 3; mouse_button++) + if (change_mapping && IsMouseClicked(mouse_button)) + g.DebugItemPickerMouseButton = (ImU8)mouse_button; + SetNextWindowBgAlpha(0.70f); + if (!BeginTooltip()) + return; + Text("HoveredId: 0x%08X", hovered_id); + Text("Press ESC to abort picking."); + const char* mouse_button_names[] = { "Left", "Right", "Middle" }; + if (change_mapping) + Text("Remap w/ Ctrl+Shift: click anywhere to select new mouse button."); + else + TextColored(GetStyleColorVec4(hovered_id ? ImGuiCol_Text : ImGuiCol_TextDisabled), "Click %s Button to break in debugger! (remap w/ Ctrl+Shift)", mouse_button_names[g.DebugItemPickerMouseButton]); + EndTooltip(); +} + +// [DEBUG] ID Stack Tool: update queries. Called by NewFrame() +void ImGui::UpdateDebugToolStackQueries() +{ + ImGuiContext& g = *GImGui; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + + // Clear hook when id stack tool is not visible + g.DebugHookIdInfo = 0; + if (g.FrameCount != tool->LastActiveFrame + 1) + return; + + // Update queries. The steps are: -1: query Stack, >= 0: query each stack item + // We can only perform 1 ID Info query every frame. This is designed so the GetID() tests are cheap and constant-time + const ImGuiID query_id = g.HoveredIdPreviousFrame ? g.HoveredIdPreviousFrame : g.ActiveId; + if (tool->QueryId != query_id) + { + tool->QueryId = query_id; + tool->StackLevel = -1; + tool->Results.resize(0); + } + if (query_id == 0) + return; + + // Advance to next stack level when we got our result, or after 2 frames (in case we never get a result) + int stack_level = tool->StackLevel; + if (stack_level >= 0 && stack_level < tool->Results.Size) + if (tool->Results[stack_level].QuerySuccess || tool->Results[stack_level].QueryFrameCount > 2) + tool->StackLevel++; + + // Update hook + stack_level = tool->StackLevel; + if (stack_level == -1) + g.DebugHookIdInfo = query_id; + if (stack_level >= 0 && stack_level < tool->Results.Size) + { + g.DebugHookIdInfo = tool->Results[stack_level].ID; + tool->Results[stack_level].QueryFrameCount++; + } +} + +// [DEBUG] ID Stack tool: hooks called by GetID() family functions +void ImGui::DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + + // Step 0: stack query + // This assumes that the ID was computed with the current ID stack, which tends to be the case for our widget. + if (tool->StackLevel == -1) + { + tool->StackLevel++; + tool->Results.resize(window->IDStack.Size + 1, ImGuiStackLevelInfo()); + for (int n = 0; n < window->IDStack.Size + 1; n++) + tool->Results[n].ID = (n < window->IDStack.Size) ? window->IDStack[n] : id; + return; + } + + // Step 1+: query for individual level + IM_ASSERT(tool->StackLevel >= 0); + if (tool->StackLevel != window->IDStack.Size) + return; + ImGuiStackLevelInfo* info = &tool->Results[tool->StackLevel]; + IM_ASSERT(info->ID == id && info->QueryFrameCount > 0); + + switch (data_type) + { + case ImGuiDataType_S32: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%d", (int)(intptr_t)data_id); + break; + case ImGuiDataType_String: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "%.*s", data_id_end ? (int)((const char*)data_id_end - (const char*)data_id) : (int)ImStrlen((const char*)data_id), (const char*)data_id); + break; + case ImGuiDataType_Pointer: + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "(void*)0x%p", data_id); + break; + case ImGuiDataType_ID: + if (info->Desc[0] != 0) // PushOverrideID() is often used to avoid hashing twice, which would lead to 2 calls to DebugHookIdInfo(). We prioritize the first one. + return; + ImFormatString(info->Desc, IM_ARRAYSIZE(info->Desc), "0x%08X [override]", id); + break; + default: + IM_ASSERT(0); + } + info->QuerySuccess = true; + info->DataType = data_type; +} + +static int StackToolFormatLevelInfo(ImGuiIDStackTool* tool, int n, bool format_for_ui, char* buf, size_t buf_size) +{ + ImGuiStackLevelInfo* info = &tool->Results[n]; + ImGuiWindow* window = (info->Desc[0] == 0 && n == 0) ? ImGui::FindWindowByID(info->ID) : NULL; + if (window) // Source: window name (because the root ID don't call GetID() and so doesn't get hooked) + return ImFormatString(buf, buf_size, format_for_ui ? "\"%s\" [window]" : "%s", window->Name); + if (info->QuerySuccess) // Source: GetID() hooks (prioritize over ItemInfo() because we frequently use patterns like: PushID(str), Button("") where they both have same id) + return ImFormatString(buf, buf_size, (format_for_ui && info->DataType == ImGuiDataType_String) ? "\"%s\"" : "%s", info->Desc); + if (tool->StackLevel < tool->Results.Size) // Only start using fallback below when all queries are done, so during queries we don't flickering ??? markers. + return (*buf = 0); +#ifdef IMGUI_ENABLE_TEST_ENGINE + if (const char* label = ImGuiTestEngine_FindItemDebugLabel(GImGui, info->ID)) // Source: ImGuiTestEngine's ItemInfo() + return ImFormatString(buf, buf_size, format_for_ui ? "??? \"%s\"" : "%s", label); +#endif + return ImFormatString(buf, buf_size, "???"); +} + +// ID Stack Tool: Display UI +void ImGui::ShowIDStackToolWindow(bool* p_open) +{ + ImGuiContext& g = *GImGui; + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0) + SetNextWindowSize(ImVec2(0.0f, GetFontSize() * 8.0f), ImGuiCond_FirstUseEver); + if (!Begin("Dear ImGui ID Stack Tool", p_open) || GetCurrentWindow()->BeginCount > 1) + { + End(); + return; + } + + // Display hovered/active status + ImGuiIDStackTool* tool = &g.DebugIDStackTool; + + // Build and display path + tool->ResultPathBuf.resize(0); + for (int stack_n = 0; stack_n < tool->Results.Size; stack_n++) + { + char level_desc[256]; + StackToolFormatLevelInfo(tool, stack_n, false, level_desc, IM_ARRAYSIZE(level_desc)); + tool->ResultPathBuf.append(stack_n == 0 ? "//" : "/"); + for (int n = 0; level_desc[n]; n++) + { + if (level_desc[n] == '/') + tool->ResultPathBuf.append("\\"); + tool->ResultPathBuf.append(level_desc + n, level_desc + n + 1); + } + } + Text("0x%08X", tool->QueryId); + SameLine(); + MetricsHelpMarker("Hover an item with the mouse to display elements of the ID Stack leading to the item's final ID.\nEach level of the stack correspond to a PushID() call.\nAll levels of the stack are hashed together to make the final ID of a widget (ID displayed at the bottom level of the stack).\nRead FAQ entry about the ID stack for details."); + + // CTRL+C to copy path + const float time_since_copy = (float)g.Time - tool->CopyToClipboardLastTime; + SameLine(); + PushStyleVarY(ImGuiStyleVar_FramePadding, 0.0f); Checkbox("Ctrl+C: copy path", &tool->CopyToClipboardOnCtrlC); PopStyleVar(); + SameLine(); + TextColored((time_since_copy >= 0.0f && time_since_copy < 0.75f && ImFmod(time_since_copy, 0.25f) < 0.25f * 0.5f) ? ImVec4(1.f, 1.f, 0.3f, 1.f) : ImVec4(), "*COPIED*"); + if (tool->CopyToClipboardOnCtrlC && Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteOverFocused)) + { + tool->CopyToClipboardLastTime = (float)g.Time; + SetClipboardText(tool->ResultPathBuf.c_str()); + } + + Text("- Path \"%s\"", tool->ResultPathBuf.c_str()); +#ifdef IMGUI_ENABLE_TEST_ENGINE + Text("- Label \"%s\"", tool->QueryId ? ImGuiTestEngine_FindItemDebugLabel(&g, tool->QueryId) : ""); +#endif + + Separator(); + + // Display decorated stack + tool->LastActiveFrame = g.FrameCount; + if (tool->Results.Size > 0 && BeginTable("##table", 3, ImGuiTableFlags_Borders)) + { + const float id_width = CalcTextSize("0xDDDDDDDD").x; + TableSetupColumn("Seed", ImGuiTableColumnFlags_WidthFixed, id_width); + TableSetupColumn("PushID", ImGuiTableColumnFlags_WidthStretch); + TableSetupColumn("Result", ImGuiTableColumnFlags_WidthFixed, id_width); + TableHeadersRow(); + for (int n = 0; n < tool->Results.Size; n++) + { + ImGuiStackLevelInfo* info = &tool->Results[n]; + TableNextColumn(); + Text("0x%08X", (n > 0) ? tool->Results[n - 1].ID : 0); + TableNextColumn(); + StackToolFormatLevelInfo(tool, n, true, g.TempBuffer.Data, g.TempBuffer.Size); + TextUnformatted(g.TempBuffer.Data); + TableNextColumn(); + Text("0x%08X", info->ID); + if (n == tool->Results.Size - 1) + TableSetBgColor(ImGuiTableBgTarget_CellBg, GetColorU32(ImGuiCol_Header)); + } + EndTable(); + } + End(); } #else -void ImGui::ShowMetricsWindow(bool*) { } +void ImGui::ShowMetricsWindow(bool*) {} +void ImGui::ShowFontAtlas(ImFontAtlas*) {} +void ImGui::DebugNodeColumns(ImGuiOldColumns*) {} +void ImGui::DebugNodeDrawList(ImGuiWindow*, ImGuiViewportP*, const ImDrawList*, const char*) {} +void ImGui::DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList*, const ImDrawList*, const ImDrawCmd*, bool, bool) {} +void ImGui::DebugNodeFont(ImFont*) {} +void ImGui::DebugNodeStorage(ImGuiStorage*, const char*) {} +void ImGui::DebugNodeTabBar(ImGuiTabBar*, const char*) {} +void ImGui::DebugNodeWindow(ImGuiWindow*, const char*) {} +void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings*) {} +void ImGui::DebugNodeWindowsList(ImVector*, const char*) {} +void ImGui::DebugNodeViewport(ImGuiViewportP*) {} -#endif +void ImGui::ShowDebugLogWindow(bool*) {} +void ImGui::ShowIDStackToolWindow(bool*) {} +void ImGui::DebugStartItemPicker() {} +void ImGui::DebugHookIdInfo(ImGuiID, ImGuiDataType, const void*, const void*) {} + +#endif // #ifndef IMGUI_DISABLE_DEBUG_TOOLS //----------------------------------------------------------------------------- @@ -10106,3 +22925,5 @@ void ImGui::ShowMetricsWindow(bool*) { } #endif //----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui.h b/imgui/imgui.h index 00a1f5cb..bafe97ff 100644 --- a/imgui/imgui.h +++ b/imgui/imgui.h @@ -1,58 +1,86 @@ -// dear imgui, v1.73 +// dear imgui, v1.91.9b // (headers) -// See imgui.cpp file for documentation. -// Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp for demo code. -// Newcomers, read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. -// Get latest version at https://github.com/ocornut/imgui +// Help: +// - See links below. +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// - Read top of imgui.cpp for more details, links and comments. +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. + +// Resources: +// - FAQ ........................ https://dearimgui.com/faq (in repository as docs/FAQ.md) +// - Homepage ................... https://github.com/ocornut/imgui +// - Releases & changelog ....... https://github.com/ocornut/imgui/releases +// - Gallery .................... https://github.com/ocornut/imgui/issues?q=label%3Agallery (please post your screenshots/video there!) +// - Wiki ....................... https://github.com/ocornut/imgui/wiki (lots of good stuff there) +// - Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started (how to integrate in an existing app by adding ~25 lines of code) +// - Third-party Extensions https://github.com/ocornut/imgui/wiki/Useful-Extensions (ImPlot & many more) +// - Bindings/Backends https://github.com/ocornut/imgui/wiki/Bindings (language bindings, backends for various tech/engines) +// - Glossary https://github.com/ocornut/imgui/wiki/Glossary +// - Debug Tools https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Software using Dear ImGui https://github.com/ocornut/imgui/wiki/Software-using-dear-imgui +// - Issues & support ........... https://github.com/ocornut/imgui/issues +// - Test Engine & Automation ... https://github.com/ocornut/imgui_test_engine (test suite, test engine to automate your apps) + +// For first-time users having issues compiling/linking/running/loading fonts: +// please post in https://github.com/ocornut/imgui/discussions if you cannot find a solution in resources above. +// Everything else should be asked in 'Issues'! We are building a database of cross-linked knowledge there. + +// Library Version +// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345') +#define IMGUI_VERSION "1.91.9b" +#define IMGUI_VERSION_NUM 19191 +#define IMGUI_HAS_TABLE +#define IMGUI_HAS_VIEWPORT // Viewport WIP branch +#define IMGUI_HAS_DOCK // Docking WIP branch /* Index of this file: -// Header mess -// Forward declarations and basic types -// ImGui API (Dear ImGui end-user API) -// Flags & Enumerations -// Memory allocations macros -// ImVector<> -// ImGuiStyle -// ImGuiIO -// Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) -// Obsolete functions -// Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, ImColor) -// Draw List API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) -// Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) +// [SECTION] Header mess +// [SECTION] Forward declarations and basic types +// [SECTION] Texture identifier (ImTextureID) +// [SECTION] Dear ImGui end-user API functions +// [SECTION] Flags & Enumerations +// [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) +// [SECTION] Helpers: Debug log, Memory allocations macros, ImVector<> +// [SECTION] ImGuiStyle +// [SECTION] ImGuiIO +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiWindowClass, ImGuiPayload) +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiMultiSelectIO, ImGuiSelectionRequest, ImGuiSelectionBasicStorage, ImGuiSelectionExternalStorage) +// [SECTION] Drawing API (ImDrawCallback, ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawFlags, ImDrawListFlags, ImDrawList, ImDrawData) +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontGlyphRangesBuilder, ImFontAtlasFlags, ImFontAtlas, ImFont) +// [SECTION] Viewports (ImGuiViewportFlags, ImGuiViewport) +// [SECTION] ImGuiPlatformIO + other Platform Dependent Interfaces (ImGuiPlatformMonitor, ImGuiPlatformImeData) +// [SECTION] Obsolete functions and types */ #pragma once -// Configuration file with compile-time options (edit imconfig.h or define IMGUI_USER_CONFIG to your own filename) +// Configuration file with compile-time options +// (edit imconfig.h or '#define IMGUI_USER_CONFIG "myfilename.h" from your build system) #ifdef IMGUI_USER_CONFIG #include IMGUI_USER_CONFIG #endif -#if !defined(IMGUI_DISABLE_INCLUDE_IMCONFIG_H) || defined(IMGUI_INCLUDE_IMCONFIG_H) #include "imconfig.h" -#endif + +#ifndef IMGUI_DISABLE //----------------------------------------------------------------------------- -// Header mess +// [SECTION] Header mess //----------------------------------------------------------------------------- -#include // FLT_MAX -#include // va_list +// Includes +#include // FLT_MIN, FLT_MAX +#include // va_list, va_start, va_end #include // ptrdiff_t, NULL #include // memset, memmove, memcpy, strlen, strchr, strcpy, strcmp -// Version -// (Integer encoded as XYYZZ for use in #if preprocessor conditionals. Work in progress versions typically starts at XYY99 then bounce up to XYY00, XYY01 etc. when release tagging happens) -#define IMGUI_VERSION "1.73" -#define IMGUI_VERSION_NUM 17300 -#define IMGUI_CHECKVERSION() ImGui::DebugCheckVersionAndDataLayout(IMGUI_VERSION, sizeof(ImGuiIO), sizeof(ImGuiStyle), sizeof(ImVec2), sizeof(ImVec4), sizeof(ImDrawVert), sizeof(ImDrawIdx)) - // Define attributes of all API symbols declarations (e.g. for DLL under Windows) -// IMGUI_API is used for core imgui functions, IMGUI_IMPL_API is used for the default bindings files (imgui_impl_xxx.h) -// Using dear imgui via a shared library is not recommended, because of function call overhead and because we don't guarantee backward nor forward ABI compatibility. +// IMGUI_API is used for core imgui functions, IMGUI_IMPL_API is used for the default backends files (imgui_impl_xxx.h) +// Using dear imgui via a shared library is not recommended: we don't guarantee backward nor forward ABI compatibility + this is a call-heavy library and function call overhead adds up. #ifndef IMGUI_API #define IMGUI_API #endif @@ -65,38 +93,75 @@ Index of this file: #include #define IM_ASSERT(_EXPR) assert(_EXPR) // You can override the default assert handler by editing imconfig.h #endif -#if defined(__clang__) || defined(__GNUC__) -#define IM_FMTARGS(FMT) __attribute__((format(printf, FMT, FMT+1))) // Apply printf-style warnings to user functions. +#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR) / sizeof(*(_ARR)))) // Size of a static C-style array. Don't use on pointers! +#define IM_UNUSED(_VAR) ((void)(_VAR)) // Used to silence "unused variable warnings". Often useful as asserts may be stripped out from final builds. + +// Check that version and structures layouts are matching between compiled imgui code and caller. Read comments above DebugCheckVersionAndDataLayout() for details. +#define IMGUI_CHECKVERSION() ImGui::DebugCheckVersionAndDataLayout(IMGUI_VERSION, sizeof(ImGuiIO), sizeof(ImGuiStyle), sizeof(ImVec2), sizeof(ImVec4), sizeof(ImDrawVert), sizeof(ImDrawIdx)) + +// Helper Macros - IM_FMTARGS, IM_FMTLIST: Apply printf-style warnings to our formatting functions. +// (MSVC provides an equivalent mechanism via SAL Annotations but it would require the macros in a different +// location. e.g. #include + void myprintf(_Printf_format_string_ const char* format, ...)) +#if !defined(IMGUI_USE_STB_SPRINTF) && defined(__MINGW32__) && !defined(__clang__) +#define IM_FMTARGS(FMT) __attribute__((format(gnu_printf, FMT, FMT+1))) +#define IM_FMTLIST(FMT) __attribute__((format(gnu_printf, FMT, 0))) +#elif !defined(IMGUI_USE_STB_SPRINTF) && (defined(__clang__) || defined(__GNUC__)) +#define IM_FMTARGS(FMT) __attribute__((format(printf, FMT, FMT+1))) #define IM_FMTLIST(FMT) __attribute__((format(printf, FMT, 0))) #else #define IM_FMTARGS(FMT) #define IM_FMTLIST(FMT) #endif -#define IM_ARRAYSIZE(_ARR) ((int)(sizeof(_ARR)/sizeof(*_ARR))) // Size of a static C-style array. Don't use on pointers! -#define IM_UNUSED(_VAR) ((void)_VAR) // Used to silence "unused variable warnings". Often useful as asserts may be stripped out from final builds. -#if (__cplusplus >= 201100) -#define IM_OFFSETOF(_TYPE,_MEMBER) offsetof(_TYPE, _MEMBER) // Offset of _MEMBER within _TYPE. Standardized as offsetof() in C++11 + +// Disable some of MSVC most aggressive Debug runtime checks in function header/footer (used in some simple/low-level functions) +#if defined(_MSC_VER) && !defined(__clang__) && !defined(__INTEL_COMPILER) && !defined(IMGUI_DEBUG_PARANOID) +#define IM_MSVC_RUNTIME_CHECKS_OFF __pragma(runtime_checks("",off)) __pragma(check_stack(off)) __pragma(strict_gs_check(push,off)) +#define IM_MSVC_RUNTIME_CHECKS_RESTORE __pragma(runtime_checks("",restore)) __pragma(check_stack()) __pragma(strict_gs_check(pop)) #else -#define IM_OFFSETOF(_TYPE,_MEMBER) ((size_t)&(((_TYPE*)0)->_MEMBER)) // Offset of _MEMBER within _TYPE. Old style macro. +#define IM_MSVC_RUNTIME_CHECKS_OFF +#define IM_MSVC_RUNTIME_CHECKS_RESTORE #endif // Warnings +#ifdef _MSC_VER +#pragma warning (push) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#endif #if defined(__clang__) #pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wold-style-cast" -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' #endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type #elif defined(__GNUC__) #pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind -#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead #endif //----------------------------------------------------------------------------- -// Forward declarations and basic types +// [SECTION] Forward declarations and basic types //----------------------------------------------------------------------------- +// Scalar data types +typedef unsigned int ImGuiID;// A unique ID used by widgets (typically the result of hashing a stack of string) +typedef signed char ImS8; // 8-bit signed integer +typedef unsigned char ImU8; // 8-bit unsigned integer +typedef signed short ImS16; // 16-bit signed integer +typedef unsigned short ImU16; // 16-bit unsigned integer +typedef signed int ImS32; // 32-bit signed integer == int +typedef unsigned int ImU32; // 32-bit unsigned integer (often used to store packed colors) +typedef signed long long ImS64; // 64-bit signed integer +typedef unsigned long long ImU64; // 64-bit unsigned integer + +// Forward declarations: ImDrawList, ImFontAtlas layer struct ImDrawChannel; // Temporary storage to output draw commands out of order, used by ImDrawListSplitter and ImDrawList::ChannelsSplit() struct ImDrawCmd; // A single draw command within a parent ImDrawList (generally maps to 1 GPU draw call, unless it is a callback) struct ImDrawData; // All draw command lists required to render the frame + pos/size coordinates to use for the projection matrix. @@ -106,205 +171,276 @@ struct ImDrawListSplitter; // Helper to split a draw list into differen struct ImDrawVert; // A single vertex (pos + uv + col = 20 bytes by default. Override layout with IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT) struct ImFont; // Runtime data for a single font within a parent ImFontAtlas struct ImFontAtlas; // Runtime data for multiple fonts, bake multiple fonts into a single texture, TTF/OTF font loader +struct ImFontBuilderIO; // Opaque interface to a font builder (stb_truetype or FreeType). struct ImFontConfig; // Configuration data when adding a font or merging fonts struct ImFontGlyph; // A single font glyph (code point + coordinates within in ImFontAtlas + offset) struct ImFontGlyphRangesBuilder; // Helper to build glyph ranges from text/string data struct ImColor; // Helper functions to create a color that can be converted to either u32 or float4 (*OBSOLETE* please avoid using) -struct ImGuiContext; // Dear ImGui context (opaque structure, unless including imgui_internal.h) -struct ImGuiIO; // Main configuration and I/O between your application and ImGui -struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) -struct ImGuiListClipper; // Helper to manually clip large list of items -struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame, used by IMGUI_ONCE_UPON_A_FRAME macro -struct ImGuiPayload; // User data payload for drag and drop operations -struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) -struct ImGuiStorage; // Helper for key->value storage -struct ImGuiStyle; // Runtime data for styling/colors -struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder) -struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbb][,ccccc]") -// Typedefs and Enums/Flags (declared as int for compatibility with old C++, to allow using as flags and to not pollute the top of this file) -// Use your programming IDE "Go to definition" facility on the names of the center columns to find the actual flags/enum lists. -#ifndef ImTextureID -typedef void* ImTextureID; // User data to identify a texture (this is whatever to you want it to be! read the FAQ about ImTextureID in imgui.cpp) -#endif -typedef unsigned int ImGuiID; // Unique ID used by widgets (typically hashed from a stack of string) -typedef unsigned short ImWchar; // A single U16 character for keyboard input/display. We encode them as multi bytes UTF-8 when used in strings. +// Forward declarations: ImGui layer +struct ImGuiContext; // Dear ImGui context (opaque structure, unless including imgui_internal.h) +struct ImGuiIO; // Main configuration and I/O between your application and ImGui (also see: ImGuiPlatformIO) +struct ImGuiInputTextCallbackData; // Shared state of InputText() when using custom ImGuiInputTextCallback (rare/advanced use) +struct ImGuiKeyData; // Storage for ImGuiIO and IsKeyDown(), IsKeyPressed() etc functions. +struct ImGuiListClipper; // Helper to manually clip large list of items +struct ImGuiMultiSelectIO; // Structure to interact with a BeginMultiSelect()/EndMultiSelect() block +struct ImGuiOnceUponAFrame; // Helper for running a block of code not more than once a frame +struct ImGuiPayload; // User data payload for drag and drop operations +struct ImGuiPlatformIO; // Interface between platform/renderer backends and ImGui (e.g. Clipboard, IME, Multi-Viewport support). Extends ImGuiIO. +struct ImGuiPlatformImeData; // Platform IME data for io.PlatformSetImeDataFn() function. +struct ImGuiPlatformMonitor; // Multi-viewport support: user-provided bounds for each connected monitor/display. Used when positioning popups and tooltips to avoid them straddling monitors +struct ImGuiSelectionBasicStorage; // Optional helper to store multi-selection state + apply multi-selection requests. +struct ImGuiSelectionExternalStorage;//Optional helper to apply multi-selection requests to existing randomly accessible storage. +struct ImGuiSelectionRequest; // A selection request (stored in ImGuiMultiSelectIO) +struct ImGuiSizeCallbackData; // Callback data when using SetNextWindowSizeConstraints() (rare/advanced use) +struct ImGuiStorage; // Helper for key->value storage (container sorted by key) +struct ImGuiStoragePair; // Helper for key->value storage (pair) +struct ImGuiStyle; // Runtime data for styling/colors +struct ImGuiTableSortSpecs; // Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +struct ImGuiTableColumnSortSpecs; // Sorting specification for one column of a table +struct ImGuiTextBuffer; // Helper to hold and append into a text buffer (~string builder) +struct ImGuiTextFilter; // Helper to parse and apply text filters (e.g. "aaaaa[,bbbbb][,ccccc]") +struct ImGuiViewport; // A Platform Window (always 1 unless multi-viewport are enabled. One per platform window to output to). In the future may represent Platform Monitor +struct ImGuiWindowClass; // Window class (rare/advanced uses: provide hints to the platform backend via altered viewport flags and parent/child info) + +// Enumerations +// - We don't use strongly typed enums much because they add constraints (can't extend in private code, can't store typed in bit fields, extra casting on iteration) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +enum ImGuiDir : int; // -> enum ImGuiDir // Enum: A cardinal direction (Left, Right, Up, Down) +enum ImGuiKey : int; // -> enum ImGuiKey // Enum: A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value) +enum ImGuiMouseSource : int; // -> enum ImGuiMouseSource // Enum; A mouse input source identifier (Mouse, TouchScreen, Pen) +enum ImGuiSortDirection : ImU8; // -> enum ImGuiSortDirection // Enum: A sorting direction (ascending or descending) typedef int ImGuiCol; // -> enum ImGuiCol_ // Enum: A color identifier for styling typedef int ImGuiCond; // -> enum ImGuiCond_ // Enum: A condition for many Set*() functions typedef int ImGuiDataType; // -> enum ImGuiDataType_ // Enum: A primary data type -typedef int ImGuiDir; // -> enum ImGuiDir_ // Enum: A cardinal direction -typedef int ImGuiKey; // -> enum ImGuiKey_ // Enum: A key identifier (ImGui-side enum) -typedef int ImGuiNavInput; // -> enum ImGuiNavInput_ // Enum: An input identifier for navigation -typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor identifier +typedef int ImGuiMouseButton; // -> enum ImGuiMouseButton_ // Enum: A mouse button identifier (0=left, 1=right, 2=middle) +typedef int ImGuiMouseCursor; // -> enum ImGuiMouseCursor_ // Enum: A mouse cursor shape typedef int ImGuiStyleVar; // -> enum ImGuiStyleVar_ // Enum: A variable identifier for styling -typedef int ImDrawCornerFlags; // -> enum ImDrawCornerFlags_ // Flags: for ImDrawList::AddRect(), AddRectFilled() etc. -typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList -typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas +typedef int ImGuiTableBgTarget; // -> enum ImGuiTableBgTarget_ // Enum: A color target for TableSetBgColor() + +// Flags (declared as int to allow using as flags without overhead, and to not pollute the top of this file) +// - Tip: Use your programming IDE navigation facilities on the names in the _central column_ below to find the actual flags/enum lists! +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +typedef int ImDrawFlags; // -> enum ImDrawFlags_ // Flags: for ImDrawList functions +typedef int ImDrawListFlags; // -> enum ImDrawListFlags_ // Flags: for ImDrawList instance +typedef int ImFontAtlasFlags; // -> enum ImFontAtlasFlags_ // Flags: for ImFontAtlas build typedef int ImGuiBackendFlags; // -> enum ImGuiBackendFlags_ // Flags: for io.BackendFlags +typedef int ImGuiButtonFlags; // -> enum ImGuiButtonFlags_ // Flags: for InvisibleButton() +typedef int ImGuiChildFlags; // -> enum ImGuiChildFlags_ // Flags: for BeginChild() typedef int ImGuiColorEditFlags; // -> enum ImGuiColorEditFlags_ // Flags: for ColorEdit4(), ColorPicker4() etc. typedef int ImGuiConfigFlags; // -> enum ImGuiConfigFlags_ // Flags: for io.ConfigFlags typedef int ImGuiComboFlags; // -> enum ImGuiComboFlags_ // Flags: for BeginCombo() +typedef int ImGuiDockNodeFlags; // -> enum ImGuiDockNodeFlags_ // Flags: for DockSpace() typedef int ImGuiDragDropFlags; // -> enum ImGuiDragDropFlags_ // Flags: for BeginDragDropSource(), AcceptDragDropPayload() typedef int ImGuiFocusedFlags; // -> enum ImGuiFocusedFlags_ // Flags: for IsWindowFocused() typedef int ImGuiHoveredFlags; // -> enum ImGuiHoveredFlags_ // Flags: for IsItemHovered(), IsWindowHovered() etc. +typedef int ImGuiInputFlags; // -> enum ImGuiInputFlags_ // Flags: for Shortcut(), SetNextItemShortcut() typedef int ImGuiInputTextFlags; // -> enum ImGuiInputTextFlags_ // Flags: for InputText(), InputTextMultiline() +typedef int ImGuiItemFlags; // -> enum ImGuiItemFlags_ // Flags: for PushItemFlag(), shared by all items +typedef int ImGuiKeyChord; // -> ImGuiKey | ImGuiMod_XXX // Flags: for IsKeyChordPressed(), Shortcut() etc. an ImGuiKey optionally OR-ed with one or more ImGuiMod_XXX values. +typedef int ImGuiPopupFlags; // -> enum ImGuiPopupFlags_ // Flags: for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() +typedef int ImGuiMultiSelectFlags; // -> enum ImGuiMultiSelectFlags_// Flags: for BeginMultiSelect() typedef int ImGuiSelectableFlags; // -> enum ImGuiSelectableFlags_ // Flags: for Selectable() +typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. typedef int ImGuiTabBarFlags; // -> enum ImGuiTabBarFlags_ // Flags: for BeginTabBar() typedef int ImGuiTabItemFlags; // -> enum ImGuiTabItemFlags_ // Flags: for BeginTabItem() +typedef int ImGuiTableFlags; // -> enum ImGuiTableFlags_ // Flags: For BeginTable() +typedef int ImGuiTableColumnFlags; // -> enum ImGuiTableColumnFlags_// Flags: For TableSetupColumn() +typedef int ImGuiTableRowFlags; // -> enum ImGuiTableRowFlags_ // Flags: For TableNextRow() typedef int ImGuiTreeNodeFlags; // -> enum ImGuiTreeNodeFlags_ // Flags: for TreeNode(), TreeNodeEx(), CollapsingHeader() +typedef int ImGuiViewportFlags; // -> enum ImGuiViewportFlags_ // Flags: for ImGuiViewport typedef int ImGuiWindowFlags; // -> enum ImGuiWindowFlags_ // Flags: for Begin(), BeginChild() -typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData *data); -typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); -// Scalar data types -typedef signed char ImS8; // 8-bit signed integer == char -typedef unsigned char ImU8; // 8-bit unsigned integer -typedef signed short ImS16; // 16-bit signed integer -typedef unsigned short ImU16; // 16-bit unsigned integer -typedef signed int ImS32; // 32-bit signed integer == int -typedef unsigned int ImU32; // 32-bit unsigned integer (often used to store packed colors) -#if defined(_MSC_VER) && !defined(__clang__) -typedef signed __int64 ImS64; // 64-bit signed integer (pre and post C++11 with Visual Studio) -typedef unsigned __int64 ImU64; // 64-bit unsigned integer (pre and post C++11 with Visual Studio) -#elif (defined(__clang__) || defined(__GNUC__)) && (__cplusplus < 201100) -#include -typedef int64_t ImS64; // 64-bit signed integer (pre C++11) -typedef uint64_t ImU64; // 64-bit unsigned integer (pre C++11) +// Character types +// (we generally use UTF-8 encoded string in the API. This is storage specifically for a decoded character used for keyboard input and display) +typedef unsigned int ImWchar32; // A single decoded U32 character/code point. We encode them as multi bytes UTF-8 when used in strings. +typedef unsigned short ImWchar16; // A single decoded U16 character/code point. We encode them as multi bytes UTF-8 when used in strings. +#ifdef IMGUI_USE_WCHAR32 // ImWchar [configurable type: override in imconfig.h with '#define IMGUI_USE_WCHAR32' to support Unicode planes 1-16] +typedef ImWchar32 ImWchar; #else -typedef signed long long ImS64; // 64-bit signed integer (post C++11) -typedef unsigned long long ImU64; // 64-bit unsigned integer (post C++11) +typedef ImWchar16 ImWchar; #endif -// 2D vector (often used to store positions, sizes, etc.) +// Multi-Selection item index or identifier when using BeginMultiSelect() +// - Used by SetNextItemSelectionUserData() + and inside ImGuiMultiSelectIO structure. +// - Most users are likely to use this store an item INDEX but this may be used to store a POINTER/ID as well. Read comments near ImGuiMultiSelectIO for details. +typedef ImS64 ImGuiSelectionUserData; + +// Callback and functions types +typedef int (*ImGuiInputTextCallback)(ImGuiInputTextCallbackData* data); // Callback function for ImGui::InputText() +typedef void (*ImGuiSizeCallback)(ImGuiSizeCallbackData* data); // Callback function for ImGui::SetNextWindowSizeConstraints() +typedef void* (*ImGuiMemAllocFunc)(size_t sz, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() +typedef void (*ImGuiMemFreeFunc)(void* ptr, void* user_data); // Function signature for ImGui::SetAllocatorFunctions() + +// ImVec2: 2D vector used to store positions, sizes etc. [Compile-time configurable type] +// - This is a frequently used type in the API. Consider using IM_VEC2_CLASS_EXTRA to create implicit cast from/to our preferred type. +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. +IM_MSVC_RUNTIME_CHECKS_OFF struct ImVec2 { - float x, y; - ImVec2() { x = y = 0.0f; } - ImVec2(float _x, float _y) { x = _x; y = _y; } - float operator[] (size_t idx) const { IM_ASSERT(idx <= 1); return (&x)[idx]; } // We very rarely use this [] operator, the assert overhead is fine. - float& operator[] (size_t idx) { IM_ASSERT(idx <= 1); return (&x)[idx]; } // We very rarely use this [] operator, the assert overhead is fine. + float x, y; + constexpr ImVec2() : x(0.0f), y(0.0f) { } + constexpr ImVec2(float _x, float _y) : x(_x), y(_y) { } + float& operator[] (size_t idx) { IM_ASSERT(idx == 0 || idx == 1); return ((float*)(void*)(char*)this)[idx]; } // We very rarely use this [] operator, so the assert overhead is fine. + float operator[] (size_t idx) const { IM_ASSERT(idx == 0 || idx == 1); return ((const float*)(const void*)(const char*)this)[idx]; } #ifdef IM_VEC2_CLASS_EXTRA IM_VEC2_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec2. #endif }; -// 4D vector (often used to store floating-point colors) +// ImVec4: 4D vector used to store clipping rectangles, colors etc. [Compile-time configurable type] struct ImVec4 { - float x, y, z, w; - ImVec4() { x = y = z = w = 0.0f; } - ImVec4(float _x, float _y, float _z, float _w) { x = _x; y = _y; z = _z; w = _w; } + float x, y, z, w; + constexpr ImVec4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) { } + constexpr ImVec4(float _x, float _y, float _z, float _w) : x(_x), y(_y), z(_z), w(_w) { } #ifdef IM_VEC4_CLASS_EXTRA IM_VEC4_CLASS_EXTRA // Define additional constructors and implicit cast operators in imconfig.h to convert back and forth between your math types and ImVec4. #endif }; +IM_MSVC_RUNTIME_CHECKS_RESTORE //----------------------------------------------------------------------------- -// ImGui: Dear ImGui end-user API -// (Inside a namespace so you can add extra functions in your own separate file. Please don't modify imgui.cpp/.h!) +// [SECTION] Texture identifier (ImTextureID) +//----------------------------------------------------------------------------- + +// ImTexture: user data for renderer backend to identify a texture [Compile-time configurable type] +// - To use something else than an opaque void* pointer: override with e.g. '#define ImTextureID MyTextureType*' in your imconfig.h file. +// - This can be whatever to you want it to be! read the FAQ about ImTextureID for details. +// - You can make this a structure with various constructors if you need. You will have to implement ==/!= operators. +// - (note: before v1.91.4 (2024/10/08) the default type for ImTextureID was void*. Use intermediary intptr_t cast and read FAQ if you have casting warnings) +#ifndef ImTextureID +typedef ImU64 ImTextureID; // Default: store a pointer or an integer fitting in a pointer (most renderer backends are ok with that) +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Dear ImGui end-user API functions +// (Note that ImGui:: being a namespace, you can add extra ImGui:: functions in your own separate file. Please don't modify imgui source files!) //----------------------------------------------------------------------------- namespace ImGui { // Context creation and access - // Each context create its own ImFontAtlas by default. You may instance one yourself and pass it to CreateContext() to share a font atlas between imgui contexts. - // All those functions are not reliant on the current context. + // - Each context create its own ImFontAtlas by default. You may instance one yourself and pass it to CreateContext() to share a font atlas between contexts. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for details. IMGUI_API ImGuiContext* CreateContext(ImFontAtlas* shared_font_atlas = NULL); IMGUI_API void DestroyContext(ImGuiContext* ctx = NULL); // NULL = destroy current context IMGUI_API ImGuiContext* GetCurrentContext(); IMGUI_API void SetCurrentContext(ImGuiContext* ctx); - IMGUI_API bool DebugCheckVersionAndDataLayout(const char* version_str, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_drawvert, size_t sz_drawidx); // Main - IMGUI_API ImGuiIO& GetIO(); // access the IO structure (mouse/keyboard/gamepad inputs, time, various configuration options/flags) - IMGUI_API ImGuiStyle& GetStyle(); // access the Style structure (colors, sizes). Always use PushStyleCol(), PushStyleVar() to modify style mid-frame. + IMGUI_API ImGuiIO& GetIO(); // access the ImGuiIO structure (mouse/keyboard/gamepad inputs, time, various configuration options/flags) + IMGUI_API ImGuiPlatformIO& GetPlatformIO(); // access the ImGuiPlatformIO structure (mostly hooks/functions to connect to platform/renderer and OS Clipboard, IME etc.) + IMGUI_API ImGuiStyle& GetStyle(); // access the Style structure (colors, sizes). Always use PushStyleColor(), PushStyleVar() to modify style mid-frame! IMGUI_API void NewFrame(); // start a new Dear ImGui frame, you can submit any command from this point until Render()/EndFrame(). - IMGUI_API void EndFrame(); // ends the Dear ImGui frame. automatically called by Render(), you likely don't need to call that yourself directly. If you don't need to render data (skipping rendering) you may call EndFrame() but you'll have wasted CPU already! If you don't need to render, better to not create any imgui windows and not call NewFrame() at all! - IMGUI_API void Render(); // ends the Dear ImGui frame, finalize the draw data. You can get call GetDrawData() to obtain it and run your rendering function. (Obsolete: this used to call io.RenderDrawListsFn(). Nowadays, we allow and prefer calling your render function yourself.) + IMGUI_API void EndFrame(); // ends the Dear ImGui frame. automatically called by Render(). If you don't need to render data (skipping rendering) you may call EndFrame() without Render()... but you'll have wasted CPU already! If you don't need to render, better to not create any windows and not call NewFrame() at all! + IMGUI_API void Render(); // ends the Dear ImGui frame, finalize the draw data. You can then get call GetDrawData(). IMGUI_API ImDrawData* GetDrawData(); // valid after Render() and until the next call to NewFrame(). this is what you have to render. // Demo, Debug, Information - IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create Demo window (previously called ShowTestWindow). demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! + IMGUI_API void ShowDemoWindow(bool* p_open = NULL); // create Demo window. demonstrate most ImGui features. call this to learn about the library! try to make it always available in your application! + IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create Metrics/Debugger window. display Dear ImGui internals: windows, draw commands, various internal state, etc. + IMGUI_API void ShowDebugLogWindow(bool* p_open = NULL); // create Debug Log window. display a simplified log of important dear imgui events. + IMGUI_API void ShowIDStackToolWindow(bool* p_open = NULL); // create Stack Tool window. hover items with mouse to query information about the source of their unique ID. IMGUI_API void ShowAboutWindow(bool* p_open = NULL); // create About window. display Dear ImGui version, credits and build/system information. - IMGUI_API void ShowMetricsWindow(bool* p_open = NULL); // create Metrics/Debug window. display Dear ImGui internals: draw commands (with individual draw calls and vertices), window list, basic internal state, etc. IMGUI_API void ShowStyleEditor(ImGuiStyle* ref = NULL); // add style editor block (not a window). you can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it uses the default style) IMGUI_API bool ShowStyleSelector(const char* label); // add style selector block (not a window), essentially a combo listing the default styles. IMGUI_API void ShowFontSelector(const char* label); // add font selector block (not a window), essentially a combo listing the loaded fonts. - IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as a end-user (mouse/keyboard controls). - IMGUI_API const char* GetVersion(); // get the compiled version string e.g. "1.23" (essentially the compiled value for IMGUI_VERSION) + IMGUI_API void ShowUserGuide(); // add basic help/info block (not a window): how to manipulate ImGui as an end-user (mouse/keyboard controls). + IMGUI_API const char* GetVersion(); // get the compiled version string e.g. "1.80 WIP" (essentially the value for IMGUI_VERSION from the compiled version of imgui.cpp) // Styles IMGUI_API void StyleColorsDark(ImGuiStyle* dst = NULL); // new, recommended style (default) - IMGUI_API void StyleColorsClassic(ImGuiStyle* dst = NULL); // classic imgui style IMGUI_API void StyleColorsLight(ImGuiStyle* dst = NULL); // best used with borders and a custom, thicker font + IMGUI_API void StyleColorsClassic(ImGuiStyle* dst = NULL); // classic imgui style // Windows // - Begin() = push window to the stack and start appending to it. End() = pop window from the stack. - // - You may append multiple times to the same window during the same frame. // - Passing 'bool* p_open != NULL' shows a window-closing widget in the upper-right corner of the window, // which clicking will set the boolean to false when clicked. + // - You may append multiple times to the same window during the same frame by calling Begin()/End() pairs multiple times. + // Some information such as 'flags' or 'p_open' will only be considered by the first call to Begin(). // - Begin() return false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting // anything to the window. Always call a matching End() for each Begin() call, regardless of its return value! - // [this is due to legacy reason and is inconsistent with most other functions such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. - // where the EndXXX call should only be called if the corresponding BeginXXX function returned true.] + // [Important: due to legacy reason, Begin/End and BeginChild/EndChild are inconsistent with all other functions + // such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding + // BeginXXX function returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] // - Note that the bottom of window stack always contains a window called "Debug". IMGUI_API bool Begin(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); IMGUI_API void End(); // Child Windows // - Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window. Child windows can embed their own child. - // - For each independent axis of 'size': ==0.0f: use remaining host window size / >0.0f: fixed size / <0.0f: use remaining window size minus abs(size) / Each axis can use a different mode, e.g. ImVec2(0,400). - // - BeginChild() returns false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting anything to the window. - // Always call a matching EndChild() for each BeginChild() call, regardless of its return value [this is due to legacy reason and is inconsistent with most other functions such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding BeginXXX function returned true.] - IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0,0), bool border = false, ImGuiWindowFlags flags = 0); - IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0,0), bool border = false, ImGuiWindowFlags flags = 0); + // - Before 1.90 (November 2023), the "ImGuiChildFlags child_flags = 0" parameter was "bool border = false". + // This API is backward compatible with old code, as we guarantee that ImGuiChildFlags_Borders == true. + // Consider updating your old code: + // BeginChild("Name", size, false) -> Begin("Name", size, 0); or Begin("Name", size, ImGuiChildFlags_None); + // BeginChild("Name", size, true) -> Begin("Name", size, ImGuiChildFlags_Borders); + // - Manual sizing (each axis can use a different setting e.g. ImVec2(0.0f, 400.0f)): + // == 0.0f: use remaining parent window size for this axis. + // > 0.0f: use specified size for this axis. + // < 0.0f: right/bottom-align to specified distance from available content boundaries. + // - Specifying ImGuiChildFlags_AutoResizeX or ImGuiChildFlags_AutoResizeY makes the sizing automatic based on child contents. + // Combining both ImGuiChildFlags_AutoResizeX _and_ ImGuiChildFlags_AutoResizeY defeats purpose of a scrolling region and is NOT recommended. + // - BeginChild() returns false to indicate the window is collapsed or fully clipped, so you may early out and omit submitting + // anything to the window. Always call a matching EndChild() for each BeginChild() call, regardless of its return value. + // [Important: due to legacy reason, Begin/End and BeginChild/EndChild are inconsistent with all other functions + // such as BeginMenu/EndMenu, BeginPopup/EndPopup, etc. where the EndXXX call should only be called if the corresponding + // BeginXXX function returned true. Begin and BeginChild are the only odd ones out. Will be fixed in a future update.] + IMGUI_API bool BeginChild(const char* str_id, const ImVec2& size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); + IMGUI_API bool BeginChild(ImGuiID id, const ImVec2& size = ImVec2(0, 0), ImGuiChildFlags child_flags = 0, ImGuiWindowFlags window_flags = 0); IMGUI_API void EndChild(); // Windows Utilities - // - "current window" = the window we are appending into while inside a Begin()/End() block. "next window" = next window we will Begin() into. + // - 'current window' = the window we are appending into while inside a Begin()/End() block. 'next window' = next window we will Begin() into. IMGUI_API bool IsWindowAppearing(); IMGUI_API bool IsWindowCollapsed(); IMGUI_API bool IsWindowFocused(ImGuiFocusedFlags flags=0); // is current window focused? or its root/child, depending on flags. see flags for options. - IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags=0); // is current window hovered (and typically: not blocked by a popup/modal)? see flags for options. NB: If you are trying to check whether your mouse should be dispatched to imgui or to your app, you should use the 'io.WantCaptureMouse' boolean for that! Please read the FAQ! + IMGUI_API bool IsWindowHovered(ImGuiHoveredFlags flags=0); // is current window hovered and hoverable (e.g. not blocked by a popup/modal)? See ImGuiHoveredFlags_ for options. IMPORTANT: If you are trying to check whether your mouse should be dispatched to Dear ImGui or to your underlying app, you should not use this function! Use the 'io.WantCaptureMouse' boolean for that! Refer to FAQ entry "How can I tell whether to dispatch mouse/keyboard to Dear ImGui or my application?" for details. IMGUI_API ImDrawList* GetWindowDrawList(); // get draw list associated to the current window, to append your own drawing primitives - IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (useful if you want to do your own drawing via the DrawList API) - IMGUI_API ImVec2 GetWindowSize(); // get current window size - IMGUI_API float GetWindowWidth(); // get current window width (shortcut for GetWindowSize().x) - IMGUI_API float GetWindowHeight(); // get current window height (shortcut for GetWindowSize().y) + IMGUI_API float GetWindowDpiScale(); // get DPI scale currently associated to the current window's viewport. + IMGUI_API ImVec2 GetWindowPos(); // get current window position in screen space (IT IS UNLIKELY YOU EVER NEED TO USE THIS. Consider always using GetCursorScreenPos() and GetContentRegionAvail() instead) + IMGUI_API ImVec2 GetWindowSize(); // get current window size (IT IS UNLIKELY YOU EVER NEED TO USE THIS. Consider always using GetCursorScreenPos() and GetContentRegionAvail() instead) + IMGUI_API float GetWindowWidth(); // get current window width (IT IS UNLIKELY YOU EVER NEED TO USE THIS). Shortcut for GetWindowSize().x. + IMGUI_API float GetWindowHeight(); // get current window height (IT IS UNLIKELY YOU EVER NEED TO USE THIS). Shortcut for GetWindowSize().y. + IMGUI_API ImGuiViewport*GetWindowViewport(); // get viewport currently associated to the current window. - // Prefer using SetNextXXX functions (before Begin) rather that SetXXX functions (after Begin). - IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0,0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. + // Window manipulation + // - Prefer using SetNextXXX functions (before Begin) rather that SetXXX functions (after Begin). + IMGUI_API void SetNextWindowPos(const ImVec2& pos, ImGuiCond cond = 0, const ImVec2& pivot = ImVec2(0, 0)); // set next window position. call before Begin(). use pivot=(0.5f,0.5f) to center on given point, etc. IMGUI_API void SetNextWindowSize(const ImVec2& size, ImGuiCond cond = 0); // set next window size. set axis to 0.0f to force an auto-fit on this axis. call before Begin() - IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use -1,-1 on either X/Y axis to preserve the current size. Sizes will be rounded down. Use callback to apply non-trivial programmatic constraints. + IMGUI_API void SetNextWindowSizeConstraints(const ImVec2& size_min, const ImVec2& size_max, ImGuiSizeCallback custom_callback = NULL, void* custom_callback_data = NULL); // set next window size limits. use 0.0f or FLT_MAX if you don't want limits. Use -1 for both min and max of same axis to preserve current size (which itself is a constraint). Use callback to apply non-trivial programmatic constraints. IMGUI_API void SetNextWindowContentSize(const ImVec2& size); // set next window content size (~ scrollable client area, which enforce the range of scrollbars). Not including window decorations (title bar, menu bar, etc.) nor WindowPadding. set an axis to 0.0f to leave it automatic. call before Begin() IMGUI_API void SetNextWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // set next window collapsed state. call before Begin() IMGUI_API void SetNextWindowFocus(); // set next window to be focused / top-most. call before Begin() - IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily modify ImGuiCol_WindowBg/ChildBg/PopupBg. you may also use ImGuiWindowFlags_NoBackground. + IMGUI_API void SetNextWindowScroll(const ImVec2& scroll); // set next window scrolling value (use < 0.0f to not affect a given axis). + IMGUI_API void SetNextWindowBgAlpha(float alpha); // set next window background color alpha. helper to easily override the Alpha component of ImGuiCol_WindowBg/ChildBg/PopupBg. you may also use ImGuiWindowFlags_NoBackground. + IMGUI_API void SetNextWindowViewport(ImGuiID viewport_id); // set next window viewport IMGUI_API void SetWindowPos(const ImVec2& pos, ImGuiCond cond = 0); // (not recommended) set current window position - call within Begin()/End(). prefer using SetNextWindowPos(), as this may incur tearing and side-effects. - IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0,0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. + IMGUI_API void SetWindowSize(const ImVec2& size, ImGuiCond cond = 0); // (not recommended) set current window size - call within Begin()/End(). set to ImVec2(0, 0) to force an auto-fit. prefer using SetNextWindowSize(), as this may incur tearing and minor side-effects. IMGUI_API void SetWindowCollapsed(bool collapsed, ImGuiCond cond = 0); // (not recommended) set current window collapsed state. prefer using SetNextWindowCollapsed(). IMGUI_API void SetWindowFocus(); // (not recommended) set current window to be focused / top-most. prefer using SetNextWindowFocus(). - IMGUI_API void SetWindowFontScale(float scale); // set font scale. Adjust IO.FontGlobalScale if you want to scale all windows. This is an old API! For correct scaling, prefer to reload font + rebuild ImFontAtlas + call style.ScaleAllSizes(). + IMGUI_API void SetWindowFontScale(float scale); // [OBSOLETE] set font scale. Adjust IO.FontGlobalScale if you want to scale all windows. This is an old API! For correct scaling, prefer to reload font + rebuild ImFontAtlas + call style.ScaleAllSizes(). IMGUI_API void SetWindowPos(const char* name, const ImVec2& pos, ImGuiCond cond = 0); // set named window position. IMGUI_API void SetWindowSize(const char* name, const ImVec2& size, ImGuiCond cond = 0); // set named window size. set axis to 0.0f to force an auto-fit on this axis. IMGUI_API void SetWindowCollapsed(const char* name, bool collapsed, ImGuiCond cond = 0); // set named window collapsed state IMGUI_API void SetWindowFocus(const char* name); // set named window to be focused / top-most. use NULL to remove focus. - // Content region - // - Those functions are bound to be redesigned soon (they are confusing, incomplete and return values in local window coordinates which increases confusion) - IMGUI_API ImVec2 GetContentRegionMax(); // current content boundaries (typically window boundaries including scrolling, or current column boundaries), in windows coordinates - IMGUI_API ImVec2 GetContentRegionAvail(); // == GetContentRegionMax() - GetCursorPos() - IMGUI_API ImVec2 GetWindowContentRegionMin(); // content boundaries min (roughly (0,0)-Scroll), in window coordinates - IMGUI_API ImVec2 GetWindowContentRegionMax(); // content boundaries max (roughly (0,0)+Size-Scroll) where Size can be override with SetNextWindowContentSize(), in window coordinates - IMGUI_API float GetWindowContentRegionWidth(); // - // Windows Scrolling - IMGUI_API float GetScrollX(); // get scrolling amount [0..GetScrollMaxX()] - IMGUI_API float GetScrollY(); // get scrolling amount [0..GetScrollMaxY()] - IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.X - WindowSize.X - IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.Y - WindowSize.Y - IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0..GetScrollMaxX()] - IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0..GetScrollMaxY()] + // - Any change of Scroll will be applied at the beginning of next frame in the first call to Begin(). + // - You may instead use SetNextWindowScroll() prior to calling Begin() to avoid this delay, as an alternative to using SetScrollX()/SetScrollY(). + IMGUI_API float GetScrollX(); // get scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API float GetScrollY(); // get scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API void SetScrollX(float scroll_x); // set scrolling amount [0 .. GetScrollMaxX()] + IMGUI_API void SetScrollY(float scroll_y); // set scrolling amount [0 .. GetScrollMaxY()] + IMGUI_API float GetScrollMaxX(); // get maximum scrolling amount ~~ ContentSize.x - WindowSize.x - DecorationsSize.x + IMGUI_API float GetScrollMaxY(); // get maximum scrolling amount ~~ ContentSize.y - WindowSize.y - DecorationsSize.y IMGUI_API void SetScrollHereX(float center_x_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_x_ratio=0.0: left, 0.5: center, 1.0: right. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. IMGUI_API void SetScrollHereY(float center_y_ratio = 0.5f); // adjust scrolling amount to make current cursor position visible. center_y_ratio=0.0: top, 0.5: center, 1.0: bottom. When using to make a "default/current item" visible, consider using SetItemDefaultFocus() instead. IMGUI_API void SetScrollFromPosX(float local_x, float center_x_ratio = 0.5f); // adjust scrolling amount to make given position visible. Generally GetCursorStartPos() + offset to compute a valid position. @@ -313,53 +449,66 @@ namespace ImGui // Parameters stacks (shared) IMGUI_API void PushFont(ImFont* font); // use NULL as a shortcut to push default font IMGUI_API void PopFont(); - IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); + IMGUI_API void PushStyleColor(ImGuiCol idx, ImU32 col); // modify a style color. always use this if you modify the style after NewFrame(). IMGUI_API void PushStyleColor(ImGuiCol idx, const ImVec4& col); IMGUI_API void PopStyleColor(int count = 1); - IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); - IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val); + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, float val); // modify a style float variable. always use this if you modify the style after NewFrame()! + IMGUI_API void PushStyleVar(ImGuiStyleVar idx, const ImVec2& val); // modify a style ImVec2 variable. " + IMGUI_API void PushStyleVarX(ImGuiStyleVar idx, float val_x); // modify X component of a style ImVec2 variable. " + IMGUI_API void PushStyleVarY(ImGuiStyleVar idx, float val_y); // modify Y component of a style ImVec2 variable. " IMGUI_API void PopStyleVar(int count = 1); - IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwise use GetColorU32() to get style color with style alpha baked in. - IMGUI_API ImFont* GetFont(); // get current font - IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied - IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a while pixel, useful to draw custom shapes via the ImDrawList API - IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier - IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied - IMGUI_API ImU32 GetColorU32(ImU32 col); // retrieve given color with style alpha applied + IMGUI_API void PushItemFlag(ImGuiItemFlags option, bool enabled); // modify specified shared item flag, e.g. PushItemFlag(ImGuiItemFlags_NoTabStop, true) + IMGUI_API void PopItemFlag(); // Parameters stacks (current window) - IMGUI_API void PushItemWidth(float item_width); // set width of items for common large "item+label" widgets. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -1.0f always align width to the right side). 0.0f = default to ~2/3 of windows width, + IMGUI_API void PushItemWidth(float item_width); // push width of items for common large "item+label" widgets. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side). IMGUI_API void PopItemWidth(); - IMGUI_API void SetNextItemWidth(float item_width); // set width of the _next_ common large "item+label" widget. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -1.0f always align width to the right side) + IMGUI_API void SetNextItemWidth(float item_width); // set width of the _next_ common large "item+label" widget. >0.0f: width in pixels, <0.0f align xx pixels to the right of window (so -FLT_MIN always align width to the right side) IMGUI_API float CalcItemWidth(); // width of item given pushed settings and current cursor position. NOT necessarily the width of last item unlike most 'Item' functions. - IMGUI_API void PushTextWrapPos(float wrap_local_pos_x = 0.0f); // word-wrapping for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space + IMGUI_API void PushTextWrapPos(float wrap_local_pos_x = 0.0f); // push word-wrapping position for Text*() commands. < 0.0f: no wrapping; 0.0f: wrap to end of window (or column); > 0.0f: wrap at 'wrap_pos_x' position in window local space IMGUI_API void PopTextWrapPos(); - IMGUI_API void PushAllowKeyboardFocus(bool allow_keyboard_focus); // allow focusing using TAB/Shift-TAB, enabled by default but you can disable it for certain widgets - IMGUI_API void PopAllowKeyboardFocus(); - IMGUI_API void PushButtonRepeat(bool repeat); // in 'repeat' mode, Button*() functions return repeated true in a typematic manner (using io.KeyRepeatDelay/io.KeyRepeatRate setting). Note that you can call IsItemActive() after any Button() to tell if the button is held in the current frame. - IMGUI_API void PopButtonRepeat(); - // Cursor / Layout + // Style read access + // - Use the ShowStyleEditor() function to interactively see/edit the colors. + IMGUI_API ImFont* GetFont(); // get current font + IMGUI_API float GetFontSize(); // get current font size (= height in pixels) of current font with current scale applied + IMGUI_API ImVec2 GetFontTexUvWhitePixel(); // get UV coordinate for a white pixel, useful to draw custom shapes via the ImDrawList API + IMGUI_API ImU32 GetColorU32(ImGuiCol idx, float alpha_mul = 1.0f); // retrieve given style color with style alpha applied and optional extra alpha multiplier, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(const ImVec4& col); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API ImU32 GetColorU32(ImU32 col, float alpha_mul = 1.0f); // retrieve given color with style alpha applied, packed as a 32-bit value suitable for ImDrawList + IMGUI_API const ImVec4& GetStyleColorVec4(ImGuiCol idx); // retrieve style color as stored in ImGuiStyle structure. use to feed back into PushStyleColor(), otherwise use GetColorU32() to get style color with style alpha baked in. + + // Layout cursor positioning // - By "cursor" we mean the current output position. // - The typical widget behavior is to output themselves at the current cursor position, then move the cursor one line down. + // - You can call SameLine() between widgets to undo the last carriage return and output at the right of the preceding widget. + // - YOU CAN DO 99% OF WHAT YOU NEED WITH ONLY GetCursorScreenPos() and GetContentRegionAvail(). + // - Attention! We currently have inconsistencies between window-local and absolute positions we will aim to fix with future API: + // - Absolute coordinate: GetCursorScreenPos(), SetCursorScreenPos(), all ImDrawList:: functions. -> this is the preferred way forward. + // - Window-local coordinates: SameLine(offset), GetCursorPos(), SetCursorPos(), GetCursorStartPos(), PushTextWrapPos() + // - Window-local coordinates: GetContentRegionMax(), GetWindowContentRegionMin(), GetWindowContentRegionMax() --> all obsoleted. YOU DON'T NEED THEM. + // - GetCursorScreenPos() = GetCursorPos() + GetWindowPos(). GetWindowPos() is almost only ever useful to convert from window-local to absolute coordinates. Try not to use it. + IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position, absolute coordinates. THIS IS YOUR BEST FRIEND (prefer using this rather than GetCursorPos(), also more useful to work with ImDrawList API). + IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position, absolute coordinates. THIS IS YOUR BEST FRIEND. + IMGUI_API ImVec2 GetContentRegionAvail(); // available space from current position. THIS IS YOUR BEST FRIEND. + IMGUI_API ImVec2 GetCursorPos(); // [window-local] cursor position in window-local coordinates. This is not your best friend. + IMGUI_API float GetCursorPosX(); // [window-local] " + IMGUI_API float GetCursorPosY(); // [window-local] " + IMGUI_API void SetCursorPos(const ImVec2& local_pos); // [window-local] " + IMGUI_API void SetCursorPosX(float local_x); // [window-local] " + IMGUI_API void SetCursorPosY(float local_y); // [window-local] " + IMGUI_API ImVec2 GetCursorStartPos(); // [window-local] initial cursor position, in window-local coordinates. Call GetCursorScreenPos() after Begin() to get the absolute coordinates version. + + // Other layout functions IMGUI_API void Separator(); // separator, generally horizontal. inside a menu bar or in horizontal layout mode, this becomes a vertical separator. IMGUI_API void SameLine(float offset_from_start_x=0.0f, float spacing=-1.0f); // call between widgets or groups to layout them horizontally. X position given in window coordinates. - IMGUI_API void NewLine(); // undo a SameLine() or force a new line when in an horizontal-layout context. + IMGUI_API void NewLine(); // undo a SameLine() or force a new line when in a horizontal-layout context. IMGUI_API void Spacing(); // add vertical spacing. IMGUI_API void Dummy(const ImVec2& size); // add a dummy item of given size. unlike InvisibleButton(), Dummy() won't take the mouse click or be navigable into. - IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by style.IndentSpacing or indent_w if != 0 - IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by style.IndentSpacing or indent_w if != 0 + IMGUI_API void Indent(float indent_w = 0.0f); // move content position toward the right, by indent_w, or style.IndentSpacing if indent_w <= 0 + IMGUI_API void Unindent(float indent_w = 0.0f); // move content position back to the left, by indent_w, or style.IndentSpacing if indent_w <= 0 IMGUI_API void BeginGroup(); // lock horizontal starting position IMGUI_API void EndGroup(); // unlock horizontal starting position + capture the whole group bounding box into one "item" (so you can use IsItemHovered() or layout primitives such as SameLine() on whole group, etc.) - IMGUI_API ImVec2 GetCursorPos(); // cursor position in window coordinates (relative to window position) - IMGUI_API float GetCursorPosX(); // (some functions are using window-relative coordinates, such as: GetCursorPos, GetCursorStartPos, GetContentRegionMax, GetWindowContentRegion* etc. - IMGUI_API float GetCursorPosY(); // other functions such as GetCursorScreenPos or everything in ImDrawList:: - IMGUI_API void SetCursorPos(const ImVec2& local_pos); // are using the main, absolute coordinate system. - IMGUI_API void SetCursorPosX(float local_x); // GetWindowPos() + GetCursorPos() == GetCursorScreenPos() etc.) - IMGUI_API void SetCursorPosY(float local_y); // - IMGUI_API ImVec2 GetCursorStartPos(); // initial cursor position in window coordinates - IMGUI_API ImVec2 GetCursorScreenPos(); // cursor position in absolute screen coordinates [0..io.DisplaySize] (useful to work with ImDrawList API) - IMGUI_API void SetCursorScreenPos(const ImVec2& pos); // cursor position in absolute screen coordinates [0..io.DisplaySize] IMGUI_API void AlignTextToFramePadding(); // vertically align upcoming text baseline to FramePadding.y so that it will align properly to regularly framed items (call if you have text on a line before a framed item) IMGUI_API float GetTextLineHeight(); // ~ FontSize IMGUI_API float GetTextLineHeightWithSpacing(); // ~ FontSize + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of text) @@ -367,11 +516,15 @@ namespace ImGui IMGUI_API float GetFrameHeightWithSpacing(); // ~ FontSize + style.FramePadding.y * 2 + style.ItemSpacing.y (distance in pixels between 2 consecutive lines of framed widgets) // ID stack/scopes - // - Read the FAQ for more details about how ID are handled in dear imgui. If you are creating widgets in a loop you most - // likely want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. - // - The resulting ID are hashes of the entire stack. + // Read the FAQ (docs/FAQ.md or http://dearimgui.com/faq) for more details about how ID are handled in dear imgui. + // - Those questions are answered and impacted by understanding of the ID stack system: + // - "Q: Why is my widget not reacting when I click on it?" + // - "Q: How can I have widgets with an empty label?" + // - "Q: How can I have multiple widgets with the same label?" + // - Short version: ID are hashes of the entire ID stack. If you are creating widgets in a loop you most likely + // want to push a unique identifier (e.g. object pointer, loop index) to uniquely differentiate them. // - You can also use the "Label##foobar" syntax within widget label to distinguish them from each others. - // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed and used as an ID, + // - In this header file we use the "label"/"name" terminology to denote a string that will be displayed + used as an ID, // whereas "str_id" denote a string that is only used as an ID and not normally displayed. IMGUI_API void PushID(const char* str_id); // push string into the ID stack (will hash string). IMGUI_API void PushID(const char* str_id_begin, const char* str_id_end); // push string into the ID stack (will hash string). @@ -381,10 +534,11 @@ namespace ImGui IMGUI_API ImGuiID GetID(const char* str_id); // calculate unique ID (hash of whole ID stack + given parameter). e.g. if you want to query into ImGuiStorage yourself IMGUI_API ImGuiID GetID(const char* str_id_begin, const char* str_id_end); IMGUI_API ImGuiID GetID(const void* ptr_id); + IMGUI_API ImGuiID GetID(int int_id); // Widgets: Text - IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. - IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // simple formatted text + IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL); // raw text without formatting. Roughly equivalent to Text("%s", text) but: A) doesn't require null terminated string if 'text_end' is specified, B) it's faster, no memory copy is done, no buffer size limits, recommended for long chunks of text. + IMGUI_API void Text(const char* fmt, ...) IM_FMTARGS(1); // formatted text IMGUI_API void TextV(const char* fmt, va_list args) IM_FMTLIST(1); IMGUI_API void TextColored(const ImVec4& col, const char* fmt, ...) IM_FMTARGS(2); // shortcut for PushStyleColor(ImGuiCol_Text, col); Text(fmt, ...); PopStyleColor(); IMGUI_API void TextColoredV(const ImVec4& col, const char* fmt, va_list args) IM_FMTLIST(2); @@ -396,74 +550,97 @@ namespace ImGui IMGUI_API void LabelTextV(const char* label, const char* fmt, va_list args) IM_FMTLIST(2); IMGUI_API void BulletText(const char* fmt, ...) IM_FMTARGS(1); // shortcut for Bullet()+Text() IMGUI_API void BulletTextV(const char* fmt, va_list args) IM_FMTLIST(1); + IMGUI_API void SeparatorText(const char* label); // currently: formatted text with a horizontal line // Widgets: Main // - Most widgets return true when the value has been changed or when pressed/selected - IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0,0)); // button - IMGUI_API bool SmallButton(const char* label); // button with FramePadding=(0,0) to easily embed within text - IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size); // button behavior without the visuals, frequently useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) + // - You may also use one of the many IsItemXXX functions (e.g. IsItemActive, IsItemHovered, etc.) to query widget state. + IMGUI_API bool Button(const char* label, const ImVec2& size = ImVec2(0, 0)); // button + IMGUI_API bool SmallButton(const char* label); // button with (FramePadding.y == 0) to easily embed within text + IMGUI_API bool InvisibleButton(const char* str_id, const ImVec2& size, ImGuiButtonFlags flags = 0); // flexible button behavior without the visuals, frequently useful to build custom behaviors using the public api (along with IsItemActive, IsItemHovered, etc.) IMGUI_API bool ArrowButton(const char* str_id, ImGuiDir dir); // square button with an arrow shape - IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0,0), const ImVec2& uv1 = ImVec2(1,1), const ImVec4& tint_col = ImVec4(1,1,1,1), const ImVec4& border_col = ImVec4(0,0,0,0)); - IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0,0), const ImVec2& uv1 = ImVec2(1,1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0,0,0,0), const ImVec4& tint_col = ImVec4(1,1,1,1)); // <0 frame_padding uses default frame padding settings. 0 for no padding IMGUI_API bool Checkbox(const char* label, bool* v); + IMGUI_API bool CheckboxFlags(const char* label, int* flags, int flags_value); IMGUI_API bool CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value); IMGUI_API bool RadioButton(const char* label, bool active); // use with e.g. if (RadioButton("one", my_value==1)) { my_value = 1; } IMGUI_API bool RadioButton(const char* label, int* v, int v_button); // shortcut to handle the above pattern when value is an integer - IMGUI_API void ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-1,0), const char* overlay = NULL); - IMGUI_API void Bullet(); // draw a small circle and keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses + IMGUI_API void ProgressBar(float fraction, const ImVec2& size_arg = ImVec2(-FLT_MIN, 0), const char* overlay = NULL); + // by zfedoran, https://github.com/ocornut/imgui/issues/1901 (MaSzyna addition) + IMGUI_API bool BufferingBar(const char* label, float value, const ImVec2& size_arg, const ImU32& bg_col, const ImU32& fg_col); + IMGUI_API bool Spinner(const char* label, float radius, int thickness, const ImU32& color); + IMGUI_API void Bullet(); // draw a small circle + keep the cursor on the same line. advance cursor x position by GetTreeNodeToLabelSpacing(), same distance that TreeNode() uses + IMGUI_API bool TextLink(const char* label); // hyperlink text button, return true when clicked + IMGUI_API void TextLinkOpenURL(const char* label, const char* url = NULL); // hyperlink text button, automatically open file/url when clicked - // Widgets: Combo Box - // - The new BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() items. - // - The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. + // Widgets: Images + // - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples + // - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. + // - Image() pads adds style.ImageBorderSize on each side, ImageButton() adds style.FramePadding on each side. + // - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1)); + IMGUI_API void ImageWithBg(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + IMGUI_API bool ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); + + // Widgets: Combo Box (Dropdown) + // - The BeginCombo()/EndCombo() api allows you to manage your contents and selection state however you want it, by creating e.g. Selectable() items. + // - The old Combo() api are helpers over BeginCombo()/EndCombo() which are kept available for convenience purpose. This is analogous to how ListBox are created. IMGUI_API bool BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags = 0); IMGUI_API void EndCombo(); // only call EndCombo() if BeginCombo() returns true! IMGUI_API bool Combo(const char* label, int* current_item, const char* const items[], int items_count, int popup_max_height_in_items = -1); IMGUI_API bool Combo(const char* label, int* current_item, const char* items_separated_by_zeros, int popup_max_height_in_items = -1); // Separate items with \0 within a string, end item-list with \0\0. e.g. "One\0Two\0Three\0" - IMGUI_API bool Combo(const char* label, int* current_item, bool(*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items = -1); - // Widgets: Drags - // - CTRL+Click on any drag box to turn them into an input box. Manually input values aren't clamped and can go off-bounds. - // - For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every functions, note that a 'float v[X]' function argument is the same as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x + // Widgets: Drag Sliders + // - CTRL+Click on any drag box to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. + // - For all the Float2/Float3/Float4/Int2/Int3/Int4 versions of every function, note that a 'float v[X]' function argument is the same as 'float* v', + // the array syntax is just a way to document the number of elements that are expected to be accessible. You can pass address of your first element out of a contiguous set, e.g. &myvector.x // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. - // - Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For gamepad/keyboard navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). - // - Use v_min < v_max to clamp edits to given limits. Note that CTRL+Click manual input can override those limits. - // - Use v_min > v_max to lock edits. - IMGUI_API bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", float power = 1.0f); // If v_min >= v_max we have no bound - IMGUI_API bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", float power = 1.0f); - IMGUI_API bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", float power = 1.0f); - IMGUI_API bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", float power = 1.0f); - IMGUI_API bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", const char* format_max = NULL, float power = 1.0f); - IMGUI_API bool DragInt(const char* label, int* v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d"); // If v_min >= v_max we have no bound - IMGUI_API bool DragInt2(const char* label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d"); - IMGUI_API bool DragInt3(const char* label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d"); - IMGUI_API bool DragInt4(const char* label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d"); - IMGUI_API bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", const char* format_max = NULL); - IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* v, float v_speed, const void* v_min = NULL, const void* v_max = NULL, const char* format = NULL, float power = 1.0f); - IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* v, int components, float v_speed, const void* v_min = NULL, const void* v_max = NULL, const char* format = NULL, float power = 1.0f); + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Speed are per-pixel of mouse movement (v_speed=0.2f: mouse needs to move by 5 pixels to increase value by 1). For keyboard/gamepad navigation, minimum speed is Max(v_speed, minimum_step_at_given_precision). + // - Use v_min < v_max to clamp edits to given limits. Note that CTRL+Click manual input can override those limits if ImGuiSliderFlags_AlwaysClamp is not used. + // - Use v_max = FLT_MAX / INT_MAX etc to avoid clamping to a maximum, same with v_min = -FLT_MAX / INT_MIN to avoid clamping to a minimum. + // - We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. + // - Legacy: Pre-1.78 there are DragXXX() function signatures that take a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool DragFloat(const char* label, float* v, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragFloat2(const char* label, float v[2], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat3(const char* label, float v[3], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloat4(const char* label, float v[4], float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed = 1.0f, float v_min = 0.0f, float v_max = 0.0f, const char* format = "%.3f", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt(const char* label, int* v, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); // If v_min >= v_max we have no bound + IMGUI_API bool DragInt2(const char* label, int v[2], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt3(const char* label, int v[3], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragInt4(const char* label, int v[4], float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed = 1.0f, int v_min = 0, int v_max = 0, const char* format = "%d", const char* format_max = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed = 1.0f, const void* p_min = NULL, const void* p_max = NULL, const char* format = NULL, ImGuiSliderFlags flags = 0); - // Widgets: Sliders - // - CTRL+Click on any slider to turn them into an input box. Manually input values aren't clamped and can go off-bounds. + // Widgets: Regular Sliders + // - CTRL+Click on any slider to turn them into an input box. Manually input values aren't clamped by default and can go off-bounds. Use ImGuiSliderFlags_AlwaysClamp to always clamp. // - Adjust format string to decorate the value with a prefix, a suffix, or adapt the editing and display precision e.g. "%.3f" -> 1.234; "%5.2f secs" -> 01.23 secs; "Biscuit: %.0f" -> Biscuit: 1; etc. - IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); // adjust format to decorate the value with a prefix or a suffix for in-slider labels or unit display. Use power!=1.0 for power curve sliders - IMGUI_API bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); - IMGUI_API bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); - IMGUI_API bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); - IMGUI_API bool SliderAngle(const char* label, float* v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f, const char* format = "%.0f deg"); - IMGUI_API bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format = "%d"); - IMGUI_API bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format = "%d"); - IMGUI_API bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format = "%d"); - IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format = "%d"); - IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* v, const void* v_min, const void* v_max, const char* format = NULL, float power = 1.0f); - IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format = NULL, float power = 1.0f); - IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format = "%.3f", float power = 1.0f); - IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format = "%d"); - IMGUI_API bool VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* v, const void* v_min, const void* v_max, const char* format = NULL, float power = 1.0f); + // - Format string may also be set to NULL or use the default format ("%f" or "%d"). + // - Legacy: Pre-1.78 there are SliderXXX() function signatures that take a final `float power=1.0f' argument instead of the `ImGuiSliderFlags flags=0' argument. + // If you get a warning converting a float to ImGuiSliderFlags, read https://github.com/ocornut/imgui/issues/3361 + IMGUI_API bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); // adjust format to decorate the value with a prefix or a suffix for in-slider labels or unit display. + IMGUI_API bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderAngle(const char* label, float* v_rad, float v_degrees_min = -360.0f, float v_degrees_max = +360.0f, const char* format = "%.0f deg", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt(const char* label, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format = "%.3f", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format = "%d", ImGuiSliderFlags flags = 0); + IMGUI_API bool VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format = NULL, ImGuiSliderFlags flags = 0); // Widgets: Input with Keyboard - // - If you want to use InputText() with a dynamic string type such as std::string or your own, see misc/cpp/imgui_stdlib.h + // - If you want to use InputText() with std::string or any custom dynamic string type, see misc/cpp/imgui_stdlib.h and comments in imgui_demo.cpp. // - Most of the ImGuiInputTextFlags flags are only useful for InputText() and not for InputFloatX, InputIntX, InputDouble etc. IMGUI_API bool InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); - IMGUI_API bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size = ImVec2(0,0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); + IMGUI_API bool InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); IMGUI_API bool InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); IMGUI_API bool InputFloat(const char* label, float* v, float step = 0.0f, float step_fast = 0.0f, const char* format = "%.3f", ImGuiInputTextFlags flags = 0); IMGUI_API bool InputFloat2(const char* label, float v[2], const char* format = "%.3f", ImGuiInputTextFlags flags = 0); @@ -474,17 +651,17 @@ namespace ImGui IMGUI_API bool InputInt3(const char* label, int v[3], ImGuiInputTextFlags flags = 0); IMGUI_API bool InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags = 0); IMGUI_API bool InputDouble(const char* label, double* v, double step = 0.0, double step_fast = 0.0, const char* format = "%.6f", ImGuiInputTextFlags flags = 0); - IMGUI_API bool InputScalar(const char* label, ImGuiDataType data_type, void* v, const void* step = NULL, const void* step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); - IMGUI_API bool InputScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* step = NULL, const void* step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); + IMGUI_API bool InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step = NULL, const void* p_step_fast = NULL, const char* format = NULL, ImGuiInputTextFlags flags = 0); - // Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little colored preview square that can be left-clicked to open a picker, and right-clicked to open an option menu.) + // Widgets: Color Editor/Picker (tip: the ColorEdit* functions have a little color square that can be left-clicked to open a picker, and right-clicked to open an option menu.) // - Note that in C++ a 'float v[X]' function argument is the _same_ as 'float* v', the array syntax is just a way to document the number of elements that are expected to be accessible. // - You can pass the address of a first float element out of a contiguous structure, e.g. &myvector.x IMGUI_API bool ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); IMGUI_API bool ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags = 0); IMGUI_API bool ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags flags = 0); IMGUI_API bool ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags flags = 0, const float* ref_col = NULL); - IMGUI_API bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags = 0, ImVec2 size = ImVec2(0,0)); // display a colored square/button, hover for details, return true when pressed. + IMGUI_API bool ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // display a color square/button, hover for details, return true when pressed. IMGUI_API void SetColorEditOptions(ImGuiColorEditFlags flags); // initialize current options (generally on application startup) if you want to select a default format, picker type, etc. User will be able to change many settings, unless you pass the _NoOptions flag to your calls. // Widgets: Trees @@ -499,33 +676,51 @@ namespace ImGui IMGUI_API bool TreeNodeEx(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) IM_FMTARGS(3); IMGUI_API bool TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); IMGUI_API bool TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) IM_FMTLIST(3); - IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushId(). Already called by TreeNode() when returning true, but you can call TreePush/TreePop yourself if desired. - IMGUI_API void TreePush(const void* ptr_id = NULL); // " - IMGUI_API void TreePop(); // ~ Unindent()+PopId() + IMGUI_API void TreePush(const char* str_id); // ~ Indent()+PushID(). Already called by TreeNode() when returning true, but you can call TreePush/TreePop yourself if desired. + IMGUI_API void TreePush(const void* ptr_id); // " + IMGUI_API void TreePop(); // ~ Unindent()+PopID() IMGUI_API float GetTreeNodeToLabelSpacing(); // horizontal distance preceding label when using TreeNode*() or Bullet() == (g.FontSize + style.FramePadding.x*2) for a regular unframed TreeNode IMGUI_API bool CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags = 0); // if returning 'true' the header is open. doesn't indent nor push on ID stack. user doesn't have to call TreePop(). - IMGUI_API bool CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags = 0); // when 'p_open' isn't NULL, display an additional small close button on upper right of the header + IMGUI_API bool CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags = 0); // when 'p_visible != NULL': if '*p_visible==true' display an additional small close button on upper right of the header which will set the bool to false when clicked, if '*p_visible==false' don't display the header. IMGUI_API void SetNextItemOpen(bool is_open, ImGuiCond cond = 0); // set next TreeNode/CollapsingHeader open state. + IMGUI_API void SetNextItemStorageID(ImGuiID storage_id); // set id to use for open/close storage (default to same as item id). // Widgets: Selectables // - A selectable highlights when hovered, and can display another color when selected. - // - Neighbors selectable extend their highlight bounds in order to leave no gap between them. - IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height - IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0,0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + // - Neighbors selectable extend their highlight bounds in order to leave no gap between them. This is so a series of selected Selectable appear contiguous. + IMGUI_API bool Selectable(const char* label, bool selected = false, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool selected" carry the selection state (read-only). Selectable() is clicked is returns true so you can modify your selection state. size.x==0.0: use remaining width, size.x>0.0: specify width. size.y==0.0: use label height, size.y>0.0: specify height + IMGUI_API bool Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags = 0, const ImVec2& size = ImVec2(0, 0)); // "bool* p_selected" point to the selection state (read-write), as a convenient helper. + + // Multi-selection system for Selectable(), Checkbox(), TreeNode() functions [BETA] + // - This enables standard multi-selection/range-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc.) in a way that also allow a clipper to be used. + // - ImGuiSelectionUserData is often used to store your item index within the current view (but may store something else). + // - Read comments near ImGuiMultiSelectIO for instructions/details and see 'Demo->Widgets->Selection State & Multi-Select' for demo. + // - TreeNode() is technically supported but... using this correctly is more complicated. You need some sort of linear/random access to your tree, + // which is suited to advanced trees setups already implementing filters and clipper. We will work simplifying the current demo. + // - 'selection_size' and 'items_count' parameters are optional and used by a few features. If they are costly for you to compute, you may avoid them. + IMGUI_API ImGuiMultiSelectIO* BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size = -1, int items_count = -1); + IMGUI_API ImGuiMultiSelectIO* EndMultiSelect(); + IMGUI_API void SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data); + IMGUI_API bool IsItemToggledSelection(); // Was the last item selection state toggled? Useful if you need the per-item information _before_ reaching EndMultiSelect(). We only returns toggle _event_ in order to handle clipping correctly. // Widgets: List Boxes - // - FIXME: To be consistent with all the newer API, ListBoxHeader/ListBoxFooter should in reality be called BeginListBox/EndListBox. Will rename them. + // - This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. + // - If you don't need a label you can probably simply use BeginChild() with the ImGuiChildFlags_FrameStyle flag for the same result. + // - You can submit contents and manage your selection state however you want it, by creating e.g. Selectable() or any other items. + // - The simplified/old ListBox() api are helpers over BeginListBox()/EndListBox() which are kept available for convenience purpose. This is analoguous to how Combos are created. + // - Choose frame width: size.x > 0.0f: custom / size.x < 0.0f or -FLT_MIN: right-align / size.x = 0.0f (default): use current ItemWidth + // - Choose frame height: size.y > 0.0f: custom / size.y < 0.0f or -FLT_MIN: bottom-align / size.y = 0.0f (default): arbitrary default height which can fit ~7 items + IMGUI_API bool BeginListBox(const char* label, const ImVec2& size = ImVec2(0, 0)); // open a framed scrolling region + IMGUI_API void EndListBox(); // only call EndListBox() if BeginListBox() returned true! IMGUI_API bool ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_in_items = -1); - IMGUI_API bool ListBox(const char* label, int* current_item, bool (*items_getter)(void* data, int idx, const char** out_text), void* data, int items_count, int height_in_items = -1); - IMGUI_API bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0,0)); // use if you want to reimplement ListBox() will custom data or interactions. if the function return true, you can output elements then call ListBoxFooter() afterwards. - IMGUI_API bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1); // " - IMGUI_API void ListBoxFooter(); // terminate the scrolling region. only call ListBoxFooter() if ListBoxHeader() returned true! + IMGUI_API bool ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items = -1); // Widgets: Data Plotting + // - Consider using ImPlot (https://github.com/epezent/implot) which is much better! IMGUI_API void PlotLines(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); IMGUI_API void PlotLines(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); IMGUI_API void PlotHistogram(const char* label, const float* values, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0), int stride = sizeof(float)); - IMGUI_API void PlotHistogram(const char* label, float(*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); + IMGUI_API void PlotHistogram(const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset = 0, const char* overlay_text = NULL, float scale_min = FLT_MAX, float scale_max = FLT_MAX, ImVec2 graph_size = ImVec2(0, 0)); // Widgets: Value() Helpers. // - Those are merely shortcut to calling Text() with a format string. Output single value in "name: value" format (tip: freely declare more in your code to handle your types. you can add functions to the ImGui namespace) @@ -535,44 +730,138 @@ namespace ImGui IMGUI_API void Value(const char* prefix, float v, const char* float_format = NULL); // Widgets: Menus - IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. - IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! + // - Use BeginMenuBar() on a window ImGuiWindowFlags_MenuBar to append to its menu bar. + // - Use BeginMainMenuBar() to create a menu bar at the top of the screen and append to it. + // - Use BeginMenu() to create a menu. You can call BeginMenu() multiple time with the same identifier to append more items to it. + // - Not that MenuItem() keyboardshortcuts are displayed as a convenience but _not processed_ by Dear ImGui at the moment. IMGUI_API bool BeginMenuBar(); // append to menu-bar of current window (requires ImGuiWindowFlags_MenuBar flag set on parent window). IMGUI_API void EndMenuBar(); // only call EndMenuBar() if BeginMenuBar() returns true! + IMGUI_API bool BeginMainMenuBar(); // create and append to a full screen menu-bar. + IMGUI_API void EndMainMenuBar(); // only call EndMainMenuBar() if BeginMainMenuBar() returns true! IMGUI_API bool BeginMenu(const char* label, bool enabled = true); // create a sub-menu entry. only call EndMenu() if this returns true! IMGUI_API void EndMenu(); // only call EndMenu() if BeginMenu() returns true! - IMGUI_API bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. shortcuts are displayed for convenience but not processed by ImGui at the moment + IMGUI_API bool MenuItem(const char* label, const char* shortcut = NULL, bool selected = false, bool enabled = true); // return true when activated. IMGUI_API bool MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled = true); // return true when activated + toggle (*p_selected) if p_selected != NULL // Tooltips - IMGUI_API void BeginTooltip(); // begin/append a tooltip window. to create full-featured tooltip (with any kind of items). - IMGUI_API void EndTooltip(); - IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip, typically use with ImGui::IsItemHovered(). override any previous call to SetTooltip(). + // - Tooltips are windows following the mouse. They do not take focus away. + // - A tooltip window can contain items of any types. + // - SetTooltip() is more or less a shortcut for the 'if (BeginTooltip()) { Text(...); EndTooltip(); }' idiom (with a subtlety that it discard any previously submitted tooltip) + IMGUI_API bool BeginTooltip(); // begin/append a tooltip window. + IMGUI_API void EndTooltip(); // only call EndTooltip() if BeginTooltip()/BeginItemTooltip() returns true! + IMGUI_API void SetTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip. Often used after a ImGui::IsItemHovered() check. Override any previous call to SetTooltip(). IMGUI_API void SetTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); - // Popups, Modals - // The properties of popups windows are: - // - They block normal mouse hovering detection outside them. (*) - // - Unless modal, they can be closed by clicking anywhere outside them, or by pressing ESCAPE. - // - Their visibility state (~bool) is held internally by imgui instead of being held by the programmer as we are used to with regular Begin() calls. - // User can manipulate the visibility state by calling OpenPopup(). - // (*) One can use IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) to bypass it and detect hovering even when normally blocked by a popup. - // Those three properties are connected. The library needs to hold their visibility state because it can close popups at any time. - IMGUI_API void OpenPopup(const char* str_id); // call to mark popup as open (don't call every frame!). popups are closed when user click outside, or if CloseCurrentPopup() is called within a BeginPopup()/EndPopup() block. By default, Selectable()/MenuItem() are calling CloseCurrentPopup(). Popup identifiers are relative to the current ID-stack (so OpenPopup and BeginPopup needs to be at the same level). - IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. only call EndPopup() if BeginPopup() returns true! - IMGUI_API bool BeginPopupContextItem(const char* str_id = NULL, int mouse_button = 1); // helper to open and begin popup when clicked on last item. if you can pass a NULL str_id only if the previous item had an id. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! - IMGUI_API bool BeginPopupContextWindow(const char* str_id = NULL, int mouse_button = 1, bool also_over_items = true); // helper to open and begin popup when clicked on current window. - IMGUI_API bool BeginPopupContextVoid(const char* str_id = NULL, int mouse_button = 1); // helper to open and begin popup when clicked in void (where there are no imgui windows). - IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // modal dialog (regular window with title bar, block interactions behind the modal window, can't close the modal window by clicking outside) - IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! - IMGUI_API bool OpenPopupOnItemClick(const char* str_id = NULL, int mouse_button = 1); // helper to open popup when clicked on last item (note: actually triggers on the mouse _released_ event to be consistent with popup behaviors). return true when just opened. - IMGUI_API bool IsPopupOpen(const char* str_id); // return true if the popup is open at the current begin-ed level of the popup stack. - IMGUI_API void CloseCurrentPopup(); // close the popup we have begin-ed into. clicking on a MenuItem or Selectable automatically close the current popup. + // Tooltips: helpers for showing a tooltip when hovering an item + // - BeginItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_ForTooltip) && BeginTooltip())' idiom. + // - SetItemTooltip() is a shortcut for the 'if (IsItemHovered(ImGuiHoveredFlags_ForTooltip)) { SetTooltip(...); }' idiom. + // - Where 'ImGuiHoveredFlags_ForTooltip' itself is a shortcut to use 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' depending on active input type. For mouse it defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + IMGUI_API bool BeginItemTooltip(); // begin/append a tooltip window if preceding item was hovered. + IMGUI_API void SetItemTooltip(const char* fmt, ...) IM_FMTARGS(1); // set a text-only tooltip if preceding item was hovered. override any previous call to SetTooltip(). + IMGUI_API void SetItemTooltipV(const char* fmt, va_list args) IM_FMTLIST(1); - // Columns + // Popups, Modals + // - They block normal mouse hovering detection (and therefore most mouse interactions) behind them. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - Their visibility state (~bool) is held internally instead of being held by the programmer as we are used to with regular Begin*() calls. + // - The 3 properties above are related: we need to retain popup visibility state in the library because popups may be closed as any time. + // - You can bypass the hovering restriction by using ImGuiHoveredFlags_AllowWhenBlockedByPopup when calling IsItemHovered() or IsWindowHovered(). + // - IMPORTANT: Popup identifiers are relative to the current ID stack, so OpenPopup and BeginPopup generally needs to be at the same level of the stack. + // This is sometimes leading to confusing mistakes. May rework this in the future. + // - BeginPopup(): query popup state, if open start appending into the window. Call EndPopup() afterwards if returned true. ImGuiWindowFlags are forwarded to the window. + // - BeginPopupModal(): block every interaction behind the window, cannot be closed by user, add a dimming background, has a title bar. + IMGUI_API bool BeginPopup(const char* str_id, ImGuiWindowFlags flags = 0); // return true if the popup is open, and you can start outputting to it. + IMGUI_API bool BeginPopupModal(const char* name, bool* p_open = NULL, ImGuiWindowFlags flags = 0); // return true if the modal is open, and you can start outputting to it. + IMGUI_API void EndPopup(); // only call EndPopup() if BeginPopupXXX() returns true! + + // Popups: open/close functions + // - OpenPopup(): set popup state to open. ImGuiPopupFlags are available for opening options. + // - If not modal: they can be closed by clicking anywhere outside them, or by pressing ESCAPE. + // - CloseCurrentPopup(): use inside the BeginPopup()/EndPopup() scope to close manually. + // - CloseCurrentPopup() is called by default by Selectable()/MenuItem() when activated (FIXME: need some options). + // - Use ImGuiPopupFlags_NoOpenOverExistingPopup to avoid opening a popup if there's already one at the same level. This is equivalent to e.g. testing for !IsAnyPopupOpen() prior to OpenPopup(). + // - Use IsWindowAppearing() after BeginPopup() to tell if a window just opened. + // - IMPORTANT: Notice that for OpenPopupOnItemClick() we exceptionally default flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter + IMGUI_API void OpenPopup(const char* str_id, ImGuiPopupFlags popup_flags = 0); // call to mark popup as open (don't call every frame!). + IMGUI_API void OpenPopup(ImGuiID id, ImGuiPopupFlags popup_flags = 0); // id overload to facilitate calling from nested stacks + IMGUI_API void OpenPopupOnItemClick(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // helper to open popup when clicked on last item. Default to ImGuiPopupFlags_MouseButtonRight == 1. (note: actually triggers on the mouse _released_ event to be consistent with popup behaviors) + IMGUI_API void CloseCurrentPopup(); // manually close the popup we have begin-ed into. + + // Popups: open+begin combined functions helpers + // - Helpers to do OpenPopup+BeginPopup where the Open action is triggered by e.g. hovering an item and right-clicking. + // - They are convenient to easily create context menus, hence the name. + // - IMPORTANT: Notice that BeginPopupContextXXX takes ImGuiPopupFlags just like OpenPopup() and unlike BeginPopup(). For full consistency, we may add ImGuiWindowFlags to the BeginPopupContextXXX functions in the future. + // - IMPORTANT: Notice that we exceptionally default their flags to 1 (== ImGuiPopupFlags_MouseButtonRight) for backward compatibility with older API taking 'int mouse_button = 1' parameter, so if you add other flags remember to re-add the ImGuiPopupFlags_MouseButtonRight. + IMGUI_API bool BeginPopupContextItem(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked on last item. Use str_id==NULL to associate the popup to previous item. If you want to use that on a non-interactive item such as Text() you need to pass in an explicit ID here. read comments in .cpp! + IMGUI_API bool BeginPopupContextWindow(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1);// open+begin popup when clicked on current window. + IMGUI_API bool BeginPopupContextVoid(const char* str_id = NULL, ImGuiPopupFlags popup_flags = 1); // open+begin popup when clicked in void (where there are no windows). + + // Popups: query functions + // - IsPopupOpen(): return true if the popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId: return true if any popup is open at the current BeginPopup() level of the popup stack. + // - IsPopupOpen() with ImGuiPopupFlags_AnyPopupId + ImGuiPopupFlags_AnyPopupLevel: return true if any popup is open. + IMGUI_API bool IsPopupOpen(const char* str_id, ImGuiPopupFlags flags = 0); // return true if the popup is open. + + // Tables + // - Full-featured replacement for old Columns API. + // - See Demo->Tables for demo code. See top of imgui_tables.cpp for general commentary. + // - See ImGuiTableFlags_ and ImGuiTableColumnFlags_ enums for a description of available flags. + // The typical call flow is: + // - 1. Call BeginTable(), early out if returning false. + // - 2. Optionally call TableSetupColumn() to submit column name/flags/defaults. + // - 3. Optionally call TableSetupScrollFreeze() to request scroll freezing of columns/rows. + // - 4. Optionally call TableHeadersRow() to submit a header row. Names are pulled from TableSetupColumn() data. + // - 5. Populate contents: + // - In most situations you can use TableNextRow() + TableSetColumnIndex(N) to start appending into a column. + // - If you are using tables as a sort of grid, where every column is holding the same type of contents, + // you may prefer using TableNextColumn() instead of TableNextRow() + TableSetColumnIndex(). + // TableNextColumn() will automatically wrap-around into the next row if needed. + // - IMPORTANT: Comparatively to the old Columns() API, we need to call TableNextColumn() for the first column! + // - Summary of possible call flow: + // - TableNextRow() -> TableSetColumnIndex(0) -> Text("Hello 0") -> TableSetColumnIndex(1) -> Text("Hello 1") // OK + // - TableNextRow() -> TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK + // - TableNextColumn() -> Text("Hello 0") -> TableNextColumn() -> Text("Hello 1") // OK: TableNextColumn() automatically gets to next row! + // - TableNextRow() -> Text("Hello 0") // Not OK! Missing TableSetColumnIndex() or TableNextColumn()! Text will not appear! + // - 5. Call EndTable() + IMGUI_API bool BeginTable(const char* str_id, int columns, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0.0f, 0.0f), float inner_width = 0.0f); + IMGUI_API void EndTable(); // only call EndTable() if BeginTable() returns true! + IMGUI_API void TableNextRow(ImGuiTableRowFlags row_flags = 0, float min_row_height = 0.0f); // append into the first cell of a new row. + IMGUI_API bool TableNextColumn(); // append into the next column (or first column of next row if currently in last column). Return true when column is visible. + IMGUI_API bool TableSetColumnIndex(int column_n); // append into the specified column. Return true when column is visible. + + // Tables: Headers & Columns declaration + // - Use TableSetupColumn() to specify label, resizing policy, default width/weight, id, various other flags etc. + // - Use TableHeadersRow() to create a header row and automatically submit a TableHeader() for each column. + // Headers are required to perform: reordering, sorting, and opening the context menu. + // The context menu can also be made available in columns body using ImGuiTableFlags_ContextMenuInBody. + // - You may manually submit headers using TableNextRow() + TableHeader() calls, but this is only useful in + // some advanced use cases (e.g. adding custom widgets in header row). + // - Use TableSetupScrollFreeze() to lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = 0.0f, ImGuiID user_id = 0); + IMGUI_API void TableSetupScrollFreeze(int cols, int rows); // lock columns/rows so they stay visible when scrolled. + IMGUI_API void TableHeader(const char* label); // submit one header cell manually (rarely used) + IMGUI_API void TableHeadersRow(); // submit a row with headers cells based on data provided to TableSetupColumn() + submit context menu + IMGUI_API void TableAngledHeadersRow(); // submit a row with angled headers for every column with the ImGuiTableColumnFlags_AngledHeader flag. MUST BE FIRST ROW. + + // Tables: Sorting & Miscellaneous functions + // - Sorting: call TableGetSortSpecs() to retrieve latest sort specs for the table. NULL when not sorting. + // When 'sort_specs->SpecsDirty == true' you should sort your data. It will be true when sorting specs have + // changed since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting, + // else you may wastefully sort your data every frame! + // - Functions args 'int column_n' treat the default value of -1 as the same as passing the current column index. + IMGUI_API ImGuiTableSortSpecs* TableGetSortSpecs(); // get latest sort specs for the table (NULL if not sorting). Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable(). + IMGUI_API int TableGetColumnCount(); // return number of columns (value passed to BeginTable) + IMGUI_API int TableGetColumnIndex(); // return current column index. + IMGUI_API int TableGetRowIndex(); // return current row index. + IMGUI_API const char* TableGetColumnName(int column_n = -1); // return "" if column didn't have a name declared by TableSetupColumn(). Pass -1 to use current column. + IMGUI_API ImGuiTableColumnFlags TableGetColumnFlags(int column_n = -1); // return column flags so you can query their Enabled/Visible/Sorted/Hovered status flags. Pass -1 to use current column. + IMGUI_API void TableSetColumnEnabled(int column_n, bool v);// change user accessible enabled/disabled state of a column. Set to false to hide the column. User can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody) + IMGUI_API int TableGetHoveredColumn(); // return hovered column. return -1 when table is not hovered. return columns_count if the unused space at the right of visible columns is hovered. Can also use (TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) instead. + IMGUI_API void TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n = -1); // change the color of a cell, row, or column. See ImGuiTableBgTarget_ flags for details. + + // Legacy Columns API (prefer using Tables!) // - You can also use SameLine(pos_x) to mimic simplified columns. - // - The columns API is work-in-progress and rather lacking (columns are arguably the worst part of dear imgui at the moment!) - IMGUI_API void Columns(int count = 1, const char* id = NULL, bool border = true); + IMGUI_API void Columns(int count = 1, const char* id = NULL, bool borders = true); IMGUI_API void NextColumn(); // next column, defaults to current row or next row if the current row is finished IMGUI_API int GetColumnIndex(); // get current column index IMGUI_API float GetColumnWidth(int column_index = -1); // get column width (in pixels). pass -1 to use current column @@ -582,13 +871,34 @@ namespace ImGui IMGUI_API int GetColumnsCount(); // Tab Bars, Tabs - // [BETA API] API may evolve! + // - Note: Tabs are automatically created by the docking system (when in 'docking' branch). Use this to create tab bars/tabs yourself. IMGUI_API bool BeginTabBar(const char* str_id, ImGuiTabBarFlags flags = 0); // create and append into a TabBar IMGUI_API void EndTabBar(); // only call EndTabBar() if BeginTabBar() returns true! - IMGUI_API bool BeginTabItem(const char* label, bool* p_open = NULL, ImGuiTabItemFlags flags = 0);// create a Tab. Returns true if the Tab is selected. + IMGUI_API bool BeginTabItem(const char* label, bool* p_open = NULL, ImGuiTabItemFlags flags = 0); // create a Tab. Returns true if the Tab is selected. IMGUI_API void EndTabItem(); // only call EndTabItem() if BeginTabItem() returns true! + IMGUI_API bool TabItemButton(const char* label, ImGuiTabItemFlags flags = 0); // create a Tab behaving like a button. return true when clicked. cannot be selected in the tab bar. IMGUI_API void SetTabItemClosed(const char* tab_or_docked_window_label); // notify TabBar or Docking system of a closed tab/window ahead (useful to reduce visual flicker on reorderable tab bars). For tab-bar: call after BeginTabBar() and before Tab submissions. Otherwise call with a window name. + // Docking + // [BETA API] Enable with io.ConfigFlags |= ImGuiConfigFlags_DockingEnable. + // Note: You can use most Docking facilities without calling any API. You DO NOT need to call DockSpace() to use Docking! + // - Drag from window title bar or their tab to dock/undock. Hold SHIFT to disable docking. + // - Drag from window menu button (upper-left button) to undock an entire node (all windows). + // - When io.ConfigDockingWithShift == true, you instead need to hold SHIFT to enable docking. + // About dockspaces: + // - Use DockSpaceOverViewport() to create a window covering the screen or a specific viewport + a dockspace inside it. + // This is often used with ImGuiDockNodeFlags_PassthruCentralNode to make it transparent. + // - Use DockSpace() to create an explicit dock node _within_ an existing window. See Docking demo for details. + // - Important: Dockspaces need to be submitted _before_ any window they can host. Submit it early in your frame! + // - Important: Dockspaces need to be kept alive if hidden, otherwise windows docked into it will be undocked. + // e.g. if you have multiple tabs with a dockspace inside each tab: submit the non-visible dockspaces with ImGuiDockNodeFlags_KeepAliveOnly. + IMGUI_API ImGuiID DockSpace(ImGuiID dockspace_id, const ImVec2& size = ImVec2(0, 0), ImGuiDockNodeFlags flags = 0, const ImGuiWindowClass* window_class = NULL); + IMGUI_API ImGuiID DockSpaceOverViewport(ImGuiID dockspace_id = 0, const ImGuiViewport* viewport = NULL, ImGuiDockNodeFlags flags = 0, const ImGuiWindowClass* window_class = NULL); + IMGUI_API void SetNextWindowDockID(ImGuiID dock_id, ImGuiCond cond = 0); // set next window dock id + IMGUI_API void SetNextWindowClass(const ImGuiWindowClass* window_class); // set next window class (control docking compatibility + provide hints to platform backend via custom viewport flags and platform parent/child relationship) + IMGUI_API ImGuiID GetWindowDockID(); + IMGUI_API bool IsWindowDocked(); // is current window docked into another window? + // Logging/Capture // - All text output from the interface can be captured into tty/file/clipboard. By default, tree nodes are automatically opened during logging. IMGUI_API void LogToTTY(int auto_open_depth = -1); // start logging to tty (stdout) @@ -597,61 +907,87 @@ namespace ImGui IMGUI_API void LogFinish(); // stop logging (close file, etc.) IMGUI_API void LogButtons(); // helper to display buttons for logging to tty/file/clipboard IMGUI_API void LogText(const char* fmt, ...) IM_FMTARGS(1); // pass text data straight to log (without being displayed) + IMGUI_API void LogTextV(const char* fmt, va_list args) IM_FMTLIST(1); // Drag and Drop - // [BETA API] API may evolve! - IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call when the current item is active. If this return true, you can call SetDragDropPayload() + EndDragDropSource() - IMGUI_API bool SetDragDropPayload(const char* type, const void* data, size_t sz, ImGuiCond cond = 0); // type is a user defined string of maximum 32 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. + // - On source items, call BeginDragDropSource(), if it returns true also call SetDragDropPayload() + EndDragDropSource(). + // - On target candidates, call BeginDragDropTarget(), if it returns true also call AcceptDragDropPayload() + EndDragDropTarget(). + // - If you stop calling BeginDragDropSource() the payload is preserved however it won't have a preview tooltip (we currently display a fallback "..." tooltip, see #1725) + // - An item can be both drag source and drop target. + IMGUI_API bool BeginDragDropSource(ImGuiDragDropFlags flags = 0); // call after submitting an item which may be dragged. when this return true, you can call SetDragDropPayload() + EndDragDropSource() + IMGUI_API bool SetDragDropPayload(const char* type, const void* data, size_t sz, ImGuiCond cond = 0); // type is a user defined string of maximum 32 characters. Strings starting with '_' are reserved for dear imgui internal types. Data is copied and held by imgui. Return true when payload has been accepted. IMGUI_API void EndDragDropSource(); // only call EndDragDropSource() if BeginDragDropSource() returns true! IMGUI_API bool BeginDragDropTarget(); // call after submitting an item that may receive a payload. If this returns true, you can call AcceptDragDropPayload() + EndDragDropTarget() IMGUI_API const ImGuiPayload* AcceptDragDropPayload(const char* type, ImGuiDragDropFlags flags = 0); // accept contents of a given type. If ImGuiDragDropFlags_AcceptBeforeDelivery is set you can peek into the payload before the mouse button is released. IMGUI_API void EndDragDropTarget(); // only call EndDragDropTarget() if BeginDragDropTarget() returns true! - IMGUI_API const ImGuiPayload* GetDragDropPayload(); // peek directly into the current payload from anywhere. may return NULL. use ImGuiPayload::IsDataType() to test for the payload type. + IMGUI_API const ImGuiPayload* GetDragDropPayload(); // peek directly into the current payload from anywhere. returns NULL when drag and drop is finished or inactive. use ImGuiPayload::IsDataType() to test for the payload type. + + // Disabling [BETA API] + // - Disable all user interactions and dim items visuals (applying style.DisabledAlpha over current colors) + // - Those can be nested but it cannot be used to enable an already disabled section (a single BeginDisabled(true) in the stack is enough to keep everything disabled) + // - Tooltips windows by exception are opted out of disabling. + // - BeginDisabled(false)/EndDisabled() essentially does nothing but is provided to facilitate use of boolean expressions (as a micro-optimization: if you have tens of thousands of BeginDisabled(false)/EndDisabled() pairs, you might want to reformulate your code to avoid making those calls) + IMGUI_API void BeginDisabled(bool disabled = true); + IMGUI_API void EndDisabled(); // Clipping + // - Mouse hovering is affected by ImGui::PushClipRect() calls, unlike direct calls to ImDrawList::PushClipRect() which are render only. IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect); IMGUI_API void PopClipRect(); // Focus, Activation - // - Prefer using "SetItemDefaultFocus()" over "if (IsWindowAppearing()) SetScrollHereY()" when applicable to signify "this is the default item" - IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a window. + IMGUI_API void SetItemDefaultFocus(); // make last item the default focused item of a newly appearing window. IMGUI_API void SetKeyboardFocusHere(int offset = 0); // focus keyboard on the next widget. Use positive 'offset' to access sub components of a multiple component widget. Use -1 to access previous widget. - // Item/Widgets Utilities - // - Most of the functions are referring to the last/previous item we submitted. + // Keyboard/Gamepad Navigation + IMGUI_API void SetNavCursorVisible(bool visible); // alter visibility of keyboard/gamepad cursor. by default: show when using an arrow key, hide when clicking with mouse. + + // Overlapping mode + IMGUI_API void SetNextItemAllowOverlap(); // allow next item to be overlapped by a subsequent item. Useful with invisible buttons, selectable, treenode covering an area where subsequent items may need to be added. Note that both Selectable() and TreeNode() have dedicated flags doing this. + + // Item/Widgets Utilities and Query Functions + // - Most of the functions are referring to the previous Item that has been submitted. // - See Demo Window under "Widgets->Querying Status" for an interactive visualization of most of those functions. IMGUI_API bool IsItemHovered(ImGuiHoveredFlags flags = 0); // is the last item hovered? (and usable, aka not blocked by a popup, etc.). See ImGuiHoveredFlags for more options. IMGUI_API bool IsItemActive(); // is the last item active? (e.g. button being held, text field being edited. This will continuously return true while holding mouse button on an item. Items that don't interact will always return false) IMGUI_API bool IsItemFocused(); // is the last item focused for keyboard/gamepad navigation? - IMGUI_API bool IsItemClicked(int mouse_button = 0); // is the last item clicked? (e.g. button/node just clicked on) == IsMouseClicked(mouse_button) && IsItemHovered() + IMGUI_API bool IsItemClicked(ImGuiMouseButton mouse_button = 0); // is the last item hovered and mouse clicked on? (**) == IsMouseClicked(mouse_button) && IsItemHovered()Important. (**) this is NOT equivalent to the behavior of e.g. Button(). Read comments in function definition. IMGUI_API bool IsItemVisible(); // is the last item visible? (items may be out of sight because of clipping/scrolling) IMGUI_API bool IsItemEdited(); // did the last item modify its underlying value this frame? or was pressed? This is generally the same as the "bool" return value of many widgets. IMGUI_API bool IsItemActivated(); // was the last item just made active (item was previously inactive). - IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that requires continuous editing. - IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that requires continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item). + IMGUI_API bool IsItemDeactivated(); // was the last item just made inactive (item was previously active). Useful for Undo/Redo patterns with widgets that require continuous editing. + IMGUI_API bool IsItemDeactivatedAfterEdit(); // was the last item just made inactive and made a value change when it was active? (e.g. Slider/Drag moved). Useful for Undo/Redo patterns with widgets that require continuous editing. Note that you may get false positives (some widgets such as Combo()/ListBox()/Selectable() will return true even when clicking an already selected item). + IMGUI_API bool IsItemToggledOpen(); // was the last item open state toggled? set by TreeNode(). IMGUI_API bool IsAnyItemHovered(); // is any item hovered? IMGUI_API bool IsAnyItemActive(); // is any item active? IMGUI_API bool IsAnyItemFocused(); // is any item focused? + IMGUI_API ImGuiID GetItemID(); // get ID of last item (~~ often same ImGui::GetID(label) beforehand) IMGUI_API ImVec2 GetItemRectMin(); // get upper-left bounding rectangle of the last item (screen space) IMGUI_API ImVec2 GetItemRectMax(); // get lower-right bounding rectangle of the last item (screen space) IMGUI_API ImVec2 GetItemRectSize(); // get size of last item - IMGUI_API void SetItemAllowOverlap(); // allow last item to be overlapped by a subsequent item. sometimes useful with invisible buttons, selectables, etc. to catch unused area. + + // Viewports + // - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. + // - In 'docking' branch with multi-viewport enabled, we extend this concept to have multiple active viewports. + // - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. + IMGUI_API ImGuiViewport* GetMainViewport(); // return primary/default viewport. This can never be NULL. + + // Background/Foreground Draw Lists + IMGUI_API ImDrawList* GetBackgroundDrawList(ImGuiViewport* viewport = NULL); // get background draw list for the given viewport or viewport associated to the current window. this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. + IMGUI_API ImDrawList* GetForegroundDrawList(ImGuiViewport* viewport = NULL); // get foreground draw list for the given viewport or viewport associated to the current window. this draw list will be the top-most rendered one. Useful to quickly draw shapes/text over dear imgui contents. // Miscellaneous Utilities IMGUI_API bool IsRectVisible(const ImVec2& size); // test if rectangle (of given size, starting from cursor position) is visible / not clipped. IMGUI_API bool IsRectVisible(const ImVec2& rect_min, const ImVec2& rect_max); // test if rectangle (in screen space) is visible / not clipped. to perform coarse clipping on user's side. IMGUI_API double GetTime(); // get global imgui time. incremented by io.DeltaTime every frame. IMGUI_API int GetFrameCount(); // get global imgui frame count. incremented by 1 every frame. - IMGUI_API ImDrawList* GetBackgroundDrawList(); // this draw list will be the first rendering one. Useful to quickly draw shapes/text behind dear imgui contents. - IMGUI_API ImDrawList* GetForegroundDrawList(); // this draw list will be the last rendered one. Useful to quickly draw shapes/text over dear imgui contents. IMGUI_API ImDrawListSharedData* GetDrawListSharedData(); // you may use this when creating your own ImDrawList instances. IMGUI_API const char* GetStyleColorName(ImGuiCol idx); // get a string corresponding to the enum value (for display, saving, etc.). IMGUI_API void SetStateStorage(ImGuiStorage* storage); // replace current window storage with our own (if you want to manipulate it yourself, typically clear subsection of it) IMGUI_API ImGuiStorage* GetStateStorage(); + + // Text Utilities IMGUI_API ImVec2 CalcTextSize(const char* text, const char* text_end = NULL, bool hide_text_after_double_hash = false, float wrap_width = -1.0f); - IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // calculate coarse clipping for large list of evenly sized items. Prefer using the ImGuiListClipper higher-level helper if you can. - IMGUI_API bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags flags = 0); // helper to create a child window / scrolling region that looks like a normal widget frame - IMGUI_API void EndChildFrame(); // always call EndChildFrame() regardless of BeginChildFrame() return values (which indicates a collapsed/clipped window) // Color Utilities IMGUI_API ImVec4 ColorConvertU32ToFloat4(ImU32 in); @@ -659,59 +995,117 @@ namespace ImGui IMGUI_API void ColorConvertRGBtoHSV(float r, float g, float b, float& out_h, float& out_s, float& out_v); IMGUI_API void ColorConvertHSVtoRGB(float h, float s, float v, float& out_r, float& out_g, float& out_b); - // Inputs Utilities - IMGUI_API int GetKeyIndex(ImGuiKey imgui_key); // map ImGuiKey_* values into user's key index. == io.KeyMap[key] - IMGUI_API bool IsKeyDown(int user_key_index); // is key being held. == io.KeysDown[user_key_index]. note that imgui doesn't know the semantic of each entry of io.KeysDown[]. Use your own indices/enums according to how your backend/engine stored them into io.KeysDown[]! - IMGUI_API bool IsKeyPressed(int user_key_index, bool repeat = true); // was key pressed (went from !Down to Down). if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate - IMGUI_API bool IsKeyReleased(int user_key_index); // was key released (went from Down to !Down).. - IMGUI_API int GetKeyPressedAmount(int key_index, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate - IMGUI_API bool IsMouseDown(int button); // is mouse button held (0=left, 1=right, 2=middle) - IMGUI_API bool IsAnyMouseDown(); // is any mouse button held - IMGUI_API bool IsMouseClicked(int button, bool repeat = false); // did mouse button clicked (went from !Down to Down) (0=left, 1=right, 2=middle) - IMGUI_API bool IsMouseDoubleClicked(int button); // did mouse button double-clicked. a double-click returns false in IsMouseClicked(). uses io.MouseDoubleClickTime. - IMGUI_API bool IsMouseReleased(int button); // did mouse button released (went from Down to !Down) - IMGUI_API bool IsMouseDragging(int button = 0, float lock_threshold = -1.0f); // is mouse dragging. if lock_threshold < -1.0f uses io.MouseDraggingThreshold - IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true); // is mouse hovering given bounding rect (in screen space). clipped by current clipping settings, but disregarding of other consideration of focus/window ordering/popup-block. - IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // by convention we use (-FLT_MAX,-FLT_MAX) to denote that there is no mouse - IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls - IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve backup of mouse position at the time of opening popup we have BeginPopup() into - IMGUI_API ImVec2 GetMouseDragDelta(int button = 0, float lock_threshold = -1.0f); // return the delta from the initial clicking position while the mouse button is pressed or was just released. This is locked and return 0.0f until the mouse moves past a distance threshold at least once. If lock_threshold < -1.0f uses io.MouseDraggingThreshold. - IMGUI_API void ResetMouseDragDelta(int button = 0); // - IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired cursor type, reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you - IMGUI_API void SetMouseCursor(ImGuiMouseCursor type); // set desired cursor type - IMGUI_API void CaptureKeyboardFromApp(bool want_capture_keyboard_value = true); // attention: misleading name! manually override io.WantCaptureKeyboard flag next frame (said flag is entirely left for your application to handle). e.g. force capture keyboard when your widget is being hovered. This is equivalent to setting "io.WantCaptureKeyboard = want_capture_keyboard_value"; after the next NewFrame() call. - IMGUI_API void CaptureMouseFromApp(bool want_capture_mouse_value = true); // attention: misleading name! manually override io.WantCaptureMouse flag next frame (said flag is entirely left for your application to handle). This is equivalent to setting "io.WantCaptureMouse = want_capture_mouse_value;" after the next NewFrame() call. + // Inputs Utilities: Keyboard/Mouse/Gamepad + // - the ImGuiKey enum contains all possible keyboard, mouse and gamepad inputs (e.g. ImGuiKey_A, ImGuiKey_MouseLeft, ImGuiKey_GamepadDpadUp...). + // - (legacy: before v1.87, we used ImGuiKey to carry native/user indices as defined by each backends. This was obsoleted in 1.87 (2022-02) and completely removed in 1.91.5 (2024-11). See https://github.com/ocornut/imgui/issues/4921) + // - (legacy: any use of ImGuiKey will assert when key < 512 to detect passing legacy native/user indices) + IMGUI_API bool IsKeyDown(ImGuiKey key); // is key being held. + IMGUI_API bool IsKeyPressed(ImGuiKey key, bool repeat = true); // was key pressed (went from !Down to Down)? if repeat=true, uses io.KeyRepeatDelay / KeyRepeatRate + IMGUI_API bool IsKeyReleased(ImGuiKey key); // was key released (went from Down to !Down)? + IMGUI_API bool IsKeyChordPressed(ImGuiKeyChord key_chord); // was key chord (mods + key) pressed, e.g. you can pass 'ImGuiMod_Ctrl | ImGuiKey_S' as a key-chord. This doesn't do any routing or focus check, please consider using Shortcut() function instead. + IMGUI_API int GetKeyPressedAmount(ImGuiKey key, float repeat_delay, float rate); // uses provided repeat rate/delay. return a count, most often 0 or 1 but might be >1 if RepeatRate is small enough that DeltaTime > RepeatRate + IMGUI_API const char* GetKeyName(ImGuiKey key); // [DEBUG] returns English name of the key. Those names are provided for debugging purpose and are not meant to be saved persistently nor compared. + IMGUI_API void SetNextFrameWantCaptureKeyboard(bool want_capture_keyboard); // Override io.WantCaptureKeyboard flag next frame (said flag is left for your application to handle, typically when true it instructs your app to ignore inputs). e.g. force capture keyboard when your widget is being hovered. This is equivalent to setting "io.WantCaptureKeyboard = want_capture_keyboard"; after the next NewFrame() call. - // Clipboard Utilities (also see the LogToClipboard() function to capture or output text data to the clipboard) + // Inputs Utilities: Shortcut Testing & Routing [BETA] + // - ImGuiKeyChord = a ImGuiKey + optional ImGuiMod_Alt/ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Super. + // ImGuiKey_C // Accepted by functions taking ImGuiKey or ImGuiKeyChord arguments) + // ImGuiMod_Ctrl | ImGuiKey_C // Accepted by functions taking ImGuiKeyChord arguments) + // only ImGuiMod_XXX values are legal to combine with an ImGuiKey. You CANNOT combine two ImGuiKey values. + // - The general idea is that several callers may register interest in a shortcut, and only one owner gets it. + // Parent -> call Shortcut(Ctrl+S) // When Parent is focused, Parent gets the shortcut. + // Child1 -> call Shortcut(Ctrl+S) // When Child1 is focused, Child1 gets the shortcut (Child1 overrides Parent shortcuts) + // Child2 -> no call // When Child2 is focused, Parent gets the shortcut. + // The whole system is order independent, so if Child1 makes its calls before Parent, results will be identical. + // This is an important property as it facilitate working with foreign code or larger codebase. + // - To understand the difference: + // - IsKeyChordPressed() compares mods and call IsKeyPressed() -> function has no side-effect. + // - Shortcut() submits a route, routes are resolved, if it currently can be routed it calls IsKeyChordPressed() -> function has (desirable) side-effects as it can prevents another call from getting the route. + // - Visualize registered routes in 'Metrics/Debugger->Inputs'. + IMGUI_API bool Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0); + IMGUI_API void SetNextItemShortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags = 0); + + // Inputs Utilities: Key/Input Ownership [BETA] + // - One common use case would be to allow your items to disable standard inputs behaviors such + // as Tab or Alt key handling, Mouse Wheel scrolling, etc. + // e.g. Button(...); SetItemKeyOwner(ImGuiKey_MouseWheelY); to make hovering/activating a button disable wheel for scrolling. + // - Reminder ImGuiKey enum include access to mouse buttons and gamepad, so key ownership can apply to them. + // - Many related features are still in imgui_internal.h. For instance, most IsKeyXXX()/IsMouseXXX() functions have an owner-id-aware version. + IMGUI_API void SetItemKeyOwner(ImGuiKey key); // Set key owner to last item ID if it is hovered or active. Equivalent to 'if (IsItemHovered() || IsItemActive()) { SetKeyOwner(key, GetItemID());'. + + // Inputs Utilities: Mouse + // - To refer to a mouse button, you may use named enums in your code e.g. ImGuiMouseButton_Left, ImGuiMouseButton_Right. + // - You can also use regular integer: it is forever guaranteed that 0=Left, 1=Right, 2=Middle. + // - Dragging operations are only reported after mouse has moved a certain distance away from the initial clicking position (see 'lock_threshold' and 'io.MouseDraggingThreshold') + IMGUI_API bool IsMouseDown(ImGuiMouseButton button); // is mouse button held? + IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, bool repeat = false); // did mouse button clicked? (went from !Down to Down). Same as GetMouseClickedCount() == 1. + IMGUI_API bool IsMouseReleased(ImGuiMouseButton button); // did mouse button released? (went from Down to !Down) + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button); // did mouse button double-clicked? Same as GetMouseClickedCount() == 2. (note that a double-click will also report IsMouseClicked() == true) + IMGUI_API bool IsMouseReleasedWithDelay(ImGuiMouseButton button, float delay); // delayed mouse release (use very sparingly!). Generally used with 'delay >= io.MouseDoubleClickTime' + combined with a 'io.MouseClickedLastCount==1' test. This is a very rarely used UI idiom, but some apps use this: e.g. MS Explorer single click on an icon to rename. + IMGUI_API int GetMouseClickedCount(ImGuiMouseButton button); // return the number of successive mouse-clicks at the time where a click happen (otherwise 0). + IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true);// is mouse hovering given bounding rect (in screen space). clipped by current clipping settings, but disregarding of other consideration of focus/window ordering/popup-block. + IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // by convention we use (-FLT_MAX,-FLT_MAX) to denote that there is no mouse available + IMGUI_API bool IsAnyMouseDown(); // [WILL OBSOLETE] is any mouse button held? This was designed for backends, but prefer having backend maintain a mask of held mouse buttons, because upcoming input queue system will make this invalid. + IMGUI_API ImVec2 GetMousePos(); // shortcut to ImGui::GetIO().MousePos provided by user, to be consistent with other calls + IMGUI_API ImVec2 GetMousePosOnOpeningCurrentPopup(); // retrieve mouse position at the time of opening popup we have BeginPopup() into (helper to avoid user backing that value themselves) + IMGUI_API bool IsMouseDragging(ImGuiMouseButton button, float lock_threshold = -1.0f); // is mouse dragging? (uses io.MouseDraggingThreshold if lock_threshold < 0.0f) + IMGUI_API ImVec2 GetMouseDragDelta(ImGuiMouseButton button = 0, float lock_threshold = -1.0f); // return the delta from the initial clicking position while the mouse button is pressed or was just released. This is locked and return 0.0f until the mouse moves past a distance threshold at least once (uses io.MouseDraggingThreshold if lock_threshold < 0.0f) + IMGUI_API void ResetMouseDragDelta(ImGuiMouseButton button = 0); // + IMGUI_API ImGuiMouseCursor GetMouseCursor(); // get desired mouse cursor shape. Important: reset in ImGui::NewFrame(), this is updated during the frame. valid before Render(). If you use software rendering by setting io.MouseDrawCursor ImGui will render those for you + IMGUI_API void SetMouseCursor(ImGuiMouseCursor cursor_type); // set desired mouse cursor shape + IMGUI_API void SetNextFrameWantCaptureMouse(bool want_capture_mouse); // Override io.WantCaptureMouse flag next frame (said flag is left for your application to handle, typical when true it instucts your app to ignore inputs). This is equivalent to setting "io.WantCaptureMouse = want_capture_mouse;" after the next NewFrame() call. + + // Clipboard Utilities + // - Also see the LogToClipboard() function to capture GUI into clipboard, or easily output text data to the clipboard. IMGUI_API const char* GetClipboardText(); IMGUI_API void SetClipboardText(const char* text); // Settings/.Ini Utilities // - The disk functions are automatically called if io.IniFilename != NULL (default is "imgui.ini"). // - Set io.IniFilename to NULL to load/save manually. Read io.WantSaveIniSettings description about handling .ini saving manually. + // - Important: default value "imgui.ini" is relative to current working dir! Most apps will want to lock this to an absolute path (e.g. same path as executables). IMGUI_API void LoadIniSettingsFromDisk(const char* ini_filename); // call after CreateContext() and before the first call to NewFrame(). NewFrame() automatically calls LoadIniSettingsFromDisk(io.IniFilename). IMGUI_API void LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size=0); // call after CreateContext() and before the first call to NewFrame() to provide .ini data from your own data source. IMGUI_API void SaveIniSettingsToDisk(const char* ini_filename); // this is automatically called (if io.IniFilename is not empty) a few seconds after any modification that should be reflected in the .ini file (and also by DestroyContext). IMGUI_API const char* SaveIniSettingsToMemory(size_t* out_ini_size = NULL); // return a zero-terminated string with the .ini data which you can save by your own mean. call when io.WantSaveIniSettings is set, then save data by your own mean and clear io.WantSaveIniSettings. + // Debug Utilities + // - Your main debugging friend is the ShowMetricsWindow() function, which is also accessible from Demo->Tools->Metrics Debugger + IMGUI_API void DebugTextEncoding(const char* text); + IMGUI_API void DebugFlashStyleColor(ImGuiCol idx); + IMGUI_API void DebugStartItemPicker(); + IMGUI_API bool DebugCheckVersionAndDataLayout(const char* version_str, size_t sz_io, size_t sz_style, size_t sz_vec2, size_t sz_vec4, size_t sz_drawvert, size_t sz_drawidx); // This is called by IMGUI_CHECKVERSION() macro. +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + IMGUI_API void DebugLog(const char* fmt, ...) IM_FMTARGS(1); // Call via IMGUI_DEBUG_LOG() for maximum stripping in caller code! + IMGUI_API void DebugLogV(const char* fmt, va_list args) IM_FMTLIST(1); +#endif + // Memory Allocators - // - All those functions are not reliant on the current context. - // - If you reload the contents of imgui.cpp at runtime, you may need to call SetCurrentContext() + SetAllocatorFunctions() again because we use global storage for those. - IMGUI_API void SetAllocatorFunctions(void* (*alloc_func)(size_t sz, void* user_data), void (*free_func)(void* ptr, void* user_data), void* user_data = NULL); + // - Those functions are not reliant on the current context. + // - DLL users: heaps and globals are not shared across DLL boundaries! You will need to call SetCurrentContext() + SetAllocatorFunctions() + // for each static/DLL boundary you are calling from. Read "Context and Memory Allocators" section of imgui.cpp for more details. + IMGUI_API void SetAllocatorFunctions(ImGuiMemAllocFunc alloc_func, ImGuiMemFreeFunc free_func, void* user_data = NULL); + IMGUI_API void GetAllocatorFunctions(ImGuiMemAllocFunc* p_alloc_func, ImGuiMemFreeFunc* p_free_func, void** p_user_data); IMGUI_API void* MemAlloc(size_t size); IMGUI_API void MemFree(void* ptr); - // by zfedoran, https://github.com/ocornut/imgui/issues/1901 - IMGUI_API bool BufferingBar( const char* label, float value, const ImVec2& size_arg, const ImU32& bg_col, const ImU32& fg_col ); - IMGUI_API bool Spinner( const char* label, float radius, int thickness, const ImU32& color ); + // (Optional) Platform/OS interface for multi-viewport support + // Read comments around the ImGuiPlatformIO structure for more details. + // Note: You may use GetWindowViewport() to get the current viewport of the current window. + IMGUI_API void UpdatePlatformWindows(); // call in main loop. will call CreateWindow/ResizeWindow/etc. platform functions for each secondary viewport, and DestroyWindow for each inactive viewport. + IMGUI_API void RenderPlatformWindowsDefault(void* platform_render_arg = NULL, void* renderer_render_arg = NULL); // call in main loop. will call RenderWindow/SwapBuffers platform functions for each secondary viewport which doesn't have the ImGuiViewportFlags_Minimized flag set. May be reimplemented by user for custom rendering needs. + IMGUI_API void DestroyPlatformWindows(); // call DestroyWindow platform functions for all viewports. call from backend Shutdown() if you need to close platform windows before imgui shutdown. otherwise will be called by DestroyContext(). + IMGUI_API ImGuiViewport* FindViewportByID(ImGuiID id); // this is a helper for backends. + IMGUI_API ImGuiViewport* FindViewportByPlatformHandle(void* platform_handle); // this is a helper for backends. the type platform_handle is decided by the backend (e.g. HWND, MyWindow*, GLFWwindow* etc.) } // namespace ImGui //----------------------------------------------------------------------------- -// Flags & Enumerations +// [SECTION] Flags & Enumerations //----------------------------------------------------------------------------- // Flags for ImGui::Begin() +// (Those are per-window flags. There are shared flags in ImGuiIO: io.ConfigWindowsResizeFromEdges and io.ConfigWindowsMoveFromTitleBarOnly) enum ImGuiWindowFlags_ { ImGuiWindowFlags_None = 0, @@ -720,7 +1114,7 @@ enum ImGuiWindowFlags_ ImGuiWindowFlags_NoMove = 1 << 2, // Disable user moving the window ImGuiWindowFlags_NoScrollbar = 1 << 3, // Disable scrollbars (window can still scroll with mouse or programmatically) ImGuiWindowFlags_NoScrollWithMouse = 1 << 4, // Disable user vertically scrolling with mouse wheel. On child window, mouse wheel will be forwarded to the parent unless NoScrollbar is also set. - ImGuiWindowFlags_NoCollapse = 1 << 5, // Disable user collapsing window by double-clicking on it + ImGuiWindowFlags_NoCollapse = 1 << 5, // Disable user collapsing window by double-clicking on it. Also referred to as Window Menu Button (e.g. within a docking node). ImGuiWindowFlags_AlwaysAutoResize = 1 << 6, // Resize every window to its content every frame ImGuiWindowFlags_NoBackground = 1 << 7, // Disable drawing background color (WindowBg, etc.) and outside border. Similar as using SetNextWindowBgAlpha(0.0f). ImGuiWindowFlags_NoSavedSettings = 1 << 8, // Never load/save settings in .ini file @@ -731,53 +1125,111 @@ enum ImGuiWindowFlags_ ImGuiWindowFlags_NoBringToFrontOnFocus = 1 << 13, // Disable bringing window to front when taking focus (e.g. clicking on it or programmatically giving it focus) ImGuiWindowFlags_AlwaysVerticalScrollbar= 1 << 14, // Always show vertical scrollbar (even if ContentSize.y < Size.y) ImGuiWindowFlags_AlwaysHorizontalScrollbar=1<< 15, // Always show horizontal scrollbar (even if ContentSize.x < Size.x) - ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 16, // Ensure child windows without border uses style.WindowPadding (ignored by default for non-bordered child windows, because more convenient) - ImGuiWindowFlags_NoNavInputs = 1 << 18, // No gamepad/keyboard navigation within the window - ImGuiWindowFlags_NoNavFocus = 1 << 19, // No focusing toward this window with gamepad/keyboard navigation (e.g. skipped by CTRL+TAB) - ImGuiWindowFlags_UnsavedDocument = 1 << 20, // Append '*' to title without affecting the ID, as a convenience to avoid using the ### operator. When used in a tab/docking context, tab is selected on closure and closure is deferred by one frame to allow code to cancel the closure (with a confirmation popup, etc.) without flicker. + ImGuiWindowFlags_NoNavInputs = 1 << 16, // No keyboard/gamepad navigation within the window + ImGuiWindowFlags_NoNavFocus = 1 << 17, // No focusing toward this window with keyboard/gamepad navigation (e.g. skipped by CTRL+TAB) + ImGuiWindowFlags_UnsavedDocument = 1 << 18, // Display a dot next to the title. When used in a tab/docking context, tab is selected when clicking the X + closure is not assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + ImGuiWindowFlags_NoDocking = 1 << 19, // Disable docking of this window ImGuiWindowFlags_NoNav = ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, ImGuiWindowFlags_NoDecoration = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse, ImGuiWindowFlags_NoInputs = ImGuiWindowFlags_NoMouseInputs | ImGuiWindowFlags_NoNavInputs | ImGuiWindowFlags_NoNavFocus, // [Internal] - ImGuiWindowFlags_NavFlattened = 1 << 23, // [BETA] Allow gamepad/keyboard navigation to cross over parent border to this child (only use on child that have no scrolling!) + ImGuiWindowFlags_DockNodeHost = 1 << 23, // Don't use! For internal use by Begin()/NewFrame() ImGuiWindowFlags_ChildWindow = 1 << 24, // Don't use! For internal use by BeginChild() ImGuiWindowFlags_Tooltip = 1 << 25, // Don't use! For internal use by BeginTooltip() ImGuiWindowFlags_Popup = 1 << 26, // Don't use! For internal use by BeginPopup() ImGuiWindowFlags_Modal = 1 << 27, // Don't use! For internal use by BeginPopupModal() - ImGuiWindowFlags_ChildMenu = 1 << 28 // Don't use! For internal use by BeginMenu() + ImGuiWindowFlags_ChildMenu = 1 << 28, // Don't use! For internal use by BeginMenu() - // [Obsolete] - //ImGuiWindowFlags_ShowBorders = 1 << 7, // --> Set style.FrameBorderSize=1.0f / style.WindowBorderSize=1.0f to enable borders around windows and items - //ImGuiWindowFlags_ResizeFromAnySide = 1 << 17, // --> Set io.ConfigWindowsResizeFromEdges and make sure mouse cursors are supported by back-end (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiWindowFlags_NavFlattened = 1 << 29, // Obsoleted in 1.90.9: Use ImGuiChildFlags_NavFlattened in BeginChild() call. + ImGuiWindowFlags_AlwaysUseWindowPadding = 1 << 30, // Obsoleted in 1.90.0: Use ImGuiChildFlags_AlwaysUseWindowPadding in BeginChild() call. +#endif +}; + +// Flags for ImGui::BeginChild() +// (Legacy: bit 0 must always correspond to ImGuiChildFlags_Borders to be backward compatible with old API using 'bool border = false'. +// About using AutoResizeX/AutoResizeY flags: +// - May be combined with SetNextWindowSizeConstraints() to set a min/max size for each axis (see "Demo->Child->Auto-resize with Constraints"). +// - Size measurement for a given axis is only performed when the child window is within visible boundaries, or is just appearing. +// - This allows BeginChild() to return false when not within boundaries (e.g. when scrolling), which is more optimal. BUT it won't update its auto-size while clipped. +// While not perfect, it is a better default behavior as the always-on performance gain is more valuable than the occasional "resizing after becoming visible again" glitch. +// - You may also use ImGuiChildFlags_AlwaysAutoResize to force an update even when child window is not in view. +// HOWEVER PLEASE UNDERSTAND THAT DOING SO WILL PREVENT BeginChild() FROM EVER RETURNING FALSE, disabling benefits of coarse clipping. +enum ImGuiChildFlags_ +{ + ImGuiChildFlags_None = 0, + ImGuiChildFlags_Borders = 1 << 0, // Show an outer border and enable WindowPadding. (IMPORTANT: this is always == 1 == true for legacy reason) + ImGuiChildFlags_AlwaysUseWindowPadding = 1 << 1, // Pad with style.WindowPadding even if no border are drawn (no padding by default for non-bordered child windows because it makes more sense) + ImGuiChildFlags_ResizeX = 1 << 2, // Allow resize from right border (layout direction). Enable .ini saving (unless ImGuiWindowFlags_NoSavedSettings passed to window flags) + ImGuiChildFlags_ResizeY = 1 << 3, // Allow resize from bottom border (layout direction). " + ImGuiChildFlags_AutoResizeX = 1 << 4, // Enable auto-resizing width. Read "IMPORTANT: Size measurement" details above. + ImGuiChildFlags_AutoResizeY = 1 << 5, // Enable auto-resizing height. Read "IMPORTANT: Size measurement" details above. + ImGuiChildFlags_AlwaysAutoResize = 1 << 6, // Combined with AutoResizeX/AutoResizeY. Always measure size even when child is hidden, always return true, always disable clipping optimization! NOT RECOMMENDED. + ImGuiChildFlags_FrameStyle = 1 << 7, // Style the child window like a framed item: use FrameBg, FrameRounding, FrameBorderSize, FramePadding instead of ChildBg, ChildRounding, ChildBorderSize, WindowPadding. + ImGuiChildFlags_NavFlattened = 1 << 8, // [BETA] Share focus scope, allow keyboard/gamepad navigation to cross over parent border to this child or between sibling child windows. + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiChildFlags_Border = ImGuiChildFlags_Borders, // Renamed in 1.91.1 (August 2024) for consistency. +#endif +}; + +// Flags for ImGui::PushItemFlag() +// (Those are shared by all items) +enum ImGuiItemFlags_ +{ + ImGuiItemFlags_None = 0, // (Default) + ImGuiItemFlags_NoTabStop = 1 << 0, // false // Disable keyboard tabbing. This is a "lighter" version of ImGuiItemFlags_NoNav. + ImGuiItemFlags_NoNav = 1 << 1, // false // Disable any form of focusing (keyboard/gamepad directional navigation and SetKeyboardFocusHere() calls). + ImGuiItemFlags_NoNavDefaultFocus = 1 << 2, // false // Disable item being a candidate for default focus (e.g. used by title bar items). + ImGuiItemFlags_ButtonRepeat = 1 << 3, // false // Any button-like behavior will have repeat mode enabled (based on io.KeyRepeatDelay and io.KeyRepeatRate values). Note that you can also call IsItemActive() after any button to tell if it is being held. + ImGuiItemFlags_AutoClosePopups = 1 << 4, // true // MenuItem()/Selectable() automatically close their parent popup window. + ImGuiItemFlags_AllowDuplicateId = 1 << 5, // false // Allow submitting an item with the same identifier as an item already submitted this frame without triggering a warning tooltip if io.ConfigDebugHighlightIdConflicts is set. }; // Flags for ImGui::InputText() +// (Those are per-item flags. There are shared flags in ImGuiIO: io.ConfigInputTextCursorBlink and io.ConfigInputTextEnterKeepActive) enum ImGuiInputTextFlags_ { + // Basic filters (also see ImGuiInputTextFlags_CallbackCharFilter) ImGuiInputTextFlags_None = 0, ImGuiInputTextFlags_CharsDecimal = 1 << 0, // Allow 0123456789.+-*/ ImGuiInputTextFlags_CharsHexadecimal = 1 << 1, // Allow 0123456789ABCDEFabcdef - ImGuiInputTextFlags_CharsUppercase = 1 << 2, // Turn a..z into A..Z - ImGuiInputTextFlags_CharsNoBlank = 1 << 3, // Filter out spaces, tabs - ImGuiInputTextFlags_AutoSelectAll = 1 << 4, // Select entire text when first taking mouse focus - ImGuiInputTextFlags_EnterReturnsTrue = 1 << 5, // Return 'true' when Enter is pressed (as opposed to every time the value was modified). Consider looking at the IsItemDeactivatedAfterEdit() function. - ImGuiInputTextFlags_CallbackCompletion = 1 << 6, // Callback on pressing TAB (for completion handling) - ImGuiInputTextFlags_CallbackHistory = 1 << 7, // Callback on pressing Up/Down arrows (for history handling) - ImGuiInputTextFlags_CallbackAlways = 1 << 8, // Callback on each iteration. User code may query cursor position, modify text buffer. - ImGuiInputTextFlags_CallbackCharFilter = 1 << 9, // Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. - ImGuiInputTextFlags_AllowTabInput = 1 << 10, // Pressing TAB input a '\t' character into the text field - ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 11, // In multi-line mode, unfocus with Enter, add new line with Ctrl+Enter (default is opposite: unfocus with Ctrl+Enter, add line with Enter). - ImGuiInputTextFlags_NoHorizontalScroll = 1 << 12, // Disable following the cursor horizontally - ImGuiInputTextFlags_AlwaysInsertMode = 1 << 13, // Insert mode - ImGuiInputTextFlags_ReadOnly = 1 << 14, // Read-only mode - ImGuiInputTextFlags_Password = 1 << 15, // Password mode, display all characters as '*' + ImGuiInputTextFlags_CharsScientific = 1 << 2, // Allow 0123456789.+-*/eE (Scientific notation input) + ImGuiInputTextFlags_CharsUppercase = 1 << 3, // Turn a..z into A..Z + ImGuiInputTextFlags_CharsNoBlank = 1 << 4, // Filter out spaces, tabs + + // Inputs + ImGuiInputTextFlags_AllowTabInput = 1 << 5, // Pressing TAB input a '\t' character into the text field + ImGuiInputTextFlags_EnterReturnsTrue = 1 << 6, // Return 'true' when Enter is pressed (as opposed to every time the value was modified). Consider using IsItemDeactivatedAfterEdit() instead! + ImGuiInputTextFlags_EscapeClearsAll = 1 << 7, // Escape key clears content if not empty, and deactivate otherwise (contrast to default behavior of Escape to revert) + ImGuiInputTextFlags_CtrlEnterForNewLine = 1 << 8, // In multi-line mode, validate with Enter, add new line with Ctrl+Enter (default is opposite: validate with Ctrl+Enter, add line with Enter). + + // Other options + ImGuiInputTextFlags_ReadOnly = 1 << 9, // Read-only mode + ImGuiInputTextFlags_Password = 1 << 10, // Password mode, display all characters as '*', disable copy + ImGuiInputTextFlags_AlwaysOverwrite = 1 << 11, // Overwrite mode + ImGuiInputTextFlags_AutoSelectAll = 1 << 12, // Select entire text when first taking mouse focus + ImGuiInputTextFlags_ParseEmptyRefVal = 1 << 13, // InputFloat(), InputInt(), InputScalar() etc. only: parse empty string as zero value. + ImGuiInputTextFlags_DisplayEmptyRefVal = 1 << 14, // InputFloat(), InputInt(), InputScalar() etc. only: when value is zero, do not display it. Generally used with ImGuiInputTextFlags_ParseEmptyRefVal. + ImGuiInputTextFlags_NoHorizontalScroll = 1 << 15, // Disable following the cursor horizontally ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID(). - ImGuiInputTextFlags_CharsScientific = 1 << 17, // Allow 0123456789.+-*/eE (Scientific notation input) - ImGuiInputTextFlags_CallbackResize = 1 << 18, // Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. Notify when the string wants to be resized (for string types which hold a cache of their Size). You will be provided a new BufSize in the callback and NEED to honor it. (see misc/cpp/imgui_stdlib.h for an example of using this) - // [Internal] - ImGuiInputTextFlags_Multiline = 1 << 20, // For internal use by InputTextMultiline() - ImGuiInputTextFlags_NoMarkEdited = 1 << 21 // For internal use by functions using InputText() before reformatting data + + // Elide display / Alignment + ImGuiInputTextFlags_ElideLeft = 1 << 17, // When text doesn't fit, elide left side to ensure right side stays visible. Useful for path/filenames. Single-line only! + + // Callback features + ImGuiInputTextFlags_CallbackCompletion = 1 << 18, // Callback on pressing TAB (for completion handling) + ImGuiInputTextFlags_CallbackHistory = 1 << 19, // Callback on pressing Up/Down arrows (for history handling) + ImGuiInputTextFlags_CallbackAlways = 1 << 20, // Callback on each iteration. User code may query cursor position, modify text buffer. + ImGuiInputTextFlags_CallbackCharFilter = 1 << 21, // Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. + ImGuiInputTextFlags_CallbackResize = 1 << 22, // Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. Notify when the string wants to be resized (for string types which hold a cache of their Size). You will be provided a new BufSize in the callback and NEED to honor it. (see misc/cpp/imgui_stdlib.h for an example of using this) + ImGuiInputTextFlags_CallbackEdit = 1 << 23, // Callback on any edit. Note that InputText() already returns true on edit + you can always use IsItemEdited(). The callback is useful to manipulate the underlying buffer while focus is active. + + // Obsolete names + //ImGuiInputTextFlags_AlwaysInsertMode = ImGuiInputTextFlags_AlwaysOverwrite // [renamed in 1.82] name was not matching behavior }; // Flags for ImGui::TreeNodeEx(), ImGui::CollapsingHeader*() @@ -785,37 +1237,71 @@ enum ImGuiTreeNodeFlags_ { ImGuiTreeNodeFlags_None = 0, ImGuiTreeNodeFlags_Selected = 1 << 0, // Draw as selected - ImGuiTreeNodeFlags_Framed = 1 << 1, // Full colored frame (e.g. for CollapsingHeader) - ImGuiTreeNodeFlags_AllowItemOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one + ImGuiTreeNodeFlags_Framed = 1 << 1, // Draw frame with background (e.g. for CollapsingHeader) + ImGuiTreeNodeFlags_AllowOverlap = 1 << 2, // Hit testing to allow subsequent widgets to overlap this one ImGuiTreeNodeFlags_NoTreePushOnOpen = 1 << 3, // Don't do a TreePush() when open (e.g. for CollapsingHeader) = no extra indent nor pushing on ID stack ImGuiTreeNodeFlags_NoAutoOpenOnLog = 1 << 4, // Don't automatically and temporarily open node when Logging is active (by default logging will automatically open tree nodes) ImGuiTreeNodeFlags_DefaultOpen = 1 << 5, // Default node to be open - ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Need double-click to open node - ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Only open when clicking on the arrow part. If ImGuiTreeNodeFlags_OpenOnDoubleClick is also set, single-click arrow or double-click all box to open. + ImGuiTreeNodeFlags_OpenOnDoubleClick = 1 << 6, // Open on double-click instead of simple click (default for multi-select unless any _OpenOnXXX behavior is set explicitly). Both behaviors may be combined. + ImGuiTreeNodeFlags_OpenOnArrow = 1 << 7, // Open when clicking on the arrow part (default for multi-select unless any _OpenOnXXX behavior is set explicitly). Both behaviors may be combined. ImGuiTreeNodeFlags_Leaf = 1 << 8, // No collapsing, no arrow (use as a convenience for leaf nodes). - ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow - ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding(). - ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line. In the future we may refactor the hit system to be front-to-back, allowing natural overlaps and then this can become the default. - ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (bypass the indented area). - ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 13, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) - //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 14, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible - ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog + ImGuiTreeNodeFlags_Bullet = 1 << 9, // Display a bullet instead of arrow. IMPORTANT: node can still be marked open/close if you don't set the _Leaf flag! + ImGuiTreeNodeFlags_FramePadding = 1 << 10, // Use FramePadding (even for an unframed text node) to vertically align text baseline to regular widget height. Equivalent to calling AlignTextToFramePadding() before the node. + ImGuiTreeNodeFlags_SpanAvailWidth = 1 << 11, // Extend hit box to the right-most edge, even if not framed. This is not the default in order to allow adding other items on the same line without using AllowOverlap mode. + ImGuiTreeNodeFlags_SpanFullWidth = 1 << 12, // Extend hit box to the left-most and right-most edges (cover the indent area). + ImGuiTreeNodeFlags_SpanLabelWidth = 1 << 13, // Narrow hit box + narrow hovering highlight, will only cover the label text. + ImGuiTreeNodeFlags_SpanAllColumns = 1 << 14, // Frame will span all columns of its container table (label will still fit in current column) + ImGuiTreeNodeFlags_LabelSpanAllColumns = 1 << 15, // Label will span all columns of its container table + //ImGuiTreeNodeFlags_NoScrollOnOpen = 1 << 16, // FIXME: TODO: Disable automatic scroll on TreePop() if node got just open and contents is not visible + ImGuiTreeNodeFlags_NavLeftJumpsBackHere = 1 << 17, // (WIP) Nav: left direction may move to this TreeNode() from any of its child (items submitted between TreeNode and TreePop) + ImGuiTreeNodeFlags_CollapsingHeader = ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_NoAutoOpenOnLog, - // Obsolete names (will be removed) #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , ImGuiTreeNodeFlags_AllowOverlapMode = ImGuiTreeNodeFlags_AllowItemOverlap // [renamed in 1.53] + ImGuiTreeNodeFlags_AllowItemOverlap = ImGuiTreeNodeFlags_AllowOverlap, // Renamed in 1.89.7 + ImGuiTreeNodeFlags_SpanTextWidth = ImGuiTreeNodeFlags_SpanLabelWidth,// Renamed in 1.90.7 #endif }; +// Flags for OpenPopup*(), BeginPopupContext*(), IsPopupOpen() functions. +// - To be backward compatible with older API which took an 'int mouse_button = 1' argument instead of 'ImGuiPopupFlags flags', +// we need to treat small flags values as a mouse button index, so we encode the mouse button in the first few bits of the flags. +// It is therefore guaranteed to be legal to pass a mouse button index in ImGuiPopupFlags. +// - For the same reason, we exceptionally default the ImGuiPopupFlags argument of BeginPopupContextXXX functions to 1 instead of 0. +// IMPORTANT: because the default parameter is 1 (==ImGuiPopupFlags_MouseButtonRight), if you rely on the default parameter +// and want to use another flag, you need to pass in the ImGuiPopupFlags_MouseButtonRight flag explicitly. +// - Multiple buttons currently cannot be combined/or-ed in those functions (we could allow it later). +enum ImGuiPopupFlags_ +{ + ImGuiPopupFlags_None = 0, + ImGuiPopupFlags_MouseButtonLeft = 0, // For BeginPopupContext*(): open on Left Mouse release. Guaranteed to always be == 0 (same as ImGuiMouseButton_Left) + ImGuiPopupFlags_MouseButtonRight = 1, // For BeginPopupContext*(): open on Right Mouse release. Guaranteed to always be == 1 (same as ImGuiMouseButton_Right) + ImGuiPopupFlags_MouseButtonMiddle = 2, // For BeginPopupContext*(): open on Middle Mouse release. Guaranteed to always be == 2 (same as ImGuiMouseButton_Middle) + ImGuiPopupFlags_MouseButtonMask_ = 0x1F, + ImGuiPopupFlags_MouseButtonDefault_ = 1, + ImGuiPopupFlags_NoReopen = 1 << 5, // For OpenPopup*(), BeginPopupContext*(): don't reopen same popup if already open (won't reposition, won't reinitialize navigation) + //ImGuiPopupFlags_NoReopenAlwaysNavInit = 1 << 6, // For OpenPopup*(), BeginPopupContext*(): focus and initialize navigation even when not reopening. + ImGuiPopupFlags_NoOpenOverExistingPopup = 1 << 7, // For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack + ImGuiPopupFlags_NoOpenOverItems = 1 << 8, // For BeginPopupContextWindow(): don't return true when hovering items, only when hovering empty space + ImGuiPopupFlags_AnyPopupId = 1 << 10, // For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup. + ImGuiPopupFlags_AnyPopupLevel = 1 << 11, // For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level) + ImGuiPopupFlags_AnyPopup = ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel, +}; + // Flags for ImGui::Selectable() enum ImGuiSelectableFlags_ { ImGuiSelectableFlags_None = 0, - ImGuiSelectableFlags_DontClosePopups = 1 << 0, // Clicking this don't close parent popup window - ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Selectable frame can span all columns (text will still fit in current column) + ImGuiSelectableFlags_NoAutoClosePopups = 1 << 0, // Clicking this doesn't close parent popup window (overrides ImGuiItemFlags_AutoClosePopups) + ImGuiSelectableFlags_SpanAllColumns = 1 << 1, // Frame will span all columns of its container table (text will still fit in current column) ImGuiSelectableFlags_AllowDoubleClick = 1 << 2, // Generate press events on double clicks too ImGuiSelectableFlags_Disabled = 1 << 3, // Cannot be selected, display grayed out text - ImGuiSelectableFlags_AllowItemOverlap = 1 << 4 // (WIP) Hit testing to allow subsequent widgets to overlap this one + ImGuiSelectableFlags_AllowOverlap = 1 << 4, // (WIP) Hit testing to allow subsequent widgets to overlap this one + ImGuiSelectableFlags_Highlight = 1 << 5, // Make the item be displayed as if it is hovered + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiSelectableFlags_DontClosePopups = ImGuiSelectableFlags_NoAutoClosePopups, // Renamed in 1.91.0 + ImGuiSelectableFlags_AllowItemOverlap = ImGuiSelectableFlags_AllowOverlap, // Renamed in 1.89.7 +#endif }; // Flags for ImGui::BeginCombo() @@ -829,7 +1315,8 @@ enum ImGuiComboFlags_ ImGuiComboFlags_HeightLargest = 1 << 4, // As many fitting items as possible ImGuiComboFlags_NoArrowButton = 1 << 5, // Display on the preview box without the square arrow button ImGuiComboFlags_NoPreview = 1 << 6, // Display only a square arrow button - ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest + ImGuiComboFlags_WidthFitPreview = 1 << 7, // Width dynamically calculated from preview contents + ImGuiComboFlags_HeightMask_ = ImGuiComboFlags_HeightSmall | ImGuiComboFlags_HeightRegular | ImGuiComboFlags_HeightLarge | ImGuiComboFlags_HeightLargest, }; // Flags for ImGui::BeginTabBar() @@ -839,37 +1326,45 @@ enum ImGuiTabBarFlags_ ImGuiTabBarFlags_Reorderable = 1 << 0, // Allow manually dragging tabs to re-order them + New tabs are appended at the end of list ImGuiTabBarFlags_AutoSelectNewTabs = 1 << 1, // Automatically select new tabs when they appear ImGuiTabBarFlags_TabListPopupButton = 1 << 2, // Disable buttons to open the tab list popup - ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabBarFlags_NoCloseWithMiddleMouseButton = 1 << 3, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You may handle this behavior manually on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. ImGuiTabBarFlags_NoTabListScrollingButtons = 1 << 4, // Disable scrolling buttons (apply when fitting policy is ImGuiTabBarFlags_FittingPolicyScroll) ImGuiTabBarFlags_NoTooltip = 1 << 5, // Disable tooltips when hovering a tab - ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 6, // Resize tabs when they don't fit - ImGuiTabBarFlags_FittingPolicyScroll = 1 << 7, // Add scroll buttons when tabs don't fit + ImGuiTabBarFlags_DrawSelectedOverline = 1 << 6, // Draw selected overline markers over selected tab + ImGuiTabBarFlags_FittingPolicyResizeDown = 1 << 7, // Resize tabs when they don't fit + ImGuiTabBarFlags_FittingPolicyScroll = 1 << 8, // Add scroll buttons when tabs don't fit ImGuiTabBarFlags_FittingPolicyMask_ = ImGuiTabBarFlags_FittingPolicyResizeDown | ImGuiTabBarFlags_FittingPolicyScroll, - ImGuiTabBarFlags_FittingPolicyDefault_ = ImGuiTabBarFlags_FittingPolicyResizeDown + ImGuiTabBarFlags_FittingPolicyDefault_ = ImGuiTabBarFlags_FittingPolicyResizeDown, }; // Flags for ImGui::BeginTabItem() enum ImGuiTabItemFlags_ { ImGuiTabItemFlags_None = 0, - ImGuiTabItemFlags_UnsavedDocument = 1 << 0, // Append '*' to title without affecting the ID, as a convenience to avoid using the ### operator. Also: tab is selected on closure and closure is deferred by one frame to allow code to undo it without flicker. + ImGuiTabItemFlags_UnsavedDocument = 1 << 0, // Display a dot next to the title + set ImGuiTabItemFlags_NoAssumedClosure. ImGuiTabItemFlags_SetSelected = 1 << 1, // Trigger flag to programmatically make the tab selected when calling BeginTabItem() - ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You can still repro this behavior on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. - ImGuiTabItemFlags_NoPushId = 1 << 3 // Don't call PushID(tab->ID)/PopID() on BeginTabItem()/EndTabItem() + ImGuiTabItemFlags_NoCloseWithMiddleMouseButton = 1 << 2, // Disable behavior of closing tabs (that are submitted with p_open != NULL) with middle mouse button. You may handle this behavior manually on user's side with if (IsItemHovered() && IsMouseClicked(2)) *p_open = false. + ImGuiTabItemFlags_NoPushId = 1 << 3, // Don't call PushID()/PopID() on BeginTabItem()/EndTabItem() + ImGuiTabItemFlags_NoTooltip = 1 << 4, // Disable tooltip for the given tab + ImGuiTabItemFlags_NoReorder = 1 << 5, // Disable reordering this tab or having another tab cross over this tab + ImGuiTabItemFlags_Leading = 1 << 6, // Enforce the tab position to the left of the tab bar (after the tab list popup button) + ImGuiTabItemFlags_Trailing = 1 << 7, // Enforce the tab position to the right of the tab bar (before the scrolling buttons) + ImGuiTabItemFlags_NoAssumedClosure = 1 << 8, // Tab is selected when trying to close + closure is not immediately assumed (will wait for user to stop submitting the tab). Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. }; // Flags for ImGui::IsWindowFocused() enum ImGuiFocusedFlags_ { ImGuiFocusedFlags_None = 0, - ImGuiFocusedFlags_ChildWindows = 1 << 0, // IsWindowFocused(): Return true if any children of the window is focused - ImGuiFocusedFlags_RootWindow = 1 << 1, // IsWindowFocused(): Test from root window (top most parent of the current hierarchy) - ImGuiFocusedFlags_AnyWindow = 1 << 2, // IsWindowFocused(): Return true if any window is focused. Important: If you are trying to tell how to dispatch your low-level inputs, do NOT use this. Use ImGui::GetIO().WantCaptureMouse instead. - ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows + ImGuiFocusedFlags_ChildWindows = 1 << 0, // Return true if any children of the window is focused + ImGuiFocusedFlags_RootWindow = 1 << 1, // Test from root window (top most parent of the current hierarchy) + ImGuiFocusedFlags_AnyWindow = 1 << 2, // Return true if any window is focused. Important: If you are trying to tell how to dispatch your low-level inputs, do NOT use this. Use 'io.WantCaptureMouse' instead! Please read the FAQ! + ImGuiFocusedFlags_NoPopupHierarchy = 1 << 3, // Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + ImGuiFocusedFlags_DockHierarchy = 1 << 4, // Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiFocusedFlags_RootAndChildWindows = ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_ChildWindows, }; // Flags for ImGui::IsItemHovered(), ImGui::IsWindowHovered() -// Note: if you are trying to check whether your mouse should be dispatched to imgui or to your app, you should use the 'io.WantCaptureMouse' boolean for that. Please read the FAQ! +// Note: if you are trying to check whether your mouse should be dispatched to Dear ImGui or to your app, you should use 'io.WantCaptureMouse' instead! Please read the FAQ! // Note: windows with the ImGuiWindowFlags_NoInputs flag are ignored by IsWindowHovered() calls. enum ImGuiHoveredFlags_ { @@ -877,13 +1372,56 @@ enum ImGuiHoveredFlags_ ImGuiHoveredFlags_ChildWindows = 1 << 0, // IsWindowHovered() only: Return true if any children of the window is hovered ImGuiHoveredFlags_RootWindow = 1 << 1, // IsWindowHovered() only: Test from root window (top most parent of the current hierarchy) ImGuiHoveredFlags_AnyWindow = 1 << 2, // IsWindowHovered() only: Return true if any window is hovered - ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 3, // Return true even if a popup window is normally blocking access to this item/window - //ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 4, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. - ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 5, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. - ImGuiHoveredFlags_AllowWhenOverlapped = 1 << 6, // Return true even if the position is obstructed or overlapped by another window - ImGuiHoveredFlags_AllowWhenDisabled = 1 << 7, // Return true even if the item is disabled + ImGuiHoveredFlags_NoPopupHierarchy = 1 << 3, // IsWindowHovered() only: Do not consider popup hierarchy (do not treat popup emitter as parent of popup) (when used with _ChildWindows or _RootWindow) + ImGuiHoveredFlags_DockHierarchy = 1 << 4, // IsWindowHovered() only: Consider docking hierarchy (treat dockspace host as parent of docked window) (when used with _ChildWindows or _RootWindow) + ImGuiHoveredFlags_AllowWhenBlockedByPopup = 1 << 5, // Return true even if a popup window is normally blocking access to this item/window + //ImGuiHoveredFlags_AllowWhenBlockedByModal = 1 << 6, // Return true even if a modal popup window is normally blocking access to this item/window. FIXME-TODO: Unavailable yet. + ImGuiHoveredFlags_AllowWhenBlockedByActiveItem = 1 << 7, // Return true even if an active item is blocking access to this item/window. Useful for Drag and Drop patterns. + ImGuiHoveredFlags_AllowWhenOverlappedByItem = 1 << 8, // IsItemHovered() only: Return true even if the item uses AllowOverlap mode and is overlapped by another hoverable item. + ImGuiHoveredFlags_AllowWhenOverlappedByWindow = 1 << 9, // IsItemHovered() only: Return true even if the position is obstructed or overlapped by another window. + ImGuiHoveredFlags_AllowWhenDisabled = 1 << 10, // IsItemHovered() only: Return true even if the item is disabled + ImGuiHoveredFlags_NoNavOverride = 1 << 11, // IsItemHovered() only: Disable using keyboard/gamepad navigation state when active, always query mouse + ImGuiHoveredFlags_AllowWhenOverlapped = ImGuiHoveredFlags_AllowWhenOverlappedByItem | ImGuiHoveredFlags_AllowWhenOverlappedByWindow, ImGuiHoveredFlags_RectOnly = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped, - ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows + ImGuiHoveredFlags_RootAndChildWindows = ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_ChildWindows, + + // Tooltips mode + // - typically used in IsItemHovered() + SetTooltip() sequence. + // - this is a shortcut to pull flags from 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' where you can reconfigure desired behavior. + // e.g. 'TooltipHoveredFlagsForMouse' defaults to 'ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayShort'. + // - for frequently actioned or hovered items providing a tooltip, you want may to use ImGuiHoveredFlags_ForTooltip (stationary + delay) so the tooltip doesn't show too often. + // - for items which main purpose is to be hovered, or items with low affordance, or in less consistent apps, prefer no delay or shorter delay. + ImGuiHoveredFlags_ForTooltip = 1 << 12, // Shortcut for standard flags when using IsItemHovered() + SetTooltip() sequence. + + // (Advanced) Mouse Hovering delays. + // - generally you can use ImGuiHoveredFlags_ForTooltip to use application-standardized flags. + // - use those if you need specific overrides. + ImGuiHoveredFlags_Stationary = 1 << 13, // Require mouse to be stationary for style.HoverStationaryDelay (~0.15 sec) _at least one time_. After this, can move on same item/window. Using the stationary test tends to reduces the need for a long delay. + ImGuiHoveredFlags_DelayNone = 1 << 14, // IsItemHovered() only: Return true immediately (default). As this is the default you generally ignore this. + ImGuiHoveredFlags_DelayShort = 1 << 15, // IsItemHovered() only: Return true after style.HoverDelayShort elapsed (~0.15 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_DelayNormal = 1 << 16, // IsItemHovered() only: Return true after style.HoverDelayNormal elapsed (~0.40 sec) (shared between items) + requires mouse to be stationary for style.HoverStationaryDelay (once per item). + ImGuiHoveredFlags_NoSharedDelay = 1 << 17, // IsItemHovered() only: Disable shared delay system where moving from one item to the next keeps the previous timer for a short time (standard for tooltips with long delays) +}; + +// Flags for ImGui::DockSpace(), shared/inherited by child nodes. +// (Some flags can be applied to individual nodes directly) +// FIXME-DOCK: Also see ImGuiDockNodeFlagsPrivate_ which may involve using the WIP and internal DockBuilder api. +enum ImGuiDockNodeFlags_ +{ + ImGuiDockNodeFlags_None = 0, + ImGuiDockNodeFlags_KeepAliveOnly = 1 << 0, // // Don't display the dockspace node but keep it alive. Windows docked into this dockspace node won't be undocked. + //ImGuiDockNodeFlags_NoCentralNode = 1 << 1, // // Disable Central Node (the node which can stay empty) + ImGuiDockNodeFlags_NoDockingOverCentralNode = 1 << 2, // // Disable docking over the Central Node, which will be always kept empty. + ImGuiDockNodeFlags_PassthruCentralNode = 1 << 3, // // Enable passthru dockspace: 1) DockSpace() will render a ImGuiCol_WindowBg background covering everything excepted the Central Node when empty. Meaning the host window should probably use SetNextWindowBgAlpha(0.0f) prior to Begin() when using this. 2) When Central Node is empty: let inputs pass-through + won't display a DockingEmptyBg background. See demo for details. + ImGuiDockNodeFlags_NoDockingSplit = 1 << 4, // // Disable other windows/nodes from splitting this node. + ImGuiDockNodeFlags_NoResize = 1 << 5, // Saved // Disable resizing node using the splitter/separators. Useful with programmatically setup dockspaces. + ImGuiDockNodeFlags_AutoHideTabBar = 1 << 6, // // Tab bar will automatically hide when there is a single window in the dock node. + ImGuiDockNodeFlags_NoUndocking = 1 << 7, // // Disable undocking this node. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiDockNodeFlags_NoSplit = ImGuiDockNodeFlags_NoDockingSplit, // Renamed in 1.90 + ImGuiDockNodeFlags_NoDockingInCentralNode = ImGuiDockNodeFlags_NoDockingOverCentralNode, // Renamed in 1.90 +#endif }; // Flags for ImGui::BeginDragDropSource(), ImGui::AcceptDragDropPayload() @@ -891,17 +1429,23 @@ enum ImGuiDragDropFlags_ { ImGuiDragDropFlags_None = 0, // BeginDragDropSource() flags - ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disable this behavior. - ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return false, to avoid subsequent user code submitting tooltips. This flag disable this behavior so you can still call IsItemHovered() on the source item. + ImGuiDragDropFlags_SourceNoPreviewTooltip = 1 << 0, // Disable preview tooltip. By default, a successful call to BeginDragDropSource opens a tooltip so you can display a preview or description of the source contents. This flag disables this behavior. + ImGuiDragDropFlags_SourceNoDisableHover = 1 << 1, // By default, when dragging we clear data so that IsItemHovered() will return false, to avoid subsequent user code submitting tooltips. This flag disables this behavior so you can still call IsItemHovered() on the source item. ImGuiDragDropFlags_SourceNoHoldToOpenOthers = 1 << 2, // Disable the behavior that allows to open tree nodes and collapsing header by holding over them while dragging a source item. ImGuiDragDropFlags_SourceAllowNullID = 1 << 3, // Allow items such as Text(), Image() that have no unique identifier to be used as drag source, by manufacturing a temporary identifier based on their window-relative position. This is extremely unusual within the dear imgui ecosystem and so we made it explicit. ImGuiDragDropFlags_SourceExtern = 1 << 4, // External source (from outside of dear imgui), won't attempt to read current item/window info. Will always return true. Only one Extern source can be active simultaneously. - ImGuiDragDropFlags_SourceAutoExpirePayload = 1 << 5, // Automatically expire the payload if the source cease to be submitted (otherwise payloads are persisting while being dragged) + ImGuiDragDropFlags_PayloadAutoExpire = 1 << 5, // Automatically expire the payload if the source cease to be submitted (otherwise payloads are persisting while being dragged) + ImGuiDragDropFlags_PayloadNoCrossContext = 1 << 6, // Hint to specify that the payload may not be copied outside current dear imgui context. + ImGuiDragDropFlags_PayloadNoCrossProcess = 1 << 7, // Hint to specify that the payload may not be copied outside current process. // AcceptDragDropPayload() flags ImGuiDragDropFlags_AcceptBeforeDelivery = 1 << 10, // AcceptDragDropPayload() will returns true even before the mouse button is released. You can then call IsDelivery() to test if the payload needs to be delivered. ImGuiDragDropFlags_AcceptNoDrawDefaultRect = 1 << 11, // Do not draw the default highlight rectangle when hovering over target. ImGuiDragDropFlags_AcceptNoPreviewTooltip = 1 << 12, // Request hiding the BeginDragDropSource tooltip from the BeginDragDropTarget site. - ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect // For peeking ahead and inspecting the payload before delivery. + ImGuiDragDropFlags_AcceptPeekOnly = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoDrawDefaultRect, // For peeking ahead and inspecting the payload before delivery. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiDragDropFlags_SourceAutoExpirePayload = ImGuiDragDropFlags_PayloadAutoExpire, // Renamed in 1.90.9 +#endif }; // Standard Drag and Drop payload types. You can define you own payload types using short strings. Types starting with '_' are defined by Dear ImGui. @@ -921,11 +1465,13 @@ enum ImGuiDataType_ ImGuiDataType_U64, // unsigned long long / unsigned __int64 ImGuiDataType_Float, // float ImGuiDataType_Double, // double + ImGuiDataType_Bool, // bool (provided for user convenience, not supported by scalar widgets) + ImGuiDataType_String, // char* (provided for user convenience, not supported by scalar widgets) ImGuiDataType_COUNT }; // A cardinal direction -enum ImGuiDir_ +enum ImGuiDir : int { ImGuiDir_None = -1, ImGuiDir_Left = 0, @@ -935,10 +1481,27 @@ enum ImGuiDir_ ImGuiDir_COUNT }; -// User fill ImGuiIO.KeyMap[] array with indices into the ImGuiIO.KeysDown[512] array -enum ImGuiKey_ +// A sorting direction +enum ImGuiSortDirection : ImU8 { - ImGuiKey_Tab, + ImGuiSortDirection_None = 0, + ImGuiSortDirection_Ascending = 1, // Ascending = 0->9, A->Z etc. + ImGuiSortDirection_Descending = 2 // Descending = 9->0, Z->A etc. +}; + +// A key identifier (ImGuiKey_XXX or ImGuiMod_XXX value): can represent Keyboard, Mouse and Gamepad values. +// All our named keys are >= 512. Keys value 0 to 511 are left unused and were legacy native/opaque key values (< 1.87). +// Support for legacy keys was completely removed in 1.91.5. +// Read details about the 1.87+ transition : https://github.com/ocornut/imgui/issues/4921 +// Note that "Keys" related to physical keys and are not the same concept as input "Characters", the later are submitted via io.AddInputCharacter(). +// The keyboard key enum values are named after the keys on a standard US keyboard, and on other keyboard types the keys reported may not match the keycaps. +enum ImGuiKey : int +{ + // Keyboard + ImGuiKey_None = 0, + ImGuiKey_NamedKey_BEGIN = 512, // First valid key value (other than 0) + + ImGuiKey_Tab = 512, // == ImGuiKey_NamedKey_BEGIN ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_UpArrow, @@ -953,76 +1516,177 @@ enum ImGuiKey_ ImGuiKey_Space, ImGuiKey_Enter, ImGuiKey_Escape, - ImGuiKey_KeyPadEnter, - ImGuiKey_A, // for text edit CTRL+A: select all - ImGuiKey_C, // for text edit CTRL+C: copy - ImGuiKey_V, // for text edit CTRL+V: paste - ImGuiKey_X, // for text edit CTRL+X: cut - ImGuiKey_Y, // for text edit CTRL+Y: redo - ImGuiKey_Z, // for text edit CTRL+Z: undo - ImGuiKey_COUNT + ImGuiKey_LeftCtrl, ImGuiKey_LeftShift, ImGuiKey_LeftAlt, ImGuiKey_LeftSuper, + ImGuiKey_RightCtrl, ImGuiKey_RightShift, ImGuiKey_RightAlt, ImGuiKey_RightSuper, + ImGuiKey_Menu, + ImGuiKey_0, ImGuiKey_1, ImGuiKey_2, ImGuiKey_3, ImGuiKey_4, ImGuiKey_5, ImGuiKey_6, ImGuiKey_7, ImGuiKey_8, ImGuiKey_9, + ImGuiKey_A, ImGuiKey_B, ImGuiKey_C, ImGuiKey_D, ImGuiKey_E, ImGuiKey_F, ImGuiKey_G, ImGuiKey_H, ImGuiKey_I, ImGuiKey_J, + ImGuiKey_K, ImGuiKey_L, ImGuiKey_M, ImGuiKey_N, ImGuiKey_O, ImGuiKey_P, ImGuiKey_Q, ImGuiKey_R, ImGuiKey_S, ImGuiKey_T, + ImGuiKey_U, ImGuiKey_V, ImGuiKey_W, ImGuiKey_X, ImGuiKey_Y, ImGuiKey_Z, + ImGuiKey_F1, ImGuiKey_F2, ImGuiKey_F3, ImGuiKey_F4, ImGuiKey_F5, ImGuiKey_F6, + ImGuiKey_F7, ImGuiKey_F8, ImGuiKey_F9, ImGuiKey_F10, ImGuiKey_F11, ImGuiKey_F12, + ImGuiKey_F13, ImGuiKey_F14, ImGuiKey_F15, ImGuiKey_F16, ImGuiKey_F17, ImGuiKey_F18, + ImGuiKey_F19, ImGuiKey_F20, ImGuiKey_F21, ImGuiKey_F22, ImGuiKey_F23, ImGuiKey_F24, + ImGuiKey_Apostrophe, // ' + ImGuiKey_Comma, // , + ImGuiKey_Minus, // - + ImGuiKey_Period, // . + ImGuiKey_Slash, // / + ImGuiKey_Semicolon, // ; + ImGuiKey_Equal, // = + ImGuiKey_LeftBracket, // [ + ImGuiKey_Backslash, // \ (this text inhibit multiline comment caused by backslash) + ImGuiKey_RightBracket, // ] + ImGuiKey_GraveAccent, // ` + ImGuiKey_CapsLock, + ImGuiKey_ScrollLock, + ImGuiKey_NumLock, + ImGuiKey_PrintScreen, + ImGuiKey_Pause, + ImGuiKey_Keypad0, ImGuiKey_Keypad1, ImGuiKey_Keypad2, ImGuiKey_Keypad3, ImGuiKey_Keypad4, + ImGuiKey_Keypad5, ImGuiKey_Keypad6, ImGuiKey_Keypad7, ImGuiKey_Keypad8, ImGuiKey_Keypad9, + ImGuiKey_KeypadDecimal, + ImGuiKey_KeypadDivide, + ImGuiKey_KeypadMultiply, + ImGuiKey_KeypadSubtract, + ImGuiKey_KeypadAdd, + ImGuiKey_KeypadEnter, + ImGuiKey_KeypadEqual, + ImGuiKey_AppBack, // Available on some keyboard/mouses. Often referred as "Browser Back" + ImGuiKey_AppForward, + ImGuiKey_Oem102, // Non-US backslash. + + // Gamepad (some of those are analog values, 0.0f to 1.0f) // NAVIGATION ACTION + // (download controller mapping PNG/PSD at http://dearimgui.com/controls_sheets) + ImGuiKey_GamepadStart, // Menu (Xbox) + (Switch) Start/Options (PS) + ImGuiKey_GamepadBack, // View (Xbox) - (Switch) Share (PS) + ImGuiKey_GamepadFaceLeft, // X (Xbox) Y (Switch) Square (PS) // Tap: Toggle Menu. Hold: Windowing mode (Focus/Move/Resize windows) + ImGuiKey_GamepadFaceRight, // B (Xbox) A (Switch) Circle (PS) // Cancel / Close / Exit + ImGuiKey_GamepadFaceUp, // Y (Xbox) X (Switch) Triangle (PS) // Text Input / On-screen Keyboard + ImGuiKey_GamepadFaceDown, // A (Xbox) B (Switch) Cross (PS) // Activate / Open / Toggle / Tweak + ImGuiKey_GamepadDpadLeft, // D-pad Left // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadRight, // D-pad Right // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadUp, // D-pad Up // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadDpadDown, // D-pad Down // Move / Tweak / Resize Window (in Windowing mode) + ImGuiKey_GamepadL1, // L Bumper (Xbox) L (Switch) L1 (PS) // Tweak Slower / Focus Previous (in Windowing mode) + ImGuiKey_GamepadR1, // R Bumper (Xbox) R (Switch) R1 (PS) // Tweak Faster / Focus Next (in Windowing mode) + ImGuiKey_GamepadL2, // L Trig. (Xbox) ZL (Switch) L2 (PS) [Analog] + ImGuiKey_GamepadR2, // R Trig. (Xbox) ZR (Switch) R2 (PS) [Analog] + ImGuiKey_GamepadL3, // L Stick (Xbox) L3 (Switch) L3 (PS) + ImGuiKey_GamepadR3, // R Stick (Xbox) R3 (Switch) R3 (PS) + ImGuiKey_GamepadLStickLeft, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickRight, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickUp, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadLStickDown, // [Analog] // Move Window (in Windowing mode) + ImGuiKey_GamepadRStickLeft, // [Analog] + ImGuiKey_GamepadRStickRight, // [Analog] + ImGuiKey_GamepadRStickUp, // [Analog] + ImGuiKey_GamepadRStickDown, // [Analog] + + // Aliases: Mouse Buttons (auto-submitted from AddMouseButtonEvent() calls) + // - This is mirroring the data also written to io.MouseDown[], io.MouseWheel, in a format allowing them to be accessed via standard key API. + ImGuiKey_MouseLeft, ImGuiKey_MouseRight, ImGuiKey_MouseMiddle, ImGuiKey_MouseX1, ImGuiKey_MouseX2, ImGuiKey_MouseWheelX, ImGuiKey_MouseWheelY, + + // [Internal] Reserved for mod storage + ImGuiKey_ReservedForModCtrl, ImGuiKey_ReservedForModShift, ImGuiKey_ReservedForModAlt, ImGuiKey_ReservedForModSuper, + ImGuiKey_NamedKey_END, + + // Keyboard Modifiers (explicitly submitted by backend via AddKeyEvent() calls) + // - This is mirroring the data also written to io.KeyCtrl, io.KeyShift, io.KeyAlt, io.KeySuper, in a format allowing + // them to be accessed via standard key API, allowing calls such as IsKeyPressed(), IsKeyReleased(), querying duration etc. + // - Code polling every key (e.g. an interface to detect a key press for input mapping) might want to ignore those + // and prefer using the real keys (e.g. ImGuiKey_LeftCtrl, ImGuiKey_RightCtrl instead of ImGuiMod_Ctrl). + // - In theory the value of keyboard modifiers should be roughly equivalent to a logical or of the equivalent left/right keys. + // In practice: it's complicated; mods are often provided from different sources. Keyboard layout, IME, sticky keys and + // backends tend to interfere and break that equivalence. The safer decision is to relay that ambiguity down to the end-user... + // - On macOS, we swap Cmd(Super) and Ctrl keys at the time of the io.AddKeyEvent() call. + ImGuiMod_None = 0, + ImGuiMod_Ctrl = 1 << 12, // Ctrl (non-macOS), Cmd (macOS) + ImGuiMod_Shift = 1 << 13, // Shift + ImGuiMod_Alt = 1 << 14, // Option/Menu + ImGuiMod_Super = 1 << 15, // Windows/Super (non-macOS), Ctrl (macOS) + ImGuiMod_Mask_ = 0xF000, // 4-bits + + // [Internal] If you need to iterate all keys (for e.g. an input mapper) you may use ImGuiKey_NamedKey_BEGIN..ImGuiKey_NamedKey_END. + ImGuiKey_NamedKey_COUNT = ImGuiKey_NamedKey_END - ImGuiKey_NamedKey_BEGIN, + //ImGuiKey_KeysData_SIZE = ImGuiKey_NamedKey_COUNT, // Size of KeysData[]: only hold named keys + //ImGuiKey_KeysData_OFFSET = ImGuiKey_NamedKey_BEGIN, // Accesses to io.KeysData[] must use (key - ImGuiKey_NamedKey_BEGIN) index. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiKey_COUNT = ImGuiKey_NamedKey_END, // Obsoleted in 1.91.5 because it was extremely misleading (since named keys don't start at 0 anymore) + ImGuiMod_Shortcut = ImGuiMod_Ctrl, // Removed in 1.90.7, you can now simply use ImGuiMod_Ctrl + ImGuiKey_ModCtrl = ImGuiMod_Ctrl, ImGuiKey_ModShift = ImGuiMod_Shift, ImGuiKey_ModAlt = ImGuiMod_Alt, ImGuiKey_ModSuper = ImGuiMod_Super, // Renamed in 1.89 + //ImGuiKey_KeyPadEnter = ImGuiKey_KeypadEnter, // Renamed in 1.87 +#endif }; -// Gamepad/Keyboard directional navigation -// Keyboard: Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard to enable. NewFrame() will automatically fill io.NavInputs[] based on your io.KeysDown[] + io.KeyMap[] arrays. -// Gamepad: Set io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad to enable. Back-end: set ImGuiBackendFlags_HasGamepad and fill the io.NavInputs[] fields before calling NewFrame(). Note that io.NavInputs[] is cleared by EndFrame(). -// Read instructions in imgui.cpp for more details. Download PNG/PSD at http://goo.gl/9LgVZW. -enum ImGuiNavInput_ +// Flags for Shortcut(), SetNextItemShortcut(), +// (and for upcoming extended versions of IsKeyPressed(), IsMouseClicked(), Shortcut(), SetKeyOwner(), SetItemKeyOwner() that are still in imgui_internal.h) +// Don't mistake with ImGuiInputTextFlags! (which is for ImGui::InputText() function) +enum ImGuiInputFlags_ { - // Gamepad Mapping - ImGuiNavInput_Activate, // activate / open / toggle / tweak value // e.g. Cross (PS4), A (Xbox), A (Switch), Space (Keyboard) - ImGuiNavInput_Cancel, // cancel / close / exit // e.g. Circle (PS4), B (Xbox), B (Switch), Escape (Keyboard) - ImGuiNavInput_Input, // text input / on-screen keyboard // e.g. Triang.(PS4), Y (Xbox), X (Switch), Return (Keyboard) - ImGuiNavInput_Menu, // tap: toggle menu / hold: focus, move, resize // e.g. Square (PS4), X (Xbox), Y (Switch), Alt (Keyboard) - ImGuiNavInput_DpadLeft, // move / tweak / resize window (w/ PadMenu) // e.g. D-pad Left/Right/Up/Down (Gamepads), Arrow keys (Keyboard) - ImGuiNavInput_DpadRight, // - ImGuiNavInput_DpadUp, // - ImGuiNavInput_DpadDown, // - ImGuiNavInput_LStickLeft, // scroll / move window (w/ PadMenu) // e.g. Left Analog Stick Left/Right/Up/Down - ImGuiNavInput_LStickRight, // - ImGuiNavInput_LStickUp, // - ImGuiNavInput_LStickDown, // - ImGuiNavInput_FocusPrev, // next window (w/ PadMenu) // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) - ImGuiNavInput_FocusNext, // prev window (w/ PadMenu) // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) - ImGuiNavInput_TweakSlow, // slower tweaks // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch) - ImGuiNavInput_TweakFast, // faster tweaks // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch) + ImGuiInputFlags_None = 0, + ImGuiInputFlags_Repeat = 1 << 0, // Enable repeat. Return true on successive repeats. Default for legacy IsKeyPressed(). NOT Default for legacy IsMouseClicked(). MUST BE == 1. - // [Internal] Don't use directly! This is used internally to differentiate keyboard from gamepad inputs for behaviors that require to differentiate them. - // Keyboard behavior that have no corresponding gamepad mapping (e.g. CTRL+TAB) will be directly reading from io.KeysDown[] instead of io.NavInputs[]. - ImGuiNavInput_KeyMenu_, // toggle menu // = io.KeyAlt - ImGuiNavInput_KeyTab_, // tab // = Tab key - ImGuiNavInput_KeyLeft_, // move left // = Arrow keys - ImGuiNavInput_KeyRight_, // move right - ImGuiNavInput_KeyUp_, // move up - ImGuiNavInput_KeyDown_, // move down - ImGuiNavInput_COUNT, - ImGuiNavInput_InternalStart_ = ImGuiNavInput_KeyMenu_ + // Flags for Shortcut(), SetNextItemShortcut() + // - Routing policies: RouteGlobal+OverActive >> RouteActive or RouteFocused (if owner is active item) >> RouteGlobal+OverFocused >> RouteFocused (if in focused window stack) >> RouteGlobal. + // - Default policy is RouteFocused. Can select only 1 policy among all available. + ImGuiInputFlags_RouteActive = 1 << 10, // Route to active item only. + ImGuiInputFlags_RouteFocused = 1 << 11, // Route to windows in the focus stack (DEFAULT). Deep-most focused window takes inputs. Active item takes inputs over deep-most focused window. + ImGuiInputFlags_RouteGlobal = 1 << 12, // Global route (unless a focused window or active item registered the route). + ImGuiInputFlags_RouteAlways = 1 << 13, // Do not register route, poll keys directly. + // - Routing options + ImGuiInputFlags_RouteOverFocused = 1 << 14, // Option: global route: higher priority than focused route (unless active item in focused route). + ImGuiInputFlags_RouteOverActive = 1 << 15, // Option: global route: higher priority than active item. Unlikely you need to use that: will interfere with every active items, e.g. CTRL+A registered by InputText will be overridden by this. May not be fully honored as user/internal code is likely to always assume they can access keys when active. + ImGuiInputFlags_RouteUnlessBgFocused = 1 << 16, // Option: global route: will not be applied if underlying background/void is focused (== no Dear ImGui windows are focused). Useful for overlay applications. + ImGuiInputFlags_RouteFromRootWindow = 1 << 17, // Option: route evaluated from the point of view of root window rather than current window. + + // Flags for SetNextItemShortcut() + ImGuiInputFlags_Tooltip = 1 << 18, // Automatically display a tooltip when hovering item [BETA] Unsure of right api (opt-in/opt-out) }; // Configuration flags stored in io.ConfigFlags. Set by user/application. enum ImGuiConfigFlags_ { ImGuiConfigFlags_None = 0, - ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. NewFrame() will automatically fill io.NavInputs[] based on io.KeysDown[]. - ImGuiConfigFlags_NavEnableGamepad = 1 << 1, // Master gamepad navigation enable flag. This is mostly to instruct your imgui back-end to fill io.NavInputs[]. Back-end also needs to set ImGuiBackendFlags_HasGamepad. - ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, // Instruct navigation to move the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is awkward. Will update io.MousePos and set io.WantSetMousePos=true. If enabled you MUST honor io.WantSetMousePos requests in your binding, otherwise ImGui will react as if the mouse is jumping around back and forth. - ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, // Instruct navigation to not set the io.WantCaptureKeyboard flag when io.NavActive is set. - ImGuiConfigFlags_NoMouse = 1 << 4, // Instruct imgui to clear mouse position/buttons in NewFrame(). This allows ignoring the mouse information set by the back-end. - ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, // Instruct back-end to not alter mouse cursor shape and visibility. Use if the back-end cursor changes are interfering with yours and you don't want to use SetMouseCursor() to change mouse cursor. You may want to honor requests from imgui by reading GetMouseCursor() yourself instead. + ImGuiConfigFlags_NavEnableKeyboard = 1 << 0, // Master keyboard navigation enable flag. Enable full Tabbing + directional arrows + space/enter to activate. + ImGuiConfigFlags_NavEnableGamepad = 1 << 1, // Master gamepad navigation enable flag. Backend also needs to set ImGuiBackendFlags_HasGamepad. + ImGuiConfigFlags_NoMouse = 1 << 4, // Instruct dear imgui to disable mouse inputs and interactions. + ImGuiConfigFlags_NoMouseCursorChange = 1 << 5, // Instruct backend to not alter mouse cursor shape and visibility. Use if the backend cursor changes are interfering with yours and you don't want to use SetMouseCursor() to change mouse cursor. You may want to honor requests from imgui by reading GetMouseCursor() yourself instead. + ImGuiConfigFlags_NoKeyboard = 1 << 6, // Instruct dear imgui to disable keyboard inputs and interactions. This is done by ignoring keyboard events and clearing existing states. - // User storage (to allow your back-end/engine to communicate to code that may be shared between multiple projects. Those flags are not used by core Dear ImGui) + // [BETA] Docking + ImGuiConfigFlags_DockingEnable = 1 << 7, // Docking enable flags. + + // [BETA] Viewports + // When using viewports it is recommended that your default value for ImGuiCol_WindowBg is opaque (Alpha=1.0) so transition to a viewport won't be noticeable. + ImGuiConfigFlags_ViewportsEnable = 1 << 10, // Viewport enable flags (require both ImGuiBackendFlags_PlatformHasViewports + ImGuiBackendFlags_RendererHasViewports set by the respective backends) + ImGuiConfigFlags_DpiEnableScaleViewports= 1 << 14, // [BETA: Don't use] FIXME-DPI: Reposition and resize imgui windows when the DpiScale of a viewport changed (mostly useful for the main viewport hosting other window). Note that resizing the main window itself is up to your application. + ImGuiConfigFlags_DpiEnableScaleFonts = 1 << 15, // [BETA: Don't use] FIXME-DPI: Request bitmap-scaled fonts to match DpiScale. This is a very low-quality workaround. The correct way to handle DPI is _currently_ to replace the atlas and/or fonts in the Platform_OnChangedViewport callback, but this is all early work in progress. + + // User storage (to allow your backend/engine to communicate to code that may be shared between multiple projects. Those flags are NOT used by core Dear ImGui) ImGuiConfigFlags_IsSRGB = 1 << 20, // Application is SRGB-aware. - ImGuiConfigFlags_IsTouchScreen = 1 << 21 // Application is using a touch screen instead of a mouse. + ImGuiConfigFlags_IsTouchScreen = 1 << 21, // Application is using a touch screen instead of a mouse. + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiConfigFlags_NavEnableSetMousePos = 1 << 2, // [moved/renamed in 1.91.4] -> use bool io.ConfigNavMoveSetMousePos + ImGuiConfigFlags_NavNoCaptureKeyboard = 1 << 3, // [moved/renamed in 1.91.4] -> use bool io.ConfigNavCaptureKeyboard +#endif }; -// Back-end capabilities flags stored in io.BackendFlags. Set by imgui_impl_xxx or custom back-end. +// Backend capabilities flags stored in io.BackendFlags. Set by imgui_impl_xxx or custom backend. enum ImGuiBackendFlags_ { ImGuiBackendFlags_None = 0, - ImGuiBackendFlags_HasGamepad = 1 << 0, // Back-end Platform supports gamepad and currently has one connected. - ImGuiBackendFlags_HasMouseCursors = 1 << 1, // Back-end Platform supports honoring GetMouseCursor() value to change the OS cursor shape. - ImGuiBackendFlags_HasSetMousePos = 1 << 2, // Back-end Platform supports io.WantSetMousePos requests to reposition the OS mouse position (only used if ImGuiConfigFlags_NavEnableSetMousePos is set). - ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3 // Back-end Renderer supports ImDrawCmd::VtxOffset. This enables output of large meshes (64K+ vertices) while still using 16-bits indices. + ImGuiBackendFlags_HasGamepad = 1 << 0, // Backend Platform supports gamepad and currently has one connected. + ImGuiBackendFlags_HasMouseCursors = 1 << 1, // Backend Platform supports honoring GetMouseCursor() value to change the OS cursor shape. + ImGuiBackendFlags_HasSetMousePos = 1 << 2, // Backend Platform supports io.WantSetMousePos requests to reposition the OS mouse position (only used if io.ConfigNavMoveSetMousePos is set). + ImGuiBackendFlags_RendererHasVtxOffset = 1 << 3, // Backend Renderer supports ImDrawCmd::VtxOffset. This enables output of large meshes (64K+ vertices) while still using 16-bit indices. + + // [BETA] Viewports + ImGuiBackendFlags_PlatformHasViewports = 1 << 10, // Backend Platform supports multiple viewports. + ImGuiBackendFlags_HasMouseHoveredViewport=1 << 11, // Backend Platform supports calling io.AddMouseViewportEvent() with the viewport under the mouse. IF POSSIBLE, ignore viewports with the ImGuiViewportFlags_NoInputs flag (Win32 backend, GLFW 3.30+ backend can do this, SDL backend cannot). If this cannot be done, Dear ImGui needs to use a flawed heuristic to find the viewport under. + ImGuiBackendFlags_RendererHasViewports = 1 << 12, // Backend Renderer supports multiple viewports. }; // Enumeration for PushStyleColor() / PopStyleColor() @@ -1038,15 +1702,15 @@ enum ImGuiCol_ ImGuiCol_FrameBg, // Background of checkbox, radio button, plot, slider, text input ImGuiCol_FrameBgHovered, ImGuiCol_FrameBgActive, - ImGuiCol_TitleBg, - ImGuiCol_TitleBgActive, - ImGuiCol_TitleBgCollapsed, + ImGuiCol_TitleBg, // Title bar + ImGuiCol_TitleBgActive, // Title bar when focused + ImGuiCol_TitleBgCollapsed, // Title bar when collapsed ImGuiCol_MenuBarBg, ImGuiCol_ScrollbarBg, ImGuiCol_ScrollbarGrab, ImGuiCol_ScrollbarGrabHovered, ImGuiCol_ScrollbarGrabActive, - ImGuiCol_CheckMark, + ImGuiCol_CheckMark, // Checkbox tick and RadioButton circle ImGuiCol_SliderGrab, ImGuiCol_SliderGrabActive, ImGuiCol_Button, @@ -1058,71 +1722,102 @@ enum ImGuiCol_ ImGuiCol_Separator, ImGuiCol_SeparatorHovered, ImGuiCol_SeparatorActive, - ImGuiCol_ResizeGrip, + ImGuiCol_ResizeGrip, // Resize grip in lower-right and lower-left corners of windows. ImGuiCol_ResizeGripHovered, ImGuiCol_ResizeGripActive, - ImGuiCol_Tab, - ImGuiCol_TabHovered, - ImGuiCol_TabActive, - ImGuiCol_TabUnfocused, - ImGuiCol_TabUnfocusedActive, + ImGuiCol_TabHovered, // Tab background, when hovered + ImGuiCol_Tab, // Tab background, when tab-bar is focused & tab is unselected + ImGuiCol_TabSelected, // Tab background, when tab-bar is focused & tab is selected + ImGuiCol_TabSelectedOverline, // Tab horizontal overline, when tab-bar is focused & tab is selected + ImGuiCol_TabDimmed, // Tab background, when tab-bar is unfocused & tab is unselected + ImGuiCol_TabDimmedSelected, // Tab background, when tab-bar is unfocused & tab is selected + ImGuiCol_TabDimmedSelectedOverline,//..horizontal overline, when tab-bar is unfocused & tab is selected + ImGuiCol_DockingPreview, // Preview overlay color when about to docking something + ImGuiCol_DockingEmptyBg, // Background color for empty node (e.g. CentralNode with no window docked into it) ImGuiCol_PlotLines, ImGuiCol_PlotLinesHovered, ImGuiCol_PlotHistogram, ImGuiCol_PlotHistogramHovered, + ImGuiCol_TableHeaderBg, // Table header background + ImGuiCol_TableBorderStrong, // Table outer and header borders (prefer using Alpha=1.0 here) + ImGuiCol_TableBorderLight, // Table inner borders (prefer using Alpha=1.0 here) + ImGuiCol_TableRowBg, // Table row background (even rows) + ImGuiCol_TableRowBgAlt, // Table row background (odd rows) + ImGuiCol_TextLink, // Hyperlink color ImGuiCol_TextSelectedBg, - ImGuiCol_DragDropTarget, - ImGuiCol_NavHighlight, // Gamepad/keyboard: current highlighted item + ImGuiCol_DragDropTarget, // Rectangle highlighting a drop target + ImGuiCol_NavCursor, // Color of keyboard/gamepad navigation cursor/rectangle, when visible ImGuiCol_NavWindowingHighlight, // Highlight window when using CTRL+TAB ImGuiCol_NavWindowingDimBg, // Darken/colorize entire screen behind the CTRL+TAB window list, when active ImGuiCol_ModalWindowDimBg, // Darken/colorize entire screen behind a modal window, when one is active - ImGuiCol_COUNT + ImGuiCol_COUNT, - // Obsolete names (will be removed) #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , ImGuiCol_ModalWindowDarkening = ImGuiCol_ModalWindowDimBg // [renamed in 1.63] - , ImGuiCol_ChildWindowBg = ImGuiCol_ChildBg // [renamed in 1.53] - //ImGuiCol_CloseButton, ImGuiCol_CloseButtonActive, ImGuiCol_CloseButtonHovered, // [unused since 1.60+] the close button now uses regular button colors. - //ImGuiCol_ComboBg, // [unused since 1.53+] ComboBg has been merged with PopupBg, so a redirect isn't accurate. + ImGuiCol_TabActive = ImGuiCol_TabSelected, // [renamed in 1.90.9] + ImGuiCol_TabUnfocused = ImGuiCol_TabDimmed, // [renamed in 1.90.9] + ImGuiCol_TabUnfocusedActive = ImGuiCol_TabDimmedSelected, // [renamed in 1.90.9] + ImGuiCol_NavHighlight = ImGuiCol_NavCursor, // [renamed in 1.91.4] #endif }; // Enumeration for PushStyleVar() / PopStyleVar() to temporarily modify the ImGuiStyle structure. -// NB: the enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. During initialization, feel free to just poke into ImGuiStyle directly. -// NB: if changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. +// - The enum only refers to fields of ImGuiStyle which makes sense to be pushed/popped inside UI code. +// During initialization or between frames, feel free to just poke into ImGuiStyle directly. +// - Tip: Use your programming IDE navigation facilities on the names in the _second column_ below to find the actual members and their description. +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +// - When changing this enum, you need to update the associated internal table GStyleVarInfo[] accordingly. This is where we link enum values to members offset/type. enum ImGuiStyleVar_ { - // Enum name --------------------- // Member in ImGuiStyle structure (see ImGuiStyle for descriptions) - ImGuiStyleVar_Alpha, // float Alpha - ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding - ImGuiStyleVar_WindowRounding, // float WindowRounding - ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize - ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize - ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign - ImGuiStyleVar_ChildRounding, // float ChildRounding - ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize - ImGuiStyleVar_PopupRounding, // float PopupRounding - ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize - ImGuiStyleVar_FramePadding, // ImVec2 FramePadding - ImGuiStyleVar_FrameRounding, // float FrameRounding - ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize - ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing - ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing - ImGuiStyleVar_IndentSpacing, // float IndentSpacing - ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize - ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding - ImGuiStyleVar_GrabMinSize, // float GrabMinSize - ImGuiStyleVar_GrabRounding, // float GrabRounding - ImGuiStyleVar_TabRounding, // float TabRounding - ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign - ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign + // Enum name -------------------------- // Member in ImGuiStyle structure (see ImGuiStyle for descriptions) + ImGuiStyleVar_Alpha, // float Alpha + ImGuiStyleVar_DisabledAlpha, // float DisabledAlpha + ImGuiStyleVar_WindowPadding, // ImVec2 WindowPadding + ImGuiStyleVar_WindowRounding, // float WindowRounding + ImGuiStyleVar_WindowBorderSize, // float WindowBorderSize + ImGuiStyleVar_WindowMinSize, // ImVec2 WindowMinSize + ImGuiStyleVar_WindowTitleAlign, // ImVec2 WindowTitleAlign + ImGuiStyleVar_ChildRounding, // float ChildRounding + ImGuiStyleVar_ChildBorderSize, // float ChildBorderSize + ImGuiStyleVar_PopupRounding, // float PopupRounding + ImGuiStyleVar_PopupBorderSize, // float PopupBorderSize + ImGuiStyleVar_FramePadding, // ImVec2 FramePadding + ImGuiStyleVar_FrameRounding, // float FrameRounding + ImGuiStyleVar_FrameBorderSize, // float FrameBorderSize + ImGuiStyleVar_ItemSpacing, // ImVec2 ItemSpacing + ImGuiStyleVar_ItemInnerSpacing, // ImVec2 ItemInnerSpacing + ImGuiStyleVar_IndentSpacing, // float IndentSpacing + ImGuiStyleVar_CellPadding, // ImVec2 CellPadding + ImGuiStyleVar_ScrollbarSize, // float ScrollbarSize + ImGuiStyleVar_ScrollbarRounding, // float ScrollbarRounding + ImGuiStyleVar_GrabMinSize, // float GrabMinSize + ImGuiStyleVar_GrabRounding, // float GrabRounding + ImGuiStyleVar_ImageBorderSize, // float ImageBorderSize + ImGuiStyleVar_TabRounding, // float TabRounding + ImGuiStyleVar_TabBorderSize, // float TabBorderSize + ImGuiStyleVar_TabBarBorderSize, // float TabBarBorderSize + ImGuiStyleVar_TabBarOverlineSize, // float TabBarOverlineSize + ImGuiStyleVar_TableAngledHeadersAngle, // float TableAngledHeadersAngle + ImGuiStyleVar_TableAngledHeadersTextAlign,// ImVec2 TableAngledHeadersTextAlign + ImGuiStyleVar_ButtonTextAlign, // ImVec2 ButtonTextAlign + ImGuiStyleVar_SelectableTextAlign, // ImVec2 SelectableTextAlign + ImGuiStyleVar_SeparatorTextBorderSize, // float SeparatorTextBorderSize + ImGuiStyleVar_SeparatorTextAlign, // ImVec2 SeparatorTextAlign + ImGuiStyleVar_SeparatorTextPadding, // ImVec2 SeparatorTextPadding + ImGuiStyleVar_DockingSeparatorSize, // float DockingSeparatorSize ImGuiStyleVar_COUNT +}; - // Obsolete names (will be removed) -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , ImGuiStyleVar_Count_ = ImGuiStyleVar_COUNT // [renamed in 1.60] - , ImGuiStyleVar_ChildWindowRounding = ImGuiStyleVar_ChildRounding // [renamed in 1.53] -#endif +// Flags for InvisibleButton() [extended in imgui_internal.h] +enum ImGuiButtonFlags_ +{ + ImGuiButtonFlags_None = 0, + ImGuiButtonFlags_MouseButtonLeft = 1 << 0, // React on left mouse button (default) + ImGuiButtonFlags_MouseButtonRight = 1 << 1, // React on right mouse button + ImGuiButtonFlags_MouseButtonMiddle = 1 << 2, // React on center mouse button + ImGuiButtonFlags_MouseButtonMask_ = ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_MouseButtonMiddle, // [Internal] + ImGuiButtonFlags_EnableNav = 1 << 3, // InvisibleButton(): do not disable navigation/tabbing. Otherwise disabled by default. }; // Flags for ColorEdit3() / ColorEdit4() / ColorPicker3() / ColorPicker4() / ColorButton() @@ -1130,19 +1825,26 @@ enum ImGuiColorEditFlags_ { ImGuiColorEditFlags_None = 0, ImGuiColorEditFlags_NoAlpha = 1 << 1, // // ColorEdit, ColorPicker, ColorButton: ignore Alpha component (will only read 3 components from the input pointer). - ImGuiColorEditFlags_NoPicker = 1 << 2, // // ColorEdit: disable picker when clicking on colored square. + ImGuiColorEditFlags_NoPicker = 1 << 2, // // ColorEdit: disable picker when clicking on color square. ImGuiColorEditFlags_NoOptions = 1 << 3, // // ColorEdit: disable toggling options menu when right-clicking on inputs/small preview. - ImGuiColorEditFlags_NoSmallPreview = 1 << 4, // // ColorEdit, ColorPicker: disable colored square preview next to the inputs. (e.g. to show only the inputs) - ImGuiColorEditFlags_NoInputs = 1 << 5, // // ColorEdit, ColorPicker: disable inputs sliders/text widgets (e.g. to show only the small preview colored square). + ImGuiColorEditFlags_NoSmallPreview = 1 << 4, // // ColorEdit, ColorPicker: disable color square preview next to the inputs. (e.g. to show only the inputs) + ImGuiColorEditFlags_NoInputs = 1 << 5, // // ColorEdit, ColorPicker: disable inputs sliders/text widgets (e.g. to show only the small preview color square). ImGuiColorEditFlags_NoTooltip = 1 << 6, // // ColorEdit, ColorPicker, ColorButton: disable tooltip when hovering the preview. ImGuiColorEditFlags_NoLabel = 1 << 7, // // ColorEdit, ColorPicker: disable display of inline text label (the label is still forwarded to the tooltip and picker). - ImGuiColorEditFlags_NoSidePreview = 1 << 8, // // ColorPicker: disable bigger color preview on right side of the picker, use small colored square preview instead. + ImGuiColorEditFlags_NoSidePreview = 1 << 8, // // ColorPicker: disable bigger color preview on right side of the picker, use small color square preview instead. ImGuiColorEditFlags_NoDragDrop = 1 << 9, // // ColorEdit: disable drag and drop target. ColorButton: disable drag and drop source. + ImGuiColorEditFlags_NoBorder = 1 << 10, // // ColorButton: disable border (which is enforced by default) + + // Alpha preview + // - Prior to 1.91.8 (2025/01/21): alpha was made opaque in the preview by default using old name ImGuiColorEditFlags_AlphaPreview. + // - We now display the preview as transparent by default. You can use ImGuiColorEditFlags_AlphaOpaque to use old behavior. + // - The new flags may be combined better and allow finer controls. + ImGuiColorEditFlags_AlphaOpaque = 1 << 11, // // ColorEdit, ColorPicker, ColorButton: disable alpha in the preview,. Contrary to _NoAlpha it may still be edited when calling ColorEdit4()/ColorPicker4(). For ColorButton() this does the same as _NoAlpha. + ImGuiColorEditFlags_AlphaNoBg = 1 << 12, // // ColorEdit, ColorPicker, ColorButton: disable rendering a checkerboard background behind transparent color. + ImGuiColorEditFlags_AlphaPreviewHalf= 1 << 13, // // ColorEdit, ColorPicker, ColorButton: display half opaque / half transparent preview. // User Options (right-click on widget to change some of them). ImGuiColorEditFlags_AlphaBar = 1 << 16, // // ColorEdit, ColorPicker: show vertical alpha bar/gradient in picker. - ImGuiColorEditFlags_AlphaPreview = 1 << 17, // // ColorEdit, ColorPicker, ColorButton: display preview as a transparent color over a checkerboard, instead of opaque. - ImGuiColorEditFlags_AlphaPreviewHalf= 1 << 18, // // ColorEdit, ColorPicker, ColorButton: display half opaque / half checkerboard, instead of opaque. ImGuiColorEditFlags_HDR = 1 << 19, // // (WIP) ColorEdit: Currently only disable 0.0f..1.0f limits in RGBA edition (note: you probably want to use ImGuiColorEditFlags_Float flag as well). ImGuiColorEditFlags_DisplayRGB = 1 << 20, // [Display] // ColorEdit: override _display_ type among RGB/HSV/Hex. ColorPicker: select any combination using one or more of RGB/HSV/Hex. ImGuiColorEditFlags_DisplayHSV = 1 << 21, // [Display] // " @@ -1156,77 +1858,297 @@ enum ImGuiColorEditFlags_ // Defaults Options. You can set application defaults using SetColorEditOptions(). The intent is that you probably don't want to // override them in most of your calls. Let the user choose via the option menu and/or call SetColorEditOptions() once during startup. - ImGuiColorEditFlags__OptionsDefault = ImGuiColorEditFlags_Uint8|ImGuiColorEditFlags_DisplayRGB|ImGuiColorEditFlags_InputRGB|ImGuiColorEditFlags_PickerHueBar, + ImGuiColorEditFlags_DefaultOptions_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_PickerHueBar, // [Internal] Masks - ImGuiColorEditFlags__DisplayMask = ImGuiColorEditFlags_DisplayRGB|ImGuiColorEditFlags_DisplayHSV|ImGuiColorEditFlags_DisplayHex, - ImGuiColorEditFlags__DataTypeMask = ImGuiColorEditFlags_Uint8|ImGuiColorEditFlags_Float, - ImGuiColorEditFlags__PickerMask = ImGuiColorEditFlags_PickerHueWheel|ImGuiColorEditFlags_PickerHueBar, - ImGuiColorEditFlags__InputMask = ImGuiColorEditFlags_InputRGB|ImGuiColorEditFlags_InputHSV + ImGuiColorEditFlags_AlphaMask_ = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque | ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf, + ImGuiColorEditFlags_DisplayMask_ = ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_DisplayHex, + ImGuiColorEditFlags_DataTypeMask_ = ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_Float, + ImGuiColorEditFlags_PickerMask_ = ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_PickerHueBar, + ImGuiColorEditFlags_InputMask_ = ImGuiColorEditFlags_InputRGB | ImGuiColorEditFlags_InputHSV, - // Obsolete names (will be removed) + // Obsolete names #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , ImGuiColorEditFlags_RGB = ImGuiColorEditFlags_DisplayRGB, ImGuiColorEditFlags_HSV = ImGuiColorEditFlags_DisplayHSV, ImGuiColorEditFlags_HEX = ImGuiColorEditFlags_DisplayHex // [renamed in 1.69] + ImGuiColorEditFlags_AlphaPreview = 0, // [Removed in 1.91.8] This is the default now. Will display a checkerboard unless ImGuiColorEditFlags_AlphaNoBg is set. #endif + //ImGuiColorEditFlags_RGB = ImGuiColorEditFlags_DisplayRGB, ImGuiColorEditFlags_HSV = ImGuiColorEditFlags_DisplayHSV, ImGuiColorEditFlags_HEX = ImGuiColorEditFlags_DisplayHex // [renamed in 1.69] +}; + +// Flags for DragFloat(), DragInt(), SliderFloat(), SliderInt() etc. +// We use the same sets of flags for DragXXX() and SliderXXX() functions as the features are the same and it makes it easier to swap them. +// (Those are per-item flags. There is shared behavior flag too: ImGuiIO: io.ConfigDragClickToInputText) +enum ImGuiSliderFlags_ +{ + ImGuiSliderFlags_None = 0, + ImGuiSliderFlags_Logarithmic = 1 << 5, // Make the widget logarithmic (linear otherwise). Consider using ImGuiSliderFlags_NoRoundToFormat with this if using a format-string with small amount of digits. + ImGuiSliderFlags_NoRoundToFormat = 1 << 6, // Disable rounding underlying value to match precision of the display format string (e.g. %.3f values are rounded to those 3 digits). + ImGuiSliderFlags_NoInput = 1 << 7, // Disable CTRL+Click or Enter key allowing to input text directly into the widget. + ImGuiSliderFlags_WrapAround = 1 << 8, // Enable wrapping around from max to min and from min to max. Only supported by DragXXX() functions for now. + ImGuiSliderFlags_ClampOnInput = 1 << 9, // Clamp value to min/max bounds when input manually with CTRL+Click. By default CTRL+Click allows going out of bounds. + ImGuiSliderFlags_ClampZeroRange = 1 << 10, // Clamp even if min==max==0.0f. Otherwise due to legacy reason DragXXX functions don't clamp with those values. When your clamping limits are dynamic you almost always want to use it. + ImGuiSliderFlags_NoSpeedTweaks = 1 << 11, // Disable keyboard modifiers altering tweak speed. Useful if you want to alter tweak speed yourself based on your own logic. + ImGuiSliderFlags_AlwaysClamp = ImGuiSliderFlags_ClampOnInput | ImGuiSliderFlags_ClampZeroRange, + ImGuiSliderFlags_InvalidMask_ = 0x7000000F, // [Internal] We treat using those bits as being potentially a 'float power' argument from the previous API that has got miscast to this enum, and will trigger an assert if needed. +}; + +// Identify a mouse button. +// Those values are guaranteed to be stable and we frequently use 0/1 directly. Named enums provided for convenience. +enum ImGuiMouseButton_ +{ + ImGuiMouseButton_Left = 0, + ImGuiMouseButton_Right = 1, + ImGuiMouseButton_Middle = 2, + ImGuiMouseButton_COUNT = 5 }; // Enumeration for GetMouseCursor() -// User code may request binding to display given cursor by calling SetMouseCursor(), which is why we have some cursors that are marked unused here +// User code may request backend to display given cursor by calling SetMouseCursor(), which is why we have some cursors that are marked unused here enum ImGuiMouseCursor_ { ImGuiMouseCursor_None = -1, ImGuiMouseCursor_Arrow = 0, ImGuiMouseCursor_TextInput, // When hovering over InputText, etc. ImGuiMouseCursor_ResizeAll, // (Unused by Dear ImGui functions) - ImGuiMouseCursor_ResizeNS, // When hovering over an horizontal border + ImGuiMouseCursor_ResizeNS, // When hovering over a horizontal border ImGuiMouseCursor_ResizeEW, // When hovering over a vertical border or a column ImGuiMouseCursor_ResizeNESW, // When hovering over the bottom-left corner of a window ImGuiMouseCursor_ResizeNWSE, // When hovering over the bottom-right corner of a window ImGuiMouseCursor_Hand, // (Unused by Dear ImGui functions. Use for e.g. hyperlinks) + ImGuiMouseCursor_Wait, // When waiting for something to process/load. + ImGuiMouseCursor_Progress, // When waiting for something to process/load, but application is still interactive. + ImGuiMouseCursor_NotAllowed, // When hovering something with disallowed interaction. Usually a crossed circle. ImGuiMouseCursor_COUNT - - // Obsolete names (will be removed) -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - , ImGuiMouseCursor_Count_ = ImGuiMouseCursor_COUNT // [renamed in 1.60] -#endif }; -// Enumateration for ImGui::SetWindow***(), SetNextWindow***(), SetNextItem***() functions +// Enumeration for AddMouseSourceEvent() actual source of Mouse Input data. +// Historically we use "Mouse" terminology everywhere to indicate pointer data, e.g. MousePos, IsMousePressed(), io.AddMousePosEvent() +// But that "Mouse" data can come from different source which occasionally may be useful for application to know about. +// You can submit a change of pointer type using io.AddMouseSourceEvent(). +enum ImGuiMouseSource : int +{ + ImGuiMouseSource_Mouse = 0, // Input is coming from an actual mouse. + ImGuiMouseSource_TouchScreen, // Input is coming from a touch screen (no hovering prior to initial press, less precise initial press aiming, dual-axis wheeling possible). + ImGuiMouseSource_Pen, // Input is coming from a pressure/magnetic pen (often used in conjunction with high-sampling rates). + ImGuiMouseSource_COUNT +}; + +// Enumeration for ImGui::SetNextWindow***(), SetWindow***(), SetNextItem***() functions // Represent a condition. // Important: Treat as a regular enum! Do NOT combine multiple values using binary operators! All the functions above treat 0 as a shortcut to ImGuiCond_Always. enum ImGuiCond_ { - ImGuiCond_Always = 1 << 0, // Set the variable - ImGuiCond_Once = 1 << 1, // Set the variable once per runtime session (only the first call with succeed) + ImGuiCond_None = 0, // No condition (always set the variable), same as _Always + ImGuiCond_Always = 1 << 0, // No condition (always set the variable), same as _None + ImGuiCond_Once = 1 << 1, // Set the variable once per runtime session (only the first call will succeed) ImGuiCond_FirstUseEver = 1 << 2, // Set the variable if the object/window has no persistently saved data (no entry in .ini file) - ImGuiCond_Appearing = 1 << 3 // Set the variable if the object/window is appearing after being hidden/inactive (or the first time) + ImGuiCond_Appearing = 1 << 3, // Set the variable if the object/window is appearing after being hidden/inactive (or the first time) }; //----------------------------------------------------------------------------- -// Helpers: Memory allocations macros -// IM_MALLOC(), IM_FREE(), IM_NEW(), IM_PLACEMENT_NEW(), IM_DELETE() -// We call C++ constructor on own allocated memory via the placement "new(ptr) Type()" syntax. -// Defining a custom placement new() with a dummy parameter allows us to bypass including which on some platforms complains when user has disabled exceptions. +// [SECTION] Tables API flags and structures (ImGuiTableFlags, ImGuiTableColumnFlags, ImGuiTableRowFlags, ImGuiTableBgTarget, ImGuiTableSortSpecs, ImGuiTableColumnSortSpecs) //----------------------------------------------------------------------------- -struct ImNewDummy {}; -inline void* operator new(size_t, ImNewDummy, void* ptr) { return ptr; } -inline void operator delete(void*, ImNewDummy, void*) {} // This is only required so we can use the symmetrical new() +// Flags for ImGui::BeginTable() +// - Important! Sizing policies have complex and subtle side effects, much more so than you would expect. +// Read comments/demos carefully + experiment with live demos to get acquainted with them. +// - The DEFAULT sizing policies are: +// - Default to ImGuiTableFlags_SizingFixedFit if ScrollX is on, or if host window has ImGuiWindowFlags_AlwaysAutoResize. +// - Default to ImGuiTableFlags_SizingStretchSame if ScrollX is off. +// - When ScrollX is off: +// - Table defaults to ImGuiTableFlags_SizingStretchSame -> all Columns defaults to ImGuiTableColumnFlags_WidthStretch with same weight. +// - Columns sizing policy allowed: Stretch (default), Fixed/Auto. +// - Fixed Columns (if any) will generally obtain their requested width (unless the table cannot fit them all). +// - Stretch Columns will share the remaining width according to their respective weight. +// - Mixed Fixed/Stretch columns is possible but has various side-effects on resizing behaviors. +// The typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// (this is because the visible order of columns have subtle but necessary effects on how they react to manual resizing). +// - When ScrollX is on: +// - Table defaults to ImGuiTableFlags_SizingFixedFit -> all Columns defaults to ImGuiTableColumnFlags_WidthFixed +// - Columns sizing policy allowed: Fixed/Auto mostly. +// - Fixed Columns can be enlarged as needed. Table will show a horizontal scrollbar if needed. +// - When using auto-resizing (non-resizable) fixed columns, querying the content width to use item right-alignment e.g. SetNextItemWidth(-FLT_MIN) doesn't make sense, would create a feedback loop. +// - Using Stretch columns OFTEN DOES NOT MAKE SENSE if ScrollX is on, UNLESS you have specified a value for 'inner_width' in BeginTable(). +// If you specify a value for 'inner_width' then effectively the scrolling space is known and Stretch or mixed Fixed/Stretch columns become meaningful again. +// - Read on documentation at the top of imgui_tables.cpp for details. +enum ImGuiTableFlags_ +{ + // Features + ImGuiTableFlags_None = 0, + ImGuiTableFlags_Resizable = 1 << 0, // Enable resizing columns. + ImGuiTableFlags_Reorderable = 1 << 1, // Enable reordering columns in header row (need calling TableSetupColumn() + TableHeadersRow() to display headers) + ImGuiTableFlags_Hideable = 1 << 2, // Enable hiding/disabling columns in context menu. + ImGuiTableFlags_Sortable = 1 << 3, // Enable sorting. Call TableGetSortSpecs() to obtain sort specs. Also see ImGuiTableFlags_SortMulti and ImGuiTableFlags_SortTristate. + ImGuiTableFlags_NoSavedSettings = 1 << 4, // Disable persisting columns order, width and sort settings in the .ini file. + ImGuiTableFlags_ContextMenuInBody = 1 << 5, // Right-click on columns body/contents will display table context menu. By default it is available in TableHeadersRow(). + // Decorations + ImGuiTableFlags_RowBg = 1 << 6, // Set each RowBg color with ImGuiCol_TableRowBg or ImGuiCol_TableRowBgAlt (equivalent of calling TableSetBgColor with ImGuiTableBgFlags_RowBg0 on each row manually) + ImGuiTableFlags_BordersInnerH = 1 << 7, // Draw horizontal borders between rows. + ImGuiTableFlags_BordersOuterH = 1 << 8, // Draw horizontal borders at the top and bottom. + ImGuiTableFlags_BordersInnerV = 1 << 9, // Draw vertical borders between columns. + ImGuiTableFlags_BordersOuterV = 1 << 10, // Draw vertical borders on the left and right sides. + ImGuiTableFlags_BordersH = ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_BordersOuterH, // Draw horizontal borders. + ImGuiTableFlags_BordersV = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersOuterV, // Draw vertical borders. + ImGuiTableFlags_BordersInner = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_BordersInnerH, // Draw inner borders. + ImGuiTableFlags_BordersOuter = ImGuiTableFlags_BordersOuterV | ImGuiTableFlags_BordersOuterH, // Draw outer borders. + ImGuiTableFlags_Borders = ImGuiTableFlags_BordersInner | ImGuiTableFlags_BordersOuter, // Draw all borders. + ImGuiTableFlags_NoBordersInBody = 1 << 11, // [ALPHA] Disable vertical borders in columns Body (borders will always appear in Headers). -> May move to style + ImGuiTableFlags_NoBordersInBodyUntilResize = 1 << 12, // [ALPHA] Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers). -> May move to style + // Sizing Policy (read above for defaults) + ImGuiTableFlags_SizingFixedFit = 1 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching contents width. + ImGuiTableFlags_SizingFixedSame = 2 << 13, // Columns default to _WidthFixed or _WidthAuto (if resizable or not resizable), matching the maximum contents width of all columns. Implicitly enable ImGuiTableFlags_NoKeepColumnsVisible. + ImGuiTableFlags_SizingStretchProp = 3 << 13, // Columns default to _WidthStretch with default weights proportional to each columns contents widths. + ImGuiTableFlags_SizingStretchSame = 4 << 13, // Columns default to _WidthStretch with default weights all equal, unless overridden by TableSetupColumn(). + // Sizing Extra Options + ImGuiTableFlags_NoHostExtendX = 1 << 16, // Make outer width auto-fit to columns, overriding outer_size.x value. Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. + ImGuiTableFlags_NoHostExtendY = 1 << 17, // Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible. + ImGuiTableFlags_NoKeepColumnsVisible = 1 << 18, // Disable keeping column always minimally visible when ScrollX is off and table gets too small. Not recommended if columns are resizable. + ImGuiTableFlags_PreciseWidths = 1 << 19, // Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth. + // Clipping + ImGuiTableFlags_NoClip = 1 << 20, // Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with TableSetupScrollFreeze(). + // Padding + ImGuiTableFlags_PadOuterX = 1 << 21, // Default if BordersOuterV is on. Enable outermost padding. Generally desirable if you have headers. + ImGuiTableFlags_NoPadOuterX = 1 << 22, // Default if BordersOuterV is off. Disable outermost padding. + ImGuiTableFlags_NoPadInnerX = 1 << 23, // Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off). + // Scrolling + ImGuiTableFlags_ScrollX = 1 << 24, // Enable horizontal scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. Changes default sizing policy. Because this creates a child window, ScrollY is currently generally recommended when using ScrollX. + ImGuiTableFlags_ScrollY = 1 << 25, // Enable vertical scrolling. Require 'outer_size' parameter of BeginTable() to specify the container size. + // Sorting + ImGuiTableFlags_SortMulti = 1 << 26, // Hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1). + ImGuiTableFlags_SortTristate = 1 << 27, // Allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0). + // Miscellaneous + ImGuiTableFlags_HighlightHoveredColumn = 1 << 28, // Highlight column headers when hovered (may evolve into a fuller highlight) + + // [Internal] Combinations and masks + ImGuiTableFlags_SizingMask_ = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_SizingFixedSame | ImGuiTableFlags_SizingStretchProp | ImGuiTableFlags_SizingStretchSame, +}; + +// Flags for ImGui::TableSetupColumn() +enum ImGuiTableColumnFlags_ +{ + // Input configuration flags + ImGuiTableColumnFlags_None = 0, + ImGuiTableColumnFlags_Disabled = 1 << 0, // Overriding/master disable flag: hide column, won't show in context menu (unlike calling TableSetColumnEnabled() which manipulates the user accessible state) + ImGuiTableColumnFlags_DefaultHide = 1 << 1, // Default as a hidden/disabled column. + ImGuiTableColumnFlags_DefaultSort = 1 << 2, // Default as a sorting column. + ImGuiTableColumnFlags_WidthStretch = 1 << 3, // Column will stretch. Preferable with horizontal scrolling disabled (default if table sizing policy is _SizingStretchSame or _SizingStretchProp). + ImGuiTableColumnFlags_WidthFixed = 1 << 4, // Column will not stretch. Preferable with horizontal scrolling enabled (default if table sizing policy is _SizingFixedFit and table is resizable). + ImGuiTableColumnFlags_NoResize = 1 << 5, // Disable manual resizing. + ImGuiTableColumnFlags_NoReorder = 1 << 6, // Disable manual reordering this column, this will also prevent other columns from crossing over this column. + ImGuiTableColumnFlags_NoHide = 1 << 7, // Disable ability to hide/disable this column. + ImGuiTableColumnFlags_NoClip = 1 << 8, // Disable clipping for this column (all NoClip columns will render in a same draw command). + ImGuiTableColumnFlags_NoSort = 1 << 9, // Disable ability to sort on this field (even if ImGuiTableFlags_Sortable is set on the table). + ImGuiTableColumnFlags_NoSortAscending = 1 << 10, // Disable ability to sort in the ascending direction. + ImGuiTableColumnFlags_NoSortDescending = 1 << 11, // Disable ability to sort in the descending direction. + ImGuiTableColumnFlags_NoHeaderLabel = 1 << 12, // TableHeadersRow() will submit an empty label for this column. Convenient for some small columns. Name will still appear in context menu or in angled headers. You may append into this cell by calling TableSetColumnIndex() right after the TableHeadersRow() call. + ImGuiTableColumnFlags_NoHeaderWidth = 1 << 13, // Disable header text width contribution to automatic column width. + ImGuiTableColumnFlags_PreferSortAscending = 1 << 14, // Make the initial sort direction Ascending when first sorting on this column (default). + ImGuiTableColumnFlags_PreferSortDescending = 1 << 15, // Make the initial sort direction Descending when first sorting on this column. + ImGuiTableColumnFlags_IndentEnable = 1 << 16, // Use current Indent value when entering cell (default for column 0). + ImGuiTableColumnFlags_IndentDisable = 1 << 17, // Ignore current Indent value when entering cell (default for columns > 0). Indentation changes _within_ the cell will still be honored. + ImGuiTableColumnFlags_AngledHeader = 1 << 18, // TableHeadersRow() will submit an angled header row for this column. Note this will add an extra row. + + // Output status flags, read-only via TableGetColumnFlags() + ImGuiTableColumnFlags_IsEnabled = 1 << 24, // Status: is enabled == not hidden by user/api (referred to as "Hide" in _DefaultHide and _NoHide) flags. + ImGuiTableColumnFlags_IsVisible = 1 << 25, // Status: is visible == is enabled AND not clipped by scrolling. + ImGuiTableColumnFlags_IsSorted = 1 << 26, // Status: is currently part of the sort specs + ImGuiTableColumnFlags_IsHovered = 1 << 27, // Status: is hovered by mouse + + // [Internal] Combinations and masks + ImGuiTableColumnFlags_WidthMask_ = ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_WidthFixed, + ImGuiTableColumnFlags_IndentMask_ = ImGuiTableColumnFlags_IndentEnable | ImGuiTableColumnFlags_IndentDisable, + ImGuiTableColumnFlags_StatusMask_ = ImGuiTableColumnFlags_IsEnabled | ImGuiTableColumnFlags_IsVisible | ImGuiTableColumnFlags_IsSorted | ImGuiTableColumnFlags_IsHovered, + ImGuiTableColumnFlags_NoDirectResize_ = 1 << 30, // [Internal] Disable user resizing this column directly (it may however we resized indirectly from its left edge) +}; + +// Flags for ImGui::TableNextRow() +enum ImGuiTableRowFlags_ +{ + ImGuiTableRowFlags_None = 0, + ImGuiTableRowFlags_Headers = 1 << 0, // Identify header row (set default background color + width of its contents accounted differently for auto column width) +}; + +// Enum for ImGui::TableSetBgColor() +// Background colors are rendering in 3 layers: +// - Layer 0: draw with RowBg0 color if set, otherwise draw with ColumnBg0 if set. +// - Layer 1: draw with RowBg1 color if set, otherwise draw with ColumnBg1 if set. +// - Layer 2: draw with CellBg color if set. +// The purpose of the two row/columns layers is to let you decide if a background color change should override or blend with the existing color. +// When using ImGuiTableFlags_RowBg on the table, each row has the RowBg0 color automatically set for odd/even rows. +// If you set the color of RowBg0 target, your color will override the existing RowBg0 color. +// If you set the color of RowBg1 or ColumnBg1 target, your color will blend over the RowBg0 color. +enum ImGuiTableBgTarget_ +{ + ImGuiTableBgTarget_None = 0, + ImGuiTableBgTarget_RowBg0 = 1, // Set row background color 0 (generally used for background, automatically set when ImGuiTableFlags_RowBg is used) + ImGuiTableBgTarget_RowBg1 = 2, // Set row background color 1 (generally used for selection marking) + ImGuiTableBgTarget_CellBg = 3, // Set cell background color (top-most color) +}; + +// Sorting specifications for a table (often handling sort specs for a single column, occasionally more) +// Obtained by calling TableGetSortSpecs(). +// When 'SpecsDirty == true' you can sort your data. It will be true with sorting specs have changed since last call, or the first time. +// Make sure to set 'SpecsDirty = false' after sorting, else you may wastefully sort your data every frame! +struct ImGuiTableSortSpecs +{ + const ImGuiTableColumnSortSpecs* Specs; // Pointer to sort spec array. + int SpecsCount; // Sort spec count. Most often 1. May be > 1 when ImGuiTableFlags_SortMulti is enabled. May be == 0 when ImGuiTableFlags_SortTristate is enabled. + bool SpecsDirty; // Set to true when specs have changed since last time! Use this to sort again, then clear the flag. + + ImGuiTableSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +// Sorting specification for one column of a table (sizeof == 12 bytes) +struct ImGuiTableColumnSortSpecs +{ + ImGuiID ColumnUserID; // User id of the column (if specified by a TableSetupColumn() call) + ImS16 ColumnIndex; // Index of the column + ImS16 SortOrder; // Index within parent ImGuiTableSortSpecs (always stored in order starting from 0, tables sorted on a single criteria will always have a 0 here) + ImGuiSortDirection SortDirection; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending + + ImGuiTableColumnSortSpecs() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Helpers: Debug log, memory allocations macros, ImVector<> +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Debug Logging into ShowDebugLogWindow(), tty and more. +//----------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS +#define IMGUI_DEBUG_LOG(...) ImGui::DebugLog(__VA_ARGS__) +#else +#define IMGUI_DEBUG_LOG(...) ((void)0) +#endif + +//----------------------------------------------------------------------------- +// IM_MALLOC(), IM_FREE(), IM_NEW(), IM_PLACEMENT_NEW(), IM_DELETE() +// We call C++ constructor on own allocated memory via the placement "new(ptr) Type()" syntax. +// Defining a custom placement new() with a custom parameter allows us to bypass including which on some platforms complains when user has disabled exceptions. +//----------------------------------------------------------------------------- + +struct ImNewWrapper {}; +inline void* operator new(size_t, ImNewWrapper, void* ptr) { return ptr; } +inline void operator delete(void*, ImNewWrapper, void*) {} // This is only required so we can use the symmetrical new() #define IM_ALLOC(_SIZE) ImGui::MemAlloc(_SIZE) #define IM_FREE(_PTR) ImGui::MemFree(_PTR) -#define IM_PLACEMENT_NEW(_PTR) new(ImNewDummy(), _PTR) -#define IM_NEW(_TYPE) new(ImNewDummy(), ImGui::MemAlloc(sizeof(_TYPE))) _TYPE +#define IM_PLACEMENT_NEW(_PTR) new(ImNewWrapper(), _PTR) +#define IM_NEW(_TYPE) new(ImNewWrapper(), ImGui::MemAlloc(sizeof(_TYPE))) _TYPE template void IM_DELETE(T* p) { if (p) { p->~T(); ImGui::MemFree(p); } } //----------------------------------------------------------------------------- -// Helper: ImVector<> +// ImVector<> // Lightweight std::vector<>-like class to avoid dragging dependencies (also, some implementations of STL with debug enabled are absurdly slow, we bypass it so our code runs fast in debug). -// You generally do NOT need to care or use this ever. But we need to make it available in imgui.h because some of our data structures are relying on it. -// Important: clear() frees memory, resize(0) keep the allocated buffer. We use resize(0) a lot to intentionally recycle allocated buffers across frames and amortize our costs. -// Important: our implementation does NOT call C++ constructors/destructors, we treat everything as raw data! This is intentional but be extra mindful of that, -// do NOT use this class as a std::vector replacement in your own code! Many of the structures used by dear imgui can be safely initialized by a zero-memset. +//----------------------------------------------------------------------------- +// - You generally do NOT need to care or use this ever. But we need to make it available in imgui.h because some of our public structures are relying on it. +// - We use std-like naming convention here, which is a little unusual for this codebase. +// - Important: clear() frees memory, resize(0) keep the allocated buffer. We use resize(0) a lot to intentionally recycle allocated buffers across frames and amortize our costs. +// - Important: our implementation does NOT call C++ constructors/destructors, we treat everything as raw data! This is intentional but be extra mindful of that, +// Do NOT use this class as a std::vector replacement in your own code! Many of the structures used by dear imgui can be safely initialized by a zero-memset. //----------------------------------------------------------------------------- +IM_MSVC_RUNTIME_CHECKS_OFF template struct ImVector { @@ -1242,17 +2164,21 @@ struct ImVector // Constructors, destructor inline ImVector() { Size = Capacity = 0; Data = NULL; } inline ImVector(const ImVector& src) { Size = Capacity = 0; Data = NULL; operator=(src); } - inline ImVector& operator=(const ImVector& src) { clear(); resize(src.Size); memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; } - inline ~ImVector() { if (Data) IM_FREE(Data); } + inline ImVector& operator=(const ImVector& src) { clear(); resize(src.Size); if (src.Data) memcpy(Data, src.Data, (size_t)Size * sizeof(T)); return *this; } + inline ~ImVector() { if (Data) IM_FREE(Data); } // Important: does not destruct anything + + inline void clear() { if (Data) { Size = Capacity = 0; IM_FREE(Data); Data = NULL; } } // Important: does not destruct anything + inline void clear_delete() { for (int n = 0; n < Size; n++) IM_DELETE(Data[n]); clear(); } // Important: never called automatically! always explicit. + inline void clear_destruct() { for (int n = 0; n < Size; n++) Data[n].~T(); clear(); } // Important: never called automatically! always explicit. inline bool empty() const { return Size == 0; } inline int size() const { return Size; } inline int size_in_bytes() const { return Size * (int)sizeof(T); } + inline int max_size() const { return 0x7FFFFFFF / (int)sizeof(T); } inline int capacity() const { return Capacity; } - inline T& operator[](int i) { IM_ASSERT(i < Size); return Data[i]; } - inline const T& operator[](int i) const { IM_ASSERT(i < Size); return Data[i]; } + inline T& operator[](int i) { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } + inline const T& operator[](int i) const { IM_ASSERT(i >= 0 && i < Size); return Data[i]; } - inline void clear() { if (Data) { Size = Capacity = 0; IM_FREE(Data); Data = NULL; } } inline T* begin() { return Data; } inline const T* begin() const { return Data; } inline T* end() { return Data + Size; } @@ -1263,29 +2189,34 @@ struct ImVector inline const T& back() const { IM_ASSERT(Size > 0); return Data[Size - 1]; } inline void swap(ImVector& rhs) { int rhs_size = rhs.Size; rhs.Size = Size; Size = rhs_size; int rhs_cap = rhs.Capacity; rhs.Capacity = Capacity; Capacity = rhs_cap; T* rhs_data = rhs.Data; rhs.Data = Data; Data = rhs_data; } - inline int _grow_capacity(int sz) const { int new_capacity = Capacity ? (Capacity + Capacity/2) : 8; return new_capacity > sz ? new_capacity : sz; } + inline int _grow_capacity(int sz) const { int new_capacity = Capacity ? (Capacity + Capacity / 2) : 8; return new_capacity > sz ? new_capacity : sz; } inline void resize(int new_size) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); Size = new_size; } inline void resize(int new_size, const T& v) { if (new_size > Capacity) reserve(_grow_capacity(new_size)); if (new_size > Size) for (int n = Size; n < new_size; n++) memcpy(&Data[n], &v, sizeof(v)); Size = new_size; } + inline void shrink(int new_size) { IM_ASSERT(new_size <= Size); Size = new_size; } // Resize a vector to a smaller size, guaranteed not to cause a reallocation inline void reserve(int new_capacity) { if (new_capacity <= Capacity) return; T* new_data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); if (Data) { memcpy(new_data, Data, (size_t)Size * sizeof(T)); IM_FREE(Data); } Data = new_data; Capacity = new_capacity; } + inline void reserve_discard(int new_capacity) { if (new_capacity <= Capacity) return; if (Data) IM_FREE(Data); Data = (T*)IM_ALLOC((size_t)new_capacity * sizeof(T)); Capacity = new_capacity; } // NB: It is illegal to call push_back/push_front/insert with a reference pointing inside the ImVector data itself! e.g. v.push_back(v[10]) is forbidden. inline void push_back(const T& v) { if (Size == Capacity) reserve(_grow_capacity(Size + 1)); memcpy(&Data[Size], &v, sizeof(v)); Size++; } inline void pop_back() { IM_ASSERT(Size > 0); Size--; } inline void push_front(const T& v) { if (Size == 0) push_back(v); else insert(Data, v); } - inline T* erase(const T* it) { IM_ASSERT(it >= Data && it < Data+Size); const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + 1, ((size_t)Size - (size_t)off - 1) * sizeof(T)); Size--; return Data + off; } - inline T* erase(const T* it, const T* it_last){ IM_ASSERT(it >= Data && it < Data+Size && it_last > it && it_last <= Data+Size); const ptrdiff_t count = it_last - it; const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + count, ((size_t)Size - (size_t)off - count) * sizeof(T)); Size -= (int)count; return Data + off; } - inline T* erase_unsorted(const T* it) { IM_ASSERT(it >= Data && it < Data+Size); const ptrdiff_t off = it - Data; if (it < Data+Size-1) memcpy(Data + off, Data + Size - 1, sizeof(T)); Size--; return Data + off; } - inline T* insert(const T* it, const T& v) { IM_ASSERT(it >= Data && it <= Data+Size); const ptrdiff_t off = it - Data; if (Size == Capacity) reserve(_grow_capacity(Size + 1)); if (off < (int)Size) memmove(Data + off + 1, Data + off, ((size_t)Size - (size_t)off) * sizeof(T)); memcpy(&Data[off], &v, sizeof(v)); Size++; return Data + off; } + inline T* erase(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + 1, ((size_t)Size - (size_t)off - 1) * sizeof(T)); Size--; return Data + off; } + inline T* erase(const T* it, const T* it_last){ IM_ASSERT(it >= Data && it < Data + Size && it_last >= it && it_last <= Data + Size); const ptrdiff_t count = it_last - it; const ptrdiff_t off = it - Data; memmove(Data + off, Data + off + count, ((size_t)Size - (size_t)off - (size_t)count) * sizeof(T)); Size -= (int)count; return Data + off; } + inline T* erase_unsorted(const T* it) { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; if (it < Data + Size - 1) memcpy(Data + off, Data + Size - 1, sizeof(T)); Size--; return Data + off; } + inline T* insert(const T* it, const T& v) { IM_ASSERT(it >= Data && it <= Data + Size); const ptrdiff_t off = it - Data; if (Size == Capacity) reserve(_grow_capacity(Size + 1)); if (off < (int)Size) memmove(Data + off + 1, Data + off, ((size_t)Size - (size_t)off) * sizeof(T)); memcpy(&Data[off], &v, sizeof(v)); Size++; return Data + off; } inline bool contains(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data++ == v) return true; return false; } inline T* find(const T& v) { T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } inline const T* find(const T& v) const { const T* data = Data; const T* data_end = Data + Size; while (data < data_end) if (*data == v) break; else ++data; return data; } + inline int find_index(const T& v) const { const T* data_end = Data + Size; const T* it = find(v); if (it == data_end) return -1; const ptrdiff_t off = it - Data; return (int)off; } inline bool find_erase(const T& v) { const T* it = find(v); if (it < Data + Size) { erase(it); return true; } return false; } inline bool find_erase_unsorted(const T& v) { const T* it = find(v); if (it < Data + Size) { erase_unsorted(it); return true; } return false; } - inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it <= Data+Size); const ptrdiff_t off = it - Data; return (int)off; } + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < Data + Size); const ptrdiff_t off = it - Data; return (int)off; } }; +IM_MSVC_RUNTIME_CHECKS_RESTORE //----------------------------------------------------------------------------- -// ImGuiStyle +// [SECTION] ImGuiStyle +//----------------------------------------------------------------------------- // You may modify the ImGui::GetStyle() main instance during initialization and before NewFrame(). // During the frame, use ImGui::PushStyleVar(ImGuiStyleVar_XXXX)/PopStyleVar() to alter the main style values, // and ImGui::PushStyleColor(ImGuiCol_XXX)/PopStyleColor() for colors. @@ -1294,10 +2225,12 @@ struct ImVector struct ImGuiStyle { float Alpha; // Global alpha applies to everything in Dear ImGui. + float DisabledAlpha; // Additional alpha multiplier applied by BeginDisabled(). Multiply over current value of Alpha. ImVec2 WindowPadding; // Padding within a window. - float WindowRounding; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. + float WindowRounding; // Radius of window corners rounding. Set to 0.0f to have rectangular windows. Large values tend to lead to variety of artifacts and are not recommended. float WindowBorderSize; // Thickness of border around windows. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). - ImVec2 WindowMinSize; // Minimum window size. This is a global setting. If you want to constraint individual windows, use SetNextWindowSizeConstraints(). + float WindowBorderHoverPadding; // Hit-testing extent outside/inside resizing border. Also extend determination of hovered window. Generally meaningfully larger than WindowBorderSize to make it easy to reach borders. + ImVec2 WindowMinSize; // Minimum window size. This is a global setting. If you want to constrain individual windows, use SetNextWindowSizeConstraints(). ImVec2 WindowTitleAlign; // Alignment for title bar text. Defaults to (0.0f,0.5f) for left-aligned,vertically centered. ImGuiDir WindowMenuButtonPosition; // Side of the collapsing/docking button in the title bar (None/Left/Right). Defaults to ImGuiDir_Left. float ChildRounding; // Radius of child window corners rounding. Set to 0.0f to have rectangular windows. @@ -1309,6 +2242,7 @@ struct ImGuiStyle float FrameBorderSize; // Thickness of border around frames. Generally set to 0.0f or 1.0f. (Other values are not well tested and more CPU/GPU costly). ImVec2 ItemSpacing; // Horizontal and vertical spacing between widgets/lines. ImVec2 ItemInnerSpacing; // Horizontal and vertical spacing between within elements of a composed widget (e.g. a slider and its label). + ImVec2 CellPadding; // Padding within a table cell. Cellpadding.x is locked for entire table. CellPadding.y may be altered between different rows. ImVec2 TouchExtraPadding; // Expand reactive bounding box for touch-based system where touch position is not accurate enough. Unfortunately we don't sort widgets so priority on overlap will always be given to the first widget. So don't grow this too much! float IndentSpacing; // Horizontal indentation when e.g. entering a tree node. Generally == (FontSize + FramePadding.x*2). float ColumnsMinSpacing; // Minimum horizontal spacing between two columns. Preferably > (FramePadding.x + 1). @@ -1316,179 +2250,340 @@ struct ImGuiStyle float ScrollbarRounding; // Radius of grab corners for scrollbar. float GrabMinSize; // Minimum width/height of a grab box for slider/scrollbar. float GrabRounding; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. + float LogSliderDeadzone; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. + float ImageBorderSize; // Thickness of border around Image() calls. float TabRounding; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. float TabBorderSize; // Thickness of border around tabs. + float TabCloseButtonMinWidthSelected; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. + float TabCloseButtonMinWidthUnselected; // -1: always visible. 0.0f: visible when hovered. >0.0f: visible when hovered if minimum width. FLT_MAX: never show close button when unselected. + float TabBarBorderSize; // Thickness of tab-bar separator, which takes on the tab active color to denote focus. + float TabBarOverlineSize; // Thickness of tab-bar overline, which highlights the selected tab-bar. + float TableAngledHeadersAngle; // Angle of angled headers (supported values range from -50.0f degrees to +50.0f degrees). + ImVec2 TableAngledHeadersTextAlign;// Alignment of angled headers within the cell ImGuiDir ColorButtonPosition; // Side of the color button in the ColorEdit4 widget (left/right). Defaults to ImGuiDir_Right. ImVec2 ButtonTextAlign; // Alignment of button text when button is larger than text. Defaults to (0.5f, 0.5f) (centered). - ImVec2 SelectableTextAlign; // Alignment of selectable text when selectable is larger than text. Defaults to (0.0f, 0.0f) (top-left aligned). - ImVec2 DisplayWindowPadding; // Window position are clamped to be visible within the display area by at least this amount. Only applies to regular windows. - ImVec2 DisplaySafeAreaPadding; // If you cannot see the edges of your screen (e.g. on a TV) increase the safe area padding. Apply to popups/tooltips as well regular windows. NB: Prefer configuring your TV sets correctly! - float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). May be removed later. - bool AntiAliasedLines; // Enable anti-aliasing on lines/borders. Disable if you are really tight on CPU/GPU. - bool AntiAliasedFill; // Enable anti-aliasing on filled shapes (rounded rectangles, circles, etc.) + ImVec2 SelectableTextAlign; // Alignment of selectable text. Defaults to (0.0f, 0.0f) (top-left aligned). It's generally important to keep this left-aligned if you want to lay multiple items on a same line. + float SeparatorTextBorderSize; // Thickness of border in SeparatorText() + ImVec2 SeparatorTextAlign; // Alignment of text within the separator. Defaults to (0.0f, 0.5f) (left aligned, center). + ImVec2 SeparatorTextPadding; // Horizontal offset of text from each edge of the separator + spacing on other axis. Generally small values. .y is recommended to be == FramePadding.y. + ImVec2 DisplayWindowPadding; // Apply to regular windows: amount which we enforce to keep visible when moving near edges of your screen. + ImVec2 DisplaySafeAreaPadding; // Apply to every windows, menus, popups, tooltips: amount where we avoid displaying contents. Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured). + float DockingSeparatorSize; // Thickness of resizing border between docked windows + float MouseCursorScale; // Scale software rendered mouse cursor (when io.MouseDrawCursor is enabled). We apply per-monitor DPI scaling over this scale. May be removed later. + bool AntiAliasedLines; // Enable anti-aliased lines/borders. Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedLinesUseTex; // Enable anti-aliased lines/borders using textures where possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). Latched at the beginning of the frame (copied to ImDrawList). + bool AntiAliasedFill; // Enable anti-aliased edges around filled shapes (rounded rectangles, circles, etc.). Disable if you are really tight on CPU/GPU. Latched at the beginning of the frame (copied to ImDrawList). float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. + float CircleTessellationMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + + // Colors ImVec4 Colors[ImGuiCol_COUNT]; + // Behaviors + // (It is possible to modify those fields mid-frame if specific behavior need it, unlike e.g. configuration fields in ImGuiIO) + float HoverStationaryDelay; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. + float HoverDelayShort; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayShort). Usually used along with HoverStationaryDelay. + float HoverDelayNormal; // Delay for IsItemHovered(ImGuiHoveredFlags_DelayNormal). " + ImGuiHoveredFlags HoverFlagsForTooltipMouse;// Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using mouse. + ImGuiHoveredFlags HoverFlagsForTooltipNav; // Default flags when using IsItemHovered(ImGuiHoveredFlags_ForTooltip) or BeginItemTooltip()/SetItemTooltip() while using keyboard/gamepad. + IMGUI_API ImGuiStyle(); IMGUI_API void ScaleAllSizes(float scale_factor); + + // Obsolete names +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // TabMinWidthForCloseButton = TabCloseButtonMinWidthUnselected // Renamed in 1.91.9. +#endif }; //----------------------------------------------------------------------------- -// ImGuiIO +// [SECTION] ImGuiIO +//----------------------------------------------------------------------------- // Communicate most settings and inputs/outputs to Dear ImGui using this structure. // Access via ImGui::GetIO(). Read 'Programmer guide' section in .cpp file for general usage. +// It is generally expected that: +// - initialization: backends and user code writes to ImGuiIO. +// - main loop: backends writes to ImGuiIO, user code and imgui code reads from ImGuiIO. //----------------------------------------------------------------------------- +// Also see ImGui::GetPlatformIO() and ImGuiPlatformIO struct for OS/platform related functions: clipboard, IME etc. +//----------------------------------------------------------------------------- + +// [Internal] Storage used by IsKeyDown(), IsKeyPressed() etc functions. +// If prior to 1.87 you used io.KeysDownDuration[] (which was marked as internal), you should use GetKeyData(key)->DownDuration and *NOT* io.KeysData[key]->DownDuration. +struct ImGuiKeyData +{ + bool Down; // True for if key is down + float DownDuration; // Duration the key has been down (<0.0f: not pressed, 0.0f: just pressed, >0.0f: time held) + float DownDurationPrev; // Last frame duration the key has been down + float AnalogValue; // 0.0f..1.0f for gamepad values +}; struct ImGuiIO { //------------------------------------------------------------------ - // Configuration (fill once) // Default value + // Configuration // Default value //------------------------------------------------------------------ - ImGuiConfigFlags ConfigFlags; // = 0 // See ImGuiConfigFlags_ enum. Set by user/application. Gamepad/keyboard navigation options, etc. - ImGuiBackendFlags BackendFlags; // = 0 // See ImGuiBackendFlags_ enum. Set by back-end (imgui_impl_xxx files or custom back-end) to communicate features supported by the back-end. - ImVec2 DisplaySize; // // Main display size, in pixels. - float DeltaTime; // = 1.0f/60.0f // Time elapsed since last frame, in seconds. + ImGuiConfigFlags ConfigFlags; // = 0 // See ImGuiConfigFlags_ enum. Set by user/application. Keyboard/Gamepad navigation options, etc. + ImGuiBackendFlags BackendFlags; // = 0 // See ImGuiBackendFlags_ enum. Set by backend (imgui_impl_xxx files or custom backend) to communicate features supported by the backend. + ImVec2 DisplaySize; // // Main display size, in pixels (generally == GetMainViewport()->Size). May change every frame. + float DeltaTime; // = 1.0f/60.0f // Time elapsed since last frame, in seconds. May change every frame. float IniSavingRate; // = 5.0f // Minimum time between saving positions/sizes to .ini file, in seconds. - const char* IniFilename; // = "imgui.ini" // Path to .ini file. Set NULL to disable automatic .ini loading/saving, if e.g. you want to manually load/save from memory. + const char* IniFilename; // = "imgui.ini" // Path to .ini file (important: default "imgui.ini" is relative to current working dir!). Set NULL to disable automatic .ini loading/saving or if you want to manually call LoadIniSettingsXXX() / SaveIniSettingsXXX() functions. const char* LogFilename; // = "imgui_log.txt"// Path to .log file (default parameter to ImGui::LogToFile when no file is specified). - float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. - float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. - float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. - int KeyMap[ImGuiKey_COUNT]; // // Map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. - float KeyRepeatDelay; // = 0.250f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). - float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. - void* UserData; // = NULL // Store your own data for retrieval by callbacks. + void* UserData; // = NULL // Store your own data. + // Font system ImFontAtlas*Fonts; // // Font atlas: load, rasterize and pack one or more fonts into a single texture. float FontGlobalScale; // = 1.0f // Global scale all fonts - bool FontAllowUserScaling; // = false // Allow user scaling text of individual window with CTRL+Wheel. + bool FontAllowUserScaling; // = false // [OBSOLETE] Allow user scaling text of individual window with CTRL+Wheel. ImFont* FontDefault; // = NULL // Font to use on NewFrame(). Use NULL to uses Fonts->Fonts[0]. ImVec2 DisplayFramebufferScale; // = (1, 1) // For retina display or other situations where window coordinates are different from framebuffer coordinates. This generally ends up in ImDrawData::FramebufferScale. + // Keyboard/Gamepad Navigation options + bool ConfigNavSwapGamepadButtons; // = false // Swap Activate<>Cancel (A<>B) buttons, matching typical "Nintendo/Japanese style" gamepad layout. + bool ConfigNavMoveSetMousePos; // = false // Directional/tabbing navigation teleports the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is difficult. Will update io.MousePos and set io.WantSetMousePos=true. + bool ConfigNavCaptureKeyboard; // = true // Sets io.WantCaptureKeyboard when io.NavActive is set. + bool ConfigNavEscapeClearFocusItem; // = true // Pressing Escape can clear focused item + navigation id/highlight. Set to false if you want to always keep highlight on. + bool ConfigNavEscapeClearFocusWindow;// = false // Pressing Escape can clear focused window as well (super set of io.ConfigNavEscapeClearFocusItem). + bool ConfigNavCursorVisibleAuto; // = true // Using directional navigation key makes the cursor visible. Mouse click hides the cursor. + bool ConfigNavCursorVisibleAlways; // = false // Navigation cursor is always visible. + + // Docking options (when ImGuiConfigFlags_DockingEnable is set) + bool ConfigDockingNoSplit; // = false // Simplified docking mode: disable window splitting, so docking is limited to merging multiple windows together into tab-bars. + bool ConfigDockingWithShift; // = false // Enable docking with holding Shift key (reduce visual noise, allows dropping in wider space) + bool ConfigDockingAlwaysTabBar; // = false // [BETA] [FIXME: This currently creates regression with auto-sizing and general overhead] Make every single floating window display within a docking node. + bool ConfigDockingTransparentPayload;// = false // [BETA] Make window or viewport transparent when docking and only display docking boxes on the target viewport. Useful if rendering of multiple viewport cannot be synced. Best used with ConfigViewportsNoAutoMerge. + + // Viewport options (when ImGuiConfigFlags_ViewportsEnable is set) + bool ConfigViewportsNoAutoMerge; // = false; // Set to make all floating imgui windows always create their own viewport. Otherwise, they are merged into the main host viewports when overlapping it. May also set ImGuiViewportFlags_NoAutoMerge on individual viewport. + bool ConfigViewportsNoTaskBarIcon; // = false // Disable default OS task bar icon flag for secondary viewports. When a viewport doesn't want a task bar icon, ImGuiViewportFlags_NoTaskBarIcon will be set on it. + bool ConfigViewportsNoDecoration; // = true // Disable default OS window decoration flag for secondary viewports. When a viewport doesn't want window decorations, ImGuiViewportFlags_NoDecoration will be set on it. Enabling decoration can create subsequent issues at OS levels (e.g. minimum window size). + bool ConfigViewportsNoDefaultParent; // = false // Disable default OS parenting to main viewport for secondary viewports. By default, viewports are marked with ParentViewportId = , expecting the platform backend to setup a parent/child relationship between the OS windows (some backend may ignore this). Set to true if you want the default to be 0, then all viewports will be top-level OS windows. + // Miscellaneous options - bool MouseDrawCursor; // = false // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). Cannot be easily renamed to 'io.ConfigXXX' because this is frequently used by back-end implementations. - bool ConfigMacOSXBehaviors; // = defined(__APPLE__) // OS X style: Text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl (was called io.OptMacOSXBehaviors prior to 1.63) - bool ConfigInputTextCursorBlink; // = true // Set to false to disable blinking cursor, for users who consider it distracting. (was called: io.OptCursorBlink prior to 1.63) - bool ConfigWindowsResizeFromEdges; // = true // Enable resizing of windows from their edges and from the lower-left corner. This requires (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) because it needs mouse cursor feedback. (This used to be a per-window ImGuiWindowFlags_ResizeFromAnySide flag) - bool ConfigWindowsMoveFromTitleBarOnly; // = false // [BETA] Set to true to only allow moving windows when clicked+dragged from the title bar. Windows without a title bar are not affected. - float ConfigWindowsMemoryCompactTimer;// = 60.0f // [BETA] Compact window memory usage when unused. Set to -1.0f to disable. + // (you can visualize and interact with all options in 'Demo->Configuration') + bool MouseDrawCursor; // = false // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). Cannot be easily renamed to 'io.ConfigXXX' because this is frequently used by backend implementations. + bool ConfigMacOSXBehaviors; // = defined(__APPLE__) // Swap Cmd<>Ctrl keys + OS X style text editing cursor movement using Alt instead of Ctrl, Shortcuts using Cmd/Super instead of Ctrl, Line/Text Start and End using Cmd+Arrows instead of Home/End, Double click selects by word instead of selecting whole text, Multi-selection in lists uses Cmd/Super instead of Ctrl. + bool ConfigInputTrickleEventQueue; // = true // Enable input queue trickling: some types of events submitted during the same frame (e.g. button down + up) will be spread over multiple frames, improving interactions with low framerates. + bool ConfigInputTextCursorBlink; // = true // Enable blinking cursor (optional as some users consider it to be distracting). + bool ConfigInputTextEnterKeepActive; // = false // [BETA] Pressing Enter will keep item active and select contents (single-line only). + bool ConfigDragClickToInputText; // = false // [BETA] Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving). Not desirable on devices without a keyboard. + bool ConfigWindowsResizeFromEdges; // = true // Enable resizing of windows from their edges and from the lower-left corner. This requires ImGuiBackendFlags_HasMouseCursors for better mouse cursor feedback. (This used to be a per-window ImGuiWindowFlags_ResizeFromAnySide flag) + bool ConfigWindowsMoveFromTitleBarOnly; // = false // Enable allowing to move windows only when clicking on their title bar. Does not apply to windows without a title bar. + bool ConfigWindowsCopyContentsWithCtrlC; // = false // [EXPERIMENTAL] CTRL+C copy the contents of focused window into the clipboard. Experimental because: (1) has known issues with nested Begin/End pairs (2) text output quality varies (3) text output is in submission order rather than spatial order. + bool ConfigScrollbarScrollByPage; // = true // Enable scrolling page by page when clicking outside the scrollbar grab. When disabled, always scroll to clicked location. When enabled, Shift+Click scrolls to clicked location. + float ConfigMemoryCompactTimer; // = 60.0f // Timer (in seconds) to free transient windows/tables memory buffers when unused. Set to -1.0f to disable. + + // Inputs Behaviors + // (other variables, ones which are expected to be tweaked within UI code, are exposed in ImGuiStyle) + float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. + float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. + float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. + float KeyRepeatDelay; // = 0.275f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). + float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. //------------------------------------------------------------------ - // Platform Functions - // (the imgui_impl_xxxx back-end files are setting those up for you) + // Debug options //------------------------------------------------------------------ - // Optional: Platform/Renderer back-end name (informational only! will be displayed in About Window) + User data for back-end/wrappers to store their own stuff. + // Options to configure Error Handling and how we handle recoverable errors [EXPERIMENTAL] + // - Error recovery is provided as a way to facilitate: + // - Recovery after a programming error (native code or scripting language - the later tends to facilitate iterating on code while running). + // - Recovery after running an exception handler or any error processing which may skip code after an error has been detected. + // - Error recovery is not perfect nor guaranteed! It is a feature to ease development. + // You not are not supposed to rely on it in the course of a normal application run. + // - Functions that support error recovery are using IM_ASSERT_USER_ERROR() instead of IM_ASSERT(). + // - By design, we do NOT allow error recovery to be 100% silent. One of the three options needs to be checked! + // - Always ensure that on programmers seats you have at minimum Asserts or Tooltips enabled when making direct imgui API calls! + // Otherwise it would severely hinder your ability to catch and correct mistakes! + // Read https://github.com/ocornut/imgui/wiki/Error-Handling for details. + // - Programmer seats: keep asserts (default), or disable asserts and keep error tooltips (new and nice!) + // - Non-programmer seats: maybe disable asserts, but make sure errors are resurfaced (tooltips, visible log entries, use callback etc.) + // - Recovery after error/exception: record stack sizes with ErrorRecoveryStoreState(), disable assert, set log callback (to e.g. trigger high-level breakpoint), recover with ErrorRecoveryTryToRecoverState(), restore settings. + bool ConfigErrorRecovery; // = true // Enable error recovery support. Some errors won't be detected and lead to direct crashes if recovery is disabled. + bool ConfigErrorRecoveryEnableAssert; // = true // Enable asserts on recoverable error. By default call IM_ASSERT() when returning from a failing IM_ASSERT_USER_ERROR() + bool ConfigErrorRecoveryEnableDebugLog; // = true // Enable debug log output on recoverable errors. + bool ConfigErrorRecoveryEnableTooltip; // = true // Enable tooltip on recoverable errors. The tooltip include a way to enable asserts if they were disabled. + + // Option to enable various debug tools showing buttons that will call the IM_DEBUG_BREAK() macro. + // - The Item Picker tool will be available regardless of this being enabled, in order to maximize its discoverability. + // - Requires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application. + // e.g. io.ConfigDebugIsDebuggerPresent = ::IsDebuggerPresent() on Win32, or refer to ImOsIsDebuggerPresent() imgui_test_engine/imgui_te_utils.cpp for a Unix compatible version). + bool ConfigDebugIsDebuggerPresent; // = false // Enable various tools calling IM_DEBUG_BREAK(). + + // Tools to detect code submitting items with conflicting/duplicate IDs + // - Code should use PushID()/PopID() in loops, or append "##xx" to same-label identifiers. + // - Empty label e.g. Button("") == same ID as parent widget/node. Use Button("##xx") instead! + // - See FAQ https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#q-about-the-id-stack-system + bool ConfigDebugHighlightIdConflicts;// = true // Highlight and show an error message popup when multiple items have conflicting identifiers. + bool ConfigDebugHighlightIdConflictsShowItemPicker;//=true // Show "Item Picker" button in aforementioned popup. + + // Tools to test correct Begin/End and BeginChild/EndChild behaviors. + // - Presently Begin()/End() and BeginChild()/EndChild() needs to ALWAYS be called in tandem, regardless of return value of BeginXXX() + // - This is inconsistent with other BeginXXX functions and create confusion for many users. + // - We expect to update the API eventually. In the meanwhile we provide tools to facilitate checking user-code behavior. + bool ConfigDebugBeginReturnValueOnce;// = false // First-time calls to Begin()/BeginChild() will return false. NEEDS TO BE SET AT APPLICATION BOOT TIME if you don't want to miss windows. + bool ConfigDebugBeginReturnValueLoop;// = false // Some calls to Begin()/BeginChild() will return false. Will cycle through window depths then repeat. Suggested use: add "io.ConfigDebugBeginReturnValue = io.KeyShift" in your main loop then occasionally press SHIFT. Windows should be flickering while running. + + // Option to deactivate io.AddFocusEvent(false) handling. + // - May facilitate interactions with a debugger when focus loss leads to clearing inputs data. + // - Backends may have other side-effects on focus loss, so this will reduce side-effects but not necessary remove all of them. + bool ConfigDebugIgnoreFocusLoss; // = false // Ignore io.AddFocusEvent(false), consequently not calling io.ClearInputKeys()/io.ClearInputMouse() in input processing. + + // Option to audit .ini data + bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower) + + //------------------------------------------------------------------ + // Platform Identifiers + // (the imgui_impl_xxxx backend files are setting those up for you) + //------------------------------------------------------------------ + + // Nowadays those would be stored in ImGuiPlatformIO but we are leaving them here for legacy reasons. + // Optional: Platform/Renderer backend name (informational only! will be displayed in About Window) + User data for backend/wrappers to store their own stuff. const char* BackendPlatformName; // = NULL const char* BackendRendererName; // = NULL - void* BackendPlatformUserData; // = NULL - void* BackendRendererUserData; // = NULL - void* BackendLanguageUserData; // = NULL + void* BackendPlatformUserData; // = NULL // User data for platform backend + void* BackendRendererUserData; // = NULL // User data for renderer backend + void* BackendLanguageUserData; // = NULL // User data for non C++ programming language backend - // Optional: Access OS clipboard - // (default to use native Win32 clipboard on Windows, otherwise uses a private clipboard. Override to access OS clipboard on other architectures) - const char* (*GetClipboardTextFn)(void* user_data); - void (*SetClipboardTextFn)(void* user_data, const char* text); - void* ClipboardUserData; + //------------------------------------------------------------------ + // Input - Call before calling NewFrame() + //------------------------------------------------------------------ - // Optional: Notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME on Windows) - // (default to use native imm32 api on Windows) - void (*ImeSetInputScreenPosFn)(int x, int y); - void* ImeWindowHandle; // = NULL // (Windows) Set this to your HWND to get automatic IME cursor positioning. + // Input Functions + IMGUI_API void AddKeyEvent(ImGuiKey key, bool down); // Queue a new key down/up event. Key should be "translated" (as in, generally ImGuiKey_A matches the key end-user would use to emit an 'A' character) + IMGUI_API void AddKeyAnalogEvent(ImGuiKey key, bool down, float v); // Queue a new key down/up event for analog values (e.g. ImGuiKey_Gamepad_ values). Dead-zones should be handled by the backend. + IMGUI_API void AddMousePosEvent(float x, float y); // Queue a mouse position update. Use -FLT_MAX,-FLT_MAX to signify no mouse (e.g. app not focused and not hovered) + IMGUI_API void AddMouseButtonEvent(int button, bool down); // Queue a mouse button change + IMGUI_API void AddMouseWheelEvent(float wheel_x, float wheel_y); // Queue a mouse wheel update. wheel_y<0: scroll down, wheel_y>0: scroll up, wheel_x<0: scroll right, wheel_x>0: scroll left. + IMGUI_API void AddMouseSourceEvent(ImGuiMouseSource source); // Queue a mouse source change (Mouse/TouchScreen/Pen) + IMGUI_API void AddMouseViewportEvent(ImGuiID id); // Queue a mouse hovered viewport. Requires backend to set ImGuiBackendFlags_HasMouseHoveredViewport to call this (for multi-viewport support). + IMGUI_API void AddFocusEvent(bool focused); // Queue a gain/loss of focus for the application (generally based on OS/platform focus of your window) + IMGUI_API void AddInputCharacter(unsigned int c); // Queue a new character input + IMGUI_API void AddInputCharacterUTF16(ImWchar16 c); // Queue a new character input from a UTF-16 character, it can be a surrogate + IMGUI_API void AddInputCharactersUTF8(const char* str); // Queue a new characters input from a UTF-8 string + IMGUI_API void SetKeyEventNativeData(ImGuiKey key, int native_keycode, int native_scancode, int native_legacy_index = -1); // [Optional] Specify index for legacy <1.87 IsKeyXXX() functions with native indices + specify native keycode, scancode. + IMGUI_API void SetAppAcceptingEvents(bool accepting_events); // Set master flag for accepting key/mouse/text events (default to true). Useful if you have native dialog boxes that are interrupting your application loop/refresh, and you want to disable events being queued while your app is frozen. + IMGUI_API void ClearEventsQueue(); // Clear all incoming events. + IMGUI_API void ClearInputKeys(); // Clear current keyboard/gamepad state + current frame text input buffer. Equivalent to releasing all keys/buttons. + IMGUI_API void ClearInputMouse(); // Clear current mouse state. #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - // [OBSOLETE since 1.60+] Rendering function, will be automatically called in Render(). Please call your rendering function yourself now! - // You can obtain the ImDrawData* by calling ImGui::GetDrawData() after Render(). See example applications if you are unsure of how to implement this. - void (*RenderDrawListsFn)(ImDrawData* data); -#else - // This is only here to keep ImGuiIO the same size/layout, so that IMGUI_DISABLE_OBSOLETE_FUNCTIONS can exceptionally be used outside of imconfig.h. - void* RenderDrawListsFnUnused; + IMGUI_API void ClearInputCharacters(); // [Obsoleted in 1.89.8] Clear the current frame text input buffer. Now included within ClearInputKeys(). #endif //------------------------------------------------------------------ - // Input - Fill before calling NewFrame() + // Output - Updated by NewFrame() or EndFrame()/Render() + // (when reading from the io.WantCaptureMouse, io.WantCaptureKeyboard flags to dispatch your inputs, it is + // generally easier and more correct to use their state BEFORE calling NewFrame(). See FAQ for details!) //------------------------------------------------------------------ - ImVec2 MousePos; // Mouse position, in pixels. Set to ImVec2(-FLT_MAX,-FLT_MAX) if mouse is unavailable (on another screen, etc.) - bool MouseDown[5]; // Mouse buttons: 0=left, 1=right, 2=middle + extras. ImGui itself mostly only uses left button (BeginPopupContext** are using right button). Others buttons allows us to track if the mouse is being used by your application + available to user as a convenience via IsMouse** API. - float MouseWheel; // Mouse wheel Vertical: 1 unit scrolls about 5 lines text. - float MouseWheelH; // Mouse wheel Horizontal. Most users don't have a mouse with an horizontal wheel, may not be filled by all back-ends. - bool KeyCtrl; // Keyboard modifier pressed: Control - bool KeyShift; // Keyboard modifier pressed: Shift - bool KeyAlt; // Keyboard modifier pressed: Alt - bool KeySuper; // Keyboard modifier pressed: Cmd/Super/Windows - bool KeysDown[512]; // Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). - float NavInputs[ImGuiNavInput_COUNT]; // Gamepad inputs. Cleared back to zero by EndFrame(). Keyboard keys will be auto-mapped and be written here by NewFrame(). - - // Functions - IMGUI_API void AddInputCharacter(unsigned int c); // Queue new character input - IMGUI_API void AddInputCharactersUTF8(const char* str); // Queue new characters input from an UTF-8 string - IMGUI_API void ClearInputCharacters(); // Clear the text input buffer manually + bool WantCaptureMouse; // Set when Dear ImGui will use mouse inputs, in this case do not dispatch them to your main game/application (either way, always pass on mouse inputs to imgui). (e.g. unclicked mouse is hovering over an imgui window, widget is active, mouse was clicked over an imgui window, etc.). + bool WantCaptureKeyboard; // Set when Dear ImGui will use keyboard inputs, in this case do not dispatch them to your main game/application (either way, always pass keyboard inputs to imgui). (e.g. InputText active, or an imgui window is focused and navigation is enabled, etc.). + bool WantTextInput; // Mobile/console: when set, you may display an on-screen keyboard. This is set by Dear ImGui when it wants textual keyboard input to happen (e.g. when a InputText widget is active). + bool WantSetMousePos; // MousePos has been altered, backend should reposition mouse on next frame. Rarely used! Set only when io.ConfigNavMoveSetMousePos is enabled. + bool WantSaveIniSettings; // When manual .ini load/save is active (io.IniFilename == NULL), this will be set to notify your application that you can call SaveIniSettingsToMemory() and save yourself. Important: clear io.WantSaveIniSettings yourself after saving! + bool NavActive; // Keyboard/Gamepad navigation is currently allowed (will handle ImGuiKey_NavXXX events) = a window is focused and it doesn't use the ImGuiWindowFlags_NoNavInputs flag. + bool NavVisible; // Keyboard/Gamepad navigation highlight is visible and allowed (will handle ImGuiKey_NavXXX events). + float Framerate; // Estimate of application framerate (rolling average over 60 frames, based on io.DeltaTime), in frame per second. Solely for convenience. Slow applications may not want to use a moving average or may want to reset underlying buffers occasionally. + int MetricsRenderVertices; // Vertices output during last call to Render() + int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 + int MetricsRenderWindows; // Number of visible windows + int MetricsActiveWindows; // Number of active windows + ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. //------------------------------------------------------------------ - // Output - Retrieve after calling NewFrame() + // [Internal] Dear ImGui will maintain those fields. Forward compatibility not guaranteed! //------------------------------------------------------------------ - bool WantCaptureMouse; // When io.WantCaptureMouse is true, imgui will use the mouse inputs, do not dispatch them to your main game/application (in both cases, always pass on mouse inputs to imgui). (e.g. unclicked mouse is hovering over an imgui window, widget is active, mouse was clicked over an imgui window, etc.). - bool WantCaptureKeyboard; // When io.WantCaptureKeyboard is true, imgui will use the keyboard inputs, do not dispatch them to your main game/application (in both cases, always pass keyboard inputs to imgui). (e.g. InputText active, or an imgui window is focused and navigation is enabled, etc.). - bool WantTextInput; // Mobile/console: when io.WantTextInput is true, you may display an on-screen keyboard. This is set by ImGui when it wants textual keyboard input to happen (e.g. when a InputText widget is active). - bool WantSetMousePos; // MousePos has been altered, back-end should reposition mouse on next frame. Set only when ImGuiConfigFlags_NavEnableSetMousePos flag is enabled. - bool WantSaveIniSettings; // When manual .ini load/save is active (io.IniFilename == NULL), this will be set to notify your application that you can call SaveIniSettingsToMemory() and save yourself. IMPORTANT: You need to clear io.WantSaveIniSettings yourself. - bool NavActive; // Directional navigation is currently allowed (will handle ImGuiKey_NavXXX events) = a window is focused and it doesn't use the ImGuiWindowFlags_NoNavInputs flag. - bool NavVisible; // Directional navigation is visible and allowed (will handle ImGuiKey_NavXXX events). - float Framerate; // Application framerate estimation, in frame per second. Solely for convenience. Rolling average estimation based on IO.DeltaTime over 120 frames - int MetricsRenderVertices; // Vertices output during last call to Render() - int MetricsRenderIndices; // Indices output during last call to Render() = number of triangles * 3 - int MetricsRenderWindows; // Number of visible windows - int MetricsActiveWindows; // Number of active windows - int MetricsActiveAllocations; // Number of active allocations, updated by MemAlloc/MemFree based on current context. May be off if you have multiple imgui contexts. - ImVec2 MouseDelta; // Mouse delta. Note that this is zero if either current or previous position are invalid (-FLT_MAX,-FLT_MAX), so a disappearing/reappearing mouse won't have a huge delta. + ImGuiContext* Ctx; // Parent UI context (needs to be set explicitly by parent). - //------------------------------------------------------------------ - // [Internal] ImGui will maintain those fields. Forward compatibility not guaranteed! - //------------------------------------------------------------------ + // Main Input State + // (this block used to be written by backend, since 1.87 it is best to NOT write to those directly, call the AddXXX functions above instead) + // (reading from those variables is fair game, as they are extremely unlikely to be moving anywhere) + ImVec2 MousePos; // Mouse position, in pixels. Set to ImVec2(-FLT_MAX, -FLT_MAX) if mouse is unavailable (on another screen, etc.) + bool MouseDown[5]; // Mouse buttons: 0=left, 1=right, 2=middle + extras (ImGuiMouseButton_COUNT == 5). Dear ImGui mostly uses left and right buttons. Other buttons allow us to track if the mouse is being used by your application + available to user as a convenience via IsMouse** API. + float MouseWheel; // Mouse wheel Vertical: 1 unit scrolls about 5 lines text. >0 scrolls Up, <0 scrolls Down. Hold SHIFT to turn vertical scroll into horizontal scroll. + float MouseWheelH; // Mouse wheel Horizontal. >0 scrolls Left, <0 scrolls Right. Most users don't have a mouse with a horizontal wheel, may not be filled by all backends. + ImGuiMouseSource MouseSource; // Mouse actual input peripheral (Mouse/TouchScreen/Pen). + ImGuiID MouseHoveredViewport; // (Optional) Modify using io.AddMouseViewportEvent(). With multi-viewports: viewport the OS mouse is hovering. If possible _IGNORING_ viewports with the ImGuiViewportFlags_NoInputs flag is much better (few backends can handle that). Set io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport if you can provide this info. If you don't imgui will infer the value using the rectangles and last focused time of the viewports it knows about (ignoring other OS windows). + bool KeyCtrl; // Keyboard modifier down: Control + bool KeyShift; // Keyboard modifier down: Shift + bool KeyAlt; // Keyboard modifier down: Alt + bool KeySuper; // Keyboard modifier down: Cmd/Super/Windows - ImVec2 MousePosPrev; // Previous mouse position (note that MouseDelta is not necessary == MousePos-MousePosPrev, in case either position is invalid) - ImVec2 MouseClickedPos[5]; // Position at time of clicking - double MouseClickedTime[5]; // Time of last click (used to figure out double-click) - bool MouseClicked[5]; // Mouse button went from !Down to Down - bool MouseDoubleClicked[5]; // Has mouse button been double-clicked? - bool MouseReleased[5]; // Mouse button went from Down to !Down - bool MouseDownOwned[5]; // Track if button was clicked inside a dear imgui window. We don't request mouse capture from the application if click started outside ImGui bounds. - bool MouseDownWasDoubleClick[5]; // Track if button down was a double-click - float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) - float MouseDownDurationPrev[5]; // Previous time the mouse button has been down - ImVec2 MouseDragMaxDistanceAbs[5]; // Maximum distance, absolute, on each axis, of how much mouse has traveled from the clicking point - float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point - float KeysDownDuration[512]; // Duration the keyboard key has been down (0.0f == just pressed) - float KeysDownDurationPrev[512]; // Previous duration the key has been down - float NavInputsDownDuration[ImGuiNavInput_COUNT]; - float NavInputsDownDurationPrev[ImGuiNavInput_COUNT]; - ImVector InputQueueCharacters; // Queue of _characters_ input (obtained by platform back-end). Fill using AddInputCharacter() helper. + // Other state maintained from data above + IO function calls + ImGuiKeyChord KeyMods; // Key mods flags (any of ImGuiMod_Ctrl/ImGuiMod_Shift/ImGuiMod_Alt/ImGuiMod_Super flags, same as io.KeyCtrl/KeyShift/KeyAlt/KeySuper but merged into flags. Read-only, updated by NewFrame() + ImGuiKeyData KeysData[ImGuiKey_NamedKey_COUNT];// Key state for all known keys. Use IsKeyXXX() functions to access this. + bool WantCaptureMouseUnlessPopupClose; // Alternative to WantCaptureMouse: (WantCaptureMouse == true && WantCaptureMouseUnlessPopupClose == false) when a click over void is expected to close a popup. + ImVec2 MousePosPrev; // Previous mouse position (note that MouseDelta is not necessary == MousePos-MousePosPrev, in case either position is invalid) + ImVec2 MouseClickedPos[5]; // Position at time of clicking + double MouseClickedTime[5]; // Time of last click (used to figure out double-click) + bool MouseClicked[5]; // Mouse button went from !Down to Down (same as MouseClickedCount[x] != 0) + bool MouseDoubleClicked[5]; // Has mouse button been double-clicked? (same as MouseClickedCount[x] == 2) + ImU16 MouseClickedCount[5]; // == 0 (not clicked), == 1 (same as MouseClicked[]), == 2 (double-clicked), == 3 (triple-clicked) etc. when going from !Down to Down + ImU16 MouseClickedLastCount[5]; // Count successive number of clicks. Stays valid after mouse release. Reset after another click is done. + bool MouseReleased[5]; // Mouse button went from Down to !Down + double MouseReleasedTime[5]; // Time of last released (rarely used! but useful to handle delayed single-click when trying to disambiguate them from double-click). + bool MouseDownOwned[5]; // Track if button was clicked inside a dear imgui window or over void blocked by a popup. We don't request mouse capture from the application if click started outside ImGui bounds. + bool MouseDownOwnedUnlessPopupClose[5]; // Track if button was clicked inside a dear imgui window. + bool MouseWheelRequestAxisSwap; // On a non-Mac system, holding SHIFT requests WheelY to perform the equivalent of a WheelX event. On a Mac system this is already enforced by the system. + bool MouseCtrlLeftAsRightClick; // (OSX) Set to true when the current click was a Ctrl+click that spawned a simulated right click + float MouseDownDuration[5]; // Duration the mouse button has been down (0.0f == just clicked) + float MouseDownDurationPrev[5]; // Previous time the mouse button has been down + ImVec2 MouseDragMaxDistanceAbs[5]; // Maximum distance, absolute, on each axis, of how much mouse has traveled from the clicking point + float MouseDragMaxDistanceSqr[5]; // Squared maximum distance of how much mouse has traveled from the clicking point (used for moving thresholds) + float PenPressure; // Touch/Pen pressure (0.0f to 1.0f, should be >0.0f only when MouseDown[0] == true). Helper storage currently unused by Dear ImGui. + bool AppFocusLost; // Only modify via AddFocusEvent() + bool AppAcceptingEvents; // Only modify via SetAppAcceptingEvents() + ImWchar16 InputQueueSurrogate; // For AddInputCharacterUTF16() + ImVector InputQueueCharacters; // Queue of _characters_ input (obtained by platform backend). Fill using AddInputCharacter() helper. + + // Legacy: before 1.87, we required backend to fill io.KeyMap[] (imgui->native map) during initialization and io.KeysDown[] (native indices) every frame. + // This is still temporarily supported as a legacy feature. However the new preferred scheme is for backend to call io.AddKeyEvent(). + // Old (<1.87): ImGui::IsKeyPressed(ImGui::GetIO().KeyMap[ImGuiKey_Space]) --> New (1.87+) ImGui::IsKeyPressed(ImGuiKey_Space) + // Old (<1.87): ImGui::IsKeyPressed(MYPLATFORM_KEY_SPACE) --> New (1.87+) ImGui::IsKeyPressed(ImGuiKey_Space) + // Read https://github.com/ocornut/imgui/issues/4921 for details. + //int KeyMap[ImGuiKey_COUNT]; // [LEGACY] Input: map of indices into the KeysDown[512] entries array which represent your "native" keyboard state. The first 512 are now unused and should be kept zero. Legacy backend will write into KeyMap[] using ImGuiKey_ indices which are always >512. + //bool KeysDown[ImGuiKey_COUNT]; // [LEGACY] Input: Keyboard keys that are pressed (ideally left in the "native" order your engine has access to keyboard keys, so you can use your own defines/enums for keys). This used to be [512] sized. It is now ImGuiKey_COUNT to allow legacy io.KeysDown[GetKeyIndex(...)] to work without an overflow. + //float NavInputs[ImGuiNavInput_COUNT]; // [LEGACY] Since 1.88, NavInputs[] was removed. Backends from 1.60 to 1.86 won't build. Feed gamepad inputs via io.AddKeyEvent() and ImGuiKey_GamepadXXX enums. + //void* ImeWindowHandle; // [Obsoleted in 1.87] Set ImGuiViewport::PlatformHandleRaw instead. Set this to your HWND to get automatic IME cursor positioning. + + // Legacy: before 1.91.1, clipboard functions were stored in ImGuiIO instead of ImGuiPlatformIO. + // As this is will affect all users of custom engines/backends, we are providing proper legacy redirection (will obsolete). +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + const char* (*GetClipboardTextFn)(void* user_data); + void (*SetClipboardTextFn)(void* user_data, const char* text); + void* ClipboardUserData; +#endif IMGUI_API ImGuiIO(); }; //----------------------------------------------------------------------------- -// Misc data structures +// [SECTION] Misc data structures (ImGuiInputTextCallbackData, ImGuiSizeCallbackData, ImGuiPayload) //----------------------------------------------------------------------------- // Shared state of InputText(), passed as an argument to your callback when a ImGuiInputTextFlags_Callback* flag is used. // The callback function should return 0 by default. // Callbacks (follow a flag name and see comments in ImGuiInputTextFlags_ declarations for more details) +// - ImGuiInputTextFlags_CallbackEdit: Callback on buffer edit. Note that InputText() already returns true on edit + you can always use IsItemEdited(). The callback is useful to manipulate the underlying buffer while focus is active. +// - ImGuiInputTextFlags_CallbackAlways: Callback on each iteration // - ImGuiInputTextFlags_CallbackCompletion: Callback on pressing TAB // - ImGuiInputTextFlags_CallbackHistory: Callback on pressing Up/Down arrows -// - ImGuiInputTextFlags_CallbackAlways: Callback on each iteration // - ImGuiInputTextFlags_CallbackCharFilter: Callback on character inputs to replace or discard them. Modify 'EventChar' to replace or discard, or return 1 in callback to discard. // - ImGuiInputTextFlags_CallbackResize: Callback on buffer capacity changes request (beyond 'buf_size' parameter value), allowing the string to grow. struct ImGuiInputTextCallbackData { + ImGuiContext* Ctx; // Parent UI context ImGuiInputTextFlags EventFlag; // One ImGuiInputTextFlags_Callback* // Read-only ImGuiInputTextFlags Flags; // What user passed to InputText() // Read-only void* UserData; // What user passed to InputText() // Read-only // Arguments for the different callback events + // - During Resize callback, Buf will be same as your input buffer. + // - However, during Completion/History/Always callback, Buf always points to our own internal data (it is not the same as your buffer)! Changes to it will be reflected into your own buffer shortly after the callback. // - To modify the text buffer in a callback, prefer using the InsertChars() / DeleteChars() function. InsertChars() will take care of calling the resize callback if necessary. // - If you know your edits are not going to resize the underlying buffer allocation, you may modify the contents of 'Buf[]' directly. You need to update 'BufTextLen' accordingly (0 <= BufTextLen < BufSize) and set 'BufDirty'' to true so InputText can update its internal state. ImWchar EventChar; // Character input // Read-write // [CharFilter] Replace character with another one, or set to zero to drop. return 1 is equivalent to setting EventChar=0; @@ -1506,19 +2601,43 @@ struct ImGuiInputTextCallbackData IMGUI_API ImGuiInputTextCallbackData(); IMGUI_API void DeleteChars(int pos, int bytes_count); IMGUI_API void InsertChars(int pos, const char* text, const char* text_end = NULL); - bool HasSelection() const { return SelectionStart != SelectionEnd; } + void SelectAll() { SelectionStart = 0; SelectionEnd = BufTextLen; } + void ClearSelection() { SelectionStart = SelectionEnd = BufTextLen; } + bool HasSelection() const { return SelectionStart != SelectionEnd; } }; // Resizing callback data to apply custom constraint. As enabled by SetNextWindowSizeConstraints(). Callback is called during the next Begin(). // NB: For basic min/max size constraint on each axis you don't need to use the callback! The SetNextWindowSizeConstraints() parameters are enough. struct ImGuiSizeCallbackData { - void* UserData; // Read-only. What user passed to SetNextWindowSizeConstraints() + void* UserData; // Read-only. What user passed to SetNextWindowSizeConstraints(). Generally store an integer or float in here (need reinterpret_cast<>). ImVec2 Pos; // Read-only. Window position, for reference. ImVec2 CurrentSize; // Read-only. Current window size. ImVec2 DesiredSize; // Read-write. Desired size, based on user's mouse position. Write to this field to restrain resizing. }; +// [ALPHA] Rarely used / very advanced uses only. Use with SetNextWindowClass() and DockSpace() functions. +// Important: the content of this class is still highly WIP and likely to change and be refactored +// before we stabilize Docking features. Please be mindful if using this. +// Provide hints: +// - To the platform backend via altered viewport flags (enable/disable OS decoration, OS task bar icons, etc.) +// - To the platform backend for OS level parent/child relationships of viewport. +// - To the docking system for various options and filtering. +struct ImGuiWindowClass +{ + ImGuiID ClassId; // User data. 0 = Default class (unclassed). Windows of different classes cannot be docked with each others. + ImGuiID ParentViewportId; // Hint for the platform backend. -1: use default. 0: request platform backend to not parent the platform. != 0: request platform backend to create a parent<>child relationship between the platform windows. Not conforming backends are free to e.g. parent every viewport to the main viewport or not. + ImGuiID FocusRouteParentWindowId; // ID of parent window for shortcut focus route evaluation, e.g. Shortcut() call from Parent Window will succeed when this window is focused. + ImGuiViewportFlags ViewportFlagsOverrideSet; // Viewport flags to set when a window of this class owns a viewport. This allows you to enforce OS decoration or task bar icon, override the defaults on a per-window basis. + ImGuiViewportFlags ViewportFlagsOverrideClear; // Viewport flags to clear when a window of this class owns a viewport. This allows you to enforce OS decoration or task bar icon, override the defaults on a per-window basis. + ImGuiTabItemFlags TabItemFlagsOverrideSet; // [EXPERIMENTAL] TabItem flags to set when a window of this class gets submitted into a dock node tab bar. May use with ImGuiTabItemFlags_Leading or ImGuiTabItemFlags_Trailing. + ImGuiDockNodeFlags DockNodeFlagsOverrideSet; // [EXPERIMENTAL] Dock node flags to set when a window of this class is hosted by a dock node (it doesn't have to be selected!) + bool DockingAlwaysTabBar; // Set to true to enforce single floating windows of this class always having their own docking node (equivalent of setting the global io.ConfigDockingAlwaysTabBar) + bool DockingAllowUnclassed; // Set to true to allow windows of this class to be docked/merged with an unclassed window. // FIXME-DOCK: Move to DockNodeFlags override? + + ImGuiWindowClass() { memset(this, 0, sizeof(*this)); ParentViewportId = (ImGuiID)-1; DockingAllowUnclassed = true; } +}; + // Data payload for Drag and Drop operations: AcceptDragDropPayload(), GetDragDropPayload() struct ImGuiPayload { @@ -1530,7 +2649,7 @@ struct ImGuiPayload ImGuiID SourceId; // Source item id ImGuiID SourceParentId; // Source parent id (if available) int DataFrameCount; // Data timestamp - char DataType[32+1]; // Data type tag (short user-supplied string, 32 characters max) + char DataType[32 + 1]; // Data type tag (short user-supplied string, 32 characters max) bool Preview; // Set when AcceptDragDropPayload() was called and mouse has been hovering the target item (nb: handle overlapping drag targets) bool Delivery; // Set when AcceptDragDropPayload() was called and mouse button is released over the target item. @@ -1542,55 +2661,18 @@ struct ImGuiPayload }; //----------------------------------------------------------------------------- -// Obsolete functions (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) -// Please keep your copy of dear imgui up to date! Occasionally set '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in imconfig.h to stay ahead. +// [SECTION] Helpers (ImGuiOnceUponAFrame, ImGuiTextFilter, ImGuiTextBuffer, ImGuiStorage, ImGuiListClipper, Math Operators, ImColor) //----------------------------------------------------------------------------- -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS -namespace ImGui -{ - // OBSOLETED in 1.72 (from July 2019) - static inline void TreeAdvanceToLabelPos() { SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()); } - // OBSOLETED in 1.71 (from June 2019) - static inline void SetNextTreeNodeOpen(bool open, ImGuiCond cond = 0) { SetNextItemOpen(open, cond); } - // OBSOLETED in 1.70 (from May 2019) - static inline float GetContentRegionAvailWidth() { return GetContentRegionAvail().x; } - // OBSOLETED in 1.69 (from Mar 2019) - static inline ImDrawList* GetOverlayDrawList() { return GetForegroundDrawList(); } - // OBSOLETED in 1.66 (from Sep 2018) - static inline void SetScrollHere(float center_ratio=0.5f){ SetScrollHereY(center_ratio); } - // OBSOLETED in 1.63 (between Aug 2018 and Sept 2018) - static inline bool IsItemDeactivatedAfterChange() { return IsItemDeactivatedAfterEdit(); } - // OBSOLETED in 1.61 (between Apr 2018 and Aug 2018) - IMGUI_API bool InputFloat(const char* label, float* v, float step, float step_fast, int decimal_precision, ImGuiInputTextFlags flags = 0); // Use the 'const char* format' version instead of 'decimal_precision'! - IMGUI_API bool InputFloat2(const char* label, float v[2], int decimal_precision, ImGuiInputTextFlags flags = 0); - IMGUI_API bool InputFloat3(const char* label, float v[3], int decimal_precision, ImGuiInputTextFlags flags = 0); - IMGUI_API bool InputFloat4(const char* label, float v[4], int decimal_precision, ImGuiInputTextFlags flags = 0); - // OBSOLETED in 1.60 (between Dec 2017 and Apr 2018) - static inline bool IsAnyWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); } - static inline bool IsAnyWindowHovered() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } - static inline ImVec2 CalcItemRectClosestPoint(const ImVec2& pos, bool on_edge = false, float outward = 0.f) { IM_UNUSED(on_edge); IM_UNUSED(outward); IM_ASSERT(0); return pos; } - // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) - static inline void ShowTestWindow() { return ShowDemoWindow(); } - static inline bool IsRootWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootWindow); } - static inline bool IsRootWindowOrAnyChildFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); } - static inline void SetNextWindowContentWidth(float w) { SetNextWindowContentSize(ImVec2(w, 0.0f)); } - static inline float GetItemsLineHeightWithSpacing() { return GetFrameHeightWithSpacing(); } - // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) - IMGUI_API bool Begin(const char* name, bool* p_open, const ImVec2& size_on_first_use, float bg_alpha_override = -1.0f, ImGuiWindowFlags flags = 0); // Use SetNextWindowSize(size, ImGuiCond_FirstUseEver) + SetNextWindowBgAlpha() instead. - static inline bool IsRootWindowOrAnyChildHovered() { return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); } - static inline void AlignFirstTextHeightToWidgets() { AlignTextToFramePadding(); } - static inline void SetNextWindowPosCenter(ImGuiCond c=0) { ImGuiIO& io = GetIO(); SetNextWindowPos(ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f), c, ImVec2(0.5f, 0.5f)); } -} -typedef ImGuiInputTextCallback ImGuiTextEditCallback; // OBSOLETED in 1.63 (from Aug 2018): made the names consistent -typedef ImGuiInputTextCallbackData ImGuiTextEditCallbackData; +// Helper: Unicode defines +#define IM_UNICODE_CODEPOINT_INVALID 0xFFFD // Invalid Unicode code point (standard value). +#ifdef IMGUI_USE_WCHAR32 +#define IM_UNICODE_CODEPOINT_MAX 0x10FFFF // Maximum Unicode code point supported by this build. +#else +#define IM_UNICODE_CODEPOINT_MAX 0xFFFF // Maximum Unicode code point supported by this build. #endif -//----------------------------------------------------------------------------- -// Helpers -//----------------------------------------------------------------------------- - -// Helper: Execute a block of code at maximum once a frame. Convenient if you want to quickly create an UI within deep-nested code that runs multiple times every frame. +// Helper: Execute a block of code at maximum once a frame. Convenient if you want to quickly create a UI within deep-nested code that runs multiple times every frame. // Usage: static ImGuiOnceUponAFrame oaf; if (oaf) ImGui::Text("This will be called only once per frame"); struct ImGuiOnceUponAFrame { @@ -1633,12 +2715,13 @@ struct ImGuiTextBuffer IMGUI_API static char EmptyString[1]; ImGuiTextBuffer() { } - inline char operator[](int i) { IM_ASSERT(Buf.Data != NULL); return Buf.Data[i]; } + inline char operator[](int i) const { IM_ASSERT(Buf.Data != NULL); return Buf.Data[i]; } const char* begin() const { return Buf.Data ? &Buf.front() : EmptyString; } - const char* end() const { return Buf.Data ? &Buf.back() : EmptyString; } // Buf is zero-terminated, so end() will point on the zero-terminator + const char* end() const { return Buf.Data ? &Buf.back() : EmptyString; } // Buf is zero-terminated, so end() will point on the zero-terminator int size() const { return Buf.Size ? Buf.Size - 1 : 0; } - bool empty() { return Buf.Size <= 1; } + bool empty() const { return Buf.Size <= 1; } void clear() { Buf.clear(); } + void resize(int size) { if (Buf.Size > size) Buf.Data[size] = 0; Buf.resize(size ? size + 1 : 0, 0); } // Similar to resize(0) on ImVector: empty string but don't free buffer. void reserve(int capacity) { Buf.reserve(capacity); } const char* c_str() const { return Buf.Data ? Buf.Data : EmptyString; } IMGUI_API void append(const char* str, const char* str_end = NULL); @@ -1646,6 +2729,16 @@ struct ImGuiTextBuffer IMGUI_API void appendfv(const char* fmt, va_list args) IM_FMTLIST(2); }; +// [Internal] Key+Value for ImGuiStorage +struct ImGuiStoragePair +{ + ImGuiID key; + union { int val_i; float val_f; void* val_p; }; + ImGuiStoragePair(ImGuiID _key, int _val) { key = _key; val_i = _val; } + ImGuiStoragePair(ImGuiID _key, float _val) { key = _key; val_f = _val; } + ImGuiStoragePair(ImGuiID _key, void* _val) { key = _key; val_p = _val; } +}; + // Helper: Key->Value storage // Typically you don't have to worry about this since a storage is held within each Window. // We use it to e.g. store collapse state for a tree (Int 0/1) @@ -1657,15 +2750,6 @@ struct ImGuiTextBuffer struct ImGuiStorage { // [Internal] - struct ImGuiStoragePair - { - ImGuiID key; - union { int val_i; float val_f; void* val_p; }; - ImGuiStoragePair(ImGuiID _key, int _val_i) { key = _key; val_i = _val_i; } - ImGuiStoragePair(ImGuiID _key, float _val_f) { key = _key; val_f = _val_f; } - ImGuiStoragePair(ImGuiID _key, void* _val_p) { key = _key; val_p = _val_p; } - }; - ImVector Data; // - Get***() functions find pair, never add/allocate. Pairs are sorted so a query is O(log N) @@ -1690,44 +2774,107 @@ struct ImGuiStorage IMGUI_API float* GetFloatRef(ImGuiID key, float default_val = 0.0f); IMGUI_API void** GetVoidPtrRef(ImGuiID key, void* default_val = NULL); - // Use on your own storage if you know only integer are being stored (open/close all tree nodes) + // Advanced: for quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. + IMGUI_API void BuildSortByKey(); + // Obsolete: use on your own storage if you know only integer are being stored (open/close all tree nodes) IMGUI_API void SetAllInt(int val); - // For quicker full rebuild of a storage (instead of an incremental one), you may add all your contents and then sort once. - IMGUI_API void BuildSortByKey(); +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //typedef ::ImGuiStoragePair ImGuiStoragePair; // 1.90.8: moved type outside struct +#endif }; // Helper: Manually clip large list of items. -// If you are submitting lots of evenly spaced items and you have a random access to the list, you can perform coarse clipping based on visibility to save yourself from processing those items at all. +// If you have lots evenly spaced items and you have random access to the list, you can perform coarse +// clipping based on visibility to only submit items that are in view. // The clipper calculates the range of visible items and advance the cursor to compensate for the non-visible items we have skipped. -// ImGui already clip items based on their bounds but it needs to measure text size to do so. Coarse clipping before submission makes this cost and your own data fetching/submission cost null. +// (Dear ImGui already clip items based on their bounds but: it needs to first layout the item to do so, and generally +// fetching/submitting your own data incurs additional cost. Coarse clipping using ImGuiListClipper allows you to easily +// scale using lists with tens of thousands of items without a problem) // Usage: -// ImGuiListClipper clipper(1000); // we have 1000 elements, evenly spaced. -// while (clipper.Step()) -// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) -// ImGui::Text("line number %d", i); -// - Step 0: the clipper let you process the first element, regardless of it being visible or not, so we can measure the element height (step skipped if we passed a known height as second arg to constructor). -// - Step 1: the clipper infer height from first element, calculate the actual range of elements to display, and position the cursor before the first element. -// - (Step 2: dummy step only required if an explicit items_height was passed to constructor or Begin() and user call Step(). Does nothing and switch to Step 3.) -// - Step 3: the clipper validate that we have reached the expected Y position (corresponding to element DisplayEnd), advance the cursor to the end of the list and then returns 'false' to end the loop. +// ImGuiListClipper clipper; +// clipper.Begin(1000); // We have 1000 elements, evenly spaced. +// while (clipper.Step()) +// for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) +// ImGui::Text("line number %d", i); +// Generally what happens is: +// - Clipper lets you process the first element (DisplayStart = 0, DisplayEnd = 1) regardless of it being visible or not. +// - User code submit that one element. +// - Clipper can measure the height of the first element +// - Clipper calculate the actual range of elements to display based on the current clipping rectangle, position the cursor before the first visible element. +// - User code submit visible elements. +// - The clipper also handles various subtleties related to keyboard/gamepad navigation, wrapping etc. struct ImGuiListClipper { - float StartPosY; - float ItemsHeight; - int ItemsCount, StepNo, DisplayStart, DisplayEnd; + ImGuiContext* Ctx; // Parent UI context + int DisplayStart; // First item to display, updated by each call to Step() + int DisplayEnd; // End of items to display (exclusive) + int ItemsCount; // [Internal] Number of items + float ItemsHeight; // [Internal] Height of item after a first step and item submission can calculate it + float StartPosY; // [Internal] Cursor position at the time of Begin() or after table frozen rows are all processed + double StartSeekOffsetY; // [Internal] Account for frozen rows in a table and initial loss of precision in very large windows. + void* TempData; // [Internal] Internal data - // items_count: Use -1 to ignore (you can call Begin later). Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step). + // items_count: Use INT_MAX if you don't know how many items you have (in which case the cursor won't be advanced in the final step, and you can call SeekCursorForItem() manually if you need) // items_height: Use -1.0f to be calculated automatically on first step. Otherwise pass in the distance between your items, typically GetTextLineHeightWithSpacing() or GetFrameHeightWithSpacing(). - // If you don't specify an items_height, you NEED to call Step(). If you specify items_height you may call the old Begin()/End() api directly, but prefer calling Step(). - ImGuiListClipper(int items_count = -1, float items_height = -1.0f) { Begin(items_count, items_height); } // NB: Begin() initialize every fields (as we allow user to call Begin/End multiple times on a same instance if they want). - ~ImGuiListClipper() { IM_ASSERT(ItemsCount == -1); } // Assert if user forgot to call End() or Step() until false. + IMGUI_API ImGuiListClipper(); + IMGUI_API ~ImGuiListClipper(); + IMGUI_API void Begin(int items_count, float items_height = -1.0f); + IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. + IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. - IMGUI_API bool Step(); // Call until it returns false. The DisplayStart/DisplayEnd fields will be set and you can process/draw those items. - IMGUI_API void Begin(int items_count, float items_height = -1.0f); // Automatically called by constructor if you passed 'items_count' or by Step() in Step 1. - IMGUI_API void End(); // Automatically called on the last call of Step() that returns false. + // Call IncludeItemByIndex() or IncludeItemsByIndex() *BEFORE* first call to Step() if you need a range of items to not be clipped, regardless of their visibility. + // (Due to alignment / padding of certain items it is possible that an extra item may be included on either end of the display range). + inline void IncludeItemByIndex(int item_index) { IncludeItemsByIndex(item_index, item_index + 1); } + IMGUI_API void IncludeItemsByIndex(int item_begin, int item_end); // item_end is exclusive e.g. use (42, 42+1) to make item 42 never clipped. + + // Seek cursor toward given item. This is automatically called while stepping. + // - The only reason to call this is: you can use ImGuiListClipper::Begin(INT_MAX) if you don't know item count ahead of time. + // - In this case, after all steps are done, you'll want to call SeekCursorForItem(item_count). + IMGUI_API void SeekCursorForItem(int item_index); + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void IncludeRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.9] + inline void ForceDisplayRangeByIndices(int item_begin, int item_end) { IncludeItemsByIndex(item_begin, item_end); } // [renamed in 1.89.6] + //inline ImGuiListClipper(int items_count, float items_height = -1.0f) { memset(this, 0, sizeof(*this)); ItemsCount = -1; Begin(items_count, items_height); } // [removed in 1.79] +#endif }; -// Helpers macros to generate 32-bits encoded colors +// Helpers: ImVec2/ImVec4 operators +// - It is important that we are keeping those disabled by default so they don't leak in user space. +// - This is in order to allow user enabling implicit cast operators between ImVec2/ImVec4 and their own types (using IM_VEC2_CLASS_EXTRA in imconfig.h) +// - Add '#define IMGUI_DEFINE_MATH_OPERATORS' before including this file (or in imconfig.h) to access courtesy maths operators for ImVec2 and ImVec4. +// - We intentionally provide ImVec2*float but not float*ImVec2: this is rare enough and we want to reduce the surface for possible user mistake. +#ifdef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED +IM_MSVC_RUNTIME_CHECKS_OFF +static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x * rhs, lhs.y * rhs); } +static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x / rhs, lhs.y / rhs); } +static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x - rhs.x, lhs.y - rhs.y); } +static inline ImVec2 operator*(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline ImVec2 operator/(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x / rhs.x, lhs.y / rhs.y); } +static inline ImVec2 operator-(const ImVec2& lhs) { return ImVec2(-lhs.x, -lhs.y); } +static inline ImVec2& operator*=(ImVec2& lhs, const float rhs) { lhs.x *= rhs; lhs.y *= rhs; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const float rhs) { lhs.x /= rhs; lhs.y /= rhs; return lhs; } +static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs) { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; } +static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs) { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; } +static inline ImVec2& operator*=(ImVec2& lhs, const ImVec2& rhs) { lhs.x *= rhs.x; lhs.y *= rhs.y; return lhs; } +static inline ImVec2& operator/=(ImVec2& lhs, const ImVec2& rhs) { lhs.x /= rhs.x; lhs.y /= rhs.y; return lhs; } +static inline bool operator==(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y; } +static inline bool operator!=(const ImVec2& lhs, const ImVec2& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y; } +static inline ImVec4 operator+(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x + rhs.x, lhs.y + rhs.y, lhs.z + rhs.z, lhs.w + rhs.w); } +static inline ImVec4 operator-(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z, lhs.w - rhs.w); } +static inline ImVec4 operator*(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z, lhs.w * rhs.w); } +static inline bool operator==(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z && lhs.w == rhs.w; } +static inline bool operator!=(const ImVec4& lhs, const ImVec4& rhs) { return lhs.x != rhs.x || lhs.y != rhs.y || lhs.z != rhs.z || lhs.w != rhs.w; } +IM_MSVC_RUNTIME_CHECKS_RESTORE +#endif + +// Helpers macros to generate 32-bit encoded colors +// - User can declare their own format by #defining the 5 _SHIFT/_MASK macros in their imconfig file. +// - Any setting other than the default will need custom backend support. The only standard backend that supports anything else than the default is DirectX9. +#ifndef IM_COL32_R_SHIFT #ifdef IMGUI_USE_BGRA_PACKED_COLOR #define IM_COL32_R_SHIFT 16 #define IM_COL32_G_SHIFT 8 @@ -1741,10 +2888,11 @@ struct ImGuiListClipper #define IM_COL32_A_SHIFT 24 #define IM_COL32_A_MASK 0xFF000000 #endif +#endif #define IM_COL32(R,G,B,A) (((ImU32)(A)<>IM_COL32_R_SHIFT)&0xFF) * sc; Value.y = (float)((rgba>>IM_COL32_G_SHIFT)&0xFF) * sc; Value.z = (float)((rgba>>IM_COL32_B_SHIFT)&0xFF) * sc; Value.w = (float)((rgba>>IM_COL32_A_SHIFT)&0xFF) * sc; } - ImColor(float r, float g, float b, float a = 1.0f) { Value.x = r; Value.y = g; Value.z = b; Value.w = a; } - ImColor(const ImVec4& col) { Value = col; } + constexpr ImColor() { } + constexpr ImColor(float r, float g, float b, float a = 1.0f) : Value(r, g, b, a) { } + constexpr ImColor(const ImVec4& col) : Value(col) {} + constexpr ImColor(int r, int g, int b, int a = 255) : Value((float)r * (1.0f / 255.0f), (float)g * (1.0f / 255.0f), (float)b * (1.0f / 255.0f), (float)a* (1.0f / 255.0f)) {} + constexpr ImColor(ImU32 rgba) : Value((float)((rgba >> IM_COL32_R_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_G_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_B_SHIFT) & 0xFF) * (1.0f / 255.0f), (float)((rgba >> IM_COL32_A_SHIFT) & 0xFF) * (1.0f / 255.0f)) {} inline operator ImU32() const { return ImGui::ColorConvertFloat4ToU32(Value); } inline operator ImVec4() const { return Value; } // FIXME-OBSOLETE: May need to obsolete/cleanup those helpers. inline void SetHSV(float h, float s, float v, float a = 1.0f){ ImGui::ColorConvertHSVtoRGB(h, s, v, Value.x, Value.y, Value.z); Value.w = a; } - static ImColor HSV(float h, float s, float v, float a = 1.0f) { float r,g,b; ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); return ImColor(r,g,b,a); } + static ImColor HSV(float h, float s, float v, float a = 1.0f) { float r, g, b; ImGui::ColorConvertHSVtoRGB(h, s, v, r, g, b); return ImColor(r, g, b, a); } }; //----------------------------------------------------------------------------- -// Draw List API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) +// [SECTION] Multi-Select API flags and structures (ImGuiMultiSelectFlags, ImGuiSelectionRequestType, ImGuiSelectionRequest, ImGuiMultiSelectIO, ImGuiSelectionBasicStorage) +//----------------------------------------------------------------------------- + +// Multi-selection system +// Documentation at: https://github.com/ocornut/imgui/wiki/Multi-Select +// - Refer to 'Demo->Widgets->Selection State & Multi-Select' for demos using this. +// - This system implements standard multi-selection idioms (CTRL+Mouse/Keyboard, SHIFT+Mouse/Keyboard, etc) +// with support for clipper (skipping non-visible items), box-select and many other details. +// - Selectable(), Checkbox() are supported but custom widgets may use it as well. +// - TreeNode() is technically supported but... using this correctly is more complicated: you need some sort of linear/random access to your tree, +// which is suited to advanced trees setups also implementing filters and clipper. We will work toward simplifying and demoing it. +// - In the spirit of Dear ImGui design, your code owns actual selection data. +// This is designed to allow all kinds of selection storage you may use in your application e.g. set/map/hash. +// About ImGuiSelectionBasicStorage: +// - This is an optional helper to store a selection state and apply selection requests. +// - It is used by our demos and provided as a convenience to quickly implement multi-selection. +// Usage: +// - Identify submitted items with SetNextItemSelectionUserData(), most likely using an index into your current data-set. +// - Store and maintain actual selection data using persistent object identifiers. +// - Usage flow: +// BEGIN - (1) Call BeginMultiSelect() and retrieve the ImGuiMultiSelectIO* result. +// - (2) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 6. +// - (3) [If using clipper] You need to make sure RangeSrcItem is always submitted. Calculate its index and pass to clipper.IncludeItemByIndex(). If storing indices in ImGuiSelectionUserData, a simple clipper.IncludeItemByIndex(ms_io->RangeSrcItem) call will work. +// LOOP - (4) Submit your items with SetNextItemSelectionUserData() + Selectable()/TreeNode() calls. +// END - (5) Call EndMultiSelect() and retrieve the ImGuiMultiSelectIO* result. +// - (6) Honor request list (SetAll/SetRange requests) by updating your selection data. Same code as Step 2. +// If you submit all items (no clipper), Step 2 and 3 are optional and will be handled by each item themselves. It is fine to always honor those steps. +// About ImGuiSelectionUserData: +// - This can store an application-defined identifier (e.g. index or pointer) submitted via SetNextItemSelectionUserData(). +// - In return we store them into RangeSrcItem/RangeFirstItem/RangeLastItem and other fields in ImGuiMultiSelectIO. +// - Most applications will store an object INDEX, hence the chosen name and type. Storing an index is natural, because +// SetRange requests will give you two end-points and you will need to iterate/interpolate between them to update your selection. +// - However it is perfectly possible to store a POINTER or another IDENTIFIER inside ImGuiSelectionUserData. +// Our system never assume that you identify items by indices, it never attempts to interpolate between two values. +// - If you enable ImGuiMultiSelectFlags_NoRangeSelect then it is guaranteed that you will never have to interpolate +// between two ImGuiSelectionUserData, which may be a convenient way to use part of the feature with less code work. +// - As most users will want to store an index, for convenience and to reduce confusion we use ImS64 instead of void*, +// being syntactically easier to downcast. Feel free to reinterpret_cast and store a pointer inside. + +// Flags for BeginMultiSelect() +enum ImGuiMultiSelectFlags_ +{ + ImGuiMultiSelectFlags_None = 0, + ImGuiMultiSelectFlags_SingleSelect = 1 << 0, // Disable selecting more than one item. This is available to allow single-selection code to share same code/logic if desired. It essentially disables the main purpose of BeginMultiSelect() tho! + ImGuiMultiSelectFlags_NoSelectAll = 1 << 1, // Disable CTRL+A shortcut to select all. + ImGuiMultiSelectFlags_NoRangeSelect = 1 << 2, // Disable Shift+selection mouse/keyboard support (useful for unordered 2D selection). With BoxSelect is also ensure contiguous SetRange requests are not combined into one. This allows not handling interpolation in SetRange requests. + ImGuiMultiSelectFlags_NoAutoSelect = 1 << 3, // Disable selecting items when navigating (useful for e.g. supporting range-select in a list of checkboxes). + ImGuiMultiSelectFlags_NoAutoClear = 1 << 4, // Disable clearing selection when navigating or selecting another one (generally used with ImGuiMultiSelectFlags_NoAutoSelect. useful for e.g. supporting range-select in a list of checkboxes). + ImGuiMultiSelectFlags_NoAutoClearOnReselect = 1 << 5, // Disable clearing selection when clicking/selecting an already selected item. + ImGuiMultiSelectFlags_BoxSelect1d = 1 << 6, // Enable box-selection with same width and same x pos items (e.g. full row Selectable()). Box-selection works better with little bit of spacing between items hit-box in order to be able to aim at empty space. + ImGuiMultiSelectFlags_BoxSelect2d = 1 << 7, // Enable box-selection with varying width or varying x pos items support (e.g. different width labels, or 2D layout/grid). This is slower: alters clipping logic so that e.g. horizontal movements will update selection of normally clipped items. + ImGuiMultiSelectFlags_BoxSelectNoScroll = 1 << 8, // Disable scrolling when box-selecting near edges of scope. + ImGuiMultiSelectFlags_ClearOnEscape = 1 << 9, // Clear selection when pressing Escape while scope is focused. + ImGuiMultiSelectFlags_ClearOnClickVoid = 1 << 10, // Clear selection when clicking on empty location within scope. + ImGuiMultiSelectFlags_ScopeWindow = 1 << 11, // Scope for _BoxSelect and _ClearOnClickVoid is whole window (Default). Use if BeginMultiSelect() covers a whole window or used a single time in same window. + ImGuiMultiSelectFlags_ScopeRect = 1 << 12, // Scope for _BoxSelect and _ClearOnClickVoid is rectangle encompassing BeginMultiSelect()/EndMultiSelect(). Use if BeginMultiSelect() is called multiple times in same window. + ImGuiMultiSelectFlags_SelectOnClick = 1 << 13, // Apply selection on mouse down when clicking on unselected item. (Default) + ImGuiMultiSelectFlags_SelectOnClickRelease = 1 << 14, // Apply selection on mouse release when clicking an unselected item. Allow dragging an unselected item without altering selection. + //ImGuiMultiSelectFlags_RangeSelect2d = 1 << 15, // Shift+Selection uses 2d geometry instead of linear sequence, so possible to use Shift+up/down to select vertically in grid. Analogous to what BoxSelect does. + ImGuiMultiSelectFlags_NavWrapX = 1 << 16, // [Temporary] Enable navigation wrapping on X axis. Provided as a convenience because we don't have a design for the general Nav API for this yet. When the more general feature be public we may obsolete this flag in favor of new one. +}; + +// Main IO structure returned by BeginMultiSelect()/EndMultiSelect(). +// This mainly contains a list of selection requests. +// - Use 'Demo->Tools->Debug Log->Selection' to see requests as they happen. +// - Some fields are only useful if your list is dynamic and allows deletion (getting post-deletion focus/state right is shown in the demo) +// - Below: who reads/writes each fields? 'r'=read, 'w'=write, 'ms'=multi-select code, 'app'=application/user code. +struct ImGuiMultiSelectIO +{ + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImVector Requests; // ms:w, app:r / ms:w app:r // Requests to apply to your selection data. + ImGuiSelectionUserData RangeSrcItem; // ms:w app:r / // (If using clipper) Begin: Source item (often the first selected item) must never be clipped: use clipper.IncludeItemByIndex() to ensure it is submitted. + ImGuiSelectionUserData NavIdItem; // ms:w, app:r / // (If using deletion) Last known SetNextItemSelectionUserData() value for NavId (if part of submitted items). + bool NavIdSelected; // ms:w, app:r / app:r // (If using deletion) Last known selection state for NavId (if part of submitted items). + bool RangeSrcReset; // app:w / ms:r // (If using deletion) Set before EndMultiSelect() to reset ResetSrcItem (e.g. if deleted selection). + int ItemsCount; // ms:w, app:r / app:r // 'int items_count' parameter to BeginMultiSelect() is copied here for convenience, allowing simpler calls to your ApplyRequests handler. Not used internally. +}; + +// Selection request type +enum ImGuiSelectionRequestType +{ + ImGuiSelectionRequestType_None = 0, + ImGuiSelectionRequestType_SetAll, // Request app to clear selection (if Selected==false) or select all items (if Selected==true). We cannot set RangeFirstItem/RangeLastItem as its contents is entirely up to user (not necessarily an index) + ImGuiSelectionRequestType_SetRange, // Request app to select/unselect [RangeFirstItem..RangeLastItem] items (inclusive) based on value of Selected. Only EndMultiSelect() request this, app code can read after BeginMultiSelect() and it will always be false. +}; + +// Selection request item +struct ImGuiSelectionRequest +{ + //------------------------------------------// BeginMultiSelect / EndMultiSelect + ImGuiSelectionRequestType Type; // ms:w, app:r / ms:w, app:r // Request type. You'll most often receive 1 Clear + 1 SetRange with a single-item range. + bool Selected; // ms:w, app:r / ms:w, app:r // Parameter for SetAll/SetRange requests (true = select, false = unselect) + ImS8 RangeDirection; // / ms:w app:r // Parameter for SetRange request: +1 when RangeFirstItem comes before RangeLastItem, -1 otherwise. Useful if you want to preserve selection order on a backward Shift+Click. + ImGuiSelectionUserData RangeFirstItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from top to bottom). + ImGuiSelectionUserData RangeLastItem; // / ms:w, app:r // Parameter for SetRange request (this is generally == RangeSrcItem when shift selecting from bottom to top). Inclusive! +}; + +// Optional helper to store multi-selection state + apply multi-selection requests. +// - Used by our demos and provided as a convenience to easily implement basic multi-selection. +// - Iterate selection with 'void* it = NULL; ImGuiID id; while (selection.GetNextSelectedItem(&it, &id)) { ... }' +// Or you can check 'if (Contains(id)) { ... }' for each possible object if their number is not too high to iterate. +// - USING THIS IS NOT MANDATORY. This is only a helper and not a required API. +// To store a multi-selection, in your application you could: +// - Use this helper as a convenience. We use our simple key->value ImGuiStorage as a std::set replacement. +// - Use your own external storage: e.g. std::set, std::vector, interval trees, intrusively stored selection etc. +// In ImGuiSelectionBasicStorage we: +// - always use indices in the multi-selection API (passed to SetNextItemSelectionUserData(), retrieved in ImGuiMultiSelectIO) +// - use the AdapterIndexToStorageId() indirection layer to abstract how persistent selection data is derived from an index. +// - use decently optimized logic to allow queries and insertion of very large selection sets. +// - do not preserve selection order. +// Many combinations are possible depending on how you prefer to store your items and how you prefer to store your selection. +// Large applications are likely to eventually want to get rid of this indirection layer and do their own thing. +// See https://github.com/ocornut/imgui/wiki/Multi-Select for details and pseudo-code using this helper. +struct ImGuiSelectionBasicStorage +{ + // Members + int Size; // // Number of selected items, maintained by this helper. + bool PreserveOrder; // = false // GetNextSelectedItem() will return ordered selection (currently implemented by two additional sorts of selection. Could be improved) + void* UserData; // = NULL // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + ImGuiID (*AdapterIndexToStorageId)(ImGuiSelectionBasicStorage* self, int idx); // e.g. selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { return ((MyItems**)self->UserData)[idx]->ID; }; + int _SelectionOrder;// [Internal] Increasing counter to store selection order + ImGuiStorage _Storage; // [Internal] Selection set. Think of this as similar to e.g. std::set. Prefer not accessing directly: iterate with GetNextSelectedItem(). + + // Methods + IMGUI_API ImGuiSelectionBasicStorage(); + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Apply selection requests coming from BeginMultiSelect() and EndMultiSelect() functions. It uses 'items_count' passed to BeginMultiSelect() + IMGUI_API bool Contains(ImGuiID id) const; // Query if an item id is in selection. + IMGUI_API void Clear(); // Clear selection + IMGUI_API void Swap(ImGuiSelectionBasicStorage& r); // Swap two selections + IMGUI_API void SetItemSelected(ImGuiID id, bool selected); // Add/remove an item from selection (generally done by ApplyRequests() function) + IMGUI_API bool GetNextSelectedItem(void** opaque_it, ImGuiID* out_id); // Iterate selection with 'void* it = NULL; ImGuiID id; while (selection.GetNextSelectedItem(&it, &id)) { ... }' + inline ImGuiID GetStorageIdFromIndex(int idx) { return AdapterIndexToStorageId(this, idx); } // Convert index to item id based on provided adapter. +}; + +// Optional helper to apply multi-selection requests to existing randomly accessible storage. +// Convenient if you want to quickly wire multi-select API on e.g. an array of bool or items storing their own selection state. +struct ImGuiSelectionExternalStorage +{ + // Members + void* UserData; // User data for use by adapter function // e.g. selection.UserData = (void*)my_items; + void (*AdapterSetItemSelected)(ImGuiSelectionExternalStorage* self, int idx, bool selected); // e.g. AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int idx, bool selected) { ((MyItems**)self->UserData)[idx]->Selected = selected; } + + // Methods + IMGUI_API ImGuiSelectionExternalStorage(); + IMGUI_API void ApplyRequests(ImGuiMultiSelectIO* ms_io); // Apply selection requests by using AdapterSetItemSelected() calls +}; + +//----------------------------------------------------------------------------- +// [SECTION] Drawing API (ImDrawCmd, ImDrawIdx, ImDrawVert, ImDrawChannel, ImDrawListSplitter, ImDrawListFlags, ImDrawList, ImDrawData) // Hold a series of drawing commands. The user provides a renderer for ImDrawData which essentially contains an array of ImDrawList. //----------------------------------------------------------------------------- -// Draw callbacks for advanced uses. +// The maximum line width to bake anti-aliased textures for. Build atlas with ImFontAtlasFlags_NoBakedLines to disable baking. +#ifndef IM_DRAWLIST_TEX_LINES_WIDTH_MAX +#define IM_DRAWLIST_TEX_LINES_WIDTH_MAX (32) +#endif + +// ImDrawIdx: vertex index. [Compile-time configurable type] +// - To use 16-bit indices + allow large meshes: backend need to set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' and handle ImDrawCmd::VtxOffset (recommended). +// - To use 32-bit indices: override with '#define ImDrawIdx unsigned int' in your imconfig.h file. +#ifndef ImDrawIdx +typedef unsigned short ImDrawIdx; // Default: 16-bit (for maximum compatibility with renderer backends) +#endif + +// ImDrawCallback: Draw callbacks for advanced uses [configurable type: override in imconfig.h] // NB: You most likely do NOT need to use draw callbacks just to create your own widget or customized UI rendering, // you can poke into the draw list for that! Draw callback may be useful for example to: // A) Change your GPU render state, // B) render a complex 3D scene inside a UI element without an intermediate texture/render target, etc. // The expected behavior from your rendering function is 'if (cmd.UserCallback != NULL) { cmd.UserCallback(parent_list, cmd); } else { RenderTriangles() }' -// If you want to override the signature of ImDrawCallback, you can simply use e.g. '#define ImDrawCallback MyDrawCallback' (in imconfig.h) + update rendering back-end accordingly. +// If you want to override the signature of ImDrawCallback, you can simply use e.g. '#define ImDrawCallback MyDrawCallback' (in imconfig.h) + update rendering backend accordingly. #ifndef ImDrawCallback typedef void (*ImDrawCallback)(const ImDrawList* parent_list, const ImDrawCmd* cmd); #endif -// Special Draw callback value to request renderer back-end to reset the graphics/render state. -// The renderer back-end needs to handle this special value, otherwise it will crash trying to call a function at this address. -// This is useful for example if you submitted callbacks which you know have altered the render state and you want it to be restored. -// It is not done by default because they are many perfectly useful way of altering render state for imgui contents (e.g. changing shader/blending settings before an Image call). -#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-1) +// Special Draw callback value to request renderer backend to reset the graphics/render state. +// The renderer backend needs to handle this special value, otherwise it will crash trying to call a function at this address. +// This is useful, for example, if you submitted callbacks which you know have altered the render state and you want it to be restored. +// Render state is not reset by default because they are many perfectly useful way of altering render state (e.g. changing shader/blending settings before an Image call). +#define ImDrawCallback_ResetRenderState (ImDrawCallback)(-8) // Typically, 1 command = 1 GPU draw call (unless command is a callback) -// Pre 1.71 back-ends will typically ignore the VtxOffset/IdxOffset fields. When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' -// is enabled, those fields allow us to render meshes larger than 64K vertices while keeping 16-bits indices. +// - VtxOffset: When 'io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset' is enabled, +// this fields allow us to render meshes larger than 64K vertices while keeping 16-bit indices. +// Backends made for <1.71. will typically ignore the VtxOffset fields. +// - The ClipRect/TextureId/VtxOffset fields must be contiguous as we memcmp() them together (this is asserted for). struct ImDrawCmd { - unsigned int ElemCount; // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[]. - ImVec4 ClipRect; // Clipping rectangle (x1, y1, x2, y2). Subtract ImDrawData->DisplayPos to get clipping rectangle in "viewport" coordinates - ImTextureID TextureId; // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas. - unsigned int VtxOffset; // Start offset in vertex buffer. Pre-1.71 or without ImGuiBackendFlags_RendererHasVtxOffset: always 0. With ImGuiBackendFlags_RendererHasVtxOffset: may be >0 to support meshes larger than 64K vertices with 16-bits indices. - unsigned int IdxOffset; // Start offset in index buffer. Always equal to sum of ElemCount drawn so far. - ImDrawCallback UserCallback; // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally. - void* UserCallbackData; // The draw callback code can access this. + ImVec4 ClipRect; // 4*4 // Clipping rectangle (x1, y1, x2, y2). Subtract ImDrawData->DisplayPos to get clipping rectangle in "viewport" coordinates + ImTextureID TextureId; // 4-8 // User-provided texture ID. Set by user in ImfontAtlas::SetTexID() for fonts or passed to Image*() functions. Ignore if never using images or multiple fonts atlas. + unsigned int VtxOffset; // 4 // Start offset in vertex buffer. ImGuiBackendFlags_RendererHasVtxOffset: always 0, otherwise may be >0 to support meshes larger than 64K vertices with 16-bit indices. + unsigned int IdxOffset; // 4 // Start offset in index buffer. + unsigned int ElemCount; // 4 // Number of indices (multiple of 3) to be rendered as triangles. Vertices are stored in the callee ImDrawList's vtx_buffer[] array, indices in idx_buffer[]. + ImDrawCallback UserCallback; // 4-8 // If != NULL, call the function instead of rendering the vertices. clip_rect and texture_id will be set normally. + void* UserCallbackData; // 4-8 // Callback user data (when UserCallback != NULL). If called AddCallback() with size == 0, this is a copy of the AddCallback() argument. If called AddCallback() with size > 0, this is pointing to a buffer where data is stored. + int UserCallbackDataSize; // 4 // Size of callback user data when using storage, otherwise 0. + int UserCallbackDataOffset;// 4 // [Internal] Offset of callback user data when using storage, otherwise -1. - ImDrawCmd() { ElemCount = 0; TextureId = (ImTextureID)NULL; VtxOffset = IdxOffset = 0; UserCallback = NULL; UserCallbackData = NULL; } + ImDrawCmd() { memset(this, 0, sizeof(*this)); } // Also ensure our padding fields are zeroed + + // Since 1.83: returns ImTextureID associated with this draw call. Warning: DO NOT assume this is always same as 'TextureId' (we will change this function for an upcoming feature) + inline ImTextureID GetTexID() const { return TextureId; } }; -// Vertex index -// (to allow large meshes with 16-bits indices: set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset' and handle ImDrawCmd::VtxOffset in the renderer back-end) -// (to use 32-bits indices: override with '#define ImDrawIdx unsigned int' in imconfig.h) -#ifndef ImDrawIdx -typedef unsigned short ImDrawIdx; -#endif - // Vertex layout #ifndef IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT struct ImDrawVert @@ -1823,12 +3131,20 @@ struct ImDrawVert #else // You can override the vertex format layout by defining IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT in imconfig.h // The code expect ImVec2 pos (8 bytes), ImVec2 uv (8 bytes), ImU32 col (4 bytes), but you can re-order them or add other fields as needed to simplify integration in your engine. -// The type has to be described within the macro (you can either declare the struct or use a typedef). This is because ImVec2/ImU32 are likely not declared a the time you'd want to set your type up. +// The type has to be described within the macro (you can either declare the struct or use a typedef). This is because ImVec2/ImU32 are likely not declared at the time you'd want to set your type up. // NOTE: IMGUI DOESN'T CLEAR THE STRUCTURE AND DOESN'T CALL A CONSTRUCTOR SO ANY CUSTOM FIELD WILL BE UNINITIALIZED. IF YOU ADD EXTRA FIELDS (SUCH AS A 'Z' COORDINATES) YOU WILL NEED TO CLEAR THEM DURING RENDER OR TO IGNORE THEM. IMGUI_OVERRIDE_DRAWVERT_STRUCT_LAYOUT; #endif -// For use by ImDrawListSplitter. +// [Internal] For use by ImDrawList +struct ImDrawCmdHeader +{ + ImVec4 ClipRect; + ImTextureID TextureId; + unsigned int VtxOffset; +}; + +// [Internal] For use by ImDrawListSplitter struct ImDrawChannel { ImVector _CmdBuffer; @@ -1836,14 +3152,14 @@ struct ImDrawChannel }; // Split/Merge functions are used to split the draw list into different layers which can be drawn into out of order. -// This is used by the Columns api, so items of each column can be batched together in a same draw call. +// This is used by the Columns/Tables API, so items of each column can be batched together in a same draw call. struct ImDrawListSplitter { int _Current; // Current channel number (0) int _Count; // Number of active channels (1+) ImVector _Channels; // Draw channels (not resized down so _Count might be < Channels.Size) - inline ImDrawListSplitter() { Clear(); } + inline ImDrawListSplitter() { memset(this, 0, sizeof(*this)); } inline ~ImDrawListSplitter() { ClearFreeMemory(); } inline void Clear() { _Current = 0; _Count = 1; } // Do not clear Channels[] so our allocations are reused next frame IMGUI_API void ClearFreeMemory(); @@ -1852,26 +3168,35 @@ struct ImDrawListSplitter IMGUI_API void SetCurrentChannel(ImDrawList* draw_list, int channel_idx); }; -enum ImDrawCornerFlags_ +// Flags for ImDrawList functions +// (Legacy: bit 0 must always correspond to ImDrawFlags_Closed to be backward compatible with old API using a bool. Bits 1..3 must be unused) +enum ImDrawFlags_ { - ImDrawCornerFlags_None = 0, - ImDrawCornerFlags_TopLeft = 1 << 0, // 0x1 - ImDrawCornerFlags_TopRight = 1 << 1, // 0x2 - ImDrawCornerFlags_BotLeft = 1 << 2, // 0x4 - ImDrawCornerFlags_BotRight = 1 << 3, // 0x8 - ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, // 0x3 - ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, // 0xC - ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, // 0x5 - ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, // 0xA - ImDrawCornerFlags_All = 0xF // In your function calls you may use ~0 (= all bits sets) instead of ImDrawCornerFlags_All, as a convenience + ImDrawFlags_None = 0, + ImDrawFlags_Closed = 1 << 0, // PathStroke(), AddPolyline(): specify that shape should be closed (Important: this is always == 1 for legacy reason) + ImDrawFlags_RoundCornersTopLeft = 1 << 4, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-left corner only (when rounding > 0.0f, we default to all corners). Was 0x01. + ImDrawFlags_RoundCornersTopRight = 1 << 5, // AddRect(), AddRectFilled(), PathRect(): enable rounding top-right corner only (when rounding > 0.0f, we default to all corners). Was 0x02. + ImDrawFlags_RoundCornersBottomLeft = 1 << 6, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-left corner only (when rounding > 0.0f, we default to all corners). Was 0x04. + ImDrawFlags_RoundCornersBottomRight = 1 << 7, // AddRect(), AddRectFilled(), PathRect(): enable rounding bottom-right corner only (when rounding > 0.0f, we default to all corners). Wax 0x08. + ImDrawFlags_RoundCornersNone = 1 << 8, // AddRect(), AddRectFilled(), PathRect(): disable rounding on all corners (when rounding > 0.0f). This is NOT zero, NOT an implicit flag! + ImDrawFlags_RoundCornersTop = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersBottom = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersLeft = ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersTopLeft, + ImDrawFlags_RoundCornersRight = ImDrawFlags_RoundCornersBottomRight | ImDrawFlags_RoundCornersTopRight, + ImDrawFlags_RoundCornersAll = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, + ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, // Default to ALL corners if none of the _RoundCornersXX flags are specified. + ImDrawFlags_RoundCornersMask_ = ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone, }; +// Flags for ImDrawList instance. Those are set automatically by ImGui:: functions from ImGuiIO settings, and generally not manipulated directly. +// It is however possible to temporarily alter flags between calls to ImDrawList:: functions. enum ImDrawListFlags_ { - ImDrawListFlags_None = 0, - ImDrawListFlags_AntiAliasedLines = 1 << 0, // Lines are anti-aliased (*2 the number of triangles for 1.0f wide line, otherwise *3 the number of triangles) - ImDrawListFlags_AntiAliasedFill = 1 << 1, // Filled shapes have anti-aliased edges (*2 the number of vertices) - ImDrawListFlags_AllowVtxOffset = 1 << 2 // Can emit 'VtxOffset > 0' to allow large meshes. Set when 'ImGuiBackendFlags_RendererHasVtxOffset' is enabled. + ImDrawListFlags_None = 0, + ImDrawListFlags_AntiAliasedLines = 1 << 0, // Enable anti-aliased lines/borders (*2 the number of triangles for 1.0f wide line or lines thin enough to be drawn using textures, otherwise *3 the number of triangles) + ImDrawListFlags_AntiAliasedLinesUseTex = 1 << 1, // Enable anti-aliased lines/borders using textures when possible. Require backend to render with bilinear filtering (NOT point/nearest filtering). + ImDrawListFlags_AntiAliasedFill = 1 << 2, // Enable anti-aliased edge around filled shapes (rounded rectangles, circles). + ImDrawListFlags_AllowVtxOffset = 1 << 3, // Can emit 'VtxOffset > 0' to allow large meshes. Set when 'ImGuiBackendFlags_RendererHasVtxOffset' is enabled. }; // Draw command list @@ -1880,7 +3205,8 @@ enum ImDrawListFlags_ // Each dear imgui window contains its own ImDrawList. You can use ImGui::GetWindowDrawList() to // access the current window draw list and draw custom primitives. // You can interleave normal ImGui:: calls and adding primitives to the current draw list. -// All positions are generally in pixel coordinates (top-left at (0,0), bottom-right at io.DisplaySize), but you are totally free to apply whatever transformation matrix to want to the data (if you apply such transformation you'll want to apply it to ClipRect as well) +// In single viewport mode, top-left is == GetMainViewport()->Pos (generally 0,0), bottom-right is == GetMainViewport()->Pos+Size (generally io.DisplaySize). +// You are totally free to apply whatever transformation matrix you want to the data (depending on the use of the transformation you may want to apply it to ClipRect as well!) // Important: Primitives are always added to the list and not culled (culling is done at higher-level by ImGui:: functions), if you use this API a lot consider coarse culling your drawn objects. struct ImDrawList { @@ -1891,21 +3217,25 @@ struct ImDrawList ImDrawListFlags Flags; // Flags, you may poke into these to adjust anti-aliasing settings per-primitive. // [Internal, used while building lists] - const ImDrawListSharedData* _Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) - const char* _OwnerName; // Pointer to owner window's name for debugging - unsigned int _VtxCurrentOffset; // [Internal] Always 0 unless 'Flags & ImDrawListFlags_AllowVtxOffset'. - unsigned int _VtxCurrentIdx; // [Internal] Generally == VtxBuffer.Size unless we are past 64K vertices, in which case this gets reset to 0. + unsigned int _VtxCurrentIdx; // [Internal] generally == VtxBuffer.Size unless we are past 64K vertices, in which case this gets reset to 0. + ImDrawListSharedData* _Data; // Pointer to shared draw data (you can use ImGui::GetDrawListSharedData() to get the one from current ImGui context) ImDrawVert* _VtxWritePtr; // [Internal] point within VtxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) ImDrawIdx* _IdxWritePtr; // [Internal] point within IdxBuffer.Data after each add command (to avoid using the ImVector<> operators too much) + ImVector _Path; // [Internal] current path building + ImDrawCmdHeader _CmdHeader; // [Internal] template of active commands. Fields should match those of CmdBuffer.back(). + ImDrawListSplitter _Splitter; // [Internal] for channels api (note: prefer using your own persistent instance of ImDrawListSplitter!) ImVector _ClipRectStack; // [Internal] ImVector _TextureIdStack; // [Internal] - ImVector _Path; // [Internal] current path building - ImDrawListSplitter _Splitter; // [Internal] for channels api + ImVector _CallbacksDataBuf; // [Internal] + float _FringeScale; // [Internal] anti-alias fringe is scaled by this value, this helps to keep things sharp while zooming at vertex buffer content + const char* _OwnerName; // Pointer to owner window's name for debugging - // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData() or create and use your own ImDrawListSharedData (so you can use ImDrawList without ImGui) - ImDrawList(const ImDrawListSharedData* shared_data) { _Data = shared_data; _OwnerName = NULL; Clear(); } - ~ImDrawList() { ClearFreeMemory(); } - IMGUI_API void PushClipRect(ImVec2 clip_rect_min, ImVec2 clip_rect_max, bool intersect_with_current_clip_rect = false); // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) + // If you want to create ImDrawList instances, pass them ImGui::GetDrawListSharedData(). + // (advanced: you may create and use your own ImDrawListSharedData so you can use ImDrawList without ImGui, but that's more involved) + IMGUI_API ImDrawList(ImDrawListSharedData* shared_data); + IMGUI_API ~ImDrawList(); + + IMGUI_API void PushClipRect(const ImVec2& clip_rect_min, const ImVec2& clip_rect_max, bool intersect_with_current_clip_rect = false); // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) IMGUI_API void PushClipRectFullScreen(); IMGUI_API void PopClipRect(); IMGUI_API void PushTextureID(ImTextureID texture_id); @@ -1914,22 +3244,37 @@ struct ImDrawList inline ImVec2 GetClipRectMax() const { const ImVec4& cr = _ClipRectStack.back(); return ImVec2(cr.z, cr.w); } // Primitives + // - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. // - For rectangular primitives, "p_min" and "p_max" represent the upper-left and lower-right corners. + // - For circle primitives, use "num_segments == 0" to automatically calculate tessellation (preferred). + // In older versions (until Dear ImGui 1.77) the AddCircle functions defaulted to num_segments == 12. + // In future versions we will use textures to provide cheaper and higher-quality circles. + // Use AddNgon() and AddNgonFilled() functions if you need to guarantee a specific number of sides. IMGUI_API void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f); - IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawCornerFlags rounding_corners = ImDrawCornerFlags_All, float thickness = 1.0f); // a: upper-left, b: lower-right (== upper-left + size), rounding_corners_flags: 4-bits corresponding to which corner to round - IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawCornerFlags rounding_corners = ImDrawCornerFlags_All); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0, float thickness = 1.0f); // a: upper-left, b: lower-right (== upper-left + size) + IMGUI_API void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawFlags flags = 0); // a: upper-left, b: lower-right (== upper-left + size) IMGUI_API void AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_max, ImU32 col_upr_left, ImU32 col_upr_right, ImU32 col_bot_right, ImU32 col_bot_left); IMGUI_API void AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness = 1.0f); IMGUI_API void AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col); IMGUI_API void AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness = 1.0f); IMGUI_API void AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col); - IMGUI_API void AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments = 12, float thickness = 1.0f); - IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 12); + IMGUI_API void AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments = 0); + IMGUI_API void AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness = 1.0f); + IMGUI_API void AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments); + IMGUI_API void AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f); + IMGUI_API void AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot = 0.0f, int num_segments = 0); IMGUI_API void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL); - IMGUI_API void AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); - IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, bool closed, float thickness); - IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); // Note: Anti-aliased filling requires points to be in clockwise order. - IMGUI_API void AddBezierCurve(const ImVec2& pos0, const ImVec2& cp0, const ImVec2& cp1, const ImVec2& pos1, ImU32 col, float thickness, int num_segments = 0); + IMGUI_API void AddText(ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL, float wrap_width = 0.0f, const ImVec4* cpu_fine_clip_rect = NULL); + IMGUI_API void AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments = 0); // Quadratic Bezier (3 control points) + + // General polygon + // - Only simple polygons are supported by filling functions (no self-intersections, no holes). + // - Concave polygon fill is more expensive than convex one: it has O(N^2) complexity. Provided as a convenience for the user but not used by the main library. + IMGUI_API void AddPolyline(const ImVec2* points, int num_points, ImU32 col, ImDrawFlags flags, float thickness); + IMGUI_API void AddConvexPolyFilled(const ImVec2* points, int num_points, ImU32 col); + IMGUI_API void AddConcavePolyFilled(const ImVec2* points, int num_points, ImU32 col); // Image primitives // - Read FAQ to understand what ImTextureID is. @@ -1937,44 +3282,80 @@ struct ImDrawList // - "uv_min" and "uv_max" represent the normalized texture coordinates to use for those corners. Using (0,0)->(1,1) texture coordinates will generally display the entire texture. IMGUI_API void AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min = ImVec2(0, 0), const ImVec2& uv_max = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE); IMGUI_API void AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1 = ImVec2(0, 0), const ImVec2& uv2 = ImVec2(1, 0), const ImVec2& uv3 = ImVec2(1, 1), const ImVec2& uv4 = ImVec2(0, 1), ImU32 col = IM_COL32_WHITE); - IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawCornerFlags rounding_corners = ImDrawCornerFlags_All); + IMGUI_API void AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0); // Stateful path API, add points then finish with PathFillConvex() or PathStroke() + // - Important: filled shapes must always use clockwise winding order! The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. + // so e.g. 'PathArcTo(center, radius, PI * -0.5f, PI)' is ok, whereas 'PathArcTo(center, radius, PI, PI * -0.5f)' won't have correct anti-aliasing when followed by PathFillConvex(). inline void PathClear() { _Path.Size = 0; } inline void PathLineTo(const ImVec2& pos) { _Path.push_back(pos); } - inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size-1], &pos, 8) != 0) _Path.push_back(pos); } - inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } // Note: Anti-aliased filling requires points to be in clockwise order. - inline void PathStroke(ImU32 col, bool closed, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, closed, thickness); _Path.Size = 0; } - IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 10); - IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle - IMGUI_API void PathBezierCurveTo(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, int num_segments = 0); - IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawCornerFlags rounding_corners = ImDrawCornerFlags_All); + inline void PathLineToMergeDuplicate(const ImVec2& pos) { if (_Path.Size == 0 || memcmp(&_Path.Data[_Path.Size - 1], &pos, 8) != 0) _Path.push_back(pos); } + inline void PathFillConvex(ImU32 col) { AddConvexPolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathFillConcave(ImU32 col) { AddConcavePolyFilled(_Path.Data, _Path.Size, col); _Path.Size = 0; } + inline void PathStroke(ImU32 col, ImDrawFlags flags = 0, float thickness = 1.0f) { AddPolyline(_Path.Data, _Path.Size, col, flags, thickness); _Path.Size = 0; } + IMGUI_API void PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments = 0); + IMGUI_API void PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12); // Use precomputed angles for a 12 steps circle + IMGUI_API void PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments = 0); // Ellipse + IMGUI_API void PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0); // Cubic Bezier (4 control points) + IMGUI_API void PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments = 0); // Quadratic Bezier (3 control points) + IMGUI_API void PathRect(const ImVec2& rect_min, const ImVec2& rect_max, float rounding = 0.0f, ImDrawFlags flags = 0); - // Advanced - IMGUI_API void AddCallback(ImDrawCallback callback, void* callback_data); // Your rendering function must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. + // Advanced: Draw Callbacks + // - May be used to alter render state (change sampler, blending, current shader). May be used to emit custom rendering commands (difficult to do correctly, but possible). + // - Use special ImDrawCallback_ResetRenderState callback to instruct backend to reset its render state to the default. + // - Your rendering loop must check for 'UserCallback' in ImDrawCmd and call the function instead of rendering triangles. All standard backends are honoring this. + // - For some backends, the callback may access selected render-states exposed by the backend in a ImGui_ImplXXXX_RenderState structure pointed to by platform_io.Renderer_RenderState. + // - IMPORTANT: please be mindful of the different level of indirection between using size==0 (copying argument) and using size>0 (copying pointed data into a buffer). + // - If userdata_size == 0: we copy/store the 'userdata' argument as-is. It will be available unmodified in ImDrawCmd::UserCallbackData during render. + // - If userdata_size > 0, we copy/store 'userdata_size' bytes pointed to by 'userdata'. We store them in a buffer stored inside the drawlist. ImDrawCmd::UserCallbackData will point inside that buffer so you have to retrieve data from there. Your callback may need to use ImDrawCmd::UserCallbackDataSize if you expect dynamically-sized data. + // - Support for userdata_size > 0 was added in v1.91.4, October 2024. So earlier code always only allowed to copy/store a simple void*. + IMGUI_API void AddCallback(ImDrawCallback callback, void* userdata, size_t userdata_size = 0); + + // Advanced: Miscellaneous IMGUI_API void AddDrawCmd(); // This is useful if you need to forcefully create a new draw call (to allow for dependent rendering / blending). Otherwise primitives are merged into the same draw-call as much as possible IMGUI_API ImDrawList* CloneOutput() const; // Create a clone of the CmdBuffer/IdxBuffer/VtxBuffer. // Advanced: Channels - // - Use to split render into layers. By switching channels to can render out-of-order (e.g. submit foreground primitives before background primitives) - // - Use to minimize draw calls (e.g. if going back-and-forth between multiple non-overlapping clipping rectangles, prefer to append into separate channels then merge at the end) + // - Use to split render into layers. By switching channels to can render out-of-order (e.g. submit FG primitives before BG primitives) + // - Use to minimize draw calls (e.g. if going back-and-forth between multiple clipping rectangles, prefer to append into separate channels then merge at the end) + // - This API shouldn't have been in ImDrawList in the first place! + // Prefer using your own persistent instance of ImDrawListSplitter as you can stack them. + // Using the ImDrawList::ChannelsXXXX you cannot stack a split over another. inline void ChannelsSplit(int count) { _Splitter.Split(this, count); } inline void ChannelsMerge() { _Splitter.Merge(this); } inline void ChannelsSetCurrent(int n) { _Splitter.SetCurrentChannel(this, n); } - // Internal helpers - // NB: all primitives needs to be reserved via PrimReserve() beforehand! - IMGUI_API void Clear(); - IMGUI_API void ClearFreeMemory(); + // Advanced: Primitives allocations + // - We render triangles (three vertices) + // - All primitives needs to be reserved via PrimReserve() beforehand. IMGUI_API void PrimReserve(int idx_count, int vtx_count); + IMGUI_API void PrimUnreserve(int idx_count, int vtx_count); IMGUI_API void PrimRect(const ImVec2& a, const ImVec2& b, ImU32 col); // Axis aligned rectangle (composed of two triangles) IMGUI_API void PrimRectUV(const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, ImU32 col); IMGUI_API void PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& d, const ImVec2& uv_a, const ImVec2& uv_b, const ImVec2& uv_c, const ImVec2& uv_d, ImU32 col); - inline void PrimWriteVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col){ _VtxWritePtr->pos = pos; _VtxWritePtr->uv = uv; _VtxWritePtr->col = col; _VtxWritePtr++; _VtxCurrentIdx++; } - inline void PrimWriteIdx(ImDrawIdx idx) { *_IdxWritePtr = idx; _IdxWritePtr++; } - inline void PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } - IMGUI_API void UpdateClipRect(); - IMGUI_API void UpdateTextureID(); + inline void PrimWriteVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { _VtxWritePtr->pos = pos; _VtxWritePtr->uv = uv; _VtxWritePtr->col = col; _VtxWritePtr++; _VtxCurrentIdx++; } + inline void PrimWriteIdx(ImDrawIdx idx) { *_IdxWritePtr = idx; _IdxWritePtr++; } + inline void PrimVtx(const ImVec2& pos, const ImVec2& uv, ImU32 col) { PrimWriteIdx((ImDrawIdx)_VtxCurrentIdx); PrimWriteVtx(pos, uv, col); } // Write vertex with unique index + + // Obsolete names + //inline void AddEllipse(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0, float thickness = 1.0f) { AddEllipse(center, ImVec2(radius_x, radius_y), col, rot, num_segments, thickness); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void AddEllipseFilled(const ImVec2& center, float radius_x, float radius_y, ImU32 col, float rot = 0.0f, int num_segments = 0) { AddEllipseFilled(center, ImVec2(radius_x, radius_y), col, rot, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void PathEllipticalArcTo(const ImVec2& center, float radius_x, float radius_y, float rot, float a_min, float a_max, int num_segments = 0) { PathEllipticalArcTo(center, ImVec2(radius_x, radius_y), rot, a_min, a_max, num_segments); } // OBSOLETED in 1.90.5 (Mar 2024) + //inline void AddBezierCurve(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments = 0) { AddBezierCubic(p1, p2, p3, p4, col, thickness, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + //inline void PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments = 0) { PathBezierCubicCurveTo(p2, p3, p4, num_segments); } // OBSOLETED in 1.80 (Jan 2021) + + // [Internal helpers] + IMGUI_API void _ResetForNewFrame(); + IMGUI_API void _ClearFreeMemory(); + IMGUI_API void _PopUnusedDrawCmd(); + IMGUI_API void _TryMergeDrawCmds(); + IMGUI_API void _OnChangedClipRect(); + IMGUI_API void _OnChangedTextureID(); + IMGUI_API void _OnChangedVtxOffset(); + IMGUI_API void _SetTextureID(ImTextureID texture_id); + IMGUI_API int _CalcCircleAutoSegmentCount(float radius) const; + IMGUI_API void _PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step); + IMGUI_API void _PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments); }; // All draw data to render a Dear ImGui frame @@ -1982,46 +3363,50 @@ struct ImDrawList // as this is one of the oldest structure exposed by the library! Basically, ImDrawList == CmdList) struct ImDrawData { - bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. - ImDrawList** CmdLists; // Array of ImDrawList* to render. The ImDrawList are owned by ImGuiContext and only pointed to from here. - int CmdListsCount; // Number of ImDrawList* to render - int TotalIdxCount; // For convenience, sum of all ImDrawList's IdxBuffer.Size - int TotalVtxCount; // For convenience, sum of all ImDrawList's VtxBuffer.Size - ImVec2 DisplayPos; // Upper-left position of the viewport to render (== upper-left of the orthogonal projection matrix to use) - ImVec2 DisplaySize; // Size of the viewport to render (== io.DisplaySize for the main viewport) (DisplayPos + DisplaySize == lower-right of the orthogonal projection matrix to use) - ImVec2 FramebufferScale; // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display. + bool Valid; // Only valid after Render() is called and before the next NewFrame() is called. + int CmdListsCount; // Number of ImDrawList* to render + int TotalIdxCount; // For convenience, sum of all ImDrawList's IdxBuffer.Size + int TotalVtxCount; // For convenience, sum of all ImDrawList's VtxBuffer.Size + ImVector CmdLists; // Array of ImDrawList* to render. The ImDrawLists are owned by ImGuiContext and only pointed to from here. + ImVec2 DisplayPos; // Top-left position of the viewport to render (== top-left of the orthogonal projection matrix to use) (== GetMainViewport()->Pos for the main viewport, == (0.0) in most single-viewport applications) + ImVec2 DisplaySize; // Size of the viewport to render (== GetMainViewport()->Size for the main viewport, == io.DisplaySize in most single-viewport applications) + ImVec2 FramebufferScale; // Amount of pixels for each unit of DisplaySize. Based on io.DisplayFramebufferScale. Generally (1,1) on normal display, (2,2) on OSX with Retina display. + ImGuiViewport* OwnerViewport; // Viewport carrying the ImDrawData instance, might be of use to the renderer (generally not). // Functions - ImDrawData() { Valid = false; Clear(); } - ~ImDrawData() { Clear(); } - void Clear() { Valid = false; CmdLists = NULL; CmdListsCount = TotalVtxCount = TotalIdxCount = 0; DisplayPos = DisplaySize = FramebufferScale = ImVec2(0.f, 0.f); } // The ImDrawList are owned by ImGuiContext! + ImDrawData() { Clear(); } + IMGUI_API void Clear(); + IMGUI_API void AddDrawList(ImDrawList* draw_list); // Helper to add an external draw list into an existing ImDrawData. IMGUI_API void DeIndexAllBuffers(); // Helper to convert all buffers from indexed to non-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! IMGUI_API void ScaleClipRects(const ImVec2& fb_scale); // Helper to scale the ClipRect field of each ImDrawCmd. Use if your final output buffer is at a different scale than Dear ImGui expects, or if there is a difference between your window resolution and framebuffer resolution. }; //----------------------------------------------------------------------------- -// Font API (ImFontConfig, ImFontGlyph, ImFontAtlasFlags, ImFontAtlas, ImFontGlyphRangesBuilder, ImFont) +// [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontAtlasFlags, ImFontAtlas, ImFontGlyphRangesBuilder, ImFont) //----------------------------------------------------------------------------- +// A font input/source (we may rename this to ImFontSource in the future) struct ImFontConfig { void* FontData; // // TTF/OTF data int FontDataSize; // // TTF/OTF data size bool FontDataOwnedByAtlas; // true // TTF/OTF data ownership taken by the container ImFontAtlas (will delete memory itself). + bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. + bool PixelSnapH; // false // Align every glyph AdvanceX to pixel boundaries. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. int FontNo; // 0 // Index of font within TTF/OTF file + int OversampleH; // 0 (2) // Rasterize at higher quality for sub-pixel positioning. 0 == auto == 1 or 2 depending on size. Note the difference between 2 and 3 is minimal. You can reduce this to 1 for large glyphs save memory. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + int OversampleV; // 0 (1) // Rasterize at higher quality for sub-pixel positioning. 0 == auto == 1. This is not really useful as we don't use sub-pixel positions on the Y axis. float SizePixels; // // Size in pixels for rasterizer (more or less maps to the resulting font height). - int OversampleH; // 3 // Rasterize at higher quality for sub-pixel positioning. Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. - int OversampleV; // 1 // Rasterize at higher quality for sub-pixel positioning. We don't use sub-pixel positions on the Y axis. - bool PixelSnapH; // false // Align every glyph to pixel boundary. Useful e.g. if you are merging a non-pixel aligned font with the default font. If enabled, you can set OversampleH/V to 1. - ImVec2 GlyphExtraSpacing; // 0, 0 // Extra spacing (in pixels) between glyphs. Only X axis is supported for now. + //ImVec2 GlyphExtraSpacing; // 0, 0 // (REMOVED IN 1.91.9: use GlyphExtraAdvanceX) ImVec2 GlyphOffset; // 0, 0 // Offset all glyphs from this font input. - const ImWchar* GlyphRanges; // NULL // Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list). THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. + const ImWchar* GlyphRanges; // NULL // THE ARRAY DATA NEEDS TO PERSIST AS LONG AS THE FONT IS ALIVE. Pointer to a user-provided list of Unicode range (2 value per range, values are inclusive, zero-terminated list). float GlyphMinAdvanceX; // 0 // Minimum AdvanceX for glyphs, set Min to align font icons, set both Min/Max to enforce mono-space font float GlyphMaxAdvanceX; // FLT_MAX // Maximum AdvanceX for glyphs - bool MergeMode; // false // Merge into previous ImFont, so you can combine multiple inputs font into one ImFont (e.g. ASCII font + icons + Japanese glyphs). You may want to use GlyphOffset.y when merge font of different heights. - unsigned int RasterizerFlags; // 0x00 // Settings for custom font rasterizer (e.g. ImGuiFreeType). Leave as zero if you aren't using one. - float RasterizerMultiply; // 1.0f // Brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. - ImWchar EllipsisChar; // -1 // Explicitly specify unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used. + float GlyphExtraAdvanceX; // 0 // Extra spacing (in pixels) between glyphs. Please contact us if you are using this. + unsigned int FontBuilderFlags; // 0 // Settings for custom font builder. THIS IS BUILDER IMPLEMENTATION DEPENDENT. Leave as zero if unsure. + float RasterizerMultiply; // 1.0f // Linearly brighten (>1.0f) or darken (<1.0f) font output. Brightening small fonts may be a good workaround to make them more readable. This is a silly thing we may remove in the future. + float RasterizerDensity; // 1.0f // DPI scale for rasterization, not altering other font metrics: make it easy to swap between e.g. a 100% and a 400% fonts for a zooming display. IMPORTANT: If you increase this it is expected that you increase font scale accordingly, otherwise quality may look lowered. + ImWchar EllipsisChar; // 0 // Explicitly specify Unicode codepoint of ellipsis character. When fonts are being merged first specified ellipsis will be used. // [Internal] char Name[40]; // Name (strictly to ease debugging) @@ -2030,10 +3415,14 @@ struct ImFontConfig IMGUI_API ImFontConfig(); }; +// Hold rendering data for one glyph. +// (Note: some language parsers may fail to convert the 31+1 bitfield members, in this case maybe drop store a single u32 or we can rework this) struct ImFontGlyph { - ImWchar Codepoint; // 0x0000..0xFFFF - float AdvanceX; // Distance to next character (= data from font + ImFontConfig::GlyphExtraSpacing.x baked in) + unsigned int Colored : 1; // Flag to indicate glyph is colored and should generally ignore tinting (make it usable with no shift on little-endian as this is used in loops) + unsigned int Visible : 1; // Flag to indicate glyph has no visible pixels (e.g. space). Allow early out when rendering. + unsigned int Codepoint : 30; // 0x0000..0x10FFFF + float AdvanceX; // Horizontal distance to advance layout with float X0, Y0, X1, Y1; // Glyph corners float U0, V0, U1, V1; // Texture coordinates }; @@ -2044,11 +3433,11 @@ struct ImFontGlyphRangesBuilder { ImVector UsedChars; // Store 1-bit per Unicode code point (0=unused, 1=used) - ImFontGlyphRangesBuilder() { Clear(); } - inline void Clear() { int size_in_bytes = 0x10000 / 8; UsedChars.resize(size_in_bytes / (int)sizeof(ImU32)); memset(UsedChars.Data, 0, (size_t)size_in_bytes); } - inline bool GetBit(int n) const { int off = (n >> 5); ImU32 mask = 1u << (n & 31); return (UsedChars[off] & mask) != 0; } // Get bit n in the array - inline void SetBit(int n) { int off = (n >> 5); ImU32 mask = 1u << (n & 31); UsedChars[off] |= mask; } // Set bit n in the array - inline void AddChar(ImWchar c) { SetBit(c); } // Add character + ImFontGlyphRangesBuilder() { Clear(); } + inline void Clear() { int size_in_bytes = (IM_UNICODE_CODEPOINT_MAX + 1) / 8; UsedChars.resize(size_in_bytes / (int)sizeof(ImU32)); memset(UsedChars.Data, 0, (size_t)size_in_bytes); } + inline bool GetBit(size_t n) const { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); return (UsedChars[off] & mask) != 0; } // Get bit n in the array + inline void SetBit(size_t n) { int off = (int)(n >> 5); ImU32 mask = 1u << (n & 31); UsedChars[off] |= mask; } // Set bit n in the array + inline void AddChar(ImWchar c) { SetBit(c); } // Add character IMGUI_API void AddText(const char* text, const char* text_end = NULL); // Add string (each character of the UTF-8 string are added) IMGUI_API void AddRanges(const ImWchar* ranges); // Add ranges, e.g. builder.AddRanges(ImFontAtlas::GetGlyphRangesDefault()) to force add all of ASCII/Latin+Ext IMGUI_API void BuildRanges(ImVector* out_ranges); // Output new ranges @@ -2057,21 +3446,26 @@ struct ImFontGlyphRangesBuilder // See ImFontAtlas::AddCustomRectXXX functions. struct ImFontAtlasCustomRect { - unsigned int ID; // Input // User ID. Use <0x10000 to map into a font glyph, >=0x10000 for other/internal/custom texture data. - unsigned short Width, Height; // Input // Desired rectangle dimension unsigned short X, Y; // Output // Packed position in Atlas - float GlyphAdvanceX; // Input // For custom font glyphs only (ID<0x10000): glyph xadvance - ImVec2 GlyphOffset; // Input // For custom font glyphs only (ID<0x10000): glyph display offset - ImFont* Font; // Input // For custom font glyphs only (ID<0x10000): target font - ImFontAtlasCustomRect() { ID = 0xFFFFFFFF; Width = Height = 0; X = Y = 0xFFFF; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0,0); Font = NULL; } + + // [Internal] + unsigned short Width, Height; // Input // Desired rectangle dimension + unsigned int GlyphID : 31; // Input // For custom font glyphs only (ID < 0x110000) + unsigned int GlyphColored : 1; // Input // For custom font glyphs only: glyph is colored, removed tinting. + float GlyphAdvanceX; // Input // For custom font glyphs only: glyph xadvance + ImVec2 GlyphOffset; // Input // For custom font glyphs only: glyph display offset + ImFont* Font; // Input // For custom font glyphs only: target font + ImFontAtlasCustomRect() { X = Y = 0xFFFF; Width = Height = 0; GlyphID = 0; GlyphColored = 0; GlyphAdvanceX = 0.0f; GlyphOffset = ImVec2(0, 0); Font = NULL; } bool IsPacked() const { return X != 0xFFFF; } }; +// Flags for ImFontAtlas build enum ImFontAtlasFlags_ { ImFontAtlasFlags_None = 0, ImFontAtlasFlags_NoPowerOfTwoHeight = 1 << 0, // Don't round the height to next power of two - ImFontAtlasFlags_NoMouseCursors = 1 << 1 // Don't build software mouse cursors into the atlas + ImFontAtlasFlags_NoMouseCursors = 1 << 1, // Don't build software mouse cursors into the atlas (save a little texture memory) + ImFontAtlasFlags_NoBakedLines = 1 << 2, // Don't build thick line textures into the atlas (save a little texture memory, allow support for point/nearest filtering). The AntiAliasedLinesUseTex features uses them, otherwise they will be rendered using polygons (more expensive for CPU/GPU). }; // Load and rasterize multiple TTF/OTF fonts into a same texture. The font atlas will build a single texture holding: @@ -2090,7 +3484,7 @@ enum ImFontAtlasFlags_ // - Important: By default, AddFontFromMemoryTTF() takes ownership of the data. Even though we are not writing to it, we will free the pointer on destruction. // You can set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed, // - Even though many functions are suffixed with "TTF", OTF data is supported just as well. -// - This is an old API and it is currently awkward for those and and various other reasons! We will address them in the future! +// - This is an old API and it is currently awkward for those and various other reasons! We will address them in the future! struct ImFontAtlas { IMGUI_API ImFontAtlas(); @@ -2098,12 +3492,12 @@ struct ImFontAtlas IMGUI_API ImFont* AddFont(const ImFontConfig* font_cfg); IMGUI_API ImFont* AddFontDefault(const ImFontConfig* font_cfg = NULL); IMGUI_API ImFont* AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); - IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. - IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. + IMGUI_API ImFont* AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // Note: Transfer ownership of 'ttf_data' to ImFontAtlas! Will be deleted after destruction of the atlas. Set font_cfg->FontDataOwnedByAtlas=false to keep ownership of your data and it won't be freed. + IMGUI_API ImFont* AddFontFromMemoryCompressedTTF(const void* compressed_font_data, int compressed_font_data_size, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data' still owned by caller. Compress with binary_to_compressed_c.cpp. IMGUI_API ImFont* AddFontFromMemoryCompressedBase85TTF(const char* compressed_font_data_base85, float size_pixels, const ImFontConfig* font_cfg = NULL, const ImWchar* glyph_ranges = NULL); // 'compressed_font_data_base85' still owned by caller. Compress with binary_to_compressed_c.cpp with -base85 parameter. IMGUI_API void ClearInputData(); // Clear input data (all ImFontConfig structures including sizes, TTF data, glyph ranges, etc.) = all the data used to build the texture and fonts. + IMGUI_API void ClearFonts(); // Clear input+output font data (same as ClearInputData() + glyphs storage, UV coordinates). IMGUI_API void ClearTexData(); // Clear output texture data (CPU side). Saves RAM once the texture has been copied to graphics memory. - IMGUI_API void ClearFonts(); // Clear output font data (glyphs storage, UV coordinates). IMGUI_API void Clear(); // Clear all input and output. // Build atlas, retrieve pixel data. @@ -2114,7 +3508,7 @@ struct ImFontAtlas IMGUI_API bool Build(); // Build pixels data. This is called automatically for you by the GetTexData*** functions. IMGUI_API void GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 1 byte per-pixel IMGUI_API void GetTexDataAsRGBA32(unsigned char** out_pixels, int* out_width, int* out_height, int* out_bytes_per_pixel = NULL); // 4 bytes-per-pixel - bool IsBuilt() { return Fonts.Size > 0 && (TexPixelsAlpha8 != NULL || TexPixelsRGBA32 != NULL); } + bool IsBuilt() const { return Fonts.Size > 0 && TexReady; } // Bit ambiguous: used to detect when user didn't build texture but effectively we should check TexID != 0 except that would be backend dependent... void SetTexID(ImTextureID id) { TexID = id; } //------------------------------------------- @@ -2122,11 +3516,13 @@ struct ImFontAtlas //------------------------------------------- // Helpers to retrieve list of common Unicode ranges (2 value per range, values are inclusive, zero-terminated list) - // NB: Make sure that your string are UTF-8 and NOT in your local code page. In C++11, you can create UTF-8 string literal using the u8"Hello world" syntax. See FAQ for details. + // NB: Make sure that your string are UTF-8 and NOT in your local code page. + // Read https://github.com/ocornut/imgui/blob/master/docs/FONTS.md/#about-utf-8-encoding for details. // NB: Consider using ImFontGlyphRangesBuilder to build glyph ranges from textual data. IMGUI_API const ImWchar* GetGlyphRangesDefault(); // Basic Latin, Extended Latin + IMGUI_API const ImWchar* GetGlyphRangesGreek(); // Default + Greek and Coptic IMGUI_API const ImWchar* GetGlyphRangesKorean(); // Default + Korean characters - IMGUI_API const ImWchar* GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 1946 Ideographs + IMGUI_API const ImWchar* GetGlyphRangesJapanese(); // Default + Hiragana, Katakana, Half-Width, Selection of 2999 Ideographs IMGUI_API const ImWchar* GetGlyphRangesChineseFull(); // Default + Half-Width + Japanese Hiragana/Katakana + full set of about 21000 CJK Unified Ideographs IMGUI_API const ImWchar* GetGlyphRangesChineseSimplifiedCommon();// Default + Half-Width + Japanese Hiragana/Katakana + set of 2500 CJK Unified Ideographs for common simplified Chinese IMGUI_API const ImWchar* GetGlyphRangesCyrillic(); // Default + about 400 Cyrillic characters @@ -2134,34 +3530,39 @@ struct ImFontAtlas IMGUI_API const ImWchar* GetGlyphRangesVietnamese(); // Default + Vietnamese characters //------------------------------------------- - // [BETA] Custom Rectangles/Glyphs API + // [ALPHA] Custom Rectangles/Glyphs API //------------------------------------------- // You can request arbitrary rectangles to be packed into the atlas, for your own purposes. - // After calling Build(), you can query the rectangle position and render your pixels. - // You can also request your rectangles to be mapped as font glyph (given a font + Unicode point), - // so you can render e.g. custom colorful icons and use them as regular glyphs. - // Read misc/fonts/README.txt for more details about using colorful icons. - IMGUI_API int AddCustomRectRegular(unsigned int id, int width, int height); // Id needs to be >= 0x10000. Id >= 0x80000000 are reserved for ImGui and ImDrawList - IMGUI_API int AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset = ImVec2(0,0)); // Id needs to be < 0x10000 to register a rectangle to map into a specific font. - const ImFontAtlasCustomRect*GetCustomRectByIndex(int index) const { if (index < 0) return NULL; return &CustomRects[index]; } + // - After calling Build(), you can query the rectangle position and render your pixels. + // - If you render colored output, set 'atlas->TexPixelsUseColors = true' as this may help some backends decide of preferred texture format. + // - You can also request your rectangles to be mapped as font glyph (given a font + Unicode point), + // so you can render e.g. custom colorful icons and use them as regular glyphs. + // - Read docs/FONTS.md for more details about using colorful icons. + // - Note: this API may be redesigned later in order to support multi-monitor varying DPI settings. + IMGUI_API int AddCustomRectRegular(int width, int height); + IMGUI_API int AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset = ImVec2(0, 0)); + ImFontAtlasCustomRect* GetCustomRectByIndex(int index) { IM_ASSERT(index >= 0); return &CustomRects[index]; } // [Internal] - IMGUI_API void CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max); - IMGUI_API bool GetMouseCursorTexData(ImGuiMouseCursor cursor, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]); + IMGUI_API void CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const; //------------------------------------------- // Members //------------------------------------------- - bool Locked; // Marked as Locked by ImGui::NewFrame() so attempt to modify the atlas will assert. + // Input ImFontAtlasFlags Flags; // Build flags (see ImFontAtlasFlags_) ImTextureID TexID; // User data to refer to the texture once it has been uploaded to user's graphic systems. It is passed back to you during rendering via the ImDrawCmd structure. int TexDesiredWidth; // Texture width desired by user before Build(). Must be a power-of-two. If have many glyphs your graphics API have texture size restrictions you may want to increase texture width to decrease height. - int TexGlyphPadding; // Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0. + int TexGlyphPadding; // FIXME: Should be called "TexPackPadding". Padding between glyphs within texture in pixels. Defaults to 1. If your rendering method doesn't rely on bilinear filtering you may set this to 0 (will also need to set AntiAliasedLinesUseTex = false). + void* UserData; // Store your own atlas related user-data (if e.g. you have multiple font atlas). // [Internal] // NB: Access texture data via GetTexData*() calls! Which will setup a default font for you. + bool Locked; // Marked as Locked by ImGui::NewFrame() so attempt to modify the atlas will assert. + bool TexReady; // Set when texture was built matching current font input + bool TexPixelsUseColors; // Tell whether our texture data is known to use colors (rather than just alpha channel), in order to help backend select a format. unsigned char* TexPixelsAlpha8; // 1 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight unsigned int* TexPixelsRGBA32; // 4 component per pixel, each component is unsigned 8-bit. Total size = TexWidth * TexHeight * 4 int TexWidth; // Texture width calculated during Build(). @@ -2170,69 +3571,440 @@ struct ImFontAtlas ImVec2 TexUvWhitePixel; // Texture coordinates to a white pixel ImVector Fonts; // Hold all the fonts returned by AddFont*. Fonts[0] is the default font upon calling ImGui::NewFrame(), use ImGui::PushFont()/PopFont() to change the current font. ImVector CustomRects; // Rectangles for packing custom texture data into the atlas. - ImVector ConfigData; // Internal data - int CustomRectIds[1]; // Identifiers of custom texture rectangle used by ImFontAtlas/ImDrawList + ImVector Sources; // Source/configuration data + ImVec4 TexUvLines[IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1]; // UVs for baked anti-aliased lines -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - typedef ImFontAtlasCustomRect CustomRect; // OBSOLETED in 1.72+ - typedef ImFontGlyphRangesBuilder GlyphRangesBuilder; // OBSOLETED in 1.67+ -#endif + // [Internal] Font builder + const ImFontBuilderIO* FontBuilderIO; // Opaque interface to a font builder (default to stb_truetype, can be changed to use FreeType by defining IMGUI_ENABLE_FREETYPE). + unsigned int FontBuilderFlags; // Shared flags (for all fonts) for custom font builder. THIS IS BUILD IMPLEMENTATION DEPENDENT. Per-font override is also available in ImFontConfig. + + // [Internal] Packing data + int PackIdMouseCursors; // Custom texture rectangle ID for white pixel and mouse cursors + int PackIdLines; // Custom texture rectangle ID for baked anti-aliased lines + + // [Obsolete] + //typedef ImFontAtlasCustomRect CustomRect; // OBSOLETED in 1.72+ + //typedef ImFontGlyphRangesBuilder GlyphRangesBuilder; // OBSOLETED in 1.67+ }; // Font runtime data and rendering // ImFontAtlas automatically loads a default embedded font for you when you call GetTexDataAsAlpha8() or GetTexDataAsRGBA32(). struct ImFont { - // Members: Hot ~20/24 bytes (for CalcTextSize) - ImVector IndexAdvanceX; // 12-16 // out // // Sparse. Glyphs->AdvanceX in a directly indexable way (cache-friendly for CalcTextSize functions which only this this info, and are often bottleneck in large UI). + // [Internal] Members: Hot ~20/24 bytes (for CalcTextSize) + ImVector IndexAdvanceX; // 12-16 // out // Sparse. Glyphs->AdvanceX in a directly indexable way (cache-friendly for CalcTextSize functions which only this info, and are often bottleneck in large UI). float FallbackAdvanceX; // 4 // out // = FallbackGlyph->AdvanceX - float FontSize; // 4 // in // // Height of characters/line, set during loading (don't change after loading) + float FontSize; // 4 // in // Height of characters/line, set during loading (don't change after loading) - // Members: Hot ~36/48 bytes (for CalcTextSize + render loop) - ImVector IndexLookup; // 12-16 // out // // Sparse. Index glyphs by Unicode code-point. - ImVector Glyphs; // 12-16 // out // // All glyphs. - const ImFontGlyph* FallbackGlyph; // 4-8 // out // = FindGlyph(FontFallbackChar) - ImVec2 DisplayOffset; // 8 // in // = (0,0) // Offset font rendering by xx pixels + // [Internal] Members: Hot ~28/40 bytes (for RenderText loop) + ImVector IndexLookup; // 12-16 // out // Sparse. Index glyphs by Unicode code-point. + ImVector Glyphs; // 12-16 // out // All glyphs. + ImFontGlyph* FallbackGlyph; // 4-8 // out // = FindGlyph(FontFallbackChar) - // Members: Cold ~32/40 bytes - ImFontAtlas* ContainerAtlas; // 4-8 // out // // What we has been loaded into - const ImFontConfig* ConfigData; // 4-8 // in // // Pointer within ContainerAtlas->ConfigData - short ConfigDataCount; // 2 // in // ~ 1 // Number of ImFontConfig involved in creating this font. Bigger than 1 when merging multiple font sources into one ImFont. - ImWchar FallbackChar; // 2 // in // = '?' // Replacement character if a glyph isn't found. Only set via SetFallbackChar() - ImWchar EllipsisChar; // 2 // out // = -1 // Character used for ellipsis rendering. - float Scale; // 4 // in // = 1.f // Base font scale, multiplied by the per-window font scale which you can adjust with SetWindowFontScale() - float Ascent, Descent; // 4+4 // out // // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] - int MetricsTotalSurface;// 4 // out // // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs) + // [Internal] Members: Cold ~32/40 bytes + // Conceptually Sources[] is the list of font sources merged to create this font. + ImFontAtlas* ContainerAtlas; // 4-8 // out // What we has been loaded into + ImFontConfig* Sources; // 4-8 // in // Pointer within ContainerAtlas->Sources[], to SourcesCount instances + short SourcesCount; // 2 // in // Number of ImFontConfig involved in creating this font. Usually 1, or >1 when merging multiple font sources into one ImFont. + short EllipsisCharCount; // 1 // out // 1 or 3 + ImWchar EllipsisChar; // 2-4 // out // Character used for ellipsis rendering ('...'). + ImWchar FallbackChar; // 2-4 // out // Character used if a glyph isn't found (U+FFFD, '?') + float EllipsisWidth; // 4 // out // Total ellipsis Width + float EllipsisCharStep; // 4 // out // Step between characters when EllipsisCount > 0 + float Scale; // 4 // in // Base font scale (1.0f), multiplied by the per-window font scale which you can adjust with SetWindowFontScale() + float Ascent, Descent; // 4+4 // out // Ascent: distance from top to bottom of e.g. 'A' [0..FontSize] (unscaled) + int MetricsTotalSurface;// 4 // out // Total surface in pixels to get an idea of the font rasterization/texture cost (not exact, we approximate the cost of padding between glyphs) bool DirtyLookupTables; // 1 // out // + ImU8 Used8kPagesMap[(IM_UNICODE_CODEPOINT_MAX+1)/8192/8]; // 1 bytes if ImWchar=ImWchar16, 16 bytes if ImWchar==ImWchar32. Store 1-bit for each block of 4K codepoints that has one active glyph. This is mainly used to facilitate iterations across all used codepoints. // Methods IMGUI_API ImFont(); IMGUI_API ~ImFont(); - IMGUI_API const ImFontGlyph*FindGlyph(ImWchar c) const; - IMGUI_API const ImFontGlyph*FindGlyphNoFallback(ImWchar c) const; - float GetCharAdvance(ImWchar c) const { return ((int)c < IndexAdvanceX.Size) ? IndexAdvanceX[(int)c] : FallbackAdvanceX; } - bool IsLoaded() const { return ContainerAtlas != NULL; } - const char* GetDebugName() const { return ConfigData ? ConfigData->Name : ""; } + IMGUI_API ImFontGlyph* FindGlyph(ImWchar c); + IMGUI_API ImFontGlyph* FindGlyphNoFallback(ImWchar c); + float GetCharAdvance(ImWchar c) { return ((int)c < IndexAdvanceX.Size) ? IndexAdvanceX[(int)c] : FallbackAdvanceX; } + bool IsLoaded() const { return ContainerAtlas != NULL; } + const char* GetDebugName() const { return Sources ? Sources->Name : ""; } + // [Internal] Don't use! // 'max_width' stops rendering after a certain width (could be turned into a 2d size). FLT_MAX to disable. // 'wrap_width' enable automatic word-wrapping across multiple lines to fit into given width. 0.0f to disable. - IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL) const; // utf8 - IMGUI_API const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const; - IMGUI_API void RenderChar(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, ImWchar c) const; - IMGUI_API void RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false) const; + IMGUI_API ImVec2 CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end = NULL, const char** remaining = NULL); // utf8 + IMGUI_API const char* CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width); + IMGUI_API void RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c); + IMGUI_API void RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width = 0.0f, bool cpu_fine_clip = false); // [Internal] Don't use! IMGUI_API void BuildLookupTable(); IMGUI_API void ClearOutputData(); IMGUI_API void GrowIndex(int new_size); - IMGUI_API void AddGlyph(ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x); + IMGUI_API void AddGlyph(const ImFontConfig* src_cfg, ImWchar c, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x); IMGUI_API void AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst = true); // Makes 'dst' character/glyph points to 'src' character/glyph. Currently needs to be called AFTER fonts have been built. - IMGUI_API void SetFallbackChar(ImWchar c); + IMGUI_API bool IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last); +}; + +//----------------------------------------------------------------------------- +// [SECTION] Viewports +//----------------------------------------------------------------------------- + +// Flags stored in ImGuiViewport::Flags, giving indications to the platform backends. +enum ImGuiViewportFlags_ +{ + ImGuiViewportFlags_None = 0, + ImGuiViewportFlags_IsPlatformWindow = 1 << 0, // Represent a Platform Window + ImGuiViewportFlags_IsPlatformMonitor = 1 << 1, // Represent a Platform Monitor (unused yet) + ImGuiViewportFlags_OwnedByApp = 1 << 2, // Platform Window: Is created/managed by the user application? (rather than our backend) + ImGuiViewportFlags_NoDecoration = 1 << 3, // Platform Window: Disable platform decorations: title bar, borders, etc. (generally set all windows, but if ImGuiConfigFlags_ViewportsDecoration is set we only set this on popups/tooltips) + ImGuiViewportFlags_NoTaskBarIcon = 1 << 4, // Platform Window: Disable platform task bar icon (generally set on popups/tooltips, or all windows if ImGuiConfigFlags_ViewportsNoTaskBarIcon is set) + ImGuiViewportFlags_NoFocusOnAppearing = 1 << 5, // Platform Window: Don't take focus when created. + ImGuiViewportFlags_NoFocusOnClick = 1 << 6, // Platform Window: Don't take focus when clicked on. + ImGuiViewportFlags_NoInputs = 1 << 7, // Platform Window: Make mouse pass through so we can drag this window while peaking behind it. + ImGuiViewportFlags_NoRendererClear = 1 << 8, // Platform Window: Renderer doesn't need to clear the framebuffer ahead (because we will fill it entirely). + ImGuiViewportFlags_NoAutoMerge = 1 << 9, // Platform Window: Avoid merging this window into another host window. This can only be set via ImGuiWindowClass viewport flags override (because we need to now ahead if we are going to create a viewport in the first place!). + ImGuiViewportFlags_TopMost = 1 << 10, // Platform Window: Display on top (for tooltips only). + ImGuiViewportFlags_CanHostOtherWindows = 1 << 11, // Viewport can host multiple imgui windows (secondary viewports are associated to a single window). // FIXME: In practice there's still probably code making the assumption that this is always and only on the MainViewport. Will fix once we add support for "no main viewport". + + // Output status flags (from Platform) + ImGuiViewportFlags_IsMinimized = 1 << 12, // Platform Window: Window is minimized, can skip render. When minimized we tend to avoid using the viewport pos/size for clipping window or testing if they are contained in the viewport. + ImGuiViewportFlags_IsFocused = 1 << 13, // Platform Window: Window is focused (last call to Platform_GetWindowFocus() returned true) +}; + +// - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. +// - With multi-viewport enabled, we extend this concept to have multiple active viewports. +// - In the future we will extend this concept further to also represent Platform Monitor and support a "no main platform window" operation mode. +// - About Main Area vs Work Area: +// - Main Area = entire viewport. +// - Work Area = entire viewport minus sections used by main menu bars (for platform windows), or by task bar (for platform monitor). +// - Windows are generally trying to stay within the Work Area of their host viewport. +struct ImGuiViewport +{ + ImGuiID ID; // Unique identifier for the viewport + ImGuiViewportFlags Flags; // See ImGuiViewportFlags_ + ImVec2 Pos; // Main Area: Position of the viewport (Dear ImGui coordinates are the same as OS desktop/native coordinates) + ImVec2 Size; // Main Area: Size of the viewport. + ImVec2 WorkPos; // Work Area: Position of the viewport minus task bars, menus bars, status bars (>= Pos) + ImVec2 WorkSize; // Work Area: Size of the viewport minus task bars, menu bars, status bars (<= Size) + float DpiScale; // 1.0f = 96 DPI = No extra scale. + ImGuiID ParentViewportId; // (Advanced) 0: no parent. Instruct the platform backend to setup a parent/child relationship between platform windows. + ImDrawData* DrawData; // The ImDrawData corresponding to this viewport. Valid after Render() and until the next call to NewFrame(). + + // Platform/Backend Dependent Data + // Our design separate the Renderer and Platform backends to facilitate combining default backends with each others. + // When our create your own backend for a custom engine, it is possible that both Renderer and Platform will be handled + // by the same system and you may not need to use all the UserData/Handle fields. + // The library never uses those fields, they are merely storage to facilitate backend implementation. + void* RendererUserData; // void* to hold custom data structure for the renderer (e.g. swap chain, framebuffers etc.). generally set by your Renderer_CreateWindow function. + void* PlatformUserData; // void* to hold custom data structure for the OS / platform (e.g. windowing info, render context). generally set by your Platform_CreateWindow function. + void* PlatformHandle; // void* to hold higher-level, platform window handle (e.g. HWND for Win32 backend, Uint32 WindowID for SDL, GLFWWindow* for GLFW), for FindViewportByPlatformHandle(). + void* PlatformHandleRaw; // void* to hold lower-level, platform-native window handle (always HWND on Win32 platform, unused for other platforms). + bool PlatformWindowCreated; // Platform window has been created (Platform_CreateWindow() has been called). This is false during the first frame where a viewport is being created. + bool PlatformRequestMove; // Platform window requested move (e.g. window was moved by the OS / host window manager, authoritative position will be OS window position) + bool PlatformRequestResize; // Platform window requested resize (e.g. window was resized by the OS / host window manager, authoritative size will be OS window size) + bool PlatformRequestClose; // Platform window requested closure (e.g. window was moved by the OS / host window manager, e.g. pressing ALT-F4) + + ImGuiViewport() { memset(this, 0, sizeof(*this)); } + ~ImGuiViewport() { IM_ASSERT(PlatformUserData == NULL && RendererUserData == NULL); } + + // Helpers + ImVec2 GetCenter() const { return ImVec2(Pos.x + Size.x * 0.5f, Pos.y + Size.y * 0.5f); } + ImVec2 GetWorkCenter() const { return ImVec2(WorkPos.x + WorkSize.x * 0.5f, WorkPos.y + WorkSize.y * 0.5f); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiPlatformIO + other Platform Dependent Interfaces (ImGuiPlatformMonitor, ImGuiPlatformImeData) +//----------------------------------------------------------------------------- + +// [BETA] (Optional) Multi-Viewport Support! +// If you are new to Dear ImGui and trying to integrate it into your engine, you can probably ignore this for now. +// +// This feature allows you to seamlessly drag Dear ImGui windows outside of your application viewport. +// This is achieved by creating new Platform/OS windows on the fly, and rendering into them. +// Dear ImGui manages the viewport structures, and the backend create and maintain one Platform/OS window for each of those viewports. +// +// See Recap: https://github.com/ocornut/imgui/wiki/Multi-Viewports +// See Glossary https://github.com/ocornut/imgui/wiki/Glossary for details about some of the terminology. +// +// About the coordinates system: +// - When multi-viewports are enabled, all Dear ImGui coordinates become absolute coordinates (same as OS coordinates!) +// - So e.g. ImGui::SetNextWindowPos(ImVec2(0,0)) will position a window relative to your primary monitor! +// - If you want to position windows relative to your main application viewport, use ImGui::GetMainViewport()->Pos as a base position. +// +// Steps to use multi-viewports in your application, when using a default backend from the examples/ folder: +// - Application: Enable feature with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// - Backend: The backend initialization will setup all necessary ImGuiPlatformIO's functions and update monitors info every frame. +// - Application: In your main loop, call ImGui::UpdatePlatformWindows(), ImGui::RenderPlatformWindowsDefault() after EndFrame() or Render(). +// - Application: Fix absolute coordinates used in ImGui::SetWindowPos() or ImGui::SetNextWindowPos() calls. +// +// Steps to use multi-viewports in your application, when using a custom backend: +// - Important: THIS IS NOT EASY TO DO and comes with many subtleties not described here! +// It's also an experimental feature, so some of the requirements may evolve. +// Consider using default backends if you can. Either way, carefully follow and refer to examples/ backends for details. +// - Application: Enable feature with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// - Backend: Hook ImGuiPlatformIO's Platform_* and Renderer_* callbacks (see below). +// Set 'io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports' and 'io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports'. +// Update ImGuiPlatformIO's Monitors list every frame. +// Update MousePos every frame, in absolute coordinates. +// - Application: In your main loop, call ImGui::UpdatePlatformWindows(), ImGui::RenderPlatformWindowsDefault() after EndFrame() or Render(). +// You may skip calling RenderPlatformWindowsDefault() if its API is not convenient for your needs. Read comments below. +// - Application: Fix absolute coordinates used in ImGui::SetWindowPos() or ImGui::SetNextWindowPos() calls. +// +// About ImGui::RenderPlatformWindowsDefault(): +// - This function is a mostly a _helper_ for the common-most cases, and to facilitate using default backends. +// - You can check its simple source code to understand what it does. +// It basically iterates secondary viewports and call 4 functions that are setup in ImGuiPlatformIO, if available: +// Platform_RenderWindow(), Renderer_RenderWindow(), Platform_SwapBuffers(), Renderer_SwapBuffers() +// Those functions pointers exists only for the benefit of RenderPlatformWindowsDefault(). +// - If you have very specific rendering needs (e.g. flipping multiple swap-chain simultaneously, unusual sync/threading issues, etc.), +// you may be tempted to ignore RenderPlatformWindowsDefault() and write customized code to perform your renderingg. +// You may decide to setup the platform_io's *RenderWindow and *SwapBuffers pointers and call your functions through those pointers, +// or you may decide to never setup those pointers and call your code directly. They are a convenience, not an obligatory interface. +//----------------------------------------------------------------------------- + +// Access via ImGui::GetPlatformIO() +struct ImGuiPlatformIO +{ + IMGUI_API ImGuiPlatformIO(); + + //------------------------------------------------------------------ + // Input - Interface with OS and Platform backend (most common stuff) + //------------------------------------------------------------------ + + // Optional: Access OS clipboard + // (default to use native Win32 clipboard on Windows, otherwise uses a private clipboard. Override to access OS clipboard on other architectures) + const char* (*Platform_GetClipboardTextFn)(ImGuiContext* ctx); + void (*Platform_SetClipboardTextFn)(ImGuiContext* ctx, const char* text); + void* Platform_ClipboardUserData; + + // Optional: Open link/folder/file in OS Shell + // (default to use ShellExecuteW() on Windows, system() on Linux/Mac) + bool (*Platform_OpenInShellFn)(ImGuiContext* ctx, const char* path); + void* Platform_OpenInShellUserData; + + // Optional: Notify OS Input Method Editor of the screen position of your cursor for text input position (e.g. when using Japanese/Chinese IME on Windows) + // (default to use native imm32 api on Windows) + void (*Platform_SetImeDataFn)(ImGuiContext* ctx, ImGuiViewport* viewport, ImGuiPlatformImeData* data); + void* Platform_ImeUserData; + //void (*SetPlatformImeDataFn)(ImGuiViewport* viewport, ImGuiPlatformImeData* data); // [Renamed to platform_io.PlatformSetImeDataFn in 1.91.1] + + // Optional: Platform locale + // [Experimental] Configure decimal point e.g. '.' or ',' useful for some languages (e.g. German), generally pulled from *localeconv()->decimal_point + ImWchar Platform_LocaleDecimalPoint; // '.' + + //------------------------------------------------------------------ + // Input - Interface with Renderer Backend + //------------------------------------------------------------------ + + // Written by some backends during ImGui_ImplXXXX_RenderDrawData() call to point backend_specific ImGui_ImplXXXX_RenderState* structure. + void* Renderer_RenderState; + + //------------------------------------------------------------------ + // Input - Interface with Platform & Renderer backends for Multi-Viewport support + //------------------------------------------------------------------ + + // For reference, the second column shows which function are generally calling the Platform Functions: + // N = ImGui::NewFrame() ~ beginning of the dear imgui frame: read info from platform/OS windows (latest size/position) + // F = ImGui::Begin(), ImGui::EndFrame() ~ during the dear imgui frame + // U = ImGui::UpdatePlatformWindows() ~ after the dear imgui frame: create and update all platform/OS windows + // R = ImGui::RenderPlatformWindowsDefault() ~ render + // D = ImGui::DestroyPlatformWindows() ~ shutdown + // The general idea is that NewFrame() we will read the current Platform/OS state, and UpdatePlatformWindows() will write to it. + + // The handlers are designed so we can mix and match two imgui_impl_xxxx files, one Platform backend and one Renderer backend. + // Custom engine backends will often provide both Platform and Renderer interfaces together and so may not need to use all functions. + // Platform functions are typically called _before_ their Renderer counterpart, apart from Destroy which are called the other way. + + // Platform Backend functions (e.g. Win32, GLFW, SDL) ------------------- Called by ----- + void (*Platform_CreateWindow)(ImGuiViewport* vp); // . . U . . // Create a new platform window for the given viewport + void (*Platform_DestroyWindow)(ImGuiViewport* vp); // N . U . D // + void (*Platform_ShowWindow)(ImGuiViewport* vp); // . . U . . // Newly created windows are initially hidden so SetWindowPos/Size/Title can be called on them before showing the window + void (*Platform_SetWindowPos)(ImGuiViewport* vp, ImVec2 pos); // . . U . . // Set platform window position (given the upper-left corner of client area) + ImVec2 (*Platform_GetWindowPos)(ImGuiViewport* vp); // N . . . . // + void (*Platform_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); // . . U . . // Set platform window client area size (ignoring OS decorations such as OS title bar etc.) + ImVec2 (*Platform_GetWindowSize)(ImGuiViewport* vp); // N . . . . // Get platform window client area size + void (*Platform_SetWindowFocus)(ImGuiViewport* vp); // N . . . . // Move window to front and set input focus + bool (*Platform_GetWindowFocus)(ImGuiViewport* vp); // . . U . . // + bool (*Platform_GetWindowMinimized)(ImGuiViewport* vp); // N . . . . // Get platform window minimized state. When minimized, we generally won't attempt to get/set size and contents will be culled more easily + void (*Platform_SetWindowTitle)(ImGuiViewport* vp, const char* str); // . . U . . // Set platform window title (given an UTF-8 string) + void (*Platform_SetWindowAlpha)(ImGuiViewport* vp, float alpha); // . . U . . // (Optional) Setup global transparency (not per-pixel transparency) + void (*Platform_UpdateWindow)(ImGuiViewport* vp); // . . U . . // (Optional) Called by UpdatePlatformWindows(). Optional hook to allow the platform backend from doing general book-keeping every frame. + void (*Platform_RenderWindow)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Main rendering (platform side! This is often unused, or just setting a "current" context for OpenGL bindings). 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + void (*Platform_SwapBuffers)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Call Present/SwapBuffers (platform side! This is often unused!). 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + float (*Platform_GetWindowDpiScale)(ImGuiViewport* vp); // N . . . . // (Optional) [BETA] FIXME-DPI: DPI handling: Return DPI scale for this viewport. 1.0f = 96 DPI. + void (*Platform_OnChangedViewport)(ImGuiViewport* vp); // . F . . . // (Optional) [BETA] FIXME-DPI: DPI handling: Called during Begin() every time the viewport we are outputting into changes, so backend has a chance to swap fonts to adjust style. + ImVec4 (*Platform_GetWindowWorkAreaInsets)(ImGuiViewport* vp); // N . . . . // (Optional) [BETA] Get initial work area inset for the viewport (won't be covered by main menu bar, dockspace over viewport etc.). Default to (0,0),(0,0). 'safeAreaInsets' in iOS land, 'DisplayCutout' in Android land. + int (*Platform_CreateVkSurface)(ImGuiViewport* vp, ImU64 vk_inst, const void* vk_allocators, ImU64* out_vk_surface); // (Optional) For a Vulkan Renderer to call into Platform code (since the surface creation needs to tie them both). + + // Renderer Backend functions (e.g. DirectX, OpenGL, Vulkan) ------------ Called by ----- + void (*Renderer_CreateWindow)(ImGuiViewport* vp); // . . U . . // Create swap chain, frame buffers etc. (called after Platform_CreateWindow) + void (*Renderer_DestroyWindow)(ImGuiViewport* vp); // N . U . D // Destroy swap chain, frame buffers etc. (called before Platform_DestroyWindow) + void (*Renderer_SetWindowSize)(ImGuiViewport* vp, ImVec2 size); // . . U . . // Resize swap chain, frame buffers etc. (called after Platform_SetWindowSize) + void (*Renderer_RenderWindow)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Clear framebuffer, setup render target, then render the viewport->DrawData. 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + void (*Renderer_SwapBuffers)(ImGuiViewport* vp, void* render_arg); // . . . R . // (Optional) Call Present/SwapBuffers. 'render_arg' is the value passed to RenderPlatformWindowsDefault(). + + // (Optional) Monitor list + // - Updated by: app/backend. Update every frame to dynamically support changing monitor or DPI configuration. + // - Used by: dear imgui to query DPI info, clamp popups/tooltips within same monitor and not have them straddle monitors. + ImVector Monitors; + + //------------------------------------------------------------------ + // Output - List of viewports to render into platform windows + //------------------------------------------------------------------ + + // Viewports list (the list is updated by calling ImGui::EndFrame or ImGui::Render) + // (in the future we will attempt to organize this feature to remove the need for a "main viewport") + ImVector Viewports; // Main viewports, followed by all secondary viewports. +}; + +// (Optional) This is required when enabling multi-viewport. Represent the bounds of each connected monitor/display and their DPI. +// We use this information for multiple DPI support + clamping the position of popups and tooltips so they don't straddle multiple monitors. +struct ImGuiPlatformMonitor +{ + ImVec2 MainPos, MainSize; // Coordinates of the area displayed on this monitor (Min = upper left, Max = bottom right) + ImVec2 WorkPos, WorkSize; // Coordinates without task bars / side bars / menu bars. Used to avoid positioning popups/tooltips inside this region. If you don't have this info, please copy the value for MainPos/MainSize. + float DpiScale; // 1.0f = 96 DPI + void* PlatformHandle; // Backend dependant data (e.g. HMONITOR, GLFWmonitor*, SDL Display Index, NSScreen*) + ImGuiPlatformMonitor() { MainPos = MainSize = WorkPos = WorkSize = ImVec2(0, 0); DpiScale = 1.0f; PlatformHandle = NULL; } +}; + +// (Optional) Support for IME (Input Method Editor) via the platform_io.Platform_SetImeDataFn() function. +struct ImGuiPlatformImeData +{ + bool WantVisible; // A widget wants the IME to be visible + ImVec2 InputPos; // Position of the input cursor + float InputLineHeight; // Line height + + ImGuiPlatformImeData() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Obsolete functions and types +// (Will be removed! Read 'API BREAKING CHANGES' section in imgui.cpp for details) +// Please keep your copy of dear imgui up to date! Occasionally set '#define IMGUI_DISABLE_OBSOLETE_FUNCTIONS' in imconfig.h to stay ahead. +//----------------------------------------------------------------------------- #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - typedef ImFontGlyph Glyph; // OBSOLETED in 1.52+ +namespace ImGui +{ + // OBSOLETED in 1.91.9 (from February 2025) + IMGUI_API void Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col); // <-- border_col was removed in favor of ImGuiCol_ImageBorder. + // OBSOLETED in 1.91.0 (from July 2024) + static inline void PushButtonRepeat(bool repeat) { PushItemFlag(ImGuiItemFlags_ButtonRepeat, repeat); } + static inline void PopButtonRepeat() { PopItemFlag(); } + static inline void PushTabStop(bool tab_stop) { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); } + static inline void PopTabStop() { PopItemFlag(); } + IMGUI_API ImVec2 GetContentRegionMax(); // Content boundaries max (e.g. window boundaries including scrolling, or current column boundaries). You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + IMGUI_API ImVec2 GetWindowContentRegionMin(); // Content boundaries min for the window (roughly (0,0)-Scroll), in window-local coordinates. You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + IMGUI_API ImVec2 GetWindowContentRegionMax(); // Content boundaries max for the window (roughly (0,0)+Size-Scroll), in window-local coordinates. You should never need this. Always use GetCursorScreenPos() and GetContentRegionAvail()! + // OBSOLETED in 1.90.0 (from September 2023) + static inline bool BeginChildFrame(ImGuiID id, const ImVec2& size, ImGuiWindowFlags window_flags = 0) { return BeginChild(id, size, ImGuiChildFlags_FrameStyle, window_flags); } + static inline void EndChildFrame() { EndChild(); } + //static inline bool BeginChild(const char* str_id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags){ return BeginChild(str_id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Borders + //static inline bool BeginChild(ImGuiID id, const ImVec2& size_arg, bool borders, ImGuiWindowFlags window_flags) { return BeginChild(id, size_arg, borders ? ImGuiChildFlags_Borders : ImGuiChildFlags_None, window_flags); } // Unnecessary as true == ImGuiChildFlags_Borders + static inline void ShowStackToolWindow(bool* p_open = NULL) { ShowIDStackToolWindow(p_open); } + IMGUI_API bool Combo(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int popup_max_height_in_items = -1); + IMGUI_API bool ListBox(const char* label, int* current_item, bool (*old_callback)(void* user_data, int idx, const char** out_text), void* user_data, int items_count, int height_in_items = -1); + // OBSOLETED in 1.89.7 (from June 2023) + IMGUI_API void SetItemAllowOverlap(); // Use SetNextItemAllowOverlap() before item. + // OBSOLETED in 1.89.4 (from March 2023) + static inline void PushAllowKeyboardFocus(bool tab_stop) { PushItemFlag(ImGuiItemFlags_NoTabStop, !tab_stop); } + static inline void PopAllowKeyboardFocus() { PopItemFlag(); } + + // Some of the older obsolete names along with their replacement (commented out so they are not reported in IDE) + //-- OBSOLETED in 1.89 (from August 2022) + //IMGUI_API bool ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0 = ImVec2(0, 0), const ImVec2& uv1 = ImVec2(1, 1), int frame_padding = -1, const ImVec4& bg_col = ImVec4(0, 0, 0, 0), const ImVec4& tint_col = ImVec4(1, 1, 1, 1)); // --> Use new ImageButton() signature (explicit item id, regular FramePadding). Refer to code in 1.91 if you want to grab a copy of this version. + //-- OBSOLETED in 1.88 (from May 2022) + //static inline void CaptureKeyboardFromApp(bool want_capture_keyboard = true) { SetNextFrameWantCaptureKeyboard(want_capture_keyboard); } // Renamed as name was misleading + removed default value. + //static inline void CaptureMouseFromApp(bool want_capture_mouse = true) { SetNextFrameWantCaptureMouse(want_capture_mouse); } // Renamed as name was misleading + removed default value. + //-- OBSOLETED in 1.87 (from February 2022, more formally obsoleted April 2024) + //IMGUI_API ImGuiKey GetKeyIndex(ImGuiKey key); { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END); const ImGuiKeyData* key_data = GetKeyData(key); return (ImGuiKey)(key_data - g.IO.KeysData); } // Map ImGuiKey_* values into legacy native key index. == io.KeyMap[key]. When using a 1.87+ backend using io.AddKeyEvent(), calling GetKeyIndex() with ANY ImGuiKey_XXXX values will return the same value! + //static inline ImGuiKey GetKeyIndex(ImGuiKey key) { IM_ASSERT(key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END); return key; } + //-- OBSOLETED in 1.86 (from November 2021) + //IMGUI_API void CalcListClipping(int items_count, float items_height, int* out_items_display_start, int* out_items_display_end); // Code removed, see 1.90 for last version of the code. Calculate range of visible items for large list of evenly sized items. Prefer using ImGuiListClipper. + //-- OBSOLETED in 1.85 (from August 2021) + //static inline float GetWindowContentRegionWidth() { return GetWindowContentRegionMax().x - GetWindowContentRegionMin().x; } + //-- OBSOLETED in 1.81 (from February 2021) + //static inline bool ListBoxHeader(const char* label, const ImVec2& size = ImVec2(0, 0)) { return BeginListBox(label, size); } + //static inline bool ListBoxHeader(const char* label, int items_count, int height_in_items = -1) { float height = GetTextLineHeightWithSpacing() * ((height_in_items < 0 ? ImMin(items_count, 7) : height_in_items) + 0.25f) + GetStyle().FramePadding.y * 2.0f; return BeginListBox(label, ImVec2(0.0f, height)); } // Helper to calculate size from items_count and height_in_items + //static inline void ListBoxFooter() { EndListBox(); } + //-- OBSOLETED in 1.79 (from August 2020) + //static inline void OpenPopupContextItem(const char* str_id = NULL, ImGuiMouseButton mb = 1) { OpenPopupOnItemClick(str_id, mb); } // Bool return value removed. Use IsWindowAppearing() in BeginPopup() instead. Renamed in 1.77, renamed back in 1.79. Sorry! + //-- OBSOLETED in 1.78 (from June 2020): Old drag/sliders functions that took a 'float power > 1.0f' argument instead of ImGuiSliderFlags_Logarithmic. See github.com/ocornut/imgui/issues/3361 for details. + //IMGUI_API bool DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, float power = 1.0f) // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //IMGUI_API bool SliderScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_min, const void* p_max, const char* format, float power = 1.0f); // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, float power = 1.0f) { return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //static inline bool SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, float power = 1.0f) { return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, power); } // OBSOLETED in 1.78 (from June 2020) + //-- OBSOLETED in 1.77 and before + //static inline bool BeginPopupContextWindow(const char* str_id, ImGuiMouseButton mb, bool over_items) { return BeginPopupContextWindow(str_id, mb | (over_items ? 0 : ImGuiPopupFlags_NoOpenOverItems)); } // OBSOLETED in 1.77 (from June 2020) + //static inline void TreeAdvanceToLabelPos() { SetCursorPosX(GetCursorPosX() + GetTreeNodeToLabelSpacing()); } // OBSOLETED in 1.72 (from July 2019) + //static inline void SetNextTreeNodeOpen(bool open, ImGuiCond cond = 0) { SetNextItemOpen(open, cond); } // OBSOLETED in 1.71 (from June 2019) + //static inline float GetContentRegionAvailWidth() { return GetContentRegionAvail().x; } // OBSOLETED in 1.70 (from May 2019) + //static inline ImDrawList* GetOverlayDrawList() { return GetForegroundDrawList(); } // OBSOLETED in 1.69 (from Mar 2019) + //static inline void SetScrollHere(float ratio = 0.5f) { SetScrollHereY(ratio); } // OBSOLETED in 1.66 (from Nov 2018) + //static inline bool IsItemDeactivatedAfterChange() { return IsItemDeactivatedAfterEdit(); } // OBSOLETED in 1.63 (from Aug 2018) + //-- OBSOLETED in 1.60 and before + //static inline bool IsAnyWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_AnyWindow); } // OBSOLETED in 1.60 (from Apr 2018) + //static inline bool IsAnyWindowHovered() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.60 (between Dec 2017 and Apr 2018) + //static inline void ShowTestWindow() { return ShowDemoWindow(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootWindow); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline bool IsRootWindowOrAnyChildFocused() { return IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline void SetNextWindowContentWidth(float w) { SetNextWindowContentSize(ImVec2(w, 0.0f)); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //static inline float GetItemsLineHeightWithSpacing() { return GetFrameHeightWithSpacing(); } // OBSOLETED in 1.53 (between Oct 2017 and Dec 2017) + //IMGUI_API bool Begin(char* name, bool* p_open, ImVec2 size_first_use, float bg_alpha = -1.0f, ImGuiWindowFlags flags=0); // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017): Equivalent of using SetNextWindowSize(size, ImGuiCond_FirstUseEver) and SetNextWindowBgAlpha(). + //static inline bool IsRootWindowOrAnyChildHovered() { return IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline void AlignFirstTextHeightToWidgets() { AlignTextToFramePadding(); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline void SetNextWindowPosCenter(ImGuiCond c=0) { SetNextWindowPos(GetMainViewport()->GetCenter(), c, ImVec2(0.5f,0.5f)); } // OBSOLETED in 1.52 (between Aug 2017 and Oct 2017) + //static inline bool IsItemHoveredRect() { return IsItemHovered(ImGuiHoveredFlags_RectOnly); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //static inline bool IsPosHoveringAnyWindow(const ImVec2&) { IM_ASSERT(0); return false; } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017): This was misleading and partly broken. You probably want to use the io.WantCaptureMouse flag instead. + //static inline bool IsMouseHoveringAnyWindow() { return IsWindowHovered(ImGuiHoveredFlags_AnyWindow); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //static inline bool IsMouseHoveringWindow() { return IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem); } // OBSOLETED in 1.51 (between Jun 2017 and Aug 2017) + //-- OBSOLETED in 1.50 and before + //static inline bool CollapsingHeader(char* label, const char* str_id, bool framed = true, bool default_open = false) { return CollapsingHeader(label, (default_open ? (1 << 5) : 0)); } // OBSOLETED in 1.49 + //static inline ImFont*GetWindowFont() { return GetFont(); } // OBSOLETED in 1.48 + //static inline float GetWindowFontSize() { return GetFontSize(); } // OBSOLETED in 1.48 + //static inline void SetScrollPosHere() { SetScrollHere(); } // OBSOLETED in 1.42 +} + +//-- OBSOLETED in 1.82 (from Mars 2021): flags for AddRect(), AddRectFilled(), AddImageRounded(), PathRect() +//typedef ImDrawFlags ImDrawCornerFlags; +//enum ImDrawCornerFlags_ +//{ +// ImDrawCornerFlags_None = ImDrawFlags_RoundCornersNone, // Was == 0 prior to 1.82, this is now == ImDrawFlags_RoundCornersNone which is != 0 and not implicit +// ImDrawCornerFlags_TopLeft = ImDrawFlags_RoundCornersTopLeft, // Was == 0x01 (1 << 0) prior to 1.82. Order matches ImDrawFlags_NoRoundCorner* flag (we exploit this internally). +// ImDrawCornerFlags_TopRight = ImDrawFlags_RoundCornersTopRight, // Was == 0x02 (1 << 1) prior to 1.82. +// ImDrawCornerFlags_BotLeft = ImDrawFlags_RoundCornersBottomLeft, // Was == 0x04 (1 << 2) prior to 1.82. +// ImDrawCornerFlags_BotRight = ImDrawFlags_RoundCornersBottomRight, // Was == 0x08 (1 << 3) prior to 1.82. +// ImDrawCornerFlags_All = ImDrawFlags_RoundCornersAll, // Was == 0x0F prior to 1.82 +// ImDrawCornerFlags_Top = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_TopRight, +// ImDrawCornerFlags_Bot = ImDrawCornerFlags_BotLeft | ImDrawCornerFlags_BotRight, +// ImDrawCornerFlags_Left = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotLeft, +// ImDrawCornerFlags_Right = ImDrawCornerFlags_TopRight | ImDrawCornerFlags_BotRight, +//}; + +// RENAMED and MERGED both ImGuiKey_ModXXX and ImGuiModFlags_XXX into ImGuiMod_XXX (from September 2022) +// RENAMED ImGuiKeyModFlags -> ImGuiModFlags in 1.88 (from April 2022). Exceptionally commented out ahead of obscolescence schedule to reduce confusion and because they were not meant to be used in the first place. +//typedef ImGuiKeyChord ImGuiModFlags; // == int. We generally use ImGuiKeyChord to mean "a ImGuiKey or-ed with any number of ImGuiMod_XXX value", so you may store mods in there. +//enum ImGuiModFlags_ { ImGuiModFlags_None = 0, ImGuiModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiModFlags_Shift = ImGuiMod_Shift, ImGuiModFlags_Alt = ImGuiMod_Alt, ImGuiModFlags_Super = ImGuiMod_Super }; +//typedef ImGuiKeyChord ImGuiKeyModFlags; // == int +//enum ImGuiKeyModFlags_ { ImGuiKeyModFlags_None = 0, ImGuiKeyModFlags_Ctrl = ImGuiMod_Ctrl, ImGuiKeyModFlags_Shift = ImGuiMod_Shift, ImGuiKeyModFlags_Alt = ImGuiMod_Alt, ImGuiKeyModFlags_Super = ImGuiMod_Super }; + +#define IM_OFFSETOF(_TYPE,_MEMBER) offsetof(_TYPE, _MEMBER) // OBSOLETED IN 1.90 (now using C++11 standard version) + +#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +// RENAMED IMGUI_DISABLE_METRICS_WINDOW > IMGUI_DISABLE_DEBUG_TOOLS in 1.88 (from June 2022) +#ifdef IMGUI_DISABLE_METRICS_WINDOW +#error IMGUI_DISABLE_METRICS_WINDOW was renamed to IMGUI_DISABLE_DEBUG_TOOLS, please use new name. #endif -}; + +//----------------------------------------------------------------------------- #if defined(__clang__) #pragma clang diagnostic pop @@ -2240,7 +4012,18 @@ struct ImFont #pragma GCC diagnostic pop #endif -// Include imgui_user.h at the end of imgui.h (convenient for user to only explicitly include vanilla imgui.h) +#ifdef _MSC_VER +#pragma warning (pop) +#endif + +// Include imgui_user.h at the end of imgui.h +// May be convenient for some users to only explicitly include vanilla imgui.h and have extra stuff included. #ifdef IMGUI_INCLUDE_IMGUI_USER_H +#ifdef IMGUI_USER_H_FILENAME +#include IMGUI_USER_H_FILENAME +#else #include "imgui_user.h" #endif +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_demo.cpp b/imgui/imgui_demo.cpp index b8576e58..cea9a8a2 100644 --- a/imgui/imgui_demo.cpp +++ b/imgui/imgui_demo.cpp @@ -1,43 +1,110 @@ -// dear imgui, v1.71 +// dear imgui, v1.91.9b // (demo code) -// Message to the person tempted to delete this file when integrating Dear ImGui into their code base: -// Do NOT remove this file from your project! Think again! It is the most useful reference code that you and other coders -// will want to refer to and call. Have the ImGui::ShowDemoWindow() function wired in an always-available debug menu of -// your game/app! Removing this file from your project is hindering access to documentation for everyone in your team, +// Help: +// - Read FAQ at http://dearimgui.com/faq +// - Call and read ImGui::ShowDemoWindow() in imgui_demo.cpp. All applications in examples/ are doing that. +// - Need help integrating Dear ImGui in your codebase? +// - Read Getting Started https://github.com/ocornut/imgui/wiki/Getting-Started +// - Read 'Programmer guide' in imgui.cpp for notes on how to setup Dear ImGui in your codebase. +// Read top of imgui.cpp and imgui.h for many details, documentation, comments, links. +// Get the latest version at https://github.com/ocornut/imgui + +// How to easily locate code? +// - Use Tools->Item Picker to debug break in code by clicking any widgets: https://github.com/ocornut/imgui/wiki/Debug-Tools +// - Browse an online version the demo with code linked to hovered widgets: https://pthom.github.io/imgui_manual_online/manual/imgui_manual.html +// - Find a visible string and search for it in the code! + +//--------------------------------------------------- +// PLEASE DO NOT REMOVE THIS FILE FROM YOUR PROJECT! +//--------------------------------------------------- +// Message to the person tempted to delete this file when integrating Dear ImGui into their codebase: +// Think again! It is the most useful reference code that you and other coders will want to refer to and call. +// Have the ImGui::ShowDemoWindow() function wired in an always-available debug menu of your game/app! +// Also include Metrics! ItemPicker! DebugLog! and other debug features. +// Removing this file from your project is hindering access to documentation for everyone in your team, // likely leading you to poorer usage of the library. // Everything in this file will be stripped out by the linker if you don't call ImGui::ShowDemoWindow(). -// If you want to link core Dear ImGui in your shipped builds but want an easy guarantee that the demo will not be linked, -// you can setup your imconfig.h with #define IMGUI_DISABLE_DEMO_WINDOWS and those functions will be empty. -// In other situation, whenever you have Dear ImGui available you probably want this to be available for reference. +// If you want to link core Dear ImGui in your shipped builds but want a thorough guarantee that the demo will not be +// linked, you can setup your imconfig.h with #define IMGUI_DISABLE_DEMO_WINDOWS and those functions will be empty. +// In another situation, whenever you have Dear ImGui available you probably want this to be available for reference. // Thank you, -// -Your beloved friend, imgui_demo.cpp (that you won't delete) +// -Your beloved friend, imgui_demo.cpp (which you won't delete) -// Message to beginner C/C++ programmers about the meaning of the 'static' keyword: -// In this demo code, we frequently we use 'static' variables inside functions. A static variable persist across calls, so it is -// essentially like a global variable but declared inside the scope of the function. We do this as a way to gather code and data -// in the same place, to make the demo source code faster to read, faster to write, and smaller in size. -// It also happens to be a convenient way of storing simple UI related information as long as your function doesn't need to be -// reentrant or used in multiple threads. This might be a pattern you will want to use in your code, but most of the real data -// you would be editing is likely going to be stored outside your functions. +//-------------------------------------------- +// ABOUT THE MEANING OF THE 'static' KEYWORD: +//-------------------------------------------- +// In this demo code, we frequently use 'static' variables inside functions. +// A static variable persists across calls. It is essentially a global variable but declared inside the scope of the function. +// Think of "static int n = 0;" as "global int n = 0;" ! +// We do this IN THE DEMO because we want: +// - to gather code and data in the same place. +// - to make the demo source code faster to read, faster to change, smaller in size. +// - it is also a convenient way of storing simple UI related information as long as your function +// doesn't need to be reentrant or used in multiple threads. +// This might be a pattern you will want to use in your code, but most of the data you would be working +// with in a complex codebase is likely going to be stored outside your functions. -// The Demo code is this file is designed to be easy to copy-and-paste in into your application! +//----------------------------------------- +// ABOUT THE CODING STYLE OF OUR DEMO CODE +//----------------------------------------- +// The Demo code in this file is designed to be easy to copy-and-paste into your application! // Because of this: -// - We never omit the ImGui:: namespace when calling functions, even though most of our code is already in the same namespace. +// - We never omit the ImGui:: prefix when calling functions, even though most code here is in the same namespace. // - We try to declare static variables in the local scope, as close as possible to the code using them. -// - We never use any of the helpers/facilities used internally by dear imgui, unless it has been exposed in the public API (imgui.h). -// - We never use maths operators on ImVec2/ImVec4. For other imgui sources files, they are provided by imgui_internal.h w/ IMGUI_DEFINE_MATH_OPERATORS, -// for your own sources file they are optional and require you either enable those, either provide your own via IM_VEC2_CLASS_EXTRA in imconfig.h. -// Because we don't want to assume anything about your support of maths operators, we don't use them in imgui_demo.cpp. +// - We never use any of the helpers/facilities used internally by Dear ImGui, unless available in the public API. +// - We never use maths operators on ImVec2/ImVec4. For our other sources files we use them, and they are provided +// by imgui.h using the IMGUI_DEFINE_MATH_OPERATORS define. For your own sources file they are optional +// and require you either enable those, either provide your own via IM_VEC2_CLASS_EXTRA in imconfig.h. +// Because we can't assume anything about your support of maths operators, we cannot use them in imgui_demo.cpp. + +// Navigating this file: +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. +// - You can search/grep for all sections listed in the index to find the section. /* Index of this file: -// [SECTION] Forward Declarations, Helpers +// [SECTION] Forward Declarations +// [SECTION] Helpers // [SECTION] Demo Window / ShowDemoWindow() +// [SECTION] DemoWindowMenuBar() +// [SECTION] Helpers: ExampleTreeNode, ExampleMemberInfo (for use by Property Editor & Multi-Select demos) +// [SECTION] DemoWindowWidgetsBasic() +// [SECTION] DemoWindowWidgetsBullets() +// [SECTION] DemoWindowWidgetsCollapsingHeaders() +// [SECTION] DemoWindowWidgetsComboBoxes() +// [SECTION] DemoWindowWidgetsColorAndPickers() +// [SECTION] DemoWindowWidgetsDataTypes() +// [SECTION] DemoWindowWidgetsDisableBlocks() +// [SECTION] DemoWindowWidgetsDragAndDrop() +// [SECTION] DemoWindowWidgetsDragsAndSliders() +// [SECTION] DemoWindowWidgetsImages() +// [SECTION] DemoWindowWidgetsListBoxes() +// [SECTION] DemoWindowWidgetsMultiComponents() +// [SECTION] DemoWindowWidgetsPlotting() +// [SECTION] DemoWindowWidgetsProgressBars() +// [SECTION] DemoWindowWidgetsQueryingStatuses() +// [SECTION] DemoWindowWidgetsSelectables() +// [SECTION] DemoWindowWidgetsSelectionAndMultiSelect() +// [SECTION] DemoWindowWidgetsTabs() +// [SECTION] DemoWindowWidgetsText() +// [SECTION] DemoWindowWidgetsTextFilter() +// [SECTION] DemoWindowWidgetsTextInput() +// [SECTION] DemoWindowWidgetsTooltips() +// [SECTION] DemoWindowWidgetsTreeNodes() +// [SECTION] DemoWindowWidgetsVerticalSliders() +// [SECTION] DemoWindowWidgets() +// [SECTION] DemoWindowLayout() +// [SECTION] DemoWindowPopups() +// [SECTION] DemoWindowTables() +// [SECTION] DemoWindowInputs() // [SECTION] About Window / ShowAboutWindow() // [SECTION] Style Editor / ShowStyleEditor() +// [SECTION] User Guide / ShowUserGuide() // [SECTION] Example App: Main Menu Bar / ShowExampleAppMainMenuBar() // [SECTION] Example App: Debug Console / ShowExampleAppConsole() // [SECTION] Example App: Debug Log / ShowExampleAppLog() @@ -46,10 +113,13 @@ Index of this file: // [SECTION] Example App: Long Text / ShowExampleAppLongText() // [SECTION] Example App: Auto Resize / ShowExampleAppAutoResize() // [SECTION] Example App: Constrained Resize / ShowExampleAppConstrainedResize() -// [SECTION] Example App: Simple Overlay / ShowExampleAppSimpleOverlay() -// [SECTION] Example App: Manipulating Window Titles / ShowExampleAppWindowTitles() +// [SECTION] Example App: Simple overlay / ShowExampleAppSimpleOverlay() +// [SECTION] Example App: Fullscreen window / ShowExampleAppFullscreen() +// [SECTION] Example App: Manipulating window titles / ShowExampleAppWindowTitles() // [SECTION] Example App: Custom Rendering using ImDrawList API / ShowExampleAppCustomRendering() +// [SECTION] Example App: Docking, DockSpace / ShowExampleAppDockSpace() // [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() +// [SECTION] Example App: Assets Browser / ShowExampleAppAssetsBrowser() */ @@ -58,90 +128,154 @@ Index of this file: #endif #include "imgui.h" +#ifndef IMGUI_DISABLE + +// System includes #include // toupper #include // INT_MIN, INT_MAX #include // sqrtf, powf, cosf, sinf, floorf, ceilf #include // vsnprintf, sscanf, printf #include // NULL, malloc, free, atoi -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -#include // intptr_t -#else #include // intptr_t +#if !defined(_MSC_VER) || _MSC_VER >= 1800 +#include // PRId64/PRIu64, not avail in some MinGW headers. +#endif +#ifdef __EMSCRIPTEN__ +#include // __EMSCRIPTEN_major__ etc. #endif +// Visual Studio warnings #ifdef _MSC_VER -#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen -#endif -#if defined(__clang__) -#pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. -#pragma clang diagnostic ignored "-Wdeprecated-declarations" // warning : 'xx' is deprecated: The POSIX name for this item.. // for strdup used in demo code (so user can copy & paste the code) -#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning : cast to 'void *' from smaller integer type 'int' -#pragma clang diagnostic ignored "-Wformat-security" // warning : warning: format string is not a string literal -#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning : declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. -#pragma clang diagnostic ignored "-Wunused-macros" // warning : warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used. -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning : zero as null pointer constant // some standard header variations use #define NULL 0 -#endif -#if __has_warning("-Wdouble-promotion") -#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. -#endif -#if __has_warning("-Wreserved-id-macro") -#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning : macro name is a reserved identifier // -#endif -#elif defined(__GNUC__) -#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size -#pragma GCC diagnostic ignored "-Wformat-security" // warning : format string is not a string literal (potentially insecure) -#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function -#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value -#if (__GNUC__ >= 6) -#pragma GCC diagnostic ignored "-Wmisleading-indentation" // warning: this 'if' clause does not guard this statement // GCC 6.0+ only. See #883 on GitHub. -#endif +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to an 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). #endif -// Play it nice with Windows users. Notepad in 2017 still doesn't display text data with Unix-style \n. +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wdeprecated-declarations" // warning: 'xx' is deprecated: The POSIX name for this.. // for strdup used in demo code (so user can copy & paste the code) +#pragma clang diagnostic ignored "-Wint-to-void-pointer-cast" // warning: cast to 'void *' from smaller integer type +#pragma clang diagnostic ignored "-Wformat" // warning: format specifies type 'int' but the argument has type 'unsigned int' +#pragma clang diagnostic ignored "-Wformat-security" // warning: format string is not a string literal +#pragma clang diagnostic ignored "-Wexit-time-destructors" // warning: declaration requires an exit-time destructor // exit-time destruction order is undefined. if MemFree() leads to users code that has been disabled before exit it might cause problems. ImGui coding style welcomes static/globals. +#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used. +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning: macro name is a reserved identifier +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wint-to-pointer-cast" // warning: cast to pointer from integer of different size +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wformat-security" // warning: format string is not a string literal (potentially insecure) +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wmisleading-indentation" // [__GNUC__ >= 6] warning: this 'if' clause does not guard this statement // GCC 6.0+ only. See #883 on GitHub. +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1 +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers +#endif + +// Play it nice with Windows users (Update: May 2018, Notepad now supports Unix-style carriage returns!) #ifdef _WIN32 #define IM_NEWLINE "\r\n" -#define snprintf _snprintf -#define vsnprintf _vsnprintf #else #define IM_NEWLINE "\n" #endif -#define IM_MAX(_A,_B) (((_A) >= (_B)) ? (_A) : (_B)) - -//----------------------------------------------------------------------------- -// [SECTION] Forward Declarations, Helpers -//----------------------------------------------------------------------------- - -#if !defined(IMGUI_DISABLE_OBSOLETE_FUNCTIONS) && defined(IMGUI_DISABLE_TEST_WINDOWS) && !defined(IMGUI_DISABLE_DEMO_WINDOWS) // Obsolete name since 1.53, TEST->DEMO -#define IMGUI_DISABLE_DEMO_WINDOWS +// Helpers +#if defined(_MSC_VER) && !defined(snprintf) +#define snprintf _snprintf #endif +#if defined(_MSC_VER) && !defined(vsnprintf) +#define vsnprintf _vsnprintf +#endif + +// Format specifiers for 64-bit values (hasn't been decently standardized before VS2013) +#if !defined(PRId64) && defined(_MSC_VER) +#define PRId64 "I64d" +#define PRIu64 "I64u" +#elif !defined(PRId64) +#define PRId64 "lld" +#define PRIu64 "llu" +#endif + +// Helpers macros +// We normally try to not use many helpers in imgui_demo.cpp in order to make code easier to copy and paste, +// but making an exception here as those are largely simplifying code... +// In other imgui sources we can use nicer internal functions from imgui_internal.h (ImMin/ImMax) but not in the demo. +#define IM_MIN(A, B) (((A) < (B)) ? (A) : (B)) +#define IM_MAX(A, B) (((A) >= (B)) ? (A) : (B)) +#define IM_CLAMP(V, MN, MX) ((V) < (MN) ? (MN) : (V) > (MX) ? (MX) : (V)) + +// Enforce cdecl calling convention for functions called by the standard library, +// in case compilation settings changed the default to e.g. __vectorcall +#ifndef IMGUI_CDECL +#ifdef _MSC_VER +#define IMGUI_CDECL __cdecl +#else +#define IMGUI_CDECL +#endif +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Forward Declarations +//----------------------------------------------------------------------------- #if !defined(IMGUI_DISABLE_DEMO_WINDOWS) // Forward Declarations -static void ShowExampleAppDocuments(bool* p_open); +struct ImGuiDemoWindowData; static void ShowExampleAppMainMenuBar(); +static void ShowExampleAppAssetsBrowser(bool* p_open); static void ShowExampleAppConsole(bool* p_open); +static void ShowExampleAppCustomRendering(bool* p_open); +static void ShowExampleAppDockSpace(bool* p_open); +static void ShowExampleAppDocuments(bool* p_open); static void ShowExampleAppLog(bool* p_open); static void ShowExampleAppLayout(bool* p_open); -static void ShowExampleAppPropertyEditor(bool* p_open); -static void ShowExampleAppLongText(bool* p_open); +static void ShowExampleAppPropertyEditor(bool* p_open, ImGuiDemoWindowData* demo_data); +static void ShowExampleAppSimpleOverlay(bool* p_open); static void ShowExampleAppAutoResize(bool* p_open); static void ShowExampleAppConstrainedResize(bool* p_open); -static void ShowExampleAppSimpleOverlay(bool* p_open); +static void ShowExampleAppFullscreen(bool* p_open); +static void ShowExampleAppLongText(bool* p_open); static void ShowExampleAppWindowTitles(bool* p_open); -static void ShowExampleAppCustomRendering(bool* p_open); static void ShowExampleMenuFile(); +// We split the contents of the big ShowDemoWindow() function into smaller functions +// (because the link time of very large functions tends to grow non-linearly) +static void DemoWindowMenuBar(ImGuiDemoWindowData* demo_data); +static void DemoWindowWidgets(ImGuiDemoWindowData* demo_data); +static void DemoWindowLayout(); +static void DemoWindowPopups(); +static void DemoWindowTables(); +static void DemoWindowColumns(); +static void DemoWindowInputs(); + +// Helper tree functions used by Property Editor & Multi-Select demos +struct ExampleTreeNode; +static ExampleTreeNode* ExampleTree_CreateNode(const char* name, int uid, ExampleTreeNode* parent); +static void ExampleTree_DestroyNode(ExampleTreeNode* node); + +//----------------------------------------------------------------------------- +// [SECTION] Helpers +//----------------------------------------------------------------------------- + // Helper to display a little (?) mark which shows a tooltip when hovered. -// In your own code you may want to display an actual icon if you are using a merged icon fonts (see misc/fonts/README.txt) +// In your own code you may want to display an actual icon if you are using a merged icon fonts (see docs/FONTS.md) static void HelpMarker(const char* desc) { ImGui::TextDisabled("(?)"); - if (ImGui::IsItemHovered()) + if (ImGui::BeginItemTooltip()) { - ImGui::BeginTooltip(); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); ImGui::TextUnformatted(desc); ImGui::PopTextWrapPos(); @@ -149,82 +283,105 @@ static void HelpMarker(const char* desc) } } -// Helper to display basic user controls. -void ImGui::ShowUserGuide() +static void ShowDockingDisabledMessage() { ImGuiIO& io = ImGui::GetIO(); - ImGui::BulletText("Double-click on title bar to collapse window."); - ImGui::BulletText("Click and drag on lower right corner to resize window\n(double-click to auto fit window to its contents)."); - ImGui::BulletText("Click and drag on any empty space to move window."); - ImGui::BulletText("TAB/SHIFT+TAB to cycle through keyboard editable fields."); - ImGui::BulletText("CTRL+Click on a slider or drag box to input value as text."); - if (io.FontAllowUserScaling) - ImGui::BulletText("CTRL+Mouse Wheel to zoom window contents."); - ImGui::BulletText("Mouse Wheel to scroll."); - ImGui::BulletText("While editing text:\n"); - ImGui::Indent(); - ImGui::BulletText("Hold SHIFT or use mouse to select text."); - ImGui::BulletText("CTRL+Left/Right to word jump."); - ImGui::BulletText("CTRL+A or double-click to select all."); - ImGui::BulletText("CTRL+X,CTRL+C,CTRL+V to use clipboard."); - ImGui::BulletText("CTRL+Z,CTRL+Y to undo/redo."); - ImGui::BulletText("ESCAPE to revert."); - ImGui::BulletText("You can apply arithmetic operators +,*,/ on numerical values.\nUse +- to subtract."); - ImGui::Unindent(); + ImGui::Text("ERROR: Docking is not enabled! See Demo > Configuration."); + ImGui::Text("Set io.ConfigFlags |= ImGuiConfigFlags_DockingEnable in your code, or "); + ImGui::SameLine(0.0f, 0.0f); + if (ImGui::SmallButton("click here")) + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; } +// Helper to wire demo markers located in code to an interactive browser +typedef void (*ImGuiDemoMarkerCallback)(const char* file, int line, const char* section, void* user_data); +extern ImGuiDemoMarkerCallback GImGuiDemoMarkerCallback; +extern void* GImGuiDemoMarkerCallbackUserData; +ImGuiDemoMarkerCallback GImGuiDemoMarkerCallback = NULL; +void* GImGuiDemoMarkerCallbackUserData = NULL; +#define IMGUI_DEMO_MARKER(section) do { if (GImGuiDemoMarkerCallback != NULL) GImGuiDemoMarkerCallback(__FILE__, __LINE__, section, GImGuiDemoMarkerCallbackUserData); } while (0) + //----------------------------------------------------------------------------- // [SECTION] Demo Window / ShowDemoWindow() //----------------------------------------------------------------------------- -// We split the contents of the big ShowDemoWindow() function into smaller functions (because the link time of very large functions grow non-linearly) -static void ShowDemoWindowWidgets(); -static void ShowDemoWindowLayout(); -static void ShowDemoWindowPopups(); -static void ShowDemoWindowColumns(); -static void ShowDemoWindowMisc(); +// Data to be shared across different functions of the demo. +struct ImGuiDemoWindowData +{ + // Examples Apps (accessible from the "Examples" menu) + bool ShowMainMenuBar = false; + bool ShowAppAssetsBrowser = false; + bool ShowAppConsole = false; + bool ShowAppCustomRendering = false; + bool ShowAppDocuments = false; + bool ShowAppDockSpace = false; + bool ShowAppLog = false; + bool ShowAppLayout = false; + bool ShowAppPropertyEditor = false; + bool ShowAppSimpleOverlay = false; + bool ShowAppAutoResize = false; + bool ShowAppConstrainedResize = false; + bool ShowAppFullscreen = false; + bool ShowAppLongText = false; + bool ShowAppWindowTitles = false; + + // Dear ImGui Tools (accessible from the "Tools" menu) + bool ShowMetrics = false; + bool ShowDebugLog = false; + bool ShowIDStackTool = false; + bool ShowStyleEditor = false; + bool ShowAbout = false; + + // Other data + bool DisableSections = false; + ExampleTreeNode* DemoTree = NULL; + + ~ImGuiDemoWindowData() { if (DemoTree) ExampleTree_DestroyNode(DemoTree); } +}; // Demonstrate most Dear ImGui features (this is big function!) -// You may execute this function to experiment with the UI and understand what it does. You may then search for keywords in the code when you are interested by a specific feature. +// You may execute this function to experiment with the UI and understand what it does. +// You may then search for keywords in the code when you are interested by a specific feature. void ImGui::ShowDemoWindow(bool* p_open) { - IM_ASSERT(ImGui::GetCurrentContext() != NULL && "Missing dear imgui context. Refer to examples app!"); // Exceptionally add an extra assert here for people confused with initial dear imgui setup + // Exceptionally add an extra assert here for people confused about initial Dear ImGui setup + // Most functions would normally just assert/crash if the context is missing. + IM_ASSERT(ImGui::GetCurrentContext() != NULL && "Missing Dear ImGui context. Refer to examples app!"); + + // Verify ABI compatibility between caller code and compiled version of Dear ImGui. This helps detects some build issues. + IMGUI_CHECKVERSION(); + + // Stored data + static ImGuiDemoWindowData demo_data; // Examples Apps (accessible from the "Examples" menu) - static bool show_app_documents = false; - static bool show_app_main_menu_bar = false; - static bool show_app_console = false; - static bool show_app_log = false; - static bool show_app_layout = false; - static bool show_app_property_editor = false; - static bool show_app_long_text = false; - static bool show_app_auto_resize = false; - static bool show_app_constrained_resize = false; - static bool show_app_simple_overlay = false; - static bool show_app_window_titles = false; - static bool show_app_custom_rendering = false; + if (demo_data.ShowMainMenuBar) { ShowExampleAppMainMenuBar(); } + if (demo_data.ShowAppDockSpace) { ShowExampleAppDockSpace(&demo_data.ShowAppDockSpace); } // Important: Process the Docking app first, as explicit DockSpace() nodes needs to be submitted early (read comments near the DockSpace function) + if (demo_data.ShowAppDocuments) { ShowExampleAppDocuments(&demo_data.ShowAppDocuments); } // ...process the Document app next, as it may also use a DockSpace() + if (demo_data.ShowAppAssetsBrowser) { ShowExampleAppAssetsBrowser(&demo_data.ShowAppAssetsBrowser); } + if (demo_data.ShowAppConsole) { ShowExampleAppConsole(&demo_data.ShowAppConsole); } + if (demo_data.ShowAppCustomRendering) { ShowExampleAppCustomRendering(&demo_data.ShowAppCustomRendering); } + if (demo_data.ShowAppLog) { ShowExampleAppLog(&demo_data.ShowAppLog); } + if (demo_data.ShowAppLayout) { ShowExampleAppLayout(&demo_data.ShowAppLayout); } + if (demo_data.ShowAppPropertyEditor) { ShowExampleAppPropertyEditor(&demo_data.ShowAppPropertyEditor, &demo_data); } + if (demo_data.ShowAppSimpleOverlay) { ShowExampleAppSimpleOverlay(&demo_data.ShowAppSimpleOverlay); } + if (demo_data.ShowAppAutoResize) { ShowExampleAppAutoResize(&demo_data.ShowAppAutoResize); } + if (demo_data.ShowAppConstrainedResize) { ShowExampleAppConstrainedResize(&demo_data.ShowAppConstrainedResize); } + if (demo_data.ShowAppFullscreen) { ShowExampleAppFullscreen(&demo_data.ShowAppFullscreen); } + if (demo_data.ShowAppLongText) { ShowExampleAppLongText(&demo_data.ShowAppLongText); } + if (demo_data.ShowAppWindowTitles) { ShowExampleAppWindowTitles(&demo_data.ShowAppWindowTitles); } - if (show_app_documents) ShowExampleAppDocuments(&show_app_documents); - if (show_app_main_menu_bar) ShowExampleAppMainMenuBar(); - if (show_app_console) ShowExampleAppConsole(&show_app_console); - if (show_app_log) ShowExampleAppLog(&show_app_log); - if (show_app_layout) ShowExampleAppLayout(&show_app_layout); - if (show_app_property_editor) ShowExampleAppPropertyEditor(&show_app_property_editor); - if (show_app_long_text) ShowExampleAppLongText(&show_app_long_text); - if (show_app_auto_resize) ShowExampleAppAutoResize(&show_app_auto_resize); - if (show_app_constrained_resize) ShowExampleAppConstrainedResize(&show_app_constrained_resize); - if (show_app_simple_overlay) ShowExampleAppSimpleOverlay(&show_app_simple_overlay); - if (show_app_window_titles) ShowExampleAppWindowTitles(&show_app_window_titles); - if (show_app_custom_rendering) ShowExampleAppCustomRendering(&show_app_custom_rendering); - - // Dear ImGui Apps (accessible from the "Help" menu) - static bool show_app_metrics = false; - static bool show_app_style_editor = false; - static bool show_app_about = false; - - if (show_app_metrics) { ImGui::ShowMetricsWindow(&show_app_metrics); } - if (show_app_style_editor) { ImGui::Begin("Style Editor", &show_app_style_editor); ImGui::ShowStyleEditor(); ImGui::End(); } - if (show_app_about) { ImGui::ShowAboutWindow(&show_app_about); } + // Dear ImGui Tools (accessible from the "Tools" menu) + if (demo_data.ShowMetrics) { ImGui::ShowMetricsWindow(&demo_data.ShowMetrics); } + if (demo_data.ShowDebugLog) { ImGui::ShowDebugLogWindow(&demo_data.ShowDebugLog); } + if (demo_data.ShowIDStackTool) { ImGui::ShowIDStackToolWindow(&demo_data.ShowIDStackTool); } + if (demo_data.ShowAbout) { ImGui::ShowAboutWindow(&demo_data.ShowAbout); } + if (demo_data.ShowStyleEditor) + { + ImGui::Begin("Dear ImGui Style Editor", &demo_data.ShowStyleEditor); + ImGui::ShowStyleEditor(); + ImGui::End(); + } // Demonstrate the various window flags. Typically you would just use the default! static bool no_titlebar = false; @@ -237,6 +394,8 @@ void ImGui::ShowDemoWindow(bool* p_open) static bool no_nav = false; static bool no_background = false; static bool no_bring_to_front = false; + static bool no_docking = false; + static bool unsaved_document = false; ImGuiWindowFlags window_flags = 0; if (no_titlebar) window_flags |= ImGuiWindowFlags_NoTitleBar; @@ -248,10 +407,14 @@ void ImGui::ShowDemoWindow(bool* p_open) if (no_nav) window_flags |= ImGuiWindowFlags_NoNav; if (no_background) window_flags |= ImGuiWindowFlags_NoBackground; if (no_bring_to_front) window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus; + if (no_docking) window_flags |= ImGuiWindowFlags_NoDocking; + if (unsaved_document) window_flags |= ImGuiWindowFlags_UnsavedDocument; if (no_close) p_open = NULL; // Don't pass our bool* to Begin - // We specify a default position/size in case there's no data in the .ini file. Typically this isn't required! We only do it to make the Demo applications a little more welcoming. - ImGui::SetNextWindowPos(ImVec2(650, 20), ImGuiCond_FirstUseEver); + // We specify a default position/size in case there's no data in the .ini file. + // We only do it to make the demo applications a little more welcoming, but typically this isn't required. + const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x + 650, main_viewport->WorkPos.y + 20), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(550, 680), ImGuiCond_FirstUseEver); // Main body of the Demo window starts here. @@ -262,121 +425,230 @@ void ImGui::ShowDemoWindow(bool* p_open) return; } - // Most "big" widgets share a common width settings by default. - //ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.65f); // Use 2/3 of the space for widgets and 1/3 for labels (default) - ImGui::PushItemWidth(ImGui::GetFontSize() * -12); // Use fixed width for labels (by passing a negative value), the rest goes to widgets. We choose a width proportional to our font size. + // Most "big" widgets share a common width settings by default. See 'Demo->Layout->Widgets Width' for details. + ImGui::PushItemWidth(ImGui::GetFontSize() * -12); // e.g. Leave a fixed amount of width for labels (by passing a negative value), the rest goes to widgets. + //ImGui::PushItemWidth(-ImGui::GetWindowWidth() * 0.35f); // e.g. Use 2/3 of the space for widgets and 1/3 for labels (right align) // Menu Bar - if (ImGui::BeginMenuBar()) - { - if (ImGui::BeginMenu("Menu")) - { - ShowExampleMenuFile(); - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Examples")) - { - ImGui::MenuItem("Main menu bar", NULL, &show_app_main_menu_bar); - ImGui::MenuItem("Console", NULL, &show_app_console); - ImGui::MenuItem("Log", NULL, &show_app_log); - ImGui::MenuItem("Simple layout", NULL, &show_app_layout); - ImGui::MenuItem("Property editor", NULL, &show_app_property_editor); - ImGui::MenuItem("Long text display", NULL, &show_app_long_text); - ImGui::MenuItem("Auto-resizing window", NULL, &show_app_auto_resize); - ImGui::MenuItem("Constrained-resizing window", NULL, &show_app_constrained_resize); - ImGui::MenuItem("Simple overlay", NULL, &show_app_simple_overlay); - ImGui::MenuItem("Manipulating window titles", NULL, &show_app_window_titles); - ImGui::MenuItem("Custom rendering", NULL, &show_app_custom_rendering); - ImGui::MenuItem("Documents", NULL, &show_app_documents); - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Help")) - { - ImGui::MenuItem("Metrics", NULL, &show_app_metrics); - ImGui::MenuItem("Style Editor", NULL, &show_app_style_editor); - ImGui::MenuItem("About Dear ImGui", NULL, &show_app_about); - ImGui::EndMenu(); - } - ImGui::EndMenuBar(); - } + DemoWindowMenuBar(&demo_data); - ImGui::Text("dear imgui says hello. (%s)", IMGUI_VERSION); + ImGui::Text("dear imgui says hello! (%s) (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); ImGui::Spacing(); + IMGUI_DEMO_MARKER("Help"); if (ImGui::CollapsingHeader("Help")) { - ImGui::Text("PROGRAMMER GUIDE:"); - ImGui::BulletText("Please see the ShowDemoWindow() code in imgui_demo.cpp. <- you are here!"); - ImGui::BulletText("Please see the comments in imgui.cpp."); - ImGui::BulletText("Please see the examples/ in application."); - ImGui::BulletText("Enable 'io.ConfigFlags |= NavEnableKeyboard' for keyboard controls."); - ImGui::BulletText("Enable 'io.ConfigFlags |= NavEnableGamepad' for gamepad controls."); - ImGui::Separator(); + ImGui::SeparatorText("ABOUT THIS DEMO:"); + ImGui::BulletText("Sections below are demonstrating many aspects of the library."); + ImGui::BulletText("The \"Examples\" menu above leads to more demo contents."); + ImGui::BulletText("The \"Tools\" menu above gives access to: About Box, Style Editor,\n" + "and Metrics/Debugger (general purpose Dear ImGui debugging tool)."); - ImGui::Text("USER GUIDE:"); + ImGui::SeparatorText("PROGRAMMER GUIDE:"); + ImGui::BulletText("See the ShowDemoWindow() code in imgui_demo.cpp. <- you are here!"); + ImGui::BulletText("See comments in imgui.cpp."); + ImGui::BulletText("See example applications in the examples/ folder."); + ImGui::BulletText("Read the FAQ at "); + ImGui::SameLine(0, 0); + ImGui::TextLinkOpenURL("https://www.dearimgui.com/faq/"); + ImGui::BulletText("Set 'io.ConfigFlags |= NavEnableKeyboard' for keyboard controls."); + ImGui::BulletText("Set 'io.ConfigFlags |= NavEnableGamepad' for gamepad controls."); + + ImGui::SeparatorText("USER GUIDE:"); ImGui::ShowUserGuide(); } + IMGUI_DEMO_MARKER("Configuration"); if (ImGui::CollapsingHeader("Configuration")) { ImGuiIO& io = ImGui::GetIO(); if (ImGui::TreeNode("Configuration##2")) { - ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); - ImGui::CheckboxFlags("io.ConfigFlags: NavEnableGamepad", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NavEnableGamepad); - ImGui::SameLine(); HelpMarker("Required back-end to feed in gamepad inputs in io.NavInputs[] and set io.BackendFlags |= ImGuiBackendFlags_HasGamepad.\n\nRead instructions in imgui.cpp for details."); - ImGui::CheckboxFlags("io.ConfigFlags: NavEnableSetMousePos", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NavEnableSetMousePos); - ImGui::SameLine(); HelpMarker("Instruct navigation to move the mouse cursor. See comment for ImGuiConfigFlags_NavEnableSetMousePos."); - ImGui::CheckboxFlags("io.ConfigFlags: NoMouse", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NoMouse); - if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) // Create a way to restore this flag otherwise we could be stuck completely! + ImGui::SeparatorText("General"); + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableKeyboard", &io.ConfigFlags, ImGuiConfigFlags_NavEnableKeyboard); + ImGui::SameLine(); HelpMarker("Enable keyboard controls."); + ImGui::CheckboxFlags("io.ConfigFlags: NavEnableGamepad", &io.ConfigFlags, ImGuiConfigFlags_NavEnableGamepad); + ImGui::SameLine(); HelpMarker("Enable gamepad controls. Require backend to set io.BackendFlags |= ImGuiBackendFlags_HasGamepad.\n\nRead instructions in imgui.cpp for details."); + ImGui::CheckboxFlags("io.ConfigFlags: NoMouse", &io.ConfigFlags, ImGuiConfigFlags_NoMouse); + ImGui::SameLine(); HelpMarker("Instruct dear imgui to disable mouse inputs and interactions."); + + // The "NoMouse" option can get us stuck with a disabled mouse! Let's provide an alternative way to fix it: + if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) { if (fmodf((float)ImGui::GetTime(), 0.40f) < 0.20f) { ImGui::SameLine(); ImGui::Text("<>"); } - if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Space))) + // Prevent both being checked + if (ImGui::IsKeyPressed(ImGuiKey_Space) || (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard)) io.ConfigFlags &= ~ImGuiConfigFlags_NoMouse; } - ImGui::CheckboxFlags("io.ConfigFlags: NoMouseCursorChange", (unsigned int *)&io.ConfigFlags, ImGuiConfigFlags_NoMouseCursorChange); - ImGui::SameLine(); HelpMarker("Instruct back-end to not alter mouse cursor shape and visibility."); - ImGui::Checkbox("io.ConfigInputTextCursorBlink", &io.ConfigInputTextCursorBlink); - ImGui::SameLine(); HelpMarker("Set to false to disable blinking cursor, for users who consider it distracting"); - ImGui::Checkbox("io.ConfigWindowsResizeFromEdges", &io.ConfigWindowsResizeFromEdges); - ImGui::SameLine(); HelpMarker("Enable resizing of windows from their edges and from the lower-left corner.\nThis requires (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) because it needs mouse cursor feedback."); - ImGui::Checkbox("io.ConfigWindowsMoveFromTitleBarOnly", &io.ConfigWindowsMoveFromTitleBarOnly); + + ImGui::CheckboxFlags("io.ConfigFlags: NoMouseCursorChange", &io.ConfigFlags, ImGuiConfigFlags_NoMouseCursorChange); + ImGui::SameLine(); HelpMarker("Instruct backend to not alter mouse cursor shape and visibility."); + ImGui::CheckboxFlags("io.ConfigFlags: NoKeyboard", &io.ConfigFlags, ImGuiConfigFlags_NoKeyboard); + ImGui::SameLine(); HelpMarker("Instruct dear imgui to disable keyboard inputs and interactions."); + + ImGui::Checkbox("io.ConfigInputTrickleEventQueue", &io.ConfigInputTrickleEventQueue); + ImGui::SameLine(); HelpMarker("Enable input queue trickling: some types of events submitted during the same frame (e.g. button down + up) will be spread over multiple frames, improving interactions with low framerates."); ImGui::Checkbox("io.MouseDrawCursor", &io.MouseDrawCursor); - ImGui::SameLine(); HelpMarker("Instruct Dear ImGui to render a mouse cursor for you. Note that a mouse cursor rendered via your application GPU rendering path will feel more laggy than hardware cursor, but will be more in sync with your other visuals.\n\nSome desktop applications may use both kinds of cursors (e.g. enable software cursor only when resizing/dragging something)."); + ImGui::SameLine(); HelpMarker("Instruct Dear ImGui to render a mouse cursor itself. Note that a mouse cursor rendered via your application GPU rendering path will feel more laggy than hardware cursor, but will be more in sync with your other visuals.\n\nSome desktop applications may use both kinds of cursors (e.g. enable software cursor only when resizing/dragging something)."); + + ImGui::SeparatorText("Keyboard/Gamepad Navigation"); + ImGui::Checkbox("io.ConfigNavSwapGamepadButtons", &io.ConfigNavSwapGamepadButtons); + ImGui::Checkbox("io.ConfigNavMoveSetMousePos", &io.ConfigNavMoveSetMousePos); + ImGui::SameLine(); HelpMarker("Directional/tabbing navigation teleports the mouse cursor. May be useful on TV/console systems where moving a virtual mouse is difficult"); + ImGui::Checkbox("io.ConfigNavCaptureKeyboard", &io.ConfigNavCaptureKeyboard); + ImGui::Checkbox("io.ConfigNavEscapeClearFocusItem", &io.ConfigNavEscapeClearFocusItem); + ImGui::SameLine(); HelpMarker("Pressing Escape clears focused item."); + ImGui::Checkbox("io.ConfigNavEscapeClearFocusWindow", &io.ConfigNavEscapeClearFocusWindow); + ImGui::SameLine(); HelpMarker("Pressing Escape clears focused window."); + ImGui::Checkbox("io.ConfigNavCursorVisibleAuto", &io.ConfigNavCursorVisibleAuto); + ImGui::SameLine(); HelpMarker("Using directional navigation key makes the cursor visible. Mouse click hides the cursor."); + ImGui::Checkbox("io.ConfigNavCursorVisibleAlways", &io.ConfigNavCursorVisibleAlways); + ImGui::SameLine(); HelpMarker("Navigation cursor is always visible."); + + ImGui::SeparatorText("Docking"); + ImGui::CheckboxFlags("io.ConfigFlags: DockingEnable", &io.ConfigFlags, ImGuiConfigFlags_DockingEnable); + ImGui::SameLine(); + if (io.ConfigDockingWithShift) + HelpMarker("Drag from window title bar or their tab to dock/undock. Hold SHIFT to enable docking.\n\nDrag from window menu button (upper-left button) to undock an entire node (all windows)."); + else + HelpMarker("Drag from window title bar or their tab to dock/undock. Hold SHIFT to disable docking.\n\nDrag from window menu button (upper-left button) to undock an entire node (all windows)."); + if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) + { + ImGui::Indent(); + ImGui::Checkbox("io.ConfigDockingNoSplit", &io.ConfigDockingNoSplit); + ImGui::SameLine(); HelpMarker("Simplified docking mode: disable window splitting, so docking is limited to merging multiple windows together into tab-bars."); + ImGui::Checkbox("io.ConfigDockingWithShift", &io.ConfigDockingWithShift); + ImGui::SameLine(); HelpMarker("Enable docking when holding Shift only (allow to drop in wider space, reduce visual noise)"); + ImGui::Checkbox("io.ConfigDockingAlwaysTabBar", &io.ConfigDockingAlwaysTabBar); + ImGui::SameLine(); HelpMarker("Create a docking node and tab-bar on single floating windows."); + ImGui::Checkbox("io.ConfigDockingTransparentPayload", &io.ConfigDockingTransparentPayload); + ImGui::SameLine(); HelpMarker("Make window or viewport transparent when docking and only display docking boxes on the target viewport. Useful if rendering of multiple viewport cannot be synced. Best used with ConfigViewportsNoAutoMerge."); + ImGui::Unindent(); + } + + ImGui::SeparatorText("Multi-viewports"); + ImGui::CheckboxFlags("io.ConfigFlags: ViewportsEnable", &io.ConfigFlags, ImGuiConfigFlags_ViewportsEnable); + ImGui::SameLine(); HelpMarker("[beta] Enable beta multi-viewports support. See ImGuiPlatformIO for details."); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + ImGui::Indent(); + ImGui::Checkbox("io.ConfigViewportsNoAutoMerge", &io.ConfigViewportsNoAutoMerge); + ImGui::SameLine(); HelpMarker("Set to make all floating imgui windows always create their own viewport. Otherwise, they are merged into the main host viewports when overlapping it."); + ImGui::Checkbox("io.ConfigViewportsNoTaskBarIcon", &io.ConfigViewportsNoTaskBarIcon); + ImGui::SameLine(); HelpMarker("Toggling this at runtime is normally unsupported (most platform backends won't refresh the task bar icon state right away)."); + ImGui::Checkbox("io.ConfigViewportsNoDecoration", &io.ConfigViewportsNoDecoration); + ImGui::SameLine(); HelpMarker("Toggling this at runtime is normally unsupported (most platform backends won't refresh the decoration right away)."); + ImGui::Checkbox("io.ConfigViewportsNoDefaultParent", &io.ConfigViewportsNoDefaultParent); + ImGui::SameLine(); HelpMarker("Toggling this at runtime is normally unsupported (most platform backends won't refresh the parenting right away)."); + ImGui::Unindent(); + } + + ImGui::SeparatorText("Windows"); + ImGui::Checkbox("io.ConfigWindowsResizeFromEdges", &io.ConfigWindowsResizeFromEdges); + ImGui::SameLine(); HelpMarker("Enable resizing of windows from their edges and from the lower-left corner.\nThis requires ImGuiBackendFlags_HasMouseCursors for better mouse cursor feedback."); + ImGui::Checkbox("io.ConfigWindowsMoveFromTitleBarOnly", &io.ConfigWindowsMoveFromTitleBarOnly); + ImGui::Checkbox("io.ConfigWindowsCopyContentsWithCtrlC", &io.ConfigWindowsCopyContentsWithCtrlC); // [EXPERIMENTAL] + ImGui::SameLine(); HelpMarker("*EXPERIMENTAL* CTRL+C copy the contents of focused window into the clipboard.\n\nExperimental because:\n- (1) has known issues with nested Begin/End pairs.\n- (2) text output quality varies.\n- (3) text output is in submission order rather than spatial order."); + ImGui::Checkbox("io.ConfigScrollbarScrollByPage", &io.ConfigScrollbarScrollByPage); + ImGui::SameLine(); HelpMarker("Enable scrolling page by page when clicking outside the scrollbar grab.\nWhen disabled, always scroll to clicked location.\nWhen enabled, Shift+Click scrolls to clicked location."); + + ImGui::SeparatorText("Widgets"); + ImGui::Checkbox("io.ConfigInputTextCursorBlink", &io.ConfigInputTextCursorBlink); + ImGui::SameLine(); HelpMarker("Enable blinking cursor (optional as some users consider it to be distracting)."); + ImGui::Checkbox("io.ConfigInputTextEnterKeepActive", &io.ConfigInputTextEnterKeepActive); + ImGui::SameLine(); HelpMarker("Pressing Enter will keep item active and select contents (single-line only)."); + ImGui::Checkbox("io.ConfigDragClickToInputText", &io.ConfigDragClickToInputText); + ImGui::SameLine(); HelpMarker("Enable turning DragXXX widgets into text input with a simple mouse click-release (without moving)."); + ImGui::Checkbox("io.ConfigMacOSXBehaviors", &io.ConfigMacOSXBehaviors); + ImGui::SameLine(); HelpMarker("Swap Cmd<>Ctrl keys, enable various MacOS style behaviors."); + ImGui::Text("Also see Style->Rendering for rendering options."); + + // Also read: https://github.com/ocornut/imgui/wiki/Error-Handling + ImGui::SeparatorText("Error Handling"); + + ImGui::Checkbox("io.ConfigErrorRecovery", &io.ConfigErrorRecovery); + ImGui::SameLine(); HelpMarker( + "Options to configure how we handle recoverable errors.\n" + "- Error recovery is not perfect nor guaranteed! It is a feature to ease development.\n" + "- You not are not supposed to rely on it in the course of a normal application run.\n" + "- Possible usage: facilitate recovery from errors triggered from a scripting language or after specific exceptions handlers.\n" + "- Always ensure that on programmers seat you have at minimum Asserts or Tooltips enabled when making direct imgui API call! " + "Otherwise it would severely hinder your ability to catch and correct mistakes!"); + ImGui::Checkbox("io.ConfigErrorRecoveryEnableAssert", &io.ConfigErrorRecoveryEnableAssert); + ImGui::Checkbox("io.ConfigErrorRecoveryEnableDebugLog", &io.ConfigErrorRecoveryEnableDebugLog); + ImGui::Checkbox("io.ConfigErrorRecoveryEnableTooltip", &io.ConfigErrorRecoveryEnableTooltip); + if (!io.ConfigErrorRecoveryEnableAssert && !io.ConfigErrorRecoveryEnableDebugLog && !io.ConfigErrorRecoveryEnableTooltip) + io.ConfigErrorRecoveryEnableAssert = io.ConfigErrorRecoveryEnableDebugLog = io.ConfigErrorRecoveryEnableTooltip = true; + + // Also read: https://github.com/ocornut/imgui/wiki/Debug-Tools + ImGui::SeparatorText("Debug"); + ImGui::Checkbox("io.ConfigDebugIsDebuggerPresent", &io.ConfigDebugIsDebuggerPresent); + ImGui::SameLine(); HelpMarker("Enable various tools calling IM_DEBUG_BREAK().\n\nRequires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application."); + ImGui::Checkbox("io.ConfigDebugHighlightIdConflicts", &io.ConfigDebugHighlightIdConflicts); + ImGui::SameLine(); HelpMarker("Highlight and show an error message when multiple items have conflicting identifiers."); + ImGui::BeginDisabled(); + ImGui::Checkbox("io.ConfigDebugBeginReturnValueOnce", &io.ConfigDebugBeginReturnValueOnce); + ImGui::EndDisabled(); + ImGui::SameLine(); HelpMarker("First calls to Begin()/BeginChild() will return false.\n\nTHIS OPTION IS DISABLED because it needs to be set at application boot-time to make sense. Showing the disabled option is a way to make this feature easier to discover."); + ImGui::Checkbox("io.ConfigDebugBeginReturnValueLoop", &io.ConfigDebugBeginReturnValueLoop); + ImGui::SameLine(); HelpMarker("Some calls to Begin()/BeginChild() will return false.\n\nWill cycle through window depths then repeat. Windows should be flickering while running."); + ImGui::Checkbox("io.ConfigDebugIgnoreFocusLoss", &io.ConfigDebugIgnoreFocusLoss); + ImGui::SameLine(); HelpMarker("Option to deactivate io.AddFocusEvent(false) handling. May facilitate interactions with a debugger when focus loss leads to clearing inputs data."); + ImGui::Checkbox("io.ConfigDebugIniSettings", &io.ConfigDebugIniSettings); + ImGui::SameLine(); HelpMarker("Option to save .ini data with extra comments (particularly helpful for Docking, but makes saving slower)."); + ImGui::TreePop(); - ImGui::Separator(); + ImGui::Spacing(); } + IMGUI_DEMO_MARKER("Configuration/Backend Flags"); if (ImGui::TreeNode("Backend Flags")) { - HelpMarker("Those flags are set by the back-ends (imgui_impl_xxx files) to specify their capabilities."); - ImGuiBackendFlags backend_flags = io.BackendFlags; // Make a local copy to avoid modifying actual back-end flags. - ImGui::CheckboxFlags("io.BackendFlags: HasGamepad", (unsigned int *)&backend_flags, ImGuiBackendFlags_HasGamepad); - ImGui::CheckboxFlags("io.BackendFlags: HasMouseCursors", (unsigned int *)&backend_flags, ImGuiBackendFlags_HasMouseCursors); - ImGui::CheckboxFlags("io.BackendFlags: HasSetMousePos", (unsigned int *)&backend_flags, ImGuiBackendFlags_HasSetMousePos); - ImGui::CheckboxFlags("io.BackendFlags: RendererHasVtxOffset", (unsigned int *)&backend_flags, ImGuiBackendFlags_RendererHasVtxOffset); + HelpMarker( + "Those flags are set by the backends (imgui_impl_xxx files) to specify their capabilities.\n" + "Here we expose them as read-only fields to avoid breaking interactions with your backend."); + + // Make a local copy to avoid modifying actual backend flags. + // FIXME: Maybe we need a BeginReadonly() equivalent to keep label bright? + ImGui::BeginDisabled(); + ImGui::CheckboxFlags("io.BackendFlags: HasGamepad", &io.BackendFlags, ImGuiBackendFlags_HasGamepad); + ImGui::CheckboxFlags("io.BackendFlags: HasMouseCursors", &io.BackendFlags, ImGuiBackendFlags_HasMouseCursors); + ImGui::CheckboxFlags("io.BackendFlags: HasSetMousePos", &io.BackendFlags, ImGuiBackendFlags_HasSetMousePos); + ImGui::CheckboxFlags("io.BackendFlags: PlatformHasViewports", &io.BackendFlags, ImGuiBackendFlags_PlatformHasViewports); + ImGui::CheckboxFlags("io.BackendFlags: HasMouseHoveredViewport",&io.BackendFlags, ImGuiBackendFlags_HasMouseHoveredViewport); + ImGui::CheckboxFlags("io.BackendFlags: RendererHasVtxOffset", &io.BackendFlags, ImGuiBackendFlags_RendererHasVtxOffset); + ImGui::CheckboxFlags("io.BackendFlags: RendererHasViewports", &io.BackendFlags, ImGuiBackendFlags_RendererHasViewports); + ImGui::EndDisabled(); + ImGui::TreePop(); - ImGui::Separator(); + ImGui::Spacing(); } + IMGUI_DEMO_MARKER("Configuration/Style"); if (ImGui::TreeNode("Style")) { - ImGui::ShowStyleEditor(); + ImGui::Checkbox("Style Editor", &demo_data.ShowStyleEditor); + ImGui::SameLine(); + HelpMarker("The same contents can be accessed in 'Tools->Style Editor' or by calling the ShowStyleEditor() function."); ImGui::TreePop(); - ImGui::Separator(); + ImGui::Spacing(); } + IMGUI_DEMO_MARKER("Configuration/Capture, Logging"); if (ImGui::TreeNode("Capture/Logging")) { - ImGui::TextWrapped("The logging API redirects all text output so you can easily capture the content of a window or a block. Tree nodes can be automatically expanded."); - HelpMarker("Try opening any of the contents below in this window and then click one of the \"Log To\" button."); + HelpMarker( + "The logging API redirects all text output so you can easily capture the content of " + "a window or a block. Tree nodes can be automatically expanded.\n" + "Try opening any of the contents below in this window and then click one of the \"Log To\" button."); ImGui::LogButtons(); - ImGui::TextWrapped("You can also call ImGui::LogText() to output directly to the log without a visual output."); + + HelpMarker("You can also call ImGui::LogText() to output directly to the log without a visual output."); if (ImGui::Button("Copy \"Hello, world!\" to clipboard")) { ImGui::LogToClipboard(); @@ -387,38 +659,218 @@ void ImGui::ShowDemoWindow(bool* p_open) } } + IMGUI_DEMO_MARKER("Window options"); if (ImGui::CollapsingHeader("Window options")) { - ImGui::Checkbox("No titlebar", &no_titlebar); ImGui::SameLine(150); - ImGui::Checkbox("No scrollbar", &no_scrollbar); ImGui::SameLine(300); - ImGui::Checkbox("No menu", &no_menu); - ImGui::Checkbox("No move", &no_move); ImGui::SameLine(150); - ImGui::Checkbox("No resize", &no_resize); ImGui::SameLine(300); - ImGui::Checkbox("No collapse", &no_collapse); - ImGui::Checkbox("No close", &no_close); ImGui::SameLine(150); - ImGui::Checkbox("No nav", &no_nav); ImGui::SameLine(300); - ImGui::Checkbox("No background", &no_background); - ImGui::Checkbox("No bring to front", &no_bring_to_front); + if (ImGui::BeginTable("split", 3)) + { + ImGui::TableNextColumn(); ImGui::Checkbox("No titlebar", &no_titlebar); + ImGui::TableNextColumn(); ImGui::Checkbox("No scrollbar", &no_scrollbar); + ImGui::TableNextColumn(); ImGui::Checkbox("No menu", &no_menu); + ImGui::TableNextColumn(); ImGui::Checkbox("No move", &no_move); + ImGui::TableNextColumn(); ImGui::Checkbox("No resize", &no_resize); + ImGui::TableNextColumn(); ImGui::Checkbox("No collapse", &no_collapse); + ImGui::TableNextColumn(); ImGui::Checkbox("No close", &no_close); + ImGui::TableNextColumn(); ImGui::Checkbox("No nav", &no_nav); + ImGui::TableNextColumn(); ImGui::Checkbox("No background", &no_background); + ImGui::TableNextColumn(); ImGui::Checkbox("No bring to front", &no_bring_to_front); + ImGui::TableNextColumn(); ImGui::Checkbox("No docking", &no_docking); + ImGui::TableNextColumn(); ImGui::Checkbox("Unsaved document", &unsaved_document); + ImGui::EndTable(); + } } // All demo contents - ShowDemoWindowWidgets(); - ShowDemoWindowLayout(); - ShowDemoWindowPopups(); - ShowDemoWindowColumns(); - ShowDemoWindowMisc(); + DemoWindowWidgets(&demo_data); + DemoWindowLayout(); + DemoWindowPopups(); + DemoWindowTables(); + DemoWindowInputs(); // End of ShowDemoWindow() + ImGui::PopItemWidth(); ImGui::End(); } -static void ShowDemoWindowWidgets() -{ - if (!ImGui::CollapsingHeader("Widgets")) - return; +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowMenuBar() +//----------------------------------------------------------------------------- +static void DemoWindowMenuBar(ImGuiDemoWindowData* demo_data) +{ + IMGUI_DEMO_MARKER("Menu"); + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Menu")) + { + IMGUI_DEMO_MARKER("Menu/File"); + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Examples")) + { + IMGUI_DEMO_MARKER("Menu/Examples"); + ImGui::MenuItem("Main menu bar", NULL, &demo_data->ShowMainMenuBar); + + ImGui::SeparatorText("Mini apps"); + ImGui::MenuItem("Assets Browser", NULL, &demo_data->ShowAppAssetsBrowser); + ImGui::MenuItem("Console", NULL, &demo_data->ShowAppConsole); + ImGui::MenuItem("Custom rendering", NULL, &demo_data->ShowAppCustomRendering); + ImGui::MenuItem("Documents", NULL, &demo_data->ShowAppDocuments); + ImGui::MenuItem("Dockspace", NULL, &demo_data->ShowAppDockSpace); + ImGui::MenuItem("Log", NULL, &demo_data->ShowAppLog); + ImGui::MenuItem("Property editor", NULL, &demo_data->ShowAppPropertyEditor); + ImGui::MenuItem("Simple layout", NULL, &demo_data->ShowAppLayout); + ImGui::MenuItem("Simple overlay", NULL, &demo_data->ShowAppSimpleOverlay); + + ImGui::SeparatorText("Concepts"); + ImGui::MenuItem("Auto-resizing window", NULL, &demo_data->ShowAppAutoResize); + ImGui::MenuItem("Constrained-resizing window", NULL, &demo_data->ShowAppConstrainedResize); + ImGui::MenuItem("Fullscreen window", NULL, &demo_data->ShowAppFullscreen); + ImGui::MenuItem("Long text display", NULL, &demo_data->ShowAppLongText); + ImGui::MenuItem("Manipulating window titles", NULL, &demo_data->ShowAppWindowTitles); + + ImGui::EndMenu(); + } + //if (ImGui::MenuItem("MenuItem")) {} // You can also use MenuItem() inside a menu bar! + if (ImGui::BeginMenu("Tools")) + { + IMGUI_DEMO_MARKER("Menu/Tools"); + ImGuiIO& io = ImGui::GetIO(); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + const bool has_debug_tools = true; +#else + const bool has_debug_tools = false; +#endif + ImGui::MenuItem("Metrics/Debugger", NULL, &demo_data->ShowMetrics, has_debug_tools); + if (ImGui::BeginMenu("Debug Options")) + { + ImGui::BeginDisabled(!has_debug_tools); + ImGui::Checkbox("Highlight ID Conflicts", &io.ConfigDebugHighlightIdConflicts); + ImGui::EndDisabled(); + ImGui::Checkbox("Assert on error recovery", &io.ConfigErrorRecoveryEnableAssert); + ImGui::TextDisabled("(see Demo->Configuration for details & more)"); + ImGui::EndMenu(); + } + ImGui::MenuItem("Debug Log", NULL, &demo_data->ShowDebugLog, has_debug_tools); + ImGui::MenuItem("ID Stack Tool", NULL, &demo_data->ShowIDStackTool, has_debug_tools); + bool is_debugger_present = io.ConfigDebugIsDebuggerPresent; + if (ImGui::MenuItem("Item Picker", NULL, false, has_debug_tools))// && is_debugger_present)) + ImGui::DebugStartItemPicker(); + if (!is_debugger_present) + ImGui::SetItemTooltip("Requires io.ConfigDebugIsDebuggerPresent=true to be set.\n\nWe otherwise disable some extra features to avoid casual users crashing the application."); + ImGui::MenuItem("Style Editor", NULL, &demo_data->ShowStyleEditor); + ImGui::MenuItem("About Dear ImGui", NULL, &demo_data->ShowAbout); + + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Helpers: ExampleTreeNode, ExampleMemberInfo (for use by Property Editor & Multi-Select demos) +//----------------------------------------------------------------------------- + +// Simple representation for a tree +// (this is designed to be simple to understand for our demos, not to be fancy or efficient etc.) +struct ExampleTreeNode +{ + // Tree structure + char Name[28] = ""; + int UID = 0; + ExampleTreeNode* Parent = NULL; + ImVector Childs; + unsigned short IndexInParent = 0; // Maintaining this allows us to implement linear traversal more easily + + // Leaf Data + bool HasData = false; // All leaves have data + bool DataMyBool = true; + int DataMyInt = 128; + ImVec2 DataMyVec2 = ImVec2(0.0f, 3.141592f); +}; + +// Simple representation of struct metadata/serialization data. +// (this is a minimal version of what a typical advanced application may provide) +struct ExampleMemberInfo +{ + const char* Name; // Member name + ImGuiDataType DataType; // Member type + int DataCount; // Member count (1 when scalar) + int Offset; // Offset inside parent structure +}; + +// Metadata description of ExampleTreeNode struct. +static const ExampleMemberInfo ExampleTreeNodeMemberInfos[] +{ + { "MyName", ImGuiDataType_String, 1, offsetof(ExampleTreeNode, Name) }, + { "MyBool", ImGuiDataType_Bool, 1, offsetof(ExampleTreeNode, DataMyBool) }, + { "MyInt", ImGuiDataType_S32, 1, offsetof(ExampleTreeNode, DataMyInt) }, + { "MyVec2", ImGuiDataType_Float, 2, offsetof(ExampleTreeNode, DataMyVec2) }, +}; + +static ExampleTreeNode* ExampleTree_CreateNode(const char* name, int uid, ExampleTreeNode* parent) +{ + ExampleTreeNode* node = IM_NEW(ExampleTreeNode); + snprintf(node->Name, IM_ARRAYSIZE(node->Name), "%s", name); + node->UID = uid; + node->Parent = parent; + node->IndexInParent = parent ? (unsigned short)parent->Childs.Size : 0; + if (parent) + parent->Childs.push_back(node); + return node; +} + +static void ExampleTree_DestroyNode(ExampleTreeNode* node) +{ + for (ExampleTreeNode* child_node : node->Childs) + ExampleTree_DestroyNode(child_node); + IM_DELETE(node); +} + +// Create example tree data +// (this allocates _many_ more times than most other code in either Dear ImGui or others demo) +static ExampleTreeNode* ExampleTree_CreateDemoTree() +{ + static const char* root_names[] = { "Apple", "Banana", "Cherry", "Kiwi", "Mango", "Orange", "Pear", "Pineapple", "Strawberry", "Watermelon" }; + const size_t NAME_MAX_LEN = sizeof(ExampleTreeNode::Name); + char name_buf[NAME_MAX_LEN]; + int uid = 0; + ExampleTreeNode* node_L0 = ExampleTree_CreateNode("", ++uid, NULL); + const int root_items_multiplier = 2; + for (int idx_L0 = 0; idx_L0 < IM_ARRAYSIZE(root_names) * root_items_multiplier; idx_L0++) + { + snprintf(name_buf, IM_ARRAYSIZE(name_buf), "%s %d", root_names[idx_L0 / root_items_multiplier], idx_L0 % root_items_multiplier); + ExampleTreeNode* node_L1 = ExampleTree_CreateNode(name_buf, ++uid, node_L0); + const int number_of_childs = (int)strlen(node_L1->Name); + for (int idx_L1 = 0; idx_L1 < number_of_childs; idx_L1++) + { + snprintf(name_buf, IM_ARRAYSIZE(name_buf), "Child %d", idx_L1); + ExampleTreeNode* node_L2 = ExampleTree_CreateNode(name_buf, ++uid, node_L1); + node_L2->HasData = true; + if (idx_L1 == 0) + { + snprintf(name_buf, IM_ARRAYSIZE(name_buf), "Sub-child %d", 0); + ExampleTreeNode* node_L3 = ExampleTree_CreateNode(name_buf, ++uid, node_L2); + node_L3->HasData = true; + } + } + } + return node_L0; +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsBasic() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsBasic() +{ + IMGUI_DEMO_MARKER("Widgets/Basic"); if (ImGui::TreeNode("Basic")) { + ImGui::SeparatorText("General"); + + IMGUI_DEMO_MARKER("Widgets/Basic/Button"); static int clicked = 0; if (ImGui::Button("Button")) clicked++; @@ -428,83 +880,82 @@ static void ShowDemoWindowWidgets() ImGui::Text("Thanks for clicking me!"); } + IMGUI_DEMO_MARKER("Widgets/Basic/Checkbox"); static bool check = true; ImGui::Checkbox("checkbox", &check); + IMGUI_DEMO_MARKER("Widgets/Basic/RadioButton"); static int e = 0; ImGui::RadioButton("radio a", &e, 0); ImGui::SameLine(); ImGui::RadioButton("radio b", &e, 1); ImGui::SameLine(); ImGui::RadioButton("radio c", &e, 2); // Color buttons, demonstrate using PushID() to add unique identifier in the ID stack, and changing style. + IMGUI_DEMO_MARKER("Widgets/Basic/Buttons (Colored)"); for (int i = 0; i < 7; i++) { if (i > 0) ImGui::SameLine(); ImGui::PushID(i); - ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(i/7.0f, 0.6f, 0.6f)); - ImGui::PushStyleColor(ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV(i/7.0f, 0.7f, 0.7f)); - ImGui::PushStyleColor(ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV(i/7.0f, 0.8f, 0.8f)); + ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(i / 7.0f, 0.6f, 0.6f)); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV(i / 7.0f, 0.7f, 0.7f)); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV(i / 7.0f, 0.8f, 0.8f)); ImGui::Button("Click"); ImGui::PopStyleColor(3); ImGui::PopID(); } - // Use AlignTextToFramePadding() to align text baseline to the baseline of framed elements (otherwise a Text+SameLine+Button sequence will have the text a little too high by default) + // Use AlignTextToFramePadding() to align text baseline to the baseline of framed widgets elements + // (otherwise a Text+SameLine+Button sequence will have the text a little too high by default!) + // See 'Demo->Layout->Text Baseline Alignment' for details. ImGui::AlignTextToFramePadding(); ImGui::Text("Hold to repeat:"); ImGui::SameLine(); // Arrow buttons with Repeater + IMGUI_DEMO_MARKER("Widgets/Basic/Buttons (Repeating)"); static int counter = 0; float spacing = ImGui::GetStyle().ItemInnerSpacing.x; - ImGui::PushButtonRepeat(true); + ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); if (ImGui::ArrowButton("##left", ImGuiDir_Left)) { counter--; } ImGui::SameLine(0.0f, spacing); if (ImGui::ArrowButton("##right", ImGuiDir_Right)) { counter++; } - ImGui::PopButtonRepeat(); + ImGui::PopItemFlag(); ImGui::SameLine(); ImGui::Text("%d", counter); - ImGui::Text("Hover over me"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("I am a tooltip"); - - ImGui::SameLine(); - ImGui::Text("- or me"); - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - ImGui::Text("I am a fancy tooltip"); - static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; - ImGui::PlotLines("Curve", arr, IM_ARRAYSIZE(arr)); - ImGui::EndTooltip(); - } - - ImGui::Separator(); + ImGui::Button("Tooltip"); + ImGui::SetItemTooltip("I am a tooltip"); ImGui::LabelText("label", "Value"); - { - // Using the _simplified_ one-liner Combo() api here - // See "Combo" section for examples of how to use the more complete BeginCombo()/EndCombo() api. - const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" }; - static int item_current = 0; - ImGui::Combo("combo", &item_current, items, IM_ARRAYSIZE(items)); - ImGui::SameLine(); HelpMarker("Refer to the \"Combo\" section below for an explanation of the full BeginCombo/EndCombo API, and demonstration of various flags.\n"); - } + ImGui::SeparatorText("Inputs"); { + // To wire InputText() with std::string or any other custom string type, + // see the "Text Input > Resize Callback" section of this demo, and the misc/cpp/imgui_stdlib.h file. + IMGUI_DEMO_MARKER("Widgets/Basic/InputText"); static char str0[128] = "Hello, world!"; ImGui::InputText("input text", str0, IM_ARRAYSIZE(str0)); - ImGui::SameLine(); HelpMarker("USER:\nHold SHIFT or use mouse to select text.\n" "CTRL+Left/Right to word jump.\n" "CTRL+A or double-click to select all.\n" "CTRL+X,CTRL+C,CTRL+V clipboard.\n" "CTRL+Z,CTRL+Y undo/redo.\n" "ESCAPE to revert.\n\nPROGRAMMER:\nYou can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputText() to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example (this is not demonstrated in imgui_demo.cpp)."); + ImGui::SameLine(); HelpMarker( + "USER:\n" + "Hold SHIFT or use mouse to select text.\n" + "CTRL+Left/Right to word jump.\n" + "CTRL+A or Double-Click to select all.\n" + "CTRL+X,CTRL+C,CTRL+V for clipboard.\n" + "CTRL+Z to undo, CTRL+Y/CTRL+SHIFT+Z to redo.\n" + "ESCAPE to revert.\n\n" + "PROGRAMMER:\n" + "You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputText() " + "to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example (this is not demonstrated " + "in imgui_demo.cpp)."); static char str1[128] = ""; ImGui::InputTextWithHint("input text (w/ hint)", "enter text here", str1, IM_ARRAYSIZE(str1)); + IMGUI_DEMO_MARKER("Widgets/Basic/InputInt, InputFloat"); static int i0 = 123; ImGui::InputInt("input int", &i0); - ImGui::SameLine(); HelpMarker("You can apply arithmetic operators +,*,/ on numerical values.\n e.g. [ 100 ], input \'*2\', result becomes [ 200 ]\nUse +- to subtract.\n"); static float f0 = 0.001f; ImGui::InputFloat("input float", &f0, 0.01f, 1.0f, "%.3f"); @@ -514,149 +965,145 @@ static void ShowDemoWindowWidgets() static float f1 = 1.e10f; ImGui::InputFloat("input scientific", &f1, 0.0f, 0.0f, "%e"); - ImGui::SameLine(); HelpMarker("You can input value using the scientific notation,\n e.g. \"1e+8\" becomes \"100000000\".\n"); + ImGui::SameLine(); HelpMarker( + "You can input value using the scientific notation,\n" + " e.g. \"1e+8\" becomes \"100000000\"."); static float vec4a[4] = { 0.10f, 0.20f, 0.30f, 0.44f }; ImGui::InputFloat3("input float3", vec4a); } + ImGui::SeparatorText("Drags"); + { - static int i1 = 50, i2 = 42; + IMGUI_DEMO_MARKER("Widgets/Basic/DragInt, DragFloat"); + static int i1 = 50, i2 = 42, i3 = 128; ImGui::DragInt("drag int", &i1, 1); - ImGui::SameLine(); HelpMarker("Click and drag to edit value.\nHold SHIFT/ALT for faster/slower edit.\nDouble-click or CTRL+click to input value."); + ImGui::SameLine(); HelpMarker( + "Click and drag to edit value.\n" + "Hold SHIFT/ALT for faster/slower edit.\n" + "Double-click or CTRL+click to input value."); + ImGui::DragInt("drag int 0..100", &i2, 1, 0, 100, "%d%%", ImGuiSliderFlags_AlwaysClamp); + ImGui::DragInt("drag int wrap 100..200", &i3, 1, 100, 200, "%d", ImGuiSliderFlags_WrapAround); - ImGui::DragInt("drag int 0..100", &i2, 1, 0, 100, "%d%%"); - - static float f1=1.00f, f2=0.0067f; + static float f1 = 1.00f, f2 = 0.0067f; ImGui::DragFloat("drag float", &f1, 0.005f); ImGui::DragFloat("drag small float", &f2, 0.0001f, 0.0f, 0.0f, "%.06f ns"); + //ImGui::DragFloat("drag wrap -1..1", &f3, 0.005f, -1.0f, 1.0f, NULL, ImGuiSliderFlags_WrapAround); } + ImGui::SeparatorText("Sliders"); + { - static int i1=0; + IMGUI_DEMO_MARKER("Widgets/Basic/SliderInt, SliderFloat"); + static int i1 = 0; ImGui::SliderInt("slider int", &i1, -1, 3); ImGui::SameLine(); HelpMarker("CTRL+click to input value."); - static float f1=0.123f, f2=0.0f; + static float f1 = 0.123f, f2 = 0.0f; ImGui::SliderFloat("slider float", &f1, 0.0f, 1.0f, "ratio = %.3f"); - ImGui::SliderFloat("slider float (curve)", &f2, -10.0f, 10.0f, "%.4f", 2.0f); + ImGui::SliderFloat("slider float (log)", &f2, -10.0f, 10.0f, "%.4f", ImGuiSliderFlags_Logarithmic); + + IMGUI_DEMO_MARKER("Widgets/Basic/SliderAngle"); static float angle = 0.0f; ImGui::SliderAngle("slider angle", &angle); + + // Using the format string to display a name instead of an integer. + // Here we completely omit '%d' from the format string, so it'll only display a name. + // This technique can also be used with DragInt(). + IMGUI_DEMO_MARKER("Widgets/Basic/Slider (enum)"); + enum Element { Element_Fire, Element_Earth, Element_Air, Element_Water, Element_COUNT }; + static int elem = Element_Fire; + const char* elems_names[Element_COUNT] = { "Fire", "Earth", "Air", "Water" }; + const char* elem_name = (elem >= 0 && elem < Element_COUNT) ? elems_names[elem] : "Unknown"; + ImGui::SliderInt("slider enum", &elem, 0, Element_COUNT - 1, elem_name); // Use ImGuiSliderFlags_NoInput flag to disable CTRL+Click here. + ImGui::SameLine(); HelpMarker("Using the format string parameter to display a name instead of the underlying integer."); } + ImGui::SeparatorText("Selectors/Pickers"); + { - static float col1[3] = { 1.0f,0.0f,0.2f }; - static float col2[4] = { 0.4f,0.7f,0.0f,0.5f }; + IMGUI_DEMO_MARKER("Widgets/Basic/ColorEdit3, ColorEdit4"); + static float col1[3] = { 1.0f, 0.0f, 0.2f }; + static float col2[4] = { 0.4f, 0.7f, 0.0f, 0.5f }; ImGui::ColorEdit3("color 1", col1); - ImGui::SameLine(); HelpMarker("Click on the colored square to open a color picker.\nClick and hold to use drag and drop.\nRight-click on the colored square to show options.\nCTRL+click on individual component to input value.\n"); + ImGui::SameLine(); HelpMarker( + "Click on the color square to open a color picker.\n" + "Click and hold to use drag and drop.\n" + "Right-click on the color square to show options.\n" + "CTRL+click on individual component to input value.\n"); ImGui::ColorEdit4("color 2", col2); } { - // List box - const char* listbox_items[] = { "Apple", "Banana", "Cherry", "Kiwi", "Mango", "Orange", "Pineapple", "Strawberry", "Watermelon" }; - static int listbox_item_current = 1; - ImGui::ListBox("listbox\n(single select)", &listbox_item_current, listbox_items, IM_ARRAYSIZE(listbox_items), 4); - - //static int listbox_item_current2 = 2; - //ImGui::SetNextItemWidth(-1); - //ImGui::ListBox("##listbox2", &listbox_item_current2, listbox_items, IM_ARRAYSIZE(listbox_items), 4); + // Using the _simplified_ one-liner Combo() api here + // See "Combo" section for examples of how to use the more flexible BeginCombo()/EndCombo() api. + IMGUI_DEMO_MARKER("Widgets/Basic/Combo"); + const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIIIIII", "JJJJ", "KKKKKKK" }; + static int item_current = 0; + ImGui::Combo("combo", &item_current, items, IM_ARRAYSIZE(items)); + ImGui::SameLine(); HelpMarker( + "Using the simplified one-liner Combo API here.\n" + "Refer to the \"Combo\" section below for an explanation of how to use the more flexible and general BeginCombo/EndCombo API."); } + { + // Using the _simplified_ one-liner ListBox() api here + // See "List boxes" section for examples of how to use the more flexible BeginListBox()/EndListBox() api. + IMGUI_DEMO_MARKER("Widgets/Basic/ListBox"); + const char* items[] = { "Apple", "Banana", "Cherry", "Kiwi", "Mango", "Orange", "Pineapple", "Strawberry", "Watermelon" }; + static int item_current = 1; + ImGui::ListBox("listbox", &item_current, items, IM_ARRAYSIZE(items), 4); + ImGui::SameLine(); HelpMarker( + "Using the simplified one-liner ListBox API here.\n" + "Refer to the \"List boxes\" section below for an explanation of how to use the more flexible and general BeginListBox/EndListBox API."); + } + + // Testing ImGuiOnceUponAFrame helper. + //static ImGuiOnceUponAFrame once; + //for (int i = 0; i < 5; i++) + // if (once) + // ImGui::Text("This will be displayed only once."); + ImGui::TreePop(); } +} - // Testing ImGuiOnceUponAFrame helper. - //static ImGuiOnceUponAFrame once; - //for (int i = 0; i < 5; i++) - // if (once) - // ImGui::Text("This will be displayed only once."); +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsBullets() +//----------------------------------------------------------------------------- - if (ImGui::TreeNode("Trees")) +static void DemoWindowWidgetsBullets() +{ + IMGUI_DEMO_MARKER("Widgets/Bullets"); + if (ImGui::TreeNode("Bullets")) { - if (ImGui::TreeNode("Basic trees")) + ImGui::BulletText("Bullet point 1"); + ImGui::BulletText("Bullet point 2\nOn multiple lines"); + if (ImGui::TreeNode("Tree node")) { - for (int i = 0; i < 5; i++) - { - // Use SetNextItemOpen() so set the default state of a node to be open. - // We could also use TreeNodeEx() with the ImGuiTreeNodeFlags_DefaultOpen flag to achieve the same thing! - if (i == 0) - ImGui::SetNextItemOpen(true, ImGuiCond_Once); - - if (ImGui::TreeNode((void*)(intptr_t)i, "Child %d", i)) - { - ImGui::Text("blah blah"); - ImGui::SameLine(); - if (ImGui::SmallButton("button")) {}; - ImGui::TreePop(); - } - } - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Advanced, with Selectable nodes")) - { - HelpMarker("This is a more typical looking tree with selectable nodes.\nClick to select, CTRL+Click to toggle, click on arrows or double-click to open."); - static bool align_label_with_current_x_position = false; - ImGui::Checkbox("Align label with current X position)", &align_label_with_current_x_position); - ImGui::Text("Hello!"); - if (align_label_with_current_x_position) - ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing()); - - static int selection_mask = (1 << 2); // Dumb representation of what may be user-side selection state. You may carry selection state inside or outside your objects in whatever format you see fit. - int node_clicked = -1; // Temporary storage of what node we have clicked to process selection at the end of the loop. May be a pointer to your own node type, etc. - ImGui::PushStyleVar(ImGuiStyleVar_IndentSpacing, ImGui::GetFontSize()*3); // Increase spacing to differentiate leaves from expanded contents. - for (int i = 0; i < 6; i++) - { - // Disable the default open on single-click behavior and pass in Selected flag according to our selection state. - ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; - if (selection_mask & (1 << i)) - node_flags |= ImGuiTreeNodeFlags_Selected; - if (i < 3) - { - // Items 0..2 are Tree Node - bool node_open = ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Node %d", i); - if (ImGui::IsItemClicked()) - node_clicked = i; - if (node_open) - { - ImGui::Text("Blah blah\nBlah Blah"); - ImGui::TreePop(); - } - } - else - { - // Items 3..5 are Tree Leaves - // The only reason we use TreeNode at all is to allow selection of the leaf. - // Otherwise we can use BulletText() or TreeAdvanceToLabelPos()+Text(). - node_flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; // ImGuiTreeNodeFlags_Bullet - ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Leaf %d", i); - if (ImGui::IsItemClicked()) - node_clicked = i; - } - } - if (node_clicked != -1) - { - // Update selection state. Process outside of tree loop to avoid visual inconsistencies during the clicking-frame. - if (ImGui::GetIO().KeyCtrl) - selection_mask ^= (1 << node_clicked); // CTRL+click to toggle - else //if (!(selection_mask & (1 << node_clicked))) // Depending on selection behavior you want, this commented bit preserve selection when clicking on item that is part of the selection - selection_mask = (1 << node_clicked); // Click to single-select - } - ImGui::PopStyleVar(); - if (align_label_with_current_x_position) - ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing()); + ImGui::BulletText("Another bullet point"); ImGui::TreePop(); } + ImGui::Bullet(); ImGui::Text("Bullet point 3 (two calls)"); + ImGui::Bullet(); ImGui::SmallButton("Button"); ImGui::TreePop(); } +} +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsCollapsingHeaders() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsCollapsingHeaders() +{ + IMGUI_DEMO_MARKER("Widgets/Collapsing Headers"); if (ImGui::TreeNode("Collapsing Headers")) { static bool closable_group = true; ImGui::Checkbox("Show 2nd header", &closable_group); - if (ImGui::CollapsingHeader("Header")) + if (ImGui::CollapsingHeader("Header", ImGuiTreeNodeFlags_None)) { ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); for (int i = 0; i < 5; i++) @@ -668,484 +1115,79 @@ static void ShowDemoWindowWidgets() for (int i = 0; i < 5; i++) ImGui::Text("More content %d", i); } + /* + if (ImGui::CollapsingHeader("Header with a bullet", ImGuiTreeNodeFlags_Bullet)) + ImGui::Text("IsItemHovered: %d", ImGui::IsItemHovered()); + */ ImGui::TreePop(); } +} - if (ImGui::TreeNode("Bullets")) - { - ImGui::BulletText("Bullet point 1"); - ImGui::BulletText("Bullet point 2\nOn multiple lines"); - ImGui::Bullet(); ImGui::Text("Bullet point 3 (two calls)"); - ImGui::Bullet(); ImGui::SmallButton("Button"); - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Text")) - { - if (ImGui::TreeNode("Colored Text")) - { - // Using shortcut. You can use PushStyleColor()/PopStyleColor() for more flexibility. - ImGui::TextColored(ImVec4(1.0f,0.0f,1.0f,1.0f), "Pink"); - ImGui::TextColored(ImVec4(1.0f,1.0f,0.0f,1.0f), "Yellow"); - ImGui::TextDisabled("Disabled"); - ImGui::SameLine(); HelpMarker("The TextDisabled color is stored in ImGuiStyle."); - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Word Wrapping")) - { - // Using shortcut. You can use PushTextWrapPos()/PopTextWrapPos() for more flexibility. - ImGui::TextWrapped("This text should automatically wrap on the edge of the window. The current implementation for text wrapping follows simple rules suitable for English and possibly other languages."); - ImGui::Spacing(); - - static float wrap_width = 200.0f; - ImGui::SliderFloat("Wrap width", &wrap_width, -20, 600, "%.0f"); - - ImGui::Text("Test paragraph 1:"); - ImVec2 pos = ImGui::GetCursorScreenPos(); - ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(pos.x + wrap_width, pos.y), ImVec2(pos.x + wrap_width + 10, pos.y + ImGui::GetTextLineHeight()), IM_COL32(255,0,255,255)); - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap_width); - ImGui::Text("The lazy dog is a good dog. This paragraph is made to fit within %.0f pixels. Testing a 1 character word. The quick brown fox jumps over the lazy dog.", wrap_width); - ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255,255,0,255)); - ImGui::PopTextWrapPos(); - - ImGui::Text("Test paragraph 2:"); - pos = ImGui::GetCursorScreenPos(); - ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(pos.x + wrap_width, pos.y), ImVec2(pos.x + wrap_width + 10, pos.y + ImGui::GetTextLineHeight()), IM_COL32(255,0,255,255)); - ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap_width); - ImGui::Text("aaaaaaaa bbbbbbbb, c cccccccc,dddddddd. d eeeeeeee ffffffff. gggggggg!hhhhhhhh"); - ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255,255,0,255)); - ImGui::PopTextWrapPos(); - - ImGui::TreePop(); - } - - if (ImGui::TreeNode("UTF-8 Text")) - { - // UTF-8 test with Japanese characters - // (Needs a suitable font, try Noto, or Arial Unicode, or M+ fonts. Read misc/fonts/README.txt for details.) - // - From C++11 you can use the u8"my text" syntax to encode literal strings as UTF-8 - // - For earlier compiler, you may be able to encode your sources as UTF-8 (e.g. Visual Studio save your file as 'UTF-8 without signature') - // - FOR THIS DEMO FILE ONLY, BECAUSE WE WANT TO SUPPORT OLD COMPILERS, WE ARE *NOT* INCLUDING RAW UTF-8 CHARACTERS IN THIS SOURCE FILE. - // Instead we are encoding a few strings with hexadecimal constants. Don't do this in your application! - // Please use u8"text in any language" in your application! - // Note that characters values are preserved even by InputText() if the font cannot be displayed, so you can safely copy & paste garbled characters into another application. - ImGui::TextWrapped("CJK text will only appears if the font was loaded with the appropriate CJK character ranges. Call io.Font->AddFontFromFileTTF() manually to load extra character ranges. Read misc/fonts/README.txt for details."); - ImGui::Text("Hiragana: \xe3\x81\x8b\xe3\x81\x8d\xe3\x81\x8f\xe3\x81\x91\xe3\x81\x93 (kakikukeko)"); // Normally we would use u8"blah blah" with the proper characters directly in the string. - ImGui::Text("Kanjis: \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e (nihongo)"); - static char buf[32] = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; - //static char buf[32] = u8"NIHONGO"; // <- this is how you would write it with C++11, using real kanjis - ImGui::InputText("UTF-8 input", buf, IM_ARRAYSIZE(buf)); - ImGui::TreePop(); - } - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Images")) - { - ImGuiIO& io = ImGui::GetIO(); - ImGui::TextWrapped("Below we are displaying the font texture (which is the only texture we have access to in this demo). Use the 'ImTextureID' type as storage to pass pointers or identifier to your own texture data. Hover the texture for a zoomed view!"); - - // Here we are grabbing the font texture because that's the only one we have access to inside the demo code. - // Remember that ImTextureID is just storage for whatever you want it to be, it is essentially a value that will be passed to the render function inside the ImDrawCmd structure. - // If you use one of the default imgui_impl_XXXX.cpp renderer, they all have comments at the top of their file to specify what they expect to be stored in ImTextureID. - // (for example, the imgui_impl_dx11.cpp renderer expect a 'ID3D11ShaderResourceView*' pointer. The imgui_impl_glfw_gl3.cpp renderer expect a GLuint OpenGL texture identifier etc.) - // If you decided that ImTextureID = MyEngineTexture*, then you can pass your MyEngineTexture* pointers to ImGui::Image(), and gather width/height through your own functions, etc. - // Using ShowMetricsWindow() as a "debugger" to inspect the draw data that are being passed to your render will help you debug issues if you are confused about this. - // Consider using the lower-level ImDrawList::AddImage() API, via ImGui::GetWindowDrawList()->AddImage(). - ImTextureID my_tex_id = io.Fonts->TexID; - float my_tex_w = (float)io.Fonts->TexWidth; - float my_tex_h = (float)io.Fonts->TexHeight; - - ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h); - ImVec2 pos = ImGui::GetCursorScreenPos(); - ImGui::Image(my_tex_id, ImVec2(my_tex_w, my_tex_h), ImVec2(0,0), ImVec2(1,1), ImVec4(1.0f,1.0f,1.0f,1.0f), ImVec4(1.0f,1.0f,1.0f,0.5f)); - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - float region_sz = 32.0f; - float region_x = io.MousePos.x - pos.x - region_sz * 0.5f; if (region_x < 0.0f) region_x = 0.0f; else if (region_x > my_tex_w - region_sz) region_x = my_tex_w - region_sz; - float region_y = io.MousePos.y - pos.y - region_sz * 0.5f; if (region_y < 0.0f) region_y = 0.0f; else if (region_y > my_tex_h - region_sz) region_y = my_tex_h - region_sz; - float zoom = 4.0f; - ImGui::Text("Min: (%.2f, %.2f)", region_x, region_y); - ImGui::Text("Max: (%.2f, %.2f)", region_x + region_sz, region_y + region_sz); - ImVec2 uv0 = ImVec2((region_x) / my_tex_w, (region_y) / my_tex_h); - ImVec2 uv1 = ImVec2((region_x + region_sz) / my_tex_w, (region_y + region_sz) / my_tex_h); - ImGui::Image(my_tex_id, ImVec2(region_sz * zoom, region_sz * zoom), uv0, uv1, ImVec4(1.0f, 1.0f, 1.0f, 1.0f), ImVec4(1.0f, 1.0f, 1.0f, 0.5f)); - ImGui::EndTooltip(); - } - ImGui::TextWrapped("And now some textured buttons.."); - static int pressed_count = 0; - for (int i = 0; i < 8; i++) - { - ImGui::PushID(i); - int frame_padding = -1 + i; // -1 = uses default padding - if (ImGui::ImageButton(my_tex_id, ImVec2(32,32), ImVec2(0,0), ImVec2(32.0f/my_tex_w,32/my_tex_h), frame_padding, ImVec4(0.0f,0.0f,0.0f,1.0f))) - pressed_count += 1; - ImGui::PopID(); - ImGui::SameLine(); - } - ImGui::NewLine(); - ImGui::Text("Pressed %d times.", pressed_count); - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Combo")) - { - // Expose flags as checkbox for the demo - static ImGuiComboFlags flags = 0; - ImGui::CheckboxFlags("ImGuiComboFlags_PopupAlignLeft", (unsigned int*)&flags, ImGuiComboFlags_PopupAlignLeft); - ImGui::SameLine(); HelpMarker("Only makes a difference if the popup is larger than the combo"); - if (ImGui::CheckboxFlags("ImGuiComboFlags_NoArrowButton", (unsigned int*)&flags, ImGuiComboFlags_NoArrowButton)) - flags &= ~ImGuiComboFlags_NoPreview; // Clear the other flag, as we cannot combine both - if (ImGui::CheckboxFlags("ImGuiComboFlags_NoPreview", (unsigned int*)&flags, ImGuiComboFlags_NoPreview)) - flags &= ~ImGuiComboFlags_NoArrowButton; // Clear the other flag, as we cannot combine both - - // General BeginCombo() API, you have full control over your selection data and display type. - // (your selection data could be an index, a pointer to the object, an id for the object, a flag stored in the object itself, etc.) - const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" }; - static const char* item_current = items[0]; // Here our selection is a single pointer stored outside the object. - if (ImGui::BeginCombo("combo 1", item_current, flags)) // The second parameter is the label previewed before opening the combo. - { - for (int n = 0; n < IM_ARRAYSIZE(items); n++) - { - bool is_selected = (item_current == items[n]); - if (ImGui::Selectable(items[n], is_selected)) - item_current = items[n]; - if (is_selected) - ImGui::SetItemDefaultFocus(); // Set the initial focus when opening the combo (scrolling + for keyboard navigation support in the upcoming navigation branch) - } - ImGui::EndCombo(); - } - - // Simplified one-liner Combo() API, using values packed in a single constant string - static int item_current_2 = 0; - ImGui::Combo("combo 2 (one-liner)", &item_current_2, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0"); - - // Simplified one-liner Combo() using an array of const char* - static int item_current_3 = -1; // If the selection isn't within 0..count, Combo won't display a preview - ImGui::Combo("combo 3 (array)", &item_current_3, items, IM_ARRAYSIZE(items)); - - // Simplified one-liner Combo() using an accessor function - struct FuncHolder { static bool ItemGetter(void* data, int idx, const char** out_str) { *out_str = ((const char**)data)[idx]; return true; } }; - static int item_current_4 = 0; - ImGui::Combo("combo 4 (function)", &item_current_4, &FuncHolder::ItemGetter, items, IM_ARRAYSIZE(items)); - - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Selectables")) - { - // Selectable() has 2 overloads: - // - The one taking "bool selected" as a read-only selection information. When Selectable() has been clicked is returns true and you can alter selection state accordingly. - // - The one taking "bool* p_selected" as a read-write selection information (convenient in some cases) - // The earlier is more flexible, as in real application your selection may be stored in a different manner (in flags within objects, as an external list, etc). - if (ImGui::TreeNode("Basic")) - { - static bool selection[5] = { false, true, false, false, false }; - ImGui::Selectable("1. I am selectable", &selection[0]); - ImGui::Selectable("2. I am selectable", &selection[1]); - ImGui::Text("3. I am not selectable"); - ImGui::Selectable("4. I am selectable", &selection[3]); - if (ImGui::Selectable("5. I am double clickable", selection[4], ImGuiSelectableFlags_AllowDoubleClick)) - if (ImGui::IsMouseDoubleClicked(0)) - selection[4] = !selection[4]; - ImGui::TreePop(); - } - if (ImGui::TreeNode("Selection State: Single Selection")) - { - static int selected = -1; - for (int n = 0; n < 5; n++) - { - char buf[32]; - sprintf(buf, "Object %d", n); - if (ImGui::Selectable(buf, selected == n)) - selected = n; - } - ImGui::TreePop(); - } - if (ImGui::TreeNode("Selection State: Multiple Selection")) - { - HelpMarker("Hold CTRL and click to select multiple items."); - static bool selection[5] = { false, false, false, false, false }; - for (int n = 0; n < 5; n++) - { - char buf[32]; - sprintf(buf, "Object %d", n); - if (ImGui::Selectable(buf, selection[n])) - { - if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held - memset(selection, 0, sizeof(selection)); - selection[n] ^= 1; - } - } - ImGui::TreePop(); - } - if (ImGui::TreeNode("Rendering more text into the same line")) - { - // Using the Selectable() override that takes "bool* p_selected" parameter and toggle your booleans automatically. - static bool selected[3] = { false, false, false }; - ImGui::Selectable("main.c", &selected[0]); ImGui::SameLine(300); ImGui::Text(" 2,345 bytes"); - ImGui::Selectable("Hello.cpp", &selected[1]); ImGui::SameLine(300); ImGui::Text("12,345 bytes"); - ImGui::Selectable("Hello.h", &selected[2]); ImGui::SameLine(300); ImGui::Text(" 2,345 bytes"); - ImGui::TreePop(); - } - if (ImGui::TreeNode("In columns")) - { - ImGui::Columns(3, NULL, false); - static bool selected[16] = { 0 }; - for (int i = 0; i < 16; i++) - { - char label[32]; sprintf(label, "Item %d", i); - if (ImGui::Selectable(label, &selected[i])) {} - ImGui::NextColumn(); - } - ImGui::Columns(1); - ImGui::TreePop(); - } - if (ImGui::TreeNode("Grid")) - { - static bool selected[4*4] = { true, false, false, false, false, true, false, false, false, false, true, false, false, false, false, true }; - for (int i = 0; i < 4*4; i++) - { - ImGui::PushID(i); - if (ImGui::Selectable("Sailor", &selected[i], 0, ImVec2(50,50))) - { - // Note: We _unnecessarily_ test for both x/y and i here only to silence some static analyzer. The second part of each test is unnecessary. - int x = i % 4; - int y = i / 4; - if (x > 0) { selected[i - 1] ^= 1; } - if (x < 3 && i < 15) { selected[i + 1] ^= 1; } - if (y > 0 && i > 3) { selected[i - 4] ^= 1; } - if (y < 3 && i < 12) { selected[i + 4] ^= 1; } - } - if ((i % 4) < 3) ImGui::SameLine(); - ImGui::PopID(); - } - ImGui::TreePop(); - } - if (ImGui::TreeNode("Alignment")) - { - HelpMarker("Alignment applies when a selectable is larger than its text content.\nBy default, Selectables uses style.SelectableTextAlign but it can be overriden on a per-item basis using PushStyleVar()."); - static bool selected[3*3] = { true, false, true, false, true, false, true, false, true }; - for (int y = 0; y < 3; y++) - { - for (int x = 0; x < 3; x++) - { - ImVec2 alignment = ImVec2((float)x / 2.0f, (float)y / 2.0f); - char name[32]; - sprintf(name, "(%.1f,%.1f)", alignment.x, alignment.y); - if (x > 0) ImGui::SameLine(); - ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, alignment); - ImGui::Selectable(name, &selected[3*y+x], ImGuiSelectableFlags_None, ImVec2(80,80)); - ImGui::PopStyleVar(); - } - } - ImGui::TreePop(); - } - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Text Input")) - { - if (ImGui::TreeNode("Multi-line Text Input")) - { - // Note: we are using a fixed-sized buffer for simplicity here. See ImGuiInputTextFlags_CallbackResize - // and the code in misc/cpp/imgui_stdlib.h for how to setup InputText() for dynamically resizing strings. - static char text[1024 * 16] = - "/*\n" - " The Pentium F00F bug, shorthand for F0 0F C7 C8,\n" - " the hexadecimal encoding of one offending instruction,\n" - " more formally, the invalid operand with locked CMPXCHG8B\n" - " instruction bug, is a design flaw in the majority of\n" - " Intel Pentium, Pentium MMX, and Pentium OverDrive\n" - " processors (all in the P5 microarchitecture).\n" - "*/\n\n" - "label:\n" - "\tlock cmpxchg8b eax\n"; - - static ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput; - HelpMarker("You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputTextMultiline() to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example. (This is not demonstrated in imgui_demo.cpp)"); - ImGui::CheckboxFlags("ImGuiInputTextFlags_ReadOnly", (unsigned int*)&flags, ImGuiInputTextFlags_ReadOnly); - ImGui::CheckboxFlags("ImGuiInputTextFlags_AllowTabInput", (unsigned int*)&flags, ImGuiInputTextFlags_AllowTabInput); - ImGui::CheckboxFlags("ImGuiInputTextFlags_CtrlEnterForNewLine", (unsigned int*)&flags, ImGuiInputTextFlags_CtrlEnterForNewLine); - ImGui::InputTextMultiline("##source", text, IM_ARRAYSIZE(text), ImVec2(-1.0f, ImGui::GetTextLineHeight() * 16), flags); - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Filtered Text Input")) - { - static char buf1[64] = ""; ImGui::InputText("default", buf1, 64); - static char buf2[64] = ""; ImGui::InputText("decimal", buf2, 64, ImGuiInputTextFlags_CharsDecimal); - static char buf3[64] = ""; ImGui::InputText("hexadecimal", buf3, 64, ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase); - static char buf4[64] = ""; ImGui::InputText("uppercase", buf4, 64, ImGuiInputTextFlags_CharsUppercase); - static char buf5[64] = ""; ImGui::InputText("no blank", buf5, 64, ImGuiInputTextFlags_CharsNoBlank); - struct TextFilters { static int FilterImGuiLetters(ImGuiInputTextCallbackData* data) { if (data->EventChar < 256 && strchr("imgui", (char)data->EventChar)) return 0; return 1; } }; - static char buf6[64] = ""; ImGui::InputText("\"imgui\" letters", buf6, 64, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterImGuiLetters); - - ImGui::Text("Password input"); - static char bufpass[64] = "password123"; - ImGui::InputText("password", bufpass, 64, ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsNoBlank); - ImGui::SameLine(); HelpMarker("Display all characters as '*'.\nDisable clipboard cut and copy.\nDisable logging.\n"); - ImGui::InputTextWithHint("password (w/ hint)", "", bufpass, 64, ImGuiInputTextFlags_Password | ImGuiInputTextFlags_CharsNoBlank); - ImGui::InputText("password (clear)", bufpass, 64, ImGuiInputTextFlags_CharsNoBlank); - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Resize Callback")) - { - // If you have a custom string type you would typically create a ImGui::InputText() wrapper than takes your type as input. - // See misc/cpp/imgui_stdlib.h and .cpp for an implementation of this using std::string. - HelpMarker("Demonstrate using ImGuiInputTextFlags_CallbackResize to wire your resizable string type to InputText().\n\nSee misc/cpp/imgui_stdlib.h for an implementation of this for std::string."); - struct Funcs - { - static int MyResizeCallback(ImGuiInputTextCallbackData* data) - { - if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) - { - ImVector* my_str = (ImVector*)data->UserData; - IM_ASSERT(my_str->begin() == data->Buf); - my_str->resize(data->BufSize); // NB: On resizing calls, generally data->BufSize == data->BufTextLen + 1 - data->Buf = my_str->begin(); - } - return 0; - } - - // Tip: Because ImGui:: is a namespace you would typicall add your own function into the namespace in your own source files. - // For example, you may add a function called ImGui::InputText(const char* label, MyString* my_str). - static bool MyInputTextMultiline(const char* label, ImVector* my_str, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0) - { - IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); - return ImGui::InputTextMultiline(label, my_str->begin(), (size_t)my_str->size(), size, flags | ImGuiInputTextFlags_CallbackResize, Funcs::MyResizeCallback, (void*)my_str); - } - }; - - // For this demo we are using ImVector as a string container. - // Note that because we need to store a terminating zero character, our size/capacity are 1 more than usually reported by a typical string class. - static ImVector my_str; - if (my_str.empty()) - my_str.push_back(0); - Funcs::MyInputTextMultiline("##MyStr", &my_str, ImVec2(-1.0f, ImGui::GetTextLineHeight() * 16)); - ImGui::Text("Data: %p\nSize: %d\nCapacity: %d", (void*)my_str.begin(), my_str.size(), my_str.capacity()); - ImGui::TreePop(); - } - - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Plots Widgets")) - { - static bool animate = true; - ImGui::Checkbox("Animate", &animate); - - static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; - ImGui::PlotLines("Frame Times", arr, IM_ARRAYSIZE(arr)); - - // Create a dummy array of contiguous float values to plot - // Tip: If your float aren't contiguous but part of a structure, you can pass a pointer to your first float and the sizeof() of your structure in the Stride parameter. - static float values[90] = { 0 }; - static int values_offset = 0; - static double refresh_time = 0.0; - if (!animate || refresh_time == 0.0) - refresh_time = ImGui::GetTime(); - while (refresh_time < ImGui::GetTime()) // Create dummy data at fixed 60 hz rate for the demo - { - static float phase = 0.0f; - values[values_offset] = cosf(phase); - values_offset = (values_offset+1) % IM_ARRAYSIZE(values); - phase += 0.10f*values_offset; - refresh_time += 1.0f/60.0f; - } - ImGui::PlotLines("Lines", values, IM_ARRAYSIZE(values), values_offset, "avg 0.0", -1.0f, 1.0f, ImVec2(0,80)); - ImGui::PlotHistogram("Histogram", arr, IM_ARRAYSIZE(arr), 0, NULL, 0.0f, 1.0f, ImVec2(0,80)); - - // Use functions to generate output - // FIXME: This is rather awkward because current plot API only pass in indices. We probably want an API passing floats and user provide sample rate/count. - struct Funcs - { - static float Sin(void*, int i) { return sinf(i * 0.1f); } - static float Saw(void*, int i) { return (i & 1) ? 1.0f : -1.0f; } - }; - static int func_type = 0, display_count = 70; - ImGui::Separator(); - ImGui::SetNextItemWidth(100); - ImGui::Combo("func", &func_type, "Sin\0Saw\0"); - ImGui::SameLine(); - ImGui::SliderInt("Sample count", &display_count, 1, 400); - float (*func)(void*, int) = (func_type == 0) ? Funcs::Sin : Funcs::Saw; - ImGui::PlotLines("Lines", func, NULL, display_count, 0, NULL, -1.0f, 1.0f, ImVec2(0,80)); - ImGui::PlotHistogram("Histogram", func, NULL, display_count, 0, NULL, -1.0f, 1.0f, ImVec2(0,80)); - ImGui::Separator(); - - // Animate a simple progress bar - static float progress = 0.0f, progress_dir = 1.0f; - if (animate) - { - progress += progress_dir * 0.4f * ImGui::GetIO().DeltaTime; - if (progress >= +1.1f) { progress = +1.1f; progress_dir *= -1.0f; } - if (progress <= -0.1f) { progress = -0.1f; progress_dir *= -1.0f; } - } - - // Typically we would use ImVec2(-1.0f,0.0f) to use all available width, or ImVec2(width,0.0f) for a specified width. ImVec2(0.0f,0.0f) uses ItemWidth. - ImGui::ProgressBar(progress, ImVec2(0.0f,0.0f)); - ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); - ImGui::Text("Progress Bar"); - - float progress_saturated = (progress < 0.0f) ? 0.0f : (progress > 1.0f) ? 1.0f : progress; - char buf[32]; - sprintf(buf, "%d/%d", (int)(progress_saturated*1753), 1753); - ImGui::ProgressBar(progress, ImVec2(0.f,0.f), buf); - ImGui::TreePop(); - } +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsColorAndPickers() +//----------------------------------------------------------------------------- +static void DemoWindowWidgetsColorAndPickers() +{ + IMGUI_DEMO_MARKER("Widgets/Color"); if (ImGui::TreeNode("Color/Picker Widgets")) { - static ImVec4 color = ImVec4(114.0f/255.0f, 144.0f/255.0f, 154.0f/255.0f, 200.0f/255.0f); + static ImVec4 color = ImVec4(114.0f / 255.0f, 144.0f / 255.0f, 154.0f / 255.0f, 200.0f / 255.0f); + static ImGuiColorEditFlags base_flags = ImGuiColorEditFlags_None; - static bool alpha_preview = true; - static bool alpha_half_preview = false; - static bool drag_and_drop = true; - static bool options_menu = true; - static bool hdr = false; - ImGui::Checkbox("With Alpha Preview", &alpha_preview); - ImGui::Checkbox("With Half Alpha Preview", &alpha_half_preview); - ImGui::Checkbox("With Drag and Drop", &drag_and_drop); - ImGui::Checkbox("With Options Menu", &options_menu); ImGui::SameLine(); HelpMarker("Right-click on the individual color widget to show options."); - ImGui::Checkbox("With HDR", &hdr); ImGui::SameLine(); HelpMarker("Currently all this does is to lift the 0..1 limits on dragging widgets."); - int misc_flags = (hdr ? ImGuiColorEditFlags_HDR : 0) | (drag_and_drop ? 0 : ImGuiColorEditFlags_NoDragDrop) | (alpha_half_preview ? ImGuiColorEditFlags_AlphaPreviewHalf : (alpha_preview ? ImGuiColorEditFlags_AlphaPreview : 0)) | (options_menu ? 0 : ImGuiColorEditFlags_NoOptions); + ImGui::SeparatorText("Options"); + ImGui::CheckboxFlags("ImGuiColorEditFlags_NoAlpha", &base_flags, ImGuiColorEditFlags_NoAlpha); + ImGui::CheckboxFlags("ImGuiColorEditFlags_AlphaOpaque", &base_flags, ImGuiColorEditFlags_AlphaOpaque); + ImGui::CheckboxFlags("ImGuiColorEditFlags_AlphaNoBg", &base_flags, ImGuiColorEditFlags_AlphaNoBg); + ImGui::CheckboxFlags("ImGuiColorEditFlags_AlphaPreviewHalf", &base_flags, ImGuiColorEditFlags_AlphaPreviewHalf); + ImGui::CheckboxFlags("ImGuiColorEditFlags_NoDragDrop", &base_flags, ImGuiColorEditFlags_NoDragDrop); + ImGui::CheckboxFlags("ImGuiColorEditFlags_NoOptions", &base_flags, ImGuiColorEditFlags_NoOptions); ImGui::SameLine(); HelpMarker("Right-click on the individual color widget to show options."); + ImGui::CheckboxFlags("ImGuiColorEditFlags_HDR", &base_flags, ImGuiColorEditFlags_HDR); ImGui::SameLine(); HelpMarker("Currently all this does is to lift the 0..1 limits on dragging widgets."); + IMGUI_DEMO_MARKER("Widgets/Color/ColorEdit"); + ImGui::SeparatorText("Inline color editor"); ImGui::Text("Color widget:"); - ImGui::SameLine(); HelpMarker("Click on the colored square to open a color picker.\nCTRL+click on individual component to input value.\n"); - ImGui::ColorEdit3("MyColor##1", (float*)&color, misc_flags); + ImGui::SameLine(); HelpMarker( + "Click on the color square to open a color picker.\n" + "CTRL+click on individual component to input value.\n"); + ImGui::ColorEdit3("MyColor##1", (float*)&color, base_flags); + IMGUI_DEMO_MARKER("Widgets/Color/ColorEdit (HSV, with Alpha)"); ImGui::Text("Color widget HSV with Alpha:"); - ImGui::ColorEdit4("MyColor##2", (float*)&color, ImGuiColorEditFlags_DisplayHSV | misc_flags); + ImGui::ColorEdit4("MyColor##2", (float*)&color, ImGuiColorEditFlags_DisplayHSV | base_flags); + IMGUI_DEMO_MARKER("Widgets/Color/ColorEdit (float display)"); ImGui::Text("Color widget with Float Display:"); - ImGui::ColorEdit4("MyColor##2f", (float*)&color, ImGuiColorEditFlags_Float | misc_flags); + ImGui::ColorEdit4("MyColor##2f", (float*)&color, ImGuiColorEditFlags_Float | base_flags); + IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (with Picker)"); ImGui::Text("Color button with Picker:"); - ImGui::SameLine(); HelpMarker("With the ImGuiColorEditFlags_NoInputs flag you can hide all the slider/text inputs.\nWith the ImGuiColorEditFlags_NoLabel flag you can pass a non-empty label which will only be used for the tooltip and picker popup."); - ImGui::ColorEdit4("MyColor##3", (float*)&color, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | misc_flags); + ImGui::SameLine(); HelpMarker( + "With the ImGuiColorEditFlags_NoInputs flag you can hide all the slider/text inputs.\n" + "With the ImGuiColorEditFlags_NoLabel flag you can pass a non-empty label which will only " + "be used for the tooltip and picker popup."); + ImGui::ColorEdit4("MyColor##3", (float*)&color, ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel | base_flags); + IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (with custom Picker popup)"); ImGui::Text("Color button with Custom Picker Popup:"); - // Generate a dummy default palette. The palette will persist and can be edited. + // Generate a default palette. The palette will persist and can be edited. static bool saved_palette_init = true; - static ImVec4 saved_palette[32] = { }; + static ImVec4 saved_palette[32] = {}; if (saved_palette_init) { for (int n = 0; n < IM_ARRAYSIZE(saved_palette); n++) { - ImGui::ColorConvertHSVtoRGB(n / 31.0f, 0.8f, 0.8f, saved_palette[n].x, saved_palette[n].y, saved_palette[n].z); + ImGui::ColorConvertHSVtoRGB(n / 31.0f, 0.8f, 0.8f, + saved_palette[n].x, saved_palette[n].y, saved_palette[n].z); saved_palette[n].w = 1.0f; // Alpha } saved_palette_init = false; } static ImVec4 backup_color; - bool open_popup = ImGui::ColorButton("MyColor##3b", color, misc_flags); - ImGui::SameLine(); + bool open_popup = ImGui::ColorButton("MyColor##3b", color, base_flags); + ImGui::SameLine(0, ImGui::GetStyle().ItemInnerSpacing.x); open_popup |= ImGui::Button("Palette"); if (open_popup) { @@ -1156,14 +1198,14 @@ static void ShowDemoWindowWidgets() { ImGui::Text("MY CUSTOM COLOR PICKER WITH AN AMAZING PALETTE!"); ImGui::Separator(); - ImGui::ColorPicker4("##picker", (float*)&color, misc_flags | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoSmallPreview); + ImGui::ColorPicker4("##picker", (float*)&color, base_flags | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoSmallPreview); ImGui::SameLine(); ImGui::BeginGroup(); // Lock X position ImGui::Text("Current"); - ImGui::ColorButton("##current", color, ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, ImVec2(60,40)); + ImGui::ColorButton("##current", color, ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, ImVec2(60, 40)); ImGui::Text("Previous"); - if (ImGui::ColorButton("##previous", backup_color, ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, ImVec2(60,40))) + if (ImGui::ColorButton("##previous", backup_color, ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_AlphaPreviewHalf, ImVec2(60, 40))) color = backup_color; ImGui::Separator(); ImGui::Text("Palette"); @@ -1172,11 +1214,13 @@ static void ShowDemoWindowWidgets() ImGui::PushID(n); if ((n % 8) != 0) ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y); - if (ImGui::ColorButton("##palette", saved_palette[n], ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip, ImVec2(20,20))) + + ImGuiColorEditFlags palette_button_flags = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker | ImGuiColorEditFlags_NoTooltip; + if (ImGui::ColorButton("##palette", saved_palette[n], palette_button_flags, ImVec2(20, 20))) color = ImVec4(saved_palette[n].x, saved_palette[n].y, saved_palette[n].z, color.w); // Preserve alpha! - // Allow user to drop colors into each palette entry - // (Note that ColorButton is already a drag source by default, unless using ImGuiColorEditFlags_NoDragDrop) + // Allow user to drop colors into each palette entry. Note that ColorButton() is already a + // drag source by default, unless specifying the ImGuiColorEditFlags_NoDragDrop flag. if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) @@ -1192,38 +1236,46 @@ static void ShowDemoWindowWidgets() ImGui::EndPopup(); } + IMGUI_DEMO_MARKER("Widgets/Color/ColorButton (simple)"); ImGui::Text("Color button only:"); - ImGui::ColorButton("MyColor##3c", *(ImVec4*)&color, misc_flags, ImVec2(80,80)); + static bool no_border = false; + ImGui::Checkbox("ImGuiColorEditFlags_NoBorder", &no_border); + ImGui::ColorButton("MyColor##3c", *(ImVec4*)&color, base_flags | (no_border ? ImGuiColorEditFlags_NoBorder : 0), ImVec2(80, 80)); + + IMGUI_DEMO_MARKER("Widgets/Color/ColorPicker"); + ImGui::SeparatorText("Color picker"); - ImGui::Text("Color picker:"); - static bool alpha = true; - static bool alpha_bar = true; - static bool side_preview = true; static bool ref_color = false; - static ImVec4 ref_color_v(1.0f,0.0f,1.0f,0.5f); - static int display_mode = 0; + static ImVec4 ref_color_v(1.0f, 0.0f, 1.0f, 0.5f); static int picker_mode = 0; - ImGui::Checkbox("With Alpha", &alpha); - ImGui::Checkbox("With Alpha Bar", &alpha_bar); - ImGui::Checkbox("With Side Preview", &side_preview); - if (side_preview) + static int display_mode = 0; + static ImGuiColorEditFlags color_picker_flags = ImGuiColorEditFlags_AlphaBar; + + ImGui::PushID("Color picker"); + ImGui::CheckboxFlags("ImGuiColorEditFlags_NoAlpha", &color_picker_flags, ImGuiColorEditFlags_NoAlpha); + ImGui::CheckboxFlags("ImGuiColorEditFlags_AlphaBar", &color_picker_flags, ImGuiColorEditFlags_AlphaBar); + ImGui::CheckboxFlags("ImGuiColorEditFlags_NoSidePreview", &color_picker_flags, ImGuiColorEditFlags_NoSidePreview); + if (color_picker_flags & ImGuiColorEditFlags_NoSidePreview) { ImGui::SameLine(); ImGui::Checkbox("With Ref Color", &ref_color); if (ref_color) { ImGui::SameLine(); - ImGui::ColorEdit4("##RefColor", &ref_color_v.x, ImGuiColorEditFlags_NoInputs | misc_flags); + ImGui::ColorEdit4("##RefColor", &ref_color_v.x, ImGuiColorEditFlags_NoInputs | base_flags); } } - ImGui::Combo("Display Mode", &display_mode, "Auto/Current\0None\0RGB Only\0HSV Only\0Hex Only\0"); - ImGui::SameLine(); HelpMarker("ColorEdit defaults to displaying RGB inputs if you don't specify a display mode, but the user can change it with a right-click.\n\nColorPicker defaults to displaying RGB+HSV+Hex if you don't specify a display mode.\n\nYou can change the defaults using SetColorEditOptions()."); - ImGui::Combo("Picker Mode", &picker_mode, "Auto/Current\0Hue bar + SV rect\0Hue wheel + SV triangle\0"); - ImGui::SameLine(); HelpMarker("User can right-click the picker to change mode."); - ImGuiColorEditFlags flags = misc_flags; - if (!alpha) flags |= ImGuiColorEditFlags_NoAlpha; // This is by default if you call ColorPicker3() instead of ColorPicker4() - if (alpha_bar) flags |= ImGuiColorEditFlags_AlphaBar; - if (!side_preview) flags |= ImGuiColorEditFlags_NoSidePreview; + + ImGui::Combo("Picker Mode", &picker_mode, "Auto/Current\0ImGuiColorEditFlags_PickerHueBar\0ImGuiColorEditFlags_PickerHueWheel\0"); + ImGui::SameLine(); HelpMarker("When not specified explicitly, user can right-click the picker to change mode."); + + ImGui::Combo("Display Mode", &display_mode, "Auto/Current\0ImGuiColorEditFlags_NoInputs\0ImGuiColorEditFlags_DisplayRGB\0ImGuiColorEditFlags_DisplayHSV\0ImGuiColorEditFlags_DisplayHex\0"); + ImGui::SameLine(); HelpMarker( + "ColorEdit defaults to displaying RGB inputs if you don't specify a display mode, " + "but the user can change it with a right-click on those inputs.\n\nColorPicker defaults to displaying RGB+HSV+Hex " + "if you don't specify a display mode.\n\nYou can change the defaults using SetColorEditOptions()."); + + ImGuiColorEditFlags flags = base_flags | color_picker_flags; if (picker_mode == 1) flags |= ImGuiColorEditFlags_PickerHueBar; if (picker_mode == 2) flags |= ImGuiColorEditFlags_PickerHueWheel; if (display_mode == 1) flags |= ImGuiColorEditFlags_NoInputs; // Disable all RGB/HSV/Hex displays @@ -1232,51 +1284,168 @@ static void ShowDemoWindowWidgets() if (display_mode == 4) flags |= ImGuiColorEditFlags_DisplayHex; ImGui::ColorPicker4("MyColor##4", (float*)&color, flags, ref_color ? &ref_color_v.x : NULL); - ImGui::Text("Programmatically set defaults:"); - ImGui::SameLine(); HelpMarker("SetColorEditOptions() is designed to allow you to set boot-time default.\nWe don't have Push/Pop functions because you can force options on a per-widget basis if needed, and the user can change non-forced ones with the options menu.\nWe don't have a getter to avoid encouraging you to persistently save values that aren't forward-compatible."); + ImGui::Text("Set defaults in code:"); + ImGui::SameLine(); HelpMarker( + "SetColorEditOptions() is designed to allow you to set boot-time default.\n" + "We don't have Push/Pop functions because you can force options on a per-widget basis if needed, " + "and the user can change non-forced ones with the options menu.\nWe don't have a getter to avoid " + "encouraging you to persistently save values that aren't forward-compatible."); if (ImGui::Button("Default: Uint8 + HSV + Hue Bar")) ImGui::SetColorEditOptions(ImGuiColorEditFlags_Uint8 | ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_PickerHueBar); if (ImGui::Button("Default: Float + HDR + Hue Wheel")) ImGui::SetColorEditOptions(ImGuiColorEditFlags_Float | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_PickerHueWheel); + // Always display a small version of both types of pickers + // (that's in order to make it more visible in the demo to people who are skimming quickly through it) + ImGui::Text("Both types:"); + float w = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.y) * 0.40f; + ImGui::SetNextItemWidth(w); + ImGui::ColorPicker3("##MyColor##5", (float*)&color, ImGuiColorEditFlags_PickerHueBar | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoAlpha); + ImGui::SameLine(); + ImGui::SetNextItemWidth(w); + ImGui::ColorPicker3("##MyColor##6", (float*)&color, ImGuiColorEditFlags_PickerHueWheel | ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoAlpha); + ImGui::PopID(); + // HSV encoded support (to avoid RGB<>HSV round trips and singularities when S==0 or V==0) - static ImVec4 color_stored_as_hsv(0.23f, 1.0f, 1.0f, 1.0f); + static ImVec4 color_hsv(0.23f, 1.0f, 1.0f, 1.0f); // Stored as HSV! ImGui::Spacing(); ImGui::Text("HSV encoded colors"); - ImGui::SameLine(); HelpMarker("By default, colors are given to ColorEdit and ColorPicker in RGB, but ImGuiColorEditFlags_InputHSV allows you to store colors as HSV and pass them to ColorEdit and ColorPicker as HSV. This comes with the added benefit that you can manipulate hue values with the picker even when saturation or value are zero."); + ImGui::SameLine(); HelpMarker( + "By default, colors are given to ColorEdit and ColorPicker in RGB, but ImGuiColorEditFlags_InputHSV " + "allows you to store colors as HSV and pass them to ColorEdit and ColorPicker as HSV. This comes with the " + "added benefit that you can manipulate hue values with the picker even when saturation or value are zero."); ImGui::Text("Color widget with InputHSV:"); - ImGui::ColorEdit4("HSV shown as RGB##1", (float*)&color_stored_as_hsv, ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_InputHSV | ImGuiColorEditFlags_Float); - ImGui::ColorEdit4("HSV shown as HSV##1", (float*)&color_stored_as_hsv, ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_InputHSV | ImGuiColorEditFlags_Float); - ImGui::DragFloat4("Raw HSV values", (float*)&color_stored_as_hsv, 0.01f, 0.0f, 1.0f); + ImGui::ColorEdit4("HSV shown as RGB##1", (float*)&color_hsv, ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_InputHSV | ImGuiColorEditFlags_Float); + ImGui::ColorEdit4("HSV shown as HSV##1", (float*)&color_hsv, ImGuiColorEditFlags_DisplayHSV | ImGuiColorEditFlags_InputHSV | ImGuiColorEditFlags_Float); + ImGui::DragFloat4("Raw HSV values", (float*)&color_hsv, 0.01f, 0.0f, 1.0f); ImGui::TreePop(); } +} - if (ImGui::TreeNode("Range Widgets")) +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsComboBoxes() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsComboBoxes() +{ + IMGUI_DEMO_MARKER("Widgets/Combo"); + if (ImGui::TreeNode("Combo")) { - static float begin = 10, end = 90; - static int begin_i = 100, end_i = 1000; - ImGui::DragFloatRange2("range", &begin, &end, 0.25f, 0.0f, 100.0f, "Min: %.1f %%", "Max: %.1f %%"); - ImGui::DragIntRange2("range int (no bounds)", &begin_i, &end_i, 5, 0, 0, "Min: %d units", "Max: %d units"); + // Combo Boxes are also called "Dropdown" in other systems + // Expose flags as checkbox for the demo + static ImGuiComboFlags flags = 0; + ImGui::CheckboxFlags("ImGuiComboFlags_PopupAlignLeft", &flags, ImGuiComboFlags_PopupAlignLeft); + ImGui::SameLine(); HelpMarker("Only makes a difference if the popup is larger than the combo"); + if (ImGui::CheckboxFlags("ImGuiComboFlags_NoArrowButton", &flags, ImGuiComboFlags_NoArrowButton)) + flags &= ~ImGuiComboFlags_NoPreview; // Clear incompatible flags + if (ImGui::CheckboxFlags("ImGuiComboFlags_NoPreview", &flags, ImGuiComboFlags_NoPreview)) + flags &= ~(ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_WidthFitPreview); // Clear incompatible flags + if (ImGui::CheckboxFlags("ImGuiComboFlags_WidthFitPreview", &flags, ImGuiComboFlags_WidthFitPreview)) + flags &= ~ImGuiComboFlags_NoPreview; + + // Override default popup height + if (ImGui::CheckboxFlags("ImGuiComboFlags_HeightSmall", &flags, ImGuiComboFlags_HeightSmall)) + flags &= ~(ImGuiComboFlags_HeightMask_ & ~ImGuiComboFlags_HeightSmall); + if (ImGui::CheckboxFlags("ImGuiComboFlags_HeightRegular", &flags, ImGuiComboFlags_HeightRegular)) + flags &= ~(ImGuiComboFlags_HeightMask_ & ~ImGuiComboFlags_HeightRegular); + if (ImGui::CheckboxFlags("ImGuiComboFlags_HeightLargest", &flags, ImGuiComboFlags_HeightLargest)) + flags &= ~(ImGuiComboFlags_HeightMask_ & ~ImGuiComboFlags_HeightLargest); + + // Using the generic BeginCombo() API, you have full control over how to display the combo contents. + // (your selection data could be an index, a pointer to the object, an id for the object, a flag intrusively + // stored in the object itself, etc.) + const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" }; + static int item_selected_idx = 0; // Here we store our selection data as an index. + + // Pass in the preview value visible before opening the combo (it could technically be different contents or not pulled from items[]) + const char* combo_preview_value = items[item_selected_idx]; + if (ImGui::BeginCombo("combo 1", combo_preview_value, flags)) + { + for (int n = 0; n < IM_ARRAYSIZE(items); n++) + { + const bool is_selected = (item_selected_idx == n); + if (ImGui::Selectable(items[n], is_selected)) + item_selected_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + // Show case embedding a filter using a simple trick: displaying the filter inside combo contents. + // See https://github.com/ocornut/imgui/issues/718 for advanced/esoteric alternatives. + if (ImGui::BeginCombo("combo 2 (w/ filter)", combo_preview_value, flags)) + { + static ImGuiTextFilter filter; + if (ImGui::IsWindowAppearing()) + { + ImGui::SetKeyboardFocusHere(); + filter.Clear(); + } + ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_F); + filter.Draw("##Filter", -FLT_MIN); + + for (int n = 0; n < IM_ARRAYSIZE(items); n++) + { + const bool is_selected = (item_selected_idx == n); + if (filter.PassFilter(items[n])) + if (ImGui::Selectable(items[n], is_selected)) + item_selected_idx = n; + } + ImGui::EndCombo(); + } + + ImGui::Spacing(); + ImGui::SeparatorText("One-liner variants"); + HelpMarker("The Combo() function is not greatly useful apart from cases were you want to embed all options in a single strings.\nFlags above don't apply to this section."); + + // Simplified one-liner Combo() API, using values packed in a single constant string + // This is a convenience for when the selection set is small and known at compile-time. + static int item_current_2 = 0; + ImGui::Combo("combo 3 (one-liner)", &item_current_2, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0"); + + // Simplified one-liner Combo() using an array of const char* + // This is not very useful (may obsolete): prefer using BeginCombo()/EndCombo() for full control. + static int item_current_3 = -1; // If the selection isn't within 0..count, Combo won't display a preview + ImGui::Combo("combo 4 (array)", &item_current_3, items, IM_ARRAYSIZE(items)); + + // Simplified one-liner Combo() using an accessor function + static int item_current_4 = 0; + ImGui::Combo("combo 5 (function)", &item_current_4, [](void* data, int n) { return ((const char**)data)[n]; }, items, IM_ARRAYSIZE(items)); + ImGui::TreePop(); } +} +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsDataTypes() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsDataTypes() +{ + IMGUI_DEMO_MARKER("Widgets/Data Types"); if (ImGui::TreeNode("Data Types")) { - // The DragScalar/InputScalar/SliderScalar functions allow various data types: signed/unsigned int/long long and float/double - // To avoid polluting the public API with all possible combinations, we use the ImGuiDataType enum to pass the type, - // and passing all arguments by address. - // This is the reason the test code below creates local variables to hold "zero" "one" etc. for each types. - // In practice, if you frequently use a given type that is not covered by the normal API entry points, you can wrap it - // yourself inside a 1 line function which can take typed argument as value instead of void*, and then pass their address - // to the generic function. For example: + // DragScalar/InputScalar/SliderScalar functions allow various data types + // - signed/unsigned + // - 8/16/32/64-bits + // - integer/float/double + // To avoid polluting the public API with all possible combinations, we use the ImGuiDataType enum + // to pass the type, and passing all arguments by pointer. + // This is the reason the test code below creates local variables to hold "zero" "one" etc. for each type. + // In practice, if you frequently use a given type that is not covered by the normal API entry points, + // you can wrap it yourself inside a 1 line function which can take typed argument as value instead of void*, + // and then pass their address to the generic function. For example: // bool MySliderU64(const char *label, u64* value, u64 min = 0, u64 max = 0, const char* format = "%lld") // { // return SliderScalar(label, ImGuiDataType_U64, value, &min, &max, format); // } - // Limits (as helper variables that we can take the address of) - // Note that the SliderScalar function has a maximum usable range of half the natural type maximum, hence the /2 below. + // Setup limits (as helper variables so we can take their address, as explained above) + // Note: SliderScalar() functions have a maximum usable range of half the natural type maximum, hence the /2. #ifndef LLONG_MIN ImS64 LLONG_MIN = -9223372036854775807LL - 1; ImS64 LLONG_MAX = 9223372036854775807LL; @@ -1307,177 +1476,127 @@ static void ShowDemoWindowWidgets() const float drag_speed = 0.2f; static bool drag_clamp = false; - ImGui::Text("Drags:"); - ImGui::Checkbox("Clamp integers to 0..50", &drag_clamp); ImGui::SameLine(); HelpMarker("As with every widgets in dear imgui, we never modify values unless there is a user interaction.\nYou can override the clamping limits by using CTRL+Click to input a value."); + IMGUI_DEMO_MARKER("Widgets/Data Types/Drags"); + ImGui::SeparatorText("Drags"); + ImGui::Checkbox("Clamp integers to 0..50", &drag_clamp); + ImGui::SameLine(); HelpMarker( + "As with every widget in dear imgui, we never modify values unless there is a user interaction.\n" + "You can override the clamping limits by using CTRL+Click to input a value."); ImGui::DragScalar("drag s8", ImGuiDataType_S8, &s8_v, drag_speed, drag_clamp ? &s8_zero : NULL, drag_clamp ? &s8_fifty : NULL); ImGui::DragScalar("drag u8", ImGuiDataType_U8, &u8_v, drag_speed, drag_clamp ? &u8_zero : NULL, drag_clamp ? &u8_fifty : NULL, "%u ms"); ImGui::DragScalar("drag s16", ImGuiDataType_S16, &s16_v, drag_speed, drag_clamp ? &s16_zero : NULL, drag_clamp ? &s16_fifty : NULL); ImGui::DragScalar("drag u16", ImGuiDataType_U16, &u16_v, drag_speed, drag_clamp ? &u16_zero : NULL, drag_clamp ? &u16_fifty : NULL, "%u ms"); ImGui::DragScalar("drag s32", ImGuiDataType_S32, &s32_v, drag_speed, drag_clamp ? &s32_zero : NULL, drag_clamp ? &s32_fifty : NULL); + ImGui::DragScalar("drag s32 hex", ImGuiDataType_S32, &s32_v, drag_speed, drag_clamp ? &s32_zero : NULL, drag_clamp ? &s32_fifty : NULL, "0x%08X"); ImGui::DragScalar("drag u32", ImGuiDataType_U32, &u32_v, drag_speed, drag_clamp ? &u32_zero : NULL, drag_clamp ? &u32_fifty : NULL, "%u ms"); ImGui::DragScalar("drag s64", ImGuiDataType_S64, &s64_v, drag_speed, drag_clamp ? &s64_zero : NULL, drag_clamp ? &s64_fifty : NULL); ImGui::DragScalar("drag u64", ImGuiDataType_U64, &u64_v, drag_speed, drag_clamp ? &u64_zero : NULL, drag_clamp ? &u64_fifty : NULL); - ImGui::DragScalar("drag float", ImGuiDataType_Float, &f32_v, 0.005f, &f32_zero, &f32_one, "%f", 1.0f); - ImGui::DragScalar("drag float ^2", ImGuiDataType_Float, &f32_v, 0.005f, &f32_zero, &f32_one, "%f", 2.0f); ImGui::SameLine(); HelpMarker("You can use the 'power' parameter to increase tweaking precision on one side of the range."); - ImGui::DragScalar("drag double", ImGuiDataType_Double, &f64_v, 0.0005f, &f64_zero, NULL, "%.10f grams", 1.0f); - ImGui::DragScalar("drag double ^2", ImGuiDataType_Double, &f64_v, 0.0005f, &f64_zero, &f64_one, "0 < %.10f < 1", 2.0f); + ImGui::DragScalar("drag float", ImGuiDataType_Float, &f32_v, 0.005f, &f32_zero, &f32_one, "%f"); + ImGui::DragScalar("drag float log", ImGuiDataType_Float, &f32_v, 0.005f, &f32_zero, &f32_one, "%f", ImGuiSliderFlags_Logarithmic); + ImGui::DragScalar("drag double", ImGuiDataType_Double, &f64_v, 0.0005f, &f64_zero, NULL, "%.10f grams"); + ImGui::DragScalar("drag double log",ImGuiDataType_Double, &f64_v, 0.0005f, &f64_zero, &f64_one, "0 < %.10f < 1", ImGuiSliderFlags_Logarithmic); - ImGui::Text("Sliders"); - ImGui::SliderScalar("slider s8 full", ImGuiDataType_S8, &s8_v, &s8_min, &s8_max, "%d"); - ImGui::SliderScalar("slider u8 full", ImGuiDataType_U8, &u8_v, &u8_min, &u8_max, "%u"); - ImGui::SliderScalar("slider s16 full", ImGuiDataType_S16, &s16_v, &s16_min, &s16_max, "%d"); - ImGui::SliderScalar("slider u16 full", ImGuiDataType_U16, &u16_v, &u16_min, &u16_max, "%u"); - ImGui::SliderScalar("slider s32 low", ImGuiDataType_S32, &s32_v, &s32_zero, &s32_fifty,"%d"); - ImGui::SliderScalar("slider s32 high", ImGuiDataType_S32, &s32_v, &s32_hi_a, &s32_hi_b, "%d"); - ImGui::SliderScalar("slider s32 full", ImGuiDataType_S32, &s32_v, &s32_min, &s32_max, "%d"); - ImGui::SliderScalar("slider u32 low", ImGuiDataType_U32, &u32_v, &u32_zero, &u32_fifty,"%u"); - ImGui::SliderScalar("slider u32 high", ImGuiDataType_U32, &u32_v, &u32_hi_a, &u32_hi_b, "%u"); - ImGui::SliderScalar("slider u32 full", ImGuiDataType_U32, &u32_v, &u32_min, &u32_max, "%u"); - ImGui::SliderScalar("slider s64 low", ImGuiDataType_S64, &s64_v, &s64_zero, &s64_fifty,"%I64d"); - ImGui::SliderScalar("slider s64 high", ImGuiDataType_S64, &s64_v, &s64_hi_a, &s64_hi_b, "%I64d"); - ImGui::SliderScalar("slider s64 full", ImGuiDataType_S64, &s64_v, &s64_min, &s64_max, "%I64d"); - ImGui::SliderScalar("slider u64 low", ImGuiDataType_U64, &u64_v, &u64_zero, &u64_fifty,"%I64u ms"); - ImGui::SliderScalar("slider u64 high", ImGuiDataType_U64, &u64_v, &u64_hi_a, &u64_hi_b, "%I64u ms"); - ImGui::SliderScalar("slider u64 full", ImGuiDataType_U64, &u64_v, &u64_min, &u64_max, "%I64u ms"); - ImGui::SliderScalar("slider float low", ImGuiDataType_Float, &f32_v, &f32_zero, &f32_one); - ImGui::SliderScalar("slider float low^2", ImGuiDataType_Float, &f32_v, &f32_zero, &f32_one, "%.10f", 2.0f); - ImGui::SliderScalar("slider float high", ImGuiDataType_Float, &f32_v, &f32_lo_a, &f32_hi_a, "%e"); - ImGui::SliderScalar("slider double low", ImGuiDataType_Double, &f64_v, &f64_zero, &f64_one, "%.10f grams", 1.0f); - ImGui::SliderScalar("slider double low^2",ImGuiDataType_Double, &f64_v, &f64_zero, &f64_one, "%.10f", 2.0f); - ImGui::SliderScalar("slider double high", ImGuiDataType_Double, &f64_v, &f64_lo_a, &f64_hi_a, "%e grams", 1.0f); + IMGUI_DEMO_MARKER("Widgets/Data Types/Sliders"); + ImGui::SeparatorText("Sliders"); + ImGui::SliderScalar("slider s8 full", ImGuiDataType_S8, &s8_v, &s8_min, &s8_max, "%d"); + ImGui::SliderScalar("slider u8 full", ImGuiDataType_U8, &u8_v, &u8_min, &u8_max, "%u"); + ImGui::SliderScalar("slider s16 full", ImGuiDataType_S16, &s16_v, &s16_min, &s16_max, "%d"); + ImGui::SliderScalar("slider u16 full", ImGuiDataType_U16, &u16_v, &u16_min, &u16_max, "%u"); + ImGui::SliderScalar("slider s32 low", ImGuiDataType_S32, &s32_v, &s32_zero, &s32_fifty,"%d"); + ImGui::SliderScalar("slider s32 high", ImGuiDataType_S32, &s32_v, &s32_hi_a, &s32_hi_b, "%d"); + ImGui::SliderScalar("slider s32 full", ImGuiDataType_S32, &s32_v, &s32_min, &s32_max, "%d"); + ImGui::SliderScalar("slider s32 hex", ImGuiDataType_S32, &s32_v, &s32_zero, &s32_fifty, "0x%04X"); + ImGui::SliderScalar("slider u32 low", ImGuiDataType_U32, &u32_v, &u32_zero, &u32_fifty,"%u"); + ImGui::SliderScalar("slider u32 high", ImGuiDataType_U32, &u32_v, &u32_hi_a, &u32_hi_b, "%u"); + ImGui::SliderScalar("slider u32 full", ImGuiDataType_U32, &u32_v, &u32_min, &u32_max, "%u"); + ImGui::SliderScalar("slider s64 low", ImGuiDataType_S64, &s64_v, &s64_zero, &s64_fifty,"%" PRId64); + ImGui::SliderScalar("slider s64 high", ImGuiDataType_S64, &s64_v, &s64_hi_a, &s64_hi_b, "%" PRId64); + ImGui::SliderScalar("slider s64 full", ImGuiDataType_S64, &s64_v, &s64_min, &s64_max, "%" PRId64); + ImGui::SliderScalar("slider u64 low", ImGuiDataType_U64, &u64_v, &u64_zero, &u64_fifty,"%" PRIu64 " ms"); + ImGui::SliderScalar("slider u64 high", ImGuiDataType_U64, &u64_v, &u64_hi_a, &u64_hi_b, "%" PRIu64 " ms"); + ImGui::SliderScalar("slider u64 full", ImGuiDataType_U64, &u64_v, &u64_min, &u64_max, "%" PRIu64 " ms"); + ImGui::SliderScalar("slider float low", ImGuiDataType_Float, &f32_v, &f32_zero, &f32_one); + ImGui::SliderScalar("slider float low log", ImGuiDataType_Float, &f32_v, &f32_zero, &f32_one, "%.10f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderScalar("slider float high", ImGuiDataType_Float, &f32_v, &f32_lo_a, &f32_hi_a, "%e"); + ImGui::SliderScalar("slider double low", ImGuiDataType_Double, &f64_v, &f64_zero, &f64_one, "%.10f grams"); + ImGui::SliderScalar("slider double low log",ImGuiDataType_Double, &f64_v, &f64_zero, &f64_one, "%.10f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderScalar("slider double high", ImGuiDataType_Double, &f64_v, &f64_lo_a, &f64_hi_a, "%e grams"); + ImGui::SeparatorText("Sliders (reverse)"); + ImGui::SliderScalar("slider s8 reverse", ImGuiDataType_S8, &s8_v, &s8_max, &s8_min, "%d"); + ImGui::SliderScalar("slider u8 reverse", ImGuiDataType_U8, &u8_v, &u8_max, &u8_min, "%u"); + ImGui::SliderScalar("slider s32 reverse", ImGuiDataType_S32, &s32_v, &s32_fifty, &s32_zero, "%d"); + ImGui::SliderScalar("slider u32 reverse", ImGuiDataType_U32, &u32_v, &u32_fifty, &u32_zero, "%u"); + ImGui::SliderScalar("slider s64 reverse", ImGuiDataType_S64, &s64_v, &s64_fifty, &s64_zero, "%" PRId64); + ImGui::SliderScalar("slider u64 reverse", ImGuiDataType_U64, &u64_v, &u64_fifty, &u64_zero, "%" PRIu64 " ms"); + + IMGUI_DEMO_MARKER("Widgets/Data Types/Inputs"); static bool inputs_step = true; - ImGui::Text("Inputs"); + static ImGuiInputTextFlags flags = ImGuiInputTextFlags_None; + ImGui::SeparatorText("Inputs"); ImGui::Checkbox("Show step buttons", &inputs_step); - ImGui::InputScalar("input s8", ImGuiDataType_S8, &s8_v, inputs_step ? &s8_one : NULL, NULL, "%d"); - ImGui::InputScalar("input u8", ImGuiDataType_U8, &u8_v, inputs_step ? &u8_one : NULL, NULL, "%u"); - ImGui::InputScalar("input s16", ImGuiDataType_S16, &s16_v, inputs_step ? &s16_one : NULL, NULL, "%d"); - ImGui::InputScalar("input u16", ImGuiDataType_U16, &u16_v, inputs_step ? &u16_one : NULL, NULL, "%u"); - ImGui::InputScalar("input s32", ImGuiDataType_S32, &s32_v, inputs_step ? &s32_one : NULL, NULL, "%d"); - ImGui::InputScalar("input s32 hex", ImGuiDataType_S32, &s32_v, inputs_step ? &s32_one : NULL, NULL, "%08X", ImGuiInputTextFlags_CharsHexadecimal); - ImGui::InputScalar("input u32", ImGuiDataType_U32, &u32_v, inputs_step ? &u32_one : NULL, NULL, "%u"); - ImGui::InputScalar("input u32 hex", ImGuiDataType_U32, &u32_v, inputs_step ? &u32_one : NULL, NULL, "%08X", ImGuiInputTextFlags_CharsHexadecimal); - ImGui::InputScalar("input s64", ImGuiDataType_S64, &s64_v, inputs_step ? &s64_one : NULL); - ImGui::InputScalar("input u64", ImGuiDataType_U64, &u64_v, inputs_step ? &u64_one : NULL); - ImGui::InputScalar("input float", ImGuiDataType_Float, &f32_v, inputs_step ? &f32_one : NULL); - ImGui::InputScalar("input double", ImGuiDataType_Double, &f64_v, inputs_step ? &f64_one : NULL); + ImGui::CheckboxFlags("ImGuiInputTextFlags_ReadOnly", &flags, ImGuiInputTextFlags_ReadOnly); + ImGui::CheckboxFlags("ImGuiInputTextFlags_ParseEmptyRefVal", &flags, ImGuiInputTextFlags_ParseEmptyRefVal); + ImGui::CheckboxFlags("ImGuiInputTextFlags_DisplayEmptyRefVal", &flags, ImGuiInputTextFlags_DisplayEmptyRefVal); + ImGui::InputScalar("input s8", ImGuiDataType_S8, &s8_v, inputs_step ? &s8_one : NULL, NULL, "%d", flags); + ImGui::InputScalar("input u8", ImGuiDataType_U8, &u8_v, inputs_step ? &u8_one : NULL, NULL, "%u", flags); + ImGui::InputScalar("input s16", ImGuiDataType_S16, &s16_v, inputs_step ? &s16_one : NULL, NULL, "%d", flags); + ImGui::InputScalar("input u16", ImGuiDataType_U16, &u16_v, inputs_step ? &u16_one : NULL, NULL, "%u", flags); + ImGui::InputScalar("input s32", ImGuiDataType_S32, &s32_v, inputs_step ? &s32_one : NULL, NULL, "%d", flags); + ImGui::InputScalar("input s32 hex", ImGuiDataType_S32, &s32_v, inputs_step ? &s32_one : NULL, NULL, "%04X", flags); + ImGui::InputScalar("input u32", ImGuiDataType_U32, &u32_v, inputs_step ? &u32_one : NULL, NULL, "%u", flags); + ImGui::InputScalar("input u32 hex", ImGuiDataType_U32, &u32_v, inputs_step ? &u32_one : NULL, NULL, "%08X", flags); + ImGui::InputScalar("input s64", ImGuiDataType_S64, &s64_v, inputs_step ? &s64_one : NULL, NULL, NULL, flags); + ImGui::InputScalar("input u64", ImGuiDataType_U64, &u64_v, inputs_step ? &u64_one : NULL, NULL, NULL, flags); + ImGui::InputScalar("input float", ImGuiDataType_Float, &f32_v, inputs_step ? &f32_one : NULL, NULL, NULL, flags); + ImGui::InputScalar("input double", ImGuiDataType_Double, &f64_v, inputs_step ? &f64_one : NULL, NULL, NULL, flags); ImGui::TreePop(); } +} - if (ImGui::TreeNode("Multi-component Widgets")) +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsDisableBlocks() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsDisableBlocks(ImGuiDemoWindowData* demo_data) +{ + IMGUI_DEMO_MARKER("Widgets/Disable Blocks"); + if (ImGui::TreeNode("Disable Blocks")) { - static float vec4f[4] = { 0.10f, 0.20f, 0.30f, 0.44f }; - static int vec4i[4] = { 1, 5, 100, 255 }; - - ImGui::InputFloat2("input float2", vec4f); - ImGui::DragFloat2("drag float2", vec4f, 0.01f, 0.0f, 1.0f); - ImGui::SliderFloat2("slider float2", vec4f, 0.0f, 1.0f); - ImGui::InputInt2("input int2", vec4i); - ImGui::DragInt2("drag int2", vec4i, 1, 0, 255); - ImGui::SliderInt2("slider int2", vec4i, 0, 255); - ImGui::Spacing(); - - ImGui::InputFloat3("input float3", vec4f); - ImGui::DragFloat3("drag float3", vec4f, 0.01f, 0.0f, 1.0f); - ImGui::SliderFloat3("slider float3", vec4f, 0.0f, 1.0f); - ImGui::InputInt3("input int3", vec4i); - ImGui::DragInt3("drag int3", vec4i, 1, 0, 255); - ImGui::SliderInt3("slider int3", vec4i, 0, 255); - ImGui::Spacing(); - - ImGui::InputFloat4("input float4", vec4f); - ImGui::DragFloat4("drag float4", vec4f, 0.01f, 0.0f, 1.0f); - ImGui::SliderFloat4("slider float4", vec4f, 0.0f, 1.0f); - ImGui::InputInt4("input int4", vec4i); - ImGui::DragInt4("drag int4", vec4i, 1, 0, 255); - ImGui::SliderInt4("slider int4", vec4i, 0, 255); - + ImGui::Checkbox("Disable entire section above", &demo_data->DisableSections); + ImGui::SameLine(); HelpMarker("Demonstrate using BeginDisabled()/EndDisabled() across other sections."); ImGui::TreePop(); } +} - if (ImGui::TreeNode("Vertical Sliders")) - { - const float spacing = 4; - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing)); - - static int int_value = 0; - ImGui::VSliderInt("##int", ImVec2(18,160), &int_value, 0, 5); - ImGui::SameLine(); - - static float values[7] = { 0.0f, 0.60f, 0.35f, 0.9f, 0.70f, 0.20f, 0.0f }; - ImGui::PushID("set1"); - for (int i = 0; i < 7; i++) - { - if (i > 0) ImGui::SameLine(); - ImGui::PushID(i); - ImGui::PushStyleColor(ImGuiCol_FrameBg, (ImVec4)ImColor::HSV(i/7.0f, 0.5f, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, (ImVec4)ImColor::HSV(i/7.0f, 0.6f, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_FrameBgActive, (ImVec4)ImColor::HSV(i/7.0f, 0.7f, 0.5f)); - ImGui::PushStyleColor(ImGuiCol_SliderGrab, (ImVec4)ImColor::HSV(i/7.0f, 0.9f, 0.9f)); - ImGui::VSliderFloat("##v", ImVec2(18,160), &values[i], 0.0f, 1.0f, ""); - if (ImGui::IsItemActive() || ImGui::IsItemHovered()) - ImGui::SetTooltip("%.3f", values[i]); - ImGui::PopStyleColor(4); - ImGui::PopID(); - } - ImGui::PopID(); - - ImGui::SameLine(); - ImGui::PushID("set2"); - static float values2[4] = { 0.20f, 0.80f, 0.40f, 0.25f }; - const int rows = 3; - const ImVec2 small_slider_size(18, (160.0f-(rows-1)*spacing)/rows); - for (int nx = 0; nx < 4; nx++) - { - if (nx > 0) ImGui::SameLine(); - ImGui::BeginGroup(); - for (int ny = 0; ny < rows; ny++) - { - ImGui::PushID(nx*rows+ny); - ImGui::VSliderFloat("##v", small_slider_size, &values2[nx], 0.0f, 1.0f, ""); - if (ImGui::IsItemActive() || ImGui::IsItemHovered()) - ImGui::SetTooltip("%.3f", values2[nx]); - ImGui::PopID(); - } - ImGui::EndGroup(); - } - ImGui::PopID(); - - ImGui::SameLine(); - ImGui::PushID("set3"); - for (int i = 0; i < 4; i++) - { - if (i > 0) ImGui::SameLine(); - ImGui::PushID(i); - ImGui::PushStyleVar(ImGuiStyleVar_GrabMinSize, 40); - ImGui::VSliderFloat("##v", ImVec2(40,160), &values[i], 0.0f, 1.0f, "%.2f\nsec"); - ImGui::PopStyleVar(); - ImGui::PopID(); - } - ImGui::PopID(); - ImGui::PopStyleVar(); - ImGui::TreePop(); - } +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsDragAndDrop() +//----------------------------------------------------------------------------- +static void DemoWindowWidgetsDragAndDrop() +{ + IMGUI_DEMO_MARKER("Widgets/Drag and drop"); if (ImGui::TreeNode("Drag and Drop")) { + IMGUI_DEMO_MARKER("Widgets/Drag and drop/Standard widgets"); + if (ImGui::TreeNode("Drag and drop in standard widgets")) { // ColorEdit widgets automatically act as drag source and drag target. - // They are using standardized payload strings IMGUI_PAYLOAD_TYPE_COLOR_3F and IMGUI_PAYLOAD_TYPE_COLOR_4F to allow your own widgets - // to use colors in their drag and drop interaction. Also see the demo in Color Picker -> Palette demo. - ImGui::BulletText("Drag and drop in standard widgets"); - ImGui::Indent(); - static float col1[3] = { 1.0f,0.0f,0.2f }; - static float col2[4] = { 0.4f,0.7f,0.0f,0.5f }; + // They are using standardized payload strings IMGUI_PAYLOAD_TYPE_COLOR_3F and IMGUI_PAYLOAD_TYPE_COLOR_4F + // to allow your own widgets to use colors in their drag and drop interaction. + // Also see 'Demo->Widgets->Color/Picker Widgets->Palette' demo. + HelpMarker("You can drag from the color squares."); + static float col1[3] = { 1.0f, 0.0f, 0.2f }; + static float col2[4] = { 0.4f, 0.7f, 0.0f, 0.5f }; ImGui::ColorEdit3("color 1", col1); ImGui::ColorEdit4("color 2", col2); - ImGui::Unindent(); + ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Drag and drop/Copy-swap items"); + if (ImGui::TreeNode("Drag and drop to copy/swap items")) { - ImGui::BulletText("Drag and drop to copy/swap items"); - ImGui::Indent(); enum Mode { Mode_Copy, @@ -1488,19 +1607,28 @@ static void ShowDemoWindowWidgets() if (ImGui::RadioButton("Copy", mode == Mode_Copy)) { mode = Mode_Copy; } ImGui::SameLine(); if (ImGui::RadioButton("Move", mode == Mode_Move)) { mode = Mode_Move; } ImGui::SameLine(); if (ImGui::RadioButton("Swap", mode == Mode_Swap)) { mode = Mode_Swap; } - static const char* names[9] = { "Bobby", "Beatrice", "Betty", "Brianna", "Barry", "Bernard", "Bibi", "Blaine", "Bryn" }; + static const char* names[9] = + { + "Bobby", "Beatrice", "Betty", + "Brianna", "Barry", "Bernard", + "Bibi", "Blaine", "Bryn" + }; for (int n = 0; n < IM_ARRAYSIZE(names); n++) { ImGui::PushID(n); if ((n % 3) != 0) ImGui::SameLine(); - ImGui::Button(names[n], ImVec2(60,60)); + ImGui::Button(names[n], ImVec2(60, 60)); // Our buttons are both drag sources and drag targets here! if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) { - ImGui::SetDragDropPayload("DND_DEMO_CELL", &n, sizeof(int)); // Set payload to carry the index of our item (could be anything) - if (mode == Mode_Copy) { ImGui::Text("Copy %s", names[n]); } // Display preview (could be anything, e.g. when dragging an image we could decide to display the filename and a small preview of the image, etc.) + // Set payload to carry the index of our item (could be anything) + ImGui::SetDragDropPayload("DND_DEMO_CELL", &n, sizeof(int)); + + // Display preview (could be anything, e.g. when dragging an image we could decide to display + // the filename and a small preview of the image, etc.) + if (mode == Mode_Copy) { ImGui::Text("Copy %s", names[n]); } if (mode == Mode_Move) { ImGui::Text("Move %s", names[n]); } if (mode == Mode_Swap) { ImGui::Text("Swap %s", names[n]); } ImGui::EndDragDropSource(); @@ -1531,49 +1659,504 @@ static void ShowDemoWindowWidgets() } ImGui::PopID(); } - ImGui::Unindent(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and Drop/Drag to reorder items (simple)"); + if (ImGui::TreeNode("Drag to reorder items (simple)")) + { + // FIXME: there is temporary (usually single-frame) ID Conflict during reordering as a same item may be submitting twice. + // This code was always slightly faulty but in a way which was not easily noticeable. + // Until we fix this, enable ImGuiItemFlags_AllowDuplicateId to disable detecting the issue. + ImGui::PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); + + // Simple reordering + HelpMarker( + "We don't use the drag and drop api at all here! " + "Instead we query when the item is held but not hovered, and order items accordingly."); + static const char* item_names[] = { "Item One", "Item Two", "Item Three", "Item Four", "Item Five" }; + for (int n = 0; n < IM_ARRAYSIZE(item_names); n++) + { + const char* item = item_names[n]; + ImGui::Selectable(item); + + if (ImGui::IsItemActive() && !ImGui::IsItemHovered()) + { + int n_next = n + (ImGui::GetMouseDragDelta(0).y < 0.f ? -1 : 1); + if (n_next >= 0 && n_next < IM_ARRAYSIZE(item_names)) + { + item_names[n] = item_names[n_next]; + item_names[n_next] = item; + ImGui::ResetMouseDragDelta(); + } + } + } + + ImGui::PopItemFlag(); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Drag and Drop/Tooltip at target location"); + if (ImGui::TreeNode("Tooltip at target location")) + { + for (int n = 0; n < 2; n++) + { + // Drop targets + ImGui::Button(n ? "drop here##1" : "drop here##0"); + if (ImGui::BeginDragDropTarget()) + { + ImGuiDragDropFlags drop_target_flags = ImGuiDragDropFlags_AcceptBeforeDelivery | ImGuiDragDropFlags_AcceptNoPreviewTooltip; + if (const ImGuiPayload* payload = ImGui::AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F, drop_target_flags)) + { + IM_UNUSED(payload); + ImGui::SetMouseCursor(ImGuiMouseCursor_NotAllowed); + ImGui::SetTooltip("Cannot drop here!"); + } + ImGui::EndDragDropTarget(); + } + + // Drop source + static ImVec4 col4 = { 1.0f, 0.0f, 0.2f, 1.0f }; + if (n == 0) + ImGui::ColorButton("drag me", col4); + + } + ImGui::TreePop(); } ImGui::TreePop(); } +} - if (ImGui::TreeNode("Querying Status (Active/Focused/Hovered etc.)")) +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsDragsAndSliders() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsDragsAndSliders() +{ + IMGUI_DEMO_MARKER("Widgets/Drag and Slider Flags"); + if (ImGui::TreeNode("Drag/Slider Flags")) { - // Display the value of IsItemHovered() and other common item state functions. Note that the flags can be combined. - // (because BulletText is an item itself and that would affect the output of IsItemHovered() we pass all state in a single call to simplify the code). - static int item_type = 1; + // Demonstrate using advanced flags for DragXXX and SliderXXX functions. Note that the flags are the same! + static ImGuiSliderFlags flags = ImGuiSliderFlags_None; + ImGui::CheckboxFlags("ImGuiSliderFlags_AlwaysClamp", &flags, ImGuiSliderFlags_AlwaysClamp); + ImGui::CheckboxFlags("ImGuiSliderFlags_ClampOnInput", &flags, ImGuiSliderFlags_ClampOnInput); + ImGui::SameLine(); HelpMarker("Clamp value to min/max bounds when input manually with CTRL+Click. By default CTRL+Click allows going out of bounds."); + ImGui::CheckboxFlags("ImGuiSliderFlags_ClampZeroRange", &flags, ImGuiSliderFlags_ClampZeroRange); + ImGui::SameLine(); HelpMarker("Clamp even if min==max==0.0f. Otherwise DragXXX functions don't clamp."); + ImGui::CheckboxFlags("ImGuiSliderFlags_Logarithmic", &flags, ImGuiSliderFlags_Logarithmic); + ImGui::SameLine(); HelpMarker("Enable logarithmic editing (more precision for small values)."); + ImGui::CheckboxFlags("ImGuiSliderFlags_NoRoundToFormat", &flags, ImGuiSliderFlags_NoRoundToFormat); + ImGui::SameLine(); HelpMarker("Disable rounding underlying value to match precision of the format string (e.g. %.3f values are rounded to those 3 digits)."); + ImGui::CheckboxFlags("ImGuiSliderFlags_NoInput", &flags, ImGuiSliderFlags_NoInput); + ImGui::SameLine(); HelpMarker("Disable CTRL+Click or Enter key allowing to input text directly into the widget."); + ImGui::CheckboxFlags("ImGuiSliderFlags_NoSpeedTweaks", &flags, ImGuiSliderFlags_NoSpeedTweaks); + ImGui::SameLine(); HelpMarker("Disable keyboard modifiers altering tweak speed. Useful if you want to alter tweak speed yourself based on your own logic."); + ImGui::CheckboxFlags("ImGuiSliderFlags_WrapAround", &flags, ImGuiSliderFlags_WrapAround); + ImGui::SameLine(); HelpMarker("Enable wrapping around from max to min and from min to max (only supported by DragXXX() functions)"); + + // Drags + static float drag_f = 0.5f; + static int drag_i = 50; + ImGui::Text("Underlying float value: %f", drag_f); + ImGui::DragFloat("DragFloat (0 -> 1)", &drag_f, 0.005f, 0.0f, 1.0f, "%.3f", flags); + ImGui::DragFloat("DragFloat (0 -> +inf)", &drag_f, 0.005f, 0.0f, FLT_MAX, "%.3f", flags); + ImGui::DragFloat("DragFloat (-inf -> 1)", &drag_f, 0.005f, -FLT_MAX, 1.0f, "%.3f", flags); + ImGui::DragFloat("DragFloat (-inf -> +inf)", &drag_f, 0.005f, -FLT_MAX, +FLT_MAX, "%.3f", flags); + //ImGui::DragFloat("DragFloat (0 -> 0)", &drag_f, 0.005f, 0.0f, 0.0f, "%.3f", flags); // To test ClampZeroRange + //ImGui::DragFloat("DragFloat (100 -> 100)", &drag_f, 0.005f, 100.0f, 100.0f, "%.3f", flags); + ImGui::DragInt("DragInt (0 -> 100)", &drag_i, 0.5f, 0, 100, "%d", flags); + + // Sliders + static float slider_f = 0.5f; + static int slider_i = 50; + const ImGuiSliderFlags flags_for_sliders = flags & ~ImGuiSliderFlags_WrapAround; + ImGui::Text("Underlying float value: %f", slider_f); + ImGui::SliderFloat("SliderFloat (0 -> 1)", &slider_f, 0.0f, 1.0f, "%.3f", flags_for_sliders); + ImGui::SliderInt("SliderInt (0 -> 100)", &slider_i, 0, 100, "%d", flags_for_sliders); + + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsImages() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsImages() +{ + IMGUI_DEMO_MARKER("Widgets/Images"); + if (ImGui::TreeNode("Images")) + { + ImGuiIO& io = ImGui::GetIO(); + ImGui::TextWrapped( + "Below we are displaying the font texture (which is the only texture we have access to in this demo). " + "Use the 'ImTextureID' type as storage to pass pointers or identifier to your own texture data. " + "Hover the texture for a zoomed view!"); + + // Below we are displaying the font texture because it is the only texture we have access to inside the demo! + // Remember that ImTextureID is just storage for whatever you want it to be. It is essentially a value that + // will be passed to the rendering backend via the ImDrawCmd structure. + // If you use one of the default imgui_impl_XXXX.cpp rendering backend, they all have comments at the top + // of their respective source file to specify what they expect to be stored in ImTextureID, for example: + // - The imgui_impl_dx11.cpp renderer expect a 'ID3D11ShaderResourceView*' pointer + // - The imgui_impl_opengl3.cpp renderer expect a GLuint OpenGL texture identifier, etc. + // More: + // - If you decided that ImTextureID = MyEngineTexture*, then you can pass your MyEngineTexture* pointers + // to ImGui::Image(), and gather width/height through your own functions, etc. + // - You can use ShowMetricsWindow() to inspect the draw data that are being passed to your renderer, + // it will help you debug issues if you are confused about it. + // - Consider using the lower-level ImDrawList::AddImage() API, via ImGui::GetWindowDrawList()->AddImage(). + // - Read https://github.com/ocornut/imgui/blob/master/docs/FAQ.md + // - Read https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples + ImTextureID my_tex_id = io.Fonts->TexID; + float my_tex_w = (float)io.Fonts->TexWidth; + float my_tex_h = (float)io.Fonts->TexHeight; + { + ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h); + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImVec2 uv_min = ImVec2(0.0f, 0.0f); // Top-left + ImVec2 uv_max = ImVec2(1.0f, 1.0f); // Lower-right + ImGui::PushStyleVar(ImGuiStyleVar_ImageBorderSize, IM_MAX(1.0f, ImGui::GetStyle().ImageBorderSize)); + ImGui::ImageWithBg(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + if (ImGui::BeginItemTooltip()) + { + float region_sz = 32.0f; + float region_x = io.MousePos.x - pos.x - region_sz * 0.5f; + float region_y = io.MousePos.y - pos.y - region_sz * 0.5f; + float zoom = 4.0f; + if (region_x < 0.0f) { region_x = 0.0f; } + else if (region_x > my_tex_w - region_sz) { region_x = my_tex_w - region_sz; } + if (region_y < 0.0f) { region_y = 0.0f; } + else if (region_y > my_tex_h - region_sz) { region_y = my_tex_h - region_sz; } + ImGui::Text("Min: (%.2f, %.2f)", region_x, region_y); + ImGui::Text("Max: (%.2f, %.2f)", region_x + region_sz, region_y + region_sz); + ImVec2 uv0 = ImVec2((region_x) / my_tex_w, (region_y) / my_tex_h); + ImVec2 uv1 = ImVec2((region_x + region_sz) / my_tex_w, (region_y + region_sz) / my_tex_h); + ImGui::ImageWithBg(my_tex_id, ImVec2(region_sz * zoom, region_sz * zoom), uv0, uv1, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::EndTooltip(); + } + ImGui::PopStyleVar(); + } + + IMGUI_DEMO_MARKER("Widgets/Images/Textured buttons"); + ImGui::TextWrapped("And now some textured buttons.."); + static int pressed_count = 0; + for (int i = 0; i < 8; i++) + { + // UV coordinates are often (0.0f, 0.0f) and (1.0f, 1.0f) to display an entire textures. + // Here are trying to display only a 32x32 pixels area of the texture, hence the UV computation. + // Read about UV coordinates here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples + ImGui::PushID(i); + if (i > 0) + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(i - 1.0f, i - 1.0f)); + ImVec2 size = ImVec2(32.0f, 32.0f); // Size of the image we want to make visible + ImVec2 uv0 = ImVec2(0.0f, 0.0f); // UV coordinates for lower-left + ImVec2 uv1 = ImVec2(32.0f / my_tex_w, 32.0f / my_tex_h); // UV coordinates for (32,32) in our texture + ImVec4 bg_col = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); // Black background + ImVec4 tint_col = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); // No tint + if (ImGui::ImageButton("", my_tex_id, size, uv0, uv1, bg_col, tint_col)) + pressed_count += 1; + if (i > 0) + ImGui::PopStyleVar(); + ImGui::PopID(); + ImGui::SameLine(); + } + ImGui::NewLine(); + ImGui::Text("Pressed %d times.", pressed_count); + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsListBoxes() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsListBoxes() +{ + IMGUI_DEMO_MARKER("Widgets/List Boxes"); + if (ImGui::TreeNode("List Boxes")) + { + // BeginListBox() is essentially a thin wrapper to using BeginChild()/EndChild() + // using the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. + // You may be tempted to simply use BeginChild() directly. However note that BeginChild() requires EndChild() + // to always be called (inconsistent with BeginListBox()/EndListBox()). + + // Using the generic BeginListBox() API, you have full control over how to display the combo contents. + // (your selection data could be an index, a pointer to the object, an id for the object, a flag intrusively + // stored in the object itself, etc.) + const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF", "GGGG", "HHHH", "IIII", "JJJJ", "KKKK", "LLLLLLL", "MMMM", "OOOOOOO" }; + static int item_selected_idx = 0; // Here we store our selected data as an index. + + static bool item_highlight = false; + int item_highlighted_idx = -1; // Here we store our highlighted data as an index. + ImGui::Checkbox("Highlight hovered item in second listbox", &item_highlight); + + if (ImGui::BeginListBox("listbox 1")) + { + for (int n = 0; n < IM_ARRAYSIZE(items); n++) + { + const bool is_selected = (item_selected_idx == n); + if (ImGui::Selectable(items[n], is_selected)) + item_selected_idx = n; + + if (item_highlight && ImGui::IsItemHovered()) + item_highlighted_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndListBox(); + } + ImGui::SameLine(); HelpMarker("Here we are sharing selection state between both boxes."); + + // Custom size: use all width, 5 items tall + ImGui::Text("Full-width:"); + if (ImGui::BeginListBox("##listbox 2", ImVec2(-FLT_MIN, 5 * ImGui::GetTextLineHeightWithSpacing()))) + { + for (int n = 0; n < IM_ARRAYSIZE(items); n++) + { + bool is_selected = (item_selected_idx == n); + ImGuiSelectableFlags flags = (item_highlighted_idx == n) ? ImGuiSelectableFlags_Highlight : 0; + if (ImGui::Selectable(items[n], is_selected, flags)) + item_selected_idx = n; + + // Set the initial focus when opening the combo (scrolling + keyboard navigation focus) + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndListBox(); + } + + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsMultiComponents() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsMultiComponents() +{ + IMGUI_DEMO_MARKER("Widgets/Multi-component Widgets"); + if (ImGui::TreeNode("Multi-component Widgets")) + { + static float vec4f[4] = { 0.10f, 0.20f, 0.30f, 0.44f }; + static int vec4i[4] = { 1, 5, 100, 255 }; + + ImGui::SeparatorText("2-wide"); + ImGui::InputFloat2("input float2", vec4f); + ImGui::DragFloat2("drag float2", vec4f, 0.01f, 0.0f, 1.0f); + ImGui::SliderFloat2("slider float2", vec4f, 0.0f, 1.0f); + ImGui::InputInt2("input int2", vec4i); + ImGui::DragInt2("drag int2", vec4i, 1, 0, 255); + ImGui::SliderInt2("slider int2", vec4i, 0, 255); + + ImGui::SeparatorText("3-wide"); + ImGui::InputFloat3("input float3", vec4f); + ImGui::DragFloat3("drag float3", vec4f, 0.01f, 0.0f, 1.0f); + ImGui::SliderFloat3("slider float3", vec4f, 0.0f, 1.0f); + ImGui::InputInt3("input int3", vec4i); + ImGui::DragInt3("drag int3", vec4i, 1, 0, 255); + ImGui::SliderInt3("slider int3", vec4i, 0, 255); + + ImGui::SeparatorText("4-wide"); + ImGui::InputFloat4("input float4", vec4f); + ImGui::DragFloat4("drag float4", vec4f, 0.01f, 0.0f, 1.0f); + ImGui::SliderFloat4("slider float4", vec4f, 0.0f, 1.0f); + ImGui::InputInt4("input int4", vec4i); + ImGui::DragInt4("drag int4", vec4i, 1, 0, 255); + ImGui::SliderInt4("slider int4", vec4i, 0, 255); + + ImGui::SeparatorText("Ranges"); + static float begin = 10, end = 90; + static int begin_i = 100, end_i = 1000; + ImGui::DragFloatRange2("range float", &begin, &end, 0.25f, 0.0f, 100.0f, "Min: %.1f %%", "Max: %.1f %%", ImGuiSliderFlags_AlwaysClamp); + ImGui::DragIntRange2("range int", &begin_i, &end_i, 5, 0, 1000, "Min: %d units", "Max: %d units"); + ImGui::DragIntRange2("range int (no bounds)", &begin_i, &end_i, 5, 0, 0, "Min: %d units", "Max: %d units"); + + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsPlotting() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsPlotting() +{ + // Plot/Graph widgets are not very good. +// Consider using a third-party library such as ImPlot: https://github.com/epezent/implot +// (see others https://github.com/ocornut/imgui/wiki/Useful-Extensions) + IMGUI_DEMO_MARKER("Widgets/Plotting"); + if (ImGui::TreeNode("Plotting")) + { + ImGui::Text("Need better plotting and graphing? Consider using ImPlot:"); + ImGui::TextLinkOpenURL("https://github.com/epezent/implot"); + ImGui::Separator(); + + static bool animate = true; + ImGui::Checkbox("Animate", &animate); + + // Plot as lines and plot as histogram + static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; + ImGui::PlotLines("Frame Times", arr, IM_ARRAYSIZE(arr)); + ImGui::PlotHistogram("Histogram", arr, IM_ARRAYSIZE(arr), 0, NULL, 0.0f, 1.0f, ImVec2(0, 80.0f)); + //ImGui::SameLine(); HelpMarker("Consider using ImPlot instead!"); + + // Fill an array of contiguous float values to plot + // Tip: If your float aren't contiguous but part of a structure, you can pass a pointer to your first float + // and the sizeof() of your structure in the "stride" parameter. + static float values[90] = {}; + static int values_offset = 0; + static double refresh_time = 0.0; + if (!animate || refresh_time == 0.0) + refresh_time = ImGui::GetTime(); + while (refresh_time < ImGui::GetTime()) // Create data at fixed 60 Hz rate for the demo + { + static float phase = 0.0f; + values[values_offset] = cosf(phase); + values_offset = (values_offset + 1) % IM_ARRAYSIZE(values); + phase += 0.10f * values_offset; + refresh_time += 1.0f / 60.0f; + } + + // Plots can display overlay texts + // (in this example, we will display an average value) + { + float average = 0.0f; + for (int n = 0; n < IM_ARRAYSIZE(values); n++) + average += values[n]; + average /= (float)IM_ARRAYSIZE(values); + char overlay[32]; + sprintf(overlay, "avg %f", average); + ImGui::PlotLines("Lines", values, IM_ARRAYSIZE(values), values_offset, overlay, -1.0f, 1.0f, ImVec2(0, 80.0f)); + } + + // Use functions to generate output + // FIXME: This is actually VERY awkward because current plot API only pass in indices. + // We probably want an API passing floats and user provide sample rate/count. + struct Funcs + { + static float Sin(void*, int i) { return sinf(i * 0.1f); } + static float Saw(void*, int i) { return (i & 1) ? 1.0f : -1.0f; } + }; + static int func_type = 0, display_count = 70; + ImGui::SeparatorText("Functions"); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::Combo("func", &func_type, "Sin\0Saw\0"); + ImGui::SameLine(); + ImGui::SliderInt("Sample count", &display_count, 1, 400); + float (*func)(void*, int) = (func_type == 0) ? Funcs::Sin : Funcs::Saw; + ImGui::PlotLines("Lines##2", func, NULL, display_count, 0, NULL, -1.0f, 1.0f, ImVec2(0, 80)); + ImGui::PlotHistogram("Histogram##2", func, NULL, display_count, 0, NULL, -1.0f, 1.0f, ImVec2(0, 80)); + + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsProgressBars() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsProgressBars() +{ + IMGUI_DEMO_MARKER("Widgets/Progress Bars"); + if (ImGui::TreeNode("Progress Bars")) + { + // Animate a simple progress bar + static float progress = 0.0f, progress_dir = 1.0f; + progress += progress_dir * 0.4f * ImGui::GetIO().DeltaTime; + if (progress >= +1.1f) { progress = +1.1f; progress_dir *= -1.0f; } + if (progress <= -0.1f) { progress = -0.1f; progress_dir *= -1.0f; } + + // Typically we would use ImVec2(-1.0f,0.0f) or ImVec2(-FLT_MIN,0.0f) to use all available width, + // or ImVec2(width,0.0f) for a specified width. ImVec2(0.0f,0.0f) uses ItemWidth. + ImGui::ProgressBar(progress, ImVec2(0.0f, 0.0f)); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Text("Progress Bar"); + + float progress_saturated = IM_CLAMP(progress, 0.0f, 1.0f); + char buf[32]; + sprintf(buf, "%d/%d", (int)(progress_saturated * 1753), 1753); + ImGui::ProgressBar(progress, ImVec2(0.f, 0.f), buf); + + // Pass an animated negative value, e.g. -1.0f * (float)ImGui::GetTime() is the recommended value. + // Adjust the factor if you want to adjust the animation speed. + ImGui::ProgressBar(-1.0f * (float)ImGui::GetTime(), ImVec2(0.0f, 0.0f), "Searching.."); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Text("Indeterminate"); + + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsQueryingStatuses() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsQueryingStatuses() +{ + IMGUI_DEMO_MARKER("Widgets/Querying Item Status (Edited,Active,Hovered etc.)"); + if (ImGui::TreeNode("Querying Item Status (Edited/Active/Hovered etc.)")) + { + // Select an item type + const char* item_names[] = + { + "Text", "Button", "Button (w/ repeat)", "Checkbox", "SliderFloat", "InputText", "InputTextMultiline", "InputFloat", + "InputFloat3", "ColorEdit4", "Selectable", "MenuItem", "TreeNode", "TreeNode (w/ double-click)", "Combo", "ListBox" + }; + static int item_type = 4; + static bool item_disabled = false; + ImGui::Combo("Item Type", &item_type, item_names, IM_ARRAYSIZE(item_names), IM_ARRAYSIZE(item_names)); + ImGui::SameLine(); + HelpMarker("Testing how various types of items are interacting with the IsItemXXX functions. Note that the bool return value of most ImGui function is generally equivalent to calling ImGui::IsItemHovered()."); + ImGui::Checkbox("Item Disabled", &item_disabled); + + // Submit selected items so we can query their status in the code following it. + bool ret = false; static bool b = false; static float col4f[4] = { 1.0f, 0.5, 0.0f, 1.0f }; static char str[16] = {}; - ImGui::RadioButton("Text", &item_type, 0); - ImGui::RadioButton("Button", &item_type, 1); - ImGui::RadioButton("Checkbox", &item_type, 2); - ImGui::RadioButton("SliderFloat", &item_type, 3); - ImGui::RadioButton("InputText", &item_type, 4); - ImGui::RadioButton("InputFloat3", &item_type, 5); - ImGui::RadioButton("ColorEdit4", &item_type, 6); - ImGui::RadioButton("MenuItem", &item_type, 7); - ImGui::RadioButton("TreeNode (w/ double-click)", &item_type, 8); - ImGui::RadioButton("ListBox", &item_type, 9); - ImGui::Separator(); - bool ret = false; + if (item_disabled) + ImGui::BeginDisabled(true); if (item_type == 0) { ImGui::Text("ITEM: Text"); } // Testing text items with no identifier/interaction if (item_type == 1) { ret = ImGui::Button("ITEM: Button"); } // Testing button - if (item_type == 2) { ret = ImGui::Checkbox("ITEM: Checkbox", &b); } // Testing checkbox - if (item_type == 3) { ret = ImGui::SliderFloat("ITEM: SliderFloat", &col4f[0], 0.0f, 1.0f); } // Testing basic item - if (item_type == 4) { ret = ImGui::InputText("ITEM: InputText", &str[0], IM_ARRAYSIZE(str)); } // Testing input text (which handles tabbing) - if (item_type == 5) { ret = ImGui::InputFloat3("ITEM: InputFloat3", col4f); } // Testing multi-component items (IsItemXXX flags are reported merged) - if (item_type == 6) { ret = ImGui::ColorEdit4("ITEM: ColorEdit4", col4f); } // Testing multi-component items (IsItemXXX flags are reported merged) - if (item_type == 7) { ret = ImGui::MenuItem("ITEM: MenuItem"); } // Testing menu item (they use ImGuiButtonFlags_PressedOnRelease button policy) - if (item_type == 8) { ret = ImGui::TreeNodeEx("ITEM: TreeNode w/ ImGuiTreeNodeFlags_OpenOnDoubleClick", ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_NoTreePushOnOpen); } // Testing tree node with ImGuiButtonFlags_PressedOnDoubleClick button policy. - if (item_type == 9) { const char* items[] = { "Apple", "Banana", "Cherry", "Kiwi" }; static int current = 1; ret = ImGui::ListBox("ITEM: ListBox", ¤t, items, IM_ARRAYSIZE(items), IM_ARRAYSIZE(items)); } + if (item_type == 2) { ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); ret = ImGui::Button("ITEM: Button"); ImGui::PopItemFlag(); } // Testing button (with repeater) + if (item_type == 3) { ret = ImGui::Checkbox("ITEM: Checkbox", &b); } // Testing checkbox + if (item_type == 4) { ret = ImGui::SliderFloat("ITEM: SliderFloat", &col4f[0], 0.0f, 1.0f); } // Testing basic item + if (item_type == 5) { ret = ImGui::InputText("ITEM: InputText", &str[0], IM_ARRAYSIZE(str)); } // Testing input text (which handles tabbing) + if (item_type == 6) { ret = ImGui::InputTextMultiline("ITEM: InputTextMultiline", &str[0], IM_ARRAYSIZE(str)); } // Testing input text (which uses a child window) + if (item_type == 7) { ret = ImGui::InputFloat("ITEM: InputFloat", col4f, 1.0f); } // Testing +/- buttons on scalar input + if (item_type == 8) { ret = ImGui::InputFloat3("ITEM: InputFloat3", col4f); } // Testing multi-component items (IsItemXXX flags are reported merged) + if (item_type == 9) { ret = ImGui::ColorEdit4("ITEM: ColorEdit4", col4f); } // Testing multi-component items (IsItemXXX flags are reported merged) + if (item_type == 10) { ret = ImGui::Selectable("ITEM: Selectable"); } // Testing selectable item + if (item_type == 11) { ret = ImGui::MenuItem("ITEM: MenuItem"); } // Testing menu item (they use ImGuiButtonFlags_PressedOnRelease button policy) + if (item_type == 12) { ret = ImGui::TreeNode("ITEM: TreeNode"); if (ret) ImGui::TreePop(); } // Testing tree node + if (item_type == 13) { ret = ImGui::TreeNodeEx("ITEM: TreeNode w/ ImGuiTreeNodeFlags_OpenOnDoubleClick", ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_NoTreePushOnOpen); } // Testing tree node with ImGuiButtonFlags_PressedOnDoubleClick button policy. + if (item_type == 14) { const char* items[] = { "Apple", "Banana", "Cherry", "Kiwi" }; static int current = 1; ret = ImGui::Combo("ITEM: Combo", ¤t, items, IM_ARRAYSIZE(items)); } + if (item_type == 15) { const char* items[] = { "Apple", "Banana", "Cherry", "Kiwi" }; static int current = 1; ret = ImGui::ListBox("ITEM: ListBox", ¤t, items, IM_ARRAYSIZE(items), IM_ARRAYSIZE(items)); } + + bool hovered_delay_none = ImGui::IsItemHovered(); + bool hovered_delay_stationary = ImGui::IsItemHovered(ImGuiHoveredFlags_Stationary); + bool hovered_delay_short = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort); + bool hovered_delay_normal = ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal); + bool hovered_delay_tooltip = ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip); // = Normal + Stationary + + // Display the values of IsItemHovered() and other common item state functions. + // Note that the ImGuiHoveredFlags_XXX flags can be combined. + // Because BulletText is an item itself and that would affect the output of IsItemXXX functions, + // we query every state in a single call to avoid storing them and to simplify the code. ImGui::BulletText( "Return value = %d\n" "IsItemFocused() = %d\n" "IsItemHovered() = %d\n" "IsItemHovered(_AllowWhenBlockedByPopup) = %d\n" "IsItemHovered(_AllowWhenBlockedByActiveItem) = %d\n" - "IsItemHovered(_AllowWhenOverlapped) = %d\n" + "IsItemHovered(_AllowWhenOverlappedByItem) = %d\n" + "IsItemHovered(_AllowWhenOverlappedByWindow) = %d\n" + "IsItemHovered(_AllowWhenDisabled) = %d\n" "IsItemHovered(_RectOnly) = %d\n" "IsItemActive() = %d\n" "IsItemEdited() = %d\n" @@ -1582,6 +2165,7 @@ static void ShowDemoWindowWidgets() "IsItemDeactivatedAfterEdit() = %d\n" "IsItemVisible() = %d\n" "IsItemClicked() = %d\n" + "IsItemToggledOpen() = %d\n" "GetItemRectMin() = (%.1f, %.1f)\n" "GetItemRectMax() = (%.1f, %.1f)\n" "GetItemRectSize() = (%.1f, %.1f)", @@ -1590,7 +2174,9 @@ static void ShowDemoWindowWidgets() ImGui::IsItemHovered(), ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup), ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), - ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlapped), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByItem), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenOverlappedByWindow), + ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled), ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly), ImGui::IsItemActive(), ImGui::IsItemEdited(), @@ -1599,63 +2185,112 @@ static void ShowDemoWindowWidgets() ImGui::IsItemDeactivatedAfterEdit(), ImGui::IsItemVisible(), ImGui::IsItemClicked(), + ImGui::IsItemToggledOpen(), ImGui::GetItemRectMin().x, ImGui::GetItemRectMin().y, ImGui::GetItemRectMax().x, ImGui::GetItemRectMax().y, ImGui::GetItemRectSize().x, ImGui::GetItemRectSize().y ); + ImGui::BulletText( + "with Hovering Delay or Stationary test:\n" + "IsItemHovered() = = %d\n" + "IsItemHovered(_Stationary) = %d\n" + "IsItemHovered(_DelayShort) = %d\n" + "IsItemHovered(_DelayNormal) = %d\n" + "IsItemHovered(_Tooltip) = %d", + hovered_delay_none, hovered_delay_stationary, hovered_delay_short, hovered_delay_normal, hovered_delay_tooltip); + if (item_disabled) + ImGui::EndDisabled(); + + char buf[1] = ""; + ImGui::InputText("unused", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_ReadOnly); + ImGui::SameLine(); + HelpMarker("This widget is only here to be able to tab-out of the widgets above and see e.g. Deactivated() status."); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Querying Window Status (Focused,Hovered etc.)"); + if (ImGui::TreeNode("Querying Window Status (Focused/Hovered etc.)")) + { static bool embed_all_inside_a_child_window = false; - ImGui::Checkbox("Embed everything inside a child window (for additional testing)", &embed_all_inside_a_child_window); + ImGui::Checkbox("Embed everything inside a child window for testing _RootWindow flag.", &embed_all_inside_a_child_window); if (embed_all_inside_a_child_window) - ImGui::BeginChild("outer_child", ImVec2(0, ImGui::GetFontSize() * 20), true); + ImGui::BeginChild("outer_child", ImVec2(0, ImGui::GetFontSize() * 20.0f), ImGuiChildFlags_Borders); - // Testing IsWindowFocused() function with its various flags. Note that the flags can be combined. + // Testing IsWindowFocused() function with its various flags. ImGui::BulletText( "IsWindowFocused() = %d\n" "IsWindowFocused(_ChildWindows) = %d\n" + "IsWindowFocused(_ChildWindows|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_ChildWindows|_DockHierarchy) = %d\n" "IsWindowFocused(_ChildWindows|_RootWindow) = %d\n" + "IsWindowFocused(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_ChildWindows|_RootWindow|_DockHierarchy) = %d\n" "IsWindowFocused(_RootWindow) = %d\n" + "IsWindowFocused(_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowFocused(_RootWindow|_DockHierarchy) = %d\n" "IsWindowFocused(_AnyWindow) = %d\n", ImGui::IsWindowFocused(), ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_DockHierarchy), ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows | ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_DockHierarchy), ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow), + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_NoPopupHierarchy), + ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow | ImGuiFocusedFlags_DockHierarchy), ImGui::IsWindowFocused(ImGuiFocusedFlags_AnyWindow)); - // Testing IsWindowHovered() function with its various flags. Note that the flags can be combined. + // Testing IsWindowHovered() function with its various flags. ImGui::BulletText( "IsWindowHovered() = %d\n" "IsWindowHovered(_AllowWhenBlockedByPopup) = %d\n" "IsWindowHovered(_AllowWhenBlockedByActiveItem) = %d\n" "IsWindowHovered(_ChildWindows) = %d\n" + "IsWindowHovered(_ChildWindows|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_ChildWindows|_DockHierarchy) = %d\n" "IsWindowHovered(_ChildWindows|_RootWindow) = %d\n" - "IsWindowHovered(_ChildWindows|_AllowWhenBlockedByPopup) = %d\n" + "IsWindowHovered(_ChildWindows|_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_ChildWindows|_RootWindow|_DockHierarchy) = %d\n" "IsWindowHovered(_RootWindow) = %d\n" - "IsWindowHovered(_AnyWindow) = %d\n", + "IsWindowHovered(_RootWindow|_NoPopupHierarchy) = %d\n" + "IsWindowHovered(_RootWindow|_DockHierarchy) = %d\n" + "IsWindowHovered(_ChildWindows|_AllowWhenBlockedByPopup) = %d\n" + "IsWindowHovered(_AnyWindow) = %d\n" + "IsWindowHovered(_Stationary) = %d\n", ImGui::IsWindowHovered(), ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup), ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_DockHierarchy), ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow), - ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByPopup), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_DockHierarchy), ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow), - ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)); + ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_NoPopupHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_DockHierarchy), + ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_AllowWhenBlockedByPopup), + ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow), + ImGui::IsWindowHovered(ImGuiHoveredFlags_Stationary)); - ImGui::BeginChild("child", ImVec2(0, 50), true); + ImGui::BeginChild("child", ImVec2(0, 50), ImGuiChildFlags_Borders); ImGui::Text("This is another child window for testing the _ChildWindows flag."); ImGui::EndChild(); if (embed_all_inside_a_child_window) ImGui::EndChild(); - static char dummy_str[] = "This is a dummy field to be able to tab-out of the widgets above."; - ImGui::InputText("dummy", dummy_str, IM_ARRAYSIZE(dummy_str), ImGuiInputTextFlags_ReadOnly); - // Calling IsItemHovered() after begin returns the hovered status of the title bar. - // This is useful in particular if you want to create a context menu (with BeginPopupContextItem) associated to the title bar of a window. + // This is useful in particular if you want to create a context menu associated to the title bar of a window. + // This will also work when docked into a Tab (the Tab replace the Title Bar and guarantee the same properties). static bool test_window = false; ImGui::Checkbox("Hovered/Active tests after Begin() for title bar testing", &test_window); if (test_window) { + // FIXME-DOCK: This window cannot be docked within the ImGui Demo window, this will cause a feedback loop and get them stuck. + // Could we fix this through an ImGuiWindowClass feature? Or an API call to tag our parent as "don't skip items"? ImGui::Begin("Title bar Hovered/Active tests", &test_window); if (ImGui::BeginPopupContextItem()) // <-- This is using IsItemHovered() { @@ -1673,219 +2308,1120 @@ static void ShowDemoWindowWidgets() } } -static void ShowDemoWindowLayout() +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsSelectables() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsSelectables() { - if (!ImGui::CollapsingHeader("Layout")) - return; - - if (ImGui::TreeNode("Child windows")) + IMGUI_DEMO_MARKER("Widgets/Selectables"); + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::TreeNode("Selectables")) { - HelpMarker("Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window."); - static bool disable_mouse_wheel = false; - static bool disable_menu = false; - ImGui::Checkbox("Disable Mouse Wheel", &disable_mouse_wheel); - ImGui::Checkbox("Disable Menu", &disable_menu); - - static int line = 50; - bool goto_line = ImGui::Button("Goto"); - ImGui::SameLine(); - ImGui::SetNextItemWidth(100); - goto_line |= ImGui::InputInt("##Line", &line, 0, 0, ImGuiInputTextFlags_EnterReturnsTrue); - - // Child 1: no border, enable horizontal scrollbar + // Selectable() has 2 overloads: + // - The one taking "bool selected" as a read-only selection information. + // When Selectable() has been clicked it returns true and you can alter selection state accordingly. + // - The one taking "bool* p_selected" as a read-write selection information (convenient in some cases) + // The earlier is more flexible, as in real application your selection may be stored in many different ways + // and not necessarily inside a bool value (e.g. in flags within objects, as an external list, etc). + IMGUI_DEMO_MARKER("Widgets/Selectables/Basic"); + if (ImGui::TreeNode("Basic")) { - ImGuiWindowFlags window_flags = ImGuiWindowFlags_HorizontalScrollbar | (disable_mouse_wheel ? ImGuiWindowFlags_NoScrollWithMouse : 0); - ImGui::BeginChild("Child1", ImVec2(ImGui::GetWindowContentRegionWidth() * 0.5f, 260), false, window_flags); - for (int i = 0; i < 100; i++) - { - ImGui::Text("%04d: scrollable region", i); - if (goto_line && line == i) - ImGui::SetScrollHereY(); - } - if (goto_line && line >= 100) - ImGui::SetScrollHereY(); - ImGui::EndChild(); + static bool selection[5] = { false, true, false, false }; + ImGui::Selectable("1. I am selectable", &selection[0]); + ImGui::Selectable("2. I am selectable", &selection[1]); + ImGui::Selectable("3. I am selectable", &selection[2]); + if (ImGui::Selectable("4. I am double clickable", selection[3], ImGuiSelectableFlags_AllowDoubleClick)) + if (ImGui::IsMouseDoubleClicked(0)) + selection[3] = !selection[3]; + ImGui::TreePop(); } - ImGui::SameLine(); - - // Child 2: rounded border + IMGUI_DEMO_MARKER("Widgets/Selectables/Rendering more items on the same line"); + if (ImGui::TreeNode("Rendering more items on the same line")) { - ImGuiWindowFlags window_flags = (disable_mouse_wheel ? ImGuiWindowFlags_NoScrollWithMouse : 0) | (disable_menu ? 0 : ImGuiWindowFlags_MenuBar); - ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f); - ImGui::BeginChild("Child2", ImVec2(0, 260), true, window_flags); - if (!disable_menu && ImGui::BeginMenuBar()) + // (1) Using SetNextItemAllowOverlap() + // (2) Using the Selectable() override that takes "bool* p_selected" parameter, the bool value is toggled automatically. + static bool selected[3] = { false, false, false }; + ImGui::SetNextItemAllowOverlap(); ImGui::Selectable("main.c", &selected[0]); ImGui::SameLine(); ImGui::SmallButton("Link 1"); + ImGui::SetNextItemAllowOverlap(); ImGui::Selectable("Hello.cpp", &selected[1]); ImGui::SameLine(); ImGui::SmallButton("Link 2"); + ImGui::SetNextItemAllowOverlap(); ImGui::Selectable("Hello.h", &selected[2]); ImGui::SameLine(); ImGui::SmallButton("Link 3"); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Selectables/In Tables"); + if (ImGui::TreeNode("In Tables")) + { + static bool selected[10] = {}; + + if (ImGui::BeginTable("split1", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_Borders)) { - if (ImGui::BeginMenu("Menu")) + for (int i = 0; i < 10; i++) { - ShowExampleMenuFile(); - ImGui::EndMenu(); + char label[32]; + sprintf(label, "Item %d", i); + ImGui::TableNextColumn(); + ImGui::Selectable(label, &selected[i]); // FIXME-TABLE: Selection overlap } - ImGui::EndMenuBar(); + ImGui::EndTable(); } - ImGui::Columns(2); - for (int i = 0; i < 100; i++) + ImGui::Spacing(); + if (ImGui::BeginTable("split2", 3, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_Borders)) + { + for (int i = 0; i < 10; i++) + { + char label[32]; + sprintf(label, "Item %d", i); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Selectable(label, &selected[i], ImGuiSelectableFlags_SpanAllColumns); + ImGui::TableNextColumn(); + ImGui::Text("Some other contents"); + ImGui::TableNextColumn(); + ImGui::Text("123456"); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Selectables/Grid"); + if (ImGui::TreeNode("Grid")) + { + static char selected[4][4] = { { 1, 0, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0, 1, 0 }, { 0, 0, 0, 1 } }; + + // Add in a bit of silly fun... + const float time = (float)ImGui::GetTime(); + const bool winning_state = memchr(selected, 0, sizeof(selected)) == NULL; // If all cells are selected... + if (winning_state) + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(0.5f + 0.5f * cosf(time * 2.0f), 0.5f + 0.5f * sinf(time * 3.0f))); + + for (int y = 0; y < 4; y++) + for (int x = 0; x < 4; x++) + { + if (x > 0) + ImGui::SameLine(); + ImGui::PushID(y * 4 + x); + if (ImGui::Selectable("Sailor", selected[y][x] != 0, 0, ImVec2(50, 50))) + { + // Toggle clicked cell + toggle neighbors + selected[y][x] ^= 1; + if (x > 0) { selected[y][x - 1] ^= 1; } + if (x < 3) { selected[y][x + 1] ^= 1; } + if (y > 0) { selected[y - 1][x] ^= 1; } + if (y < 3) { selected[y + 1][x] ^= 1; } + } + ImGui::PopID(); + } + + if (winning_state) + ImGui::PopStyleVar(); + ImGui::TreePop(); + } + IMGUI_DEMO_MARKER("Widgets/Selectables/Alignment"); + if (ImGui::TreeNode("Alignment")) + { + HelpMarker( + "By default, Selectables uses style.SelectableTextAlign but it can be overridden on a per-item " + "basis using PushStyleVar(). You'll probably want to always keep your default situation to " + "left-align otherwise it becomes difficult to layout multiple items on a same line"); + static bool selected[3 * 3] = { true, false, true, false, true, false, true, false, true }; + for (int y = 0; y < 3; y++) + { + for (int x = 0; x < 3; x++) + { + ImVec2 alignment = ImVec2((float)x / 2.0f, (float)y / 2.0f); + char name[32]; + sprintf(name, "(%.1f,%.1f)", alignment.x, alignment.y); + if (x > 0) ImGui::SameLine(); + ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, alignment); + ImGui::Selectable(name, &selected[3 * y + x], ImGuiSelectableFlags_None, ImVec2(80, 80)); + ImGui::PopStyleVar(); + } + } + ImGui::TreePop(); + } + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsSelectionAndMultiSelect() +//----------------------------------------------------------------------------- +// Multi-selection demos +// Also read: https://github.com/ocornut/imgui/wiki/Multi-Select +//----------------------------------------------------------------------------- + +static const char* ExampleNames[] = +{ + "Artichoke", "Arugula", "Asparagus", "Avocado", "Bamboo Shoots", "Bean Sprouts", "Beans", "Beet", "Belgian Endive", "Bell Pepper", + "Bitter Gourd", "Bok Choy", "Broccoli", "Brussels Sprouts", "Burdock Root", "Cabbage", "Calabash", "Capers", "Carrot", "Cassava", + "Cauliflower", "Celery", "Celery Root", "Celcuce", "Chayote", "Chinese Broccoli", "Corn", "Cucumber" +}; + +// Extra functions to add deletion support to ImGuiSelectionBasicStorage +struct ExampleSelectionWithDeletion : ImGuiSelectionBasicStorage +{ + // Find which item should be Focused after deletion. + // Call _before_ item submission. Retunr an index in the before-deletion item list, your item loop should call SetKeyboardFocusHere() on it. + // The subsequent ApplyDeletionPostLoop() code will use it to apply Selection. + // - We cannot provide this logic in core Dear ImGui because we don't have access to selection data. + // - We don't actually manipulate the ImVector<> here, only in ApplyDeletionPostLoop(), but using similar API for consistency and flexibility. + // - Important: Deletion only works if the underlying ImGuiID for your items are stable: aka not depend on their index, but on e.g. item id/ptr. + // FIXME-MULTISELECT: Doesn't take account of the possibility focus target will be moved during deletion. Need refocus or scroll offset. + int ApplyDeletionPreLoop(ImGuiMultiSelectIO* ms_io, int items_count) + { + if (Size == 0) + return -1; + + // If focused item is not selected... + const int focused_idx = (int)ms_io->NavIdItem; // Index of currently focused item + if (ms_io->NavIdSelected == false) // This is merely a shortcut, == Contains(adapter->IndexToStorage(items, focused_idx)) + { + ms_io->RangeSrcReset = true; // Request to recover RangeSrc from NavId next frame. Would be ok to reset even when NavIdSelected==true, but it would take an extra frame to recover RangeSrc when deleting a selected item. + return focused_idx; // Request to focus same item after deletion. + } + + // If focused item is selected: land on first unselected item after focused item. + for (int idx = focused_idx + 1; idx < items_count; idx++) + if (!Contains(GetStorageIdFromIndex(idx))) + return idx; + + // If focused item is selected: otherwise return last unselected item before focused item. + for (int idx = IM_MIN(focused_idx, items_count) - 1; idx >= 0; idx--) + if (!Contains(GetStorageIdFromIndex(idx))) + return idx; + + return -1; + } + + // Rewrite item list (delete items) + update selection. + // - Call after EndMultiSelect() + // - We cannot provide this logic in core Dear ImGui because we don't have access to your items, nor to selection data. + template + void ApplyDeletionPostLoop(ImGuiMultiSelectIO* ms_io, ImVector& items, int item_curr_idx_to_select) + { + // Rewrite item list (delete items) + convert old selection index (before deletion) to new selection index (after selection). + // If NavId was not part of selection, we will stay on same item. + ImVector new_items; + new_items.reserve(items.Size - Size); + int item_next_idx_to_select = -1; + for (int idx = 0; idx < items.Size; idx++) + { + if (!Contains(GetStorageIdFromIndex(idx))) + new_items.push_back(items[idx]); + if (item_curr_idx_to_select == idx) + item_next_idx_to_select = new_items.Size - 1; + } + items.swap(new_items); + + // Update selection + Clear(); + if (item_next_idx_to_select != -1 && ms_io->NavIdSelected) + SetItemSelected(GetStorageIdFromIndex(item_next_idx_to_select), true); + } +}; + +// Example: Implement dual list box storage and interface +struct ExampleDualListBox +{ + ImVector Items[2]; // ID is index into ExampleName[] + ImGuiSelectionBasicStorage Selections[2]; // Store ExampleItemId into selection + bool OptKeepSorted = true; + + void MoveAll(int src, int dst) + { + IM_ASSERT((src == 0 && dst == 1) || (src == 1 && dst == 0)); + for (ImGuiID item_id : Items[src]) + Items[dst].push_back(item_id); + Items[src].clear(); + SortItems(dst); + Selections[src].Swap(Selections[dst]); + Selections[src].Clear(); + } + void MoveSelected(int src, int dst) + { + for (int src_n = 0; src_n < Items[src].Size; src_n++) + { + ImGuiID item_id = Items[src][src_n]; + if (!Selections[src].Contains(item_id)) + continue; + Items[src].erase(&Items[src][src_n]); // FIXME-OPT: Could be implemented more optimally (rebuild src items and swap) + Items[dst].push_back(item_id); + src_n--; + } + if (OptKeepSorted) + SortItems(dst); + Selections[src].Swap(Selections[dst]); + Selections[src].Clear(); + } + void ApplySelectionRequests(ImGuiMultiSelectIO* ms_io, int side) + { + // In this example we store item id in selection (instead of item index) + Selections[side].UserData = Items[side].Data; + Selections[side].AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImGuiID* items = (ImGuiID*)self->UserData; return items[idx]; }; + Selections[side].ApplyRequests(ms_io); + } + static int IMGUI_CDECL CompareItemsByValue(const void* lhs, const void* rhs) + { + const int* a = (const int*)lhs; + const int* b = (const int*)rhs; + return (*a - *b) > 0 ? +1 : -1; + } + void SortItems(int n) + { + qsort(Items[n].Data, (size_t)Items[n].Size, sizeof(Items[n][0]), CompareItemsByValue); + } + void Show() + { + //ImGui::Checkbox("Sorted", &OptKeepSorted); + if (ImGui::BeginTable("split", 3, ImGuiTableFlags_None)) + { + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); // Left side + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed); // Buttons + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch); // Right side + ImGui::TableNextRow(); + + int request_move_selected = -1; + int request_move_all = -1; + float child_height_0 = 0.0f; + for (int side = 0; side < 2; side++) + { + // FIXME-MULTISELECT: Dual List Box: Add context menus + // FIXME-NAV: Using ImGuiWindowFlags_NavFlattened exhibit many issues. + ImVector& items = Items[side]; + ImGuiSelectionBasicStorage& selection = Selections[side]; + + ImGui::TableSetColumnIndex((side == 0) ? 0 : 2); + ImGui::Text("%s (%d)", (side == 0) ? "Available" : "Basket", items.Size); + + // Submit scrolling range to avoid glitches on moving/deletion + const float items_height = ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); + + bool child_visible; + if (side == 0) + { + // Left child is resizable + ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, ImGui::GetFrameHeightWithSpacing() * 4), ImVec2(FLT_MAX, FLT_MAX)); + child_visible = ImGui::BeginChild("0", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY); + child_height_0 = ImGui::GetWindowSize().y; + } + else + { + // Right child use same height as left one + child_visible = ImGui::BeginChild("1", ImVec2(-FLT_MIN, child_height_0), ImGuiChildFlags_FrameStyle); + } + if (child_visible) + { + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_None; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, items.Size); + ApplySelectionRequests(ms_io, side); + + for (int item_n = 0; item_n < items.Size; item_n++) + { + ImGuiID item_id = items[item_n]; + bool item_is_selected = selection.Contains(item_id); + ImGui::SetNextItemSelectionUserData(item_n); + ImGui::Selectable(ExampleNames[item_id], item_is_selected, ImGuiSelectableFlags_AllowDoubleClick); + if (ImGui::IsItemFocused()) + { + // FIXME-MULTISELECT: Dual List Box: Transfer focus + if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) + request_move_selected = side; + if (ImGui::IsMouseDoubleClicked(0)) // FIXME-MULTISELECT: Double-click on multi-selection? + request_move_selected = side; + } + } + + ms_io = ImGui::EndMultiSelect(); + ApplySelectionRequests(ms_io, side); + } + ImGui::EndChild(); + } + + // Buttons columns + ImGui::TableSetColumnIndex(1); + ImGui::NewLine(); + //ImVec2 button_sz = { ImGui::CalcTextSize(">>").x + ImGui::GetStyle().FramePadding.x * 2.0f, ImGui::GetFrameHeight() + padding.y * 2.0f }; + ImVec2 button_sz = { ImGui::GetFrameHeight(), ImGui::GetFrameHeight() }; + + // (Using BeginDisabled()/EndDisabled() works but feels distracting given how it is currently visualized) + if (ImGui::Button(">>", button_sz)) + request_move_all = 0; + if (ImGui::Button(">", button_sz)) + request_move_selected = 0; + if (ImGui::Button("<", button_sz)) + request_move_selected = 1; + if (ImGui::Button("<<", button_sz)) + request_move_all = 1; + + // Process requests + if (request_move_all != -1) + MoveAll(request_move_all, request_move_all ^ 1); + if (request_move_selected != -1) + MoveSelected(request_move_selected, request_move_selected ^ 1); + + // FIXME-MULTISELECT: Support action from outside + /* + if (OptKeepSorted == false) + { + ImGui::NewLine(); + if (ImGui::ArrowButton("MoveUp", ImGuiDir_Up)) {} + if (ImGui::ArrowButton("MoveDown", ImGuiDir_Down)) {} + } + */ + + ImGui::EndTable(); + } + } +}; + +static void DemoWindowWidgetsSelectionAndMultiSelect(ImGuiDemoWindowData* demo_data) +{ + IMGUI_DEMO_MARKER("Widgets/Selection State & Multi-Select"); + if (ImGui::TreeNode("Selection State & Multi-Select")) + { + HelpMarker("Selections can be built using Selectable(), TreeNode() or other widgets. Selection state is owned by application code/data."); + + // Without any fancy API: manage single-selection yourself. + IMGUI_DEMO_MARKER("Widgets/Selection State/Single-Select"); + if (ImGui::TreeNode("Single-Select")) + { + static int selected = -1; + for (int n = 0; n < 5; n++) { char buf[32]; - sprintf(buf, "%03d", i); - ImGui::Button(buf, ImVec2(-1.0f, 0.0f)); - ImGui::NextColumn(); + sprintf(buf, "Object %d", n); + if (ImGui::Selectable(buf, selected == n)) + selected = n; + } + ImGui::TreePop(); + } + + // Demonstrate implementation a most-basic form of multi-selection manually + // This doesn't support the SHIFT modifier which requires BeginMultiSelect()! + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (manual/simplified, without BeginMultiSelect)"); + if (ImGui::TreeNode("Multi-Select (manual/simplified, without BeginMultiSelect)")) + { + HelpMarker("Hold CTRL and click to select multiple items."); + static bool selection[5] = { false, false, false, false, false }; + for (int n = 0; n < 5; n++) + { + char buf[32]; + sprintf(buf, "Object %d", n); + if (ImGui::Selectable(buf, selection[n])) + { + if (!ImGui::GetIO().KeyCtrl) // Clear selection when CTRL is not held + memset(selection, 0, sizeof(selection)); + selection[n] ^= 1; // Toggle current item + } + } + ImGui::TreePop(); + } + + // Demonstrate handling proper multi-selection using the BeginMultiSelect/EndMultiSelect API. + // SHIFT+Click w/ CTRL and other standard features are supported. + // We use the ImGuiSelectionBasicStorage helper which you may freely reimplement. + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select"); + if (ImGui::TreeNode("Multi-Select")) + { + ImGui::Text("Supported features:"); + ImGui::BulletText("Keyboard navigation (arrows, page up/down, home/end, space)."); + ImGui::BulletText("Ctrl modifier to preserve and toggle selection."); + ImGui::BulletText("Shift modifier for range selection."); + ImGui::BulletText("CTRL+A to select all."); + ImGui::BulletText("Escape to clear selection."); + ImGui::BulletText("Click and drag to box-select."); + ImGui::Text("Tip: Use 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen."); + + // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection + const int ITEMS_COUNT = 50; + static ImGuiSelectionBasicStorage selection; + ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); + + // The BeginChild() has no purpose for selection logic, other that offering a scrolling region. + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) + { + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, ITEMS_COUNT); + selection.ApplyRequests(ms_io); + + for (int n = 0; n < ITEMS_COUNT; n++) + { + char label[64]; + sprintf(label, "Object %05d: %s", n, ExampleNames[n % IM_ARRAYSIZE(ExampleNames)]); + bool item_is_selected = selection.Contains((ImGuiID)n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Selectable(label, item_is_selected); + } + + ms_io = ImGui::EndMultiSelect(); + selection.ApplyRequests(ms_io); } ImGui::EndChild(); - ImGui::PopStyleVar(); + ImGui::TreePop(); } - ImGui::Separator(); - - // Demonstrate a few extra things - // - Changing ImGuiCol_ChildBg (which is transparent black in default styles) - // - Using SetCursorPos() to position the child window (because the child window is an item from the POV of the parent window) - // You can also call SetNextWindowPos() to position the child window. The parent window will effectively layout from this position. - // - Using ImGui::GetItemRectMin/Max() to query the "item" state (because the child window is an item from the POV of the parent window) - // See "Widgets" -> "Querying Status (Active/Focused/Hovered etc.)" section for more details about this. + // Demonstrate using the clipper with BeginMultiSelect()/EndMultiSelect() + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (with clipper)"); + if (ImGui::TreeNode("Multi-Select (with clipper)")) { - ImGui::SetCursorPosX(50); - ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(255, 0, 0, 100)); - ImGui::BeginChild("blah", ImVec2(200, 100), true, ImGuiWindowFlags_None); - for (int n = 0; n < 50; n++) - ImGui::Text("Some test %d", n); + // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection + static ImGuiSelectionBasicStorage selection; + + ImGui::Text("Added features:"); + ImGui::BulletText("Using ImGuiListClipper."); + + const int ITEMS_COUNT = 10000; + ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) + { + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, ITEMS_COUNT); + selection.ApplyRequests(ms_io); + + ImGuiListClipper clipper; + clipper.Begin(ITEMS_COUNT); + if (ms_io->RangeSrcItem != -1) + clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. + while (clipper.Step()) + { + for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) + { + char label[64]; + sprintf(label, "Object %05d: %s", n, ExampleNames[n % IM_ARRAYSIZE(ExampleNames)]); + bool item_is_selected = selection.Contains((ImGuiID)n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Selectable(label, item_is_selected); + } + } + + ms_io = ImGui::EndMultiSelect(); + selection.ApplyRequests(ms_io); + } ImGui::EndChild(); - ImVec2 child_rect_min = ImGui::GetItemRectMin(); - ImVec2 child_rect_max = ImGui::GetItemRectMax(); - ImGui::PopStyleColor(); - ImGui::Text("Rect of child window is: (%.0f,%.0f) (%.0f,%.0f)", child_rect_min.x, child_rect_min.y, child_rect_max.x, child_rect_max.y); + ImGui::TreePop(); } - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Widgets Width")) - { - // Use SetNextItemWidth() to set the width of a single upcoming item. - // Use PushItemWidth()/PopItemWidth() to set the width of a group of items. - static float f = 0.0f; - ImGui::Text("SetNextItemWidth/PushItemWidth(100)"); - ImGui::SameLine(); HelpMarker("Fixed width."); - ImGui::SetNextItemWidth(100); - ImGui::DragFloat("float##1", &f); - - ImGui::Text("SetNextItemWidth/PushItemWidth(GetWindowWidth() * 0.5f)"); - ImGui::SameLine(); HelpMarker("Half of window width."); - ImGui::SetNextItemWidth(ImGui::GetWindowWidth() * 0.5f); - ImGui::DragFloat("float##2", &f); - - ImGui::Text("SetNextItemWidth/PushItemWidth(GetContentRegionAvail().x * 0.5f)"); - ImGui::SameLine(); HelpMarker("Half of available width.\n(~ right-cursor_pos)\n(works within a column set)"); - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.5f); - ImGui::DragFloat("float##3", &f); - - ImGui::Text("SetNextItemWidth/PushItemWidth(-100)"); - ImGui::SameLine(); HelpMarker("Align to right edge minus 100"); - ImGui::SetNextItemWidth(-100); - ImGui::DragFloat("float##4", &f); - - // Demonstrate using PushItemWidth to surround three items. Calling SetNextItemWidth() before each of them would have the same effect. - ImGui::Text("SetNextItemWidth/PushItemWidth(-1)"); - ImGui::SameLine(); HelpMarker("Align to right edge"); - ImGui::PushItemWidth(-1); - ImGui::DragFloat("float##5a", &f); - ImGui::DragFloat("float##5b", &f); - ImGui::DragFloat("float##5c", &f); - ImGui::PopItemWidth(); - - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Basic Horizontal Layout")) - { - ImGui::TextWrapped("(Use ImGui::SameLine() to keep adding items to the right of the preceding item)"); - - // Text - ImGui::Text("Two items: Hello"); ImGui::SameLine(); - ImGui::TextColored(ImVec4(1,1,0,1), "Sailor"); - - // Adjust spacing - ImGui::Text("More spacing: Hello"); ImGui::SameLine(0, 20); - ImGui::TextColored(ImVec4(1,1,0,1), "Sailor"); - - // Button - ImGui::AlignTextToFramePadding(); - ImGui::Text("Normal buttons"); ImGui::SameLine(); - ImGui::Button("Banana"); ImGui::SameLine(); - ImGui::Button("Apple"); ImGui::SameLine(); - ImGui::Button("Corniflower"); - - // Button - ImGui::Text("Small buttons"); ImGui::SameLine(); - ImGui::SmallButton("Like this one"); ImGui::SameLine(); - ImGui::Text("can fit within a text block."); - - // Aligned to arbitrary position. Easy/cheap column. - ImGui::Text("Aligned"); - ImGui::SameLine(150); ImGui::Text("x=150"); - ImGui::SameLine(300); ImGui::Text("x=300"); - ImGui::Text("Aligned"); - ImGui::SameLine(150); ImGui::SmallButton("x=150"); - ImGui::SameLine(300); ImGui::SmallButton("x=300"); - - // Checkbox - static bool c1 = false, c2 = false, c3 = false, c4 = false; - ImGui::Checkbox("My", &c1); ImGui::SameLine(); - ImGui::Checkbox("Tailor", &c2); ImGui::SameLine(); - ImGui::Checkbox("Is", &c3); ImGui::SameLine(); - ImGui::Checkbox("Rich", &c4); - - // Various - static float f0 = 1.0f, f1 = 2.0f, f2 = 3.0f; - ImGui::PushItemWidth(80); - const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD" }; - static int item = -1; - ImGui::Combo("Combo", &item, items, IM_ARRAYSIZE(items)); ImGui::SameLine(); - ImGui::SliderFloat("X", &f0, 0.0f, 5.0f); ImGui::SameLine(); - ImGui::SliderFloat("Y", &f1, 0.0f, 5.0f); ImGui::SameLine(); - ImGui::SliderFloat("Z", &f2, 0.0f, 5.0f); - ImGui::PopItemWidth(); - - ImGui::PushItemWidth(80); - ImGui::Text("Lists:"); - static int selection[4] = { 0, 1, 2, 3 }; - for (int i = 0; i < 4; i++) + // Demonstrate dynamic item list + deletion support using the BeginMultiSelect/EndMultiSelect API. + // In order to support Deletion without any glitches you need to: + // - (1) If items are submitted in their own scrolling area, submit contents size SetNextWindowContentSize() ahead of time to prevent one-frame readjustment of scrolling. + // - (2) Items needs to have persistent ID Stack identifier = ID needs to not depends on their index. PushID(index) = KO. PushID(item_id) = OK. This is in order to focus items reliably after a selection. + // - (3) BeginXXXX process + // - (4) Focus process + // - (5) EndXXXX process + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (with deletion)"); + if (ImGui::TreeNode("Multi-Select (with deletion)")) { - if (i > 0) ImGui::SameLine(); - ImGui::PushID(i); - ImGui::ListBox("", &selection[i], items, IM_ARRAYSIZE(items)); - ImGui::PopID(); - //if (ImGui::IsItemHovered()) ImGui::SetTooltip("ListBox %d hovered", i); + // Storing items data separately from selection data. + // (you may decide to store selection data inside your item (aka intrusive storage) if you don't need multiple views over same items) + // Use a custom selection.Adapter: store item identifier in Selection (instead of index) + static ImVector items; + static ExampleSelectionWithDeletion selection; + selection.UserData = (void*)&items; + selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self, int idx) { ImVector* p_items = (ImVector*)self->UserData; return (*p_items)[idx]; }; // Index -> ID + + ImGui::Text("Added features:"); + ImGui::BulletText("Dynamic list with Delete key support."); + ImGui::Text("Selection size: %d/%d", selection.Size, items.Size); + + // Initialize default list with 50 items + button to add/remove items. + static ImGuiID items_next_id = 0; + if (items_next_id == 0) + for (ImGuiID n = 0; n < 50; n++) + items.push_back(items_next_id++); + if (ImGui::SmallButton("Add 20 items")) { for (int n = 0; n < 20; n++) { items.push_back(items_next_id++); } } + ImGui::SameLine(); + if (ImGui::SmallButton("Remove 20 items")) { for (int n = IM_MIN(20, items.Size); n > 0; n--) { selection.SetItemSelected(items.back(), false); items.pop_back(); } } + + // (1) Extra to support deletion: Submit scrolling range to avoid glitches on deletion + const float items_height = ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); + + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) + { + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, items.Size); + selection.ApplyRequests(ms_io); + + const bool want_delete = ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (selection.Size > 0); + const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1; + + for (int n = 0; n < items.Size; n++) + { + const ImGuiID item_id = items[n]; + char label[64]; + sprintf(label, "Object %05u: %s", item_id, ExampleNames[item_id % IM_ARRAYSIZE(ExampleNames)]); + + bool item_is_selected = selection.Contains(item_id); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Selectable(label, item_is_selected); + if (item_curr_idx_to_focus == n) + ImGui::SetKeyboardFocusHere(-1); + } + + // Apply multi-select requests + ms_io = ImGui::EndMultiSelect(); + selection.ApplyRequests(ms_io); + if (want_delete) + selection.ApplyDeletionPostLoop(ms_io, items, item_curr_idx_to_focus); + } + ImGui::EndChild(); + ImGui::TreePop(); } - ImGui::PopItemWidth(); - // Dummy - ImVec2 button_sz(40, 40); - ImGui::Button("A", button_sz); ImGui::SameLine(); - ImGui::Dummy(button_sz); ImGui::SameLine(); - ImGui::Button("B", button_sz); - - // Manually wrapping (we should eventually provide this as an automatic layout feature, but for now you can do it manually) - ImGui::Text("Manually wrapping:"); - ImGuiStyle& style = ImGui::GetStyle(); - int buttons_count = 20; - float window_visible_x2 = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x; - for (int n = 0; n < buttons_count; n++) + // Implement a Dual List Box (#6648) + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (dual list box)"); + if (ImGui::TreeNode("Multi-Select (dual list box)")) { - ImGui::PushID(n); - ImGui::Button("Box", button_sz); - float last_button_x2 = ImGui::GetItemRectMax().x; - float next_button_x2 = last_button_x2 + style.ItemSpacing.x + button_sz.x; // Expected position if next button was on same line - if (n + 1 < buttons_count && next_button_x2 < window_visible_x2) + // Init default state + static ExampleDualListBox dlb; + if (dlb.Items[0].Size == 0 && dlb.Items[1].Size == 0) + for (int item_id = 0; item_id < IM_ARRAYSIZE(ExampleNames); item_id++) + dlb.Items[0].push_back((ImGuiID)item_id); + + // Show + dlb.Show(); + + ImGui::TreePop(); + } + + // Demonstrate using the clipper with BeginMultiSelect()/EndMultiSelect() + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (in a table)"); + if (ImGui::TreeNode("Multi-Select (in a table)")) + { + static ImGuiSelectionBasicStorage selection; + + const int ITEMS_COUNT = 10000; + ImGui::Text("Selection: %d/%d", selection.Size, ITEMS_COUNT); + if (ImGui::BeginTable("##Basket", 2, ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter)) + { + ImGui::TableSetupColumn("Object"); + ImGui::TableSetupColumn("Action"); + ImGui::TableSetupScrollFreeze(0, 1); + ImGui::TableHeadersRow(); + + ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, ITEMS_COUNT); + selection.ApplyRequests(ms_io); + + ImGuiListClipper clipper; + clipper.Begin(ITEMS_COUNT); + if (ms_io->RangeSrcItem != -1) + clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. + while (clipper.Step()) + { + for (int n = clipper.DisplayStart; n < clipper.DisplayEnd; n++) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + char label[64]; + sprintf(label, "Object %05d: %s", n, ExampleNames[n % IM_ARRAYSIZE(ExampleNames)]); + bool item_is_selected = selection.Contains((ImGuiID)n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Selectable(label, item_is_selected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap); + ImGui::TableNextColumn(); + ImGui::SmallButton("hello"); + } + } + + ms_io = ImGui::EndMultiSelect(); + selection.ApplyRequests(ms_io); + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (checkboxes)"); + if (ImGui::TreeNode("Multi-Select (checkboxes)")) + { + ImGui::Text("In a list of checkboxes (not selectable):"); + ImGui::BulletText("Using _NoAutoSelect + _NoAutoClear flags."); + ImGui::BulletText("Shift+Click to check multiple boxes."); + ImGui::BulletText("Shift+Keyboard to copy current value to other boxes."); + + // If you have an array of checkboxes, you may want to use NoAutoSelect + NoAutoClear and the ImGuiSelectionExternalStorage helper. + static bool items[20] = {}; + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_NoAutoSelect | ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_ClearOnEscape; + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect2d", &flags, ImGuiMultiSelectFlags_BoxSelect2d); // Cannot use ImGuiMultiSelectFlags_BoxSelect1d as checkboxes are varying width. + + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) + { + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, -1, IM_ARRAYSIZE(items)); + ImGuiSelectionExternalStorage storage_wrapper; + storage_wrapper.UserData = (void*)items; + storage_wrapper.AdapterSetItemSelected = [](ImGuiSelectionExternalStorage* self, int n, bool selected) { bool* array = (bool*)self->UserData; array[n] = selected; }; + storage_wrapper.ApplyRequests(ms_io); + for (int n = 0; n < 20; n++) + { + char label[32]; + sprintf(label, "Item %d", n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Checkbox(label, &items[n]); + } + ms_io = ImGui::EndMultiSelect(); + storage_wrapper.ApplyRequests(ms_io); + } + ImGui::EndChild(); + + ImGui::TreePop(); + } + + // Demonstrate individual selection scopes in same window + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (multiple scopes)"); + if (ImGui::TreeNode("Multi-Select (multiple scopes)")) + { + // Use default select: Pass index to SetNextItemSelectionUserData(), store index in Selection + const int SCOPES_COUNT = 3; + const int ITEMS_COUNT = 8; // Per scope + static ImGuiSelectionBasicStorage selections_data[SCOPES_COUNT]; + + // Use ImGuiMultiSelectFlags_ScopeRect to not affect other selections in same window. + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ScopeRect | ImGuiMultiSelectFlags_ClearOnEscape;// | ImGuiMultiSelectFlags_ClearOnClickVoid; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) + flags &= ~ImGuiMultiSelectFlags_ScopeRect; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeRect", &flags, ImGuiMultiSelectFlags_ScopeRect) && (flags & ImGuiMultiSelectFlags_ScopeRect)) + flags &= ~ImGuiMultiSelectFlags_ScopeWindow; + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect1d", &flags, ImGuiMultiSelectFlags_BoxSelect1d); + + for (int selection_scope_n = 0; selection_scope_n < SCOPES_COUNT; selection_scope_n++) + { + ImGui::PushID(selection_scope_n); + ImGuiSelectionBasicStorage* selection = &selections_data[selection_scope_n]; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection->Size, ITEMS_COUNT); + selection->ApplyRequests(ms_io); + + ImGui::SeparatorText("Selection scope"); + ImGui::Text("Selection size: %d/%d", selection->Size, ITEMS_COUNT); + + for (int n = 0; n < ITEMS_COUNT; n++) + { + char label[64]; + sprintf(label, "Object %05d: %s", n, ExampleNames[n % IM_ARRAYSIZE(ExampleNames)]); + bool item_is_selected = selection->Contains((ImGuiID)n); + ImGui::SetNextItemSelectionUserData(n); + ImGui::Selectable(label, item_is_selected); + } + + // Apply multi-select requests + ms_io = ImGui::EndMultiSelect(); + selection->ApplyRequests(ms_io); + ImGui::PopID(); + } + ImGui::TreePop(); + } + + // See ShowExampleAppAssetsBrowser() + if (ImGui::TreeNode("Multi-Select (tiled assets browser)")) + { + ImGui::Checkbox("Assets Browser", &demo_data->ShowAppAssetsBrowser); + ImGui::Text("(also access from 'Examples->Assets Browser' in menu)"); + ImGui::TreePop(); + } + + // Demonstrate supporting multiple-selection in a tree. + // - We don't use linear indices for selection user data, but our ExampleTreeNode* pointer directly! + // This showcase how SetNextItemSelectionUserData() never assume indices! + // - The difficulty here is to "interpolate" from RangeSrcItem to RangeDstItem in the SetAll/SetRange request. + // We want this interpolation to match what the user sees: in visible order, skipping closed nodes. + // This is implemented by our TreeGetNextNodeInVisibleOrder() user-space helper. + // - Important: In a real codebase aiming to implement full-featured selectable tree with custom filtering, you + // are more likely to build an array mapping sequential indices to visible tree nodes, since your + // filtering/search + clipping process will benefit from it. Having this will make this interpolation much easier. + // - Consider this a prototype: we are working toward simplifying some of it. + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (trees)"); + if (ImGui::TreeNode("Multi-Select (trees)")) + { + HelpMarker( + "This is rather advanced and experimental. If you are getting started with multi-select, " + "please don't start by looking at how to use it for a tree!\n\n" + "Future versions will try to simplify and formalize some of this."); + + struct ExampleTreeFuncs + { + static void DrawNode(ExampleTreeNode* node, ImGuiSelectionBasicStorage* selection) + { + ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + tree_node_flags |= ImGuiTreeNodeFlags_NavLeftJumpsBackHere; // Enable pressing left to jump to parent + if (node->Childs.Size == 0) + tree_node_flags |= ImGuiTreeNodeFlags_Bullet | ImGuiTreeNodeFlags_Leaf; + if (selection->Contains((ImGuiID)node->UID)) + tree_node_flags |= ImGuiTreeNodeFlags_Selected; + + // Using SetNextItemStorageID() to specify storage id, so we can easily peek into + // the storage holding open/close stage, using our TreeNodeGetOpen/TreeNodeSetOpen() functions. + ImGui::SetNextItemSelectionUserData((ImGuiSelectionUserData)(intptr_t)node); + ImGui::SetNextItemStorageID((ImGuiID)node->UID); + if (ImGui::TreeNodeEx(node->Name, tree_node_flags)) + { + for (ExampleTreeNode* child : node->Childs) + DrawNode(child, selection); + ImGui::TreePop(); + } + else if (ImGui::IsItemToggledOpen()) + { + TreeCloseAndUnselectChildNodes(node, selection); + } + } + + static bool TreeNodeGetOpen(ExampleTreeNode* node) + { + return ImGui::GetStateStorage()->GetBool((ImGuiID)node->UID); + } + + static void TreeNodeSetOpen(ExampleTreeNode* node, bool open) + { + ImGui::GetStateStorage()->SetBool((ImGuiID)node->UID, open); + } + + // When closing a node: 1) close and unselect all child nodes, 2) select parent if any child was selected. + // FIXME: This is currently handled by user logic but I'm hoping to eventually provide tree node + // features to do this automatically, e.g. a ImGuiTreeNodeFlags_AutoCloseChildNodes etc. + static int TreeCloseAndUnselectChildNodes(ExampleTreeNode* node, ImGuiSelectionBasicStorage* selection, int depth = 0) + { + // Recursive close (the test for depth == 0 is because we call this on a node that was just closed!) + int unselected_count = selection->Contains((ImGuiID)node->UID) ? 1 : 0; + if (depth == 0 || TreeNodeGetOpen(node)) + { + for (ExampleTreeNode* child : node->Childs) + unselected_count += TreeCloseAndUnselectChildNodes(child, selection, depth + 1); + TreeNodeSetOpen(node, false); + } + + // Select root node if any of its child was selected, otherwise unselect + selection->SetItemSelected((ImGuiID)node->UID, (depth == 0 && unselected_count > 0)); + return unselected_count; + } + + // Apply multi-selection requests + static void ApplySelectionRequests(ImGuiMultiSelectIO* ms_io, ExampleTreeNode* tree, ImGuiSelectionBasicStorage* selection) + { + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) + { + if (req.Selected) + TreeSetAllInOpenNodes(tree, selection, req.Selected); + else + selection->Clear(); + } + else if (req.Type == ImGuiSelectionRequestType_SetRange) + { + ExampleTreeNode* first_node = (ExampleTreeNode*)(intptr_t)req.RangeFirstItem; + ExampleTreeNode* last_node = (ExampleTreeNode*)(intptr_t)req.RangeLastItem; + for (ExampleTreeNode* node = first_node; node != NULL; node = TreeGetNextNodeInVisibleOrder(node, last_node)) + selection->SetItemSelected((ImGuiID)node->UID, req.Selected); + } + } + } + + static void TreeSetAllInOpenNodes(ExampleTreeNode* node, ImGuiSelectionBasicStorage* selection, bool selected) + { + if (node->Parent != NULL) // Root node isn't visible nor selectable in our scheme + selection->SetItemSelected((ImGuiID)node->UID, selected); + if (node->Parent == NULL || TreeNodeGetOpen(node)) + for (ExampleTreeNode* child : node->Childs) + TreeSetAllInOpenNodes(child, selection, selected); + } + + // Interpolate in *user-visible order* AND only *over opened nodes*. + // If you have a sequential mapping tables (e.g. generated after a filter/search pass) this would be simpler. + // Here the tricks are that: + // - we store/maintain ExampleTreeNode::IndexInParent which allows implementing a linear iterator easily, without searches, without recursion. + // this could be replaced by a search in parent, aka 'int index_in_parent = curr_node->Parent->Childs.find_index(curr_node)' + // which would only be called when crossing from child to a parent, aka not too much. + // - we call SetNextItemStorageID() before our TreeNode() calls with an ID which doesn't relate to UI stack, + // making it easier to call TreeNodeGetOpen()/TreeNodeSetOpen() from any location. + static ExampleTreeNode* TreeGetNextNodeInVisibleOrder(ExampleTreeNode* curr_node, ExampleTreeNode* last_node) + { + // Reached last node + if (curr_node == last_node) + return NULL; + + // Recurse into childs. Query storage to tell if the node is open. + if (curr_node->Childs.Size > 0 && TreeNodeGetOpen(curr_node)) + return curr_node->Childs[0]; + + // Next sibling, then into our own parent + while (curr_node->Parent != NULL) + { + if (curr_node->IndexInParent + 1 < curr_node->Parent->Childs.Size) + return curr_node->Parent->Childs[curr_node->IndexInParent + 1]; + curr_node = curr_node->Parent; + } + return NULL; + } + + }; // ExampleTreeFuncs + + static ImGuiSelectionBasicStorage selection; + if (demo_data->DemoTree == NULL) + demo_data->DemoTree = ExampleTree_CreateDemoTree(); // Create tree once + ImGui::Text("Selection size: %d", selection.Size); + + if (ImGui::BeginChild("##Tree", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) + { + ExampleTreeNode* tree = demo_data->DemoTree; + ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect2d; + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, selection.Size, -1); + ExampleTreeFuncs::ApplySelectionRequests(ms_io, tree, &selection); + for (ExampleTreeNode* node : tree->Childs) + ExampleTreeFuncs::DrawNode(node, &selection); + ms_io = ImGui::EndMultiSelect(); + ExampleTreeFuncs::ApplySelectionRequests(ms_io, tree, &selection); + } + ImGui::EndChild(); + + ImGui::TreePop(); + } + + // Advanced demonstration of BeginMultiSelect() + // - Showcase clipping. + // - Showcase deletion. + // - Showcase basic drag and drop. + // - Showcase TreeNode variant (note that tree node don't expand in the demo: supporting expanding tree nodes + clipping a separate thing). + // - Showcase using inside a table. + IMGUI_DEMO_MARKER("Widgets/Selection State/Multi-Select (advanced)"); + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (ImGui::TreeNode("Multi-Select (advanced)")) + { + // Options + enum WidgetType { WidgetType_Selectable, WidgetType_TreeNode }; + static bool use_clipper = true; + static bool use_deletion = true; + static bool use_drag_drop = true; + static bool show_in_table = false; + static bool show_color_button = true; + static ImGuiMultiSelectFlags flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_BoxSelect1d; + static WidgetType widget_type = WidgetType_Selectable; + + if (ImGui::TreeNode("Options")) + { + if (ImGui::RadioButton("Selectables", widget_type == WidgetType_Selectable)) { widget_type = WidgetType_Selectable; } ImGui::SameLine(); - ImGui::PopID(); - } + if (ImGui::RadioButton("Tree nodes", widget_type == WidgetType_TreeNode)) { widget_type = WidgetType_TreeNode; } + ImGui::SameLine(); + HelpMarker("TreeNode() is technically supported but... using this correctly is more complicated (you need some sort of linear/random access to your tree, which is suited to advanced trees setups already implementing filters and clipper. We will work toward simplifying and demoing this.\n\nFor now the tree demo is actually a little bit meaningless because it is an empty tree with only root nodes."); + ImGui::Checkbox("Enable clipper", &use_clipper); + ImGui::Checkbox("Enable deletion", &use_deletion); + ImGui::Checkbox("Enable drag & drop", &use_drag_drop); + ImGui::Checkbox("Show in a table", &show_in_table); + ImGui::Checkbox("Show color button", &show_color_button); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SingleSelect", &flags, ImGuiMultiSelectFlags_SingleSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoSelectAll", &flags, ImGuiMultiSelectFlags_NoSelectAll); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoRangeSelect", &flags, ImGuiMultiSelectFlags_NoRangeSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoSelect", &flags, ImGuiMultiSelectFlags_NoAutoSelect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClear", &flags, ImGuiMultiSelectFlags_NoAutoClear); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_NoAutoClearOnReselect", &flags, ImGuiMultiSelectFlags_NoAutoClearOnReselect); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect1d", &flags, ImGuiMultiSelectFlags_BoxSelect1d); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelect2d", &flags, ImGuiMultiSelectFlags_BoxSelect2d); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_BoxSelectNoScroll", &flags, ImGuiMultiSelectFlags_BoxSelectNoScroll); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnEscape", &flags, ImGuiMultiSelectFlags_ClearOnEscape); + ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ClearOnClickVoid", &flags, ImGuiMultiSelectFlags_ClearOnClickVoid); + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeWindow", &flags, ImGuiMultiSelectFlags_ScopeWindow) && (flags & ImGuiMultiSelectFlags_ScopeWindow)) + flags &= ~ImGuiMultiSelectFlags_ScopeRect; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_ScopeRect", &flags, ImGuiMultiSelectFlags_ScopeRect) && (flags & ImGuiMultiSelectFlags_ScopeRect)) + flags &= ~ImGuiMultiSelectFlags_ScopeWindow; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClick", &flags, ImGuiMultiSelectFlags_SelectOnClick) && (flags & ImGuiMultiSelectFlags_SelectOnClick)) + flags &= ~ImGuiMultiSelectFlags_SelectOnClickRelease; + if (ImGui::CheckboxFlags("ImGuiMultiSelectFlags_SelectOnClickRelease", &flags, ImGuiMultiSelectFlags_SelectOnClickRelease) && (flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) + flags &= ~ImGuiMultiSelectFlags_SelectOnClick; + ImGui::SameLine(); HelpMarker("Allow dragging an unselected item without altering selection."); + ImGui::TreePop(); + } + // Initialize default list with 1000 items. + // Use default selection.Adapter: Pass index to SetNextItemSelectionUserData(), store index in Selection + static ImVector items; + static int items_next_id = 0; + if (items_next_id == 0) { for (int n = 0; n < 1000; n++) { items.push_back(items_next_id++); } } + static ExampleSelectionWithDeletion selection; + static bool request_deletion_from_menu = false; // Queue deletion triggered from context menu + + ImGui::Text("Selection size: %d/%d", selection.Size, items.Size); + + const float items_height = (widget_type == WidgetType_TreeNode) ? ImGui::GetTextLineHeight() : ImGui::GetTextLineHeightWithSpacing(); + ImGui::SetNextWindowContentSize(ImVec2(0.0f, items.Size * items_height)); + if (ImGui::BeginChild("##Basket", ImVec2(-FLT_MIN, ImGui::GetFontSize() * 20), ImGuiChildFlags_FrameStyle | ImGuiChildFlags_ResizeY)) + { + ImVec2 color_button_sz(ImGui::GetFontSize(), ImGui::GetFontSize()); + if (widget_type == WidgetType_TreeNode) + ImGui::PushStyleVarY(ImGuiStyleVar_ItemSpacing, 0.0f); + + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(flags, selection.Size, items.Size); + selection.ApplyRequests(ms_io); + + const bool want_delete = (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (selection.Size > 0)) || request_deletion_from_menu; + const int item_curr_idx_to_focus = want_delete ? selection.ApplyDeletionPreLoop(ms_io, items.Size) : -1; + request_deletion_from_menu = false; + + if (show_in_table) + { + if (widget_type == WidgetType_TreeNode) + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(0.0f, 0.0f)); + ImGui::BeginTable("##Split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings | ImGuiTableFlags_NoPadOuterX); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.70f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 0.30f); + //ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacingY, 0.0f); + } + + ImGuiListClipper clipper; + if (use_clipper) + { + clipper.Begin(items.Size); + if (item_curr_idx_to_focus != -1) + clipper.IncludeItemByIndex(item_curr_idx_to_focus); // Ensure focused item is not clipped. + if (ms_io->RangeSrcItem != -1) + clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem); // Ensure RangeSrc item is not clipped. + } + + while (!use_clipper || clipper.Step()) + { + const int item_begin = use_clipper ? clipper.DisplayStart : 0; + const int item_end = use_clipper ? clipper.DisplayEnd : items.Size; + for (int n = item_begin; n < item_end; n++) + { + if (show_in_table) + ImGui::TableNextColumn(); + + const int item_id = items[n]; + const char* item_category = ExampleNames[item_id % IM_ARRAYSIZE(ExampleNames)]; + char label[64]; + sprintf(label, "Object %05d: %s", item_id, item_category); + + // IMPORTANT: for deletion refocus to work we need object ID to be stable, + // aka not depend on their index in the list. Here we use our persistent item_id + // instead of index to build a unique ID that will persist. + // (If we used PushID(index) instead, focus wouldn't be restored correctly after deletion). + ImGui::PushID(item_id); + + // Emit a color button, to test that Shift+LeftArrow landing on an item that is not part + // of the selection scope doesn't erroneously alter our selection. + if (show_color_button) + { + ImU32 dummy_col = (ImU32)((unsigned int)n * 0xC250B74B) | IM_COL32_A_MASK; + ImGui::ColorButton("##", ImColor(dummy_col), ImGuiColorEditFlags_NoTooltip, color_button_sz); + ImGui::SameLine(); + } + + // Submit item + bool item_is_selected = selection.Contains((ImGuiID)n); + bool item_is_open = false; + ImGui::SetNextItemSelectionUserData(n); + if (widget_type == WidgetType_Selectable) + { + ImGui::Selectable(label, item_is_selected, ImGuiSelectableFlags_None); + } + else if (widget_type == WidgetType_TreeNode) + { + ImGuiTreeNodeFlags tree_node_flags = ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; + if (item_is_selected) + tree_node_flags |= ImGuiTreeNodeFlags_Selected; + item_is_open = ImGui::TreeNodeEx(label, tree_node_flags); + } + + // Focus (for after deletion) + if (item_curr_idx_to_focus == n) + ImGui::SetKeyboardFocusHere(-1); + + // Drag and Drop + if (use_drag_drop && ImGui::BeginDragDropSource()) + { + // Create payload with full selection OR single unselected item. + // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease) + if (ImGui::GetDragDropPayload() == NULL) + { + ImVector payload_items; + void* it = NULL; + ImGuiID id = 0; + if (!item_is_selected) + payload_items.push_back(item_id); + else + while (selection.GetNextSelectedItem(&it, &id)) + payload_items.push_back((int)id); + ImGui::SetDragDropPayload("MULTISELECT_DEMO_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); + } + + // Display payload content in tooltip + const ImGuiPayload* payload = ImGui::GetDragDropPayload(); + const int* payload_items = (int*)payload->Data; + const int payload_count = (int)payload->DataSize / (int)sizeof(int); + if (payload_count == 1) + ImGui::Text("Object %05d: %s", payload_items[0], ExampleNames[payload_items[0] % IM_ARRAYSIZE(ExampleNames)]); + else + ImGui::Text("Dragging %d objects", payload_count); + + ImGui::EndDragDropSource(); + } + + if (widget_type == WidgetType_TreeNode && item_is_open) + ImGui::TreePop(); + + // Right-click: context menu + if (ImGui::BeginPopupContextItem()) + { + ImGui::BeginDisabled(!use_deletion || selection.Size == 0); + sprintf(label, "Delete %d item(s)###DeleteSelected", selection.Size); + if (ImGui::Selectable(label)) + request_deletion_from_menu = true; + ImGui::EndDisabled(); + ImGui::Selectable("Close"); + ImGui::EndPopup(); + } + + // Demo content within a table + if (show_in_table) + { + ImGui::TableNextColumn(); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::InputText("###NoLabel", (char*)(void*)item_category, strlen(item_category), ImGuiInputTextFlags_ReadOnly); + ImGui::PopStyleVar(); + } + + ImGui::PopID(); + } + if (!use_clipper) + break; + } + + if (show_in_table) + { + ImGui::EndTable(); + if (widget_type == WidgetType_TreeNode) + ImGui::PopStyleVar(); + } + + // Apply multi-select requests + ms_io = ImGui::EndMultiSelect(); + selection.ApplyRequests(ms_io); + if (want_delete) + selection.ApplyDeletionPostLoop(ms_io, items, item_curr_idx_to_focus); + + if (widget_type == WidgetType_TreeNode) + ImGui::PopStyleVar(); + } + ImGui::EndChild(); + ImGui::TreePop(); + } ImGui::TreePop(); } +} +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsTabs() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsTabs() +{ + IMGUI_DEMO_MARKER("Widgets/Tabs"); if (ImGui::TreeNode("Tabs")) { + IMGUI_DEMO_MARKER("Widgets/Tabs/Basic"); if (ImGui::TreeNode("Basic")) { ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_None; @@ -1912,35 +3448,40 @@ static void ShowDemoWindowLayout() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Widgets/Tabs/Advanced & Close Button"); if (ImGui::TreeNode("Advanced & Close Button")) { // Expose a couple of the available flags. In most cases you may just call BeginTabBar() with no flags (0). static ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_Reorderable; - ImGui::CheckboxFlags("ImGuiTabBarFlags_Reorderable", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_Reorderable); - ImGui::CheckboxFlags("ImGuiTabBarFlags_AutoSelectNewTabs", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_AutoSelectNewTabs); - ImGui::CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_TabListPopupButton); - ImGui::CheckboxFlags("ImGuiTabBarFlags_NoCloseWithMiddleMouseButton", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_NoCloseWithMiddleMouseButton); + ImGui::CheckboxFlags("ImGuiTabBarFlags_Reorderable", &tab_bar_flags, ImGuiTabBarFlags_Reorderable); + ImGui::CheckboxFlags("ImGuiTabBarFlags_AutoSelectNewTabs", &tab_bar_flags, ImGuiTabBarFlags_AutoSelectNewTabs); + ImGui::CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", &tab_bar_flags, ImGuiTabBarFlags_TabListPopupButton); + ImGui::CheckboxFlags("ImGuiTabBarFlags_NoCloseWithMiddleMouseButton", &tab_bar_flags, ImGuiTabBarFlags_NoCloseWithMiddleMouseButton); + ImGui::CheckboxFlags("ImGuiTabBarFlags_DrawSelectedOverline", &tab_bar_flags, ImGuiTabBarFlags_DrawSelectedOverline); if ((tab_bar_flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0) tab_bar_flags |= ImGuiTabBarFlags_FittingPolicyDefault_; - if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_FittingPolicyResizeDown)) + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyResizeDown)) tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyResizeDown); - if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", (unsigned int*)&tab_bar_flags, ImGuiTabBarFlags_FittingPolicyScroll)) + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyScroll)) tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyScroll); // Tab Bar + ImGui::AlignTextToFramePadding(); + ImGui::Text("Opened:"); const char* names[4] = { "Artichoke", "Beetroot", "Celery", "Daikon" }; static bool opened[4] = { true, true, true, true }; // Persistent user state for (int n = 0; n < IM_ARRAYSIZE(opened); n++) { - if (n > 0) { ImGui::SameLine(); } + ImGui::SameLine(); ImGui::Checkbox(names[n], &opened[n]); } - // Passing a bool* to BeginTabItem() is similar to passing one to Begin(): the underlying bool will be set to false when the tab is closed. + // Passing a bool* to BeginTabItem() is similar to passing one to Begin(): + // the underlying bool will be set to false when the tab is closed. if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags)) { for (int n = 0; n < IM_ARRAYSIZE(opened); n++) - if (opened[n] && ImGui::BeginTabItem(names[n], &opened[n])) + if (opened[n] && ImGui::BeginTabItem(names[n], &opened[n], ImGuiTabItemFlags_None)) { ImGui::Text("This is the %s tab!", names[n]); if (n & 1) @@ -1952,12 +3493,1077 @@ static void ShowDemoWindowLayout() ImGui::Separator(); ImGui::TreePop(); } + + IMGUI_DEMO_MARKER("Widgets/Tabs/TabItemButton & Leading-Trailing flags"); + if (ImGui::TreeNode("TabItemButton & Leading/Trailing flags")) + { + static ImVector active_tabs; + static int next_tab_id = 0; + if (next_tab_id == 0) // Initialize with some default tabs + for (int i = 0; i < 3; i++) + active_tabs.push_back(next_tab_id++); + + // TabItemButton() and Leading/Trailing flags are distinct features which we will demo together. + // (It is possible to submit regular tabs with Leading/Trailing flags, or TabItemButton tabs without Leading/Trailing flags... + // but they tend to make more sense together) + static bool show_leading_button = true; + static bool show_trailing_button = true; + ImGui::Checkbox("Show Leading TabItemButton()", &show_leading_button); + ImGui::Checkbox("Show Trailing TabItemButton()", &show_trailing_button); + + // Expose some other flags which are useful to showcase how they interact with Leading/Trailing tabs + static ImGuiTabBarFlags tab_bar_flags = ImGuiTabBarFlags_AutoSelectNewTabs | ImGuiTabBarFlags_Reorderable | ImGuiTabBarFlags_FittingPolicyResizeDown; + ImGui::CheckboxFlags("ImGuiTabBarFlags_TabListPopupButton", &tab_bar_flags, ImGuiTabBarFlags_TabListPopupButton); + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyResizeDown", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyResizeDown)) + tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyResizeDown); + if (ImGui::CheckboxFlags("ImGuiTabBarFlags_FittingPolicyScroll", &tab_bar_flags, ImGuiTabBarFlags_FittingPolicyScroll)) + tab_bar_flags &= ~(ImGuiTabBarFlags_FittingPolicyMask_ ^ ImGuiTabBarFlags_FittingPolicyScroll); + + if (ImGui::BeginTabBar("MyTabBar", tab_bar_flags)) + { + // Demo a Leading TabItemButton(): click the "?" button to open a menu + if (show_leading_button) + if (ImGui::TabItemButton("?", ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_NoTooltip)) + ImGui::OpenPopup("MyHelpMenu"); + if (ImGui::BeginPopup("MyHelpMenu")) + { + ImGui::Selectable("Hello!"); + ImGui::EndPopup(); + } + + // Demo Trailing Tabs: click the "+" button to add a new tab. + // (In your app you may want to use a font icon instead of the "+") + // We submit it before the regular tabs, but thanks to the ImGuiTabItemFlags_Trailing flag it will always appear at the end. + if (show_trailing_button) + if (ImGui::TabItemButton("+", ImGuiTabItemFlags_Trailing | ImGuiTabItemFlags_NoTooltip)) + active_tabs.push_back(next_tab_id++); // Add new tab + + // Submit our regular tabs + for (int n = 0; n < active_tabs.Size; ) + { + bool open = true; + char name[16]; + snprintf(name, IM_ARRAYSIZE(name), "%04d", active_tabs[n]); + if (ImGui::BeginTabItem(name, &open, ImGuiTabItemFlags_None)) + { + ImGui::Text("This is the %s tab!", name); + ImGui::EndTabItem(); + } + + if (!open) + active_tabs.erase(active_tabs.Data + n); + else + n++; + } + + ImGui::EndTabBar(); + } + ImGui::Separator(); + ImGui::TreePop(); + } + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsText() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsText() +{ + IMGUI_DEMO_MARKER("Widgets/Text"); + if (ImGui::TreeNode("Text")) + { + IMGUI_DEMO_MARKER("Widgets/Text/Colored Text"); + if (ImGui::TreeNode("Colorful Text")) + { + // Using shortcut. You can use PushStyleColor()/PopStyleColor() for more flexibility. + ImGui::TextColored(ImVec4(1.0f, 0.0f, 1.0f, 1.0f), "Pink"); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "Yellow"); + ImGui::TextDisabled("Disabled"); + ImGui::SameLine(); HelpMarker("The TextDisabled color is stored in ImGuiStyle."); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text/Word Wrapping"); + if (ImGui::TreeNode("Word Wrapping")) + { + // Using shortcut. You can use PushTextWrapPos()/PopTextWrapPos() for more flexibility. + ImGui::TextWrapped( + "This text should automatically wrap on the edge of the window. The current implementation " + "for text wrapping follows simple rules suitable for English and possibly other languages."); + ImGui::Spacing(); + + static float wrap_width = 200.0f; + ImGui::SliderFloat("Wrap width", &wrap_width, -20, 600, "%.0f"); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + for (int n = 0; n < 2; n++) + { + ImGui::Text("Test paragraph %d:", n); + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImVec2 marker_min = ImVec2(pos.x + wrap_width, pos.y); + ImVec2 marker_max = ImVec2(pos.x + wrap_width + 10, pos.y + ImGui::GetTextLineHeight()); + ImGui::PushTextWrapPos(ImGui::GetCursorPos().x + wrap_width); + if (n == 0) + ImGui::Text("The lazy dog is a good dog. This paragraph should fit within %.0f pixels. Testing a 1 character word. The quick brown fox jumps over the lazy dog.", wrap_width); + else + ImGui::Text("aaaaaaaa bbbbbbbb, c cccccccc,dddddddd. d eeeeeeee ffffffff. gggggggg!hhhhhhhh"); + + // Draw actual text bounding box, following by marker of our expected limit (should not overlap!) + draw_list->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), IM_COL32(255, 255, 0, 255)); + draw_list->AddRectFilled(marker_min, marker_max, IM_COL32(255, 0, 255, 255)); + ImGui::PopTextWrapPos(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text/UTF-8 Text"); + if (ImGui::TreeNode("UTF-8 Text")) + { + // UTF-8 test with Japanese characters + // (Needs a suitable font? Try "Google Noto" or "Arial Unicode". See docs/FONTS.md for details.) + // - From C++11 you can use the u8"my text" syntax to encode literal strings as UTF-8 + // - For earlier compiler, you may be able to encode your sources as UTF-8 (e.g. in Visual Studio, you + // can save your source files as 'UTF-8 without signature'). + // - FOR THIS DEMO FILE ONLY, BECAUSE WE WANT TO SUPPORT OLD COMPILERS, WE ARE *NOT* INCLUDING RAW UTF-8 + // CHARACTERS IN THIS SOURCE FILE. Instead we are encoding a few strings with hexadecimal constants. + // Don't do this in your application! Please use u8"text in any language" in your application! + // Note that characters values are preserved even by InputText() if the font cannot be displayed, + // so you can safely copy & paste garbled characters into another application. + ImGui::TextWrapped( + "CJK text will only appear if the font was loaded with the appropriate CJK character ranges. " + "Call io.Fonts->AddFontFromFileTTF() manually to load extra character ranges. " + "Read docs/FONTS.md for details."); + ImGui::Text("Hiragana: \xe3\x81\x8b\xe3\x81\x8d\xe3\x81\x8f\xe3\x81\x91\xe3\x81\x93 (kakikukeko)"); + ImGui::Text("Kanjis: \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e (nihongo)"); + static char buf[32] = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; + //static char buf[32] = u8"NIHONGO"; // <- this is how you would write it with C++11, using real kanjis + ImGui::InputText("UTF-8 input", buf, IM_ARRAYSIZE(buf)); + ImGui::TreePop(); + } + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsTextFilter() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsTextFilter() +{ + IMGUI_DEMO_MARKER("Widgets/Text Filter"); + if (ImGui::TreeNode("Text Filter")) + { + // Helper class to easy setup a text filter. + // You may want to implement a more feature-full filtering scheme in your own application. + HelpMarker("Not a widget per-se, but ImGuiTextFilter is a helper to perform simple filtering on text strings."); + static ImGuiTextFilter filter; + ImGui::Text("Filter usage:\n" + " \"\" display all lines\n" + " \"xxx\" display lines containing \"xxx\"\n" + " \"xxx,yyy\" display lines containing \"xxx\" or \"yyy\"\n" + " \"-xxx\" hide lines containing \"xxx\""); + filter.Draw(); + const char* lines[] = { "aaa1.c", "bbb1.c", "ccc1.c", "aaa2.cpp", "bbb2.cpp", "ccc2.cpp", "abc.h", "hello, world" }; + for (int i = 0; i < IM_ARRAYSIZE(lines); i++) + if (filter.PassFilter(lines[i])) + ImGui::BulletText("%s", lines[i]); + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsTextInput() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsTextInput() +{ + // To wire InputText() with std::string or any other custom string type, + // see the "Text Input > Resize Callback" section of this demo, and the misc/cpp/imgui_stdlib.h file. + IMGUI_DEMO_MARKER("Widgets/Text Input"); + if (ImGui::TreeNode("Text Input")) + { + IMGUI_DEMO_MARKER("Widgets/Text Input/Multi-line Text Input"); + if (ImGui::TreeNode("Multi-line Text Input")) + { + // Note: we are using a fixed-sized buffer for simplicity here. See ImGuiInputTextFlags_CallbackResize + // and the code in misc/cpp/imgui_stdlib.h for how to setup InputText() for dynamically resizing strings. + static char text[1024 * 16] = + "/*\n" + " The Pentium F00F bug, shorthand for F0 0F C7 C8,\n" + " the hexadecimal encoding of one offending instruction,\n" + " more formally, the invalid operand with locked CMPXCHG8B\n" + " instruction bug, is a design flaw in the majority of\n" + " Intel Pentium, Pentium MMX, and Pentium OverDrive\n" + " processors (all in the P5 microarchitecture).\n" + "*/\n\n" + "label:\n" + "\tlock cmpxchg8b eax\n"; + + static ImGuiInputTextFlags flags = ImGuiInputTextFlags_AllowTabInput; + HelpMarker("You can use the ImGuiInputTextFlags_CallbackResize facility if you need to wire InputTextMultiline() to a dynamic string type. See misc/cpp/imgui_stdlib.h for an example. (This is not demonstrated in imgui_demo.cpp because we don't want to include in here)"); + ImGui::CheckboxFlags("ImGuiInputTextFlags_ReadOnly", &flags, ImGuiInputTextFlags_ReadOnly); + ImGui::CheckboxFlags("ImGuiInputTextFlags_AllowTabInput", &flags, ImGuiInputTextFlags_AllowTabInput); + ImGui::SameLine(); HelpMarker("When _AllowTabInput is set, passing through the widget with Tabbing doesn't automatically activate it, in order to also cycling through subsequent widgets."); + ImGui::CheckboxFlags("ImGuiInputTextFlags_CtrlEnterForNewLine", &flags, ImGuiInputTextFlags_CtrlEnterForNewLine); + ImGui::InputTextMultiline("##source", text, IM_ARRAYSIZE(text), ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 16), flags); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Filtered Text Input"); + if (ImGui::TreeNode("Filtered Text Input")) + { + struct TextFilters + { + // Modify character input by altering 'data->Eventchar' (ImGuiInputTextFlags_CallbackCharFilter callback) + static int FilterCasingSwap(ImGuiInputTextCallbackData* data) + { + if (data->EventChar >= 'a' && data->EventChar <= 'z') { data->EventChar -= 'a' - 'A'; } // Lowercase becomes uppercase + else if (data->EventChar >= 'A' && data->EventChar <= 'Z') { data->EventChar += 'a' - 'A'; } // Uppercase becomes lowercase + return 0; + } + + // Return 0 (pass) if the character is 'i' or 'm' or 'g' or 'u' or 'i', otherwise return 1 (filter out) + static int FilterImGuiLetters(ImGuiInputTextCallbackData* data) + { + if (data->EventChar < 256 && strchr("imgui", (char)data->EventChar)) + return 0; + return 1; + } + }; + + static char buf1[32] = ""; ImGui::InputText("default", buf1, 32); + static char buf2[32] = ""; ImGui::InputText("decimal", buf2, 32, ImGuiInputTextFlags_CharsDecimal); + static char buf3[32] = ""; ImGui::InputText("hexadecimal", buf3, 32, ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase); + static char buf4[32] = ""; ImGui::InputText("uppercase", buf4, 32, ImGuiInputTextFlags_CharsUppercase); + static char buf5[32] = ""; ImGui::InputText("no blank", buf5, 32, ImGuiInputTextFlags_CharsNoBlank); + static char buf6[32] = ""; ImGui::InputText("casing swap", buf6, 32, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterCasingSwap); // Use CharFilter callback to replace characters. + static char buf7[32] = ""; ImGui::InputText("\"imgui\"", buf7, 32, ImGuiInputTextFlags_CallbackCharFilter, TextFilters::FilterImGuiLetters); // Use CharFilter callback to disable some characters. + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Password input"); + if (ImGui::TreeNode("Password Input")) + { + static char password[64] = "password123"; + ImGui::InputText("password", password, IM_ARRAYSIZE(password), ImGuiInputTextFlags_Password); + ImGui::SameLine(); HelpMarker("Display all characters as '*'.\nDisable clipboard cut and copy.\nDisable logging.\n"); + ImGui::InputTextWithHint("password (w/ hint)", "", password, IM_ARRAYSIZE(password), ImGuiInputTextFlags_Password); + ImGui::InputText("password (clear)", password, IM_ARRAYSIZE(password)); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Completion, History, Edit Callbacks"); + if (ImGui::TreeNode("Completion, History, Edit Callbacks")) + { + struct Funcs + { + static int MyCallback(ImGuiInputTextCallbackData* data) + { + if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) + { + data->InsertChars(data->CursorPos, ".."); + } + else if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) + { + if (data->EventKey == ImGuiKey_UpArrow) + { + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, "Pressed Up!"); + data->SelectAll(); + } + else if (data->EventKey == ImGuiKey_DownArrow) + { + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, "Pressed Down!"); + data->SelectAll(); + } + } + else if (data->EventFlag == ImGuiInputTextFlags_CallbackEdit) + { + // Toggle casing of first character + char c = data->Buf[0]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) data->Buf[0] ^= 32; + data->BufDirty = true; + + // Increment a counter + int* p_int = (int*)data->UserData; + *p_int = *p_int + 1; + } + return 0; + } + }; + static char buf1[64]; + ImGui::InputText("Completion", buf1, 64, ImGuiInputTextFlags_CallbackCompletion, Funcs::MyCallback); + ImGui::SameLine(); HelpMarker( + "Here we append \"..\" each time Tab is pressed. " + "See 'Examples>Console' for a more meaningful demonstration of using this callback."); + + static char buf2[64]; + ImGui::InputText("History", buf2, 64, ImGuiInputTextFlags_CallbackHistory, Funcs::MyCallback); + ImGui::SameLine(); HelpMarker( + "Here we replace and select text each time Up/Down are pressed. " + "See 'Examples>Console' for a more meaningful demonstration of using this callback."); + + static char buf3[64]; + static int edit_count = 0; + ImGui::InputText("Edit", buf3, 64, ImGuiInputTextFlags_CallbackEdit, Funcs::MyCallback, (void*)&edit_count); + ImGui::SameLine(); HelpMarker( + "Here we toggle the casing of the first character on every edit + count edits."); + ImGui::SameLine(); ImGui::Text("(%d)", edit_count); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Resize Callback"); + if (ImGui::TreeNode("Resize Callback")) + { + // To wire InputText() with std::string or any other custom string type, + // you can use the ImGuiInputTextFlags_CallbackResize flag + create a custom ImGui::InputText() wrapper + // using your preferred type. See misc/cpp/imgui_stdlib.h for an implementation of this using std::string. + HelpMarker( + "Using ImGuiInputTextFlags_CallbackResize to wire your custom string type to InputText().\n\n" + "See misc/cpp/imgui_stdlib.h for an implementation of this for std::string."); + struct Funcs + { + static int MyResizeCallback(ImGuiInputTextCallbackData* data) + { + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + ImVector* my_str = (ImVector*)data->UserData; + IM_ASSERT(my_str->begin() == data->Buf); + my_str->resize(data->BufSize); // NB: On resizing calls, generally data->BufSize == data->BufTextLen + 1 + data->Buf = my_str->begin(); + } + return 0; + } + + // Note: Because ImGui:: is a namespace you would typically add your own function into the namespace. + // For example, you code may declare a function 'ImGui::InputText(const char* label, MyString* my_str)' + static bool MyInputTextMultiline(const char* label, ImVector* my_str, const ImVec2& size = ImVec2(0, 0), ImGuiInputTextFlags flags = 0) + { + IM_ASSERT((flags & ImGuiInputTextFlags_CallbackResize) == 0); + return ImGui::InputTextMultiline(label, my_str->begin(), (size_t)my_str->size(), size, flags | ImGuiInputTextFlags_CallbackResize, Funcs::MyResizeCallback, (void*)my_str); + } + }; + + // For this demo we are using ImVector as a string container. + // Note that because we need to store a terminating zero character, our size/capacity are 1 more + // than usually reported by a typical string class. + static ImVector my_str; + if (my_str.empty()) + my_str.push_back(0); + Funcs::MyInputTextMultiline("##MyStr", &my_str, ImVec2(-FLT_MIN, ImGui::GetTextLineHeight() * 16)); + ImGui::Text("Data: %p\nSize: %d\nCapacity: %d", (void*)my_str.begin(), my_str.size(), my_str.capacity()); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Eliding, Alignment"); + if (ImGui::TreeNode("Eliding, Alignment")) + { + static char buf1[128] = "/path/to/some/folder/with/long/filename.cpp"; + static ImGuiInputTextFlags flags = ImGuiInputTextFlags_ElideLeft; + ImGui::CheckboxFlags("ImGuiInputTextFlags_ElideLeft", &flags, ImGuiInputTextFlags_ElideLeft); + ImGui::InputText("Path", buf1, IM_ARRAYSIZE(buf1), flags); + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Text Input/Miscellaneous"); + if (ImGui::TreeNode("Miscellaneous")) + { + static char buf1[16]; + static ImGuiInputTextFlags flags = ImGuiInputTextFlags_EscapeClearsAll; + ImGui::CheckboxFlags("ImGuiInputTextFlags_EscapeClearsAll", &flags, ImGuiInputTextFlags_EscapeClearsAll); + ImGui::CheckboxFlags("ImGuiInputTextFlags_ReadOnly", &flags, ImGuiInputTextFlags_ReadOnly); + ImGui::CheckboxFlags("ImGuiInputTextFlags_NoUndoRedo", &flags, ImGuiInputTextFlags_NoUndoRedo); + ImGui::InputText("Hello", buf1, IM_ARRAYSIZE(buf1), flags); + ImGui::TreePop(); + } + ImGui::TreePop(); } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsTooltips() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsTooltips() +{ + IMGUI_DEMO_MARKER("Widgets/Tooltips"); + if (ImGui::TreeNode("Tooltips")) + { + // Tooltips are windows following the mouse. They do not take focus away. + ImGui::SeparatorText("General"); + + // Typical use cases: + // - Short-form (text only): SetItemTooltip("Hello"); + // - Short-form (any contents): if (BeginItemTooltip()) { Text("Hello"); EndTooltip(); } + + // - Full-form (text only): if (IsItemHovered(...)) { SetTooltip("Hello"); } + // - Full-form (any contents): if (IsItemHovered(...) && BeginTooltip()) { Text("Hello"); EndTooltip(); } + + HelpMarker( + "Tooltip are typically created by using a IsItemHovered() + SetTooltip() sequence.\n\n" + "We provide a helper SetItemTooltip() function to perform the two with standards flags."); + + ImVec2 sz = ImVec2(-FLT_MIN, 0.0f); + + ImGui::Button("Basic", sz); + ImGui::SetItemTooltip("I am a tooltip"); + + ImGui::Button("Fancy", sz); + if (ImGui::BeginItemTooltip()) + { + ImGui::Text("I am a fancy tooltip"); + static float arr[] = { 0.6f, 0.1f, 1.0f, 0.5f, 0.92f, 0.1f, 0.2f }; + ImGui::PlotLines("Curve", arr, IM_ARRAYSIZE(arr)); + ImGui::Text("Sin(time) = %f", sinf((float)ImGui::GetTime())); + ImGui::EndTooltip(); + } + + ImGui::SeparatorText("Always On"); + + // Showcase NOT relying on a IsItemHovered() to emit a tooltip. + // Here the tooltip is always emitted when 'always_on == true'. + static int always_on = 0; + ImGui::RadioButton("Off", &always_on, 0); + ImGui::SameLine(); + ImGui::RadioButton("Always On (Simple)", &always_on, 1); + ImGui::SameLine(); + ImGui::RadioButton("Always On (Advanced)", &always_on, 2); + if (always_on == 1) + ImGui::SetTooltip("I am following you around."); + else if (always_on == 2 && ImGui::BeginTooltip()) + { + ImGui::ProgressBar(sinf((float)ImGui::GetTime()) * 0.5f + 0.5f, ImVec2(ImGui::GetFontSize() * 25, 0.0f)); + ImGui::EndTooltip(); + } + + ImGui::SeparatorText("Custom"); + + HelpMarker( + "Passing ImGuiHoveredFlags_ForTooltip to IsItemHovered() is the preferred way to standardize " + "tooltip activation details across your application. You may however decide to use custom " + "flags for a specific tooltip instance."); + + // The following examples are passed for documentation purpose but may not be useful to most users. + // Passing ImGuiHoveredFlags_ForTooltip to IsItemHovered() will pull ImGuiHoveredFlags flags values from + // 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav' depending on whether mouse or keyboard/gamepad is being used. + // With default settings, ImGuiHoveredFlags_ForTooltip is equivalent to ImGuiHoveredFlags_DelayShort + ImGuiHoveredFlags_Stationary. + ImGui::Button("Manual", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + ImGui::SetTooltip("I am a manually emitted tooltip."); + + ImGui::Button("DelayNone", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNone)) + ImGui::SetTooltip("I am a tooltip with no delay."); + + ImGui::Button("DelayShort", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_NoSharedDelay)) + ImGui::SetTooltip("I am a tooltip with a short delay (%0.2f sec).", ImGui::GetStyle().HoverDelayShort); + + ImGui::Button("DelayLong", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay)) + ImGui::SetTooltip("I am a tooltip with a long delay (%0.2f sec).", ImGui::GetStyle().HoverDelayNormal); + + ImGui::Button("Stationary", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_Stationary)) + ImGui::SetTooltip("I am a tooltip requiring mouse to be stationary before activating."); + + // Using ImGuiHoveredFlags_ForTooltip will pull flags from 'style.HoverFlagsForTooltipMouse' or 'style.HoverFlagsForTooltipNav', + // which default value include the ImGuiHoveredFlags_AllowWhenDisabled flag. + ImGui::BeginDisabled(); + ImGui::Button("Disabled item", sz); + if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + ImGui::SetTooltip("I am a a tooltip for a disabled item."); + ImGui::EndDisabled(); + + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsTreeNodes() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsTreeNodes() +{ + IMGUI_DEMO_MARKER("Widgets/Tree Nodes"); + if (ImGui::TreeNode("Tree Nodes")) + { + IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Basic trees"); + if (ImGui::TreeNode("Basic trees")) + { + for (int i = 0; i < 5; i++) + { + // Use SetNextItemOpen() so set the default state of a node to be open. We could + // also use TreeNodeEx() with the ImGuiTreeNodeFlags_DefaultOpen flag to achieve the same thing! + if (i == 0) + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + + // Here we use PushID() to generate a unique base ID, and then the "" used as TreeNode id won't conflict. + // An alternative to using 'PushID() + TreeNode("", ...)' to generate a unique ID is to use 'TreeNode((void*)(intptr_t)i, ...)', + // aka generate a dummy pointer-sized value to be hashed. The demo below uses that technique. Both are fine. + ImGui::PushID(i); + if (ImGui::TreeNode("", "Child %d", i)) + { + ImGui::Text("blah blah"); + ImGui::SameLine(); + if (ImGui::SmallButton("button")) {} + ImGui::TreePop(); + } + ImGui::PopID(); + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Widgets/Tree Nodes/Advanced, with Selectable nodes"); + if (ImGui::TreeNode("Advanced, with Selectable nodes")) + { + HelpMarker( + "This is a more typical looking tree with selectable nodes.\n" + "Click to select, CTRL+Click to toggle, click on arrows or double-click to open."); + static ImGuiTreeNodeFlags base_flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_SpanAvailWidth; + static bool align_label_with_current_x_position = false; + static bool test_drag_and_drop = false; + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnArrow", &base_flags, ImGuiTreeNodeFlags_OpenOnArrow); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_OpenOnDoubleClick", &base_flags, ImGuiTreeNodeFlags_OpenOnDoubleClick); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAvailWidth", &base_flags, ImGuiTreeNodeFlags_SpanAvailWidth); ImGui::SameLine(); HelpMarker("Extend hit area to all available width instead of allowing more items to be laid out after the node."); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &base_flags, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanLabelWidth", &base_flags, ImGuiTreeNodeFlags_SpanLabelWidth); ImGui::SameLine(); HelpMarker("Reduce hit area to the text label and a bit of margin."); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &base_flags, ImGuiTreeNodeFlags_SpanAllColumns); ImGui::SameLine(); HelpMarker("For use in Tables only."); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_AllowOverlap", &base_flags, ImGuiTreeNodeFlags_AllowOverlap); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_Framed", &base_flags, ImGuiTreeNodeFlags_Framed); ImGui::SameLine(); HelpMarker("Draw frame with background (e.g. for CollapsingHeader)"); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_NavLeftJumpsBackHere", &base_flags, ImGuiTreeNodeFlags_NavLeftJumpsBackHere); + ImGui::Checkbox("Align label with current X position", &align_label_with_current_x_position); + ImGui::Checkbox("Test tree node as drag source", &test_drag_and_drop); + ImGui::Text("Hello!"); + if (align_label_with_current_x_position) + ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing()); + + // 'selection_mask' is dumb representation of what may be user-side selection state. + // You may retain selection state inside or outside your objects in whatever format you see fit. + // 'node_clicked' is temporary storage of what node we have clicked to process selection at the end + /// of the loop. May be a pointer to your own node type, etc. + static int selection_mask = (1 << 2); + int node_clicked = -1; + for (int i = 0; i < 6; i++) + { + // Disable the default "open on single-click behavior" + set Selected flag according to our selection. + // To alter selection we use IsItemClicked() && !IsItemToggledOpen(), so clicking on an arrow doesn't alter selection. + ImGuiTreeNodeFlags node_flags = base_flags; + const bool is_selected = (selection_mask & (1 << i)) != 0; + if (is_selected) + node_flags |= ImGuiTreeNodeFlags_Selected; + if (i < 3) + { + // Items 0..2 are Tree Node + bool node_open = ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Node %d", i); + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) + node_clicked = i; + if (test_drag_and_drop && ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload("_TREENODE", NULL, 0); + ImGui::Text("This is a drag and drop source"); + ImGui::EndDragDropSource(); + } + if (i == 2 && (base_flags & ImGuiTreeNodeFlags_SpanLabelWidth)) + { + // Item 2 has an additional inline button to help demonstrate SpanLabelWidth. + ImGui::SameLine(); + if (ImGui::SmallButton("button")) {} + } + if (node_open) + { + ImGui::BulletText("Blah blah\nBlah Blah"); + ImGui::SameLine(); + ImGui::SmallButton("Button"); + ImGui::TreePop(); + } + } + else + { + // Items 3..5 are Tree Leaves + // The only reason we use TreeNode at all is to allow selection of the leaf. Otherwise we can + // use BulletText() or advance the cursor by GetTreeNodeToLabelSpacing() and call Text(). + node_flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen; // ImGuiTreeNodeFlags_Bullet + ImGui::TreeNodeEx((void*)(intptr_t)i, node_flags, "Selectable Leaf %d", i); + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) + node_clicked = i; + if (test_drag_and_drop && ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload("_TREENODE", NULL, 0); + ImGui::Text("This is a drag and drop source"); + ImGui::EndDragDropSource(); + } + } + } + if (node_clicked != -1) + { + // Update selection state + // (process outside of tree loop to avoid visual inconsistencies during the clicking frame) + if (ImGui::GetIO().KeyCtrl) + selection_mask ^= (1 << node_clicked); // CTRL+click to toggle + else //if (!(selection_mask & (1 << node_clicked))) // Depending on selection behavior you want, may want to preserve selection when clicking on item that is part of the selection + selection_mask = (1 << node_clicked); // Click to single-select + } + if (align_label_with_current_x_position) + ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing()); + ImGui::TreePop(); + } + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgetsVerticalSliders() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgetsVerticalSliders() +{ + IMGUI_DEMO_MARKER("Widgets/Vertical Sliders"); + if (ImGui::TreeNode("Vertical Sliders")) + { + const float spacing = 4; + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing)); + + static int int_value = 0; + ImGui::VSliderInt("##int", ImVec2(18, 160), &int_value, 0, 5); + ImGui::SameLine(); + + static float values[7] = { 0.0f, 0.60f, 0.35f, 0.9f, 0.70f, 0.20f, 0.0f }; + ImGui::PushID("set1"); + for (int i = 0; i < 7; i++) + { + if (i > 0) ImGui::SameLine(); + ImGui::PushID(i); + ImGui::PushStyleColor(ImGuiCol_FrameBg, (ImVec4)ImColor::HSV(i / 7.0f, 0.5f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgHovered, (ImVec4)ImColor::HSV(i / 7.0f, 0.6f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_FrameBgActive, (ImVec4)ImColor::HSV(i / 7.0f, 0.7f, 0.5f)); + ImGui::PushStyleColor(ImGuiCol_SliderGrab, (ImVec4)ImColor::HSV(i / 7.0f, 0.9f, 0.9f)); + ImGui::VSliderFloat("##v", ImVec2(18, 160), &values[i], 0.0f, 1.0f, ""); + if (ImGui::IsItemActive() || ImGui::IsItemHovered()) + ImGui::SetTooltip("%.3f", values[i]); + ImGui::PopStyleColor(4); + ImGui::PopID(); + } + ImGui::PopID(); + + ImGui::SameLine(); + ImGui::PushID("set2"); + static float values2[4] = { 0.20f, 0.80f, 0.40f, 0.25f }; + const int rows = 3; + const ImVec2 small_slider_size(18, (float)(int)((160.0f - (rows - 1) * spacing) / rows)); + for (int nx = 0; nx < 4; nx++) + { + if (nx > 0) ImGui::SameLine(); + ImGui::BeginGroup(); + for (int ny = 0; ny < rows; ny++) + { + ImGui::PushID(nx * rows + ny); + ImGui::VSliderFloat("##v", small_slider_size, &values2[nx], 0.0f, 1.0f, ""); + if (ImGui::IsItemActive() || ImGui::IsItemHovered()) + ImGui::SetTooltip("%.3f", values2[nx]); + ImGui::PopID(); + } + ImGui::EndGroup(); + } + ImGui::PopID(); + + ImGui::SameLine(); + ImGui::PushID("set3"); + for (int i = 0; i < 4; i++) + { + if (i > 0) ImGui::SameLine(); + ImGui::PushID(i); + ImGui::PushStyleVar(ImGuiStyleVar_GrabMinSize, 40); + ImGui::VSliderFloat("##v", ImVec2(40, 160), &values[i], 0.0f, 1.0f, "%.2f\nsec"); + ImGui::PopStyleVar(); + ImGui::PopID(); + } + ImGui::PopID(); + ImGui::PopStyleVar(); + ImGui::TreePop(); + } +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowWidgets() +//----------------------------------------------------------------------------- + +static void DemoWindowWidgets(ImGuiDemoWindowData* demo_data) +{ + IMGUI_DEMO_MARKER("Widgets"); + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); + if (!ImGui::CollapsingHeader("Widgets")) + return; + + const bool disable_all = demo_data->DisableSections; // The Checkbox for that is inside the "Disabled" section at the bottom + if (disable_all) + ImGui::BeginDisabled(); + + DemoWindowWidgetsBasic(); + DemoWindowWidgetsBullets(); + DemoWindowWidgetsCollapsingHeaders(); + DemoWindowWidgetsComboBoxes(); + DemoWindowWidgetsColorAndPickers(); + DemoWindowWidgetsDataTypes(); + + if (disable_all) + ImGui::EndDisabled(); + DemoWindowWidgetsDisableBlocks(demo_data); + if (disable_all) + ImGui::BeginDisabled(); + + DemoWindowWidgetsDragAndDrop(); + DemoWindowWidgetsDragsAndSliders(); + DemoWindowWidgetsImages(); + DemoWindowWidgetsListBoxes(); + DemoWindowWidgetsMultiComponents(); + DemoWindowWidgetsPlotting(); + DemoWindowWidgetsProgressBars(); + DemoWindowWidgetsQueryingStatuses(); + DemoWindowWidgetsSelectables(); + DemoWindowWidgetsSelectionAndMultiSelect(demo_data); + DemoWindowWidgetsTabs(); + DemoWindowWidgetsText(); + DemoWindowWidgetsTextFilter(); + DemoWindowWidgetsTextInput(); + DemoWindowWidgetsTooltips(); + DemoWindowWidgetsTreeNodes(); + DemoWindowWidgetsVerticalSliders(); + + if (disable_all) + ImGui::EndDisabled(); +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowLayout() +//----------------------------------------------------------------------------- + +static void DemoWindowLayout() +{ + IMGUI_DEMO_MARKER("Layout"); + if (!ImGui::CollapsingHeader("Layout & Scrolling")) + return; + + IMGUI_DEMO_MARKER("Layout/Child windows"); + if (ImGui::TreeNode("Child windows")) + { + ImGui::SeparatorText("Child windows"); + + HelpMarker("Use child windows to begin into a self-contained independent scrolling/clipping regions within a host window."); + static bool disable_mouse_wheel = false; + static bool disable_menu = false; + ImGui::Checkbox("Disable Mouse Wheel", &disable_mouse_wheel); + ImGui::Checkbox("Disable Menu", &disable_menu); + + // Child 1: no border, enable horizontal scrollbar + { + ImGuiWindowFlags window_flags = ImGuiWindowFlags_HorizontalScrollbar; + if (disable_mouse_wheel) + window_flags |= ImGuiWindowFlags_NoScrollWithMouse; + ImGui::BeginChild("ChildL", ImVec2(ImGui::GetContentRegionAvail().x * 0.5f, 260), ImGuiChildFlags_None, window_flags); + for (int i = 0; i < 100; i++) + ImGui::Text("%04d: scrollable region", i); + ImGui::EndChild(); + } + + ImGui::SameLine(); + + // Child 2: rounded border + { + ImGuiWindowFlags window_flags = ImGuiWindowFlags_None; + if (disable_mouse_wheel) + window_flags |= ImGuiWindowFlags_NoScrollWithMouse; + if (!disable_menu) + window_flags |= ImGuiWindowFlags_MenuBar; + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 5.0f); + ImGui::BeginChild("ChildR", ImVec2(0, 260), ImGuiChildFlags_Borders, window_flags); + if (!disable_menu && ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Menu")) + { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + if (ImGui::BeginTable("split", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_NoSavedSettings)) + { + for (int i = 0; i < 100; i++) + { + char buf[32]; + sprintf(buf, "%03d", i); + ImGui::TableNextColumn(); + ImGui::Button(buf, ImVec2(-FLT_MIN, 0.0f)); + } + ImGui::EndTable(); + } + ImGui::EndChild(); + ImGui::PopStyleVar(); + } + + // Child 3: manual-resize + ImGui::SeparatorText("Manual-resize"); + { + HelpMarker("Drag bottom border to resize. Double-click bottom border to auto-fit to vertical contents."); + //if (ImGui::Button("Set Height to 200")) + // ImGui::SetNextWindowSize(ImVec2(-FLT_MIN, 200.0f)); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyleColorVec4(ImGuiCol_FrameBg)); + if (ImGui::BeginChild("ResizableChild", ImVec2(-FLT_MIN, ImGui::GetTextLineHeightWithSpacing() * 8), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) + for (int n = 0; n < 10; n++) + ImGui::Text("Line %04d", n); + ImGui::PopStyleColor(); + ImGui::EndChild(); + } + + // Child 4: auto-resizing height with a limit + ImGui::SeparatorText("Auto-resize with constraints"); + { + static int draw_lines = 3; + static int max_height_in_lines = 10; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("Lines Count", &draw_lines, 0.2f); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("Max Height (in Lines)", &max_height_in_lines, 0.2f); + + ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 1), ImVec2(FLT_MAX, ImGui::GetTextLineHeightWithSpacing() * max_height_in_lines)); + if (ImGui::BeginChild("ConstrainedChild", ImVec2(-FLT_MIN, 0.0f), ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY)) + for (int n = 0; n < draw_lines; n++) + ImGui::Text("Line %04d", n); + ImGui::EndChild(); + } + + ImGui::SeparatorText("Misc/Advanced"); + + // Demonstrate a few extra things + // - Changing ImGuiCol_ChildBg (which is transparent black in default styles) + // - Using SetCursorPos() to position child window (the child window is an item from the POV of parent window) + // You can also call SetNextWindowPos() to position the child window. The parent window will effectively + // layout from this position. + // - Using ImGui::GetItemRectMin/Max() to query the "item" state (because the child window is an item from + // the POV of the parent window). See 'Demo->Querying Status (Edited/Active/Hovered etc.)' for details. + { + static int offset_x = 0; + static bool override_bg_color = true; + static ImGuiChildFlags child_flags = ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX | ImGuiChildFlags_ResizeY; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("Offset X", &offset_x, 1.0f, -1000, 1000); + ImGui::Checkbox("Override ChildBg color", &override_bg_color); + ImGui::CheckboxFlags("ImGuiChildFlags_Borders", &child_flags, ImGuiChildFlags_Borders); + ImGui::CheckboxFlags("ImGuiChildFlags_AlwaysUseWindowPadding", &child_flags, ImGuiChildFlags_AlwaysUseWindowPadding); + ImGui::CheckboxFlags("ImGuiChildFlags_ResizeX", &child_flags, ImGuiChildFlags_ResizeX); + ImGui::CheckboxFlags("ImGuiChildFlags_ResizeY", &child_flags, ImGuiChildFlags_ResizeY); + ImGui::CheckboxFlags("ImGuiChildFlags_FrameStyle", &child_flags, ImGuiChildFlags_FrameStyle); + ImGui::SameLine(); HelpMarker("Style the child window like a framed item: use FrameBg, FrameRounding, FrameBorderSize, FramePadding instead of ChildBg, ChildRounding, ChildBorderSize, WindowPadding."); + if (child_flags & ImGuiChildFlags_FrameStyle) + override_bg_color = false; + + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (float)offset_x); + if (override_bg_color) + ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(255, 0, 0, 100)); + ImGui::BeginChild("Red", ImVec2(200, 100), child_flags, ImGuiWindowFlags_None); + if (override_bg_color) + ImGui::PopStyleColor(); + + for (int n = 0; n < 50; n++) + ImGui::Text("Some test %d", n); + ImGui::EndChild(); + bool child_is_hovered = ImGui::IsItemHovered(); + ImVec2 child_rect_min = ImGui::GetItemRectMin(); + ImVec2 child_rect_max = ImGui::GetItemRectMax(); + ImGui::Text("Hovered: %d", child_is_hovered); + ImGui::Text("Rect of child window is: (%.0f,%.0f) (%.0f,%.0f)", child_rect_min.x, child_rect_min.y, child_rect_max.x, child_rect_max.y); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Widgets Width"); + if (ImGui::TreeNode("Widgets Width")) + { + static float f = 0.0f; + static bool show_indented_items = true; + ImGui::Checkbox("Show indented items", &show_indented_items); + + // Use SetNextItemWidth() to set the width of a single upcoming item. + // Use PushItemWidth()/PopItemWidth() to set the width of a group of items. + // In real code use you'll probably want to choose width values that are proportional to your font size + // e.g. Using '20.0f * GetFontSize()' as width instead of '200.0f', etc. + + ImGui::Text("SetNextItemWidth/PushItemWidth(100)"); + ImGui::SameLine(); HelpMarker("Fixed width."); + ImGui::PushItemWidth(100); + ImGui::DragFloat("float##1b", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##1b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + ImGui::Text("SetNextItemWidth/PushItemWidth(-100)"); + ImGui::SameLine(); HelpMarker("Align to right edge minus 100"); + ImGui::PushItemWidth(-100); + ImGui::DragFloat("float##2a", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##2b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + ImGui::Text("SetNextItemWidth/PushItemWidth(GetContentRegionAvail().x * 0.5f)"); + ImGui::SameLine(); HelpMarker("Half of available width.\n(~ right-cursor_pos)\n(works within a column set)"); + ImGui::PushItemWidth(ImGui::GetContentRegionAvail().x * 0.5f); + ImGui::DragFloat("float##3a", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##3b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + ImGui::Text("SetNextItemWidth/PushItemWidth(-GetContentRegionAvail().x * 0.5f)"); + ImGui::SameLine(); HelpMarker("Align to right edge minus half"); + ImGui::PushItemWidth(-ImGui::GetContentRegionAvail().x * 0.5f); + ImGui::DragFloat("float##4a", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##4b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + // Demonstrate using PushItemWidth to surround three items. + // Calling SetNextItemWidth() before each of them would have the same effect. + ImGui::Text("SetNextItemWidth/PushItemWidth(-FLT_MIN)"); + ImGui::SameLine(); HelpMarker("Align to right edge"); + ImGui::PushItemWidth(-FLT_MIN); + ImGui::DragFloat("##float5a", &f); + if (show_indented_items) + { + ImGui::Indent(); + ImGui::DragFloat("float (indented)##5b", &f); + ImGui::Unindent(); + } + ImGui::PopItemWidth(); + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout"); + if (ImGui::TreeNode("Basic Horizontal Layout")) + { + ImGui::TextWrapped("(Use ImGui::SameLine() to keep adding items to the right of the preceding item)"); + + // Text + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/SameLine"); + ImGui::Text("Two items: Hello"); ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Sailor"); + + // Adjust spacing + ImGui::Text("More spacing: Hello"); ImGui::SameLine(0, 20); + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Sailor"); + + // Button + ImGui::AlignTextToFramePadding(); + ImGui::Text("Normal buttons"); ImGui::SameLine(); + ImGui::Button("Banana"); ImGui::SameLine(); + ImGui::Button("Apple"); ImGui::SameLine(); + ImGui::Button("Corniflower"); + + // Button + ImGui::Text("Small buttons"); ImGui::SameLine(); + ImGui::SmallButton("Like this one"); ImGui::SameLine(); + ImGui::Text("can fit within a text block."); + + // Aligned to arbitrary position. Easy/cheap column. + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/SameLine (with offset)"); + ImGui::Text("Aligned"); + ImGui::SameLine(150); ImGui::Text("x=150"); + ImGui::SameLine(300); ImGui::Text("x=300"); + ImGui::Text("Aligned"); + ImGui::SameLine(150); ImGui::SmallButton("x=150"); + ImGui::SameLine(300); ImGui::SmallButton("x=300"); + + // Checkbox + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/SameLine (more)"); + static bool c1 = false, c2 = false, c3 = false, c4 = false; + ImGui::Checkbox("My", &c1); ImGui::SameLine(); + ImGui::Checkbox("Tailor", &c2); ImGui::SameLine(); + ImGui::Checkbox("Is", &c3); ImGui::SameLine(); + ImGui::Checkbox("Rich", &c4); + + // Various + static float f0 = 1.0f, f1 = 2.0f, f2 = 3.0f; + ImGui::PushItemWidth(80); + const char* items[] = { "AAAA", "BBBB", "CCCC", "DDDD" }; + static int item = -1; + ImGui::Combo("Combo", &item, items, IM_ARRAYSIZE(items)); ImGui::SameLine(); + ImGui::SliderFloat("X", &f0, 0.0f, 5.0f); ImGui::SameLine(); + ImGui::SliderFloat("Y", &f1, 0.0f, 5.0f); ImGui::SameLine(); + ImGui::SliderFloat("Z", &f2, 0.0f, 5.0f); + ImGui::PopItemWidth(); + + ImGui::PushItemWidth(80); + ImGui::Text("Lists:"); + static int selection[4] = { 0, 1, 2, 3 }; + for (int i = 0; i < 4; i++) + { + if (i > 0) ImGui::SameLine(); + ImGui::PushID(i); + ImGui::ListBox("", &selection[i], items, IM_ARRAYSIZE(items)); + ImGui::PopID(); + //ImGui::SetItemTooltip("ListBox %d hovered", i); + } + ImGui::PopItemWidth(); + + // Dummy + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/Dummy"); + ImVec2 button_sz(40, 40); + ImGui::Button("A", button_sz); ImGui::SameLine(); + ImGui::Dummy(button_sz); ImGui::SameLine(); + ImGui::Button("B", button_sz); + + // Manually wrapping + // (we should eventually provide this as an automatic layout feature, but for now you can do it manually) + IMGUI_DEMO_MARKER("Layout/Basic Horizontal Layout/Manual wrapping"); + ImGui::Text("Manual wrapping:"); + ImGuiStyle& style = ImGui::GetStyle(); + int buttons_count = 20; + float window_visible_x2 = ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x; + for (int n = 0; n < buttons_count; n++) + { + ImGui::PushID(n); + ImGui::Button("Box", button_sz); + float last_button_x2 = ImGui::GetItemRectMax().x; + float next_button_x2 = last_button_x2 + style.ItemSpacing.x + button_sz.x; // Expected position if next button was on same line + if (n + 1 < buttons_count && next_button_x2 < window_visible_x2) + ImGui::SameLine(); + ImGui::PopID(); + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Groups"); if (ImGui::TreeNode("Groups")) { - HelpMarker("Using ImGui::BeginGroup()/EndGroup() to layout items. BeginGroup() basically locks the horizontal position. EndGroup() bundles the whole group so that you can use functions such as IsItemHovered() on it."); + HelpMarker( + "BeginGroup() basically locks the horizontal position for new line. " + "EndGroup() bundles the whole group so that you can use \"item\" functions such as " + "IsItemHovered()/IsItemActive() or SameLine() etc. on the whole group."); ImGui::BeginGroup(); { ImGui::BeginGroup(); @@ -1972,134 +4578,217 @@ static void ShowDemoWindowLayout() ImGui::SameLine(); ImGui::Button("EEE"); ImGui::EndGroup(); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("First group hovered"); + ImGui::SetItemTooltip("First group hovered"); } // Capture the group size and create widgets using the same size ImVec2 size = ImGui::GetItemRectSize(); const float values[5] = { 0.5f, 0.20f, 0.80f, 0.60f, 0.25f }; ImGui::PlotHistogram("##values", values, IM_ARRAYSIZE(values), 0, NULL, 0.0f, 1.0f, size); - ImGui::Button("ACTION", ImVec2((size.x - ImGui::GetStyle().ItemSpacing.x)*0.5f, size.y)); + ImGui::Button("ACTION", ImVec2((size.x - ImGui::GetStyle().ItemSpacing.x) * 0.5f, size.y)); ImGui::SameLine(); - ImGui::Button("REACTION", ImVec2((size.x - ImGui::GetStyle().ItemSpacing.x)*0.5f, size.y)); + ImGui::Button("REACTION", ImVec2((size.x - ImGui::GetStyle().ItemSpacing.x) * 0.5f, size.y)); ImGui::EndGroup(); ImGui::SameLine(); ImGui::Button("LEVERAGE\nBUZZWORD", size); ImGui::SameLine(); - if (ImGui::ListBoxHeader("List", size)) + if (ImGui::BeginListBox("List", size)) { ImGui::Selectable("Selected", true); ImGui::Selectable("Not Selected", false); - ImGui::ListBoxFooter(); + ImGui::EndListBox(); } ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Layout/Text Baseline Alignment"); if (ImGui::TreeNode("Text Baseline Alignment")) { - HelpMarker("This is testing the vertical alignment that gets applied on text to keep it aligned with widgets. Lines only composed of text or \"small\" widgets fit in less vertical spaces than lines with normal widgets."); + { + ImGui::BulletText("Text baseline:"); + ImGui::SameLine(); HelpMarker( + "This is testing the vertical alignment that gets applied on text to keep it aligned with widgets. " + "Lines only composed of text or \"small\" widgets use less vertical space than lines with framed widgets."); + ImGui::Indent(); - ImGui::Text("One\nTwo\nThree"); ImGui::SameLine(); - ImGui::Text("Hello\nWorld"); ImGui::SameLine(); - ImGui::Text("Banana"); + ImGui::Text("KO Blahblah"); ImGui::SameLine(); + ImGui::Button("Some framed item"); ImGui::SameLine(); + HelpMarker("Baseline of button will look misaligned with text.."); - ImGui::Text("Banana"); ImGui::SameLine(); - ImGui::Text("Hello\nWorld"); ImGui::SameLine(); - ImGui::Text("One\nTwo\nThree"); + // If your line starts with text, call AlignTextToFramePadding() to align text to upcoming widgets. + // (because we don't know what's coming after the Text() statement, we need to move the text baseline + // down by FramePadding.y ahead of time) + ImGui::AlignTextToFramePadding(); + ImGui::Text("OK Blahblah"); ImGui::SameLine(); + ImGui::Button("Some framed item##2"); ImGui::SameLine(); + HelpMarker("We call AlignTextToFramePadding() to vertically align the text baseline by +FramePadding.y"); - ImGui::Button("HOP##1"); ImGui::SameLine(); - ImGui::Text("Banana"); ImGui::SameLine(); - ImGui::Text("Hello\nWorld"); ImGui::SameLine(); - ImGui::Text("Banana"); + // SmallButton() uses the same vertical padding as Text + ImGui::Button("TEST##1"); ImGui::SameLine(); + ImGui::Text("TEST"); ImGui::SameLine(); + ImGui::SmallButton("TEST##2"); - ImGui::Button("HOP##2"); ImGui::SameLine(); - ImGui::Text("Hello\nWorld"); ImGui::SameLine(); - ImGui::Text("Banana"); + // If your line starts with text, call AlignTextToFramePadding() to align text to upcoming widgets. + ImGui::AlignTextToFramePadding(); + ImGui::Text("Text aligned to framed item"); ImGui::SameLine(); + ImGui::Button("Item##1"); ImGui::SameLine(); + ImGui::Text("Item"); ImGui::SameLine(); + ImGui::SmallButton("Item##2"); ImGui::SameLine(); + ImGui::Button("Item##3"); - ImGui::Button("TEST##1"); ImGui::SameLine(); - ImGui::Text("TEST"); ImGui::SameLine(); - ImGui::SmallButton("TEST##2"); + ImGui::Unindent(); + } - ImGui::AlignTextToFramePadding(); // If your line starts with text, call this to align it to upcoming widgets. - ImGui::Text("Text aligned to Widget"); ImGui::SameLine(); - ImGui::Button("Widget##1"); ImGui::SameLine(); - ImGui::Text("Widget"); ImGui::SameLine(); - ImGui::SmallButton("Widget##2"); ImGui::SameLine(); - ImGui::Button("Widget##3"); + ImGui::Spacing(); - // Tree - const float spacing = ImGui::GetStyle().ItemInnerSpacing.x; - ImGui::Button("Button##1"); - ImGui::SameLine(0.0f, spacing); - if (ImGui::TreeNode("Node##1")) { for (int i = 0; i < 6; i++) ImGui::BulletText("Item %d..", i); ImGui::TreePop(); } // Dummy tree data + { + ImGui::BulletText("Multi-line text:"); + ImGui::Indent(); + ImGui::Text("One\nTwo\nThree"); ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); ImGui::SameLine(); + ImGui::Text("Banana"); - ImGui::AlignTextToFramePadding(); // Vertically align text node a bit lower so it'll be vertically centered with upcoming widget. Otherwise you can use SmallButton (smaller fit). - bool node_open = ImGui::TreeNode("Node##2"); // Common mistake to avoid: if we want to SameLine after TreeNode we need to do it before we add child content. - ImGui::SameLine(0.0f, spacing); ImGui::Button("Button##2"); - if (node_open) { for (int i = 0; i < 6; i++) ImGui::BulletText("Item %d..", i); ImGui::TreePop(); } // Dummy tree data + ImGui::Text("Banana"); ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); ImGui::SameLine(); + ImGui::Text("One\nTwo\nThree"); - // Bullet - ImGui::Button("Button##3"); - ImGui::SameLine(0.0f, spacing); - ImGui::BulletText("Bullet text"); + ImGui::Button("HOP##1"); ImGui::SameLine(); + ImGui::Text("Banana"); ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); ImGui::SameLine(); + ImGui::Text("Banana"); - ImGui::AlignTextToFramePadding(); - ImGui::BulletText("Node"); - ImGui::SameLine(0.0f, spacing); ImGui::Button("Button##4"); + ImGui::Button("HOP##2"); ImGui::SameLine(); + ImGui::Text("Hello\nWorld"); ImGui::SameLine(); + ImGui::Text("Banana"); + ImGui::Unindent(); + } + + ImGui::Spacing(); + + { + ImGui::BulletText("Misc items:"); + ImGui::Indent(); + + // SmallButton() sets FramePadding to zero. Text baseline is aligned to match baseline of previous Button. + ImGui::Button("80x80", ImVec2(80, 80)); + ImGui::SameLine(); + ImGui::Button("50x50", ImVec2(50, 50)); + ImGui::SameLine(); + ImGui::Button("Button()"); + ImGui::SameLine(); + ImGui::SmallButton("SmallButton()"); + + // Tree + const float spacing = ImGui::GetStyle().ItemInnerSpacing.x; + ImGui::Button("Button##1"); + ImGui::SameLine(0.0f, spacing); + if (ImGui::TreeNode("Node##1")) + { + // Placeholder tree data + for (int i = 0; i < 6; i++) + ImGui::BulletText("Item %d..", i); + ImGui::TreePop(); + } + + // Vertically align text node a bit lower so it'll be vertically centered with upcoming widget. + // Otherwise you can use SmallButton() (smaller fit). + ImGui::AlignTextToFramePadding(); + + // Common mistake to avoid: if we want to SameLine after TreeNode we need to do it before we add + // other contents below the node. + bool node_open = ImGui::TreeNode("Node##2"); + ImGui::SameLine(0.0f, spacing); ImGui::Button("Button##2"); + if (node_open) + { + // Placeholder tree data + for (int i = 0; i < 6; i++) + ImGui::BulletText("Item %d..", i); + ImGui::TreePop(); + } + + // Bullet + ImGui::Button("Button##3"); + ImGui::SameLine(0.0f, spacing); + ImGui::BulletText("Bullet text"); + + ImGui::AlignTextToFramePadding(); + ImGui::BulletText("Node"); + ImGui::SameLine(0.0f, spacing); ImGui::Button("Button##4"); + ImGui::Unindent(); + } ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Layout/Scrolling"); if (ImGui::TreeNode("Scrolling")) { - HelpMarker("Use SetScrollHereY() or SetScrollFromPosY() to scroll to a given position."); + // Vertical scroll functions + IMGUI_DEMO_MARKER("Layout/Scrolling/Vertical"); + HelpMarker("Use SetScrollHereY() or SetScrollFromPosY() to scroll to a given vertical position."); - static bool track = true; - static int track_line = 50; + static int track_item = 50; + static bool enable_track = true; + static bool enable_extra_decorations = false; static float scroll_to_off_px = 0.0f; static float scroll_to_pos_px = 200.0f; - ImGui::Checkbox("Track", &track); + + ImGui::Checkbox("Decoration", &enable_extra_decorations); + + ImGui::Checkbox("Track", &enable_track); ImGui::PushItemWidth(100); - ImGui::SameLine(140); track |= ImGui::DragInt("##line", &track_line, 0.25f, 0, 99, "Line = %d"); + ImGui::SameLine(140); enable_track |= ImGui::DragInt("##item", &track_item, 0.25f, 0, 99, "Item = %d"); bool scroll_to_off = ImGui::Button("Scroll Offset"); - ImGui::SameLine(140); scroll_to_off |= ImGui::DragFloat("##off_y", &scroll_to_off_px, 1.00f, 0, 9999, "+%.0f px"); + ImGui::SameLine(140); scroll_to_off |= ImGui::DragFloat("##off", &scroll_to_off_px, 1.00f, 0, FLT_MAX, "+%.0f px"); bool scroll_to_pos = ImGui::Button("Scroll To Pos"); - ImGui::SameLine(140); scroll_to_pos |= ImGui::DragFloat("##pos_y", &scroll_to_pos_px, 1.00f, 0, 9999, "Y = %.0f px"); - + ImGui::SameLine(140); scroll_to_pos |= ImGui::DragFloat("##pos", &scroll_to_pos_px, 1.00f, -10, FLT_MAX, "X/Y = %.0f px"); ImGui::PopItemWidth(); + if (scroll_to_off || scroll_to_pos) - track = false; + enable_track = false; ImGuiStyle& style = ImGui::GetStyle(); float child_w = (ImGui::GetContentRegionAvail().x - 4 * style.ItemSpacing.x) / 5; + if (child_w < 1.0f) + child_w = 1.0f; + ImGui::PushID("##VerticalScrolling"); for (int i = 0; i < 5; i++) { if (i > 0) ImGui::SameLine(); ImGui::BeginGroup(); - ImGui::Text("%s", i == 0 ? "Top" : i == 1 ? "25%" : i == 2 ? "Center" : i == 3 ? "75%" : "Bottom"); + const char* names[] = { "Top", "25%", "Center", "75%", "Bottom" }; + ImGui::TextUnformatted(names[i]); - ImGuiWindowFlags child_flags = ImGuiWindowFlags_MenuBar; - ImGui::BeginChild(ImGui::GetID((void*)(intptr_t)i), ImVec2(child_w, 200.0f), true, child_flags); + const ImGuiWindowFlags child_flags = enable_extra_decorations ? ImGuiWindowFlags_MenuBar : 0; + const ImGuiID child_id = ImGui::GetID((void*)(intptr_t)i); + const bool child_is_visible = ImGui::BeginChild(child_id, ImVec2(child_w, 200.0f), ImGuiChildFlags_Borders, child_flags); + if (ImGui::BeginMenuBar()) + { + ImGui::TextUnformatted("abc"); + ImGui::EndMenuBar(); + } if (scroll_to_off) ImGui::SetScrollY(scroll_to_off_px); if (scroll_to_pos) ImGui::SetScrollFromPosY(ImGui::GetCursorStartPos().y + scroll_to_pos_px, i * 0.25f); - for (int line = 0; line < 100; line++) + if (child_is_visible) // Avoid calling SetScrollHereY when running with culled items { - if (track && line == track_line) + for (int item = 0; item < 100; item++) { - ImGui::TextColored(ImVec4(1,1,0,1), "Line %d", line); - ImGui::SetScrollHereY(i * 0.25f); // 0.0f:top, 0.5f:center, 1.0f:bottom - } - else - { - ImGui::Text("Line %d", line); + if (enable_track && item == track_item) + { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Item %d", item); + ImGui::SetScrollHereY(i * 0.25f); // 0.0f:top, 0.5f:center, 1.0f:bottom + } + else + { + ImGui::Text("Item %d", item); + } } } float scroll_y = ImGui::GetScrollY(); @@ -2108,21 +4797,71 @@ static void ShowDemoWindowLayout() ImGui::Text("%.0f/%.0f", scroll_y, scroll_max_y); ImGui::EndGroup(); } - ImGui::TreePop(); - } + ImGui::PopID(); - if (ImGui::TreeNode("Horizontal Scrolling")) - { - HelpMarker("Horizontal scrolling for a window has to be enabled explicitly via the ImGuiWindowFlags_HorizontalScrollbar flag.\n\nYou may want to explicitly specify content width by calling SetNextWindowContentWidth() before Begin()."); + // Horizontal scroll functions + IMGUI_DEMO_MARKER("Layout/Scrolling/Horizontal"); + ImGui::Spacing(); + HelpMarker( + "Use SetScrollHereX() or SetScrollFromPosX() to scroll to a given horizontal position.\n\n" + "Because the clipping rectangle of most window hides half worth of WindowPadding on the " + "left/right, using SetScrollFromPosX(+1) will usually result in clipped text whereas the " + "equivalent SetScrollFromPosY(+1) wouldn't."); + ImGui::PushID("##HorizontalScrolling"); + for (int i = 0; i < 5; i++) + { + float child_height = ImGui::GetTextLineHeight() + style.ScrollbarSize + style.WindowPadding.y * 2.0f; + ImGuiWindowFlags child_flags = ImGuiWindowFlags_HorizontalScrollbar | (enable_extra_decorations ? ImGuiWindowFlags_AlwaysVerticalScrollbar : 0); + ImGuiID child_id = ImGui::GetID((void*)(intptr_t)i); + bool child_is_visible = ImGui::BeginChild(child_id, ImVec2(-100, child_height), ImGuiChildFlags_Borders, child_flags); + if (scroll_to_off) + ImGui::SetScrollX(scroll_to_off_px); + if (scroll_to_pos) + ImGui::SetScrollFromPosX(ImGui::GetCursorStartPos().x + scroll_to_pos_px, i * 0.25f); + if (child_is_visible) // Avoid calling SetScrollHereY when running with culled items + { + for (int item = 0; item < 100; item++) + { + if (item > 0) + ImGui::SameLine(); + if (enable_track && item == track_item) + { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "Item %d", item); + ImGui::SetScrollHereX(i * 0.25f); // 0.0f:left, 0.5f:center, 1.0f:right + } + else + { + ImGui::Text("Item %d", item); + } + } + } + float scroll_x = ImGui::GetScrollX(); + float scroll_max_x = ImGui::GetScrollMaxX(); + ImGui::EndChild(); + ImGui::SameLine(); + const char* names[] = { "Left", "25%", "Center", "75%", "Right" }; + ImGui::Text("%s\n%.0f/%.0f", names[i], scroll_x, scroll_max_x); + ImGui::Spacing(); + } + ImGui::PopID(); + + // Miscellaneous Horizontal Scrolling Demo + IMGUI_DEMO_MARKER("Layout/Scrolling/Horizontal (more)"); + HelpMarker( + "Horizontal scrolling for a window is enabled via the ImGuiWindowFlags_HorizontalScrollbar flag.\n\n" + "You may want to also explicitly specify content width by using SetNextWindowContentWidth() before Begin()."); static int lines = 7; ImGui::SliderInt("Lines", &lines, 1, 15); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2.0f, 1.0f)); - ImGui::BeginChild("scrolling", ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 7 + 30), true, ImGuiWindowFlags_HorizontalScrollbar); + ImVec2 scrolling_child_size = ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 7 + 30); + ImGui::BeginChild("scrolling", scrolling_child_size, ImGuiChildFlags_Borders, ImGuiWindowFlags_HorizontalScrollbar); for (int line = 0; line < lines; line++) { - // Display random stuff (for the sake of this trivial demo we are using basic Button+SameLine. If you want to create your own time line for a real application you may be better off - // manipulating the cursor position yourself, aka using SetCursorPos/SetCursorScreenPos to position the widgets yourself. You may also want to use the lower-level ImDrawList API) + // Display random stuff. For the sake of this trivial demo we are using basic Button() + SameLine() + // If you want to create your own time line for a real application you may be better off manipulating + // the cursor position yourself, aka using SetCursorPos/SetCursorScreenPos to position the widgets + // yourself. You may also want to use the lower-level ImDrawList API. int num_buttons = 10 + ((line & 1) ? line * 9 : line * 3); for (int n = 0; n < num_buttons; n++) { @@ -2130,8 +4869,8 @@ static void ShowDemoWindowLayout() ImGui::PushID(n + line * 1000); char num_buf[16]; sprintf(num_buf, "%d", n); - const char* label = (!(n%15)) ? "FizzBuzz" : (!(n%3)) ? "Fizz" : (!(n%5)) ? "Buzz" : num_buf; - float hue = n*0.05f; + const char* label = (!(n % 15)) ? "FizzBuzz" : (!(n % 3)) ? "Fizz" : (!(n % 5)) ? "Buzz" : num_buf; + float hue = n * 0.05f; ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(hue, 0.6f, 0.6f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, (ImVec4)ImColor::HSV(hue, 0.7f, 0.7f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, (ImVec4)ImColor::HSV(hue, 0.8f, 0.8f)); @@ -2145,13 +4884,21 @@ static void ShowDemoWindowLayout() ImGui::EndChild(); ImGui::PopStyleVar(2); float scroll_x_delta = 0.0f; - ImGui::SmallButton("<<"); if (ImGui::IsItemActive()) { scroll_x_delta = -ImGui::GetIO().DeltaTime * 1000.0f; } ImGui::SameLine(); + ImGui::SmallButton("<<"); + if (ImGui::IsItemActive()) + scroll_x_delta = -ImGui::GetIO().DeltaTime * 1000.0f; + ImGui::SameLine(); ImGui::Text("Scroll from code"); ImGui::SameLine(); - ImGui::SmallButton(">>"); if (ImGui::IsItemActive()) { scroll_x_delta = +ImGui::GetIO().DeltaTime * 1000.0f; } ImGui::SameLine(); + ImGui::SmallButton(">>"); + if (ImGui::IsItemActive()) + scroll_x_delta = +ImGui::GetIO().DeltaTime * 1000.0f; + ImGui::SameLine(); ImGui::Text("%.0f/%.0f", scroll_x, scroll_max_x); if (scroll_x_delta != 0.0f) { - ImGui::BeginChild("scrolling"); // Demonstrate a trick: you can use Begin to set yourself in the context of another window (here we are already out of your child window) + // Demonstrate a trick: you can use Begin to set yourself in the context of another window + // (here we are already out of your child window) + ImGui::BeginChild("scrolling"); ImGui::SetScrollX(ImGui::GetScrollX() + scroll_x_delta); ImGui::EndChild(); } @@ -2174,9 +4921,12 @@ static void ShowDemoWindowLayout() if (explicit_content_size) ImGui::SetNextWindowContentSize(ImVec2(contents_size_x, 0.0f)); ImGui::Begin("Horizontal contents size demo window", &show_horizontal_contents_size_demo_window, show_h_scrollbar ? ImGuiWindowFlags_HorizontalScrollbar : 0); + IMGUI_DEMO_MARKER("Layout/Scrolling/Horizontal contents size demo window"); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 0)); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 0)); - HelpMarker("Test of different widgets react and impact the work rectangle growing when horizontal scrolling is enabled.\n\nUse 'Metrics->Tools->Show windows rectangles' to visualize rectangles."); + HelpMarker( + "Test how different widgets react and impact the work rectangle growing when horizontal scrolling is enabled.\n\n" + "Use 'Metrics->Tools->Show windows rectangles' to visualize rectangles."); ImGui::Checkbox("H-scrollbar", &show_h_scrollbar); ImGui::Checkbox("Button", &show_button); // Will grow contents size (unless explicitly overwritten) ImGui::Checkbox("Tree nodes", &show_tree_nodes); // Will grow contents size and display highlight over full width @@ -2191,7 +4941,7 @@ static void ShowDemoWindowLayout() ImGui::SameLine(); ImGui::SetNextItemWidth(100); ImGui::DragFloat("##csx", &contents_size_x); - ImVec2 p = ImGui::GetCursorScreenPos(); + ImVec2 p = ImGui::GetCursorScreenPos(); ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + 10, p.y + 10), IM_COL32_WHITE); ImGui::GetWindowDrawList()->AddRectFilled(ImVec2(p.x + contents_size_x - 10, p.y), ImVec2(p.x + contents_size_x, p.y + 10), IM_COL32_WHITE); ImGui::Dummy(ImVec2(0, 10)); @@ -2222,6 +4972,17 @@ static void ShowDemoWindowLayout() } if (show_columns) { + ImGui::Text("Tables:"); + if (ImGui::BeginTable("table", 4, ImGuiTableFlags_Borders)) + { + for (int n = 0; n < 4; n++) + { + ImGui::TableNextColumn(); + ImGui::Text("Width %.2f", ImGui::GetContentRegionAvail().x); + } + ImGui::EndTable(); + } + ImGui::Text("Columns:"); ImGui::Columns(4); for (int n = 0; n < 4; n++) { @@ -2240,7 +5001,7 @@ static void ShowDemoWindowLayout() } if (show_child) { - ImGui::BeginChild("child", ImVec2(0,0), true); + ImGui::BeginChild("child", ImVec2(0, 0), ImGuiChildFlags_Borders); ImGui::EndChild(); } ImGui::End(); @@ -2249,34 +5010,122 @@ static void ShowDemoWindowLayout() ImGui::TreePop(); } - if (ImGui::TreeNode("Clipping")) + IMGUI_DEMO_MARKER("Layout/Text Clipping"); + if (ImGui::TreeNode("Text Clipping")) { - static ImVec2 size(100, 100), offset(50, 20); - ImGui::TextWrapped("On a per-widget basis we are occasionally clipping text CPU-side if it won't fit in its frame. Otherwise we are doing coarser clipping + passing a scissor rectangle to the renderer. The system is designed to try minimizing both execution and CPU/GPU rendering cost."); + static ImVec2 size(100.0f, 100.0f); + static ImVec2 offset(30.0f, 30.0f); ImGui::DragFloat2("size", (float*)&size, 0.5f, 1.0f, 200.0f, "%.0f"); - ImGui::TextWrapped("(Click and drag)"); - ImVec2 pos = ImGui::GetCursorScreenPos(); - ImVec4 clip_rect(pos.x, pos.y, pos.x + size.x, pos.y + size.y); - ImGui::InvisibleButton("##dummy", size); - if (ImGui::IsItemActive() && ImGui::IsMouseDragging()) { offset.x += ImGui::GetIO().MouseDelta.x; offset.y += ImGui::GetIO().MouseDelta.y; } - ImGui::GetWindowDrawList()->AddRectFilled(pos, ImVec2(pos.x + size.x, pos.y + size.y), IM_COL32(90, 90, 120, 255)); - ImGui::GetWindowDrawList()->AddText(ImGui::GetFont(), ImGui::GetFontSize()*2.0f, ImVec2(pos.x + offset.x, pos.y + offset.y), IM_COL32(255, 255, 255, 255), "Line 1 hello\nLine 2 clip me!", NULL, 0.0f, &clip_rect); + ImGui::TextWrapped("(Click and drag to scroll)"); + + HelpMarker( + "(Left) Using ImGui::PushClipRect():\n" + "Will alter ImGui hit-testing logic + ImDrawList rendering.\n" + "(use this if you want your clipping rectangle to affect interactions)\n\n" + "(Center) Using ImDrawList::PushClipRect():\n" + "Will alter ImDrawList rendering only.\n" + "(use this as a shortcut if you are only using ImDrawList calls)\n\n" + "(Right) Using ImDrawList::AddText() with a fine ClipRect:\n" + "Will alter only this specific ImDrawList::AddText() rendering.\n" + "This is often used internally to avoid altering the clipping rectangle and minimize draw calls."); + + for (int n = 0; n < 3; n++) + { + if (n > 0) + ImGui::SameLine(); + + ImGui::PushID(n); + ImGui::InvisibleButton("##canvas", size); + if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) + { + offset.x += ImGui::GetIO().MouseDelta.x; + offset.y += ImGui::GetIO().MouseDelta.y; + } + ImGui::PopID(); + if (!ImGui::IsItemVisible()) // Skip rendering as ImDrawList elements are not clipped. + continue; + + const ImVec2 p0 = ImGui::GetItemRectMin(); + const ImVec2 p1 = ImGui::GetItemRectMax(); + const char* text_str = "Line 1 hello\nLine 2 clip me!"; + const ImVec2 text_pos = ImVec2(p0.x + offset.x, p0.y + offset.y); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + switch (n) + { + case 0: + ImGui::PushClipRect(p0, p1, true); + draw_list->AddRectFilled(p0, p1, IM_COL32(90, 90, 120, 255)); + draw_list->AddText(text_pos, IM_COL32_WHITE, text_str); + ImGui::PopClipRect(); + break; + case 1: + draw_list->PushClipRect(p0, p1, true); + draw_list->AddRectFilled(p0, p1, IM_COL32(90, 90, 120, 255)); + draw_list->AddText(text_pos, IM_COL32_WHITE, text_str); + draw_list->PopClipRect(); + break; + case 2: + ImVec4 clip_rect(p0.x, p0.y, p1.x, p1.y); // AddText() takes a ImVec4* here so let's convert. + draw_list->AddRectFilled(p0, p1, IM_COL32(90, 90, 120, 255)); + draw_list->AddText(ImGui::GetFont(), ImGui::GetFontSize(), text_pos, IM_COL32_WHITE, text_str, NULL, 0.0f, &clip_rect); + break; + } + } + + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Layout/Overlap Mode"); + if (ImGui::TreeNode("Overlap Mode")) + { + static bool enable_allow_overlap = true; + + HelpMarker( + "Hit-testing is by default performed in item submission order, which generally is perceived as 'back-to-front'.\n\n" + "By using SetNextItemAllowOverlap() you can notify that an item may be overlapped by another. " + "Doing so alters the hovering logic: items using AllowOverlap mode requires an extra frame to accept hovered state."); + ImGui::Checkbox("Enable AllowOverlap", &enable_allow_overlap); + + ImVec2 button1_pos = ImGui::GetCursorScreenPos(); + ImVec2 button2_pos = ImVec2(button1_pos.x + 50.0f, button1_pos.y + 50.0f); + if (enable_allow_overlap) + ImGui::SetNextItemAllowOverlap(); + ImGui::Button("Button 1", ImVec2(80, 80)); + ImGui::SetCursorScreenPos(button2_pos); + ImGui::Button("Button 2", ImVec2(80, 80)); + + // This is typically used with width-spanning items. + // (note that Selectable() has a dedicated flag ImGuiSelectableFlags_AllowOverlap, which is a shortcut + // for using SetNextItemAllowOverlap(). For demo purpose we use SetNextItemAllowOverlap() here.) + if (enable_allow_overlap) + ImGui::SetNextItemAllowOverlap(); + ImGui::Selectable("Some Selectable", false); + ImGui::SameLine(); + ImGui::SmallButton("++"); + ImGui::TreePop(); } } -static void ShowDemoWindowPopups() +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowPopups() +//----------------------------------------------------------------------------- + +static void DemoWindowPopups() { + IMGUI_DEMO_MARKER("Popups"); if (!ImGui::CollapsingHeader("Popups & Modal windows")) return; // The properties of popups windows are: // - They block normal mouse hovering detection outside them. (*) // - Unless modal, they can be closed by clicking anywhere outside them, or by pressing ESCAPE. - // - Their visibility state (~bool) is held internally by Dear ImGui instead of being held by the programmer as we are used to with regular Begin() calls. - // User can manipulate the visibility state by calling OpenPopup(). - // (*) One can use IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) to bypass it and detect hovering even when normally blocked by a popup. - // Those three properties are connected. The library needs to hold their visibility state because it can close popups at any time. + // - Their visibility state (~bool) is held internally by Dear ImGui instead of being held by the programmer as + // we are used to with regular Begin() calls. User can manipulate the visibility state by calling OpenPopup(). + // (*) One can use IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup) to bypass it and detect hovering even + // when normally blocked by a popup. + // Those three properties are connected. The library needs to hold their visibility state BECAUSE it can close + // popups at any time. // Typical use for regular windows: // bool my_tool_is_active = false; if (ImGui::Button("Open")) my_tool_is_active = true; [...] if (my_tool_is_active) Begin("My Tool", &my_tool_is_active) { [...] } End(); @@ -2286,24 +5135,26 @@ static void ShowDemoWindowPopups() // With popups we have to go through a library call (here OpenPopup) to manipulate the visibility state. // This may be a bit confusing at first but it should quickly make sense. Follow on the examples below. + IMGUI_DEMO_MARKER("Popups/Popups"); if (ImGui::TreeNode("Popups")) { - ImGui::TextWrapped("When a popup is active, it inhibits interacting with windows that are behind the popup. Clicking outside the popup closes it."); + ImGui::TextWrapped( + "When a popup is active, it inhibits interacting with windows that are behind the popup. " + "Clicking outside the popup closes it."); static int selected_fish = -1; const char* names[] = { "Bream", "Haddock", "Mackerel", "Pollock", "Tilefish" }; static bool toggles[] = { true, false, false, false, false }; - // Simple selection popup - // (If you want to show the current selection inside the Button itself, you may want to build a string using the "###" operator to preserve a constant ID with a variable label) + // Simple selection popup (if you want to show the current selection inside the Button itself, + // you may want to build a string using the "###" operator to preserve a constant ID with a variable label) if (ImGui::Button("Select..")) ImGui::OpenPopup("my_select_popup"); ImGui::SameLine(); ImGui::TextUnformatted(selected_fish == -1 ? "" : names[selected_fish]); if (ImGui::BeginPopup("my_select_popup")) { - ImGui::Text("Aquarium"); - ImGui::Separator(); + ImGui::SeparatorText("Aquarium"); for (int i = 0; i < IM_ARRAYSIZE(names); i++) if (ImGui::Selectable(names[i])) selected_fish = i; @@ -2325,8 +5176,7 @@ static void ShowDemoWindowPopups() ImGui::Separator(); ImGui::Text("Tooltip here"); - if (ImGui::IsItemHovered()) - ImGui::SetTooltip("I am a tooltip over a popup"); + ImGui::SetItemTooltip("I am a tooltip over a popup"); if (ImGui::Button("Stacked Popup")) ImGui::OpenPopup("another popup"); @@ -2352,73 +5202,138 @@ static void ShowDemoWindowPopups() } // Call the more complete ShowExampleMenuFile which we use in various places of this demo - if (ImGui::Button("File Menu..")) + if (ImGui::Button("With a menu..")) ImGui::OpenPopup("my_file_popup"); - if (ImGui::BeginPopup("my_file_popup")) + if (ImGui::BeginPopup("my_file_popup", ImGuiWindowFlags_MenuBar)) { - ShowExampleMenuFile(); + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + ShowExampleMenuFile(); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) + { + ImGui::MenuItem("Dummy"); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + ImGui::Text("Hello from popup!"); + ImGui::Button("This is a dummy button.."); ImGui::EndPopup(); } ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Popups/Context menus"); if (ImGui::TreeNode("Context menus")) { + HelpMarker("\"Context\" functions are simple helpers to associate a Popup to a given Item or Window identifier."); + // BeginPopupContextItem() is a helper to provide common/simple popup behavior of essentially doing: - // if (IsItemHovered() && IsMouseReleased(0)) - // OpenPopup(id); - // return BeginPopup(id); - // For more advanced uses you may want to replicate and cuztomize this code. This the comments inside BeginPopupContextItem() implementation. - static float value = 0.5f; - ImGui::Text("Value = %.3f (<-- right-click here)", value); - if (ImGui::BeginPopupContextItem("item context menu")) + // if (id == 0) + // id = GetItemID(); // Use last item id + // if (IsItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) + // OpenPopup(id); + // return BeginPopup(id); + // For advanced uses you may want to replicate and customize this code. + // See more details in BeginPopupContextItem(). + + // Example 1 + // When used after an item that has an ID (e.g. Button), we can skip providing an ID to BeginPopupContextItem(), + // and BeginPopupContextItem() will use the last item ID as the popup ID. { - if (ImGui::Selectable("Set to zero")) value = 0.0f; - if (ImGui::Selectable("Set to PI")) value = 3.1415f; - ImGui::SetNextItemWidth(-1); - ImGui::DragFloat("##Value", &value, 0.1f, 0.0f, 0.0f); - ImGui::EndPopup(); + const char* names[5] = { "Label1", "Label2", "Label3", "Label4", "Label5" }; + static int selected = -1; + for (int n = 0; n < 5; n++) + { + if (ImGui::Selectable(names[n], selected == n)) + selected = n; + if (ImGui::BeginPopupContextItem()) // <-- use last item id as popup id + { + selected = n; + ImGui::Text("This a popup for \"%s\"!", names[n]); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::SetItemTooltip("Right-click to open popup"); + } } - // We can also use OpenPopupOnItemClick() which is the same as BeginPopupContextItem() but without the Begin call. - // So here we will make it that clicking on the text field with the right mouse button (1) will toggle the visibility of the popup above. - ImGui::Text("(You can also right-click me to open the same popup as above.)"); - ImGui::OpenPopupOnItemClick("item context menu", 1); - - // When used after an item that has an ID (here the Button), we can skip providing an ID to BeginPopupContextItem(). - // BeginPopupContextItem() will use the last item ID as the popup ID. - // In addition here, we want to include your editable label inside the button label. We use the ### operator to override the ID (read FAQ about ID for details) - static char name[32] = "Label1"; - char buf[64]; sprintf(buf, "Button: %s###Button", name); // ### operator override ID ignoring the preceding label - ImGui::Button(buf); - if (ImGui::BeginPopupContextItem()) + // Example 2 + // Popup on a Text() element which doesn't have an identifier: we need to provide an identifier to BeginPopupContextItem(). + // Using an explicit identifier is also convenient if you want to activate the popups from different locations. { - ImGui::Text("Edit name:"); - ImGui::InputText("##edit", name, IM_ARRAYSIZE(name)); - if (ImGui::Button("Close")) - ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); + HelpMarker("Text() elements don't have stable identifiers so we need to provide one."); + static float value = 0.5f; + ImGui::Text("Value = %.3f <-- (1) right-click this text", value); + if (ImGui::BeginPopupContextItem("my popup")) + { + if (ImGui::Selectable("Set to zero")) value = 0.0f; + if (ImGui::Selectable("Set to PI")) value = 3.1415f; + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::DragFloat("##Value", &value, 0.1f, 0.0f, 0.0f); + ImGui::EndPopup(); + } + + // We can also use OpenPopupOnItemClick() to toggle the visibility of a given popup. + // Here we make it that right-clicking this other text element opens the same popup as above. + // The popup itself will be submitted by the code above. + ImGui::Text("(2) Or right-click this text"); + ImGui::OpenPopupOnItemClick("my popup", ImGuiPopupFlags_MouseButtonRight); + + // Back to square one: manually open the same popup. + if (ImGui::Button("(3) Or click this button")) + ImGui::OpenPopup("my popup"); + } + + // Example 3 + // When using BeginPopupContextItem() with an implicit identifier (NULL == use last item ID), + // we need to make sure your item identifier is stable. + // In this example we showcase altering the item label while preserving its identifier, using the ### operator (see FAQ). + { + HelpMarker("Showcase using a popup ID linked to item ID, with the item having a changing label + stable ID using the ### operator."); + static char name[32] = "Label1"; + char buf[64]; + sprintf(buf, "Button: %s###Button", name); // ### operator override ID ignoring the preceding label + ImGui::Button(buf); + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("Edit name:"); + ImGui::InputText("##edit", name, IM_ARRAYSIZE(name)); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::SameLine(); ImGui::Text("(<-- right-click here)"); } - ImGui::SameLine(); ImGui::Text("(<-- right-click here)"); ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Popups/Modals"); if (ImGui::TreeNode("Modals")) { - ImGui::TextWrapped("Modal windows are like popups but the user cannot close them by clicking outside the window."); + ImGui::TextWrapped("Modal windows are like popups but the user cannot close them by clicking outside."); if (ImGui::Button("Delete..")) ImGui::OpenPopup("Delete?"); + // Always center this window when appearing + ImVec2 center = ImGui::GetMainViewport()->GetCenter(); + ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + if (ImGui::BeginPopupModal("Delete?", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("All those beautiful files will be deleted.\nThis operation cannot be undone!\n\n"); + ImGui::Text("All those beautiful files will be deleted.\nThis operation cannot be undone!"); ImGui::Separator(); - //static int dummy_i = 0; - //ImGui::Combo("Combo", &dummy_i, "Delete\0Delete harder\0"); + //static int unused_i = 0; + //ImGui::Combo("Combo", &unused_i, "Delete\0Delete harder\0"); static bool dont_ask_me_next_time = false; ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); @@ -2440,7 +5355,7 @@ static void ShowDemoWindowPopups() { if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Dummy menu item")) {} + if (ImGui::MenuItem("Some menu item")) {} ImGui::EndMenu(); } ImGui::EndMenuBar(); @@ -2449,19 +5364,21 @@ static void ShowDemoWindowPopups() // Testing behavior of widgets stacking their own regular popups over the modal. static int item = 1; - static float color[4] = { 0.4f,0.7f,0.0f,0.5f }; + static float color[4] = { 0.4f, 0.7f, 0.0f, 0.5f }; ImGui::Combo("Combo", &item, "aaaa\0bbbb\0cccc\0dddd\0eeee\0\0"); - ImGui::ColorEdit4("color", color); + ImGui::ColorEdit4("Color", color); if (ImGui::Button("Add another modal..")) ImGui::OpenPopup("Stacked 2"); - // Also demonstrate passing a bool* to BeginPopupModal(), this will create a regular close button which will close the popup. - // Note that the visibility state of popups is owned by imgui, so the input value of the bool actually doesn't matter here. - bool dummy_open = true; - if (ImGui::BeginPopupModal("Stacked 2", &dummy_open)) + // Also demonstrate passing a bool* to BeginPopupModal(), this will create a regular close button which + // will close the popup. Note that the visibility state of popups is owned by imgui, so the input value + // of the bool actually doesn't matter here. + bool unused_open = true; + if (ImGui::BeginPopupModal("Stacked 2", &unused_open)) { ImGui::Text("Hello from Stacked The Second!"); + ImGui::ColorEdit4("Color", color); // Allow opening another nested popup if (ImGui::Button("Close")) ImGui::CloseCurrentPopup(); ImGui::EndPopup(); @@ -2475,41 +5392,2105 @@ static void ShowDemoWindowPopups() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Popups/Menus inside a regular window"); if (ImGui::TreeNode("Menus inside a regular window")) { ImGui::TextWrapped("Below we are testing adding menu items to a regular window. It's rather unusual but should work!"); ImGui::Separator(); - // NB: As a quirk in this very specific example, we want to differentiate the parent of this menu from the parent of the various popup menus above. - // To do so we are encloding the items in a PushID()/PopID() block to make them two different menusets. If we don't, opening any popup above and hovering our menu here - // would open it. This is because once a menu is active, we allow to switch to a sibling menu by just hovering on it, which is the desired behavior for regular menus. - ImGui::PushID("foo"); + ImGui::MenuItem("Menu item", "CTRL+M"); if (ImGui::BeginMenu("Menu inside a regular window")) { ShowExampleMenuFile(); ImGui::EndMenu(); } - ImGui::PopID(); ImGui::Separator(); ImGui::TreePop(); } } -static void ShowDemoWindowColumns() +// Dummy data structure that we use for the Table demo. +// (pre-C++11 doesn't allow us to instantiate ImVector template if this structure is defined inside the demo function) +namespace { - if (!ImGui::CollapsingHeader("Columns")) +// We are passing our own identifier to TableSetupColumn() to facilitate identifying columns in the sorting code. +// This identifier will be passed down into ImGuiTableSortSpec::ColumnUserID. +// But it is possible to omit the user id parameter of TableSetupColumn() and just use the column index instead! (ImGuiTableSortSpec::ColumnIndex) +// If you don't use sorting, you will generally never care about giving column an ID! +enum MyItemColumnID +{ + MyItemColumnID_ID, + MyItemColumnID_Name, + MyItemColumnID_Action, + MyItemColumnID_Quantity, + MyItemColumnID_Description +}; + +struct MyItem +{ + int ID; + const char* Name; + int Quantity; + + // We have a problem which is affecting _only this demo_ and should not affect your code: + // As we don't rely on std:: or other third-party library to compile dear imgui, we only have reliable access to qsort(), + // however qsort doesn't allow passing user data to comparing function. + // As a workaround, we are storing the sort specs in a static/global for the comparing function to access. + // In your own use case you would probably pass the sort specs to your sorting/comparing functions directly and not use a global. + // We could technically call ImGui::TableGetSortSpecs() in CompareWithSortSpecs(), but considering that this function is called + // very often by the sorting algorithm it would be a little wasteful. + static const ImGuiTableSortSpecs* s_current_sort_specs; + + static void SortWithSortSpecs(ImGuiTableSortSpecs* sort_specs, MyItem* items, int items_count) + { + s_current_sort_specs = sort_specs; // Store in variable accessible by the sort function. + if (items_count > 1) + qsort(items, (size_t)items_count, sizeof(items[0]), MyItem::CompareWithSortSpecs); + s_current_sort_specs = NULL; + } + + // Compare function to be used by qsort() + static int IMGUI_CDECL CompareWithSortSpecs(const void* lhs, const void* rhs) + { + const MyItem* a = (const MyItem*)lhs; + const MyItem* b = (const MyItem*)rhs; + for (int n = 0; n < s_current_sort_specs->SpecsCount; n++) + { + // Here we identify columns using the ColumnUserID value that we ourselves passed to TableSetupColumn() + // We could also choose to identify columns based on their index (sort_spec->ColumnIndex), which is simpler! + const ImGuiTableColumnSortSpecs* sort_spec = &s_current_sort_specs->Specs[n]; + int delta = 0; + switch (sort_spec->ColumnUserID) + { + case MyItemColumnID_ID: delta = (a->ID - b->ID); break; + case MyItemColumnID_Name: delta = (strcmp(a->Name, b->Name)); break; + case MyItemColumnID_Quantity: delta = (a->Quantity - b->Quantity); break; + case MyItemColumnID_Description: delta = (strcmp(a->Name, b->Name)); break; + default: IM_ASSERT(0); break; + } + if (delta > 0) + return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? +1 : -1; + if (delta < 0) + return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? -1 : +1; + } + + // qsort() is instable so always return a way to differenciate items. + // Your own compare function may want to avoid fallback on implicit sort specs. + // e.g. a Name compare if it wasn't already part of the sort specs. + return (a->ID - b->ID); + } +}; +const ImGuiTableSortSpecs* MyItem::s_current_sort_specs = NULL; +} + +// Make the UI compact because there are so many fields +static void PushStyleCompact() +{ + ImGuiStyle& style = ImGui::GetStyle(); + ImGui::PushStyleVarY(ImGuiStyleVar_FramePadding, (float)(int)(style.FramePadding.y * 0.60f)); + ImGui::PushStyleVarY(ImGuiStyleVar_ItemSpacing, (float)(int)(style.ItemSpacing.y * 0.60f)); +} + +static void PopStyleCompact() +{ + ImGui::PopStyleVar(2); +} + +// Show a combo box with a choice of sizing policies +static void EditTableSizingFlags(ImGuiTableFlags* p_flags) +{ + struct EnumDesc { ImGuiTableFlags Value; const char* Name; const char* Tooltip; }; + static const EnumDesc policies[] = + { + { ImGuiTableFlags_None, "Default", "Use default sizing policy:\n- ImGuiTableFlags_SizingFixedFit if ScrollX is on or if host window has ImGuiWindowFlags_AlwaysAutoResize.\n- ImGuiTableFlags_SizingStretchSame otherwise." }, + { ImGuiTableFlags_SizingFixedFit, "ImGuiTableFlags_SizingFixedFit", "Columns default to _WidthFixed (if resizable) or _WidthAuto (if not resizable), matching contents width." }, + { ImGuiTableFlags_SizingFixedSame, "ImGuiTableFlags_SizingFixedSame", "Columns are all the same width, matching the maximum contents width.\nImplicitly disable ImGuiTableFlags_Resizable and enable ImGuiTableFlags_NoKeepColumnsVisible." }, + { ImGuiTableFlags_SizingStretchProp, "ImGuiTableFlags_SizingStretchProp", "Columns default to _WidthStretch with weights proportional to their widths." }, + { ImGuiTableFlags_SizingStretchSame, "ImGuiTableFlags_SizingStretchSame", "Columns default to _WidthStretch with same weights." } + }; + int idx; + for (idx = 0; idx < IM_ARRAYSIZE(policies); idx++) + if (policies[idx].Value == (*p_flags & ImGuiTableFlags_SizingMask_)) + break; + const char* preview_text = (idx < IM_ARRAYSIZE(policies)) ? policies[idx].Name + (idx > 0 ? strlen("ImGuiTableFlags") : 0) : ""; + if (ImGui::BeginCombo("Sizing Policy", preview_text)) + { + for (int n = 0; n < IM_ARRAYSIZE(policies); n++) + if (ImGui::Selectable(policies[n].Name, idx == n)) + *p_flags = (*p_flags & ~ImGuiTableFlags_SizingMask_) | policies[n].Value; + ImGui::EndCombo(); + } + ImGui::SameLine(); + ImGui::TextDisabled("(?)"); + if (ImGui::BeginItemTooltip()) + { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 50.0f); + for (int m = 0; m < IM_ARRAYSIZE(policies); m++) + { + ImGui::Separator(); + ImGui::Text("%s:", policies[m].Name); + ImGui::Separator(); + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetStyle().IndentSpacing * 0.5f); + ImGui::TextUnformatted(policies[m].Tooltip); + } + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } +} + +static void EditTableColumnsFlags(ImGuiTableColumnFlags* p_flags) +{ + ImGui::CheckboxFlags("_Disabled", p_flags, ImGuiTableColumnFlags_Disabled); ImGui::SameLine(); HelpMarker("Master disable flag (also hide from context menu)"); + ImGui::CheckboxFlags("_DefaultHide", p_flags, ImGuiTableColumnFlags_DefaultHide); + ImGui::CheckboxFlags("_DefaultSort", p_flags, ImGuiTableColumnFlags_DefaultSort); + if (ImGui::CheckboxFlags("_WidthStretch", p_flags, ImGuiTableColumnFlags_WidthStretch)) + *p_flags &= ~(ImGuiTableColumnFlags_WidthMask_ ^ ImGuiTableColumnFlags_WidthStretch); + if (ImGui::CheckboxFlags("_WidthFixed", p_flags, ImGuiTableColumnFlags_WidthFixed)) + *p_flags &= ~(ImGuiTableColumnFlags_WidthMask_ ^ ImGuiTableColumnFlags_WidthFixed); + ImGui::CheckboxFlags("_NoResize", p_flags, ImGuiTableColumnFlags_NoResize); + ImGui::CheckboxFlags("_NoReorder", p_flags, ImGuiTableColumnFlags_NoReorder); + ImGui::CheckboxFlags("_NoHide", p_flags, ImGuiTableColumnFlags_NoHide); + ImGui::CheckboxFlags("_NoClip", p_flags, ImGuiTableColumnFlags_NoClip); + ImGui::CheckboxFlags("_NoSort", p_flags, ImGuiTableColumnFlags_NoSort); + ImGui::CheckboxFlags("_NoSortAscending", p_flags, ImGuiTableColumnFlags_NoSortAscending); + ImGui::CheckboxFlags("_NoSortDescending", p_flags, ImGuiTableColumnFlags_NoSortDescending); + ImGui::CheckboxFlags("_NoHeaderLabel", p_flags, ImGuiTableColumnFlags_NoHeaderLabel); + ImGui::CheckboxFlags("_NoHeaderWidth", p_flags, ImGuiTableColumnFlags_NoHeaderWidth); + ImGui::CheckboxFlags("_PreferSortAscending", p_flags, ImGuiTableColumnFlags_PreferSortAscending); + ImGui::CheckboxFlags("_PreferSortDescending", p_flags, ImGuiTableColumnFlags_PreferSortDescending); + ImGui::CheckboxFlags("_IndentEnable", p_flags, ImGuiTableColumnFlags_IndentEnable); ImGui::SameLine(); HelpMarker("Default for column 0"); + ImGui::CheckboxFlags("_IndentDisable", p_flags, ImGuiTableColumnFlags_IndentDisable); ImGui::SameLine(); HelpMarker("Default for column >0"); + ImGui::CheckboxFlags("_AngledHeader", p_flags, ImGuiTableColumnFlags_AngledHeader); +} + +static void ShowTableColumnsStatusFlags(ImGuiTableColumnFlags flags) +{ + ImGui::CheckboxFlags("_IsEnabled", &flags, ImGuiTableColumnFlags_IsEnabled); + ImGui::CheckboxFlags("_IsVisible", &flags, ImGuiTableColumnFlags_IsVisible); + ImGui::CheckboxFlags("_IsSorted", &flags, ImGuiTableColumnFlags_IsSorted); + ImGui::CheckboxFlags("_IsHovered", &flags, ImGuiTableColumnFlags_IsHovered); +} + +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowTables() +//----------------------------------------------------------------------------- + +static void DemoWindowTables() +{ + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); + IMGUI_DEMO_MARKER("Tables"); + if (!ImGui::CollapsingHeader("Tables & Columns")) return; - ImGui::PushID("Columns"); + // Using those as a base value to create width/height that are factor of the size of our font + const float TEXT_BASE_WIDTH = ImGui::CalcTextSize("A").x; + const float TEXT_BASE_HEIGHT = ImGui::GetTextLineHeightWithSpacing(); + ImGui::PushID("Tables"); + + int open_action = -1; + if (ImGui::Button("Expand all")) + open_action = 1; + ImGui::SameLine(); + if (ImGui::Button("Collapse all")) + open_action = 0; + ImGui::SameLine(); + + // Options static bool disable_indent = false; ImGui::Checkbox("Disable tree indentation", &disable_indent); ImGui::SameLine(); - HelpMarker("Disable the indenting of tree nodes so demo columns can use the full window width."); + HelpMarker("Disable the indenting of tree nodes so demo tables can use the full window width."); + ImGui::Separator(); if (disable_indent) ImGui::PushStyleVar(ImGuiStyleVar_IndentSpacing, 0.0f); + // About Styling of tables + // Most settings are configured on a per-table basis via the flags passed to BeginTable() and TableSetupColumns APIs. + // There are however a few settings that a shared and part of the ImGuiStyle structure: + // style.CellPadding // Padding within each cell + // style.Colors[ImGuiCol_TableHeaderBg] // Table header background + // style.Colors[ImGuiCol_TableBorderStrong] // Table outer and header borders + // style.Colors[ImGuiCol_TableBorderLight] // Table inner borders + // style.Colors[ImGuiCol_TableRowBg] // Table row background when ImGuiTableFlags_RowBg is enabled (even rows) + // style.Colors[ImGuiCol_TableRowBgAlt] // Table row background when ImGuiTableFlags_RowBg is enabled (odds rows) + + // Demos + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Basic"); + if (ImGui::TreeNode("Basic")) + { + // Here we will showcase three different ways to output a table. + // They are very simple variations of a same thing! + + // [Method 1] Using TableNextRow() to create a new row, and TableSetColumnIndex() to select the column. + // In many situations, this is the most flexible and easy to use pattern. + HelpMarker("Using TableNextRow() + calling TableSetColumnIndex() _before_ each cell, in a loop."); + if (ImGui::BeginTable("table1", 3)) + { + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Row %d Column %d", row, column); + } + } + ImGui::EndTable(); + } + + // [Method 2] Using TableNextColumn() called multiple times, instead of using a for loop + TableSetColumnIndex(). + // This is generally more convenient when you have code manually submitting the contents of each column. + HelpMarker("Using TableNextRow() + calling TableNextColumn() _before_ each cell, manually."); + if (ImGui::BeginTable("table2", 3)) + { + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("Row %d", row); + ImGui::TableNextColumn(); + ImGui::Text("Some contents"); + ImGui::TableNextColumn(); + ImGui::Text("123.456"); + } + ImGui::EndTable(); + } + + // [Method 3] We call TableNextColumn() _before_ each cell. We never call TableNextRow(), + // as TableNextColumn() will automatically wrap around and create new rows as needed. + // This is generally more convenient when your cells all contains the same type of data. + HelpMarker( + "Only using TableNextColumn(), which tends to be convenient for tables where every cell contains " + "the same type of contents.\n This is also more similar to the old NextColumn() function of the " + "Columns API, and provided to facilitate the Columns->Tables API transition."); + if (ImGui::BeginTable("table3", 3)) + { + for (int item = 0; item < 14; item++) + { + ImGui::TableNextColumn(); + ImGui::Text("Item %d", item); + } + ImGui::EndTable(); + } + + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Borders, background"); + if (ImGui::TreeNode("Borders, background")) + { + // Expose a few Borders related flags interactively + enum ContentsType { CT_Text, CT_FillButton }; + static ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg; + static bool display_headers = false; + static int contents_type = CT_Text; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_RowBg", &flags, ImGuiTableFlags_RowBg); + ImGui::CheckboxFlags("ImGuiTableFlags_Borders", &flags, ImGuiTableFlags_Borders); + ImGui::SameLine(); HelpMarker("ImGuiTableFlags_Borders\n = ImGuiTableFlags_BordersInnerV\n | ImGuiTableFlags_BordersOuterV\n | ImGuiTableFlags_BordersInnerH\n | ImGuiTableFlags_BordersOuterH"); + ImGui::Indent(); + + ImGui::CheckboxFlags("ImGuiTableFlags_BordersH", &flags, ImGuiTableFlags_BordersH); + ImGui::Indent(); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterH", &flags, ImGuiTableFlags_BordersOuterH); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerH", &flags, ImGuiTableFlags_BordersInnerH); + ImGui::Unindent(); + + ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags, ImGuiTableFlags_BordersV); + ImGui::Indent(); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterV", &flags, ImGuiTableFlags_BordersOuterV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerV", &flags, ImGuiTableFlags_BordersInnerV); + ImGui::Unindent(); + + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuter", &flags, ImGuiTableFlags_BordersOuter); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInner", &flags, ImGuiTableFlags_BordersInner); + ImGui::Unindent(); + + ImGui::AlignTextToFramePadding(); ImGui::Text("Cell contents:"); + ImGui::SameLine(); ImGui::RadioButton("Text", &contents_type, CT_Text); + ImGui::SameLine(); ImGui::RadioButton("FillButton", &contents_type, CT_FillButton); + ImGui::Checkbox("Display headers", &display_headers); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers"); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 3, flags)) + { + // Display headers so we can inspect their interaction with borders + // (Headers are not the main purpose of this section of the demo, so we are not elaborating on them now. See other sections for details) + if (display_headers) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + } + + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + char buf[32]; + sprintf(buf, "Hello %d,%d", column, row); + if (contents_type == CT_Text) + ImGui::TextUnformatted(buf); + else if (contents_type == CT_FillButton) + ImGui::Button(buf, ImVec2(-FLT_MIN, 0.0f)); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Resizable, stretch"); + if (ImGui::TreeNode("Resizable, stretch")) + { + // By default, if we don't enable ScrollX the sizing policy for each column is "Stretch" + // All columns maintain a sizing weight, and they will occupy all available width. + static ImGuiTableFlags flags = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_ContextMenuInBody; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags, ImGuiTableFlags_BordersV); + ImGui::SameLine(); HelpMarker( + "Using the _Resizable flag automatically enables the _BordersInnerV flag as well, " + "this is why the resize borders are still showing when unchecking this."); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 3, flags)) + { + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Resizable, fixed"); + if (ImGui::TreeNode("Resizable, fixed")) + { + // Here we use ImGuiTableFlags_SizingFixedFit (even though _ScrollX is not set) + // So columns will adopt the "Fixed" policy and will maintain a fixed width regardless of the whole available width (unless table is small) + // If there is not enough available width to fit all columns, they will however be resized down. + // FIXME-TABLE: Providing a stretch-on-init would make sense especially for tables which don't have saved settings + HelpMarker( + "Using _Resizable + _SizingFixedFit flags.\n" + "Fixed-width columns generally makes more sense if you want to use horizontal scrolling.\n\n" + "Double-click a column border to auto-fit the column to its contents."); + PushStyleCompact(); + static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_ContextMenuInBody; + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendX", &flags, ImGuiTableFlags_NoHostExtendX); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 3, flags)) + { + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Resizable, mixed"); + if (ImGui::TreeNode("Resizable, mixed")) + { + HelpMarker( + "Using TableSetupColumn() to alter resizing policy on a per-column basis.\n\n" + "When combining Fixed and Stretch columns, generally you only want one, maybe two trailing columns to use _WidthStretch."); + static ImGuiTableFlags flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable; + + if (ImGui::BeginTable("table1", 3, flags)) + { + ImGui::TableSetupColumn("AAA", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("BBB", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("CCC", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("%s %d,%d", (column == 2) ? "Stretch" : "Fixed", column, row); + } + } + ImGui::EndTable(); + } + if (ImGui::BeginTable("table2", 6, flags)) + { + ImGui::TableSetupColumn("AAA", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("BBB", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("CCC", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_DefaultHide); + ImGui::TableSetupColumn("DDD", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("EEE", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("FFF", ImGuiTableColumnFlags_WidthStretch | ImGuiTableColumnFlags_DefaultHide); + ImGui::TableHeadersRow(); + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 6; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("%s %d,%d", (column >= 3) ? "Stretch" : "Fixed", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Reorderable, hideable, with headers"); + if (ImGui::TreeNode("Reorderable, hideable, with headers")) + { + HelpMarker( + "Click and drag column headers to reorder columns.\n\n" + "Right-click on a header to open a context menu."); + static ImGuiTableFlags flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_Reorderable", &flags, ImGuiTableFlags_Reorderable); + ImGui::CheckboxFlags("ImGuiTableFlags_Hideable", &flags, ImGuiTableFlags_Hideable); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags, ImGuiTableFlags_NoBordersInBodyUntilResize); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers)"); + ImGui::CheckboxFlags("ImGuiTableFlags_HighlightHoveredColumn", &flags, ImGuiTableFlags_HighlightHoveredColumn); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 3, flags)) + { + // Submit columns name with TableSetupColumn() and call TableHeadersRow() to create a row with a header in each column. + // (Later we will show how TableSetupColumn() has other uses, optional flags, sizing weight etc.) + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + for (int row = 0; row < 6; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + // Use outer_size.x == 0.0f instead of default to make the table as tight as possible + // (only valid when no scrolling and no stretch column) + if (ImGui::BeginTable("table2", 3, flags | ImGuiTableFlags_SizingFixedFit, ImVec2(0.0f, 0.0f))) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + for (int row = 0; row < 6; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Fixed %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Padding"); + if (ImGui::TreeNode("Padding")) + { + // First example: showcase use of padding flags and effect of BorderOuterV/BorderInnerV on X padding. + // We don't expose BorderOuterH/BorderInnerH here because they have no effect on X padding. + HelpMarker( + "We often want outer padding activated when any using features which makes the edges of a column visible:\n" + "e.g.:\n" + "- BorderOuterV\n" + "- any form of row selection\n" + "Because of this, activating BorderOuterV sets the default to PadOuterX. " + "Using PadOuterX or NoPadOuterX you can override the default.\n\n" + "Actual padding values are using style.CellPadding.\n\n" + "In this demo we don't show horizontal borders to emphasize how they don't affect default horizontal padding."); + + static ImGuiTableFlags flags1 = ImGuiTableFlags_BordersV; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_PadOuterX", &flags1, ImGuiTableFlags_PadOuterX); + ImGui::SameLine(); HelpMarker("Enable outer-most padding (default if ImGuiTableFlags_BordersOuterV is set)"); + ImGui::CheckboxFlags("ImGuiTableFlags_NoPadOuterX", &flags1, ImGuiTableFlags_NoPadOuterX); + ImGui::SameLine(); HelpMarker("Disable outer-most padding (default if ImGuiTableFlags_BordersOuterV is not set)"); + ImGui::CheckboxFlags("ImGuiTableFlags_NoPadInnerX", &flags1, ImGuiTableFlags_NoPadInnerX); + ImGui::SameLine(); HelpMarker("Disable inner padding between columns (double inner padding if BordersOuterV is on, single inner padding if BordersOuterV is off)"); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterV", &flags1, ImGuiTableFlags_BordersOuterV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerV", &flags1, ImGuiTableFlags_BordersInnerV); + static bool show_headers = false; + ImGui::Checkbox("show_headers", &show_headers); + PopStyleCompact(); + + if (ImGui::BeginTable("table_padding", 3, flags1)) + { + if (show_headers) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + } + + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + if (row == 0) + { + ImGui::Text("Avail %.2f", ImGui::GetContentRegionAvail().x); + } + else + { + char buf[32]; + sprintf(buf, "Hello %d,%d", column, row); + ImGui::Button(buf, ImVec2(-FLT_MIN, 0.0f)); + } + //if (ImGui::TableGetColumnFlags() & ImGuiTableColumnFlags_IsHovered) + // ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, IM_COL32(0, 100, 0, 255)); + } + } + ImGui::EndTable(); + } + + // Second example: set style.CellPadding to (0.0) or a custom value. + // FIXME-TABLE: Vertical border effectively not displayed the same way as horizontal one... + HelpMarker("Setting style.CellPadding to (0,0) or a custom value."); + static ImGuiTableFlags flags2 = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg; + static ImVec2 cell_padding(0.0f, 0.0f); + static bool show_widget_frame_bg = true; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Borders", &flags2, ImGuiTableFlags_Borders); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersH", &flags2, ImGuiTableFlags_BordersH); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags2, ImGuiTableFlags_BordersV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInner", &flags2, ImGuiTableFlags_BordersInner); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuter", &flags2, ImGuiTableFlags_BordersOuter); + ImGui::CheckboxFlags("ImGuiTableFlags_RowBg", &flags2, ImGuiTableFlags_RowBg); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags2, ImGuiTableFlags_Resizable); + ImGui::Checkbox("show_widget_frame_bg", &show_widget_frame_bg); + ImGui::SliderFloat2("CellPadding", &cell_padding.x, 0.0f, 10.0f, "%.0f"); + PopStyleCompact(); + + ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, cell_padding); + if (ImGui::BeginTable("table_padding_2", 3, flags2)) + { + static char text_bufs[3 * 5][16]; // Mini text storage for 3x5 cells + static bool init = true; + if (!show_widget_frame_bg) + ImGui::PushStyleColor(ImGuiCol_FrameBg, 0); + for (int cell = 0; cell < 3 * 5; cell++) + { + ImGui::TableNextColumn(); + if (init) + strcpy(text_bufs[cell], "edit me"); + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::PushID(cell); + ImGui::InputText("##cell", text_bufs[cell], IM_ARRAYSIZE(text_bufs[cell])); + ImGui::PopID(); + } + if (!show_widget_frame_bg) + ImGui::PopStyleColor(); + init = false; + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Explicit widths"); + if (ImGui::TreeNode("Sizing policies")) + { + static ImGuiTableFlags flags1 = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_RowBg | ImGuiTableFlags_ContextMenuInBody; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags1, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendX", &flags1, ImGuiTableFlags_NoHostExtendX); + PopStyleCompact(); + + static ImGuiTableFlags sizing_policy_flags[4] = { ImGuiTableFlags_SizingFixedFit, ImGuiTableFlags_SizingFixedSame, ImGuiTableFlags_SizingStretchProp, ImGuiTableFlags_SizingStretchSame }; + for (int table_n = 0; table_n < 4; table_n++) + { + ImGui::PushID(table_n); + ImGui::SetNextItemWidth(TEXT_BASE_WIDTH * 30); + EditTableSizingFlags(&sizing_policy_flags[table_n]); + + // To make it easier to understand the different sizing policy, + // For each policy: we display one table where the columns have equal contents width, + // and one where the columns have different contents width. + if (ImGui::BeginTable("table1", 3, sizing_policy_flags[table_n] | flags1)) + { + for (int row = 0; row < 3; row++) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); ImGui::Text("Oh dear"); + ImGui::TableNextColumn(); ImGui::Text("Oh dear"); + ImGui::TableNextColumn(); ImGui::Text("Oh dear"); + } + ImGui::EndTable(); + } + if (ImGui::BeginTable("table2", 3, sizing_policy_flags[table_n] | flags1)) + { + for (int row = 0; row < 3; row++) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); ImGui::Text("AAAA"); + ImGui::TableNextColumn(); ImGui::Text("BBBBBBBB"); + ImGui::TableNextColumn(); ImGui::Text("CCCCCCCCCCCC"); + } + ImGui::EndTable(); + } + ImGui::PopID(); + } + + ImGui::Spacing(); + ImGui::TextUnformatted("Advanced"); + ImGui::SameLine(); + HelpMarker( + "This section allows you to interact and see the effect of various sizing policies " + "depending on whether Scroll is enabled and the contents of your columns."); + + enum ContentsType { CT_ShowWidth, CT_ShortText, CT_LongText, CT_Button, CT_FillButton, CT_InputText }; + static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable; + static int contents_type = CT_ShowWidth; + static int column_count = 3; + + PushStyleCompact(); + ImGui::PushID("Advanced"); + ImGui::PushItemWidth(TEXT_BASE_WIDTH * 30); + EditTableSizingFlags(&flags); + ImGui::Combo("Contents", &contents_type, "Show width\0Short Text\0Long Text\0Button\0Fill Button\0InputText\0"); + if (contents_type == CT_FillButton) + { + ImGui::SameLine(); + HelpMarker( + "Be mindful that using right-alignment (e.g. size.x = -FLT_MIN) creates a feedback loop " + "where contents width can feed into auto-column width can feed into contents width."); + } + ImGui::DragInt("Columns", &column_count, 0.1f, 1, 64, "%d", ImGuiSliderFlags_AlwaysClamp); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_PreciseWidths", &flags, ImGuiTableFlags_PreciseWidths); + ImGui::SameLine(); HelpMarker("Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth."); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollX", &flags, ImGuiTableFlags_ScrollX); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + ImGui::CheckboxFlags("ImGuiTableFlags_NoClip", &flags, ImGuiTableFlags_NoClip); + ImGui::PopItemWidth(); + ImGui::PopID(); + PopStyleCompact(); + + if (ImGui::BeginTable("table2", column_count, flags, ImVec2(0.0f, TEXT_BASE_HEIGHT * 7))) + { + for (int cell = 0; cell < 10 * column_count; cell++) + { + ImGui::TableNextColumn(); + int column = ImGui::TableGetColumnIndex(); + int row = ImGui::TableGetRowIndex(); + + ImGui::PushID(cell); + char label[32]; + static char text_buf[32] = ""; + sprintf(label, "Hello %d,%d", column, row); + switch (contents_type) + { + case CT_ShortText: ImGui::TextUnformatted(label); break; + case CT_LongText: ImGui::Text("Some %s text %d,%d\nOver two lines..", column == 0 ? "long" : "longeeer", column, row); break; + case CT_ShowWidth: ImGui::Text("W: %.1f", ImGui::GetContentRegionAvail().x); break; + case CT_Button: ImGui::Button(label); break; + case CT_FillButton: ImGui::Button(label, ImVec2(-FLT_MIN, 0.0f)); break; + case CT_InputText: ImGui::SetNextItemWidth(-FLT_MIN); ImGui::InputText("##", text_buf, IM_ARRAYSIZE(text_buf)); break; + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Vertical scrolling, with clipping"); + if (ImGui::TreeNode("Vertical scrolling, with clipping")) + { + HelpMarker( + "Here we activate ScrollY, which will create a child window container to allow hosting scrollable contents.\n\n" + "We also demonstrate using ImGuiListClipper to virtualize the submission of many items."); + static ImGuiTableFlags flags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + PopStyleCompact(); + + // When using ScrollX or ScrollY we need to specify a size for our table container! + // Otherwise by default the table will fit all available space, like a BeginChild() call. + ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 8); + if (ImGui::BeginTable("table_scrolly", 3, flags, outer_size)) + { + ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible + ImGui::TableSetupColumn("One", ImGuiTableColumnFlags_None); + ImGui::TableSetupColumn("Two", ImGuiTableColumnFlags_None); + ImGui::TableSetupColumn("Three", ImGuiTableColumnFlags_None); + ImGui::TableHeadersRow(); + + // Demonstrate using clipper for large vertical lists + ImGuiListClipper clipper; + clipper.Begin(1000); + while (clipper.Step()) + { + for (int row = clipper.DisplayStart; row < clipper.DisplayEnd; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Hello %d,%d", column, row); + } + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Horizontal scrolling"); + if (ImGui::TreeNode("Horizontal scrolling")) + { + HelpMarker( + "When ScrollX is enabled, the default sizing policy becomes ImGuiTableFlags_SizingFixedFit, " + "as automatically stretching columns doesn't make much sense with horizontal scrolling.\n\n" + "Also note that as of the current version, you will almost always want to enable ScrollY along with ScrollX, " + "because the container window won't automatically extend vertically to fix contents " + "(this may be improved in future versions)."); + static ImGuiTableFlags flags = ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable; + static int freeze_cols = 1; + static int freeze_rows = 1; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollX", &flags, ImGuiTableFlags_ScrollX); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::DragInt("freeze_cols", &freeze_cols, 0.2f, 0, 9, NULL, ImGuiSliderFlags_NoInput); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::DragInt("freeze_rows", &freeze_rows, 0.2f, 0, 9, NULL, ImGuiSliderFlags_NoInput); + PopStyleCompact(); + + // When using ScrollX or ScrollY we need to specify a size for our table container! + // Otherwise by default the table will fit all available space, like a BeginChild() call. + ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 8); + if (ImGui::BeginTable("table_scrollx", 7, flags, outer_size)) + { + ImGui::TableSetupScrollFreeze(freeze_cols, freeze_rows); + ImGui::TableSetupColumn("Line #", ImGuiTableColumnFlags_NoHide); // Make the first column not hideable to match our use of TableSetupScrollFreeze() + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableSetupColumn("Four"); + ImGui::TableSetupColumn("Five"); + ImGui::TableSetupColumn("Six"); + ImGui::TableHeadersRow(); + for (int row = 0; row < 20; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 7; column++) + { + // Both TableNextColumn() and TableSetColumnIndex() return true when a column is visible or performing width measurement. + // Because here we know that: + // - A) all our columns are contributing the same to row height + // - B) column 0 is always visible, + // We only always submit this one column and can skip others. + // More advanced per-column clipping behaviors may benefit from polling the status flags via TableGetColumnFlags(). + if (!ImGui::TableSetColumnIndex(column) && column > 0) + continue; + if (column == 0) + ImGui::Text("Line %d", row); + else + ImGui::Text("Hello world %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + ImGui::Spacing(); + ImGui::TextUnformatted("Stretch + ScrollX"); + ImGui::SameLine(); + HelpMarker( + "Showcase using Stretch columns + ScrollX together: " + "this is rather unusual and only makes sense when specifying an 'inner_width' for the table!\n" + "Without an explicit value, inner_width is == outer_size.x and therefore using Stretch columns " + "along with ScrollX doesn't make sense."); + static ImGuiTableFlags flags2 = ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_RowBg | ImGuiTableFlags_ContextMenuInBody; + static float inner_width = 1000.0f; + PushStyleCompact(); + ImGui::PushID("flags3"); + ImGui::PushItemWidth(TEXT_BASE_WIDTH * 30); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollX", &flags2, ImGuiTableFlags_ScrollX); + ImGui::DragFloat("inner_width", &inner_width, 1.0f, 0.0f, FLT_MAX, "%.1f"); + ImGui::PopItemWidth(); + ImGui::PopID(); + PopStyleCompact(); + if (ImGui::BeginTable("table2", 7, flags2, outer_size, inner_width)) + { + for (int cell = 0; cell < 20 * 7; cell++) + { + ImGui::TableNextColumn(); + ImGui::Text("Hello world %d,%d", ImGui::TableGetColumnIndex(), ImGui::TableGetRowIndex()); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Columns flags"); + if (ImGui::TreeNode("Columns flags")) + { + // Create a first table just to show all the options/flags we want to make visible in our example! + const int column_count = 3; + const char* column_names[column_count] = { "One", "Two", "Three" }; + static ImGuiTableColumnFlags column_flags[column_count] = { ImGuiTableColumnFlags_DefaultSort, ImGuiTableColumnFlags_None, ImGuiTableColumnFlags_DefaultHide }; + static ImGuiTableColumnFlags column_flags_out[column_count] = { 0, 0, 0 }; // Output from TableGetColumnFlags() + + if (ImGui::BeginTable("table_columns_flags_checkboxes", column_count, ImGuiTableFlags_None)) + { + PushStyleCompact(); + for (int column = 0; column < column_count; column++) + { + ImGui::TableNextColumn(); + ImGui::PushID(column); + ImGui::AlignTextToFramePadding(); // FIXME-TABLE: Workaround for wrong text baseline propagation across columns + ImGui::Text("'%s'", column_names[column]); + ImGui::Spacing(); + ImGui::Text("Input flags:"); + EditTableColumnsFlags(&column_flags[column]); + ImGui::Spacing(); + ImGui::Text("Output flags:"); + ImGui::BeginDisabled(); + ShowTableColumnsStatusFlags(column_flags_out[column]); + ImGui::EndDisabled(); + ImGui::PopID(); + } + PopStyleCompact(); + ImGui::EndTable(); + } + + // Create the real table we care about for the example! + // We use a scrolling table to be able to showcase the difference between the _IsEnabled and _IsVisible flags above, + // otherwise in a non-scrolling table columns are always visible (unless using ImGuiTableFlags_NoKeepColumnsVisible + // + resizing the parent window down). + const ImGuiTableFlags flags + = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY + | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV + | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Sortable; + ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 9); + if (ImGui::BeginTable("table_columns_flags", column_count, flags, outer_size)) + { + bool has_angled_header = false; + for (int column = 0; column < column_count; column++) + { + has_angled_header |= (column_flags[column] & ImGuiTableColumnFlags_AngledHeader) != 0; + ImGui::TableSetupColumn(column_names[column], column_flags[column]); + } + if (has_angled_header) + ImGui::TableAngledHeadersRow(); + ImGui::TableHeadersRow(); + for (int column = 0; column < column_count; column++) + column_flags_out[column] = ImGui::TableGetColumnFlags(column); + float indent_step = (float)((int)TEXT_BASE_WIDTH / 2); + for (int row = 0; row < 8; row++) + { + // Add some indentation to demonstrate usage of per-column IndentEnable/IndentDisable flags. + ImGui::Indent(indent_step); + ImGui::TableNextRow(); + for (int column = 0; column < column_count; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("%s %s", (column == 0) ? "Indented" : "Hello", ImGui::TableGetColumnName(column)); + } + } + ImGui::Unindent(indent_step * 8.0f); + + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Columns widths"); + if (ImGui::TreeNode("Columns widths")) + { + HelpMarker("Using TableSetupColumn() to setup default width."); + + static ImGuiTableFlags flags1 = ImGuiTableFlags_Borders | ImGuiTableFlags_NoBordersInBodyUntilResize; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags1, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags1, ImGuiTableFlags_NoBordersInBodyUntilResize); + PopStyleCompact(); + if (ImGui::BeginTable("table1", 3, flags1)) + { + // We could also set ImGuiTableFlags_SizingFixedFit on the table and all columns will default to ImGuiTableColumnFlags_WidthFixed. + ImGui::TableSetupColumn("one", ImGuiTableColumnFlags_WidthFixed, 100.0f); // Default to 100.0f + ImGui::TableSetupColumn("two", ImGuiTableColumnFlags_WidthFixed, 200.0f); // Default to 200.0f + ImGui::TableSetupColumn("three", ImGuiTableColumnFlags_WidthFixed); // Default to auto + ImGui::TableHeadersRow(); + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableSetColumnIndex(column); + if (row == 0) + ImGui::Text("(w: %5.1f)", ImGui::GetContentRegionAvail().x); + else + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + HelpMarker( + "Using TableSetupColumn() to setup explicit width.\n\nUnless _NoKeepColumnsVisible is set, " + "fixed columns with set width may still be shrunk down if there's not enough space in the host."); + + static ImGuiTableFlags flags2 = ImGuiTableFlags_None; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_NoKeepColumnsVisible", &flags2, ImGuiTableFlags_NoKeepColumnsVisible); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerV", &flags2, ImGuiTableFlags_BordersInnerV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterV", &flags2, ImGuiTableFlags_BordersOuterV); + PopStyleCompact(); + if (ImGui::BeginTable("table2", 4, flags2)) + { + // We could also set ImGuiTableFlags_SizingFixedFit on the table and then all columns + // will default to ImGuiTableColumnFlags_WidthFixed. + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, 100.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 15.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 30.0f); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 15.0f); + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 4; column++) + { + ImGui::TableSetColumnIndex(column); + if (row == 0) + ImGui::Text("(w: %5.1f)", ImGui::GetContentRegionAvail().x); + else + ImGui::Text("Hello %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Nested tables"); + if (ImGui::TreeNode("Nested tables")) + { + HelpMarker("This demonstrates embedding a table into another table cell."); + + if (ImGui::BeginTable("table_nested1", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + ImGui::TableSetupColumn("A0"); + ImGui::TableSetupColumn("A1"); + ImGui::TableHeadersRow(); + + ImGui::TableNextColumn(); + ImGui::Text("A0 Row 0"); + { + float rows_height = TEXT_BASE_HEIGHT * 2; + if (ImGui::BeginTable("table_nested2", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + ImGui::TableSetupColumn("B0"); + ImGui::TableSetupColumn("B1"); + ImGui::TableHeadersRow(); + + ImGui::TableNextRow(ImGuiTableRowFlags_None, rows_height); + ImGui::TableNextColumn(); + ImGui::Text("B0 Row 0"); + ImGui::TableNextColumn(); + ImGui::Text("B1 Row 0"); + ImGui::TableNextRow(ImGuiTableRowFlags_None, rows_height); + ImGui::TableNextColumn(); + ImGui::Text("B0 Row 1"); + ImGui::TableNextColumn(); + ImGui::Text("B1 Row 1"); + + ImGui::EndTable(); + } + } + ImGui::TableNextColumn(); ImGui::Text("A1 Row 0"); + ImGui::TableNextColumn(); ImGui::Text("A0 Row 1"); + ImGui::TableNextColumn(); ImGui::Text("A1 Row 1"); + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Row height"); + if (ImGui::TreeNode("Row height")) + { + HelpMarker( + "You can pass a 'min_row_height' to TableNextRow().\n\nRows are padded with 'style.CellPadding.y' on top and bottom, " + "so effectively the minimum row height will always be >= 'style.CellPadding.y * 2.0f'.\n\n" + "We cannot honor a _maximum_ row height as that would require a unique clipping rectangle per row."); + if (ImGui::BeginTable("table_row_height", 1, ImGuiTableFlags_Borders)) + { + for (int row = 0; row < 8; row++) + { + float min_row_height = (float)(int)(TEXT_BASE_HEIGHT * 0.30f * row); + ImGui::TableNextRow(ImGuiTableRowFlags_None, min_row_height); + ImGui::TableNextColumn(); + ImGui::Text("min_row_height = %.2f", min_row_height); + } + ImGui::EndTable(); + } + + HelpMarker( + "Showcase using SameLine(0,0) to share Current Line Height between cells.\n\n" + "Please note that Tables Row Height is not the same thing as Current Line Height, " + "as a table cell may contains multiple lines."); + if (ImGui::BeginTable("table_share_lineheight", 2, ImGuiTableFlags_Borders)) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::ColorButton("##1", ImVec4(0.13f, 0.26f, 0.40f, 1.0f), ImGuiColorEditFlags_None, ImVec2(40, 40)); + ImGui::TableNextColumn(); + ImGui::Text("Line 1"); + ImGui::Text("Line 2"); + + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::ColorButton("##2", ImVec4(0.13f, 0.26f, 0.40f, 1.0f), ImGuiColorEditFlags_None, ImVec2(40, 40)); + ImGui::TableNextColumn(); + ImGui::SameLine(0.0f, 0.0f); // Reuse line height from previous column + ImGui::Text("Line 1, with SameLine(0,0)"); + ImGui::Text("Line 2"); + + ImGui::EndTable(); + } + + HelpMarker("Showcase altering CellPadding.y between rows. Note that CellPadding.x is locked for the entire table."); + if (ImGui::BeginTable("table_changing_cellpadding_y", 1, ImGuiTableFlags_Borders)) + { + ImGuiStyle& style = ImGui::GetStyle(); + for (int row = 0; row < 8; row++) + { + if ((row % 3) == 2) + ImGui::PushStyleVarY(ImGuiStyleVar_CellPadding, 20.0f); + ImGui::TableNextRow(ImGuiTableRowFlags_None); + ImGui::TableNextColumn(); + ImGui::Text("CellPadding.y = %.2f", style.CellPadding.y); + if ((row % 3) == 2) + ImGui::PopStyleVar(); + } + ImGui::EndTable(); + } + + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Outer size"); + if (ImGui::TreeNode("Outer size")) + { + // Showcasing use of ImGuiTableFlags_NoHostExtendX and ImGuiTableFlags_NoHostExtendY + // Important to that note how the two flags have slightly different behaviors! + ImGui::Text("Using NoHostExtendX and NoHostExtendY:"); + PushStyleCompact(); + static ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_Resizable | ImGuiTableFlags_ContextMenuInBody | ImGuiTableFlags_RowBg | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoHostExtendX; + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendX", &flags, ImGuiTableFlags_NoHostExtendX); + ImGui::SameLine(); HelpMarker("Make outer width auto-fit to columns, overriding outer_size.x value.\n\nOnly available when ScrollX/ScrollY are disabled and Stretch columns are not used."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendY", &flags, ImGuiTableFlags_NoHostExtendY); + ImGui::SameLine(); HelpMarker("Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit).\n\nOnly available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible."); + PopStyleCompact(); + + ImVec2 outer_size = ImVec2(0.0f, TEXT_BASE_HEIGHT * 5.5f); + if (ImGui::BeginTable("table1", 3, flags, outer_size)) + { + for (int row = 0; row < 10; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableNextColumn(); + ImGui::Text("Cell %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::SameLine(); + ImGui::Text("Hello!"); + + ImGui::Spacing(); + + ImGui::Text("Using explicit size:"); + if (ImGui::BeginTable("table2", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg, ImVec2(TEXT_BASE_WIDTH * 30, 0.0f))) + { + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + ImGui::TableNextColumn(); + ImGui::Text("Cell %d,%d", column, row); + } + } + ImGui::EndTable(); + } + ImGui::SameLine(); + if (ImGui::BeginTable("table3", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg, ImVec2(TEXT_BASE_WIDTH * 30, 0.0f))) + { + for (int row = 0; row < 3; row++) + { + ImGui::TableNextRow(0, TEXT_BASE_HEIGHT * 1.5f); + for (int column = 0; column < 3; column++) + { + ImGui::TableNextColumn(); + ImGui::Text("Cell %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Background color"); + if (ImGui::TreeNode("Background color")) + { + static ImGuiTableFlags flags = ImGuiTableFlags_RowBg; + static int row_bg_type = 1; + static int row_bg_target = 1; + static int cell_bg_type = 1; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_Borders", &flags, ImGuiTableFlags_Borders); + ImGui::CheckboxFlags("ImGuiTableFlags_RowBg", &flags, ImGuiTableFlags_RowBg); + ImGui::SameLine(); HelpMarker("ImGuiTableFlags_RowBg automatically sets RowBg0 to alternative colors pulled from the Style."); + ImGui::Combo("row bg type", (int*)&row_bg_type, "None\0Red\0Gradient\0"); + ImGui::Combo("row bg target", (int*)&row_bg_target, "RowBg0\0RowBg1\0"); ImGui::SameLine(); HelpMarker("Target RowBg0 to override the alternating odd/even colors,\nTarget RowBg1 to blend with them."); + ImGui::Combo("cell bg type", (int*)&cell_bg_type, "None\0Blue\0"); ImGui::SameLine(); HelpMarker("We are colorizing cells to B1->C2 here."); + IM_ASSERT(row_bg_type >= 0 && row_bg_type <= 2); + IM_ASSERT(row_bg_target >= 0 && row_bg_target <= 1); + IM_ASSERT(cell_bg_type >= 0 && cell_bg_type <= 1); + PopStyleCompact(); + + if (ImGui::BeginTable("table1", 5, flags)) + { + for (int row = 0; row < 6; row++) + { + ImGui::TableNextRow(); + + // Demonstrate setting a row background color with 'ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBgX, ...)' + // We use a transparent color so we can see the one behind in case our target is RowBg1 and RowBg0 was already targeted by the ImGuiTableFlags_RowBg flag. + if (row_bg_type != 0) + { + ImU32 row_bg_color = ImGui::GetColorU32(row_bg_type == 1 ? ImVec4(0.7f, 0.3f, 0.3f, 0.65f) : ImVec4(0.2f + row * 0.1f, 0.2f, 0.2f, 0.65f)); // Flat or Gradient? + ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0 + row_bg_target, row_bg_color); + } + + // Fill cells + for (int column = 0; column < 5; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("%c%c", 'A' + row, '0' + column); + + // Change background of Cells B1->C2 + // Demonstrate setting a cell background color with 'ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, ...)' + // (the CellBg color will be blended over the RowBg and ColumnBg colors) + // We can also pass a column number as a third parameter to TableSetBgColor() and do this outside the column loop. + if (row >= 1 && row <= 2 && column >= 1 && column <= 2 && cell_bg_type == 1) + { + ImU32 cell_bg_color = ImGui::GetColorU32(ImVec4(0.3f, 0.3f, 0.7f, 0.65f)); + ImGui::TableSetBgColor(ImGuiTableBgTarget_CellBg, cell_bg_color); + } + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Tree view"); + if (ImGui::TreeNode("Tree view")) + { + static ImGuiTableFlags table_flags = ImGuiTableFlags_BordersV | ImGuiTableFlags_BordersOuterH | ImGuiTableFlags_Resizable | ImGuiTableFlags_RowBg | ImGuiTableFlags_NoBordersInBody; + + static ImGuiTreeNodeFlags tree_node_flags_base = ImGuiTreeNodeFlags_SpanAllColumns; + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanFullWidth", &tree_node_flags_base, ImGuiTreeNodeFlags_SpanFullWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanLabelWidth", &tree_node_flags_base, ImGuiTreeNodeFlags_SpanLabelWidth); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_SpanAllColumns", &tree_node_flags_base, ImGuiTreeNodeFlags_SpanAllColumns); + ImGui::CheckboxFlags("ImGuiTreeNodeFlags_LabelSpanAllColumns", &tree_node_flags_base, ImGuiTreeNodeFlags_LabelSpanAllColumns); + ImGui::SameLine(); HelpMarker("Useful if you know that you aren't displaying contents in other columns"); + + HelpMarker("See \"Columns flags\" section to configure how indentation is applied to individual columns."); + if (ImGui::BeginTable("3ways", 3, table_flags)) + { + // The first column will use the default _WidthStretch when ScrollX is Off and _WidthFixed when ScrollX is On + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_NoHide); + ImGui::TableSetupColumn("Size", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 12.0f); + ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, TEXT_BASE_WIDTH * 18.0f); + ImGui::TableHeadersRow(); + + // Simple storage to output a dummy file-system. + struct MyTreeNode + { + const char* Name; + const char* Type; + int Size; + int ChildIdx; + int ChildCount; + static void DisplayNode(const MyTreeNode* node, const MyTreeNode* all_nodes) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + const bool is_folder = (node->ChildCount > 0); + + ImGuiTreeNodeFlags node_flags = tree_node_flags_base; + if (node != &all_nodes[0]) + node_flags &= ~ImGuiTreeNodeFlags_LabelSpanAllColumns; // Only demonstrate this on the root node. + + if (is_folder) + { + bool open = ImGui::TreeNodeEx(node->Name, node_flags); + if ((node_flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) == 0) + { + ImGui::TableNextColumn(); + ImGui::TextDisabled("--"); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(node->Type); + } + if (open) + { + for (int child_n = 0; child_n < node->ChildCount; child_n++) + DisplayNode(&all_nodes[node->ChildIdx + child_n], all_nodes); + ImGui::TreePop(); + } + } + else + { + ImGui::TreeNodeEx(node->Name, node_flags | ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet | ImGuiTreeNodeFlags_NoTreePushOnOpen); + ImGui::TableNextColumn(); + ImGui::Text("%d", node->Size); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(node->Type); + } + } + }; + static const MyTreeNode nodes[] = + { + { "Root with Long Name", "Folder", -1, 1, 3 }, // 0 + { "Music", "Folder", -1, 4, 2 }, // 1 + { "Textures", "Folder", -1, 6, 3 }, // 2 + { "desktop.ini", "System file", 1024, -1,-1 }, // 3 + { "File1_a.wav", "Audio file", 123000, -1,-1 }, // 4 + { "File1_b.wav", "Audio file", 456000, -1,-1 }, // 5 + { "Image001.png", "Image file", 203128, -1,-1 }, // 6 + { "Copy of Image001.png", "Image file", 203256, -1,-1 }, // 7 + { "Copy of Image001 (Final2).png","Image file", 203512, -1,-1 }, // 8 + }; + + MyTreeNode::DisplayNode(&nodes[0], nodes); + + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Item width"); + if (ImGui::TreeNode("Item width")) + { + HelpMarker( + "Showcase using PushItemWidth() and how it is preserved on a per-column basis.\n\n" + "Note that on auto-resizing non-resizable fixed columns, querying the content width for " + "e.g. right-alignment doesn't make sense."); + if (ImGui::BeginTable("table_item_width", 3, ImGuiTableFlags_Borders)) + { + ImGui::TableSetupColumn("small"); + ImGui::TableSetupColumn("half"); + ImGui::TableSetupColumn("right-align"); + ImGui::TableHeadersRow(); + + for (int row = 0; row < 3; row++) + { + ImGui::TableNextRow(); + if (row == 0) + { + // Setup ItemWidth once (instead of setting up every time, which is also possible but less efficient) + ImGui::TableSetColumnIndex(0); + ImGui::PushItemWidth(TEXT_BASE_WIDTH * 3.0f); // Small + ImGui::TableSetColumnIndex(1); + ImGui::PushItemWidth(-ImGui::GetContentRegionAvail().x * 0.5f); + ImGui::TableSetColumnIndex(2); + ImGui::PushItemWidth(-FLT_MIN); // Right-aligned + } + + // Draw our contents + static float dummy_f = 0.0f; + ImGui::PushID(row); + ImGui::TableSetColumnIndex(0); + ImGui::SliderFloat("float0", &dummy_f, 0.0f, 1.0f); + ImGui::TableSetColumnIndex(1); + ImGui::SliderFloat("float1", &dummy_f, 0.0f, 1.0f); + ImGui::TableSetColumnIndex(2); + ImGui::SliderFloat("##float2", &dummy_f, 0.0f, 1.0f); // No visible label since right-aligned + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + // Demonstrate using TableHeader() calls instead of TableHeadersRow() + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Custom headers"); + if (ImGui::TreeNode("Custom headers")) + { + const int COLUMNS_COUNT = 3; + if (ImGui::BeginTable("table_custom_headers", COLUMNS_COUNT, ImGuiTableFlags_Borders | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + ImGui::TableSetupColumn("Apricot"); + ImGui::TableSetupColumn("Banana"); + ImGui::TableSetupColumn("Cherry"); + + // Dummy entire-column selection storage + // FIXME: It would be nice to actually demonstrate full-featured selection using those checkbox. + static bool column_selected[3] = {}; + + // Instead of calling TableHeadersRow() we'll submit custom headers ourselves. + // (A different approach is also possible: + // - Specify ImGuiTableColumnFlags_NoHeaderLabel in some TableSetupColumn() call. + // - Call TableHeadersRow() normally. This will submit TableHeader() with no name. + // - Then call TableSetColumnIndex() to position yourself in the column and submit your stuff e.g. Checkbox().) + ImGui::TableNextRow(ImGuiTableRowFlags_Headers); + for (int column = 0; column < COLUMNS_COUNT; column++) + { + ImGui::TableSetColumnIndex(column); + const char* column_name = ImGui::TableGetColumnName(column); // Retrieve name passed to TableSetupColumn() + ImGui::PushID(column); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::Checkbox("##checkall", &column_selected[column]); + ImGui::PopStyleVar(); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::TableHeader(column_name); + ImGui::PopID(); + } + + // Submit table contents + for (int row = 0; row < 5; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < 3; column++) + { + char buf[32]; + sprintf(buf, "Cell %d,%d", column, row); + ImGui::TableSetColumnIndex(column); + ImGui::Selectable(buf, column_selected[column]); + } + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + // Demonstrate using ImGuiTableColumnFlags_AngledHeader flag to create angled headers + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Angled headers"); + if (ImGui::TreeNode("Angled headers")) + { + const char* column_names[] = { "Track", "cabasa", "ride", "smash", "tom-hi", "tom-mid", "tom-low", "hihat-o", "hihat-c", "snare-s", "snare-c", "clap", "rim", "kick" }; + const int columns_count = IM_ARRAYSIZE(column_names); + const int rows_count = 12; + + static ImGuiTableFlags table_flags = ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_Hideable | ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_HighlightHoveredColumn; + static ImGuiTableColumnFlags column_flags = ImGuiTableColumnFlags_AngledHeader | ImGuiTableColumnFlags_WidthFixed; + static bool bools[columns_count * rows_count] = {}; // Dummy storage selection storage + static int frozen_cols = 1; + static int frozen_rows = 2; + ImGui::CheckboxFlags("_ScrollX", &table_flags, ImGuiTableFlags_ScrollX); + ImGui::CheckboxFlags("_ScrollY", &table_flags, ImGuiTableFlags_ScrollY); + ImGui::CheckboxFlags("_Resizable", &table_flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("_Sortable", &table_flags, ImGuiTableFlags_Sortable); + ImGui::CheckboxFlags("_NoBordersInBody", &table_flags, ImGuiTableFlags_NoBordersInBody); + ImGui::CheckboxFlags("_HighlightHoveredColumn", &table_flags, ImGuiTableFlags_HighlightHoveredColumn); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderInt("Frozen columns", &frozen_cols, 0, 2); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderInt("Frozen rows", &frozen_rows, 0, 2); + ImGui::CheckboxFlags("Disable header contributing to column width", &column_flags, ImGuiTableColumnFlags_NoHeaderWidth); + + if (ImGui::TreeNode("Style settings")) + { + ImGui::SameLine(); + HelpMarker("Giving access to some ImGuiStyle value in this demo for convenience."); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderAngle("style.TableAngledHeadersAngle", &ImGui::GetStyle().TableAngledHeadersAngle, -50.0f, +50.0f); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::SliderFloat2("style.TableAngledHeadersTextAlign", (float*)&ImGui::GetStyle().TableAngledHeadersTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::TreePop(); + } + + if (ImGui::BeginTable("table_angled_headers", columns_count, table_flags, ImVec2(0.0f, TEXT_BASE_HEIGHT * 12))) + { + ImGui::TableSetupColumn(column_names[0], ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_NoReorder); + for (int n = 1; n < columns_count; n++) + ImGui::TableSetupColumn(column_names[n], column_flags); + ImGui::TableSetupScrollFreeze(frozen_cols, frozen_rows); + + ImGui::TableAngledHeadersRow(); // Draw angled headers for all columns with the ImGuiTableColumnFlags_AngledHeader flag. + ImGui::TableHeadersRow(); // Draw remaining headers and allow access to context-menu and other functions. + for (int row = 0; row < rows_count; row++) + { + ImGui::PushID(row); + ImGui::TableNextRow(); + ImGui::TableSetColumnIndex(0); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Track %d", row); + for (int column = 1; column < columns_count; column++) + if (ImGui::TableSetColumnIndex(column)) + { + ImGui::PushID(column); + ImGui::Checkbox("", &bools[row * columns_count + column]); + ImGui::PopID(); + } + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + // Demonstrate creating custom context menus inside columns, + // while playing it nice with context menus provided by TableHeadersRow()/TableHeader() + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Context menus"); + if (ImGui::TreeNode("Context menus")) + { + HelpMarker( + "By default, right-clicking over a TableHeadersRow()/TableHeader() line will open the default context-menu.\n" + "Using ImGuiTableFlags_ContextMenuInBody we also allow right-clicking over columns body."); + static ImGuiTableFlags flags1 = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_ContextMenuInBody; + + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_ContextMenuInBody", &flags1, ImGuiTableFlags_ContextMenuInBody); + PopStyleCompact(); + + // Context Menus: first example + // [1.1] Right-click on the TableHeadersRow() line to open the default table context menu. + // [1.2] Right-click in columns also open the default table context menu (if ImGuiTableFlags_ContextMenuInBody is set) + const int COLUMNS_COUNT = 3; + if (ImGui::BeginTable("table_context_menu", COLUMNS_COUNT, flags1)) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + + // [1.1]] Right-click on the TableHeadersRow() line to open the default table context menu. + ImGui::TableHeadersRow(); + + // Submit dummy contents + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < COLUMNS_COUNT; column++) + { + ImGui::TableSetColumnIndex(column); + ImGui::Text("Cell %d,%d", column, row); + } + } + ImGui::EndTable(); + } + + // Context Menus: second example + // [2.1] Right-click on the TableHeadersRow() line to open the default table context menu. + // [2.2] Right-click on the ".." to open a custom popup + // [2.3] Right-click in columns to open another custom popup + HelpMarker( + "Demonstrate mixing table context menu (over header), item context button (over button) " + "and custom per-colunm context menu (over column body)."); + ImGuiTableFlags flags2 = ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders; + if (ImGui::BeginTable("table_context_menu_2", COLUMNS_COUNT, flags2)) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + + // [2.1] Right-click on the TableHeadersRow() line to open the default table context menu. + ImGui::TableHeadersRow(); + for (int row = 0; row < 4; row++) + { + ImGui::TableNextRow(); + for (int column = 0; column < COLUMNS_COUNT; column++) + { + // Submit dummy contents + ImGui::TableSetColumnIndex(column); + ImGui::Text("Cell %d,%d", column, row); + ImGui::SameLine(); + + // [2.2] Right-click on the ".." to open a custom popup + ImGui::PushID(row * COLUMNS_COUNT + column); + ImGui::SmallButton(".."); + if (ImGui::BeginPopupContextItem()) + { + ImGui::Text("This is the popup for Button(\"..\") in Cell %d,%d", column, row); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::PopID(); + } + } + + // [2.3] Right-click anywhere in columns to open another custom popup + // (instead of testing for !IsAnyItemHovered() we could also call OpenPopup() with ImGuiPopupFlags_NoOpenOverExistingPopup + // to manage popup priority as the popups triggers, here "are we hovering a column" are overlapping) + int hovered_column = -1; + for (int column = 0; column < COLUMNS_COUNT + 1; column++) + { + ImGui::PushID(column); + if (ImGui::TableGetColumnFlags(column) & ImGuiTableColumnFlags_IsHovered) + hovered_column = column; + if (hovered_column == column && !ImGui::IsAnyItemHovered() && ImGui::IsMouseReleased(1)) + ImGui::OpenPopup("MyPopup"); + if (ImGui::BeginPopup("MyPopup")) + { + if (column == COLUMNS_COUNT) + ImGui::Text("This is a custom popup for unused space after the last column."); + else + ImGui::Text("This is a custom popup for Column %d", column); + if (ImGui::Button("Close")) + ImGui::CloseCurrentPopup(); + ImGui::EndPopup(); + } + ImGui::PopID(); + } + + ImGui::EndTable(); + ImGui::Text("Hovered column: %d", hovered_column); + } + ImGui::TreePop(); + } + + // Demonstrate creating multiple tables with the same ID + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Synced instances"); + if (ImGui::TreeNode("Synced instances")) + { + HelpMarker("Multiple tables with the same identifier will share their settings, width, visibility, order etc."); + + static ImGuiTableFlags flags = ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Borders | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_NoSavedSettings; + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + ImGui::CheckboxFlags("ImGuiTableFlags_SizingFixedFit", &flags, ImGuiTableFlags_SizingFixedFit); + ImGui::CheckboxFlags("ImGuiTableFlags_HighlightHoveredColumn", &flags, ImGuiTableFlags_HighlightHoveredColumn); + for (int n = 0; n < 3; n++) + { + char buf[32]; + sprintf(buf, "Synced Table %d", n); + bool open = ImGui::CollapsingHeader(buf, ImGuiTreeNodeFlags_DefaultOpen); + if (open && ImGui::BeginTable("Table", 3, flags, ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 5))) + { + ImGui::TableSetupColumn("One"); + ImGui::TableSetupColumn("Two"); + ImGui::TableSetupColumn("Three"); + ImGui::TableHeadersRow(); + const int cell_count = (n == 1) ? 27 : 9; // Make second table have a scrollbar to verify that additional decoration is not affecting column positions. + for (int cell = 0; cell < cell_count; cell++) + { + ImGui::TableNextColumn(); + ImGui::Text("this cell %d", cell); + } + ImGui::EndTable(); + } + } + ImGui::TreePop(); + } + + // Demonstrate using Sorting facilities + // This is a simplified version of the "Advanced" example, where we mostly focus on the code necessary to handle sorting. + // Note that the "Advanced" example also showcase manually triggering a sort (e.g. if item quantities have been modified) + static const char* template_items_names[] = + { + "Banana", "Apple", "Cherry", "Watermelon", "Grapefruit", "Strawberry", "Mango", + "Kiwi", "Orange", "Pineapple", "Blueberry", "Plum", "Coconut", "Pear", "Apricot" + }; + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Sorting"); + if (ImGui::TreeNode("Sorting")) + { + // Create item list + static ImVector items; + if (items.Size == 0) + { + items.resize(50, MyItem()); + for (int n = 0; n < items.Size; n++) + { + const int template_n = n % IM_ARRAYSIZE(template_items_names); + MyItem& item = items[n]; + item.ID = n; + item.Name = template_items_names[template_n]; + item.Quantity = (n * n - n) % 20; // Assign default quantities + } + } + + // Options + static ImGuiTableFlags flags = + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti + | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV | ImGuiTableFlags_NoBordersInBody + | ImGuiTableFlags_ScrollY; + PushStyleCompact(); + ImGui::CheckboxFlags("ImGuiTableFlags_SortMulti", &flags, ImGuiTableFlags_SortMulti); + ImGui::SameLine(); HelpMarker("When sorting is enabled: hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1)."); + ImGui::CheckboxFlags("ImGuiTableFlags_SortTristate", &flags, ImGuiTableFlags_SortTristate); + ImGui::SameLine(); HelpMarker("When sorting is enabled: allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0)."); + PopStyleCompact(); + + if (ImGui::BeginTable("table_sorting", 4, flags, ImVec2(0.0f, TEXT_BASE_HEIGHT * 15), 0.0f)) + { + // Declare columns + // We use the "user_id" parameter of TableSetupColumn() to specify a user id that will be stored in the sort specifications. + // This is so our sort function can identify a column given our own identifier. We could also identify them based on their index! + // Demonstrate using a mixture of flags among available sort-related flags: + // - ImGuiTableColumnFlags_DefaultSort + // - ImGuiTableColumnFlags_NoSort / ImGuiTableColumnFlags_NoSortAscending / ImGuiTableColumnFlags_NoSortDescending + // - ImGuiTableColumnFlags_PreferSortAscending / ImGuiTableColumnFlags_PreferSortDescending + ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_ID); + ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Name); + ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Action); + ImGui::TableSetupColumn("Quantity", ImGuiTableColumnFlags_PreferSortDescending | ImGuiTableColumnFlags_WidthStretch, 0.0f, MyItemColumnID_Quantity); + ImGui::TableSetupScrollFreeze(0, 1); // Make row always visible + ImGui::TableHeadersRow(); + + // Sort our data if sort specs have been changed! + if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) + if (sort_specs->SpecsDirty) + { + MyItem::SortWithSortSpecs(sort_specs, items.Data, items.Size); + sort_specs->SpecsDirty = false; + } + + // Demonstrate using clipper for large vertical lists + ImGuiListClipper clipper; + clipper.Begin(items.Size); + while (clipper.Step()) + for (int row_n = clipper.DisplayStart; row_n < clipper.DisplayEnd; row_n++) + { + // Display a data item + MyItem* item = &items[row_n]; + ImGui::PushID(item->ID); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::Text("%04d", item->ID); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(item->Name); + ImGui::TableNextColumn(); + ImGui::SmallButton("None"); + ImGui::TableNextColumn(); + ImGui::Text("%d", item->Quantity); + ImGui::PopID(); + } + ImGui::EndTable(); + } + ImGui::TreePop(); + } + + // In this example we'll expose most table flags and settings. + // For specific flags and settings refer to the corresponding section for more detailed explanation. + // This section is mostly useful to experiment with combining certain flags or settings with each others. + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); // [DEBUG] + if (open_action != -1) + ImGui::SetNextItemOpen(open_action != 0); + IMGUI_DEMO_MARKER("Tables/Advanced"); + if (ImGui::TreeNode("Advanced")) + { + static ImGuiTableFlags flags = + ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable + | ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti + | ImGuiTableFlags_RowBg | ImGuiTableFlags_Borders | ImGuiTableFlags_NoBordersInBody + | ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY + | ImGuiTableFlags_SizingFixedFit; + static ImGuiTableColumnFlags columns_base_flags = ImGuiTableColumnFlags_None; + + enum ContentsType { CT_Text, CT_Button, CT_SmallButton, CT_FillButton, CT_Selectable, CT_SelectableSpanRow }; + static int contents_type = CT_SelectableSpanRow; + const char* contents_type_names[] = { "Text", "Button", "SmallButton", "FillButton", "Selectable", "Selectable (span row)" }; + static int freeze_cols = 1; + static int freeze_rows = 1; + static int items_count = IM_ARRAYSIZE(template_items_names) * 2; + static ImVec2 outer_size_value = ImVec2(0.0f, TEXT_BASE_HEIGHT * 12); + static float row_min_height = 0.0f; // Auto + static float inner_width_with_scroll = 0.0f; // Auto-extend + static bool outer_size_enabled = true; + static bool show_headers = true; + static bool show_wrapped_text = false; + //static ImGuiTextFilter filter; + //ImGui::SetNextItemOpen(true, ImGuiCond_Once); // FIXME-TABLE: Enabling this results in initial clipped first pass on table which tend to affect column sizing + if (ImGui::TreeNode("Options")) + { + // Make the UI compact because there are so many fields + PushStyleCompact(); + ImGui::PushItemWidth(TEXT_BASE_WIDTH * 28.0f); + + if (ImGui::TreeNodeEx("Features:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_Resizable", &flags, ImGuiTableFlags_Resizable); + ImGui::CheckboxFlags("ImGuiTableFlags_Reorderable", &flags, ImGuiTableFlags_Reorderable); + ImGui::CheckboxFlags("ImGuiTableFlags_Hideable", &flags, ImGuiTableFlags_Hideable); + ImGui::CheckboxFlags("ImGuiTableFlags_Sortable", &flags, ImGuiTableFlags_Sortable); + ImGui::CheckboxFlags("ImGuiTableFlags_NoSavedSettings", &flags, ImGuiTableFlags_NoSavedSettings); + ImGui::CheckboxFlags("ImGuiTableFlags_ContextMenuInBody", &flags, ImGuiTableFlags_ContextMenuInBody); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Decorations:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_RowBg", &flags, ImGuiTableFlags_RowBg); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersV", &flags, ImGuiTableFlags_BordersV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterV", &flags, ImGuiTableFlags_BordersOuterV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerV", &flags, ImGuiTableFlags_BordersInnerV); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersH", &flags, ImGuiTableFlags_BordersH); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersOuterH", &flags, ImGuiTableFlags_BordersOuterH); + ImGui::CheckboxFlags("ImGuiTableFlags_BordersInnerH", &flags, ImGuiTableFlags_BordersInnerH); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBody", &flags, ImGuiTableFlags_NoBordersInBody); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body (borders will always appear in Headers"); + ImGui::CheckboxFlags("ImGuiTableFlags_NoBordersInBodyUntilResize", &flags, ImGuiTableFlags_NoBordersInBodyUntilResize); ImGui::SameLine(); HelpMarker("Disable vertical borders in columns Body until hovered for resize (borders will always appear in Headers)"); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Sizing:", ImGuiTreeNodeFlags_DefaultOpen)) + { + EditTableSizingFlags(&flags); + ImGui::SameLine(); HelpMarker("In the Advanced demo we override the policy of each column so those table-wide settings have less effect that typical."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendX", &flags, ImGuiTableFlags_NoHostExtendX); + ImGui::SameLine(); HelpMarker("Make outer width auto-fit to columns, overriding outer_size.x value.\n\nOnly available when ScrollX/ScrollY are disabled and Stretch columns are not used."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoHostExtendY", &flags, ImGuiTableFlags_NoHostExtendY); + ImGui::SameLine(); HelpMarker("Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit).\n\nOnly available when ScrollX/ScrollY are disabled. Data below the limit will be clipped and not visible."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoKeepColumnsVisible", &flags, ImGuiTableFlags_NoKeepColumnsVisible); + ImGui::SameLine(); HelpMarker("Only available if ScrollX is disabled."); + ImGui::CheckboxFlags("ImGuiTableFlags_PreciseWidths", &flags, ImGuiTableFlags_PreciseWidths); + ImGui::SameLine(); HelpMarker("Disable distributing remainder width to stretched columns (width allocation on a 100-wide table with 3 columns: Without this flag: 33,33,34. With this flag: 33,33,33). With larger number of columns, resizing will appear to be less smooth."); + ImGui::CheckboxFlags("ImGuiTableFlags_NoClip", &flags, ImGuiTableFlags_NoClip); + ImGui::SameLine(); HelpMarker("Disable clipping rectangle for every individual columns (reduce draw command count, items will be able to overflow into other columns). Generally incompatible with ScrollFreeze options."); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Padding:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_PadOuterX", &flags, ImGuiTableFlags_PadOuterX); + ImGui::CheckboxFlags("ImGuiTableFlags_NoPadOuterX", &flags, ImGuiTableFlags_NoPadOuterX); + ImGui::CheckboxFlags("ImGuiTableFlags_NoPadInnerX", &flags, ImGuiTableFlags_NoPadInnerX); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Scrolling:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollX", &flags, ImGuiTableFlags_ScrollX); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::DragInt("freeze_cols", &freeze_cols, 0.2f, 0, 9, NULL, ImGuiSliderFlags_NoInput); + ImGui::CheckboxFlags("ImGuiTableFlags_ScrollY", &flags, ImGuiTableFlags_ScrollY); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetFrameHeight()); + ImGui::DragInt("freeze_rows", &freeze_rows, 0.2f, 0, 9, NULL, ImGuiSliderFlags_NoInput); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Sorting:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::CheckboxFlags("ImGuiTableFlags_SortMulti", &flags, ImGuiTableFlags_SortMulti); + ImGui::SameLine(); HelpMarker("When sorting is enabled: hold shift when clicking headers to sort on multiple column. TableGetSortSpecs() may return specs where (SpecsCount > 1)."); + ImGui::CheckboxFlags("ImGuiTableFlags_SortTristate", &flags, ImGuiTableFlags_SortTristate); + ImGui::SameLine(); HelpMarker("When sorting is enabled: allow no sorting, disable default sorting. TableGetSortSpecs() may return specs where (SpecsCount == 0)."); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Headers:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Checkbox("show_headers", &show_headers); + ImGui::CheckboxFlags("ImGuiTableFlags_HighlightHoveredColumn", &flags, ImGuiTableFlags_HighlightHoveredColumn); + ImGui::CheckboxFlags("ImGuiTableColumnFlags_AngledHeader", &columns_base_flags, ImGuiTableColumnFlags_AngledHeader); + ImGui::SameLine(); HelpMarker("Enable AngledHeader on all columns. Best enabled on selected narrow columns (see \"Angled headers\" section of the demo)."); + ImGui::TreePop(); + } + + if (ImGui::TreeNodeEx("Other:", ImGuiTreeNodeFlags_DefaultOpen)) + { + ImGui::Checkbox("show_wrapped_text", &show_wrapped_text); + + ImGui::DragFloat2("##OuterSize", &outer_size_value.x); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + ImGui::Checkbox("outer_size", &outer_size_enabled); + ImGui::SameLine(); + HelpMarker("If scrolling is disabled (ScrollX and ScrollY not set):\n" + "- The table is output directly in the parent window.\n" + "- OuterSize.x < 0.0f will right-align the table.\n" + "- OuterSize.x = 0.0f will narrow fit the table unless there are any Stretch columns.\n" + "- OuterSize.y then becomes the minimum size for the table, which will extend vertically if there are more rows (unless NoHostExtendY is set)."); + + // From a user point of view we will tend to use 'inner_width' differently depending on whether our table is embedding scrolling. + // To facilitate toying with this demo we will actually pass 0.0f to the BeginTable() when ScrollX is disabled. + ImGui::DragFloat("inner_width (when ScrollX active)", &inner_width_with_scroll, 1.0f, 0.0f, FLT_MAX); + + ImGui::DragFloat("row_min_height", &row_min_height, 1.0f, 0.0f, FLT_MAX); + ImGui::SameLine(); HelpMarker("Specify height of the Selectable item."); + + ImGui::DragInt("items_count", &items_count, 0.1f, 0, 9999); + ImGui::Combo("items_type (first column)", &contents_type, contents_type_names, IM_ARRAYSIZE(contents_type_names)); + //filter.Draw("filter"); + ImGui::TreePop(); + } + + ImGui::PopItemWidth(); + PopStyleCompact(); + ImGui::Spacing(); + ImGui::TreePop(); + } + + // Update item list if we changed the number of items + static ImVector items; + static ImVector selection; + static bool items_need_sort = false; + if (items.Size != items_count) + { + items.resize(items_count, MyItem()); + for (int n = 0; n < items_count; n++) + { + const int template_n = n % IM_ARRAYSIZE(template_items_names); + MyItem& item = items[n]; + item.ID = n; + item.Name = template_items_names[template_n]; + item.Quantity = (template_n == 3) ? 10 : (template_n == 4) ? 20 : 0; // Assign default quantities + } + } + + const ImDrawList* parent_draw_list = ImGui::GetWindowDrawList(); + const int parent_draw_list_draw_cmd_count = parent_draw_list->CmdBuffer.Size; + ImVec2 table_scroll_cur, table_scroll_max; // For debug display + const ImDrawList* table_draw_list = NULL; // " + + // Submit table + const float inner_width_to_use = (flags & ImGuiTableFlags_ScrollX) ? inner_width_with_scroll : 0.0f; + if (ImGui::BeginTable("table_advanced", 6, flags, outer_size_enabled ? outer_size_value : ImVec2(0, 0), inner_width_to_use)) + { + // Declare columns + // We use the "user_id" parameter of TableSetupColumn() to specify a user id that will be stored in the sort specifications. + // This is so our sort function can identify a column given our own identifier. We could also identify them based on their index! + ImGui::TableSetupColumn("ID", columns_base_flags | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHide, 0.0f, MyItemColumnID_ID); + ImGui::TableSetupColumn("Name", columns_base_flags | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Name); + ImGui::TableSetupColumn("Action", columns_base_flags | ImGuiTableColumnFlags_NoSort | ImGuiTableColumnFlags_WidthFixed, 0.0f, MyItemColumnID_Action); + ImGui::TableSetupColumn("Quantity", columns_base_flags | ImGuiTableColumnFlags_PreferSortDescending, 0.0f, MyItemColumnID_Quantity); + ImGui::TableSetupColumn("Description", columns_base_flags | ((flags & ImGuiTableFlags_NoHostExtendX) ? 0 : ImGuiTableColumnFlags_WidthStretch), 0.0f, MyItemColumnID_Description); + ImGui::TableSetupColumn("Hidden", columns_base_flags | ImGuiTableColumnFlags_DefaultHide | ImGuiTableColumnFlags_NoSort); + ImGui::TableSetupScrollFreeze(freeze_cols, freeze_rows); + + // Sort our data if sort specs have been changed! + ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs(); + if (sort_specs && sort_specs->SpecsDirty) + items_need_sort = true; + if (sort_specs && items_need_sort && items.Size > 1) + { + MyItem::SortWithSortSpecs(sort_specs, items.Data, items.Size); + sort_specs->SpecsDirty = false; + } + items_need_sort = false; + + // Take note of whether we are currently sorting based on the Quantity field, + // we will use this to trigger sorting when we know the data of this column has been modified. + const bool sorts_specs_using_quantity = (ImGui::TableGetColumnFlags(3) & ImGuiTableColumnFlags_IsSorted) != 0; + + // Show headers + if (show_headers && (columns_base_flags & ImGuiTableColumnFlags_AngledHeader) != 0) + ImGui::TableAngledHeadersRow(); + if (show_headers) + ImGui::TableHeadersRow(); + + // Show data + // FIXME-TABLE FIXME-NAV: How we can get decent up/down even though we have the buttons here? +#if 1 + // Demonstrate using clipper for large vertical lists + ImGuiListClipper clipper; + clipper.Begin(items.Size); + while (clipper.Step()) + { + for (int row_n = clipper.DisplayStart; row_n < clipper.DisplayEnd; row_n++) +#else + // Without clipper + { + for (int row_n = 0; row_n < items.Size; row_n++) +#endif + { + MyItem* item = &items[row_n]; + //if (!filter.PassFilter(item->Name)) + // continue; + + const bool item_is_selected = selection.contains(item->ID); + ImGui::PushID(item->ID); + ImGui::TableNextRow(ImGuiTableRowFlags_None, row_min_height); + + // For the demo purpose we can select among different type of items submitted in the first column + ImGui::TableSetColumnIndex(0); + char label[32]; + sprintf(label, "%04d", item->ID); + if (contents_type == CT_Text) + ImGui::TextUnformatted(label); + else if (contents_type == CT_Button) + ImGui::Button(label); + else if (contents_type == CT_SmallButton) + ImGui::SmallButton(label); + else if (contents_type == CT_FillButton) + ImGui::Button(label, ImVec2(-FLT_MIN, 0.0f)); + else if (contents_type == CT_Selectable || contents_type == CT_SelectableSpanRow) + { + ImGuiSelectableFlags selectable_flags = (contents_type == CT_SelectableSpanRow) ? ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap : ImGuiSelectableFlags_None; + if (ImGui::Selectable(label, item_is_selected, selectable_flags, ImVec2(0, row_min_height))) + { + if (ImGui::GetIO().KeyCtrl) + { + if (item_is_selected) + selection.find_erase_unsorted(item->ID); + else + selection.push_back(item->ID); + } + else + { + selection.clear(); + selection.push_back(item->ID); + } + } + } + + if (ImGui::TableSetColumnIndex(1)) + ImGui::TextUnformatted(item->Name); + + // Here we demonstrate marking our data set as needing to be sorted again if we modified a quantity, + // and we are currently sorting on the column showing the Quantity. + // To avoid triggering a sort while holding the button, we only trigger it when the button has been released. + // You will probably need some extra logic if you want to automatically sort when a specific entry changes. + if (ImGui::TableSetColumnIndex(2)) + { + if (ImGui::SmallButton("Chop")) { item->Quantity += 1; } + if (sorts_specs_using_quantity && ImGui::IsItemDeactivated()) { items_need_sort = true; } + ImGui::SameLine(); + if (ImGui::SmallButton("Eat")) { item->Quantity -= 1; } + if (sorts_specs_using_quantity && ImGui::IsItemDeactivated()) { items_need_sort = true; } + } + + if (ImGui::TableSetColumnIndex(3)) + ImGui::Text("%d", item->Quantity); + + ImGui::TableSetColumnIndex(4); + if (show_wrapped_text) + ImGui::TextWrapped("Lorem ipsum dolor sit amet"); + else + ImGui::Text("Lorem ipsum dolor sit amet"); + + if (ImGui::TableSetColumnIndex(5)) + ImGui::Text("1234"); + + ImGui::PopID(); + } + } + + // Store some info to display debug details below + table_scroll_cur = ImVec2(ImGui::GetScrollX(), ImGui::GetScrollY()); + table_scroll_max = ImVec2(ImGui::GetScrollMaxX(), ImGui::GetScrollMaxY()); + table_draw_list = ImGui::GetWindowDrawList(); + ImGui::EndTable(); + } + static bool show_debug_details = false; + ImGui::Checkbox("Debug details", &show_debug_details); + if (show_debug_details && table_draw_list) + { + ImGui::SameLine(0.0f, 0.0f); + const int table_draw_list_draw_cmd_count = table_draw_list->CmdBuffer.Size; + if (table_draw_list == parent_draw_list) + ImGui::Text(": DrawCmd: +%d (in same window)", + table_draw_list_draw_cmd_count - parent_draw_list_draw_cmd_count); + else + ImGui::Text(": DrawCmd: +%d (in child window), Scroll: (%.f/%.f) (%.f/%.f)", + table_draw_list_draw_cmd_count - 1, table_scroll_cur.x, table_scroll_max.x, table_scroll_cur.y, table_scroll_max.y); + } + ImGui::TreePop(); + } + + ImGui::PopID(); + + DemoWindowColumns(); + + if (disable_indent) + ImGui::PopStyleVar(); +} + +// Demonstrate old/legacy Columns API! +// [2020: Columns are under-featured and not maintained. Prefer using the more flexible and powerful BeginTable() API!] +static void DemoWindowColumns() +{ + IMGUI_DEMO_MARKER("Columns (legacy API)"); + bool open = ImGui::TreeNode("Legacy Columns API"); + ImGui::SameLine(); + HelpMarker("Columns() is an old API! Prefer using the more flexible and powerful BeginTable() API!"); + if (!open) + return; + // Basic columns + IMGUI_DEMO_MARKER("Columns (legacy API)/Basic"); if (ImGui::TreeNode("Basic")) { ImGui::Text("Without border:"); @@ -2520,7 +7501,7 @@ static void ShowDemoWindowColumns() char label[32]; sprintf(label, "Item %d", n); if (ImGui::Selectable(label)) {} - //if (ImGui::Button(label, ImVec2(-1,0))) {} + //if (ImGui::Button(label, ImVec2(-FLT_MIN,0.0f))) {} ImGui::NextColumn(); } ImGui::Columns(1); @@ -2554,24 +7535,35 @@ static void ShowDemoWindowColumns() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Columns (legacy API)/Borders"); if (ImGui::TreeNode("Borders")) { // NB: Future columns API should allow automatic horizontal borders. static bool h_borders = true; static bool v_borders = true; + static int columns_count = 4; + const int lines_count = 3; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragInt("##columns_count", &columns_count, 0.1f, 2, 10, "%d columns"); + if (columns_count < 2) + columns_count = 2; + ImGui::SameLine(); ImGui::Checkbox("horizontal", &h_borders); ImGui::SameLine(); ImGui::Checkbox("vertical", &v_borders); - ImGui::Columns(4, NULL, v_borders); - for (int i = 0; i < 4 * 3; i++) + ImGui::Columns(columns_count, NULL, v_borders); + for (int i = 0; i < columns_count * lines_count; i++) { if (h_borders && ImGui::GetColumnIndex() == 0) ImGui::Separator(); + ImGui::PushID(i); ImGui::Text("%c%c%c", 'a' + i, 'a' + i, 'a' + i); ImGui::Text("Width %.2f", ImGui::GetColumnWidth()); + ImGui::Text("Avail %.2f", ImGui::GetContentRegionAvail().x); ImGui::Text("Offset %.2f", ImGui::GetColumnOffset()); ImGui::Text("Long text that is likely to clip"); - ImGui::Button("Button", ImVec2(-1.0f, 0.0f)); + ImGui::Button("Button", ImVec2(-FLT_MIN, 0.0f)); + ImGui::PopID(); ImGui::NextColumn(); } ImGui::Columns(1); @@ -2581,6 +7573,7 @@ static void ShowDemoWindowColumns() } // Create multiple items in a same cell before switching to next column + IMGUI_DEMO_MARKER("Columns (legacy API)/Mixed items"); if (ImGui::TreeNode("Mixed items")) { ImGui::Columns(3, "mixed"); @@ -2612,6 +7605,7 @@ static void ShowDemoWindowColumns() } // Word wrapping + IMGUI_DEMO_MARKER("Columns (legacy API)/Word-wrapping"); if (ImGui::TreeNode("Word-wrapping")) { ImGui::Columns(2, "word-wrapping"); @@ -2626,39 +7620,18 @@ static void ShowDemoWindowColumns() ImGui::TreePop(); } - // Scrolling columns - /* - if (ImGui::TreeNode("Vertical Scrolling")) - { - ImGui::BeginChild("##header", ImVec2(0, ImGui::GetTextLineHeightWithSpacing()+ImGui::GetStyle().ItemSpacing.y)); - ImGui::Columns(3); - ImGui::Text("ID"); ImGui::NextColumn(); - ImGui::Text("Name"); ImGui::NextColumn(); - ImGui::Text("Path"); ImGui::NextColumn(); - ImGui::Columns(1); - ImGui::Separator(); - ImGui::EndChild(); - ImGui::BeginChild("##scrollingregion", ImVec2(0, 60)); - ImGui::Columns(3); - for (int i = 0; i < 10; i++) - { - ImGui::Text("%04d", i); ImGui::NextColumn(); - ImGui::Text("Foobar"); ImGui::NextColumn(); - ImGui::Text("/path/foobar/%04d/", i); ImGui::NextColumn(); - } - ImGui::Columns(1); - ImGui::EndChild(); - ImGui::TreePop(); - } - */ - + IMGUI_DEMO_MARKER("Columns (legacy API)/Horizontal Scrolling"); if (ImGui::TreeNode("Horizontal Scrolling")) { ImGui::SetNextWindowContentSize(ImVec2(1500.0f, 0.0f)); - ImGui::BeginChild("##ScrollingRegion", ImVec2(0, ImGui::GetFontSize() * 20), false, ImGuiWindowFlags_HorizontalScrollbar); + ImVec2 child_size = ImVec2(0, ImGui::GetFontSize() * 20.0f); + ImGui::BeginChild("##ScrollingRegion", child_size, ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar); ImGui::Columns(10); + + // Also demonstrate using clipper for large vertical lists int ITEMS_COUNT = 2000; - ImGuiListClipper clipper(ITEMS_COUNT); // Also demonstrate using the clipper for large list + ImGuiListClipper clipper; + clipper.Begin(ITEMS_COUNT); while (clipper.Step()) { for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) @@ -2673,6 +7646,7 @@ static void ShowDemoWindowColumns() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Columns (legacy API)/Tree"); if (ImGui::TreeNode("Tree")) { ImGui::Columns(2, "tree", true); @@ -2709,87 +7683,245 @@ static void ShowDemoWindowColumns() ImGui::TreePop(); } - if (disable_indent) - ImGui::PopStyleVar(); - ImGui::PopID(); + ImGui::TreePop(); } -static void ShowDemoWindowMisc() -{ - if (ImGui::CollapsingHeader("Filtering")) - { - static ImGuiTextFilter filter; - ImGui::Text("Filter usage:\n" - " \"\" display all lines\n" - " \"xxx\" display lines containing \"xxx\"\n" - " \"xxx,yyy\" display lines containing \"xxx\" or \"yyy\"\n" - " \"-xxx\" hide lines containing \"xxx\""); - filter.Draw(); - const char* lines[] = { "aaa1.c", "bbb1.c", "ccc1.c", "aaa2.cpp", "bbb2.cpp", "ccc2.cpp", "abc.h", "hello, world" }; - for (int i = 0; i < IM_ARRAYSIZE(lines); i++) - if (filter.PassFilter(lines[i])) - ImGui::BulletText("%s", lines[i]); - } +//----------------------------------------------------------------------------- +// [SECTION] DemoWindowInputs() +//----------------------------------------------------------------------------- - if (ImGui::CollapsingHeader("Inputs, Navigation & Focus")) +static void DemoWindowInputs() +{ + IMGUI_DEMO_MARKER("Inputs & Focus"); + if (ImGui::CollapsingHeader("Inputs & Focus")) { ImGuiIO& io = ImGui::GetIO(); - ImGui::Text("WantCaptureMouse: %d", io.WantCaptureMouse); - ImGui::Text("WantCaptureKeyboard: %d", io.WantCaptureKeyboard); - ImGui::Text("WantTextInput: %d", io.WantTextInput); - ImGui::Text("WantSetMousePos: %d", io.WantSetMousePos); - ImGui::Text("NavActive: %d, NavVisible: %d", io.NavActive, io.NavVisible); - - if (ImGui::TreeNode("Keyboard, Mouse & Navigation State")) + // Display inputs submitted to ImGuiIO + IMGUI_DEMO_MARKER("Inputs & Focus/Inputs"); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + bool inputs_opened = ImGui::TreeNode("Inputs"); + ImGui::SameLine(); + HelpMarker( + "This is a simplified view. See more detailed input state:\n" + "- in 'Tools->Metrics/Debugger->Inputs'.\n" + "- in 'Tools->Debug Log->IO'."); + if (inputs_opened) { if (ImGui::IsMousePosValid()) ImGui::Text("Mouse pos: (%g, %g)", io.MousePos.x, io.MousePos.y); else ImGui::Text("Mouse pos: "); ImGui::Text("Mouse delta: (%g, %g)", io.MouseDelta.x, io.MouseDelta.y); - ImGui::Text("Mouse down:"); for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) if (io.MouseDownDuration[i] >= 0.0f) { ImGui::SameLine(); ImGui::Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); } - ImGui::Text("Mouse clicked:"); for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) if (ImGui::IsMouseClicked(i)) { ImGui::SameLine(); ImGui::Text("b%d", i); } - ImGui::Text("Mouse dbl-clicked:"); for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) if (ImGui::IsMouseDoubleClicked(i)) { ImGui::SameLine(); ImGui::Text("b%d", i); } - ImGui::Text("Mouse released:"); for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) if (ImGui::IsMouseReleased(i)) { ImGui::SameLine(); ImGui::Text("b%d", i); } + ImGui::Text("Mouse down:"); + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) if (ImGui::IsMouseDown(i)) { ImGui::SameLine(); ImGui::Text("b%d (%.02f secs)", i, io.MouseDownDuration[i]); } ImGui::Text("Mouse wheel: %.1f", io.MouseWheel); + ImGui::Text("Mouse clicked count:"); + for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) if (io.MouseClickedCount[i] > 0) { ImGui::SameLine(); ImGui::Text("b%d: %d", i, io.MouseClickedCount[i]); } - ImGui::Text("Keys down:"); for (int i = 0; i < IM_ARRAYSIZE(io.KeysDown); i++) if (io.KeysDownDuration[i] >= 0.0f) { ImGui::SameLine(); ImGui::Text("%d (0x%X) (%.02f secs)", i, i, io.KeysDownDuration[i]); } - ImGui::Text("Keys pressed:"); for (int i = 0; i < IM_ARRAYSIZE(io.KeysDown); i++) if (ImGui::IsKeyPressed(i)) { ImGui::SameLine(); ImGui::Text("%d (0x%X)", i, i); } - ImGui::Text("Keys release:"); for (int i = 0; i < IM_ARRAYSIZE(io.KeysDown); i++) if (ImGui::IsKeyReleased(i)) { ImGui::SameLine(); ImGui::Text("%d (0x%X)", i, i); } + // We iterate both legacy native range and named ImGuiKey ranges. This is a little unusual/odd but this allows + // displaying the data for old/new backends. + // User code should never have to go through such hoops! + // You can generally iterate between ImGuiKey_NamedKey_BEGIN and ImGuiKey_NamedKey_END. + struct funcs { static bool IsLegacyNativeDupe(ImGuiKey) { return false; } }; + ImGuiKey start_key = ImGuiKey_NamedKey_BEGIN; + ImGui::Text("Keys down:"); for (ImGuiKey key = start_key; key < ImGuiKey_NamedKey_END; key = (ImGuiKey)(key + 1)) { if (funcs::IsLegacyNativeDupe(key) || !ImGui::IsKeyDown(key)) continue; ImGui::SameLine(); ImGui::Text((key < ImGuiKey_NamedKey_BEGIN) ? "\"%s\"" : "\"%s\" %d", ImGui::GetKeyName(key), key); } ImGui::Text("Keys mods: %s%s%s%s", io.KeyCtrl ? "CTRL " : "", io.KeyShift ? "SHIFT " : "", io.KeyAlt ? "ALT " : "", io.KeySuper ? "SUPER " : ""); - ImGui::Text("Chars queue:"); for (int i = 0; i < io.InputQueueCharacters.Size; i++) { ImWchar c = io.InputQueueCharacters[i]; ImGui::SameLine(); ImGui::Text("\'%c\' (0x%04X)", (c > ' ' && c <= 255) ? (char)c : '?', c); } // FIXME: We should convert 'c' to UTF-8 here but the functions are not public. - - ImGui::Text("NavInputs down:"); for (int i = 0; i < IM_ARRAYSIZE(io.NavInputs); i++) if (io.NavInputs[i] > 0.0f) { ImGui::SameLine(); ImGui::Text("[%d] %.2f", i, io.NavInputs[i]); } - ImGui::Text("NavInputs pressed:"); for (int i = 0; i < IM_ARRAYSIZE(io.NavInputs); i++) if (io.NavInputsDownDuration[i] == 0.0f) { ImGui::SameLine(); ImGui::Text("[%d]", i); } - ImGui::Text("NavInputs duration:"); for (int i = 0; i < IM_ARRAYSIZE(io.NavInputs); i++) if (io.NavInputsDownDuration[i] >= 0.0f) { ImGui::SameLine(); ImGui::Text("[%d] %.2f", i, io.NavInputsDownDuration[i]); } - - ImGui::Button("Hovering me sets the\nkeyboard capture flag"); - if (ImGui::IsItemHovered()) - ImGui::CaptureKeyboardFromApp(true); - ImGui::SameLine(); - ImGui::Button("Holding me clears the\nthe keyboard capture flag"); - if (ImGui::IsItemActive()) - ImGui::CaptureKeyboardFromApp(false); + ImGui::Text("Chars queue:"); for (int i = 0; i < io.InputQueueCharacters.Size; i++) { ImWchar c = io.InputQueueCharacters[i]; ImGui::SameLine(); ImGui::Text("\'%c\' (0x%04X)", (c > ' ' && c <= 255) ? (char)c : '?', c); } // FIXME: We should convert 'c' to UTF-8 here but the functions are not public. ImGui::TreePop(); } + // Display ImGuiIO output flags + IMGUI_DEMO_MARKER("Inputs & Focus/Outputs"); + ImGui::SetNextItemOpen(true, ImGuiCond_Once); + bool outputs_opened = ImGui::TreeNode("Outputs"); + ImGui::SameLine(); + HelpMarker( + "The value of io.WantCaptureMouse and io.WantCaptureKeyboard are normally set by Dear ImGui " + "to instruct your application of how to route inputs. Typically, when a value is true, it means " + "Dear ImGui wants the corresponding inputs and we expect the underlying application to ignore them.\n\n" + "The most typical case is: when hovering a window, Dear ImGui set io.WantCaptureMouse to true, " + "and underlying application should ignore mouse inputs (in practice there are many and more subtle " + "rules leading to how those flags are set)."); + if (outputs_opened) + { + ImGui::Text("io.WantCaptureMouse: %d", io.WantCaptureMouse); + ImGui::Text("io.WantCaptureMouseUnlessPopupClose: %d", io.WantCaptureMouseUnlessPopupClose); + ImGui::Text("io.WantCaptureKeyboard: %d", io.WantCaptureKeyboard); + ImGui::Text("io.WantTextInput: %d", io.WantTextInput); + ImGui::Text("io.WantSetMousePos: %d", io.WantSetMousePos); + ImGui::Text("io.NavActive: %d, io.NavVisible: %d", io.NavActive, io.NavVisible); + + IMGUI_DEMO_MARKER("Inputs & Focus/Outputs/WantCapture override"); + if (ImGui::TreeNode("WantCapture override")) + { + HelpMarker( + "Hovering the colored canvas will override io.WantCaptureXXX fields.\n" + "Notice how normally (when set to none), the value of io.WantCaptureKeyboard would be false when hovering " + "and true when clicking."); + static int capture_override_mouse = -1; + static int capture_override_keyboard = -1; + const char* capture_override_desc[] = { "None", "Set to false", "Set to true" }; + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 15); + ImGui::SliderInt("SetNextFrameWantCaptureMouse() on hover", &capture_override_mouse, -1, +1, capture_override_desc[capture_override_mouse + 1], ImGuiSliderFlags_AlwaysClamp); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 15); + ImGui::SliderInt("SetNextFrameWantCaptureKeyboard() on hover", &capture_override_keyboard, -1, +1, capture_override_desc[capture_override_keyboard + 1], ImGuiSliderFlags_AlwaysClamp); + + ImGui::ColorButton("##panel", ImVec4(0.7f, 0.1f, 0.7f, 1.0f), ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, ImVec2(128.0f, 96.0f)); // Dummy item + if (ImGui::IsItemHovered() && capture_override_mouse != -1) + ImGui::SetNextFrameWantCaptureMouse(capture_override_mouse == 1); + if (ImGui::IsItemHovered() && capture_override_keyboard != -1) + ImGui::SetNextFrameWantCaptureKeyboard(capture_override_keyboard == 1); + + ImGui::TreePop(); + } + ImGui::TreePop(); + } + + // Demonstrate using Shortcut() and Routing Policies. + // The general flow is: + // - Code interested in a chord (e.g. "Ctrl+A") declares their intent. + // - Multiple locations may be interested in same chord! Routing helps find a winner. + // - Every frame, we resolve all claims and assign one owner if the modifiers are matching. + // - The lower-level function is 'bool SetShortcutRouting()', returns true when caller got the route. + // - Most of the times, SetShortcutRouting() is not called directly. User mostly calls Shortcut() with routing flags. + // - If you call Shortcut() WITHOUT any routing option, it uses ImGuiInputFlags_RouteFocused. + // TL;DR: Most uses will simply be: + // - Shortcut(ImGuiMod_Ctrl | ImGuiKey_A); // Use ImGuiInputFlags_RouteFocused policy. + IMGUI_DEMO_MARKER("Inputs & Focus/Shortcuts"); + if (ImGui::TreeNode("Shortcuts")) + { + static ImGuiInputFlags route_options = ImGuiInputFlags_Repeat; + static ImGuiInputFlags route_type = ImGuiInputFlags_RouteFocused; + ImGui::CheckboxFlags("ImGuiInputFlags_Repeat", &route_options, ImGuiInputFlags_Repeat); + ImGui::RadioButton("ImGuiInputFlags_RouteActive", &route_type, ImGuiInputFlags_RouteActive); + ImGui::RadioButton("ImGuiInputFlags_RouteFocused (default)", &route_type, ImGuiInputFlags_RouteFocused); + ImGui::RadioButton("ImGuiInputFlags_RouteGlobal", &route_type, ImGuiInputFlags_RouteGlobal); + ImGui::Indent(); + ImGui::BeginDisabled(route_type != ImGuiInputFlags_RouteGlobal); + ImGui::CheckboxFlags("ImGuiInputFlags_RouteOverFocused", &route_options, ImGuiInputFlags_RouteOverFocused); + ImGui::CheckboxFlags("ImGuiInputFlags_RouteOverActive", &route_options, ImGuiInputFlags_RouteOverActive); + ImGui::CheckboxFlags("ImGuiInputFlags_RouteUnlessBgFocused", &route_options, ImGuiInputFlags_RouteUnlessBgFocused); + ImGui::EndDisabled(); + ImGui::Unindent(); + ImGui::RadioButton("ImGuiInputFlags_RouteAlways", &route_type, ImGuiInputFlags_RouteAlways); + ImGuiInputFlags flags = route_type | route_options; // Merged flags + if (route_type != ImGuiInputFlags_RouteGlobal) + flags &= ~(ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive | ImGuiInputFlags_RouteUnlessBgFocused); + + ImGui::SeparatorText("Using SetNextItemShortcut()"); + ImGui::Text("Ctrl+S"); + ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_S, flags | ImGuiInputFlags_Tooltip); + ImGui::Button("Save"); + ImGui::Text("Alt+F"); + ImGui::SetNextItemShortcut(ImGuiMod_Alt | ImGuiKey_F, flags | ImGuiInputFlags_Tooltip); + static float f = 0.5f; + ImGui::SliderFloat("Factor", &f, 0.0f, 1.0f); + + ImGui::SeparatorText("Using Shortcut()"); + const float line_height = ImGui::GetTextLineHeightWithSpacing(); + const ImGuiKeyChord key_chord = ImGuiMod_Ctrl | ImGuiKey_A; + + ImGui::Text("Ctrl+A"); + ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags) ? "PRESSED" : "..."); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(1.0f, 0.0f, 1.0f, 0.1f)); + + ImGui::BeginChild("WindowA", ImVec2(-FLT_MIN, line_height * 14), true); + ImGui::Text("Press CTRL+A and see who receives it!"); + ImGui::Separator(); + + // 1: Window polling for CTRL+A + ImGui::Text("(in WindowA)"); + ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags) ? "PRESSED" : "..."); + + // 2: InputText also polling for CTRL+A: it always uses _RouteFocused internally (gets priority when active) + // (Commmented because the owner-aware version of Shortcut() is still in imgui_internal.h) + //char str[16] = "Press CTRL+A"; + //ImGui::Spacing(); + //ImGui::InputText("InputTextB", str, IM_ARRAYSIZE(str), ImGuiInputTextFlags_ReadOnly); + //ImGuiID item_id = ImGui::GetItemID(); + //ImGui::SameLine(); HelpMarker("Internal widgets always use _RouteFocused"); + //ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags, item_id) ? "PRESSED" : "..."); + + // 3: Dummy child is not claiming the route: focusing them shouldn't steal route away from WindowA + ImGui::BeginChild("ChildD", ImVec2(-FLT_MIN, line_height * 4), true); + ImGui::Text("(in ChildD: not using same Shortcut)"); + ImGui::Text("IsWindowFocused: %d", ImGui::IsWindowFocused()); + ImGui::EndChild(); + + // 4: Child window polling for CTRL+A. It is deeper than WindowA and gets priority when focused. + ImGui::BeginChild("ChildE", ImVec2(-FLT_MIN, line_height * 4), true); + ImGui::Text("(in ChildE: using same Shortcut)"); + ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags) ? "PRESSED" : "..."); + ImGui::EndChild(); + + // 5: In a popup + if (ImGui::Button("Open Popup")) + ImGui::OpenPopup("PopupF"); + if (ImGui::BeginPopup("PopupF")) + { + ImGui::Text("(in PopupF)"); + ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags) ? "PRESSED" : "..."); + // (Commmented because the owner-aware version of Shortcut() is still in imgui_internal.h) + //ImGui::InputText("InputTextG", str, IM_ARRAYSIZE(str), ImGuiInputTextFlags_ReadOnly); + //ImGui::Text("IsWindowFocused: %d, Shortcut: %s", ImGui::IsWindowFocused(), ImGui::Shortcut(key_chord, flags, ImGui::GetItemID()) ? "PRESSED" : "..."); + ImGui::EndPopup(); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + + ImGui::TreePop(); + } + + // Display mouse cursors + IMGUI_DEMO_MARKER("Inputs & Focus/Mouse Cursors"); + if (ImGui::TreeNode("Mouse Cursors")) + { + const char* mouse_cursors_names[] = { "Arrow", "TextInput", "ResizeAll", "ResizeNS", "ResizeEW", "ResizeNESW", "ResizeNWSE", "Hand", "Wait", "Progress", "NotAllowed" }; + IM_ASSERT(IM_ARRAYSIZE(mouse_cursors_names) == ImGuiMouseCursor_COUNT); + + ImGuiMouseCursor current = ImGui::GetMouseCursor(); + const char* cursor_name = (current >= ImGuiMouseCursor_Arrow) && (current < ImGuiMouseCursor_COUNT) ? mouse_cursors_names[current] : "N/A"; + ImGui::Text("Current mouse cursor = %d: %s", current, cursor_name); + ImGui::BeginDisabled(true); + ImGui::CheckboxFlags("io.BackendFlags: HasMouseCursors", &io.BackendFlags, ImGuiBackendFlags_HasMouseCursors); + ImGui::EndDisabled(); + + ImGui::Text("Hover to see mouse cursors:"); + ImGui::SameLine(); HelpMarker( + "Your application can render a different mouse cursor based on what ImGui::GetMouseCursor() returns. " + "If software cursor rendering (io.MouseDrawCursor) is set ImGui will draw the right cursor for you, " + "otherwise your backend needs to handle it."); + for (int i = 0; i < ImGuiMouseCursor_COUNT; i++) + { + char label[32]; + sprintf(label, "Mouse cursor %d: %s", i, mouse_cursors_names[i]); + ImGui::Bullet(); ImGui::Selectable(label, false); + if (ImGui::IsItemHovered()) + ImGui::SetMouseCursor(i); + } + ImGui::TreePop(); + } + + IMGUI_DEMO_MARKER("Inputs & Focus/Tabbing"); if (ImGui::TreeNode("Tabbing")) { ImGui::Text("Use TAB/SHIFT+TAB to cycle through keyboard editable fields."); - static char buf[32] = "dummy"; + static char buf[32] = "hello"; ImGui::InputText("1", buf, IM_ARRAYSIZE(buf)); ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); ImGui::InputText("3", buf, IM_ARRAYSIZE(buf)); - ImGui::PushAllowKeyboardFocus(false); + ImGui::PushItemFlag(ImGuiItemFlags_NoTabStop, true); ImGui::InputText("4 (tab skip)", buf, IM_ARRAYSIZE(buf)); - //ImGui::SameLine(); HelpMarker("Use ImGui::PushAllowKeyboardFocus(bool)\nto disable tabbing through certain widgets."); - ImGui::PopAllowKeyboardFocus(); + ImGui::SameLine(); HelpMarker("Item won't be cycled through when using TAB or Shift+Tab."); + ImGui::PopItemFlag(); ImGui::InputText("5", buf, IM_ARRAYSIZE(buf)); ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Inputs & Focus/Focus from code"); if (ImGui::TreeNode("Focus from code")) { bool focus_1 = ImGui::Button("Focus on 1"); ImGui::SameLine(); @@ -2806,11 +7938,12 @@ static void ShowDemoWindowMisc() ImGui::InputText("2", buf, IM_ARRAYSIZE(buf)); if (ImGui::IsItemActive()) has_focus = 2; - ImGui::PushAllowKeyboardFocus(false); + ImGui::PushItemFlag(ImGuiItemFlags_NoTabStop, true); if (focus_3) ImGui::SetKeyboardFocusHere(); ImGui::InputText("3 (tab skip)", buf, IM_ARRAYSIZE(buf)); if (ImGui::IsItemActive()) has_focus = 3; - ImGui::PopAllowKeyboardFocus(); + ImGui::SameLine(); HelpMarker("Item won't be cycled through when using TAB or Shift+Tab."); + ImGui::PopItemFlag(); if (has_focus) ImGui::Text("Item with focus: %d", has_focus); @@ -2830,42 +7963,32 @@ static void ShowDemoWindowMisc() ImGui::TreePop(); } + IMGUI_DEMO_MARKER("Inputs & Focus/Dragging"); if (ImGui::TreeNode("Dragging")) { ImGui::TextWrapped("You can use ImGui::GetMouseDragDelta(0) to query for the dragged amount on any widget."); for (int button = 0; button < 3; button++) - ImGui::Text("IsMouseDragging(%d):\n w/ default threshold: %d,\n w/ zero threshold: %d\n w/ large threshold: %d", - button, ImGui::IsMouseDragging(button), ImGui::IsMouseDragging(button, 0.0f), ImGui::IsMouseDragging(button, 20.0f)); + { + ImGui::Text("IsMouseDragging(%d):", button); + ImGui::Text(" w/ default threshold: %d,", ImGui::IsMouseDragging(button)); + ImGui::Text(" w/ zero threshold: %d,", ImGui::IsMouseDragging(button, 0.0f)); + ImGui::Text(" w/ large threshold: %d,", ImGui::IsMouseDragging(button, 20.0f)); + } ImGui::Button("Drag Me"); if (ImGui::IsItemActive()) ImGui::GetForegroundDrawList()->AddLine(io.MouseClickedPos[0], io.MousePos, ImGui::GetColorU32(ImGuiCol_Button), 4.0f); // Draw a line between the button and the mouse cursor - // Drag operations gets "unlocked" when the mouse has moved past a certain threshold (the default threshold is stored in io.MouseDragThreshold) - // You can request a lower or higher threshold using the second parameter of IsMouseDragging() and GetMouseDragDelta() + // Drag operations gets "unlocked" when the mouse has moved past a certain threshold + // (the default threshold is stored in io.MouseDragThreshold). You can request a lower or higher + // threshold using the second parameter of IsMouseDragging() and GetMouseDragDelta(). ImVec2 value_raw = ImGui::GetMouseDragDelta(0, 0.0f); ImVec2 value_with_lock_threshold = ImGui::GetMouseDragDelta(0); ImVec2 mouse_delta = io.MouseDelta; - ImGui::Text("GetMouseDragDelta(0):\n w/ default threshold: (%.1f, %.1f),\n w/ zero threshold: (%.1f, %.1f)\nMouseDelta: (%.1f, %.1f)", value_with_lock_threshold.x, value_with_lock_threshold.y, value_raw.x, value_raw.y, mouse_delta.x, mouse_delta.y); - ImGui::TreePop(); - } - - if (ImGui::TreeNode("Mouse cursors")) - { - const char* mouse_cursors_names[] = { "Arrow", "TextInput", "Move", "ResizeNS", "ResizeEW", "ResizeNESW", "ResizeNWSE", "Hand" }; - IM_ASSERT(IM_ARRAYSIZE(mouse_cursors_names) == ImGuiMouseCursor_COUNT); - - ImGui::Text("Current mouse cursor = %d: %s", ImGui::GetMouseCursor(), mouse_cursors_names[ImGui::GetMouseCursor()]); - ImGui::Text("Hover to see mouse cursors:"); - ImGui::SameLine(); HelpMarker("Your application can render a different mouse cursor based on what ImGui::GetMouseCursor() returns. If software cursor rendering (io.MouseDrawCursor) is set ImGui will draw the right cursor for you, otherwise your backend needs to handle it."); - for (int i = 0; i < ImGuiMouseCursor_COUNT; i++) - { - char label[32]; - sprintf(label, "Mouse cursor %d: %s", i, mouse_cursors_names[i]); - ImGui::Bullet(); ImGui::Selectable(label, false); - if (ImGui::IsItemHovered() || ImGui::IsItemFocused()) - ImGui::SetMouseCursor(i); - } + ImGui::Text("GetMouseDragDelta(0):"); + ImGui::Text(" w/ default threshold: (%.1f, %.1f)", value_with_lock_threshold.x, value_with_lock_threshold.y); + ImGui::Text(" w/ zero threshold: (%.1f, %.1f)", value_raw.x, value_raw.y); + ImGui::Text("io.MouseDelta: (%.1f, %.1f)", mouse_delta.x, mouse_delta.y); ImGui::TreePop(); } } @@ -2873,7 +7996,7 @@ static void ShowDemoWindowMisc() //----------------------------------------------------------------------------- // [SECTION] About Window / ShowAboutWindow() -// Access from Dear ImGui Demo -> Help -> About +// Access from Dear ImGui Demo -> Tools -> About //----------------------------------------------------------------------------- void ImGui::ShowAboutWindow(bool* p_open) @@ -2883,10 +8006,24 @@ void ImGui::ShowAboutWindow(bool* p_open) ImGui::End(); return; } - ImGui::Text("Dear ImGui %s", ImGui::GetVersion()); + IMGUI_DEMO_MARKER("Tools/About Dear ImGui"); + ImGui::Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); + + ImGui::TextLinkOpenURL("Homepage", "https://github.com/ocornut/imgui"); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("FAQ", "https://github.com/ocornut/imgui/blob/master/docs/FAQ.md"); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Wiki", "https://github.com/ocornut/imgui/wiki"); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Releases", "https://github.com/ocornut/imgui/releases"); + ImGui::SameLine(); + ImGui::TextLinkOpenURL("Funding", "https://github.com/ocornut/imgui/wiki/Funding"); + ImGui::Separator(); - ImGui::Text("By Omar Cornut and all dear imgui contributors."); + ImGui::Text("(c) 2014-2025 Omar Cornut"); + ImGui::Text("Developed by Omar Cornut and all Dear ImGui contributors."); ImGui::Text("Dear ImGui is licensed under the MIT License, see LICENSE for more information."); + ImGui::Text("If your company uses this, please consider funding the project."); static bool show_config_info = false; ImGui::Checkbox("Config/Build Information", &show_config_info); @@ -2896,9 +8033,13 @@ void ImGui::ShowAboutWindow(bool* p_open) ImGuiStyle& style = ImGui::GetStyle(); bool copy_to_clipboard = ImGui::Button("Copy to clipboard"); - ImGui::BeginChildFrame(ImGui::GetID("cfginfos"), ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 18), ImGuiWindowFlags_NoMove); + ImVec2 child_size = ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 18); + ImGui::BeginChild(ImGui::GetID("cfg_infos"), child_size, ImGuiChildFlags_FrameStyle); if (copy_to_clipboard) + { ImGui::LogToClipboard(); + ImGui::LogText("```\n"); // Back quotes will make text appears without formatting when pasting on GitHub + } ImGui::Text("Dear ImGui %s (%d)", IMGUI_VERSION, IMGUI_VERSION_NUM); ImGui::Separator(); @@ -2916,11 +8057,20 @@ void ImGui::ShowAboutWindow(bool* p_open) #ifdef IMGUI_DISABLE_WIN32_FUNCTIONS ImGui::Text("define: IMGUI_DISABLE_WIN32_FUNCTIONS"); #endif -#ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS - ImGui::Text("define: IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS"); +#ifdef IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS"); #endif -#ifdef IMGUI_DISABLE_MATH_FUNCTIONS - ImGui::Text("define: IMGUI_DISABLE_MATH_FUNCTIONS"); +#ifdef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS"); +#endif +#ifdef IMGUI_DISABLE_FILE_FUNCTIONS + ImGui::Text("define: IMGUI_DISABLE_FILE_FUNCTIONS"); #endif #ifdef IMGUI_DISABLE_DEFAULT_ALLOCATORS ImGui::Text("define: IMGUI_DISABLE_DEFAULT_ALLOCATORS"); @@ -2943,6 +8093,9 @@ void ImGui::ShowAboutWindow(bool* p_open) #ifdef _MSC_VER ImGui::Text("define: _MSC_VER=%d", _MSC_VER); #endif +#ifdef _MSVC_LANG + ImGui::Text("define: _MSVC_LANG=%d", (int)_MSVC_LANG); +#endif #ifdef __MINGW32__ ImGui::Text("define: __MINGW32__"); #endif @@ -2954,6 +8107,16 @@ void ImGui::ShowAboutWindow(bool* p_open) #endif #ifdef __clang_version__ ImGui::Text("define: __clang_version__=%s", __clang_version__); +#endif +#ifdef __EMSCRIPTEN__ + ImGui::Text("define: __EMSCRIPTEN__"); + ImGui::Text("Emscripten: %d.%d.%d", __EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__); +#endif +#ifdef IMGUI_HAS_VIEWPORT + ImGui::Text("define: IMGUI_HAS_VIEWPORT"); +#endif +#ifdef IMGUI_HAS_DOCK + ImGui::Text("define: IMGUI_HAS_DOCK"); #endif ImGui::Separator(); ImGui::Text("io.BackendPlatformName: %s", io.BackendPlatformName ? io.BackendPlatformName : "NULL"); @@ -2961,20 +8124,37 @@ void ImGui::ShowAboutWindow(bool* p_open) ImGui::Text("io.ConfigFlags: 0x%08X", io.ConfigFlags); if (io.ConfigFlags & ImGuiConfigFlags_NavEnableKeyboard) ImGui::Text(" NavEnableKeyboard"); if (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) ImGui::Text(" NavEnableGamepad"); - if (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) ImGui::Text(" NavEnableSetMousePos"); - if (io.ConfigFlags & ImGuiConfigFlags_NavNoCaptureKeyboard) ImGui::Text(" NavNoCaptureKeyboard"); if (io.ConfigFlags & ImGuiConfigFlags_NoMouse) ImGui::Text(" NoMouse"); if (io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) ImGui::Text(" NoMouseCursorChange"); + if (io.ConfigFlags & ImGuiConfigFlags_NoKeyboard) ImGui::Text(" NoKeyboard"); + if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) ImGui::Text(" DockingEnable"); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) ImGui::Text(" ViewportsEnable"); + if (io.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleViewports) ImGui::Text(" DpiEnableScaleViewports"); + if (io.ConfigFlags & ImGuiConfigFlags_DpiEnableScaleFonts) ImGui::Text(" DpiEnableScaleFonts"); if (io.MouseDrawCursor) ImGui::Text("io.MouseDrawCursor"); + if (io.ConfigViewportsNoAutoMerge) ImGui::Text("io.ConfigViewportsNoAutoMerge"); + if (io.ConfigViewportsNoTaskBarIcon) ImGui::Text("io.ConfigViewportsNoTaskBarIcon"); + if (io.ConfigViewportsNoDecoration) ImGui::Text("io.ConfigViewportsNoDecoration"); + if (io.ConfigViewportsNoDefaultParent) ImGui::Text("io.ConfigViewportsNoDefaultParent"); + if (io.ConfigDockingNoSplit) ImGui::Text("io.ConfigDockingNoSplit"); + if (io.ConfigDockingWithShift) ImGui::Text("io.ConfigDockingWithShift"); + if (io.ConfigDockingAlwaysTabBar) ImGui::Text("io.ConfigDockingAlwaysTabBar"); + if (io.ConfigDockingTransparentPayload) ImGui::Text("io.ConfigDockingTransparentPayload"); if (io.ConfigMacOSXBehaviors) ImGui::Text("io.ConfigMacOSXBehaviors"); + if (io.ConfigNavMoveSetMousePos) ImGui::Text("io.ConfigNavMoveSetMousePos"); + if (io.ConfigNavCaptureKeyboard) ImGui::Text("io.ConfigNavCaptureKeyboard"); if (io.ConfigInputTextCursorBlink) ImGui::Text("io.ConfigInputTextCursorBlink"); if (io.ConfigWindowsResizeFromEdges) ImGui::Text("io.ConfigWindowsResizeFromEdges"); if (io.ConfigWindowsMoveFromTitleBarOnly) ImGui::Text("io.ConfigWindowsMoveFromTitleBarOnly"); + if (io.ConfigMemoryCompactTimer >= 0.0f) ImGui::Text("io.ConfigMemoryCompactTimer = %.1f", io.ConfigMemoryCompactTimer); ImGui::Text("io.BackendFlags: 0x%08X", io.BackendFlags); if (io.BackendFlags & ImGuiBackendFlags_HasGamepad) ImGui::Text(" HasGamepad"); if (io.BackendFlags & ImGuiBackendFlags_HasMouseCursors) ImGui::Text(" HasMouseCursors"); if (io.BackendFlags & ImGuiBackendFlags_HasSetMousePos) ImGui::Text(" HasSetMousePos"); + if (io.BackendFlags & ImGuiBackendFlags_PlatformHasViewports) ImGui::Text(" PlatformHasViewports"); + if (io.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport)ImGui::Text(" HasMouseHoveredViewport"); if (io.BackendFlags & ImGuiBackendFlags_RendererHasVtxOffset) ImGui::Text(" RendererHasVtxOffset"); + if (io.BackendFlags & ImGuiBackendFlags_RendererHasViewports) ImGui::Text(" RendererHasViewports"); ImGui::Separator(); ImGui::Text("io.Fonts: %d fonts, Flags: 0x%08X, TexSize: %d,%d", io.Fonts->Fonts.Size, io.Fonts->Flags, io.Fonts->TexWidth, io.Fonts->TexHeight); ImGui::Text("io.DisplaySize: %.2f,%.2f", io.DisplaySize.x, io.DisplaySize.y); @@ -2989,8 +8169,11 @@ void ImGui::ShowAboutWindow(bool* p_open) ImGui::Text("style.ItemInnerSpacing: %.2f,%.2f", style.ItemInnerSpacing.x, style.ItemInnerSpacing.y); if (copy_to_clipboard) + { + ImGui::LogText("\n```\n"); ImGui::LogFinish(); - ImGui::EndChildFrame(); + } + ImGui::EndChild(); } ImGui::End(); } @@ -2998,39 +8181,29 @@ void ImGui::ShowAboutWindow(bool* p_open) //----------------------------------------------------------------------------- // [SECTION] Style Editor / ShowStyleEditor() //----------------------------------------------------------------------------- +// - ShowFontSelector() +// - ShowStyleSelector() +// - ShowStyleEditor() +//----------------------------------------------------------------------------- -// Demo helper function to select among default colors. See ShowStyleEditor() for more advanced options. -// Here we use the simplified Combo() api that packs items into a single literal string. Useful for quick combo boxes where the choices are known locally. -bool ImGui::ShowStyleSelector(const char* label) -{ - static int style_idx = -1; - if (ImGui::Combo(label, &style_idx, "Classic\0Dark\0Light\0")) - { - switch (style_idx) - { - case 0: ImGui::StyleColorsClassic(); break; - case 1: ImGui::StyleColorsDark(); break; - case 2: ImGui::StyleColorsLight(); break; - } - return true; - } - return false; -} +// Forward declare ShowFontAtlas() which isn't worth putting in public API yet +namespace ImGui { IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); } // Demo helper function to select among loaded fonts. -// Here we use the regular BeginCombo()/EndCombo() api which is more the more flexible one. +// Here we use the regular BeginCombo()/EndCombo() api which is the more flexible one. void ImGui::ShowFontSelector(const char* label) { ImGuiIO& io = ImGui::GetIO(); ImFont* font_current = ImGui::GetFont(); if (ImGui::BeginCombo(label, font_current->GetDebugName())) { - for (int n = 0; n < io.Fonts->Fonts.Size; n++) + for (ImFont* font : io.Fonts->Fonts) { - ImFont* font = io.Fonts->Fonts[n]; ImGui::PushID((void*)font); if (ImGui::Selectable(font->GetDebugName(), font == font_current)) io.FontDefault = font; + if (font == font_current) + ImGui::SetItemDefaultFocus(); ImGui::PopID(); } ImGui::EndCombo(); @@ -3039,13 +8212,34 @@ void ImGui::ShowFontSelector(const char* label) HelpMarker( "- Load additional fonts with io.Fonts->AddFontFromFileTTF().\n" "- The font atlas is built when calling io.Fonts->GetTexDataAsXXXX() or io.Fonts->Build().\n" - "- Read FAQ and documentation in misc/fonts/ for more details.\n" + "- Read FAQ and docs/FONTS.md for more details.\n" "- If you need to add/remove fonts at runtime (e.g. for DPI change), do it before calling NewFrame()."); } +// Demo helper function to select among default colors. See ShowStyleEditor() for more advanced options. +// Here we use the simplified Combo() api that packs items into a single literal string. +// Useful for quick combo boxes where the choices are known locally. +bool ImGui::ShowStyleSelector(const char* label) +{ + static int style_idx = -1; + if (ImGui::Combo(label, &style_idx, "Dark\0Light\0Classic\0")) + { + switch (style_idx) + { + case 0: ImGui::StyleColorsDark(); break; + case 1: ImGui::StyleColorsLight(); break; + case 2: ImGui::StyleColorsClassic(); break; + } + return true; + } + return false; +} + void ImGui::ShowStyleEditor(ImGuiStyle* ref) { - // You can pass in a reference ImGuiStyle structure to compare to, revert to and save to (else it compares to an internally stored reference) + IMGUI_DEMO_MARKER("Tools/Style Editor"); + // You can pass in a reference ImGuiStyle structure to compare to, revert to and save to + // (without a reference style pointer, we will use one compared locally as a reference) ImGuiStyle& style = ImGui::GetStyle(); static ImGuiStyle ref_saved_style; @@ -3063,14 +8257,14 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ref_saved_style = style; ImGui::ShowFontSelector("Fonts##Selector"); - // Simplified Settings + // Simplified Settings (expose floating-pointer border sizes as boolean representing 0.0f or 1.0f) if (ImGui::SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f")) style.GrabRounding = style.FrameRounding; // Make GrabRounding always the same value as FrameRounding - { bool window_border = (style.WindowBorderSize > 0.0f); if (ImGui::Checkbox("WindowBorder", &window_border)) style.WindowBorderSize = window_border ? 1.0f : 0.0f; } + { bool border = (style.WindowBorderSize > 0.0f); if (ImGui::Checkbox("WindowBorder", &border)) { style.WindowBorderSize = border ? 1.0f : 0.0f; } } ImGui::SameLine(); - { bool frame_border = (style.FrameBorderSize > 0.0f); if (ImGui::Checkbox("FrameBorder", &frame_border)) style.FrameBorderSize = frame_border ? 1.0f : 0.0f; } + { bool border = (style.FrameBorderSize > 0.0f); if (ImGui::Checkbox("FrameBorder", &border)) { style.FrameBorderSize = border ? 1.0f : 0.0f; } } ImGui::SameLine(); - { bool popup_border = (style.PopupBorderSize > 0.0f); if (ImGui::Checkbox("PopupBorder", &popup_border)) style.PopupBorderSize = popup_border ? 1.0f : 0.0f; } + { bool border = (style.PopupBorderSize > 0.0f); if (ImGui::Checkbox("PopupBorder", &border)) { style.PopupBorderSize = border ? 1.0f : 0.0f; } } // Save/Revert button if (ImGui::Button("Save Ref")) @@ -3079,7 +8273,9 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) if (ImGui::Button("Revert Ref")) style = *ref; ImGui::SameLine(); - HelpMarker("Save/Revert in local non-persistent storage. Default Colors definition are not affected. Use \"Export Colors\" below to save them somewhere."); + HelpMarker( + "Save/Revert in local non-persistent storage. Default Colors definition are not affected. " + "Use \"Export\" below to save them somewhere."); ImGui::Separator(); @@ -3087,7 +8283,7 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) { if (ImGui::BeginTabItem("Sizes")) { - ImGui::Text("Main"); + ImGui::SeparatorText("Main"); ImGui::SliderFloat2("WindowPadding", (float*)&style.WindowPadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("FramePadding", (float*)&style.FramePadding, 0.0f, 20.0f, "%.0f"); ImGui::SliderFloat2("ItemSpacing", (float*)&style.ItemSpacing, 0.0f, 20.0f, "%.0f"); @@ -3096,27 +8292,74 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::SliderFloat("IndentSpacing", &style.IndentSpacing, 0.0f, 30.0f, "%.0f"); ImGui::SliderFloat("ScrollbarSize", &style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); ImGui::SliderFloat("GrabMinSize", &style.GrabMinSize, 1.0f, 20.0f, "%.0f"); - ImGui::Text("Borders"); + + ImGui::SeparatorText("Borders"); ImGui::SliderFloat("WindowBorderSize", &style.WindowBorderSize, 0.0f, 1.0f, "%.0f"); ImGui::SliderFloat("ChildBorderSize", &style.ChildBorderSize, 0.0f, 1.0f, "%.0f"); ImGui::SliderFloat("PopupBorderSize", &style.PopupBorderSize, 0.0f, 1.0f, "%.0f"); ImGui::SliderFloat("FrameBorderSize", &style.FrameBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui::SliderFloat("TabBorderSize", &style.TabBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui::Text("Rounding"); + + ImGui::SeparatorText("Rounding"); ImGui::SliderFloat("WindowRounding", &style.WindowRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("ChildRounding", &style.ChildRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("FrameRounding", &style.FrameRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("PopupRounding", &style.PopupRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("ScrollbarRounding", &style.ScrollbarRounding, 0.0f, 12.0f, "%.0f"); ImGui::SliderFloat("GrabRounding", &style.GrabRounding, 0.0f, 12.0f, "%.0f"); + + ImGui::SeparatorText("Tabs"); + ImGui::SliderFloat("TabBorderSize", &style.TabBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui::SliderFloat("TabBarBorderSize", &style.TabBarBorderSize, 0.0f, 2.0f, "%.0f"); + ImGui::SliderFloat("TabBarOverlineSize", &style.TabBarOverlineSize, 0.0f, 3.0f, "%.0f"); + ImGui::SameLine(); HelpMarker("Overline is only drawn over the selected tab when ImGuiTabBarFlags_DrawSelectedOverline is set."); + ImGui::DragFloat("TabCloseButtonMinWidthSelected", &style.TabCloseButtonMinWidthSelected, 0.1f, -1.0f, 100.0f, (style.TabCloseButtonMinWidthSelected < 0.0f) ? "%.0f (Always)" : "%.0f"); + ImGui::DragFloat("TabCloseButtonMinWidthUnselected", &style.TabCloseButtonMinWidthUnselected, 0.1f, -1.0f, 100.0f, (style.TabCloseButtonMinWidthUnselected < 0.0f) ? "%.0f (Always)" : "%.0f"); ImGui::SliderFloat("TabRounding", &style.TabRounding, 0.0f, 12.0f, "%.0f"); - ImGui::Text("Alignment"); + + ImGui::SeparatorText("Tables"); + ImGui::SliderFloat2("CellPadding", (float*)&style.CellPadding, 0.0f, 20.0f, "%.0f"); + ImGui::SliderAngle("TableAngledHeadersAngle", &style.TableAngledHeadersAngle, -50.0f, +50.0f); + ImGui::SliderFloat2("TableAngledHeadersTextAlign", (float*)&style.TableAngledHeadersTextAlign, 0.0f, 1.0f, "%.2f"); + + ImGui::SeparatorText("Windows"); ImGui::SliderFloat2("WindowTitleAlign", (float*)&style.WindowTitleAlign, 0.0f, 1.0f, "%.2f"); - ImGui::Combo("WindowMenuButtonPosition", (int*)&style.WindowMenuButtonPosition, "Left\0Right\0"); - ImGui::SliderFloat2("ButtonTextAlign", (float*)&style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); ImGui::SameLine(); HelpMarker("Alignment applies when a button is larger than its text content."); - ImGui::SliderFloat2("SelectableTextAlign", (float*)&style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); ImGui::SameLine(); HelpMarker("Alignment applies when a selectable is larger than its text content."); - ImGui::Text("Safe Area Padding"); ImGui::SameLine(); HelpMarker("Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured)."); - ImGui::SliderFloat2("DisplaySafeAreaPadding", (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); + ImGui::SliderFloat("WindowBorderHoverPadding", &style.WindowBorderHoverPadding, 1.0f, 20.0f, "%.0f"); + int window_menu_button_position = style.WindowMenuButtonPosition + 1; + if (ImGui::Combo("WindowMenuButtonPosition", (int*)&window_menu_button_position, "None\0Left\0Right\0")) + style.WindowMenuButtonPosition = (ImGuiDir)(window_menu_button_position - 1); + + ImGui::SeparatorText("Widgets"); + ImGui::Combo("ColorButtonPosition", (int*)&style.ColorButtonPosition, "Left\0Right\0"); + ImGui::SliderFloat2("ButtonTextAlign", (float*)&style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::SameLine(); HelpMarker("Alignment applies when a button is larger than its text content."); + ImGui::SliderFloat2("SelectableTextAlign", (float*)&style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::SameLine(); HelpMarker("Alignment applies when a selectable is larger than its text content."); + ImGui::SliderFloat("SeparatorTextBorderSize", &style.SeparatorTextBorderSize, 0.0f, 10.0f, "%.0f"); + ImGui::SliderFloat2("SeparatorTextAlign", (float*)&style.SeparatorTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui::SliderFloat2("SeparatorTextPadding", (float*)&style.SeparatorTextPadding, 0.0f, 40.0f, "%.0f"); + ImGui::SliderFloat("LogSliderDeadzone", &style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); + ImGui::SliderFloat("ImageBorderSize", &style.ImageBorderSize, 0.0f, 1.0f, "%.0f"); + + ImGui::SeparatorText("Docking"); + ImGui::SliderFloat("DockingSplitterSize", &style.DockingSeparatorSize, 0.0f, 12.0f, "%.0f"); + + ImGui::SeparatorText("Tooltips"); + for (int n = 0; n < 2; n++) + if (ImGui::TreeNodeEx(n == 0 ? "HoverFlagsForTooltipMouse" : "HoverFlagsForTooltipNav")) + { + ImGuiHoveredFlags* p = (n == 0) ? &style.HoverFlagsForTooltipMouse : &style.HoverFlagsForTooltipNav; + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayNone", p, ImGuiHoveredFlags_DelayNone); + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayShort", p, ImGuiHoveredFlags_DelayShort); + ImGui::CheckboxFlags("ImGuiHoveredFlags_DelayNormal", p, ImGuiHoveredFlags_DelayNormal); + ImGui::CheckboxFlags("ImGuiHoveredFlags_Stationary", p, ImGuiHoveredFlags_Stationary); + ImGui::CheckboxFlags("ImGuiHoveredFlags_NoSharedDelay", p, ImGuiHoveredFlags_NoSharedDelay); + ImGui::TreePop(); + } + + ImGui::SeparatorText("Misc"); + ImGui::SliderFloat2("DisplayWindowPadding", (float*)&style.DisplayWindowPadding, 0.0f, 30.0f, "%.0f"); ImGui::SameLine(); HelpMarker("Apply to regular windows: amount which we enforce to keep visible when moving near edges of your screen."); + ImGui::SliderFloat2("DisplaySafeAreaPadding", (float*)&style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); ImGui::SameLine(); HelpMarker("Apply to every windows, menus, popups, tooltips: amount where we avoid displaying contents. Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured)."); + ImGui::EndTabItem(); } @@ -3124,7 +8367,7 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) { static int output_dest = 0; static bool output_only_modified = true; - if (ImGui::Button("Export Unsaved")) + if (ImGui::Button("Export")) { if (output_dest == 0) ImGui::LogToClipboard(); @@ -3136,7 +8379,8 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) const ImVec4& col = style.Colors[i]; const char* name = ImGui::GetStyleColorName(i); if (!output_only_modified || memcmp(&col, &ref->Colors[i], sizeof(ImVec4)) != 0) - ImGui::LogText("colors[ImGuiCol_%s]%*s= ImVec4(%.2ff, %.2ff, %.2ff, %.2ff);" IM_NEWLINE, name, 23 - (int)strlen(name), "", col.x, col.y, col.z, col.w); + ImGui::LogText("colors[ImGuiCol_%s]%*s= ImVec4(%.2ff, %.2ff, %.2ff, %.2ff);" IM_NEWLINE, + name, 23 - (int)strlen(name), "", col.x, col.y, col.z, col.w); } ImGui::LogFinish(); } @@ -3147,26 +8391,37 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) filter.Draw("Filter colors", ImGui::GetFontSize() * 16); static ImGuiColorEditFlags alpha_flags = 0; - ImGui::RadioButton("Opaque", &alpha_flags, 0); ImGui::SameLine(); - ImGui::RadioButton("Alpha", &alpha_flags, ImGuiColorEditFlags_AlphaPreview); ImGui::SameLine(); - ImGui::RadioButton("Both", &alpha_flags, ImGuiColorEditFlags_AlphaPreviewHalf); ImGui::SameLine(); - HelpMarker("In the color list:\nLeft-click on colored square to open color picker,\nRight-click to open edit options menu."); + if (ImGui::RadioButton("Opaque", alpha_flags == ImGuiColorEditFlags_AlphaOpaque)) { alpha_flags = ImGuiColorEditFlags_AlphaOpaque; } ImGui::SameLine(); + if (ImGui::RadioButton("Alpha", alpha_flags == ImGuiColorEditFlags_None)) { alpha_flags = ImGuiColorEditFlags_None; } ImGui::SameLine(); + if (ImGui::RadioButton("Both", alpha_flags == ImGuiColorEditFlags_AlphaPreviewHalf)) { alpha_flags = ImGuiColorEditFlags_AlphaPreviewHalf; } ImGui::SameLine(); + HelpMarker( + "In the color list:\n" + "Left-click on color square to open color picker,\n" + "Right-click to open edit options menu."); - ImGui::BeginChild("##colors", ImVec2(0, 0), true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar | ImGuiWindowFlags_NavFlattened); - ImGui::PushItemWidth(-160); + ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing() * 10), ImVec2(FLT_MAX, FLT_MAX)); + ImGui::BeginChild("##colors", ImVec2(0, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_AlwaysHorizontalScrollbar); + ImGui::PushItemWidth(ImGui::GetFontSize() * -12); for (int i = 0; i < ImGuiCol_COUNT; i++) { const char* name = ImGui::GetStyleColorName(i); if (!filter.PassFilter(name)) continue; ImGui::PushID(i); +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + if (ImGui::Button("?")) + ImGui::DebugFlashStyleColor((ImGuiCol)i); + ImGui::SetItemTooltip("Flash given color to identify places where it is used."); + ImGui::SameLine(); +#endif ImGui::ColorEdit4("##color", (float*)&style.Colors[i], ImGuiColorEditFlags_AlphaBar | alpha_flags); if (memcmp(&style.Colors[i], &ref->Colors[i], sizeof(ImVec4)) != 0) { - // Tips: in a real user application, you may want to merge and use an icon font into the main font, so instead of "Save"/"Revert" you'd use icons. - // Read the FAQ and misc/fonts/README.txt about using icon fonts. It's really easy and super convenient! - ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); if (ImGui::Button("Save")) ref->Colors[i] = style.Colors[i]; - ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); if (ImGui::Button("Revert")) style.Colors[i] = ref->Colors[i]; + // Tips: in a real user application, you may want to merge and use an icon font into the main font, + // so instead of "Save"/"Revert" you'd use icons! + // Read the FAQ and docs/FONTS.md about using icon fonts. It's really easy and super convenient! + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); if (ImGui::Button("Save")) { ref->Colors[i] = style.Colors[i]; } + ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); if (ImGui::Button("Revert")) { style.Colors[i] = ref->Colors[i]; } } ImGui::SameLine(0.0f, style.ItemInnerSpacing.x); ImGui::TextUnformatted(name); @@ -3182,84 +8437,23 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) { ImGuiIO& io = ImGui::GetIO(); ImFontAtlas* atlas = io.Fonts; - HelpMarker("Read FAQ and misc/fonts/README.txt for details on font loading."); - ImGui::PushItemWidth(120); - for (int i = 0; i < atlas->Fonts.Size; i++) - { - ImFont* font = atlas->Fonts[i]; - ImGui::PushID(font); - bool font_details_opened = ImGui::TreeNode(font, "Font %d: \"%s\"\n%.2f px, %d glyphs, %d file(s)", i, font->ConfigData ? font->ConfigData[0].Name : "", font->FontSize, font->Glyphs.Size, font->ConfigDataCount); - ImGui::SameLine(); if (ImGui::SmallButton("Set as default")) { io.FontDefault = font; } - if (font_details_opened) - { - ImGui::PushFont(font); - ImGui::Text("The quick brown fox jumps over the lazy dog"); - ImGui::PopFont(); - ImGui::DragFloat("Font scale", &font->Scale, 0.005f, 0.3f, 2.0f, "%.1f"); // Scale only this font - ImGui::SameLine(); HelpMarker("Note than the default embedded font is NOT meant to be scaled.\n\nFont are currently rendered into bitmaps at a given size at the time of building the atlas. You may oversample them to get some flexibility with scaling. You can also render at multiple sizes and select which one to use at runtime.\n\n(Glimmer of hope: the atlas system should hopefully be rewritten in the future to make scaling more natural and automatic.)"); - ImGui::InputFloat("Font offset", &font->DisplayOffset.y, 1, 1, "%.0f"); - ImGui::Text("Ascent: %f, Descent: %f, Height: %f", font->Ascent, font->Descent, font->Ascent - font->Descent); - ImGui::Text("Fallback character: '%c' (%d)", font->FallbackChar, font->FallbackChar); - const float surface_sqrt = sqrtf((float)font->MetricsTotalSurface); - ImGui::Text("Texture surface: %d pixels (approx) ~ %dx%d", font->MetricsTotalSurface, (int)surface_sqrt, (int)surface_sqrt); - for (int config_i = 0; config_i < font->ConfigDataCount; config_i++) - if (const ImFontConfig* cfg = &font->ConfigData[config_i]) - ImGui::BulletText("Input %d: \'%s\', Oversample: (%d,%d), PixelSnapH: %d", config_i, cfg->Name, cfg->OversampleH, cfg->OversampleV, cfg->PixelSnapH); - if (ImGui::TreeNode("Glyphs", "Glyphs (%d)", font->Glyphs.Size)) - { - // Display all glyphs of the fonts in separate pages of 256 characters - for (int base = 0; base < 0x10000; base += 256) - { - int count = 0; - for (int n = 0; n < 256; n++) - count += font->FindGlyphNoFallback((ImWchar)(base + n)) ? 1 : 0; - if (count > 0 && ImGui::TreeNode((void*)(intptr_t)base, "U+%04X..U+%04X (%d %s)", base, base + 255, count, count > 1 ? "glyphs" : "glyph")) - { - float cell_size = font->FontSize * 1; - float cell_spacing = style.ItemSpacing.y; - ImVec2 base_pos = ImGui::GetCursorScreenPos(); - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - for (int n = 0; n < 256; n++) - { - ImVec2 cell_p1(base_pos.x + (n % 16) * (cell_size + cell_spacing), base_pos.y + (n / 16) * (cell_size + cell_spacing)); - ImVec2 cell_p2(cell_p1.x + cell_size, cell_p1.y + cell_size); - const ImFontGlyph* glyph = font->FindGlyphNoFallback((ImWchar)(base + n)); - draw_list->AddRect(cell_p1, cell_p2, glyph ? IM_COL32(255, 255, 255, 100) : IM_COL32(255, 255, 255, 50)); - if (glyph) - font->RenderChar(draw_list, cell_size, cell_p1, ImGui::GetColorU32(ImGuiCol_Text), (ImWchar)(base + n)); // We use ImFont::RenderChar as a shortcut because we don't have UTF-8 conversion functions available to generate a string. - if (glyph && ImGui::IsMouseHoveringRect(cell_p1, cell_p2)) - { - ImGui::BeginTooltip(); - ImGui::Text("Codepoint: U+%04X", base + n); - ImGui::Separator(); - ImGui::Text("AdvanceX: %.1f", glyph->AdvanceX); - ImGui::Text("Pos: (%.2f,%.2f)->(%.2f,%.2f)", glyph->X0, glyph->Y0, glyph->X1, glyph->Y1); - ImGui::Text("UV: (%.3f,%.3f)->(%.3f,%.3f)", glyph->U0, glyph->V0, glyph->U1, glyph->V1); - ImGui::EndTooltip(); - } - } - ImGui::Dummy(ImVec2((cell_size + cell_spacing) * 16, (cell_size + cell_spacing) * 16)); - ImGui::TreePop(); - } - } - ImGui::TreePop(); - } - ImGui::TreePop(); - } - ImGui::PopID(); - } - if (ImGui::TreeNode("Atlas texture", "Atlas texture (%dx%d pixels)", atlas->TexWidth, atlas->TexHeight)) - { - ImVec4 tint_col = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); - ImVec4 border_col = ImVec4(1.0f, 1.0f, 1.0f, 0.5f); - ImGui::Image(atlas->TexID, ImVec2((float)atlas->TexWidth, (float)atlas->TexHeight), ImVec2(0, 0), ImVec2(1, 1), tint_col, border_col); - ImGui::TreePop(); - } + HelpMarker("Read FAQ and docs/FONTS.md for details on font loading."); + ImGui::ShowFontAtlas(atlas); + // Post-baking font scaling. Note that this is NOT the nice way of scaling fonts, read below. + // (we enforce hard clamping manually as by default DragFloat/SliderFloat allows CTRL+Click text to get out of bounds). + const float MIN_SCALE = 0.3f; + const float MAX_SCALE = 2.0f; + HelpMarker( + "Those are old settings provided for convenience.\n" + "However, the _correct_ way of scaling your UI is currently to reload your font at the designed size, " + "rebuild the font atlas, and call style.ScaleAllSizes() on a reference ImGuiStyle structure.\n" + "Using those settings here will give you poor quality results."); static float window_scale = 1.0f; - if (ImGui::DragFloat("this window scale", &window_scale, 0.005f, 0.3f, 2.0f, "%.2f")) // scale only this window + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + if (ImGui::DragFloat("window scale", &window_scale, 0.005f, MIN_SCALE, MAX_SCALE, "%.2f", ImGuiSliderFlags_AlwaysClamp)) // Scale only this window ImGui::SetWindowFontScale(window_scale); - ImGui::DragFloat("global scale", &io.FontGlobalScale, 0.005f, 0.3f, 2.0f, "%.2f"); // scale everything + ImGui::DragFloat("global scale", &io.FontGlobalScale, 0.005f, MIN_SCALE, MAX_SCALE, "%.2f", ImGuiSliderFlags_AlwaysClamp); // Scale everything ImGui::PopItemWidth(); ImGui::EndTabItem(); @@ -3267,12 +8461,65 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) if (ImGui::BeginTabItem("Rendering")) { - ImGui::Checkbox("Anti-aliased lines", &style.AntiAliasedLines); ImGui::SameLine(); HelpMarker("When disabling anti-aliasing lines, you'll probably want to disable borders in your style as well."); + ImGui::Checkbox("Anti-aliased lines", &style.AntiAliasedLines); + ImGui::SameLine(); + HelpMarker("When disabling anti-aliasing lines, you'll probably want to disable borders in your style as well."); + + ImGui::Checkbox("Anti-aliased lines use texture", &style.AntiAliasedLinesUseTex); + ImGui::SameLine(); + HelpMarker("Faster lines using texture data. Require backend to render with bilinear filtering (not point/nearest filtering)."); + ImGui::Checkbox("Anti-aliased fill", &style.AntiAliasedFill); - ImGui::PushItemWidth(100); - ImGui::DragFloat("Curve Tessellation Tolerance", &style.CurveTessellationTol, 0.02f, 0.10f, FLT_MAX, "%.2f", 2.0f); + ImGui::PushItemWidth(ImGui::GetFontSize() * 8); + ImGui::DragFloat("Curve Tessellation Tolerance", &style.CurveTessellationTol, 0.02f, 0.10f, 10.0f, "%.2f"); if (style.CurveTessellationTol < 0.10f) style.CurveTessellationTol = 0.10f; + + // When editing the "Circle Segment Max Error" value, draw a preview of its effect on auto-tessellated circles. + ImGui::DragFloat("Circle Tessellation Max Error", &style.CircleTessellationMaxError , 0.005f, 0.10f, 5.0f, "%.2f", ImGuiSliderFlags_AlwaysClamp); + const bool show_samples = ImGui::IsItemActive(); + if (show_samples) + ImGui::SetNextWindowPos(ImGui::GetCursorScreenPos()); + if (show_samples && ImGui::BeginTooltip()) + { + ImGui::TextUnformatted("(R = radius, N = approx number of segments)"); + ImGui::Spacing(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const float min_widget_width = ImGui::CalcTextSize("R: MMM\nN: MMM").x; + for (int n = 0; n < 8; n++) + { + const float RAD_MIN = 5.0f; + const float RAD_MAX = 70.0f; + const float rad = RAD_MIN + (RAD_MAX - RAD_MIN) * (float)n / (8.0f - 1.0f); + + ImGui::BeginGroup(); + + // N is not always exact here due to how PathArcTo() function work internally + ImGui::Text("R: %.f\nN: %d", rad, draw_list->_CalcCircleAutoSegmentCount(rad)); + + const float canvas_width = IM_MAX(min_widget_width, rad * 2.0f); + const float offset_x = floorf(canvas_width * 0.5f); + const float offset_y = floorf(RAD_MAX); + + const ImVec2 p1 = ImGui::GetCursorScreenPos(); + draw_list->AddCircle(ImVec2(p1.x + offset_x, p1.y + offset_y), rad, ImGui::GetColorU32(ImGuiCol_Text)); + ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2)); + + /* + const ImVec2 p2 = ImGui::GetCursorScreenPos(); + draw_list->AddCircleFilled(ImVec2(p2.x + offset_x, p2.y + offset_y), rad, ImGui::GetColorU32(ImGuiCol_Text)); + ImGui::Dummy(ImVec2(canvas_width, RAD_MAX * 2)); + */ + + ImGui::EndGroup(); + ImGui::SameLine(); + } + ImGui::EndTooltip(); + } + ImGui::SameLine(); + HelpMarker("When drawing circle primitives with \"num_segments == 0\" tessellation will be calculated automatically."); + ImGui::DragFloat("Global Alpha", &style.Alpha, 0.005f, 0.20f, 1.0f, "%.2f"); // Not exposing zero here so user doesn't "lose" the UI (zero alpha clips all widgets). But application code could have a toggle to switch between zero and non-zero. + ImGui::DragFloat("Disabled Alpha", &style.DisabledAlpha, 0.005f, 0.0f, 1.0f, "%.2f"); ImGui::SameLine(); HelpMarker("Additional alpha multiplier for disabled items (multiply over current value of Alpha)."); ImGui::PopItemWidth(); ImGui::EndTabItem(); @@ -3284,13 +8531,50 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) ImGui::PopItemWidth(); } +//----------------------------------------------------------------------------- +// [SECTION] User Guide / ShowUserGuide() +//----------------------------------------------------------------------------- + +void ImGui::ShowUserGuide() +{ + ImGuiIO& io = ImGui::GetIO(); + ImGui::BulletText("Double-click on title bar to collapse window."); + ImGui::BulletText( + "Click and drag on lower corner to resize window\n" + "(double-click to auto fit window to its contents)."); + ImGui::BulletText("CTRL+Click on a slider or drag box to input value as text."); + ImGui::BulletText("TAB/SHIFT+TAB to cycle through keyboard editable fields."); + ImGui::BulletText("CTRL+Tab to select a window."); + if (io.FontAllowUserScaling) + ImGui::BulletText("CTRL+Mouse Wheel to zoom window contents."); + ImGui::BulletText("While inputting text:\n"); + ImGui::Indent(); + ImGui::BulletText("CTRL+Left/Right to word jump."); + ImGui::BulletText("CTRL+A or double-click to select all."); + ImGui::BulletText("CTRL+X/C/V to use clipboard cut/copy/paste."); + ImGui::BulletText("CTRL+Z to undo, CTRL+Y/CTRL+SHIFT+Z to redo."); + ImGui::BulletText("ESCAPE to revert."); + ImGui::Unindent(); + ImGui::BulletText("With keyboard navigation enabled:"); + ImGui::Indent(); + ImGui::BulletText("Arrow keys to navigate."); + ImGui::BulletText("Space to activate a widget."); + ImGui::BulletText("Return to input text into a widget."); + ImGui::BulletText("Escape to deactivate a widget, close popup, exit child window."); + ImGui::BulletText("Alt to jump to the menu layer of a window."); + ImGui::Unindent(); +} + //----------------------------------------------------------------------------- // [SECTION] Example App: Main Menu Bar / ShowExampleAppMainMenuBar() //----------------------------------------------------------------------------- +// - ShowExampleAppMainMenuBar() +// - ShowExampleMenuFile() +//----------------------------------------------------------------------------- // Demonstrate creating a "main" fullscreen menu bar and populating it. // Note the difference between BeginMainMenuBar() and BeginMenuBar(): -// - BeginMenuBar() = menu-bar inside current window we Begin()-ed into (the window needs the ImGuiWindowFlags_MenuBar flag) +// - BeginMenuBar() = menu-bar inside current window (which needs the ImGuiWindowFlags_MenuBar flag!) // - BeginMainMenuBar() = helper to create menu-bar-sized window at the top of the main viewport + call BeginMenuBar() into it. static void ShowExampleAppMainMenuBar() { @@ -3304,7 +8588,7 @@ static void ShowExampleAppMainMenuBar() if (ImGui::BeginMenu("Edit")) { if (ImGui::MenuItem("Undo", "CTRL+Z")) {} - if (ImGui::MenuItem("Redo", "CTRL+Y", false, false)) {} // Disabled item + if (ImGui::MenuItem("Redo", "CTRL+Y", false, false)) {} // Disabled item ImGui::Separator(); if (ImGui::MenuItem("Cut", "CTRL+X")) {} if (ImGui::MenuItem("Copy", "CTRL+C")) {} @@ -3315,10 +8599,12 @@ static void ShowExampleAppMainMenuBar() } } -// Note that shortcuts are currently provided for display only (future version will add flags to BeginMenu to process shortcuts) +// Note that shortcuts are currently provided for display only +// (future version will add explicit flags to BeginMenu() to request processing shortcuts) static void ShowExampleMenuFile() { - ImGui::MenuItem("(dummy menu)", NULL, false, false); + IMGUI_DEMO_MARKER("Examples/Menu"); + ImGui::MenuItem("(demo menu)", NULL, false, false); if (ImGui::MenuItem("New")) {} if (ImGui::MenuItem("Open", "Ctrl+O")) {} if (ImGui::BeginMenu("Open Recent")) @@ -3341,24 +8627,26 @@ static void ShowExampleMenuFile() } if (ImGui::MenuItem("Save", "Ctrl+S")) {} if (ImGui::MenuItem("Save As..")) {} + ImGui::Separator(); + IMGUI_DEMO_MARKER("Examples/Menu/Options"); if (ImGui::BeginMenu("Options")) { static bool enabled = true; ImGui::MenuItem("Enabled", "", &enabled); - ImGui::BeginChild("child", ImVec2(0, 60), true); + ImGui::BeginChild("child", ImVec2(0, 60), ImGuiChildFlags_Borders); for (int i = 0; i < 10; i++) ImGui::Text("Scrolling Text %d", i); ImGui::EndChild(); static float f = 0.5f; static int n = 0; - static bool b = true; ImGui::SliderFloat("Value", &f, 0.0f, 1.0f); ImGui::InputFloat("Input", &f, 0.1f); ImGui::Combo("Combo", &n, "Yes\0No\0Maybe\0\0"); - ImGui::Checkbox("Check", &b); ImGui::EndMenu(); } + + IMGUI_DEMO_MARKER("Examples/Menu/Colors"); if (ImGui::BeginMenu("Colors")) { float sz = ImGui::GetTextLineHeight(); @@ -3366,18 +8654,31 @@ static void ShowExampleMenuFile() { const char* name = ImGui::GetStyleColorName((ImGuiCol)i); ImVec2 p = ImGui::GetCursorScreenPos(); - ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x+sz, p.y+sz), ImGui::GetColorU32((ImGuiCol)i)); + ImGui::GetWindowDrawList()->AddRectFilled(p, ImVec2(p.x + sz, p.y + sz), ImGui::GetColorU32((ImGuiCol)i)); ImGui::Dummy(ImVec2(sz, sz)); ImGui::SameLine(); ImGui::MenuItem(name); } ImGui::EndMenu(); } + + // Here we demonstrate appending again to the "Options" menu (which we already created above) + // Of course in this demo it is a little bit silly that this function calls BeginMenu("Options") twice. + // In a real code-base using it would make senses to use this feature from very different code locations. + if (ImGui::BeginMenu("Options")) // <-- Append! + { + IMGUI_DEMO_MARKER("Examples/Menu/Append to an existing menu"); + static bool b = true; + ImGui::Checkbox("SomeOption", &b); + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Disabled", false)) // Disabled { IM_ASSERT(0); } if (ImGui::MenuItem("Checked", NULL, true)) {} + ImGui::Separator(); if (ImGui::MenuItem("Quit", "Alt+F4")) {} } @@ -3386,7 +8687,7 @@ static void ShowExampleMenuFile() //----------------------------------------------------------------------------- // Demonstrate creating a simple console window, with scrolling, filtering, completion and history. -// For the console example, here we are using a more C++ like approach of declaring a class to hold the data and the functions. +// For the console example, we are using a more C++ like approach of declaring a class to hold both data and functions. struct ExampleAppConsole { char InputBuf[256]; @@ -3400,36 +8701,38 @@ struct ExampleAppConsole ExampleAppConsole() { + IMGUI_DEMO_MARKER("Examples/Console"); ClearLog(); memset(InputBuf, 0, sizeof(InputBuf)); HistoryPos = -1; + + // "CLASSIFY" is here to provide the test case where "C"+[tab] completes to "CL" and display multiple matches. Commands.push_back("HELP"); Commands.push_back("HISTORY"); Commands.push_back("CLEAR"); - Commands.push_back("CLASSIFY"); // "classify" is only here to provide an example of "C"+[tab] completing to "CL" and displaying matches. + Commands.push_back("CLASSIFY"); AutoScroll = true; - ScrollToBottom = true; + ScrollToBottom = false; AddLog("Welcome to Dear ImGui!"); } ~ExampleAppConsole() { ClearLog(); for (int i = 0; i < History.Size; i++) - free(History[i]); + ImGui::MemFree(History[i]); } // Portable helpers - static int Stricmp(const char* str1, const char* str2) { int d; while ((d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; } return d; } - static int Strnicmp(const char* str1, const char* str2, int n) { int d = 0; while (n > 0 && (d = toupper(*str2) - toupper(*str1)) == 0 && *str1) { str1++; str2++; n--; } return d; } - static char* Strdup(const char *str) { size_t len = strlen(str) + 1; void* buf = malloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)str, len); } - static void Strtrim(char* str) { char* str_end = str + strlen(str); while (str_end > str && str_end[-1] == ' ') str_end--; *str_end = 0; } + static int Stricmp(const char* s1, const char* s2) { int d; while ((d = toupper(*s2) - toupper(*s1)) == 0 && *s1) { s1++; s2++; } return d; } + static int Strnicmp(const char* s1, const char* s2, int n) { int d = 0; while (n > 0 && (d = toupper(*s2) - toupper(*s1)) == 0 && *s1) { s1++; s2++; n--; } return d; } + static char* Strdup(const char* s) { IM_ASSERT(s); size_t len = strlen(s) + 1; void* buf = ImGui::MemAlloc(len); IM_ASSERT(buf); return (char*)memcpy(buf, (const void*)s, len); } + static void Strtrim(char* s) { char* str_end = s + strlen(s); while (str_end > s && str_end[-1] == ' ') str_end--; *str_end = 0; } void ClearLog() { for (int i = 0; i < Items.Size; i++) - free(Items[i]); + ImGui::MemFree(Items[i]); Items.clear(); - ScrollToBottom = true; } void AddLog(const char* fmt, ...) IM_FMTARGS(2) @@ -3442,20 +8745,19 @@ struct ExampleAppConsole buf[IM_ARRAYSIZE(buf)-1] = 0; va_end(args); Items.push_back(Strdup(buf)); - if (AutoScroll) - ScrollToBottom = true; } void Draw(const char* title, bool* p_open) { - ImGui::SetNextWindowSize(ImVec2(520,600), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); if (!ImGui::Begin(title, p_open)) { ImGui::End(); return; } - // As a specific feature guaranteed by the library, after calling Begin() the last Item represent the title bar. So e.g. IsItemHovered() will return true when hovering the title bar. + // As a specific feature guaranteed by the library, after calling Begin() the last Item represent the title bar. + // So e.g. IsItemHovered() will return true when hovering the title bar. // Here we create a context menu only available from the title bar. if (ImGui::BeginPopupContextItem()) { @@ -3464,16 +8766,20 @@ struct ExampleAppConsole ImGui::EndPopup(); } - ImGui::TextWrapped("This example implements a console with basic coloring, completion and history. A more elaborate implementation may want to store entries along with extra data such as timestamp, emitter, etc."); - ImGui::TextWrapped("Enter 'HELP' for help, press TAB to use text completion."); + ImGui::TextWrapped( + "This example implements a console with basic coloring, completion (TAB key) and history (Up/Down keys). A more elaborate " + "implementation may want to store entries along with extra data such as timestamp, emitter, etc."); + ImGui::TextWrapped("Enter 'HELP' for help."); // TODO: display items starting from the bottom - if (ImGui::SmallButton("Add Dummy Text")) { AddLog("%d some text", Items.Size); AddLog("some more text"); AddLog("display very important message here!"); } ImGui::SameLine(); - if (ImGui::SmallButton("Add Dummy Error")) { AddLog("[error] something went wrong"); } ImGui::SameLine(); - if (ImGui::SmallButton("Clear")) { ClearLog(); } ImGui::SameLine(); - bool copy_to_clipboard = ImGui::SmallButton("Copy"); ImGui::SameLine(); - if (ImGui::SmallButton("Scroll to bottom")) ScrollToBottom = true; + if (ImGui::SmallButton("Add Debug Text")) { AddLog("%d some text", Items.Size); AddLog("some more text"); AddLog("display very important message here!"); } + ImGui::SameLine(); + if (ImGui::SmallButton("Add Debug Error")) { AddLog("[error] something went wrong"); } + ImGui::SameLine(); + if (ImGui::SmallButton("Clear")) { ClearLog(); } + ImGui::SameLine(); + bool copy_to_clipboard = ImGui::SmallButton("Copy"); //static float t = 0.0f; if (ImGui::GetTime() - t > 0.02f) { t = ImGui::GetTime(); AddLog("Spam %f", t); } ImGui::Separator(); @@ -3481,67 +8787,90 @@ struct ExampleAppConsole // Options menu if (ImGui::BeginPopup("Options")) { - if (ImGui::Checkbox("Auto-scroll", &AutoScroll)) - if (AutoScroll) - ScrollToBottom = true; + ImGui::Checkbox("Auto-scroll", &AutoScroll); ImGui::EndPopup(); } // Options, Filter + ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_O, ImGuiInputFlags_Tooltip); if (ImGui::Button("Options")) ImGui::OpenPopup("Options"); ImGui::SameLine(); Filter.Draw("Filter (\"incl,-excl\") (\"error\")", 180); ImGui::Separator(); - const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); // 1 separator, 1 input text - ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), false, ImGuiWindowFlags_HorizontalScrollbar); // Leave room for 1 separator + 1 InputText - if (ImGui::BeginPopupContextWindow()) + // Reserve enough left-over height for 1 separator + 1 input text + const float footer_height_to_reserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); + if (ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footer_height_to_reserve), ImGuiChildFlags_NavFlattened, ImGuiWindowFlags_HorizontalScrollbar)) { - if (ImGui::Selectable("Clear")) ClearLog(); - ImGui::EndPopup(); - } + if (ImGui::BeginPopupContextWindow()) + { + if (ImGui::Selectable("Clear")) ClearLog(); + ImGui::EndPopup(); + } - // Display every line as a separate entry so we can change their color or add custom widgets. If you only want raw text you can use ImGui::TextUnformatted(log.begin(), log.end()); - // NB- if you have thousands of entries this approach may be too inefficient and may require user-side clipping to only process visible items. - // You can seek and display only the lines that are visible using the ImGuiListClipper helper, if your elements are evenly spaced and you have cheap random access to the elements. - // To use the clipper we could replace the 'for (int i = 0; i < Items.Size; i++)' loop with: - // ImGuiListClipper clipper(Items.Size); - // while (clipper.Step()) - // for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) - // However, note that you can not use this code as is if a filter is active because it breaks the 'cheap random-access' property. We would need random-access on the post-filtered list. - // A typical application wanting coarse clipping and filtering may want to pre-compute an array of indices that passed the filtering test, recomputing this array when user changes the filter, - // and appending newly elements as they are inserted. This is left as a task to the user until we can manage to improve this example code! - // If your items are of variable size you may want to implement code similar to what ImGuiListClipper does. Or split your data into fixed height items to allow random-seeking into your list. - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4,1)); // Tighten spacing - if (copy_to_clipboard) - ImGui::LogToClipboard(); - for (int i = 0; i < Items.Size; i++) - { - const char* item = Items[i]; - if (!Filter.PassFilter(item)) - continue; + // Display every line as a separate entry so we can change their color or add custom widgets. + // If you only want raw text you can use ImGui::TextUnformatted(log.begin(), log.end()); + // NB- if you have thousands of entries this approach may be too inefficient and may require user-side clipping + // to only process visible items. The clipper will automatically measure the height of your first item and then + // "seek" to display only items in the visible area. + // To use the clipper we can replace your standard loop: + // for (int i = 0; i < Items.Size; i++) + // With: + // ImGuiListClipper clipper; + // clipper.Begin(Items.Size); + // while (clipper.Step()) + // for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + // - That your items are evenly spaced (same height) + // - That you have cheap random access to your elements (you can access them given their index, + // without processing all the ones before) + // You cannot this code as-is if a filter is active because it breaks the 'cheap random-access' property. + // We would need random-access on the post-filtered list. + // A typical application wanting coarse clipping and filtering may want to pre-compute an array of indices + // or offsets of items that passed the filtering test, recomputing this array when user changes the filter, + // and appending newly elements as they are inserted. This is left as a task to the user until we can manage + // to improve this example code! + // If your items are of variable height: + // - Split them into same height items would be simpler and facilitate random-seeking into your list. + // - Consider using manual call to IsRectVisible() and skipping extraneous decoration from your items. + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); // Tighten spacing + if (copy_to_clipboard) + ImGui::LogToClipboard(); + for (const char* item : Items) + { + if (!Filter.PassFilter(item)) + continue; - // Normally you would store more information in your item (e.g. make Items[] an array of structure, store color/type etc.) - bool pop_color = false; - if (strstr(item, "[error]")) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f)); pop_color = true; } - else if (strncmp(item, "# ", 2) == 0) { ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.8f, 0.6f, 1.0f)); pop_color = true; } - ImGui::TextUnformatted(item); - if (pop_color) - ImGui::PopStyleColor(); + // Normally you would store more information in your item than just a string. + // (e.g. make Items[] an array of structure, store color/type etc.) + ImVec4 color; + bool has_color = false; + if (strstr(item, "[error]")) { color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f); has_color = true; } + else if (strncmp(item, "# ", 2) == 0) { color = ImVec4(1.0f, 0.8f, 0.6f, 1.0f); has_color = true; } + if (has_color) + ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::TextUnformatted(item); + if (has_color) + ImGui::PopStyleColor(); + } + if (copy_to_clipboard) + ImGui::LogFinish(); + + // Keep up at the bottom of the scroll region if we were already at the bottom at the beginning of the frame. + // Using a scrollbar or mouse-wheel will take away from the bottom edge. + if (ScrollToBottom || (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())) + ImGui::SetScrollHereY(1.0f); + ScrollToBottom = false; + + ImGui::PopStyleVar(); } - if (copy_to_clipboard) - ImGui::LogFinish(); - if (ScrollToBottom) - ImGui::SetScrollHereY(1.0f); - ScrollToBottom = false; - ImGui::PopStyleVar(); ImGui::EndChild(); ImGui::Separator(); // Command-line bool reclaim_focus = false; - if (ImGui::InputText("Input", InputBuf, IM_ARRAYSIZE(InputBuf), ImGuiInputTextFlags_EnterReturnsTrue|ImGuiInputTextFlags_CallbackCompletion|ImGuiInputTextFlags_CallbackHistory, &TextEditCallbackStub, (void*)this)) + ImGuiInputTextFlags input_text_flags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_EscapeClearsAll | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory; + if (ImGui::InputText("Input", InputBuf, IM_ARRAYSIZE(InputBuf), input_text_flags, &TextEditCallbackStub, (void*)this)) { char* s = InputBuf; Strtrim(s); @@ -3563,12 +8892,13 @@ struct ExampleAppConsole { AddLog("# %s\n", command_line); - // Insert into history. First find match and delete it so it can be pushed to the back. This isn't trying to be smart or optimal. + // Insert into history. First find match and delete it so it can be pushed to the back. + // This isn't trying to be smart or optimal. HistoryPos = -1; - for (int i = History.Size-1; i >= 0; i--) + for (int i = History.Size - 1; i >= 0; i--) if (Stricmp(History[i], command_line) == 0) { - free(History[i]); + ImGui::MemFree(History[i]); History.erase(History.begin() + i); break; } @@ -3596,11 +8926,12 @@ struct ExampleAppConsole AddLog("Unknown command: '%s'\n", command_line); } - // On commad input, we scroll to bottom even if AutoScroll==false + // On command input, we scroll to bottom even if AutoScroll==false ScrollToBottom = true; } - static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) // In C++11 you are better off using lambdas for this sort of forwarding callbacks + // In C++11 you'd be better off using lambdas for this sort of forwarding callbacks + static int TextEditCallbackStub(ImGuiInputTextCallbackData* data) { ExampleAppConsole* console = (ExampleAppConsole*)data->UserData; return console->TextEditCallback(data); @@ -3629,24 +8960,25 @@ struct ExampleAppConsole // Build a list of candidates ImVector candidates; for (int i = 0; i < Commands.Size; i++) - if (Strnicmp(Commands[i], word_start, (int)(word_end-word_start)) == 0) + if (Strnicmp(Commands[i], word_start, (int)(word_end - word_start)) == 0) candidates.push_back(Commands[i]); if (candidates.Size == 0) { // No match - AddLog("No match for \"%.*s\"!\n", (int)(word_end-word_start), word_start); + AddLog("No match for \"%.*s\"!\n", (int)(word_end - word_start), word_start); } else if (candidates.Size == 1) { - // Single match. Delete the beginning of the word and replace it entirely so we've got nice casing - data->DeleteChars((int)(word_start-data->Buf), (int)(word_end-word_start)); + // Single match. Delete the beginning of the word and replace it entirely so we've got nice casing. + data->DeleteChars((int)(word_start - data->Buf), (int)(word_end - word_start)); data->InsertChars(data->CursorPos, candidates[0]); data->InsertChars(data->CursorPos, " "); } else { - // Multiple matches. Complete as much as we can, so inputing "C" will complete to "CL" and display "CLEAR" and "CLASSIFY" + // Multiple matches. Complete as much as we can.. + // So inputting "C"+Tab will complete to "CL" then display "CLEAR" and "CLASSIFY" as matches. int match_len = (int)(word_end - word_start); for (;;) { @@ -3664,7 +8996,7 @@ struct ExampleAppConsole if (match_len > 0) { - data->DeleteChars((int)(word_start - data->Buf), (int)(word_end-word_start)); + data->DeleteChars((int)(word_start - data->Buf), (int)(word_end - word_start)); data->InsertChars(data->CursorPos, candidates[0], candidates[0] + match_len); } @@ -3725,14 +9057,12 @@ struct ExampleAppLog { ImGuiTextBuffer Buf; ImGuiTextFilter Filter; - ImVector LineOffsets; // Index to lines offset. We maintain this with AddLog() calls, allowing us to have a random access on lines - bool AutoScroll; - bool ScrollToBottom; + ImVector LineOffsets; // Index to lines offset. We maintain this with AddLog() calls. + bool AutoScroll; // Keep scrolling if already at the bottom. ExampleAppLog() { AutoScroll = true; - ScrollToBottom = false; Clear(); } @@ -3753,8 +9083,6 @@ struct ExampleAppLog for (int new_size = Buf.size(); old_size < new_size; old_size++) if (Buf[old_size] == '\n') LineOffsets.push_back(old_size + 1); - if (AutoScroll) - ScrollToBottom = true; } void Draw(const char* title, bool* p_open = NULL) @@ -3768,9 +9096,7 @@ struct ExampleAppLog // Options menu if (ImGui::BeginPopup("Options")) { - if (ImGui::Checkbox("Auto-scroll", &AutoScroll)) - if (AutoScroll) - ScrollToBottom = true; + ImGui::Checkbox("Auto-scroll", &AutoScroll); ImGui::EndPopup(); } @@ -3785,59 +9111,66 @@ struct ExampleAppLog Filter.Draw("Filter", -100.0f); ImGui::Separator(); - ImGui::BeginChild("scrolling", ImVec2(0,0), false, ImGuiWindowFlags_HorizontalScrollbar); - if (clear) - Clear(); - if (copy) - ImGui::LogToClipboard(); + if (ImGui::BeginChild("scrolling", ImVec2(0, 0), ImGuiChildFlags_None, ImGuiWindowFlags_HorizontalScrollbar)) + { + if (clear) + Clear(); + if (copy) + ImGui::LogToClipboard(); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - const char* buf = Buf.begin(); - const char* buf_end = Buf.end(); - if (Filter.IsActive()) - { - // In this example we don't use the clipper when Filter is enabled. - // This is because we don't have a random access on the result on our filter. - // A real application processing logs with ten of thousands of entries may want to store the result of search/filter. - // especially if the filtering function is not trivial (e.g. reg-exp). - for (int line_no = 0; line_no < LineOffsets.Size; line_no++) + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + const char* buf = Buf.begin(); + const char* buf_end = Buf.end(); + if (Filter.IsActive()) { - const char* line_start = buf + LineOffsets[line_no]; - const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; - if (Filter.PassFilter(line_start, line_end)) - ImGui::TextUnformatted(line_start, line_end); - } - } - else - { - // The simplest and easy way to display the entire buffer: - // ImGui::TextUnformatted(buf_begin, buf_end); - // And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward to skip non-visible lines. - // Here we instead demonstrate using the clipper to only process lines that are within the visible area. - // If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them on your side is recommended. - // Using ImGuiListClipper requires A) random access into your data, and B) items all being the same height, - // both of which we can handle since we an array pointing to the beginning of each line of text. - // When using the filter (in the block of code above) we don't have random access into the data to display anymore, which is why we don't use the clipper. - // Storing or skimming through the search result would make it possible (and would be recommended if you want to search through tens of thousands of entries) - ImGuiListClipper clipper; - clipper.Begin(LineOffsets.Size); - while (clipper.Step()) - { - for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + // In this example we don't use the clipper when Filter is enabled. + // This is because we don't have random access to the result of our filter. + // A real application processing logs with ten of thousands of entries may want to store the result of + // search/filter.. especially if the filtering function is not trivial (e.g. reg-exp). + for (int line_no = 0; line_no < LineOffsets.Size; line_no++) { const char* line_start = buf + LineOffsets[line_no]; const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; - ImGui::TextUnformatted(line_start, line_end); + if (Filter.PassFilter(line_start, line_end)) + ImGui::TextUnformatted(line_start, line_end); } } - clipper.End(); - } - ImGui::PopStyleVar(); + else + { + // The simplest and easy way to display the entire buffer: + // ImGui::TextUnformatted(buf_begin, buf_end); + // And it'll just work. TextUnformatted() has specialization for large blob of text and will fast-forward + // to skip non-visible lines. Here we instead demonstrate using the clipper to only process lines that are + // within the visible area. + // If you have tens of thousands of items and their processing cost is non-negligible, coarse clipping them + // on your side is recommended. Using ImGuiListClipper requires + // - A) random access into your data + // - B) items all being the same height, + // both of which we can handle since we have an array pointing to the beginning of each line of text. + // When using the filter (in the block of code above) we don't have random access into the data to display + // anymore, which is why we don't use the clipper. Storing or skimming through the search result would make + // it possible (and would be recommended if you want to search through tens of thousands of entries). + ImGuiListClipper clipper; + clipper.Begin(LineOffsets.Size); + while (clipper.Step()) + { + for (int line_no = clipper.DisplayStart; line_no < clipper.DisplayEnd; line_no++) + { + const char* line_start = buf + LineOffsets[line_no]; + const char* line_end = (line_no + 1 < LineOffsets.Size) ? (buf + LineOffsets[line_no + 1] - 1) : buf_end; + ImGui::TextUnformatted(line_start, line_end); + } + } + clipper.End(); + } + ImGui::PopStyleVar(); - if (ScrollToBottom) - ImGui::SetScrollHereY(1.0f); - ScrollToBottom = false; + // Keep up at the bottom of the scroll region if we were already at the bottom at the beginning of the frame. + // Using a scrollbar or mouse-wheel will take away from the bottom edge. + if (AutoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) + ImGui::SetScrollHereY(1.0f); + } ImGui::EndChild(); ImGui::End(); } @@ -3853,15 +9186,18 @@ static void ShowExampleAppLog(bool* p_open) // Most of the contents of the window will be added by the log.Draw() call. ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver); ImGui::Begin("Example: Log", p_open); + IMGUI_DEMO_MARKER("Examples/Log"); if (ImGui::SmallButton("[Debug] Add 5 entries")) { static int counter = 0; + const char* categories[3] = { "info", "warn", "error" }; + const char* words[] = { "Bumfuzzled", "Cattywampus", "Snickersnee", "Abibliophobia", "Absquatulate", "Nincompoop", "Pauciloquent" }; for (int n = 0; n < 5; n++) { - const char* categories[3] = { "info", "warn", "error" }; - const char* words[] = { "Bumfuzzled", "Cattywampus", "Snickersnee", "Abibliophobia", "Absquatulate", "Nincompoop", "Pauciloquent" }; + const char* category = categories[counter % IM_ARRAYSIZE(categories)]; + const char* word = words[counter % IM_ARRAYSIZE(words)]; log.AddLog("[%05d] [%s] Hello, current time is %.1f, here's a word: '%s'\n", - ImGui::GetFrameCount(), categories[counter % IM_ARRAYSIZE(categories)], ImGui::GetTime(), words[counter % IM_ARRAYSIZE(words)]); + ImGui::GetFrameCount(), category, ImGui::GetTime(), word); counter++; } } @@ -3881,53 +9217,59 @@ static void ShowExampleAppLayout(bool* p_open) ImGui::SetNextWindowSize(ImVec2(500, 440), ImGuiCond_FirstUseEver); if (ImGui::Begin("Example: Simple layout", p_open, ImGuiWindowFlags_MenuBar)) { + IMGUI_DEMO_MARKER("Examples/Simple layout"); if (ImGui::BeginMenuBar()) { if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Close")) *p_open = false; + if (ImGui::MenuItem("Close", "Ctrl+W")) { *p_open = false; } ImGui::EndMenu(); } ImGui::EndMenuBar(); } - // left + // Left static int selected = 0; - ImGui::BeginChild("left pane", ImVec2(150, 0), true); - for (int i = 0; i < 100; i++) { - char label[128]; - sprintf(label, "MyObject %d", i); - if (ImGui::Selectable(label, selected == i)) - selected = i; + ImGui::BeginChild("left pane", ImVec2(150, 0), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeX); + for (int i = 0; i < 100; i++) + { + // FIXME: Good candidate to use ImGuiSelectableFlags_SelectOnNav + char label[128]; + sprintf(label, "MyObject %d", i); + if (ImGui::Selectable(label, selected == i)) + selected = i; + } + ImGui::EndChild(); } - ImGui::EndChild(); ImGui::SameLine(); - // right - ImGui::BeginGroup(); + // Right + { + ImGui::BeginGroup(); ImGui::BeginChild("item view", ImVec2(0, -ImGui::GetFrameHeightWithSpacing())); // Leave room for 1 line below us - ImGui::Text("MyObject: %d", selected); - ImGui::Separator(); - if (ImGui::BeginTabBar("##Tabs", ImGuiTabBarFlags_None)) + ImGui::Text("MyObject: %d", selected); + ImGui::Separator(); + if (ImGui::BeginTabBar("##Tabs", ImGuiTabBarFlags_None)) + { + if (ImGui::BeginTabItem("Description")) { - if (ImGui::BeginTabItem("Description")) - { - ImGui::TextWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "); - ImGui::EndTabItem(); - } - if (ImGui::BeginTabItem("Details")) - { - ImGui::Text("ID: 0123456789"); - ImGui::EndTabItem(); - } - ImGui::EndTabBar(); + ImGui::TextWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. "); + ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("Details")) + { + ImGui::Text("ID: 0123456789"); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } ImGui::EndChild(); if (ImGui::Button("Revert")) {} ImGui::SameLine(); if (ImGui::Button("Save")) {} - ImGui::EndGroup(); + ImGui::EndGroup(); + } } ImGui::End(); } @@ -3935,72 +9277,152 @@ static void ShowExampleAppLayout(bool* p_open) //----------------------------------------------------------------------------- // [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() //----------------------------------------------------------------------------- +// Some of the interactions are a bit lack-luster: +// - We would want pressing validating or leaving the filter to somehow restore focus. +// - We may want more advanced filtering (child nodes) and clipper support: both will need extra work. +// - We would want to customize some keyboard interactions to easily keyboard navigate between the tree and the properties. +//----------------------------------------------------------------------------- -// Demonstrate create a simple property editor. -static void ShowExampleAppPropertyEditor(bool* p_open) +struct ExampleAppPropertyEditor { - ImGui::SetNextWindowSize(ImVec2(430,450), ImGuiCond_FirstUseEver); + ImGuiTextFilter Filter; + ExampleTreeNode* VisibleNode = NULL; + + void Draw(ExampleTreeNode* root_node) + { + // Left side: draw tree + // - Currently using a table to benefit from RowBg feature + if (ImGui::BeginChild("##tree", ImVec2(300, 0), ImGuiChildFlags_ResizeX | ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened)) + { + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_F, ImGuiInputFlags_Tooltip); + ImGui::PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true); + if (ImGui::InputTextWithHint("##Filter", "incl,-excl", Filter.InputBuf, IM_ARRAYSIZE(Filter.InputBuf), ImGuiInputTextFlags_EscapeClearsAll)) + Filter.Build(); + ImGui::PopItemFlag(); + + if (ImGui::BeginTable("##bg", 1, ImGuiTableFlags_RowBg)) + { + for (ExampleTreeNode* node : root_node->Childs) + if (Filter.PassFilter(node->Name)) // Filter root node + DrawTreeNode(node); + ImGui::EndTable(); + } + } + ImGui::EndChild(); + + // Right side: draw properties + ImGui::SameLine(); + + ImGui::BeginGroup(); // Lock X position + if (ExampleTreeNode* node = VisibleNode) + { + ImGui::Text("%s", node->Name); + ImGui::TextDisabled("UID: 0x%08X", node->UID); + ImGui::Separator(); + if (ImGui::BeginTable("##properties", 2, ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY)) + { + // Push object ID after we entered the table, so table is shared for all objects + ImGui::PushID((int)node->UID); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthFixed); + ImGui::TableSetupColumn("", ImGuiTableColumnFlags_WidthStretch, 2.0f); // Default twice larger + if (node->HasData) + { + // In a typical application, the structure description would be derived from a data-driven system. + // - We try to mimic this with our ExampleMemberInfo structure and the ExampleTreeNodeMemberInfos[] array. + // - Limits and some details are hard-coded to simplify the demo. + for (const ExampleMemberInfo& field_desc : ExampleTreeNodeMemberInfos) + { + ImGui::TableNextRow(); + ImGui::PushID(field_desc.Name); + ImGui::TableNextColumn(); + ImGui::AlignTextToFramePadding(); + ImGui::TextUnformatted(field_desc.Name); + ImGui::TableNextColumn(); + void* field_ptr = (void*)(((unsigned char*)node) + field_desc.Offset); + switch (field_desc.DataType) + { + case ImGuiDataType_Bool: + { + IM_ASSERT(field_desc.DataCount == 1); + ImGui::Checkbox("##Editor", (bool*)field_ptr); + break; + } + case ImGuiDataType_S32: + { + int v_min = INT_MIN, v_max = INT_MAX; + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::DragScalarN("##Editor", field_desc.DataType, field_ptr, field_desc.DataCount, 1.0f, &v_min, &v_max); + break; + } + case ImGuiDataType_Float: + { + float v_min = 0.0f, v_max = 1.0f; + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::SliderScalarN("##Editor", field_desc.DataType, field_ptr, field_desc.DataCount, &v_min, &v_max); + break; + } + case ImGuiDataType_String: + { + ImGui::InputText("##Editor", reinterpret_cast(field_ptr), 28); + break; + } + } + ImGui::PopID(); + } + } + ImGui::PopID(); + ImGui::EndTable(); + } + } + ImGui::EndGroup(); + } + + void DrawTreeNode(ExampleTreeNode* node) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::PushID(node->UID); + ImGuiTreeNodeFlags tree_flags = ImGuiTreeNodeFlags_None; + tree_flags |= ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick; // Standard opening mode as we are likely to want to add selection afterwards + tree_flags |= ImGuiTreeNodeFlags_NavLeftJumpsBackHere; // Left arrow support + if (node == VisibleNode) + tree_flags |= ImGuiTreeNodeFlags_Selected; + if (node->Childs.Size == 0) + tree_flags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Bullet; + if (node->DataMyBool == false) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyle().Colors[ImGuiCol_TextDisabled]); + bool node_open = ImGui::TreeNodeEx("", tree_flags, "%s", node->Name); + if (node->DataMyBool == false) + ImGui::PopStyleColor(); + if (ImGui::IsItemFocused()) + VisibleNode = node; + if (node_open) + { + for (ExampleTreeNode* child : node->Childs) + DrawTreeNode(child); + ImGui::TreePop(); + } + ImGui::PopID(); + } +}; + +// Demonstrate creating a simple property editor. +static void ShowExampleAppPropertyEditor(bool* p_open, ImGuiDemoWindowData* demo_data) +{ + ImGui::SetNextWindowSize(ImVec2(430, 450), ImGuiCond_FirstUseEver); if (!ImGui::Begin("Example: Property editor", p_open)) { ImGui::End(); return; } - HelpMarker("This example shows how you may implement a property editor using two columns.\nAll objects/fields data are dummies here.\nRemember that in many simple cases, you can use ImGui::SameLine(xxx) to position\nyour cursor horizontally instead of using the Columns() API."); + IMGUI_DEMO_MARKER("Examples/Property Editor"); + static ExampleAppPropertyEditor property_editor; + if (demo_data->DemoTree == NULL) + demo_data->DemoTree = ExampleTree_CreateDemoTree(); + property_editor.Draw(demo_data->DemoTree); - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2,2)); - ImGui::Columns(2); - ImGui::Separator(); - - struct funcs - { - static void ShowDummyObject(const char* prefix, int uid) - { - ImGui::PushID(uid); // Use object uid as identifier. Most commonly you could also use the object pointer as a base ID. - ImGui::AlignTextToFramePadding(); // Text and Tree nodes are less high than regular widgets, here we add vertical spacing to make the tree lines equal high. - bool node_open = ImGui::TreeNode("Object", "%s_%u", prefix, uid); - ImGui::NextColumn(); - ImGui::AlignTextToFramePadding(); - ImGui::Text("my sailor is rich"); - ImGui::NextColumn(); - if (node_open) - { - static float dummy_members[8] = { 0.0f,0.0f,1.0f,3.1416f,100.0f,999.0f }; - for (int i = 0; i < 8; i++) - { - ImGui::PushID(i); // Use field index as identifier. - if (i < 2) - { - ShowDummyObject("Child", 424242); - } - else - { - // Here we use a TreeNode to highlight on hover (we could use e.g. Selectable as well) - ImGui::AlignTextToFramePadding(); - ImGui::TreeNodeEx("Field", ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen | ImGuiTreeNodeFlags_Bullet, "Field_%d", i); - ImGui::NextColumn(); - ImGui::SetNextItemWidth(-1); - if (i >= 5) - ImGui::InputFloat("##value", &dummy_members[i], 1.0f); - else - ImGui::DragFloat("##value", &dummy_members[i], 0.01f); - ImGui::NextColumn(); - } - ImGui::PopID(); - } - ImGui::TreePop(); - } - ImGui::PopID(); - } - }; - - // Iterate dummy objects with dummy members (all the same data) - for (int obj_i = 0; obj_i < 3; obj_i++) - funcs::ShowDummyObject("Object", obj_i); - - ImGui::Columns(1); - ImGui::Separator(); - ImGui::PopStyleVar(); ImGui::End(); } @@ -4011,25 +9433,29 @@ static void ShowExampleAppPropertyEditor(bool* p_open) // Demonstrate/test rendering huge amount of text, and the incidence of clipping. static void ShowExampleAppLongText(bool* p_open) { - ImGui::SetNextWindowSize(ImVec2(520,600), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver); if (!ImGui::Begin("Example: Long text display", p_open)) { ImGui::End(); return; } + IMGUI_DEMO_MARKER("Examples/Long text display"); static int test_type = 0; static ImGuiTextBuffer log; static int lines = 0; ImGui::Text("Printing unusually long amount of text."); - ImGui::Combo("Test type", &test_type, "Single call to TextUnformatted()\0Multiple calls to Text(), clipped manually\0Multiple calls to Text(), not clipped (slow)\0"); + ImGui::Combo("Test type", &test_type, + "Single call to TextUnformatted()\0" + "Multiple calls to Text(), clipped\0" + "Multiple calls to Text(), not clipped (slow)\0"); ImGui::Text("Buffer contents: %d lines, %d bytes", lines, log.size()); if (ImGui::Button("Clear")) { log.clear(); lines = 0; } ImGui::SameLine(); if (ImGui::Button("Add 1000 lines")) { for (int i = 0; i < 1000; i++) - log.appendf("%i The quick brown fox jumps over the lazy dog\n", lines+i); + log.appendf("%i The quick brown fox jumps over the lazy dog\n", lines + i); lines += 1000; } ImGui::BeginChild("Log"); @@ -4042,8 +9468,9 @@ static void ShowExampleAppLongText(bool* p_open) case 1: { // Multiple calls to Text(), manually coarsely clipped - demonstrate how to use the ImGuiListClipper helper. - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,0)); - ImGuiListClipper clipper(lines); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGuiListClipper clipper; + clipper.Begin(lines); while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) ImGui::Text("%i The quick brown fox jumps over the lazy dog", i); @@ -4052,7 +9479,7 @@ static void ShowExampleAppLongText(bool* p_open) } case 2: // Multiple calls to Text(), not clipped (slow) - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0,0)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); for (int i = 0; i < lines; i++) ImGui::Text("%i The quick brown fox jumps over the lazy dog", i); ImGui::PopStyleVar(); @@ -4074,9 +9501,13 @@ static void ShowExampleAppAutoResize(bool* p_open) ImGui::End(); return; } + IMGUI_DEMO_MARKER("Examples/Auto-resizing window"); static int lines = 10; - ImGui::Text("Window will resize every-frame to the size of its content.\nNote that you probably don't want to query the window size to\noutput your content because that would create a feedback loop."); + ImGui::TextUnformatted( + "Window will resize every-frame to the size of its content.\n" + "Note that you probably don't want to query the window size to\n" + "output your content because that would create a feedback loop."); ImGui::SliderInt("Number of lines", &lines, 1, 20); for (int i = 0; i < lines; i++) ImGui::Text("%*sThis is line %d", i * 4, "", i); // Pad with space to extend size horizontally @@ -4088,72 +9519,139 @@ static void ShowExampleAppAutoResize(bool* p_open) //----------------------------------------------------------------------------- // Demonstrate creating a window with custom resize constraints. +// Note that size constraints currently don't work on a docked window (when in 'docking' branch) static void ShowExampleAppConstrainedResize(bool* p_open) { - struct CustomConstraints // Helper functions to demonstrate programmatic constraints + struct CustomConstraints { - static void Square(ImGuiSizeCallbackData* data) { data->DesiredSize = ImVec2(IM_MAX(data->DesiredSize.x, data->DesiredSize.y), IM_MAX(data->DesiredSize.x, data->DesiredSize.y)); } - static void Step(ImGuiSizeCallbackData* data) { float step = (float)(int)(intptr_t)data->UserData; data->DesiredSize = ImVec2((int)(data->DesiredSize.x / step + 0.5f) * step, (int)(data->DesiredSize.y / step + 0.5f) * step); } + // Helper functions to demonstrate programmatic constraints + // FIXME: This doesn't take account of decoration size (e.g. title bar), library should make this easier. + // FIXME: None of the three demos works consistently when resizing from borders. + static void AspectRatio(ImGuiSizeCallbackData* data) + { + float aspect_ratio = *(float*)data->UserData; + data->DesiredSize.y = (float)(int)(data->DesiredSize.x / aspect_ratio); + } + static void Square(ImGuiSizeCallbackData* data) + { + data->DesiredSize.x = data->DesiredSize.y = IM_MAX(data->DesiredSize.x, data->DesiredSize.y); + } + static void Step(ImGuiSizeCallbackData* data) + { + float step = *(float*)data->UserData; + data->DesiredSize = ImVec2((int)(data->DesiredSize.x / step + 0.5f) * step, (int)(data->DesiredSize.y / step + 0.5f) * step); + } }; - static bool auto_resize = false; - static int type = 0; - static int display_lines = 10; - if (type == 0) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 0), ImVec2(-1, FLT_MAX)); // Vertical only - if (type == 1) ImGui::SetNextWindowSizeConstraints(ImVec2(0, -1), ImVec2(FLT_MAX, -1)); // Horizontal only - if (type == 2) ImGui::SetNextWindowSizeConstraints(ImVec2(100, 100), ImVec2(FLT_MAX, FLT_MAX)); // Width > 100, Height > 100 - if (type == 3) ImGui::SetNextWindowSizeConstraints(ImVec2(400, -1), ImVec2(500, -1)); // Width 400-500 - if (type == 4) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 400), ImVec2(-1, 500)); // Height 400-500 - if (type == 5) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Square); // Always Square - if (type == 6) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Step, (void*)(intptr_t)100); // Fixed Step - - ImGuiWindowFlags flags = auto_resize ? ImGuiWindowFlags_AlwaysAutoResize : 0; - if (ImGui::Begin("Example: Constrained Resize", p_open, flags)) + const char* test_desc[] = { - const char* desc[] = + "Between 100x100 and 500x500", + "At least 100x100", + "Resize vertical + lock current width", + "Resize horizontal + lock current height", + "Width Between 400 and 500", + "Height at least 400", + "Custom: Aspect Ratio 16:9", + "Custom: Always Square", + "Custom: Fixed Steps (100)", + }; + + // Options + static bool auto_resize = false; + static bool window_padding = true; + static int type = 6; // Aspect Ratio + static int display_lines = 10; + + // Submit constraint + float aspect_ratio = 16.0f / 9.0f; + float fixed_step = 100.0f; + if (type == 0) ImGui::SetNextWindowSizeConstraints(ImVec2(100, 100), ImVec2(500, 500)); // Between 100x100 and 500x500 + if (type == 1) ImGui::SetNextWindowSizeConstraints(ImVec2(100, 100), ImVec2(FLT_MAX, FLT_MAX)); // Width > 100, Height > 100 + if (type == 2) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 0), ImVec2(-1, FLT_MAX)); // Resize vertical + lock current width + if (type == 3) ImGui::SetNextWindowSizeConstraints(ImVec2(0, -1), ImVec2(FLT_MAX, -1)); // Resize horizontal + lock current height + if (type == 4) ImGui::SetNextWindowSizeConstraints(ImVec2(400, -1), ImVec2(500, -1)); // Width Between and 400 and 500 + if (type == 5) ImGui::SetNextWindowSizeConstraints(ImVec2(-1, 400), ImVec2(-1, FLT_MAX)); // Height at least 400 + if (type == 6) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::AspectRatio, (void*)&aspect_ratio); // Aspect ratio + if (type == 7) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Square); // Always Square + if (type == 8) ImGui::SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, FLT_MAX), CustomConstraints::Step, (void*)&fixed_step); // Fixed Step + + // Submit window + if (!window_padding) + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + const ImGuiWindowFlags window_flags = auto_resize ? ImGuiWindowFlags_AlwaysAutoResize : 0; + const bool window_open = ImGui::Begin("Example: Constrained Resize", p_open, window_flags); + if (!window_padding) + ImGui::PopStyleVar(); + if (window_open) + { + IMGUI_DEMO_MARKER("Examples/Constrained Resizing window"); + if (ImGui::GetIO().KeyShift) { - "Resize vertical only", - "Resize horizontal only", - "Width > 100, Height > 100", - "Width 400-500", - "Height 400-500", - "Custom: Always Square", - "Custom: Fixed Steps (100)", - }; - if (ImGui::Button("200x200")) { ImGui::SetWindowSize(ImVec2(200, 200)); } ImGui::SameLine(); - if (ImGui::Button("500x500")) { ImGui::SetWindowSize(ImVec2(500, 500)); } ImGui::SameLine(); - if (ImGui::Button("800x200")) { ImGui::SetWindowSize(ImVec2(800, 200)); } - ImGui::SetNextItemWidth(200); - ImGui::Combo("Constraint", &type, desc, IM_ARRAYSIZE(desc)); - ImGui::SetNextItemWidth(200); - ImGui::DragInt("Lines", &display_lines, 0.2f, 1, 100); - ImGui::Checkbox("Auto-resize", &auto_resize); - for (int i = 0; i < display_lines; i++) - ImGui::Text("%*sHello, sailor! Making this line long enough for the example.", i * 4, ""); + // Display a dummy viewport (in your real app you would likely use ImageButton() to display a texture. + ImVec2 avail_size = ImGui::GetContentRegionAvail(); + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImGui::ColorButton("viewport", ImVec4(0.5f, 0.2f, 0.5f, 1.0f), ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop, avail_size); + ImGui::SetCursorScreenPos(ImVec2(pos.x + 10, pos.y + 10)); + ImGui::Text("%.2f x %.2f", avail_size.x, avail_size.y); + } + else + { + ImGui::Text("(Hold SHIFT to display a dummy viewport)"); + if (ImGui::IsWindowDocked()) + ImGui::Text("Warning: Sizing Constraints won't work if the window is docked!"); + if (ImGui::Button("Set 200x200")) { ImGui::SetWindowSize(ImVec2(200, 200)); } ImGui::SameLine(); + if (ImGui::Button("Set 500x500")) { ImGui::SetWindowSize(ImVec2(500, 500)); } ImGui::SameLine(); + if (ImGui::Button("Set 800x200")) { ImGui::SetWindowSize(ImVec2(800, 200)); } + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 20); + ImGui::Combo("Constraint", &type, test_desc, IM_ARRAYSIZE(test_desc)); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 20); + ImGui::DragInt("Lines", &display_lines, 0.2f, 1, 100); + ImGui::Checkbox("Auto-resize", &auto_resize); + ImGui::Checkbox("Window padding", &window_padding); + for (int i = 0; i < display_lines; i++) + ImGui::Text("%*sHello, sailor! Making this line long enough for the example.", i * 4, ""); + } } ImGui::End(); } //----------------------------------------------------------------------------- -// [SECTION] Example App: Simple Overlay / ShowExampleAppSimpleOverlay() +// [SECTION] Example App: Simple overlay / ShowExampleAppSimpleOverlay() //----------------------------------------------------------------------------- -// Demonstrate creating a simple static window with no decoration + a context-menu to choose which corner of the screen to use. +// Demonstrate creating a simple static window with no decoration +// + a context-menu to choose which corner of the screen to use. static void ShowExampleAppSimpleOverlay(bool* p_open) { - const float DISTANCE = 10.0f; - static int corner = 0; + static int location = 0; ImGuiIO& io = ImGui::GetIO(); - if (corner != -1) + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; + if (location >= 0) { - ImVec2 window_pos = ImVec2((corner & 1) ? io.DisplaySize.x - DISTANCE : DISTANCE, (corner & 2) ? io.DisplaySize.y - DISTANCE : DISTANCE); - ImVec2 window_pos_pivot = ImVec2((corner & 1) ? 1.0f : 0.0f, (corner & 2) ? 1.0f : 0.0f); + const float PAD = 10.0f; + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImVec2 work_pos = viewport->WorkPos; // Use work area to avoid menu-bar/task-bar, if any! + ImVec2 work_size = viewport->WorkSize; + ImVec2 window_pos, window_pos_pivot; + window_pos.x = (location & 1) ? (work_pos.x + work_size.x - PAD) : (work_pos.x + PAD); + window_pos.y = (location & 2) ? (work_pos.y + work_size.y - PAD) : (work_pos.y + PAD); + window_pos_pivot.x = (location & 1) ? 1.0f : 0.0f; + window_pos_pivot.y = (location & 2) ? 1.0f : 0.0f; ImGui::SetNextWindowPos(window_pos, ImGuiCond_Always, window_pos_pivot); + ImGui::SetNextWindowViewport(viewport->ID); + window_flags |= ImGuiWindowFlags_NoMove; } - ImGui::SetNextWindowBgAlpha(0.35f); // Forward background - if (ImGui::Begin("Example: Simple overlay", p_open, (corner != -1 ? ImGuiWindowFlags_NoMove : 0) | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) + else if (location == -2) { - ImGui::Text("Simple overlay\n" "in the corner of the screen.\n" "(right-click to change position)"); + // Center window + ImGui::SetNextWindowPos(ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); + window_flags |= ImGuiWindowFlags_NoMove; + } + ImGui::SetNextWindowBgAlpha(0.35f); // Transparent background + if (ImGui::Begin("Example: Simple overlay", p_open, window_flags)) + { + IMGUI_DEMO_MARKER("Examples/Simple Overlay"); + ImGui::Text("Simple overlay\n" "(right-click to change position)"); ImGui::Separator(); if (ImGui::IsMousePosValid()) ImGui::Text("Mouse Position: (%.1f,%.1f)", io.MousePos.x, io.MousePos.y); @@ -4161,11 +9659,12 @@ static void ShowExampleAppSimpleOverlay(bool* p_open) ImGui::Text("Mouse Position: "); if (ImGui::BeginPopupContextWindow()) { - if (ImGui::MenuItem("Custom", NULL, corner == -1)) corner = -1; - if (ImGui::MenuItem("Top-left", NULL, corner == 0)) corner = 0; - if (ImGui::MenuItem("Top-right", NULL, corner == 1)) corner = 1; - if (ImGui::MenuItem("Bottom-left", NULL, corner == 2)) corner = 2; - if (ImGui::MenuItem("Bottom-right", NULL, corner == 3)) corner = 3; + if (ImGui::MenuItem("Custom", NULL, location == -1)) location = -1; + if (ImGui::MenuItem("Center", NULL, location == -2)) location = -2; + if (ImGui::MenuItem("Top-left", NULL, location == 0)) location = 0; + if (ImGui::MenuItem("Top-right", NULL, location == 1)) location = 1; + if (ImGui::MenuItem("Bottom-left", NULL, location == 2)) location = 2; + if (ImGui::MenuItem("Bottom-right", NULL, location == 3)) location = 3; if (p_open && ImGui::MenuItem("Close")) *p_open = false; ImGui::EndPopup(); } @@ -4173,24 +9672,65 @@ static void ShowExampleAppSimpleOverlay(bool* p_open) ImGui::End(); } +//----------------------------------------------------------------------------- +// [SECTION] Example App: Fullscreen window / ShowExampleAppFullscreen() +//----------------------------------------------------------------------------- + +// Demonstrate creating a window covering the entire screen/viewport +static void ShowExampleAppFullscreen(bool* p_open) +{ + static bool use_work_area = true; + static ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings; + + // We demonstrate using the full viewport area or the work area (without menu-bars, task-bars etc.) + // Based on your use case you may want one or the other. + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(use_work_area ? viewport->WorkPos : viewport->Pos); + ImGui::SetNextWindowSize(use_work_area ? viewport->WorkSize : viewport->Size); + + if (ImGui::Begin("Example: Fullscreen window", p_open, flags)) + { + ImGui::Checkbox("Use work area instead of main area", &use_work_area); + ImGui::SameLine(); + HelpMarker("Main Area = entire viewport,\nWork Area = entire viewport minus sections used by the main menu bars, task bars etc.\n\nEnable the main-menu bar in Examples menu to see the difference."); + + ImGui::CheckboxFlags("ImGuiWindowFlags_NoBackground", &flags, ImGuiWindowFlags_NoBackground); + ImGui::CheckboxFlags("ImGuiWindowFlags_NoDecoration", &flags, ImGuiWindowFlags_NoDecoration); + ImGui::Indent(); + ImGui::CheckboxFlags("ImGuiWindowFlags_NoTitleBar", &flags, ImGuiWindowFlags_NoTitleBar); + ImGui::CheckboxFlags("ImGuiWindowFlags_NoCollapse", &flags, ImGuiWindowFlags_NoCollapse); + ImGui::CheckboxFlags("ImGuiWindowFlags_NoScrollbar", &flags, ImGuiWindowFlags_NoScrollbar); + ImGui::Unindent(); + + if (p_open && ImGui::Button("Close this window")) + *p_open = false; + } + ImGui::End(); +} + //----------------------------------------------------------------------------- // [SECTION] Example App: Manipulating Window Titles / ShowExampleAppWindowTitles() //----------------------------------------------------------------------------- -// Demonstrate using "##" and "###" in identifiers to manipulate ID generation. -// This apply to all regular items as well. Read FAQ section "How can I have multiple widgets with the same label? Can I have widget without a label? (Yes). A primer on the purpose of labels/IDs." for details. +// Demonstrate the use of "##" and "###" in identifiers to manipulate ID generation. +// This applies to all regular items as well. +// Read FAQ section "How can I have multiple widgets with the same label?" for details. static void ShowExampleAppWindowTitles(bool*) { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImVec2 base_pos = viewport->Pos; + // By default, Windows are uniquely identified by their title. // You can use the "##" and "###" markers to manipulate the display/ID. // Using "##" to display same title but have unique identifier. - ImGui::SetNextWindowPos(ImVec2(100, 100), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(base_pos.x + 100, base_pos.y + 100), ImGuiCond_FirstUseEver); ImGui::Begin("Same title as another window##1"); + IMGUI_DEMO_MARKER("Examples/Manipulating window titles"); ImGui::Text("This is window 1.\nMy title is the same as window 2, but my identifier is unique."); ImGui::End(); - ImGui::SetNextWindowPos(ImVec2(100, 200), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(base_pos.x + 100, base_pos.y + 200), ImGuiCond_FirstUseEver); ImGui::Begin("Same title as another window##2"); ImGui::Text("This is window 2.\nMy title is the same as window 1, but my identifier is unique."); ImGui::End(); @@ -4198,7 +9738,7 @@ static void ShowExampleAppWindowTitles(bool*) // Using "###" to display a changing title but keep a static identifier "AnimatedTitle" char buf[128]; sprintf(buf, "Animated title %c %d###AnimatedTitle", "|/-\\"[(int)(ImGui::GetTime() / 0.25f) & 3], ImGui::GetFrameCount()); - ImGui::SetNextWindowPos(ImVec2(100, 300), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(base_pos.x + 100, base_pos.y + 300), ImGuiCond_FirstUseEver); ImGui::Begin(buf); ImGui::Text("This window has a changing title."); ImGui::End(); @@ -4208,121 +9748,252 @@ static void ShowExampleAppWindowTitles(bool*) // [SECTION] Example App: Custom Rendering using ImDrawList API / ShowExampleAppCustomRendering() //----------------------------------------------------------------------------- +// Add a |_| looking shape +static void PathConcaveShape(ImDrawList* draw_list, float x, float y, float sz) +{ + const ImVec2 pos_norms[] = { { 0.0f, 0.0f }, { 0.3f, 0.0f }, { 0.3f, 0.7f }, { 0.7f, 0.7f }, { 0.7f, 0.0f }, { 1.0f, 0.0f }, { 1.0f, 1.0f }, { 0.0f, 1.0f } }; + for (const ImVec2& p : pos_norms) + draw_list->PathLineTo(ImVec2(x + 0.5f + (int)(sz * p.x), y + 0.5f + (int)(sz * p.y))); +} + // Demonstrate using the low-level ImDrawList to draw custom shapes. static void ShowExampleAppCustomRendering(bool* p_open) { - ImGui::SetNextWindowSize(ImVec2(350, 560), ImGuiCond_FirstUseEver); if (!ImGui::Begin("Example: Custom rendering", p_open)) { ImGui::End(); return; } + IMGUI_DEMO_MARKER("Examples/Custom Rendering"); - // Tip: If you do a lot of custom rendering, you probably want to use your own geometrical types and benefit of overloaded operators, etc. - // Define IM_VEC2_CLASS_EXTRA in imconfig.h to create implicit conversions between your types and ImVec2/ImVec4. - // ImGui defines overloaded operators but they are internal to imgui.cpp and not exposed outside (to avoid messing with your types) - // In this example we are not using the maths operators! - ImDrawList* draw_list = ImGui::GetWindowDrawList(); + // Tip: If you do a lot of custom rendering, you probably want to use your own geometrical types and benefit of + // overloaded operators, etc. Define IM_VEC2_CLASS_EXTRA in imconfig.h to create implicit conversions between your + // types and ImVec2/ImVec4. Dear ImGui defines overloaded operators but they are internal to imgui.cpp and not + // exposed outside (to avoid messing with your types) In this example we are not using the maths operators! if (ImGui::BeginTabBar("##TabBar")) { - // Primitives if (ImGui::BeginTabItem("Primitives")) { + ImGui::PushItemWidth(-ImGui::GetFontSize() * 15); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Draw gradients + // (note that those are currently exacerbating our sRGB/Linear issues) + // Calling ImGui::GetColorU32() multiplies the given colors by the current Style Alpha, but you may pass the IM_COL32() directly as well.. + ImGui::Text("Gradients"); + ImVec2 gradient_size = ImVec2(ImGui::CalcItemWidth(), ImGui::GetFrameHeight()); + { + ImVec2 p0 = ImGui::GetCursorScreenPos(); + ImVec2 p1 = ImVec2(p0.x + gradient_size.x, p0.y + gradient_size.y); + ImU32 col_a = ImGui::GetColorU32(IM_COL32(0, 0, 0, 255)); + ImU32 col_b = ImGui::GetColorU32(IM_COL32(255, 255, 255, 255)); + draw_list->AddRectFilledMultiColor(p0, p1, col_a, col_b, col_b, col_a); + ImGui::InvisibleButton("##gradient1", gradient_size); + } + { + ImVec2 p0 = ImGui::GetCursorScreenPos(); + ImVec2 p1 = ImVec2(p0.x + gradient_size.x, p0.y + gradient_size.y); + ImU32 col_a = ImGui::GetColorU32(IM_COL32(0, 255, 0, 255)); + ImU32 col_b = ImGui::GetColorU32(IM_COL32(255, 0, 0, 255)); + draw_list->AddRectFilledMultiColor(p0, p1, col_a, col_b, col_b, col_a); + ImGui::InvisibleButton("##gradient2", gradient_size); + } + + // Draw a bunch of primitives + ImGui::Text("All primitives"); static float sz = 36.0f; static float thickness = 3.0f; + static int ngon_sides = 6; + static bool circle_segments_override = false; + static int circle_segments_override_v = 12; + static bool curve_segments_override = false; + static int curve_segments_override_v = 8; static ImVec4 colf = ImVec4(1.0f, 1.0f, 0.4f, 1.0f); - ImGui::DragFloat("Size", &sz, 0.2f, 2.0f, 72.0f, "%.0f"); + ImGui::DragFloat("Size", &sz, 0.2f, 2.0f, 100.0f, "%.0f"); ImGui::DragFloat("Thickness", &thickness, 0.05f, 1.0f, 8.0f, "%.02f"); + ImGui::SliderInt("N-gon sides", &ngon_sides, 3, 12); + ImGui::Checkbox("##circlesegmentoverride", &circle_segments_override); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + circle_segments_override |= ImGui::SliderInt("Circle segments override", &circle_segments_override_v, 3, 40); + ImGui::Checkbox("##curvessegmentoverride", &curve_segments_override); + ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x); + curve_segments_override |= ImGui::SliderInt("Curves segments override", &curve_segments_override_v, 3, 40); ImGui::ColorEdit4("Color", &colf.x); + const ImVec2 p = ImGui::GetCursorScreenPos(); const ImU32 col = ImColor(colf); - float x = p.x + 4.0f, y = p.y + 4.0f; - float spacing = 10.0f; - ImDrawCornerFlags corners_none = 0; - ImDrawCornerFlags corners_all = ImDrawCornerFlags_All; - ImDrawCornerFlags corners_tl_br = ImDrawCornerFlags_TopLeft | ImDrawCornerFlags_BotRight; + const float spacing = 10.0f; + const ImDrawFlags corners_tl_br = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersBottomRight; + const float rounding = sz / 5.0f; + const int circle_segments = circle_segments_override ? circle_segments_override_v : 0; + const int curve_segments = curve_segments_override ? curve_segments_override_v : 0; + const ImVec2 cp3[3] = { ImVec2(0.0f, sz * 0.6f), ImVec2(sz * 0.5f, -sz * 0.4f), ImVec2(sz, sz) }; // Control points for curves + const ImVec2 cp4[4] = { ImVec2(0.0f, 0.0f), ImVec2(sz * 1.3f, sz * 0.3f), ImVec2(sz - sz * 1.3f, sz - sz * 0.3f), ImVec2(sz, sz) }; + + float x = p.x + 4.0f; + float y = p.y + 4.0f; for (int n = 0; n < 2; n++) { // First line uses a thickness of 1.0f, second line uses the configurable thickness float th = (n == 0) ? 1.0f : thickness; - draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, 6, th); x += sz + spacing; // Hexagon - draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, 20, th); x += sz + spacing; // Circle - draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 0.0f, corners_none, th); x += sz + spacing; // Square - draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_all, th); x += sz + spacing; // Square with all rounded corners - draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_tl_br, th); x += sz + spacing; // Square with two rounded corners - draw_list->AddTriangle(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col, th); x += sz + spacing; // Triangle - draw_list->AddTriangle(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col, th); x += sz*0.4f + spacing; // Thin triangle - draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y), col, th); x += sz + spacing; // Horizontal line (note: drawing a filled rectangle will be faster!) - draw_list->AddLine(ImVec2(x, y), ImVec2(x, y + sz), col, th); x += spacing; // Vertical line (note: drawing a filled rectangle will be faster!) - draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y + sz), col, th); x += sz + spacing; // Diagonal line - draw_list->AddBezierCurve(ImVec2(x, y), ImVec2(x + sz*1.3f, y + sz*0.3f), ImVec2(x + sz - sz*1.3f, y + sz - sz*0.3f), ImVec2(x + sz, y + sz), col, th); + draw_list->AddNgon(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, ngon_sides, th); x += sz + spacing; // N-gon + draw_list->AddCircle(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, circle_segments, th); x += sz + spacing; // Circle + draw_list->AddEllipse(ImVec2(x + sz*0.5f, y + sz*0.5f), ImVec2(sz*0.5f, sz*0.3f), col, -0.3f, circle_segments, th); x += sz + spacing; // Ellipse + draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 0.0f, ImDrawFlags_None, th); x += sz + spacing; // Square + draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, ImDrawFlags_None, th); x += sz + spacing; // Square with all rounded corners + draw_list->AddRect(ImVec2(x, y), ImVec2(x + sz, y + sz), col, rounding, corners_tl_br, th); x += sz + spacing; // Square with two rounded corners + draw_list->AddTriangle(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col, th);x += sz + spacing; // Triangle + //draw_list->AddTriangle(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col, th);x+= sz*0.4f + spacing; // Thin triangle + PathConcaveShape(draw_list, x, y, sz); draw_list->PathStroke(col, ImDrawFlags_Closed, th); x += sz + spacing; // Concave Shape + //draw_list->AddPolyline(concave_shape, IM_ARRAYSIZE(concave_shape), col, ImDrawFlags_Closed, th); + draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y), col, th); x += sz + spacing; // Horizontal line (note: drawing a filled rectangle will be faster!) + draw_list->AddLine(ImVec2(x, y), ImVec2(x, y + sz), col, th); x += spacing; // Vertical line (note: drawing a filled rectangle will be faster!) + draw_list->AddLine(ImVec2(x, y), ImVec2(x + sz, y + sz), col, th); x += sz + spacing; // Diagonal line + + // Path + draw_list->PathArcTo(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, 3.141592f, 3.141592f * -0.5f); + draw_list->PathStroke(col, ImDrawFlags_None, th); + x += sz + spacing; + + // Quadratic Bezier Curve (3 control points) + draw_list->AddBezierQuadratic(ImVec2(x + cp3[0].x, y + cp3[0].y), ImVec2(x + cp3[1].x, y + cp3[1].y), ImVec2(x + cp3[2].x, y + cp3[2].y), col, th, curve_segments); + x += sz + spacing; + + // Cubic Bezier Curve (4 control points) + draw_list->AddBezierCubic(ImVec2(x + cp4[0].x, y + cp4[0].y), ImVec2(x + cp4[1].x, y + cp4[1].y), ImVec2(x + cp4[2].x, y + cp4[2].y), ImVec2(x + cp4[3].x, y + cp4[3].y), col, th, curve_segments); + x = p.x + 4; y += sz + spacing; } - draw_list->AddCircleFilled(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, 6); x += sz + spacing; // Hexagon - draw_list->AddCircleFilled(ImVec2(x + sz*0.5f, y + sz*0.5f), sz*0.5f, col, 32); x += sz + spacing; // Circle - draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col); x += sz + spacing; // Square - draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f); x += sz + spacing; // Square with all rounded corners - draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_tl_br); x += sz + spacing; // Square with two rounded corners - draw_list->AddTriangleFilled(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col); x += sz + spacing; // Triangle - draw_list->AddTriangleFilled(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col); x += sz*0.4f + spacing; // Thin triangle - draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + thickness), col); x += sz + spacing; // Horizontal line (faster than AddLine, but only handle integer thickness) - draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + thickness, y + sz), col); x += spacing*2.0f; // Vertical line (faster than AddLine, but only handle integer thickness) - draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + 1, y + 1), col); x += sz; // Pixel (faster than AddLine) + + // Filled shapes + draw_list->AddNgonFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, col, ngon_sides); x += sz + spacing; // N-gon + draw_list->AddCircleFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, col, circle_segments); x += sz + spacing; // Circle + draw_list->AddEllipseFilled(ImVec2(x + sz * 0.5f, y + sz * 0.5f), ImVec2(sz * 0.5f, sz * 0.3f), col, -0.3f, circle_segments); x += sz + spacing;// Ellipse + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col); x += sz + spacing; // Square + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f); x += sz + spacing; // Square with all rounded corners + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + sz), col, 10.0f, corners_tl_br); x += sz + spacing; // Square with two rounded corners + draw_list->AddTriangleFilled(ImVec2(x+sz*0.5f,y), ImVec2(x+sz, y+sz-0.5f), ImVec2(x, y+sz-0.5f), col); x += sz + spacing; // Triangle + //draw_list->AddTriangleFilled(ImVec2(x+sz*0.2f,y), ImVec2(x, y+sz-0.5f), ImVec2(x+sz*0.4f, y+sz-0.5f), col); x += sz*0.4f + spacing; // Thin triangle + PathConcaveShape(draw_list, x, y, sz); draw_list->PathFillConcave(col); x += sz + spacing; // Concave shape + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + sz, y + thickness), col); x += sz + spacing; // Horizontal line (faster than AddLine, but only handle integer thickness) + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + thickness, y + sz), col); x += spacing * 2.0f;// Vertical line (faster than AddLine, but only handle integer thickness) + draw_list->AddRectFilled(ImVec2(x, y), ImVec2(x + 1, y + 1), col); x += sz; // Pixel (faster than AddLine) + + // Path + draw_list->PathArcTo(ImVec2(x + sz * 0.5f, y + sz * 0.5f), sz * 0.5f, 3.141592f * -0.5f, 3.141592f); + draw_list->PathFillConvex(col); + x += sz + spacing; + + // Quadratic Bezier Curve (3 control points) + draw_list->PathLineTo(ImVec2(x + cp3[0].x, y + cp3[0].y)); + draw_list->PathBezierQuadraticCurveTo(ImVec2(x + cp3[1].x, y + cp3[1].y), ImVec2(x + cp3[2].x, y + cp3[2].y), curve_segments); + draw_list->PathFillConvex(col); + x += sz + spacing; + draw_list->AddRectFilledMultiColor(ImVec2(x, y), ImVec2(x + sz, y + sz), IM_COL32(0, 0, 0, 255), IM_COL32(255, 0, 0, 255), IM_COL32(255, 255, 0, 255), IM_COL32(0, 255, 0, 255)); - ImGui::Dummy(ImVec2((sz + spacing) * 9.8f, (sz + spacing) * 3)); + x += sz + spacing; + + ImGui::Dummy(ImVec2((sz + spacing) * 13.2f, (sz + spacing) * 3.0f)); + ImGui::PopItemWidth(); ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Canvas")) { static ImVector points; + static ImVec2 scrolling(0.0f, 0.0f); + static bool opt_enable_grid = true; + static bool opt_enable_context_menu = true; static bool adding_line = false; - if (ImGui::Button("Clear")) points.clear(); - if (points.Size >= 2) { ImGui::SameLine(); if (ImGui::Button("Undo")) { points.pop_back(); points.pop_back(); } } - ImGui::Text("Left-click and drag to add lines,\nRight-click to undo"); - // Here we are using InvisibleButton() as a convenience to 1) advance the cursor and 2) allows us to use IsItemHovered() - // But you can also draw directly and poll mouse/keyboard by yourself. You can manipulate the cursor using GetCursorPos() and SetCursorPos(). - // If you only use the ImDrawList API, you can notify the owner window of its extends by using SetCursorPos(max). - ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); // ImDrawList API uses screen coordinates! - ImVec2 canvas_size = ImGui::GetContentRegionAvail(); // Resize canvas to what's available - if (canvas_size.x < 50.0f) canvas_size.x = 50.0f; - if (canvas_size.y < 50.0f) canvas_size.y = 50.0f; - draw_list->AddRectFilledMultiColor(canvas_pos, ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y), IM_COL32(50, 50, 50, 255), IM_COL32(50, 50, 60, 255), IM_COL32(60, 60, 70, 255), IM_COL32(50, 50, 60, 255)); - draw_list->AddRect(canvas_pos, ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y), IM_COL32(255, 255, 255, 255)); + ImGui::Checkbox("Enable grid", &opt_enable_grid); + ImGui::Checkbox("Enable context menu", &opt_enable_context_menu); + ImGui::Text("Mouse Left: drag to add lines,\nMouse Right: drag to scroll, click for context menu."); - bool adding_preview = false; - ImGui::InvisibleButton("canvas", canvas_size); - ImVec2 mouse_pos_in_canvas = ImVec2(ImGui::GetIO().MousePos.x - canvas_pos.x, ImGui::GetIO().MousePos.y - canvas_pos.y); + // Typically you would use a BeginChild()/EndChild() pair to benefit from a clipping region + own scrolling. + // Here we demonstrate that this can be replaced by simple offsetting + custom drawing + PushClipRect/PopClipRect() calls. + // To use a child window instead we could use, e.g: + // ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Disable padding + // ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(50, 50, 50, 255)); // Set a background color + // ImGui::BeginChild("canvas", ImVec2(0.0f, 0.0f), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove); + // ImGui::PopStyleColor(); + // ImGui::PopStyleVar(); + // [...] + // ImGui::EndChild(); + + // Using InvisibleButton() as a convenience 1) it will advance the layout cursor and 2) allows us to use IsItemHovered()/IsItemActive() + ImVec2 canvas_p0 = ImGui::GetCursorScreenPos(); // ImDrawList API uses screen coordinates! + ImVec2 canvas_sz = ImGui::GetContentRegionAvail(); // Resize canvas to what's available + if (canvas_sz.x < 50.0f) canvas_sz.x = 50.0f; + if (canvas_sz.y < 50.0f) canvas_sz.y = 50.0f; + ImVec2 canvas_p1 = ImVec2(canvas_p0.x + canvas_sz.x, canvas_p0.y + canvas_sz.y); + + // Draw border and background color + ImGuiIO& io = ImGui::GetIO(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + draw_list->AddRectFilled(canvas_p0, canvas_p1, IM_COL32(50, 50, 50, 255)); + draw_list->AddRect(canvas_p0, canvas_p1, IM_COL32(255, 255, 255, 255)); + + // This will catch our interactions + ImGui::InvisibleButton("canvas", canvas_sz, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight); + const bool is_hovered = ImGui::IsItemHovered(); // Hovered + const bool is_active = ImGui::IsItemActive(); // Held + const ImVec2 origin(canvas_p0.x + scrolling.x, canvas_p0.y + scrolling.y); // Lock scrolled origin + const ImVec2 mouse_pos_in_canvas(io.MousePos.x - origin.x, io.MousePos.y - origin.y); + + // Add first and second point + if (is_hovered && !adding_line && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + { + points.push_back(mouse_pos_in_canvas); + points.push_back(mouse_pos_in_canvas); + adding_line = true; + } if (adding_line) { - adding_preview = true; - points.push_back(mouse_pos_in_canvas); - if (!ImGui::IsMouseDown(0)) - adding_line = adding_preview = false; + points.back() = mouse_pos_in_canvas; + if (!ImGui::IsMouseDown(ImGuiMouseButton_Left)) + adding_line = false; } - if (ImGui::IsItemHovered()) + + // Pan (we use a zero mouse threshold when there's no context menu) + // You may decide to make that threshold dynamic based on whether the mouse is hovering something etc. + const float mouse_threshold_for_pan = opt_enable_context_menu ? -1.0f : 0.0f; + if (is_active && ImGui::IsMouseDragging(ImGuiMouseButton_Right, mouse_threshold_for_pan)) { - if (!adding_line && ImGui::IsMouseClicked(0)) - { - points.push_back(mouse_pos_in_canvas); - adding_line = true; - } - if (ImGui::IsMouseClicked(1) && !points.empty()) - { - adding_line = adding_preview = false; - points.pop_back(); - points.pop_back(); - } + scrolling.x += io.MouseDelta.x; + scrolling.y += io.MouseDelta.y; } - draw_list->PushClipRect(canvas_pos, ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y), true); // clip lines within the canvas (if we resize it, etc.) - for (int i = 0; i < points.Size - 1; i += 2) - draw_list->AddLine(ImVec2(canvas_pos.x + points[i].x, canvas_pos.y + points[i].y), ImVec2(canvas_pos.x + points[i + 1].x, canvas_pos.y + points[i + 1].y), IM_COL32(255, 255, 0, 255), 2.0f); + + // Context menu (under default mouse threshold) + ImVec2 drag_delta = ImGui::GetMouseDragDelta(ImGuiMouseButton_Right); + if (opt_enable_context_menu && drag_delta.x == 0.0f && drag_delta.y == 0.0f) + ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); + if (ImGui::BeginPopup("context")) + { + if (adding_line) + points.resize(points.size() - 2); + adding_line = false; + if (ImGui::MenuItem("Remove one", NULL, false, points.Size > 0)) { points.resize(points.size() - 2); } + if (ImGui::MenuItem("Remove all", NULL, false, points.Size > 0)) { points.clear(); } + ImGui::EndPopup(); + } + + // Draw grid + all lines in the canvas + draw_list->PushClipRect(canvas_p0, canvas_p1, true); + if (opt_enable_grid) + { + const float GRID_STEP = 64.0f; + for (float x = fmodf(scrolling.x, GRID_STEP); x < canvas_sz.x; x += GRID_STEP) + draw_list->AddLine(ImVec2(canvas_p0.x + x, canvas_p0.y), ImVec2(canvas_p0.x + x, canvas_p1.y), IM_COL32(200, 200, 200, 40)); + for (float y = fmodf(scrolling.y, GRID_STEP); y < canvas_sz.y; y += GRID_STEP) + draw_list->AddLine(ImVec2(canvas_p0.x, canvas_p0.y + y), ImVec2(canvas_p1.x, canvas_p0.y + y), IM_COL32(200, 200, 200, 40)); + } + for (int n = 0; n < points.Size; n += 2) + draw_list->AddLine(ImVec2(origin.x + points[n].x, origin.y + points[n].y), ImVec2(origin.x + points[n + 1].x, origin.y + points[n + 1].y), IM_COL32(255, 255, 0, 255), 2.0f); draw_list->PopClipRect(); - if (adding_preview) - points.pop_back(); + ImGui::EndTabItem(); } @@ -4331,14 +10002,53 @@ static void ShowExampleAppCustomRendering(bool* p_open) static bool draw_bg = true; static bool draw_fg = true; ImGui::Checkbox("Draw in Background draw list", &draw_bg); + ImGui::SameLine(); HelpMarker("The Background draw list will be rendered below every Dear ImGui windows."); ImGui::Checkbox("Draw in Foreground draw list", &draw_fg); + ImGui::SameLine(); HelpMarker("The Foreground draw list will be rendered over every Dear ImGui windows."); ImVec2 window_pos = ImGui::GetWindowPos(); ImVec2 window_size = ImGui::GetWindowSize(); ImVec2 window_center = ImVec2(window_pos.x + window_size.x * 0.5f, window_pos.y + window_size.y * 0.5f); if (draw_bg) - ImGui::GetBackgroundDrawList()->AddCircle(window_center, window_size.x * 0.6f, IM_COL32(255, 0, 0, 200), 32, 10+4); + ImGui::GetBackgroundDrawList()->AddCircle(window_center, window_size.x * 0.6f, IM_COL32(255, 0, 0, 200), 0, 10 + 4); if (draw_fg) - ImGui::GetForegroundDrawList()->AddCircle(window_center, window_size.y * 0.6f, IM_COL32(0, 255, 0, 200), 32, 10); + ImGui::GetForegroundDrawList()->AddCircle(window_center, window_size.y * 0.6f, IM_COL32(0, 255, 0, 200), 0, 10); + ImGui::EndTabItem(); + } + + // Demonstrate out-of-order rendering via channels splitting + // We use functions in ImDrawList as each draw list contains a convenience splitter, + // but you can also instantiate your own ImDrawListSplitter if you need to nest them. + if (ImGui::BeginTabItem("Draw Channels")) + { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + { + ImGui::Text("Blue shape is drawn first: appears in back"); + ImGui::Text("Red shape is drawn after: appears in front"); + ImVec2 p0 = ImGui::GetCursorScreenPos(); + draw_list->AddRectFilled(ImVec2(p0.x, p0.y), ImVec2(p0.x + 50, p0.y + 50), IM_COL32(0, 0, 255, 255)); // Blue + draw_list->AddRectFilled(ImVec2(p0.x + 25, p0.y + 25), ImVec2(p0.x + 75, p0.y + 75), IM_COL32(255, 0, 0, 255)); // Red + ImGui::Dummy(ImVec2(75, 75)); + } + ImGui::Separator(); + { + ImGui::Text("Blue shape is drawn first, into channel 1: appears in front"); + ImGui::Text("Red shape is drawn after, into channel 0: appears in back"); + ImVec2 p1 = ImGui::GetCursorScreenPos(); + + // Create 2 channels and draw a Blue shape THEN a Red shape. + // You can create any number of channels. Tables API use 1 channel per column in order to better batch draw calls. + draw_list->ChannelsSplit(2); + draw_list->ChannelsSetCurrent(1); + draw_list->AddRectFilled(ImVec2(p1.x, p1.y), ImVec2(p1.x + 50, p1.y + 50), IM_COL32(0, 0, 255, 255)); // Blue + draw_list->ChannelsSetCurrent(0); + draw_list->AddRectFilled(ImVec2(p1.x + 25, p1.y + 25), ImVec2(p1.x + 75, p1.y + 75), IM_COL32(255, 0, 0, 255)); // Red + + // Flatten/reorder channels. Red shape is in channel 0 and it appears below the Blue shape in channel 1. + // This works by copying draw indices only (vertices are not copied). + draw_list->ChannelsMerge(); + ImGui::Dummy(ImVec2(75, 75)); + ImGui::Text("After reordering, contents of channel 0 appears below channel 1."); + } ImGui::EndTabItem(); } @@ -4348,6 +10058,134 @@ static void ShowExampleAppCustomRendering(bool* p_open) ImGui::End(); } +//----------------------------------------------------------------------------- +// [SECTION] Example App: Docking, DockSpace / ShowExampleAppDockSpace() +//----------------------------------------------------------------------------- + +// Demonstrate using DockSpace() to create an explicit docking node within an existing window. +// Note: You can use most Docking facilities without calling any API. You DO NOT need to call DockSpace() to use Docking! +// - Drag from window title bar or their tab to dock/undock. Hold SHIFT to disable docking. +// - Drag from window menu button (upper-left button) to undock an entire node (all windows). +// - When io.ConfigDockingWithShift == true, you instead need to hold SHIFT to enable docking. +// About dockspaces: +// - Use DockSpace() to create an explicit dock node _within_ an existing window. +// - Use DockSpaceOverViewport() to create an explicit dock node covering the screen or a specific viewport. +// This is often used with ImGuiDockNodeFlags_PassthruCentralNode. +// - Important: Dockspaces need to be submitted _before_ any window they can host. Submit it early in your frame! (*) +// - Important: Dockspaces need to be kept alive if hidden, otherwise windows docked into it will be undocked. +// e.g. if you have multiple tabs with a dockspace inside each tab: submit the non-visible dockspaces with ImGuiDockNodeFlags_KeepAliveOnly. +// (*) because of this constraint, the implicit \"Debug\" window can not be docked into an explicit DockSpace() node, +// because that window is submitted as part of the part of the NewFrame() call. An easy workaround is that you can create +// your own implicit "Debug##2" window after calling DockSpace() and leave it in the window stack for anyone to use. +void ShowExampleAppDockSpace(bool* p_open) +{ + // READ THIS !!! + // TL;DR; this demo is more complicated than what most users you would normally use. + // If we remove all options we are showcasing, this demo would become: + // void ShowExampleAppDockSpace() + // { + // ImGui::DockSpaceOverViewport(0, ImGui::GetMainViewport()); + // } + // In most cases you should be able to just call DockSpaceOverViewport() and ignore all the code below! + // In this specific demo, we are not using DockSpaceOverViewport() because: + // - (1) we allow the host window to be floating/moveable instead of filling the viewport (when opt_fullscreen == false) + // - (2) we allow the host window to have padding (when opt_padding == true) + // - (3) we expose many flags and need a way to have them visible. + // - (4) we have a local menu bar in the host window (vs. you could use BeginMainMenuBar() + DockSpaceOverViewport() + // in your code, but we don't here because we allow the window to be floating) + + static bool opt_fullscreen = true; + static bool opt_padding = false; + static ImGuiDockNodeFlags dockspace_flags = ImGuiDockNodeFlags_None; + + // We are using the ImGuiWindowFlags_NoDocking flag to make the parent window not dockable into, + // because it would be confusing to have two docking targets within each others. + ImGuiWindowFlags window_flags = ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking; + if (opt_fullscreen) + { + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->WorkPos); + ImGui::SetNextWindowSize(viewport->WorkSize); + ImGui::SetNextWindowViewport(viewport->ID); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; + } + else + { + dockspace_flags &= ~ImGuiDockNodeFlags_PassthruCentralNode; + } + + // When using ImGuiDockNodeFlags_PassthruCentralNode, DockSpace() will render our background + // and handle the pass-thru hole, so we ask Begin() to not render a background. + if (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode) + window_flags |= ImGuiWindowFlags_NoBackground; + + // Important: note that we proceed even if Begin() returns false (aka window is collapsed). + // This is because we want to keep our DockSpace() active. If a DockSpace() is inactive, + // all active windows docked into it will lose their parent and become undocked. + // We cannot preserve the docking relationship between an active window and an inactive docking, otherwise + // any change of dockspace/settings would lead to windows being stuck in limbo and never being visible. + if (!opt_padding) + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); + ImGui::Begin("DockSpace Demo", p_open, window_flags); + if (!opt_padding) + ImGui::PopStyleVar(); + + if (opt_fullscreen) + ImGui::PopStyleVar(2); + + // Submit the DockSpace + ImGuiIO& io = ImGui::GetIO(); + if (io.ConfigFlags & ImGuiConfigFlags_DockingEnable) + { + ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); + ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags); + } + else + { + ShowDockingDisabledMessage(); + } + + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("Options")) + { + // Disabling fullscreen would allow the window to be moved to the front of other windows, + // which we can't undo at the moment without finer window depth/z control. + ImGui::MenuItem("Fullscreen", NULL, &opt_fullscreen); + ImGui::MenuItem("Padding", NULL, &opt_padding); + ImGui::Separator(); + + if (ImGui::MenuItem("Flag: NoDockingOverCentralNode", "", (dockspace_flags & ImGuiDockNodeFlags_NoDockingOverCentralNode) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_NoDockingOverCentralNode; } + if (ImGui::MenuItem("Flag: NoDockingSplit", "", (dockspace_flags & ImGuiDockNodeFlags_NoDockingSplit) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_NoDockingSplit; } + if (ImGui::MenuItem("Flag: NoUndocking", "", (dockspace_flags & ImGuiDockNodeFlags_NoUndocking) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_NoUndocking; } + if (ImGui::MenuItem("Flag: NoResize", "", (dockspace_flags & ImGuiDockNodeFlags_NoResize) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_NoResize; } + if (ImGui::MenuItem("Flag: AutoHideTabBar", "", (dockspace_flags & ImGuiDockNodeFlags_AutoHideTabBar) != 0)) { dockspace_flags ^= ImGuiDockNodeFlags_AutoHideTabBar; } + if (ImGui::MenuItem("Flag: PassthruCentralNode", "", (dockspace_flags & ImGuiDockNodeFlags_PassthruCentralNode) != 0, opt_fullscreen)) { dockspace_flags ^= ImGuiDockNodeFlags_PassthruCentralNode; } + ImGui::Separator(); + + if (ImGui::MenuItem("Close", NULL, false, p_open != NULL)) + *p_open = false; + ImGui::EndMenu(); + } + HelpMarker( + "When docking is enabled, you can ALWAYS dock MOST window into another! Try it now!" "\n" + "- Drag from window title bar or their tab to dock/undock." "\n" + "- Drag from window menu button (upper-left button) to undock an entire node (all windows)." "\n" + "- Hold SHIFT to disable docking (if io.ConfigDockingWithShift == false, default)" "\n" + "- Hold SHIFT to enable docking (if io.ConfigDockingWithShift == true)" "\n" + "This demo app has nothing to do with enabling docking!" "\n\n" + "This demo app only demonstrate the use of ImGui::DockSpace() which allows you to manually create a docking node _within_ another window." "\n\n" + "Read comments in ShowExampleAppDockSpace() for more details."); + + ImGui::EndMenuBar(); + } + + ImGui::End(); +} + //----------------------------------------------------------------------------- // [SECTION] Example App: Documents Handling / ShowExampleAppDocuments() //----------------------------------------------------------------------------- @@ -4355,101 +10193,143 @@ static void ShowExampleAppCustomRendering(bool* p_open) // Simplified structure to mimic a Document model struct MyDocument { - const char* Name; // Document title - bool Open; // Set when the document is open (in this demo, we keep an array of all available documents to simplify the demo) - bool OpenPrev; // Copy of Open from last update. - bool Dirty; // Set when the document has been modified - bool WantClose; // Set when the document - ImVec4 Color; // An arbitrary variable associated to the document + char Name[32]; // Document title + int UID; // Unique ID (necessary as we can change title) + bool Open; // Set when open (we keep an array of all available documents to simplify demo code!) + bool OpenPrev; // Copy of Open from last update. + bool Dirty; // Set when the document has been modified + ImVec4 Color; // An arbitrary variable associated to the document - MyDocument(const char* name, bool open = true, const ImVec4& color = ImVec4(1.0f,1.0f,1.0f,1.0f)) + MyDocument(int uid, const char* name, bool open = true, const ImVec4& color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f)) { - Name = name; + UID = uid; + snprintf(Name, sizeof(Name), "%s", name); Open = OpenPrev = open; Dirty = false; - WantClose = false; Color = color; } void DoOpen() { Open = true; } - void DoQueueClose() { WantClose = true; } void DoForceClose() { Open = false; Dirty = false; } void DoSave() { Dirty = false; } +}; - // Display dummy contents for the Document - static void DisplayContents(MyDocument* doc) +struct ExampleAppDocuments +{ + ImVector Documents; + ImVector CloseQueue; + MyDocument* RenamingDoc = NULL; + bool RenamingStarted = false; + + ExampleAppDocuments() + { + Documents.push_back(MyDocument(0, "Lettuce", true, ImVec4(0.4f, 0.8f, 0.4f, 1.0f))); + Documents.push_back(MyDocument(1, "Eggplant", true, ImVec4(0.8f, 0.5f, 1.0f, 1.0f))); + Documents.push_back(MyDocument(2, "Carrot", true, ImVec4(1.0f, 0.8f, 0.5f, 1.0f))); + Documents.push_back(MyDocument(3, "Tomato", false, ImVec4(1.0f, 0.3f, 0.4f, 1.0f))); + Documents.push_back(MyDocument(4, "A Rather Long Title", false, ImVec4(0.4f, 0.8f, 0.8f, 1.0f))); + Documents.push_back(MyDocument(5, "Some Document", false, ImVec4(0.8f, 0.8f, 1.0f, 1.0f))); + } + + // As we allow to change document name, we append a never-changing document ID so tabs are stable + void GetTabName(MyDocument* doc, char* out_buf, size_t out_buf_size) + { + snprintf(out_buf, out_buf_size, "%s###doc%d", doc->Name, doc->UID); + } + + // Display placeholder contents for the Document + void DisplayDocContents(MyDocument* doc) { ImGui::PushID(doc); ImGui::Text("Document \"%s\"", doc->Name); ImGui::PushStyleColor(ImGuiCol_Text, doc->Color); ImGui::TextWrapped("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."); ImGui::PopStyleColor(); - if (ImGui::Button("Modify", ImVec2(100, 0))) - doc->Dirty = true; + + ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_R, ImGuiInputFlags_Tooltip); + if (ImGui::Button("Rename..")) + { + RenamingDoc = doc; + RenamingStarted = true; + } ImGui::SameLine(); - if (ImGui::Button("Save", ImVec2(100, 0))) + + ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_M, ImGuiInputFlags_Tooltip); + if (ImGui::Button("Modify")) + doc->Dirty = true; + + ImGui::SameLine(); + ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_S, ImGuiInputFlags_Tooltip); + if (ImGui::Button("Save")) doc->DoSave(); + + ImGui::SameLine(); + ImGui::SetNextItemShortcut(ImGuiMod_Ctrl | ImGuiKey_W, ImGuiInputFlags_Tooltip); + if (ImGui::Button("Close")) + CloseQueue.push_back(doc); ImGui::ColorEdit3("color", &doc->Color.x); // Useful to test drag and drop and hold-dragged-to-open-tab behavior. ImGui::PopID(); } // Display context menu for the Document - static void DisplayContextMenu(MyDocument* doc) + void DisplayDocContextMenu(MyDocument* doc) { if (!ImGui::BeginPopupContextItem()) return; char buf[256]; sprintf(buf, "Save %s", doc->Name); - if (ImGui::MenuItem(buf, "CTRL+S", false, doc->Open)) + if (ImGui::MenuItem(buf, "Ctrl+S", false, doc->Open)) doc->DoSave(); - if (ImGui::MenuItem("Close", "CTRL+W", false, doc->Open)) - doc->DoQueueClose(); + if (ImGui::MenuItem("Rename...", "Ctrl+R", false, doc->Open)) + RenamingDoc = doc; + if (ImGui::MenuItem("Close", "Ctrl+W", false, doc->Open)) + CloseQueue.push_back(doc); ImGui::EndPopup(); } -}; -struct ExampleAppDocuments -{ - ImVector Documents; - - ExampleAppDocuments() + // [Optional] Notify the system of Tabs/Windows closure that happened outside the regular tab interface. + // If a tab has been closed programmatically (aka closed from another source such as the Checkbox() in the demo, + // as opposed to clicking on the regular tab closing button) and stops being submitted, it will take a frame for + // the tab bar to notice its absence. During this frame there will be a gap in the tab bar, and if the tab that has + // disappeared was the selected one, the tab bar will report no selected tab during the frame. This will effectively + // give the impression of a flicker for one frame. + // We call SetTabItemClosed() to manually notify the Tab Bar or Docking system of removed tabs to avoid this glitch. + // Note that this completely optional, and only affect tab bars with the ImGuiTabBarFlags_Reorderable flag. + void NotifyOfDocumentsClosedElsewhere() { - Documents.push_back(MyDocument("Lettuce", true, ImVec4(0.4f, 0.8f, 0.4f, 1.0f))); - Documents.push_back(MyDocument("Eggplant", true, ImVec4(0.8f, 0.5f, 1.0f, 1.0f))); - Documents.push_back(MyDocument("Carrot", true, ImVec4(1.0f, 0.8f, 0.5f, 1.0f))); - Documents.push_back(MyDocument("Tomato", false, ImVec4(1.0f, 0.3f, 0.4f, 1.0f))); - Documents.push_back(MyDocument("A Rather Long Title", false)); - Documents.push_back(MyDocument("Some Document", false)); + for (MyDocument& doc : Documents) + { + if (!doc.Open && doc.OpenPrev) + ImGui::SetTabItemClosed(doc.Name); + doc.OpenPrev = doc.Open; + } } }; -// [Optional] Notify the system of Tabs/Windows closure that happened outside the regular tab interface. -// If a tab has been closed programmatically (aka closed from another source such as the Checkbox() in the demo, as opposed -// to clicking on the regular tab closing button) and stops being submitted, it will take a frame for the tab bar to notice its absence. -// During this frame there will be a gap in the tab bar, and if the tab that has disappeared was the selected one, the tab bar -// will report no selected tab during the frame. This will effectively give the impression of a flicker for one frame. -// We call SetTabItemClosed() to manually notify the Tab Bar or Docking system of removed tabs to avoid this glitch. -// Note that this completely optional, and only affect tab bars with the ImGuiTabBarFlags_Reorderable flag. -static void NotifyOfDocumentsClosedElsewhere(ExampleAppDocuments& app) -{ - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) - { - MyDocument* doc = &app.Documents[doc_n]; - if (!doc->Open && doc->OpenPrev) - ImGui::SetTabItemClosed(doc->Name); - doc->OpenPrev = doc->Open; - } -} - void ShowExampleAppDocuments(bool* p_open) { static ExampleAppDocuments app; // Options + enum Target + { + Target_None, + Target_Tab, // Create documents as local tab into a local tab bar + Target_DockSpaceAndWindow // Create documents as regular windows, and create an embedded dockspace + }; + static Target opt_target = Target_Tab; static bool opt_reorderable = true; static ImGuiTabBarFlags opt_fitting_flags = ImGuiTabBarFlags_FittingPolicyDefault_; - if (!ImGui::Begin("Example: Documents", p_open, ImGuiWindowFlags_MenuBar)) + // When (opt_target == Target_DockSpaceAndWindow) there is the possibily that one of our child Document window (e.g. "Eggplant") + // that we emit gets docked into the same spot as the parent window ("Example: Documents"). + // This would create a problematic feedback loop because selecting the "Eggplant" tab would make the "Example: Documents" tab + // not visible, which in turn would stop submitting the "Eggplant" window. + // We avoid this problem by submitting our documents window even if our parent window is not currently visible. + // Another solution may be to make the "Example: Documents" window use the ImGuiWindowFlags_NoDocking. + + bool window_contents_visible = ImGui::Begin("Example: Documents", p_open, ImGuiWindowFlags_MenuBar); + if (!window_contents_visible && opt_target != Target_DockSpaceAndWindow) { ImGui::End(); return; @@ -4461,24 +10341,21 @@ void ShowExampleAppDocuments(bool* p_open) if (ImGui::BeginMenu("File")) { int open_count = 0; - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) - open_count += app.Documents[doc_n].Open ? 1 : 0; + for (MyDocument& doc : app.Documents) + open_count += doc.Open ? 1 : 0; if (ImGui::BeginMenu("Open", open_count < app.Documents.Size)) { - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) - { - MyDocument* doc = &app.Documents[doc_n]; - if (!doc->Open) - if (ImGui::MenuItem(doc->Name)) - doc->DoOpen(); - } + for (MyDocument& doc : app.Documents) + if (!doc.Open && ImGui::MenuItem(doc.Name)) + doc.DoOpen(); ImGui::EndMenu(); } if (ImGui::MenuItem("Close All Documents", NULL, false, open_count > 0)) - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) - app.Documents[doc_n].DoQueueClose(); - if (ImGui::MenuItem("Exit", "Alt+F4")) {} + for (MyDocument& doc : app.Documents) + app.CloseQueue.push_back(&doc); + if (ImGui::MenuItem("Exit") && p_open) + *p_open = false; ImGui::EndMenu(); } ImGui::EndMenuBar(); @@ -4487,51 +10364,71 @@ void ShowExampleAppDocuments(bool* p_open) // [Debug] List documents with one checkbox for each for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) { - MyDocument* doc = &app.Documents[doc_n]; + MyDocument& doc = app.Documents[doc_n]; if (doc_n > 0) ImGui::SameLine(); - ImGui::PushID(doc); - if (ImGui::Checkbox(doc->Name, &doc->Open)) - if (!doc->Open) - doc->DoForceClose(); + ImGui::PushID(&doc); + if (ImGui::Checkbox(doc.Name, &doc.Open)) + if (!doc.Open) + doc.DoForceClose(); ImGui::PopID(); } + ImGui::PushItemWidth(ImGui::GetFontSize() * 12); + ImGui::Combo("Output", (int*)&opt_target, "None\0TabBar+Tabs\0DockSpace+Window\0"); + ImGui::PopItemWidth(); + bool redock_all = false; + if (opt_target == Target_Tab) { ImGui::SameLine(); ImGui::Checkbox("Reorderable Tabs", &opt_reorderable); } + if (opt_target == Target_DockSpaceAndWindow) { ImGui::SameLine(); redock_all = ImGui::Button("Redock all"); } ImGui::Separator(); - // Submit Tab Bar and Tabs + // About the ImGuiWindowFlags_UnsavedDocument / ImGuiTabItemFlags_UnsavedDocument flags. + // They have multiple effects: + // - Display a dot next to the title. + // - Tab is selected when clicking the X close button. + // - Closure is not assumed (will wait for user to stop submitting the tab). + // Otherwise closure is assumed when pressing the X, so if you keep submitting the tab may reappear at end of tab bar. + // We need to assume closure by default otherwise waiting for "lack of submission" on the next frame would leave an empty + // hole for one-frame, both in the tab-bar and in tab-contents when closing a tab/window. + // The rarely used SetTabItemClosed() function is a way to notify of programmatic closure to avoid the one-frame hole. + + // Tabs + if (opt_target == Target_Tab) { ImGuiTabBarFlags tab_bar_flags = (opt_fitting_flags) | (opt_reorderable ? ImGuiTabBarFlags_Reorderable : 0); + tab_bar_flags |= ImGuiTabBarFlags_DrawSelectedOverline; if (ImGui::BeginTabBar("##tabs", tab_bar_flags)) { if (opt_reorderable) - NotifyOfDocumentsClosedElsewhere(app); + app.NotifyOfDocumentsClosedElsewhere(); // [DEBUG] Stress tests //if ((ImGui::GetFrameCount() % 30) == 0) docs[1].Open ^= 1; // [DEBUG] Automatically show/hide a tab. Test various interactions e.g. dragging with this on. //if (ImGui::GetIO().KeyCtrl) ImGui::SetTabItemSelected(docs[1].Name); // [DEBUG] Test SetTabItemSelected(), probably not very useful as-is anyway.. // Submit Tabs - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + for (MyDocument& doc : app.Documents) { - MyDocument* doc = &app.Documents[doc_n]; - if (!doc->Open) + if (!doc.Open) continue; - ImGuiTabItemFlags tab_flags = (doc->Dirty ? ImGuiTabItemFlags_UnsavedDocument : 0); - bool visible = ImGui::BeginTabItem(doc->Name, &doc->Open, tab_flags); + // As we allow to change document name, we append a never-changing document id so tabs are stable + char doc_name_buf[64]; + app.GetTabName(&doc, doc_name_buf, sizeof(doc_name_buf)); + ImGuiTabItemFlags tab_flags = (doc.Dirty ? ImGuiTabItemFlags_UnsavedDocument : 0); + bool visible = ImGui::BeginTabItem(doc_name_buf, &doc.Open, tab_flags); // Cancel attempt to close when unsaved add to save queue so we can display a popup. - if (!doc->Open && doc->Dirty) + if (!doc.Open && doc.Dirty) { - doc->Open = true; - doc->DoQueueClose(); + doc.Open = true; + app.CloseQueue.push_back(&doc); } - MyDocument::DisplayContextMenu(doc); + app.DisplayDocContextMenu(&doc); if (visible) { - MyDocument::DisplayContents(doc); + app.DisplayDocContents(&doc); ImGui::EndTabItem(); } } @@ -4539,77 +10436,131 @@ void ShowExampleAppDocuments(bool* p_open) ImGui::EndTabBar(); } } - - // Update closing queue - static ImVector close_queue; - if (close_queue.empty()) + else if (opt_target == Target_DockSpaceAndWindow) { - // Close queue is locked once we started a popup - for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_DockingEnable) { - MyDocument* doc = &app.Documents[doc_n]; - if (doc->WantClose) + app.NotifyOfDocumentsClosedElsewhere(); + + // Create a DockSpace node where any window can be docked + ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); + ImGui::DockSpace(dockspace_id); + + // Create Windows + for (int doc_n = 0; doc_n < app.Documents.Size; doc_n++) { - doc->WantClose = false; - close_queue.push_back(doc); + MyDocument* doc = &app.Documents[doc_n]; + if (!doc->Open) + continue; + + ImGui::SetNextWindowDockID(dockspace_id, redock_all ? ImGuiCond_Always : ImGuiCond_FirstUseEver); + ImGuiWindowFlags window_flags = (doc->Dirty ? ImGuiWindowFlags_UnsavedDocument : 0); + bool visible = ImGui::Begin(doc->Name, &doc->Open, window_flags); + + // Cancel attempt to close when unsaved add to save queue so we can display a popup. + if (!doc->Open && doc->Dirty) + { + doc->Open = true; + app.CloseQueue.push_back(doc); + } + + app.DisplayDocContextMenu(doc); + if (visible) + app.DisplayDocContents(doc); + + ImGui::End(); } } + else + { + ShowDockingDisabledMessage(); + } + } + + // Early out other contents + if (!window_contents_visible) + { + ImGui::End(); + return; + } + + // Display renaming UI + if (app.RenamingDoc != NULL) + { + if (app.RenamingStarted) + ImGui::OpenPopup("Rename"); + if (ImGui::BeginPopup("Rename")) + { + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 30); + if (ImGui::InputText("###Name", app.RenamingDoc->Name, IM_ARRAYSIZE(app.RenamingDoc->Name), ImGuiInputTextFlags_EnterReturnsTrue)) + { + ImGui::CloseCurrentPopup(); + app.RenamingDoc = NULL; + } + if (app.RenamingStarted) + ImGui::SetKeyboardFocusHere(-1); + ImGui::EndPopup(); + } + else + { + app.RenamingDoc = NULL; + } + app.RenamingStarted = false; } // Display closing confirmation UI - if (!close_queue.empty()) + if (!app.CloseQueue.empty()) { int close_queue_unsaved_documents = 0; - for (int n = 0; n < close_queue.Size; n++) - if (close_queue[n]->Dirty) + for (int n = 0; n < app.CloseQueue.Size; n++) + if (app.CloseQueue[n]->Dirty) close_queue_unsaved_documents++; if (close_queue_unsaved_documents == 0) { // Close documents when all are unsaved - for (int n = 0; n < close_queue.Size; n++) - close_queue[n]->DoForceClose(); - close_queue.clear(); + for (int n = 0; n < app.CloseQueue.Size; n++) + app.CloseQueue[n]->DoForceClose(); + app.CloseQueue.clear(); } else { if (!ImGui::IsPopupOpen("Save?")) ImGui::OpenPopup("Save?"); - if (ImGui::BeginPopupModal("Save?")) + if (ImGui::BeginPopupModal("Save?", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::Text("Save change to the following items?"); - ImGui::SetNextItemWidth(-1.0f); - if (ImGui::ListBoxHeader("##", close_queue_unsaved_documents, 6)) - { - for (int n = 0; n < close_queue.Size; n++) - if (close_queue[n]->Dirty) - ImGui::Text("%s", close_queue[n]->Name); - ImGui::ListBoxFooter(); - } + float item_height = ImGui::GetTextLineHeightWithSpacing(); + if (ImGui::BeginChild(ImGui::GetID("frame"), ImVec2(-FLT_MIN, 6.25f * item_height), ImGuiChildFlags_FrameStyle)) + for (MyDocument* doc : app.CloseQueue) + if (doc->Dirty) + ImGui::Text("%s", doc->Name); + ImGui::EndChild(); - if (ImGui::Button("Yes", ImVec2(80, 0))) + ImVec2 button_size(ImGui::GetFontSize() * 7.0f, 0.0f); + if (ImGui::Button("Yes", button_size)) { - for (int n = 0; n < close_queue.Size; n++) + for (MyDocument* doc : app.CloseQueue) { - if (close_queue[n]->Dirty) - close_queue[n]->DoSave(); - close_queue[n]->DoForceClose(); + if (doc->Dirty) + doc->DoSave(); + doc->DoForceClose(); } - close_queue.clear(); + app.CloseQueue.clear(); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("No", ImVec2(80, 0))) + if (ImGui::Button("No", button_size)) { - for (int n = 0; n < close_queue.Size; n++) - close_queue[n]->DoForceClose(); - close_queue.clear(); + for (MyDocument* doc : app.CloseQueue) + doc->DoForceClose(); + app.CloseQueue.clear(); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); - if (ImGui::Button("Cancel", ImVec2(80, 0))) + if (ImGui::Button("Cancel", button_size)) { - close_queue.clear(); + app.CloseQueue.clear(); ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); @@ -4620,6 +10571,404 @@ void ShowExampleAppDocuments(bool* p_open) ImGui::End(); } +//----------------------------------------------------------------------------- +// [SECTION] Example App: Assets Browser / ShowExampleAppAssetsBrowser() +//----------------------------------------------------------------------------- + +//#include "imgui_internal.h" // NavMoveRequestTryWrapping() + +struct ExampleAsset +{ + ImGuiID ID; + int Type; + + ExampleAsset(ImGuiID id, int type) { ID = id; Type = type; } + + static const ImGuiTableSortSpecs* s_current_sort_specs; + + static void SortWithSortSpecs(ImGuiTableSortSpecs* sort_specs, ExampleAsset* items, int items_count) + { + s_current_sort_specs = sort_specs; // Store in variable accessible by the sort function. + if (items_count > 1) + qsort(items, (size_t)items_count, sizeof(items[0]), ExampleAsset::CompareWithSortSpecs); + s_current_sort_specs = NULL; + } + + // Compare function to be used by qsort() + static int IMGUI_CDECL CompareWithSortSpecs(const void* lhs, const void* rhs) + { + const ExampleAsset* a = (const ExampleAsset*)lhs; + const ExampleAsset* b = (const ExampleAsset*)rhs; + for (int n = 0; n < s_current_sort_specs->SpecsCount; n++) + { + const ImGuiTableColumnSortSpecs* sort_spec = &s_current_sort_specs->Specs[n]; + int delta = 0; + if (sort_spec->ColumnIndex == 0) + delta = ((int)a->ID - (int)b->ID); + else if (sort_spec->ColumnIndex == 1) + delta = (a->Type - b->Type); + if (delta > 0) + return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? +1 : -1; + if (delta < 0) + return (sort_spec->SortDirection == ImGuiSortDirection_Ascending) ? -1 : +1; + } + return ((int)a->ID - (int)b->ID); + } +}; +const ImGuiTableSortSpecs* ExampleAsset::s_current_sort_specs = NULL; + +struct ExampleAssetsBrowser +{ + // Options + bool ShowTypeOverlay = true; + bool AllowSorting = true; + bool AllowDragUnselected = false; + bool AllowBoxSelect = true; + float IconSize = 32.0f; + int IconSpacing = 10; + int IconHitSpacing = 4; // Increase hit-spacing if you want to make it possible to clear or box-select from gaps. Some spacing is required to able to amend with Shift+box-select. Value is small in Explorer. + bool StretchSpacing = true; + + // State + ImVector Items; // Our items + ExampleSelectionWithDeletion Selection; // Our selection (ImGuiSelectionBasicStorage + helper funcs to handle deletion) + ImGuiID NextItemId = 0; // Unique identifier when creating new items + bool RequestDelete = false; // Deferred deletion request + bool RequestSort = false; // Deferred sort request + float ZoomWheelAccum = 0.0f; // Mouse wheel accumulator to handle smooth wheels better + + // Calculated sizes for layout, output of UpdateLayoutSizes(). Could be locals but our code is simpler this way. + ImVec2 LayoutItemSize; + ImVec2 LayoutItemStep; // == LayoutItemSize + LayoutItemSpacing + float LayoutItemSpacing = 0.0f; + float LayoutSelectableSpacing = 0.0f; + float LayoutOuterPadding = 0.0f; + int LayoutColumnCount = 0; + int LayoutLineCount = 0; + + // Functions + ExampleAssetsBrowser() + { + AddItems(10000); + } + void AddItems(int count) + { + if (Items.Size == 0) + NextItemId = 0; + Items.reserve(Items.Size + count); + for (int n = 0; n < count; n++, NextItemId++) + Items.push_back(ExampleAsset(NextItemId, (NextItemId % 20) < 15 ? 0 : (NextItemId % 20) < 18 ? 1 : 2)); + RequestSort = true; + } + void ClearItems() + { + Items.clear(); + Selection.Clear(); + } + + // Logic would be written in the main code BeginChild() and outputing to local variables. + // We extracted it into a function so we can call it easily from multiple places. + void UpdateLayoutSizes(float avail_width) + { + // Layout: when not stretching: allow extending into right-most spacing. + LayoutItemSpacing = (float)IconSpacing; + if (StretchSpacing == false) + avail_width += floorf(LayoutItemSpacing * 0.5f); + + // Layout: calculate number of icon per line and number of lines + LayoutItemSize = ImVec2(floorf(IconSize), floorf(IconSize)); + LayoutColumnCount = IM_MAX((int)(avail_width / (LayoutItemSize.x + LayoutItemSpacing)), 1); + LayoutLineCount = (Items.Size + LayoutColumnCount - 1) / LayoutColumnCount; + + // Layout: when stretching: allocate remaining space to more spacing. Round before division, so item_spacing may be non-integer. + if (StretchSpacing && LayoutColumnCount > 1) + LayoutItemSpacing = floorf(avail_width - LayoutItemSize.x * LayoutColumnCount) / LayoutColumnCount; + + LayoutItemStep = ImVec2(LayoutItemSize.x + LayoutItemSpacing, LayoutItemSize.y + LayoutItemSpacing); + LayoutSelectableSpacing = IM_MAX(floorf(LayoutItemSpacing) - IconHitSpacing, 0.0f); + LayoutOuterPadding = floorf(LayoutItemSpacing * 0.5f); + } + + void Draw(const char* title, bool* p_open) + { + ImGui::SetNextWindowSize(ImVec2(IconSize * 25, IconSize * 15), ImGuiCond_FirstUseEver); + if (!ImGui::Begin(title, p_open, ImGuiWindowFlags_MenuBar)) + { + ImGui::End(); + return; + } + + // Menu bar + if (ImGui::BeginMenuBar()) + { + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("Add 10000 items")) + AddItems(10000); + if (ImGui::MenuItem("Clear items")) + ClearItems(); + ImGui::Separator(); + if (ImGui::MenuItem("Close", NULL, false, p_open != NULL)) + *p_open = false; + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Edit")) + { + if (ImGui::MenuItem("Delete", "Del", false, Selection.Size > 0)) + RequestDelete = true; + ImGui::EndMenu(); + } + if (ImGui::BeginMenu("Options")) + { + ImGui::PushItemWidth(ImGui::GetFontSize() * 10); + + ImGui::SeparatorText("Contents"); + ImGui::Checkbox("Show Type Overlay", &ShowTypeOverlay); + ImGui::Checkbox("Allow Sorting", &AllowSorting); + + ImGui::SeparatorText("Selection Behavior"); + ImGui::Checkbox("Allow dragging unselected item", &AllowDragUnselected); + ImGui::Checkbox("Allow box-selection", &AllowBoxSelect); + + ImGui::SeparatorText("Layout"); + ImGui::SliderFloat("Icon Size", &IconSize, 16.0f, 128.0f, "%.0f"); + ImGui::SameLine(); HelpMarker("Use CTRL+Wheel to zoom"); + ImGui::SliderInt("Icon Spacing", &IconSpacing, 0, 32); + ImGui::SliderInt("Icon Hit Spacing", &IconHitSpacing, 0, 32); + ImGui::Checkbox("Stretch Spacing", &StretchSpacing); + ImGui::PopItemWidth(); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } + + // Show a table with ONLY one header row to showcase the idea/possibility of using this to provide a sorting UI + if (AllowSorting) + { + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + ImGuiTableFlags table_flags_for_sort_specs = ImGuiTableFlags_Sortable | ImGuiTableFlags_SortMulti | ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Borders; + if (ImGui::BeginTable("for_sort_specs_only", 2, table_flags_for_sort_specs, ImVec2(0.0f, ImGui::GetFrameHeight()))) + { + ImGui::TableSetupColumn("Index"); + ImGui::TableSetupColumn("Type"); + ImGui::TableHeadersRow(); + if (ImGuiTableSortSpecs* sort_specs = ImGui::TableGetSortSpecs()) + if (sort_specs->SpecsDirty || RequestSort) + { + ExampleAsset::SortWithSortSpecs(sort_specs, Items.Data, Items.Size); + sort_specs->SpecsDirty = RequestSort = false; + } + ImGui::EndTable(); + } + ImGui::PopStyleVar(); + } + + ImGuiIO& io = ImGui::GetIO(); + ImGui::SetNextWindowContentSize(ImVec2(0.0f, LayoutOuterPadding + LayoutLineCount * (LayoutItemSize.y + LayoutItemSpacing))); + if (ImGui::BeginChild("Assets", ImVec2(0.0f, -ImGui::GetTextLineHeightWithSpacing()), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove)) + { + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + const float avail_width = ImGui::GetContentRegionAvail().x; + UpdateLayoutSizes(avail_width); + + // Calculate and store start position. + ImVec2 start_pos = ImGui::GetCursorScreenPos(); + start_pos = ImVec2(start_pos.x + LayoutOuterPadding, start_pos.y + LayoutOuterPadding); + ImGui::SetCursorScreenPos(start_pos); + + // Multi-select + ImGuiMultiSelectFlags ms_flags = ImGuiMultiSelectFlags_ClearOnEscape | ImGuiMultiSelectFlags_ClearOnClickVoid; + + // - Enable box-select (in 2D mode, so that changing box-select rectangle X1/X2 boundaries will affect clipped items) + if (AllowBoxSelect) + ms_flags |= ImGuiMultiSelectFlags_BoxSelect2d; + + // - This feature allows dragging an unselected item without selecting it (rarely used) + if (AllowDragUnselected) + ms_flags |= ImGuiMultiSelectFlags_SelectOnClickRelease; + + // - Enable keyboard wrapping on X axis + // (FIXME-MULTISELECT: We haven't designed/exposed a general nav wrapping api yet, so this flag is provided as a courtesy to avoid doing: + // ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); + // When we finish implementing a more general API for this, we will obsolete this flag in favor of the new system) + ms_flags |= ImGuiMultiSelectFlags_NavWrapX; + + ImGuiMultiSelectIO* ms_io = ImGui::BeginMultiSelect(ms_flags, Selection.Size, Items.Size); + + // Use custom selection adapter: store ID in selection (recommended) + Selection.UserData = this; + Selection.AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage* self_, int idx) { ExampleAssetsBrowser* self = (ExampleAssetsBrowser*)self_->UserData; return self->Items[idx].ID; }; + Selection.ApplyRequests(ms_io); + + const bool want_delete = (ImGui::Shortcut(ImGuiKey_Delete, ImGuiInputFlags_Repeat) && (Selection.Size > 0)) || RequestDelete; + const int item_curr_idx_to_focus = want_delete ? Selection.ApplyDeletionPreLoop(ms_io, Items.Size) : -1; + RequestDelete = false; + + // Push LayoutSelectableSpacing (which is LayoutItemSpacing minus hit-spacing, if we decide to have hit gaps between items) + // Altering style ItemSpacing may seem unnecessary as we position every items using SetCursorScreenPos()... + // But it is necessary for two reasons: + // - Selectables uses it by default to visually fill the space between two items. + // - The vertical spacing would be measured by Clipper to calculate line height if we didn't provide it explicitly (here we do). + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(LayoutSelectableSpacing, LayoutSelectableSpacing)); + + // Rendering parameters + const ImU32 icon_type_overlay_colors[3] = { 0, IM_COL32(200, 70, 70, 255), IM_COL32(70, 170, 70, 255) }; + const ImU32 icon_bg_color = ImGui::GetColorU32(IM_COL32(35, 35, 35, 220)); + const ImVec2 icon_type_overlay_size = ImVec2(4.0f, 4.0f); + const bool display_label = (LayoutItemSize.x >= ImGui::CalcTextSize("999").x); + + const int column_count = LayoutColumnCount; + ImGuiListClipper clipper; + clipper.Begin(LayoutLineCount, LayoutItemStep.y); + if (item_curr_idx_to_focus != -1) + clipper.IncludeItemByIndex(item_curr_idx_to_focus / column_count); // Ensure focused item line is not clipped. + if (ms_io->RangeSrcItem != -1) + clipper.IncludeItemByIndex((int)ms_io->RangeSrcItem / column_count); // Ensure RangeSrc item line is not clipped. + while (clipper.Step()) + { + for (int line_idx = clipper.DisplayStart; line_idx < clipper.DisplayEnd; line_idx++) + { + const int item_min_idx_for_current_line = line_idx * column_count; + const int item_max_idx_for_current_line = IM_MIN((line_idx + 1) * column_count, Items.Size); + for (int item_idx = item_min_idx_for_current_line; item_idx < item_max_idx_for_current_line; ++item_idx) + { + ExampleAsset* item_data = &Items[item_idx]; + ImGui::PushID((int)item_data->ID); + + // Position item + ImVec2 pos = ImVec2(start_pos.x + (item_idx % column_count) * LayoutItemStep.x, start_pos.y + line_idx * LayoutItemStep.y); + ImGui::SetCursorScreenPos(pos); + + ImGui::SetNextItemSelectionUserData(item_idx); + bool item_is_selected = Selection.Contains((ImGuiID)item_data->ID); + bool item_is_visible = ImGui::IsRectVisible(LayoutItemSize); + ImGui::Selectable("", item_is_selected, ImGuiSelectableFlags_None, LayoutItemSize); + + // Update our selection state immediately (without waiting for EndMultiSelect() requests) + // because we use this to alter the color of our text/icon. + if (ImGui::IsItemToggledSelection()) + item_is_selected = !item_is_selected; + + // Focus (for after deletion) + if (item_curr_idx_to_focus == item_idx) + ImGui::SetKeyboardFocusHere(-1); + + // Drag and drop + if (ImGui::BeginDragDropSource()) + { + // Create payload with full selection OR single unselected item. + // (the later is only possible when using ImGuiMultiSelectFlags_SelectOnClickRelease) + if (ImGui::GetDragDropPayload() == NULL) + { + ImVector payload_items; + void* it = NULL; + ImGuiID id = 0; + if (!item_is_selected) + payload_items.push_back(item_data->ID); + else + while (Selection.GetNextSelectedItem(&it, &id)) + payload_items.push_back(id); + ImGui::SetDragDropPayload("ASSETS_BROWSER_ITEMS", payload_items.Data, (size_t)payload_items.size_in_bytes()); + } + + // Display payload content in tooltip, by extracting it from the payload data + // (we could read from selection, but it is more correct and reusable to read from payload) + const ImGuiPayload* payload = ImGui::GetDragDropPayload(); + const int payload_count = (int)payload->DataSize / (int)sizeof(ImGuiID); + ImGui::Text("%d assets", payload_count); + + ImGui::EndDragDropSource(); + } + + // Render icon (a real app would likely display an image/thumbnail here) + // Because we use ImGuiMultiSelectFlags_BoxSelect2d, clipping vertical may occasionally be larger, so we coarse-clip our rendering as well. + if (item_is_visible) + { + ImVec2 box_min(pos.x - 1, pos.y - 1); + ImVec2 box_max(box_min.x + LayoutItemSize.x + 2, box_min.y + LayoutItemSize.y + 2); // Dubious + draw_list->AddRectFilled(box_min, box_max, icon_bg_color); // Background color + if (ShowTypeOverlay && item_data->Type != 0) + { + ImU32 type_col = icon_type_overlay_colors[item_data->Type % IM_ARRAYSIZE(icon_type_overlay_colors)]; + draw_list->AddRectFilled(ImVec2(box_max.x - 2 - icon_type_overlay_size.x, box_min.y + 2), ImVec2(box_max.x - 2, box_min.y + 2 + icon_type_overlay_size.y), type_col); + } + if (display_label) + { + ImU32 label_col = ImGui::GetColorU32(item_is_selected ? ImGuiCol_Text : ImGuiCol_TextDisabled); + char label[32]; + sprintf(label, "%d", item_data->ID); + draw_list->AddText(ImVec2(box_min.x, box_max.y - ImGui::GetFontSize()), label_col, label); + } + } + + ImGui::PopID(); + } + } + } + clipper.End(); + ImGui::PopStyleVar(); // ImGuiStyleVar_ItemSpacing + + // Context menu + if (ImGui::BeginPopupContextWindow()) + { + ImGui::Text("Selection: %d items", Selection.Size); + ImGui::Separator(); + if (ImGui::MenuItem("Delete", "Del", false, Selection.Size > 0)) + RequestDelete = true; + ImGui::EndPopup(); + } + + ms_io = ImGui::EndMultiSelect(); + Selection.ApplyRequests(ms_io); + if (want_delete) + Selection.ApplyDeletionPostLoop(ms_io, Items, item_curr_idx_to_focus); + + // Zooming with CTRL+Wheel + if (ImGui::IsWindowAppearing()) + ZoomWheelAccum = 0.0f; + if (ImGui::IsWindowHovered() && io.MouseWheel != 0.0f && ImGui::IsKeyDown(ImGuiMod_Ctrl) && ImGui::IsAnyItemActive() == false) + { + ZoomWheelAccum += io.MouseWheel; + if (fabsf(ZoomWheelAccum) >= 1.0f) + { + // Calculate hovered item index from mouse location + // FIXME: Locking aiming on 'hovered_item_idx' (with a cool-down timer) would ensure zoom keeps on it. + const float hovered_item_nx = (io.MousePos.x - start_pos.x + LayoutItemSpacing * 0.5f) / LayoutItemStep.x; + const float hovered_item_ny = (io.MousePos.y - start_pos.y + LayoutItemSpacing * 0.5f) / LayoutItemStep.y; + const int hovered_item_idx = ((int)hovered_item_ny * LayoutColumnCount) + (int)hovered_item_nx; + //ImGui::SetTooltip("%f,%f -> item %d", hovered_item_nx, hovered_item_ny, hovered_item_idx); // Move those 4 lines in block above for easy debugging + + // Zoom + IconSize *= powf(1.1f, (float)(int)ZoomWheelAccum); + IconSize = IM_CLAMP(IconSize, 16.0f, 128.0f); + ZoomWheelAccum -= (int)ZoomWheelAccum; + UpdateLayoutSizes(avail_width); + + // Manipulate scroll to that we will land at the same Y location of currently hovered item. + // - Calculate next frame position of item under mouse + // - Set new scroll position to be used in next ImGui::BeginChild() call. + float hovered_item_rel_pos_y = ((float)(hovered_item_idx / LayoutColumnCount) + fmodf(hovered_item_ny, 1.0f)) * LayoutItemStep.y; + hovered_item_rel_pos_y += ImGui::GetStyle().WindowPadding.y; + float mouse_local_y = io.MousePos.y - ImGui::GetWindowPos().y; + ImGui::SetScrollY(hovered_item_rel_pos_y - mouse_local_y); + } + } + } + ImGui::EndChild(); + + ImGui::Text("Selected: %d/%d items", Selection.Size, Items.Size); + ImGui::End(); + } +}; + +void ShowExampleAppAssetsBrowser(bool* p_open) +{ + IMGUI_DEMO_MARKER("Examples/Assets Browser"); + static ExampleAssetsBrowser assets_browser; + assets_browser.Draw("Example: Assets Browser", p_open); +} + // End of Demo code #else @@ -4627,5 +10976,9 @@ void ImGui::ShowAboutWindow(bool*) {} void ImGui::ShowDemoWindow(bool*) {} void ImGui::ShowUserGuide() {} void ImGui::ShowStyleEditor(ImGuiStyle*) {} +bool ImGui::ShowStyleSelector(const char* label) { return false; } +void ImGui::ShowFontSelector(const char* label) {} #endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_draw.cpp b/imgui/imgui_draw.cpp index 845cd04e..7c4eaa4b 100644 --- a/imgui/imgui_draw.cpp +++ b/imgui/imgui_draw.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.73 +// dear imgui, v1.91.9b // (drawing and font code) /* @@ -8,15 +8,16 @@ Index of this file: // [SECTION] STB libraries implementation // [SECTION] Style functions // [SECTION] ImDrawList +// [SECTION] ImTriangulator, ImDrawList concave polygon fill // [SECTION] ImDrawListSplitter // [SECTION] ImDrawData // [SECTION] Helpers ShadeVertsXXX functions // [SECTION] ImFontConfig // [SECTION] ImFontAtlas -// [SECTION] ImFontAtlas glyph ranges helpers +// [SECTION] ImFontAtlas: glyph ranges helpers // [SECTION] ImFontGlyphRangesBuilder // [SECTION] ImFont -// [SECTION] Internal Render Helpers +// [SECTION] ImGui Internal Render Helpers // [SECTION] Decompression code // [SECTION] Default font data (ProggyClean.ttf) @@ -26,62 +27,62 @@ Index of this file: #define _CRT_SECURE_NO_WARNINGS #endif -#include "imgui.h" #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_internal.h" +#ifdef IMGUI_ENABLE_FREETYPE +#include "misc/freetype/imgui_freetype.h" +#endif #include // vsnprintf, sscanf, printf -#if !defined(alloca) -#if defined(__GLIBC__) || defined(__sun) || defined(__CYGWIN__) || defined(__APPLE__) || defined(__SWITCH__) -#include // alloca (glibc uses . Note that Cygwin may have _WIN32 defined, so the order matters here) -#elif defined(_WIN32) -#include // alloca -#if !defined(alloca) -#define alloca _alloca // for clang with MS Codegen -#endif -#else -#include // alloca -#endif -#endif // Visual Studio warnings #ifdef _MSC_VER -#pragma warning (disable: 4127) // condition expression is constant -#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) -#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4505) // unreferenced local function has been removed (stb stuff) +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) #endif // Clang/GCC warnings with -Weverything #if defined(__clang__) -#pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. -#pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants ok. -#pragma clang diagnostic ignored "-Wglobal-constructors" // warning : declaration requires a global destructor // similar to above, not sure what the exact difference is. -#pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning : zero as null pointer constant // some standard header variations use #define NULL 0 -#endif -#if __has_warning("-Wcomma") -#pragma clang diagnostic ignored "-Wcomma" // warning : possible misuse of comma operator here // -#endif -#if __has_warning("-Wreserved-id-macro") -#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning : macro name is a reserved identifier // -#endif -#if __has_warning("-Wdouble-promotion") -#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! #endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok. +#pragma clang diagnostic ignored "-Wglobal-constructors" // warning: declaration requires a global destructor // similar to above, not sure what the exact difference is. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wcomma" // warning: possible misuse of comma operator here +#pragma clang diagnostic ignored "-Wreserved-id-macro" // warning: macro name is a reserved identifier +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#pragma clang diagnostic ignored "-Wcast-qual" // warning: cast from 'const xxxx *' to 'xxx *' drops const qualifier +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label #elif defined(__GNUC__) -#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind -#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used -#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function -#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value -#pragma GCC diagnostic ignored "-Wstack-protector" // warning: stack protector not protecting local variables: variable length buffer -#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wconversion" // warning: conversion to 'xxxx' from 'xxxx' may alter its value +#pragma GCC diagnostic ignored "-Wstack-protector" // warning: stack protector not protecting local variables: variable length buffer +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1 +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers #endif //------------------------------------------------------------------------- -// [SECTION] STB libraries implementation +// [SECTION] STB libraries implementation (for stb_truetype and stb_rect_pack) //------------------------------------------------------------------------- // Compile time options: @@ -99,26 +100,28 @@ namespace IMGUI_STB_NAMESPACE #ifdef _MSC_VER #pragma warning (push) #pragma warning (disable: 4456) // declaration of 'xx' hides previous local declaration +#pragma warning (disable: 6011) // (stb_rectpack) Dereferencing NULL pointer 'cur->next'. +#pragma warning (disable: 6385) // (stb_truetype) Reading invalid data from 'buffer': the readable size is '_Old_3`kernel_width' bytes, but '3' bytes may be read. +#pragma warning (disable: 28182) // (stb_rectpack) Dereferencing NULL pointer. 'cur' contains the same NULL value as 'cur->next' did. #endif #if defined(__clang__) #pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-function" +#pragma clang diagnostic ignored "-Wunused-function" // warning: 'xxxx' defined but not used #pragma clang diagnostic ignored "-Wmissing-prototypes" #pragma clang diagnostic ignored "-Wimplicit-fallthrough" -#pragma clang diagnostic ignored "-Wcast-qual" // warning : cast from 'const xxxx *' to 'xxx *' drops const qualifier // #endif #if defined(__GNUC__) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wtype-limits" // warning: comparison is always true due to limited range of data type [-Wtype-limits] -#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers +#pragma GCC diagnostic ignored "-Wimplicit-fallthrough" // warning: this statement may fall through #endif #ifndef STB_RECT_PACK_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) -#ifndef IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION +#ifndef IMGUI_DISABLE_STB_RECT_PACK_IMPLEMENTATION // in case the user already have an implementation in another compilation unit #define STBRP_STATIC -#define STBRP_ASSERT(x) IM_ASSERT(x) +#define STBRP_ASSERT(x) do { IM_ASSERT(x); } while (0) #define STBRP_SORT ImQsort #define STB_RECT_PACK_IMPLEMENTATION #endif @@ -129,17 +132,19 @@ namespace IMGUI_STB_NAMESPACE #endif #endif +#ifdef IMGUI_ENABLE_STB_TRUETYPE #ifndef STB_TRUETYPE_IMPLEMENTATION // in case the user already have an implementation in the _same_ compilation unit (e.g. unity builds) -#ifndef IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION +#ifndef IMGUI_DISABLE_STB_TRUETYPE_IMPLEMENTATION // in case the user already have an implementation in another compilation unit #define STBTT_malloc(x,u) ((void)(u), IM_ALLOC(x)) #define STBTT_free(x,u) ((void)(u), IM_FREE(x)) -#define STBTT_assert(x) IM_ASSERT(x) +#define STBTT_assert(x) do { IM_ASSERT(x); } while(0) #define STBTT_fmod(x,y) ImFmod(x,y) #define STBTT_sqrt(x) ImSqrt(x) #define STBTT_pow(x,y) ImPow(x,y) #define STBTT_fabs(x) ImFabs(x) -#define STBTT_ifloor(x) ((int)ImFloorStd(x)) +#define STBTT_ifloor(x) ((int)ImFloor(x)) #define STBTT_iceil(x) ((int)ImCeil(x)) +#define STBTT_strlen(x) ImStrlen(x) #define STBTT_STATIC #define STB_TRUETYPE_IMPLEMENTATION #else @@ -151,6 +156,7 @@ namespace IMGUI_STB_NAMESPACE #include "imstb_truetype.h" #endif #endif +#endif // IMGUI_ENABLE_STB_TRUETYPE #if defined(__GNUC__) #pragma GCC diagnostic pop @@ -208,21 +214,31 @@ void ImGui::StyleColorsDark(ImGuiStyle* dst) colors[ImGuiCol_Separator] = colors[ImGuiCol_Border]; colors[ImGuiCol_SeparatorHovered] = ImVec4(0.10f, 0.40f, 0.75f, 0.78f); colors[ImGuiCol_SeparatorActive] = ImVec4(0.10f, 0.40f, 0.75f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.25f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.26f, 0.59f, 0.98f, 0.20f); colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); - colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; - colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); - colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); - colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.50f, 0.50f, 0.50f, 0.00f); + colors[ImGuiCol_DockingPreview] = colors[ImGuiCol_HeaderActive] * ImVec4(1.0f, 1.0f, 1.0f, 0.7f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(0.61f, 0.61f, 0.61f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.19f, 0.19f, 0.20f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.35f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.23f, 0.23f, 0.25f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.06f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); - colors[ImGuiCol_NavHighlight] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); + colors[ImGuiCol_NavCursor] = ImVec4(0.26f, 0.59f, 0.98f, 1.00f); colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); @@ -235,7 +251,7 @@ void ImGui::StyleColorsClassic(ImGuiStyle* dst) colors[ImGuiCol_Text] = ImVec4(0.90f, 0.90f, 0.90f, 1.00f); colors[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.60f, 0.60f, 1.00f); - colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.70f); + colors[ImGuiCol_WindowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.85f); colors[ImGuiCol_ChildBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); colors[ImGuiCol_PopupBg] = ImVec4(0.11f, 0.11f, 0.14f, 0.92f); colors[ImGuiCol_Border] = ImVec4(0.50f, 0.50f, 0.50f, 0.50f); @@ -263,21 +279,31 @@ void ImGui::StyleColorsClassic(ImGuiStyle* dst) colors[ImGuiCol_Separator] = ImVec4(0.50f, 0.50f, 0.50f, 0.60f); colors[ImGuiCol_SeparatorHovered] = ImVec4(0.60f, 0.60f, 0.70f, 1.00f); colors[ImGuiCol_SeparatorActive] = ImVec4(0.70f, 0.70f, 0.90f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.16f); + colors[ImGuiCol_ResizeGrip] = ImVec4(1.00f, 1.00f, 1.00f, 0.10f); colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.78f, 0.82f, 1.00f, 0.60f); colors[ImGuiCol_ResizeGripActive] = ImVec4(0.78f, 0.82f, 1.00f, 0.90f); - colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; - colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); - colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); - colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.80f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.53f, 0.53f, 0.87f, 0.00f); + colors[ImGuiCol_DockingPreview] = colors[ImGuiCol_Header] * ImVec4(1.0f, 1.0f, 1.0f, 0.7f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(1.00f, 1.00f, 1.00f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.60f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.27f, 0.27f, 0.38f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.31f, 0.31f, 0.45f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.26f, 0.26f, 0.28f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(1.00f, 1.00f, 1.00f, 0.07f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; colors[ImGuiCol_TextSelectedBg] = ImVec4(0.00f, 0.00f, 1.00f, 0.35f); colors[ImGuiCol_DragDropTarget] = ImVec4(1.00f, 1.00f, 0.00f, 0.90f); - colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavCursor] = colors[ImGuiCol_HeaderHovered]; colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); @@ -319,72 +345,118 @@ void ImGui::StyleColorsLight(ImGuiStyle* dst) colors[ImGuiCol_Separator] = ImVec4(0.39f, 0.39f, 0.39f, 0.62f); colors[ImGuiCol_SeparatorHovered] = ImVec4(0.14f, 0.44f, 0.80f, 0.78f); colors[ImGuiCol_SeparatorActive] = ImVec4(0.14f, 0.44f, 0.80f, 1.00f); - colors[ImGuiCol_ResizeGrip] = ImVec4(0.80f, 0.80f, 0.80f, 0.56f); + colors[ImGuiCol_ResizeGrip] = ImVec4(0.35f, 0.35f, 0.35f, 0.17f); colors[ImGuiCol_ResizeGripHovered] = ImVec4(0.26f, 0.59f, 0.98f, 0.67f); colors[ImGuiCol_ResizeGripActive] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); - colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.90f); colors[ImGuiCol_TabHovered] = colors[ImGuiCol_HeaderHovered]; - colors[ImGuiCol_TabActive] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); - colors[ImGuiCol_TabUnfocused] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); - colors[ImGuiCol_TabUnfocusedActive] = ImLerp(colors[ImGuiCol_TabActive], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_Tab] = ImLerp(colors[ImGuiCol_Header], colors[ImGuiCol_TitleBgActive], 0.90f); + colors[ImGuiCol_TabSelected] = ImLerp(colors[ImGuiCol_HeaderActive], colors[ImGuiCol_TitleBgActive], 0.60f); + colors[ImGuiCol_TabSelectedOverline] = colors[ImGuiCol_HeaderActive]; + colors[ImGuiCol_TabDimmed] = ImLerp(colors[ImGuiCol_Tab], colors[ImGuiCol_TitleBg], 0.80f); + colors[ImGuiCol_TabDimmedSelected] = ImLerp(colors[ImGuiCol_TabSelected], colors[ImGuiCol_TitleBg], 0.40f); + colors[ImGuiCol_TabDimmedSelectedOverline] = ImVec4(0.26f, 0.59f, 1.00f, 0.00f); + colors[ImGuiCol_DockingPreview] = colors[ImGuiCol_Header] * ImVec4(1.0f, 1.0f, 1.0f, 0.7f); + colors[ImGuiCol_DockingEmptyBg] = ImVec4(0.20f, 0.20f, 0.20f, 1.00f); colors[ImGuiCol_PlotLines] = ImVec4(0.39f, 0.39f, 0.39f, 1.00f); colors[ImGuiCol_PlotLinesHovered] = ImVec4(1.00f, 0.43f, 0.35f, 1.00f); colors[ImGuiCol_PlotHistogram] = ImVec4(0.90f, 0.70f, 0.00f, 1.00f); colors[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00f, 0.45f, 0.00f, 1.00f); + colors[ImGuiCol_TableHeaderBg] = ImVec4(0.78f, 0.87f, 0.98f, 1.00f); + colors[ImGuiCol_TableBorderStrong] = ImVec4(0.57f, 0.57f, 0.64f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableBorderLight] = ImVec4(0.68f, 0.68f, 0.74f, 1.00f); // Prefer using Alpha=1.0 here + colors[ImGuiCol_TableRowBg] = ImVec4(0.00f, 0.00f, 0.00f, 0.00f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(0.30f, 0.30f, 0.30f, 0.09f); + colors[ImGuiCol_TextLink] = colors[ImGuiCol_HeaderActive]; colors[ImGuiCol_TextSelectedBg] = ImVec4(0.26f, 0.59f, 0.98f, 0.35f); colors[ImGuiCol_DragDropTarget] = ImVec4(0.26f, 0.59f, 0.98f, 0.95f); - colors[ImGuiCol_NavHighlight] = colors[ImGuiCol_HeaderHovered]; + colors[ImGuiCol_NavCursor] = colors[ImGuiCol_HeaderHovered]; colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.70f, 0.70f, 0.70f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); } //----------------------------------------------------------------------------- -// ImDrawList +// [SECTION] ImDrawList //----------------------------------------------------------------------------- ImDrawListSharedData::ImDrawListSharedData() { - Font = NULL; - FontSize = 0.0f; - CurveTessellationTol = 0.0f; - ClipRectFullscreen = ImVec4(-8192.0f, -8192.0f, +8192.0f, +8192.0f); - InitialFlags = ImDrawListFlags_None; - - // Const data - for (int i = 0; i < IM_ARRAYSIZE(CircleVtx12); i++) + memset(this, 0, sizeof(*this)); + InitialFringeScale = 1.0f; + for (int i = 0; i < IM_ARRAYSIZE(ArcFastVtx); i++) { - const float a = ((float)i * 2 * IM_PI) / (float)IM_ARRAYSIZE(CircleVtx12); - CircleVtx12[i] = ImVec2(ImCos(a), ImSin(a)); + const float a = ((float)i * 2 * IM_PI) / (float)IM_ARRAYSIZE(ArcFastVtx); + ArcFastVtx[i] = ImVec2(ImCos(a), ImSin(a)); } + ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); } -void ImDrawList::Clear() +void ImDrawListSharedData::SetCircleTessellationMaxError(float max_error) { + if (CircleSegmentMaxError == max_error) + return; + + IM_ASSERT(max_error > 0.0f); + CircleSegmentMaxError = max_error; + for (int i = 0; i < IM_ARRAYSIZE(CircleSegmentCounts); i++) + { + const float radius = (float)i; + CircleSegmentCounts[i] = (ImU8)((i > 0) ? IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, CircleSegmentMaxError) : IM_DRAWLIST_ARCFAST_SAMPLE_MAX); + } + ArcFastRadiusCutoff = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(IM_DRAWLIST_ARCFAST_SAMPLE_MAX, CircleSegmentMaxError); +} + +ImDrawList::ImDrawList(ImDrawListSharedData* shared_data) +{ + memset(this, 0, sizeof(*this)); + _Data = shared_data; +} + +ImDrawList::~ImDrawList() +{ + _ClearFreeMemory(); +} + +// Initialize before use in a new frame. We always have a command ready in the buffer. +// In the majority of cases, you would want to call PushClipRect() and PushTextureID() after this. +void ImDrawList::_ResetForNewFrame() +{ + // Verify that the ImDrawCmd fields we want to memcmp() are contiguous in memory. + IM_STATIC_ASSERT(offsetof(ImDrawCmd, ClipRect) == 0); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, TextureId) == sizeof(ImVec4)); + IM_STATIC_ASSERT(offsetof(ImDrawCmd, VtxOffset) == sizeof(ImVec4) + sizeof(ImTextureID)); + if (_Splitter._Count > 1) + _Splitter.Merge(this); + CmdBuffer.resize(0); IdxBuffer.resize(0); VtxBuffer.resize(0); - Flags = _Data ? _Data->InitialFlags : ImDrawListFlags_None; - _VtxCurrentOffset = 0; + Flags = _Data->InitialFlags; + memset(&_CmdHeader, 0, sizeof(_CmdHeader)); _VtxCurrentIdx = 0; _VtxWritePtr = NULL; _IdxWritePtr = NULL; _ClipRectStack.resize(0); _TextureIdStack.resize(0); + _CallbacksDataBuf.resize(0); _Path.resize(0); _Splitter.Clear(); + CmdBuffer.push_back(ImDrawCmd()); + _FringeScale = _Data->InitialFringeScale; } -void ImDrawList::ClearFreeMemory() +void ImDrawList::_ClearFreeMemory() { CmdBuffer.clear(); IdxBuffer.clear(); VtxBuffer.clear(); + Flags = ImDrawListFlags_None; _VtxCurrentIdx = 0; _VtxWritePtr = NULL; _IdxWritePtr = NULL; _ClipRectStack.clear(); _TextureIdStack.clear(); + _CallbacksDataBuf.clear(); _Path.clear(); _Splitter.ClearFreeMemory(); } @@ -399,86 +471,163 @@ ImDrawList* ImDrawList::CloneOutput() const return dst; } -// Using macros because C++ is a terrible language, we want guaranteed inline, no code in header, and no overhead in Debug builds -#define GetCurrentClipRect() (_ClipRectStack.Size ? _ClipRectStack.Data[_ClipRectStack.Size-1] : _Data->ClipRectFullscreen) -#define GetCurrentTextureId() (_TextureIdStack.Size ? _TextureIdStack.Data[_TextureIdStack.Size-1] : (ImTextureID)NULL) - void ImDrawList::AddDrawCmd() { ImDrawCmd draw_cmd; - draw_cmd.ClipRect = GetCurrentClipRect(); - draw_cmd.TextureId = GetCurrentTextureId(); - draw_cmd.VtxOffset = _VtxCurrentOffset; + draw_cmd.ClipRect = _CmdHeader.ClipRect; // Same as calling ImDrawCmd_HeaderCopy() + draw_cmd.TextureId = _CmdHeader.TextureId; + draw_cmd.VtxOffset = _CmdHeader.VtxOffset; draw_cmd.IdxOffset = IdxBuffer.Size; IM_ASSERT(draw_cmd.ClipRect.x <= draw_cmd.ClipRect.z && draw_cmd.ClipRect.y <= draw_cmd.ClipRect.w); CmdBuffer.push_back(draw_cmd); } -void ImDrawList::AddCallback(ImDrawCallback callback, void* callback_data) +// Pop trailing draw command (used before merging or presenting to user) +// Note that this leaves the ImDrawList in a state unfit for further commands, as most code assume that CmdBuffer.Size > 0 && CmdBuffer.back().UserCallback == NULL +void ImDrawList::_PopUnusedDrawCmd() { - ImDrawCmd* current_cmd = CmdBuffer.Size ? &CmdBuffer.back() : NULL; - if (!current_cmd || current_cmd->ElemCount != 0 || current_cmd->UserCallback != NULL) + while (CmdBuffer.Size > 0) + { + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 || curr_cmd->UserCallback != NULL) + return;// break; + CmdBuffer.pop_back(); + } +} + +void ImDrawList::AddCallback(ImDrawCallback callback, void* userdata, size_t userdata_size) +{ + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + IM_ASSERT(curr_cmd->UserCallback == NULL); + if (curr_cmd->ElemCount != 0) { AddDrawCmd(); - current_cmd = &CmdBuffer.back(); + curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + } + + curr_cmd->UserCallback = callback; + if (userdata_size == 0) + { + // Store user data directly in command (no indirection) + curr_cmd->UserCallbackData = userdata; + curr_cmd->UserCallbackDataSize = 0; + curr_cmd->UserCallbackDataOffset = -1; + } + else + { + // Copy and store user data in a buffer + IM_ASSERT(userdata != NULL); + IM_ASSERT(userdata_size < (1u << 31)); + curr_cmd->UserCallbackData = NULL; // Will be resolved during Render() + curr_cmd->UserCallbackDataSize = (int)userdata_size; + curr_cmd->UserCallbackDataOffset = _CallbacksDataBuf.Size; + _CallbacksDataBuf.resize(_CallbacksDataBuf.Size + (int)userdata_size); + memcpy(_CallbacksDataBuf.Data + (size_t)curr_cmd->UserCallbackDataOffset, userdata, userdata_size); } - current_cmd->UserCallback = callback; - current_cmd->UserCallbackData = callback_data; AddDrawCmd(); // Force a new command after us (see comment below) } +// Compare ClipRect, TextureId and VtxOffset with a single memcmp() +#define ImDrawCmd_HeaderSize (offsetof(ImDrawCmd, VtxOffset) + sizeof(unsigned int)) +#define ImDrawCmd_HeaderCompare(CMD_LHS, CMD_RHS) (memcmp(CMD_LHS, CMD_RHS, ImDrawCmd_HeaderSize)) // Compare ClipRect, TextureId, VtxOffset +#define ImDrawCmd_HeaderCopy(CMD_DST, CMD_SRC) (memcpy(CMD_DST, CMD_SRC, ImDrawCmd_HeaderSize)) // Copy ClipRect, TextureId, VtxOffset +#define ImDrawCmd_AreSequentialIdxOffset(CMD_0, CMD_1) (CMD_0->IdxOffset + CMD_0->ElemCount == CMD_1->IdxOffset) + +// Try to merge two last draw commands +void ImDrawList::_TryMergeDrawCmds() +{ + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (ImDrawCmd_HeaderCompare(curr_cmd, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && curr_cmd->UserCallback == NULL && prev_cmd->UserCallback == NULL) + { + prev_cmd->ElemCount += curr_cmd->ElemCount; + CmdBuffer.pop_back(); + } +} + // Our scheme may appears a bit unusual, basically we want the most-common calls AddLine AddRect etc. to not have to perform any check so we always have a command ready in the stack. // The cost of figuring out if a new command has to be added or if we can merge is paid in those Update** functions only. -void ImDrawList::UpdateClipRect() +void ImDrawList::_OnChangedClipRect() { // If current command is used with different settings we need to add a new command - const ImVec4 curr_clip_rect = GetCurrentClipRect(); - ImDrawCmd* curr_cmd = CmdBuffer.Size > 0 ? &CmdBuffer.Data[CmdBuffer.Size-1] : NULL; - if (!curr_cmd || (curr_cmd->ElemCount != 0 && memcmp(&curr_cmd->ClipRect, &curr_clip_rect, sizeof(ImVec4)) != 0) || curr_cmd->UserCallback != NULL) + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 && memcmp(&curr_cmd->ClipRect, &_CmdHeader.ClipRect, sizeof(ImVec4)) != 0) { AddDrawCmd(); return; } + IM_ASSERT(curr_cmd->UserCallback == NULL); // Try to merge with previous command if it matches, else use current command - ImDrawCmd* prev_cmd = CmdBuffer.Size > 1 ? curr_cmd - 1 : NULL; - if (curr_cmd->ElemCount == 0 && prev_cmd && memcmp(&prev_cmd->ClipRect, &curr_clip_rect, sizeof(ImVec4)) == 0 && prev_cmd->TextureId == GetCurrentTextureId() && prev_cmd->UserCallback == NULL) + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (curr_cmd->ElemCount == 0 && CmdBuffer.Size > 1 && ImDrawCmd_HeaderCompare(&_CmdHeader, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && prev_cmd->UserCallback == NULL) + { CmdBuffer.pop_back(); - else - curr_cmd->ClipRect = curr_clip_rect; + return; + } + curr_cmd->ClipRect = _CmdHeader.ClipRect; } -void ImDrawList::UpdateTextureID() +void ImDrawList::_OnChangedTextureID() { // If current command is used with different settings we need to add a new command - const ImTextureID curr_texture_id = GetCurrentTextureId(); - ImDrawCmd* curr_cmd = CmdBuffer.Size ? &CmdBuffer.back() : NULL; - if (!curr_cmd || (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != curr_texture_id) || curr_cmd->UserCallback != NULL) + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount != 0 && curr_cmd->TextureId != _CmdHeader.TextureId) { AddDrawCmd(); return; } + IM_ASSERT(curr_cmd->UserCallback == NULL); // Try to merge with previous command if it matches, else use current command - ImDrawCmd* prev_cmd = CmdBuffer.Size > 1 ? curr_cmd - 1 : NULL; - if (curr_cmd->ElemCount == 0 && prev_cmd && prev_cmd->TextureId == curr_texture_id && memcmp(&prev_cmd->ClipRect, &GetCurrentClipRect(), sizeof(ImVec4)) == 0 && prev_cmd->UserCallback == NULL) + ImDrawCmd* prev_cmd = curr_cmd - 1; + if (curr_cmd->ElemCount == 0 && CmdBuffer.Size > 1 && ImDrawCmd_HeaderCompare(&_CmdHeader, prev_cmd) == 0 && ImDrawCmd_AreSequentialIdxOffset(prev_cmd, curr_cmd) && prev_cmd->UserCallback == NULL) + { CmdBuffer.pop_back(); - else - curr_cmd->TextureId = curr_texture_id; + return; + } + curr_cmd->TextureId = _CmdHeader.TextureId; } -#undef GetCurrentClipRect -#undef GetCurrentTextureId +void ImDrawList::_OnChangedVtxOffset() +{ + // We don't need to compare curr_cmd->VtxOffset != _CmdHeader.VtxOffset because we know it'll be different at the time we call this. + _VtxCurrentIdx = 0; + IM_ASSERT_PARANOID(CmdBuffer.Size > 0); + ImDrawCmd* curr_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + //IM_ASSERT(curr_cmd->VtxOffset != _CmdHeader.VtxOffset); // See #3349 + if (curr_cmd->ElemCount != 0) + { + AddDrawCmd(); + return; + } + IM_ASSERT(curr_cmd->UserCallback == NULL); + curr_cmd->VtxOffset = _CmdHeader.VtxOffset; +} + +int ImDrawList::_CalcCircleAutoSegmentCount(float radius) const +{ + // Automatic segment count + const int radius_idx = (int)(radius + 0.999999f); // ceil to never reduce accuracy + if (radius_idx >= 0 && radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) + return _Data->CircleSegmentCounts[radius_idx]; // Use cached value + else + return IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(radius, _Data->CircleSegmentMaxError); +} // Render-level scissoring. This is passed down to your render function but not used for CPU-side coarse clipping. Prefer using higher-level ImGui::PushClipRect() to affect logic (hit-testing and widget culling) -void ImDrawList::PushClipRect(ImVec2 cr_min, ImVec2 cr_max, bool intersect_with_current_clip_rect) +void ImDrawList::PushClipRect(const ImVec2& cr_min, const ImVec2& cr_max, bool intersect_with_current_clip_rect) { ImVec4 cr(cr_min.x, cr_min.y, cr_max.x, cr_max.y); - if (intersect_with_current_clip_rect && _ClipRectStack.Size) + if (intersect_with_current_clip_rect) { - ImVec4 current = _ClipRectStack.Data[_ClipRectStack.Size-1]; + ImVec4 current = _CmdHeader.ClipRect; if (cr.x < current.x) cr.x = current.x; if (cr.y < current.y) cr.y = current.y; if (cr.z > current.z) cr.z = current.z; @@ -488,7 +637,8 @@ void ImDrawList::PushClipRect(ImVec2 cr_min, ImVec2 cr_max, bool intersect_with_ cr.w = ImMax(cr.y, cr.w); _ClipRectStack.push_back(cr); - UpdateClipRect(); + _CmdHeader.ClipRect = cr; + _OnChangedClipRect(); } void ImDrawList::PushClipRectFullScreen() @@ -498,37 +648,52 @@ void ImDrawList::PushClipRectFullScreen() void ImDrawList::PopClipRect() { - IM_ASSERT(_ClipRectStack.Size > 0); _ClipRectStack.pop_back(); - UpdateClipRect(); + _CmdHeader.ClipRect = (_ClipRectStack.Size == 0) ? _Data->ClipRectFullscreen : _ClipRectStack.Data[_ClipRectStack.Size - 1]; + _OnChangedClipRect(); } void ImDrawList::PushTextureID(ImTextureID texture_id) { _TextureIdStack.push_back(texture_id); - UpdateTextureID(); + _CmdHeader.TextureId = texture_id; + _OnChangedTextureID(); } void ImDrawList::PopTextureID() { - IM_ASSERT(_TextureIdStack.Size > 0); _TextureIdStack.pop_back(); - UpdateTextureID(); + _CmdHeader.TextureId = (_TextureIdStack.Size == 0) ? (ImTextureID)NULL : _TextureIdStack.Data[_TextureIdStack.Size - 1]; + _OnChangedTextureID(); } -// NB: this can be called with negative count for removing primitives (as long as the result does not underflow) +// This is used by ImGui::PushFont()/PopFont(). It works because we never use _TextureIdStack[] elsewhere than in PushTextureID()/PopTextureID(). +void ImDrawList::_SetTextureID(ImTextureID texture_id) +{ + if (_CmdHeader.TextureId == texture_id) + return; + _CmdHeader.TextureId = texture_id; + _OnChangedTextureID(); +} + +// Reserve space for a number of vertices and indices. +// You must finish filling your reserved data before calling PrimReserve() again, as it may reallocate or +// submit the intermediate results. PrimUnreserve() can be used to release unused allocations. void ImDrawList::PrimReserve(int idx_count, int vtx_count) { // Large mesh support (when enabled) + IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); if (sizeof(ImDrawIdx) == 2 && (_VtxCurrentIdx + vtx_count >= (1 << 16)) && (Flags & ImDrawListFlags_AllowVtxOffset)) { - _VtxCurrentOffset = VtxBuffer.Size; - _VtxCurrentIdx = 0; - AddDrawCmd(); + // FIXME: In theory we should be testing that vtx_count <64k here. + // In practice, RenderText() relies on reserving ahead for a worst case scenario so it is currently useful for us + // to not make that check until we rework the text functions to handle clipping and large horizontal lines better. + _CmdHeader.VtxOffset = VtxBuffer.Size; + _OnChangedVtxOffset(); } - ImDrawCmd& draw_cmd = CmdBuffer.Data[CmdBuffer.Size-1]; - draw_cmd.ElemCount += idx_count; + ImDrawCmd* draw_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + draw_cmd->ElemCount += idx_count; int vtx_buffer_old_size = VtxBuffer.Size; VtxBuffer.resize(vtx_buffer_old_size + vtx_count); @@ -539,6 +704,17 @@ void ImDrawList::PrimReserve(int idx_count, int vtx_count) _IdxWritePtr = IdxBuffer.Data + idx_buffer_old_size; } +// Release the number of reserved vertices/indices from the end of the last reservation made with PrimReserve(). +void ImDrawList::PrimUnreserve(int idx_count, int vtx_count) +{ + IM_ASSERT_PARANOID(idx_count >= 0 && vtx_count >= 0); + + ImDrawCmd* draw_cmd = &CmdBuffer.Data[CmdBuffer.Size - 1]; + draw_cmd->ElemCount -= idx_count; + VtxBuffer.shrink(VtxBuffer.Size - vtx_count); + IdxBuffer.shrink(IdxBuffer.Size - idx_count); +} + // Fully unrolled with inline call to keep our debug builds decently fast. void ImDrawList::PrimRect(const ImVec2& a, const ImVec2& c, ImU32 col) { @@ -584,42 +760,58 @@ void ImDrawList::PrimQuadUV(const ImVec2& a, const ImVec2& b, const ImVec2& c, c _IdxWritePtr += 6; } -// On AddPolyline() and AddConvexPolyFilled() we intentionally avoid using ImVec2 and superflous function calls to optimize debug/non-inlined builds. -// Those macros expects l-values. -#define IM_NORMALIZE2F_OVER_ZERO(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.0f) { float inv_len = 1.0f / ImSqrt(d2); VX *= inv_len; VY *= inv_len; } } -#define IM_FIXNORMAL2F(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 < 0.5f) d2 = 0.5f; float inv_lensq = 1.0f / d2; VX *= inv_lensq; VY *= inv_lensq; } +// On AddPolyline() and AddConvexPolyFilled() we intentionally avoid using ImVec2 and superfluous function calls to optimize debug/non-inlined builds. +// - Those macros expects l-values and need to be used as their own statement. +// - Those macros are intentionally not surrounded by the 'do {} while (0)' idiom because even that translates to runtime with debug compilers. +#define IM_NORMALIZE2F_OVER_ZERO(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.0f) { float inv_len = ImRsqrt(d2); VX *= inv_len; VY *= inv_len; } } (void)0 +#define IM_FIXNORMAL2F_MAX_INVLEN2 100.0f // 500.0f (see #4053, #3366) +#define IM_FIXNORMAL2F(VX,VY) { float d2 = VX*VX + VY*VY; if (d2 > 0.000001f) { float inv_len2 = 1.0f / d2; if (inv_len2 > IM_FIXNORMAL2F_MAX_INVLEN2) inv_len2 = IM_FIXNORMAL2F_MAX_INVLEN2; VX *= inv_len2; VY *= inv_len2; } } (void)0 // TODO: Thickness anti-aliased lines cap are missing their AA fringe. // We avoid using the ImVec2 math operators here to reduce cost to a minimum for debug/non-inlined builds. -void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, bool closed, float thickness) +void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 col, ImDrawFlags flags, float thickness) { - if (points_count < 2) + if (points_count < 2 || (col & IM_COL32_A_MASK) == 0) return; - const ImVec2 uv = _Data->TexUvWhitePixel; + const bool closed = (flags & ImDrawFlags_Closed) != 0; + const ImVec2 opaque_uv = _Data->TexUvWhitePixel; + const int count = closed ? points_count : points_count - 1; // The number of line segments we need to draw + const bool thick_line = (thickness > _FringeScale); - int count = points_count; - if (!closed) - count = points_count-1; - - const bool thick_line = thickness > 1.0f; if (Flags & ImDrawListFlags_AntiAliasedLines) { // Anti-aliased stroke - const float AA_SIZE = 1.0f; + const float AA_SIZE = _FringeScale; const ImU32 col_trans = col & ~IM_COL32_A_MASK; - const int idx_count = thick_line ? count*18 : count*12; - const int vtx_count = thick_line ? points_count*4 : points_count*3; + // Thicknesses <1.0 should behave like thickness 1.0 + thickness = ImMax(thickness, 1.0f); + const int integer_thickness = (int)thickness; + const float fractional_thickness = thickness - integer_thickness; + + // Do we want to draw this line using a texture? + // - For now, only draw integer-width lines using textures to avoid issues with the way scaling occurs, could be improved. + // - If AA_SIZE is not 1.0f we cannot use the texture path. + const bool use_texture = (Flags & ImDrawListFlags_AntiAliasedLinesUseTex) && (integer_thickness < IM_DRAWLIST_TEX_LINES_WIDTH_MAX) && (fractional_thickness <= 0.00001f) && (AA_SIZE == 1.0f); + + // We should never hit this, because NewFrame() doesn't set ImDrawListFlags_AntiAliasedLinesUseTex unless ImFontAtlasFlags_NoBakedLines is off + IM_ASSERT_PARANOID(!use_texture || !(_Data->Font->ContainerAtlas->Flags & ImFontAtlasFlags_NoBakedLines)); + + const int idx_count = use_texture ? (count * 6) : (thick_line ? count * 18 : count * 12); + const int vtx_count = use_texture ? (points_count * 2) : (thick_line ? points_count * 4 : points_count * 3); PrimReserve(idx_count, vtx_count); // Temporary buffer - ImVec2* temp_normals = (ImVec2*)alloca(points_count * (thick_line ? 5 : 3) * sizeof(ImVec2)); //-V630 + // The first items are normals at each line point, then after that there are either 2 or 4 temp points for each line point + _Data->TempBuffer.reserve_discard(points_count * ((use_texture || !thick_line) ? 3 : 5)); + ImVec2* temp_normals = _Data->TempBuffer.Data; ImVec2* temp_points = temp_normals + points_count; + // Calculate normals (tangents) for each line segment for (int i1 = 0; i1 < count; i1++) { - const int i2 = (i1+1) == points_count ? 0 : i1+1; + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; float dx = points[i2].x - points[i1].x; float dy = points[i2].y - points[i1].y; IM_NORMALIZE2F_OVER_ZERO(dx, dy); @@ -627,79 +819,134 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 temp_normals[i1].y = -dx; } if (!closed) - temp_normals[points_count-1] = temp_normals[points_count-2]; + temp_normals[points_count - 1] = temp_normals[points_count - 2]; - if (!thick_line) + // If we are drawing a one-pixel-wide line without a texture, or a textured line of any width, we only need 2 or 3 vertices per point + if (use_texture || !thick_line) { + // [PATH 1] Texture-based lines (thick or non-thick) + // [PATH 2] Non texture-based lines (non-thick) + + // The width of the geometry we need to draw - this is essentially pixels for the line itself, plus "one pixel" for AA. + // - In the texture-based path, we don't use AA_SIZE here because the +1 is tied to the generated texture + // (see ImFontAtlasBuildRenderLinesTexData() function), and so alternate values won't work without changes to that code. + // - In the non texture-based paths, we would allow AA_SIZE to potentially be != 1.0f with a patch (e.g. fringe_scale patch to + // allow scaling geometry while preserving one-screen-pixel AA fringe). + const float half_draw_size = use_texture ? ((thickness * 0.5f) + 1) : AA_SIZE; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend if (!closed) { - temp_points[0] = points[0] + temp_normals[0] * AA_SIZE; - temp_points[1] = points[0] - temp_normals[0] * AA_SIZE; - temp_points[(points_count-1)*2+0] = points[points_count-1] + temp_normals[points_count-1] * AA_SIZE; - temp_points[(points_count-1)*2+1] = points[points_count-1] - temp_normals[points_count-1] * AA_SIZE; + temp_points[0] = points[0] + temp_normals[0] * half_draw_size; + temp_points[1] = points[0] - temp_normals[0] * half_draw_size; + temp_points[(points_count-1)*2+0] = points[points_count-1] + temp_normals[points_count-1] * half_draw_size; + temp_points[(points_count-1)*2+1] = points[points_count-1] - temp_normals[points_count-1] * half_draw_size; } + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. - unsigned int idx1 = _VtxCurrentIdx; - for (int i1 = 0; i1 < count; i1++) + unsigned int idx1 = _VtxCurrentIdx; // Vertex index for start of line segment + for (int i1 = 0; i1 < count; i1++) // i1 is the first point of the line segment { - const int i2 = (i1+1) == points_count ? 0 : i1+1; - unsigned int idx2 = (i1+1) == points_count ? _VtxCurrentIdx : idx1+3; + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; // i2 is the second point of the line segment + const unsigned int idx2 = ((i1 + 1) == points_count) ? _VtxCurrentIdx : (idx1 + (use_texture ? 2 : 3)); // Vertex index for end of segment // Average normals float dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5f; float dm_y = (temp_normals[i1].y + temp_normals[i2].y) * 0.5f; - IM_FIXNORMAL2F(dm_x, dm_y) - dm_x *= AA_SIZE; - dm_y *= AA_SIZE; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= half_draw_size; // dm_x, dm_y are offset to the outer edge of the AA area + dm_y *= half_draw_size; - // Add temporary vertexes - ImVec2* out_vtx = &temp_points[i2*2]; + // Add temporary vertexes for the outer edges + ImVec2* out_vtx = &temp_points[i2 * 2]; out_vtx[0].x = points[i2].x + dm_x; out_vtx[0].y = points[i2].y + dm_y; out_vtx[1].x = points[i2].x - dm_x; out_vtx[1].y = points[i2].y - dm_y; - // Add indexes - _IdxWritePtr[0] = (ImDrawIdx)(idx2+0); _IdxWritePtr[1] = (ImDrawIdx)(idx1+0); _IdxWritePtr[2] = (ImDrawIdx)(idx1+2); - _IdxWritePtr[3] = (ImDrawIdx)(idx1+2); _IdxWritePtr[4] = (ImDrawIdx)(idx2+2); _IdxWritePtr[5] = (ImDrawIdx)(idx2+0); - _IdxWritePtr[6] = (ImDrawIdx)(idx2+1); _IdxWritePtr[7] = (ImDrawIdx)(idx1+1); _IdxWritePtr[8] = (ImDrawIdx)(idx1+0); - _IdxWritePtr[9] = (ImDrawIdx)(idx1+0); _IdxWritePtr[10]= (ImDrawIdx)(idx2+0); _IdxWritePtr[11]= (ImDrawIdx)(idx2+1); - _IdxWritePtr += 12; + if (use_texture) + { + // Add indices for two triangles + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 1); // Right tri + _IdxWritePtr[3] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[4] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 0); // Left tri + _IdxWritePtr += 6; + } + else + { + // Add indexes for four triangles + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 2); // Right tri 1 + _IdxWritePtr[3] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[4] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 0); // Right tri 2 + _IdxWritePtr[6] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[7] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[8] = (ImDrawIdx)(idx1 + 0); // Left tri 1 + _IdxWritePtr[9] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[10] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[11] = (ImDrawIdx)(idx2 + 1); // Left tri 2 + _IdxWritePtr += 12; + } idx1 = idx2; } - // Add vertexes - for (int i = 0; i < points_count; i++) + // Add vertexes for each point on the line + if (use_texture) { - _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; - _VtxWritePtr[1].pos = temp_points[i*2+0]; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; - _VtxWritePtr[2].pos = temp_points[i*2+1]; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col_trans; - _VtxWritePtr += 3; + // If we're using textures we only need to emit the left/right edge vertices + ImVec4 tex_uvs = _Data->TexUvLines[integer_thickness]; + /*if (fractional_thickness != 0.0f) // Currently always zero when use_texture==false! + { + const ImVec4 tex_uvs_1 = _Data->TexUvLines[integer_thickness + 1]; + tex_uvs.x = tex_uvs.x + (tex_uvs_1.x - tex_uvs.x) * fractional_thickness; // inlined ImLerp() + tex_uvs.y = tex_uvs.y + (tex_uvs_1.y - tex_uvs.y) * fractional_thickness; + tex_uvs.z = tex_uvs.z + (tex_uvs_1.z - tex_uvs.z) * fractional_thickness; + tex_uvs.w = tex_uvs.w + (tex_uvs_1.w - tex_uvs.w) * fractional_thickness; + }*/ + ImVec2 tex_uv0(tex_uvs.x, tex_uvs.y); + ImVec2 tex_uv1(tex_uvs.z, tex_uvs.w); + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = temp_points[i * 2 + 0]; _VtxWritePtr[0].uv = tex_uv0; _VtxWritePtr[0].col = col; // Left-side outer edge + _VtxWritePtr[1].pos = temp_points[i * 2 + 1]; _VtxWritePtr[1].uv = tex_uv1; _VtxWritePtr[1].col = col; // Right-side outer edge + _VtxWritePtr += 2; + } + } + else + { + // If we're not using a texture, we need the center vertex as well + for (int i = 0; i < points_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col; // Center of line + _VtxWritePtr[1].pos = temp_points[i * 2 + 0]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col_trans; // Left-side outer edge + _VtxWritePtr[2].pos = temp_points[i * 2 + 1]; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col_trans; // Right-side outer edge + _VtxWritePtr += 3; + } } } else { + // [PATH 2] Non texture-based lines (thick): we need to draw the solid line core and thus require four vertices per point const float half_inner_thickness = (thickness - AA_SIZE) * 0.5f; + + // If line is not closed, the first and last points need to be generated differently as there are no normals to blend if (!closed) { + const int points_last = points_count - 1; temp_points[0] = points[0] + temp_normals[0] * (half_inner_thickness + AA_SIZE); temp_points[1] = points[0] + temp_normals[0] * (half_inner_thickness); temp_points[2] = points[0] - temp_normals[0] * (half_inner_thickness); temp_points[3] = points[0] - temp_normals[0] * (half_inner_thickness + AA_SIZE); - temp_points[(points_count-1)*4+0] = points[points_count-1] + temp_normals[points_count-1] * (half_inner_thickness + AA_SIZE); - temp_points[(points_count-1)*4+1] = points[points_count-1] + temp_normals[points_count-1] * (half_inner_thickness); - temp_points[(points_count-1)*4+2] = points[points_count-1] - temp_normals[points_count-1] * (half_inner_thickness); - temp_points[(points_count-1)*4+3] = points[points_count-1] - temp_normals[points_count-1] * (half_inner_thickness + AA_SIZE); + temp_points[points_last * 4 + 0] = points[points_last] + temp_normals[points_last] * (half_inner_thickness + AA_SIZE); + temp_points[points_last * 4 + 1] = points[points_last] + temp_normals[points_last] * (half_inner_thickness); + temp_points[points_last * 4 + 2] = points[points_last] - temp_normals[points_last] * (half_inner_thickness); + temp_points[points_last * 4 + 3] = points[points_last] - temp_normals[points_last] * (half_inner_thickness + AA_SIZE); } + // Generate the indices to form a number of triangles for each line segment, and the vertices for the line edges + // This takes points n and n+1 and writes into n+1, with the first point in a closed line being generated from the final one (as n+1 wraps) // FIXME-OPT: Merge the different loops, possibly remove the temporary buffer. - unsigned int idx1 = _VtxCurrentIdx; - for (int i1 = 0; i1 < count; i1++) + unsigned int idx1 = _VtxCurrentIdx; // Vertex index for start of line segment + for (int i1 = 0; i1 < count; i1++) // i1 is the first point of the line segment { - const int i2 = (i1+1) == points_count ? 0 : i1+1; - unsigned int idx2 = (i1+1) == points_count ? _VtxCurrentIdx : idx1+4; + const int i2 = (i1 + 1) == points_count ? 0 : (i1 + 1); // i2 is the second point of the line segment + const unsigned int idx2 = (i1 + 1) == points_count ? _VtxCurrentIdx : (idx1 + 4); // Vertex index for end of segment // Average normals float dm_x = (temp_normals[i1].x + temp_normals[i2].x) * 0.5f; @@ -710,8 +957,8 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 float dm_in_x = dm_x * half_inner_thickness; float dm_in_y = dm_y * half_inner_thickness; - // Add temporary vertexes - ImVec2* out_vtx = &temp_points[i2*4]; + // Add temporary vertices + ImVec2* out_vtx = &temp_points[i2 * 4]; out_vtx[0].x = points[i2].x + dm_out_x; out_vtx[0].y = points[i2].y + dm_out_y; out_vtx[1].x = points[i2].x + dm_in_x; @@ -722,24 +969,24 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 out_vtx[3].y = points[i2].y - dm_out_y; // Add indexes - _IdxWritePtr[0] = (ImDrawIdx)(idx2+1); _IdxWritePtr[1] = (ImDrawIdx)(idx1+1); _IdxWritePtr[2] = (ImDrawIdx)(idx1+2); - _IdxWritePtr[3] = (ImDrawIdx)(idx1+2); _IdxWritePtr[4] = (ImDrawIdx)(idx2+2); _IdxWritePtr[5] = (ImDrawIdx)(idx2+1); - _IdxWritePtr[6] = (ImDrawIdx)(idx2+1); _IdxWritePtr[7] = (ImDrawIdx)(idx1+1); _IdxWritePtr[8] = (ImDrawIdx)(idx1+0); - _IdxWritePtr[9] = (ImDrawIdx)(idx1+0); _IdxWritePtr[10] = (ImDrawIdx)(idx2+0); _IdxWritePtr[11] = (ImDrawIdx)(idx2+1); - _IdxWritePtr[12] = (ImDrawIdx)(idx2+2); _IdxWritePtr[13] = (ImDrawIdx)(idx1+2); _IdxWritePtr[14] = (ImDrawIdx)(idx1+3); - _IdxWritePtr[15] = (ImDrawIdx)(idx1+3); _IdxWritePtr[16] = (ImDrawIdx)(idx2+3); _IdxWritePtr[17] = (ImDrawIdx)(idx2+2); + _IdxWritePtr[0] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[1] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[2] = (ImDrawIdx)(idx1 + 2); + _IdxWritePtr[3] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[4] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[5] = (ImDrawIdx)(idx2 + 1); + _IdxWritePtr[6] = (ImDrawIdx)(idx2 + 1); _IdxWritePtr[7] = (ImDrawIdx)(idx1 + 1); _IdxWritePtr[8] = (ImDrawIdx)(idx1 + 0); + _IdxWritePtr[9] = (ImDrawIdx)(idx1 + 0); _IdxWritePtr[10] = (ImDrawIdx)(idx2 + 0); _IdxWritePtr[11] = (ImDrawIdx)(idx2 + 1); + _IdxWritePtr[12] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr[13] = (ImDrawIdx)(idx1 + 2); _IdxWritePtr[14] = (ImDrawIdx)(idx1 + 3); + _IdxWritePtr[15] = (ImDrawIdx)(idx1 + 3); _IdxWritePtr[16] = (ImDrawIdx)(idx2 + 3); _IdxWritePtr[17] = (ImDrawIdx)(idx2 + 2); _IdxWritePtr += 18; idx1 = idx2; } - // Add vertexes + // Add vertices for (int i = 0; i < points_count; i++) { - _VtxWritePtr[0].pos = temp_points[i*4+0]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col_trans; - _VtxWritePtr[1].pos = temp_points[i*4+1]; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; - _VtxWritePtr[2].pos = temp_points[i*4+2]; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; - _VtxWritePtr[3].pos = temp_points[i*4+3]; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col_trans; + _VtxWritePtr[0].pos = temp_points[i * 4 + 0]; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col_trans; + _VtxWritePtr[1].pos = temp_points[i * 4 + 1]; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos = temp_points[i * 4 + 2]; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos = temp_points[i * 4 + 3]; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = col_trans; _VtxWritePtr += 4; } } @@ -747,14 +994,14 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 } else { - // Non Anti-aliased Stroke - const int idx_count = count*6; - const int vtx_count = count*4; // FIXME-OPT: Not sharing edges + // [PATH 4] Non texture-based, Non anti-aliased lines + const int idx_count = count * 6; + const int vtx_count = count * 4; // FIXME-OPT: Not sharing edges PrimReserve(idx_count, vtx_count); for (int i1 = 0; i1 < count; i1++) { - const int i2 = (i1+1) == points_count ? 0 : i1+1; + const int i2 = (i1 + 1) == points_count ? 0 : i1 + 1; const ImVec2& p1 = points[i1]; const ImVec2& p2 = points[i2]; @@ -764,24 +1011,25 @@ void ImDrawList::AddPolyline(const ImVec2* points, const int points_count, ImU32 dx *= (thickness * 0.5f); dy *= (thickness * 0.5f); - _VtxWritePtr[0].pos.x = p1.x + dy; _VtxWritePtr[0].pos.y = p1.y - dx; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; - _VtxWritePtr[1].pos.x = p2.x + dy; _VtxWritePtr[1].pos.y = p2.y - dx; _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col; - _VtxWritePtr[2].pos.x = p2.x - dy; _VtxWritePtr[2].pos.y = p2.y + dx; _VtxWritePtr[2].uv = uv; _VtxWritePtr[2].col = col; - _VtxWritePtr[3].pos.x = p1.x - dy; _VtxWritePtr[3].pos.y = p1.y + dx; _VtxWritePtr[3].uv = uv; _VtxWritePtr[3].col = col; + _VtxWritePtr[0].pos.x = p1.x + dy; _VtxWritePtr[0].pos.y = p1.y - dx; _VtxWritePtr[0].uv = opaque_uv; _VtxWritePtr[0].col = col; + _VtxWritePtr[1].pos.x = p2.x + dy; _VtxWritePtr[1].pos.y = p2.y - dx; _VtxWritePtr[1].uv = opaque_uv; _VtxWritePtr[1].col = col; + _VtxWritePtr[2].pos.x = p2.x - dy; _VtxWritePtr[2].pos.y = p2.y + dx; _VtxWritePtr[2].uv = opaque_uv; _VtxWritePtr[2].col = col; + _VtxWritePtr[3].pos.x = p1.x - dy; _VtxWritePtr[3].pos.y = p1.y + dx; _VtxWritePtr[3].uv = opaque_uv; _VtxWritePtr[3].col = col; _VtxWritePtr += 4; - _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx+1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx+2); - _IdxWritePtr[3] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[4] = (ImDrawIdx)(_VtxCurrentIdx+2); _IdxWritePtr[5] = (ImDrawIdx)(_VtxCurrentIdx+3); + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + 1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + 2); + _IdxWritePtr[3] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[4] = (ImDrawIdx)(_VtxCurrentIdx + 2); _IdxWritePtr[5] = (ImDrawIdx)(_VtxCurrentIdx + 3); _IdxWritePtr += 6; _VtxCurrentIdx += 4; } } } -// We intentionally avoid using ImVec2 and its math operators here to reduce cost to a minimum for debug/non-inlined builds. +// - We intentionally avoid using ImVec2 and its math operators here to reduce cost to a minimum for debug/non-inlined builds. +// - Filled shapes must always use clockwise winding order. The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_count, ImU32 col) { - if (points_count < 3) + if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) return; const ImVec2 uv = _Data->TexUvWhitePixel; @@ -789,24 +1037,25 @@ void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_coun if (Flags & ImDrawListFlags_AntiAliasedFill) { // Anti-aliased Fill - const float AA_SIZE = 1.0f; + const float AA_SIZE = _FringeScale; const ImU32 col_trans = col & ~IM_COL32_A_MASK; - const int idx_count = (points_count-2)*3 + points_count*6; - const int vtx_count = (points_count*2); + const int idx_count = (points_count - 2)*3 + points_count * 6; + const int vtx_count = (points_count * 2); PrimReserve(idx_count, vtx_count); // Add indexes for fill unsigned int vtx_inner_idx = _VtxCurrentIdx; - unsigned int vtx_outer_idx = _VtxCurrentIdx+1; + unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; for (int i = 2; i < points_count; i++) { - _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx+((i-1)<<1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx+(i<<1)); + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + ((i - 1) << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + (i << 1)); _IdxWritePtr += 3; } // Compute normals - ImVec2* temp_normals = (ImVec2*)alloca(points_count * sizeof(ImVec2)); //-V630 - for (int i0 = points_count-1, i1 = 0; i1 < points_count; i0 = i1++) + _Data->TempBuffer.reserve_discard(points_count); + ImVec2* temp_normals = _Data->TempBuffer.Data; + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) { const ImVec2& p0 = points[i0]; const ImVec2& p1 = points[i1]; @@ -817,7 +1066,7 @@ void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_coun temp_normals[i0].y = -dx; } - for (int i0 = points_count-1, i1 = 0; i1 < points_count; i0 = i1++) + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) { // Average normals const ImVec2& n0 = temp_normals[i0]; @@ -834,8 +1083,8 @@ void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_coun _VtxWritePtr += 2; // Add indexes for fringes - _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx+(i1<<1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx+(i0<<1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx+(i0<<1)); - _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx+(i0<<1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx+(i1<<1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx+(i1<<1)); + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (i0 << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx + (i1 << 1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr += 6; } _VtxCurrentIdx += (ImDrawIdx)vtx_count; @@ -843,7 +1092,7 @@ void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_coun else { // Non Anti-aliased Fill - const int idx_count = (points_count-2)*3; + const int idx_count = (points_count - 2)*3; const int vtx_count = points_count; PrimReserve(idx_count, vtx_count); for (int i = 0; i < vtx_count; i++) @@ -853,31 +1102,108 @@ void ImDrawList::AddConvexPolyFilled(const ImVec2* points, const int points_coun } for (int i = 2; i < points_count; i++) { - _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx+i-1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx+i); + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + i - 1); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + i); _IdxWritePtr += 3; } _VtxCurrentIdx += (ImDrawIdx)vtx_count; } } -void ImDrawList::PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12) +void ImDrawList::_PathArcToFastEx(const ImVec2& center, float radius, int a_min_sample, int a_max_sample, int a_step) { - if (radius == 0.0f || a_min_of_12 > a_max_of_12) + if (radius < 0.5f) { _Path.push_back(center); return; } - _Path.reserve(_Path.Size + (a_max_of_12 - a_min_of_12 + 1)); - for (int a = a_min_of_12; a <= a_max_of_12; a++) + + // Calculate arc auto segment step size + if (a_step <= 0) + a_step = IM_DRAWLIST_ARCFAST_SAMPLE_MAX / _CalcCircleAutoSegmentCount(radius); + + // Make sure we never do steps larger than one quarter of the circle + a_step = ImClamp(a_step, 1, IM_DRAWLIST_ARCFAST_TABLE_SIZE / 4); + + const int sample_range = ImAbs(a_max_sample - a_min_sample); + const int a_next_step = a_step; + + int samples = sample_range + 1; + bool extra_max_sample = false; + if (a_step > 1) { - const ImVec2& c = _Data->CircleVtx12[a % IM_ARRAYSIZE(_Data->CircleVtx12)]; - _Path.push_back(ImVec2(center.x + c.x * radius, center.y + c.y * radius)); + samples = sample_range / a_step + 1; + const int overstep = sample_range % a_step; + + if (overstep > 0) + { + extra_max_sample = true; + samples++; + + // When we have overstep to avoid awkwardly looking one long line and one tiny one at the end, + // distribute first step range evenly between them by reducing first step size. + if (sample_range > 0) + a_step -= (a_step - overstep) / 2; + } } + + _Path.resize(_Path.Size + samples); + ImVec2* out_ptr = _Path.Data + (_Path.Size - samples); + + int sample_index = a_min_sample; + if (sample_index < 0 || sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX) + { + sample_index = sample_index % IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + if (sample_index < 0) + sample_index += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + } + + if (a_max_sample >= a_min_sample) + { + for (int a = a_min_sample; a <= a_max_sample; a += a_step, sample_index += a_step, a_step = a_next_step) + { + // a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more + if (sample_index >= IM_DRAWLIST_ARCFAST_SAMPLE_MAX) + sample_index -= IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[sample_index]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + } + else + { + for (int a = a_min_sample; a >= a_max_sample; a -= a_step, sample_index -= a_step, a_step = a_next_step) + { + // a_step is clamped to IM_DRAWLIST_ARCFAST_SAMPLE_MAX, so we have guaranteed that it will not wrap over range twice or more + if (sample_index < 0) + sample_index += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[sample_index]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + } + + if (extra_max_sample) + { + int normalized_max_sample = a_max_sample % IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + if (normalized_max_sample < 0) + normalized_max_sample += IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + + const ImVec2 s = _Data->ArcFastVtx[normalized_max_sample]; + out_ptr->x = center.x + s.x * radius; + out_ptr->y = center.y + s.y * radius; + out_ptr++; + } + + IM_ASSERT_PARANOID(_Path.Data + _Path.Size == out_ptr); } -void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) +void ImDrawList::_PathArcToN(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) { - if (radius == 0.0f) + if (radius < 0.5f) { _Path.push_back(center); return; @@ -893,62 +1219,215 @@ void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, floa } } -static void PathBezierToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) +// 0: East, 3: South, 6: West, 9: North, 12: East +void ImDrawList::PathArcToFast(const ImVec2& center, float radius, int a_min_of_12, int a_max_of_12) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + _PathArcToFastEx(center, radius, a_min_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, a_max_of_12 * IM_DRAWLIST_ARCFAST_SAMPLE_MAX / 12, 0); +} + +void ImDrawList::PathArcTo(const ImVec2& center, float radius, float a_min, float a_max, int num_segments) +{ + if (radius < 0.5f) + { + _Path.push_back(center); + return; + } + + if (num_segments > 0) + { + _PathArcToN(center, radius, a_min, a_max, num_segments); + return; + } + + // Automatic segment count + if (radius <= _Data->ArcFastRadiusCutoff) + { + const bool a_is_reverse = a_max < a_min; + + // We are going to use precomputed values for mid samples. + // Determine first and last sample in lookup table that belong to the arc. + const float a_min_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_min / (IM_PI * 2.0f); + const float a_max_sample_f = IM_DRAWLIST_ARCFAST_SAMPLE_MAX * a_max / (IM_PI * 2.0f); + + const int a_min_sample = a_is_reverse ? (int)ImFloor(a_min_sample_f) : (int)ImCeil(a_min_sample_f); + const int a_max_sample = a_is_reverse ? (int)ImCeil(a_max_sample_f) : (int)ImFloor(a_max_sample_f); + const int a_mid_samples = a_is_reverse ? ImMax(a_min_sample - a_max_sample, 0) : ImMax(a_max_sample - a_min_sample, 0); + + const float a_min_segment_angle = a_min_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + const float a_max_segment_angle = a_max_sample * IM_PI * 2.0f / IM_DRAWLIST_ARCFAST_SAMPLE_MAX; + const bool a_emit_start = ImAbs(a_min_segment_angle - a_min) >= 1e-5f; + const bool a_emit_end = ImAbs(a_max - a_max_segment_angle) >= 1e-5f; + + _Path.reserve(_Path.Size + (a_mid_samples + 1 + (a_emit_start ? 1 : 0) + (a_emit_end ? 1 : 0))); + if (a_emit_start) + _Path.push_back(ImVec2(center.x + ImCos(a_min) * radius, center.y + ImSin(a_min) * radius)); + if (a_mid_samples > 0) + _PathArcToFastEx(center, radius, a_min_sample, a_max_sample, 0); + if (a_emit_end) + _Path.push_back(ImVec2(center.x + ImCos(a_max) * radius, center.y + ImSin(a_max) * radius)); + } + else + { + const float arc_length = ImAbs(a_max - a_min); + const int circle_segment_count = _CalcCircleAutoSegmentCount(radius); + const int arc_segment_count = ImMax((int)ImCeil(circle_segment_count * arc_length / (IM_PI * 2.0f)), (int)(2.0f * IM_PI / arc_length)); + _PathArcToN(center, radius, a_min, a_max, arc_segment_count); + } +} + +void ImDrawList::PathEllipticalArcTo(const ImVec2& center, const ImVec2& radius, float rot, float a_min, float a_max, int num_segments) +{ + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + _Path.reserve(_Path.Size + (num_segments + 1)); + + const float cos_rot = ImCos(rot); + const float sin_rot = ImSin(rot); + for (int i = 0; i <= num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + ImVec2 point(ImCos(a) * radius.x, ImSin(a) * radius.y); + const ImVec2 rel((point.x * cos_rot) - (point.y * sin_rot), (point.x * sin_rot) + (point.y * cos_rot)); + point.x = rel.x + center.x; + point.y = rel.y + center.y; + _Path.push_back(point); + } +} + +ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t) +{ + float u = 1.0f - t; + float w1 = u * u * u; + float w2 = 3 * u * u * t; + float w3 = 3 * u * t * t; + float w4 = t * t * t; + return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x + w4 * p4.x, w1 * p1.y + w2 * p2.y + w3 * p3.y + w4 * p4.y); +} + +ImVec2 ImBezierQuadraticCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float t) +{ + float u = 1.0f - t; + float w1 = u * u; + float w2 = 2 * u * t; + float w3 = t * t; + return ImVec2(w1 * p1.x + w2 * p2.x + w3 * p3.x, w1 * p1.y + w2 * p2.y + w3 * p3.y); +} + +// Closely mimics ImBezierCubicClosestPointCasteljau() in imgui.cpp +static void PathBezierCubicCurveToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, float tess_tol, int level) { float dx = x4 - x1; float dy = y4 - y1; - float d2 = ((x2 - x4) * dy - (y2 - y4) * dx); - float d3 = ((x3 - x4) * dy - (y3 - y4) * dx); + float d2 = (x2 - x4) * dy - (y2 - y4) * dx; + float d3 = (x3 - x4) * dy - (y3 - y4) * dx; d2 = (d2 >= 0) ? d2 : -d2; d3 = (d3 >= 0) ? d3 : -d3; - if ((d2+d3) * (d2+d3) < tess_tol * (dx*dx + dy*dy)) + if ((d2 + d3) * (d2 + d3) < tess_tol * (dx * dx + dy * dy)) { path->push_back(ImVec2(x4, y4)); } else if (level < 10) { - float x12 = (x1+x2)*0.5f, y12 = (y1+y2)*0.5f; - float x23 = (x2+x3)*0.5f, y23 = (y2+y3)*0.5f; - float x34 = (x3+x4)*0.5f, y34 = (y3+y4)*0.5f; - float x123 = (x12+x23)*0.5f, y123 = (y12+y23)*0.5f; - float x234 = (x23+x34)*0.5f, y234 = (y23+y34)*0.5f; - float x1234 = (x123+x234)*0.5f, y1234 = (y123+y234)*0.5f; - - PathBezierToCasteljau(path, x1,y1, x12,y12, x123,y123, x1234,y1234, tess_tol, level+1); - PathBezierToCasteljau(path, x1234,y1234, x234,y234, x34,y34, x4,y4, tess_tol, level+1); + float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; + float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; + float x34 = (x3 + x4) * 0.5f, y34 = (y3 + y4) * 0.5f; + float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; + float x234 = (x23 + x34) * 0.5f, y234 = (y23 + y34) * 0.5f; + float x1234 = (x123 + x234) * 0.5f, y1234 = (y123 + y234) * 0.5f; + PathBezierCubicCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, x1234, y1234, tess_tol, level + 1); + PathBezierCubicCurveToCasteljau(path, x1234, y1234, x234, y234, x34, y34, x4, y4, tess_tol, level + 1); } } -void ImDrawList::PathBezierCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) +static void PathBezierQuadraticCurveToCasteljau(ImVector* path, float x1, float y1, float x2, float y2, float x3, float y3, float tess_tol, int level) +{ + float dx = x3 - x1, dy = y3 - y1; + float det = (x2 - x3) * dy - (y2 - y3) * dx; + if (det * det * 4.0f < tess_tol * (dx * dx + dy * dy)) + { + path->push_back(ImVec2(x3, y3)); + } + else if (level < 10) + { + float x12 = (x1 + x2) * 0.5f, y12 = (y1 + y2) * 0.5f; + float x23 = (x2 + x3) * 0.5f, y23 = (y2 + y3) * 0.5f; + float x123 = (x12 + x23) * 0.5f, y123 = (y12 + y23) * 0.5f; + PathBezierQuadraticCurveToCasteljau(path, x1, y1, x12, y12, x123, y123, tess_tol, level + 1); + PathBezierQuadraticCurveToCasteljau(path, x123, y123, x23, y23, x3, y3, tess_tol, level + 1); + } +} + +void ImDrawList::PathBezierCubicCurveTo(const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, int num_segments) { ImVec2 p1 = _Path.back(); if (num_segments == 0) { - // Auto-tessellated - PathBezierToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, _Data->CurveTessellationTol, 0); + IM_ASSERT(_Data->CurveTessellationTol > 0.0f); + PathBezierCubicCurveToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, p4.x, p4.y, _Data->CurveTessellationTol, 0); // Auto-tessellated } else { float t_step = 1.0f / (float)num_segments; for (int i_step = 1; i_step <= num_segments; i_step++) - { - float t = t_step * i_step; - float u = 1.0f - t; - float w1 = u*u*u; - float w2 = 3*u*u*t; - float w3 = 3*u*t*t; - float w4 = t*t*t; - _Path.push_back(ImVec2(w1*p1.x + w2*p2.x + w3*p3.x + w4*p4.x, w1*p1.y + w2*p2.y + w3*p3.y + w4*p4.y)); - } + _Path.push_back(ImBezierCubicCalc(p1, p2, p3, p4, t_step * i_step)); } } -void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDrawCornerFlags rounding_corners) +void ImDrawList::PathBezierQuadraticCurveTo(const ImVec2& p2, const ImVec2& p3, int num_segments) { - rounding = ImMin(rounding, ImFabs(b.x - a.x) * ( ((rounding_corners & ImDrawCornerFlags_Top) == ImDrawCornerFlags_Top) || ((rounding_corners & ImDrawCornerFlags_Bot) == ImDrawCornerFlags_Bot) ? 0.5f : 1.0f ) - 1.0f); - rounding = ImMin(rounding, ImFabs(b.y - a.y) * ( ((rounding_corners & ImDrawCornerFlags_Left) == ImDrawCornerFlags_Left) || ((rounding_corners & ImDrawCornerFlags_Right) == ImDrawCornerFlags_Right) ? 0.5f : 1.0f ) - 1.0f); + ImVec2 p1 = _Path.back(); + if (num_segments == 0) + { + IM_ASSERT(_Data->CurveTessellationTol > 0.0f); + PathBezierQuadraticCurveToCasteljau(&_Path, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, _Data->CurveTessellationTol, 0);// Auto-tessellated + } + else + { + float t_step = 1.0f / (float)num_segments; + for (int i_step = 1; i_step <= num_segments; i_step++) + _Path.push_back(ImBezierQuadraticCalc(p1, p2, p3, t_step * i_step)); + } +} - if (rounding <= 0.0f || rounding_corners == 0) +static inline ImDrawFlags FixRectCornerFlags(ImDrawFlags flags) +{ + /* + IM_STATIC_ASSERT(ImDrawFlags_RoundCornersTopLeft == (1 << 4)); +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + // Obsoleted in 1.82 (from February 2021). This code was stripped/simplified and mostly commented in 1.90 (from September 2023) + // - Legacy Support for hard coded ~0 (used to be a suggested equivalent to ImDrawCornerFlags_All) + if (flags == ~0) { return ImDrawFlags_RoundCornersAll; } + // - Legacy Support for hard coded 0x01 to 0x0F (matching 15 out of 16 old flags combinations). Read details in older version of this code. + if (flags >= 0x01 && flags <= 0x0F) { return (flags << 4); } + // We cannot support hard coded 0x00 with 'float rounding > 0.0f' --> replace with ImDrawFlags_RoundCornersNone or use 'float rounding = 0.0f' +#endif + */ + // If this assert triggers, please update your code replacing hardcoded values with new ImDrawFlags_RoundCorners* values. + // Note that ImDrawFlags_Closed (== 0x01) is an invalid flag for AddRect(), AddRectFilled(), PathRect() etc. anyway. + // See details in 1.82 Changelog as well as 2021/03/12 and 2023/09/08 entries in "API BREAKING CHANGES" section. + IM_ASSERT((flags & 0x0F) == 0 && "Misuse of legacy hardcoded ImDrawCornerFlags values!"); + + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags |= ImDrawFlags_RoundCornersAll; + + return flags; +} + +void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDrawFlags flags) +{ + if (rounding >= 0.5f) + { + flags = FixRectCornerFlags(flags); + rounding = ImMin(rounding, ImFabs(b.x - a.x) * (((flags & ImDrawFlags_RoundCornersTop) == ImDrawFlags_RoundCornersTop) || ((flags & ImDrawFlags_RoundCornersBottom) == ImDrawFlags_RoundCornersBottom) ? 0.5f : 1.0f) - 1.0f); + rounding = ImMin(rounding, ImFabs(b.y - a.y) * (((flags & ImDrawFlags_RoundCornersLeft) == ImDrawFlags_RoundCornersLeft) || ((flags & ImDrawFlags_RoundCornersRight) == ImDrawFlags_RoundCornersRight) ? 0.5f : 1.0f) - 1.0f); + } + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) { PathLineTo(a); PathLineTo(ImVec2(b.x, a.y)); @@ -957,10 +1436,10 @@ void ImDrawList::PathRect(const ImVec2& a, const ImVec2& b, float rounding, ImDr } else { - const float rounding_tl = (rounding_corners & ImDrawCornerFlags_TopLeft) ? rounding : 0.0f; - const float rounding_tr = (rounding_corners & ImDrawCornerFlags_TopRight) ? rounding : 0.0f; - const float rounding_br = (rounding_corners & ImDrawCornerFlags_BotRight) ? rounding : 0.0f; - const float rounding_bl = (rounding_corners & ImDrawCornerFlags_BotLeft) ? rounding : 0.0f; + const float rounding_tl = (flags & ImDrawFlags_RoundCornersTopLeft) ? rounding : 0.0f; + const float rounding_tr = (flags & ImDrawFlags_RoundCornersTopRight) ? rounding : 0.0f; + const float rounding_br = (flags & ImDrawFlags_RoundCornersBottomRight) ? rounding : 0.0f; + const float rounding_bl = (flags & ImDrawFlags_RoundCornersBottomLeft) ? rounding : 0.0f; PathArcToFast(ImVec2(a.x + rounding_tl, a.y + rounding_tl), rounding_tl, 6, 9); PathArcToFast(ImVec2(b.x - rounding_tr, a.y + rounding_tr), rounding_tr, 9, 12); PathArcToFast(ImVec2(b.x - rounding_br, b.y - rounding_br), rounding_br, 0, 3); @@ -974,36 +1453,36 @@ void ImDrawList::AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float th return; PathLineTo(p1 + ImVec2(0.5f, 0.5f)); PathLineTo(p2 + ImVec2(0.5f, 0.5f)); - PathStroke(col, false, thickness); + PathStroke(col, 0, thickness); } // p_min = upper-left, p_max = lower-right // Note we don't render 1 pixels sized rectangles properly. -void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawCornerFlags rounding_corners, float thickness) +void ImDrawList::AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags, float thickness) { if ((col & IM_COL32_A_MASK) == 0) return; if (Flags & ImDrawListFlags_AntiAliasedLines) - PathRect(p_min + ImVec2(0.50f,0.50f), p_max - ImVec2(0.50f,0.50f), rounding, rounding_corners); + PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.50f, 0.50f), rounding, flags); else - PathRect(p_min + ImVec2(0.50f,0.50f), p_max - ImVec2(0.49f,0.49f), rounding, rounding_corners); // Better looking lower-right corner and rounded non-AA shapes. - PathStroke(col, true, thickness); + PathRect(p_min + ImVec2(0.50f, 0.50f), p_max - ImVec2(0.49f, 0.49f), rounding, flags); // Better looking lower-right corner and rounded non-AA shapes. + PathStroke(col, ImDrawFlags_Closed, thickness); } -void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawCornerFlags rounding_corners) +void ImDrawList::AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding, ImDrawFlags flags) { if ((col & IM_COL32_A_MASK) == 0) return; - if (rounding > 0.0f) - { - PathRect(p_min, p_max, rounding, rounding_corners); - PathFillConvex(col); - } - else + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) { PrimReserve(6, 4); PrimRect(p_min, p_max, col); } + else + { + PathRect(p_min, p_max, rounding, flags); + PathFillConvex(col); + } } // p_min = upper-left, p_max = lower-right @@ -1014,8 +1493,8 @@ void ImDrawList::AddRectFilledMultiColor(const ImVec2& p_min, const ImVec2& p_ma const ImVec2 uv = _Data->TexUvWhitePixel; PrimReserve(6, 4); - PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx+1)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx+2)); - PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx+2)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx+3)); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 1)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 2)); + PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 2)); PrimWriteIdx((ImDrawIdx)(_VtxCurrentIdx + 3)); PrimWriteVtx(p_min, uv, col_upr_left); PrimWriteVtx(ImVec2(p_max.x, p_min.y), uv, col_upr_right); PrimWriteVtx(p_max, uv, col_bot_right); @@ -1031,7 +1510,7 @@ void ImDrawList::AddQuad(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, c PathLineTo(p2); PathLineTo(p3); PathLineTo(p4); - PathStroke(col, true, thickness); + PathStroke(col, ImDrawFlags_Closed, thickness); } void ImDrawList::AddQuadFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col) @@ -1054,7 +1533,7 @@ void ImDrawList::AddTriangle(const ImVec2& p1, const ImVec2& p2, const ImVec2& p PathLineTo(p1); PathLineTo(p2); PathLineTo(p3); - PathStroke(col, true, thickness); + PathStroke(col, ImDrawFlags_Closed, thickness); } void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col) @@ -1069,6 +1548,55 @@ void ImDrawList::AddTriangleFilled(const ImVec2& p1, const ImVec2& p2, const ImV } void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) +{ + if ((col & IM_COL32_A_MASK) == 0 || radius < 0.5f) + return; + + if (num_segments <= 0) + { + // Use arc with automatic segment count + _PathArcToFastEx(center, radius - 0.5f, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0); + _Path.Size--; + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); + } + + PathStroke(col, ImDrawFlags_Closed, thickness); +} + +void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0 || radius < 0.5f) + return; + + if (num_segments <= 0) + { + // Use arc with automatic segment count + _PathArcToFastEx(center, radius, 0, IM_DRAWLIST_ARCFAST_SAMPLE_MAX, 0); + _Path.Size--; + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + PathArcTo(center, radius, 0.0f, a_max, num_segments - 1); + } + + PathFillConvex(col); +} + +// Guaranteed to honor 'num_segments' +void ImDrawList::AddNgon(const ImVec2& center, float radius, ImU32 col, int num_segments, float thickness) { if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2) return; @@ -1076,10 +1604,11 @@ void ImDrawList::AddCircle(const ImVec2& center, float radius, ImU32 col, int nu // Because we are filling a closed shape we remove 1 from the count of segments/points const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; PathArcTo(center, radius - 0.5f, 0.0f, a_max, num_segments - 1); - PathStroke(col, true, thickness); + PathStroke(col, ImDrawFlags_Closed, thickness); } -void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) +// Guaranteed to honor 'num_segments' +void ImDrawList::AddNgonFilled(const ImVec2& center, float radius, ImU32 col, int num_segments) { if ((col & IM_COL32_A_MASK) == 0 || num_segments <= 2) return; @@ -1090,35 +1619,76 @@ void ImDrawList::AddCircleFilled(const ImVec2& center, float radius, ImU32 col, PathFillConvex(col); } -void ImDrawList::AddBezierCurve(const ImVec2& pos0, const ImVec2& cp0, const ImVec2& cp1, const ImVec2& pos1, ImU32 col, float thickness, int num_segments) +// Ellipse +void ImDrawList::AddEllipse(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments, float thickness) { if ((col & IM_COL32_A_MASK) == 0) return; - PathLineTo(pos0); - PathBezierCurveTo(cp0, cp1, pos1, num_segments); - PathStroke(col, false, thickness); + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); + PathStroke(col, true, thickness); } -void ImDrawList::AddText(const ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) +void ImDrawList::AddEllipseFilled(const ImVec2& center, const ImVec2& radius, ImU32 col, float rot, int num_segments) { if ((col & IM_COL32_A_MASK) == 0) return; - if (text_end == NULL) - text_end = text_begin + strlen(text_begin); - if (text_begin == text_end) + if (num_segments <= 0) + num_segments = _CalcCircleAutoSegmentCount(ImMax(radius.x, radius.y)); // A bit pessimistic, maybe there's a better computation to do here. + + // Because we are filling a closed shape we remove 1 from the count of segments/points + const float a_max = IM_PI * 2.0f * ((float)num_segments - 1.0f) / (float)num_segments; + PathEllipticalArcTo(center, radius, rot, 0.0f, a_max, num_segments - 1); + PathFillConvex(col); +} + +// Cubic Bezier takes 4 controls points +void ImDrawList::AddBezierCubic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) return; + PathLineTo(p1); + PathBezierCubicCurveTo(p2, p3, p4, num_segments); + PathStroke(col, 0, thickness); +} + +// Quadratic Bezier takes 3 controls points +void ImDrawList::AddBezierQuadratic(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, ImU32 col, float thickness, int num_segments) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + PathLineTo(p1); + PathBezierQuadraticCurveTo(p2, p3, num_segments); + PathStroke(col, 0, thickness); +} + +void ImDrawList::AddText(ImFont* font, float font_size, const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end, float wrap_width, const ImVec4* cpu_fine_clip_rect) +{ + if ((col & IM_COL32_A_MASK) == 0) + return; + + // Accept null ranges + if (text_begin == text_end || text_begin[0] == 0) + return; + // No need to strlen() here: font->RenderText() will do it and may early out. + // Pull default font/size from the shared ImDrawListSharedData instance if (font == NULL) font = _Data->Font; if (font_size == 0.0f) font_size = _Data->FontSize; - IM_ASSERT(font->ContainerAtlas->TexID == _TextureIdStack.back()); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. + IM_ASSERT(font->ContainerAtlas->TexID == _CmdHeader.TextureId); // Use high-level ImGui::PushFont() or low-level ImDrawList::PushTextureId() to change font. - ImVec4 clip_rect = _ClipRectStack.back(); + ImVec4 clip_rect = _CmdHeader.ClipRect; if (cpu_fine_clip_rect) { clip_rect.x = ImMax(clip_rect.x, cpu_fine_clip_rect->x); @@ -1131,7 +1701,7 @@ void ImDrawList::AddText(const ImFont* font, float font_size, const ImVec2& pos, void ImDrawList::AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end) { - AddText(NULL, 0.0f, pos, col, text_begin, text_end); + AddText(_Data->Font, _Data->FontSize, pos, col, text_begin, text_end); } void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col) @@ -1139,7 +1709,7 @@ void ImDrawList::AddImage(ImTextureID user_texture_id, const ImVec2& p_min, cons if ((col & IM_COL32_A_MASK) == 0) return; - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; if (push_texture_id) PushTextureID(user_texture_id); @@ -1155,7 +1725,7 @@ void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, con if ((col & IM_COL32_A_MASK) == 0) return; - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; if (push_texture_id) PushTextureID(user_texture_id); @@ -1166,23 +1736,24 @@ void ImDrawList::AddImageQuad(ImTextureID user_texture_id, const ImVec2& p1, con PopTextureID(); } -void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawCornerFlags rounding_corners) +void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags) { if ((col & IM_COL32_A_MASK) == 0) return; - if (rounding <= 0.0f || (rounding_corners & ImDrawCornerFlags_All) == 0) + flags = FixRectCornerFlags(flags); + if (rounding < 0.5f || (flags & ImDrawFlags_RoundCornersMask_) == ImDrawFlags_RoundCornersNone) { AddImage(user_texture_id, p_min, p_max, uv_min, uv_max, col); return; } - const bool push_texture_id = _TextureIdStack.empty() || user_texture_id != _TextureIdStack.back(); + const bool push_texture_id = user_texture_id != _CmdHeader.TextureId; if (push_texture_id) PushTextureID(user_texture_id); int vert_start_idx = VtxBuffer.Size; - PathRect(p_min, p_max, rounding, rounding_corners); + PathRect(p_min, p_max, rounding, flags); PathFillConvex(col); int vert_end_idx = VtxBuffer.Size; ImGui::ShadeVertsLinearUV(this, vert_start_idx, vert_end_idx, p_min, p_max, uv_min, uv_max, true); @@ -1191,9 +1762,319 @@ void ImDrawList::AddImageRounded(ImTextureID user_texture_id, const ImVec2& p_mi PopTextureID(); } +//----------------------------------------------------------------------------- +// [SECTION] ImTriangulator, ImDrawList concave polygon fill +//----------------------------------------------------------------------------- +// Triangulate concave polygons. Based on "Triangulation by Ear Clipping" paper, O(N^2) complexity. +// Reference: https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf +// Provided as a convenience for user but not used by main library. +//----------------------------------------------------------------------------- +// - ImTriangulator [Internal] +// - AddConcavePolyFilled() +//----------------------------------------------------------------------------- + +enum ImTriangulatorNodeType +{ + ImTriangulatorNodeType_Convex, + ImTriangulatorNodeType_Ear, + ImTriangulatorNodeType_Reflex +}; + +struct ImTriangulatorNode +{ + ImTriangulatorNodeType Type; + int Index; + ImVec2 Pos; + ImTriangulatorNode* Next; + ImTriangulatorNode* Prev; + + void Unlink() { Next->Prev = Prev; Prev->Next = Next; } +}; + +struct ImTriangulatorNodeSpan +{ + ImTriangulatorNode** Data = NULL; + int Size = 0; + + void push_back(ImTriangulatorNode* node) { Data[Size++] = node; } + void find_erase_unsorted(int idx) { for (int i = Size - 1; i >= 0; i--) if (Data[i]->Index == idx) { Data[i] = Data[Size - 1]; Size--; return; } } +}; + +struct ImTriangulator +{ + static int EstimateTriangleCount(int points_count) { return (points_count < 3) ? 0 : points_count - 2; } + static int EstimateScratchBufferSize(int points_count) { return sizeof(ImTriangulatorNode) * points_count + sizeof(ImTriangulatorNode*) * points_count * 2; } + + void Init(const ImVec2* points, int points_count, void* scratch_buffer); + void GetNextTriangle(unsigned int out_triangle[3]); // Return relative indexes for next triangle + + // Internal functions + void BuildNodes(const ImVec2* points, int points_count); + void BuildReflexes(); + void BuildEars(); + void FlipNodeList(); + bool IsEar(int i0, int i1, int i2, const ImVec2& v0, const ImVec2& v1, const ImVec2& v2) const; + void ReclassifyNode(ImTriangulatorNode* node); + + // Internal members + int _TrianglesLeft = 0; + ImTriangulatorNode* _Nodes = NULL; + ImTriangulatorNodeSpan _Ears; + ImTriangulatorNodeSpan _Reflexes; +}; + +// Distribute storage for nodes, ears and reflexes. +// FIXME-OPT: if everything is convex, we could report it to caller and let it switch to an convex renderer +// (this would require first building reflexes to bail to convex if empty, without even building nodes) +void ImTriangulator::Init(const ImVec2* points, int points_count, void* scratch_buffer) +{ + IM_ASSERT(scratch_buffer != NULL && points_count >= 3); + _TrianglesLeft = EstimateTriangleCount(points_count); + _Nodes = (ImTriangulatorNode*)scratch_buffer; // points_count x Node + _Ears.Data = (ImTriangulatorNode**)(_Nodes + points_count); // points_count x Node* + _Reflexes.Data = (ImTriangulatorNode**)(_Nodes + points_count) + points_count; // points_count x Node* + BuildNodes(points, points_count); + BuildReflexes(); + BuildEars(); +} + +void ImTriangulator::BuildNodes(const ImVec2* points, int points_count) +{ + for (int i = 0; i < points_count; i++) + { + _Nodes[i].Type = ImTriangulatorNodeType_Convex; + _Nodes[i].Index = i; + _Nodes[i].Pos = points[i]; + _Nodes[i].Next = _Nodes + i + 1; + _Nodes[i].Prev = _Nodes + i - 1; + } + _Nodes[0].Prev = _Nodes + points_count - 1; + _Nodes[points_count - 1].Next = _Nodes; +} + +void ImTriangulator::BuildReflexes() +{ + ImTriangulatorNode* n1 = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, n1 = n1->Next) + { + if (ImTriangleIsClockwise(n1->Prev->Pos, n1->Pos, n1->Next->Pos)) + continue; + n1->Type = ImTriangulatorNodeType_Reflex; + _Reflexes.push_back(n1); + } +} + +void ImTriangulator::BuildEars() +{ + ImTriangulatorNode* n1 = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, n1 = n1->Next) + { + if (n1->Type != ImTriangulatorNodeType_Convex) + continue; + if (!IsEar(n1->Prev->Index, n1->Index, n1->Next->Index, n1->Prev->Pos, n1->Pos, n1->Next->Pos)) + continue; + n1->Type = ImTriangulatorNodeType_Ear; + _Ears.push_back(n1); + } +} + +void ImTriangulator::GetNextTriangle(unsigned int out_triangle[3]) +{ + if (_Ears.Size == 0) + { + FlipNodeList(); + + ImTriangulatorNode* node = _Nodes; + for (int i = _TrianglesLeft; i >= 0; i--, node = node->Next) + node->Type = ImTriangulatorNodeType_Convex; + _Reflexes.Size = 0; + BuildReflexes(); + BuildEars(); + + // If we still don't have ears, it means geometry is degenerated. + if (_Ears.Size == 0) + { + // Return first triangle available, mimicking the behavior of convex fill. + IM_ASSERT(_TrianglesLeft > 0); // Geometry is degenerated + _Ears.Data[0] = _Nodes; + _Ears.Size = 1; + } + } + + ImTriangulatorNode* ear = _Ears.Data[--_Ears.Size]; + out_triangle[0] = ear->Prev->Index; + out_triangle[1] = ear->Index; + out_triangle[2] = ear->Next->Index; + + ear->Unlink(); + if (ear == _Nodes) + _Nodes = ear->Next; + + ReclassifyNode(ear->Prev); + ReclassifyNode(ear->Next); + _TrianglesLeft--; +} + +void ImTriangulator::FlipNodeList() +{ + ImTriangulatorNode* prev = _Nodes; + ImTriangulatorNode* temp = _Nodes; + ImTriangulatorNode* current = _Nodes->Next; + prev->Next = prev; + prev->Prev = prev; + while (current != _Nodes) + { + temp = current->Next; + + current->Next = prev; + prev->Prev = current; + _Nodes->Next = current; + current->Prev = _Nodes; + + prev = current; + current = temp; + } + _Nodes = prev; +} + +// A triangle is an ear is no other vertex is inside it. We can test reflexes vertices only (see reference algorithm) +bool ImTriangulator::IsEar(int i0, int i1, int i2, const ImVec2& v0, const ImVec2& v1, const ImVec2& v2) const +{ + ImTriangulatorNode** p_end = _Reflexes.Data + _Reflexes.Size; + for (ImTriangulatorNode** p = _Reflexes.Data; p < p_end; p++) + { + ImTriangulatorNode* reflex = *p; + if (reflex->Index != i0 && reflex->Index != i1 && reflex->Index != i2) + if (ImTriangleContainsPoint(v0, v1, v2, reflex->Pos)) + return false; + } + return true; +} + +void ImTriangulator::ReclassifyNode(ImTriangulatorNode* n1) +{ + // Classify node + ImTriangulatorNodeType type; + const ImTriangulatorNode* n0 = n1->Prev; + const ImTriangulatorNode* n2 = n1->Next; + if (!ImTriangleIsClockwise(n0->Pos, n1->Pos, n2->Pos)) + type = ImTriangulatorNodeType_Reflex; + else if (IsEar(n0->Index, n1->Index, n2->Index, n0->Pos, n1->Pos, n2->Pos)) + type = ImTriangulatorNodeType_Ear; + else + type = ImTriangulatorNodeType_Convex; + + // Update lists when a type changes + if (type == n1->Type) + return; + if (n1->Type == ImTriangulatorNodeType_Reflex) + _Reflexes.find_erase_unsorted(n1->Index); + else if (n1->Type == ImTriangulatorNodeType_Ear) + _Ears.find_erase_unsorted(n1->Index); + if (type == ImTriangulatorNodeType_Reflex) + _Reflexes.push_back(n1); + else if (type == ImTriangulatorNodeType_Ear) + _Ears.push_back(n1); + n1->Type = type; +} + +// Use ear-clipping algorithm to triangulate a simple polygon (no self-interaction, no holes). +// (Reminder: we don't perform any coarse clipping/culling in ImDrawList layer! +// It is up to caller to ensure not making costly calls that will be outside of visible area. +// As concave fill is noticeably more expensive than other primitives, be mindful of this... +// Caller can build AABB of points, and avoid filling if 'draw_list->_CmdHeader.ClipRect.Overlays(points_bb) == false') +void ImDrawList::AddConcavePolyFilled(const ImVec2* points, const int points_count, ImU32 col) +{ + if (points_count < 3 || (col & IM_COL32_A_MASK) == 0) + return; + + const ImVec2 uv = _Data->TexUvWhitePixel; + ImTriangulator triangulator; + unsigned int triangle[3]; + if (Flags & ImDrawListFlags_AntiAliasedFill) + { + // Anti-aliased Fill + const float AA_SIZE = _FringeScale; + const ImU32 col_trans = col & ~IM_COL32_A_MASK; + const int idx_count = (points_count - 2) * 3 + points_count * 6; + const int vtx_count = (points_count * 2); + PrimReserve(idx_count, vtx_count); + + // Add indexes for fill + unsigned int vtx_inner_idx = _VtxCurrentIdx; + unsigned int vtx_outer_idx = _VtxCurrentIdx + 1; + + _Data->TempBuffer.reserve_discard((ImTriangulator::EstimateScratchBufferSize(points_count) + sizeof(ImVec2)) / sizeof(ImVec2)); + triangulator.Init(points, points_count, _Data->TempBuffer.Data); + while (triangulator._TrianglesLeft > 0) + { + triangulator.GetNextTriangle(triangle); + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (triangle[0] << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (triangle[1] << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_inner_idx + (triangle[2] << 1)); + _IdxWritePtr += 3; + } + + // Compute normals + _Data->TempBuffer.reserve_discard(points_count); + ImVec2* temp_normals = _Data->TempBuffer.Data; + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + const ImVec2& p0 = points[i0]; + const ImVec2& p1 = points[i1]; + float dx = p1.x - p0.x; + float dy = p1.y - p0.y; + IM_NORMALIZE2F_OVER_ZERO(dx, dy); + temp_normals[i0].x = dy; + temp_normals[i0].y = -dx; + } + + for (int i0 = points_count - 1, i1 = 0; i1 < points_count; i0 = i1++) + { + // Average normals + const ImVec2& n0 = temp_normals[i0]; + const ImVec2& n1 = temp_normals[i1]; + float dm_x = (n0.x + n1.x) * 0.5f; + float dm_y = (n0.y + n1.y) * 0.5f; + IM_FIXNORMAL2F(dm_x, dm_y); + dm_x *= AA_SIZE * 0.5f; + dm_y *= AA_SIZE * 0.5f; + + // Add vertices + _VtxWritePtr[0].pos.x = (points[i1].x - dm_x); _VtxWritePtr[0].pos.y = (points[i1].y - dm_y); _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; // Inner + _VtxWritePtr[1].pos.x = (points[i1].x + dm_x); _VtxWritePtr[1].pos.y = (points[i1].y + dm_y); _VtxWritePtr[1].uv = uv; _VtxWritePtr[1].col = col_trans; // Outer + _VtxWritePtr += 2; + + // Add indexes for fringes + _IdxWritePtr[0] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); _IdxWritePtr[1] = (ImDrawIdx)(vtx_inner_idx + (i0 << 1)); _IdxWritePtr[2] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); + _IdxWritePtr[3] = (ImDrawIdx)(vtx_outer_idx + (i0 << 1)); _IdxWritePtr[4] = (ImDrawIdx)(vtx_outer_idx + (i1 << 1)); _IdxWritePtr[5] = (ImDrawIdx)(vtx_inner_idx + (i1 << 1)); + _IdxWritePtr += 6; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } + else + { + // Non Anti-aliased Fill + const int idx_count = (points_count - 2) * 3; + const int vtx_count = points_count; + PrimReserve(idx_count, vtx_count); + for (int i = 0; i < vtx_count; i++) + { + _VtxWritePtr[0].pos = points[i]; _VtxWritePtr[0].uv = uv; _VtxWritePtr[0].col = col; + _VtxWritePtr++; + } + _Data->TempBuffer.reserve_discard((ImTriangulator::EstimateScratchBufferSize(points_count) + sizeof(ImVec2)) / sizeof(ImVec2)); + triangulator.Init(points, points_count, _Data->TempBuffer.Data); + while (triangulator._TrianglesLeft > 0) + { + triangulator.GetNextTriangle(triangle); + _IdxWritePtr[0] = (ImDrawIdx)(_VtxCurrentIdx + triangle[0]); _IdxWritePtr[1] = (ImDrawIdx)(_VtxCurrentIdx + triangle[1]); _IdxWritePtr[2] = (ImDrawIdx)(_VtxCurrentIdx + triangle[2]); + _IdxWritePtr += 3; + } + _VtxCurrentIdx += (ImDrawIdx)vtx_count; + } +} //----------------------------------------------------------------------------- -// ImDrawListSplitter +// [SECTION] ImDrawListSplitter //----------------------------------------------------------------------------- // FIXME: This may be a little confusing, trying to be a little too low-level/optimal instead of just doing vector swap.. //----------------------------------------------------------------------------- @@ -1214,10 +2095,14 @@ void ImDrawListSplitter::ClearFreeMemory() void ImDrawListSplitter::Split(ImDrawList* draw_list, int channels_count) { - IM_ASSERT(_Current == 0 && _Count <= 1); + IM_UNUSED(draw_list); + IM_ASSERT(_Current == 0 && _Count <= 1 && "Nested channel splitting is not supported. Please use separate instances of ImDrawListSplitter."); int old_channels_count = _Channels.Size; if (old_channels_count < channels_count) + { + _Channels.reserve(channels_count); // Avoid over reserving since this is likely to stay stable _Channels.resize(channels_count); + } _Count = channels_count; // Channels[] (24/32 bytes each) hold storage that we'll swap with draw_list->_CmdBuffer/_IdxBuffer @@ -1235,30 +2120,17 @@ void ImDrawListSplitter::Split(ImDrawList* draw_list, int channels_count) _Channels[i]._CmdBuffer.resize(0); _Channels[i]._IdxBuffer.resize(0); } - if (_Channels[i]._CmdBuffer.Size == 0) - { - ImDrawCmd draw_cmd; - draw_cmd.ClipRect = draw_list->_ClipRectStack.back(); - draw_cmd.TextureId = draw_list->_TextureIdStack.back(); - _Channels[i]._CmdBuffer.push_back(draw_cmd); - } } } -static inline bool CanMergeDrawCommands(ImDrawCmd* a, ImDrawCmd* b) -{ - return memcmp(&a->ClipRect, &b->ClipRect, sizeof(a->ClipRect)) == 0 && a->TextureId == b->TextureId && a->VtxOffset == b->VtxOffset && !a->UserCallback && !b->UserCallback; -} - void ImDrawListSplitter::Merge(ImDrawList* draw_list) { - // Note that we never use or rely on channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. + // Note that we never use or rely on _Channels.Size because it is merely a buffer that we never shrink back to 0 to keep all sub-buffers ready for use. if (_Count <= 1) return; SetCurrentChannel(draw_list, 0); - if (draw_list->CmdBuffer.Size != 0 && draw_list->CmdBuffer.back().ElemCount == 0) - draw_list->CmdBuffer.pop_back(); + draw_list->_PopUnusedDrawCmd(); // Calculate our final buffer sizes. Also fix the incorrect IdxOffset values in each command. int new_cmd_buffer_count = 0; @@ -1268,14 +2140,21 @@ void ImDrawListSplitter::Merge(ImDrawList* draw_list) for (int i = 1; i < _Count; i++) { ImDrawChannel& ch = _Channels[i]; - if (ch._CmdBuffer.Size > 0 && ch._CmdBuffer.back().ElemCount == 0) + if (ch._CmdBuffer.Size > 0 && ch._CmdBuffer.back().ElemCount == 0 && ch._CmdBuffer.back().UserCallback == NULL) // Equivalent of PopUnusedDrawCmd() ch._CmdBuffer.pop_back(); - if (ch._CmdBuffer.Size > 0 && last_cmd != NULL && CanMergeDrawCommands(last_cmd, &ch._CmdBuffer[0])) + + if (ch._CmdBuffer.Size > 0 && last_cmd != NULL) { - // Merge previous channel last draw command with current channel first draw command if matching. - last_cmd->ElemCount += ch._CmdBuffer[0].ElemCount; - idx_offset += ch._CmdBuffer[0].ElemCount; - ch._CmdBuffer.erase(ch._CmdBuffer.Data); + // Do not include ImDrawCmd_AreSequentialIdxOffset() in the compare as we rebuild IdxOffset values ourselves. + // Manipulating IdxOffset (e.g. by reordering draw commands like done by RenderDimmedBackgroundBehindWindow()) is not supported within a splitter. + ImDrawCmd* next_cmd = &ch._CmdBuffer[0]; + if (ImDrawCmd_HeaderCompare(last_cmd, next_cmd) == 0 && last_cmd->UserCallback == NULL && next_cmd->UserCallback == NULL) + { + // Merge previous channel last draw command with current channel first draw command if matching. + last_cmd->ElemCount += next_cmd->ElemCount; + idx_offset += next_cmd->ElemCount; + ch._CmdBuffer.erase(ch._CmdBuffer.Data); // FIXME-OPT: Improve for multiple merges. + } } if (ch._CmdBuffer.Size > 0) last_cmd = &ch._CmdBuffer.back(); @@ -1300,8 +2179,18 @@ void ImDrawListSplitter::Merge(ImDrawList* draw_list) if (int sz = ch._IdxBuffer.Size) { memcpy(idx_write, ch._IdxBuffer.Data, sz * sizeof(ImDrawIdx)); idx_write += sz; } } draw_list->_IdxWritePtr = idx_write; - draw_list->UpdateClipRect(); // We call this instead of AddDrawCmd(), so that empty channels won't produce an extra draw call. - draw_list->UpdateTextureID(); + + // Ensure there's always a non-callback draw command trailing the command-buffer + if (draw_list->CmdBuffer.Size == 0 || draw_list->CmdBuffer.back().UserCallback != NULL) + draw_list->AddDrawCmd(); + + // If current command is used with different settings we need to add a new command + ImDrawCmd* curr_cmd = &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1]; + if (curr_cmd->ElemCount == 0) + ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset + else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0) + draw_list->AddDrawCmd(); + _Count = 1; } @@ -1310,6 +2199,7 @@ void ImDrawListSplitter::SetCurrentChannel(ImDrawList* draw_list, int idx) IM_ASSERT(idx >= 0 && idx < _Count); if (_Current == idx) return; + // Overwrite ImVector (12/16 bytes), four times. This is merely a silly optimization instead of doing .swap() memcpy(&_Channels.Data[_Current]._CmdBuffer, &draw_list->CmdBuffer, sizeof(draw_list->CmdBuffer)); memcpy(&_Channels.Data[_Current]._IdxBuffer, &draw_list->IdxBuffer, sizeof(draw_list->IdxBuffer)); @@ -1317,12 +2207,84 @@ void ImDrawListSplitter::SetCurrentChannel(ImDrawList* draw_list, int idx) memcpy(&draw_list->CmdBuffer, &_Channels.Data[idx]._CmdBuffer, sizeof(draw_list->CmdBuffer)); memcpy(&draw_list->IdxBuffer, &_Channels.Data[idx]._IdxBuffer, sizeof(draw_list->IdxBuffer)); draw_list->_IdxWritePtr = draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size; + + // If current command is used with different settings we need to add a new command + ImDrawCmd* curr_cmd = (draw_list->CmdBuffer.Size == 0) ? NULL : &draw_list->CmdBuffer.Data[draw_list->CmdBuffer.Size - 1]; + if (curr_cmd == NULL) + draw_list->AddDrawCmd(); + else if (curr_cmd->ElemCount == 0) + ImDrawCmd_HeaderCopy(curr_cmd, &draw_list->_CmdHeader); // Copy ClipRect, TextureId, VtxOffset + else if (ImDrawCmd_HeaderCompare(curr_cmd, &draw_list->_CmdHeader) != 0) + draw_list->AddDrawCmd(); } //----------------------------------------------------------------------------- // [SECTION] ImDrawData //----------------------------------------------------------------------------- +void ImDrawData::Clear() +{ + Valid = false; + CmdListsCount = TotalIdxCount = TotalVtxCount = 0; + CmdLists.resize(0); // The ImDrawList are NOT owned by ImDrawData but e.g. by ImGuiContext, so we don't clear them. + DisplayPos = DisplaySize = FramebufferScale = ImVec2(0.0f, 0.0f); + OwnerViewport = NULL; +} + +// Important: 'out_list' is generally going to be draw_data->CmdLists, but may be another temporary list +// as long at it is expected that the result will be later merged into draw_data->CmdLists[]. +void ImGui::AddDrawListToDrawDataEx(ImDrawData* draw_data, ImVector* out_list, ImDrawList* draw_list) +{ + if (draw_list->CmdBuffer.Size == 0) + return; + if (draw_list->CmdBuffer.Size == 1 && draw_list->CmdBuffer[0].ElemCount == 0 && draw_list->CmdBuffer[0].UserCallback == NULL) + return; + + // Draw list sanity check. Detect mismatch between PrimReserve() calls and incrementing _VtxCurrentIdx, _VtxWritePtr etc. + // May trigger for you if you are using PrimXXX functions incorrectly. + IM_ASSERT(draw_list->VtxBuffer.Size == 0 || draw_list->_VtxWritePtr == draw_list->VtxBuffer.Data + draw_list->VtxBuffer.Size); + IM_ASSERT(draw_list->IdxBuffer.Size == 0 || draw_list->_IdxWritePtr == draw_list->IdxBuffer.Data + draw_list->IdxBuffer.Size); + if (!(draw_list->Flags & ImDrawListFlags_AllowVtxOffset)) + IM_ASSERT((int)draw_list->_VtxCurrentIdx == draw_list->VtxBuffer.Size); + + // Check that draw_list doesn't use more vertices than indexable (default ImDrawIdx = unsigned short = 2 bytes = 64K vertices per ImDrawList = per window) + // If this assert triggers because you are drawing lots of stuff manually: + // - First, make sure you are coarse clipping yourself and not trying to draw many things outside visible bounds. + // Be mindful that the lower-level ImDrawList API doesn't filter vertices. Use the Metrics/Debugger window to inspect draw list contents. + // - If you want large meshes with more than 64K vertices, you can either: + // (A) Handle the ImDrawCmd::VtxOffset value in your renderer backend, and set 'io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset'. + // Most example backends already support this from 1.71. Pre-1.71 backends won't. + // Some graphics API such as GL ES 1/2 don't have a way to offset the starting vertex so it is not supported for them. + // (B) Or handle 32-bit indices in your renderer backend, and uncomment '#define ImDrawIdx unsigned int' line in imconfig.h. + // Most example backends already support this. For example, the OpenGL example code detect index size at compile-time: + // glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); + // Your own engine or render API may use different parameters or function calls to specify index sizes. + // 2 and 4 bytes indices are generally supported by most graphics API. + // - If for some reason neither of those solutions works for you, a workaround is to call BeginChild()/EndChild() before reaching + // the 64K limit to split your draw commands in multiple draw lists. + if (sizeof(ImDrawIdx) == 2) + IM_ASSERT(draw_list->_VtxCurrentIdx < (1 << 16) && "Too many vertices in ImDrawList using 16-bit indices. Read comment above"); + + // Resolve callback data pointers + if (draw_list->_CallbacksDataBuf.Size > 0) + for (ImDrawCmd& cmd : draw_list->CmdBuffer) + if (cmd.UserCallback != NULL && cmd.UserCallbackDataOffset != -1 && cmd.UserCallbackDataSize > 0) + cmd.UserCallbackData = draw_list->_CallbacksDataBuf.Data + cmd.UserCallbackDataOffset; + + // Add to output list + records state in ImDrawData + out_list->push_back(draw_list); + draw_data->CmdListsCount++; + draw_data->TotalVtxCount += draw_list->VtxBuffer.Size; + draw_data->TotalIdxCount += draw_list->IdxBuffer.Size; +} + +void ImDrawData::AddDrawList(ImDrawList* draw_list) +{ + IM_ASSERT(CmdLists.Size == CmdListsCount); + draw_list->_PopUnusedDrawCmd(); + ImGui::AddDrawListToDrawDataEx(this, &CmdLists, draw_list); +} + // For backward compatibility: convert all buffers from indexed to de-indexed, in case you cannot render indexed. Note: this is slow and most likely a waste of resources. Always prefer indexed rendering! void ImDrawData::DeIndexAllBuffers() { @@ -1347,15 +2309,9 @@ void ImDrawData::DeIndexAllBuffers() // or if there is a difference between your window resolution and framebuffer resolution. void ImDrawData::ScaleClipRects(const ImVec2& fb_scale) { - for (int i = 0; i < CmdListsCount; i++) - { - ImDrawList* cmd_list = CmdLists[i]; - for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) - { - ImDrawCmd* cmd = &cmd_list->CmdBuffer[cmd_i]; - cmd->ClipRect = ImVec4(cmd->ClipRect.x * fb_scale.x, cmd->ClipRect.y * fb_scale.y, cmd->ClipRect.z * fb_scale.x, cmd->ClipRect.w * fb_scale.y); - } - } + for (ImDrawList* draw_list : CmdLists) + for (ImDrawCmd& cmd : draw_list->CmdBuffer) + cmd.ClipRect = ImVec4(cmd.ClipRect.x * fb_scale.x, cmd.ClipRect.y * fb_scale.y, cmd.ClipRect.z * fb_scale.x, cmd.ClipRect.w * fb_scale.y); } //----------------------------------------------------------------------------- @@ -1369,13 +2325,19 @@ void ImGui::ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int ve float gradient_inv_length2 = 1.0f / ImLengthSqr(gradient_extent); ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + const int col0_r = (int)(col0 >> IM_COL32_R_SHIFT) & 0xFF; + const int col0_g = (int)(col0 >> IM_COL32_G_SHIFT) & 0xFF; + const int col0_b = (int)(col0 >> IM_COL32_B_SHIFT) & 0xFF; + const int col_delta_r = ((int)(col1 >> IM_COL32_R_SHIFT) & 0xFF) - col0_r; + const int col_delta_g = ((int)(col1 >> IM_COL32_G_SHIFT) & 0xFF) - col0_g; + const int col_delta_b = ((int)(col1 >> IM_COL32_B_SHIFT) & 0xFF) - col0_b; for (ImDrawVert* vert = vert_start; vert < vert_end; vert++) { float d = ImDot(vert->pos - gradient_p0, gradient_extent); float t = ImClamp(d * gradient_inv_length2, 0.0f, 1.0f); - int r = ImLerp((int)(col0 >> IM_COL32_R_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_R_SHIFT) & 0xFF, t); - int g = ImLerp((int)(col0 >> IM_COL32_G_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_G_SHIFT) & 0xFF, t); - int b = ImLerp((int)(col0 >> IM_COL32_B_SHIFT) & 0xFF, (int)(col1 >> IM_COL32_B_SHIFT) & 0xFF, t); + int r = (int)(col0_r + col_delta_r * t); + int g = (int)(col0_g + col_delta_g * t); + int b = (int)(col0_b + col_delta_b * t); vert->col = (r << IM_COL32_R_SHIFT) | (g << IM_COL32_G_SHIFT) | (b << IM_COL32_B_SHIFT) | (vert->col & IM_COL32_A_MASK); } } @@ -1405,71 +2367,100 @@ void ImGui::ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int ve } } +void ImGui::ShadeVertsTransformPos(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& pivot_in, float cos_a, float sin_a, const ImVec2& pivot_out) +{ + ImDrawVert* vert_start = draw_list->VtxBuffer.Data + vert_start_idx; + ImDrawVert* vert_end = draw_list->VtxBuffer.Data + vert_end_idx; + for (ImDrawVert* vertex = vert_start; vertex < vert_end; ++vertex) + vertex->pos = ImRotate(vertex->pos- pivot_in, cos_a, sin_a) + pivot_out; +} + //----------------------------------------------------------------------------- // [SECTION] ImFontConfig //----------------------------------------------------------------------------- ImFontConfig::ImFontConfig() { - FontData = NULL; - FontDataSize = 0; + memset(this, 0, sizeof(*this)); FontDataOwnedByAtlas = true; - FontNo = 0; - SizePixels = 0.0f; - OversampleH = 3; // FIXME: 2 may be a better default? - OversampleV = 1; - PixelSnapH = false; - GlyphExtraSpacing = ImVec2(0.0f, 0.0f); - GlyphOffset = ImVec2(0.0f, 0.0f); - GlyphRanges = NULL; - GlyphMinAdvanceX = 0.0f; + OversampleH = 0; // Auto == 1 or 2 depending on size + OversampleV = 0; // Auto == 1 GlyphMaxAdvanceX = FLT_MAX; - MergeMode = false; - RasterizerFlags = 0x00; RasterizerMultiply = 1.0f; - EllipsisChar = (ImWchar)-1; - memset(Name, 0, sizeof(Name)); - DstFont = NULL; + RasterizerDensity = 1.0f; + EllipsisChar = 0; } //----------------------------------------------------------------------------- // [SECTION] ImFontAtlas //----------------------------------------------------------------------------- +// - Default texture data encoded in ASCII +// - ImFontAtlas::ClearInputData() +// - ImFontAtlas::ClearTexData() +// - ImFontAtlas::ClearFonts() +// - ImFontAtlas::Clear() +// - ImFontAtlas::GetTexDataAsAlpha8() +// - ImFontAtlas::GetTexDataAsRGBA32() +// - ImFontAtlas::AddFont() +// - ImFontAtlas::AddFontDefault() +// - ImFontAtlas::AddFontFromFileTTF() +// - ImFontAtlas::AddFontFromMemoryTTF() +// - ImFontAtlas::AddFontFromMemoryCompressedTTF() +// - ImFontAtlas::AddFontFromMemoryCompressedBase85TTF() +// - ImFontAtlas::AddCustomRectRegular() +// - ImFontAtlas::AddCustomRectFontGlyph() +// - ImFontAtlas::CalcCustomRectUV() +// - ImFontAtlasGetMouseCursorTexData() +// - ImFontAtlas::Build() +// - ImFontAtlasBuildMultiplyCalcLookupTable() +// - ImFontAtlasBuildMultiplyRectAlpha8() +// - ImFontAtlasBuildWithStbTruetype() +// - ImFontAtlasGetBuilderForStbTruetype() +// - ImFontAtlasUpdateSourcesPointers() +// - ImFontAtlasBuildSetupFont() +// - ImFontAtlasBuildPackCustomRects() +// - ImFontAtlasBuildRender8bppRectFromString() +// - ImFontAtlasBuildRender32bppRectFromString() +// - ImFontAtlasBuildRenderDefaultTexData() +// - ImFontAtlasBuildRenderLinesTexData() +// - ImFontAtlasBuildInit() +// - ImFontAtlasBuildFinish() +//----------------------------------------------------------------------------- // A work of art lies ahead! (. = white layer, X = black layer, others are blank) -// The white texels on the top left are the ones we'll use everywhere in Dear ImGui to render filled shapes. -const int FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF = 108; -const int FONT_ATLAS_DEFAULT_TEX_DATA_H = 27; -const unsigned int FONT_ATLAS_DEFAULT_TEX_DATA_ID = 0x80000000; -static const char FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF * FONT_ATLAS_DEFAULT_TEX_DATA_H + 1] = +// The 2x2 white texels on the top left are the ones we'll use everywhere in Dear ImGui to render filled shapes. +// (This is used when io.MouseDrawCursor = true) +const int FONT_ATLAS_DEFAULT_TEX_DATA_W = 122; // Actual texture will be 2 times that + 1 spacing. +const int FONT_ATLAS_DEFAULT_TEX_DATA_H = 27; +static const char FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[FONT_ATLAS_DEFAULT_TEX_DATA_W * FONT_ATLAS_DEFAULT_TEX_DATA_H + 1] = { - "..- -XXXXXXX- X - X -XXXXXXX - XXXXXXX- XX " - "..- -X.....X- X.X - X.X -X.....X - X.....X- X..X " - "--- -XXX.XXX- X...X - X...X -X....X - X....X- X..X " - "X - X.X - X.....X - X.....X -X...X - X...X- X..X " - "XX - X.X -X.......X- X.......X -X..X.X - X.X..X- X..X " - "X.X - X.X -XXXX.XXXX- XXXX.XXXX -X.X X.X - X.X X.X- X..XXX " - "X..X - X.X - X.X - X.X -XX X.X - X.X XX- X..X..XXX " - "X...X - X.X - X.X - XX X.X XX - X.X - X.X - X..X..X..XX " - "X....X - X.X - X.X - X.X X.X X.X - X.X - X.X - X..X..X..X.X " - "X.....X - X.X - X.X - X..X X.X X..X - X.X - X.X -XXX X..X..X..X..X" - "X......X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X XX-XX X.X -X..XX........X..X" - "X.......X - X.X - X.X -X.....................X- X.X X.X-X.X X.X -X...X...........X" - "X........X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X..X-X..X.X - X..............X" - "X.........X -XXX.XXX- X.X - X..X X.X X..X - X...X-X...X - X.............X" - "X..........X-X.....X- X.X - X.X X.X X.X - X....X-X....X - X.............X" - "X......XXXXX-XXXXXXX- X.X - XX X.X XX - X.....X-X.....X - X............X" - "X...X..X --------- X.X - X.X - XXXXXXX-XXXXXXX - X...........X " - "X..X X..X - -XXXX.XXXX- XXXX.XXXX ------------------------------------- X..........X " - "X.X X..X - -X.......X- X.......X - XX XX - - X..........X " - "XX X..X - - X.....X - X.....X - X.X X.X - - X........X " - " X..X - X...X - X...X - X..X X..X - - X........X " - " XX - X.X - X.X - X...XXXXXXXXXXXXX...X - - XXXXXXXXXX " - "------------ - X - X -X.....................X- ------------------" - " ----------------------------------- X...XXXXXXXXXXXXX...X - " - " - X..X X..X - " - " - X.X X.X - " - " - XX XX - " + "..- -XXXXXXX- X - X -XXXXXXX - XXXXXXX- XX - XX XX " + "..- -X.....X- X.X - X.X -X.....X - X.....X- X..X -X..X X..X" + "--- -XXX.XXX- X...X - X...X -X....X - X....X- X..X -X...X X...X" + "X - X.X - X.....X - X.....X -X...X - X...X- X..X - X...X X...X " + "XX - X.X -X.......X- X.......X -X..X.X - X.X..X- X..X - X...X...X " + "X.X - X.X -XXXX.XXXX- XXXX.XXXX -X.X X.X - X.X X.X- X..XXX - X.....X " + "X..X - X.X - X.X - X.X -XX X.X - X.X XX- X..X..XXX - X...X " + "X...X - X.X - X.X - XX X.X XX - X.X - X.X - X..X..X..XX - X.X " + "X....X - X.X - X.X - X.X X.X X.X - X.X - X.X - X..X..X..X.X - X...X " + "X.....X - X.X - X.X - X..X X.X X..X - X.X - X.X -XXX X..X..X..X..X- X.....X " + "X......X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X XX-XX X.X -X..XX........X..X- X...X...X " + "X.......X - X.X - X.X -X.....................X- X.X X.X-X.X X.X -X...X...........X- X...X X...X " + "X........X - X.X - X.X - X...XXXXXX.XXXXXX...X - X.X..X-X..X.X - X..............X-X...X X...X" + "X.........X -XXX.XXX- X.X - X..X X.X X..X - X...X-X...X - X.............X-X..X X..X" + "X..........X-X.....X- X.X - X.X X.X X.X - X....X-X....X - X.............X- XX XX " + "X......XXXXX-XXXXXXX- X.X - XX X.X XX - X.....X-X.....X - X............X--------------" + "X...X..X --------- X.X - X.X - XXXXXXX-XXXXXXX - X...........X - " + "X..X X..X - -XXXX.XXXX- XXXX.XXXX ------------------------------------- X..........X - " + "X.X X..X - -X.......X- X.......X - XX XX - - X..........X - " + "XX X..X - - X.....X - X.....X - X.X X.X - - X........X - " + " X..X - - X...X - X...X - X..X X..X - - X........X - " + " XX - - X.X - X.X - X...XXXXXXXXXXXXX...X - - XXXXXXXXXX - " + "------------- - X - X -X.....................X- ------------------- " + " ----------------------------------- X...XXXXXXXXXXXXX...X - " + " - X..X X..X - " + " - X.X X.X - " + " - XX XX - " }; static const ImVec2 FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[ImGuiMouseCursor_COUNT][3] = @@ -1483,23 +2474,16 @@ static const ImVec2 FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[ImGuiMouseCursor_COUNT][3 { ImVec2(73,0), ImVec2(17,17), ImVec2( 8, 8) }, // ImGuiMouseCursor_ResizeNESW { ImVec2(55,0), ImVec2(17,17), ImVec2( 8, 8) }, // ImGuiMouseCursor_ResizeNWSE { ImVec2(91,0), ImVec2(17,22), ImVec2( 5, 0) }, // ImGuiMouseCursor_Hand + { ImVec2(0,3), ImVec2(12,19), ImVec2(0, 0) }, // ImGuiMouseCursor_Wait // Arrow + custom code in ImGui::RenderMouseCursor() + { ImVec2(0,3), ImVec2(12,19), ImVec2(0, 0) }, // ImGuiMouseCursor_Progress // Arrow + custom code in ImGui::RenderMouseCursor() + { ImVec2(109,0),ImVec2(13,15), ImVec2( 6, 7) }, // ImGuiMouseCursor_NotAllowed }; ImFontAtlas::ImFontAtlas() { - Locked = false; - Flags = ImFontAtlasFlags_None; - TexID = (ImTextureID)NULL; - TexDesiredWidth = 0; + memset(this, 0, sizeof(*this)); TexGlyphPadding = 1; - - TexPixelsAlpha8 = NULL; - TexPixelsRGBA32 = NULL; - TexWidth = TexHeight = 0; - TexUvScale = ImVec2(0.0f, 0.0f); - TexUvWhitePixel = ImVec2(0.0f, 0.0f); - for (int n = 0; n < IM_ARRAYSIZE(CustomRectIds); n++) - CustomRectIds[n] = -1; + PackIdMouseCursors = PackIdLines = -1; } ImFontAtlas::~ImFontAtlas() @@ -1511,24 +2495,24 @@ ImFontAtlas::~ImFontAtlas() void ImFontAtlas::ClearInputData() { IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); - for (int i = 0; i < ConfigData.Size; i++) - if (ConfigData[i].FontData && ConfigData[i].FontDataOwnedByAtlas) + for (ImFontConfig& font_cfg : Sources) + if (font_cfg.FontData && font_cfg.FontDataOwnedByAtlas) { - IM_FREE(ConfigData[i].FontData); - ConfigData[i].FontData = NULL; + IM_FREE(font_cfg.FontData); + font_cfg.FontData = NULL; } // When clearing this we lose access to the font name and other information used to build the font. - for (int i = 0; i < Fonts.Size; i++) - if (Fonts[i]->ConfigData >= ConfigData.Data && Fonts[i]->ConfigData < ConfigData.Data + ConfigData.Size) + for (ImFont* font : Fonts) + if (font->Sources >= Sources.Data && font->Sources < Sources.Data + Sources.Size) { - Fonts[i]->ConfigData = NULL; - Fonts[i]->ConfigDataCount = 0; + font->Sources = NULL; + font->SourcesCount = 0; } - ConfigData.clear(); + Sources.clear(); CustomRects.clear(); - for (int n = 0; n < IM_ARRAYSIZE(CustomRectIds); n++) - CustomRectIds[n] = -1; + PackIdMouseCursors = PackIdLines = -1; + // Important: we leave TexReady untouched } void ImFontAtlas::ClearTexData() @@ -1540,14 +2524,16 @@ void ImFontAtlas::ClearTexData() IM_FREE(TexPixelsRGBA32); TexPixelsAlpha8 = NULL; TexPixelsRGBA32 = NULL; + TexPixelsUseColors = false; + // Important: we leave TexReady untouched } void ImFontAtlas::ClearFonts() { IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); - for (int i = 0; i < Fonts.Size; i++) - IM_DELETE(Fonts[i]); - Fonts.clear(); + ClearInputData(); + Fonts.clear_delete(); + TexReady = false; } void ImFontAtlas::Clear() @@ -1561,11 +2547,7 @@ void ImFontAtlas::GetTexDataAsAlpha8(unsigned char** out_pixels, int* out_wid { // Build atlas on demand if (TexPixelsAlpha8 == NULL) - { - if (ConfigData.empty()) - AddFontDefault(); Build(); - } *out_pixels = TexPixelsAlpha8; if (out_width) *out_width = TexWidth; @@ -1601,16 +2583,17 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) { IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); IM_ASSERT(font_cfg->FontData != NULL && font_cfg->FontDataSize > 0); - IM_ASSERT(font_cfg->SizePixels > 0.0f); + IM_ASSERT(font_cfg->SizePixels > 0.0f && "Is ImFontConfig struct correctly initialized?"); + IM_ASSERT(font_cfg->RasterizerDensity > 0.0f && "Is ImFontConfig struct correctly initialized?"); // Create new font if (!font_cfg->MergeMode) Fonts.push_back(IM_NEW(ImFont)); else - IM_ASSERT(!Fonts.empty() && "Cannot use MergeMode for the first font"); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font. + IM_ASSERT(Fonts.Size > 0 && "Cannot use MergeMode for the first font"); // When using MergeMode make sure that a font has already been added before. You can use ImGui::GetIO().Fonts->AddFontDefault() to add the default imgui font. - ConfigData.push_back(*font_cfg); - ImFontConfig& new_font_cfg = ConfigData.back(); + Sources.push_back(*font_cfg); + ImFontConfig& new_font_cfg = Sources.back(); if (new_font_cfg.DstFont == NULL) new_font_cfg.DstFont = Fonts.back(); if (!new_font_cfg.FontDataOwnedByAtlas) @@ -1620,33 +2603,43 @@ ImFont* ImFontAtlas::AddFont(const ImFontConfig* font_cfg) memcpy(new_font_cfg.FontData, font_cfg->FontData, (size_t)new_font_cfg.FontDataSize); } - if (new_font_cfg.DstFont->EllipsisChar == (ImWchar)-1) - new_font_cfg.DstFont->EllipsisChar = font_cfg->EllipsisChar; + // Round font size + // - We started rounding in 1.90 WIP (18991) as our layout system currently doesn't support non-rounded font size well yet. + // - Note that using io.FontGlobalScale or SetWindowFontScale(), with are legacy-ish, partially supported features, can still lead to unrounded sizes. + // - We may support it better later and remove this rounding. + new_font_cfg.SizePixels = ImTrunc(new_font_cfg.SizePixels); + + // Pointers to Sources data are otherwise dangling + ImFontAtlasUpdateSourcesPointers(this); // Invalidate texture + TexReady = false; ClearTexData(); return new_font_cfg.DstFont; } // Default font TTF is compressed with stb_compress then base85 encoded (see misc/fonts/binary_to_compressed_c.cpp for encoder) -static unsigned int stb_decompress_length(const unsigned char *input); -static unsigned int stb_decompress(unsigned char *output, const unsigned char *input, unsigned int length); -static const char* GetDefaultCompressedFontDataTTFBase85(); +static unsigned int stb_decompress_length(const unsigned char* input); +static unsigned int stb_decompress(unsigned char* output, const unsigned char* input, unsigned int length); static unsigned int Decode85Byte(char c) { return c >= '\\' ? c-36 : c-35; } static void Decode85(const unsigned char* src, unsigned char* dst) { while (*src) { - unsigned int tmp = Decode85Byte(src[0]) + 85*(Decode85Byte(src[1]) + 85*(Decode85Byte(src[2]) + 85*(Decode85Byte(src[3]) + 85*Decode85Byte(src[4])))); + unsigned int tmp = Decode85Byte(src[0]) + 85 * (Decode85Byte(src[1]) + 85 * (Decode85Byte(src[2]) + 85 * (Decode85Byte(src[3]) + 85 * Decode85Byte(src[4])))); dst[0] = ((tmp >> 0) & 0xFF); dst[1] = ((tmp >> 8) & 0xFF); dst[2] = ((tmp >> 16) & 0xFF); dst[3] = ((tmp >> 24) & 0xFF); // We can't assume little-endianness. src += 5; dst += 4; } } +#ifndef IMGUI_DISABLE_DEFAULT_FONT +static const char* GetDefaultCompressedFontDataTTF(int* out_size); +#endif // Load embedded ProggyClean.ttf at size 13, disable oversampling ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) { +#ifndef IMGUI_DISABLE_DEFAULT_FONT ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); if (!font_cfg_template) { @@ -1658,12 +2651,18 @@ ImFont* ImFontAtlas::AddFontDefault(const ImFontConfig* font_cfg_template) if (font_cfg.Name[0] == '\0') ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "ProggyClean.ttf, %dpx", (int)font_cfg.SizePixels); font_cfg.EllipsisChar = (ImWchar)0x0085; + font_cfg.GlyphOffset.y = 1.0f * IM_TRUNC(font_cfg.SizePixels / 13.0f); // Add +1 offset per 13 units - const char* ttf_compressed_base85 = GetDefaultCompressedFontDataTTFBase85(); + int ttf_compressed_size = 0; + const char* ttf_compressed = GetDefaultCompressedFontDataTTF(&ttf_compressed_size); const ImWchar* glyph_ranges = font_cfg.GlyphRanges != NULL ? font_cfg.GlyphRanges : GetGlyphRangesDefault(); - ImFont* font = AddFontFromMemoryCompressedBase85TTF(ttf_compressed_base85, font_cfg.SizePixels, &font_cfg, glyph_ranges); - font->DisplayOffset.y = 1.0f; + ImFont* font = AddFontFromMemoryCompressedTTF(ttf_compressed, ttf_compressed_size, font_cfg.SizePixels, &font_cfg, glyph_ranges); return font; +#else + IM_ASSERT(0 && "AddFontDefault() disabled in this build."); + IM_UNUSED(font_cfg_template); + return NULL; +#endif // #ifndef IMGUI_DISABLE_DEFAULT_FONT } ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) @@ -1673,7 +2672,7 @@ ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, void* data = ImFileLoadToMemory(filename, "rb", &data_size, 0); if (!data) { - IM_ASSERT(0); // Could not load file. + IM_ASSERT_USER_ERROR(0, "Could not load font file!"); return NULL; } ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); @@ -1681,21 +2680,22 @@ ImFont* ImFontAtlas::AddFontFromFileTTF(const char* filename, float size_pixels, { // Store a short copy of filename into into the font name for convenience const char* p; - for (p = filename + strlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) {} + for (p = filename + ImStrlen(filename); p > filename && p[-1] != '/' && p[-1] != '\\'; p--) {} ImFormatString(font_cfg.Name, IM_ARRAYSIZE(font_cfg.Name), "%s, %.0fpx", p, size_pixels); } return AddFontFromMemoryTTF(data, (int)data_size, size_pixels, &font_cfg, glyph_ranges); } // NB: Transfer ownership of 'ttf_data' to ImFontAtlas, unless font_cfg_template->FontDataOwnedByAtlas == false. Owned TTF buffer will be deleted after Build(). -ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* ttf_data, int ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) +ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* font_data, int font_data_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) { IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); IM_ASSERT(font_cfg.FontData == NULL); - font_cfg.FontData = ttf_data; - font_cfg.FontDataSize = ttf_size; - font_cfg.SizePixels = size_pixels; + IM_ASSERT(font_data_size > 100 && "Incorrect value for font_data_size!"); // Heuristic to prevent accidentally passing a wrong value to font_data_size. + font_cfg.FontData = font_data; + font_cfg.FontDataSize = font_data_size; + font_cfg.SizePixels = size_pixels > 0.0f ? size_pixels : font_cfg.SizePixels; if (glyph_ranges) font_cfg.GlyphRanges = glyph_ranges; return AddFont(&font_cfg); @@ -1704,7 +2704,7 @@ ImFont* ImFontAtlas::AddFontFromMemoryTTF(void* ttf_data, int ttf_size, float si ImFont* ImFontAtlas::AddFontFromMemoryCompressedTTF(const void* compressed_ttf_data, int compressed_ttf_size, float size_pixels, const ImFontConfig* font_cfg_template, const ImWchar* glyph_ranges) { const unsigned int buf_decompressed_size = stb_decompress_length((const unsigned char*)compressed_ttf_data); - unsigned char* buf_decompressed_data = (unsigned char *)IM_ALLOC(buf_decompressed_size); + unsigned char* buf_decompressed_data = (unsigned char*)IM_ALLOC(buf_decompressed_size); stb_decompress(buf_decompressed_data, (const unsigned char*)compressed_ttf_data, (unsigned int)compressed_ttf_size); ImFontConfig font_cfg = font_cfg_template ? *font_cfg_template : ImFontConfig(); @@ -1715,7 +2715,7 @@ ImFont* ImFontAtlas::AddFontFromMemoryCompressedTTF(const void* compressed_ttf_d ImFont* ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char* compressed_ttf_data_base85, float size_pixels, const ImFontConfig* font_cfg, const ImWchar* glyph_ranges) { - int compressed_ttf_size = (((int)strlen(compressed_ttf_data_base85) + 4) / 5) * 4; + int compressed_ttf_size = (((int)ImStrlen(compressed_ttf_data_base85) + 4) / 5) * 4; void* compressed_ttf = IM_ALLOC((size_t)compressed_ttf_size); Decode85((const unsigned char*)compressed_ttf_data_base85, (unsigned char*)compressed_ttf); ImFont* font = AddFontFromMemoryCompressedTTF(compressed_ttf, compressed_ttf_size, size_pixels, font_cfg, glyph_ranges); @@ -1723,13 +2723,11 @@ ImFont* ImFontAtlas::AddFontFromMemoryCompressedBase85TTF(const char* compressed return font; } -int ImFontAtlas::AddCustomRectRegular(unsigned int id, int width, int height) +int ImFontAtlas::AddCustomRectRegular(int width, int height) { - IM_ASSERT(id >= 0x10000); IM_ASSERT(width > 0 && width <= 0xFFFF); IM_ASSERT(height > 0 && height <= 0xFFFF); ImFontAtlasCustomRect r; - r.ID = id; r.Width = (unsigned short)width; r.Height = (unsigned short)height; CustomRects.push_back(r); @@ -1738,13 +2736,17 @@ int ImFontAtlas::AddCustomRectRegular(unsigned int id, int width, int height) int ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int height, float advance_x, const ImVec2& offset) { +#ifdef IMGUI_USE_WCHAR32 + IM_ASSERT(id <= IM_UNICODE_CODEPOINT_MAX); +#endif IM_ASSERT(font != NULL); IM_ASSERT(width > 0 && width <= 0xFFFF); IM_ASSERT(height > 0 && height <= 0xFFFF); ImFontAtlasCustomRect r; - r.ID = id; r.Width = (unsigned short)width; r.Height = (unsigned short)height; + r.GlyphID = id; + r.GlyphColored = 0; // Set to 1 manually to mark glyph as colored // FIXME: No official API for that (#8133) r.GlyphAdvanceX = advance_x; r.GlyphOffset = offset; r.Font = font; @@ -1752,7 +2754,7 @@ int ImFontAtlas::AddCustomRectFontGlyph(ImFont* font, ImWchar id, int width, int return CustomRects.Size - 1; // Return index } -void ImFontAtlas::CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) +void ImFontAtlas::CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* out_uv_min, ImVec2* out_uv_max) const { IM_ASSERT(TexWidth > 0 && TexHeight > 0); // Font atlas needs to be built before we can calculate UV coordinates IM_ASSERT(rect->IsPacked()); // Make sure the rectangle has been packed @@ -1760,32 +2762,54 @@ void ImFontAtlas::CalcCustomRectUV(const ImFontAtlasCustomRect* rect, ImVec2* ou *out_uv_max = ImVec2((float)(rect->X + rect->Width) * TexUvScale.x, (float)(rect->Y + rect->Height) * TexUvScale.y); } -bool ImFontAtlas::GetMouseCursorTexData(ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]) +bool ImFontAtlasGetMouseCursorTexData(ImFontAtlas* atlas, ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]) { if (cursor_type <= ImGuiMouseCursor_None || cursor_type >= ImGuiMouseCursor_COUNT) return false; - if (Flags & ImFontAtlasFlags_NoMouseCursors) + if (atlas->Flags & ImFontAtlasFlags_NoMouseCursors) return false; - IM_ASSERT(CustomRectIds[0] != -1); - ImFontAtlasCustomRect& r = CustomRects[CustomRectIds[0]]; - IM_ASSERT(r.ID == FONT_ATLAS_DEFAULT_TEX_DATA_ID); - ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r.X, (float)r.Y); + IM_ASSERT(atlas->PackIdMouseCursors != -1); + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdMouseCursors); + ImVec2 pos = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][0] + ImVec2((float)r->X, (float)r->Y); ImVec2 size = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][1]; *out_size = size; *out_offset = FONT_ATLAS_DEFAULT_TEX_CURSOR_DATA[cursor_type][2]; - out_uv_border[0] = (pos) * TexUvScale; - out_uv_border[1] = (pos + size) * TexUvScale; - pos.x += FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF + 1; - out_uv_fill[0] = (pos) * TexUvScale; - out_uv_fill[1] = (pos + size) * TexUvScale; + out_uv_border[0] = (pos) * atlas->TexUvScale; + out_uv_border[1] = (pos + size) * atlas->TexUvScale; + pos.x += FONT_ATLAS_DEFAULT_TEX_DATA_W + 1; + out_uv_fill[0] = (pos) * atlas->TexUvScale; + out_uv_fill[1] = (pos + size) * atlas->TexUvScale; return true; } bool ImFontAtlas::Build() { IM_ASSERT(!Locked && "Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"); - return ImFontAtlasBuildWithStbTruetype(this); + + // Default font is none are specified + if (Sources.Size == 0) + AddFontDefault(); + + // Select builder + // - Note that we do not reassign to atlas->FontBuilderIO, since it is likely to point to static data which + // may mess with some hot-reloading schemes. If you need to assign to this (for dynamic selection) AND are + // using a hot-reloading scheme that messes up static data, store your own instance of ImFontBuilderIO somewhere + // and point to it instead of pointing directly to return value of the GetBuilderXXX functions. + const ImFontBuilderIO* builder_io = FontBuilderIO; + if (builder_io == NULL) + { +#ifdef IMGUI_ENABLE_FREETYPE + builder_io = ImGuiFreeType::GetBuilderForFreeType(); +#elif defined(IMGUI_ENABLE_STB_TRUETYPE) + builder_io = ImFontAtlasGetBuilderForStbTruetype(); +#else + IM_ASSERT(0); // Invalid Build function +#endif + } + + // Build + return builder_io->FontBuilder_Build(this); } void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_brighten_factor) @@ -1799,12 +2823,21 @@ void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], fl void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride) { + IM_ASSERT_PARANOID(w <= stride); unsigned char* data = pixels + x + y * stride; - for (int j = h; j > 0; j--, data += stride) - for (int i = 0; i < w; i++) - data[i] = table[data[i]]; + for (int j = h; j > 0; j--, data += stride - w) + for (int i = w; i > 0; i--, data++) + *data = table[*data]; } +void ImFontAtlasBuildGetOversampleFactors(const ImFontConfig* src, int* out_oversample_h, int* out_oversample_v) +{ + // Automatically disable horizontal oversampling over size 36 + *out_oversample_h = (src->OversampleH != 0) ? src->OversampleH : (src->SizePixels * src->RasterizerDensity > 36.0f || src->PixelSnapH) ? 1 : 2; + *out_oversample_v = (src->OversampleV != 0) ? src->OversampleV : 1; +} + +#ifdef IMGUI_ENABLE_STB_TRUETYPE // Temporary data for one source font (multiple source fonts can be merged into one destination ImFont) // (C++03 doesn't allow instancing ImVector<> with function-local types so we declare the type here.) struct ImFontBuildSrcData @@ -1817,8 +2850,8 @@ struct ImFontBuildSrcData int DstIndex; // Index into atlas->Fonts[] and dst_tmp_array[] int GlyphsHighest; // Highest requested codepoint int GlyphsCount; // Glyph count (excluding missing glyphs and glyphs already set by an earlier source font) - ImBoolVector GlyphsSet; // Glyph bit map (random access, 1-bit per codepoint. This will be a maximum of 8KB) - ImVector GlyphsList; // Glyph codepoints list (flattened version of GlyphsMap) + ImBitVector GlyphsSet; // Glyph bit map (random access, 1-bit per codepoint. This will be a maximum of 8KB) + ImVector GlyphsList; // Glyph codepoints list (flattened version of GlyphsSet) }; // Temporary data for one destination ImFont* (multiple source fonts can be merged into one destination ImFont) @@ -1827,26 +2860,26 @@ struct ImFontBuildDstData int SrcCount; // Number of source fonts targeting this destination font. int GlyphsHighest; int GlyphsCount; - ImBoolVector GlyphsSet; // This is used to resolve collision when multiple sources are merged into a same destination font. + ImBitVector GlyphsSet; // This is used to resolve collision when multiple sources are merged into a same destination font. }; -static void UnpackBoolVectorToFlatIndexList(const ImBoolVector* in, ImVector* out) +static void UnpackBitVectorToFlatIndexList(const ImBitVector* in, ImVector* out) { IM_ASSERT(sizeof(in->Storage.Data[0]) == sizeof(int)); - const int* it_begin = in->Storage.begin(); - const int* it_end = in->Storage.end(); - for (const int* it = it_begin; it < it_end; it++) - if (int entries_32 = *it) - for (int bit_n = 0; bit_n < 32; bit_n++) - if (entries_32 & (1u << bit_n)) - out->push_back((int)((it - it_begin) << 5) + bit_n); + const ImU32* it_begin = in->Storage.begin(); + const ImU32* it_end = in->Storage.end(); + for (const ImU32* it = it_begin; it < it_end; it++) + if (ImU32 entries_32 = *it) + for (ImU32 bit_n = 0; bit_n < 32; bit_n++) + if (entries_32 & ((ImU32)1 << bit_n)) + out->push_back((int)(((it - it_begin) << 5) + bit_n)); } -bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) +static bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) { - IM_ASSERT(atlas->ConfigData.Size > 0); + IM_ASSERT(atlas->Sources.Size > 0); - ImFontAtlasBuildRegisterDefaultCustomRects(atlas); + ImFontAtlasBuildInit(atlas); // Clear atlas atlas->TexID = (ImTextureID)NULL; @@ -1858,38 +2891,48 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) // Temporary storage for building ImVector src_tmp_array; ImVector dst_tmp_array; - src_tmp_array.resize(atlas->ConfigData.Size); + src_tmp_array.resize(atlas->Sources.Size); dst_tmp_array.resize(atlas->Fonts.Size); memset(src_tmp_array.Data, 0, (size_t)src_tmp_array.size_in_bytes()); memset(dst_tmp_array.Data, 0, (size_t)dst_tmp_array.size_in_bytes()); // 1. Initialize font loading structure, check font data validity - for (int src_i = 0; src_i < atlas->ConfigData.Size; src_i++) + for (int src_i = 0; src_i < atlas->Sources.Size; src_i++) { ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; - ImFontConfig& cfg = atlas->ConfigData[src_i]; - IM_ASSERT(cfg.DstFont && (!cfg.DstFont->IsLoaded() || cfg.DstFont->ContainerAtlas == atlas)); + ImFontConfig& src = atlas->Sources[src_i]; + IM_ASSERT(src.DstFont && (!src.DstFont->IsLoaded() || src.DstFont->ContainerAtlas == atlas)); - // Find index from cfg.DstFont (we allow the user to set cfg.DstFont. Also it makes casual debugging nicer than when storing indices) + // Find index from src.DstFont (we allow the user to set cfg.DstFont. Also it makes casual debugging nicer than when storing indices) src_tmp.DstIndex = -1; for (int output_i = 0; output_i < atlas->Fonts.Size && src_tmp.DstIndex == -1; output_i++) - if (cfg.DstFont == atlas->Fonts[output_i]) + if (src.DstFont == atlas->Fonts[output_i]) src_tmp.DstIndex = output_i; - IM_ASSERT(src_tmp.DstIndex != -1); // cfg.DstFont not pointing within atlas->Fonts[] array? if (src_tmp.DstIndex == -1) + { + IM_ASSERT(src_tmp.DstIndex != -1); // src.DstFont not pointing within atlas->Fonts[] array? return false; - + } // Initialize helper structure for font loading and verify that the TTF/OTF data is correct - const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)cfg.FontData, cfg.FontNo); + const int font_offset = stbtt_GetFontOffsetForIndex((unsigned char*)src.FontData, src.FontNo); IM_ASSERT(font_offset >= 0 && "FontData is incorrect, or FontNo cannot be found."); - if (!stbtt_InitFont(&src_tmp.FontInfo, (unsigned char*)cfg.FontData, font_offset)) + if (!stbtt_InitFont(&src_tmp.FontInfo, (unsigned char*)src.FontData, font_offset)) + { + IM_ASSERT(0 && "stbtt_InitFont(): failed to parse FontData. It is correct and complete? Check FontDataSize."); return false; + } // Measure highest codepoints ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; - src_tmp.SrcRanges = cfg.GlyphRanges ? cfg.GlyphRanges : atlas->GetGlyphRangesDefault(); + src_tmp.SrcRanges = src.GlyphRanges ? src.GlyphRanges : atlas->GetGlyphRangesDefault(); for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2) + { + // Check for valid range. This may also help detect *some* dangling pointers, because a common + // user error is to setup ImFontConfig::GlyphRanges with a pointer to data that isn't persistent, + // or to forget to zero-terminate the glyph range array. + IM_ASSERT(src_range[0] <= src_range[1] && "Invalid range: is your glyph range array persistent? it is zero-terminated?"); src_tmp.GlyphsHighest = ImMax(src_tmp.GlyphsHighest, (int)src_range[1]); + } dst_tmp.SrcCount++; dst_tmp.GlyphsHighest = ImMax(dst_tmp.GlyphsHighest, src_tmp.GlyphsHighest); } @@ -1900,14 +2943,14 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) { ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; ImFontBuildDstData& dst_tmp = dst_tmp_array[src_tmp.DstIndex]; - src_tmp.GlyphsSet.Resize(src_tmp.GlyphsHighest + 1); + src_tmp.GlyphsSet.Create(src_tmp.GlyphsHighest + 1); if (dst_tmp.GlyphsSet.Storage.empty()) - dst_tmp.GlyphsSet.Resize(dst_tmp.GlyphsHighest + 1); + dst_tmp.GlyphsSet.Create(dst_tmp.GlyphsHighest + 1); for (const ImWchar* src_range = src_tmp.SrcRanges; src_range[0] && src_range[1]; src_range += 2) - for (int codepoint = src_range[0]; codepoint <= src_range[1]; codepoint++) + for (unsigned int codepoint = src_range[0]; codepoint <= src_range[1]; codepoint++) { - if (dst_tmp.GlyphsSet.GetBit(codepoint)) // Don't overwrite existing glyphs. We could make this an option for MergeMode (e.g. MergeOverwrite==true) + if (dst_tmp.GlyphsSet.TestBit(codepoint)) // Don't overwrite existing glyphs. We could make this an option for MergeMode (e.g. MergeOverwrite==true) continue; if (!stbtt_FindGlyphIndex(&src_tmp.FontInfo, codepoint)) // It is actually in the font? continue; @@ -1915,8 +2958,8 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) // Add to avail set/counters src_tmp.GlyphsCount++; dst_tmp.GlyphsCount++; - src_tmp.GlyphsSet.SetBit(codepoint, true); - dst_tmp.GlyphsSet.SetBit(codepoint, true); + src_tmp.GlyphsSet.SetBit(codepoint); + dst_tmp.GlyphsSet.SetBit(codepoint); total_glyphs_count++; } } @@ -1926,7 +2969,7 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) { ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; src_tmp.GlyphsList.reserve(src_tmp.GlyphsCount); - UnpackBoolVectorToFlatIndexList(&src_tmp.GlyphsSet, &src_tmp.GlyphsList); + UnpackBitVectorToFlatIndexList(&src_tmp.GlyphsSet, &src_tmp.GlyphsList); src_tmp.GlyphsSet.Clear(); IM_ASSERT(src_tmp.GlyphsList.Size == src_tmp.GlyphsCount); } @@ -1947,6 +2990,7 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) int total_surface = 0; int buf_rects_out_n = 0; int buf_packedchars_out_n = 0; + const int pack_padding = atlas->TexGlyphPadding; for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) { ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; @@ -1958,30 +3002,35 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) buf_rects_out_n += src_tmp.GlyphsCount; buf_packedchars_out_n += src_tmp.GlyphsCount; + // Automatic selection of oversampling parameters + ImFontConfig& src = atlas->Sources[src_i]; + int oversample_h, oversample_v; + ImFontAtlasBuildGetOversampleFactors(&src, &oversample_h, &oversample_v); + // Convert our ranges in the format stb_truetype wants - ImFontConfig& cfg = atlas->ConfigData[src_i]; - src_tmp.PackRange.font_size = cfg.SizePixels; + src_tmp.PackRange.font_size = src.SizePixels * src.RasterizerDensity; src_tmp.PackRange.first_unicode_codepoint_in_range = 0; src_tmp.PackRange.array_of_unicode_codepoints = src_tmp.GlyphsList.Data; src_tmp.PackRange.num_chars = src_tmp.GlyphsList.Size; src_tmp.PackRange.chardata_for_range = src_tmp.PackedChars; - src_tmp.PackRange.h_oversample = (unsigned char)cfg.OversampleH; - src_tmp.PackRange.v_oversample = (unsigned char)cfg.OversampleV; + src_tmp.PackRange.h_oversample = (unsigned char)oversample_h; + src_tmp.PackRange.v_oversample = (unsigned char)oversample_v; // Gather the sizes of all rectangles we will need to pack (this loop is based on stbtt_PackFontRangesGatherRects) - const float scale = (cfg.SizePixels > 0) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -cfg.SizePixels); - const int padding = atlas->TexGlyphPadding; + const float scale = (src.SizePixels > 0.0f) ? stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, src.SizePixels * src.RasterizerDensity) : stbtt_ScaleForMappingEmToPixels(&src_tmp.FontInfo, -src.SizePixels * src.RasterizerDensity); for (int glyph_i = 0; glyph_i < src_tmp.GlyphsList.Size; glyph_i++) { int x0, y0, x1, y1; const int glyph_index_in_font = stbtt_FindGlyphIndex(&src_tmp.FontInfo, src_tmp.GlyphsList[glyph_i]); IM_ASSERT(glyph_index_in_font != 0); - stbtt_GetGlyphBitmapBoxSubpixel(&src_tmp.FontInfo, glyph_index_in_font, scale * cfg.OversampleH, scale * cfg.OversampleV, 0, 0, &x0, &y0, &x1, &y1); - src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + padding + cfg.OversampleH - 1); - src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + padding + cfg.OversampleV - 1); + stbtt_GetGlyphBitmapBoxSubpixel(&src_tmp.FontInfo, glyph_index_in_font, scale * oversample_h, scale * oversample_v, 0, 0, &x0, &y0, &x1, &y1); + src_tmp.Rects[glyph_i].w = (stbrp_coord)(x1 - x0 + pack_padding + oversample_h - 1); + src_tmp.Rects[glyph_i].h = (stbrp_coord)(y1 - y0 + pack_padding + oversample_v - 1); total_surface += src_tmp.Rects[glyph_i].w * src_tmp.Rects[glyph_i].h; } } + for (int i = 0; i < atlas->CustomRects.Size; i++) + total_surface += (atlas->CustomRects[i].Width + pack_padding) * (atlas->CustomRects[i].Height + pack_padding); // We need a width for the skyline algorithm, any width! // The exact width doesn't really matter much, but some API/GPU have texture size limitations and increasing width can decrease height. @@ -1991,13 +3040,14 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) if (atlas->TexDesiredWidth > 0) atlas->TexWidth = atlas->TexDesiredWidth; else - atlas->TexWidth = (surface_sqrt >= 4096*0.7f) ? 4096 : (surface_sqrt >= 2048*0.7f) ? 2048 : (surface_sqrt >= 1024*0.7f) ? 1024 : 512; + atlas->TexWidth = (surface_sqrt >= 4096 * 0.7f) ? 4096 : (surface_sqrt >= 2048 * 0.7f) ? 2048 : (surface_sqrt >= 1024 * 0.7f) ? 1024 : 512; // 5. Start packing // Pack our extra data rectangles first, so it will be on the upper-left corner of our texture (UV will have small values). const int TEX_HEIGHT_MAX = 1024 * 32; stbtt_pack_context spc = {}; - stbtt_PackBegin(&spc, NULL, atlas->TexWidth, TEX_HEIGHT_MAX, 0, atlas->TexGlyphPadding, NULL); + stbtt_PackBegin(&spc, NULL, atlas->TexWidth, TEX_HEIGHT_MAX, 0, 0, NULL); + spc.padding = atlas->TexGlyphPadding; // Because we mixup stbtt_PackXXX and stbrp_PackXXX there's a bit of a hack here, not passing the value to stbtt_PackBegin() allows us to still pack a TexWidth-1 wide item. (#8107) ImFontAtlasBuildPackCustomRects(atlas, spc.pack_info); // 6. Pack each source font. No rendering yet, we are working with rectangles in an infinitely tall texture at this point. @@ -2027,7 +3077,7 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) // 8. Render/rasterize font characters into the texture for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) { - ImFontConfig& cfg = atlas->ConfigData[src_i]; + ImFontConfig& src = atlas->Sources[src_i]; ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; if (src_tmp.GlyphsCount == 0) continue; @@ -2035,10 +3085,10 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) stbtt_PackFontRangesRenderIntoRects(&spc, &src_tmp.FontInfo, &src_tmp.PackRange, 1, src_tmp.Rects); // Apply multiply operator - if (cfg.RasterizerMultiply != 1.0f) + if (src.RasterizerMultiply != 1.0f) { unsigned char multiply_table[256]; - ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, cfg.RasterizerMultiply); + ImFontAtlasBuildMultiplyCalcLookupTable(multiply_table, src.RasterizerMultiply); stbrp_rect* r = &src_tmp.Rects[0]; for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++, r++) if (r->was_packed) @@ -2054,58 +3104,69 @@ bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas) // 9. Setup ImFont and glyphs for runtime for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) { + // When merging fonts with MergeMode=true: + // - We can have multiple input fonts writing into a same destination font. + // - dst_font->Sources is != from src which is our source configuration. ImFontBuildSrcData& src_tmp = src_tmp_array[src_i]; - if (src_tmp.GlyphsCount == 0) - continue; + ImFontConfig& src = atlas->Sources[src_i]; + ImFont* dst_font = src.DstFont; - ImFontConfig& cfg = atlas->ConfigData[src_i]; - ImFont* dst_font = cfg.DstFont; // We can have multiple input fonts writing into a same destination font (when using MergeMode=true) - - const float font_scale = stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, cfg.SizePixels); + const float font_scale = stbtt_ScaleForPixelHeight(&src_tmp.FontInfo, src.SizePixels); int unscaled_ascent, unscaled_descent, unscaled_line_gap; stbtt_GetFontVMetrics(&src_tmp.FontInfo, &unscaled_ascent, &unscaled_descent, &unscaled_line_gap); - const float ascent = ImFloor(unscaled_ascent * font_scale + ((unscaled_ascent > 0.0f) ? +1 : -1)); - const float descent = ImFloor(unscaled_descent * font_scale + ((unscaled_descent > 0.0f) ? +1 : -1)); - ImFontAtlasBuildSetupFont(atlas, dst_font, &cfg, ascent, descent); - const float font_off_x = cfg.GlyphOffset.x; - const float font_off_y = cfg.GlyphOffset.y + (float)(int)(dst_font->Ascent + 0.5f); + const float ascent = ImCeil(unscaled_ascent * font_scale); + const float descent = ImFloor(unscaled_descent * font_scale); + ImFontAtlasBuildSetupFont(atlas, dst_font, &src, ascent, descent); + const float font_off_x = src.GlyphOffset.x; + const float font_off_y = src.GlyphOffset.y + IM_ROUND(dst_font->Ascent); + + const float inv_rasterization_scale = 1.0f / src.RasterizerDensity; for (int glyph_i = 0; glyph_i < src_tmp.GlyphsCount; glyph_i++) { + // Register glyph const int codepoint = src_tmp.GlyphsList[glyph_i]; const stbtt_packedchar& pc = src_tmp.PackedChars[glyph_i]; - - const float char_advance_x_org = pc.xadvance; - const float char_advance_x_mod = ImClamp(char_advance_x_org, cfg.GlyphMinAdvanceX, cfg.GlyphMaxAdvanceX); - float char_off_x = font_off_x; - if (char_advance_x_org != char_advance_x_mod) - char_off_x += cfg.PixelSnapH ? (float)(int)((char_advance_x_mod - char_advance_x_org) * 0.5f) : (char_advance_x_mod - char_advance_x_org) * 0.5f; - - // Register glyph stbtt_aligned_quad q; - float dummy_x = 0.0f, dummy_y = 0.0f; - stbtt_GetPackedQuad(src_tmp.PackedChars, atlas->TexWidth, atlas->TexHeight, glyph_i, &dummy_x, &dummy_y, &q, 0); - dst_font->AddGlyph((ImWchar)codepoint, q.x0 + char_off_x, q.y0 + font_off_y, q.x1 + char_off_x, q.y1 + font_off_y, q.s0, q.t0, q.s1, q.t1, char_advance_x_mod); + float unused_x = 0.0f, unused_y = 0.0f; + stbtt_GetPackedQuad(src_tmp.PackedChars, atlas->TexWidth, atlas->TexHeight, glyph_i, &unused_x, &unused_y, &q, 0); + float x0 = q.x0 * inv_rasterization_scale + font_off_x; + float y0 = q.y0 * inv_rasterization_scale + font_off_y; + float x1 = q.x1 * inv_rasterization_scale + font_off_x; + float y1 = q.y1 * inv_rasterization_scale + font_off_y; + dst_font->AddGlyph(&src, (ImWchar)codepoint, x0, y0, x1, y1, q.s0, q.t0, q.s1, q.t1, pc.xadvance * inv_rasterization_scale); } } - // Cleanup temporary (ImVector doesn't honor destructor) - for (int src_i = 0; src_i < src_tmp_array.Size; src_i++) - src_tmp_array[src_i].~ImFontBuildSrcData(); + // Cleanup + src_tmp_array.clear_destruct(); ImFontAtlasBuildFinish(atlas); return true; } -void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas* atlas) +const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype() { - if (atlas->CustomRectIds[0] >= 0) - return; - if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) - atlas->CustomRectIds[0] = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_ID, FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF*2+1, FONT_ATLAS_DEFAULT_TEX_DATA_H); - else - atlas->CustomRectIds[0] = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_ID, 2, 2); + static ImFontBuilderIO io; + io.FontBuilder_Build = ImFontAtlasBuildWithStbTruetype; + return &io; +} + +#endif // IMGUI_ENABLE_STB_TRUETYPE + +void ImFontAtlasUpdateSourcesPointers(ImFontAtlas* atlas) +{ + for (ImFontConfig& src : atlas->Sources) + { + ImFont* font = src.DstFont; + if (!src.MergeMode) + { + font->Sources = &src; + font->SourcesCount = 0; + } + font->SourcesCount++; + } } void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent) @@ -2114,12 +3175,11 @@ void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* f { font->ClearOutputData(); font->FontSize = font_config->SizePixels; - font->ConfigData = font_config; + IM_ASSERT(font->Sources == font_config); font->ContainerAtlas = atlas; font->Ascent = ascent; font->Descent = descent; } - font->ConfigDataCount++; } void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque) @@ -2129,98 +3189,208 @@ void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opa ImVector& user_rects = atlas->CustomRects; IM_ASSERT(user_rects.Size >= 1); // We expect at least the default custom rects to be registered, else something went wrong. +#ifdef __GNUC__ + if (user_rects.Size < 1) { __builtin_unreachable(); } // Workaround for GCC bug if IM_ASSERT() is defined to conditionally throw (see #5343) +#endif + const int pack_padding = atlas->TexGlyphPadding; ImVector pack_rects; pack_rects.resize(user_rects.Size); memset(pack_rects.Data, 0, (size_t)pack_rects.size_in_bytes()); for (int i = 0; i < user_rects.Size; i++) { - pack_rects[i].w = user_rects[i].Width; - pack_rects[i].h = user_rects[i].Height; + pack_rects[i].w = user_rects[i].Width + pack_padding; + pack_rects[i].h = user_rects[i].Height + pack_padding; } stbrp_pack_rects(pack_context, &pack_rects[0], pack_rects.Size); for (int i = 0; i < pack_rects.Size; i++) if (pack_rects[i].was_packed) { - user_rects[i].X = pack_rects[i].x; - user_rects[i].Y = pack_rects[i].y; - IM_ASSERT(pack_rects[i].w == user_rects[i].Width && pack_rects[i].h == user_rects[i].Height); + user_rects[i].X = (unsigned short)pack_rects[i].x; + user_rects[i].Y = (unsigned short)pack_rects[i].y; + IM_ASSERT(pack_rects[i].w == user_rects[i].Width + pack_padding && pack_rects[i].h == user_rects[i].Height + pack_padding); atlas->TexHeight = ImMax(atlas->TexHeight, pack_rects[i].y + pack_rects[i].h); } } +void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value) +{ + IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth); + IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight); + unsigned char* out_pixel = atlas->TexPixelsAlpha8 + x + (y * atlas->TexWidth); + for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w) + for (int off_x = 0; off_x < w; off_x++) + out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : 0x00; +} + +void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value) +{ + IM_ASSERT(x >= 0 && x + w <= atlas->TexWidth); + IM_ASSERT(y >= 0 && y + h <= atlas->TexHeight); + unsigned int* out_pixel = atlas->TexPixelsRGBA32 + x + (y * atlas->TexWidth); + for (int off_y = 0; off_y < h; off_y++, out_pixel += atlas->TexWidth, in_str += w) + for (int off_x = 0; off_x < w; off_x++) + out_pixel[off_x] = (in_str[off_x] == in_marker_char) ? in_marker_pixel_value : IM_COL32_BLACK_TRANS; +} + static void ImFontAtlasBuildRenderDefaultTexData(ImFontAtlas* atlas) { - IM_ASSERT(atlas->CustomRectIds[0] >= 0); - IM_ASSERT(atlas->TexPixelsAlpha8 != NULL); - ImFontAtlasCustomRect& r = atlas->CustomRects[atlas->CustomRectIds[0]]; - IM_ASSERT(r.ID == FONT_ATLAS_DEFAULT_TEX_DATA_ID); - IM_ASSERT(r.IsPacked()); + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdMouseCursors); + IM_ASSERT(r->IsPacked()); const int w = atlas->TexWidth; - if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + if (atlas->Flags & ImFontAtlasFlags_NoMouseCursors) { - // Render/copy pixels - IM_ASSERT(r.Width == FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF * 2 + 1 && r.Height == FONT_ATLAS_DEFAULT_TEX_DATA_H); - for (int y = 0, n = 0; y < FONT_ATLAS_DEFAULT_TEX_DATA_H; y++) - for (int x = 0; x < FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF; x++, n++) - { - const int offset0 = (int)(r.X + x) + (int)(r.Y + y) * w; - const int offset1 = offset0 + FONT_ATLAS_DEFAULT_TEX_DATA_W_HALF + 1; - atlas->TexPixelsAlpha8[offset0] = FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[n] == '.' ? 0xFF : 0x00; - atlas->TexPixelsAlpha8[offset1] = FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS[n] == 'X' ? 0xFF : 0x00; - } + // White pixels only + IM_ASSERT(r->Width == 2 && r->Height == 2); + const int offset = (int)r->X + (int)r->Y * w; + if (atlas->TexPixelsAlpha8 != NULL) + { + atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF; + } + else + { + atlas->TexPixelsRGBA32[offset] = atlas->TexPixelsRGBA32[offset + 1] = atlas->TexPixelsRGBA32[offset + w] = atlas->TexPixelsRGBA32[offset + w + 1] = IM_COL32_WHITE; + } } else { - IM_ASSERT(r.Width == 2 && r.Height == 2); - const int offset = (int)(r.X) + (int)(r.Y) * w; - atlas->TexPixelsAlpha8[offset] = atlas->TexPixelsAlpha8[offset + 1] = atlas->TexPixelsAlpha8[offset + w] = atlas->TexPixelsAlpha8[offset + w + 1] = 0xFF; + // White pixels and mouse cursor + IM_ASSERT(r->Width == FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1 && r->Height == FONT_ATLAS_DEFAULT_TEX_DATA_H); + const int x_for_white = r->X; + const int x_for_black = r->X + FONT_ATLAS_DEFAULT_TEX_DATA_W + 1; + if (atlas->TexPixelsAlpha8 != NULL) + { + ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', 0xFF); + ImFontAtlasBuildRender8bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', 0xFF); + } + else + { + ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_white, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, '.', IM_COL32_WHITE); + ImFontAtlasBuildRender32bppRectFromString(atlas, x_for_black, r->Y, FONT_ATLAS_DEFAULT_TEX_DATA_W, FONT_ATLAS_DEFAULT_TEX_DATA_H, FONT_ATLAS_DEFAULT_TEX_DATA_PIXELS, 'X', IM_COL32_WHITE); + } } - atlas->TexUvWhitePixel = ImVec2((r.X + 0.5f) * atlas->TexUvScale.x, (r.Y + 0.5f) * atlas->TexUvScale.y); + atlas->TexUvWhitePixel = ImVec2((r->X + 0.5f) * atlas->TexUvScale.x, (r->Y + 0.5f) * atlas->TexUvScale.y); } +static void ImFontAtlasBuildRenderLinesTexData(ImFontAtlas* atlas) +{ + if (atlas->Flags & ImFontAtlasFlags_NoBakedLines) + return; + + // This generates a triangular shape in the texture, with the various line widths stacked on top of each other to allow interpolation between them + ImFontAtlasCustomRect* r = atlas->GetCustomRectByIndex(atlas->PackIdLines); + IM_ASSERT(r->IsPacked()); + for (int n = 0; n < IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1; n++) // +1 because of the zero-width row + { + // Each line consists of at least two empty pixels at the ends, with a line of solid pixels in the middle + int y = n; + int line_width = n; + int pad_left = (r->Width - line_width) / 2; + int pad_right = r->Width - (pad_left + line_width); + + // Write each slice + IM_ASSERT(pad_left + line_width + pad_right == r->Width && y < r->Height); // Make sure we're inside the texture bounds before we start writing pixels + if (atlas->TexPixelsAlpha8 != NULL) + { + unsigned char* write_ptr = &atlas->TexPixelsAlpha8[r->X + ((r->Y + y) * atlas->TexWidth)]; + for (int i = 0; i < pad_left; i++) + *(write_ptr + i) = 0x00; + + for (int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = 0xFF; + + for (int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = 0x00; + } + else + { + unsigned int* write_ptr = &atlas->TexPixelsRGBA32[r->X + ((r->Y + y) * atlas->TexWidth)]; + for (int i = 0; i < pad_left; i++) + *(write_ptr + i) = IM_COL32(255, 255, 255, 0); + + for (int i = 0; i < line_width; i++) + *(write_ptr + pad_left + i) = IM_COL32_WHITE; + + for (int i = 0; i < pad_right; i++) + *(write_ptr + pad_left + line_width + i) = IM_COL32(255, 255, 255, 0); + } + + // Calculate UVs for this line + ImVec2 uv0 = ImVec2((float)(r->X + pad_left - 1), (float)(r->Y + y)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(r->X + pad_left + line_width + 1), (float)(r->Y + y + 1)) * atlas->TexUvScale; + float half_v = (uv0.y + uv1.y) * 0.5f; // Calculate a constant V in the middle of the row to avoid sampling artifacts + atlas->TexUvLines[n] = ImVec4(uv0.x, half_v, uv1.x, half_v); + } +} + +// Note: this is called / shared by both the stb_truetype and the FreeType builder +void ImFontAtlasBuildInit(ImFontAtlas* atlas) +{ + // Register texture region for mouse cursors or standard white pixels + if (atlas->PackIdMouseCursors < 0) + { + if (!(atlas->Flags & ImFontAtlasFlags_NoMouseCursors)) + atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(FONT_ATLAS_DEFAULT_TEX_DATA_W * 2 + 1, FONT_ATLAS_DEFAULT_TEX_DATA_H); + else + atlas->PackIdMouseCursors = atlas->AddCustomRectRegular(2, 2); + } + + // Register texture region for thick lines + // The +2 here is to give space for the end caps, whilst height +1 is to accommodate the fact we have a zero-width row + if (atlas->PackIdLines < 0) + { + if (!(atlas->Flags & ImFontAtlasFlags_NoBakedLines)) + atlas->PackIdLines = atlas->AddCustomRectRegular(IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 2, IM_DRAWLIST_TEX_LINES_WIDTH_MAX + 1); + } +} + +// This is called/shared by both the stb_truetype and the FreeType builder. void ImFontAtlasBuildFinish(ImFontAtlas* atlas) { - // Render into our custom data block + // Render into our custom data blocks + IM_ASSERT(atlas->TexPixelsAlpha8 != NULL || atlas->TexPixelsRGBA32 != NULL); ImFontAtlasBuildRenderDefaultTexData(atlas); + ImFontAtlasBuildRenderLinesTexData(atlas); // Register custom rectangle glyphs for (int i = 0; i < atlas->CustomRects.Size; i++) { - const ImFontAtlasCustomRect& r = atlas->CustomRects[i]; - if (r.Font == NULL || r.ID > 0x10000) + const ImFontAtlasCustomRect* r = &atlas->CustomRects[i]; + if (r->Font == NULL || r->GlyphID == 0) continue; - IM_ASSERT(r.Font->ContainerAtlas == atlas); + // Will ignore ImFontConfig settings: GlyphMinAdvanceX, GlyphMinAdvanceY, PixelSnapH + IM_ASSERT(r->Font->ContainerAtlas == atlas); ImVec2 uv0, uv1; - atlas->CalcCustomRectUV(&r, &uv0, &uv1); - r.Font->AddGlyph((ImWchar)r.ID, r.GlyphOffset.x, r.GlyphOffset.y, r.GlyphOffset.x + r.Width, r.GlyphOffset.y + r.Height, uv0.x, uv0.y, uv1.x, uv1.y, r.GlyphAdvanceX); + atlas->CalcCustomRectUV(r, &uv0, &uv1); + r->Font->AddGlyph(NULL, (ImWchar)r->GlyphID, r->GlyphOffset.x, r->GlyphOffset.y, r->GlyphOffset.x + r->Width, r->GlyphOffset.y + r->Height, uv0.x, uv0.y, uv1.x, uv1.y, r->GlyphAdvanceX); + if (r->GlyphColored) + r->Font->Glyphs.back().Colored = 1; } // Build all fonts lookup tables - for (int i = 0; i < atlas->Fonts.Size; i++) - if (atlas->Fonts[i]->DirtyLookupTables) - atlas->Fonts[i]->BuildLookupTable(); + for (ImFont* font : atlas->Fonts) + if (font->DirtyLookupTables) + font->BuildLookupTable(); - // Ellipsis character is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis). - // However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character. - // FIXME: Also note that 0x2026 is currently seldomly included in our font ranges. Because of this we are more likely to use three individual dots. - for (int i = 0; i < atlas->Fonts.size(); i++) - { - ImFont* font = atlas->Fonts[i]; - if (font->EllipsisChar != (ImWchar)-1) - continue; - const ImWchar ellipsis_variants[] = { (ImWchar)0x2026, (ImWchar)0x0085 }; - for (int j = 0; j < IM_ARRAYSIZE(ellipsis_variants); j++) - if (font->FindGlyphNoFallback(ellipsis_variants[j]) != NULL) // Verify glyph exists - { - font->EllipsisChar = ellipsis_variants[j]; - break; - } - } + atlas->TexReady = true; } +//------------------------------------------------------------------------- +// [SECTION] ImFontAtlas: glyph ranges helpers +//------------------------------------------------------------------------- +// - GetGlyphRangesDefault() +// - GetGlyphRangesGreek() +// - GetGlyphRangesKorean() +// - GetGlyphRangesChineseFull() +// - GetGlyphRangesChineseSimplifiedCommon() +// - GetGlyphRangesJapanese() +// - GetGlyphRangesCyrillic() +// - GetGlyphRangesThai() +// - GetGlyphRangesVietnamese() +//----------------------------------------------------------------------------- + // Retrieve list of range (2 int per range, values are inclusive) const ImWchar* ImFontAtlas::GetGlyphRangesDefault() { @@ -2232,13 +3402,25 @@ const ImWchar* ImFontAtlas::GetGlyphRangesDefault() return &ranges[0]; } +const ImWchar* ImFontAtlas::GetGlyphRangesGreek() +{ + static const ImWchar ranges[] = + { + 0x0020, 0x00FF, // Basic Latin + Latin Supplement + 0x0370, 0x03FF, // Greek and Coptic + 0, + }; + return &ranges[0]; +} + const ImWchar* ImFontAtlas::GetGlyphRangesKorean() { static const ImWchar ranges[] = { 0x0020, 0x00FF, // Basic Latin + Latin Supplement 0x3131, 0x3163, // Korean alphabets - 0xAC00, 0xD79D, // Korean characters + 0xAC00, 0xD7A3, // Korean characters + 0xFFFD, 0xFFFD, // Invalid 0, }; return &ranges[0]; @@ -2253,6 +3435,7 @@ const ImWchar* ImFontAtlas::GetGlyphRangesChineseFull() 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana 0x31F0, 0x31FF, // Katakana Phonetic Extensions 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD, // Invalid 0x4e00, 0x9FAF, // CJK Ideograms 0, }; @@ -2269,10 +3452,6 @@ static void UnpackAccumulativeOffsetsIntoRanges(int base_codepoint, const short* out_ranges[0] = 0; } -//------------------------------------------------------------------------- -// [SECTION] ImFontAtlas glyph ranges helpers -//------------------------------------------------------------------------- - const ImWchar* ImFontAtlas::GetGlyphRangesChineseSimplifiedCommon() { // Store 2500 regularly used characters for Simplified Chinese. @@ -2329,7 +3508,8 @@ const ImWchar* ImFontAtlas::GetGlyphRangesChineseSimplifiedCommon() 0x2000, 0x206F, // General Punctuation 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana 0x31F0, 0x31FF, // Katakana Phonetic Extensions - 0xFF00, 0xFFEF // Half-width characters + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD // Invalid }; static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(accumulative_offsets_from_0x4E00) * 2 + 1] = { 0 }; if (!full_ranges[0]) @@ -2342,52 +3522,84 @@ const ImWchar* ImFontAtlas::GetGlyphRangesChineseSimplifiedCommon() const ImWchar* ImFontAtlas::GetGlyphRangesJapanese() { - // 1946 common ideograms code points for Japanese - // Sourced from http://theinstructionlimit.com/common-kanji-character-ranges-for-xna-spritefont-rendering - // FIXME: Source a list of the revised 2136 Joyo Kanji list from 2010 and rebuild this. + // 2999 ideograms code points for Japanese + // - 2136 Joyo (meaning "for regular use" or "for common use") Kanji code points + // - 863 Jinmeiyo (meaning "for personal name") Kanji code points + // - Sourced from official information provided by the government agencies of Japan: + // - List of Joyo Kanji by the Agency for Cultural Affairs + // - https://www.bunka.go.jp/kokugo_nihongo/sisaku/joho/joho/kijun/naikaku/kanji/ + // - List of Jinmeiyo Kanji by the Ministry of Justice + // - http://www.moj.go.jp/MINJI/minji86.html + // - Available under the terms of the Creative Commons Attribution 4.0 International (CC BY 4.0). + // - https://creativecommons.org/licenses/by/4.0/legalcode + // - You can generate this code by the script at: + // - https://github.com/vaiorabbit/everyday_use_kanji + // - References: + // - List of Joyo Kanji + // - (Wikipedia) https://en.wikipedia.org/wiki/List_of_j%C5%8Dy%C5%8D_kanji + // - List of Jinmeiyo Kanji + // - (Wikipedia) https://en.wikipedia.org/wiki/Jinmeiy%C5%8D_kanji + // - Missing 1 Joyo Kanji: U+20B9F (Kun'yomi: Shikaru, On'yomi: Shitsu,shichi), see https://github.com/ocornut/imgui/pull/3627 for details. // You can use ImFontGlyphRangesBuilder to create your own ranges derived from this, by merging existing ranges or adding new characters. // (Stored as accumulative offsets from the initial unicode codepoint 0x4E00. This encoding is designed to helps us compact the source code size.) static const short accumulative_offsets_from_0x4E00[] = { - 0,1,2,4,1,1,1,1,2,1,6,2,2,1,8,5,7,11,1,2,10,10,8,2,4,20,2,11,8,2,1,2,1,6,2,1,7,5,3,7,1,1,13,7,9,1,4,6,1,2,1,10,1,1,9,2,2,4,5,6,14,1,1,9,3,18, - 5,4,2,2,10,7,1,1,1,3,2,4,3,23,2,10,12,2,14,2,4,13,1,6,10,3,1,7,13,6,4,13,5,2,3,17,2,2,5,7,6,4,1,7,14,16,6,13,9,15,1,1,7,16,4,7,1,19,9,2,7,15, - 2,6,5,13,25,4,14,13,11,25,1,1,1,2,1,2,2,3,10,11,3,3,1,1,4,4,2,1,4,9,1,4,3,5,5,2,7,12,11,15,7,16,4,5,16,2,1,1,6,3,3,1,1,2,7,6,6,7,1,4,7,6,1,1, - 2,1,12,3,3,9,5,8,1,11,1,2,3,18,20,4,1,3,6,1,7,3,5,5,7,2,2,12,3,1,4,2,3,2,3,11,8,7,4,17,1,9,25,1,1,4,2,2,4,1,2,7,1,1,1,3,1,2,6,16,1,2,1,1,3,12, - 20,2,5,20,8,7,6,2,1,1,1,1,6,2,1,2,10,1,1,6,1,3,1,2,1,4,1,12,4,1,3,1,1,1,1,1,10,4,7,5,13,1,15,1,1,30,11,9,1,15,38,14,1,32,17,20,1,9,31,2,21,9, - 4,49,22,2,1,13,1,11,45,35,43,55,12,19,83,1,3,2,3,13,2,1,7,3,18,3,13,8,1,8,18,5,3,7,25,24,9,24,40,3,17,24,2,1,6,2,3,16,15,6,7,3,12,1,9,7,3,3, - 3,15,21,5,16,4,5,12,11,11,3,6,3,2,31,3,2,1,1,23,6,6,1,4,2,6,5,2,1,1,3,3,22,2,6,2,3,17,3,2,4,5,1,9,5,1,1,6,15,12,3,17,2,14,2,8,1,23,16,4,2,23, - 8,15,23,20,12,25,19,47,11,21,65,46,4,3,1,5,6,1,2,5,26,2,1,1,3,11,1,1,1,2,1,2,3,1,1,10,2,3,1,1,1,3,6,3,2,2,6,6,9,2,2,2,6,2,5,10,2,4,1,2,1,2,2, - 3,1,1,3,1,2,9,23,9,2,1,1,1,1,5,3,2,1,10,9,6,1,10,2,31,25,3,7,5,40,1,15,6,17,7,27,180,1,3,2,2,1,1,1,6,3,10,7,1,3,6,17,8,6,2,2,1,3,5,5,8,16,14, - 15,1,1,4,1,2,1,1,1,3,2,7,5,6,2,5,10,1,4,2,9,1,1,11,6,1,44,1,3,7,9,5,1,3,1,1,10,7,1,10,4,2,7,21,15,7,2,5,1,8,3,4,1,3,1,6,1,4,2,1,4,10,8,1,4,5, - 1,5,10,2,7,1,10,1,1,3,4,11,10,29,4,7,3,5,2,3,33,5,2,19,3,1,4,2,6,31,11,1,3,3,3,1,8,10,9,12,11,12,8,3,14,8,6,11,1,4,41,3,1,2,7,13,1,5,6,2,6,12, - 12,22,5,9,4,8,9,9,34,6,24,1,1,20,9,9,3,4,1,7,2,2,2,6,2,28,5,3,6,1,4,6,7,4,2,1,4,2,13,6,4,4,3,1,8,8,3,2,1,5,1,2,2,3,1,11,11,7,3,6,10,8,6,16,16, - 22,7,12,6,21,5,4,6,6,3,6,1,3,2,1,2,8,29,1,10,1,6,13,6,6,19,31,1,13,4,4,22,17,26,33,10,4,15,12,25,6,67,10,2,3,1,6,10,2,6,2,9,1,9,4,4,1,2,16,2, - 5,9,2,3,8,1,8,3,9,4,8,6,4,8,11,3,2,1,1,3,26,1,7,5,1,11,1,5,3,5,2,13,6,39,5,1,5,2,11,6,10,5,1,15,5,3,6,19,21,22,2,4,1,6,1,8,1,4,8,2,4,2,2,9,2, - 1,1,1,4,3,6,3,12,7,1,14,2,4,10,2,13,1,17,7,3,2,1,3,2,13,7,14,12,3,1,29,2,8,9,15,14,9,14,1,3,1,6,5,9,11,3,38,43,20,7,7,8,5,15,12,19,15,81,8,7, - 1,5,73,13,37,28,8,8,1,15,18,20,165,28,1,6,11,8,4,14,7,15,1,3,3,6,4,1,7,14,1,1,11,30,1,5,1,4,14,1,4,2,7,52,2,6,29,3,1,9,1,21,3,5,1,26,3,11,14, - 11,1,17,5,1,2,1,3,2,8,1,2,9,12,1,1,2,3,8,3,24,12,7,7,5,17,3,3,3,1,23,10,4,4,6,3,1,16,17,22,3,10,21,16,16,6,4,10,2,1,1,2,8,8,6,5,3,3,3,39,25, - 15,1,1,16,6,7,25,15,6,6,12,1,22,13,1,4,9,5,12,2,9,1,12,28,8,3,5,10,22,60,1,2,40,4,61,63,4,1,13,12,1,4,31,12,1,14,89,5,16,6,29,14,2,5,49,18,18, - 5,29,33,47,1,17,1,19,12,2,9,7,39,12,3,7,12,39,3,1,46,4,12,3,8,9,5,31,15,18,3,2,2,66,19,13,17,5,3,46,124,13,57,34,2,5,4,5,8,1,1,1,4,3,1,17,5, - 3,5,3,1,8,5,6,3,27,3,26,7,12,7,2,17,3,7,18,78,16,4,36,1,2,1,6,2,1,39,17,7,4,13,4,4,4,1,10,4,2,4,6,3,10,1,19,1,26,2,4,33,2,73,47,7,3,8,2,4,15, - 18,1,29,2,41,14,1,21,16,41,7,39,25,13,44,2,2,10,1,13,7,1,7,3,5,20,4,8,2,49,1,10,6,1,6,7,10,7,11,16,3,12,20,4,10,3,1,2,11,2,28,9,2,4,7,2,15,1, - 27,1,28,17,4,5,10,7,3,24,10,11,6,26,3,2,7,2,2,49,16,10,16,15,4,5,27,61,30,14,38,22,2,7,5,1,3,12,23,24,17,17,3,3,2,4,1,6,2,7,5,1,1,5,1,1,9,4, - 1,3,6,1,8,2,8,4,14,3,5,11,4,1,3,32,1,19,4,1,13,11,5,2,1,8,6,8,1,6,5,13,3,23,11,5,3,16,3,9,10,1,24,3,198,52,4,2,2,5,14,5,4,22,5,20,4,11,6,41, - 1,5,2,2,11,5,2,28,35,8,22,3,18,3,10,7,5,3,4,1,5,3,8,9,3,6,2,16,22,4,5,5,3,3,18,23,2,6,23,5,27,8,1,33,2,12,43,16,5,2,3,6,1,20,4,2,9,7,1,11,2, - 10,3,14,31,9,3,25,18,20,2,5,5,26,14,1,11,17,12,40,19,9,6,31,83,2,7,9,19,78,12,14,21,76,12,113,79,34,4,1,1,61,18,85,10,2,2,13,31,11,50,6,33,159, - 179,6,6,7,4,4,2,4,2,5,8,7,20,32,22,1,3,10,6,7,28,5,10,9,2,77,19,13,2,5,1,4,4,7,4,13,3,9,31,17,3,26,2,6,6,5,4,1,7,11,3,4,2,1,6,2,20,4,1,9,2,6, - 3,7,1,1,1,20,2,3,1,6,2,3,6,2,4,8,1,5,13,8,4,11,23,1,10,6,2,1,3,21,2,2,4,24,31,4,10,10,2,5,192,15,4,16,7,9,51,1,2,1,1,5,1,1,2,1,3,5,3,1,3,4,1, - 3,1,3,3,9,8,1,2,2,2,4,4,18,12,92,2,10,4,3,14,5,25,16,42,4,14,4,2,21,5,126,30,31,2,1,5,13,3,22,5,6,6,20,12,1,14,12,87,3,19,1,8,2,9,9,3,3,23,2, - 3,7,6,3,1,2,3,9,1,3,1,6,3,2,1,3,11,3,1,6,10,3,2,3,1,2,1,5,1,1,11,3,6,4,1,7,2,1,2,5,5,34,4,14,18,4,19,7,5,8,2,6,79,1,5,2,14,8,2,9,2,1,36,28,16, - 4,1,1,1,2,12,6,42,39,16,23,7,15,15,3,2,12,7,21,64,6,9,28,8,12,3,3,41,59,24,51,55,57,294,9,9,2,6,2,15,1,2,13,38,90,9,9,9,3,11,7,1,1,1,5,6,3,2, - 1,2,2,3,8,1,4,4,1,5,7,1,4,3,20,4,9,1,1,1,5,5,17,1,5,2,6,2,4,1,4,5,7,3,18,11,11,32,7,5,4,7,11,127,8,4,3,3,1,10,1,1,6,21,14,1,16,1,7,1,3,6,9,65, - 51,4,3,13,3,10,1,1,12,9,21,110,3,19,24,1,1,10,62,4,1,29,42,78,28,20,18,82,6,3,15,6,84,58,253,15,155,264,15,21,9,14,7,58,40,39, + 0,1,2,4,1,1,1,1,2,1,3,3,2,2,1,5,3,5,7,5,6,1,2,1,7,2,6,3,1,8,1,1,4,1,1,18,2,11,2,6,2,1,2,1,5,1,2,1,3,1,2,1,2,3,3,1,1,2,3,1,1,1,12,7,9,1,4,5,1, + 1,2,1,10,1,1,9,2,2,4,5,6,9,3,1,1,1,1,9,3,18,5,2,2,2,2,1,6,3,7,1,1,1,1,2,2,4,2,1,23,2,10,4,3,5,2,4,10,2,4,13,1,6,1,9,3,1,1,6,6,7,6,3,1,2,11,3, + 2,2,3,2,15,2,2,5,4,3,6,4,1,2,5,2,12,16,6,13,9,13,2,1,1,7,16,4,7,1,19,1,5,1,2,2,7,7,8,2,6,5,4,9,18,7,4,5,9,13,11,8,15,2,1,1,1,2,1,2,2,1,2,2,8, + 2,9,3,3,1,1,4,4,1,1,1,4,9,1,4,3,5,5,2,7,5,3,4,8,2,1,13,2,3,3,1,14,1,1,4,5,1,3,6,1,5,2,1,1,3,3,3,3,1,1,2,7,6,6,7,1,4,7,6,1,1,1,1,1,12,3,3,9,5, + 2,6,1,5,6,1,2,3,18,2,4,14,4,1,3,6,1,1,6,3,5,5,3,2,2,2,2,12,3,1,4,2,3,2,3,11,1,7,4,1,2,1,3,17,1,9,1,24,1,1,4,2,2,4,1,2,7,1,1,1,3,1,2,2,4,15,1, + 1,2,1,1,2,1,5,2,5,20,2,5,9,1,10,8,7,6,1,1,1,1,1,1,6,2,1,2,8,1,1,1,1,5,1,1,3,1,1,1,1,3,1,1,12,4,1,3,1,1,1,1,1,10,3,1,7,5,13,1,2,3,4,6,1,1,30, + 2,9,9,1,15,38,11,3,1,8,24,7,1,9,8,10,2,1,9,31,2,13,6,2,9,4,49,5,2,15,2,1,10,2,1,1,1,2,2,6,15,30,35,3,14,18,8,1,16,10,28,12,19,45,38,1,3,2,3, + 13,2,1,7,3,6,5,3,4,3,1,5,7,8,1,5,3,18,5,3,6,1,21,4,24,9,24,40,3,14,3,21,3,2,1,2,4,2,3,1,15,15,6,5,1,1,3,1,5,6,1,9,7,3,3,2,1,4,3,8,21,5,16,4, + 5,2,10,11,11,3,6,3,2,9,3,6,13,1,2,1,1,1,1,11,12,6,6,1,4,2,6,5,2,1,1,3,3,6,13,3,1,1,5,1,2,3,3,14,2,1,2,2,2,5,1,9,5,1,1,6,12,3,12,3,4,13,2,14, + 2,8,1,17,5,1,16,4,2,2,21,8,9,6,23,20,12,25,19,9,38,8,3,21,40,25,33,13,4,3,1,4,1,2,4,1,2,5,26,2,1,1,2,1,3,6,2,1,1,1,1,1,1,2,3,1,1,1,9,2,3,1,1, + 1,3,6,3,2,1,1,6,6,1,8,2,2,2,1,4,1,2,3,2,7,3,2,4,1,2,1,2,2,1,1,1,1,1,3,1,2,5,4,10,9,4,9,1,1,1,1,1,1,5,3,2,1,6,4,9,6,1,10,2,31,17,8,3,7,5,40,1, + 7,7,1,6,5,2,10,7,8,4,15,39,25,6,28,47,18,10,7,1,3,1,1,2,1,1,1,3,3,3,1,1,1,3,4,2,1,4,1,3,6,10,7,8,6,2,2,1,3,3,2,5,8,7,9,12,2,15,1,1,4,1,2,1,1, + 1,3,2,1,3,3,5,6,2,3,2,10,1,4,2,8,1,1,1,11,6,1,21,4,16,3,1,3,1,4,2,3,6,5,1,3,1,1,3,3,4,6,1,1,10,4,2,7,10,4,7,4,2,9,4,3,1,1,1,4,1,8,3,4,1,3,1, + 6,1,4,2,1,4,7,2,1,8,1,4,5,1,1,2,2,4,6,2,7,1,10,1,1,3,4,11,10,8,21,4,6,1,3,5,2,1,2,28,5,5,2,3,13,1,2,3,1,4,2,1,5,20,3,8,11,1,3,3,3,1,8,10,9,2, + 10,9,2,3,1,1,2,4,1,8,3,6,1,7,8,6,11,1,4,29,8,4,3,1,2,7,13,1,4,1,6,2,6,12,12,2,20,3,2,3,6,4,8,9,2,7,34,5,1,18,6,1,1,4,4,5,7,9,1,2,2,4,3,4,1,7, + 2,2,2,6,2,3,25,5,3,6,1,4,6,7,4,2,1,4,2,13,6,4,4,3,1,5,3,4,4,3,2,1,1,4,1,2,1,1,3,1,11,1,6,3,1,7,3,6,2,8,8,6,9,3,4,11,3,2,10,12,2,5,11,1,6,4,5, + 3,1,8,5,4,6,6,3,5,1,1,3,2,1,2,2,6,17,12,1,10,1,6,12,1,6,6,19,9,6,16,1,13,4,4,15,7,17,6,11,9,15,12,6,7,2,1,2,2,15,9,3,21,4,6,49,18,7,3,2,3,1, + 6,8,2,2,6,2,9,1,3,6,4,4,1,2,16,2,5,2,1,6,2,3,5,3,1,2,5,1,2,1,9,3,1,8,6,4,8,11,3,1,1,1,1,3,1,13,8,4,1,3,2,2,1,4,1,11,1,5,2,1,5,2,5,8,6,1,1,7, + 4,3,8,3,2,7,2,1,5,1,5,2,4,7,6,2,8,5,1,11,4,5,3,6,18,1,2,13,3,3,1,21,1,1,4,1,4,1,1,1,8,1,2,2,7,1,2,4,2,2,9,2,1,1,1,4,3,6,3,12,5,1,1,1,5,6,3,2, + 4,8,2,2,4,2,7,1,8,9,5,2,3,2,1,3,2,13,7,14,6,5,1,1,2,1,4,2,23,2,1,1,6,3,1,4,1,15,3,1,7,3,9,14,1,3,1,4,1,1,5,8,1,3,8,3,8,15,11,4,14,4,4,2,5,5, + 1,7,1,6,14,7,7,8,5,15,4,8,6,5,6,2,1,13,1,20,15,11,9,2,5,6,2,11,2,6,2,5,1,5,8,4,13,19,25,4,1,1,11,1,34,2,5,9,14,6,2,2,6,1,1,14,1,3,14,13,1,6, + 12,21,14,14,6,32,17,8,32,9,28,1,2,4,11,8,3,1,14,2,5,15,1,1,1,1,3,6,4,1,3,4,11,3,1,1,11,30,1,5,1,4,1,5,8,1,1,3,2,4,3,17,35,2,6,12,17,3,1,6,2, + 1,1,12,2,7,3,3,2,1,16,2,8,3,6,5,4,7,3,3,8,1,9,8,5,1,2,1,3,2,8,1,2,9,12,1,1,2,3,8,3,24,12,4,3,7,5,8,3,3,3,3,3,3,1,23,10,3,1,2,2,6,3,1,16,1,16, + 22,3,10,4,11,6,9,7,7,3,6,2,2,2,4,10,2,1,1,2,8,7,1,6,4,1,3,3,3,5,10,12,12,2,3,12,8,15,1,1,16,6,6,1,5,9,11,4,11,4,2,6,12,1,17,5,13,1,4,9,5,1,11, + 2,1,8,1,5,7,28,8,3,5,10,2,17,3,38,22,1,2,18,12,10,4,38,18,1,4,44,19,4,1,8,4,1,12,1,4,31,12,1,14,7,75,7,5,10,6,6,13,3,2,11,11,3,2,5,28,15,6,18, + 18,5,6,4,3,16,1,7,18,7,36,3,5,3,1,7,1,9,1,10,7,2,4,2,6,2,9,7,4,3,32,12,3,7,10,2,23,16,3,1,12,3,31,4,11,1,3,8,9,5,1,30,15,6,12,3,2,2,11,19,9, + 14,2,6,2,3,19,13,17,5,3,3,25,3,14,1,1,1,36,1,3,2,19,3,13,36,9,13,31,6,4,16,34,2,5,4,2,3,3,5,1,1,1,4,3,1,17,3,2,3,5,3,1,3,2,3,5,6,3,12,11,1,3, + 1,2,26,7,12,7,2,14,3,3,7,7,11,25,25,28,16,4,36,1,2,1,6,2,1,9,3,27,17,4,3,4,13,4,1,3,2,2,1,10,4,2,4,6,3,8,2,1,18,1,1,24,2,2,4,33,2,3,63,7,1,6, + 40,7,3,4,4,2,4,15,18,1,16,1,1,11,2,41,14,1,3,18,13,3,2,4,16,2,17,7,15,24,7,18,13,44,2,2,3,6,1,1,7,5,1,7,1,4,3,3,5,10,8,2,3,1,8,1,1,27,4,2,1, + 12,1,2,1,10,6,1,6,7,5,2,3,7,11,5,11,3,6,6,2,3,15,4,9,1,1,2,1,2,11,2,8,12,8,5,4,2,3,1,5,2,2,1,14,1,12,11,4,1,11,17,17,4,3,2,5,5,7,3,1,5,9,9,8, + 2,5,6,6,13,13,2,1,2,6,1,2,2,49,4,9,1,2,10,16,7,8,4,3,2,23,4,58,3,29,1,14,19,19,11,11,2,7,5,1,3,4,6,2,18,5,12,12,17,17,3,3,2,4,1,6,2,3,4,3,1, + 1,1,1,5,1,1,9,1,3,1,3,6,1,8,1,1,2,6,4,14,3,1,4,11,4,1,3,32,1,2,4,13,4,1,2,4,2,1,3,1,11,1,4,2,1,4,4,6,3,5,1,6,5,7,6,3,23,3,5,3,5,3,3,13,3,9,10, + 1,12,10,2,3,18,13,7,160,52,4,2,2,3,2,14,5,4,12,4,6,4,1,20,4,11,6,2,12,27,1,4,1,2,2,7,4,5,2,28,3,7,25,8,3,19,3,6,10,2,2,1,10,2,5,4,1,3,4,1,5, + 3,2,6,9,3,6,2,16,3,3,16,4,5,5,3,2,1,2,16,15,8,2,6,21,2,4,1,22,5,8,1,1,21,11,2,1,11,11,19,13,12,4,2,3,2,3,6,1,8,11,1,4,2,9,5,2,1,11,2,9,1,1,2, + 14,31,9,3,4,21,14,4,8,1,7,2,2,2,5,1,4,20,3,3,4,10,1,11,9,8,2,1,4,5,14,12,14,2,17,9,6,31,4,14,1,20,13,26,5,2,7,3,6,13,2,4,2,19,6,2,2,18,9,3,5, + 12,12,14,4,6,2,3,6,9,5,22,4,5,25,6,4,8,5,2,6,27,2,35,2,16,3,7,8,8,6,6,5,9,17,2,20,6,19,2,13,3,1,1,1,4,17,12,2,14,7,1,4,18,12,38,33,2,10,1,1, + 2,13,14,17,11,50,6,33,20,26,74,16,23,45,50,13,38,33,6,6,7,4,4,2,1,3,2,5,8,7,8,9,3,11,21,9,13,1,3,10,6,7,1,2,2,18,5,5,1,9,9,2,68,9,19,13,2,5, + 1,4,4,7,4,13,3,9,10,21,17,3,26,2,1,5,2,4,5,4,1,7,4,7,3,4,2,1,6,1,1,20,4,1,9,2,2,1,3,3,2,3,2,1,1,1,20,2,3,1,6,2,3,6,2,4,8,1,3,2,10,3,5,3,4,4, + 3,4,16,1,6,1,10,2,4,2,1,1,2,10,11,2,2,3,1,24,31,4,10,10,2,5,12,16,164,15,4,16,7,9,15,19,17,1,2,1,1,5,1,1,1,1,1,3,1,4,3,1,3,1,3,1,2,1,1,3,3,7, + 2,8,1,2,2,2,1,3,4,3,7,8,12,92,2,10,3,1,3,14,5,25,16,42,4,7,7,4,2,21,5,27,26,27,21,25,30,31,2,1,5,13,3,22,5,6,6,11,9,12,1,5,9,7,5,5,22,60,3,5, + 13,1,1,8,1,1,3,3,2,1,9,3,3,18,4,1,2,3,7,6,3,1,2,3,9,1,3,1,3,2,1,3,1,1,1,2,1,11,3,1,6,9,1,3,2,3,1,2,1,5,1,1,4,3,4,1,2,2,4,4,1,7,2,1,2,2,3,5,13, + 18,3,4,14,9,9,4,16,3,7,5,8,2,6,48,28,3,1,1,4,2,14,8,2,9,2,1,15,2,4,3,2,10,16,12,8,7,1,1,3,1,1,1,2,7,4,1,6,4,38,39,16,23,7,15,15,3,2,12,7,21, + 37,27,6,5,4,8,2,10,8,8,6,5,1,2,1,3,24,1,16,17,9,23,10,17,6,1,51,55,44,13,294,9,3,6,2,4,2,2,15,1,1,1,13,21,17,68,14,8,9,4,1,4,9,3,11,7,1,1,1, + 5,6,3,2,1,1,1,2,3,8,1,2,2,4,1,5,5,2,1,4,3,7,13,4,1,4,1,3,1,1,1,5,5,10,1,6,1,5,2,1,5,2,4,1,4,5,7,3,18,2,9,11,32,4,3,3,2,4,7,11,16,9,11,8,13,38, + 32,8,4,2,1,1,2,1,2,4,4,1,1,1,4,1,21,3,11,1,16,1,1,6,1,3,2,4,9,8,57,7,44,1,3,3,13,3,10,1,1,7,5,2,7,21,47,63,3,15,4,7,1,16,1,1,2,8,2,3,42,15,4, + 1,29,7,22,10,3,78,16,12,20,18,4,67,11,5,1,3,15,6,21,31,32,27,18,13,71,35,5,142,4,10,1,2,50,19,33,16,35,37,16,19,27,7,1,133,19,1,4,8,7,20,1,4, + 4,1,10,3,1,6,1,2,51,5,40,15,24,43,22928,11,1,13,154,70,3,1,1,7,4,10,1,2,1,1,2,1,2,1,2,2,1,1,2,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1, + 3,2,1,1,1,1,2,1,1, }; static ImWchar base_ranges[] = // not zero-terminated { 0x0020, 0x00FF, // Basic Latin + Latin Supplement 0x3000, 0x30FF, // CJK Symbols and Punctuations, Hiragana, Katakana 0x31F0, 0x31FF, // Katakana Phonetic Extensions - 0xFF00, 0xFFEF // Half-width characters + 0xFF00, 0xFFEF, // Half-width characters + 0xFFFD, 0xFFFD // Invalid }; static ImWchar full_ranges[IM_ARRAYSIZE(base_ranges) + IM_ARRAYSIZE(accumulative_offsets_from_0x4E00)*2 + 1] = { 0 }; if (!full_ranges[0]) @@ -2453,26 +3665,25 @@ void ImFontGlyphRangesBuilder::AddText(const char* text, const char* text_end) text += c_len; if (c_len == 0) break; - if (c < 0x10000) - AddChar((ImWchar)c); + AddChar((ImWchar)c); } } void ImFontGlyphRangesBuilder::AddRanges(const ImWchar* ranges) { for (; ranges[0]; ranges += 2) - for (ImWchar c = ranges[0]; c <= ranges[1]; c++) - AddChar(c); + for (unsigned int c = ranges[0]; c <= ranges[1] && c <= IM_UNICODE_CODEPOINT_MAX; c++) //-V560 + AddChar((ImWchar)c); } void ImFontGlyphRangesBuilder::BuildRanges(ImVector* out_ranges) { - int max_codepoint = 0x10000; - for (int n = 0; n < max_codepoint; n++) + const int max_codepoint = IM_UNICODE_CODEPOINT_MAX; + for (int n = 0; n <= max_codepoint; n++) if (GetBit(n)) { out_ranges->push_back((ImWchar)n); - while (n < max_codepoint - 1 && GetBit(n + 1)) + while (n < max_codepoint && GetBit(n + 1)) n++; out_ranges->push_back((ImWchar)n); } @@ -2485,19 +3696,8 @@ void ImFontGlyphRangesBuilder::BuildRanges(ImVector* out_ranges) ImFont::ImFont() { - FontSize = 0.0f; - FallbackAdvanceX = 0.0f; - FallbackChar = (ImWchar)'?'; - EllipsisChar = (ImWchar)-1; - DisplayOffset = ImVec2(0.0f, 0.0f); - FallbackGlyph = NULL; - ContainerAtlas = NULL; - ConfigData = NULL; - ConfigDataCount = 0; - DirtyLookupTables = false; + memset(this, 0, sizeof(*this)); Scale = 1.0f; - Ascent = Descent = 0.0f; - MetricsTotalSurface = 0; } ImFont::~ImFont() @@ -2517,6 +3717,15 @@ void ImFont::ClearOutputData() DirtyLookupTables = true; Ascent = Descent = 0.0f; MetricsTotalSurface = 0; + memset(Used8kPagesMap, 0, sizeof(Used8kPagesMap)); +} + +static ImWchar FindFirstExistingGlyph(ImFont* font, const ImWchar* candidate_chars, int candidate_chars_count) +{ + for (int n = 0; n < candidate_chars_count; n++) + if (font->FindGlyphNoFallback(candidate_chars[n]) != NULL) + return candidate_chars[n]; + return 0; } void ImFont::BuildLookupTable() @@ -2525,43 +3734,97 @@ void ImFont::BuildLookupTable() for (int i = 0; i != Glyphs.Size; i++) max_codepoint = ImMax(max_codepoint, (int)Glyphs[i].Codepoint); + // Build lookup table + IM_ASSERT(Glyphs.Size > 0 && "Font has not loaded glyph!"); IM_ASSERT(Glyphs.Size < 0xFFFF); // -1 is reserved IndexAdvanceX.clear(); IndexLookup.clear(); DirtyLookupTables = false; + memset(Used8kPagesMap, 0, sizeof(Used8kPagesMap)); GrowIndex(max_codepoint + 1); for (int i = 0; i < Glyphs.Size; i++) { int codepoint = (int)Glyphs[i].Codepoint; IndexAdvanceX[codepoint] = Glyphs[i].AdvanceX; - IndexLookup[codepoint] = (ImWchar)i; + IndexLookup[codepoint] = (ImU16)i; + + // Mark 4K page as used + const int page_n = codepoint / 8192; + Used8kPagesMap[page_n >> 3] |= 1 << (page_n & 7); } // Create a glyph to handle TAB // FIXME: Needs proper TAB handling but it needs to be contextualized (or we could arbitrary say that each string starts at "column 0" ?) if (FindGlyph((ImWchar)' ')) { - if (Glyphs.back().Codepoint != '\t') // So we can call this function multiple times + if (Glyphs.back().Codepoint != '\t') // So we can call this function multiple times (FIXME: Flaky) Glyphs.resize(Glyphs.Size + 1); ImFontGlyph& tab_glyph = Glyphs.back(); tab_glyph = *FindGlyph((ImWchar)' '); tab_glyph.Codepoint = '\t'; tab_glyph.AdvanceX *= IM_TABSIZE; IndexAdvanceX[(int)tab_glyph.Codepoint] = (float)tab_glyph.AdvanceX; - IndexLookup[(int)tab_glyph.Codepoint] = (ImWchar)(Glyphs.Size-1); + IndexLookup[(int)tab_glyph.Codepoint] = (ImU16)(Glyphs.Size - 1); } + // Mark special glyphs as not visible (note that AddGlyph already mark as non-visible glyphs with zero-size polygons) + if (ImFontGlyph* glyph = (ImFontGlyph*)(void*)FindGlyph((ImWchar)' ')) + glyph->Visible = false; + if (ImFontGlyph* glyph = (ImFontGlyph*)(void*)FindGlyph((ImWchar)'\t')) + glyph->Visible = false; + + // Setup Fallback character + const ImWchar fallback_chars[] = { (ImWchar)IM_UNICODE_CODEPOINT_INVALID, (ImWchar)'?', (ImWchar)' ' }; FallbackGlyph = FindGlyphNoFallback(FallbackChar); - FallbackAdvanceX = FallbackGlyph ? FallbackGlyph->AdvanceX : 0.0f; + if (FallbackGlyph == NULL) + { + FallbackChar = FindFirstExistingGlyph(this, fallback_chars, IM_ARRAYSIZE(fallback_chars)); + FallbackGlyph = FindGlyphNoFallback(FallbackChar); + if (FallbackGlyph == NULL) + { + FallbackGlyph = &Glyphs.back(); + FallbackChar = (ImWchar)FallbackGlyph->Codepoint; + } + } + FallbackAdvanceX = FallbackGlyph->AdvanceX; for (int i = 0; i < max_codepoint + 1; i++) if (IndexAdvanceX[i] < 0.0f) IndexAdvanceX[i] = FallbackAdvanceX; + + // Setup Ellipsis character. It is required for rendering elided text. We prefer using U+2026 (horizontal ellipsis). + // However some old fonts may contain ellipsis at U+0085. Here we auto-detect most suitable ellipsis character. + // FIXME: Note that 0x2026 is rarely included in our font ranges. Because of this we are more likely to use three individual dots. + const ImWchar ellipsis_chars[] = { Sources->EllipsisChar, (ImWchar)0x2026, (ImWchar)0x0085 }; + const ImWchar dots_chars[] = { (ImWchar)'.', (ImWchar)0xFF0E }; + if (EllipsisChar == 0) + EllipsisChar = FindFirstExistingGlyph(this, ellipsis_chars, IM_ARRAYSIZE(ellipsis_chars)); + const ImWchar dot_char = FindFirstExistingGlyph(this, dots_chars, IM_ARRAYSIZE(dots_chars)); + if (EllipsisChar != 0) + { + EllipsisCharCount = 1; + EllipsisWidth = EllipsisCharStep = FindGlyph(EllipsisChar)->X1; + } + else if (dot_char != 0) + { + const ImFontGlyph* dot_glyph = FindGlyph(dot_char); + EllipsisChar = dot_char; + EllipsisCharCount = 3; + EllipsisCharStep = (float)(int)(dot_glyph->X1 - dot_glyph->X0) + 1.0f; + EllipsisWidth = ImMax(dot_glyph->AdvanceX, dot_glyph->X0 + EllipsisCharStep * 3.0f - 1.0f); // FIXME: Slightly odd for normally mono-space fonts but since this is used for trailing contents. + } } -void ImFont::SetFallbackChar(ImWchar c) +// API is designed this way to avoid exposing the 8K page size +// e.g. use with IsGlyphRangeUnused(0, 255) +bool ImFont::IsGlyphRangeUnused(unsigned int c_begin, unsigned int c_last) { - FallbackChar = c; - BuildLookupTable(); + unsigned int page_begin = (c_begin / 8192); + unsigned int page_last = (c_last / 8192); + for (unsigned int page_n = page_begin; page_n <= page_last; page_n++) + if ((page_n >> 3) < sizeof(Used8kPagesMap)) + if (Used8kPagesMap[page_n >> 3] & (1 << (page_n & 7))) + return false; + return true; } void ImFont::GrowIndex(int new_size) @@ -2570,16 +3833,40 @@ void ImFont::GrowIndex(int new_size) if (new_size <= IndexLookup.Size) return; IndexAdvanceX.resize(new_size, -1.0f); - IndexLookup.resize(new_size, (ImWchar)-1); + IndexLookup.resize(new_size, (ImU16)-1); } // x0/y0/x1/y1 are offset from the character upper-left layout position, in pixels. Therefore x0/y0 are often fairly close to zero. // Not to be mistaken with texture coordinates, which are held by u0/v0/u1/v1 in normalized format (0.0..1.0 on each texture axis). -void ImFont::AddGlyph(ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x) +// 'src' is not necessarily == 'this->Sources' because multiple source fonts+configs can be used to build one target font. +void ImFont::AddGlyph(const ImFontConfig* src, ImWchar codepoint, float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float advance_x) { + if (src != NULL) + { + // Clamp & recenter if needed + const float advance_x_original = advance_x; + advance_x = ImClamp(advance_x, src->GlyphMinAdvanceX, src->GlyphMaxAdvanceX); + if (advance_x != advance_x_original) + { + float char_off_x = src->PixelSnapH ? ImTrunc((advance_x - advance_x_original) * 0.5f) : (advance_x - advance_x_original) * 0.5f; + x0 += char_off_x; + x1 += char_off_x; + } + + // Snap to pixel + if (src->PixelSnapH) + advance_x = IM_ROUND(advance_x); + + // Bake extra spacing + advance_x += src->GlyphExtraAdvanceX; + } + + int glyph_idx = Glyphs.Size; Glyphs.resize(Glyphs.Size + 1); - ImFontGlyph& glyph = Glyphs.back(); - glyph.Codepoint = (ImWchar)codepoint; + ImFontGlyph& glyph = Glyphs[glyph_idx]; + glyph.Codepoint = (unsigned int)codepoint; + glyph.Visible = (x0 != x1) && (y0 != y1); + glyph.Colored = false; glyph.X0 = x0; glyph.Y0 = y0; glyph.X1 = x1; @@ -2588,56 +3875,69 @@ void ImFont::AddGlyph(ImWchar codepoint, float x0, float y0, float x1, float y1, glyph.V0 = v0; glyph.U1 = u1; glyph.V1 = v1; - glyph.AdvanceX = advance_x + ConfigData->GlyphExtraSpacing.x; // Bake spacing into AdvanceX - - if (ConfigData->PixelSnapH) - glyph.AdvanceX = (float)(int)(glyph.AdvanceX + 0.5f); + glyph.AdvanceX = advance_x; + IM_ASSERT(Glyphs.Size < 0xFFFF); // IndexLookup[] hold 16-bit values and -1 is reserved. // Compute rough surface usage metrics (+1 to account for average padding, +0.99 to round) + // We use (U1-U0)*TexWidth instead of X1-X0 to account for oversampling. + float pad = ContainerAtlas->TexGlyphPadding + 0.99f; DirtyLookupTables = true; - MetricsTotalSurface += (int)((glyph.U1 - glyph.U0) * ContainerAtlas->TexWidth + 1.99f) * (int)((glyph.V1 - glyph.V0) * ContainerAtlas->TexHeight + 1.99f); + MetricsTotalSurface += (int)((glyph.U1 - glyph.U0) * ContainerAtlas->TexWidth + pad) * (int)((glyph.V1 - glyph.V0) * ContainerAtlas->TexHeight + pad); } void ImFont::AddRemapChar(ImWchar dst, ImWchar src, bool overwrite_dst) { IM_ASSERT(IndexLookup.Size > 0); // Currently this can only be called AFTER the font has been built, aka after calling ImFontAtlas::GetTexDataAs*() function. - int index_size = IndexLookup.Size; + unsigned int index_size = (unsigned int)IndexLookup.Size; - if (dst < index_size && IndexLookup.Data[dst] == (ImWchar)-1 && !overwrite_dst) // 'dst' already exists + if (dst < index_size && IndexLookup.Data[dst] == (ImU16)-1 && !overwrite_dst) // 'dst' already exists return; if (src >= index_size && dst >= index_size) // both 'dst' and 'src' don't exist -> no-op return; GrowIndex(dst + 1); - IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (ImWchar)-1; + IndexLookup[dst] = (src < index_size) ? IndexLookup.Data[src] : (ImU16)-1; IndexAdvanceX[dst] = (src < index_size) ? IndexAdvanceX.Data[src] : 1.0f; } -const ImFontGlyph* ImFont::FindGlyph(ImWchar c) const +// Find glyph, return fallback if missing +ImFontGlyph* ImFont::FindGlyph(ImWchar c) { - if (c >= IndexLookup.Size) + if (c >= (size_t)IndexLookup.Size) return FallbackGlyph; - const ImWchar i = IndexLookup.Data[c]; - if (i == (ImWchar)-1) + const ImU16 i = IndexLookup.Data[c]; + if (i == (ImU16)-1) return FallbackGlyph; return &Glyphs.Data[i]; } -const ImFontGlyph* ImFont::FindGlyphNoFallback(ImWchar c) const +ImFontGlyph* ImFont::FindGlyphNoFallback(ImWchar c) { - if (c >= IndexLookup.Size) + if (c >= (size_t)IndexLookup.Size) return NULL; - const ImWchar i = IndexLookup.Data[c]; - if (i == (ImWchar)-1) + const ImU16 i = IndexLookup.Data[c]; + if (i == (ImU16)-1) return NULL; return &Glyphs.Data[i]; } -const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) const +// Trim trailing space and find beginning of next line +static inline const char* CalcWordWrapNextLineStartA(const char* text, const char* text_end) { - // Simple word-wrapping for English, not full-featured. Please submit failing cases! - // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) + while (text < text_end && ImCharIsBlankA(*text)) + text++; + if (*text == '\n') + text++; + return text; +} +#define ImFontGetCharAdvanceX(_FONT, _CH) ((int)(_CH) < (_FONT)->IndexAdvanceX.Size ? (_FONT)->IndexAdvanceX.Data[_CH] : (_FONT)->FallbackAdvanceX) + +// Simple word-wrapping for English, not full-featured. Please submit failing cases! +// This will return the next location to wrap from. If no wrapping if necessary, this will fast-forward to e.g. text_end. +// FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.) +const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width) +{ // For references, possible wrap point marked with ^ // "aaa bbb, ccc,ddd. eee fff. ggg!" // ^ ^ ^ ^ ^__ ^ ^ @@ -2649,7 +3949,6 @@ const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const c // Cut words that cannot possibly fit within one line. // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish" - float line_width = 0.0f; float word_width = 0.0f; float blank_width = 0.0f; @@ -2660,6 +3959,7 @@ const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const c bool inside_word = true; const char* s = text; + IM_ASSERT(text_end != NULL); while (s < text_end) { unsigned int c = (unsigned int)*s; @@ -2668,8 +3968,6 @@ const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const c next_s = s + 1; else next_s = s + ImTextCharFromUtf8(&c, s, text_end); - if (c == 0) - break; if (c < 32) { @@ -2687,7 +3985,7 @@ const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const c } } - const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX.Data[c] : FallbackAdvanceX); + const float char_width = ImFontGetCharAdvanceX(this, c); if (ImCharIsBlankW(c)) { if (inside_word) @@ -2714,7 +4012,7 @@ const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const c } // Allow wrapping after punctuation. - inside_word = !(c == '.' || c == ',' || c == ';' || c == '!' || c == '?' || c == '\"'); + inside_word = (c != '.' && c != ',' && c != ';' && c != '!' && c != '?' && c != '\"'); } // We ignore blank width at the end of the line (they can be skipped) @@ -2729,18 +4027,22 @@ const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const c s = next_s; } + // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. + // +1 may not be a character start point in UTF-8 but it's ok because caller loops use (text >= word_wrap_eol). + if (s == text && text < text_end) + return s + 1; return s; } -ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) const +ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, const char* text_begin, const char* text_end, const char** remaining) { if (!text_end) - text_end = text_begin + strlen(text_begin); // FIXME-OPT: Need to avoid this. + text_end = text_begin + ImStrlen(text_begin); // FIXME-OPT: Need to avoid this. const float line_height = size; const float scale = size / FontSize; - ImVec2 text_size = ImVec2(0,0); + ImVec2 text_size = ImVec2(0, 0); float line_width = 0.0f; const bool word_wrap_enabled = (wrap_width > 0.0f); @@ -2753,11 +4055,7 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons { // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if (!word_wrap_eol) - { word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - line_width); - if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. - word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below - } if (s >= word_wrap_eol) { @@ -2766,13 +4064,7 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons text_size.y += line_height; line_width = 0.0f; word_wrap_eol = NULL; - - // Wrapping skips upcoming blanks - while (s < text_end) - { - const char c = *s; - if (ImCharIsBlankA(c)) { s++; } else if (c == '\n') { s++; break; } else { break; } - } + s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks continue; } } @@ -2781,15 +4073,9 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons const char* prev_s = s; unsigned int c = (unsigned int)*s; if (c < 0x80) - { s += 1; - } else - { s += ImTextCharFromUtf8(&c, s, text_end); - if (c == 0) // Malformed UTF-8? - break; - } if (c < 32) { @@ -2804,7 +4090,7 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons continue; } - const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX.Data[c] : FallbackAdvanceX) * scale; + const float char_width = ImFontGetCharAdvanceX(this, c) * scale; if (line_width + char_width >= max_width) { s = prev_s; @@ -2826,45 +4112,56 @@ ImVec2 ImFont::CalcTextSizeA(float size, float max_width, float wrap_width, cons return text_size; } -void ImFont::RenderChar(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, ImWchar c) const +// Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. +void ImFont::RenderChar(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, ImWchar c) { - if (c == ' ' || c == '\t' || c == '\n' || c == '\r') // Match behavior of RenderText(), those 4 codepoints are hard-coded. + const ImFontGlyph* glyph = FindGlyph(c); + if (!glyph || !glyph->Visible) return; - if (const ImFontGlyph* glyph = FindGlyph(c)) - { - float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; - pos.x = (float)(int)pos.x + DisplayOffset.x; - pos.y = (float)(int)pos.y + DisplayOffset.y; - draw_list->PrimReserve(6, 4); - draw_list->PrimRectUV(ImVec2(pos.x + glyph->X0 * scale, pos.y + glyph->Y0 * scale), ImVec2(pos.x + glyph->X1 * scale, pos.y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); - } + if (glyph->Colored) + col |= ~IM_COL32_A_MASK; + float scale = (size >= 0.0f) ? (size / FontSize) : 1.0f; + float x = IM_TRUNC(pos.x); + float y = IM_TRUNC(pos.y); + draw_list->PrimReserve(6, 4); + draw_list->PrimRectUV(ImVec2(x + glyph->X0 * scale, y + glyph->Y0 * scale), ImVec2(x + glyph->X1 * scale, y + glyph->Y1 * scale), ImVec2(glyph->U0, glyph->V0), ImVec2(glyph->U1, glyph->V1), col); } -void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) const +// Note: as with every ImDrawList drawing function, this expects that the font atlas texture is bound. +void ImFont::RenderText(ImDrawList* draw_list, float size, const ImVec2& pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) { - if (!text_end) - text_end = text_begin + strlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls. - // Align to be pixel perfect - pos.x = (float)(int)pos.x + DisplayOffset.x; - pos.y = (float)(int)pos.y + DisplayOffset.y; - float x = pos.x; - float y = pos.y; + float x = IM_TRUNC(pos.x); + float y = IM_TRUNC(pos.y); if (y > clip_rect.w) return; + if (!text_end) + text_end = text_begin + ImStrlen(text_begin); // ImGui:: functions generally already provides a valid text_end, so this is merely to handle direct calls. + const float scale = size / FontSize; const float line_height = FontSize * scale; + const float origin_x = x; const bool word_wrap_enabled = (wrap_width > 0.0f); - const char* word_wrap_eol = NULL; // Fast-forward to first visible line const char* s = text_begin; - if (y + line_height < clip_rect.y && !word_wrap_enabled) + if (y + line_height < clip_rect.y) while (y + line_height < clip_rect.y && s < text_end) { - s = (const char*)memchr(s, '\n', text_end - s); - s = s ? s + 1 : text_end; + const char* line_end = (const char*)ImMemchr(s, '\n', text_end - s); + if (word_wrap_enabled) + { + // FIXME-OPT: This is not optimal as do first do a search for \n before calling CalcWordWrapPositionA(). + // If the specs for CalcWordWrapPositionA() were reworked to optionally return on \n we could combine both. + // However it is still better than nothing performing the fast-forward! + s = CalcWordWrapPositionA(scale, s, line_end ? line_end : text_end, wrap_width); + s = CalcWordWrapNextLineStartA(s, text_end); + } + else + { + s = line_end ? line_end + 1 : text_end; + } y += line_height; } @@ -2876,7 +4173,7 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col float y_end = y; while (y_end < clip_rect.w && s_end < text_end) { - s_end = (const char*)memchr(s_end, '\n', text_end - s_end); + s_end = (const char*)ImMemchr(s_end, '\n', text_end - s_end); s_end = s_end ? s_end + 1 : text_end; y_end += line_height; } @@ -2890,10 +4187,12 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col const int idx_count_max = (int)(text_end - s) * 6; const int idx_expected_size = draw_list->IdxBuffer.Size + idx_count_max; draw_list->PrimReserve(idx_count_max, vtx_count_max); + ImDrawVert* vtx_write = draw_list->_VtxWritePtr; + ImDrawIdx* idx_write = draw_list->_IdxWritePtr; + unsigned int vtx_index = draw_list->_VtxCurrentIdx; - ImDrawVert* vtx_write = draw_list->_VtxWritePtr; - ImDrawIdx* idx_write = draw_list->_IdxWritePtr; - unsigned int vtx_current_idx = draw_list->_VtxCurrentIdx; + const ImU32 col_untinted = col | ~IM_COL32_A_MASK; + const char* word_wrap_eol = NULL; while (s < text_end) { @@ -2901,24 +4200,16 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col { // Calculate how far we can render. Requires two passes on the string data but keeps the code simple and not intrusive for what's essentially an uncommon feature. if (!word_wrap_eol) - { - word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - pos.x)); - if (word_wrap_eol == s) // Wrap_width is too small to fit anything. Force displaying 1 character to minimize the height discontinuity. - word_wrap_eol++; // +1 may not be a character start point in UTF-8 but it's ok because we use s >= word_wrap_eol below - } + word_wrap_eol = CalcWordWrapPositionA(scale, s, text_end, wrap_width - (x - origin_x)); if (s >= word_wrap_eol) { - x = pos.x; + x = origin_x; y += line_height; + if (y > clip_rect.w) + break; // break out of main loop word_wrap_eol = NULL; - - // Wrapping skips upcoming blanks - while (s < text_end) - { - const char c = *s; - if (ImCharIsBlankA(c)) { s++; } else if (c == '\n') { s++; break; } else { break; } - } + s = CalcWordWrapNextLineStartA(s, text_end); // Wrapping skips upcoming blanks continue; } } @@ -2926,21 +4217,15 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col // Decode and advance source unsigned int c = (unsigned int)*s; if (c < 0x80) - { s += 1; - } else - { s += ImTextCharFromUtf8(&c, s, text_end); - if (c == 0) // Malformed UTF-8? - break; - } if (c < 32) { if (c == '\n') { - x = pos.x; + x = origin_x; y += line_height; if (y > clip_rect.w) break; // break out of main loop @@ -2950,113 +4235,152 @@ void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col continue; } - float char_width = 0.0f; - if (const ImFontGlyph* glyph = FindGlyph((ImWchar)c)) + const ImFontGlyph* glyph = FindGlyph((ImWchar)c); + if (glyph == NULL) + continue; + + float char_width = glyph->AdvanceX * scale; + if (glyph->Visible) { - char_width = glyph->AdvanceX * scale; - - // Arbitrarily assume that both space and tabs are empty glyphs as an optimization - if (c != ' ' && c != '\t') + // We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w + float x1 = x + glyph->X0 * scale; + float x2 = x + glyph->X1 * scale; + float y1 = y + glyph->Y0 * scale; + float y2 = y + glyph->Y1 * scale; + if (x1 <= clip_rect.z && x2 >= clip_rect.x) { - // We don't do a second finer clipping test on the Y axis as we've already skipped anything before clip_rect.y and exit once we pass clip_rect.w - float x1 = x + glyph->X0 * scale; - float x2 = x + glyph->X1 * scale; - float y1 = y + glyph->Y0 * scale; - float y2 = y + glyph->Y1 * scale; - if (x1 <= clip_rect.z && x2 >= clip_rect.x) + // Render a character + float u1 = glyph->U0; + float v1 = glyph->V0; + float u2 = glyph->U1; + float v2 = glyph->V1; + + // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. + if (cpu_fine_clip) { - // Render a character - float u1 = glyph->U0; - float v1 = glyph->V0; - float u2 = glyph->U1; - float v2 = glyph->V1; - - // CPU side clipping used to fit text in their frame when the frame is too small. Only does clipping for axis aligned quads. - if (cpu_fine_clip) + if (x1 < clip_rect.x) { - if (x1 < clip_rect.x) - { - u1 = u1 + (1.0f - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1); - x1 = clip_rect.x; - } - if (y1 < clip_rect.y) - { - v1 = v1 + (1.0f - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1); - y1 = clip_rect.y; - } - if (x2 > clip_rect.z) - { - u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1); - x2 = clip_rect.z; - } - if (y2 > clip_rect.w) - { - v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1); - y2 = clip_rect.w; - } - if (y1 >= y2) - { - x += char_width; - continue; - } + u1 = u1 + (1.0f - (x2 - clip_rect.x) / (x2 - x1)) * (u2 - u1); + x1 = clip_rect.x; } - - // We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: + if (y1 < clip_rect.y) { - idx_write[0] = (ImDrawIdx)(vtx_current_idx); idx_write[1] = (ImDrawIdx)(vtx_current_idx+1); idx_write[2] = (ImDrawIdx)(vtx_current_idx+2); - idx_write[3] = (ImDrawIdx)(vtx_current_idx); idx_write[4] = (ImDrawIdx)(vtx_current_idx+2); idx_write[5] = (ImDrawIdx)(vtx_current_idx+3); - vtx_write[0].pos.x = x1; vtx_write[0].pos.y = y1; vtx_write[0].col = col; vtx_write[0].uv.x = u1; vtx_write[0].uv.y = v1; - vtx_write[1].pos.x = x2; vtx_write[1].pos.y = y1; vtx_write[1].col = col; vtx_write[1].uv.x = u2; vtx_write[1].uv.y = v1; - vtx_write[2].pos.x = x2; vtx_write[2].pos.y = y2; vtx_write[2].col = col; vtx_write[2].uv.x = u2; vtx_write[2].uv.y = v2; - vtx_write[3].pos.x = x1; vtx_write[3].pos.y = y2; vtx_write[3].col = col; vtx_write[3].uv.x = u1; vtx_write[3].uv.y = v2; - vtx_write += 4; - vtx_current_idx += 4; - idx_write += 6; + v1 = v1 + (1.0f - (y2 - clip_rect.y) / (y2 - y1)) * (v2 - v1); + y1 = clip_rect.y; } + if (x2 > clip_rect.z) + { + u2 = u1 + ((clip_rect.z - x1) / (x2 - x1)) * (u2 - u1); + x2 = clip_rect.z; + } + if (y2 > clip_rect.w) + { + v2 = v1 + ((clip_rect.w - y1) / (y2 - y1)) * (v2 - v1); + y2 = clip_rect.w; + } + if (y1 >= y2) + { + x += char_width; + continue; + } + } + + // Support for untinted glyphs + ImU32 glyph_col = glyph->Colored ? col_untinted : col; + + // We are NOT calling PrimRectUV() here because non-inlined causes too much overhead in a debug builds. Inlined here: + { + vtx_write[0].pos.x = x1; vtx_write[0].pos.y = y1; vtx_write[0].col = glyph_col; vtx_write[0].uv.x = u1; vtx_write[0].uv.y = v1; + vtx_write[1].pos.x = x2; vtx_write[1].pos.y = y1; vtx_write[1].col = glyph_col; vtx_write[1].uv.x = u2; vtx_write[1].uv.y = v1; + vtx_write[2].pos.x = x2; vtx_write[2].pos.y = y2; vtx_write[2].col = glyph_col; vtx_write[2].uv.x = u2; vtx_write[2].uv.y = v2; + vtx_write[3].pos.x = x1; vtx_write[3].pos.y = y2; vtx_write[3].col = glyph_col; vtx_write[3].uv.x = u1; vtx_write[3].uv.y = v2; + idx_write[0] = (ImDrawIdx)(vtx_index); idx_write[1] = (ImDrawIdx)(vtx_index + 1); idx_write[2] = (ImDrawIdx)(vtx_index + 2); + idx_write[3] = (ImDrawIdx)(vtx_index); idx_write[4] = (ImDrawIdx)(vtx_index + 2); idx_write[5] = (ImDrawIdx)(vtx_index + 3); + vtx_write += 4; + vtx_index += 4; + idx_write += 6; } } } - x += char_width; } - // Give back unused vertices - draw_list->VtxBuffer.resize((int)(vtx_write - draw_list->VtxBuffer.Data)); - draw_list->IdxBuffer.resize((int)(idx_write - draw_list->IdxBuffer.Data)); - draw_list->CmdBuffer[draw_list->CmdBuffer.Size-1].ElemCount -= (idx_expected_size - draw_list->IdxBuffer.Size); + // Give back unused vertices (clipped ones, blanks) ~ this is essentially a PrimUnreserve() action. + draw_list->VtxBuffer.Size = (int)(vtx_write - draw_list->VtxBuffer.Data); // Same as calling shrink() + draw_list->IdxBuffer.Size = (int)(idx_write - draw_list->IdxBuffer.Data); + draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ElemCount -= (idx_expected_size - draw_list->IdxBuffer.Size); draw_list->_VtxWritePtr = vtx_write; draw_list->_IdxWritePtr = idx_write; - draw_list->_VtxCurrentIdx = vtx_current_idx; + draw_list->_VtxCurrentIdx = vtx_index; } //----------------------------------------------------------------------------- -// [SECTION] Internal Render Helpers -// (progressively moved from imgui.cpp to here when they are redesigned to stop accessing ImGui global state) +// [SECTION] ImGui Internal Render Helpers //----------------------------------------------------------------------------- -// - RenderMouseCursor() +// Vaguely redesigned to stop accessing ImGui global state: +// - RenderArrow() +// - RenderBullet() +// - RenderCheckMark() +// - RenderArrowDockMenu() // - RenderArrowPointingAt() // - RenderRectFilledRangeH() +// - RenderRectFilledWithHole() +//----------------------------------------------------------------------------- +// Function in need of a redesign (legacy mess) +// - RenderColorRectWithAlphaCheckerboard() //----------------------------------------------------------------------------- -void ImGui::RenderMouseCursor(ImDrawList* draw_list, ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow) +// Render an arrow aimed to be aligned with text (p_min is a position in the same space text would be positioned). To e.g. denote expanded/collapsed state +void ImGui::RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale) { - if (mouse_cursor == ImGuiMouseCursor_None) - return; - IM_ASSERT(mouse_cursor > ImGuiMouseCursor_None && mouse_cursor < ImGuiMouseCursor_COUNT); + const float h = draw_list->_Data->FontSize * 1.00f; + float r = h * 0.40f * scale; + ImVec2 center = pos + ImVec2(h * 0.50f, h * 0.50f * scale); - ImFontAtlas* font_atlas = draw_list->_Data->Font->ContainerAtlas; - ImVec2 offset, size, uv[4]; - if (font_atlas->GetMouseCursorTexData(mouse_cursor, &offset, &size, &uv[0], &uv[2])) + ImVec2 a, b, c; + switch (dir) { - pos -= offset; - const ImTextureID tex_id = font_atlas->TexID; - draw_list->PushTextureID(tex_id); - draw_list->AddImage(tex_id, pos + ImVec2(1,0)*scale, pos + ImVec2(1,0)*scale + size*scale, uv[2], uv[3], col_shadow); - draw_list->AddImage(tex_id, pos + ImVec2(2,0)*scale, pos + ImVec2(2,0)*scale + size*scale, uv[2], uv[3], col_shadow); - draw_list->AddImage(tex_id, pos, pos + size*scale, uv[2], uv[3], col_border); - draw_list->AddImage(tex_id, pos, pos + size*scale, uv[0], uv[1], col_fill); - draw_list->PopTextureID(); + case ImGuiDir_Up: + case ImGuiDir_Down: + if (dir == ImGuiDir_Up) r = -r; + a = ImVec2(+0.000f, +0.750f) * r; + b = ImVec2(-0.866f, -0.750f) * r; + c = ImVec2(+0.866f, -0.750f) * r; + break; + case ImGuiDir_Left: + case ImGuiDir_Right: + if (dir == ImGuiDir_Left) r = -r; + a = ImVec2(+0.750f, +0.000f) * r; + b = ImVec2(-0.750f, +0.866f) * r; + c = ImVec2(-0.750f, -0.866f) * r; + break; + case ImGuiDir_None: + case ImGuiDir_COUNT: + IM_ASSERT(0); + break; } + draw_list->AddTriangleFilled(center + a, center + b, center + c, col); +} + +void ImGui::RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col) +{ + // FIXME-OPT: This should be baked in font. + draw_list->AddCircleFilled(pos, draw_list->_Data->FontSize * 0.20f, col, 8); +} + +void ImGui::RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz) +{ + float thickness = ImMax(sz / 5.0f, 1.0f); + sz -= thickness * 0.5f; + pos += ImVec2(thickness * 0.25f, thickness * 0.25f); + + float third = sz / 3.0f; + float bx = pos.x + third; + float by = pos.y + sz - third * 0.5f; + draw_list->PathLineTo(ImVec2(bx - third, by - third)); + draw_list->PathLineTo(ImVec2(bx, by)); + draw_list->PathLineTo(ImVec2(bx + third * 2.0f, by - third * 2.0f)); + draw_list->PathStroke(col, 0, thickness); } // Render an arrow. 'pos' is position of the arrow tip. half_sz.x is length from base to tip. half_sz.y is length on each side. @@ -3072,6 +4396,14 @@ void ImGui::RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half } } +// This is less wide than RenderArrow() and we use in dock nodes instead of the regular RenderArrow() to denote a change of functionality, +// and because the saved space means that the left-most tab label can stay at exactly the same position as the label of a loose window. +void ImGui::RenderArrowDockMenu(ImDrawList* draw_list, ImVec2 p_min, float sz, ImU32 col) +{ + draw_list->AddRectFilled(p_min + ImVec2(sz * 0.20f, sz * 0.15f), p_min + ImVec2(sz * 0.80f, sz * 0.30f), col); + RenderArrowPointingAt(draw_list, p_min + ImVec2(sz * 0.50f, sz * 0.85f), ImVec2(sz * 0.30f, sz * 0.40f), ImGuiDir_Down, col); +} + static inline float ImAcos01(float x) { if (x <= 0.0f) return IM_PI * 0.5f; @@ -3114,8 +4446,8 @@ void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, Im } else { - draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b, 3); // BL - draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e, 3); // TR + draw_list->PathArcTo(ImVec2(x0, p1.y - rounding), rounding, IM_PI - arc0_e, IM_PI - arc0_b); // BL + draw_list->PathArcTo(ImVec2(x0, p0.y + rounding), rounding, IM_PI + arc0_b, IM_PI + arc0_e); // TR } if (p1.x > rect.Min.x + rounding) { @@ -3134,13 +4466,81 @@ void ImGui::RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, Im } else { - draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b, 3); // TR - draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e, 3); // BR + draw_list->PathArcTo(ImVec2(x1, p0.y + rounding), rounding, -arc1_e, -arc1_b); // TR + draw_list->PathArcTo(ImVec2(x1, p1.y - rounding), rounding, +arc1_b, +arc1_e); // BR } } draw_list->PathFillConvex(col); } +void ImGui::RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding) +{ + const bool fill_L = (inner.Min.x > outer.Min.x); + const bool fill_R = (inner.Max.x < outer.Max.x); + const bool fill_U = (inner.Min.y > outer.Min.y); + const bool fill_D = (inner.Max.y < outer.Max.y); + if (fill_L) draw_list->AddRectFilled(ImVec2(outer.Min.x, inner.Min.y), ImVec2(inner.Min.x, inner.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_U ? 0 : ImDrawFlags_RoundCornersTopLeft) | (fill_D ? 0 : ImDrawFlags_RoundCornersBottomLeft)); + if (fill_R) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Min.y), ImVec2(outer.Max.x, inner.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_U ? 0 : ImDrawFlags_RoundCornersTopRight) | (fill_D ? 0 : ImDrawFlags_RoundCornersBottomRight)); + if (fill_U) draw_list->AddRectFilled(ImVec2(inner.Min.x, outer.Min.y), ImVec2(inner.Max.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_L ? 0 : ImDrawFlags_RoundCornersTopLeft) | (fill_R ? 0 : ImDrawFlags_RoundCornersTopRight)); + if (fill_D) draw_list->AddRectFilled(ImVec2(inner.Min.x, inner.Max.y), ImVec2(inner.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersNone | (fill_L ? 0 : ImDrawFlags_RoundCornersBottomLeft) | (fill_R ? 0 : ImDrawFlags_RoundCornersBottomRight)); + if (fill_L && fill_U) draw_list->AddRectFilled(ImVec2(outer.Min.x, outer.Min.y), ImVec2(inner.Min.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersTopLeft); + if (fill_R && fill_U) draw_list->AddRectFilled(ImVec2(inner.Max.x, outer.Min.y), ImVec2(outer.Max.x, inner.Min.y), col, rounding, ImDrawFlags_RoundCornersTopRight); + if (fill_L && fill_D) draw_list->AddRectFilled(ImVec2(outer.Min.x, inner.Max.y), ImVec2(inner.Min.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomLeft); + if (fill_R && fill_D) draw_list->AddRectFilled(ImVec2(inner.Max.x, inner.Max.y), ImVec2(outer.Max.x, outer.Max.y), col, rounding, ImDrawFlags_RoundCornersBottomRight); +} + +ImDrawFlags ImGui::CalcRoundingFlagsForRectInRect(const ImRect& r_in, const ImRect& r_outer, float threshold) +{ + bool round_l = r_in.Min.x <= r_outer.Min.x + threshold; + bool round_r = r_in.Max.x >= r_outer.Max.x - threshold; + bool round_t = r_in.Min.y <= r_outer.Min.y + threshold; + bool round_b = r_in.Max.y >= r_outer.Max.y - threshold; + return ImDrawFlags_RoundCornersNone + | ((round_t && round_l) ? ImDrawFlags_RoundCornersTopLeft : 0) | ((round_t && round_r) ? ImDrawFlags_RoundCornersTopRight : 0) + | ((round_b && round_l) ? ImDrawFlags_RoundCornersBottomLeft : 0) | ((round_b && round_r) ? ImDrawFlags_RoundCornersBottomRight : 0); +} + +// Helper for ColorPicker4() +// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. +// Spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding altogether. +// FIXME: uses ImGui::GetColorU32 +void ImGui::RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, ImDrawFlags flags) +{ + if ((flags & ImDrawFlags_RoundCornersMask_) == 0) + flags = ImDrawFlags_RoundCornersDefault_; + if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) + { + ImU32 col_bg1 = GetColorU32(ImAlphaBlendColors(IM_COL32(204, 204, 204, 255), col)); + ImU32 col_bg2 = GetColorU32(ImAlphaBlendColors(IM_COL32(128, 128, 128, 255), col)); + draw_list->AddRectFilled(p_min, p_max, col_bg1, rounding, flags); + + int yi = 0; + for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) + { + float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); + if (y2 <= y1) + continue; + for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) + { + float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); + if (x2 <= x1) + continue; + ImDrawFlags cell_flags = ImDrawFlags_RoundCornersNone; + if (y1 <= p_min.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersTopLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersTopRight; } + if (y2 >= p_max.y) { if (x1 <= p_min.x) cell_flags |= ImDrawFlags_RoundCornersBottomLeft; if (x2 >= p_max.x) cell_flags |= ImDrawFlags_RoundCornersBottomRight; } + + // Combine flags + cell_flags = (flags == ImDrawFlags_RoundCornersNone || cell_flags == ImDrawFlags_RoundCornersNone) ? ImDrawFlags_RoundCornersNone : (cell_flags & flags); + draw_list->AddRectFilled(ImVec2(x1, y1), ImVec2(x2, y2), col_bg2, rounding, cell_flags); + } + } + } + else + { + draw_list->AddRectFilled(p_min, p_max, col, rounding, flags); + } +} + //----------------------------------------------------------------------------- // [SECTION] Decompression code //----------------------------------------------------------------------------- @@ -3201,9 +4601,9 @@ static unsigned int stb_adler32(unsigned int adler32, unsigned char *buffer, uns { const unsigned long ADLER_MOD = 65521; unsigned long s1 = adler32 & 0xffff, s2 = adler32 >> 16; - unsigned long blocklen, i; + unsigned long blocklen = buflen % 5552; - blocklen = buflen % 5552; + unsigned long i; while (buflen) { for (i=0; i + 7 < blocklen; i += 8) { s1 += buffer[0], s2 += s1; @@ -3230,10 +4630,9 @@ static unsigned int stb_adler32(unsigned int adler32, unsigned char *buffer, uns static unsigned int stb_decompress(unsigned char *output, const unsigned char *i, unsigned int /*length*/) { - unsigned int olen; if (stb__in4(0) != 0x57bC0000) return 0; if (stb__in4(4) != 0) return 0; // error! stream is > 4GB - olen = stb_decompress_length(i); + const unsigned int olen = stb_decompress_length(i); stb__barrier_in_b = i; stb__barrier_out_e = output + olen; stb__barrier_out_b = output; @@ -3266,102 +4665,190 @@ static unsigned int stb_decompress(unsigned char *output, const unsigned char *i //----------------------------------------------------------------------------- // ProggyClean.ttf // Copyright (c) 2004, 2005 Tristan Grimmer -// MIT license (see License.txt in http://www.upperbounds.net/download/ProggyClean.ttf.zip) -// Download and more information at http://upperbounds.net +// MIT license (see License.txt in http://www.proggyfonts.net/index.php?menu=download) +// Download and more information at http://www.proggyfonts.net or http://upperboundsinteractive.com/fonts.php //----------------------------------------------------------------------------- -// File: 'ProggyClean.ttf' (41208 bytes) -// Exported using misc/fonts/binary_to_compressed_c.cpp (with compression + base85 string encoding). -// The purpose of encoding as base85 instead of "0x00,0x01,..." style is only save on _source code_ size. -//----------------------------------------------------------------------------- -static const char proggy_clean_ttf_compressed_data_base85[11980+1] = - "7])#######hV0qs'/###[),##/l:$#Q6>##5[n42>c-TH`->>#/e>11NNV=Bv(*:.F?uu#(gRU.o0XGH`$vhLG1hxt9?W`#,5LsCp#-i>.r$<$6pD>Lb';9Crc6tgXmKVeU2cD4Eo3R/" - "2*>]b(MC;$jPfY.;h^`IWM9Qo#t'X#(v#Y9w0#1D$CIf;W'#pWUPXOuxXuU(H9M(1=Ke$$'5F%)]0^#0X@U.a$FBjVQTSDgEKnIS7EM9>ZY9w0#L;>>#Mx&4Mvt//L[MkA#W@lK.N'[0#7RL_&#w+F%HtG9M#XL`N&.,GM4Pg;--VsM.M0rJfLH2eTM`*oJMHRC`N" - "kfimM2J,W-jXS:)r0wK#@Fge$U>`w'N7G#$#fB#$E^$#:9:hk+eOe--6x)F7*E%?76%^GMHePW-Z5l'&GiF#$956:rS?dA#fiK:)Yr+`�j@'DbG&#^$PG.Ll+DNa&VZ>1i%h1S9u5o@YaaW$e+bROPOpxTO7Stwi1::iB1q)C_=dV26J;2,]7op$]uQr@_V7$q^%lQwtuHY]=DX,n3L#0PHDO4f9>dC@O>HBuKPpP*E,N+b3L#lpR/MrTEH.IAQk.a>D[.e;mc." - "x]Ip.PH^'/aqUO/$1WxLoW0[iLAw=4h(9.`G" - "CRUxHPeR`5Mjol(dUWxZa(>STrPkrJiWx`5U7F#.g*jrohGg`cg:lSTvEY/EV_7H4Q9[Z%cnv;JQYZ5q.l7Zeas:HOIZOB?Ggv:[7MI2k).'2($5FNP&EQ(,)" - "U]W]+fh18.vsai00);D3@4ku5P?DP8aJt+;qUM]=+b'8@;mViBKx0DE[-auGl8:PJ&Dj+M6OC]O^((##]`0i)drT;-7X`=-H3[igUnPG-NZlo.#k@h#=Ork$m>a>$-?Tm$UV(?#P6YY#" - "'/###xe7q.73rI3*pP/$1>s9)W,JrM7SN]'/4C#v$U`0#V.[0>xQsH$fEmPMgY2u7Kh(G%siIfLSoS+MK2eTM$=5,M8p`A.;_R%#u[K#$x4AG8.kK/HSB==-'Ie/QTtG?-.*^N-4B/ZM" - "_3YlQC7(p7q)&](`6_c)$/*JL(L-^(]$wIM`dPtOdGA,U3:w2M-0+WomX2u7lqM2iEumMTcsF?-aT=Z-97UEnXglEn1K-bnEO`gu" - "Ft(c%=;Am_Qs@jLooI&NX;]0#j4#F14;gl8-GQpgwhrq8'=l_f-b49'UOqkLu7-##oDY2L(te+Mch&gLYtJ,MEtJfLh'x'M=$CS-ZZ%P]8bZ>#S?YY#%Q&q'3^Fw&?D)UDNrocM3A76/" - "/oL?#h7gl85[qW/NDOk%16ij;+:1a'iNIdb-ou8.P*w,v5#EI$TWS>Pot-R*H'-SEpA:g)f+O$%%`kA#G=8RMmG1&O`>to8bC]T&$,n.LoO>29sp3dt-52U%VM#q7'DHpg+#Z9%H[Ket`e;)f#Km8&+DC$I46>#Kr]]u-[=99tts1.qb#q72g1WJO81q+eN'03'eM>&1XxY-caEnO" - "j%2n8)),?ILR5^.Ibn<-X-Mq7[a82Lq:F&#ce+S9wsCK*x`569E8ew'He]h:sI[2LM$[guka3ZRd6:t%IG:;$%YiJ:Nq=?eAw;/:nnDq0(CYcMpG)qLN4$##&J-XTt,%OVU4)S1+R-#dg0/Nn?Ku1^0f$B*P:Rowwm-`0PKjYDDM'3]d39VZHEl4,.j']Pk-M.h^&:0FACm$maq-&sgw0t7/6(^xtk%" - "LuH88Fj-ekm>GA#_>568x6(OFRl-IZp`&b,_P'$MhLbxfc$mj`,O;&%W2m`Zh:/)Uetw:aJ%]K9h:TcF]u_-Sj9,VK3M.*'&0D[Ca]J9gp8,kAW]" - "%(?A%R$f<->Zts'^kn=-^@c4%-pY6qI%J%1IGxfLU9CP8cbPlXv);C=b),<2mOvP8up,UVf3839acAWAW-W?#ao/^#%KYo8fRULNd2.>%m]UK:n%r$'sw]J;5pAoO_#2mO3n,'=H5(et" - "Hg*`+RLgv>=4U8guD$I%D:W>-r5V*%j*W:Kvej.Lp$'?;++O'>()jLR-^u68PHm8ZFWe+ej8h:9r6L*0//c&iH&R8pRbA#Kjm%upV1g:" - "a_#Ur7FuA#(tRh#.Y5K+@?3<-8m0$PEn;J:rh6?I6uG<-`wMU'ircp0LaE_OtlMb&1#6T.#FDKu#1Lw%u%+GM+X'e?YLfjM[VO0MbuFp7;>Q&#WIo)0@F%q7c#4XAXN-U&VBpqB>0ie&jhZ[?iLR@@_AvA-iQC(=ksRZRVp7`.=+NpBC%rh&3]R:8XDmE5^V8O(x<-+k?'(^](H.aREZSi,#1:[IXaZFOm<-ui#qUq2$##Ri;u75OK#(RtaW-K-F`S+cF]uN`-KMQ%rP/Xri.LRcB##=YL3BgM/3M" - "D?@f&1'BW-)Ju#bmmWCMkk&#TR`C,5d>g)F;t,4:@_l8G/5h4vUd%&%950:VXD'QdWoY-F$BtUwmfe$YqL'8(PWX(" - "P?^@Po3$##`MSs?DWBZ/S>+4%>fX,VWv/w'KD`LP5IbH;rTV>n3cEK8U#bX]l-/V+^lj3;vlMb&[5YQ8#pekX9JP3XUC72L,,?+Ni&co7ApnO*5NK,((W-i:$,kp'UDAO(G0Sq7MVjJs" - "bIu)'Z,*[>br5fX^:FPAWr-m2KgLQ_nN6'8uTGT5g)uLv:873UpTLgH+#FgpH'_o1780Ph8KmxQJ8#H72L4@768@Tm&Q" - "h4CB/5OvmA&,Q&QbUoi$a_%3M01H)4x7I^&KQVgtFnV+;[Pc>[m4k//,]1?#`VY[Jr*3&&slRfLiVZJ:]?=K3Sw=[$=uRB?3xk48@aege0jT6'N#(q%.O=?2S]u*(m<-" - "V8J'(1)G][68hW$5'q[GC&5j`TE?m'esFGNRM)j,ffZ?-qx8;->g4t*:CIP/[Qap7/9'#(1sao7w-.qNUdkJ)tCF&#B^;xGvn2r9FEPFFFcL@.iFNkTve$m%#QvQS8U@)2Z+3K:AKM5i" - "sZ88+dKQ)W6>J%CL`.d*(B`-n8D9oK-XV1q['-5k'cAZ69e;D_?$ZPP&s^+7])$*$#@QYi9,5P r+$%CE=68>K8r0=dSC%%(@p7" - ".m7jilQ02'0-VWAgTlGW'b)Tq7VT9q^*^$$.:&N@@" - "$&)WHtPm*5_rO0&e%K&#-30j(E4#'Zb.o/(Tpm$>K'f@[PvFl,hfINTNU6u'0pao7%XUp9]5.>%h`8_=VYbxuel.NTSsJfLacFu3B'lQSu/m6-Oqem8T+oE--$0a/k]uj9EwsG>%veR*" - "hv^BFpQj:K'#SJ,sB-'#](j.Lg92rTw-*n%@/;39rrJF,l#qV%OrtBeC6/,;qB3ebNW[?,Hqj2L.1NP&GjUR=1D8QaS3Up&@*9wP?+lo7b?@%'k4`p0Z$22%K3+iCZj?XJN4Nm&+YF]u" - "@-W$U%VEQ/,,>>#)D#%8cY#YZ?=,`Wdxu/ae&#" - "w6)R89tI#6@s'(6Bf7a&?S=^ZI_kS&ai`&=tE72L_D,;^R)7[$so8lKN%5/$(vdfq7+ebA#" - "u1p]ovUKW&Y%q]'>$1@-[xfn$7ZTp7mM,G,Ko7a&Gu%G[RMxJs[0MM%wci.LFDK)(%:_i2B5CsR8&9Z&#=mPEnm0f`<&c)QL5uJ#%u%lJj+D-r;BoFDoS97h5g)E#o:&S4weDF,9^Hoe`h*L+_a*NrLW-1pG_&2UdB8" - "6e%B/:=>)N4xeW.*wft-;$'58-ESqr#U`'6AQ]m&6/`Z>#S?YY#Vc;r7U2&326d=w&H####?TZ`*4?&.MK?LP8Vxg>$[QXc%QJv92.(Db*B)gb*BM9dM*hJMAo*c&#" - "b0v=Pjer]$gG&JXDf->'StvU7505l9$AFvgYRI^&<^b68?j#q9QX4SM'RO#&sL1IM.rJfLUAj221]d##DW=m83u5;'bYx,*Sl0hL(W;;$doB&O/TQ:(Z^xBdLjLV#*8U_72Lh+2Q8Cj0i:6hp&$C/:p(HK>T8Y[gHQ4`4)'$Ab(Nof%V'8hL&#SfD07&6D@M.*J:;$-rv29'M]8qMv-tLp,'886iaC=Hb*YJoKJ,(j%K=H`K.v9HggqBIiZu'QvBT.#=)0ukruV&.)3=(^1`o*Pj4<-#MJ+gLq9-##@HuZPN0]u:h7.T..G:;$/Usj(T7`Q8tT72LnYl<-qx8;-HV7Q-&Xdx%1a,hC=0u+HlsV>nuIQL-5" - "_>@kXQtMacfD.m-VAb8;IReM3$wf0''hra*so568'Ip&vRs849'MRYSp%:t:h5qSgwpEr$B>Q,;s(C#$)`svQuF$##-D,##,g68@2[T;.XSdN9Qe)rpt._K-#5wF)sP'##p#C0c%-Gb%" - "hd+<-j'Ai*x&&HMkT]C'OSl##5RG[JXaHN;d'uA#x._U;.`PU@(Z3dt4r152@:v,'R.Sj'w#0<-;kPI)FfJ&#AYJ&#//)>-k=m=*XnK$>=)72L]0I%>.G690a:$##<,);?;72#?x9+d;" - "^V'9;jY@;)br#q^YQpx:X#Te$Z^'=-=bGhLf:D6&bNwZ9-ZD#n^9HhLMr5G;']d&6'wYmTFmLq9wI>P(9mI[>kC-ekLC/R&CH+s'B;K-M6$EB%is00:" - "+A4[7xks.LrNk0&E)wILYF@2L'0Nb$+pv<(2.768/FrY&h$^3i&@+G%JT'<-,v`3;_)I9M^AE]CN?Cl2AZg+%4iTpT3$U4O]GKx'm9)b@p7YsvK3w^YR-" - "CdQ*:Ir<($u&)#(&?L9Rg3H)4fiEp^iI9O8KnTj,]H?D*r7'M;PwZ9K0E^k&-cpI;.p/6_vwoFMV<->#%Xi.LxVnrU(4&8/P+:hLSKj$#U%]49t'I:rgMi'FL@a:0Y-uA[39',(vbma*" - "hU%<-SRF`Tt:542R_VV$p@[p8DV[A,?1839FWdFTi1O*H&#(AL8[_P%.M>v^-))qOT*F5Cq0`Ye%+$B6i:7@0IXSsDiWP,##P`%/L-" - "S(qw%sf/@%#B6;/U7K]uZbi^Oc^2n%t<)'mEVE''n`WnJra$^TKvX5B>;_aSEK',(hwa0:i4G?.Bci.(X[?b*($,=-n<.Q%`(X=?+@Am*Js0&=3bh8K]mL69=Lb,OcZV/);TTm8VI;?%OtJ<(b4mq7M6:u?KRdFl*:xP?Yb.5)%w_I?7uk5JC+FS(m#i'k.'a0i)9<7b'fs'59hq$*5Uhv##pi^8+hIEBF`nvo`;'l0.^S1<-wUK2/Coh58KKhLj" - "M=SO*rfO`+qC`W-On.=AJ56>>i2@2LH6A:&5q`?9I3@@'04&p2/LVa*T-4<-i3;M9UvZd+N7>b*eIwg:CC)c<>nO&#$(>.Z-I&J(Q0Hd5Q%7Co-b`-cP)hI;*_F]u`Rb[.j8_Q/<&>uu+VsH$sM9TA%?)(vmJ80),P7E>)tjD%2L=-t#fK[%`v=Q8WlA2);Sa" - ">gXm8YB`1d@K#n]76-a$U,mF%Ul:#/'xoFM9QX-$.QN'>" - "[%$Z$uF6pA6Ki2O5:8w*vP1<-1`[G,)-m#>0`P&#eb#.3i)rtB61(o'$?X3B2Qft^ae_5tKL9MUe9b*sLEQ95C&`=G?@Mj=wh*'3E>=-<)Gt*Iw)'QG:`@I" - "wOf7&]1i'S01B+Ev/Nac#9S;=;YQpg_6U`*kVY39xK,[/6Aj7:'1Bm-_1EYfa1+o&o4hp7KN_Q(OlIo@S%;jVdn0'1h19w,WQhLI)3S#f$2(eb,jr*b;3Vw]*7NH%$c4Vs,eD9>XW8?N]o+(*pgC%/72LV-uW%iewS8W6m2rtCpo'RS1R84=@paTKt)>=%&1[)*vp'u+x,VrwN;&]kuO9JDbg=pO$J*.jVe;u'm0dr9l,<*wMK*Oe=g8lV_KEBFkO'oU]^=[-792#ok,)" - "i]lR8qQ2oA8wcRCZ^7w/Njh;?.stX?Q1>S1q4Bn$)K1<-rGdO'$Wr.Lc.CG)$/*JL4tNR/,SVO3,aUw'DJN:)Ss;wGn9A32ijw%FL+Z0Fn.U9;reSq)bmI32U==5ALuG&#Vf1398/pVo" - "1*c-(aY168o<`JsSbk-,1N;$>0:OUas(3:8Z972LSfF8eb=c-;>SPw7.6hn3m`9^Xkn(r.qS[0;T%&Qc=+STRxX'q1BNk3&*eu2;&8q$&x>Q#Q7^Tf+6<(d%ZVmj2bDi%.3L2n+4W'$P" - "iDDG)g,r%+?,$@?uou5tSe2aN_AQU*'IAO" - "URQ##V^Fv-XFbGM7Fl(N<3DhLGF%q.1rC$#:T__&Pi68%0xi_&[qFJ(77j_&JWoF.V735&T,[R*:xFR*K5>>#`bW-?4Ne_&6Ne_&6Ne_&n`kr-#GJcM6X;uM6X;uM(.a..^2TkL%oR(#" - ";u.T%fAr%4tJ8&><1=GHZ_+m9/#H1F^R#SC#*N=BA9(D?v[UiFY>>^8p,KKF.W]L29uLkLlu/+4T" - "w$)F./^n3+rlo+DB;5sIYGNk+i1t-69Jg--0pao7Sm#K)pdHW&;LuDNH@H>#/X-TI(;P>#,Gc>#0Su>#4`1?#8lC?#xL$#B.`$#F:r$#JF.%#NR@%#R_R%#Vke%#Zww%#_-4^Rh%Sflr-k'MS.o?.5/sWel/wpEM0%3'/1)K^f1-d>G21&v(35>V`39V7A4=onx4" - "A1OY5EI0;6Ibgr6M$HS7Q<)58C5w,;WoA*#[%T*#`1g*#d=#+#hI5+#lUG+#pbY+#tnl+#x$),#&1;,#*=M,#.I`,#2Ur,#6b.-#;w[H#iQtA#m^0B#qjBB#uvTB##-hB#'9$C#+E6C#" - "/QHC#3^ZC#7jmC#;v)D#?,)4kMYD4lVu`4m`:&5niUA5@(A5BA1]PBB:xlBCC=2CDLXMCEUtiCf&0g2'tN?PGT4CPGT4CPGT4CPGT4CPGT4CPGT4CPGT4CP" - "GT4CPGT4CPGT4CPGT4CPGT4CPGT4CP-qekC`.9kEg^+F$kwViFJTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5KTB&5o,^<-28ZI'O?;xp" - "O?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xpO?;xp;7q-#lLYI:xvD=#"; -static const char* GetDefaultCompressedFontDataTTFBase85() +#ifndef IMGUI_DISABLE_DEFAULT_FONT + +// File: 'ProggyClean.ttf' (41208 bytes) +// Exported using binary_to_compressed_c.exe -u8 "ProggyClean.ttf" proggy_clean_ttf +static const unsigned int proggy_clean_ttf_compressed_size = 9583; +static const unsigned char proggy_clean_ttf_compressed_data[9583] = { - return proggy_clean_ttf_compressed_data_base85; + 87,188,0,0,0,0,0,0,0,0,160,248,0,4,0,0,55,0,1,0,0,0,12,0,128,0,3,0,64,79,83,47,50,136,235,116,144,0,0,1,72,130,21,44,78,99,109,97,112,2,18,35,117,0,0,3,160,130,19,36,82,99,118,116, + 32,130,23,130,2,33,4,252,130,4,56,2,103,108,121,102,18,175,137,86,0,0,7,4,0,0,146,128,104,101,97,100,215,145,102,211,130,27,32,204,130,3,33,54,104,130,16,39,8,66,1,195,0,0,1,4,130, + 15,59,36,104,109,116,120,138,0,126,128,0,0,1,152,0,0,2,6,108,111,99,97,140,115,176,216,0,0,5,130,30,41,2,4,109,97,120,112,1,174,0,218,130,31,32,40,130,16,44,32,110,97,109,101,37,89, + 187,150,0,0,153,132,130,19,44,158,112,111,115,116,166,172,131,239,0,0,155,36,130,51,44,210,112,114,101,112,105,2,1,18,0,0,4,244,130,47,32,8,132,203,46,1,0,0,60,85,233,213,95,15,60, + 245,0,3,8,0,131,0,34,183,103,119,130,63,43,0,0,189,146,166,215,0,0,254,128,3,128,131,111,130,241,33,2,0,133,0,32,1,130,65,38,192,254,64,0,0,3,128,131,16,130,5,32,1,131,7,138,3,33,2, + 0,130,17,36,1,1,0,144,0,130,121,130,23,38,2,0,8,0,64,0,10,130,9,32,118,130,9,130,6,32,0,130,59,33,1,144,131,200,35,2,188,2,138,130,16,32,143,133,7,37,1,197,0,50,2,0,131,0,33,4,9,131, + 5,145,3,43,65,108,116,115,0,64,0,0,32,172,8,0,131,0,35,5,0,1,128,131,77,131,3,33,3,128,191,1,33,1,128,130,184,35,0,0,128,0,130,3,131,11,32,1,130,7,33,0,128,131,1,32,1,136,9,32,0,132, + 15,135,5,32,1,131,13,135,27,144,35,32,1,149,25,131,21,32,0,130,0,32,128,132,103,130,35,132,39,32,0,136,45,136,97,133,17,130,5,33,0,0,136,19,34,0,128,1,133,13,133,5,32,128,130,15,132, + 131,32,3,130,5,32,3,132,27,144,71,32,0,133,27,130,29,130,31,136,29,131,63,131,3,65,63,5,132,5,132,205,130,9,33,0,0,131,9,137,119,32,3,132,19,138,243,130,55,32,1,132,35,135,19,131,201, + 136,11,132,143,137,13,130,41,32,0,131,3,144,35,33,128,0,135,1,131,223,131,3,141,17,134,13,136,63,134,15,136,53,143,15,130,96,33,0,3,131,4,130,3,34,28,0,1,130,5,34,0,0,76,130,17,131, + 9,36,28,0,4,0,48,130,17,46,8,0,8,0,2,0,0,0,127,0,255,32,172,255,255,130,9,34,0,0,129,132,9,130,102,33,223,213,134,53,132,22,33,1,6,132,6,64,4,215,32,129,165,216,39,177,0,1,141,184, + 1,255,133,134,45,33,198,0,193,1,8,190,244,1,28,1,158,2,20,2,136,2,252,3,20,3,88,3,156,3,222,4,20,4,50,4,80,4,98,4,162,5,22,5,102,5,188,6,18,6,116,6,214,7,56,7,126,7,236,8,78,8,108, + 8,150,8,208,9,16,9,74,9,136,10,22,10,128,11,4,11,86,11,200,12,46,12,130,12,234,13,94,13,164,13,234,14,80,14,150,15,40,15,176,16,18,16,116,16,224,17,82,17,182,18,4,18,110,18,196,19, + 76,19,172,19,246,20,88,20,174,20,234,21,64,21,128,21,166,21,184,22,18,22,126,22,198,23,52,23,142,23,224,24,86,24,186,24,238,25,54,25,150,25,212,26,72,26,156,26,240,27,92,27,200,28, + 4,28,76,28,150,28,234,29,42,29,146,29,210,30,64,30,142,30,224,31,36,31,118,31,166,31,166,32,16,130,1,52,46,32,138,32,178,32,200,33,20,33,116,33,152,33,238,34,98,34,134,35,12,130,1, + 33,128,35,131,1,60,152,35,176,35,216,36,0,36,74,36,104,36,144,36,174,37,6,37,96,37,130,37,248,37,248,38,88,38,170,130,1,8,190,216,39,64,39,154,40,10,40,104,40,168,41,14,41,32,41,184, + 41,248,42,54,42,96,42,96,43,2,43,42,43,94,43,172,43,230,44,32,44,52,44,154,45,40,45,92,45,120,45,170,45,232,46,38,46,166,47,38,47,182,47,244,48,94,48,200,49,62,49,180,50,30,50,158, + 51,30,51,130,51,238,52,92,52,206,53,58,53,134,53,212,54,38,54,114,54,230,55,118,55,216,56,58,56,166,57,18,57,116,57,174,58,46,58,154,59,6,59,124,59,232,60,58,60,150,61,34,61,134,61, + 236,62,86,62,198,63,42,63,154,64,18,64,106,64,208,65,54,65,162,66,8,66,64,66,122,66,184,66,240,67,98,67,204,68,42,68,138,68,238,69,88,69,182,69,226,70,84,70,180,71,20,71,122,71,218, + 72,84,72,198,73,64,0,36,70,21,8,8,77,3,0,7,0,11,0,15,0,19,0,23,0,27,0,31,0,35,0,39,0,43,0,47,0,51,0,55,0,59,0,63,0,67,0,71,0,75,0,79,0,83,0,87,0,91,0,95,0,99,0,103,0,107,0,111,0,115, + 0,119,0,123,0,127,0,131,0,135,0,139,0,143,0,0,17,53,51,21,49,150,3,32,5,130,23,32,33,130,3,211,7,151,115,32,128,133,0,37,252,128,128,2,128,128,190,5,133,74,32,4,133,6,206,5,42,0,7, + 1,128,0,0,2,0,4,0,0,65,139,13,37,0,1,53,51,21,7,146,3,32,3,130,19,32,1,141,133,32,3,141,14,131,13,38,255,0,128,128,0,6,1,130,84,35,2,128,4,128,140,91,132,89,32,51,65,143,6,139,7,33, + 1,0,130,57,32,254,130,3,32,128,132,4,32,4,131,14,138,89,35,0,0,24,0,130,0,33,3,128,144,171,66,55,33,148,115,65,187,19,32,5,130,151,143,155,163,39,32,1,136,182,32,253,134,178,132,7, + 132,200,145,17,32,3,65,48,17,165,17,39,0,0,21,0,128,255,128,3,65,175,17,65,3,27,132,253,131,217,139,201,155,233,155,27,131,67,131,31,130,241,33,255,0,131,181,137,232,132,15,132,4,138, + 247,34,255,0,128,179,238,32,0,130,0,32,20,65,239,48,33,0,19,67,235,10,32,51,65,203,14,65,215,11,32,7,154,27,135,39,32,33,130,35,33,128,128,130,231,32,253,132,231,32,128,132,232,34, + 128,128,254,133,13,136,8,32,253,65,186,5,130,36,130,42,176,234,133,231,34,128,0,0,66,215,44,33,0,1,68,235,6,68,211,19,32,49,68,239,14,139,207,139,47,66,13,7,32,51,130,47,33,1,0,130, + 207,35,128,128,1,0,131,222,131,5,130,212,130,6,131,212,32,0,130,10,133,220,130,233,130,226,32,254,133,255,178,233,39,3,1,128,3,0,2,0,4,68,15,7,68,99,12,130,89,130,104,33,128,4,133, + 93,130,10,38,0,0,11,1,0,255,0,68,63,16,70,39,9,66,215,8,32,7,68,77,6,68,175,14,32,29,68,195,6,132,7,35,2,0,128,255,131,91,132,4,65,178,5,141,111,67,129,23,165,135,140,107,142,135,33, + 21,5,69,71,6,131,7,33,1,0,140,104,132,142,130,4,137,247,140,30,68,255,12,39,11,0,128,0,128,3,0,3,69,171,15,67,251,7,65,15,8,66,249,11,65,229,7,67,211,7,66,13,7,35,1,128,128,254,133, + 93,32,254,131,145,132,4,132,18,32,2,151,128,130,23,34,0,0,9,154,131,65,207,8,68,107,15,68,51,7,32,7,70,59,7,135,121,130,82,32,128,151,111,41,0,0,4,0,128,255,0,1,128,1,137,239,33,0, + 37,70,145,10,65,77,10,65,212,14,37,0,0,0,5,0,128,66,109,5,70,123,10,33,0,19,72,33,18,133,237,70,209,11,33,0,2,130,113,137,119,136,115,33,1,0,133,43,130,5,34,0,0,10,69,135,6,70,219, + 13,66,155,7,65,9,12,66,157,11,66,9,11,32,7,130,141,132,252,66,151,9,137,9,66,15,30,36,0,20,0,128,0,130,218,71,11,42,68,51,8,65,141,7,73,19,15,69,47,23,143,39,66,81,7,32,1,66,55,6,34, + 1,128,128,68,25,5,69,32,6,137,6,136,25,32,254,131,42,32,3,66,88,26,148,26,32,0,130,0,32,14,164,231,70,225,12,66,233,7,67,133,19,71,203,15,130,161,32,255,130,155,32,254,139,127,134, + 12,164,174,33,0,15,164,159,33,59,0,65,125,20,66,25,7,32,5,68,191,6,66,29,7,144,165,65,105,9,35,128,128,255,0,137,2,133,182,164,169,33,128,128,197,171,130,155,68,235,7,32,21,70,77,19, + 66,21,10,68,97,8,66,30,5,66,4,43,34,0,17,0,71,19,41,65,253,20,71,25,23,65,91,15,65,115,7,34,2,128,128,66,9,8,130,169,33,1,0,66,212,13,132,28,72,201,43,35,0,0,0,18,66,27,38,76,231,5, + 68,157,20,135,157,32,7,68,185,13,65,129,28,66,20,5,32,253,66,210,11,65,128,49,133,61,32,0,65,135,6,74,111,37,72,149,12,66,203,19,65,147,19,68,93,7,68,85,8,76,4,5,33,255,0,133,129,34, + 254,0,128,68,69,8,181,197,34,0,0,12,65,135,32,65,123,20,69,183,27,133,156,66,50,5,72,87,10,67,137,32,33,0,19,160,139,78,251,13,68,55,20,67,119,19,65,91,36,69,177,15,32,254,143,16,65, + 98,53,32,128,130,0,32,0,66,43,54,70,141,23,66,23,15,131,39,69,47,11,131,15,70,129,19,74,161,9,36,128,255,0,128,254,130,153,65,148,32,67,41,9,34,0,0,4,79,15,5,73,99,10,71,203,8,32,3, + 72,123,6,72,43,8,32,2,133,56,131,99,130,9,34,0,0,6,72,175,5,73,159,14,144,63,135,197,132,189,133,66,33,255,0,73,6,7,70,137,12,35,0,0,0,10,130,3,73,243,25,67,113,12,65,73,7,69,161,7, + 138,7,37,21,2,0,128,128,254,134,3,73,116,27,33,128,128,130,111,39,12,0,128,1,0,3,128,2,72,219,21,35,43,0,47,0,67,47,20,130,111,33,21,1,68,167,13,81,147,8,133,230,32,128,77,73,6,32, + 128,131,142,134,18,130,6,32,255,75,18,12,131,243,37,128,0,128,3,128,3,74,231,21,135,123,32,29,134,107,135,7,32,21,74,117,7,135,7,134,96,135,246,74,103,23,132,242,33,0,10,67,151,28, + 67,133,20,66,141,11,131,11,32,3,77,71,6,32,128,130,113,32,1,81,4,6,134,218,66,130,24,131,31,34,0,26,0,130,0,77,255,44,83,15,11,148,155,68,13,7,32,49,78,231,18,79,7,11,73,243,11,32, + 33,65,187,10,130,63,65,87,8,73,239,19,35,0,128,1,0,131,226,32,252,65,100,6,32,128,139,8,33,1,0,130,21,32,253,72,155,44,73,255,20,32,128,71,67,8,81,243,39,67,15,20,74,191,23,68,121, + 27,32,1,66,150,6,32,254,79,19,11,131,214,32,128,130,215,37,2,0,128,253,0,128,136,5,65,220,24,147,212,130,210,33,0,24,72,219,42,84,255,13,67,119,16,69,245,19,72,225,19,65,3,15,69,93, + 19,131,55,132,178,71,115,14,81,228,6,142,245,33,253,0,132,43,172,252,65,16,11,75,219,8,65,219,31,66,223,24,75,223,10,33,29,1,80,243,10,66,175,8,131,110,134,203,133,172,130,16,70,30, + 7,164,183,130,163,32,20,65,171,48,65,163,36,65,143,23,65,151,19,65,147,13,65,134,17,133,17,130,216,67,114,5,164,217,65,137,12,72,147,48,79,71,19,74,169,22,80,251,8,65,173,7,66,157, + 15,74,173,15,32,254,65,170,8,71,186,45,72,131,6,77,143,40,187,195,152,179,65,123,38,68,215,57,68,179,15,65,85,7,69,187,14,32,21,66,95,15,67,19,25,32,1,83,223,6,32,2,76,240,7,77,166, + 43,65,8,5,130,206,32,0,67,39,54,143,167,66,255,19,82,193,11,151,47,85,171,5,67,27,17,132,160,69,172,11,69,184,56,66,95,6,33,12,1,130,237,32,2,68,179,27,68,175,16,80,135,15,72,55,7, + 71,87,12,73,3,12,132,12,66,75,32,76,215,5,169,139,147,135,148,139,81,12,12,81,185,36,75,251,7,65,23,27,76,215,9,87,165,12,65,209,15,72,157,7,65,245,31,32,128,71,128,6,32,1,82,125,5, + 34,0,128,254,131,169,32,254,131,187,71,180,9,132,27,32,2,88,129,44,32,0,78,47,40,65,79,23,79,171,14,32,21,71,87,8,72,15,14,65,224,33,130,139,74,27,62,93,23,7,68,31,7,75,27,7,139,15, + 74,3,7,74,23,27,65,165,11,65,177,15,67,123,5,32,1,130,221,32,252,71,96,5,74,12,12,133,244,130,25,34,1,0,128,130,2,139,8,93,26,8,65,9,32,65,57,14,140,14,32,0,73,79,67,68,119,11,135, + 11,32,51,90,75,14,139,247,65,43,7,131,19,139,11,69,159,11,65,247,6,36,1,128,128,253,0,90,71,9,33,1,0,132,14,32,128,89,93,14,69,133,6,130,44,131,30,131,6,65,20,56,33,0,16,72,179,40, + 75,47,12,65,215,19,74,95,19,65,43,11,131,168,67,110,5,75,23,17,69,106,6,75,65,5,71,204,43,32,0,80,75,47,71,203,15,159,181,68,91,11,67,197,7,73,101,13,68,85,6,33,128,128,130,214,130, + 25,32,254,74,236,48,130,194,37,0,18,0,128,255,128,77,215,40,65,139,64,32,51,80,159,10,65,147,39,130,219,84,212,43,130,46,75,19,97,74,33,11,65,201,23,65,173,31,33,1,0,79,133,6,66,150, + 5,67,75,48,85,187,6,70,207,37,32,71,87,221,13,73,163,14,80,167,15,132,15,83,193,19,82,209,8,78,99,9,72,190,11,77,110,49,89,63,5,80,91,35,99,63,32,70,235,23,81,99,10,69,148,10,65,110, + 36,32,0,65,99,47,95,219,11,68,171,51,66,87,7,72,57,7,74,45,17,143,17,65,114,50,33,14,0,65,111,40,159,195,98,135,15,35,7,53,51,21,100,78,9,95,146,16,32,254,82,114,6,32,128,67,208,37, + 130,166,99,79,58,32,17,96,99,14,72,31,19,72,87,31,82,155,7,67,47,14,32,21,131,75,134,231,72,51,17,72,78,8,133,8,80,133,6,33,253,128,88,37,9,66,124,36,72,65,12,134,12,71,55,43,66,139, + 27,85,135,10,91,33,12,65,35,11,66,131,11,71,32,8,90,127,6,130,244,71,76,11,168,207,33,0,12,66,123,32,32,0,65,183,15,68,135,11,66,111,7,67,235,11,66,111,15,32,254,97,66,12,160,154,67, + 227,52,80,33,15,87,249,15,93,45,31,75,111,12,93,45,11,77,99,9,160,184,81,31,12,32,15,98,135,30,104,175,7,77,249,36,69,73,15,78,5,12,32,254,66,151,19,34,128,128,4,87,32,12,149,35,133, + 21,96,151,31,32,19,72,35,5,98,173,15,143,15,32,21,143,99,158,129,33,0,0,65,35,52,65,11,15,147,15,98,75,11,33,1,0,143,151,132,15,32,254,99,200,37,132,43,130,4,39,0,10,0,128,1,128,3, + 0,104,151,14,97,187,20,69,131,15,67,195,11,87,227,7,33,128,128,132,128,33,254,0,68,131,9,65,46,26,42,0,0,0,7,0,0,255,128,3,128,0,88,223,15,33,0,21,89,61,22,66,209,12,65,2,12,37,0,2, + 1,0,3,128,101,83,8,36,0,1,53,51,29,130,3,34,21,1,0,66,53,8,32,0,68,215,6,100,55,25,107,111,9,66,193,11,72,167,8,73,143,31,139,31,33,1,0,131,158,32,254,132,5,33,253,128,65,16,9,133, + 17,89,130,25,141,212,33,0,0,93,39,8,90,131,25,93,39,14,66,217,6,106,179,8,159,181,71,125,15,139,47,138,141,87,11,14,76,23,14,65,231,26,140,209,66,122,8,81,179,5,101,195,26,32,47,74, + 75,13,69,159,11,83,235,11,67,21,16,136,167,131,106,130,165,130,15,32,128,101,90,24,134,142,32,0,65,103,51,108,23,11,101,231,15,75,173,23,74,237,23,66,15,6,66,46,17,66,58,17,65,105, + 49,66,247,55,71,179,12,70,139,15,86,229,7,84,167,15,32,1,95,72,12,89,49,6,33,128,128,65,136,38,66,30,9,32,0,100,239,7,66,247,29,70,105,20,65,141,19,69,81,15,130,144,32,128,83,41,5, + 32,255,131,177,68,185,5,133,126,65,97,37,32,0,130,0,33,21,0,130,55,66,195,28,67,155,13,34,79,0,83,66,213,13,73,241,19,66,59,19,65,125,11,135,201,66,249,16,32,128,66,44,11,66,56,17, + 68,143,8,68,124,38,67,183,12,96,211,9,65,143,29,112,171,5,32,0,68,131,63,34,33,53,51,71,121,11,32,254,98,251,16,32,253,74,231,10,65,175,37,133,206,37,0,0,8,1,0,0,107,123,11,113,115, + 9,33,0,1,130,117,131,3,73,103,7,66,51,18,66,44,5,133,75,70,88,5,32,254,65,39,12,68,80,9,34,12,0,128,107,179,28,68,223,6,155,111,86,147,15,32,2,131,82,141,110,33,254,0,130,15,32,4,103, + 184,15,141,35,87,176,5,83,11,5,71,235,23,114,107,11,65,189,16,70,33,15,86,153,31,135,126,86,145,30,65,183,41,32,0,130,0,32,10,65,183,24,34,35,0,39,67,85,9,65,179,15,143,15,33,1,0,65, + 28,17,157,136,130,123,32,20,130,3,32,0,97,135,24,115,167,19,80,71,12,32,51,110,163,14,78,35,19,131,19,155,23,77,229,8,78,9,17,151,17,67,231,46,94,135,8,73,31,31,93,215,56,82,171,25, + 72,77,8,162,179,169,167,99,131,11,69,85,19,66,215,15,76,129,13,68,115,22,72,79,35,67,113,5,34,0,0,19,70,31,46,65,89,52,73,223,15,85,199,33,95,33,8,132,203,73,29,32,67,48,16,177,215, + 101,13,15,65,141,43,69,141,15,75,89,5,70,0,11,70,235,21,178,215,36,10,0,128,0,0,71,207,24,33,0,19,100,67,6,80,215,11,66,67,7,80,43,12,71,106,7,80,192,5,65,63,5,66,217,26,33,0,13,156, + 119,68,95,5,72,233,12,134,129,85,81,11,76,165,20,65,43,8,73,136,8,75,10,31,38,128,128,0,0,0,13,1,130,4,32,3,106,235,29,114,179,12,66,131,23,32,7,77,133,6,67,89,12,131,139,116,60,9, + 89,15,37,32,0,74,15,7,103,11,22,65,35,5,33,55,0,93,81,28,67,239,23,78,85,5,107,93,14,66,84,17,65,193,26,74,183,10,66,67,34,143,135,79,91,15,32,7,117,111,8,75,56,9,84,212,9,154,134, + 32,0,130,0,32,18,130,3,70,171,41,83,7,16,70,131,19,84,191,15,84,175,19,84,167,30,84,158,12,154,193,68,107,15,33,0,0,65,79,42,65,71,7,73,55,7,118,191,16,83,180,9,32,255,76,166,9,154, + 141,32,0,130,0,69,195,52,65,225,15,151,15,75,215,31,80,56,10,68,240,17,100,32,9,70,147,39,65,93,12,71,71,41,92,85,15,84,135,23,78,35,15,110,27,10,84,125,8,107,115,29,136,160,38,0,0, + 14,0,128,255,0,82,155,24,67,239,8,119,255,11,69,131,11,77,29,6,112,31,8,134,27,105,203,8,32,2,75,51,11,75,195,12,74,13,29,136,161,37,128,0,0,0,11,1,130,163,82,115,8,125,191,17,69,35, + 12,74,137,15,143,15,32,1,65,157,12,136,12,161,142,65,43,40,65,199,6,65,19,24,102,185,11,76,123,11,99,6,12,135,12,32,254,130,8,161,155,101,23,9,39,8,0,0,1,128,3,128,2,78,63,17,72,245, + 12,67,41,11,90,167,9,32,128,97,49,9,32,128,109,51,14,132,97,81,191,8,130,97,125,99,12,121,35,9,127,75,15,71,79,12,81,151,23,87,97,7,70,223,15,80,245,16,105,97,15,32,254,113,17,6,32, + 128,130,8,105,105,8,76,122,18,65,243,21,74,63,7,38,4,1,0,255,0,2,0,119,247,28,133,65,32,255,141,91,35,0,0,0,16,67,63,36,34,59,0,63,77,59,9,119,147,11,143,241,66,173,15,66,31,11,67, + 75,8,81,74,16,32,128,131,255,87,181,42,127,43,5,34,255,128,2,120,235,11,37,19,0,23,0,0,37,109,191,14,118,219,7,127,43,14,65,79,14,35,0,0,0,3,73,91,5,130,5,38,3,0,7,0,11,0,0,70,205, + 11,88,221,12,32,0,73,135,7,87,15,22,73,135,10,79,153,15,97,71,19,65,49,11,32,1,131,104,121,235,11,80,65,11,142,179,144,14,81,123,46,32,1,88,217,5,112,5,8,65,201,15,83,29,15,122,147, + 11,135,179,142,175,143,185,67,247,39,66,199,7,35,5,0,128,3,69,203,15,123,163,12,67,127,7,130,119,71,153,10,141,102,70,175,8,32,128,121,235,30,136,89,100,191,11,116,195,11,111,235,15, + 72,39,7,32,2,97,43,5,132,5,94,67,8,131,8,125,253,10,32,3,65,158,16,146,16,130,170,40,0,21,0,128,0,0,3,128,5,88,219,15,24,64,159,32,135,141,65,167,15,68,163,10,97,73,49,32,255,82,58, + 7,93,80,8,97,81,16,24,67,87,52,34,0,0,5,130,231,33,128,2,80,51,13,65,129,8,113,61,6,132,175,65,219,5,130,136,77,152,17,32,0,95,131,61,70,215,6,33,21,51,90,53,10,78,97,23,105,77,31, + 65,117,7,139,75,24,68,195,9,24,64,22,9,33,0,128,130,11,33,128,128,66,25,5,121,38,5,134,5,134,45,66,40,36,66,59,18,34,128,0,0,66,59,81,135,245,123,103,19,120,159,19,77,175,12,33,255, + 0,87,29,10,94,70,21,66,59,54,39,3,1,128,3,0,2,128,4,24,65,7,15,66,47,7,72,98,12,37,0,0,0,3,1,0,24,65,55,21,131,195,32,1,67,178,6,33,4,0,77,141,8,32,6,131,47,74,67,16,24,69,3,20,24, + 65,251,7,133,234,130,229,94,108,17,35,0,0,6,0,141,175,86,59,5,162,79,85,166,8,70,112,13,32,13,24,64,67,26,24,71,255,7,123,211,12,80,121,11,69,215,15,66,217,11,69,71,10,131,113,132, + 126,119,90,9,66,117,19,132,19,32,0,130,0,24,64,47,59,33,7,0,73,227,5,68,243,15,85,13,12,76,37,22,74,254,15,130,138,33,0,4,65,111,6,137,79,65,107,16,32,1,77,200,6,34,128,128,3,75,154, + 12,37,0,16,0,0,2,0,104,115,36,140,157,68,67,19,68,51,15,106,243,15,134,120,70,37,10,68,27,10,140,152,65,121,24,32,128,94,155,7,67,11,8,24,74,11,25,65,3,12,83,89,18,82,21,37,67,200, + 5,130,144,24,64,172,12,33,4,0,134,162,74,80,14,145,184,32,0,130,0,69,251,20,32,19,81,243,5,82,143,8,33,5,53,89,203,5,133,112,79,109,15,33,0,21,130,71,80,175,41,36,75,0,79,0,83,121, + 117,9,87,89,27,66,103,11,70,13,15,75,191,11,135,67,87,97,20,109,203,5,69,246,8,108,171,5,78,195,38,65,51,13,107,203,11,77,3,17,24,75,239,17,65,229,28,79,129,39,130,175,32,128,123,253, + 7,132,142,24,65,51,15,65,239,41,36,128,128,0,0,13,65,171,5,66,163,28,136,183,118,137,11,80,255,15,67,65,7,74,111,8,32,0,130,157,32,253,24,76,35,10,103,212,5,81,175,9,69,141,7,66,150, + 29,131,158,24,75,199,28,124,185,7,76,205,15,68,124,14,32,3,123,139,16,130,16,33,128,128,108,199,6,33,0,3,65,191,35,107,11,6,73,197,11,24,70,121,15,83,247,15,24,70,173,23,69,205,14, + 32,253,131,140,32,254,136,4,94,198,9,32,3,78,4,13,66,127,13,143,13,32,0,130,0,33,16,0,24,69,59,39,109,147,12,76,253,19,24,69,207,15,69,229,15,130,195,71,90,10,139,10,130,152,73,43, + 40,91,139,10,65,131,37,35,75,0,79,0,84,227,12,143,151,68,25,15,80,9,23,95,169,11,34,128,2,128,112,186,5,130,6,83,161,19,76,50,6,130,37,65,145,44,110,83,5,32,16,67,99,6,71,67,15,76, + 55,17,140,215,67,97,23,76,69,15,77,237,11,104,211,23,77,238,11,65,154,43,33,0,10,83,15,28,83,13,20,67,145,19,67,141,14,97,149,21,68,9,15,86,251,5,66,207,5,66,27,37,82,1,23,127,71,12, + 94,235,10,110,175,24,98,243,15,132,154,132,4,24,66,69,10,32,4,67,156,43,130,198,35,2,1,0,4,75,27,9,69,85,9,95,240,7,32,128,130,35,32,28,66,43,40,24,82,63,23,83,123,12,72,231,15,127, + 59,23,116,23,19,117,71,7,24,77,99,15,67,111,15,71,101,8,36,2,128,128,252,128,127,60,11,32,1,132,16,130,18,141,24,67,107,9,32,3,68,194,15,175,15,38,0,11,0,128,1,128,2,80,63,25,32,0, + 24,65,73,11,69,185,15,83,243,16,32,0,24,81,165,8,130,86,77,35,6,155,163,88,203,5,24,66,195,30,70,19,19,24,80,133,15,32,1,75,211,8,32,254,108,133,8,79,87,20,65,32,9,41,0,0,7,0,128,0, + 0,2,128,2,68,87,15,66,1,16,92,201,16,24,76,24,17,133,17,34,128,0,30,66,127,64,34,115,0,119,73,205,9,66,43,11,109,143,15,24,79,203,11,90,143,15,131,15,155,31,65,185,15,86,87,11,35,128, + 128,253,0,69,7,6,130,213,33,1,0,119,178,15,142,17,66,141,74,83,28,6,36,7,0,0,4,128,82,39,18,76,149,12,67,69,21,32,128,79,118,15,32,0,130,0,32,8,131,206,32,2,79,83,9,100,223,14,102, + 113,23,115,115,7,24,65,231,12,130,162,32,4,68,182,19,130,102,93,143,8,69,107,29,24,77,255,12,143,197,72,51,7,76,195,15,132,139,85,49,15,130,152,131,18,71,81,23,70,14,11,36,0,10,0,128, + 2,69,59,9,89,151,15,66,241,11,76,165,12,71,43,15,75,49,13,65,12,23,132,37,32,0,179,115,130,231,95,181,16,132,77,32,254,67,224,8,65,126,20,79,171,8,32,2,89,81,5,75,143,6,80,41,8,34, + 2,0,128,24,81,72,9,32,0,130,0,35,17,0,0,255,77,99,39,95,65,36,67,109,15,24,69,93,11,77,239,5,95,77,23,35,128,1,0,128,24,86,7,8,132,167,32,2,69,198,41,130,202,33,0,26,120,75,44,24,89, + 51,15,71,243,12,70,239,11,24,84,3,11,66,7,11,71,255,10,32,21,69,155,35,88,151,12,32,128,74,38,10,65,210,8,74,251,5,65,226,5,75,201,13,32,3,65,9,41,146,41,40,0,0,0,9,1,0,1,0,2,91,99, + 19,32,35,106,119,13,70,219,15,83,239,12,137,154,32,2,67,252,19,36,128,0,0,4,1,130,196,32,2,130,8,91,107,8,32,0,135,81,24,73,211,8,132,161,73,164,13,36,0,8,0,128,2,105,123,26,139,67, + 76,99,15,34,1,0,128,135,76,83,156,20,92,104,8,67,251,30,24,86,47,27,123,207,12,24,86,7,15,71,227,8,32,4,65,20,20,131,127,32,0,130,123,32,0,71,223,26,32,19,90,195,22,71,223,15,84,200, + 6,32,128,133,241,24,84,149,9,67,41,25,36,0,0,0,22,0,88,111,49,32,87,66,21,5,77,3,27,123,75,7,71,143,19,135,183,71,183,19,130,171,74,252,5,131,5,89,87,17,32,1,132,18,130,232,68,11,10, + 33,1,128,70,208,16,66,230,18,147,18,130,254,223,255,75,27,23,65,59,15,135,39,155,255,34,128,128,254,104,92,8,33,0,128,65,32,11,65,1,58,33,26,0,130,0,72,71,18,78,55,17,76,11,19,86,101, + 12,75,223,11,89,15,11,24,76,87,15,75,235,15,131,15,72,95,7,85,71,11,72,115,11,73,64,6,34,1,128,128,66,215,9,34,128,254,128,134,14,33,128,255,67,102,5,32,0,130,16,70,38,11,66,26,57, + 88,11,8,24,76,215,34,78,139,7,95,245,7,32,7,24,73,75,23,32,128,131,167,130,170,101,158,9,82,49,22,118,139,6,32,18,67,155,44,116,187,9,108,55,14,80,155,23,66,131,15,93,77,10,131,168, + 32,128,73,211,12,24,75,187,22,32,4,96,71,20,67,108,19,132,19,120,207,8,32,5,76,79,15,66,111,21,66,95,8,32,3,190,211,111,3,8,211,212,32,20,65,167,44,34,75,0,79,97,59,13,32,33,112,63, + 10,65,147,19,69,39,19,143,39,24,66,71,9,130,224,65,185,43,94,176,12,65,183,24,71,38,8,24,72,167,7,65,191,38,136,235,24,96,167,12,65,203,62,115,131,13,65,208,42,175,235,67,127,6,32, + 4,76,171,29,114,187,5,32,71,65,211,5,65,203,68,72,51,8,164,219,32,0,172,214,71,239,58,78,3,27,66,143,15,77,19,15,147,31,35,33,53,51,21,66,183,10,173,245,66,170,30,150,30,34,0,0,23, + 80,123,54,76,1,16,73,125,15,82,245,11,167,253,24,76,85,12,70,184,5,32,254,131,185,37,254,0,128,1,0,128,133,16,117,158,18,92,27,38,65,3,17,130,251,35,17,0,128,254,24,69,83,39,140,243, + 121,73,19,109,167,7,81,41,15,24,95,175,12,102,227,15,121,96,11,24,95,189,7,32,3,145,171,154,17,24,77,47,9,33,0,5,70,71,37,68,135,7,32,29,117,171,11,69,87,15,24,79,97,19,24,79,149,23, + 131,59,32,1,75,235,5,72,115,11,72,143,7,132,188,71,27,46,131,51,32,0,69,95,6,175,215,32,21,131,167,81,15,19,151,191,151,23,131,215,71,43,5,32,254,24,79,164,24,74,109,8,77,166,13,65, + 176,26,88,162,5,98,159,6,171,219,120,247,6,79,29,8,99,169,10,103,59,19,65,209,35,131,35,91,25,19,112,94,15,83,36,8,173,229,33,20,0,88,75,43,71,31,12,65,191,71,33,1,0,130,203,32,254, + 131,4,68,66,7,67,130,6,104,61,13,173,215,38,13,1,0,0,0,2,128,67,111,28,74,129,16,104,35,19,79,161,16,87,14,7,138,143,132,10,67,62,36,114,115,5,162,151,67,33,16,108,181,15,143,151,67, + 5,5,24,100,242,15,170,153,34,0,0,14,65,51,34,32,55,79,75,9,32,51,74,7,10,65,57,38,132,142,32,254,72,0,14,139,163,32,128,80,254,8,67,158,21,65,63,7,32,4,72,227,27,95,155,12,67,119,19, + 124,91,24,149,154,72,177,34,97,223,8,155,151,24,108,227,15,88,147,16,72,117,19,68,35,11,92,253,15,70,199,15,24,87,209,17,32,2,87,233,7,32,1,24,88,195,10,119,24,8,32,3,81,227,24,65, + 125,21,35,128,128,0,25,76,59,48,24,90,187,9,97,235,12,66,61,11,91,105,19,24,79,141,11,24,79,117,15,24,79,129,27,90,53,13,130,13,32,253,131,228,24,79,133,40,69,70,8,66,137,31,65,33, + 19,96,107,8,68,119,29,66,7,5,68,125,16,65,253,19,65,241,27,24,90,179,13,24,79,143,18,33,128,128,130,246,32,254,130,168,68,154,36,77,51,9,97,47,5,167,195,32,21,131,183,78,239,27,155, + 195,78,231,14,201,196,77,11,6,32,5,73,111,37,97,247,12,77,19,31,155,207,78,215,19,162,212,69,17,14,66,91,19,80,143,57,78,203,39,159,215,32,128,93,134,8,24,80,109,24,66,113,15,169,215, + 66,115,6,32,4,69,63,33,32,0,101,113,7,86,227,35,143,211,36,49,53,51,21,1,77,185,14,65,159,28,69,251,34,67,56,8,33,9,0,24,107,175,25,90,111,12,110,251,11,119,189,24,119,187,34,87,15, + 9,32,4,66,231,37,90,39,7,66,239,8,84,219,15,69,105,23,24,85,27,27,87,31,11,33,1,128,76,94,6,32,1,85,241,7,33,128,128,106,48,10,33,128,128,69,136,11,133,13,24,79,116,49,84,236,8,24, + 91,87,9,32,5,165,255,69,115,12,66,27,15,159,15,24,72,247,12,74,178,5,24,80,64,15,33,0,128,143,17,77,89,51,130,214,24,81,43,7,170,215,74,49,8,159,199,143,31,139,215,69,143,5,32,254, + 24,81,50,35,181,217,84,123,70,143,195,159,15,65,187,16,66,123,7,65,175,15,65,193,29,68,207,39,79,27,5,70,131,6,32,4,68,211,33,33,67,0,83,143,14,159,207,143,31,140,223,33,0,128,24,80, + 82,14,24,93,16,23,32,253,65,195,5,68,227,40,133,214,107,31,7,32,5,67,115,27,87,9,8,107,31,43,66,125,6,32,0,103,177,23,131,127,72,203,36,32,0,110,103,8,155,163,73,135,6,32,19,24,112, + 99,10,65,71,11,73,143,19,143,31,126,195,5,24,85,21,9,24,76,47,14,32,254,24,93,77,36,68,207,11,39,25,0,0,255,128,3,128,4,66,51,37,95,247,13,82,255,24,76,39,19,147,221,66,85,27,24,118, + 7,8,24,74,249,12,76,74,8,91,234,8,67,80,17,131,222,33,253,0,121,30,44,73,0,16,69,15,6,32,0,65,23,38,69,231,12,65,179,6,98,131,16,86,31,27,24,108,157,14,80,160,8,24,65,46,17,33,4,0, + 96,2,18,144,191,65,226,8,68,19,5,171,199,80,9,15,180,199,67,89,5,32,255,24,79,173,28,174,201,24,79,179,50,32,1,24,122,5,10,82,61,10,180,209,83,19,8,32,128,24,80,129,27,111,248,43,131, + 71,24,115,103,8,67,127,41,78,213,24,100,247,19,66,115,39,75,107,5,32,254,165,219,78,170,40,24,112,163,49,32,1,97,203,6,65,173,64,32,0,83,54,7,133,217,88,37,12,32,254,131,28,33,128, + 3,67,71,44,84,183,6,32,5,69,223,33,96,7,7,123,137,16,192,211,24,112,14,9,32,255,67,88,29,68,14,10,84,197,38,33,0,22,116,47,50,32,87,106,99,9,116,49,15,89,225,15,97,231,23,70,41,19, + 82,85,8,93,167,6,32,253,132,236,108,190,7,89,251,5,116,49,58,33,128,128,131,234,32,15,24,74,67,38,70,227,24,24,83,45,23,89,219,12,70,187,12,89,216,19,32,2,69,185,24,141,24,70,143,66, + 24,82,119,56,78,24,10,32,253,133,149,132,6,24,106,233,7,69,198,48,178,203,81,243,12,68,211,15,106,255,23,66,91,15,69,193,7,100,39,10,24,83,72,16,176,204,33,19,0,88,207,45,68,21,12, + 68,17,10,65,157,53,68,17,6,32,254,92,67,10,65,161,25,69,182,43,24,118,91,47,69,183,18,181,209,111,253,12,89,159,8,66,112,12,69,184,45,35,0,0,0,9,24,80,227,26,73,185,16,118,195,15,131, + 15,33,1,0,65,59,15,66,39,27,160,111,66,205,12,148,111,143,110,33,128,128,156,112,24,81,199,8,75,199,23,66,117,20,155,121,32,254,68,126,12,72,213,29,134,239,149,123,89,27,16,148,117, + 65,245,8,24,71,159,14,141,134,134,28,73,51,55,109,77,15,105,131,11,68,67,11,76,169,27,107,209,12,102,174,8,32,128,72,100,18,116,163,56,79,203,11,75,183,44,85,119,19,71,119,23,151,227, + 32,1,93,27,8,65,122,5,77,102,8,110,120,20,66,23,8,66,175,17,66,63,12,133,12,79,35,8,74,235,33,67,149,16,69,243,15,78,57,15,69,235,16,67,177,7,151,192,130,23,67,84,29,141,192,174,187, + 77,67,15,69,11,12,159,187,77,59,10,199,189,24,70,235,50,96,83,19,66,53,23,105,65,19,77,47,12,163,199,66,67,37,78,207,50,67,23,23,174,205,67,228,6,71,107,13,67,22,14,66,85,11,83,187, + 38,124,47,49,95,7,19,66,83,23,67,23,19,24,96,78,17,80,101,16,71,98,40,33,0,7,88,131,22,24,89,245,12,84,45,12,102,213,5,123,12,9,32,2,126,21,14,43,255,0,128,128,0,0,20,0,128,255,128, + 3,126,19,39,32,75,106,51,7,113,129,15,24,110,135,19,126,47,15,115,117,11,69,47,11,32,2,109,76,9,102,109,9,32,128,75,2,10,130,21,32,254,69,47,6,32,3,94,217,47,32,0,65,247,10,69,15,46, + 65,235,31,65,243,15,101,139,10,66,174,14,65,247,16,72,102,28,69,17,14,84,243,9,165,191,88,47,48,66,53,12,32,128,71,108,6,203,193,32,17,75,187,42,73,65,16,65,133,52,114,123,9,167,199, + 69,21,37,86,127,44,75,171,11,180,197,78,213,12,148,200,81,97,46,24,95,243,9,32,4,66,75,33,113,103,9,87,243,36,143,225,24,84,27,31,90,145,8,148,216,67,49,5,24,84,34,14,75,155,27,67, + 52,13,140,13,36,0,20,0,128,255,24,135,99,46,88,59,43,155,249,80,165,7,136,144,71,161,23,32,253,132,33,32,254,88,87,44,136,84,35,128,0,0,21,81,103,5,94,47,44,76,51,12,143,197,151,15, + 65,215,31,24,64,77,13,65,220,20,65,214,14,71,4,40,65,213,13,32,0,130,0,35,21,1,2,0,135,0,34,36,0,72,134,10,36,1,0,26,0,130,134,11,36,2,0,14,0,108,134,11,32,3,138,23,32,4,138,11,34, + 5,0,20,134,33,34,0,0,6,132,23,32,1,134,15,32,18,130,25,133,11,37,1,0,13,0,49,0,133,11,36,2,0,7,0,38,134,11,36,3,0,17,0,45,134,11,32,4,138,35,36,5,0,10,0,62,134,23,32,6,132,23,36,3, + 0,1,4,9,130,87,131,167,133,11,133,167,133,11,133,167,133,11,37,3,0,34,0,122,0,133,11,133,167,133,11,133,167,133,11,133,167,34,50,0,48,130,1,34,52,0,47,134,5,8,49,49,0,53,98,121,32, + 84,114,105,115,116,97,110,32,71,114,105,109,109,101,114,82,101,103,117,108,97,114,84,84,88,32,80,114,111,103,103,121,67,108,101,97,110,84,84,50,48,48,52,47,130,2,53,49,53,0,98,0,121, + 0,32,0,84,0,114,0,105,0,115,0,116,0,97,0,110,130,15,32,71,132,15,36,109,0,109,0,101,130,9,32,82,130,5,36,103,0,117,0,108,130,29,32,114,130,43,34,84,0,88,130,35,32,80,130,25,34,111, + 0,103,130,1,34,121,0,67,130,27,32,101,132,59,32,84,130,31,33,0,0,65,155,9,34,20,0,0,65,11,6,130,8,135,2,33,1,1,130,9,8,120,1,1,2,1,3,1,4,1,5,1,6,1,7,1,8,1,9,1,10,1,11,1,12,1,13,1,14, + 1,15,1,16,1,17,1,18,1,19,1,20,1,21,1,22,1,23,1,24,1,25,1,26,1,27,1,28,1,29,1,30,1,31,1,32,0,3,0,4,0,5,0,6,0,7,0,8,0,9,0,10,0,11,0,12,0,13,0,14,0,15,0,16,0,17,0,18,0,19,0,20,0,21,0, + 22,0,23,0,24,0,25,0,26,0,27,0,28,0,29,0,30,0,31,130,187,8,66,33,0,34,0,35,0,36,0,37,0,38,0,39,0,40,0,41,0,42,0,43,0,44,0,45,0,46,0,47,0,48,0,49,0,50,0,51,0,52,0,53,0,54,0,55,0,56,0, + 57,0,58,0,59,0,60,0,61,0,62,0,63,0,64,0,65,0,66,130,243,9,75,68,0,69,0,70,0,71,0,72,0,73,0,74,0,75,0,76,0,77,0,78,0,79,0,80,0,81,0,82,0,83,0,84,0,85,0,86,0,87,0,88,0,89,0,90,0,91,0, + 92,0,93,0,94,0,95,0,96,0,97,1,33,1,34,1,35,1,36,1,37,1,38,1,39,1,40,1,41,1,42,1,43,1,44,1,45,1,46,1,47,1,48,1,49,1,50,1,51,1,52,1,53,1,54,1,55,1,56,1,57,1,58,1,59,1,60,1,61,1,62,1, + 63,1,64,1,65,0,172,0,163,0,132,0,133,0,189,0,150,0,232,0,134,0,142,0,139,0,157,0,169,0,164,0,239,0,138,0,218,0,131,0,147,0,242,0,243,0,141,0,151,0,136,0,195,0,222,0,241,0,158,0,170, + 0,245,0,244,0,246,0,162,0,173,0,201,0,199,0,174,0,98,0,99,0,144,0,100,0,203,0,101,0,200,0,202,0,207,0,204,0,205,0,206,0,233,0,102,0,211,0,208,0,209,0,175,0,103,0,240,0,145,0,214,0, + 212,0,213,0,104,0,235,0,237,0,137,0,106,0,105,0,107,0,109,0,108,0,110,0,160,0,111,0,113,0,112,0,114,0,115,0,117,0,116,0,118,0,119,0,234,0,120,0,122,0,121,0,123,0,125,0,124,0,184,0, + 161,0,127,0,126,0,128,0,129,0,236,0,238,0,186,14,117,110,105,99,111,100,101,35,48,120,48,48,48,49,141,14,32,50,141,14,32,51,141,14,32,52,141,14,32,53,141,14,32,54,141,14,32,55,141, + 14,32,56,141,14,32,57,141,14,32,97,141,14,32,98,141,14,32,99,141,14,32,100,141,14,32,101,141,14,32,102,140,14,33,49,48,141,14,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49, + 141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,32,49,141,239,45,49,102,6,100,101,108,101,116,101,4,69,117,114, + 111,140,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236,32,56,141,236, + 32,56,141,236,32,56,141,236,32,56,65,220,13,32,57,65,220,13,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141, + 239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,32,57,141,239,35,57,102,0,0,5,250,72,249,98,247, +}; + +static const char* GetDefaultCompressedFontDataTTF(int* out_size) +{ + *out_size = proggy_clean_ttf_compressed_size; + return (const char*)proggy_clean_ttf_compressed_data; } +#endif // #ifndef IMGUI_DISABLE_DEFAULT_FONT + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_impl_glfw.cpp b/imgui/imgui_impl_glfw.cpp index a9e72c6c..35040b5d 100644 --- a/imgui/imgui_impl_glfw.cpp +++ b/imgui/imgui_impl_glfw.cpp @@ -1,20 +1,79 @@ -// dear imgui: Platform Binding for GLFW -// This needs to be used along with a Renderer (e.g. OpenGL3, Vulkan..) +// dear imgui: Platform Backend for GLFW +// This needs to be used along with a Renderer (e.g. OpenGL3, Vulkan, WebGPU..) // (Info: GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan graphics context creation, etc.) -// (Requires: GLFW 3.1+) +// (Requires: GLFW 3.1+. Prefer GLFW 3.3+ or GLFW 3.4+ for full feature support.) // Implemented features: // [X] Platform: Clipboard support. +// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen (Windows only). +// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy GLFW_KEY_* values are obsolete since 1.87 and not supported since 1.91.5] // [X] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. -// [x] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: 3 cursors types are missing from GLFW. -// [X] Platform: Keyboard arrays indexed using GLFW_KEY_* codes, e.g. ImGui::IsKeyPressed(GLFW_KEY_SPACE). +// [X] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Resizing cursors requires GLFW 3.4+! Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. +// [X] Platform: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// Missing features or Issues: +// [ ] Touch events are only correctly identified as Touch on Windows. This create issues with some interactions. GLFW doesn't provide a way to identify touch inputs from mouse inputs, we cannot call io.AddMouseSourceEvent() to identify the source. We provide a Windows-specific workaround. +// [ ] Missing ImGuiMouseCursor_Wait and ImGuiMouseCursor_Progress cursors. +// [ ] Multi-viewport: ParentViewportID not honored, and so io.ConfigViewportsNoDefaultParent has no effect (minor). -// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this. -// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp. -// https://github.com/ocornut/imgui +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +// About Emscripten support: +// - Emscripten provides its own GLFW (3.2.1) implementation (syntax: "-sUSE_GLFW=3"), but Joystick is broken and several features are not supported (multiple windows, clipboard, timer, etc.) +// - A third-party Emscripten GLFW (3.4.0) implementation (syntax: "--use-port=contrib.glfw3") fixes the Joystick issue and implements all relevant features for the browser. +// See https://github.com/pongasoft/emscripten-glfw/blob/master/docs/Comparison.md for details. // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2025-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. +// 2025-03-10: Map GLFW_KEY_WORLD_1 and GLFW_KEY_WORLD_2 into ImGuiKey_Oem102. +// 2025-03-03: Fixed clipboard handler assertion when using GLFW <= 3.2.1 compiled with asserts enabled. +// 2025-02-21: [Docking] Update monitors and work areas information every frame, as the later may change regardless of monitor changes. (#8415) +// 2024-11-05: [Docking] Added Linux workaround for spurious mouse up events emitted while dragging and creating new viewport. (#3158, #7733, #7922) +// 2024-08-22: Moved some OS/backend related function pointers from ImGuiIO to ImGuiPlatformIO: +// - io.GetClipboardTextFn -> platform_io.Platform_GetClipboardTextFn +// - io.SetClipboardTextFn -> platform_io.Platform_SetClipboardTextFn +// - io.PlatformOpenInShellFn -> platform_io.Platform_OpenInShellFn +// 2024-07-31: Added ImGui_ImplGlfw_Sleep() helper function for usage by our examples app, since GLFW doesn't provide one. +// 2024-07-08: *BREAKING* Renamed ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback to ImGui_ImplGlfw_InstallEmscriptenCallbacks(), added GLFWWindow* parameter. +// 2024-07-08: Emscripten: Added support for GLFW3 contrib port (GLFW 3.4.0 features + bug fixes): to enable, replace -sUSE_GLFW=3 with --use-port=contrib.glfw3 (requires emscripten 3.1.59+) (https://github.com/pongasoft/emscripten-glfw) +// 2024-07-02: Emscripten: Added io.PlatformOpenInShellFn() handler for Emscripten versions. +// 2023-12-19: Emscripten: Added ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback() to register canvas selector and auto-resize GLFW window. +// 2023-10-05: Inputs: Added support for extra ImGuiKey values: F13 to F24 function keys. +// 2023-07-18: Inputs: Revert ignoring mouse data on GLFW_CURSOR_DISABLED as it can be used differently. User may set ImGuiConfigFLags_NoMouse if desired. (#5625, #6609) +// 2023-06-12: Accept glfwGetTime() not returning a monotonically increasing value. This seems to happens on some Windows setup when peripherals disconnect, and is likely to also happen on browser + Emscripten. (#6491) +// 2023-04-04: Inputs: Added support for io.AddMouseSourceEvent() to discriminate ImGuiMouseSource_Mouse/ImGuiMouseSource_TouchScreen/ImGuiMouseSource_Pen on Windows ONLY, using a custom WndProc hook. (#2702) +// 2023-03-16: Inputs: Fixed key modifiers handling on secondary viewports (docking branch). Broken on 2023/01/04. (#6248, #6034) +// 2023-03-14: Emscripten: Avoid using glfwGetError() and glfwGetGamepadState() which are not correctly implemented in Emscripten emulation. (#6240) +// 2023-02-03: Emscripten: Registering custom low-level mouse wheel handler to get more accurate scrolling impulses on Emscripten. (#4019, #6096) +// 2023-01-18: Handle unsupported glfwGetVideoMode() call on e.g. Emscripten. +// 2023-01-04: Inputs: Fixed mods state on Linux when using Alt-GR text input (e.g. German keyboard layout), could lead to broken text input. Revert a 2022/01/17 change were we resumed using mods provided by GLFW, turns out they were faulty. +// 2022-11-22: Perform a dummy glfwGetError() read to cancel missing names with glfwGetKeyName(). (#5908) +// 2022-10-18: Perform a dummy glfwGetError() read to cancel missing mouse cursors errors. Using GLFW_VERSION_COMBINED directly. (#5785) +// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11. +// 2022-09-26: Inputs: Renamed ImGuiKey_ModXXX introduced in 1.87 to ImGuiMod_XXX (old names still supported). +// 2022-09-01: Inputs: Honor GLFW_CURSOR_DISABLED by not setting mouse position *EDIT* Reverted 2023-07-18. +// 2022-04-30: Inputs: Fixed ImGui_ImplGlfw_TranslateUntranslatedKey() for lower case letters on OSX. +// 2022-03-23: Inputs: Fixed a regression in 1.87 which resulted in keyboard modifiers events being reported incorrectly on Linux/X11. +// 2022-02-07: Added ImGui_ImplGlfw_InstallCallbacks()/ImGui_ImplGlfw_RestoreCallbacks() helpers to facilitate user installing callbacks after initializing backend. +// 2022-01-26: Inputs: replaced short-lived io.AddKeyModsEvent() (added two weeks ago) with io.AddKeyEvent() using ImGuiKey_ModXXX flags. Sorry for the confusion. +// 2021-01-20: Inputs: calling new io.AddKeyAnalogEvent() for gamepad support, instead of writing directly to io.NavInputs[]. +// 2022-01-17: Inputs: calling new io.AddMousePosEvent(), io.AddMouseButtonEvent(), io.AddMouseWheelEvent() API (1.87+). +// 2022-01-17: Inputs: always update key mods next and before key event (not in NewFrame) to fix input queue with very low framerates. +// 2022-01-12: *BREAKING CHANGE*: Now using glfwSetCursorPosCallback(). If you called ImGui_ImplGlfw_InitXXX() with install_callbacks = false, you MUST install glfwSetCursorPosCallback() and forward it to the backend via ImGui_ImplGlfw_CursorPosCallback(). +// 2022-01-10: Inputs: calling new io.AddKeyEvent(), io.AddKeyModsEvent() + io.SetKeyEventNativeData() API (1.87+). Support for full ImGuiKey range. +// 2022-01-05: Inputs: Converting GLFW untranslated keycodes back to translated keycodes (in the ImGui_ImplGlfw_KeyCallback() function) in order to match the behavior of every other backend, and facilitate the use of GLFW with lettered-shortcuts API. +// 2021-08-17: *BREAKING CHANGE*: Now using glfwSetWindowFocusCallback() to calling io.AddFocusEvent(). If you called ImGui_ImplGlfw_InitXXX() with install_callbacks = false, you MUST install glfwSetWindowFocusCallback() and forward it to the backend via ImGui_ImplGlfw_WindowFocusCallback(). +// 2021-07-29: *BREAKING CHANGE*: Now using glfwSetCursorEnterCallback(). MousePos is correctly reported when the host platform window is hovered but not focused. If you called ImGui_ImplGlfw_InitXXX() with install_callbacks = false, you MUST install glfwSetWindowFocusCallback() callback and forward it to the backend via ImGui_ImplGlfw_CursorEnterCallback(). +// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX). +// 2020-01-17: Inputs: Disable error callback while assigning mouse cursors because some X11 setup don't have them and it generates errors. +// 2019-12-05: Inputs: Added support for new mouse cursors added in GLFW 3.4+ (resizing cursors, not allowed cursor). +// 2019-10-18: Misc: Previously installed user callbacks are now restored on shutdown. // 2019-07-21: Inputs: Added mapping for ImGuiKey_KeyPadEnter. // 2019-05-11: Inputs: Don't filter value from character callback before calling AddInputCharacter(). // 2019-03-12: Misc: Preserve DisplayFramebufferScale when main window is minimized. @@ -35,161 +94,631 @@ // 2016-10-15: Misc: Added a void* user_data parameter to Clipboard function handlers. #include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_impl_glfw.h" +// Clang warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#endif + // GLFW #include + #ifdef _WIN32 #undef APIENTRY +#ifndef GLFW_EXPOSE_NATIVE_WIN32 #define GLFW_EXPOSE_NATIVE_WIN32 -#include // for glfwGetWin32Window #endif -#define GLFW_HAS_WINDOW_TOPMOST (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3200) // 3.2+ GLFW_FLOATING -#define GLFW_HAS_WINDOW_HOVERED (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3300) // 3.3+ GLFW_HOVERED -#define GLFW_HAS_WINDOW_ALPHA (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3300) // 3.3+ glfwSetWindowOpacity -#define GLFW_HAS_PER_MONITOR_DPI (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3300) // 3.3+ glfwGetMonitorContentScale -#define GLFW_HAS_VULKAN (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 >= 3200) // 3.2+ glfwCreateWindowSurface +#include // for glfwGetWin32Window() +#endif +#ifdef __APPLE__ +#ifndef GLFW_EXPOSE_NATIVE_COCOA +#define GLFW_EXPOSE_NATIVE_COCOA +#endif +#include // for glfwGetCocoaWindow() +#endif +#ifndef _WIN32 +#include // for usleep() +#endif -// Data +#ifdef __EMSCRIPTEN__ +#include +#include +#ifdef EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 +#include +#else +#define EMSCRIPTEN_USE_EMBEDDED_GLFW3 +#endif +#endif + +// We gather version tests as define in order to easily see which features are version-dependent. +#define GLFW_VERSION_COMBINED (GLFW_VERSION_MAJOR * 1000 + GLFW_VERSION_MINOR * 100 + GLFW_VERSION_REVISION) +#define GLFW_HAS_WINDOW_TOPMOST (GLFW_VERSION_COMBINED >= 3200) // 3.2+ GLFW_FLOATING +#define GLFW_HAS_WINDOW_HOVERED (GLFW_VERSION_COMBINED >= 3300) // 3.3+ GLFW_HOVERED +#define GLFW_HAS_WINDOW_ALPHA (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwSetWindowOpacity +#define GLFW_HAS_PER_MONITOR_DPI (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetMonitorContentScale +#if defined(__EMSCRIPTEN__) || defined(__SWITCH__) // no Vulkan support in GLFW for Emscripten or homebrew Nintendo Switch +#define GLFW_HAS_VULKAN (0) +#else +#define GLFW_HAS_VULKAN (GLFW_VERSION_COMBINED >= 3200) // 3.2+ glfwCreateWindowSurface +#endif +#define GLFW_HAS_FOCUS_WINDOW (GLFW_VERSION_COMBINED >= 3200) // 3.2+ glfwFocusWindow +#define GLFW_HAS_FOCUS_ON_SHOW (GLFW_VERSION_COMBINED >= 3300) // 3.3+ GLFW_FOCUS_ON_SHOW +#define GLFW_HAS_MONITOR_WORK_AREA (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetMonitorWorkarea +#define GLFW_HAS_OSX_WINDOW_POS_FIX (GLFW_VERSION_COMBINED >= 3301) // 3.3.1+ Fixed: Resizing window repositions it on MacOS #1553 +#ifdef GLFW_RESIZE_NESW_CURSOR // Let's be nice to people who pulled GLFW between 2019-04-16 (3.4 define) and 2019-11-29 (cursors defines) // FIXME: Remove when GLFW 3.4 is released? +#define GLFW_HAS_NEW_CURSORS (GLFW_VERSION_COMBINED >= 3400) // 3.4+ GLFW_RESIZE_ALL_CURSOR, GLFW_RESIZE_NESW_CURSOR, GLFW_RESIZE_NWSE_CURSOR, GLFW_NOT_ALLOWED_CURSOR +#else +#define GLFW_HAS_NEW_CURSORS (0) +#endif +#ifdef GLFW_MOUSE_PASSTHROUGH // Let's be nice to people who pulled GLFW between 2019-04-16 (3.4 define) and 2020-07-17 (passthrough) +#define GLFW_HAS_MOUSE_PASSTHROUGH (GLFW_VERSION_COMBINED >= 3400) // 3.4+ GLFW_MOUSE_PASSTHROUGH +#else +#define GLFW_HAS_MOUSE_PASSTHROUGH (0) +#endif +#define GLFW_HAS_GAMEPAD_API (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetGamepadState() new api +#define GLFW_HAS_GETKEYNAME (GLFW_VERSION_COMBINED >= 3200) // 3.2+ glfwGetKeyName() +#define GLFW_HAS_GETERROR (GLFW_VERSION_COMBINED >= 3300) // 3.3+ glfwGetError() + +// GLFW data enum GlfwClientApi { GlfwClientApi_Unknown, GlfwClientApi_OpenGL, - GlfwClientApi_Vulkan + GlfwClientApi_Vulkan, }; -static GLFWwindow* g_Window = NULL; // Main window -static GlfwClientApi g_ClientApi = GlfwClientApi_Unknown; -static double g_Time = 0.0; -static bool g_MouseJustPressed[5] = { false, false, false, false, false }; -static GLFWcursor* g_MouseCursors[ImGuiMouseCursor_COUNT] = { 0 }; -// Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any. -static GLFWmousebuttonfun g_PrevUserCallbackMousebutton = NULL; -static GLFWscrollfun g_PrevUserCallbackScroll = NULL; -static GLFWkeyfun g_PrevUserCallbackKey = NULL; -static GLFWcharfun g_PrevUserCallbackChar = NULL; - -static const char* ImGui_ImplGlfw_GetClipboardText(void* user_data) +struct ImGui_ImplGlfw_Data { - return glfwGetClipboardString((GLFWwindow*)user_data); + GLFWwindow* Window; + GlfwClientApi ClientApi; + double Time; + GLFWwindow* MouseWindow; + GLFWcursor* MouseCursors[ImGuiMouseCursor_COUNT]; + bool MouseIgnoreButtonUpWaitForFocusLoss; + bool MouseIgnoreButtonUp; + ImVec2 LastValidMousePos; + ImVec2 MainWindowOrigin; // MaSzyna: desktop position of the main window, see ImGui_ImplGlfw_UpdateMainWindowOrigin() + GLFWwindow* KeyOwnerWindows[GLFW_KEY_LAST]; + bool InstalledCallbacks; + bool CallbacksChainForAllWindows; +#ifdef EMSCRIPTEN_USE_EMBEDDED_GLFW3 + const char* CanvasSelector; +#endif + + // Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any. + GLFWwindowfocusfun PrevUserCallbackWindowFocus; + GLFWcursorposfun PrevUserCallbackCursorPos; + GLFWcursorenterfun PrevUserCallbackCursorEnter; + GLFWmousebuttonfun PrevUserCallbackMousebutton; + GLFWscrollfun PrevUserCallbackScroll; + GLFWkeyfun PrevUserCallbackKey; + GLFWcharfun PrevUserCallbackChar; + GLFWmonitorfun PrevUserCallbackMonitor; +#ifdef _WIN32 + WNDPROC PrevWndProc; +#endif + + ImGui_ImplGlfw_Data() { memset((void*)this, 0, sizeof(*this)); } +}; + +// Backend data stored in io.BackendPlatformUserData to allow support for multiple Dear ImGui contexts +// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts. +// FIXME: multi-context support is not well tested and probably dysfunctional in this backend. +// - Because glfwPollEvents() process all windows and some events may be called outside of it, you will need to register your own callbacks +// (passing install_callbacks=false in ImGui_ImplGlfw_InitXXX functions), set the current dear imgui context and then call our callbacks. +// - Otherwise we may need to store a GLFWWindow* -> ImGuiContext* map and handle this in the backend, adding a little bit of extra complexity to it. +// FIXME: some shared resources (mouse cursor shape, gamepad) are mishandled when using multi-context. +static ImGui_ImplGlfw_Data* ImGui_ImplGlfw_GetBackendData() +{ + return ImGui::GetCurrentContext() ? (ImGui_ImplGlfw_Data*)ImGui::GetIO().BackendPlatformUserData : nullptr; } -static void ImGui_ImplGlfw_SetClipboardText(void* user_data, const char* text) +// Forward Declarations +static void ImGui_ImplGlfw_UpdateMonitors(); +static void ImGui_ImplGlfw_InitMultiViewportSupport(); +static void ImGui_ImplGlfw_UpdateMainWindowOrigin(); +static void ImGui_ImplGlfw_ShutdownMultiViewportSupport(); + +// Functions + +// Not static to allow third-party code to use that if they want to (but undocumented) +ImGuiKey ImGui_ImplGlfw_KeyToImGuiKey(int keycode, int scancode); +ImGuiKey ImGui_ImplGlfw_KeyToImGuiKey(int keycode, int scancode) { - glfwSetClipboardString((GLFWwindow*)user_data, text); + IM_UNUSED(scancode); + switch (keycode) + { + case GLFW_KEY_TAB: return ImGuiKey_Tab; + case GLFW_KEY_LEFT: return ImGuiKey_LeftArrow; + case GLFW_KEY_RIGHT: return ImGuiKey_RightArrow; + case GLFW_KEY_UP: return ImGuiKey_UpArrow; + case GLFW_KEY_DOWN: return ImGuiKey_DownArrow; + case GLFW_KEY_PAGE_UP: return ImGuiKey_PageUp; + case GLFW_KEY_PAGE_DOWN: return ImGuiKey_PageDown; + case GLFW_KEY_HOME: return ImGuiKey_Home; + case GLFW_KEY_END: return ImGuiKey_End; + case GLFW_KEY_INSERT: return ImGuiKey_Insert; + case GLFW_KEY_DELETE: return ImGuiKey_Delete; + case GLFW_KEY_BACKSPACE: return ImGuiKey_Backspace; + case GLFW_KEY_SPACE: return ImGuiKey_Space; + case GLFW_KEY_ENTER: return ImGuiKey_Enter; + case GLFW_KEY_ESCAPE: return ImGuiKey_Escape; + case GLFW_KEY_APOSTROPHE: return ImGuiKey_Apostrophe; + case GLFW_KEY_COMMA: return ImGuiKey_Comma; + case GLFW_KEY_MINUS: return ImGuiKey_Minus; + case GLFW_KEY_PERIOD: return ImGuiKey_Period; + case GLFW_KEY_SLASH: return ImGuiKey_Slash; + case GLFW_KEY_SEMICOLON: return ImGuiKey_Semicolon; + case GLFW_KEY_EQUAL: return ImGuiKey_Equal; + case GLFW_KEY_LEFT_BRACKET: return ImGuiKey_LeftBracket; + case GLFW_KEY_BACKSLASH: return ImGuiKey_Backslash; + case GLFW_KEY_WORLD_1: return ImGuiKey_Oem102; + case GLFW_KEY_WORLD_2: return ImGuiKey_Oem102; + case GLFW_KEY_RIGHT_BRACKET: return ImGuiKey_RightBracket; + case GLFW_KEY_GRAVE_ACCENT: return ImGuiKey_GraveAccent; + case GLFW_KEY_CAPS_LOCK: return ImGuiKey_CapsLock; + case GLFW_KEY_SCROLL_LOCK: return ImGuiKey_ScrollLock; + case GLFW_KEY_NUM_LOCK: return ImGuiKey_NumLock; + case GLFW_KEY_PRINT_SCREEN: return ImGuiKey_PrintScreen; + case GLFW_KEY_PAUSE: return ImGuiKey_Pause; + case GLFW_KEY_KP_0: return ImGuiKey_Keypad0; + case GLFW_KEY_KP_1: return ImGuiKey_Keypad1; + case GLFW_KEY_KP_2: return ImGuiKey_Keypad2; + case GLFW_KEY_KP_3: return ImGuiKey_Keypad3; + case GLFW_KEY_KP_4: return ImGuiKey_Keypad4; + case GLFW_KEY_KP_5: return ImGuiKey_Keypad5; + case GLFW_KEY_KP_6: return ImGuiKey_Keypad6; + case GLFW_KEY_KP_7: return ImGuiKey_Keypad7; + case GLFW_KEY_KP_8: return ImGuiKey_Keypad8; + case GLFW_KEY_KP_9: return ImGuiKey_Keypad9; + case GLFW_KEY_KP_DECIMAL: return ImGuiKey_KeypadDecimal; + case GLFW_KEY_KP_DIVIDE: return ImGuiKey_KeypadDivide; + case GLFW_KEY_KP_MULTIPLY: return ImGuiKey_KeypadMultiply; + case GLFW_KEY_KP_SUBTRACT: return ImGuiKey_KeypadSubtract; + case GLFW_KEY_KP_ADD: return ImGuiKey_KeypadAdd; + case GLFW_KEY_KP_ENTER: return ImGuiKey_KeypadEnter; + case GLFW_KEY_KP_EQUAL: return ImGuiKey_KeypadEqual; + case GLFW_KEY_LEFT_SHIFT: return ImGuiKey_LeftShift; + case GLFW_KEY_LEFT_CONTROL: return ImGuiKey_LeftCtrl; + case GLFW_KEY_LEFT_ALT: return ImGuiKey_LeftAlt; + case GLFW_KEY_LEFT_SUPER: return ImGuiKey_LeftSuper; + case GLFW_KEY_RIGHT_SHIFT: return ImGuiKey_RightShift; + case GLFW_KEY_RIGHT_CONTROL: return ImGuiKey_RightCtrl; + case GLFW_KEY_RIGHT_ALT: return ImGuiKey_RightAlt; + case GLFW_KEY_RIGHT_SUPER: return ImGuiKey_RightSuper; + case GLFW_KEY_MENU: return ImGuiKey_Menu; + case GLFW_KEY_0: return ImGuiKey_0; + case GLFW_KEY_1: return ImGuiKey_1; + case GLFW_KEY_2: return ImGuiKey_2; + case GLFW_KEY_3: return ImGuiKey_3; + case GLFW_KEY_4: return ImGuiKey_4; + case GLFW_KEY_5: return ImGuiKey_5; + case GLFW_KEY_6: return ImGuiKey_6; + case GLFW_KEY_7: return ImGuiKey_7; + case GLFW_KEY_8: return ImGuiKey_8; + case GLFW_KEY_9: return ImGuiKey_9; + case GLFW_KEY_A: return ImGuiKey_A; + case GLFW_KEY_B: return ImGuiKey_B; + case GLFW_KEY_C: return ImGuiKey_C; + case GLFW_KEY_D: return ImGuiKey_D; + case GLFW_KEY_E: return ImGuiKey_E; + case GLFW_KEY_F: return ImGuiKey_F; + case GLFW_KEY_G: return ImGuiKey_G; + case GLFW_KEY_H: return ImGuiKey_H; + case GLFW_KEY_I: return ImGuiKey_I; + case GLFW_KEY_J: return ImGuiKey_J; + case GLFW_KEY_K: return ImGuiKey_K; + case GLFW_KEY_L: return ImGuiKey_L; + case GLFW_KEY_M: return ImGuiKey_M; + case GLFW_KEY_N: return ImGuiKey_N; + case GLFW_KEY_O: return ImGuiKey_O; + case GLFW_KEY_P: return ImGuiKey_P; + case GLFW_KEY_Q: return ImGuiKey_Q; + case GLFW_KEY_R: return ImGuiKey_R; + case GLFW_KEY_S: return ImGuiKey_S; + case GLFW_KEY_T: return ImGuiKey_T; + case GLFW_KEY_U: return ImGuiKey_U; + case GLFW_KEY_V: return ImGuiKey_V; + case GLFW_KEY_W: return ImGuiKey_W; + case GLFW_KEY_X: return ImGuiKey_X; + case GLFW_KEY_Y: return ImGuiKey_Y; + case GLFW_KEY_Z: return ImGuiKey_Z; + case GLFW_KEY_F1: return ImGuiKey_F1; + case GLFW_KEY_F2: return ImGuiKey_F2; + case GLFW_KEY_F3: return ImGuiKey_F3; + case GLFW_KEY_F4: return ImGuiKey_F4; + case GLFW_KEY_F5: return ImGuiKey_F5; + case GLFW_KEY_F6: return ImGuiKey_F6; + case GLFW_KEY_F7: return ImGuiKey_F7; + case GLFW_KEY_F8: return ImGuiKey_F8; + case GLFW_KEY_F9: return ImGuiKey_F9; + case GLFW_KEY_F10: return ImGuiKey_F10; + case GLFW_KEY_F11: return ImGuiKey_F11; + case GLFW_KEY_F12: return ImGuiKey_F12; + case GLFW_KEY_F13: return ImGuiKey_F13; + case GLFW_KEY_F14: return ImGuiKey_F14; + case GLFW_KEY_F15: return ImGuiKey_F15; + case GLFW_KEY_F16: return ImGuiKey_F16; + case GLFW_KEY_F17: return ImGuiKey_F17; + case GLFW_KEY_F18: return ImGuiKey_F18; + case GLFW_KEY_F19: return ImGuiKey_F19; + case GLFW_KEY_F20: return ImGuiKey_F20; + case GLFW_KEY_F21: return ImGuiKey_F21; + case GLFW_KEY_F22: return ImGuiKey_F22; + case GLFW_KEY_F23: return ImGuiKey_F23; + case GLFW_KEY_F24: return ImGuiKey_F24; + default: return ImGuiKey_None; + } +} + +// X11 does not include current pressed/released modifier key in 'mods' flags submitted by GLFW +// See https://github.com/ocornut/imgui/issues/6034 and https://github.com/glfw/glfw/issues/1630 +static void ImGui_ImplGlfw_UpdateKeyModifiers(GLFWwindow* window) +{ + ImGuiIO& io = ImGui::GetIO(); + io.AddKeyEvent(ImGuiMod_Ctrl, (glfwGetKey(window, GLFW_KEY_LEFT_CONTROL) == GLFW_PRESS) || (glfwGetKey(window, GLFW_KEY_RIGHT_CONTROL) == GLFW_PRESS)); + io.AddKeyEvent(ImGuiMod_Shift, (glfwGetKey(window, GLFW_KEY_LEFT_SHIFT) == GLFW_PRESS) || (glfwGetKey(window, GLFW_KEY_RIGHT_SHIFT) == GLFW_PRESS)); + io.AddKeyEvent(ImGuiMod_Alt, (glfwGetKey(window, GLFW_KEY_LEFT_ALT) == GLFW_PRESS) || (glfwGetKey(window, GLFW_KEY_RIGHT_ALT) == GLFW_PRESS)); + io.AddKeyEvent(ImGuiMod_Super, (glfwGetKey(window, GLFW_KEY_LEFT_SUPER) == GLFW_PRESS) || (glfwGetKey(window, GLFW_KEY_RIGHT_SUPER) == GLFW_PRESS)); +} + +static bool ImGui_ImplGlfw_ShouldChainCallback(GLFWwindow* window) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + return bd->CallbacksChainForAllWindows ? true : (window == bd->Window); } void ImGui_ImplGlfw_MouseButtonCallback(GLFWwindow* window, int button, int action, int mods) { - if (g_PrevUserCallbackMousebutton != NULL) - g_PrevUserCallbackMousebutton(window, button, action, mods); + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (bd->PrevUserCallbackMousebutton != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) + bd->PrevUserCallbackMousebutton(window, button, action, mods); - if (action == GLFW_PRESS && button >= 0 && button < IM_ARRAYSIZE(g_MouseJustPressed)) - g_MouseJustPressed[button] = true; + // Workaround for Linux: ignore mouse up events which are following an focus loss following a viewport creation + if (bd->MouseIgnoreButtonUp && action == GLFW_RELEASE) + return; + + ImGui_ImplGlfw_UpdateKeyModifiers(window); + + ImGuiIO& io = ImGui::GetIO(); + if (button >= 0 && button < ImGuiMouseButton_COUNT) + io.AddMouseButtonEvent(button, action == GLFW_PRESS); } void ImGui_ImplGlfw_ScrollCallback(GLFWwindow* window, double xoffset, double yoffset) { - if (g_PrevUserCallbackScroll != NULL) - g_PrevUserCallbackScroll(window, xoffset, yoffset); + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (bd->PrevUserCallbackScroll != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) + bd->PrevUserCallbackScroll(window, xoffset, yoffset); + +#ifdef EMSCRIPTEN_USE_EMBEDDED_GLFW3 + // Ignore GLFW events: will be processed in ImGui_ImplEmscripten_WheelCallback(). + return; +#endif ImGuiIO& io = ImGui::GetIO(); - io.MouseWheelH += (float)xoffset; - io.MouseWheel += (float)yoffset; + io.AddMouseWheelEvent((float)xoffset, (float)yoffset); } -void ImGui_ImplGlfw_KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) +// FIXME: should this be baked into ImGui_ImplGlfw_KeyToImGuiKey()? then what about the values passed to io.SetKeyEventNativeData()? +static int ImGui_ImplGlfw_TranslateUntranslatedKey(int key, int scancode) { - if (g_PrevUserCallbackKey != NULL) - g_PrevUserCallbackKey(window, key, scancode, action, mods); +#if GLFW_HAS_GETKEYNAME && !defined(EMSCRIPTEN_USE_EMBEDDED_GLFW3) + // GLFW 3.1+ attempts to "untranslate" keys, which goes the opposite of what every other framework does, making using lettered shortcuts difficult. + // (It had reasons to do so: namely GLFW is/was more likely to be used for WASD-type game controls rather than lettered shortcuts, but IHMO the 3.1 change could have been done differently) + // See https://github.com/glfw/glfw/issues/1502 for details. + // Adding a workaround to undo this (so our keys are translated->untranslated->translated, likely a lossy process). + // This won't cover edge cases but this is at least going to cover common cases. + if (key >= GLFW_KEY_KP_0 && key <= GLFW_KEY_KP_EQUAL) + return key; + GLFWerrorfun prev_error_callback = glfwSetErrorCallback(nullptr); + const char* key_name = glfwGetKeyName(key, scancode); + glfwSetErrorCallback(prev_error_callback); +#if GLFW_HAS_GETERROR && !defined(EMSCRIPTEN_USE_EMBEDDED_GLFW3) // Eat errors (see #5908) + (void)glfwGetError(nullptr); +#endif + if (key_name && key_name[0] != 0 && key_name[1] == 0) + { + const char char_names[] = "`-=[]\\,;\'./"; + const int char_keys[] = { GLFW_KEY_GRAVE_ACCENT, GLFW_KEY_MINUS, GLFW_KEY_EQUAL, GLFW_KEY_LEFT_BRACKET, GLFW_KEY_RIGHT_BRACKET, GLFW_KEY_BACKSLASH, GLFW_KEY_COMMA, GLFW_KEY_SEMICOLON, GLFW_KEY_APOSTROPHE, GLFW_KEY_PERIOD, GLFW_KEY_SLASH, 0 }; + IM_ASSERT(IM_ARRAYSIZE(char_names) == IM_ARRAYSIZE(char_keys)); + if (key_name[0] >= '0' && key_name[0] <= '9') { key = GLFW_KEY_0 + (key_name[0] - '0'); } + else if (key_name[0] >= 'A' && key_name[0] <= 'Z') { key = GLFW_KEY_A + (key_name[0] - 'A'); } + else if (key_name[0] >= 'a' && key_name[0] <= 'z') { key = GLFW_KEY_A + (key_name[0] - 'a'); } + else if (const char* p = strchr(char_names, key_name[0])) { key = char_keys[p - char_names]; } + } + // if (action == GLFW_PRESS) printf("key %d scancode %d name '%s'\n", key, scancode, key_name); +#else + IM_UNUSED(scancode); +#endif + return key; +} + +void ImGui_ImplGlfw_KeyCallback(GLFWwindow* window, int keycode, int scancode, int action, int mods) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (bd->PrevUserCallbackKey != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) + bd->PrevUserCallbackKey(window, keycode, scancode, action, mods); + + if (action != GLFW_PRESS && action != GLFW_RELEASE) + return; + + ImGui_ImplGlfw_UpdateKeyModifiers(window); + + if (keycode >= 0 && keycode < IM_ARRAYSIZE(bd->KeyOwnerWindows)) + bd->KeyOwnerWindows[keycode] = (action == GLFW_PRESS) ? window : nullptr; + + keycode = ImGui_ImplGlfw_TranslateUntranslatedKey(keycode, scancode); ImGuiIO& io = ImGui::GetIO(); - if (action == GLFW_PRESS) - io.KeysDown[key] = true; - if (action == GLFW_RELEASE) - io.KeysDown[key] = false; + ImGuiKey imgui_key = ImGui_ImplGlfw_KeyToImGuiKey(keycode, scancode); + io.AddKeyEvent(imgui_key, (action == GLFW_PRESS)); + io.SetKeyEventNativeData(imgui_key, keycode, scancode); // To support legacy indexing (<1.87 user code) +} - // Modifiers are not reliable across systems - io.KeyCtrl = io.KeysDown[GLFW_KEY_LEFT_CONTROL] || io.KeysDown[GLFW_KEY_RIGHT_CONTROL]; - io.KeyShift = io.KeysDown[GLFW_KEY_LEFT_SHIFT] || io.KeysDown[GLFW_KEY_RIGHT_SHIFT]; - io.KeyAlt = io.KeysDown[GLFW_KEY_LEFT_ALT] || io.KeysDown[GLFW_KEY_RIGHT_ALT]; - io.KeySuper = io.KeysDown[GLFW_KEY_LEFT_SUPER] || io.KeysDown[GLFW_KEY_RIGHT_SUPER]; +void ImGui_ImplGlfw_WindowFocusCallback(GLFWwindow* window, int focused) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (bd->PrevUserCallbackWindowFocus != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) + bd->PrevUserCallbackWindowFocus(window, focused); + + // Workaround for Linux: when losing focus with MouseIgnoreButtonUpWaitForFocusLoss set, we will temporarily ignore subsequent Mouse Up events + bd->MouseIgnoreButtonUp = (bd->MouseIgnoreButtonUpWaitForFocusLoss && focused == 0); + bd->MouseIgnoreButtonUpWaitForFocusLoss = false; + + ImGuiIO& io = ImGui::GetIO(); + io.AddFocusEvent(focused != 0); +} + +void ImGui_ImplGlfw_CursorPosCallback(GLFWwindow* window, double x, double y) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (bd->PrevUserCallbackCursorPos != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) + bd->PrevUserCallbackCursorPos(window, x, y); + + ImGuiIO& io = ImGui::GetIO(); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + int window_x, window_y; + glfwGetWindowPos(window, &window_x, &window_y); + x += window_x - bd->MainWindowOrigin.x; + y += window_y - bd->MainWindowOrigin.y; + } + io.AddMousePosEvent((float)x, (float)y); + bd->LastValidMousePos = ImVec2((float)x, (float)y); +} + +// Workaround: X11 seems to send spurious Leave/Enter events which would make us lose our position, +// so we back it up and restore on Leave/Enter (see https://github.com/ocornut/imgui/issues/4984) +void ImGui_ImplGlfw_CursorEnterCallback(GLFWwindow* window, int entered) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (bd->PrevUserCallbackCursorEnter != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) + bd->PrevUserCallbackCursorEnter(window, entered); + + ImGuiIO& io = ImGui::GetIO(); + if (entered) + { + bd->MouseWindow = window; + io.AddMousePosEvent(bd->LastValidMousePos.x, bd->LastValidMousePos.y); + } + else if (!entered && bd->MouseWindow == window) + { + bd->LastValidMousePos = io.MousePos; + bd->MouseWindow = nullptr; + io.AddMousePosEvent(-FLT_MAX, -FLT_MAX); + } } void ImGui_ImplGlfw_CharCallback(GLFWwindow* window, unsigned int c) { - if (g_PrevUserCallbackChar != NULL) - g_PrevUserCallbackChar(window, c); + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (bd->PrevUserCallbackChar != nullptr && ImGui_ImplGlfw_ShouldChainCallback(window)) + bd->PrevUserCallbackChar(window, c); ImGuiIO& io = ImGui::GetIO(); io.AddInputCharacter(c); } -static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, GlfwClientApi client_api) +void ImGui_ImplGlfw_MonitorCallback(GLFWmonitor*, int) { - g_Window = window; - g_Time = 0.0; + // This function is technically part of the API even if we stopped using the callback, so leaving it around. +} - // Setup back-end capabilities flags +#ifdef EMSCRIPTEN_USE_EMBEDDED_GLFW3 +static EM_BOOL ImGui_ImplEmscripten_WheelCallback(int, const EmscriptenWheelEvent* ev, void*) +{ + // Mimic Emscripten_HandleWheel() in SDL. + // Corresponding equivalent in GLFW JS emulation layer has incorrect quantizing preventing small values. See #6096 + float multiplier = 0.0f; + if (ev->deltaMode == DOM_DELTA_PIXEL) { multiplier = 1.0f / 100.0f; } // 100 pixels make up a step. + else if (ev->deltaMode == DOM_DELTA_LINE) { multiplier = 1.0f / 3.0f; } // 3 lines make up a step. + else if (ev->deltaMode == DOM_DELTA_PAGE) { multiplier = 80.0f; } // A page makes up 80 steps. + float wheel_x = ev->deltaX * -multiplier; + float wheel_y = ev->deltaY * -multiplier; ImGuiIO& io = ImGui::GetIO(); - io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) - io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used) - io.BackendPlatformName = "imgui_impl_glfw"; - - // Keyboard mapping. ImGui will use those indices to peek into the io.KeysDown[] array. - io.KeyMap[ImGuiKey_Tab] = GLFW_KEY_TAB; - io.KeyMap[ImGuiKey_LeftArrow] = GLFW_KEY_LEFT; - io.KeyMap[ImGuiKey_RightArrow] = GLFW_KEY_RIGHT; - io.KeyMap[ImGuiKey_UpArrow] = GLFW_KEY_UP; - io.KeyMap[ImGuiKey_DownArrow] = GLFW_KEY_DOWN; - io.KeyMap[ImGuiKey_PageUp] = GLFW_KEY_PAGE_UP; - io.KeyMap[ImGuiKey_PageDown] = GLFW_KEY_PAGE_DOWN; - io.KeyMap[ImGuiKey_Home] = GLFW_KEY_HOME; - io.KeyMap[ImGuiKey_End] = GLFW_KEY_END; - io.KeyMap[ImGuiKey_Insert] = GLFW_KEY_INSERT; - io.KeyMap[ImGuiKey_Delete] = GLFW_KEY_DELETE; - io.KeyMap[ImGuiKey_Backspace] = GLFW_KEY_BACKSPACE; - io.KeyMap[ImGuiKey_Space] = GLFW_KEY_SPACE; - io.KeyMap[ImGuiKey_Enter] = GLFW_KEY_ENTER; - io.KeyMap[ImGuiKey_Escape] = GLFW_KEY_ESCAPE; - io.KeyMap[ImGuiKey_KeyPadEnter] = GLFW_KEY_KP_ENTER; - io.KeyMap[ImGuiKey_A] = GLFW_KEY_A; - io.KeyMap[ImGuiKey_C] = GLFW_KEY_C; - io.KeyMap[ImGuiKey_V] = GLFW_KEY_V; - io.KeyMap[ImGuiKey_X] = GLFW_KEY_X; - io.KeyMap[ImGuiKey_Y] = GLFW_KEY_Y; - io.KeyMap[ImGuiKey_Z] = GLFW_KEY_Z; - - io.SetClipboardTextFn = ImGui_ImplGlfw_SetClipboardText; - io.GetClipboardTextFn = ImGui_ImplGlfw_GetClipboardText; - io.ClipboardUserData = g_Window; -#if defined(_WIN32) - io.ImeWindowHandle = (void*)glfwGetWin32Window(g_Window); + io.AddMouseWheelEvent(wheel_x, wheel_y); + //IMGUI_DEBUG_LOG("[Emsc] mode %d dx: %.2f, dy: %.2f, dz: %.2f --> feed %.2f %.2f\n", (int)ev->deltaMode, ev->deltaX, ev->deltaY, ev->deltaZ, wheel_x, wheel_y); + return EM_TRUE; +} #endif - g_MouseCursors[ImGuiMouseCursor_Arrow] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); - g_MouseCursors[ImGuiMouseCursor_TextInput] = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR); - g_MouseCursors[ImGuiMouseCursor_ResizeAll] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); // FIXME: GLFW doesn't have this. - g_MouseCursors[ImGuiMouseCursor_ResizeNS] = glfwCreateStandardCursor(GLFW_VRESIZE_CURSOR); - g_MouseCursors[ImGuiMouseCursor_ResizeEW] = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR); - g_MouseCursors[ImGuiMouseCursor_ResizeNESW] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); // FIXME: GLFW doesn't have this. - g_MouseCursors[ImGuiMouseCursor_ResizeNWSE] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); // FIXME: GLFW doesn't have this. - g_MouseCursors[ImGuiMouseCursor_Hand] = glfwCreateStandardCursor(GLFW_HAND_CURSOR); +#ifdef _WIN32 +static LRESULT CALLBACK ImGui_ImplGlfw_WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +#endif + +void ImGui_ImplGlfw_InstallCallbacks(GLFWwindow* window) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + IM_ASSERT(bd->InstalledCallbacks == false && "Callbacks already installed!"); + IM_ASSERT(bd->Window == window); + + bd->PrevUserCallbackWindowFocus = glfwSetWindowFocusCallback(window, ImGui_ImplGlfw_WindowFocusCallback); + bd->PrevUserCallbackCursorEnter = glfwSetCursorEnterCallback(window, ImGui_ImplGlfw_CursorEnterCallback); + bd->PrevUserCallbackCursorPos = glfwSetCursorPosCallback(window, ImGui_ImplGlfw_CursorPosCallback); + bd->PrevUserCallbackMousebutton = glfwSetMouseButtonCallback(window, ImGui_ImplGlfw_MouseButtonCallback); + bd->PrevUserCallbackScroll = glfwSetScrollCallback(window, ImGui_ImplGlfw_ScrollCallback); + bd->PrevUserCallbackKey = glfwSetKeyCallback(window, ImGui_ImplGlfw_KeyCallback); + bd->PrevUserCallbackChar = glfwSetCharCallback(window, ImGui_ImplGlfw_CharCallback); + bd->PrevUserCallbackMonitor = glfwSetMonitorCallback(ImGui_ImplGlfw_MonitorCallback); + bd->InstalledCallbacks = true; +} + +void ImGui_ImplGlfw_RestoreCallbacks(GLFWwindow* window) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + IM_ASSERT(bd->InstalledCallbacks == true && "Callbacks not installed!"); + IM_ASSERT(bd->Window == window); + + glfwSetWindowFocusCallback(window, bd->PrevUserCallbackWindowFocus); + glfwSetCursorEnterCallback(window, bd->PrevUserCallbackCursorEnter); + glfwSetCursorPosCallback(window, bd->PrevUserCallbackCursorPos); + glfwSetMouseButtonCallback(window, bd->PrevUserCallbackMousebutton); + glfwSetScrollCallback(window, bd->PrevUserCallbackScroll); + glfwSetKeyCallback(window, bd->PrevUserCallbackKey); + glfwSetCharCallback(window, bd->PrevUserCallbackChar); + glfwSetMonitorCallback(bd->PrevUserCallbackMonitor); + bd->InstalledCallbacks = false; + bd->PrevUserCallbackWindowFocus = nullptr; + bd->PrevUserCallbackCursorEnter = nullptr; + bd->PrevUserCallbackCursorPos = nullptr; + bd->PrevUserCallbackMousebutton = nullptr; + bd->PrevUserCallbackScroll = nullptr; + bd->PrevUserCallbackKey = nullptr; + bd->PrevUserCallbackChar = nullptr; + bd->PrevUserCallbackMonitor = nullptr; +} + +// Set to 'true' to enable chaining installed callbacks for all windows (including secondary viewports created by backends or by user. +// This is 'false' by default meaning we only chain callbacks for the main viewport. +// We cannot set this to 'true' by default because user callbacks code may be not testing the 'window' parameter of their callback. +// If you set this to 'true' your user callback code will need to make sure you are testing the 'window' parameter. +void ImGui_ImplGlfw_SetCallbacksChainForAllWindows(bool chain_for_all_windows) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + bd->CallbacksChainForAllWindows = chain_for_all_windows; +} + +#ifdef __EMSCRIPTEN__ +#if EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 >= 34020240817 +void ImGui_ImplGlfw_EmscriptenOpenURL(const char* url) { if (url) emscripten::glfw3::OpenURL(url); } +#else +EM_JS(void, ImGui_ImplGlfw_EmscriptenOpenURL, (const char* url), { url = url ? UTF8ToString(url) : null; if (url) window.open(url, '_blank'); }); +#endif +#endif + +static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, GlfwClientApi client_api) +{ + ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendPlatformUserData == nullptr && "Already initialized a platform backend!"); + //printf("GLFW_VERSION: %d.%d.%d (%d)", GLFW_VERSION_MAJOR, GLFW_VERSION_MINOR, GLFW_VERSION_REVISION, GLFW_VERSION_COMBINED); + + // Setup backend capabilities flags + ImGui_ImplGlfw_Data* bd = IM_NEW(ImGui_ImplGlfw_Data)(); + io.BackendPlatformUserData = (void*)bd; + io.BackendPlatformName = "imgui_impl_glfw"; + io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) + io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used) +#ifndef __EMSCRIPTEN__ + io.BackendFlags |= ImGuiBackendFlags_PlatformHasViewports; // We can create multi-viewports on the Platform side (optional) +#endif +#if GLFW_HAS_MOUSE_PASSTHROUGH || GLFW_HAS_WINDOW_HOVERED + io.BackendFlags |= ImGuiBackendFlags_HasMouseHoveredViewport; // We can call io.AddMouseViewportEvent() with correct data (optional) +#endif + + bd->Window = window; + bd->Time = 0.0; + + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); +#if GLFW_VERSION_COMBINED < 3300 + platform_io.Platform_SetClipboardTextFn = [](ImGuiContext*, const char* text) { glfwSetClipboardString(ImGui_ImplGlfw_GetBackendData()->Window, text); }; + platform_io.Platform_GetClipboardTextFn = [](ImGuiContext*) { return glfwGetClipboardString(ImGui_ImplGlfw_GetBackendData()->Window); }; +#else + platform_io.Platform_SetClipboardTextFn = [](ImGuiContext*, const char* text) { glfwSetClipboardString(nullptr, text); }; + platform_io.Platform_GetClipboardTextFn = [](ImGuiContext*) { return glfwGetClipboardString(nullptr); }; +#endif + +#ifdef __EMSCRIPTEN__ + platform_io.Platform_OpenInShellFn = [](ImGuiContext*, const char* url) { ImGui_ImplGlfw_EmscriptenOpenURL(url); return true; }; +#endif + + // Create mouse cursors + // (By design, on X11 cursors are user configurable and some cursors may be missing. When a cursor doesn't exist, + // GLFW will emit an error which will often be printed by the app, so we temporarily disable error reporting. + // Missing cursors will return nullptr and our _UpdateMouseCursor() function will use the Arrow cursor instead.) + GLFWerrorfun prev_error_callback = glfwSetErrorCallback(nullptr); + bd->MouseCursors[ImGuiMouseCursor_Arrow] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_TextInput] = glfwCreateStandardCursor(GLFW_IBEAM_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_ResizeNS] = glfwCreateStandardCursor(GLFW_VRESIZE_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_ResizeEW] = glfwCreateStandardCursor(GLFW_HRESIZE_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_Hand] = glfwCreateStandardCursor(GLFW_HAND_CURSOR); +#if GLFW_HAS_NEW_CURSORS + bd->MouseCursors[ImGuiMouseCursor_ResizeAll] = glfwCreateStandardCursor(GLFW_RESIZE_ALL_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_ResizeNESW] = glfwCreateStandardCursor(GLFW_RESIZE_NESW_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_ResizeNWSE] = glfwCreateStandardCursor(GLFW_RESIZE_NWSE_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_NotAllowed] = glfwCreateStandardCursor(GLFW_NOT_ALLOWED_CURSOR); +#else + bd->MouseCursors[ImGuiMouseCursor_ResizeAll] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_ResizeNESW] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_ResizeNWSE] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); + bd->MouseCursors[ImGuiMouseCursor_NotAllowed] = glfwCreateStandardCursor(GLFW_ARROW_CURSOR); +#endif + glfwSetErrorCallback(prev_error_callback); +#if GLFW_HAS_GETERROR && !defined(__EMSCRIPTEN__) // Eat errors (see #5908) + (void)glfwGetError(nullptr); +#endif // Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any. - g_PrevUserCallbackMousebutton = NULL; - g_PrevUserCallbackScroll = NULL; - g_PrevUserCallbackKey = NULL; - g_PrevUserCallbackChar = NULL; if (install_callbacks) - { - g_PrevUserCallbackMousebutton = glfwSetMouseButtonCallback(window, ImGui_ImplGlfw_MouseButtonCallback); - g_PrevUserCallbackScroll = glfwSetScrollCallback(window, ImGui_ImplGlfw_ScrollCallback); - g_PrevUserCallbackKey = glfwSetKeyCallback(window, ImGui_ImplGlfw_KeyCallback); - g_PrevUserCallbackChar = glfwSetCharCallback(window, ImGui_ImplGlfw_CharCallback); - } + ImGui_ImplGlfw_InstallCallbacks(window); - g_ClientApi = client_api; + // Update monitor a first time during init + // (note: monitor callback are broken in GLFW 3.2 and earlier, see github.com/glfw/glfw/issues/784) + ImGui_ImplGlfw_UpdateMonitors(); + glfwSetMonitorCallback(ImGui_ImplGlfw_MonitorCallback); + + // Set platform dependent data in viewport + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + main_viewport->PlatformHandle = (void*)bd->Window; +#ifdef _WIN32 + main_viewport->PlatformHandleRaw = glfwGetWin32Window(bd->Window); +#elif defined(__APPLE__) + main_viewport->PlatformHandleRaw = (void*)glfwGetCocoaWindow(bd->Window); +#else + IM_UNUSED(main_viewport); +#endif + ImGui_ImplGlfw_InitMultiViewportSupport(); + + // Windows: register a WndProc hook so we can intercept some messages. +#ifdef _WIN32 + bd->PrevWndProc = (WNDPROC)::GetWindowLongPtrW((HWND)main_viewport->PlatformHandleRaw, GWLP_WNDPROC); + IM_ASSERT(bd->PrevWndProc != nullptr); + ::SetWindowLongPtrW((HWND)main_viewport->PlatformHandleRaw, GWLP_WNDPROC, (LONG_PTR)ImGui_ImplGlfw_WndProc); +#endif + + // Emscripten: the same application can run on various platforms, so we detect the Apple platform at runtime + // to override io.ConfigMacOSXBehaviors from its default (which is always false in Emscripten). +#ifdef __EMSCRIPTEN__ +#if EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 >= 34020240817 + if (emscripten::glfw3::IsRuntimePlatformApple()) + { + ImGui::GetIO().ConfigMacOSXBehaviors = true; + + // Due to how the browser (poorly) handles the Meta Key, this line essentially disables repeats when used. + // This means that Meta + V only registers a single key-press, even if the keys are held. + // This is a compromise for dealing with this issue in ImGui since ImGui implements key repeat itself. + // See https://github.com/pongasoft/emscripten-glfw/blob/v3.4.0.20240817/docs/Usage.md#the-problem-of-the-super-key + emscripten::glfw3::SetSuperPlusKeyTimeouts(10, 10); + } +#endif +#endif + + bd->ClientApi = client_api; return true; } @@ -203,130 +732,759 @@ bool ImGui_ImplGlfw_InitForVulkan(GLFWwindow* window, bool install_callbacks) return ImGui_ImplGlfw_Init(window, install_callbacks, GlfwClientApi_Vulkan); } -void ImGui_ImplGlfw_Shutdown() +bool ImGui_ImplGlfw_InitForOther(GLFWwindow* window, bool install_callbacks) { - for (ImGuiMouseCursor cursor_n = 0; cursor_n < ImGuiMouseCursor_COUNT; cursor_n++) - { - glfwDestroyCursor(g_MouseCursors[cursor_n]); - g_MouseCursors[cursor_n] = NULL; - } - g_ClientApi = GlfwClientApi_Unknown; + return ImGui_ImplGlfw_Init(window, install_callbacks, GlfwClientApi_Unknown); } -static void ImGui_ImplGlfw_UpdateMousePosAndButtons() +void ImGui_ImplGlfw_Shutdown() { - // Update buttons + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + IM_ASSERT(bd != nullptr && "No platform backend to shutdown, or already shutdown?"); ImGuiIO& io = ImGui::GetIO(); - for (int i = 0; i < IM_ARRAYSIZE(io.MouseDown); i++) + + ImGui_ImplGlfw_ShutdownMultiViewportSupport(); + + if (bd->InstalledCallbacks) + ImGui_ImplGlfw_RestoreCallbacks(bd->Window); +#ifdef EMSCRIPTEN_USE_EMBEDDED_GLFW3 + if (bd->CanvasSelector) + emscripten_set_wheel_callback(bd->CanvasSelector, nullptr, false, nullptr); +#endif + + for (ImGuiMouseCursor cursor_n = 0; cursor_n < ImGuiMouseCursor_COUNT; cursor_n++) + glfwDestroyCursor(bd->MouseCursors[cursor_n]); + + // Windows: restore our WndProc hook +#ifdef _WIN32 + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ::SetWindowLongPtrW((HWND)main_viewport->PlatformHandleRaw, GWLP_WNDPROC, (LONG_PTR)bd->PrevWndProc); + bd->PrevWndProc = nullptr; +#endif + + io.BackendPlatformName = nullptr; + io.BackendPlatformUserData = nullptr; + io.BackendFlags &= ~(ImGuiBackendFlags_HasMouseCursors | ImGuiBackendFlags_HasSetMousePos | ImGuiBackendFlags_HasGamepad | ImGuiBackendFlags_PlatformHasViewports | ImGuiBackendFlags_HasMouseHoveredViewport); + IM_DELETE(bd); +} + +static void ImGui_ImplGlfw_UpdateMouseData() +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGuiIO& io = ImGui::GetIO(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + + ImGuiID mouse_viewport_id = 0; + const ImVec2 mouse_pos_prev = io.MousePos; + for (int n = 0; n < platform_io.Viewports.Size; n++) { - // If a mouse press event came, always pass it as "mouse held this frame", so we don't miss click-release events that are shorter than 1 frame. - io.MouseDown[i] = g_MouseJustPressed[i] || glfwGetMouseButton(g_Window, i) != 0; - g_MouseJustPressed[i] = false; + ImGuiViewport* viewport = platform_io.Viewports[n]; + GLFWwindow* window = (GLFWwindow*)viewport->PlatformHandle; + +#ifdef EMSCRIPTEN_USE_EMBEDDED_GLFW3 + const bool is_window_focused = true; +#else + const bool is_window_focused = glfwGetWindowAttrib(window, GLFW_FOCUSED) != 0; +#endif + if (is_window_focused) + { + // (Optional) Set OS mouse position from Dear ImGui if requested (rarely used, only when io.ConfigNavMoveSetMousePos is enabled by user) + // When multi-viewports are enabled, all Dear ImGui positions are same as OS positions. + if (io.WantSetMousePos) + glfwSetCursorPos(window, (double)(mouse_pos_prev.x - viewport->Pos.x), (double)(mouse_pos_prev.y - viewport->Pos.y)); + + // (Optional) Fallback to provide mouse position when focused (ImGui_ImplGlfw_CursorPosCallback already provides this when hovered or captured) + if (bd->MouseWindow == nullptr) + { + double mouse_x, mouse_y; + glfwGetCursorPos(window, &mouse_x, &mouse_y); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + // Single viewport mode: mouse position in client window coordinates (io.MousePos is (0,0) when the mouse is on the upper-left corner of the app window) + // Multi-viewport mode: mouse position in OS absolute coordinates (io.MousePos is (0,0) when the mouse is on the upper-left of the primary monitor) + int window_x, window_y; + glfwGetWindowPos(window, &window_x, &window_y); + mouse_x += window_x - bd->MainWindowOrigin.x; + mouse_y += window_y - bd->MainWindowOrigin.y; + } + bd->LastValidMousePos = ImVec2((float)mouse_x, (float)mouse_y); + io.AddMousePosEvent((float)mouse_x, (float)mouse_y); + } + } + + // (Optional) When using multiple viewports: call io.AddMouseViewportEvent() with the viewport the OS mouse cursor is hovering. + // If ImGuiBackendFlags_HasMouseHoveredViewport is not set by the backend, Dear imGui will ignore this field and infer the information using its flawed heuristic. + // - [X] GLFW >= 3.3 backend ON WINDOWS ONLY does correctly ignore viewports with the _NoInputs flag (since we implement hit via our WndProc hook) + // On other platforms we rely on the library fallbacking to its own search when reporting a viewport with _NoInputs flag. + // - [!] GLFW <= 3.2 backend CANNOT correctly ignore viewports with the _NoInputs flag, and CANNOT reported Hovered Viewport because of mouse capture. + // Some backend are not able to handle that correctly. If a backend report an hovered viewport that has the _NoInputs flag (e.g. when dragging a window + // for docking, the viewport has the _NoInputs flag in order to allow us to find the viewport under), then Dear ImGui is forced to ignore the value reported + // by the backend, and use its flawed heuristic to guess the viewport behind. + // - [X] GLFW backend correctly reports this regardless of another viewport behind focused and dragged from (we need this to find a useful drag and drop target). + // FIXME: This is currently only correct on Win32. See what we do below with the WM_NCHITTEST, missing an equivalent for other systems. + // See https://github.com/glfw/glfw/issues/1236 if you want to help in making this a GLFW feature. +#if GLFW_HAS_MOUSE_PASSTHROUGH + const bool window_no_input = (viewport->Flags & ImGuiViewportFlags_NoInputs) != 0; + glfwSetWindowAttrib(window, GLFW_MOUSE_PASSTHROUGH, window_no_input); +#endif +#if GLFW_HAS_MOUSE_PASSTHROUGH || GLFW_HAS_WINDOW_HOVERED + if (glfwGetWindowAttrib(window, GLFW_HOVERED)) + mouse_viewport_id = viewport->ID; +#else + // We cannot use bd->MouseWindow maintained from CursorEnter/Leave callbacks, because it is locked to the window capturing mouse. +#endif } - // Update mouse position - const ImVec2 mouse_pos_backup = io.MousePos; - io.MousePos = ImVec2(-FLT_MAX, -FLT_MAX); -#ifdef __EMSCRIPTEN__ - const bool focused = true; // Emscripten -#else - const bool focused = glfwGetWindowAttrib(g_Window, GLFW_FOCUSED) != 0; -#endif - if (focused) - { - if (io.WantSetMousePos) - { - glfwSetCursorPos(g_Window, (double)mouse_pos_backup.x, (double)mouse_pos_backup.y); - } - else - { - double mouse_x, mouse_y; - glfwGetCursorPos(g_Window, &mouse_x, &mouse_y); - io.MousePos = ImVec2((float)mouse_x, (float)mouse_y); - } - } + if (io.BackendFlags & ImGuiBackendFlags_HasMouseHoveredViewport) + io.AddMouseViewportEvent(mouse_viewport_id); } static void ImGui_ImplGlfw_UpdateMouseCursor() { ImGuiIO& io = ImGui::GetIO(); - if ((io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) || glfwGetInputMode(g_Window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED) + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if ((io.ConfigFlags & ImGuiConfigFlags_NoMouseCursorChange) || glfwGetInputMode(bd->Window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED) return; ImGuiMouseCursor imgui_cursor = ImGui::GetMouseCursor(); - if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + for (int n = 0; n < platform_io.Viewports.Size; n++) { - // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor - glfwSetInputMode(g_Window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); - } - else - { - // Show OS mouse cursor - // FIXME-PLATFORM: Unfocused windows seems to fail changing the mouse cursor with GLFW 3.2, but 3.3 works here. - glfwSetCursor(g_Window, g_MouseCursors[imgui_cursor] ? g_MouseCursors[imgui_cursor] : g_MouseCursors[ImGuiMouseCursor_Arrow]); - glfwSetInputMode(g_Window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + GLFWwindow* window = (GLFWwindow*)platform_io.Viewports[n]->PlatformHandle; + if (imgui_cursor == ImGuiMouseCursor_None || io.MouseDrawCursor) + { + // Hide OS mouse cursor if imgui is drawing it or if it wants no cursor + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN); + } + else + { + // Show OS mouse cursor + // FIXME-PLATFORM: Unfocused windows seems to fail changing the mouse cursor with GLFW 3.2, but 3.3 works here. + glfwSetCursor(window, bd->MouseCursors[imgui_cursor] ? bd->MouseCursors[imgui_cursor] : bd->MouseCursors[ImGuiMouseCursor_Arrow]); + glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); + } } } +// Update gamepad inputs +static inline float Saturate(float v) { return v < 0.0f ? 0.0f : v > 1.0f ? 1.0f : v; } static void ImGui_ImplGlfw_UpdateGamepads() { ImGuiIO& io = ImGui::GetIO(); - memset(io.NavInputs, 0, sizeof(io.NavInputs)); - if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0) + if ((io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) == 0) // FIXME: Technically feeding gamepad shouldn't depend on this now that they are regular inputs. return; - // Update gamepad inputs - #define MAP_BUTTON(NAV_NO, BUTTON_NO) { if (buttons_count > BUTTON_NO && buttons[BUTTON_NO] == GLFW_PRESS) io.NavInputs[NAV_NO] = 1.0f; } - #define MAP_ANALOG(NAV_NO, AXIS_NO, V0, V1) { float v = (axes_count > AXIS_NO) ? axes[AXIS_NO] : V0; v = (v - V0) / (V1 - V0); if (v > 1.0f) v = 1.0f; if (io.NavInputs[NAV_NO] < v) io.NavInputs[NAV_NO] = v; } + io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; +#if GLFW_HAS_GAMEPAD_API && !defined(EMSCRIPTEN_USE_EMBEDDED_GLFW3) + GLFWgamepadstate gamepad; + if (!glfwGetGamepadState(GLFW_JOYSTICK_1, &gamepad)) + return; + #define MAP_BUTTON(KEY_NO, BUTTON_NO, _UNUSED) do { io.AddKeyEvent(KEY_NO, gamepad.buttons[BUTTON_NO] != 0); } while (0) + #define MAP_ANALOG(KEY_NO, AXIS_NO, _UNUSED, V0, V1) do { float v = gamepad.axes[AXIS_NO]; v = (v - V0) / (V1 - V0); io.AddKeyAnalogEvent(KEY_NO, v > 0.10f, Saturate(v)); } while (0) +#else int axes_count = 0, buttons_count = 0; const float* axes = glfwGetJoystickAxes(GLFW_JOYSTICK_1, &axes_count); const unsigned char* buttons = glfwGetJoystickButtons(GLFW_JOYSTICK_1, &buttons_count); - MAP_BUTTON(ImGuiNavInput_Activate, 0); // Cross / A - MAP_BUTTON(ImGuiNavInput_Cancel, 1); // Circle / B - MAP_BUTTON(ImGuiNavInput_Menu, 2); // Square / X - MAP_BUTTON(ImGuiNavInput_Input, 3); // Triangle / Y - MAP_BUTTON(ImGuiNavInput_DpadLeft, 13); // D-Pad Left - MAP_BUTTON(ImGuiNavInput_DpadRight, 11); // D-Pad Right - MAP_BUTTON(ImGuiNavInput_DpadUp, 10); // D-Pad Up - MAP_BUTTON(ImGuiNavInput_DpadDown, 12); // D-Pad Down - MAP_BUTTON(ImGuiNavInput_FocusPrev, 4); // L1 / LB - MAP_BUTTON(ImGuiNavInput_FocusNext, 5); // R1 / RB - MAP_BUTTON(ImGuiNavInput_TweakSlow, 4); // L1 / LB - MAP_BUTTON(ImGuiNavInput_TweakFast, 5); // R1 / RB - MAP_ANALOG(ImGuiNavInput_LStickLeft, 0, -0.3f, -0.9f); - MAP_ANALOG(ImGuiNavInput_LStickRight,0, +0.3f, +0.9f); - MAP_ANALOG(ImGuiNavInput_LStickUp, 1, +0.3f, +0.9f); - MAP_ANALOG(ImGuiNavInput_LStickDown, 1, -0.3f, -0.9f); + if (axes_count == 0 || buttons_count == 0) + return; + #define MAP_BUTTON(KEY_NO, _UNUSED, BUTTON_NO) do { io.AddKeyEvent(KEY_NO, (buttons_count > BUTTON_NO && buttons[BUTTON_NO] == GLFW_PRESS)); } while (0) + #define MAP_ANALOG(KEY_NO, _UNUSED, AXIS_NO, V0, V1) do { float v = (axes_count > AXIS_NO) ? axes[AXIS_NO] : V0; v = (v - V0) / (V1 - V0); io.AddKeyAnalogEvent(KEY_NO, v > 0.10f, Saturate(v)); } while (0) +#endif + io.BackendFlags |= ImGuiBackendFlags_HasGamepad; + MAP_BUTTON(ImGuiKey_GamepadStart, GLFW_GAMEPAD_BUTTON_START, 7); + MAP_BUTTON(ImGuiKey_GamepadBack, GLFW_GAMEPAD_BUTTON_BACK, 6); + MAP_BUTTON(ImGuiKey_GamepadFaceLeft, GLFW_GAMEPAD_BUTTON_X, 2); // Xbox X, PS Square + MAP_BUTTON(ImGuiKey_GamepadFaceRight, GLFW_GAMEPAD_BUTTON_B, 1); // Xbox B, PS Circle + MAP_BUTTON(ImGuiKey_GamepadFaceUp, GLFW_GAMEPAD_BUTTON_Y, 3); // Xbox Y, PS Triangle + MAP_BUTTON(ImGuiKey_GamepadFaceDown, GLFW_GAMEPAD_BUTTON_A, 0); // Xbox A, PS Cross + MAP_BUTTON(ImGuiKey_GamepadDpadLeft, GLFW_GAMEPAD_BUTTON_DPAD_LEFT, 13); + MAP_BUTTON(ImGuiKey_GamepadDpadRight, GLFW_GAMEPAD_BUTTON_DPAD_RIGHT, 11); + MAP_BUTTON(ImGuiKey_GamepadDpadUp, GLFW_GAMEPAD_BUTTON_DPAD_UP, 10); + MAP_BUTTON(ImGuiKey_GamepadDpadDown, GLFW_GAMEPAD_BUTTON_DPAD_DOWN, 12); + MAP_BUTTON(ImGuiKey_GamepadL1, GLFW_GAMEPAD_BUTTON_LEFT_BUMPER, 4); + MAP_BUTTON(ImGuiKey_GamepadR1, GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER, 5); + MAP_ANALOG(ImGuiKey_GamepadL2, GLFW_GAMEPAD_AXIS_LEFT_TRIGGER, 4, -0.75f, +1.0f); + MAP_ANALOG(ImGuiKey_GamepadR2, GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER, 5, -0.75f, +1.0f); + MAP_BUTTON(ImGuiKey_GamepadL3, GLFW_GAMEPAD_BUTTON_LEFT_THUMB, 8); + MAP_BUTTON(ImGuiKey_GamepadR3, GLFW_GAMEPAD_BUTTON_RIGHT_THUMB, 9); + MAP_ANALOG(ImGuiKey_GamepadLStickLeft, GLFW_GAMEPAD_AXIS_LEFT_X, 0, -0.25f, -1.0f); + MAP_ANALOG(ImGuiKey_GamepadLStickRight, GLFW_GAMEPAD_AXIS_LEFT_X, 0, +0.25f, +1.0f); + MAP_ANALOG(ImGuiKey_GamepadLStickUp, GLFW_GAMEPAD_AXIS_LEFT_Y, 1, -0.25f, -1.0f); + MAP_ANALOG(ImGuiKey_GamepadLStickDown, GLFW_GAMEPAD_AXIS_LEFT_Y, 1, +0.25f, +1.0f); + MAP_ANALOG(ImGuiKey_GamepadRStickLeft, GLFW_GAMEPAD_AXIS_RIGHT_X, 2, -0.25f, -1.0f); + MAP_ANALOG(ImGuiKey_GamepadRStickRight, GLFW_GAMEPAD_AXIS_RIGHT_X, 2, +0.25f, +1.0f); + MAP_ANALOG(ImGuiKey_GamepadRStickUp, GLFW_GAMEPAD_AXIS_RIGHT_Y, 3, -0.25f, -1.0f); + MAP_ANALOG(ImGuiKey_GamepadRStickDown, GLFW_GAMEPAD_AXIS_RIGHT_Y, 3, +0.25f, +1.0f); #undef MAP_BUTTON #undef MAP_ANALOG - if (axes_count > 0 && buttons_count > 0) - io.BackendFlags |= ImGuiBackendFlags_HasGamepad; - else - io.BackendFlags &= ~ImGuiBackendFlags_HasGamepad; +} + +static void ImGui_ImplGlfw_UpdateMonitors() +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + + int monitors_count = 0; + GLFWmonitor** glfw_monitors = glfwGetMonitors(&monitors_count); + if (monitors_count == 0) // Preserve existing monitor list if there are none. Happens on macOS sleeping (#5683) + return; + + platform_io.Monitors.resize(0); + for (int n = 0; n < monitors_count; n++) + { + ImGuiPlatformMonitor monitor; + int x, y; + glfwGetMonitorPos(glfw_monitors[n], &x, &y); + const GLFWvidmode* vid_mode = glfwGetVideoMode(glfw_monitors[n]); + if (vid_mode == nullptr) + continue; // Failed to get Video mode (e.g. Emscripten does not support this function) + monitor.MainPos = monitor.WorkPos = ImVec2((float)x, (float)y); + monitor.MainSize = monitor.WorkSize = ImVec2((float)vid_mode->width, (float)vid_mode->height); +#if GLFW_HAS_MONITOR_WORK_AREA + int w, h; + glfwGetMonitorWorkarea(glfw_monitors[n], &x, &y, &w, &h); + if (w > 0 && h > 0) // Workaround a small GLFW issue reporting zero on monitor changes: https://github.com/glfw/glfw/pull/1761 + { + monitor.WorkPos = ImVec2((float)x, (float)y); + monitor.WorkSize = ImVec2((float)w, (float)h); + } +#endif +#if GLFW_HAS_PER_MONITOR_DPI + // Warning: the validity of monitor DPI information on Windows depends on the application DPI awareness settings, which generally needs to be set in the manifest or at runtime. + float x_scale, y_scale; + glfwGetMonitorContentScale(glfw_monitors[n], &x_scale, &y_scale); + if (x_scale == 0.0f) + continue; // Some accessibility applications are declaring virtual monitors with a DPI of 0, see #7902. + monitor.DpiScale = x_scale; +#endif + // MaSzyna: monitors in imgui space as well + monitor.MainPos = ImVec2(monitor.MainPos.x - bd->MainWindowOrigin.x, monitor.MainPos.y - bd->MainWindowOrigin.y); + monitor.WorkPos = ImVec2(monitor.WorkPos.x - bd->MainWindowOrigin.x, monitor.WorkPos.y - bd->MainWindowOrigin.y); + monitor.PlatformHandle = (void*)glfw_monitors[n]; // [...] GLFW doc states: "guaranteed to be valid only until the monitor configuration changes" + platform_io.Monitors.push_back(monitor); + } } void ImGui_ImplGlfw_NewFrame() { ImGuiIO& io = ImGui::GetIO(); - IM_ASSERT(io.Fonts->IsBuilt() && "Font atlas not built! It is generally built by the renderer back-end. Missing call to renderer _NewFrame() function? e.g. ImGui_ImplOpenGL3_NewFrame()."); + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplGlfw_InitForXXX()?"); // Setup display size (every frame to accommodate for window resizing) int w, h; int display_w, display_h; - glfwGetWindowSize(g_Window, &w, &h); - glfwGetFramebufferSize(g_Window, &display_w, &display_h); + glfwGetWindowSize(bd->Window, &w, &h); + glfwGetFramebufferSize(bd->Window, &display_w, &display_h); io.DisplaySize = ImVec2((float)w, (float)h); if (w > 0 && h > 0) - io.DisplayFramebufferScale = ImVec2((float)display_w / w, (float)display_h / h); + io.DisplayFramebufferScale = ImVec2((float)display_w / (float)w, (float)display_h / (float)h); + ImGui_ImplGlfw_UpdateMainWindowOrigin(); + ImGui_ImplGlfw_UpdateMonitors(); // Setup time step + // (Accept glfwGetTime() not returning a monotonically increasing value. Seems to happens on disconnecting peripherals and probably on VMs and Emscripten, see #6491, #6189, #6114, #3644) double current_time = glfwGetTime(); - io.DeltaTime = g_Time > 0.0 ? (float)(current_time - g_Time) : (float)(1.0f/60.0f); - g_Time = current_time; + if (current_time <= bd->Time) + current_time = bd->Time + 0.00001f; + io.DeltaTime = bd->Time > 0.0 ? (float)(current_time - bd->Time) : (float)(1.0f / 60.0f); + bd->Time = current_time; - ImGui_ImplGlfw_UpdateMousePosAndButtons(); + bd->MouseIgnoreButtonUp = false; + ImGui_ImplGlfw_UpdateMouseData(); ImGui_ImplGlfw_UpdateMouseCursor(); // Update game controllers (if enabled and available) ImGui_ImplGlfw_UpdateGamepads(); } + +// GLFW doesn't provide a portable sleep function +void ImGui_ImplGlfw_Sleep(int milliseconds) +{ +#ifdef _WIN32 + ::Sleep(milliseconds); +#else + usleep(milliseconds * 1000); +#endif +} + +#ifdef EMSCRIPTEN_USE_EMBEDDED_GLFW3 +static EM_BOOL ImGui_ImplGlfw_OnCanvasSizeChange(int event_type, const EmscriptenUiEvent* event, void* user_data) +{ + ImGui_ImplGlfw_Data* bd = (ImGui_ImplGlfw_Data*)user_data; + double canvas_width, canvas_height; + emscripten_get_element_css_size(bd->CanvasSelector, &canvas_width, &canvas_height); + glfwSetWindowSize(bd->Window, (int)canvas_width, (int)canvas_height); + return true; +} + +static EM_BOOL ImGui_ImplEmscripten_FullscreenChangeCallback(int event_type, const EmscriptenFullscreenChangeEvent* event, void* user_data) +{ + ImGui_ImplGlfw_Data* bd = (ImGui_ImplGlfw_Data*)user_data; + double canvas_width, canvas_height; + emscripten_get_element_css_size(bd->CanvasSelector, &canvas_width, &canvas_height); + glfwSetWindowSize(bd->Window, (int)canvas_width, (int)canvas_height); + return true; +} + +// 'canvas_selector' is a CSS selector. The event listener is applied to the first element that matches the query. +// STRING MUST PERSIST FOR THE APPLICATION DURATION. PLEASE USE A STRING LITERAL OR ENSURE POINTER WILL STAY VALID. +void ImGui_ImplGlfw_InstallEmscriptenCallbacks(GLFWwindow*, const char* canvas_selector) +{ + IM_ASSERT(canvas_selector != nullptr); + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplGlfw_InitForXXX()?"); + + bd->CanvasSelector = canvas_selector; + emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, bd, false, ImGui_ImplGlfw_OnCanvasSizeChange); + emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, bd, false, ImGui_ImplEmscripten_FullscreenChangeCallback); + + // Change the size of the GLFW window according to the size of the canvas + ImGui_ImplGlfw_OnCanvasSizeChange(EMSCRIPTEN_EVENT_RESIZE, {}, bd); + + // Register Emscripten Wheel callback to workaround issue in Emscripten GLFW Emulation (#6096) + // We intentionally do not check 'if (install_callbacks)' here, as some users may set it to false and call GLFW callback themselves. + // FIXME: May break chaining in case user registered their own Emscripten callback? + emscripten_set_wheel_callback(bd->CanvasSelector, nullptr, false, ImGui_ImplEmscripten_WheelCallback); +} +#elif defined(EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3) +// When using --use-port=contrib.glfw3 for the GLFW implementation, you can override the behavior of this call +// by invoking emscripten_glfw_make_canvas_resizable afterward. +// See https://github.com/pongasoft/emscripten-glfw/blob/master/docs/Usage.md#how-to-make-the-canvas-resizable-by-the-user for an explanation +void ImGui_ImplGlfw_InstallEmscriptenCallbacks(GLFWwindow* window, const char* canvas_selector) +{ + GLFWwindow* w = (GLFWwindow*)(EM_ASM_INT({ return Module.glfwGetWindow(UTF8ToString($0)); }, canvas_selector)); + IM_ASSERT(window == w); // Sanity check + IM_UNUSED(w); + emscripten_glfw_make_canvas_resizable(window, "window", nullptr); +} +#endif // #ifdef EMSCRIPTEN_USE_PORT_CONTRIB_GLFW3 + + +//-------------------------------------------------------------------------------------------------------- +// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT +// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously. +// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first.. +//-------------------------------------------------------------------------------------------------------- + +// Helper structure we store in the void* PlatformUserData field of each ImGuiViewport to easily retrieve our backend data. +struct ImGui_ImplGlfw_ViewportData +{ + GLFWwindow* Window; // Stored in ImGuiViewport::PlatformHandle + bool WindowOwned; + int IgnoreWindowPosEventFrame; + int IgnoreWindowSizeEventFrame; +#ifdef _WIN32 + WNDPROC PrevWndProc; +#endif + + ImGui_ImplGlfw_ViewportData() { memset((void*)this, 0, sizeof(*this)); IgnoreWindowSizeEventFrame = IgnoreWindowPosEventFrame = -1; } + ~ImGui_ImplGlfw_ViewportData() { IM_ASSERT(Window == nullptr); } +}; + +static void ImGui_ImplGlfw_WindowCloseCallback(GLFWwindow* window) +{ + if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle(window)) + viewport->PlatformRequestClose = true; +} + +// GLFW may dispatch window pos/size events after calling glfwSetWindowPos()/glfwSetWindowSize(). +// However: depending on the platform the callback may be invoked at different time: +// - on Windows it appears to be called within the glfwSetWindowPos()/glfwSetWindowSize() call +// - on Linux it is queued and invoked during glfwPollEvents() +// Because the event doesn't always fire on glfwSetWindowXXX() we use a frame counter tag to only +// ignore recent glfwSetWindowXXX() calls. +static void ImGui_ImplGlfw_WindowPosCallback(GLFWwindow* window, int, int) +{ + if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle(window)) + { + if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) + { + bool ignore_event = (ImGui::GetFrameCount() <= vd->IgnoreWindowPosEventFrame + 1); + //data->IgnoreWindowPosEventFrame = -1; + if (ignore_event) + return; + } + viewport->PlatformRequestMove = true; + } +} + +static void ImGui_ImplGlfw_WindowSizeCallback(GLFWwindow* window, int, int) +{ + if (ImGuiViewport* viewport = ImGui::FindViewportByPlatformHandle(window)) + { + if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) + { + bool ignore_event = (ImGui::GetFrameCount() <= vd->IgnoreWindowSizeEventFrame + 1); + //data->IgnoreWindowSizeEventFrame = -1; + if (ignore_event) + return; + } + viewport->PlatformRequestResize = true; + } +} + +static void ImGui_ImplGlfw_CreateWindow(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = IM_NEW(ImGui_ImplGlfw_ViewportData)(); + viewport->PlatformUserData = vd; + + // Workaround for Linux: ignore mouse up events corresponding to losing focus of the previously focused window (#7733, #3158, #7922) +#ifdef __linux__ + bd->MouseIgnoreButtonUpWaitForFocusLoss = true; +#endif + + // GLFW 3.2 unfortunately always set focus on glfwCreateWindow() if GLFW_VISIBLE is set, regardless of GLFW_FOCUSED + // With GLFW 3.3, the hint GLFW_FOCUS_ON_SHOW fixes this problem + glfwWindowHint(GLFW_VISIBLE, false); + glfwWindowHint(GLFW_FOCUSED, false); +#if GLFW_HAS_FOCUS_ON_SHOW + glfwWindowHint(GLFW_FOCUS_ON_SHOW, false); + #endif + glfwWindowHint(GLFW_DECORATED, (viewport->Flags & ImGuiViewportFlags_NoDecoration) ? false : true); +#if GLFW_HAS_WINDOW_TOPMOST + glfwWindowHint(GLFW_FLOATING, (viewport->Flags & ImGuiViewportFlags_TopMost) ? true : false); +#endif + GLFWwindow* share_window = (bd->ClientApi == GlfwClientApi_OpenGL) ? bd->Window : nullptr; + vd->Window = glfwCreateWindow((int)viewport->Size.x, (int)viewport->Size.y, "No Title Yet", nullptr, share_window); + vd->WindowOwned = true; + viewport->PlatformHandle = (void*)vd->Window; +#ifdef _WIN32 + viewport->PlatformHandleRaw = glfwGetWin32Window(vd->Window); +#elif defined(__APPLE__) + viewport->PlatformHandleRaw = (void*)glfwGetCocoaWindow(vd->Window); +#endif + glfwSetWindowPos(vd->Window, (int)(viewport->Pos.x + bd->MainWindowOrigin.x), (int)(viewport->Pos.y + bd->MainWindowOrigin.y)); + + // Install GLFW callbacks for secondary viewports + glfwSetWindowFocusCallback(vd->Window, ImGui_ImplGlfw_WindowFocusCallback); + glfwSetCursorEnterCallback(vd->Window, ImGui_ImplGlfw_CursorEnterCallback); + glfwSetCursorPosCallback(vd->Window, ImGui_ImplGlfw_CursorPosCallback); + glfwSetMouseButtonCallback(vd->Window, ImGui_ImplGlfw_MouseButtonCallback); + glfwSetScrollCallback(vd->Window, ImGui_ImplGlfw_ScrollCallback); + glfwSetKeyCallback(vd->Window, ImGui_ImplGlfw_KeyCallback); + glfwSetCharCallback(vd->Window, ImGui_ImplGlfw_CharCallback); + glfwSetWindowCloseCallback(vd->Window, ImGui_ImplGlfw_WindowCloseCallback); + glfwSetWindowPosCallback(vd->Window, ImGui_ImplGlfw_WindowPosCallback); + glfwSetWindowSizeCallback(vd->Window, ImGui_ImplGlfw_WindowSizeCallback); + if (bd->ClientApi == GlfwClientApi_OpenGL) + { + glfwMakeContextCurrent(vd->Window); + glfwSwapInterval(0); + } +} + +static void ImGui_ImplGlfw_DestroyWindow(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) + { + if (vd->WindowOwned) + { +#if !GLFW_HAS_MOUSE_PASSTHROUGH && GLFW_HAS_WINDOW_HOVERED && defined(_WIN32) + HWND hwnd = (HWND)viewport->PlatformHandleRaw; + ::RemovePropA(hwnd, "IMGUI_VIEWPORT"); +#endif + + // Release any keys that were pressed in the window being destroyed and are still held down, + // because we will not receive any release events after window is destroyed. + for (int i = 0; i < IM_ARRAYSIZE(bd->KeyOwnerWindows); i++) + if (bd->KeyOwnerWindows[i] == vd->Window) + ImGui_ImplGlfw_KeyCallback(vd->Window, i, 0, GLFW_RELEASE, 0); // Later params are only used for main viewport, on which this function is never called. + + glfwDestroyWindow(vd->Window); + } + vd->Window = nullptr; + IM_DELETE(vd); + } + viewport->PlatformUserData = viewport->PlatformHandle = nullptr; +} + +static void ImGui_ImplGlfw_ShowWindow(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + +#if defined(_WIN32) + // GLFW hack: Hide icon from task bar + HWND hwnd = (HWND)viewport->PlatformHandleRaw; + if (viewport->Flags & ImGuiViewportFlags_NoTaskBarIcon) + { + LONG ex_style = ::GetWindowLong(hwnd, GWL_EXSTYLE); + ex_style &= ~WS_EX_APPWINDOW; + ex_style |= WS_EX_TOOLWINDOW; + ::SetWindowLong(hwnd, GWL_EXSTYLE, ex_style); + } + + // GLFW hack: install hook for WM_NCHITTEST message handler +#if !GLFW_HAS_MOUSE_PASSTHROUGH && GLFW_HAS_WINDOW_HOVERED && defined(_WIN32) + ::SetPropA(hwnd, "IMGUI_VIEWPORT", viewport); + vd->PrevWndProc = (WNDPROC)::GetWindowLongPtrW(hwnd, GWLP_WNDPROC); + ::SetWindowLongPtrW(hwnd, GWLP_WNDPROC, (LONG_PTR)ImGui_ImplGlfw_WndProc); +#endif + +#if !GLFW_HAS_FOCUS_ON_SHOW + // GLFW hack: GLFW 3.2 has a bug where glfwShowWindow() also activates/focus the window. + // The fix was pushed to GLFW repository on 2018/01/09 and should be included in GLFW 3.3 via a GLFW_FOCUS_ON_SHOW window attribute. + // See https://github.com/glfw/glfw/issues/1189 + // FIXME-VIEWPORT: Implement same work-around for Linux/OSX in the meanwhile. + if (viewport->Flags & ImGuiViewportFlags_NoFocusOnAppearing) + { + ::ShowWindow(hwnd, SW_SHOWNA); + return; + } +#endif +#endif + + glfwShowWindow(vd->Window); +} + +static ImVec2 ImGui_ImplGlfw_GetWindowPos(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + int x = 0, y = 0; + glfwGetWindowPos(vd->Window, &x, &y); + return ImVec2((float)x - bd->MainWindowOrigin.x, (float)y - bd->MainWindowOrigin.y); +} + +static void ImGui_ImplGlfw_SetWindowPos(ImGuiViewport* viewport, ImVec2 pos) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + vd->IgnoreWindowPosEventFrame = ImGui::GetFrameCount(); + glfwSetWindowPos(vd->Window, (int)(pos.x + bd->MainWindowOrigin.x), (int)(pos.y + bd->MainWindowOrigin.y)); +} + +// MaSzyna: with multi-viewports imgui positions are given relative to the main window of the simulator instead of the desktop, +// so the main viewport stays at (0,0) as it was without them: the simulator draws its overlays and reads the mouse in the +// coordinates of its window. When the main window moves, the windows of the other viewports are read again in the new space. +static void ImGui_ImplGlfw_UpdateMainWindowOrigin() +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImVec2 origin(0.0f, 0.0f); + if (ImGui::GetIO().ConfigFlags & ImGuiConfigFlags_ViewportsEnable) + { + int x = 0, y = 0; + glfwGetWindowPos(bd->Window, &x, &y); + origin = ImVec2((float)x, (float)y); + } + if (origin.x == bd->MainWindowOrigin.x && origin.y == bd->MainWindowOrigin.y) + return; + bd->MainWindowOrigin = origin; + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + for (int i = 1; i < platform_io.Viewports.Size; i++) + platform_io.Viewports[i]->PlatformRequestMove = true; +} + +static ImVec2 ImGui_ImplGlfw_GetWindowSize(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + int w = 0, h = 0; + glfwGetWindowSize(vd->Window, &w, &h); + return ImVec2((float)w, (float)h); +} + +static void ImGui_ImplGlfw_SetWindowSize(ImGuiViewport* viewport, ImVec2 size) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; +#if __APPLE__ && !GLFW_HAS_OSX_WINDOW_POS_FIX + // Native OS windows are positioned from the bottom-left corner on macOS, whereas on other platforms they are + // positioned from the upper-left corner. GLFW makes an effort to convert macOS style coordinates, however it + // doesn't handle it when changing size. We are manually moving the window in order for changes of size to be based + // on the upper-left corner. + int x, y, width, height; + glfwGetWindowPos(vd->Window, &x, &y); + glfwGetWindowSize(vd->Window, &width, &height); + glfwSetWindowPos(vd->Window, x, y - height + size.y); +#endif + vd->IgnoreWindowSizeEventFrame = ImGui::GetFrameCount(); + glfwSetWindowSize(vd->Window, (int)size.x, (int)size.y); +} + +static void ImGui_ImplGlfw_SetWindowTitle(ImGuiViewport* viewport, const char* title) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + glfwSetWindowTitle(vd->Window, title); +} + +static void ImGui_ImplGlfw_SetWindowFocus(ImGuiViewport* viewport) +{ +#if GLFW_HAS_FOCUS_WINDOW + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + glfwFocusWindow(vd->Window); +#else + // FIXME: What are the effect of not having this function? At the moment imgui doesn't actually call SetWindowFocus - we set that up ahead, will answer that question later. + (void)viewport; +#endif +} + +static bool ImGui_ImplGlfw_GetWindowFocus(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + return glfwGetWindowAttrib(vd->Window, GLFW_FOCUSED) != 0; +} + +static bool ImGui_ImplGlfw_GetWindowMinimized(ImGuiViewport* viewport) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + return glfwGetWindowAttrib(vd->Window, GLFW_ICONIFIED) != 0; +} + +#if GLFW_HAS_WINDOW_ALPHA +static void ImGui_ImplGlfw_SetWindowAlpha(ImGuiViewport* viewport, float alpha) +{ + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + glfwSetWindowOpacity(vd->Window, alpha); +} +#endif + +static void ImGui_ImplGlfw_RenderWindow(ImGuiViewport* viewport, void*) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + if (bd->ClientApi == GlfwClientApi_OpenGL) + glfwMakeContextCurrent(vd->Window); +} + +static void ImGui_ImplGlfw_SwapBuffers(ImGuiViewport* viewport, void*) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + if (bd->ClientApi == GlfwClientApi_OpenGL) + { + glfwMakeContextCurrent(vd->Window); + glfwSwapBuffers(vd->Window); + } +} + +//-------------------------------------------------------------------------------------------------------- +// Vulkan support (the Vulkan renderer needs to call a platform-side support function to create the surface) +//-------------------------------------------------------------------------------------------------------- + +// Avoid including so we can build without it +#if GLFW_HAS_VULKAN +#ifndef VULKAN_H_ +#define VK_DEFINE_HANDLE(object) typedef struct object##_T* object; +#if defined(__LP64__) || defined(_WIN64) || defined(__x86_64__) || defined(_M_X64) || defined(__ia64) || defined (_M_IA64) || defined(__aarch64__) || defined(__powerpc64__) +#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef struct object##_T *object; +#else +#define VK_DEFINE_NON_DISPATCHABLE_HANDLE(object) typedef uint64_t object; +#endif +VK_DEFINE_HANDLE(VkInstance) +VK_DEFINE_NON_DISPATCHABLE_HANDLE(VkSurfaceKHR) +struct VkAllocationCallbacks; +enum VkResult { VK_RESULT_MAX_ENUM = 0x7FFFFFFF }; +#endif // VULKAN_H_ +extern "C" { extern GLFWAPI VkResult glfwCreateWindowSurface(VkInstance instance, GLFWwindow* window, const VkAllocationCallbacks* allocator, VkSurfaceKHR* surface); } +static int ImGui_ImplGlfw_CreateVkSurface(ImGuiViewport* viewport, ImU64 vk_instance, const void* vk_allocator, ImU64* out_vk_surface) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData; + IM_UNUSED(bd); + IM_ASSERT(bd->ClientApi == GlfwClientApi_Vulkan); + VkResult err = glfwCreateWindowSurface((VkInstance)vk_instance, vd->Window, (const VkAllocationCallbacks*)vk_allocator, (VkSurfaceKHR*)out_vk_surface); + return (int)err; +} +#endif // GLFW_HAS_VULKAN + +static void ImGui_ImplGlfw_InitMultiViewportSupport() +{ + // Register platform interface (will be coupled with a renderer interface) + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Platform_CreateWindow = ImGui_ImplGlfw_CreateWindow; + platform_io.Platform_DestroyWindow = ImGui_ImplGlfw_DestroyWindow; + platform_io.Platform_ShowWindow = ImGui_ImplGlfw_ShowWindow; + platform_io.Platform_SetWindowPos = ImGui_ImplGlfw_SetWindowPos; + platform_io.Platform_GetWindowPos = ImGui_ImplGlfw_GetWindowPos; + platform_io.Platform_SetWindowSize = ImGui_ImplGlfw_SetWindowSize; + platform_io.Platform_GetWindowSize = ImGui_ImplGlfw_GetWindowSize; + platform_io.Platform_SetWindowFocus = ImGui_ImplGlfw_SetWindowFocus; + platform_io.Platform_GetWindowFocus = ImGui_ImplGlfw_GetWindowFocus; + platform_io.Platform_GetWindowMinimized = ImGui_ImplGlfw_GetWindowMinimized; + platform_io.Platform_SetWindowTitle = ImGui_ImplGlfw_SetWindowTitle; + platform_io.Platform_RenderWindow = ImGui_ImplGlfw_RenderWindow; + platform_io.Platform_SwapBuffers = ImGui_ImplGlfw_SwapBuffers; +#if GLFW_HAS_WINDOW_ALPHA + platform_io.Platform_SetWindowAlpha = ImGui_ImplGlfw_SetWindowAlpha; +#endif +#if GLFW_HAS_VULKAN + platform_io.Platform_CreateVkSurface = ImGui_ImplGlfw_CreateVkSurface; +#endif + + // Register main window handle (which is owned by the main application, not by us) + // This is mostly for simplicity and consistency, so that our code (e.g. mouse handling etc.) can use same logic for main and secondary viewports. + ImGuiViewport* main_viewport = ImGui::GetMainViewport(); + ImGui_ImplGlfw_ViewportData* vd = IM_NEW(ImGui_ImplGlfw_ViewportData)(); + vd->Window = bd->Window; + vd->WindowOwned = false; + main_viewport->PlatformUserData = vd; + main_viewport->PlatformHandle = (void*)bd->Window; +} + +static void ImGui_ImplGlfw_ShutdownMultiViewportSupport() +{ + ImGui::DestroyPlatformWindows(); +} + +//----------------------------------------------------------------------------- + +// WndProc hook (declared here because we will need access to ImGui_ImplGlfw_ViewportData) +#ifdef _WIN32 +static ImGuiMouseSource GetMouseSourceFromMessageExtraInfo() +{ + LPARAM extra_info = ::GetMessageExtraInfo(); + if ((extra_info & 0xFFFFFF80) == 0xFF515700) + return ImGuiMouseSource_Pen; + if ((extra_info & 0xFFFFFF80) == 0xFF515780) + return ImGuiMouseSource_TouchScreen; + return ImGuiMouseSource_Mouse; +} +static LRESULT CALLBACK ImGui_ImplGlfw_WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + ImGui_ImplGlfw_Data* bd = ImGui_ImplGlfw_GetBackendData(); + WNDPROC prev_wndproc = bd->PrevWndProc; + ImGuiViewport* viewport = (ImGuiViewport*)::GetPropA(hWnd, "IMGUI_VIEWPORT"); + if (viewport != NULL) + if (ImGui_ImplGlfw_ViewportData* vd = (ImGui_ImplGlfw_ViewportData*)viewport->PlatformUserData) + prev_wndproc = vd->PrevWndProc; + + switch (msg) + { + // GLFW doesn't allow to distinguish Mouse vs TouchScreen vs Pen. + // Add support for Win32 (based on imgui_impl_win32), because we rely on _TouchScreen info to trickle inputs differently. + case WM_MOUSEMOVE: case WM_NCMOUSEMOVE: + case WM_LBUTTONDOWN: case WM_LBUTTONDBLCLK: case WM_LBUTTONUP: + case WM_RBUTTONDOWN: case WM_RBUTTONDBLCLK: case WM_RBUTTONUP: + case WM_MBUTTONDOWN: case WM_MBUTTONDBLCLK: case WM_MBUTTONUP: + case WM_XBUTTONDOWN: case WM_XBUTTONDBLCLK: case WM_XBUTTONUP: + ImGui::GetIO().AddMouseSourceEvent(GetMouseSourceFromMessageExtraInfo()); + break; + + // We have submitted https://github.com/glfw/glfw/pull/1568 to allow GLFW to support "transparent inputs". + // In the meanwhile we implement custom per-platform workarounds here (FIXME-VIEWPORT: Implement same work-around for Linux/OSX!) +#if !GLFW_HAS_MOUSE_PASSTHROUGH && GLFW_HAS_WINDOW_HOVERED + case WM_NCHITTEST: + { + // Let mouse pass-through the window. This will allow the backend to call io.AddMouseViewportEvent() properly (which is OPTIONAL). + // The ImGuiViewportFlags_NoInputs flag is set while dragging a viewport, as want to detect the window behind the one we are dragging. + // If you cannot easily access those viewport flags from your windowing/event code: you may manually synchronize its state e.g. in + // your main loop after calling UpdatePlatformWindows(). Iterate all viewports/platform windows and pass the flag to your windowing system. + if (viewport && (viewport->Flags & ImGuiViewportFlags_NoInputs)) + return HTTRANSPARENT; + break; + } +#endif + } + return ::CallWindowProcW(prev_wndproc, hWnd, msg, wParam, lParam); +} +#endif // #ifdef _WIN32 + +//----------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_impl_glfw.h b/imgui/imgui_impl_glfw.h index ccbe840d..fb6f3118 100644 --- a/imgui/imgui_impl_glfw.h +++ b/imgui/imgui_impl_glfw.h @@ -1,33 +1,69 @@ -// dear imgui: Platform Binding for GLFW -// This needs to be used along with a Renderer (e.g. OpenGL3, Vulkan..) +// dear imgui: Platform Backend for GLFW +// This needs to be used along with a Renderer (e.g. OpenGL3, Vulkan, WebGPU..) // (Info: GLFW is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan graphics context creation, etc.) +// (Requires: GLFW 3.1+. Prefer GLFW 3.3+ for full feature support.) // Implemented features: // [X] Platform: Clipboard support. +// [X] Platform: Mouse support. Can discriminate Mouse/TouchScreen/Pen (Windows only). +// [X] Platform: Keyboard support. Since 1.87 we are using the io.AddKeyEvent() function. Pass ImGuiKey values to all key functions e.g. ImGui::IsKeyPressed(ImGuiKey_Space). [Legacy GLFW_KEY_* values are obsolete since 1.87 and not supported since 1.91.5] // [X] Platform: Gamepad support. Enable with 'io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad'. -// [x] Platform: Mouse cursor shape and visibility. Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. FIXME: 3 cursors types are missing from GLFW. -// [X] Platform: Keyboard arrays indexed using GLFW_KEY_* codes, e.g. ImGui::IsKeyPressed(GLFW_KEY_SPACE). +// [X] Platform: Mouse cursor shape and visibility (ImGuiBackendFlags_HasMouseCursors). Resizing cursors requires GLFW 3.4+! Disable with 'io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange'. +// [X] Platform: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// Missing features or Issues: +// [ ] Touch events are only correctly identified as Touch on Windows. This create issues with some interactions. GLFW doesn't provide a way to identify touch inputs from mouse inputs, we cannot call io.AddMouseSourceEvent() to identify the source. We provide a Windows-specific workaround. +// [ ] Missing ImGuiMouseCursor_Wait and ImGuiMouseCursor_Progress cursors. +// [ ] Multi-viewport: ParentViewportID not honored, and so io.ConfigViewportsNoDefaultParent has no effect (minor). -// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this. -// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp. -// https://github.com/ocornut/imgui - -// About GLSL version: -// The 'glsl_version' initialization parameter defaults to "#version 150" if NULL. -// Only override if your GL version doesn't handle this GLSL version. Keep NULL if unsure! +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp #pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE struct GLFWwindow; +struct GLFWmonitor; +// Follow "Getting Started" link and check examples/ folder to learn about using backends! IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForOpenGL(GLFWwindow* window, bool install_callbacks); IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForVulkan(GLFWwindow* window, bool install_callbacks); +IMGUI_IMPL_API bool ImGui_ImplGlfw_InitForOther(GLFWwindow* window, bool install_callbacks); IMGUI_IMPL_API void ImGui_ImplGlfw_Shutdown(); IMGUI_IMPL_API void ImGui_ImplGlfw_NewFrame(); -// InitXXX function with 'install_callbacks=true': install GLFW callbacks. They will call user's previously installed callbacks, if any. -// InitXXX function with 'install_callbacks=false': do not install GLFW callbacks. You will need to call them yourself from your own GLFW callbacks. +// Emscripten related initialization phase methods (call after ImGui_ImplGlfw_InitForOpenGL) +#ifdef __EMSCRIPTEN__ +IMGUI_IMPL_API void ImGui_ImplGlfw_InstallEmscriptenCallbacks(GLFWwindow* window, const char* canvas_selector); +//static inline void ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback(const char* canvas_selector) { ImGui_ImplGlfw_InstallEmscriptenCallbacks(nullptr, canvas_selector); } } // Renamed in 1.91.0 +#endif + +// GLFW callbacks install +// - When calling Init with 'install_callbacks=true': ImGui_ImplGlfw_InstallCallbacks() is called. GLFW callbacks will be installed for you. They will chain-call user's previously installed callbacks, if any. +// - When calling Init with 'install_callbacks=false': GLFW callbacks won't be installed. You will need to call individual function yourself from your own GLFW callbacks. +IMGUI_IMPL_API void ImGui_ImplGlfw_InstallCallbacks(GLFWwindow* window); +IMGUI_IMPL_API void ImGui_ImplGlfw_RestoreCallbacks(GLFWwindow* window); + +// GFLW callbacks options: +// - Set 'chain_for_all_windows=true' to enable chaining callbacks for all windows (including secondary viewports created by backends or by user) +IMGUI_IMPL_API void ImGui_ImplGlfw_SetCallbacksChainForAllWindows(bool chain_for_all_windows); + +// GLFW callbacks (individual callbacks to call yourself if you didn't install callbacks) +IMGUI_IMPL_API void ImGui_ImplGlfw_WindowFocusCallback(GLFWwindow* window, int focused); // Since 1.84 +IMGUI_IMPL_API void ImGui_ImplGlfw_CursorEnterCallback(GLFWwindow* window, int entered); // Since 1.84 +IMGUI_IMPL_API void ImGui_ImplGlfw_CursorPosCallback(GLFWwindow* window, double x, double y); // Since 1.87 IMGUI_IMPL_API void ImGui_ImplGlfw_MouseButtonCallback(GLFWwindow* window, int button, int action, int mods); IMGUI_IMPL_API void ImGui_ImplGlfw_ScrollCallback(GLFWwindow* window, double xoffset, double yoffset); IMGUI_IMPL_API void ImGui_ImplGlfw_KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods); IMGUI_IMPL_API void ImGui_ImplGlfw_CharCallback(GLFWwindow* window, unsigned int c); +IMGUI_IMPL_API void ImGui_ImplGlfw_MonitorCallback(GLFWmonitor* monitor, int event); + +// GLFW helpers +IMGUI_IMPL_API void ImGui_ImplGlfw_Sleep(int milliseconds); + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_impl_opengl2.cpp b/imgui/imgui_impl_opengl2.cpp index 8bb303ac..d970e52a 100644 --- a/imgui/imgui_impl_opengl2.cpp +++ b/imgui/imgui_impl_opengl2.cpp @@ -1,12 +1,19 @@ -// dear imgui: Renderer for OpenGL2 (legacy OpenGL, fixed pipeline) -// This needs to be used along with a Platform Binding (e.g. GLFW, SDL, Win32, custom..) +// dear imgui: Renderer Backend for OpenGL2 (legacy OpenGL, fixed pipeline) +// This needs to be used along with a Platform Backend (e.g. GLFW, SDL, Win32, custom..) // Implemented features: -// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID in imgui.cpp. +// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID! +// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// Missing features or Issues: +// [ ] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). -// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this. -// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp. -// https://github.com/ocornut/imgui +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // **DO NOT USE THIS CODE IF YOUR CODE/ENGINE IS USING MODERN OPENGL (SHADERS, VBO, VAO, etc.)** // **Prefer using the code in imgui_impl_opengl3.cpp** @@ -18,6 +25,15 @@ // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2025-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. +// 2024-10-07: OpenGL: Changed default texture sampler to Clamp instead of Repeat/Wrap. +// 2024-06-28: OpenGL: ImGui_ImplOpenGL2_NewFrame() recreates font texture if it has been destroyed by ImGui_ImplOpenGL2_DestroyFontsTexture(). (#7748) +// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11. +// 2021-12-08: OpenGL: Fixed mishandling of the ImDrawCmd::IdxOffset field! This is an old bug but it never had an effect until some internal rendering changes in 1.86. +// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX). +// 2021-05-19: OpenGL: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement) +// 2021-01-03: OpenGL: Backup, setup and restore GL_SHADE_MODEL state, disable GL_STENCIL_TEST and disable GL_NORMAL_ARRAY client state to increase compatibility with legacy OpenGL applications. +// 2020-01-23: OpenGL: Backup, setup and restore GL_TEXTURE_ENV to increase compatibility with legacy OpenGL applications. // 2019-04-30: OpenGL: Added support for special ImDrawCallback_ResetRenderState callback to reset render state. // 2019-02-11: OpenGL: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display. // 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window. @@ -30,31 +46,88 @@ // 2016-09-05: OpenGL: Fixed save and restore of current scissor rectangle. #include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_impl_opengl2.h" +#include // intptr_t -#include // Needs to be initialized with gladLoadGL() in user's code +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used +#pragma clang diagnostic ignored "-Wnonportable-system-include-path" +#endif -// OpenGL Data -static GLuint g_FontTexture = 0; +#include // MaSzyna: GL functions come from glad, initialized by the simulator + +// [Debugging] +//#define IMGUI_IMPL_OPENGL_DEBUG +#ifdef IMGUI_IMPL_OPENGL_DEBUG +#include +#define GL_CALL(_CALL) do { _CALL; GLenum gl_err = glGetError(); if (gl_err != 0) fprintf(stderr, "GL error 0x%x returned from '%s'.\n", gl_err, #_CALL); } while (0) // Call with error check +#else +#define GL_CALL(_CALL) _CALL // Call without error check +#endif + +// OpenGL data +struct ImGui_ImplOpenGL2_Data +{ + GLuint FontTexture; + + ImGui_ImplOpenGL2_Data() { memset((void*)this, 0, sizeof(*this)); } +}; + +// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts +// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts. +static ImGui_ImplOpenGL2_Data* ImGui_ImplOpenGL2_GetBackendData() +{ + return ImGui::GetCurrentContext() ? (ImGui_ImplOpenGL2_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; +} + +// Forward Declarations +static void ImGui_ImplOpenGL2_InitMultiViewportSupport(); +static void ImGui_ImplOpenGL2_ShutdownMultiViewportSupport(); // Functions bool ImGui_ImplOpenGL2_Init() { - // Setup back-end capabilities flags ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); + + // Setup backend capabilities flags + ImGui_ImplOpenGL2_Data* bd = IM_NEW(ImGui_ImplOpenGL2_Data)(); + io.BackendRendererUserData = (void*)bd; io.BackendRendererName = "imgui_impl_opengl2"; + io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional) + + ImGui_ImplOpenGL2_InitMultiViewportSupport(); + return true; } void ImGui_ImplOpenGL2_Shutdown() { + ImGui_ImplOpenGL2_Data* bd = ImGui_ImplOpenGL2_GetBackendData(); + IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); + ImGuiIO& io = ImGui::GetIO(); + + ImGui_ImplOpenGL2_ShutdownMultiViewportSupport(); ImGui_ImplOpenGL2_DestroyDeviceObjects(); + io.BackendRendererName = nullptr; + io.BackendRendererUserData = nullptr; + io.BackendFlags &= ~ImGuiBackendFlags_RendererHasViewports; + IM_DELETE(bd); } void ImGui_ImplOpenGL2_NewFrame() { - if (!g_FontTexture) + ImGui_ImplOpenGL2_Data* bd = ImGui_ImplOpenGL2_GetBackendData(); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplOpenGL2_Init()?"); + + if (!bd->FontTexture) ImGui_ImplOpenGL2_CreateDeviceObjects(); + if (!bd->FontTexture) + ImGui_ImplOpenGL2_CreateFontsTexture(); } static void ImGui_ImplOpenGL2_SetupRenderState(ImDrawData* draw_data, int fb_width, int fb_height) @@ -62,28 +135,36 @@ static void ImGui_ImplOpenGL2_SetupRenderState(ImDrawData* draw_data, int fb_wid // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, vertex/texcoord/color pointers, polygon fill. glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + //glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); // In order to composite our output buffer we need to preserve alpha glDisable(GL_CULL_FACE); glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); glDisable(GL_LIGHTING); glDisable(GL_COLOR_MATERIAL); glEnable(GL_SCISSOR_TEST); glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glEnableClientState(GL_COLOR_ARRAY); + glDisableClientState(GL_NORMAL_ARRAY); glEnable(GL_TEXTURE_2D); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glShadeModel(GL_SMOOTH); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); // If you are using this code with non-legacy OpenGL header/contexts (which you should not, prefer using imgui_impl_opengl3.cpp!!), - // you may need to backup/reset/restore current shader using the lines below. DO NOT MODIFY THIS FILE! Add the code in your calling function: - // GLint last_program; - // glGetIntegerv(GL_CURRENT_PROGRAM, &last_program); - // glUseProgram(0); - // ImGui_ImplOpenGL2_RenderDrawData(...); - // glUseProgram(last_program) + // you may need to backup/reset/restore other state, e.g. for current shader using the commented lines below. + // (DO NOT MODIFY THIS FILE! Add the code in your calling function) + // GLint last_program; + // glGetIntegerv(GL_CURRENT_PROGRAM, &last_program); + // glUseProgram(0); + // ImGui_ImplOpenGL2_RenderDrawData(...); + // glUseProgram(last_program) + // There are potentially many more states you could need to clear/setup that we can't access from default headers. + // e.g. glBindBuffer(GL_ARRAY_BUFFER, 0), glDisable(GL_TEXTURE_CUBE_MAP). // Setup viewport, orthographic projection matrix // Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps. - glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height); + GL_CALL(glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height)); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); @@ -94,8 +175,8 @@ static void ImGui_ImplOpenGL2_SetupRenderState(ImDrawData* draw_data, int fb_wid } // OpenGL2 Render function. -// (this used to be set in io.RenderDrawListsFn and called by ImGui::Render(), but you can now call this directly from your main loop) -// Note that this implementation is little overcomplicated because we are saving/setting up/restoring every OpenGL state explicitly, in order to be able to run within any OpenGL engine that doesn't do so. +// Note that this implementation is little overcomplicated because we are saving/setting up/restoring every OpenGL state explicitly. +// This is in order to be able to run within an OpenGL engine that doesn't do so. void ImGui_ImplOpenGL2_RenderDrawData(ImDrawData* draw_data) { // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) @@ -109,6 +190,8 @@ void ImGui_ImplOpenGL2_RenderDrawData(ImDrawData* draw_data) GLint last_polygon_mode[2]; glGetIntegerv(GL_POLYGON_MODE, last_polygon_mode); GLint last_viewport[4]; glGetIntegerv(GL_VIEWPORT, last_viewport); GLint last_scissor_box[4]; glGetIntegerv(GL_SCISSOR_BOX, last_scissor_box); + GLint last_shade_model; glGetIntegerv(GL_SHADE_MODEL, &last_shade_model); + GLint last_tex_env_mode; glGetTexEnviv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, &last_tex_env_mode); glPushAttrib(GL_ENABLE_BIT | GL_COLOR_BUFFER_BIT | GL_TRANSFORM_BIT); // Setup desired GL state @@ -121,16 +204,16 @@ void ImGui_ImplOpenGL2_RenderDrawData(ImDrawData* draw_data) // Render command lists for (int n = 0; n < draw_data->CmdListsCount; n++) { - const ImDrawList* cmd_list = draw_data->CmdLists[n]; - const ImDrawVert* vtx_buffer = cmd_list->VtxBuffer.Data; - const ImDrawIdx* idx_buffer = cmd_list->IdxBuffer.Data; - glVertexPointer(2, GL_FLOAT, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + IM_OFFSETOF(ImDrawVert, pos))); - glTexCoordPointer(2, GL_FLOAT, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + IM_OFFSETOF(ImDrawVert, uv))); - glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + IM_OFFSETOF(ImDrawVert, col))); + const ImDrawList* draw_list = draw_data->CmdLists[n]; + const ImDrawVert* vtx_buffer = draw_list->VtxBuffer.Data; + const ImDrawIdx* idx_buffer = draw_list->IdxBuffer.Data; + glVertexPointer(2, GL_FLOAT, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + offsetof(ImDrawVert, pos))); + glTexCoordPointer(2, GL_FLOAT, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + offsetof(ImDrawVert, uv))); + glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(ImDrawVert), (const GLvoid*)((const char*)vtx_buffer + offsetof(ImDrawVert, col))); - for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++) { - const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; + const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i]; if (pcmd->UserCallback) { // User callback, registered via ImDrawList::AddCallback() @@ -138,28 +221,23 @@ void ImGui_ImplOpenGL2_RenderDrawData(ImDrawData* draw_data) if (pcmd->UserCallback == ImDrawCallback_ResetRenderState) ImGui_ImplOpenGL2_SetupRenderState(draw_data, fb_width, fb_height); else - pcmd->UserCallback(cmd_list, pcmd); + pcmd->UserCallback(draw_list, pcmd); } else { // Project scissor/clipping rectangles into framebuffer space - ImVec4 clip_rect; - clip_rect.x = (pcmd->ClipRect.x - clip_off.x) * clip_scale.x; - clip_rect.y = (pcmd->ClipRect.y - clip_off.y) * clip_scale.y; - clip_rect.z = (pcmd->ClipRect.z - clip_off.x) * clip_scale.x; - clip_rect.w = (pcmd->ClipRect.w - clip_off.y) * clip_scale.y; + ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; - if (clip_rect.x < fb_width && clip_rect.y < fb_height && clip_rect.z >= 0.0f && clip_rect.w >= 0.0f) - { - // Apply scissor/clipping rectangle - glScissor((int)clip_rect.x, (int)(fb_height - clip_rect.w), (int)(clip_rect.z - clip_rect.x), (int)(clip_rect.w - clip_rect.y)); + // Apply scissor/clipping rectangle (Y is inverted in OpenGL) + glScissor((int)clip_min.x, (int)((float)fb_height - clip_max.y), (int)(clip_max.x - clip_min.x), (int)(clip_max.y - clip_min.y)); - // Bind texture, Draw - glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->TextureId); - glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer); - } + // Bind texture, Draw + glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->GetTexID()); + glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer + pcmd->IdxOffset); } - idx_buffer += pcmd->ElemCount; } } @@ -176,28 +254,34 @@ void ImGui_ImplOpenGL2_RenderDrawData(ImDrawData* draw_data) glPolygonMode(GL_FRONT, (GLenum)last_polygon_mode[0]); glPolygonMode(GL_BACK, (GLenum)last_polygon_mode[1]); glViewport(last_viewport[0], last_viewport[1], (GLsizei)last_viewport[2], (GLsizei)last_viewport[3]); glScissor(last_scissor_box[0], last_scissor_box[1], (GLsizei)last_scissor_box[2], (GLsizei)last_scissor_box[3]); + glShadeModel(last_shade_model); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, last_tex_env_mode); } bool ImGui_ImplOpenGL2_CreateFontsTexture() { // Build texture atlas ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplOpenGL2_Data* bd = ImGui_ImplOpenGL2_GetBackendData(); unsigned char* pixels; int width, height; - io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); // Load as RGBA 32-bits (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory. + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); // Load as RGBA 32-bit (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory. // Upload texture to graphics system + // (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling) GLint last_texture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); - glGenTextures(1, &g_FontTexture); - glBindTexture(GL_TEXTURE_2D, g_FontTexture); + glGenTextures(1, &bd->FontTexture); + glBindTexture(GL_TEXTURE_2D, bd->FontTexture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); // Store our identifier - io.Fonts->TexID = (ImTextureID)(intptr_t)g_FontTexture; + io.Fonts->SetTexID((ImTextureID)(intptr_t)bd->FontTexture); // Restore state glBindTexture(GL_TEXTURE_2D, last_texture); @@ -207,12 +291,13 @@ bool ImGui_ImplOpenGL2_CreateFontsTexture() void ImGui_ImplOpenGL2_DestroyFontsTexture() { - if (g_FontTexture) + ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplOpenGL2_Data* bd = ImGui_ImplOpenGL2_GetBackendData(); + if (bd->FontTexture) { - ImGuiIO& io = ImGui::GetIO(); - glDeleteTextures(1, &g_FontTexture); - io.Fonts->TexID = 0; - g_FontTexture = 0; + glDeleteTextures(1, &bd->FontTexture); + io.Fonts->SetTexID(0); + bd->FontTexture = 0; } } @@ -225,3 +310,40 @@ void ImGui_ImplOpenGL2_DestroyDeviceObjects() { ImGui_ImplOpenGL2_DestroyFontsTexture(); } + + +//-------------------------------------------------------------------------------------------------------- +// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT +// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously. +// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first.. +//-------------------------------------------------------------------------------------------------------- + +static void ImGui_ImplOpenGL2_RenderWindow(ImGuiViewport* viewport, void*) +{ + if (!(viewport->Flags & ImGuiViewportFlags_NoRendererClear)) + { + ImVec4 clear_color = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); + glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w); + glClear(GL_COLOR_BUFFER_BIT); + } + ImGui_ImplOpenGL2_RenderDrawData(viewport->DrawData); +} + +static void ImGui_ImplOpenGL2_InitMultiViewportSupport() +{ + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Renderer_RenderWindow = ImGui_ImplOpenGL2_RenderWindow; +} + +static void ImGui_ImplOpenGL2_ShutdownMultiViewportSupport() +{ + ImGui::DestroyPlatformWindows(); +} + +//----------------------------------------------------------------------------- + +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_impl_opengl2.h b/imgui/imgui_impl_opengl2.h index 2e3271dc..c7b40e07 100644 --- a/imgui/imgui_impl_opengl2.h +++ b/imgui/imgui_impl_opengl2.h @@ -1,12 +1,19 @@ -// dear imgui: Renderer for OpenGL2 (legacy OpenGL, fixed pipeline) -// This needs to be used along with a Platform Binding (e.g. GLFW, SDL, Win32, custom..) +// dear imgui: Renderer Backend for OpenGL2 (legacy OpenGL, fixed pipeline) +// This needs to be used along with a Platform Backend (e.g. GLFW, SDL, Win32, custom..) // Implemented features: -// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID in imgui.cpp. +// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID! +// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. +// Missing features or Issues: +// [ ] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). -// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this. -// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp. -// https://github.com/ocornut/imgui +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // **DO NOT USE THIS CODE IF YOUR CODE/ENGINE IS USING MODERN OPENGL (SHADERS, VBO, VAO, etc.)** // **Prefer using the code in imgui_impl_opengl3.cpp** @@ -17,7 +24,10 @@ // The GL2 code is unable to reset attributes or even call e.g. "glUseProgram(0)" because they don't exist in that API. #pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE +// Follow "Getting Started" link and check examples/ folder to learn about using backends! IMGUI_IMPL_API bool ImGui_ImplOpenGL2_Init(); IMGUI_IMPL_API void ImGui_ImplOpenGL2_Shutdown(); IMGUI_IMPL_API void ImGui_ImplOpenGL2_NewFrame(); @@ -28,3 +38,5 @@ IMGUI_IMPL_API bool ImGui_ImplOpenGL2_CreateFontsTexture(); IMGUI_IMPL_API void ImGui_ImplOpenGL2_DestroyFontsTexture(); IMGUI_IMPL_API bool ImGui_ImplOpenGL2_CreateDeviceObjects(); IMGUI_IMPL_API void ImGui_ImplOpenGL2_DestroyDeviceObjects(); + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_impl_opengl3.cpp b/imgui/imgui_impl_opengl3.cpp index e4bf631c..4ce48f5c 100644 --- a/imgui/imgui_impl_opengl3.cpp +++ b/imgui/imgui_impl_opengl3.cpp @@ -1,24 +1,75 @@ -// dear imgui: Renderer for modern OpenGL with shaders / programmatic pipeline +// dear imgui: Renderer Backend for modern OpenGL with shaders / programmatic pipeline // - Desktop GL: 2.x 3.x 4.x // - Embedded GL: ES 2.0 (WebGL 1.0), ES 3.0 (WebGL 2.0) -// This needs to be used along with a Platform Binding (e.g. GLFW, SDL, Win32, custom..) +// This needs to be used along with a Platform Backend (e.g. GLFW, SDL, Win32, custom..) // Implemented features: -// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID in imgui.cpp. -// [x] Renderer: Desktop GL only: Support for large meshes (64k+ vertices) with 16-bits indices. +// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID! +// [x] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset) [Desktop OpenGL only!] +// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. -// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this. -// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp. -// https://github.com/ocornut/imgui +// About WebGL/ES: +// - You need to '#define IMGUI_IMPL_OPENGL_ES2' or '#define IMGUI_IMPL_OPENGL_ES3' to use WebGL or OpenGL ES. +// - This is done automatically on iOS, Android and Emscripten targets. +// - For other targets, the define needs to be visible from the imgui_impl_opengl3.cpp compilation unit. If unsure, define globally or in imconfig.h. + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // CHANGELOG // (minor and older changes stripped away, please see git history for details) +// 2025-XX-XX: Platform: Added support for multiple windows via the ImGuiPlatformIO interface. +// 2025-02-18: OpenGL: Lazily reinitialize embedded GL loader for when calling backend from e.g. other DLL boundaries. (#8406) +// 2024-10-07: OpenGL: Changed default texture sampler to Clamp instead of Repeat/Wrap. +// 2024-06-28: OpenGL: ImGui_ImplOpenGL3_NewFrame() recreates font texture if it has been destroyed by ImGui_ImplOpenGL3_DestroyFontsTexture(). (#7748) +// 2024-05-07: OpenGL: Update loader for Linux to support EGL/GLVND. (#7562) +// 2024-04-16: OpenGL: Detect ES3 contexts on desktop based on version string, to e.g. avoid calling glPolygonMode() on them. (#7447) +// 2024-01-09: OpenGL: Update GL3W based imgui_impl_opengl3_loader.h to load "libGL.so" and variants, fixing regression on distros missing a symlink. +// 2023-11-08: OpenGL: Update GL3W based imgui_impl_opengl3_loader.h to load "libGL.so" instead of "libGL.so.1", accommodating for NetBSD systems having only "libGL.so.3" available. (#6983) +// 2023-10-05: OpenGL: Rename symbols in our internal loader so that LTO compilation with another copy of gl3w is possible. (#6875, #6668, #4445) +// 2023-06-20: OpenGL: Fixed erroneous use glGetIntegerv(GL_CONTEXT_PROFILE_MASK) on contexts lower than 3.2. (#6539, #6333) +// 2023-05-09: OpenGL: Support for glBindSampler() backup/restore on ES3. (#6375) +// 2023-04-18: OpenGL: Restore front and back polygon mode separately when supported by context. (#6333) +// 2023-03-23: OpenGL: Properly restoring "no shader program bound" if it was the case prior to running the rendering function. (#6267, #6220, #6224) +// 2023-03-15: OpenGL: Fixed GL loader crash when GL_VERSION returns NULL. (#6154, #4445, #3530) +// 2023-03-06: OpenGL: Fixed restoration of a potentially deleted OpenGL program, by calling glIsProgram(). (#6220, #6224) +// 2022-11-09: OpenGL: Reverted use of glBufferSubData(), too many corruptions issues + old issues seemingly can't be reproed with Intel drivers nowadays (revert 2021-12-15 and 2022-05-23 changes). +// 2022-10-11: Using 'nullptr' instead of 'NULL' as per our switch to C++11. +// 2022-09-27: OpenGL: Added ability to '#define IMGUI_IMPL_OPENGL_DEBUG'. +// 2022-05-23: OpenGL: Reworking 2021-12-15 "Using buffer orphaning" so it only happens on Intel GPU, seems to cause problems otherwise. (#4468, #4825, #4832, #5127). +// 2022-05-13: OpenGL: Fixed state corruption on OpenGL ES 2.0 due to not preserving GL_ELEMENT_ARRAY_BUFFER_BINDING and vertex attribute states. +// 2021-12-15: OpenGL: Using buffer orphaning + glBufferSubData(), seems to fix leaks with multi-viewports with some Intel HD drivers. +// 2021-08-23: OpenGL: Fixed ES 3.0 shader ("#version 300 es") use normal precision floats to avoid wobbly rendering at HD resolutions. +// 2021-08-19: OpenGL: Embed and use our own minimal GL loader (imgui_impl_opengl3_loader.h), removing requirement and support for third-party loader. +// 2021-06-29: Reorganized backend to pull data from a single structure to facilitate usage with multiple-contexts (all g_XXXX access changed to bd->XXXX). +// 2021-06-25: OpenGL: Use OES_vertex_array extension on Emscripten + backup/restore current state. +// 2021-06-21: OpenGL: Destroy individual vertex/fragment shader objects right after they are linked into the main shader. +// 2021-05-24: OpenGL: Access GL_CLIP_ORIGIN when "GL_ARB_clip_control" extension is detected, inside of just OpenGL 4.5 version. +// 2021-05-19: OpenGL: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement) +// 2021-04-06: OpenGL: Don't try to read GL_CLIP_ORIGIN unless we're OpenGL 4.5 or greater. +// 2021-02-18: OpenGL: Change blending equation to preserve alpha in output buffer. +// 2021-01-03: OpenGL: Backup, setup and restore GL_STENCIL_TEST state. +// 2020-10-23: OpenGL: Backup, setup and restore GL_PRIMITIVE_RESTART state. +// 2020-10-15: OpenGL: Use glGetString(GL_VERSION) instead of glGetIntegerv(GL_MAJOR_VERSION, ...) when the later returns zero (e.g. Desktop GL 2.x) +// 2020-09-17: OpenGL: Fix to avoid compiling/calling glBindSampler() on ES or pre-3.3 context which have the defines set by a loader. +// 2020-07-10: OpenGL: Added support for glad2 OpenGL loader. +// 2020-05-08: OpenGL: Made default GLSL version 150 (instead of 130) on OSX. +// 2020-04-21: OpenGL: Fixed handling of glClipControl(GL_UPPER_LEFT) by inverting projection matrix. +// 2020-04-12: OpenGL: Fixed context version check mistakenly testing for 4.0+ instead of 3.2+ to enable ImGuiBackendFlags_RendererHasVtxOffset. +// 2020-03-24: OpenGL: Added support for glbinding 2.x OpenGL loader. +// 2020-01-07: OpenGL: Added support for glbinding 3.x OpenGL loader. +// 2019-10-25: OpenGL: Using a combination of GL define and runtime GL version to decide whether to use glDrawElementsBaseVertex(). Fix building with pre-3.2 GL loaders. // 2019-09-22: OpenGL: Detect default GL loader using __has_include compiler facility. // 2019-09-16: OpenGL: Tweak initialization code to allow application calling ImGui_ImplOpenGL3_CreateFontsTexture() before the first NewFrame() call. // 2019-05-29: OpenGL: Desktop GL only: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag. // 2019-04-30: OpenGL: Added support for special ImDrawCallback_ResetRenderState callback to reset render state. // 2019-03-29: OpenGL: Not calling glBindBuffer more than necessary in the render loop. -// 2019-03-15: OpenGL: Added a dummy GL call + comments in ImGui_ImplOpenGL3_Init() to detect uninitialized GL function loaders early. +// 2019-03-15: OpenGL: Added a GL call + comments in ImGui_ImplOpenGL3_Init() to detect uninitialized GL function loaders early. // 2019-03-03: OpenGL: Fix support for ES 2.0 (WebGL 1.0). // 2019-02-20: OpenGL: Fix for OSX not supporting OpenGL 4.5, we don't try to read GL_CLIP_ORIGIN even if defined by the headers/loader. // 2019-02-11: OpenGL: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display. @@ -32,7 +83,7 @@ // 2018-06-08: Misc: Extracted imgui_impl_opengl3.cpp/.h away from the old combined GLFW/SDL+OpenGL3 examples. // 2018-06-08: OpenGL: Use draw_data->DisplayPos and draw_data->DisplaySize to setup projection matrix and clipping rectangle. // 2018-05-25: OpenGL: Removed unnecessary backup/restore of GL_ELEMENT_ARRAY_BUFFER_BINDING since this is part of the VAO state. -// 2018-05-14: OpenGL: Making the call to glBindSampler() optional so 3.2 context won't fail if the function is a NULL pointer. +// 2018-05-14: OpenGL: Making the call to glBindSampler() optional so 3.2 context won't fail if the function is a nullptr pointer. // 2018-03-06: OpenGL: Added const char* glsl_version parameter to ImGui_ImplOpenGL3_Init() so user can override the GLSL version e.g. "#version 150". // 2018-02-23: OpenGL: Create the VAO in the render function so the setup can more easily be used with multiple shared GL context. // 2018-02-16: Misc: Obsoleted the io.RenderDrawListsFn callback and exposed ImGui_ImplSdlGL3_RenderDrawData() in the .h file so you can call it yourself. @@ -61,94 +112,378 @@ // ES 3.0 300 "#version 300 es" = WebGL 2.0 //---------------------------------------- -#include "stdafx.h" -#include "utilities/Globals.h" - #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) #define _CRT_SECURE_NO_WARNINGS #endif #include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_impl_opengl3.h" +// MaSzyna: GL functions come from glad, initialized by the simulator (desktop GL and GLES contexts alike); +// srgb and gamma handling of the ui follows the settings of the renderer +#include "stdafx.h" +#include "utilities/Globals.h" +#include +#include +#include // intptr_t +#if defined(__APPLE__) +#include +#endif -#include // Needs to be initialized with gladLoadGL() in user's code +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: ignore unknown flags +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used +#pragma clang diagnostic ignored "-Wnonportable-system-include-path" +#pragma clang diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader) +#endif +#if defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' +#pragma GCC diagnostic ignored "-Wcast-function-type" // warning: cast between incompatible function types (for loader) +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1 +#endif + +// GL includes +#if defined(IMGUI_IMPL_OPENGL_ES2) +#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)) +#include // Use GL ES 2 +#else +#include // Use GL ES 2 +#endif +#if defined(__EMSCRIPTEN__) +#ifndef GL_GLEXT_PROTOTYPES +#define GL_GLEXT_PROTOTYPES +#endif +#include +#endif +#elif defined(IMGUI_IMPL_OPENGL_ES3) +#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)) +#include // Use GL ES 3 +#else +#include // Use GL ES 3 +#endif +#elif !defined(IMGUI_IMPL_OPENGL_LOADER_CUSTOM) +// Modern desktop OpenGL doesn't have a standard portable header file to load OpenGL function pointers. +// Helper libraries are often used for this purpose! Here we are using our own minimal custom loader based on gl3w. +// In the rest of your app/engine, you can use another loader of your choice (gl3w, glew, glad, glbinding, glext, glLoadGen, etc.). +// If you happen to be developing a new feature for this backend (imgui_impl_opengl3.cpp): +// - You may need to regenerate imgui_impl_opengl3_loader.h to add new symbols. See https://github.com/dearimgui/gl3w_stripped +// Typically you would run: python3 ./gl3w_gen.py --output ../imgui/backends/imgui_impl_opengl3_loader.h --ref ../imgui/backends/imgui_impl_opengl3.cpp ./extra_symbols.txt +// - You can temporarily use an unstripped version. See https://github.com/dearimgui/gl3w_stripped/releases +// Changes to this backend using new APIs should be accompanied by a regenerated stripped loader version. +#define IMGL3W_IMPL +#define IMGUI_IMPL_OPENGL_LOADER_IMGL3W +#include "imgui_impl_opengl3_loader.h" +#endif + +// Vertex arrays are not supported on ES2/WebGL1 unless Emscripten which uses an extension +#ifndef IMGUI_IMPL_OPENGL_ES2 +#define IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY +#elif defined(__EMSCRIPTEN__) +#define IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY +#define glBindVertexArray glBindVertexArrayOES +#define glGenVertexArrays glGenVertexArraysOES +#define glDeleteVertexArrays glDeleteVertexArraysOES +#define GL_VERTEX_ARRAY_BINDING GL_VERTEX_ARRAY_BINDING_OES +#endif + +// Desktop GL 2.0+ has extension and glPolygonMode() which GL ES and WebGL don't have.. +// A desktop ES context can technically compile fine with our loader, so we also perform a runtime checks +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) +#define IMGUI_IMPL_OPENGL_HAS_EXTENSIONS // has glGetIntegerv(GL_NUM_EXTENSIONS) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE // may have glPolygonMode() +#endif + +// Desktop GL 2.1+ and GL ES 3.0+ have glBindBuffer() with GL_PIXEL_UNPACK_BUFFER target. +#if !defined(IMGUI_IMPL_OPENGL_ES2) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK +#endif + +// Desktop GL 3.1+ has GL_PRIMITIVE_RESTART state +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && defined(GL_VERSION_3_1) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART +#endif + +// Desktop GL 3.2+ has glDrawElementsBaseVertex() which GL ES and WebGL don't have. +#if !defined(IMGUI_IMPL_OPENGL_ES2) && !defined(IMGUI_IMPL_OPENGL_ES3) && defined(GL_VERSION_3_2) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET +#endif + +// Desktop GL 3.3+ and GL ES 3.0+ have glBindSampler() +#if !defined(IMGUI_IMPL_OPENGL_ES2) && (defined(IMGUI_IMPL_OPENGL_ES3) || defined(GL_VERSION_3_3)) +#define IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER +#endif + +// [Debugging] +//#define IMGUI_IMPL_OPENGL_DEBUG +#ifdef IMGUI_IMPL_OPENGL_DEBUG +#include +#define GL_CALL(_CALL) do { _CALL; GLenum gl_err = glGetError(); if (gl_err != 0) fprintf(stderr, "GL error 0x%x returned from '%s'.\n", gl_err, #_CALL); } while (0) // Call with error check +#else +#define GL_CALL(_CALL) _CALL // Call without error check +#endif // OpenGL Data -static char g_GlslVersionString[128] = ""; -static GLuint g_FontTexture = 0; -static GLuint g_ShaderHandle = 0, g_VertHandle = 0, g_FragHandle = 0; -static int g_AttribLocationTex = 0, g_AttribLocationProjMtx = 0; // Uniforms location -static int g_AttribLocationVtxPos = 0, g_AttribLocationVtxUV = 0, g_AttribLocationVtxColor = 0; // Vertex attributes location -static unsigned int g_VboHandle = 0, g_ElementsHandle = 0; -static int IMGUI_IMPL_OPENGL_HAS_DRAW_WITH_BASE_VERTEX = 0; +struct ImGui_ImplOpenGL3_Data +{ + GLuint GlVersion; // Extracted at runtime using GL_MAJOR_VERSION, GL_MINOR_VERSION queries (e.g. 320 for GL 3.2) + char GlslVersionString[128]; // Specified by user or detected based on compile time GL settings. + bool GlProfileIsES2; + bool GlProfileIsES3; + bool GlProfileIsCompat; + GLint GlProfileMask; + GLuint FontTexture; + GLuint ShaderHandle; + GLint AttribLocationTex; // Uniforms location + GLint AttribLocationProjMtx; + GLuint AttribLocationVtxPos; // Vertex attributes location + GLuint AttribLocationVtxUV; + GLuint AttribLocationVtxColor; + unsigned int VboHandle, ElementsHandle; + GLsizeiptr VertexBufferSize; + GLsizeiptr IndexBufferSize; + bool HasPolygonMode; + bool HasClipOrigin; + bool UseBufferSubData; + + ImGui_ImplOpenGL3_Data() { memset((void*)this, 0, sizeof(*this)); } +}; + +// Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts +// It is STRONGLY preferred that you use docking branch with multi-viewports (== single Dear ImGui context + multiple windows) instead of multiple Dear ImGui contexts. +static ImGui_ImplOpenGL3_Data* ImGui_ImplOpenGL3_GetBackendData() +{ + return ImGui::GetCurrentContext() ? (ImGui_ImplOpenGL3_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; +} + +// Forward Declarations +static void ImGui_ImplOpenGL3_InitMultiViewportSupport(); +static void ImGui_ImplOpenGL3_ShutdownMultiViewportSupport(); + +// OpenGL vertex attribute state (for ES 1.0 and ES 2.0 only) +#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY +struct ImGui_ImplOpenGL3_VtxAttribState +{ + GLint Enabled, Size, Type, Normalized, Stride; + GLvoid* Ptr; + + void GetState(GLint index) + { + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_ENABLED, &Enabled); + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_SIZE, &Size); + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_TYPE, &Type); + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_NORMALIZED, &Normalized); + glGetVertexAttribiv(index, GL_VERTEX_ATTRIB_ARRAY_STRIDE, &Stride); + glGetVertexAttribPointerv(index, GL_VERTEX_ATTRIB_ARRAY_POINTER, &Ptr); + } + void SetState(GLint index) + { + glVertexAttribPointer(index, Size, Type, (GLboolean)Normalized, Stride, Ptr); + if (Enabled) glEnableVertexAttribArray(index); else glDisableVertexAttribArray(index); + } +}; +#endif + +// Not static to allow third-party code to use that if they want to (but undocumented) +bool ImGui_ImplOpenGL3_InitLoader(); +bool ImGui_ImplOpenGL3_InitLoader() +{ + // Initialize our loader +#ifdef IMGUI_IMPL_OPENGL_LOADER_IMGL3W + if (glGetIntegerv == nullptr && imgl3wInit() != 0) + { + fprintf(stderr, "Failed to initialize OpenGL loader!\n"); + return false; + } +#endif + return true; +} // Functions bool ImGui_ImplOpenGL3_Init(const char* glsl_version) { - // Setup back-end capabilities flags ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); + + // Initialize loader + if (!ImGui_ImplOpenGL3_InitLoader()) + return false; + + // Setup backend capabilities flags + ImGui_ImplOpenGL3_Data* bd = IM_NEW(ImGui_ImplOpenGL3_Data)(); + io.BackendRendererUserData = (void*)bd; io.BackendRendererName = "imgui_impl_opengl3"; - if (GLAD_GL_VERSION_3_3 || GLAD_GL_ES_VERSION_3_2) - IMGUI_IMPL_OPENGL_HAS_DRAW_WITH_BASE_VERTEX = 1; - -#if IMGUI_IMPL_OPENGL_HAS_DRAW_WITH_BASE_VERTEX - io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. + // Query for GL version (e.g. 320 for GL 3.2) + const char* gl_version_str = (const char*)glGetString(GL_VERSION); +#if defined(IMGUI_IMPL_OPENGL_ES2) + // GLES 2 + bd->GlVersion = 200; + bd->GlProfileIsES2 = true; + IM_UNUSED(gl_version_str); +#else + // Desktop or GLES 3 + GLint major = 0; + GLint minor = 0; + glGetIntegerv(GL_MAJOR_VERSION, &major); + glGetIntegerv(GL_MINOR_VERSION, &minor); + if (major == 0 && minor == 0) + sscanf(gl_version_str, "%d.%d", &major, &minor); // Query GL_VERSION in desktop GL 2.x, the string will start with "." + bd->GlVersion = (GLuint)(major * 100 + minor * 10); +#if defined(GL_CONTEXT_PROFILE_MASK) + if (bd->GlVersion >= 320) + glGetIntegerv(GL_CONTEXT_PROFILE_MASK, &bd->GlProfileMask); + bd->GlProfileIsCompat = (bd->GlProfileMask & GL_CONTEXT_COMPATIBILITY_PROFILE_BIT) != 0; #endif - // Store GLSL version string so we can refer to it later in case we recreate shaders. Note: GLSL version is NOT the same as GL version. Leave this to NULL if unsure. +#if defined(IMGUI_IMPL_OPENGL_ES3) + bd->GlProfileIsES3 = true; +#else + if (strncmp(gl_version_str, "OpenGL ES 3", 11) == 0) + bd->GlProfileIsES3 = true; +#endif + + bd->UseBufferSubData = false; + /* + // Query vendor to enable glBufferSubData kludge +#ifdef _WIN32 + if (const char* vendor = (const char*)glGetString(GL_VENDOR)) + if (strncmp(vendor, "Intel", 5) == 0) + bd->UseBufferSubData = true; +#endif + */ +#endif + +#ifdef IMGUI_IMPL_OPENGL_DEBUG + printf("GlVersion = %d, \"%s\"\nGlProfileIsCompat = %d\nGlProfileMask = 0x%X\nGlProfileIsES2/IsEs3 = %d/%d\nGL_VENDOR = '%s'\nGL_RENDERER = '%s'\n", bd->GlVersion, gl_version_str, bd->GlProfileIsCompat, bd->GlProfileMask, bd->GlProfileIsES2, bd->GlProfileIsES3, (const char*)glGetString(GL_VENDOR), (const char*)glGetString(GL_RENDERER)); // [DEBUG] +#endif + +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET + if (bd->GlVersion >= 320) + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. +#endif + io.BackendFlags |= ImGuiBackendFlags_RendererHasViewports; // We can create multi-viewports on the Renderer side (optional) + + // Store GLSL version string so we can refer to it later in case we recreate shaders. + // Note: GLSL version is NOT the same as GL version. Leave this to nullptr if unsure. + if (glsl_version == nullptr) + { #if defined(IMGUI_IMPL_OPENGL_ES2) - if (glsl_version == NULL) glsl_version = "#version 100"; #elif defined(IMGUI_IMPL_OPENGL_ES3) - if (glsl_version == NULL) glsl_version = "#version 300 es"; +#elif defined(__APPLE__) + glsl_version = "#version 150"; #else - if (glsl_version == NULL) glsl_version = "#version 130"; #endif - IM_ASSERT((int)strlen(glsl_version) + 2 < IM_ARRAYSIZE(g_GlslVersionString)); - strcpy(g_GlslVersionString, glsl_version); - strcat(g_GlslVersionString, "\n"); + } + IM_ASSERT((int)strlen(glsl_version) + 2 < IM_ARRAYSIZE(bd->GlslVersionString)); + strcpy(bd->GlslVersionString, glsl_version); + strcat(bd->GlslVersionString, "\n"); - // Make a dummy GL call (we don't actually need the result) - // IF YOU GET A CRASH HERE: it probably means that you haven't initialized the OpenGL function loader used by this code. - // Desktop OpenGL 3/4 need a function loader. See the IMGUI_IMPL_OPENGL_LOADER_xxx explanation above. + // Make an arbitrary GL call (we don't actually need the result) + // IF YOU GET A CRASH HERE: it probably means the OpenGL function loader didn't do its job. Let us know! GLint current_texture; glGetIntegerv(GL_TEXTURE_BINDING_2D, ¤t_texture); + // Detect extensions we support +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + bd->HasPolygonMode = (!bd->GlProfileIsES2 && !bd->GlProfileIsES3); +#endif + bd->HasClipOrigin = (bd->GlVersion >= 450); +#ifdef IMGUI_IMPL_OPENGL_HAS_EXTENSIONS + GLint num_extensions = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &num_extensions); + for (GLint i = 0; i < num_extensions; i++) + { + const char* extension = (const char*)glGetStringi(GL_EXTENSIONS, i); + if (extension != nullptr && strcmp(extension, "GL_ARB_clip_control") == 0) + bd->HasClipOrigin = true; + } +#endif + + ImGui_ImplOpenGL3_InitMultiViewportSupport(); + return true; } void ImGui_ImplOpenGL3_Shutdown() { + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); + ImGuiIO& io = ImGui::GetIO(); + + ImGui_ImplOpenGL3_ShutdownMultiViewportSupport(); ImGui_ImplOpenGL3_DestroyDeviceObjects(); + io.BackendRendererName = nullptr; + io.BackendRendererUserData = nullptr; + io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasViewports); + IM_DELETE(bd); } void ImGui_ImplOpenGL3_NewFrame() { - if (!g_ShaderHandle) + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + IM_ASSERT(bd != nullptr && "Context or backend not initialized! Did you call ImGui_ImplOpenGL3_Init()?"); + + ImGui_ImplOpenGL3_InitLoader(); // Lazily init loader if not already done for e.g. DLL boundaries. + + if (!bd->ShaderHandle) ImGui_ImplOpenGL3_CreateDeviceObjects(); + if (!bd->FontTexture) + ImGui_ImplOpenGL3_CreateFontsTexture(); } static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_width, int fb_height, GLuint vertex_array_object) { + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + // Setup render state: alpha-blending enabled, no face culling, no depth testing, scissor enabled, polygon fill glEnable(GL_BLEND); glBlendEquation(GL_FUNC_ADD); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA); glDisable(GL_CULL_FACE); glDisable(GL_DEPTH_TEST); + glDisable(GL_STENCIL_TEST); glEnable(GL_SCISSOR_TEST); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART + if (bd->GlVersion >= 310) + glDisable(GL_PRIMITIVE_RESTART); +#endif +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + if (bd->HasPolygonMode) + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); +#endif + + // Support for GL 4.5 rarely used glClipControl(GL_UPPER_LEFT) +#if defined(GL_CLIP_ORIGIN) + bool clip_origin_lower_left = true; + if (bd->HasClipOrigin) + { + GLenum current_clip_origin = 0; glGetIntegerv(GL_CLIP_ORIGIN, (GLint*)¤t_clip_origin); + if (current_clip_origin == GL_UPPER_LEFT) + clip_origin_lower_left = false; + } +#endif // Setup viewport, orthographic projection matrix // Our visible imgui space lies from draw_data->DisplayPos (top left) to draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayPos is (0,0) for single viewport apps. - glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height); + GL_CALL(glViewport(0, 0, (GLsizei)fb_width, (GLsizei)fb_height)); float L = draw_data->DisplayPos.x; float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x; float T = draw_data->DisplayPos.y; float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y; +#if defined(GL_CLIP_ORIGIN) + if (!clip_origin_lower_left) { float tmp = T; T = B; B = tmp; } // Swap top and bottom if origin is upper left +#endif const float ortho_projection[4][4] = { { 2.0f/(R-L), 0.0f, 0.0f, 0.0f }, @@ -156,29 +491,34 @@ static void ImGui_ImplOpenGL3_SetupRenderState(ImDrawData* draw_data, int fb_wid { 0.0f, 0.0f, -1.0f, 0.0f }, { (R+L)/(L-R), (T+B)/(B-T), 0.0f, 1.0f }, }; - glUseProgram(g_ShaderHandle); - glUniform1i(g_AttribLocationTex, 0); - glUniformMatrix4fv(g_AttribLocationProjMtx, 1, GL_FALSE, &ortho_projection[0][0]); + glUseProgram(bd->ShaderHandle); + glUniform1i(bd->AttribLocationTex, 0); + glUniformMatrix4fv(bd->AttribLocationProjMtx, 1, GL_FALSE, &ortho_projection[0][0]); + +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER + if (bd->GlVersion >= 330 || bd->GlProfileIsES3) + glBindSampler(0, 0); // We use combined texture/sampler state. Applications using GL 3.3 and GL ES 3.0 may set that otherwise. +#endif (void)vertex_array_object; -#ifndef IMGUI_IMPL_OPENGL_ES2 +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY glBindVertexArray(vertex_array_object); #endif // Bind vertex/index buffers and setup attributes for ImDrawVert - glBindBuffer(GL_ARRAY_BUFFER, g_VboHandle); - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, g_ElementsHandle); - glEnableVertexAttribArray(g_AttribLocationVtxPos); - glEnableVertexAttribArray(g_AttribLocationVtxUV); - glEnableVertexAttribArray(g_AttribLocationVtxColor); - glVertexAttribPointer(g_AttribLocationVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)IM_OFFSETOF(ImDrawVert, pos)); - glVertexAttribPointer(g_AttribLocationVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)IM_OFFSETOF(ImDrawVert, uv)); - glVertexAttribPointer(g_AttribLocationVtxColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)IM_OFFSETOF(ImDrawVert, col)); + GL_CALL(glBindBuffer(GL_ARRAY_BUFFER, bd->VboHandle)); + GL_CALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, bd->ElementsHandle)); + GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxPos)); + GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxUV)); + GL_CALL(glEnableVertexAttribArray(bd->AttribLocationVtxColor)); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxPos, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, pos))); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxUV, 2, GL_FLOAT, GL_FALSE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, uv))); + GL_CALL(glVertexAttribPointer(bd->AttribLocationVtxColor, 4, GL_UNSIGNED_BYTE, GL_TRUE, sizeof(ImDrawVert), (GLvoid*)offsetof(ImDrawVert, col))); } // OpenGL3 Render function. -// (this used to be set in io.RenderDrawListsFn and called by ImGui::Render(), but you can now call this directly from your main loop) -// Note that this implementation is little overcomplicated because we are saving/setting up/restoring every OpenGL state explicitly, in order to be able to run within any OpenGL engine that doesn't do so. +// Note that this implementation is little overcomplicated because we are saving/setting up/restoring every OpenGL state explicitly. +// This is in order to be able to run within an OpenGL engine that doesn't do so. void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) { // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) @@ -187,25 +527,38 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) if (fb_width <= 0 || fb_height <= 0) return; + ImGui_ImplOpenGL3_InitLoader(); // Lazily init loader if not already done for e.g. DLL boundaries. + + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + // Backup GL state + GLboolean last_enable_srgb = GL_FALSE; + if (!Global.gfx_usegles && !Global.gfx_shadergamma) + { + last_enable_srgb = glIsEnabled(GL_FRAMEBUFFER_SRGB); + glEnable(GL_FRAMEBUFFER_SRGB); + } GLenum last_active_texture; glGetIntegerv(GL_ACTIVE_TEXTURE, (GLint*)&last_active_texture); glActiveTexture(GL_TEXTURE0); - GLint last_program; glGetIntegerv(GL_CURRENT_PROGRAM, &last_program); - GLint last_texture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); - GLint last_array_buffer; glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &last_array_buffer); -#ifndef IMGUI_IMPL_OPENGL_ES2 - GLint last_vertex_array_object; glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &last_vertex_array_object); + GLuint last_program; glGetIntegerv(GL_CURRENT_PROGRAM, (GLint*)&last_program); + GLuint last_texture; glGetIntegerv(GL_TEXTURE_BINDING_2D, (GLint*)&last_texture); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER + GLuint last_sampler; if (bd->GlVersion >= 330 || bd->GlProfileIsES3) { glGetIntegerv(GL_SAMPLER_BINDING, (GLint*)&last_sampler); } else { last_sampler = 0; } +#endif + GLuint last_array_buffer; glGetIntegerv(GL_ARRAY_BUFFER_BINDING, (GLint*)&last_array_buffer); +#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + // This is part of VAO on OpenGL 3.0+ and OpenGL ES 3.0+. + GLint last_element_array_buffer; glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &last_element_array_buffer); + ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_pos; last_vtx_attrib_state_pos.GetState(bd->AttribLocationVtxPos); + ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_uv; last_vtx_attrib_state_uv.GetState(bd->AttribLocationVtxUV); + ImGui_ImplOpenGL3_VtxAttribState last_vtx_attrib_state_color; last_vtx_attrib_state_color.GetState(bd->AttribLocationVtxColor); +#endif +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + GLuint last_vertex_array_object; glGetIntegerv(GL_VERTEX_ARRAY_BINDING, (GLint*)&last_vertex_array_object); +#endif +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + GLint last_polygon_mode[2]; if (bd->HasPolygonMode) { glGetIntegerv(GL_POLYGON_MODE, last_polygon_mode); } #endif - GLboolean last_enable_srgb; - if (!Global.gfx_usegles) - { - if (!Global.gfx_shadergamma) - { - last_enable_srgb = glIsEnabled(GL_FRAMEBUFFER_SRGB); - glEnable(GL_FRAMEBUFFER_SRGB); - } - } - GLint last_viewport[4]; glGetIntegerv(GL_VIEWPORT, last_viewport); GLint last_scissor_box[4]; glGetIntegerv(GL_SCISSOR_BOX, last_scissor_box); GLenum last_blend_src_rgb; glGetIntegerv(GL_BLEND_SRC_RGB, (GLint*)&last_blend_src_rgb); @@ -217,15 +570,18 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) GLboolean last_enable_blend = glIsEnabled(GL_BLEND); GLboolean last_enable_cull_face = glIsEnabled(GL_CULL_FACE); GLboolean last_enable_depth_test = glIsEnabled(GL_DEPTH_TEST); + GLboolean last_enable_stencil_test = glIsEnabled(GL_STENCIL_TEST); GLboolean last_enable_scissor_test = glIsEnabled(GL_SCISSOR_TEST); - bool clip_origin_lower_left = true; +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART + GLboolean last_enable_primitive_restart = (bd->GlVersion >= 310) ? glIsEnabled(GL_PRIMITIVE_RESTART) : GL_FALSE; +#endif // Setup desired GL state // Recreate the VAO every time (this is to easily allow multiple GL contexts to be rendered to. VAO are not shared among GL contexts) // The renderer would actually work without any VAO bound, but then our VertexAttrib calls would overwrite the default one currently bound. GLuint vertex_array_object = 0; -#ifndef IMGUI_IMPL_OPENGL_ES2 - glGenVertexArrays(1, &vertex_array_object); +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + GL_CALL(glGenVertexArrays(1, &vertex_array_object)); #endif ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object); @@ -236,140 +592,183 @@ void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data) // Render command lists for (int n = 0; n < draw_data->CmdListsCount; n++) { - const ImDrawList* cmd_list = draw_data->CmdLists[n]; + const ImDrawList* draw_list = draw_data->CmdLists[n]; // Upload vertex/index buffers - glBufferData(GL_ARRAY_BUFFER, (GLsizeiptr)cmd_list->VtxBuffer.Size * sizeof(ImDrawVert), (const GLvoid*)cmd_list->VtxBuffer.Data, GL_STREAM_DRAW); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, (GLsizeiptr)cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx), (const GLvoid*)cmd_list->IdxBuffer.Data, GL_STREAM_DRAW); - - for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) + // - OpenGL drivers are in a very sorry state nowadays.... + // During 2021 we attempted to switch from glBufferData() to orphaning+glBufferSubData() following reports + // of leaks on Intel GPU when using multi-viewports on Windows. + // - After this we kept hearing of various display corruptions issues. We started disabling on non-Intel GPU, but issues still got reported on Intel. + // - We are now back to using exclusively glBufferData(). So bd->UseBufferSubData IS ALWAYS FALSE in this code. + // We are keeping the old code path for a while in case people finding new issues may want to test the bd->UseBufferSubData path. + // - See https://github.com/ocornut/imgui/issues/4468 and please report any corruption issues. + const GLsizeiptr vtx_buffer_size = (GLsizeiptr)draw_list->VtxBuffer.Size * (int)sizeof(ImDrawVert); + const GLsizeiptr idx_buffer_size = (GLsizeiptr)draw_list->IdxBuffer.Size * (int)sizeof(ImDrawIdx); + if (bd->UseBufferSubData) { - const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; - if (pcmd->UserCallback != NULL) + if (bd->VertexBufferSize < vtx_buffer_size) + { + bd->VertexBufferSize = vtx_buffer_size; + GL_CALL(glBufferData(GL_ARRAY_BUFFER, bd->VertexBufferSize, nullptr, GL_STREAM_DRAW)); + } + if (bd->IndexBufferSize < idx_buffer_size) + { + bd->IndexBufferSize = idx_buffer_size; + GL_CALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, bd->IndexBufferSize, nullptr, GL_STREAM_DRAW)); + } + GL_CALL(glBufferSubData(GL_ARRAY_BUFFER, 0, vtx_buffer_size, (const GLvoid*)draw_list->VtxBuffer.Data)); + GL_CALL(glBufferSubData(GL_ELEMENT_ARRAY_BUFFER, 0, idx_buffer_size, (const GLvoid*)draw_list->IdxBuffer.Data)); + } + else + { + GL_CALL(glBufferData(GL_ARRAY_BUFFER, vtx_buffer_size, (const GLvoid*)draw_list->VtxBuffer.Data, GL_STREAM_DRAW)); + GL_CALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx_buffer_size, (const GLvoid*)draw_list->IdxBuffer.Data, GL_STREAM_DRAW)); + } + + for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback != nullptr) { // User callback, registered via ImDrawList::AddCallback() // (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.) if (pcmd->UserCallback == ImDrawCallback_ResetRenderState) ImGui_ImplOpenGL3_SetupRenderState(draw_data, fb_width, fb_height, vertex_array_object); else - pcmd->UserCallback(cmd_list, pcmd); + pcmd->UserCallback(draw_list, pcmd); } else { // Project scissor/clipping rectangles into framebuffer space - ImVec4 clip_rect; - clip_rect.x = (pcmd->ClipRect.x - clip_off.x) * clip_scale.x; - clip_rect.y = (pcmd->ClipRect.y - clip_off.y) * clip_scale.y; - clip_rect.z = (pcmd->ClipRect.z - clip_off.x) * clip_scale.x; - clip_rect.w = (pcmd->ClipRect.w - clip_off.y) * clip_scale.y; + ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; - if (clip_rect.x < fb_width && clip_rect.y < fb_height && clip_rect.z >= 0.0f && clip_rect.w >= 0.0f) - { - // Apply scissor/clipping rectangle - if (clip_origin_lower_left) - glScissor((int)clip_rect.x, (int)(fb_height - clip_rect.w), (int)(clip_rect.z - clip_rect.x), (int)(clip_rect.w - clip_rect.y)); - else - glScissor((int)clip_rect.x, (int)clip_rect.y, (int)clip_rect.z, (int)clip_rect.w); // Support for GL 4.5 rarely used glClipControl(GL_UPPER_LEFT) + // Apply scissor/clipping rectangle (Y is inverted in OpenGL) + GL_CALL(glScissor((int)clip_min.x, (int)((float)fb_height - clip_max.y), (int)(clip_max.x - clip_min.x), (int)(clip_max.y - clip_min.y))); - // Bind texture, Draw - glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->TextureId); -#if IMGUI_IMPL_OPENGL_HAS_DRAW_WITH_BASE_VERTEX - glDrawElementsBaseVertex(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)), (GLint)pcmd->VtxOffset); -#else - glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx))); + // Bind texture, Draw + GL_CALL(glBindTexture(GL_TEXTURE_2D, (GLuint)(intptr_t)pcmd->GetTexID())); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_VTX_OFFSET + if (bd->GlVersion >= 320) + GL_CALL(glDrawElementsBaseVertex(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)), (GLint)pcmd->VtxOffset)); + else #endif - } + GL_CALL(glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, (void*)(intptr_t)(pcmd->IdxOffset * sizeof(ImDrawIdx)))); } } } // Destroy the temporary VAO -#ifndef IMGUI_IMPL_OPENGL_ES2 - glDeleteVertexArrays(1, &vertex_array_object); +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + GL_CALL(glDeleteVertexArrays(1, &vertex_array_object)); #endif // Restore modified GL state - glUseProgram(last_program); + // This "glIsProgram()" check is required because if the program is "pending deletion" at the time of binding backup, it will have been deleted by now and will cause an OpenGL error. See #6220. + if (last_program == 0 || glIsProgram(last_program)) glUseProgram(last_program); glBindTexture(GL_TEXTURE_2D, last_texture); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_SAMPLER + if (bd->GlVersion >= 330 || bd->GlProfileIsES3) + glBindSampler(0, last_sampler); +#endif glActiveTexture(last_active_texture); -#ifndef IMGUI_IMPL_OPENGL_ES2 +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY glBindVertexArray(last_vertex_array_object); #endif glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer); +#ifndef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, last_element_array_buffer); + last_vtx_attrib_state_pos.SetState(bd->AttribLocationVtxPos); + last_vtx_attrib_state_uv.SetState(bd->AttribLocationVtxUV); + last_vtx_attrib_state_color.SetState(bd->AttribLocationVtxColor); +#endif glBlendEquationSeparate(last_blend_equation_rgb, last_blend_equation_alpha); glBlendFuncSeparate(last_blend_src_rgb, last_blend_dst_rgb, last_blend_src_alpha, last_blend_dst_alpha); if (last_enable_blend) glEnable(GL_BLEND); else glDisable(GL_BLEND); if (last_enable_cull_face) glEnable(GL_CULL_FACE); else glDisable(GL_CULL_FACE); if (last_enable_depth_test) glEnable(GL_DEPTH_TEST); else glDisable(GL_DEPTH_TEST); + if (last_enable_stencil_test) glEnable(GL_STENCIL_TEST); else glDisable(GL_STENCIL_TEST); if (last_enable_scissor_test) glEnable(GL_SCISSOR_TEST); else glDisable(GL_SCISSOR_TEST); +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_PRIMITIVE_RESTART + if (bd->GlVersion >= 310) { if (last_enable_primitive_restart) glEnable(GL_PRIMITIVE_RESTART); else glDisable(GL_PRIMITIVE_RESTART); } +#endif + +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + // Desktop OpenGL 3.0 and OpenGL 3.1 had separate polygon draw modes for front-facing and back-facing faces of polygons + if (bd->HasPolygonMode) { if (bd->GlVersion <= 310 || bd->GlProfileIsCompat) { glPolygonMode(GL_FRONT, (GLenum)last_polygon_mode[0]); glPolygonMode(GL_BACK, (GLenum)last_polygon_mode[1]); } else { glPolygonMode(GL_FRONT_AND_BACK, (GLenum)last_polygon_mode[0]); } } +#endif // IMGUI_IMPL_OPENGL_MAY_HAVE_POLYGON_MODE + glViewport(last_viewport[0], last_viewport[1], (GLsizei)last_viewport[2], (GLsizei)last_viewport[3]); glScissor(last_scissor_box[0], last_scissor_box[1], (GLsizei)last_scissor_box[2], (GLsizei)last_scissor_box[3]); - - if (!Global.gfx_usegles) + if (!Global.gfx_usegles && !Global.gfx_shadergamma) { - if (!Global.gfx_shadergamma) - { - if (last_enable_srgb) - glEnable(GL_FRAMEBUFFER_SRGB); - else - glDisable(GL_FRAMEBUFFER_SRGB); - } + if (last_enable_srgb) glEnable(GL_FRAMEBUFFER_SRGB); else glDisable(GL_FRAMEBUFFER_SRGB); } + (void)bd; // Not all compilation paths use this } bool ImGui_ImplOpenGL3_CreateFontsTexture() { - // Build texture atlas ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + + // Build texture atlas unsigned char* pixels; int width, height; - io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); // Load as RGBA 32-bits (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory. + io.Fonts->GetTexDataAsRGBA32(&pixels, &width, &height); // Load as RGBA 32-bit (75% of the memory is wasted, but default font is so small) because it is more likely to be compatible with user's existing shaders. If your ImTextureId represent a higher-level concept than just a GL texture id, consider calling GetTexDataAsAlpha8() instead to save on GPU memory. // Upload texture to graphics system + // (Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling) GLint last_texture; - glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); - glGenTextures(1, &g_FontTexture); - glBindTexture(GL_TEXTURE_2D, g_FontTexture); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); -#ifdef GL_UNPACK_ROW_LENGTH - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + GL_CALL(glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture)); + GL_CALL(glGenTextures(1, &bd->FontTexture)); + GL_CALL(glBindTexture(GL_TEXTURE_2D, bd->FontTexture)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + GL_CALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); +#ifdef GL_UNPACK_ROW_LENGTH // Not on WebGL/ES + GL_CALL(glPixelStorei(GL_UNPACK_ROW_LENGTH, 0)); #endif - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); + GL_CALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels)); - // Store our identifier - io.Fonts->TexID = (ImTextureID)(intptr_t)g_FontTexture; + // Store identifier + io.Fonts->SetTexID((ImTextureID)(intptr_t)bd->FontTexture); // Restore state - glBindTexture(GL_TEXTURE_2D, last_texture); + GL_CALL(glBindTexture(GL_TEXTURE_2D, last_texture)); return true; } void ImGui_ImplOpenGL3_DestroyFontsTexture() { - if (g_FontTexture) + ImGuiIO& io = ImGui::GetIO(); + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + if (bd->FontTexture) { - ImGuiIO& io = ImGui::GetIO(); - glDeleteTextures(1, &g_FontTexture); - io.Fonts->TexID = 0; - g_FontTexture = 0; + glDeleteTextures(1, &bd->FontTexture); + io.Fonts->SetTexID(0); + bd->FontTexture = 0; } } // If you get an error please report on github. You may try different GL context version or GLSL version. See GL<>GLSL version table at the top of this file. static bool CheckShader(GLuint handle, const char* desc) { + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); GLint status = 0, log_length = 0; glGetShaderiv(handle, GL_COMPILE_STATUS, &status); glGetShaderiv(handle, GL_INFO_LOG_LENGTH, &log_length); if ((GLboolean)status == GL_FALSE) - fprintf(stderr, "ERROR: ImGui_ImplOpenGL3_CreateDeviceObjects: failed to compile %s!\n", desc); + fprintf(stderr, "ERROR: ImGui_ImplOpenGL3_CreateDeviceObjects: failed to compile %s! With GLSL: %s\n", desc, bd->GlslVersionString); if (log_length > 1) { ImVector buf; buf.resize((int)(log_length + 1)); - glGetShaderInfoLog(handle, log_length, NULL, (GLchar*)buf.begin()); + glGetShaderInfoLog(handle, log_length, nullptr, (GLchar*)buf.begin()); fprintf(stderr, "%s\n", buf.begin()); } return (GLboolean)status == GL_TRUE; @@ -378,16 +777,17 @@ static bool CheckShader(GLuint handle, const char* desc) // If you get an error please report on GitHub. You may try different GL context version or GLSL version. static bool CheckProgram(GLuint handle, const char* desc) { + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); GLint status = 0, log_length = 0; glGetProgramiv(handle, GL_LINK_STATUS, &status); glGetProgramiv(handle, GL_INFO_LOG_LENGTH, &log_length); if ((GLboolean)status == GL_FALSE) - fprintf(stderr, "ERROR: ImGui_ImplOpenGL3_CreateDeviceObjects: failed to link %s! (with GLSL '%s')\n", desc, g_GlslVersionString); + fprintf(stderr, "ERROR: ImGui_ImplOpenGL3_CreateDeviceObjects: failed to link %s! With GLSL %s\n", desc, bd->GlslVersionString); if (log_length > 1) { ImVector buf; buf.resize((int)(log_length + 1)); - glGetProgramInfoLog(handle, log_length, NULL, (GLchar*)buf.begin()); + glGetProgramInfoLog(handle, log_length, nullptr, (GLchar*)buf.begin()); fprintf(stderr, "%s\n", buf.begin()); } return (GLboolean)status == GL_TRUE; @@ -395,18 +795,24 @@ static bool CheckProgram(GLuint handle, const char* desc) bool ImGui_ImplOpenGL3_CreateDeviceObjects() { + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + // Backup GL state GLint last_texture, last_array_buffer; glGetIntegerv(GL_TEXTURE_BINDING_2D, &last_texture); glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &last_array_buffer); -#ifndef IMGUI_IMPL_OPENGL_ES2 +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK + GLint last_pixel_unpack_buffer = 0; + if (bd->GlVersion >= 210) { glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &last_pixel_unpack_buffer); glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); } +#endif +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY GLint last_vertex_array; glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &last_vertex_array); #endif // Parse GLSL version string int glsl_version = 130; - sscanf(g_GlslVersionString, "#version %d", &glsl_version); + sscanf(bd->GlslVersionString, "#version %d", &glsl_version); const GLchar* vertex_shader_glsl_120 = "uniform mat4 ProjMtx;\n" @@ -437,7 +843,7 @@ bool ImGui_ImplOpenGL3_CreateDeviceObjects() "}\n"; const GLchar* vertex_shader_glsl_300_es = - "precision mediump float;\n" + "precision highp float;\n" "layout (location = 0) in vec2 Position;\n" "layout (location = 1) in vec2 UV;\n" "layout (location = 2) in vec4 Color;\n" @@ -509,8 +915,8 @@ bool ImGui_ImplOpenGL3_CreateDeviceObjects() "}\n"; // Select shaders matching our GLSL versions - const GLchar* vertex_shader = NULL; - const GLchar* fragment_shader = NULL; + const GLchar* vertex_shader = nullptr; + const GLchar* fragment_shader = nullptr; if (glsl_version < 130) { vertex_shader = vertex_shader_glsl_120; @@ -533,43 +939,54 @@ bool ImGui_ImplOpenGL3_CreateDeviceObjects() } // Create shaders - const GLchar* vertex_shader_with_version[2] = { g_GlslVersionString, vertex_shader }; - g_VertHandle = glCreateShader(GL_VERTEX_SHADER); - glShaderSource(g_VertHandle, 2, vertex_shader_with_version, NULL); - glCompileShader(g_VertHandle); - CheckShader(g_VertHandle, "vertex shader"); + const GLchar* vertex_shader_with_version[2] = { bd->GlslVersionString, vertex_shader }; + GLuint vert_handle; + GL_CALL(vert_handle = glCreateShader(GL_VERTEX_SHADER)); + glShaderSource(vert_handle, 2, vertex_shader_with_version, nullptr); + glCompileShader(vert_handle); + CheckShader(vert_handle, "vertex shader"); + // MaSzyna: FBOUT() encodes the gamma when the renderer does it in shaders instead of a srgb framebuffer const GLchar* fbout_copy = "vec4 FBOUT(vec4 x) { return x; }\n"; const GLchar* fbout_gamma = "vec4 FBOUT(vec4 x) { return vec4(pow(x.rgb, vec3(1.0 / 2.2)), x.a); }\n"; + const GLchar* fragment_shader_with_version[3] = { bd->GlslVersionString, Global.gfx_shadergamma ? fbout_gamma : fbout_copy, fragment_shader }; + GLuint frag_handle; + GL_CALL(frag_handle = glCreateShader(GL_FRAGMENT_SHADER)); + glShaderSource(frag_handle, 3, fragment_shader_with_version, nullptr); + glCompileShader(frag_handle); + CheckShader(frag_handle, "fragment shader"); - const GLchar* fragment_shader_with_version[3] = { g_GlslVersionString, Global.gfx_shadergamma ? fbout_gamma : fbout_copy, fragment_shader }; - g_FragHandle = glCreateShader(GL_FRAGMENT_SHADER); - glShaderSource(g_FragHandle, 3, fragment_shader_with_version, NULL); - glCompileShader(g_FragHandle); - CheckShader(g_FragHandle, "fragment shader"); + // Link + bd->ShaderHandle = glCreateProgram(); + glAttachShader(bd->ShaderHandle, vert_handle); + glAttachShader(bd->ShaderHandle, frag_handle); + glLinkProgram(bd->ShaderHandle); + CheckProgram(bd->ShaderHandle, "shader program"); - g_ShaderHandle = glCreateProgram(); - glAttachShader(g_ShaderHandle, g_VertHandle); - glAttachShader(g_ShaderHandle, g_FragHandle); - glLinkProgram(g_ShaderHandle); - CheckProgram(g_ShaderHandle, "shader program"); + glDetachShader(bd->ShaderHandle, vert_handle); + glDetachShader(bd->ShaderHandle, frag_handle); + glDeleteShader(vert_handle); + glDeleteShader(frag_handle); - g_AttribLocationTex = glGetUniformLocation(g_ShaderHandle, "Texture"); - g_AttribLocationProjMtx = glGetUniformLocation(g_ShaderHandle, "ProjMtx"); - g_AttribLocationVtxPos = glGetAttribLocation(g_ShaderHandle, "Position"); - g_AttribLocationVtxUV = glGetAttribLocation(g_ShaderHandle, "UV"); - g_AttribLocationVtxColor = glGetAttribLocation(g_ShaderHandle, "Color"); + bd->AttribLocationTex = glGetUniformLocation(bd->ShaderHandle, "Texture"); + bd->AttribLocationProjMtx = glGetUniformLocation(bd->ShaderHandle, "ProjMtx"); + bd->AttribLocationVtxPos = (GLuint)glGetAttribLocation(bd->ShaderHandle, "Position"); + bd->AttribLocationVtxUV = (GLuint)glGetAttribLocation(bd->ShaderHandle, "UV"); + bd->AttribLocationVtxColor = (GLuint)glGetAttribLocation(bd->ShaderHandle, "Color"); // Create buffers - glGenBuffers(1, &g_VboHandle); - glGenBuffers(1, &g_ElementsHandle); + glGenBuffers(1, &bd->VboHandle); + glGenBuffers(1, &bd->ElementsHandle); ImGui_ImplOpenGL3_CreateFontsTexture(); // Restore modified GL state glBindTexture(GL_TEXTURE_2D, last_texture); glBindBuffer(GL_ARRAY_BUFFER, last_array_buffer); -#ifndef IMGUI_IMPL_OPENGL_ES2 +#ifdef IMGUI_IMPL_OPENGL_MAY_HAVE_BIND_BUFFER_PIXEL_UNPACK + if (bd->GlVersion >= 210) { glBindBuffer(GL_PIXEL_UNPACK_BUFFER, last_pixel_unpack_buffer); } +#endif +#ifdef IMGUI_IMPL_OPENGL_USE_VERTEX_ARRAY glBindVertexArray(last_vertex_array); #endif @@ -578,13 +995,48 @@ bool ImGui_ImplOpenGL3_CreateDeviceObjects() void ImGui_ImplOpenGL3_DestroyDeviceObjects() { - if (g_VboHandle) { glDeleteBuffers(1, &g_VboHandle); g_VboHandle = 0; } - if (g_ElementsHandle) { glDeleteBuffers(1, &g_ElementsHandle); g_ElementsHandle = 0; } - if (g_ShaderHandle && g_VertHandle) { glDetachShader(g_ShaderHandle, g_VertHandle); } - if (g_ShaderHandle && g_FragHandle) { glDetachShader(g_ShaderHandle, g_FragHandle); } - if (g_VertHandle) { glDeleteShader(g_VertHandle); g_VertHandle = 0; } - if (g_FragHandle) { glDeleteShader(g_FragHandle); g_FragHandle = 0; } - if (g_ShaderHandle) { glDeleteProgram(g_ShaderHandle); g_ShaderHandle = 0; } - + ImGui_ImplOpenGL3_Data* bd = ImGui_ImplOpenGL3_GetBackendData(); + if (bd->VboHandle) { glDeleteBuffers(1, &bd->VboHandle); bd->VboHandle = 0; } + if (bd->ElementsHandle) { glDeleteBuffers(1, &bd->ElementsHandle); bd->ElementsHandle = 0; } + if (bd->ShaderHandle) { glDeleteProgram(bd->ShaderHandle); bd->ShaderHandle = 0; } ImGui_ImplOpenGL3_DestroyFontsTexture(); } + +//-------------------------------------------------------------------------------------------------------- +// MULTI-VIEWPORT / PLATFORM INTERFACE SUPPORT +// This is an _advanced_ and _optional_ feature, allowing the backend to create and handle multiple viewports simultaneously. +// If you are new to dear imgui or creating a new binding for dear imgui, it is recommended that you completely ignore this section first.. +//-------------------------------------------------------------------------------------------------------- + +static void ImGui_ImplOpenGL3_RenderWindow(ImGuiViewport* viewport, void*) +{ + if (!(viewport->Flags & ImGuiViewportFlags_NoRendererClear)) + { + ImVec4 clear_color = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); + glClearColor(clear_color.x, clear_color.y, clear_color.z, clear_color.w); + glClear(GL_COLOR_BUFFER_BIT); + } + ImGui_ImplOpenGL3_RenderDrawData(viewport->DrawData); +} + +static void ImGui_ImplOpenGL3_InitMultiViewportSupport() +{ + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.Renderer_RenderWindow = ImGui_ImplOpenGL3_RenderWindow; +} + +static void ImGui_ImplOpenGL3_ShutdownMultiViewportSupport() +{ + ImGui::DestroyPlatformWindows(); +} + +//----------------------------------------------------------------------------- + +#if defined(__GNUC__) +#pragma GCC diagnostic pop +#endif +#if defined(__clang__) +#pragma clang diagnostic pop +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_impl_opengl3.h b/imgui/imgui_impl_opengl3.h index 1cb790b1..d5621304 100644 --- a/imgui/imgui_impl_opengl3.h +++ b/imgui/imgui_impl_opengl3.h @@ -1,30 +1,37 @@ -// dear imgui: Renderer for modern OpenGL with shaders / programmatic pipeline +// dear imgui: Renderer Backend for modern OpenGL with shaders / programmatic pipeline // - Desktop GL: 2.x 3.x 4.x // - Embedded GL: ES 2.0 (WebGL 1.0), ES 3.0 (WebGL 2.0) -// This needs to be used along with a Platform Binding (e.g. GLFW, SDL, Win32, custom..) +// This needs to be used along with a Platform Backend (e.g. GLFW, SDL, Win32, custom..) // Implemented features: -// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID in imgui.cpp. -// [x] Renderer: Desktop GL only: Support for large meshes (64k+ vertices) with 16-bits indices. +// [X] Renderer: User texture binding. Use 'GLuint' OpenGL texture identifier as void*/ImTextureID. Read the FAQ about ImTextureID! +// [x] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset) [Desktop OpenGL only!] +// [X] Renderer: Multi-viewport support (multiple windows). Enable with 'io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable'. -// You can copy and use unmodified imgui_impl_* files in your project. See main.cpp for an example of using this. -// If you are new to dear imgui, read examples/README.txt and read the documentation at the top of imgui.cpp. -// https://github.com/ocornut/imgui +// About WebGL/ES: +// - You need to '#define IMGUI_IMPL_OPENGL_ES2' or '#define IMGUI_IMPL_OPENGL_ES3' to use WebGL or OpenGL ES. +// - This is done automatically on iOS, Android and Emscripten targets. +// - For other targets, the define needs to be visible from the imgui_impl_opengl3.cpp compilation unit. If unsure, define globally or in imconfig.h. -// About Desktop OpenGL function loaders: -// Modern desktop OpenGL doesn't have a standard portable header file to load OpenGL function pointers. -// Helper libraries are often used for this purpose! Here we are supporting a few common ones (gl3w, glew, glad). -// You may use another loader/header of your choice (glext, glLoadGen, etc.), or chose to manually implement your own. +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp // About GLSL version: -// The 'glsl_version' initialization parameter should be NULL (default) or a "#version XXX" string. +// The 'glsl_version' initialization parameter should be nullptr (default) or a "#version XXX" string. // On computer platform the GLSL version default to "#version 130". On OpenGL ES 3 platform it defaults to "#version 300 es" // Only override if your GL version doesn't handle this GLSL version. See GLSL version table at the top of imgui_impl_opengl3.cpp. #pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE -// Backend API -IMGUI_IMPL_API bool ImGui_ImplOpenGL3_Init(const char* glsl_version = NULL); +// Follow "Getting Started" link and check examples/ folder to learn about using backends! +IMGUI_IMPL_API bool ImGui_ImplOpenGL3_Init(const char* glsl_version = nullptr); IMGUI_IMPL_API void ImGui_ImplOpenGL3_Shutdown(); IMGUI_IMPL_API void ImGui_ImplOpenGL3_NewFrame(); IMGUI_IMPL_API void ImGui_ImplOpenGL3_RenderDrawData(ImDrawData* draw_data); @@ -35,30 +42,26 @@ IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyFontsTexture(); IMGUI_IMPL_API bool ImGui_ImplOpenGL3_CreateDeviceObjects(); IMGUI_IMPL_API void ImGui_ImplOpenGL3_DestroyDeviceObjects(); -// Specific OpenGL versions -//#define IMGUI_IMPL_OPENGL_ES2 // Auto-detected on Emscripten -//#define IMGUI_IMPL_OPENGL_ES3 // Auto-detected on iOS/Android +// Configuration flags to add in your imconfig file: +//#define IMGUI_IMPL_OPENGL_ES2 // Enable ES 2 (Auto-detected on Emscripten) +//#define IMGUI_IMPL_OPENGL_ES3 // Enable ES 3 (Auto-detected on iOS/Android) -// Desktop OpenGL: attempt to detect default GL loader based on available header files. -// If auto-detection fails or doesn't select the same GL loader file as used by your application, -// you are likely to get a crash in ImGui_ImplOpenGL3_Init(). -// You can explicitly select a loader by using '#define IMGUI_IMPL_OPENGL_LOADER_XXX' in imconfig.h or compiler command-line. -#if !defined(IMGUI_IMPL_OPENGL_LOADER_GL3W) \ - && !defined(IMGUI_IMPL_OPENGL_LOADER_GLEW) \ - && !defined(IMGUI_IMPL_OPENGL_LOADER_GLAD) \ - && !defined(IMGUI_IMPL_OPENGL_LOADER_CUSTOM) - #if defined(__has_include) - #if __has_include() - #define IMGUI_IMPL_OPENGL_LOADER_GLEW - #elif __has_include() - #define IMGUI_IMPL_OPENGL_LOADER_GLAD - #elif __has_include() - #define IMGUI_IMPL_OPENGL_LOADER_GL3W - #else - #error "Cannot detect OpenGL loader!" - #endif - #else - #define IMGUI_IMPL_OPENGL_LOADER_GL3W // Default to GL3W - #endif +// You can explicitly select GLES2 or GLES3 API by using one of the '#define IMGUI_IMPL_OPENGL_LOADER_XXX' in imconfig.h or compiler command-line. +#if !defined(IMGUI_IMPL_OPENGL_ES2) \ + && !defined(IMGUI_IMPL_OPENGL_ES3) + +// Try to detect GLES on matching platforms +#if defined(__APPLE__) +#include +#endif +#if (defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_TV)) || (defined(__ANDROID__)) +#define IMGUI_IMPL_OPENGL_ES3 // iOS, Android -> GL ES 3, "#version 300 es" +#elif defined(__EMSCRIPTEN__) || defined(__amigaos4__) +#define IMGUI_IMPL_OPENGL_ES2 // Emscripten -> GL ES 2, "#version 100" +#else +// Otherwise imgui_impl_opengl3_loader.h will be used. #endif +#endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_internal.h b/imgui/imgui_internal.h index 6d393103..0f106d43 100644 --- a/imgui/imgui_internal.h +++ b/imgui/imgui_internal.h @@ -1,129 +1,220 @@ -// dear imgui, v1.73 +// dear imgui, v1.91.9b // (internal structures/api) -// You may use this file to debug, understand or extend ImGui features but we don't provide any guarantee of forward compatibility! -// Set: -// #define IMGUI_DEFINE_MATH_OPERATORS -// To implement maths operators for ImVec2 (disabled by default to not collide with using IM_VEC2_CLASS_EXTRA along with your own math types+operators) +// You may use this file to debug, understand or extend Dear ImGui features but we don't provide any guarantee of forward compatibility. /* Index of this file: -// Header mess -// Forward declarations -// STB libraries includes -// Context pointer -// Generic helpers -// Misc data structures -// Main imgui context -// Tab bar, tab item -// Internal API + +// [SECTION] Header mess +// [SECTION] Forward declarations +// [SECTION] Context pointer +// [SECTION] STB libraries includes +// [SECTION] Macros +// [SECTION] Generic helpers +// [SECTION] ImDrawList support +// [SECTION] Style support +// [SECTION] Data types support +// [SECTION] Widgets support: flags, enums, data structures +// [SECTION] Popup support +// [SECTION] Inputs support +// [SECTION] Clipper support +// [SECTION] Navigation support +// [SECTION] Typing-select support +// [SECTION] Columns support +// [SECTION] Box-select support +// [SECTION] Multi-select support +// [SECTION] Docking support +// [SECTION] Viewport support +// [SECTION] Settings support +// [SECTION] Localization support +// [SECTION] Error handling, State recovery support +// [SECTION] Metrics, Debug tools +// [SECTION] Generic context hooks +// [SECTION] ImGuiContext (main imgui context) +// [SECTION] ImGuiWindowTempData, ImGuiWindow +// [SECTION] Tab bar, Tab item support +// [SECTION] Table support +// [SECTION] ImGui internal API +// [SECTION] ImFontAtlas internal API +// [SECTION] Test Engine specific hooks (imgui_test_engine) */ #pragma once +#ifndef IMGUI_DISABLE //----------------------------------------------------------------------------- -// Header mess +// [SECTION] Header mess //----------------------------------------------------------------------------- #ifndef IMGUI_VERSION -#error Must include imgui.h before imgui_internal.h +#include "imgui.h" #endif -#include // FILE* +#include // FILE*, sscanf #include // NULL, malloc, free, qsort, atoi, atof #include // sqrtf, fabsf, fmodf, powf, floorf, ceilf, cosf, sinf #include // INT_MIN, INT_MAX +// Enable SSE intrinsics if available +#if (defined __SSE__ || defined __x86_64__ || defined _M_X64 || (defined(_M_IX86_FP) && (_M_IX86_FP >= 1))) && !defined(IMGUI_DISABLE_SSE) +#define IMGUI_ENABLE_SSE +#include +#if (defined __AVX__ || defined __SSE4_2__) +#define IMGUI_ENABLE_SSE4_2 +#include +#endif +#endif +// Emscripten has partial SSE 4.2 support where _mm_crc32_u32 is not available. See https://emscripten.org/docs/porting/simd.html#id11 and #8213 +#if defined(IMGUI_ENABLE_SSE4_2) && !defined(IMGUI_USE_LEGACY_CRC32_ADLER) && !defined(__EMSCRIPTEN__) +#define IMGUI_ENABLE_SSE4_2_CRC +#endif + // Visual Studio warnings #ifdef _MSC_VER #pragma warning (push) -#pragma warning (disable: 4251) // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport) +#pragma warning (disable: 4251) // class 'xxx' needs to have dll-interface to be used by clients of struct 'xxx' // when IMGUI_API is set to__declspec(dllexport) +#pragma warning (disable: 26812) // The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). [MSVC Static Analyzer) +#pragma warning (disable: 26495) // [Static Analyzer] Variable 'XXX' is uninitialized. Always initialize a member variable (type.6). +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif #endif // Clang/GCC warnings with -Weverything #if defined(__clang__) #pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-function" // for stb_textedit.h -#pragma clang diagnostic ignored "-Wmissing-prototypes" // for stb_textedit.h -#pragma clang diagnostic ignored "-Wold-style-cast" -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" -#endif -#if __has_warning("-Wdouble-promotion") -#pragma clang diagnostic ignored "-Wdouble-promotion" +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' #endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants ok, for ImFloor() +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wmissing-noreturn" // warning: function 'xxx' could be declared with attribute 'noreturn' +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type #elif defined(__GNUC__) #pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind -#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#endif + +// In 1.89.4, we moved the implementation of "courtesy maths operators" from imgui_internal.h in imgui.h +// As they are frequently requested, we do not want to encourage to many people using imgui_internal.h +#if defined(IMGUI_DEFINE_MATH_OPERATORS) && !defined(IMGUI_DEFINE_MATH_OPERATORS_IMPLEMENTED) +#error Please '#define IMGUI_DEFINE_MATH_OPERATORS' _BEFORE_ including imgui.h! +#endif + +// Legacy defines +#ifdef IMGUI_DISABLE_FORMAT_STRING_FUNCTIONS // Renamed in 1.74 +#error Use IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +#endif +#ifdef IMGUI_DISABLE_MATH_FUNCTIONS // Renamed in 1.74 +#error Use IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS +#endif + +// Enable stb_truetype by default unless FreeType is enabled. +// You can compile with both by defining both IMGUI_ENABLE_FREETYPE and IMGUI_ENABLE_STB_TRUETYPE together. +#ifndef IMGUI_ENABLE_FREETYPE +#define IMGUI_ENABLE_STB_TRUETYPE #endif //----------------------------------------------------------------------------- -// Forward declarations +// [SECTION] Forward declarations //----------------------------------------------------------------------------- +// Utilities +// (other types which are not forwarded declared are: ImBitArray<>, ImSpan<>, ImSpanAllocator<>, ImPool<>, ImChunkStream<>) +struct ImBitVector; // Store 1-bit per value struct ImRect; // An axis-aligned rectangle (2 points) +struct ImGuiTextIndex; // Maintain a line index for a text buffer. + +// ImDrawList/ImFontAtlas struct ImDrawDataBuilder; // Helper to build a ImDrawData instance struct ImDrawListSharedData; // Data shared between all ImDrawList instances + +// ImGui +struct ImGuiBoxSelectState; // Box-selection state (currently used by multi-selection, could potentially be used by others) struct ImGuiColorMod; // Stacked color modifier, backup of modified data so we can restore it -struct ImGuiColumnData; // Storage data for a single column -struct ImGuiColumns; // Storage data for a columns set struct ImGuiContext; // Main Dear ImGui context +struct ImGuiContextHook; // Hook for extensions like ImGuiTestEngine struct ImGuiDataTypeInfo; // Type information associated to a ImGuiDataType enum +struct ImGuiDeactivatedItemData; // Data for IsItemDeactivated()/IsItemDeactivatedAfterEdit() function. +struct ImGuiDockContext; // Docking system context +struct ImGuiDockRequest; // Docking system dock/undock queued request +struct ImGuiDockNode; // Docking system node (hold a list of Windows OR two child dock nodes) +struct ImGuiDockNodeSettings; // Storage for a dock node in .ini file (we preserve those even if the associated dock node isn't active during the session) +struct ImGuiErrorRecoveryState; // Storage of stack sizes for error handling and recovery struct ImGuiGroupData; // Stacked storage data for BeginGroup()/EndGroup() struct ImGuiInputTextState; // Internal state of the currently focused/edited text input box -struct ImGuiItemHoveredDataBackup; // Backup and restore IsItemHovered() internal data +struct ImGuiInputTextDeactivateData;// Short term storage to backup text of a deactivating InputText() while another is stealing active id +struct ImGuiLastItemData; // Status storage for last submitted items +struct ImGuiLocEntry; // A localization entry. struct ImGuiMenuColumns; // Simple column measurement, currently used for MenuItem() only -struct ImGuiNavMoveResult; // Result of a directional navigation move query result +struct ImGuiMultiSelectState; // Multi-selection persistent state (for focused selection). +struct ImGuiMultiSelectTempData; // Multi-selection temporary state (while traversing). +struct ImGuiNavItemData; // Result of a keyboard/gamepad directional navigation move query result +struct ImGuiMetricsConfig; // Storage for ShowMetricsWindow() and DebugNodeXXX() functions struct ImGuiNextWindowData; // Storage for SetNextWindow** functions struct ImGuiNextItemData; // Storage for SetNextItem** functions +struct ImGuiOldColumnData; // Storage data for a single column for legacy Columns() api +struct ImGuiOldColumns; // Storage data for a columns set for legacy Columns() api struct ImGuiPopupData; // Storage for current popup stack struct ImGuiSettingsHandler; // Storage for one type registered in the .ini file struct ImGuiStyleMod; // Stacked style modifier, backup of modified data so we can restore it +struct ImGuiStyleVarInfo; // Style variable information (e.g. to access style variables from an enum) struct ImGuiTabBar; // Storage for a tab bar struct ImGuiTabItem; // Storage for a tab item (within a tab bar) +struct ImGuiTable; // Storage for a table +struct ImGuiTableHeaderData; // Storage for TableAngledHeadersRow() +struct ImGuiTableColumn; // Storage for one column of a table +struct ImGuiTableInstanceData; // Storage for one instance of a same table +struct ImGuiTableTempData; // Temporary storage for one table (one per table in the stack), shared between tables. +struct ImGuiTableSettings; // Storage for a table .ini settings +struct ImGuiTableColumnsSettings; // Storage for a column .ini settings +struct ImGuiTreeNodeStackData; // Temporary storage for TreeNode(). +struct ImGuiTypingSelectState; // Storage for GetTypingSelectRequest() +struct ImGuiTypingSelectRequest; // Storage for GetTypingSelectRequest() (aimed to be public) struct ImGuiWindow; // Storage for one window -struct ImGuiWindowTempData; // Temporary storage for one window (that's the data which in theory we could ditch at the end of the frame) -struct ImGuiWindowSettings; // Storage for window settings stored in .ini file (we keep one of those even if the actual window wasn't instanced during this session) +struct ImGuiWindowDockStyle; // Storage for window-style data which needs to be stored for docking purpose +struct ImGuiWindowTempData; // Temporary storage for one window (that's the data which in theory we could ditch at the end of the frame, in practice we currently keep it for each window) +struct ImGuiWindowSettings; // Storage for a window .ini settings (we keep one of those even if the actual window wasn't instanced during this session) +// Enumerations // Use your programming IDE "Go to definition" facility on the names of the center columns to find the actual flags/enum lists. +enum ImGuiLocKey : int; // -> enum ImGuiLocKey // Enum: a localization entry for translation. +typedef int ImGuiDataAuthority; // -> enum ImGuiDataAuthority_ // Enum: for storing the source authority (dock node vs window) of a field typedef int ImGuiLayoutType; // -> enum ImGuiLayoutType_ // Enum: Horizontal or vertical -typedef int ImGuiButtonFlags; // -> enum ImGuiButtonFlags_ // Flags: for ButtonEx(), ButtonBehavior() -typedef int ImGuiColumnsFlags; // -> enum ImGuiColumnsFlags_ // Flags: BeginColumns() -typedef int ImGuiDragFlags; // -> enum ImGuiDragFlags_ // Flags: for DragBehavior() -typedef int ImGuiItemFlags; // -> enum ImGuiItemFlags_ // Flags: for PushItemFlag() -typedef int ImGuiItemStatusFlags; // -> enum ImGuiItemStatusFlags_ // Flags: for DC.LastItemStatusFlags -typedef int ImGuiNavHighlightFlags; // -> enum ImGuiNavHighlightFlags_ // Flags: for RenderNavHighlight() -typedef int ImGuiNavDirSourceFlags; // -> enum ImGuiNavDirSourceFlags_ // Flags: for GetNavInputAmount2d() + +// Flags +typedef int ImGuiActivateFlags; // -> enum ImGuiActivateFlags_ // Flags: for navigation/focus function (will be for ActivateItem() later) +typedef int ImGuiDebugLogFlags; // -> enum ImGuiDebugLogFlags_ // Flags: for ShowDebugLogWindow(), g.DebugLogFlags +typedef int ImGuiFocusRequestFlags; // -> enum ImGuiFocusRequestFlags_ // Flags: for FocusWindow() +typedef int ImGuiItemStatusFlags; // -> enum ImGuiItemStatusFlags_ // Flags: for g.LastItemData.StatusFlags +typedef int ImGuiOldColumnFlags; // -> enum ImGuiOldColumnFlags_ // Flags: for BeginColumns() +typedef int ImGuiLogFlags; // -> enum ImGuiLogFlags_ // Flags: for LogBegin() text capturing function +typedef int ImGuiNavRenderCursorFlags; // -> enum ImGuiNavRenderCursorFlags_//Flags: for RenderNavCursor() typedef int ImGuiNavMoveFlags; // -> enum ImGuiNavMoveFlags_ // Flags: for navigation requests typedef int ImGuiNextItemDataFlags; // -> enum ImGuiNextItemDataFlags_ // Flags: for SetNextItemXXX() functions typedef int ImGuiNextWindowDataFlags; // -> enum ImGuiNextWindowDataFlags_// Flags: for SetNextWindowXXX() functions +typedef int ImGuiScrollFlags; // -> enum ImGuiScrollFlags_ // Flags: for ScrollToItem() and navigation requests typedef int ImGuiSeparatorFlags; // -> enum ImGuiSeparatorFlags_ // Flags: for SeparatorEx() -typedef int ImGuiSliderFlags; // -> enum ImGuiSliderFlags_ // Flags: for SliderBehavior() typedef int ImGuiTextFlags; // -> enum ImGuiTextFlags_ // Flags: for TextEx() - -//------------------------------------------------------------------------- -// STB libraries includes -//------------------------------------------------------------------------- - -namespace ImStb -{ - -#undef STB_TEXTEDIT_STRING -#undef STB_TEXTEDIT_CHARTYPE -#define STB_TEXTEDIT_STRING ImGuiInputTextState -#define STB_TEXTEDIT_CHARTYPE ImWchar -#define STB_TEXTEDIT_GETWIDTH_NEWLINE -1.0f -#define STB_TEXTEDIT_UNDOSTATECOUNT 99 -#define STB_TEXTEDIT_UNDOCHARCOUNT 999 -#include "imstb_textedit.h" - -} // namespace ImStb +typedef int ImGuiTooltipFlags; // -> enum ImGuiTooltipFlags_ // Flags: for BeginTooltipEx() +typedef int ImGuiTypingSelectFlags; // -> enum ImGuiTypingSelectFlags_ // Flags: for GetTypingSelectRequest() +typedef int ImGuiWindowRefreshFlags; // -> enum ImGuiWindowRefreshFlags_ // Flags: for SetNextWindowRefreshPolicy() //----------------------------------------------------------------------------- -// Context pointer +// [SECTION] Context pointer +// See implementation of this variable in imgui.cpp for comments and details. //----------------------------------------------------------------------------- #ifndef GImGui @@ -131,23 +222,76 @@ extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer #endif //----------------------------------------------------------------------------- -// Generic helpers +// [SECTION] Macros //----------------------------------------------------------------------------- -#define IM_PI 3.14159265358979323846f -#ifdef _WIN32 -#define IM_NEWLINE "\r\n" // Play it nice with Windows users (2018/05 news: Microsoft announced that Notepad will finally display Unix-style carriage returns!) +// Internal Drag and Drop payload types. String starting with '_' are reserved for Dear ImGui. +#define IMGUI_PAYLOAD_TYPE_WINDOW "_IMWINDOW" // Payload == ImGuiWindow* + +// Debug Printing Into TTY +// (since IMGUI_VERSION_NUM >= 18729: IMGUI_DEBUG_LOG was reworked into IMGUI_DEBUG_PRINTF (and removed framecount from it). If you were using a #define IMGUI_DEBUG_LOG please rename) +#ifndef IMGUI_DEBUG_PRINTF +#ifndef IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS +#define IMGUI_DEBUG_PRINTF(_FMT,...) printf(_FMT, __VA_ARGS__) #else -#define IM_NEWLINE "\n" +#define IMGUI_DEBUG_PRINTF(_FMT,...) ((void)0) #endif -#define IM_TABSIZE (4) -#define IM_STATIC_ASSERT(_COND) typedef char static_assertion_##__line__[(_COND)?1:-1] +#endif + +// Debug Logging for ShowDebugLogWindow(). This is designed for relatively rare events so please don't spam. +#define IMGUI_DEBUG_LOG_ERROR(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventError) IMGUI_DEBUG_LOG(__VA_ARGS__); else g.DebugLogSkippedErrors++; } while (0) +#define IMGUI_DEBUG_LOG_ACTIVEID(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventActiveId) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_FOCUS(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFocus) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_POPUP(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventPopup) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_NAV(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventNav) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_SELECTION(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_CLIPPER(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventClipper) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_IO(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventIO) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_FONT(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventFont) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_INPUTROUTING(...) do{if (g.DebugLogFlags & ImGuiDebugLogFlags_EventInputRouting)IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_DOCKING(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventDocking) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) +#define IMGUI_DEBUG_LOG_VIEWPORT(...) do { if (g.DebugLogFlags & ImGuiDebugLogFlags_EventViewport) IMGUI_DEBUG_LOG(__VA_ARGS__); } while (0) + +// Static Asserts +#define IM_STATIC_ASSERT(_COND) static_assert(_COND, "") + +// "Paranoid" Debug Asserts are meant to only be enabled during specific debugging/work, otherwise would slow down the code too much. +// We currently don't have many of those so the effect is currently negligible, but onward intent to add more aggressive ones in the code. +//#define IMGUI_DEBUG_PARANOID +#ifdef IMGUI_DEBUG_PARANOID +#define IM_ASSERT_PARANOID(_EXPR) IM_ASSERT(_EXPR) +#else +#define IM_ASSERT_PARANOID(_EXPR) +#endif + +// Misc Macros +#define IM_PI 3.14159265358979323846f +#ifdef _WIN32 +#define IM_NEWLINE "\r\n" // Play it nice with Windows users (Update: since 2018-05, Notepad finally appears to support Unix-style carriage returns!) +#else +#define IM_NEWLINE "\n" +#endif +#ifndef IM_TABSIZE // Until we move this to runtime and/or add proper tab support, at least allow users to compile-time override +#define IM_TABSIZE (4) +#endif +#define IM_MEMALIGN(_OFF,_ALIGN) (((_OFF) + ((_ALIGN) - 1)) & ~((_ALIGN) - 1)) // Memory align e.g. IM_ALIGN(0,4)=0, IM_ALIGN(1,4)=4, IM_ALIGN(4,4)=4, IM_ALIGN(5,4)=8 #define IM_F32_TO_INT8_UNBOUND(_VAL) ((int)((_VAL) * 255.0f + ((_VAL)>=0 ? 0.5f : -0.5f))) // Unsaturated, for display purpose #define IM_F32_TO_INT8_SAT(_VAL) ((int)(ImSaturate(_VAL) * 255.0f + 0.5f)) // Saturated, always output 0..255 +#define IM_TRUNC(_VAL) ((float)(int)(_VAL)) // ImTrunc() is not inlined in MSVC debug builds +#define IM_ROUND(_VAL) ((float)(int)((_VAL) + 0.5f)) // +#define IM_STRINGIFY_HELPER(_X) #_X +#define IM_STRINGIFY(_X) IM_STRINGIFY_HELPER(_X) // Preprocessor idiom to stringify e.g. an integer. +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +#define IM_FLOOR IM_TRUNC +#endif -// Debug Logging -#ifndef IMGUI_DEBUG_LOG -#define IMGUI_DEBUG_LOG(_FMT,...) printf("[%05d] " _FMT, GImGui->FrameCount, __VA_ARGS__) +// Hint for branch prediction +#if (defined(__cplusplus) && (__cplusplus >= 202002L)) || (defined(_MSVC_LANG) && (_MSVC_LANG >= 202002L)) +#define IM_LIKELY [[likely]] +#define IM_UNLIKELY [[unlikely]] +#else +#define IM_LIKELY +#define IM_UNLIKELY #endif // Enforce cdecl calling convention for functions called by the standard library, in case compilation settings changed the default to e.g. __vectorcall @@ -157,92 +301,186 @@ extern IMGUI_API ImGuiContext* GImGui; // Current implicit context pointer #define IMGUI_CDECL #endif -// Helpers: UTF-8 <> wchar -IMGUI_API int ImTextStrToUtf8(char* buf, int buf_size, const ImWchar* in_text, const ImWchar* in_text_end); // return output UTF-8 bytes count -IMGUI_API int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end); // read one character. return input UTF-8 bytes count -IMGUI_API int ImTextStrFromUtf8(ImWchar* buf, int buf_size, const char* in_text, const char* in_text_end, const char** in_remaining = NULL); // return input UTF-8 bytes count -IMGUI_API int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end); // return number of UTF-8 code-points (NOT bytes count) -IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end); // return number of bytes to express one char in UTF-8 -IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 - -// Helpers: Misc -IMGUI_API ImU32 ImHashData(const void* data, size_t data_size, ImU32 seed = 0); -IMGUI_API ImU32 ImHashStr(const char* data, size_t data_size = 0, ImU32 seed = 0); -IMGUI_API void* ImFileLoadToMemory(const char* filename, const char* file_open_mode, size_t* out_file_size = NULL, int padding_bytes = 0); -IMGUI_API FILE* ImFileOpen(const char* filename, const char* file_open_mode); -static inline bool ImCharIsBlankA(char c) { return c == ' ' || c == '\t'; } -static inline bool ImCharIsBlankW(unsigned int c) { return c == ' ' || c == '\t' || c == 0x3000; } -static inline bool ImIsPowerOfTwo(int v) { return v != 0 && (v & (v - 1)) == 0; } -static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } -#define ImQsort qsort -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS -static inline ImU32 ImHash(const void* data, int size, ImU32 seed = 0) { return size ? ImHashData(data, (size_t)size, seed) : ImHashStr((const char*)data, 0, seed); } // [moved to ImHashStr/ImHashData in 1.68] +// Warnings +#if defined(_MSC_VER) && !defined(__clang__) +#define IM_MSVC_WARNING_SUPPRESS(XXXX) __pragma(warning(suppress: XXXX)) +#else +#define IM_MSVC_WARNING_SUPPRESS(XXXX) #endif -// Helpers: Geometry -IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p); -IMGUI_API bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); -IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); -IMGUI_API void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w); -IMGUI_API ImGuiDir ImGetDirQuadrantFromDelta(float dx, float dy); +// Debug Tools +// Use 'Metrics/Debugger->Tools->Item Picker' to break into the call-stack of a specific item. +// This will call IM_DEBUG_BREAK() which you may redefine yourself. See https://github.com/scottt/debugbreak for more reference. +#ifndef IM_DEBUG_BREAK +#if defined (_MSC_VER) +#define IM_DEBUG_BREAK() __debugbreak() +#elif defined(__clang__) +#define IM_DEBUG_BREAK() __builtin_debugtrap() +#elif defined(__GNUC__) && (defined(__i386__) || defined(__x86_64__)) +#define IM_DEBUG_BREAK() __asm__ volatile("int3;nop") +#elif defined(__GNUC__) && defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xde01") +#elif defined(__GNUC__) && defined(__arm__) && !defined(__thumb__) +#define IM_DEBUG_BREAK() __asm__ volatile(".inst 0xe7f001f0") +#else +#define IM_DEBUG_BREAK() IM_ASSERT(0) // It is expected that you define IM_DEBUG_BREAK() into something that will break nicely in a debugger! +#endif +#endif // #ifndef IM_DEBUG_BREAK + +// Format specifiers, printing 64-bit hasn't been decently standardized... +// In a real application you should be using PRId64 and PRIu64 from (non-windows) and on Windows define them yourself. +#if defined(_MSC_VER) && !defined(__clang__) +#define IM_PRId64 "I64d" +#define IM_PRIu64 "I64u" +#define IM_PRIX64 "I64X" +#else +#define IM_PRId64 "lld" +#define IM_PRIu64 "llu" +#define IM_PRIX64 "llX" +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Generic helpers +// Note that the ImXXX helpers functions are lower-level than ImGui functions. +// ImGui functions or the ImGui context are never called/used from other ImXXX functions. +//----------------------------------------------------------------------------- +// - Helpers: Hashing +// - Helpers: Sorting +// - Helpers: Bit manipulation +// - Helpers: String +// - Helpers: Formatting +// - Helpers: UTF-8 <> wchar conversions +// - Helpers: ImVec2/ImVec4 operators +// - Helpers: Maths +// - Helpers: Geometry +// - Helper: ImVec1 +// - Helper: ImVec2ih +// - Helper: ImRect +// - Helper: ImBitArray +// - Helper: ImBitVector +// - Helper: ImSpan<>, ImSpanAllocator<> +// - Helper: ImPool<> +// - Helper: ImChunkStream<> +// - Helper: ImGuiTextIndex +// - Helper: ImGuiStorage +//----------------------------------------------------------------------------- + +// Helpers: Hashing +IMGUI_API ImGuiID ImHashData(const void* data, size_t data_size, ImGuiID seed = 0); +IMGUI_API ImGuiID ImHashStr(const char* data, size_t data_size = 0, ImGuiID seed = 0); + +// Helpers: Sorting +#ifndef ImQsort +static inline void ImQsort(void* base, size_t count, size_t size_of_element, int(IMGUI_CDECL *compare_func)(void const*, void const*)) { if (count > 1) qsort(base, count, size_of_element, compare_func); } +#endif + +// Helpers: Color Blending +IMGUI_API ImU32 ImAlphaBlendColors(ImU32 col_a, ImU32 col_b); + +// Helpers: Bit manipulation +static inline bool ImIsPowerOfTwo(int v) { return v != 0 && (v & (v - 1)) == 0; } +static inline bool ImIsPowerOfTwo(ImU64 v) { return v != 0 && (v & (v - 1)) == 0; } +static inline int ImUpperPowerOfTwo(int v) { v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++; return v; } +static inline unsigned int ImCountSetBits(unsigned int v) { unsigned int count = 0; while (v > 0) { v = v & (v - 1); count++; } return count; } // Helpers: String -IMGUI_API int ImStricmp(const char* str1, const char* str2); -IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); -IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); -IMGUI_API char* ImStrdup(const char* str); -IMGUI_API char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); -IMGUI_API const char* ImStrchrRange(const char* str_begin, const char* str_end, char c); -IMGUI_API int ImStrlenW(const ImWchar* str); +#define ImStrlen strlen +#define ImMemchr memchr +IMGUI_API int ImStricmp(const char* str1, const char* str2); // Case insensitive compare. +IMGUI_API int ImStrnicmp(const char* str1, const char* str2, size_t count); // Case insensitive compare to a certain count. +IMGUI_API void ImStrncpy(char* dst, const char* src, size_t count); // Copy to a certain count and always zero terminate (strncpy doesn't). +IMGUI_API char* ImStrdup(const char* str); // Duplicate a string. +IMGUI_API char* ImStrdupcpy(char* dst, size_t* p_dst_size, const char* str); // Copy in provided buffer, recreate buffer if needed. +IMGUI_API const char* ImStrchrRange(const char* str_begin, const char* str_end, char c); // Find first occurrence of 'c' in string range. IMGUI_API const char* ImStreolRange(const char* str, const char* str_end); // End end-of-line -IMGUI_API const ImWchar*ImStrbolW(const ImWchar* buf_mid_line, const ImWchar* buf_begin); // Find beginning-of-line -IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); -IMGUI_API void ImStrTrimBlanks(char* str); +IMGUI_API const char* ImStristr(const char* haystack, const char* haystack_end, const char* needle, const char* needle_end); // Find a substring in a string range. +IMGUI_API void ImStrTrimBlanks(char* str); // Remove leading and trailing blanks from a buffer. +IMGUI_API const char* ImStrSkipBlank(const char* str); // Find first non-blank character. +IMGUI_API int ImStrlenW(const ImWchar* str); // Computer string length (ImWchar string) +IMGUI_API const char* ImStrbol(const char* buf_mid_line, const char* buf_begin); // Find beginning-of-line +IM_MSVC_RUNTIME_CHECKS_OFF +static inline char ImToUpper(char c) { return (c >= 'a' && c <= 'z') ? c &= ~32 : c; } +static inline bool ImCharIsBlankA(char c) { return c == ' ' || c == '\t'; } +static inline bool ImCharIsBlankW(unsigned int c) { return c == ' ' || c == '\t' || c == 0x3000; } +static inline bool ImCharIsXdigitA(char c) { return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); } +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helpers: Formatting IMGUI_API int ImFormatString(char* buf, size_t buf_size, const char* fmt, ...) IM_FMTARGS(3); IMGUI_API int ImFormatStringV(char* buf, size_t buf_size, const char* fmt, va_list args) IM_FMTLIST(3); +IMGUI_API void ImFormatStringToTempBuffer(const char** out_buf, const char** out_buf_end, const char* fmt, ...) IM_FMTARGS(3); +IMGUI_API void ImFormatStringToTempBufferV(const char** out_buf, const char** out_buf_end, const char* fmt, va_list args) IM_FMTLIST(3); IMGUI_API const char* ImParseFormatFindStart(const char* format); IMGUI_API const char* ImParseFormatFindEnd(const char* format); IMGUI_API const char* ImParseFormatTrimDecorations(const char* format, char* buf, size_t buf_size); +IMGUI_API void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size); +IMGUI_API const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size); IMGUI_API int ImParseFormatPrecision(const char* format, int default_value); -// Helpers: ImVec2/ImVec4 operators -// We are keeping those disabled by default so they don't leak in user space, to allow user enabling implicit cast operators between ImVec2 and their own types (using IM_VEC2_CLASS_EXTRA etc.) -// We unfortunately don't have a unary- operator for ImVec2 because this would needs to be defined inside the class itself. -#ifdef IMGUI_DEFINE_MATH_OPERATORS -static inline ImVec2 operator*(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x*rhs, lhs.y*rhs); } -static inline ImVec2 operator/(const ImVec2& lhs, const float rhs) { return ImVec2(lhs.x/rhs, lhs.y/rhs); } -static inline ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x+rhs.x, lhs.y+rhs.y); } -static inline ImVec2 operator-(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x-rhs.x, lhs.y-rhs.y); } -static inline ImVec2 operator*(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x*rhs.x, lhs.y*rhs.y); } -static inline ImVec2 operator/(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x/rhs.x, lhs.y/rhs.y); } -static inline ImVec2& operator+=(ImVec2& lhs, const ImVec2& rhs) { lhs.x += rhs.x; lhs.y += rhs.y; return lhs; } -static inline ImVec2& operator-=(ImVec2& lhs, const ImVec2& rhs) { lhs.x -= rhs.x; lhs.y -= rhs.y; return lhs; } -static inline ImVec2& operator*=(ImVec2& lhs, const float rhs) { lhs.x *= rhs; lhs.y *= rhs; return lhs; } -static inline ImVec2& operator/=(ImVec2& lhs, const float rhs) { lhs.x /= rhs; lhs.y /= rhs; return lhs; } -static inline ImVec4 operator+(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x+rhs.x, lhs.y+rhs.y, lhs.z+rhs.z, lhs.w+rhs.w); } -static inline ImVec4 operator-(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x-rhs.x, lhs.y-rhs.y, lhs.z-rhs.z, lhs.w-rhs.w); } -static inline ImVec4 operator*(const ImVec4& lhs, const ImVec4& rhs) { return ImVec4(lhs.x*rhs.x, lhs.y*rhs.y, lhs.z*rhs.z, lhs.w*rhs.w); } +// Helpers: UTF-8 <> wchar conversions +IMGUI_API const char* ImTextCharToUtf8(char out_buf[5], unsigned int c); // return out_buf +IMGUI_API int ImTextStrToUtf8(char* out_buf, int out_buf_size, const ImWchar* in_text, const ImWchar* in_text_end); // return output UTF-8 bytes count +IMGUI_API int ImTextCharFromUtf8(unsigned int* out_char, const char* in_text, const char* in_text_end); // read one character. return input UTF-8 bytes count +IMGUI_API int ImTextStrFromUtf8(ImWchar* out_buf, int out_buf_size, const char* in_text, const char* in_text_end, const char** in_remaining = NULL); // return input UTF-8 bytes count +IMGUI_API int ImTextCountCharsFromUtf8(const char* in_text, const char* in_text_end); // return number of UTF-8 code-points (NOT bytes count) +IMGUI_API int ImTextCountUtf8BytesFromChar(const char* in_text, const char* in_text_end); // return number of bytes to express one char in UTF-8 +IMGUI_API int ImTextCountUtf8BytesFromStr(const ImWchar* in_text, const ImWchar* in_text_end); // return number of bytes to express string in UTF-8 +IMGUI_API const char* ImTextFindPreviousUtf8Codepoint(const char* in_text_start, const char* in_text_curr); // return previous UTF-8 code-point. +IMGUI_API int ImTextCountLines(const char* in_text, const char* in_text_end); // return number of lines taken by text. trailing carriage return doesn't count as an extra line. + +// Helpers: File System +#ifdef IMGUI_DISABLE_FILE_FUNCTIONS +#define IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS +typedef void* ImFileHandle; +static inline ImFileHandle ImFileOpen(const char*, const char*) { return NULL; } +static inline bool ImFileClose(ImFileHandle) { return false; } +static inline ImU64 ImFileGetSize(ImFileHandle) { return (ImU64)-1; } +static inline ImU64 ImFileRead(void*, ImU64, ImU64, ImFileHandle) { return 0; } +static inline ImU64 ImFileWrite(const void*, ImU64, ImU64, ImFileHandle) { return 0; } #endif +#ifndef IMGUI_DISABLE_DEFAULT_FILE_FUNCTIONS +typedef FILE* ImFileHandle; +IMGUI_API ImFileHandle ImFileOpen(const char* filename, const char* mode); +IMGUI_API bool ImFileClose(ImFileHandle file); +IMGUI_API ImU64 ImFileGetSize(ImFileHandle file); +IMGUI_API ImU64 ImFileRead(void* data, ImU64 size, ImU64 count, ImFileHandle file); +IMGUI_API ImU64 ImFileWrite(const void* data, ImU64 size, ImU64 count, ImFileHandle file); +#else +#define IMGUI_DISABLE_TTY_FUNCTIONS // Can't use stdout, fflush if we are not using default file functions +#endif +IMGUI_API void* ImFileLoadToMemory(const char* filename, const char* mode, size_t* out_file_size = NULL, int padding_bytes = 0); // Helpers: Maths +IM_MSVC_RUNTIME_CHECKS_OFF // - Wrapper for standard libs functions. (Note that imgui_demo.cpp does _not_ use them to keep the code easy to copy) -#ifndef IMGUI_DISABLE_MATH_FUNCTIONS -static inline float ImFabs(float x) { return fabsf(x); } -static inline float ImSqrt(float x) { return sqrtf(x); } -static inline float ImPow(float x, float y) { return powf(x, y); } -static inline double ImPow(double x, double y) { return pow(x, y); } -static inline float ImFmod(float x, float y) { return fmodf(x, y); } -static inline double ImFmod(double x, double y) { return fmod(x, y); } -static inline float ImCos(float x) { return cosf(x); } -static inline float ImSin(float x) { return sinf(x); } -static inline float ImAcos(float x) { return acosf(x); } -static inline float ImAtan2(float y, float x) { return atan2f(y, x); } -static inline double ImAtof(const char* s) { return atof(s); } -static inline float ImFloorStd(float x) { return floorf(x); } // we already uses our own ImFloor() { return (float)(int)v } internally so the standard one wrapper is named differently (it's used by stb_truetype) -static inline float ImCeil(float x) { return ceilf(x); } +#ifndef IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS +#define ImFabs(X) fabsf(X) +#define ImSqrt(X) sqrtf(X) +#define ImFmod(X, Y) fmodf((X), (Y)) +#define ImCos(X) cosf(X) +#define ImSin(X) sinf(X) +#define ImAcos(X) acosf(X) +#define ImAtan2(Y, X) atan2f((Y), (X)) +#define ImAtof(STR) atof(STR) +#define ImCeil(X) ceilf(X) +static inline float ImPow(float x, float y) { return powf(x, y); } // DragBehaviorT/SliderBehaviorT uses ImPow with either float/double and need the precision +static inline double ImPow(double x, double y) { return pow(x, y); } +static inline float ImLog(float x) { return logf(x); } // DragBehaviorT/SliderBehaviorT uses ImLog with either float/double and need the precision +static inline double ImLog(double x) { return log(x); } +static inline int ImAbs(int x) { return x < 0 ? -x : x; } +static inline float ImAbs(float x) { return fabsf(x); } +static inline double ImAbs(double x) { return fabs(x); } +static inline float ImSign(float x) { return (x < 0.0f) ? -1.0f : (x > 0.0f) ? 1.0f : 0.0f; } // Sign operator - returns -1, 0 or 1 based on sign of argument +static inline double ImSign(double x) { return (x < 0.0) ? -1.0 : (x > 0.0) ? 1.0 : 0.0; } +#ifdef IMGUI_ENABLE_SSE +static inline float ImRsqrt(float x) { return _mm_cvtss_f32(_mm_rsqrt_ss(_mm_set_ss(x))); } +#else +static inline float ImRsqrt(float x) { return 1.0f / sqrtf(x); } #endif -// - ImMin/ImMax/ImClamp/ImLerp/ImSwap are used by widgets which support for variety of types: signed/unsigned int/long long float/double -// (Exceptionally using templates here but we could also redefine them for variety of types) +static inline double ImRsqrt(double x) { return 1.0 / sqrt(x); } +#endif +// - ImMin/ImMax/ImClamp/ImLerp/ImSwap are used by widgets which support variety of types: signed/unsigned int/long long float/double +// (Exceptionally using templates here but we could also redefine them for those types) template static inline T ImMin(T lhs, T rhs) { return lhs < rhs ? lhs : rhs; } template static inline T ImMax(T lhs, T rhs) { return lhs >= rhs ? lhs : rhs; } template static inline T ImClamp(T v, T mn, T mx) { return (v < mn) ? mn : (v > mx) ? mx : v; } @@ -253,170 +491,553 @@ template static inline T ImSubClampOverflow(T a, T b, T mn, T mx) // - Misc maths helpers static inline ImVec2 ImMin(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x < rhs.x ? lhs.x : rhs.x, lhs.y < rhs.y ? lhs.y : rhs.y); } static inline ImVec2 ImMax(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x >= rhs.x ? lhs.x : rhs.x, lhs.y >= rhs.y ? lhs.y : rhs.y); } -static inline ImVec2 ImClamp(const ImVec2& v, const ImVec2& mn, ImVec2 mx) { return ImVec2((v.x < mn.x) ? mn.x : (v.x > mx.x) ? mx.x : v.x, (v.y < mn.y) ? mn.y : (v.y > mx.y) ? mx.y : v.y); } +static inline ImVec2 ImClamp(const ImVec2& v, const ImVec2&mn, const ImVec2&mx) { return ImVec2((v.x < mn.x) ? mn.x : (v.x > mx.x) ? mx.x : v.x, (v.y < mn.y) ? mn.y : (v.y > mx.y) ? mx.y : v.y); } static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, float t) { return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); } static inline ImVec2 ImLerp(const ImVec2& a, const ImVec2& b, const ImVec2& t) { return ImVec2(a.x + (b.x - a.x) * t.x, a.y + (b.y - a.y) * t.y); } static inline ImVec4 ImLerp(const ImVec4& a, const ImVec4& b, float t) { return ImVec4(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t, a.w + (b.w - a.w) * t); } static inline float ImSaturate(float f) { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; } -static inline float ImLengthSqr(const ImVec2& lhs) { return lhs.x*lhs.x + lhs.y*lhs.y; } -static inline float ImLengthSqr(const ImVec4& lhs) { return lhs.x*lhs.x + lhs.y*lhs.y + lhs.z*lhs.z + lhs.w*lhs.w; } -static inline float ImInvLength(const ImVec2& lhs, float fail_value) { float d = lhs.x*lhs.x + lhs.y*lhs.y; if (d > 0.0f) return 1.0f / ImSqrt(d); return fail_value; } -static inline float ImFloor(float f) { return (float)(int)f; } -static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2((float)(int)v.x, (float)(int)v.y); } +static inline float ImLengthSqr(const ImVec2& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y); } +static inline float ImLengthSqr(const ImVec4& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y) + (lhs.z * lhs.z) + (lhs.w * lhs.w); } +static inline float ImInvLength(const ImVec2& lhs, float fail_value) { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImRsqrt(d); return fail_value; } +static inline float ImTrunc(float f) { return (float)(int)(f); } +static inline ImVec2 ImTrunc(const ImVec2& v) { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); } +static inline float ImFloor(float f) { return (float)((f >= 0 || (float)(int)f == f) ? (int)f : (int)f - 1); } // Decent replacement for floorf() +static inline ImVec2 ImFloor(const ImVec2& v) { return ImVec2(ImFloor(v.x), ImFloor(v.y)); } static inline int ImModPositive(int a, int b) { return (a + b) % b; } static inline float ImDot(const ImVec2& a, const ImVec2& b) { return a.x * b.x + a.y * b.y; } static inline ImVec2 ImRotate(const ImVec2& v, float cos_a, float sin_a) { return ImVec2(v.x * cos_a - v.y * sin_a, v.x * sin_a + v.y * cos_a); } static inline float ImLinearSweep(float current, float target, float speed) { if (current < target) return ImMin(current + speed, target); if (current > target) return ImMax(current - speed, target); return current; } +static inline float ImLinearRemapClamp(float s0, float s1, float d0, float d1, float x) { return ImSaturate((x - s0) / (s1 - s0)) * (d1 - d0) + d0; } static inline ImVec2 ImMul(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x * rhs.x, lhs.y * rhs.y); } +static inline bool ImIsFloatAboveGuaranteedIntegerPrecision(float f) { return f <= -16777216 || f >= 16777216; } +static inline float ImExponentialMovingAverage(float avg, float sample, int n) { avg -= avg / n; avg += sample / n; return avg; } +IM_MSVC_RUNTIME_CHECKS_RESTORE -// Helper: ImBoolVector. Store 1-bit per value. -// Note that Resize() currently clears the whole vector. -struct ImBoolVector +// Helpers: Geometry +IMGUI_API ImVec2 ImBezierCubicCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, float t); +IMGUI_API ImVec2 ImBezierCubicClosestPoint(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, int num_segments); // For curves with explicit number of segments +IMGUI_API ImVec2 ImBezierCubicClosestPointCasteljau(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& p, float tess_tol);// For auto-tessellated curves you can use tess_tol = style.CurveTessellationTol +IMGUI_API ImVec2 ImBezierQuadraticCalc(const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, float t); +IMGUI_API ImVec2 ImLineClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& p); +IMGUI_API bool ImTriangleContainsPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API ImVec2 ImTriangleClosestPoint(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p); +IMGUI_API void ImTriangleBarycentricCoords(const ImVec2& a, const ImVec2& b, const ImVec2& c, const ImVec2& p, float& out_u, float& out_v, float& out_w); +inline float ImTriangleArea(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ImFabs((a.x * (b.y - c.y)) + (b.x * (c.y - a.y)) + (c.x * (a.y - b.y))) * 0.5f; } +inline bool ImTriangleIsClockwise(const ImVec2& a, const ImVec2& b, const ImVec2& c) { return ((b.x - a.x) * (c.y - b.y)) - ((c.x - b.x) * (b.y - a.y)) > 0.0f; } + +// Helper: ImVec1 (1D vector) +// (this odd construct is used to facilitate the transition between 1D and 2D, and the maintenance of some branches/patches) +IM_MSVC_RUNTIME_CHECKS_OFF +struct ImVec1 { - ImVector Storage; - ImBoolVector() { } - void Resize(int sz) { Storage.resize((sz + 31) >> 5); memset(Storage.Data, 0, (size_t)Storage.Size * sizeof(Storage.Data[0])); } - void Clear() { Storage.clear(); } - bool GetBit(int n) const { int off = (n >> 5); int mask = 1 << (n & 31); return (Storage[off] & mask) != 0; } - void SetBit(int n, bool v) { int off = (n >> 5); int mask = 1 << (n & 31); if (v) Storage[off] |= mask; else Storage[off] &= ~mask; } + float x; + constexpr ImVec1() : x(0.0f) { } + constexpr ImVec1(float _x) : x(_x) { } }; -// Helper: ImPool<>. Basic keyed storage for contiguous instances, slow/amortized insertion, O(1) indexable, O(Log N) queries by ID over a dense/hot buffer, +// Helper: ImVec2ih (2D vector, half-size integer, for long-term packed storage) +struct ImVec2ih +{ + short x, y; + constexpr ImVec2ih() : x(0), y(0) {} + constexpr ImVec2ih(short _x, short _y) : x(_x), y(_y) {} + constexpr explicit ImVec2ih(const ImVec2& rhs) : x((short)rhs.x), y((short)rhs.y) {} +}; + +// Helper: ImRect (2D axis aligned bounding-box) +// NB: we can't rely on ImVec2 math operators being available here! +struct IMGUI_API ImRect +{ + ImVec2 Min; // Upper-left + ImVec2 Max; // Lower-right + + constexpr ImRect() : Min(0.0f, 0.0f), Max(0.0f, 0.0f) {} + constexpr ImRect(const ImVec2& min, const ImVec2& max) : Min(min), Max(max) {} + constexpr ImRect(const ImVec4& v) : Min(v.x, v.y), Max(v.z, v.w) {} + constexpr ImRect(float x1, float y1, float x2, float y2) : Min(x1, y1), Max(x2, y2) {} + + ImVec2 GetCenter() const { return ImVec2((Min.x + Max.x) * 0.5f, (Min.y + Max.y) * 0.5f); } + ImVec2 GetSize() const { return ImVec2(Max.x - Min.x, Max.y - Min.y); } + float GetWidth() const { return Max.x - Min.x; } + float GetHeight() const { return Max.y - Min.y; } + float GetArea() const { return (Max.x - Min.x) * (Max.y - Min.y); } + ImVec2 GetTL() const { return Min; } // Top-left + ImVec2 GetTR() const { return ImVec2(Max.x, Min.y); } // Top-right + ImVec2 GetBL() const { return ImVec2(Min.x, Max.y); } // Bottom-left + ImVec2 GetBR() const { return Max; } // Bottom-right + bool Contains(const ImVec2& p) const { return p.x >= Min.x && p.y >= Min.y && p.x < Max.x && p.y < Max.y; } + bool Contains(const ImRect& r) const { return r.Min.x >= Min.x && r.Min.y >= Min.y && r.Max.x <= Max.x && r.Max.y <= Max.y; } + bool ContainsWithPad(const ImVec2& p, const ImVec2& pad) const { return p.x >= Min.x - pad.x && p.y >= Min.y - pad.y && p.x < Max.x + pad.x && p.y < Max.y + pad.y; } + bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } + void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } + void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } + void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; } + void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; } + void Translate(const ImVec2& d) { Min.x += d.x; Min.y += d.y; Max.x += d.x; Max.y += d.y; } + void TranslateX(float dx) { Min.x += dx; Max.x += dx; } + void TranslateY(float dy) { Min.y += dy; Max.y += dy; } + void ClipWith(const ImRect& r) { Min = ImMax(Min, r.Min); Max = ImMin(Max, r.Max); } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display. + void ClipWithFull(const ImRect& r) { Min = ImClamp(Min, r.Min, r.Max); Max = ImClamp(Max, r.Min, r.Max); } // Full version, ensure both points are fully clipped. + void Floor() { Min.x = IM_TRUNC(Min.x); Min.y = IM_TRUNC(Min.y); Max.x = IM_TRUNC(Max.x); Max.y = IM_TRUNC(Max.y); } + bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; } + ImVec4 ToVec4() const { return ImVec4(Min.x, Min.y, Max.x, Max.y); } +}; + +// Helper: ImBitArray +#define IM_BITARRAY_TESTBIT(_ARRAY, _N) ((_ARRAY[(_N) >> 5] & ((ImU32)1 << ((_N) & 31))) != 0) // Macro version of ImBitArrayTestBit(): ensure args have side-effect or are costly! +#define IM_BITARRAY_CLEARBIT(_ARRAY, _N) ((_ARRAY[(_N) >> 5] &= ~((ImU32)1 << ((_N) & 31)))) // Macro version of ImBitArrayClearBit(): ensure args have side-effect or are costly! +inline size_t ImBitArrayGetStorageSizeInBytes(int bitcount) { return (size_t)((bitcount + 31) >> 5) << 2; } +inline void ImBitArrayClearAllBits(ImU32* arr, int bitcount){ memset(arr, 0, ImBitArrayGetStorageSizeInBytes(bitcount)); } +inline bool ImBitArrayTestBit(const ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); return (arr[n >> 5] & mask) != 0; } +inline void ImBitArrayClearBit(ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); arr[n >> 5] &= ~mask; } +inline void ImBitArraySetBit(ImU32* arr, int n) { ImU32 mask = (ImU32)1 << (n & 31); arr[n >> 5] |= mask; } +inline void ImBitArraySetBitRange(ImU32* arr, int n, int n2) // Works on range [n..n2) +{ + n2--; + while (n <= n2) + { + int a_mod = (n & 31); + int b_mod = (n2 > (n | 31) ? 31 : (n2 & 31)) + 1; + ImU32 mask = (ImU32)(((ImU64)1 << b_mod) - 1) & ~(ImU32)(((ImU64)1 << a_mod) - 1); + arr[n >> 5] |= mask; + n = (n + 32) & ~31; + } +} + +typedef ImU32* ImBitArrayPtr; // Name for use in structs + +// Helper: ImBitArray class (wrapper over ImBitArray functions) +// Store 1-bit per value. +template +struct ImBitArray +{ + ImU32 Storage[(BITCOUNT + 31) >> 5]; + ImBitArray() { ClearAllBits(); } + void ClearAllBits() { memset(Storage, 0, sizeof(Storage)); } + void SetAllBits() { memset(Storage, 255, sizeof(Storage)); } + bool TestBit(int n) const { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); return IM_BITARRAY_TESTBIT(Storage, n); } + void SetBit(int n) { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); ImBitArraySetBit(Storage, n); } + void ClearBit(int n) { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); ImBitArrayClearBit(Storage, n); } + void SetBitRange(int n, int n2) { n += OFFSET; n2 += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT && n2 > n && n2 <= BITCOUNT); ImBitArraySetBitRange(Storage, n, n2); } // Works on range [n..n2) + bool operator[](int n) const { n += OFFSET; IM_ASSERT(n >= 0 && n < BITCOUNT); return IM_BITARRAY_TESTBIT(Storage, n); } +}; + +// Helper: ImBitVector +// Store 1-bit per value. +struct IMGUI_API ImBitVector +{ + ImVector Storage; + void Create(int sz) { Storage.resize((sz + 31) >> 5); memset(Storage.Data, 0, (size_t)Storage.Size * sizeof(Storage.Data[0])); } + void Clear() { Storage.clear(); } + bool TestBit(int n) const { IM_ASSERT(n < (Storage.Size << 5)); return IM_BITARRAY_TESTBIT(Storage.Data, n); } + void SetBit(int n) { IM_ASSERT(n < (Storage.Size << 5)); ImBitArraySetBit(Storage.Data, n); } + void ClearBit(int n) { IM_ASSERT(n < (Storage.Size << 5)); ImBitArrayClearBit(Storage.Data, n); } +}; +IM_MSVC_RUNTIME_CHECKS_RESTORE + +// Helper: ImSpan<> +// Pointing to a span of data we don't own. +template +struct ImSpan +{ + T* Data; + T* DataEnd; + + // Constructors, destructor + inline ImSpan() { Data = DataEnd = NULL; } + inline ImSpan(T* data, int size) { Data = data; DataEnd = data + size; } + inline ImSpan(T* data, T* data_end) { Data = data; DataEnd = data_end; } + + inline void set(T* data, int size) { Data = data; DataEnd = data + size; } + inline void set(T* data, T* data_end) { Data = data; DataEnd = data_end; } + inline int size() const { return (int)(ptrdiff_t)(DataEnd - Data); } + inline int size_in_bytes() const { return (int)(ptrdiff_t)(DataEnd - Data) * (int)sizeof(T); } + inline T& operator[](int i) { T* p = Data + i; IM_ASSERT(p >= Data && p < DataEnd); return *p; } + inline const T& operator[](int i) const { const T* p = Data + i; IM_ASSERT(p >= Data && p < DataEnd); return *p; } + + inline T* begin() { return Data; } + inline const T* begin() const { return Data; } + inline T* end() { return DataEnd; } + inline const T* end() const { return DataEnd; } + + // Utilities + inline int index_from_ptr(const T* it) const { IM_ASSERT(it >= Data && it < DataEnd); const ptrdiff_t off = it - Data; return (int)off; } +}; + +// Helper: ImSpanAllocator<> +// Facilitate storing multiple chunks into a single large block (the "arena") +// - Usage: call Reserve() N times, allocate GetArenaSizeInBytes() worth, pass it to SetArenaBasePtr(), call GetSpan() N times to retrieve the aligned ranges. +template +struct ImSpanAllocator +{ + char* BasePtr; + int CurrOff; + int CurrIdx; + int Offsets[CHUNKS]; + int Sizes[CHUNKS]; + + ImSpanAllocator() { memset(this, 0, sizeof(*this)); } + inline void Reserve(int n, size_t sz, int a=4) { IM_ASSERT(n == CurrIdx && n < CHUNKS); CurrOff = IM_MEMALIGN(CurrOff, a); Offsets[n] = CurrOff; Sizes[n] = (int)sz; CurrIdx++; CurrOff += (int)sz; } + inline int GetArenaSizeInBytes() { return CurrOff; } + inline void SetArenaBasePtr(void* base_ptr) { BasePtr = (char*)base_ptr; } + inline void* GetSpanPtrBegin(int n) { IM_ASSERT(n >= 0 && n < CHUNKS && CurrIdx == CHUNKS); return (void*)(BasePtr + Offsets[n]); } + inline void* GetSpanPtrEnd(int n) { IM_ASSERT(n >= 0 && n < CHUNKS && CurrIdx == CHUNKS); return (void*)(BasePtr + Offsets[n] + Sizes[n]); } + template + inline void GetSpan(int n, ImSpan* span) { span->set((T*)GetSpanPtrBegin(n), (T*)GetSpanPtrEnd(n)); } +}; + +// Helper: ImPool<> +// Basic keyed storage for contiguous instances, slow/amortized insertion, O(1) indexable, O(Log N) queries by ID over a dense/hot buffer, // Honor constructor/destructor. Add/remove invalidate all pointers. Indexes have the same lifetime as the associated object. typedef int ImPoolIdx; template -struct IMGUI_API ImPool +struct ImPool { - ImVector Data; // Contiguous data + ImVector Buf; // Contiguous data ImGuiStorage Map; // ID->Index ImPoolIdx FreeIdx; // Next free idx to use + ImPoolIdx AliveCount; // Number of active/alive items (for display purpose) - ImPool() { FreeIdx = 0; } + ImPool() { FreeIdx = AliveCount = 0; } ~ImPool() { Clear(); } - T* GetByKey(ImGuiID key) { int idx = Map.GetInt(key, -1); return (idx != -1) ? &Data[idx] : NULL; } - T* GetByIndex(ImPoolIdx n) { return &Data[n]; } - ImPoolIdx GetIndex(const T* p) const { IM_ASSERT(p >= Data.Data && p < Data.Data + Data.Size); return (ImPoolIdx)(p - Data.Data); } - T* GetOrAddByKey(ImGuiID key) { int* p_idx = Map.GetIntRef(key, -1); if (*p_idx != -1) return &Data[*p_idx]; *p_idx = FreeIdx; return Add(); } - bool Contains(const T* p) const { return (p >= Data.Data && p < Data.Data + Data.Size); } - void Clear() { for (int n = 0; n < Map.Data.Size; n++) { int idx = Map.Data[n].val_i; if (idx != -1) Data[idx].~T(); } Map.Clear(); Data.clear(); FreeIdx = 0; } - T* Add() { int idx = FreeIdx; if (idx == Data.Size) { Data.resize(Data.Size + 1); FreeIdx++; } else { FreeIdx = *(int*)&Data[idx]; } IM_PLACEMENT_NEW(&Data[idx]) T(); return &Data[idx]; } + T* GetByKey(ImGuiID key) { int idx = Map.GetInt(key, -1); return (idx != -1) ? &Buf[idx] : NULL; } + T* GetByIndex(ImPoolIdx n) { return &Buf[n]; } + ImPoolIdx GetIndex(const T* p) const { IM_ASSERT(p >= Buf.Data && p < Buf.Data + Buf.Size); return (ImPoolIdx)(p - Buf.Data); } + T* GetOrAddByKey(ImGuiID key) { int* p_idx = Map.GetIntRef(key, -1); if (*p_idx != -1) return &Buf[*p_idx]; *p_idx = FreeIdx; return Add(); } + bool Contains(const T* p) const { return (p >= Buf.Data && p < Buf.Data + Buf.Size); } + void Clear() { for (int n = 0; n < Map.Data.Size; n++) { int idx = Map.Data[n].val_i; if (idx != -1) Buf[idx].~T(); } Map.Clear(); Buf.clear(); FreeIdx = AliveCount = 0; } + T* Add() { int idx = FreeIdx; if (idx == Buf.Size) { Buf.resize(Buf.Size + 1); FreeIdx++; } else { FreeIdx = *(int*)&Buf[idx]; } IM_PLACEMENT_NEW(&Buf[idx]) T(); AliveCount++; return &Buf[idx]; } void Remove(ImGuiID key, const T* p) { Remove(key, GetIndex(p)); } - void Remove(ImGuiID key, ImPoolIdx idx) { Data[idx].~T(); *(int*)&Data[idx] = FreeIdx; FreeIdx = idx; Map.SetInt(key, -1); } - void Reserve(int capacity) { Data.reserve(capacity); Map.Data.reserve(capacity); } - int GetSize() const { return Data.Size; } + void Remove(ImGuiID key, ImPoolIdx idx) { Buf[idx].~T(); *(int*)&Buf[idx] = FreeIdx; FreeIdx = idx; Map.SetInt(key, -1); AliveCount--; } + void Reserve(int capacity) { Buf.reserve(capacity); Map.Data.reserve(capacity); } + + // To iterate a ImPool: for (int n = 0; n < pool.GetMapSize(); n++) if (T* t = pool.TryGetMapData(n)) { ... } + // Can be avoided if you know .Remove() has never been called on the pool, or AliveCount == GetMapSize() + int GetAliveCount() const { return AliveCount; } // Number of active/alive items in the pool (for display purpose) + int GetBufSize() const { return Buf.Size; } + int GetMapSize() const { return Map.Data.Size; } // It is the map we need iterate to find valid items, since we don't have "alive" storage anywhere + T* TryGetMapData(ImPoolIdx n) { int idx = Map.Data[n].val_i; if (idx == -1) return NULL; return GetByIndex(idx); } +}; + +// Helper: ImChunkStream<> +// Build and iterate a contiguous stream of variable-sized structures. +// This is used by Settings to store persistent data while reducing allocation count. +// We store the chunk size first, and align the final size on 4 bytes boundaries. +// The tedious/zealous amount of casting is to avoid -Wcast-align warnings. +template +struct ImChunkStream +{ + ImVector Buf; + + void clear() { Buf.clear(); } + bool empty() const { return Buf.Size == 0; } + int size() const { return Buf.Size; } + T* alloc_chunk(size_t sz) { size_t HDR_SZ = 4; sz = IM_MEMALIGN(HDR_SZ + sz, 4u); int off = Buf.Size; Buf.resize(off + (int)sz); ((int*)(void*)(Buf.Data + off))[0] = (int)sz; return (T*)(void*)(Buf.Data + off + (int)HDR_SZ); } + T* begin() { size_t HDR_SZ = 4; if (!Buf.Data) return NULL; return (T*)(void*)(Buf.Data + HDR_SZ); } + T* next_chunk(T* p) { size_t HDR_SZ = 4; IM_ASSERT(p >= begin() && p < end()); p = (T*)(void*)((char*)(void*)p + chunk_size(p)); if (p == (T*)(void*)((char*)end() + HDR_SZ)) return (T*)0; IM_ASSERT(p < end()); return p; } + int chunk_size(const T* p) { return ((const int*)p)[-1]; } + T* end() { return (T*)(void*)(Buf.Data + Buf.Size); } + int offset_from_ptr(const T* p) { IM_ASSERT(p >= begin() && p < end()); const ptrdiff_t off = (const char*)p - Buf.Data; return (int)off; } + T* ptr_from_offset(int off) { IM_ASSERT(off >= 4 && off < Buf.Size); return (T*)(void*)(Buf.Data + off); } + void swap(ImChunkStream& rhs) { rhs.Buf.swap(Buf); } +}; + +// Helper: ImGuiTextIndex +// Maintain a line index for a text buffer. This is a strong candidate to be moved into the public API. +struct ImGuiTextIndex +{ + ImVector LineOffsets; + int EndOffset = 0; // Because we don't own text buffer we need to maintain EndOffset (may bake in LineOffsets?) + + void clear() { LineOffsets.clear(); EndOffset = 0; } + int size() { return LineOffsets.Size; } + const char* get_line_begin(const char* base, int n) { return base + LineOffsets[n]; } + const char* get_line_end(const char* base, int n) { return base + (n + 1 < LineOffsets.Size ? (LineOffsets[n + 1] - 1) : EndOffset); } + void append(const char* base, int old_size, int new_size); +}; + +// Helper: ImGuiStorage +IMGUI_API ImGuiStoragePair* ImLowerBound(ImGuiStoragePair* in_begin, ImGuiStoragePair* in_end, ImGuiID key); + +//----------------------------------------------------------------------------- +// [SECTION] ImDrawList support +//----------------------------------------------------------------------------- + +// ImDrawList: Helper function to calculate a circle's segment count given its radius and a "maximum error" value. +// Estimation of number of circle segment based on error is derived using method described in https://stackoverflow.com/a/2244088/15194693 +// Number of segments (N) is calculated using equation: +// N = ceil ( pi / acos(1 - error / r) ) where r > 0, error <= r +// Our equation is significantly simpler that one in the post thanks for choosing segment that is +// perpendicular to X axis. Follow steps in the article from this starting condition and you will +// will get this result. +// +// Rendering circles with an odd number of segments, while mathematically correct will produce +// asymmetrical results on the raster grid. Therefore we're rounding N to next even number (7->8, 8->8, 9->10 etc.) +#define IM_ROUNDUP_TO_EVEN(_V) ((((_V) + 1) / 2) * 2) +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN 4 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX 512 +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(_RAD,_MAXERROR) ImClamp(IM_ROUNDUP_TO_EVEN((int)ImCeil(IM_PI / ImAcos(1 - ImMin((_MAXERROR), (_RAD)) / (_RAD)))), IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MIN, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX) + +// Raw equation from IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC rewritten for 'r' and 'error'. +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_R(_N,_MAXERROR) ((_MAXERROR) / (1 - ImCos(IM_PI / ImMax((float)(_N), IM_PI)))) +#define IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC_ERROR(_N,_RAD) ((1 - ImCos(IM_PI / ImMax((float)(_N), IM_PI))) / (_RAD)) + +// ImDrawList: Lookup table size for adaptive arc drawing, cover full circle. +#ifndef IM_DRAWLIST_ARCFAST_TABLE_SIZE +#define IM_DRAWLIST_ARCFAST_TABLE_SIZE 48 // Number of samples in lookup table. +#endif +#define IM_DRAWLIST_ARCFAST_SAMPLE_MAX IM_DRAWLIST_ARCFAST_TABLE_SIZE // Sample index _PathArcToFastEx() for 360 angle. + +// Data shared between all ImDrawList instances +// Conceptually this could have been called e.g. ImDrawListSharedContext +// Typically one ImGui context would create and maintain one of this. +// You may want to create your own instance of you try to ImDrawList completely without ImGui. In that case, watch out for future changes to this structure. +struct IMGUI_API ImDrawListSharedData +{ + ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas + const ImVec4* TexUvLines; // UV of anti-aliased lines in the atlas + ImFont* Font; // Current/default font (optional, for simplified AddText overload) + float FontSize; // Current/default font size (optional, for simplified AddText overload) + float FontScale; // Current/default font scale (== FontSize / Font->FontSize) + float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() + float CircleSegmentMaxError; // Number of circle segments to use per pixel of radius for AddCircle() etc + float InitialFringeScale; // Initial scale to apply to AA fringe + ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards) + ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() + ImVector TempBuffer; // Temporary write buffer + + // Lookup tables + ImVec2 ArcFastVtx[IM_DRAWLIST_ARCFAST_TABLE_SIZE]; // Sample points on the quarter of the circle. + float ArcFastRadiusCutoff; // Cutoff radius after which arc drawing will fallback to slower PathArcTo() + ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead) + + ImDrawListSharedData(); + void SetCircleTessellationMaxError(float max_error); +}; + +struct ImDrawDataBuilder +{ + ImVector* Layers[2]; // Pointers to global layers for: regular, tooltip. LayersP[0] is owned by DrawData. + ImVector LayerData1; + + ImDrawDataBuilder() { memset(this, 0, sizeof(*this)); } }; //----------------------------------------------------------------------------- -// Misc data structures +// [SECTION] Style support //----------------------------------------------------------------------------- -enum ImGuiButtonFlags_ +struct ImGuiStyleVarInfo { - ImGuiButtonFlags_None = 0, - ImGuiButtonFlags_Repeat = 1 << 0, // hold to repeat - ImGuiButtonFlags_PressedOnClickRelease = 1 << 1, // [Default] return true on click + release on same item - ImGuiButtonFlags_PressedOnClick = 1 << 2, // return true on click (default requires click+release) - ImGuiButtonFlags_PressedOnRelease = 1 << 3, // return true on release (default requires click+release) - ImGuiButtonFlags_PressedOnDoubleClick = 1 << 4, // return true on double-click (default requires click+release) - ImGuiButtonFlags_FlattenChildren = 1 << 5, // allow interactions even if a child window is overlapping - ImGuiButtonFlags_AllowItemOverlap = 1 << 6, // require previous frame HoveredId to either match id or be null before being usable, use along with SetItemAllowOverlap() - ImGuiButtonFlags_DontClosePopups = 1 << 7, // disable automatically closing parent popup on press // [UNUSED] - ImGuiButtonFlags_Disabled = 1 << 8, // disable interactions - ImGuiButtonFlags_AlignTextBaseLine = 1 << 9, // vertically align button to match text baseline - ButtonEx() only // FIXME: Should be removed and handled by SmallButton(), not possible currently because of DC.CursorPosPrevLine - ImGuiButtonFlags_NoKeyModifiers = 1 << 10, // disable interaction if a key modifier is held - ImGuiButtonFlags_NoHoldingActiveID = 1 << 11, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) - ImGuiButtonFlags_PressedOnDragDropHold = 1 << 12, // press when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) - ImGuiButtonFlags_NoNavFocus = 1 << 13, // don't override navigation focus when activated - ImGuiButtonFlags_NoHoveredOnNav = 1 << 14 // don't report as hovered when navigated on + ImU32 Count : 8; // 1+ + ImGuiDataType DataType : 8; + ImU32 Offset : 16; // Offset in parent structure + void* GetVarPtr(void* parent) const { return (void*)((unsigned char*)parent + Offset); } }; -enum ImGuiSliderFlags_ +// Stacked color modifier, backup of modified data so we can restore it +struct ImGuiColorMod { - ImGuiSliderFlags_None = 0, - ImGuiSliderFlags_Vertical = 1 << 0 + ImGuiCol Col; + ImVec4 BackupValue; }; -enum ImGuiDragFlags_ +// Stacked style modifier, backup of modified data so we can restore it. Data type inferred from the variable. +struct ImGuiStyleMod { - ImGuiDragFlags_None = 0, - ImGuiDragFlags_Vertical = 1 << 0 + ImGuiStyleVar VarIdx; + union { int BackupInt[2]; float BackupFloat[2]; }; + ImGuiStyleMod(ImGuiStyleVar idx, int v) { VarIdx = idx; BackupInt[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, float v) { VarIdx = idx; BackupFloat[0] = v; } + ImGuiStyleMod(ImGuiStyleVar idx, ImVec2 v) { VarIdx = idx; BackupFloat[0] = v.x; BackupFloat[1] = v.y; } }; -enum ImGuiColumnsFlags_ +//----------------------------------------------------------------------------- +// [SECTION] Data types support +//----------------------------------------------------------------------------- + +struct ImGuiDataTypeStorage { - // Default: 0 - ImGuiColumnsFlags_None = 0, - ImGuiColumnsFlags_NoBorder = 1 << 0, // Disable column dividers - ImGuiColumnsFlags_NoResize = 1 << 1, // Disable resizing columns when clicking on the dividers - ImGuiColumnsFlags_NoPreserveWidths = 1 << 2, // Disable column width preservation when adjusting columns - ImGuiColumnsFlags_NoForceWithinWindow = 1 << 3, // Disable forcing columns to fit within window - ImGuiColumnsFlags_GrowParentContentsSize= 1 << 4 // (WIP) Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. + ImU8 Data[8]; // Opaque storage to fit any data up to ImGuiDataType_COUNT +}; + +// Type information associated to one ImGuiDataType. Retrieve with DataTypeGetInfo(). +struct ImGuiDataTypeInfo +{ + size_t Size; // Size in bytes + const char* Name; // Short descriptive name for the type, for debugging + const char* PrintFmt; // Default printf format for the type + const char* ScanFmt; // Default scanf format for the type +}; + +// Extend ImGuiDataType_ +enum ImGuiDataTypePrivate_ +{ + ImGuiDataType_Pointer = ImGuiDataType_COUNT, + ImGuiDataType_ID, +}; + +//----------------------------------------------------------------------------- +// [SECTION] Widgets support: flags, enums, data structures +//----------------------------------------------------------------------------- + +// Extend ImGuiItemFlags +// - input: PushItemFlag() manipulates g.CurrentItemFlags, g.NextItemData.ItemFlags, ItemAdd() calls may add extra flags too. +// - output: stored in g.LastItemData.ItemFlags +enum ImGuiItemFlagsPrivate_ +{ + // Controlled by user + ImGuiItemFlags_Disabled = 1 << 10, // false // Disable interactions (DOES NOT affect visuals. DO NOT mix direct use of this with BeginDisabled(). See BeginDisabled()/EndDisabled() for full disable feature, and github #211). + ImGuiItemFlags_ReadOnly = 1 << 11, // false // [ALPHA] Allow hovering interactions but underlying value is not changed. + ImGuiItemFlags_MixedValue = 1 << 12, // false // [BETA] Represent a mixed/indeterminate value, generally multi-selection where values differ. Currently only supported by Checkbox() (later should support all sorts of widgets) + ImGuiItemFlags_NoWindowHoverableCheck = 1 << 13, // false // Disable hoverable check in ItemHoverable() + ImGuiItemFlags_AllowOverlap = 1 << 14, // false // Allow being overlapped by another widget. Not-hovered to Hovered transition deferred by a frame. + ImGuiItemFlags_NoNavDisableMouseHover = 1 << 15, // false // Nav keyboard/gamepad mode doesn't disable hover highlight (behave as if NavHighlightItemUnderNav==false). + ImGuiItemFlags_NoMarkEdited = 1 << 16, // false // Skip calling MarkItemEdited() + + // Controlled by widget code + ImGuiItemFlags_Inputable = 1 << 20, // false // [WIP] Auto-activate input mode when tab focused. Currently only used and supported by a few items before it becomes a generic feature. + ImGuiItemFlags_HasSelectionUserData = 1 << 21, // false // Set by SetNextItemSelectionUserData() + ImGuiItemFlags_IsMultiSelect = 1 << 22, // false // Set by SetNextItemSelectionUserData() + + ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups, // Please don't change, use PushItemFlag() instead. + + // Obsolete + //ImGuiItemFlags_SelectableDontClosePopup = !ImGuiItemFlags_AutoClosePopups, // Can't have a redirect as we inverted the behavior +}; + +// Status flags for an already submitted item +// - output: stored in g.LastItemData.StatusFlags +enum ImGuiItemStatusFlags_ +{ + ImGuiItemStatusFlags_None = 0, + ImGuiItemStatusFlags_HoveredRect = 1 << 0, // Mouse position is within item rectangle (does NOT mean that the window is in correct z-order and can be hovered!, this is only one part of the most-common IsItemHovered test) + ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, // g.LastItemData.DisplayRect is valid + ImGuiItemStatusFlags_Edited = 1 << 2, // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) + ImGuiItemStatusFlags_ToggledSelection = 1 << 3, // Set when Selectable(), TreeNode() reports toggling a selection. We can't report "Selected", only state changes, in order to easily handle clipping with less issues. + ImGuiItemStatusFlags_ToggledOpen = 1 << 4, // Set when TreeNode() reports toggling their open state. + ImGuiItemStatusFlags_HasDeactivated = 1 << 5, // Set if the widget/group is able to provide data for the ImGuiItemStatusFlags_Deactivated flag. + ImGuiItemStatusFlags_Deactivated = 1 << 6, // Only valid if ImGuiItemStatusFlags_HasDeactivated is set. + ImGuiItemStatusFlags_HoveredWindow = 1 << 7, // Override the HoveredWindow test to allow cross-window hover testing. + ImGuiItemStatusFlags_Visible = 1 << 8, // [WIP] Set when item is overlapping the current clipping rectangle (Used internally. Please don't use yet: API/system will change as we refactor Itemadd()). + ImGuiItemStatusFlags_HasClipRect = 1 << 9, // g.LastItemData.ClipRect is valid. + ImGuiItemStatusFlags_HasShortcut = 1 << 10, // g.LastItemData.Shortcut valid. Set by SetNextItemShortcut() -> ItemAdd(). + + // Additional status + semantic for ImGuiTestEngine +#ifdef IMGUI_ENABLE_TEST_ENGINE + ImGuiItemStatusFlags_Openable = 1 << 20, // Item is an openable (e.g. TreeNode) + ImGuiItemStatusFlags_Opened = 1 << 21, // Opened status + ImGuiItemStatusFlags_Checkable = 1 << 22, // Item is a checkable (e.g. CheckBox, MenuItem) + ImGuiItemStatusFlags_Checked = 1 << 23, // Checked status + ImGuiItemStatusFlags_Inputable = 1 << 24, // Item is a text-inputable (e.g. InputText, SliderXXX, DragXXX) +#endif +}; + +// Extend ImGuiHoveredFlags_ +enum ImGuiHoveredFlagsPrivate_ +{ + ImGuiHoveredFlags_DelayMask_ = ImGuiHoveredFlags_DelayNone | ImGuiHoveredFlags_DelayShort | ImGuiHoveredFlags_DelayNormal | ImGuiHoveredFlags_NoSharedDelay, + ImGuiHoveredFlags_AllowedMaskForIsWindowHovered = ImGuiHoveredFlags_ChildWindows | ImGuiHoveredFlags_RootWindow | ImGuiHoveredFlags_AnyWindow | ImGuiHoveredFlags_NoPopupHierarchy | ImGuiHoveredFlags_DockHierarchy | ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary, + ImGuiHoveredFlags_AllowedMaskForIsItemHovered = ImGuiHoveredFlags_AllowWhenBlockedByPopup | ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlapped | ImGuiHoveredFlags_AllowWhenDisabled | ImGuiHoveredFlags_NoNavOverride | ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_Stationary | ImGuiHoveredFlags_DelayMask_, +}; + +// Extend ImGuiInputTextFlags_ +enum ImGuiInputTextFlagsPrivate_ +{ + // [Internal] + ImGuiInputTextFlags_Multiline = 1 << 26, // For internal use by InputTextMultiline() + ImGuiInputTextFlags_MergedItem = 1 << 27, // For internal use by TempInputText(), will skip calling ItemAdd(). Require bounding-box to strictly match. + ImGuiInputTextFlags_LocalizeDecimalPoint= 1 << 28, // For internal use by InputScalar() and TempInputScalar() +}; + +// Extend ImGuiButtonFlags_ +enum ImGuiButtonFlagsPrivate_ +{ + ImGuiButtonFlags_PressedOnClick = 1 << 4, // return true on click (mouse down event) + ImGuiButtonFlags_PressedOnClickRelease = 1 << 5, // [Default] return true on click + release on same item <-- this is what the majority of Button are using + ImGuiButtonFlags_PressedOnClickReleaseAnywhere = 1 << 6, // return true on click + release even if the release event is not done while hovering the item + ImGuiButtonFlags_PressedOnRelease = 1 << 7, // return true on release (default requires click+release) + ImGuiButtonFlags_PressedOnDoubleClick = 1 << 8, // return true on double-click (default requires click+release) + ImGuiButtonFlags_PressedOnDragDropHold = 1 << 9, // return true when held into while we are drag and dropping another item (used by e.g. tree nodes, collapsing headers) + //ImGuiButtonFlags_Repeat = 1 << 10, // hold to repeat -> use ImGuiItemFlags_ButtonRepeat instead. + ImGuiButtonFlags_FlattenChildren = 1 << 11, // allow interactions even if a child window is overlapping + ImGuiButtonFlags_AllowOverlap = 1 << 12, // require previous frame HoveredId to either match id or be null before being usable. + //ImGuiButtonFlags_DontClosePopups = 1 << 13, // disable automatically closing parent popup on press + //ImGuiButtonFlags_Disabled = 1 << 14, // disable interactions -> use BeginDisabled() or ImGuiItemFlags_Disabled + ImGuiButtonFlags_AlignTextBaseLine = 1 << 15, // vertically align button to match text baseline - ButtonEx() only // FIXME: Should be removed and handled by SmallButton(), not possible currently because of DC.CursorPosPrevLine + ImGuiButtonFlags_NoKeyModsAllowed = 1 << 16, // disable mouse interaction if a key modifier is held + ImGuiButtonFlags_NoHoldingActiveId = 1 << 17, // don't set ActiveId while holding the mouse (ImGuiButtonFlags_PressedOnClick only) + ImGuiButtonFlags_NoNavFocus = 1 << 18, // don't override navigation focus when activated (FIXME: this is essentially used every time an item uses ImGuiItemFlags_NoNav, but because legacy specs don't requires LastItemData to be set ButtonBehavior(), we can't poll g.LastItemData.ItemFlags) + ImGuiButtonFlags_NoHoveredOnFocus = 1 << 19, // don't report as hovered when nav focus is on this item + ImGuiButtonFlags_NoSetKeyOwner = 1 << 20, // don't set key/input owner on the initial click (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) + ImGuiButtonFlags_NoTestKeyOwner = 1 << 21, // don't test key/input owner when polling the key (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) + ImGuiButtonFlags_PressedOnMask_ = ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_PressedOnDragDropHold, + ImGuiButtonFlags_PressedOnDefault_ = ImGuiButtonFlags_PressedOnClickRelease, +}; + +// Extend ImGuiComboFlags_ +enum ImGuiComboFlagsPrivate_ +{ + ImGuiComboFlags_CustomPreview = 1 << 20, // enable BeginComboPreview() +}; + +// Extend ImGuiSliderFlags_ +enum ImGuiSliderFlagsPrivate_ +{ + ImGuiSliderFlags_Vertical = 1 << 20, // Should this slider be orientated vertically? + ImGuiSliderFlags_ReadOnly = 1 << 21, // Consider using g.NextItemData.ItemFlags |= ImGuiItemFlags_ReadOnly instead. }; // Extend ImGuiSelectableFlags_ enum ImGuiSelectableFlagsPrivate_ { // NB: need to be in sync with last value of ImGuiSelectableFlags_ - ImGuiSelectableFlags_NoHoldingActiveID = 1 << 20, - ImGuiSelectableFlags_PressedOnClick = 1 << 21, - ImGuiSelectableFlags_PressedOnRelease = 1 << 22, - ImGuiSelectableFlags_DrawFillAvailWidth = 1 << 23, // FIXME: We may be able to remove this (added in 6251d379 for menus) - ImGuiSelectableFlags_DrawHoveredWhenHeld= 1 << 24, // Always show active when held, even is not hovered. This concept could probably be renamed/formalized somehow. - ImGuiSelectableFlags_SetNavIdOnHover = 1 << 25 + ImGuiSelectableFlags_NoHoldingActiveID = 1 << 20, + ImGuiSelectableFlags_SelectOnNav = 1 << 21, // (WIP) Auto-select when moved into. This is not exposed in public API as to handle multi-select and modifiers we will need user to explicitly control focus scope. May be replaced with a BeginSelection() API. + ImGuiSelectableFlags_SelectOnClick = 1 << 22, // Override button behavior to react on Click (default is Click+Release) + ImGuiSelectableFlags_SelectOnRelease = 1 << 23, // Override button behavior to react on Release (default is Click+Release) + ImGuiSelectableFlags_SpanAvailWidth = 1 << 24, // Span all avail width even if we declared less for layout purpose. FIXME: We may be able to remove this (added in 6251d379, 2bcafc86 for menus) + ImGuiSelectableFlags_SetNavIdOnHover = 1 << 25, // Set Nav/Focus ID on mouse hover (used by MenuItem) + ImGuiSelectableFlags_NoPadWithHalfSpacing = 1 << 26, // Disable padding each side with ItemSpacing * 0.5f + ImGuiSelectableFlags_NoSetKeyOwner = 1 << 27, // Don't set key/input owner on the initial click (note: mouse buttons are keys! often, the key in question will be ImGuiKey_MouseLeft!) }; // Extend ImGuiTreeNodeFlags_ enum ImGuiTreeNodeFlagsPrivate_ { - ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 20 + ImGuiTreeNodeFlags_ClipLabelForTrailingButton = 1 << 28,// FIXME-WIP: Hard-coded for CollapsingHeader() + ImGuiTreeNodeFlags_UpsideDownArrow = 1 << 29,// FIXME-WIP: Turn Down arrow into an Up arrow, for reversed trees (#6517) + ImGuiTreeNodeFlags_OpenOnMask_ = ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_OpenOnArrow, }; enum ImGuiSeparatorFlags_ { - ImGuiSeparatorFlags_None = 0, - ImGuiSeparatorFlags_Horizontal = 1 << 0, // Axis default to current layout type, so generally Horizontal unless e.g. in a menu bar - ImGuiSeparatorFlags_Vertical = 1 << 1, - ImGuiSeparatorFlags_SpanAllColumns = 1 << 2 + ImGuiSeparatorFlags_None = 0, + ImGuiSeparatorFlags_Horizontal = 1 << 0, // Axis default to current layout type, so generally Horizontal unless e.g. in a menu bar + ImGuiSeparatorFlags_Vertical = 1 << 1, + ImGuiSeparatorFlags_SpanAllColumns = 1 << 2, // Make separator cover all columns of a legacy Columns() set. }; -// Transient per-window flags, reset at the beginning of the frame. For child window, inherited from parent on first Begin(). -// This is going to be exposed in imgui.h when stabilized enough. -enum ImGuiItemFlags_ +// Flags for FocusWindow(). This is not called ImGuiFocusFlags to avoid confusion with public-facing ImGuiFocusedFlags. +// FIXME: Once we finishing replacing more uses of GetTopMostPopupModal()+IsWindowWithinBeginStackOf() +// and FindBlockingModal() with this, we may want to change the flag to be opt-out instead of opt-in. +enum ImGuiFocusRequestFlags_ { - ImGuiItemFlags_NoTabStop = 1 << 0, // false - ImGuiItemFlags_ButtonRepeat = 1 << 1, // false // Button() will return true multiple times based on io.KeyRepeatDelay and io.KeyRepeatRate settings. - ImGuiItemFlags_Disabled = 1 << 2, // false // [BETA] Disable interactions but doesn't affect visuals yet. See github.com/ocornut/imgui/issues/211 - ImGuiItemFlags_NoNav = 1 << 3, // false - ImGuiItemFlags_NoNavDefaultFocus = 1 << 4, // false - ImGuiItemFlags_SelectableDontClosePopup = 1 << 5, // false // MenuItem/Selectable() automatically closes current Popup window - ImGuiItemFlags_MixedValue = 1 << 6, // false // [BETA] Represent a mixed/indeterminate value, generally multi-selection where values differ. Currently only supported by Checkbox() (later should support all sorts of widgets) - ImGuiItemFlags_Default_ = 0 -}; - -// Storage for LastItem data -enum ImGuiItemStatusFlags_ -{ - ImGuiItemStatusFlags_None = 0, - ImGuiItemStatusFlags_HoveredRect = 1 << 0, - ImGuiItemStatusFlags_HasDisplayRect = 1 << 1, - ImGuiItemStatusFlags_Edited = 1 << 2, // Value exposed by item was edited in the current frame (should match the bool return value of most widgets) - ImGuiItemStatusFlags_ToggledSelection = 1 << 3, // Set when Selectable(), TreeNode() reports toggling a selection. We can't report "Selected" because reporting the change allows us to handle clipping with less issues. - ImGuiItemStatusFlags_HasDeactivated = 1 << 4, // Set if the widget/group is able to provide data for the ImGuiItemStatusFlags_Deactivated flag. - ImGuiItemStatusFlags_Deactivated = 1 << 5 // Only valid if ImGuiItemStatusFlags_HasDeactivated is set. - -#ifdef IMGUI_ENABLE_TEST_ENGINE - , // [imgui_tests only] - ImGuiItemStatusFlags_Openable = 1 << 10, // - ImGuiItemStatusFlags_Opened = 1 << 11, // - ImGuiItemStatusFlags_Checkable = 1 << 12, // - ImGuiItemStatusFlags_Checked = 1 << 13 // -#endif + ImGuiFocusRequestFlags_None = 0, + ImGuiFocusRequestFlags_RestoreFocusedChild = 1 << 0, // Find last focused child (if any) and focus it instead. + ImGuiFocusRequestFlags_UnlessBelowModal = 1 << 1, // Do not set focus if the window is below a modal. }; enum ImGuiTextFlags_ { - ImGuiTextFlags_None = 0, - ImGuiTextFlags_NoWidthForLargeClippedText = 1 << 0 + ImGuiTextFlags_None = 0, + ImGuiTextFlags_NoWidthForLargeClippedText = 1 << 0, +}; + +enum ImGuiTooltipFlags_ +{ + ImGuiTooltipFlags_None = 0, + ImGuiTooltipFlags_OverridePrevious = 1 << 1, // Clear/ignore previously submitted tooltip (defaults to append) }; // FIXME: this is in development, not exposed/functional as a generic feature yet. @@ -427,13 +1048,16 @@ enum ImGuiLayoutType_ ImGuiLayoutType_Vertical = 1 }; -enum ImGuiLogType +// Flags for LogBegin() text capturing function +enum ImGuiLogFlags_ { - ImGuiLogType_None = 0, - ImGuiLogType_TTY, - ImGuiLogType_File, - ImGuiLogType_Buffer, - ImGuiLogType_Clipboard + ImGuiLogFlags_None = 0, + + ImGuiLogFlags_OutputTTY = 1 << 0, + ImGuiLogFlags_OutputFile = 1 << 1, + ImGuiLogFlags_OutputBuffer = 1 << 2, + ImGuiLogFlags_OutputClipboard = 1 << 3, + ImGuiLogFlags_OutputMask_ = ImGuiLogFlags_OutputTTY | ImGuiLogFlags_OutputFile | ImGuiLogFlags_OutputBuffer | ImGuiLogFlags_OutputClipboard, }; // X/Y enums are fixed to 0/1 so they may be used to index ImVec2 @@ -447,336 +1071,136 @@ enum ImGuiAxis enum ImGuiPlotType { ImGuiPlotType_Lines, - ImGuiPlotType_Histogram + ImGuiPlotType_Histogram, }; -enum ImGuiInputSource +// Storage data for BeginComboPreview()/EndComboPreview() +struct IMGUI_API ImGuiComboPreviewData { - ImGuiInputSource_None = 0, - ImGuiInputSource_Mouse, - ImGuiInputSource_Nav, - ImGuiInputSource_NavKeyboard, // Only used occasionally for storage, not tested/handled by most code - ImGuiInputSource_NavGamepad, // " - ImGuiInputSource_COUNT -}; + ImRect PreviewRect; + ImVec2 BackupCursorPos; + ImVec2 BackupCursorMaxPos; + ImVec2 BackupCursorPosPrevLine; + float BackupPrevLineTextBaseOffset; + ImGuiLayoutType BackupLayout; -// FIXME-NAV: Clarify/expose various repeat delay/rate -enum ImGuiInputReadMode -{ - ImGuiInputReadMode_Down, - ImGuiInputReadMode_Pressed, - ImGuiInputReadMode_Released, - ImGuiInputReadMode_Repeat, - ImGuiInputReadMode_RepeatSlow, - ImGuiInputReadMode_RepeatFast -}; - -enum ImGuiNavHighlightFlags_ -{ - ImGuiNavHighlightFlags_None = 0, - ImGuiNavHighlightFlags_TypeDefault = 1 << 0, - ImGuiNavHighlightFlags_TypeThin = 1 << 1, - ImGuiNavHighlightFlags_AlwaysDraw = 1 << 2, // Draw rectangular highlight if (g.NavId == id) _even_ when using the mouse. - ImGuiNavHighlightFlags_NoRounding = 1 << 3 -}; - -enum ImGuiNavDirSourceFlags_ -{ - ImGuiNavDirSourceFlags_None = 0, - ImGuiNavDirSourceFlags_Keyboard = 1 << 0, - ImGuiNavDirSourceFlags_PadDPad = 1 << 1, - ImGuiNavDirSourceFlags_PadLStick = 1 << 2 -}; - -enum ImGuiNavMoveFlags_ -{ - ImGuiNavMoveFlags_None = 0, - ImGuiNavMoveFlags_LoopX = 1 << 0, // On failed request, restart from opposite side - ImGuiNavMoveFlags_LoopY = 1 << 1, - ImGuiNavMoveFlags_WrapX = 1 << 2, // On failed request, request from opposite side one line down (when NavDir==right) or one line up (when NavDir==left) - ImGuiNavMoveFlags_WrapY = 1 << 3, // This is not super useful for provided for completeness - ImGuiNavMoveFlags_AllowCurrentNavId = 1 << 4, // Allow scoring and considering the current NavId as a move target candidate. This is used when the move source is offset (e.g. pressing PageDown actually needs to send a Up move request, if we are pressing PageDown from the bottom-most item we need to stay in place) - ImGuiNavMoveFlags_AlsoScoreVisibleSet = 1 << 5, // Store alternate result in NavMoveResultLocalVisibleSet that only comprise elements that are already fully visible. - ImGuiNavMoveFlags_ScrollToEdge = 1 << 6 -}; - -enum ImGuiNavForward -{ - ImGuiNavForward_None, - ImGuiNavForward_ForwardQueued, - ImGuiNavForward_ForwardActive -}; - -enum ImGuiNavLayer -{ - ImGuiNavLayer_Main = 0, // Main scrolling layer - ImGuiNavLayer_Menu = 1, // Menu layer (access with Alt/ImGuiNavInput_Menu) - ImGuiNavLayer_COUNT -}; - -enum ImGuiPopupPositionPolicy -{ - ImGuiPopupPositionPolicy_Default, - ImGuiPopupPositionPolicy_ComboBox -}; - -// 1D vector (this odd construct is used to facilitate the transition between 1D and 2D, and the maintenance of some branches/patches) -struct ImVec1 -{ - float x; - ImVec1() { x = 0.0f; } - ImVec1(float _x) { x = _x; } -}; - -// 2D vector (half-size integer) -struct ImVec2ih -{ - short x, y; - ImVec2ih() { x = y = 0; } - ImVec2ih(short _x, short _y) { x = _x; y = _y; } -}; - -// 2D axis aligned bounding-box -// NB: we can't rely on ImVec2 math operators being available here -struct IMGUI_API ImRect -{ - ImVec2 Min; // Upper-left - ImVec2 Max; // Lower-right - - ImRect() : Min(FLT_MAX,FLT_MAX), Max(-FLT_MAX,-FLT_MAX) {} - ImRect(const ImVec2& min, const ImVec2& max) : Min(min), Max(max) {} - ImRect(const ImVec4& v) : Min(v.x, v.y), Max(v.z, v.w) {} - ImRect(float x1, float y1, float x2, float y2) : Min(x1, y1), Max(x2, y2) {} - - ImVec2 GetCenter() const { return ImVec2((Min.x + Max.x) * 0.5f, (Min.y + Max.y) * 0.5f); } - ImVec2 GetSize() const { return ImVec2(Max.x - Min.x, Max.y - Min.y); } - float GetWidth() const { return Max.x - Min.x; } - float GetHeight() const { return Max.y - Min.y; } - ImVec2 GetTL() const { return Min; } // Top-left - ImVec2 GetTR() const { return ImVec2(Max.x, Min.y); } // Top-right - ImVec2 GetBL() const { return ImVec2(Min.x, Max.y); } // Bottom-left - ImVec2 GetBR() const { return Max; } // Bottom-right - bool Contains(const ImVec2& p) const { return p.x >= Min.x && p.y >= Min.y && p.x < Max.x && p.y < Max.y; } - bool Contains(const ImRect& r) const { return r.Min.x >= Min.x && r.Min.y >= Min.y && r.Max.x <= Max.x && r.Max.y <= Max.y; } - bool Overlaps(const ImRect& r) const { return r.Min.y < Max.y && r.Max.y > Min.y && r.Min.x < Max.x && r.Max.x > Min.x; } - void Add(const ImVec2& p) { if (Min.x > p.x) Min.x = p.x; if (Min.y > p.y) Min.y = p.y; if (Max.x < p.x) Max.x = p.x; if (Max.y < p.y) Max.y = p.y; } - void Add(const ImRect& r) { if (Min.x > r.Min.x) Min.x = r.Min.x; if (Min.y > r.Min.y) Min.y = r.Min.y; if (Max.x < r.Max.x) Max.x = r.Max.x; if (Max.y < r.Max.y) Max.y = r.Max.y; } - void Expand(const float amount) { Min.x -= amount; Min.y -= amount; Max.x += amount; Max.y += amount; } - void Expand(const ImVec2& amount) { Min.x -= amount.x; Min.y -= amount.y; Max.x += amount.x; Max.y += amount.y; } - void Translate(const ImVec2& d) { Min.x += d.x; Min.y += d.y; Max.x += d.x; Max.y += d.y; } - void TranslateX(float dx) { Min.x += dx; Max.x += dx; } - void TranslateY(float dy) { Min.y += dy; Max.y += dy; } - void ClipWith(const ImRect& r) { Min = ImMax(Min, r.Min); Max = ImMin(Max, r.Max); } // Simple version, may lead to an inverted rectangle, which is fine for Contains/Overlaps test but not for display. - void ClipWithFull(const ImRect& r) { Min = ImClamp(Min, r.Min, r.Max); Max = ImClamp(Max, r.Min, r.Max); } // Full version, ensure both points are fully clipped. - void Floor() { Min.x = (float)(int)Min.x; Min.y = (float)(int)Min.y; Max.x = (float)(int)Max.x; Max.y = (float)(int)Max.y; } - bool IsInverted() const { return Min.x > Max.x || Min.y > Max.y; } -}; - -// Type information associated to one ImGuiDataType. Retrieve with DataTypeGetInfo(). -struct ImGuiDataTypeInfo -{ - size_t Size; // Size in byte - const char* PrintFmt; // Default printf format for the type - const char* ScanFmt; // Default scanf format for the type -}; - -// Stacked color modifier, backup of modified data so we can restore it -struct ImGuiColorMod -{ - ImGuiCol Col; - ImVec4 BackupValue; -}; - -// Stacked style modifier, backup of modified data so we can restore it. Data type inferred from the variable. -struct ImGuiStyleMod -{ - ImGuiStyleVar VarIdx; - union { int BackupInt[2]; float BackupFloat[2]; }; - ImGuiStyleMod(ImGuiStyleVar idx, int v) { VarIdx = idx; BackupInt[0] = v; } - ImGuiStyleMod(ImGuiStyleVar idx, float v) { VarIdx = idx; BackupFloat[0] = v; } - ImGuiStyleMod(ImGuiStyleVar idx, ImVec2 v) { VarIdx = idx; BackupFloat[0] = v.x; BackupFloat[1] = v.y; } + ImGuiComboPreviewData() { memset(this, 0, sizeof(*this)); } }; // Stacked storage data for BeginGroup()/EndGroup() -struct ImGuiGroupData +struct IMGUI_API ImGuiGroupData { + ImGuiID WindowID; ImVec2 BackupCursorPos; ImVec2 BackupCursorMaxPos; + ImVec2 BackupCursorPosPrevLine; ImVec1 BackupIndent; ImVec1 BackupGroupOffset; ImVec2 BackupCurrLineSize; float BackupCurrLineTextBaseOffset; ImGuiID BackupActiveIdIsAlive; - bool BackupActiveIdPreviousFrameIsAlive; + bool BackupDeactivatedIdIsAlive; + bool BackupHoveredIdIsAlive; + bool BackupIsSameLine; bool EmitItem; }; // Simple column measurement, currently used for MenuItem() only.. This is very short-sighted/throw-away code and NOT a generic helper. struct IMGUI_API ImGuiMenuColumns { - float Spacing; - float Width, NextWidth; - float Pos[3], NextWidths[3]; + ImU32 TotalWidth; + ImU32 NextTotalWidth; + ImU16 Spacing; + ImU16 OffsetIcon; // Always zero for now + ImU16 OffsetLabel; // Offsets are locked in Update() + ImU16 OffsetShortcut; + ImU16 OffsetMark; + ImU16 Widths[4]; // Width of: Icon, Label, Shortcut, Mark (accumulators for current frame) - ImGuiMenuColumns(); - void Update(int count, float spacing, bool clear); - float DeclColumns(float w0, float w1, float w2); - float CalcExtraSpace(float avail_w); + ImGuiMenuColumns() { memset(this, 0, sizeof(*this)); } + void Update(float spacing, bool window_reappearing); + float DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark); + void CalcNextTotalWidth(bool update_offsets); }; +// Internal temporary state for deactivating InputText() instances. +struct IMGUI_API ImGuiInputTextDeactivatedState +{ + ImGuiID ID; // widget id owning the text state (which just got deactivated) + ImVector TextA; // text buffer + + ImGuiInputTextDeactivatedState() { memset(this, 0, sizeof(*this)); } + void ClearFreeMemory() { ID = 0; TextA.clear(); } +}; + +// Forward declare imstb_textedit.h structure + make its main configuration define accessible +#undef IMSTB_TEXTEDIT_STRING +#undef IMSTB_TEXTEDIT_CHARTYPE +#define IMSTB_TEXTEDIT_STRING ImGuiInputTextState +#define IMSTB_TEXTEDIT_CHARTYPE char +#define IMSTB_TEXTEDIT_GETWIDTH_NEWLINE (-1.0f) +#define IMSTB_TEXTEDIT_UNDOSTATECOUNT 99 +#define IMSTB_TEXTEDIT_UNDOCHARCOUNT 999 +namespace ImStb { struct STB_TexteditState; } +typedef ImStb::STB_TexteditState ImStbTexteditState; + // Internal state of the currently focused/edited text input box +// For a given item ID, access with ImGui::GetInputTextState() struct IMGUI_API ImGuiInputTextState { + ImGuiContext* Ctx; // parent UI context (needs to be set explicitly by parent). + ImStbTexteditState* Stb; // State for stb_textedit.h + ImGuiInputTextFlags Flags; // copy of InputText() flags. may be used to check if e.g. ImGuiInputTextFlags_Password is set. ImGuiID ID; // widget id owning the text state - int CurLenW, CurLenA; // we need to maintain our buffer length in both UTF-8 and wchar format. UTF-8 len is valid even if TextA is not. - ImVector TextW; // edit buffer, we need to persist but can't guarantee the persistence of the user-provided buffer. so we copy into own buffer. - ImVector TextA; // temporary UTF8 buffer for callbacks and other operations. this is not updated in every code-path! size=capacity. - ImVector InitialTextA; // backup of end-user buffer at the time of focus (in UTF-8, unaltered) - bool TextAIsValid; // temporary UTF8 buffer is not initially valid before we make the widget active (until then we pull the data from user argument) - int BufCapacityA; // end-user buffer capacity - float ScrollX; // horizontal scrolling/offset - ImStb::STB_TexteditState Stb; // state for stb_textedit.h + int TextLen; // UTF-8 length of the string in TextA (in bytes) + const char* TextSrc; // == TextA.Data unless read-only, in which case == buf passed to InputText(). Field only set and valid _inside_ the call InputText() call. + ImVector TextA; // main UTF8 buffer. TextA.Size is a buffer size! Should always be >= buf_size passed by user (and of course >= CurLenA + 1). + ImVector TextToRevertTo; // value to revert to when pressing Escape = backup of end-user buffer at the time of focus (in UTF-8, unaltered) + ImVector CallbackTextBackup; // temporary storage for callback to support automatic reconcile of undo-stack + int BufCapacity; // end-user buffer capacity (include zero terminator) + ImVec2 Scroll; // horizontal offset (managed manually) + vertical scrolling (pulled from child window's own Scroll.y) float CursorAnim; // timer for cursor blink, reset on every user action so the cursor reappears immediately bool CursorFollow; // set when we want scrolling to follow the current cursor position (not always!) bool SelectedAllMouseLock; // after a double-click to select all, we ignore further mouse drags to update selection - ImGuiInputTextFlags UserFlags; // Temporarily set while we call user's callback - ImGuiInputTextCallback UserCallback; // " - void* UserCallbackData; // " + bool Edited; // edited this frame + bool WantReloadUserBuf; // force a reload of user buf so it may be modified externally. may be automatic in future version. + int ReloadSelectionStart; + int ReloadSelectionEnd; - ImGuiInputTextState() { memset(this, 0, sizeof(*this)); } - void ClearText() { CurLenW = CurLenA = 0; TextW[0] = 0; TextA[0] = 0; CursorClamp(); } - void ClearFreeMemory() { TextW.clear(); TextA.clear(); InitialTextA.clear(); } - int GetUndoAvailCount() const { return Stb.undostate.undo_point; } - int GetRedoAvailCount() const { return STB_TEXTEDIT_UNDOSTATECOUNT - Stb.undostate.redo_point; } + ImGuiInputTextState(); + ~ImGuiInputTextState(); + void ClearText() { TextLen = 0; TextA[0] = 0; CursorClamp(); } + void ClearFreeMemory() { TextA.clear(); TextToRevertTo.clear(); } void OnKeyPressed(int key); // Cannot be inline because we call in code in stb_textedit.h implementation + void OnCharPressed(unsigned int c); // Cursor & Selection - void CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking - void CursorClamp() { Stb.cursor = ImMin(Stb.cursor, CurLenW); Stb.select_start = ImMin(Stb.select_start, CurLenW); Stb.select_end = ImMin(Stb.select_end, CurLenW); } - bool HasSelection() const { return Stb.select_start != Stb.select_end; } - void ClearSelection() { Stb.select_start = Stb.select_end = Stb.cursor; } - void SelectAll() { Stb.select_start = 0; Stb.cursor = Stb.select_end = CurLenW; Stb.has_preferred_x = 0; } + void CursorAnimReset(); + void CursorClamp(); + bool HasSelection() const; + void ClearSelection(); + int GetCursorPos() const; + int GetSelectionStart() const; + int GetSelectionEnd() const; + void SelectAll(); + + // Reload user buf (WIP #2890) + // If you modify underlying user-passed const char* while active you need to call this (InputText V2 may lift this) + // strcpy(my_buf, "hello"); + // if (ImGuiInputTextState* state = ImGui::GetInputTextState(id)) // id may be ImGui::GetItemID() is last item + // state->ReloadUserBufAndSelectAll(); + void ReloadUserBufAndSelectAll(); + void ReloadUserBufAndKeepSelection(); + void ReloadUserBufAndMoveToEnd(); }; -// Windows data saved in imgui.ini file -struct ImGuiWindowSettings +enum ImGuiWindowRefreshFlags_ { - char* Name; - ImGuiID ID; - ImVec2ih Pos; - ImVec2ih Size; - bool Collapsed; - - ImGuiWindowSettings() { Name = NULL; ID = 0; Pos = Size = ImVec2ih(0, 0); Collapsed = false; } -}; - -struct ImGuiSettingsHandler -{ - const char* TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' - ImGuiID TypeHash; // == ImHashStr(TypeName) - void* (*ReadOpenFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name); // Read: Called when entering into a new ini entry e.g. "[Window][Name]" - void (*ReadLineFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line); // Read: Called for every line of text within an ini entry - void (*WriteAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf); // Write: Output every entries into 'out_buf' - void* UserData; - - ImGuiSettingsHandler() { memset(this, 0, sizeof(*this)); } -}; - -// Storage for current popup stack -struct ImGuiPopupData -{ - ImGuiID PopupId; // Set on OpenPopup() - ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() - ImGuiWindow* SourceWindow; // Set on OpenPopup() copy of NavWindow at the time of opening the popup - int OpenFrameCount; // Set on OpenPopup() - ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) - ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) - ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup - - ImGuiPopupData() { PopupId = 0; Window = SourceWindow = NULL; OpenFrameCount = -1; OpenParentId = 0; } -}; - -struct ImGuiColumnData -{ - float OffsetNorm; // Column start offset, normalized 0.0 (far left) -> 1.0 (far right) - float OffsetNormBeforeResize; - ImGuiColumnsFlags Flags; // Not exposed - ImRect ClipRect; - - ImGuiColumnData() { OffsetNorm = OffsetNormBeforeResize = 0.0f; Flags = ImGuiColumnsFlags_None; } -}; - -struct ImGuiColumns -{ - ImGuiID ID; - ImGuiColumnsFlags Flags; - bool IsFirstFrame; - bool IsBeingResized; - int Current; - int Count; - float OffMinX, OffMaxX; // Offsets from HostWorkRect.Min.x - float LineMinY, LineMaxY; - float HostCursorPosY; // Backup of CursorPos at the time of BeginColumns() - float HostCursorMaxPosX; // Backup of CursorMaxPos at the time of BeginColumns() - ImRect HostClipRect; // Backup of ClipRect at the time of BeginColumns() - ImRect HostWorkRect; // Backup of WorkRect at the time of BeginColumns() - ImVector Columns; - - ImGuiColumns() { Clear(); } - void Clear() - { - ID = 0; - Flags = ImGuiColumnsFlags_None; - IsFirstFrame = false; - IsBeingResized = false; - Current = 0; - Count = 1; - OffMinX = OffMaxX = 0.0f; - LineMinY = LineMaxY = 0.0f; - HostCursorPosY = 0.0f; - HostCursorMaxPosX = 0.0f; - Columns.clear(); - } -}; - -// Data shared between all ImDrawList instances -struct IMGUI_API ImDrawListSharedData -{ - ImVec2 TexUvWhitePixel; // UV of white pixel in the atlas - ImFont* Font; // Current/default font (optional, for simplified AddText overload) - float FontSize; // Current/default font size (optional, for simplified AddText overload) - float CurveTessellationTol; - ImVec4 ClipRectFullscreen; // Value for PushClipRectFullscreen() - ImDrawListFlags InitialFlags; // Initial flags at the beginning of the frame (it is possible to alter flags on a per-drawlist basis afterwards) - - // Const data - // FIXME: Bake rounded corners fill/borders in atlas - ImVec2 CircleVtx12[12]; - - ImDrawListSharedData(); -}; - -struct ImDrawDataBuilder -{ - ImVector Layers[2]; // Global layers for: regular, tooltip - - void Clear() { for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) Layers[n].resize(0); } - void ClearFreeMemory() { for (int n = 0; n < IM_ARRAYSIZE(Layers); n++) Layers[n].clear(); } - IMGUI_API void FlattenIntoSingleLayer(); -}; - -struct ImGuiNavMoveResult -{ - ImGuiID ID; // Best candidate - ImGuiID SelectScopeId;// Best candidate window current selectable group ID - ImGuiWindow* Window; // Best candidate window - float DistBox; // Best candidate box distance to current NavId - float DistCenter; // Best candidate center distance to current NavId - float DistAxial; - ImRect RectRel; // Best candidate bounding box in window relative space - - ImGuiNavMoveResult() { Clear(); } - void Clear() { ID = SelectScopeId = 0; Window = NULL; DistBox = DistCenter = DistAxial = FLT_MAX; RectRel = ImRect(); } + ImGuiWindowRefreshFlags_None = 0, + ImGuiWindowRefreshFlags_TryToAvoidRefresh = 1 << 0, // [EXPERIMENTAL] Try to keep existing contents, USER MUST NOT HONOR BEGIN() RETURNING FALSE AND NOT APPEND. + ImGuiWindowRefreshFlags_RefreshOnHover = 1 << 1, // [EXPERIMENTAL] Always refresh on hover + ImGuiWindowRefreshFlags_RefreshOnFocus = 1 << 2, // [EXPERIMENTAL] Always refresh on focus + // Refresh policy/frequency, Load Balancing etc. }; enum ImGuiNextWindowDataFlags_ @@ -788,216 +1212,1286 @@ enum ImGuiNextWindowDataFlags_ ImGuiNextWindowDataFlags_HasCollapsed = 1 << 3, ImGuiNextWindowDataFlags_HasSizeConstraint = 1 << 4, ImGuiNextWindowDataFlags_HasFocus = 1 << 5, - ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6 + ImGuiNextWindowDataFlags_HasBgAlpha = 1 << 6, + ImGuiNextWindowDataFlags_HasScroll = 1 << 7, + ImGuiNextWindowDataFlags_HasWindowFlags = 1 << 8, + ImGuiNextWindowDataFlags_HasChildFlags = 1 << 9, + ImGuiNextWindowDataFlags_HasRefreshPolicy = 1 << 10, + ImGuiNextWindowDataFlags_HasViewport = 1 << 11, + ImGuiNextWindowDataFlags_HasDock = 1 << 12, + ImGuiNextWindowDataFlags_HasWindowClass = 1 << 13, }; // Storage for SetNexWindow** functions struct ImGuiNextWindowData { - ImGuiNextWindowDataFlags Flags; + ImGuiNextWindowDataFlags HasFlags; + + // Members below are NOT cleared. Always rely on HasFlags. ImGuiCond PosCond; ImGuiCond SizeCond; ImGuiCond CollapsedCond; + ImGuiCond DockCond; ImVec2 PosVal; ImVec2 PosPivotVal; ImVec2 SizeVal; ImVec2 ContentSizeVal; + ImVec2 ScrollVal; + ImGuiWindowFlags WindowFlags; // Only honored by BeginTable() + ImGuiChildFlags ChildFlags; + bool PosUndock; bool CollapsedVal; ImRect SizeConstraintRect; ImGuiSizeCallback SizeCallback; void* SizeCallbackUserData; - float BgAlphaVal; - ImVec2 MenuBarOffsetMinVal; // *Always on* This is not exposed publicly, so we don't clear it. + float BgAlphaVal; // Override background alpha + ImGuiID ViewportId; + ImGuiID DockId; + ImGuiWindowClass WindowClass; + ImVec2 MenuBarOffsetMinVal; // (Always on) This is not exposed publicly, so we don't clear it and it doesn't have a corresponding flag (could we? for consistency?) + ImGuiWindowRefreshFlags RefreshFlagsVal; ImGuiNextWindowData() { memset(this, 0, sizeof(*this)); } - inline void ClearFlags() { Flags = ImGuiNextWindowDataFlags_None; } + inline void ClearFlags() { HasFlags = ImGuiNextWindowDataFlags_None; } }; enum ImGuiNextItemDataFlags_ { - ImGuiNextItemDataFlags_None = 0, - ImGuiNextItemDataFlags_HasWidth = 1 << 0, - ImGuiNextItemDataFlags_HasOpen = 1 << 1 + ImGuiNextItemDataFlags_None = 0, + ImGuiNextItemDataFlags_HasWidth = 1 << 0, + ImGuiNextItemDataFlags_HasOpen = 1 << 1, + ImGuiNextItemDataFlags_HasShortcut = 1 << 2, + ImGuiNextItemDataFlags_HasRefVal = 1 << 3, + ImGuiNextItemDataFlags_HasStorageID = 1 << 4, }; struct ImGuiNextItemData { - ImGuiNextItemDataFlags Flags; - float Width; // Set by SetNextItemWidth(). - bool OpenVal; // Set by SetNextItemOpen() function. - ImGuiCond OpenCond; + ImGuiNextItemDataFlags HasFlags; // Called HasFlags instead of Flags to avoid mistaking this + ImGuiItemFlags ItemFlags; // Currently only tested/used for ImGuiItemFlags_AllowOverlap and ImGuiItemFlags_HasSelectionUserData. - ImGuiNextItemData() { memset(this, 0, sizeof(*this)); } - inline void ClearFlags() { Flags = ImGuiNextItemDataFlags_None; } + // Members below are NOT cleared by ItemAdd() meaning they are still valid during e.g. NavProcessItem(). Always rely on HasFlags. + ImGuiID FocusScopeId; // Set by SetNextItemSelectionUserData() + ImGuiSelectionUserData SelectionUserData; // Set by SetNextItemSelectionUserData() (note that NULL/0 is a valid value, we use -1 == ImGuiSelectionUserData_Invalid to mark invalid values) + float Width; // Set by SetNextItemWidth() + ImGuiKeyChord Shortcut; // Set by SetNextItemShortcut() + ImGuiInputFlags ShortcutFlags; // Set by SetNextItemShortcut() + bool OpenVal; // Set by SetNextItemOpen() + ImU8 OpenCond; // Set by SetNextItemOpen() + ImGuiDataTypeStorage RefVal; // Not exposed yet, for ImGuiInputTextFlags_ParseEmptyAsRefVal + ImGuiID StorageId; // Set by SetNextItemStorageID() + + ImGuiNextItemData() { memset(this, 0, sizeof(*this)); SelectionUserData = -1; } + inline void ClearFlags() { HasFlags = ImGuiNextItemDataFlags_None; ItemFlags = ImGuiItemFlags_None; } // Also cleared manually by ItemAdd()! }; -//----------------------------------------------------------------------------- -// Tabs -//----------------------------------------------------------------------------- +// Status storage for the last submitted item +struct ImGuiLastItemData +{ + ImGuiID ID; + ImGuiItemFlags ItemFlags; // See ImGuiItemFlags_ (called 'InFlags' before v1.91.4). + ImGuiItemStatusFlags StatusFlags; // See ImGuiItemStatusFlags_ + ImRect Rect; // Full rectangle + ImRect NavRect; // Navigation scoring rectangle (not displayed) + // Rarely used fields are not explicitly cleared, only valid when the corresponding ImGuiItemStatusFlags are set. + ImRect DisplayRect; // Display rectangle. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasDisplayRect) is set. + ImRect ClipRect; // Clip rectangle at the time of submitting item. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasClipRect) is set.. + ImGuiKeyChord Shortcut; // Shortcut at the time of submitting item. ONLY VALID IF (StatusFlags & ImGuiItemStatusFlags_HasShortcut) is set.. + + ImGuiLastItemData() { memset(this, 0, sizeof(*this)); } +}; + +// Store data emitted by TreeNode() for usage by TreePop() +// - To implement ImGuiTreeNodeFlags_NavLeftJumpsBackHere: store the minimum amount of data +// which we can't infer in TreePop(), to perform the equivalent of NavApplyItemToResult(). +// Only stored when the node is a potential candidate for landing on a Left arrow jump. +struct ImGuiTreeNodeStackData +{ + ImGuiID ID; + ImGuiTreeNodeFlags TreeFlags; + ImGuiItemFlags ItemFlags; // Used for nav landing + ImRect NavRect; // Used for nav landing +}; + +// sizeof() = 20 +struct IMGUI_API ImGuiErrorRecoveryState +{ + short SizeOfWindowStack; + short SizeOfIDStack; + short SizeOfTreeStack; + short SizeOfColorStack; + short SizeOfStyleVarStack; + short SizeOfFontStack; + short SizeOfFocusScopeStack; + short SizeOfGroupStack; + short SizeOfItemFlagsStack; + short SizeOfBeginPopupStack; + short SizeOfDisabledStack; + + ImGuiErrorRecoveryState() { memset(this, 0, sizeof(*this)); } +}; + +// Data saved for each window pushed into the stack +struct ImGuiWindowStackData +{ + ImGuiWindow* Window; + ImGuiLastItemData ParentLastItemDataBackup; + ImGuiErrorRecoveryState StackSizesInBegin; // Store size of various stacks for asserting + bool DisabledOverrideReenable; // Non-child window override disabled flag + float DisabledOverrideReenableAlphaBackup; +}; struct ImGuiShrinkWidthItem { - int Index; - float Width; + int Index; + float Width; + float InitialWidth; }; struct ImGuiPtrOrIndex { - void* Ptr; // Either field can be set, not both. e.g. Dock node tab bars are loose while BeginTabBar() ones are in a pool. - int Index; // Usually index in a main pool. + void* Ptr; // Either field can be set, not both. e.g. Dock node tab bars are loose while BeginTabBar() ones are in a pool. + int Index; // Usually index in a main pool. - ImGuiPtrOrIndex(void* ptr) { Ptr = ptr; Index = -1; } - ImGuiPtrOrIndex(int index) { Ptr = NULL; Index = index; } + ImGuiPtrOrIndex(void* ptr) { Ptr = ptr; Index = -1; } + ImGuiPtrOrIndex(int index) { Ptr = NULL; Index = index; } +}; + +// Data used by IsItemDeactivated()/IsItemDeactivatedAfterEdit() functions +struct ImGuiDeactivatedItemData +{ + ImGuiID ID; + int ElapseFrame; + bool HasBeenEditedBefore; + bool IsAlive; }; //----------------------------------------------------------------------------- -// Main imgui context +// [SECTION] Popup support +//----------------------------------------------------------------------------- + +enum ImGuiPopupPositionPolicy +{ + ImGuiPopupPositionPolicy_Default, + ImGuiPopupPositionPolicy_ComboBox, + ImGuiPopupPositionPolicy_Tooltip, +}; + +// Storage for popup stacks (g.OpenPopupStack and g.BeginPopupStack) +struct ImGuiPopupData +{ + ImGuiID PopupId; // Set on OpenPopup() + ImGuiWindow* Window; // Resolved on BeginPopup() - may stay unresolved if user never calls OpenPopup() + ImGuiWindow* RestoreNavWindow;// Set on OpenPopup(), a NavWindow that will be restored on popup close + int ParentNavLayer; // Resolved on BeginPopup(). Actually a ImGuiNavLayer type (declared down below), initialized to -1 which is not part of an enum, but serves well-enough as "not any of layers" value + int OpenFrameCount; // Set on OpenPopup() + ImGuiID OpenParentId; // Set on OpenPopup(), we need this to differentiate multiple menu sets from each others (e.g. inside menu bar vs loose menu items) + ImVec2 OpenPopupPos; // Set on OpenPopup(), preferred popup position (typically == OpenMousePos when using mouse) + ImVec2 OpenMousePos; // Set on OpenPopup(), copy of mouse position at the time of opening popup + + ImGuiPopupData() { memset(this, 0, sizeof(*this)); ParentNavLayer = OpenFrameCount = -1; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Inputs support +//----------------------------------------------------------------------------- + +// Bit array for named keys +typedef ImBitArray ImBitArrayForNamedKeys; + +// [Internal] Key ranges +#define ImGuiKey_LegacyNativeKey_BEGIN 0 +#define ImGuiKey_LegacyNativeKey_END 512 +#define ImGuiKey_Keyboard_BEGIN (ImGuiKey_NamedKey_BEGIN) +#define ImGuiKey_Keyboard_END (ImGuiKey_GamepadStart) +#define ImGuiKey_Gamepad_BEGIN (ImGuiKey_GamepadStart) +#define ImGuiKey_Gamepad_END (ImGuiKey_GamepadRStickDown + 1) +#define ImGuiKey_Mouse_BEGIN (ImGuiKey_MouseLeft) +#define ImGuiKey_Mouse_END (ImGuiKey_MouseWheelY + 1) +#define ImGuiKey_Aliases_BEGIN (ImGuiKey_Mouse_BEGIN) +#define ImGuiKey_Aliases_END (ImGuiKey_Mouse_END) + +// [Internal] Named shortcuts for Navigation +#define ImGuiKey_NavKeyboardTweakSlow ImGuiMod_Ctrl +#define ImGuiKey_NavKeyboardTweakFast ImGuiMod_Shift +#define ImGuiKey_NavGamepadTweakSlow ImGuiKey_GamepadL1 +#define ImGuiKey_NavGamepadTweakFast ImGuiKey_GamepadR1 +#define ImGuiKey_NavGamepadActivate (g.IO.ConfigNavSwapGamepadButtons ? ImGuiKey_GamepadFaceRight : ImGuiKey_GamepadFaceDown) +#define ImGuiKey_NavGamepadCancel (g.IO.ConfigNavSwapGamepadButtons ? ImGuiKey_GamepadFaceDown : ImGuiKey_GamepadFaceRight) +#define ImGuiKey_NavGamepadMenu ImGuiKey_GamepadFaceLeft +#define ImGuiKey_NavGamepadInput ImGuiKey_GamepadFaceUp + +enum ImGuiInputEventType +{ + ImGuiInputEventType_None = 0, + ImGuiInputEventType_MousePos, + ImGuiInputEventType_MouseWheel, + ImGuiInputEventType_MouseButton, + ImGuiInputEventType_MouseViewport, + ImGuiInputEventType_Key, + ImGuiInputEventType_Text, + ImGuiInputEventType_Focus, + ImGuiInputEventType_COUNT +}; + +enum ImGuiInputSource +{ + ImGuiInputSource_None = 0, + ImGuiInputSource_Mouse, // Note: may be Mouse or TouchScreen or Pen. See io.MouseSource to distinguish them. + ImGuiInputSource_Keyboard, + ImGuiInputSource_Gamepad, + ImGuiInputSource_COUNT +}; + +// FIXME: Structures in the union below need to be declared as anonymous unions appears to be an extension? +// Using ImVec2() would fail on Clang 'union member 'MousePos' has a non-trivial default constructor' +struct ImGuiInputEventMousePos { float PosX, PosY; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventMouseWheel { float WheelX, WheelY; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventMouseButton { int Button; bool Down; ImGuiMouseSource MouseSource; }; +struct ImGuiInputEventMouseViewport { ImGuiID HoveredViewportID; }; +struct ImGuiInputEventKey { ImGuiKey Key; bool Down; float AnalogValue; }; +struct ImGuiInputEventText { unsigned int Char; }; +struct ImGuiInputEventAppFocused { bool Focused; }; + +struct ImGuiInputEvent +{ + ImGuiInputEventType Type; + ImGuiInputSource Source; + ImU32 EventId; // Unique, sequential increasing integer to identify an event (if you need to correlate them to other data). + union + { + ImGuiInputEventMousePos MousePos; // if Type == ImGuiInputEventType_MousePos + ImGuiInputEventMouseWheel MouseWheel; // if Type == ImGuiInputEventType_MouseWheel + ImGuiInputEventMouseButton MouseButton; // if Type == ImGuiInputEventType_MouseButton + ImGuiInputEventMouseViewport MouseViewport; // if Type == ImGuiInputEventType_MouseViewport + ImGuiInputEventKey Key; // if Type == ImGuiInputEventType_Key + ImGuiInputEventText Text; // if Type == ImGuiInputEventType_Text + ImGuiInputEventAppFocused AppFocused; // if Type == ImGuiInputEventType_Focus + }; + bool AddedByTestEngine; + + ImGuiInputEvent() { memset(this, 0, sizeof(*this)); } +}; + +// Input function taking an 'ImGuiID owner_id' argument defaults to (ImGuiKeyOwner_Any == 0) aka don't test ownership, which matches legacy behavior. +#define ImGuiKeyOwner_Any ((ImGuiID)0) // Accept key that have an owner, UNLESS a call to SetKeyOwner() explicitly used ImGuiInputFlags_LockThisFrame or ImGuiInputFlags_LockUntilRelease. +#define ImGuiKeyOwner_NoOwner ((ImGuiID)-1) // Require key to have no owner. +//#define ImGuiKeyOwner_None ImGuiKeyOwner_NoOwner // We previously called this 'ImGuiKeyOwner_None' but it was inconsistent with our pattern that _None values == 0 and quite dangerous. Also using _NoOwner makes the IsKeyPressed() calls more explicit. + +typedef ImS16 ImGuiKeyRoutingIndex; + +// Routing table entry (sizeof() == 16 bytes) +struct ImGuiKeyRoutingData +{ + ImGuiKeyRoutingIndex NextEntryIndex; + ImU16 Mods; // Technically we'd only need 4-bits but for simplify we store ImGuiMod_ values which need 16-bits. + ImU8 RoutingCurrScore; // [DEBUG] For debug display + ImU8 RoutingNextScore; // Lower is better (0: perfect score) + ImGuiID RoutingCurr; + ImGuiID RoutingNext; + + ImGuiKeyRoutingData() { NextEntryIndex = -1; Mods = 0; RoutingCurrScore = RoutingNextScore = 255; RoutingCurr = RoutingNext = ImGuiKeyOwner_NoOwner; } +}; + +// Routing table: maintain a desired owner for each possible key-chord (key + mods), and setup owner in NewFrame() when mods are matching. +// Stored in main context (1 instance) +struct ImGuiKeyRoutingTable +{ + ImGuiKeyRoutingIndex Index[ImGuiKey_NamedKey_COUNT]; // Index of first entry in Entries[] + ImVector Entries; + ImVector EntriesNext; // Double-buffer to avoid reallocation (could use a shared buffer) + + ImGuiKeyRoutingTable() { Clear(); } + void Clear() { for (int n = 0; n < IM_ARRAYSIZE(Index); n++) Index[n] = -1; Entries.clear(); EntriesNext.clear(); } +}; + +// This extends ImGuiKeyData but only for named keys (legacy keys don't support the new features) +// Stored in main context (1 per named key). In the future it might be merged into ImGuiKeyData. +struct ImGuiKeyOwnerData +{ + ImGuiID OwnerCurr; + ImGuiID OwnerNext; + bool LockThisFrame; // Reading this key requires explicit owner id (until end of frame). Set by ImGuiInputFlags_LockThisFrame. + bool LockUntilRelease; // Reading this key requires explicit owner id (until key is released). Set by ImGuiInputFlags_LockUntilRelease. When this is true LockThisFrame is always true as well. + + ImGuiKeyOwnerData() { OwnerCurr = OwnerNext = ImGuiKeyOwner_NoOwner; LockThisFrame = LockUntilRelease = false; } +}; + +// Extend ImGuiInputFlags_ +// Flags for extended versions of IsKeyPressed(), IsMouseClicked(), Shortcut(), SetKeyOwner(), SetItemKeyOwner() +// Don't mistake with ImGuiInputTextFlags! (which is for ImGui::InputText() function) +enum ImGuiInputFlagsPrivate_ +{ + // Flags for IsKeyPressed(), IsKeyChordPressed(), IsMouseClicked(), Shortcut() + // - Repeat mode: Repeat rate selection + ImGuiInputFlags_RepeatRateDefault = 1 << 1, // Repeat rate: Regular (default) + ImGuiInputFlags_RepeatRateNavMove = 1 << 2, // Repeat rate: Fast + ImGuiInputFlags_RepeatRateNavTweak = 1 << 3, // Repeat rate: Faster + // - Repeat mode: Specify when repeating key pressed can be interrupted. + // - In theory ImGuiInputFlags_RepeatUntilOtherKeyPress may be a desirable default, but it would break too many behavior so everything is opt-in. + ImGuiInputFlags_RepeatUntilRelease = 1 << 4, // Stop repeating when released (default for all functions except Shortcut). This only exists to allow overriding Shortcut() default behavior. + ImGuiInputFlags_RepeatUntilKeyModsChange = 1 << 5, // Stop repeating when released OR if keyboard mods are changed (default for Shortcut) + ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone = 1 << 6, // Stop repeating when released OR if keyboard mods are leaving the None state. Allows going from Mod+Key to Key by releasing Mod. + ImGuiInputFlags_RepeatUntilOtherKeyPress = 1 << 7, // Stop repeating when released OR if any other keyboard key is pressed during the repeat + + // Flags for SetKeyOwner(), SetItemKeyOwner() + // - Locking key away from non-input aware code. Locking is useful to make input-owner-aware code steal keys from non-input-owner-aware code. If all code is input-owner-aware locking would never be necessary. + ImGuiInputFlags_LockThisFrame = 1 << 20, // Further accesses to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared at end of frame. + ImGuiInputFlags_LockUntilRelease = 1 << 21, // Further accesses to key data will require EXPLICIT owner ID (ImGuiKeyOwner_Any/0 will NOT accepted for polling). Cleared when the key is released or at end of each frame if key is released. + + // - Condition for SetItemKeyOwner() + ImGuiInputFlags_CondHovered = 1 << 22, // Only set if item is hovered (default to both) + ImGuiInputFlags_CondActive = 1 << 23, // Only set if item is active (default to both) + ImGuiInputFlags_CondDefault_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, + + // [Internal] Mask of which function support which flags + ImGuiInputFlags_RepeatRateMask_ = ImGuiInputFlags_RepeatRateDefault | ImGuiInputFlags_RepeatRateNavMove | ImGuiInputFlags_RepeatRateNavTweak, + ImGuiInputFlags_RepeatUntilMask_ = ImGuiInputFlags_RepeatUntilRelease | ImGuiInputFlags_RepeatUntilKeyModsChange | ImGuiInputFlags_RepeatUntilKeyModsChangeFromNone | ImGuiInputFlags_RepeatUntilOtherKeyPress, + ImGuiInputFlags_RepeatMask_ = ImGuiInputFlags_Repeat | ImGuiInputFlags_RepeatRateMask_ | ImGuiInputFlags_RepeatUntilMask_, + ImGuiInputFlags_CondMask_ = ImGuiInputFlags_CondHovered | ImGuiInputFlags_CondActive, + ImGuiInputFlags_RouteTypeMask_ = ImGuiInputFlags_RouteActive | ImGuiInputFlags_RouteFocused | ImGuiInputFlags_RouteGlobal | ImGuiInputFlags_RouteAlways, + ImGuiInputFlags_RouteOptionsMask_ = ImGuiInputFlags_RouteOverFocused | ImGuiInputFlags_RouteOverActive | ImGuiInputFlags_RouteUnlessBgFocused | ImGuiInputFlags_RouteFromRootWindow, + ImGuiInputFlags_SupportedByIsKeyPressed = ImGuiInputFlags_RepeatMask_, + ImGuiInputFlags_SupportedByIsMouseClicked = ImGuiInputFlags_Repeat, + ImGuiInputFlags_SupportedByShortcut = ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteTypeMask_ | ImGuiInputFlags_RouteOptionsMask_, + ImGuiInputFlags_SupportedBySetNextItemShortcut = ImGuiInputFlags_RepeatMask_ | ImGuiInputFlags_RouteTypeMask_ | ImGuiInputFlags_RouteOptionsMask_ | ImGuiInputFlags_Tooltip, + ImGuiInputFlags_SupportedBySetKeyOwner = ImGuiInputFlags_LockThisFrame | ImGuiInputFlags_LockUntilRelease, + ImGuiInputFlags_SupportedBySetItemKeyOwner = ImGuiInputFlags_SupportedBySetKeyOwner | ImGuiInputFlags_CondMask_, +}; + +//----------------------------------------------------------------------------- +// [SECTION] Clipper support +//----------------------------------------------------------------------------- + +// Note that Max is exclusive, so perhaps should be using a Begin/End convention. +struct ImGuiListClipperRange +{ + int Min; + int Max; + bool PosToIndexConvert; // Begin/End are absolute position (will be converted to indices later) + ImS8 PosToIndexOffsetMin; // Add to Min after converting to indices + ImS8 PosToIndexOffsetMax; // Add to Min after converting to indices + + static ImGuiListClipperRange FromIndices(int min, int max) { ImGuiListClipperRange r = { min, max, false, 0, 0 }; return r; } + static ImGuiListClipperRange FromPositions(float y1, float y2, int off_min, int off_max) { ImGuiListClipperRange r = { (int)y1, (int)y2, true, (ImS8)off_min, (ImS8)off_max }; return r; } +}; + +// Temporary clipper data, buffers shared/reused between instances +struct ImGuiListClipperData +{ + ImGuiListClipper* ListClipper; + float LossynessOffset; + int StepNo; + int ItemsFrozen; + ImVector Ranges; + + ImGuiListClipperData() { memset(this, 0, sizeof(*this)); } + void Reset(ImGuiListClipper* clipper) { ListClipper = clipper; StepNo = ItemsFrozen = 0; Ranges.resize(0); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Navigation support +//----------------------------------------------------------------------------- + +enum ImGuiActivateFlags_ +{ + ImGuiActivateFlags_None = 0, + ImGuiActivateFlags_PreferInput = 1 << 0, // Favor activation that requires keyboard text input (e.g. for Slider/Drag). Default for Enter key. + ImGuiActivateFlags_PreferTweak = 1 << 1, // Favor activation for tweaking with arrows or gamepad (e.g. for Slider/Drag). Default for Space key and if keyboard is not used. + ImGuiActivateFlags_TryToPreserveState = 1 << 2, // Request widget to preserve state if it can (e.g. InputText will try to preserve cursor/selection) + ImGuiActivateFlags_FromTabbing = 1 << 3, // Activation requested by a tabbing request + ImGuiActivateFlags_FromShortcut = 1 << 4, // Activation requested by an item shortcut via SetNextItemShortcut() function. +}; + +// Early work-in-progress API for ScrollToItem() +enum ImGuiScrollFlags_ +{ + ImGuiScrollFlags_None = 0, + ImGuiScrollFlags_KeepVisibleEdgeX = 1 << 0, // If item is not visible: scroll as little as possible on X axis to bring item back into view [default for X axis] + ImGuiScrollFlags_KeepVisibleEdgeY = 1 << 1, // If item is not visible: scroll as little as possible on Y axis to bring item back into view [default for Y axis for windows that are already visible] + ImGuiScrollFlags_KeepVisibleCenterX = 1 << 2, // If item is not visible: scroll to make the item centered on X axis [rarely used] + ImGuiScrollFlags_KeepVisibleCenterY = 1 << 3, // If item is not visible: scroll to make the item centered on Y axis + ImGuiScrollFlags_AlwaysCenterX = 1 << 4, // Always center the result item on X axis [rarely used] + ImGuiScrollFlags_AlwaysCenterY = 1 << 5, // Always center the result item on Y axis [default for Y axis for appearing window) + ImGuiScrollFlags_NoScrollParent = 1 << 6, // Disable forwarding scrolling to parent window if required to keep item/rect visible (only scroll window the function was applied to). + ImGuiScrollFlags_MaskX_ = ImGuiScrollFlags_KeepVisibleEdgeX | ImGuiScrollFlags_KeepVisibleCenterX | ImGuiScrollFlags_AlwaysCenterX, + ImGuiScrollFlags_MaskY_ = ImGuiScrollFlags_KeepVisibleEdgeY | ImGuiScrollFlags_KeepVisibleCenterY | ImGuiScrollFlags_AlwaysCenterY, +}; + +enum ImGuiNavRenderCursorFlags_ +{ + ImGuiNavRenderCursorFlags_None = 0, + ImGuiNavRenderCursorFlags_Compact = 1 << 1, // Compact highlight, no padding/distance from focused item + ImGuiNavRenderCursorFlags_AlwaysDraw = 1 << 2, // Draw rectangular highlight if (g.NavId == id) even when g.NavCursorVisible == false, aka even when using the mouse. + ImGuiNavRenderCursorFlags_NoRounding = 1 << 3, +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + ImGuiNavHighlightFlags_None = ImGuiNavRenderCursorFlags_None, // Renamed in 1.91.4 + ImGuiNavHighlightFlags_Compact = ImGuiNavRenderCursorFlags_Compact, // Renamed in 1.91.4 + ImGuiNavHighlightFlags_AlwaysDraw = ImGuiNavRenderCursorFlags_AlwaysDraw, // Renamed in 1.91.4 + ImGuiNavHighlightFlags_NoRounding = ImGuiNavRenderCursorFlags_NoRounding, // Renamed in 1.91.4 +#endif +}; + +enum ImGuiNavMoveFlags_ +{ + ImGuiNavMoveFlags_None = 0, + ImGuiNavMoveFlags_LoopX = 1 << 0, // On failed request, restart from opposite side + ImGuiNavMoveFlags_LoopY = 1 << 1, + ImGuiNavMoveFlags_WrapX = 1 << 2, // On failed request, request from opposite side one line down (when NavDir==right) or one line up (when NavDir==left) + ImGuiNavMoveFlags_WrapY = 1 << 3, // This is not super useful but provided for completeness + ImGuiNavMoveFlags_WrapMask_ = ImGuiNavMoveFlags_LoopX | ImGuiNavMoveFlags_LoopY | ImGuiNavMoveFlags_WrapX | ImGuiNavMoveFlags_WrapY, + ImGuiNavMoveFlags_AllowCurrentNavId = 1 << 4, // Allow scoring and considering the current NavId as a move target candidate. This is used when the move source is offset (e.g. pressing PageDown actually needs to send a Up move request, if we are pressing PageDown from the bottom-most item we need to stay in place) + ImGuiNavMoveFlags_AlsoScoreVisibleSet = 1 << 5, // Store alternate result in NavMoveResultLocalVisible that only comprise elements that are already fully visible (used by PageUp/PageDown) + ImGuiNavMoveFlags_ScrollToEdgeY = 1 << 6, // Force scrolling to min/max (used by Home/End) // FIXME-NAV: Aim to remove or reword, probably unnecessary + ImGuiNavMoveFlags_Forwarded = 1 << 7, + ImGuiNavMoveFlags_DebugNoResult = 1 << 8, // Dummy scoring for debug purpose, don't apply result + ImGuiNavMoveFlags_FocusApi = 1 << 9, // Requests from focus API can land/focus/activate items even if they are marked with _NoTabStop (see NavProcessItemForTabbingRequest() for details) + ImGuiNavMoveFlags_IsTabbing = 1 << 10, // == Focus + Activate if item is Inputable + DontChangeNavHighlight + ImGuiNavMoveFlags_IsPageMove = 1 << 11, // Identify a PageDown/PageUp request. + ImGuiNavMoveFlags_Activate = 1 << 12, // Activate/select target item. + ImGuiNavMoveFlags_NoSelect = 1 << 13, // Don't trigger selection by not setting g.NavJustMovedTo + ImGuiNavMoveFlags_NoSetNavCursorVisible = 1 << 14, // Do not alter the nav cursor visible state + ImGuiNavMoveFlags_NoClearActiveId = 1 << 15, // (Experimental) Do not clear active id when applying move result +}; + +enum ImGuiNavLayer +{ + ImGuiNavLayer_Main = 0, // Main scrolling layer + ImGuiNavLayer_Menu = 1, // Menu layer (access with Alt) + ImGuiNavLayer_COUNT +}; + +// Storage for navigation query/results +struct ImGuiNavItemData +{ + ImGuiWindow* Window; // Init,Move // Best candidate window (result->ItemWindow->RootWindowForNav == request->Window) + ImGuiID ID; // Init,Move // Best candidate item ID + ImGuiID FocusScopeId; // Init,Move // Best candidate focus scope ID + ImRect RectRel; // Init,Move // Best candidate bounding box in window relative space + ImGuiItemFlags ItemFlags; // ????,Move // Best candidate item flags + float DistBox; // Move // Best candidate box distance to current NavId + float DistCenter; // Move // Best candidate center distance to current NavId + float DistAxial; // Move // Best candidate axial distance to current NavId + ImGuiSelectionUserData SelectionUserData;//I+Mov // Best candidate SetNextItemSelectionUserData() value. Valid if (ItemFlags & ImGuiItemFlags_HasSelectionUserData) + + ImGuiNavItemData() { Clear(); } + void Clear() { Window = NULL; ID = FocusScopeId = 0; ItemFlags = 0; SelectionUserData = -1; DistBox = DistCenter = DistAxial = FLT_MAX; } +}; + +// Storage for PushFocusScope(), g.FocusScopeStack[], g.NavFocusRoute[] +struct ImGuiFocusScopeData +{ + ImGuiID ID; + ImGuiID WindowID; +}; + +//----------------------------------------------------------------------------- +// [SECTION] Typing-select support +//----------------------------------------------------------------------------- + +// Flags for GetTypingSelectRequest() +enum ImGuiTypingSelectFlags_ +{ + ImGuiTypingSelectFlags_None = 0, + ImGuiTypingSelectFlags_AllowBackspace = 1 << 0, // Backspace to delete character inputs. If using: ensure GetTypingSelectRequest() is not called more than once per frame (filter by e.g. focus state) + ImGuiTypingSelectFlags_AllowSingleCharMode = 1 << 1, // Allow "single char" search mode which is activated when pressing the same character multiple times. +}; + +// Returned by GetTypingSelectRequest(), designed to eventually be public. +struct IMGUI_API ImGuiTypingSelectRequest +{ + ImGuiTypingSelectFlags Flags; // Flags passed to GetTypingSelectRequest() + int SearchBufferLen; + const char* SearchBuffer; // Search buffer contents (use full string. unless SingleCharMode is set, in which case use SingleCharSize). + bool SelectRequest; // Set when buffer was modified this frame, requesting a selection. + bool SingleCharMode; // Notify when buffer contains same character repeated, to implement special mode. In this situation it preferred to not display any on-screen search indication. + ImS8 SingleCharSize; // Length in bytes of first letter codepoint (1 for ascii, 2-4 for UTF-8). If (SearchBufferLen==RepeatCharSize) only 1 letter has been input. +}; + +// Storage for GetTypingSelectRequest() +struct IMGUI_API ImGuiTypingSelectState +{ + ImGuiTypingSelectRequest Request; // User-facing data + char SearchBuffer[64]; // Search buffer: no need to make dynamic as this search is very transient. + ImGuiID FocusScope; + int LastRequestFrame = 0; + float LastRequestTime = 0.0f; + bool SingleCharModeLock = false; // After a certain single char repeat count we lock into SingleCharMode. Two benefits: 1) buffer never fill, 2) we can provide an immediate SingleChar mode without timer elapsing. + + ImGuiTypingSelectState() { memset(this, 0, sizeof(*this)); } + void Clear() { SearchBuffer[0] = 0; SingleCharModeLock = false; } // We preserve remaining data for easier debugging +}; + +//----------------------------------------------------------------------------- +// [SECTION] Columns support +//----------------------------------------------------------------------------- + +// Flags for internal's BeginColumns(). This is an obsolete API. Prefer using BeginTable() nowadays! +enum ImGuiOldColumnFlags_ +{ + ImGuiOldColumnFlags_None = 0, + ImGuiOldColumnFlags_NoBorder = 1 << 0, // Disable column dividers + ImGuiOldColumnFlags_NoResize = 1 << 1, // Disable resizing columns when clicking on the dividers + ImGuiOldColumnFlags_NoPreserveWidths = 1 << 2, // Disable column width preservation when adjusting columns + ImGuiOldColumnFlags_NoForceWithinWindow = 1 << 3, // Disable forcing columns to fit within window + ImGuiOldColumnFlags_GrowParentContentsSize = 1 << 4, // Restore pre-1.51 behavior of extending the parent window contents size but _without affecting the columns width at all_. Will eventually remove. + + // Obsolete names (will be removed) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //ImGuiColumnsFlags_None = ImGuiOldColumnFlags_None, + //ImGuiColumnsFlags_NoBorder = ImGuiOldColumnFlags_NoBorder, + //ImGuiColumnsFlags_NoResize = ImGuiOldColumnFlags_NoResize, + //ImGuiColumnsFlags_NoPreserveWidths = ImGuiOldColumnFlags_NoPreserveWidths, + //ImGuiColumnsFlags_NoForceWithinWindow = ImGuiOldColumnFlags_NoForceWithinWindow, + //ImGuiColumnsFlags_GrowParentContentsSize = ImGuiOldColumnFlags_GrowParentContentsSize, +#endif +}; + +struct ImGuiOldColumnData +{ + float OffsetNorm; // Column start offset, normalized 0.0 (far left) -> 1.0 (far right) + float OffsetNormBeforeResize; + ImGuiOldColumnFlags Flags; // Not exposed + ImRect ClipRect; + + ImGuiOldColumnData() { memset(this, 0, sizeof(*this)); } +}; + +struct ImGuiOldColumns +{ + ImGuiID ID; + ImGuiOldColumnFlags Flags; + bool IsFirstFrame; + bool IsBeingResized; + int Current; + int Count; + float OffMinX, OffMaxX; // Offsets from HostWorkRect.Min.x + float LineMinY, LineMaxY; + float HostCursorPosY; // Backup of CursorPos at the time of BeginColumns() + float HostCursorMaxPosX; // Backup of CursorMaxPos at the time of BeginColumns() + ImRect HostInitialClipRect; // Backup of ClipRect at the time of BeginColumns() + ImRect HostBackupClipRect; // Backup of ClipRect during PushColumnsBackground()/PopColumnsBackground() + ImRect HostBackupParentWorkRect;//Backup of WorkRect at the time of BeginColumns() + ImVector Columns; + ImDrawListSplitter Splitter; + + ImGuiOldColumns() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Box-select support +//----------------------------------------------------------------------------- + +struct ImGuiBoxSelectState +{ + // Active box-selection data (persistent, 1 active at a time) + ImGuiID ID; + bool IsActive; + bool IsStarting; + bool IsStartedFromVoid; // Starting click was not from an item. + bool IsStartedSetNavIdOnce; + bool RequestClear; + ImGuiKeyChord KeyMods : 16; // Latched key-mods for box-select logic. + ImVec2 StartPosRel; // Start position in window-contents relative space (to support scrolling) + ImVec2 EndPosRel; // End position in window-contents relative space + ImVec2 ScrollAccum; // Scrolling accumulator (to behave at high-frame spaces) + ImGuiWindow* Window; + + // Temporary/Transient data + bool UnclipMode; // (Temp/Transient, here in hot area). Set/cleared by the BeginMultiSelect()/EndMultiSelect() owning active box-select. + ImRect UnclipRect; // Rectangle where ItemAdd() clipping may be temporarily disabled. Need support by multi-select supporting widgets. + ImRect BoxSelectRectPrev; // Selection rectangle in absolute coordinates (derived every frame from BoxSelectStartPosRel and MousePos) + ImRect BoxSelectRectCurr; + + ImGuiBoxSelectState() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Multi-select support +//----------------------------------------------------------------------------- + +// We always assume that -1 is an invalid value (which works for indices and pointers) +#define ImGuiSelectionUserData_Invalid ((ImGuiSelectionUserData)-1) + +// Temporary storage for multi-select +struct IMGUI_API ImGuiMultiSelectTempData +{ + ImGuiMultiSelectIO IO; // MUST BE FIRST FIELD. Requests are set and returned by BeginMultiSelect()/EndMultiSelect() + written to by user during the loop. + ImGuiMultiSelectState* Storage; + ImGuiID FocusScopeId; // Copied from g.CurrentFocusScopeId (unless another selection scope was pushed manually) + ImGuiMultiSelectFlags Flags; + ImVec2 ScopeRectMin; + ImVec2 BackupCursorMaxPos; + ImGuiSelectionUserData LastSubmittedItem; // Copy of last submitted item data, used to merge output ranges. + ImGuiID BoxSelectId; + ImGuiKeyChord KeyMods; + ImS8 LoopRequestSetAll; // -1: no operation, 0: clear all, 1: select all. + bool IsEndIO; // Set when switching IO from BeginMultiSelect() to EndMultiSelect() state. + bool IsFocused; // Set if currently focusing the selection scope (any item of the selection). May be used if you have custom shortcut associated to selection. + bool IsKeyboardSetRange; // Set by BeginMultiSelect() when using Shift+Navigation. Because scrolling may be affected we can't afford a frame of lag with Shift+Navigation. + bool NavIdPassedBy; + bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. + bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. + + ImGuiMultiSelectTempData() { Clear(); } + void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. + void ClearIO() { IO.Requests.resize(0); IO.RangeSrcItem = IO.NavIdItem = ImGuiSelectionUserData_Invalid; IO.NavIdSelected = IO.RangeSrcReset = false; } +}; + +// Persistent storage for multi-select (as long as selection is alive) +struct IMGUI_API ImGuiMultiSelectState +{ + ImGuiWindow* Window; + ImGuiID ID; + int LastFrameActive; // Last used frame-count, for GC. + int LastSelectionSize; // Set by BeginMultiSelect() based on optional info provided by user. May be -1 if unknown. + ImS8 RangeSelected; // -1 (don't have) or true/false + ImS8 NavIdSelected; // -1 (don't have) or true/false + ImGuiSelectionUserData RangeSrcItem; // + ImGuiSelectionUserData NavIdItem; // SetNextItemSelectionUserData() value for NavId (if part of submitted items) + + ImGuiMultiSelectState() { Window = NULL; ID = 0; LastFrameActive = LastSelectionSize = 0; RangeSelected = NavIdSelected = -1; RangeSrcItem = NavIdItem = ImGuiSelectionUserData_Invalid; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Docking support +//----------------------------------------------------------------------------- + +#define DOCKING_HOST_DRAW_CHANNEL_BG 0 // Dock host: background fill +#define DOCKING_HOST_DRAW_CHANNEL_FG 1 // Dock host: decorations and contents + +#ifdef IMGUI_HAS_DOCK + +// Extend ImGuiDockNodeFlags_ +enum ImGuiDockNodeFlagsPrivate_ +{ + // [Internal] + ImGuiDockNodeFlags_DockSpace = 1 << 10, // Saved // A dockspace is a node that occupy space within an existing user window. Otherwise the node is floating and create its own window. + ImGuiDockNodeFlags_CentralNode = 1 << 11, // Saved // The central node has 2 main properties: stay visible when empty, only use "remaining" spaces from its neighbor. + ImGuiDockNodeFlags_NoTabBar = 1 << 12, // Saved // Tab bar is completely unavailable. No triangle in the corner to enable it back. + ImGuiDockNodeFlags_HiddenTabBar = 1 << 13, // Saved // Tab bar is hidden, with a triangle in the corner to show it again (NB: actual tab-bar instance may be destroyed as this is only used for single-window tab bar) + ImGuiDockNodeFlags_NoWindowMenuButton = 1 << 14, // Saved // Disable window/docking menu (that one that appears instead of the collapse button) + ImGuiDockNodeFlags_NoCloseButton = 1 << 15, // Saved // Disable close button + ImGuiDockNodeFlags_NoResizeX = 1 << 16, // // + ImGuiDockNodeFlags_NoResizeY = 1 << 17, // // + ImGuiDockNodeFlags_DockedWindowsInFocusRoute= 1 << 18, // // Any docked window will be automatically be focus-route chained (window->ParentWindowForFocusRoute set to this) so Shortcut() in this window can run when any docked window is focused. + + // Disable docking/undocking actions in this dockspace or individual node (existing docked nodes will be preserved) + // Those are not exposed in public because the desirable sharing/inheriting/copy-flag-on-split behaviors are quite difficult to design and understand. + // The two public flags ImGuiDockNodeFlags_NoDockingOverCentralNode/ImGuiDockNodeFlags_NoDockingSplit don't have those issues. + ImGuiDockNodeFlags_NoDockingSplitOther = 1 << 19, // // Disable this node from splitting other windows/nodes. + ImGuiDockNodeFlags_NoDockingOverMe = 1 << 20, // // Disable other windows/nodes from being docked over this node. + ImGuiDockNodeFlags_NoDockingOverOther = 1 << 21, // // Disable this node from being docked over another window or non-empty node. + ImGuiDockNodeFlags_NoDockingOverEmpty = 1 << 22, // // Disable this node from being docked over an empty node (e.g. DockSpace with no other windows) + ImGuiDockNodeFlags_NoDocking = ImGuiDockNodeFlags_NoDockingOverMe | ImGuiDockNodeFlags_NoDockingOverOther | ImGuiDockNodeFlags_NoDockingOverEmpty | ImGuiDockNodeFlags_NoDockingSplit | ImGuiDockNodeFlags_NoDockingSplitOther, + + // Masks + ImGuiDockNodeFlags_SharedFlagsInheritMask_ = ~0, + ImGuiDockNodeFlags_NoResizeFlagsMask_ = (int)ImGuiDockNodeFlags_NoResize | ImGuiDockNodeFlags_NoResizeX | ImGuiDockNodeFlags_NoResizeY, + + // When splitting, those local flags are moved to the inheriting child, never duplicated + ImGuiDockNodeFlags_LocalFlagsTransferMask_ = (int)ImGuiDockNodeFlags_NoDockingSplit | ImGuiDockNodeFlags_NoResizeFlagsMask_ | (int)ImGuiDockNodeFlags_AutoHideTabBar | ImGuiDockNodeFlags_CentralNode | ImGuiDockNodeFlags_NoTabBar | ImGuiDockNodeFlags_HiddenTabBar | ImGuiDockNodeFlags_NoWindowMenuButton | ImGuiDockNodeFlags_NoCloseButton, + ImGuiDockNodeFlags_SavedFlagsMask_ = ImGuiDockNodeFlags_NoResizeFlagsMask_ | ImGuiDockNodeFlags_DockSpace | ImGuiDockNodeFlags_CentralNode | ImGuiDockNodeFlags_NoTabBar | ImGuiDockNodeFlags_HiddenTabBar | ImGuiDockNodeFlags_NoWindowMenuButton | ImGuiDockNodeFlags_NoCloseButton, +}; + +// Store the source authority (dock node vs window) of a field +enum ImGuiDataAuthority_ +{ + ImGuiDataAuthority_Auto, + ImGuiDataAuthority_DockNode, + ImGuiDataAuthority_Window, +}; + +enum ImGuiDockNodeState +{ + ImGuiDockNodeState_Unknown, + ImGuiDockNodeState_HostWindowHiddenBecauseSingleWindow, + ImGuiDockNodeState_HostWindowHiddenBecauseWindowsAreResizing, + ImGuiDockNodeState_HostWindowVisible, +}; + +// sizeof() 156~192 +struct IMGUI_API ImGuiDockNode +{ + ImGuiID ID; + ImGuiDockNodeFlags SharedFlags; // (Write) Flags shared by all nodes of a same dockspace hierarchy (inherited from the root node) + ImGuiDockNodeFlags LocalFlags; // (Write) Flags specific to this node + ImGuiDockNodeFlags LocalFlagsInWindows; // (Write) Flags specific to this node, applied from windows + ImGuiDockNodeFlags MergedFlags; // (Read) Effective flags (== SharedFlags | LocalFlagsInNode | LocalFlagsInWindows) + ImGuiDockNodeState State; + ImGuiDockNode* ParentNode; + ImGuiDockNode* ChildNodes[2]; // [Split node only] Child nodes (left/right or top/bottom). Consider switching to an array. + ImVector Windows; // Note: unordered list! Iterate TabBar->Tabs for user-order. + ImGuiTabBar* TabBar; + ImVec2 Pos; // Current position + ImVec2 Size; // Current size + ImVec2 SizeRef; // [Split node only] Last explicitly written-to size (overridden when using a splitter affecting the node), used to calculate Size. + ImGuiAxis SplitAxis; // [Split node only] Split axis (X or Y) + ImGuiWindowClass WindowClass; // [Root node only] + ImU32 LastBgColor; + + ImGuiWindow* HostWindow; + ImGuiWindow* VisibleWindow; // Generally point to window which is ID is == SelectedTabID, but when CTRL+Tabbing this can be a different window. + ImGuiDockNode* CentralNode; // [Root node only] Pointer to central node. + ImGuiDockNode* OnlyNodeWithWindows; // [Root node only] Set when there is a single visible node within the hierarchy. + int CountNodeWithWindows; // [Root node only] + int LastFrameAlive; // Last frame number the node was updated or kept alive explicitly with DockSpace() + ImGuiDockNodeFlags_KeepAliveOnly + int LastFrameActive; // Last frame number the node was updated. + int LastFrameFocused; // Last frame number the node was focused. + ImGuiID LastFocusedNodeId; // [Root node only] Which of our child docking node (any ancestor in the hierarchy) was last focused. + ImGuiID SelectedTabId; // [Leaf node only] Which of our tab/window is selected. + ImGuiID WantCloseTabId; // [Leaf node only] Set when closing a specific tab/window. + ImGuiID RefViewportId; // Reference viewport ID from visible window when HostWindow == NULL. + ImGuiDataAuthority AuthorityForPos :3; + ImGuiDataAuthority AuthorityForSize :3; + ImGuiDataAuthority AuthorityForViewport :3; + bool IsVisible :1; // Set to false when the node is hidden (usually disabled as it has no active window) + bool IsFocused :1; + bool IsBgDrawnThisFrame :1; + bool HasCloseButton :1; // Provide space for a close button (if any of the docked window has one). Note that button may be hidden on window without one. + bool HasWindowMenuButton :1; + bool HasCentralNodeChild :1; + bool WantCloseAll :1; // Set when closing all tabs at once. + bool WantLockSizeOnce :1; + bool WantMouseMove :1; // After a node extraction we need to transition toward moving the newly created host window + bool WantHiddenTabBarUpdate :1; + bool WantHiddenTabBarToggle :1; + + ImGuiDockNode(ImGuiID id); + ~ImGuiDockNode(); + bool IsRootNode() const { return ParentNode == NULL; } + bool IsDockSpace() const { return (MergedFlags & ImGuiDockNodeFlags_DockSpace) != 0; } + bool IsFloatingNode() const { return ParentNode == NULL && (MergedFlags & ImGuiDockNodeFlags_DockSpace) == 0; } + bool IsCentralNode() const { return (MergedFlags & ImGuiDockNodeFlags_CentralNode) != 0; } + bool IsHiddenTabBar() const { return (MergedFlags & ImGuiDockNodeFlags_HiddenTabBar) != 0; } // Hidden tab bar can be shown back by clicking the small triangle + bool IsNoTabBar() const { return (MergedFlags & ImGuiDockNodeFlags_NoTabBar) != 0; } // Never show a tab bar + bool IsSplitNode() const { return ChildNodes[0] != NULL; } + bool IsLeafNode() const { return ChildNodes[0] == NULL; } + bool IsEmpty() const { return ChildNodes[0] == NULL && Windows.Size == 0; } + ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + + void SetLocalFlags(ImGuiDockNodeFlags flags) { LocalFlags = flags; UpdateMergedFlags(); } + void UpdateMergedFlags() { MergedFlags = SharedFlags | LocalFlags | LocalFlagsInWindows; } +}; + +// List of colors that are stored at the time of Begin() into Docked Windows. +// We currently store the packed colors in a simple array window->DockStyle.Colors[]. +// A better solution may involve appending into a log of colors in ImGuiContext + store offsets into those arrays in ImGuiWindow, +// but it would be more complex as we'd need to double-buffer both as e.g. drop target may refer to window from last frame. +enum ImGuiWindowDockStyleCol +{ + ImGuiWindowDockStyleCol_Text, + ImGuiWindowDockStyleCol_TabHovered, + ImGuiWindowDockStyleCol_TabFocused, + ImGuiWindowDockStyleCol_TabSelected, + ImGuiWindowDockStyleCol_TabSelectedOverline, + ImGuiWindowDockStyleCol_TabDimmed, + ImGuiWindowDockStyleCol_TabDimmedSelected, + ImGuiWindowDockStyleCol_TabDimmedSelectedOverline, + ImGuiWindowDockStyleCol_COUNT +}; + +// We don't store style.Alpha: dock_node->LastBgColor embeds it and otherwise it would only affect the docking tab, which intuitively I would say we don't want to. +struct ImGuiWindowDockStyle +{ + ImU32 Colors[ImGuiWindowDockStyleCol_COUNT]; +}; + +struct ImGuiDockContext +{ + ImGuiStorage Nodes; // Map ID -> ImGuiDockNode*: Active nodes + ImVector Requests; + ImVector NodesSettings; + bool WantFullRebuild; + ImGuiDockContext() { memset(this, 0, sizeof(*this)); } +}; + +#endif // #ifdef IMGUI_HAS_DOCK + +//----------------------------------------------------------------------------- +// [SECTION] Viewport support +//----------------------------------------------------------------------------- + +// ImGuiViewport Private/Internals fields (cardinal sin: we are using inheritance!) +// Every instance of ImGuiViewport is in fact a ImGuiViewportP. +struct ImGuiViewportP : public ImGuiViewport +{ + ImGuiWindow* Window; // Set when the viewport is owned by a window (and ImGuiViewportFlags_CanHostOtherWindows is NOT set) + int Idx; + int LastFrameActive; // Last frame number this viewport was activated by a window + int LastFocusedStampCount; // Last stamp number from when a window hosted by this viewport was focused (by comparing this value between two viewport we have an implicit viewport z-order we use as fallback) + ImGuiID LastNameHash; + ImVec2 LastPos; + ImVec2 LastSize; + float Alpha; // Window opacity (when dragging dockable windows/viewports we make them transparent) + float LastAlpha; + bool LastFocusedHadNavWindow;// Instead of maintaining a LastFocusedWindow (which may harder to correctly maintain), we merely store weither NavWindow != NULL last time the viewport was focused. + short PlatformMonitor; + int BgFgDrawListsLastFrame[2]; // Last frame number the background (0) and foreground (1) draw lists were used + ImDrawList* BgFgDrawLists[2]; // Convenience background (0) and foreground (1) draw lists. We use them to draw software mouser cursor when io.MouseDrawCursor is set and to draw most debug overlays. + ImDrawData DrawDataP; + ImDrawDataBuilder DrawDataBuilder; // Temporary data while building final ImDrawData + ImVec2 LastPlatformPos; + ImVec2 LastPlatformSize; + ImVec2 LastRendererSize; + + // Per-viewport work area + // - Insets are >= 0.0f values, distance from viewport corners to work area. + // - BeginMainMenuBar() and DockspaceOverViewport() tend to use work area to avoid stepping over existing contents. + // - Generally 'safeAreaInsets' in iOS land, 'DisplayCutout' in Android land. + ImVec2 WorkInsetMin; // Work Area inset locked for the frame. GetWorkRect() always fits within GetMainRect(). + ImVec2 WorkInsetMax; // " + ImVec2 BuildWorkInsetMin; // Work Area inset accumulator for current frame, to become next frame's WorkInset + ImVec2 BuildWorkInsetMax; // " + + ImGuiViewportP() { Window = NULL; Idx = -1; LastFrameActive = BgFgDrawListsLastFrame[0] = BgFgDrawListsLastFrame[1] = LastFocusedStampCount = -1; LastNameHash = 0; Alpha = LastAlpha = 1.0f; LastFocusedHadNavWindow = false; PlatformMonitor = -1; BgFgDrawLists[0] = BgFgDrawLists[1] = NULL; LastPlatformPos = LastPlatformSize = LastRendererSize = ImVec2(FLT_MAX, FLT_MAX); } + ~ImGuiViewportP() { if (BgFgDrawLists[0]) IM_DELETE(BgFgDrawLists[0]); if (BgFgDrawLists[1]) IM_DELETE(BgFgDrawLists[1]); } + void ClearRequestFlags() { PlatformRequestClose = PlatformRequestMove = PlatformRequestResize = false; } + + // Calculate work rect pos/size given a set of offset (we have 1 pair of offset for rect locked from last frame data, and 1 pair for currently building rect) + ImVec2 CalcWorkRectPos(const ImVec2& inset_min) const { return ImVec2(Pos.x + inset_min.x, Pos.y + inset_min.y); } + ImVec2 CalcWorkRectSize(const ImVec2& inset_min, const ImVec2& inset_max) const { return ImVec2(ImMax(0.0f, Size.x - inset_min.x - inset_max.x), ImMax(0.0f, Size.y - inset_min.y - inset_max.y)); } + void UpdateWorkRect() { WorkPos = CalcWorkRectPos(WorkInsetMin); WorkSize = CalcWorkRectSize(WorkInsetMin, WorkInsetMax); } // Update public fields + + // Helpers to retrieve ImRect (we don't need to store BuildWorkRect as every access tend to change it, hence the code asymmetry) + ImRect GetMainRect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + ImRect GetWorkRect() const { return ImRect(WorkPos.x, WorkPos.y, WorkPos.x + WorkSize.x, WorkPos.y + WorkSize.y); } + ImRect GetBuildWorkRect() const { ImVec2 pos = CalcWorkRectPos(BuildWorkInsetMin); ImVec2 size = CalcWorkRectSize(BuildWorkInsetMin, BuildWorkInsetMax); return ImRect(pos.x, pos.y, pos.x + size.x, pos.y + size.y); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Settings support +//----------------------------------------------------------------------------- + +// Windows data saved in imgui.ini file +// Because we never destroy or rename ImGuiWindowSettings, we can store the names in a separate buffer easily. +// (this is designed to be stored in a ImChunkStream buffer, with the variable-length Name following our structure) +struct ImGuiWindowSettings +{ + ImGuiID ID; + ImVec2ih Pos; // NB: Settings position are stored RELATIVE to the viewport! Whereas runtime ones are absolute positions. + ImVec2ih Size; + ImVec2ih ViewportPos; + ImGuiID ViewportId; + ImGuiID DockId; // ID of last known DockNode (even if the DockNode is invisible because it has only 1 active window), or 0 if none. + ImGuiID ClassId; // ID of window class if specified + short DockOrder; // Order of the last time the window was visible within its DockNode. This is used to reorder windows that are reappearing on the same frame. Same value between windows that were active and windows that were none are possible. + bool Collapsed; + bool IsChild; + bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + bool WantDelete; // Set to invalidate/delete the settings entry + + ImGuiWindowSettings() { memset(this, 0, sizeof(*this)); DockOrder = -1; } + char* GetName() { return (char*)(this + 1); } +}; + +struct ImGuiSettingsHandler +{ + const char* TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' + ImGuiID TypeHash; // == ImHashStr(TypeName) + void (*ClearAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Clear all settings data + void (*ReadInitFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called before reading (in registration order) + void* (*ReadOpenFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, const char* name); // Read: Called when entering into a new ini entry e.g. "[Window][Name]" + void (*ReadLineFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line); // Read: Called for every line of text within an ini entry + void (*ApplyAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called after reading (in registration order) + void (*WriteAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf); // Write: Output every entries into 'out_buf' + void* UserData; + + ImGuiSettingsHandler() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Localization support +//----------------------------------------------------------------------------- + +// This is experimental and not officially supported, it'll probably fall short of features, if/when it does we may backtrack. +enum ImGuiLocKey : int +{ + ImGuiLocKey_VersionStr, + ImGuiLocKey_TableSizeOne, + ImGuiLocKey_TableSizeAllFit, + ImGuiLocKey_TableSizeAllDefault, + ImGuiLocKey_TableResetOrder, + ImGuiLocKey_WindowingMainMenuBar, + ImGuiLocKey_WindowingPopup, + ImGuiLocKey_WindowingUntitled, + ImGuiLocKey_OpenLink_s, + ImGuiLocKey_CopyLink, + ImGuiLocKey_DockingHideTabBar, + ImGuiLocKey_DockingHoldShiftToDock, + ImGuiLocKey_DockingDragToUndockOrMoveNode, + ImGuiLocKey_COUNT +}; + +struct ImGuiLocEntry +{ + ImGuiLocKey Key; + const char* Text; +}; + +//----------------------------------------------------------------------------- +// [SECTION] Error handling, State recovery support +//----------------------------------------------------------------------------- + +// Macros used by Recoverable Error handling +// - Only dispatch error if _EXPR: evaluate as assert (similar to an assert macro). +// - The message will always be a string literal, in order to increase likelihood of being display by an assert handler. +// - See 'Demo->Configuration->Error Handling' and ImGuiIO definitions for details on error handling. +// - Read https://github.com/ocornut/imgui/wiki/Error-Handling for details on error handling. +#ifndef IM_ASSERT_USER_ERROR +#define IM_ASSERT_USER_ERROR(_EXPR,_MSG) do { if (!(_EXPR) && ImGui::ErrorLog(_MSG)) { IM_ASSERT((_EXPR) && _MSG); } } while (0) // Recoverable User Error +#endif + +// The error callback is currently not public, as it is expected that only advanced users will rely on it. +typedef void (*ImGuiErrorCallback)(ImGuiContext* ctx, void* user_data, const char* msg); // Function signature for g.ErrorCallback + +//----------------------------------------------------------------------------- +// [SECTION] Metrics, Debug Tools +//----------------------------------------------------------------------------- + +// See IMGUI_DEBUG_LOG() and IMGUI_DEBUG_LOG_XXX() macros. +enum ImGuiDebugLogFlags_ +{ + // Event types + ImGuiDebugLogFlags_None = 0, + ImGuiDebugLogFlags_EventError = 1 << 0, // Error submitted by IM_ASSERT_USER_ERROR() + ImGuiDebugLogFlags_EventActiveId = 1 << 1, + ImGuiDebugLogFlags_EventFocus = 1 << 2, + ImGuiDebugLogFlags_EventPopup = 1 << 3, + ImGuiDebugLogFlags_EventNav = 1 << 4, + ImGuiDebugLogFlags_EventClipper = 1 << 5, + ImGuiDebugLogFlags_EventSelection = 1 << 6, + ImGuiDebugLogFlags_EventIO = 1 << 7, + ImGuiDebugLogFlags_EventFont = 1 << 8, + ImGuiDebugLogFlags_EventInputRouting = 1 << 9, + ImGuiDebugLogFlags_EventDocking = 1 << 10, + ImGuiDebugLogFlags_EventViewport = 1 << 11, + + ImGuiDebugLogFlags_EventMask_ = ImGuiDebugLogFlags_EventError | ImGuiDebugLogFlags_EventActiveId | ImGuiDebugLogFlags_EventFocus | ImGuiDebugLogFlags_EventPopup | ImGuiDebugLogFlags_EventNav | ImGuiDebugLogFlags_EventClipper | ImGuiDebugLogFlags_EventSelection | ImGuiDebugLogFlags_EventIO | ImGuiDebugLogFlags_EventFont | ImGuiDebugLogFlags_EventInputRouting | ImGuiDebugLogFlags_EventDocking | ImGuiDebugLogFlags_EventViewport, + ImGuiDebugLogFlags_OutputToTTY = 1 << 20, // Also send output to TTY + ImGuiDebugLogFlags_OutputToTestEngine = 1 << 21, // Also send output to Test Engine +}; + +struct ImGuiDebugAllocEntry +{ + int FrameCount; + ImS16 AllocCount; + ImS16 FreeCount; +}; + +struct ImGuiDebugAllocInfo +{ + int TotalAllocCount; // Number of call to MemAlloc(). + int TotalFreeCount; + ImS16 LastEntriesIdx; // Current index in buffer + ImGuiDebugAllocEntry LastEntriesBuf[6]; // Track last 6 frames that had allocations + + ImGuiDebugAllocInfo() { memset(this, 0, sizeof(*this)); } +}; + +struct ImGuiMetricsConfig +{ + bool ShowDebugLog = false; + bool ShowIDStackTool = false; + bool ShowWindowsRects = false; + bool ShowWindowsBeginOrder = false; + bool ShowTablesRects = false; + bool ShowDrawCmdMesh = true; + bool ShowDrawCmdBoundingBoxes = true; + bool ShowTextEncodingViewer = false; + bool ShowDockingNodes = false; + int ShowWindowsRectsType = -1; + int ShowTablesRectsType = -1; + int HighlightMonitorIdx = -1; + ImGuiID HighlightViewportID = 0; +}; + +struct ImGuiStackLevelInfo +{ + ImGuiID ID; + ImS8 QueryFrameCount; // >= 1: Query in progress + bool QuerySuccess; // Obtained result from DebugHookIdInfo() + ImGuiDataType DataType : 8; + char Desc[57]; // Arbitrarily sized buffer to hold a result (FIXME: could replace Results[] with a chunk stream?) FIXME: Now that we added CTRL+C this should be fixed. + + ImGuiStackLevelInfo() { memset(this, 0, sizeof(*this)); } +}; + +// State for ID Stack tool queries +struct ImGuiIDStackTool +{ + int LastActiveFrame; + int StackLevel; // -1: query stack and resize Results, >= 0: individual stack level + ImGuiID QueryId; // ID to query details for + ImVector Results; + bool CopyToClipboardOnCtrlC; + float CopyToClipboardLastTime; + ImGuiTextBuffer ResultPathBuf; + + ImGuiIDStackTool() { memset(this, 0, sizeof(*this)); CopyToClipboardLastTime = -FLT_MAX; } +}; + +//----------------------------------------------------------------------------- +// [SECTION] Generic context hooks +//----------------------------------------------------------------------------- + +typedef void (*ImGuiContextHookCallback)(ImGuiContext* ctx, ImGuiContextHook* hook); +enum ImGuiContextHookType { ImGuiContextHookType_NewFramePre, ImGuiContextHookType_NewFramePost, ImGuiContextHookType_EndFramePre, ImGuiContextHookType_EndFramePost, ImGuiContextHookType_RenderPre, ImGuiContextHookType_RenderPost, ImGuiContextHookType_Shutdown, ImGuiContextHookType_PendingRemoval_ }; + +struct ImGuiContextHook +{ + ImGuiID HookId; // A unique ID assigned by AddContextHook() + ImGuiContextHookType Type; + ImGuiID Owner; + ImGuiContextHookCallback Callback; + void* UserData; + + ImGuiContextHook() { memset(this, 0, sizeof(*this)); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGuiContext (main Dear ImGui context) //----------------------------------------------------------------------------- struct ImGuiContext { bool Initialized; - bool FrameScopeActive; // Set by NewFrame(), cleared by EndFrame() - bool FrameScopePushedImplicitWindow; // Set by NewFrame(), cleared by EndFrame() - bool FontAtlasOwnedByContext; // Io.Fonts-> is owned by the ImGuiContext and will be destructed along with it. + bool FontAtlasOwnedByContext; // IO.Fonts-> is owned by the ImGuiContext and will be destructed along with it. ImGuiIO IO; + ImGuiPlatformIO PlatformIO; ImGuiStyle Style; + ImGuiConfigFlags ConfigFlagsCurrFrame; // = g.IO.ConfigFlags at the time of NewFrame() + ImGuiConfigFlags ConfigFlagsLastFrame; ImFont* Font; // (Shortcut) == FontStack.empty() ? IO.Font : FontStack.back() float FontSize; // (Shortcut) == FontBaseSize * g.CurrentWindow->FontWindowScale == window->FontSize(). Text height for current window. float FontBaseSize; // (Shortcut) == IO.FontGlobalScale * Font->Scale * Font->FontSize. Base text height. + float FontScale; // == FontSize / Font->FontSize + float CurrentDpiScale; // Current window/viewport DpiScale == CurrentViewport->DpiScale ImDrawListSharedData DrawListSharedData; double Time; int FrameCount; int FrameCountEnded; + int FrameCountPlatformEnded; int FrameCountRendered; + ImGuiID WithinEndChildID; // Set within EndChild() + bool WithinFrameScope; // Set by NewFrame(), cleared by EndFrame() + bool WithinFrameScopeWithImplicitWindow; // Set by NewFrame(), cleared by EndFrame() when the implicit debug window has been pushed + bool GcCompactAll; // Request full GC + bool TestEngineHookItems; // Will call test engine hooks: ImGuiTestEngineHook_ItemAdd(), ImGuiTestEngineHook_ItemInfo(), ImGuiTestEngineHook_Log() + void* TestEngine; // Test engine user data + char ContextName[16]; // Storage for a context name (to facilitate debugging multi-context setups) + + // Inputs + ImVector InputEventsQueue; // Input events which will be trickled/written into IO structure. + ImVector InputEventsTrail; // Past input events processed in NewFrame(). This is to allow domain-specific application to access e.g mouse/pen trail. + ImGuiMouseSource InputEventsNextMouseSource; + ImU32 InputEventsNextEventId; // Windows state ImVector Windows; // Windows, sorted in display order, back to front - ImVector WindowsFocusOrder; // Windows, sorted in focus order, back to front - ImVector WindowsSortBuffer; - ImVector CurrentWindowStack; - ImGuiStorage WindowsById; - int WindowsActiveCount; - ImGuiWindow* CurrentWindow; // Being drawn into - ImGuiWindow* HoveredWindow; // Will catch mouse inputs - ImGuiWindow* HoveredRootWindow; // Will catch mouse inputs (for focus/move only) - ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actually window that is moved is generally MovingWindow->RootWindow. - ImGuiWindow* WheelingWindow; + ImVector WindowsFocusOrder; // Root windows, sorted in focus order, back to front. + ImVector WindowsTempSortBuffer; // Temporary buffer used in EndFrame() to reorder windows so parents are kept before their child + ImVector CurrentWindowStack; + ImGuiStorage WindowsById; // Map window's ImGuiID to ImGuiWindow* + int WindowsActiveCount; // Number of unique windows submitted by frame + float WindowsBorderHoverPadding; // Padding around resizable windows for which hovering on counts as hovering the window == ImMax(style.TouchExtraPadding, style.WindowBorderHoverPadding). This isn't so multi-dpi friendly. + ImGuiID DebugBreakInWindow; // Set to break in Begin() call. + ImGuiWindow* CurrentWindow; // Window being drawn into + ImGuiWindow* HoveredWindow; // Window the mouse is hovering. Will typically catch mouse inputs. + ImGuiWindow* HoveredWindowUnderMovingWindow; // Hovered window ignoring MovingWindow. Only set if MovingWindow is set. + ImGuiWindow* HoveredWindowBeforeClear; // Window the mouse is hovering. Filled even with _NoMouse. This is currently useful for multi-context compositors. + ImGuiWindow* MovingWindow; // Track the window we clicked on (in order to preserve focus). The actual window that is moved is generally MovingWindow->RootWindowDockTree. + ImGuiWindow* WheelingWindow; // Track the window we started mouse-wheeling on. Until a timer elapse or mouse has moved, generally keep scrolling the same window even if during the course of scrolling the mouse ends up hovering a child window. ImVec2 WheelingWindowRefMousePos; - float WheelingWindowTimer; + int WheelingWindowStartFrame; // This may be set one frame before WheelingWindow is != NULL + int WheelingWindowScrolledFrame; + float WheelingWindowReleaseTimer; + ImVec2 WheelingWindowWheelRemainder; + ImVec2 WheelingAxisAvg; // Item/widgets state and tracking information - ImGuiID HoveredId; // Hovered widget - bool HoveredIdAllowOverlap; + ImGuiID DebugDrawIdConflicts; // Set when we detect multiple items with the same identifier + ImGuiID DebugHookIdInfo; // Will call core hooks: DebugHookIdInfo() from GetID functions, used by ID Stack Tool [next HoveredId/ActiveId to not pull in an extra cache-line] + ImGuiID HoveredId; // Hovered widget, filled during the frame ImGuiID HoveredIdPreviousFrame; + int HoveredIdPreviousFrameItemCount; // Count numbers of items using the same ID as last frame's hovered id float HoveredIdTimer; // Measure contiguous hovering time float HoveredIdNotActiveTimer; // Measure contiguous hovering time where the item has not been active + bool HoveredIdAllowOverlap; + bool HoveredIdIsDisabled; // At least one widget passed the rect test, but has been discarded by disabled flag or popup inhibit. May be true even if HoveredId == 0. + bool ItemUnclipByLog; // Disable ItemAdd() clipping, essentially a memory-locality friendly copy of LogEnabled ImGuiID ActiveId; // Active widget ImGuiID ActiveIdIsAlive; // Active widget has been seen this frame (we can't use a bool as the ActiveId may change within the frame) float ActiveIdTimer; bool ActiveIdIsJustActivated; // Set at the time of activation for one frame bool ActiveIdAllowOverlap; // Active widget allows another widget to steal active id (generally for overlapping widgets, but not always) + bool ActiveIdNoClearOnFocusLoss; // Disable losing active id if the active id window gets unfocused. bool ActiveIdHasBeenPressedBefore; // Track whether the active id led to a press (this is to allow changing between PressOnClick and PressOnRelease without pressing twice). Used by range_select branch. bool ActiveIdHasBeenEditedBefore; // Was the value associated to the widget Edited over the course of the Active state. bool ActiveIdHasBeenEditedThisFrame; - int ActiveIdAllowNavDirFlags; // Active widget allows using directional navigation (e.g. can activate a button and move away from it) - int ActiveIdBlockNavInputFlags; + bool ActiveIdFromShortcut; + int ActiveIdMouseButton : 8; ImVec2 ActiveIdClickOffset; // Clicked offset from upper-left corner, if applicable (currently only set by ButtonBehavior) ImGuiWindow* ActiveIdWindow; - ImGuiInputSource ActiveIdSource; // Activating with mouse or nav (gamepad/keyboard) + ImGuiInputSource ActiveIdSource; // Activating source: ImGuiInputSource_Mouse OR ImGuiInputSource_Keyboard OR ImGuiInputSource_Gamepad ImGuiID ActiveIdPreviousFrame; - bool ActiveIdPreviousFrameIsAlive; - bool ActiveIdPreviousFrameHasBeenEditedBefore; - ImGuiWindow* ActiveIdPreviousFrameWindow; - + ImGuiDeactivatedItemData DeactivatedItemData; + ImGuiDataTypeStorage ActiveIdValueOnActivation; // Backup of initial value at the time of activation. ONLY SET BY SPECIFIC WIDGETS: DragXXX and SliderXXX. ImGuiID LastActiveId; // Store the last non-zero ActiveId, useful for animation. float LastActiveIdTimer; // Store the last non-zero ActiveId timer since the beginning of activation, useful for animation. + // Key/Input Ownership + Shortcut Routing system + // - The idea is that instead of "eating" a given key, we can link to an owner. + // - Input query can then read input by specifying ImGuiKeyOwner_Any (== 0), ImGuiKeyOwner_NoOwner (== -1) or a custom ID. + // - Routing is requested ahead of time for a given chord (Key + Mods) and granted in NewFrame(). + double LastKeyModsChangeTime; // Record the last time key mods changed (affect repeat delay when using shortcut logic) + double LastKeyModsChangeFromNoneTime; // Record the last time key mods changed away from being 0 (affect repeat delay when using shortcut logic) + double LastKeyboardKeyPressTime; // Record the last time a keyboard key (ignore mouse/gamepad ones) was pressed. + ImBitArrayForNamedKeys KeysMayBeCharInput; // Lookup to tell if a key can emit char input, see IsKeyChordPotentiallyCharInput(). sizeof() = 20 bytes + ImGuiKeyOwnerData KeysOwnerData[ImGuiKey_NamedKey_COUNT]; + ImGuiKeyRoutingTable KeysRoutingTable; + ImU32 ActiveIdUsingNavDirMask; // Active widget will want to read those nav move requests (e.g. can activate a button and move away from it) + bool ActiveIdUsingAllKeyboardKeys; // Active widget will want to read all keyboard keys inputs. (this is a shortcut for not taking ownership of 100+ keys, frequently used by drag operations) + ImGuiKeyChord DebugBreakInShortcutRouting; // Set to break in SetShortcutRouting()/Shortcut() calls. + //ImU32 ActiveIdUsingNavInputMask; // [OBSOLETE] Since (IMGUI_VERSION_NUM >= 18804) : 'g.ActiveIdUsingNavInputMask |= (1 << ImGuiNavInput_Cancel);' becomes --> 'SetKeyOwner(ImGuiKey_Escape, g.ActiveId) and/or SetKeyOwner(ImGuiKey_NavGamepadCancel, g.ActiveId);' + // Next window/item data - ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + ImGuiID CurrentFocusScopeId; // Value for currently appending items == g.FocusScopeStack.back(). Not to be mistaken with g.NavFocusScopeId. + ImGuiItemFlags CurrentItemFlags; // Value for currently appending items == g.ItemFlagsStack.back() + ImGuiID DebugLocateId; // Storage for DebugLocateItemOnHover() feature: this is read by ItemAdd() so we keep it in a hot/cached location ImGuiNextItemData NextItemData; // Storage for SetNextItem** functions + ImGuiLastItemData LastItemData; // Storage for last submitted item (setup by ItemAdd) + ImGuiNextWindowData NextWindowData; // Storage for SetNextWindow** functions + bool DebugShowGroupRects; // Shared stacks - ImVector ColorModifiers; // Stack for PushStyleColor()/PopStyleColor() - ImVector StyleModifiers; // Stack for PushStyleVar()/PopStyleVar() - ImVector FontStack; // Stack for PushFont()/PopFont() - ImVectorOpenPopupStack; // Which popups are open (persistent) - ImVectorBeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) + ImGuiCol DebugFlashStyleColorIdx; // (Keep close to ColorStack to share cache line) + ImVector ColorStack; // Stack for PushStyleColor()/PopStyleColor() - inherited by Begin() + ImVector StyleVarStack; // Stack for PushStyleVar()/PopStyleVar() - inherited by Begin() + ImVector FontStack; // Stack for PushFont()/PopFont() - inherited by Begin() + ImVector FocusScopeStack; // Stack for PushFocusScope()/PopFocusScope() - inherited by BeginChild(), pushed into by Begin() + ImVector ItemFlagsStack; // Stack for PushItemFlag()/PopItemFlag() - inherited by Begin() + ImVector GroupStack; // Stack for BeginGroup()/EndGroup() - not inherited by Begin() + ImVector OpenPopupStack; // Which popups are open (persistent) + ImVector BeginPopupStack; // Which level of BeginPopup() we are in (reset every frame) + ImVectorTreeNodeStack; // Stack for TreeNode() - // Navigation data (for gamepad/keyboard) - ImGuiWindow* NavWindow; // Focused window for navigation. Could be called 'FocusWindow' + // Viewports + ImVector Viewports; // Active viewports (always 1+, and generally 1 unless multi-viewports are enabled). Each viewports hold their copy of ImDrawData. + ImGuiViewportP* CurrentViewport; // We track changes of viewport (happening in Begin) so we can call Platform_OnChangedViewport() + ImGuiViewportP* MouseViewport; + ImGuiViewportP* MouseLastHoveredViewport; // Last known viewport that was hovered by mouse (even if we are not hovering any viewport any more) + honoring the _NoInputs flag. + ImGuiID PlatformLastFocusedViewportId; + ImGuiPlatformMonitor FallbackMonitor; // Virtual monitor used as fallback if backend doesn't provide monitor information. + ImRect PlatformMonitorsFullWorkRect; // Bounding box of all platform monitors + int ViewportCreatedCount; // Unique sequential creation counter (mostly for testing/debugging) + int PlatformWindowsCreatedCount; // Unique sequential creation counter (mostly for testing/debugging) + int ViewportFocusedStampCount; // Every time the front-most window changes, we stamp its viewport with an incrementing counter + + // Keyboard/Gamepad Navigation + bool NavCursorVisible; // Nav focus cursor/rectangle is visible? We hide it after a mouse click. We show it after a nav move. + bool NavHighlightItemUnderNav; // Disable mouse hovering highlight. Highlight navigation focused item instead of mouse hovered item. + //bool NavDisableHighlight; // Old name for !g.NavCursorVisible before 1.91.4 (2024/10/18). OPPOSITE VALUE (g.NavDisableHighlight == !g.NavCursorVisible) + //bool NavDisableMouseHover; // Old name for g.NavHighlightItemUnderNav before 1.91.1 (2024/10/18) this was called When user starts using keyboard/gamepad, we hide mouse hovering highlight until mouse is touched again. + bool NavMousePosDirty; // When set we will update mouse position if io.ConfigNavMoveSetMousePos is set (not enabled by default) + bool NavIdIsAlive; // Nav widget has been seen this frame ~~ NavRectRel is valid ImGuiID NavId; // Focused item for navigation - ImGuiID NavActivateId; // ~~ (g.ActiveId == 0) && IsNavInputPressed(ImGuiNavInput_Activate) ? NavId : 0, also set when calling ActivateItem() - ImGuiID NavActivateDownId; // ~~ IsNavInputDown(ImGuiNavInput_Activate) ? NavId : 0 - ImGuiID NavActivatePressedId; // ~~ IsNavInputPressed(ImGuiNavInput_Activate) ? NavId : 0 - ImGuiID NavInputId; // ~~ IsNavInputPressed(ImGuiNavInput_Input) ? NavId : 0 - ImGuiID NavJustTabbedId; // Just tabbed to this id. - ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest). - ImGuiID NavJustMovedToMultiSelectScopeId; // Just navigated to this select scope id (result of a successfully MoveRequest). + ImGuiWindow* NavWindow; // Focused window for navigation. Could be called 'FocusedWindow' + ImGuiID NavFocusScopeId; // Focused focus scope (e.g. selection code often wants to "clear other items" when landing on an item of the same scope) + ImGuiNavLayer NavLayer; // Focused layer (main scrolling layer, or menu/title bar layer) + ImGuiID NavActivateId; // ~~ (g.ActiveId == 0) && (IsKeyPressed(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate)) ? NavId : 0, also set when calling ActivateItem() + ImGuiID NavActivateDownId; // ~~ IsKeyDown(ImGuiKey_Space) || IsKeyDown(ImGuiKey_Enter) || IsKeyDown(ImGuiKey_NavGamepadActivate) ? NavId : 0 + ImGuiID NavActivatePressedId; // ~~ IsKeyPressed(ImGuiKey_Space) || IsKeyPressed(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_NavGamepadActivate) ? NavId : 0 (no repeat) + ImGuiActivateFlags NavActivateFlags; + ImVector NavFocusRoute; // Reversed copy focus scope stack for NavId (should contains NavFocusScopeId). This essentially follow the window->ParentWindowForFocusRoute chain. + ImGuiID NavHighlightActivatedId; + float NavHighlightActivatedTimer; ImGuiID NavNextActivateId; // Set by ActivateItem(), queued until next frame. - ImGuiInputSource NavInputSource; // Keyboard or Gamepad mode? THIS WILL ONLY BE None or NavGamepad or NavKeyboard. - ImRect NavScoringRectScreen; // Rectangle used for scoring, in screen space. Based of window->DC.NavRefRectRel[], modified for directional navigation scoring. - int NavScoringCount; // Metrics for debugging - ImGuiWindow* NavWindowingTarget; // When selecting a window (holding Menu+FocusPrev/Next, or equivalent of CTRL-TAB) this window is temporarily displayed top-most. - ImGuiWindow* NavWindowingTargetAnim; // Record of last valid NavWindowingTarget until DimBgRatio and NavWindowingHighlightAlpha becomes 0.0f - ImGuiWindow* NavWindowingList; + ImGuiActivateFlags NavNextActivateFlags; + ImGuiInputSource NavInputSource; // Keyboard or Gamepad mode? THIS CAN ONLY BE ImGuiInputSource_Keyboard or ImGuiInputSource_Mouse + ImGuiSelectionUserData NavLastValidSelectionUserData; // Last valid data passed to SetNextItemSelectionUser(), or -1. For current window. Not reset when focusing an item that doesn't have selection data. + ImS8 NavCursorHideFrames; + + // Navigation: Init & Move Requests + bool NavAnyRequest; // ~~ NavMoveRequest || NavInitRequest this is to perform early out in ItemAdd() + bool NavInitRequest; // Init request for appearing window to select first item + bool NavInitRequestFromMove; + ImGuiNavItemData NavInitResult; // Init request result (first item of the window, or one for which SetItemDefaultFocus() was called) + bool NavMoveSubmitted; // Move request submitted, will process result on next NewFrame() + bool NavMoveScoringItems; // Move request submitted, still scoring incoming items + bool NavMoveForwardToNextFrame; + ImGuiNavMoveFlags NavMoveFlags; + ImGuiScrollFlags NavMoveScrollFlags; + ImGuiKeyChord NavMoveKeyMods; + ImGuiDir NavMoveDir; // Direction of the move request (left/right/up/down) + ImGuiDir NavMoveDirForDebug; + ImGuiDir NavMoveClipDir; // FIXME-NAV: Describe the purpose of this better. Might want to rename? + ImRect NavScoringRect; // Rectangle used for scoring, in screen space. Based of window->NavRectRel[], modified for directional navigation scoring. + ImRect NavScoringNoClipRect; // Some nav operations (such as PageUp/PageDown) enforce a region which clipper will attempt to always keep submitted + int NavScoringDebugCount; // Metrics for debugging + int NavTabbingDir; // Generally -1 or +1, 0 when tabbing without a nav id + int NavTabbingCounter; // >0 when counting items for tabbing + ImGuiNavItemData NavMoveResultLocal; // Best move request candidate within NavWindow + ImGuiNavItemData NavMoveResultLocalVisible; // Best move request candidate within NavWindow that are mostly visible (when using ImGuiNavMoveFlags_AlsoScoreVisibleSet flag) + ImGuiNavItemData NavMoveResultOther; // Best move request candidate within NavWindow's flattened hierarchy (when using ImGuiWindowFlags_NavFlattened flag) + ImGuiNavItemData NavTabbingResultFirst; // First tabbing request candidate within NavWindow and flattened hierarchy + + // Navigation: record of last move request + ImGuiID NavJustMovedFromFocusScopeId; // Just navigated from this focus scope id (result of a successfully MoveRequest). + ImGuiID NavJustMovedToId; // Just navigated to this id (result of a successfully MoveRequest). + ImGuiID NavJustMovedToFocusScopeId; // Just navigated to this focus scope id (result of a successfully MoveRequest). + ImGuiKeyChord NavJustMovedToKeyMods; + bool NavJustMovedToIsTabbing; // Copy of ImGuiNavMoveFlags_IsTabbing. Maybe we should store whole flags. + bool NavJustMovedToHasSelectionData; // Copy of move result's ItemFlags & ImGuiItemFlags_HasSelectionUserData). Maybe we should just store ImGuiNavItemData. + + // Navigation: Windowing (CTRL+TAB for list, or Menu button + keys or directional pads to move/resize) + ImGuiKeyChord ConfigNavWindowingKeyNext; // = ImGuiMod_Ctrl | ImGuiKey_Tab (or ImGuiMod_Super | ImGuiKey_Tab on OS X). For reconfiguration (see #4828) + ImGuiKeyChord ConfigNavWindowingKeyPrev; // = ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Tab (or ImGuiMod_Super | ImGuiMod_Shift | ImGuiKey_Tab on OS X) + ImGuiWindow* NavWindowingTarget; // Target window when doing CTRL+Tab (or Pad Menu + FocusPrev/Next), this window is temporarily displayed top-most! + ImGuiWindow* NavWindowingTargetAnim; // Record of last valid NavWindowingTarget until DimBgRatio and NavWindowingHighlightAlpha becomes 0.0f, so the fade-out can stay on it. + ImGuiWindow* NavWindowingListWindow; // Internal window actually listing the CTRL+Tab contents float NavWindowingTimer; float NavWindowingHighlightAlpha; bool NavWindowingToggleLayer; - ImGuiNavLayer NavLayer; // Layer we are navigating on. For now the system is hard-coded for 0=main contents and 1=menu/title bar, may expose layers later. - int NavIdTabCounter; // == NavWindow->DC.FocusIdxTabCounter at time of NavId processing - bool NavIdIsAlive; // Nav widget has been seen this frame ~~ NavRefRectRel is valid - bool NavMousePosDirty; // When set we will update mouse position if (io.ConfigFlags & ImGuiConfigFlags_NavEnableSetMousePos) if set (NB: this not enabled by default) - bool NavDisableHighlight; // When user starts using mouse, we hide gamepad/keyboard highlight (NB: but they are still available, which is why NavDisableHighlight isn't always != NavDisableMouseHover) - bool NavDisableMouseHover; // When user starts using gamepad/keyboard, we hide mouse hovering highlight until mouse is touched again. - bool NavAnyRequest; // ~~ NavMoveRequest || NavInitRequest - bool NavInitRequest; // Init request for appearing window to select first item - bool NavInitRequestFromMove; - ImGuiID NavInitResultId; - ImRect NavInitResultRectRel; - bool NavMoveFromClampedRefRect; // Set by manual scrolling, if we scroll to a point where NavId isn't visible we reset navigation from visible items - bool NavMoveRequest; // Move request for this frame - ImGuiNavMoveFlags NavMoveRequestFlags; - ImGuiNavForward NavMoveRequestForward; // None / ForwardQueued / ForwardActive (this is used to navigate sibling parent menus from a child menu) - ImGuiDir NavMoveDir, NavMoveDirLast; // Direction of the move request (left/right/up/down), direction of the previous move request - ImGuiDir NavMoveClipDir; // FIXME-NAV: Describe the purpose of this better. Might want to rename? - ImGuiNavMoveResult NavMoveResultLocal; // Best move request candidate within NavWindow - ImGuiNavMoveResult NavMoveResultLocalVisibleSet; // Best move request candidate within NavWindow that are mostly visible (when using ImGuiNavMoveFlags_AlsoScoreVisibleSet flag) - ImGuiNavMoveResult NavMoveResultOther; // Best move request candidate within NavWindow's flattened hierarchy (when using ImGuiWindowFlags_NavFlattened flag) - - // Tabbing system (older than Nav, active even if Nav is disabled. FIXME-NAV: This needs a redesign!) - ImGuiWindow* FocusRequestCurrWindow; // - ImGuiWindow* FocusRequestNextWindow; // - int FocusRequestCurrCounterAll; // Any item being requested for focus, stored as an index (we on layout to be stable between the frame pressing TAB and the next frame, semi-ouch) - int FocusRequestCurrCounterTab; // Tab item being requested for focus, stored as an index - int FocusRequestNextCounterAll; // Stored for next frame - int FocusRequestNextCounterTab; // " - bool FocusTabPressed; // + ImGuiKey NavWindowingToggleKey; + ImVec2 NavWindowingAccumDeltaPos; + ImVec2 NavWindowingAccumDeltaSize; // Render - ImDrawData DrawData; // Main ImDrawData instance to pass render information to the user - ImDrawDataBuilder DrawDataBuilder; float DimBgRatio; // 0.0..1.0 animation when fading in a dimming background (for modal window and CTRL+TAB list) - ImDrawList BackgroundDrawList; // First draw list to be rendered. - ImDrawList ForegroundDrawList; // Last draw list to be rendered. This is where we the render software mouse cursor (if io.MouseDrawCursor is set) and most debug overlays. - ImGuiMouseCursor MouseCursor; // Drag and Drop bool DragDropActive; - bool DragDropWithinSourceOrTarget; + bool DragDropWithinSource; // Set when within a BeginDragDropXXX/EndDragDropXXX block for a drag source. + bool DragDropWithinTarget; // Set when within a BeginDragDropXXX/EndDragDropXXX block for a drag target. ImGuiDragDropFlags DragDropSourceFlags; int DragDropSourceFrameCount; int DragDropMouseButton; ImGuiPayload DragDropPayload; - ImRect DragDropTargetRect; + ImRect DragDropTargetRect; // Store rectangle of current target candidate (we favor small targets when overlapping) + ImRect DragDropTargetClipRect; // Store ClipRect at the time of item's drawing ImGuiID DragDropTargetId; ImGuiDragDropFlags DragDropAcceptFlags; float DragDropAcceptIdCurrRectSurface; // Target item surface (we resolve overlapping targets by prioritizing the smaller surface) ImGuiID DragDropAcceptIdCurr; // Target item id (set at the time of accepting the payload) ImGuiID DragDropAcceptIdPrev; // Target item id from previous frame (we need to store this to allow for overlapping drag and drop targets) int DragDropAcceptFrameCount; // Last time a target expressed a desire to accept the source - ImVector DragDropPayloadBufHeap; // We don't expose the ImVector<> directly - unsigned char DragDropPayloadBufLocal[8]; // Local buffer for small payloads + ImGuiID DragDropHoldJustPressedId; // Set when holding a payload just made ButtonBehavior() return a press. + ImVector DragDropPayloadBufHeap; // We don't expose the ImVector<> directly, ImGuiPayload only holds pointer+size + unsigned char DragDropPayloadBufLocal[16]; // Local buffer for small payloads + + // Clipper + int ClipperTempDataStacked; + ImVector ClipperTempData; + + // Tables + ImGuiTable* CurrentTable; + ImGuiID DebugBreakInTable; // Set to break in BeginTable() call. + int TablesTempDataStacked; // Temporary table data size (because we leave previous instances undestructed, we generally don't use TablesTempData.Size) + ImVector TablesTempData; // Temporary table data (buffers reused/shared across instances, support nesting) + ImPool Tables; // Persistent table data + ImVector TablesLastTimeActive; // Last used timestamp of each tables (SOA, for efficient GC) + ImVector DrawChannelsTempMergeBuffer; // Tab bars ImGuiTabBar* CurrentTabBar; @@ -1005,361 +2499,337 @@ struct ImGuiContext ImVector CurrentTabBarStack; ImVector ShrinkWidthBuffer; + // Multi-Select state + ImGuiBoxSelectState BoxSelectState; + ImGuiMultiSelectTempData* CurrentMultiSelect; + int MultiSelectTempDataStacked; // Temporary multi-select data size (because we leave previous instances undestructed, we generally don't use MultiSelectTempData.Size) + ImVector MultiSelectTempData; + ImPool MultiSelectStorage; + + // Hover Delay system + ImGuiID HoverItemDelayId; + ImGuiID HoverItemDelayIdPreviousFrame; + float HoverItemDelayTimer; // Currently used by IsItemHovered() + float HoverItemDelayClearTimer; // Currently used by IsItemHovered(): grace time before g.TooltipHoverTimer gets cleared. + ImGuiID HoverItemUnlockedStationaryId; // Mouse has once been stationary on this item. Only reset after departing the item. + ImGuiID HoverWindowUnlockedStationaryId; // Mouse has once been stationary on this window. Only reset after departing the window. + + // Mouse state + ImGuiMouseCursor MouseCursor; + float MouseStationaryTimer; // Time the mouse has been stationary (with some loose heuristic) + ImVec2 MouseLastValidPos; + // Widget state - ImVec2 LastValidMousePos; ImGuiInputTextState InputTextState; + ImGuiInputTextDeactivatedState InputTextDeactivatedState; ImFont InputTextPasswordFont; - ImGuiID TempInputTextId; // Temporary text input when CTRL+clicking on a slider, etc. + ImGuiID TempInputId; // Temporary text input when CTRL+clicking on a slider, etc. + ImGuiDataTypeStorage DataTypeZeroValue; // 0 for all data types + int BeginMenuDepth; + int BeginComboDepth; ImGuiColorEditFlags ColorEditOptions; // Store user options for color edit widgets - float ColorEditLastHue; - float ColorEditLastColor[3]; - ImVec4 ColorPickerRef; + ImGuiID ColorEditCurrentID; // Set temporarily while inside of the parent-most ColorEdit4/ColorPicker4 (because they call each others). + ImGuiID ColorEditSavedID; // ID we are saving/restoring HS for + float ColorEditSavedHue; // Backup of last Hue associated to LastColor, so we can restore Hue in lossy RGB<>HSV round trips + float ColorEditSavedSat; // Backup of last Saturation associated to LastColor, so we can restore Saturation in lossy RGB<>HSV round trips + ImU32 ColorEditSavedColor; // RGB value with alpha set to 0. + ImVec4 ColorPickerRef; // Initial/reference color at the time of opening the color picker. + ImGuiComboPreviewData ComboPreviewData; + ImRect WindowResizeBorderExpectedRect; // Expected border rect, switch to relative edit if moving + bool WindowResizeRelativeMode; + short ScrollbarSeekMode; // 0: scroll to clicked location, -1/+1: prev/next page. + float ScrollbarClickDeltaToGrabCenter; // When scrolling to mouse location: distance between mouse and center of grab box, normalized in parent space. + float SliderGrabClickOffset; + float SliderCurrentAccum; // Accumulated slider delta when using navigation controls. + bool SliderCurrentAccumDirty; // Has the accumulated slider delta changed since last time we tried to apply it? bool DragCurrentAccumDirty; float DragCurrentAccum; // Accumulator for dragging modification. Always high-precision, not rounded by end-user precision settings float DragSpeedDefaultRatio; // If speed == 0.0f, uses (max-min) * DragSpeedDefaultRatio - float ScrollbarClickDeltaToGrabCenter; // Distance between mouse and center of grab box, normalized in parent space. Use storage? - int TooltipOverrideCount; - ImVector PrivateClipboard; // If no custom clipboard handler is defined - - // Range-Select/Multi-Select - // [This is unused in this branch, but left here to facilitate merging/syncing multiple branches] - ImGuiID MultiSelectScopeId; + float DisabledAlphaBackup; // Backup for style.Alpha for BeginDisabled() + short DisabledStackSize; + short TooltipOverrideCount; + ImGuiWindow* TooltipPreviousWindow; // Window of last tooltip submitted during the frame + ImVector ClipboardHandlerData; // If no custom clipboard handler is defined + ImVector MenusIdSubmittedThisFrame; // A list of menu IDs that were rendered at least once + ImGuiTypingSelectState TypingSelectState; // State for GetTypingSelectRequest() // Platform support - ImVec2 PlatformImePos; // Cursor position request & last passed to the OS Input Method Editor - ImVec2 PlatformImeLastPos; + ImGuiPlatformImeData PlatformImeData; // Data updated by current frame + ImGuiPlatformImeData PlatformImeDataPrev; // Previous frame data. When changed we call the platform_io.Platform_SetImeDataFn() handler. + ImGuiID PlatformImeViewport; + + // Extensions + // FIXME: We could provide an API to register one slot in an array held in ImGuiContext? + ImGuiDockContext DockContext; + void (*DockNodeWindowMenuHandler)(ImGuiContext* ctx, ImGuiDockNode* node, ImGuiTabBar* tab_bar); // Settings - bool SettingsLoaded; - float SettingsDirtyTimer; // Save .ini Settings to memory when time reaches zero - ImGuiTextBuffer SettingsIniData; // In memory .ini settings - ImVector SettingsHandlers; // List of .ini settings handlers - ImVector SettingsWindows; // ImGuiWindow .ini settings entries (parsed from the last loaded .ini file and maintained on saving) + bool SettingsLoaded; + float SettingsDirtyTimer; // Save .ini Settings to memory when time reaches zero + ImGuiTextBuffer SettingsIniData; // In memory .ini settings + ImVector SettingsHandlers; // List of .ini settings handlers + ImChunkStream SettingsWindows; // ImGuiWindow .ini settings entries + ImChunkStream SettingsTables; // ImGuiTable .ini settings entries + ImVector Hooks; // Hooks for extensions (e.g. test engine) + ImGuiID HookIdNext; // Next available HookId - // Logging - bool LogEnabled; - ImGuiLogType LogType; - FILE* LogFile; // If != NULL log to stdout/ file + // Localization + const char* LocalizationTable[ImGuiLocKey_COUNT]; + + // Capture/Logging + bool LogEnabled; // Currently capturing + ImGuiLogFlags LogFlags; // Capture flags/type + ImGuiWindow* LogWindow; + ImFileHandle LogFile; // If != NULL log to stdout/ file ImGuiTextBuffer LogBuffer; // Accumulation buffer when log to clipboard. This is pointer so our GImGui static constructor doesn't call heap allocators. + const char* LogNextPrefix; + const char* LogNextSuffix; float LogLinePosY; bool LogLineFirstItem; int LogDepthRef; int LogDepthToExpand; int LogDepthToExpandDefault; // Default/stored value for LogDepthMaxExpand if not specified in the LogXXX function call. + // Error Handling + ImGuiErrorCallback ErrorCallback; // = NULL. May be exposed in public API eventually. + void* ErrorCallbackUserData; // = NULL + ImVec2 ErrorTooltipLockedPos; + bool ErrorFirst; + int ErrorCountCurrentFrame; // [Internal] Number of errors submitted this frame. + ImGuiErrorRecoveryState StackSizesInNewFrame; // [Internal] + ImGuiErrorRecoveryState*StackSizesInBeginForCurrentWindow; // [Internal] + // Debug Tools - bool DebugItemPickerActive; - ImGuiID DebugItemPickerBreakID; // Will call IM_DEBUG_BREAK() when encountering this id + // (some of the highly frequently used data are interleaved in other structures above: DebugBreakXXX fields, DebugHookIdInfo, DebugLocateId etc.) + int DebugDrawIdConflictsCount; // Locked count (preserved when holding CTRL) + ImGuiDebugLogFlags DebugLogFlags; + ImGuiTextBuffer DebugLogBuf; + ImGuiTextIndex DebugLogIndex; + int DebugLogSkippedErrors; + ImGuiDebugLogFlags DebugLogAutoDisableFlags; + ImU8 DebugLogAutoDisableFrames; + ImU8 DebugLocateFrames; // For DebugLocateItemOnHover(). This is used together with DebugLocateId which is in a hot/cached spot above. + bool DebugBreakInLocateId; // Debug break in ItemAdd() call for g.DebugLocateId. + ImGuiKeyChord DebugBreakKeyChord; // = ImGuiKey_Pause + ImS8 DebugBeginReturnValueCullDepth; // Cycle between 0..9 then wrap around. + bool DebugItemPickerActive; // Item picker is active (started with DebugStartItemPicker()) + ImU8 DebugItemPickerMouseButton; + ImGuiID DebugItemPickerBreakId; // Will call IM_DEBUG_BREAK() when encountering this ID + float DebugFlashStyleColorTime; + ImVec4 DebugFlashStyleColorBackup; + ImGuiMetricsConfig DebugMetricsConfig; + ImGuiIDStackTool DebugIDStackTool; + ImGuiDebugAllocInfo DebugAllocInfo; + ImGuiDockNode* DebugHoveredDockNode; // Hovered dock node. // Misc - float FramerateSecPerFrame[120]; // Calculate estimate of framerate for user over the last 2 seconds. + float FramerateSecPerFrame[60]; // Calculate estimate of framerate for user over the last 60 frames.. int FramerateSecPerFrameIdx; + int FramerateSecPerFrameCount; float FramerateSecPerFrameAccum; - int WantCaptureMouseNextFrame; // Explicit capture via CaptureKeyboardFromApp()/CaptureMouseFromApp() sets those flags - int WantCaptureKeyboardNextFrame; + int WantCaptureMouseNextFrame; // Explicit capture override via SetNextFrameWantCaptureMouse()/SetNextFrameWantCaptureKeyboard(). Default to -1. + int WantCaptureKeyboardNextFrame; // " int WantTextInputNextFrame; - char TempBuffer[1024*3+1]; // Temporary text buffer + ImVector TempBuffer; // Temporary text buffer + char TempKeychordName[64]; - ImGuiContext(ImFontAtlas* shared_font_atlas) : BackgroundDrawList(&DrawListSharedData), ForegroundDrawList(&DrawListSharedData) - { - Initialized = false; - FrameScopeActive = FrameScopePushedImplicitWindow = false; - Font = NULL; - FontSize = FontBaseSize = 0.0f; - FontAtlasOwnedByContext = shared_font_atlas ? false : true; - IO.Fonts = shared_font_atlas ? shared_font_atlas : IM_NEW(ImFontAtlas)(); - Time = 0.0f; - FrameCount = 0; - FrameCountEnded = FrameCountRendered = -1; - - WindowsActiveCount = 0; - CurrentWindow = NULL; - HoveredWindow = NULL; - HoveredRootWindow = NULL; - MovingWindow = NULL; - WheelingWindow = NULL; - WheelingWindowTimer = 0.0f; - - HoveredId = 0; - HoveredIdAllowOverlap = false; - HoveredIdPreviousFrame = 0; - HoveredIdTimer = HoveredIdNotActiveTimer = 0.0f; - ActiveId = 0; - ActiveIdIsAlive = 0; - ActiveIdTimer = 0.0f; - ActiveIdIsJustActivated = false; - ActiveIdAllowOverlap = false; - ActiveIdHasBeenPressedBefore = false; - ActiveIdHasBeenEditedBefore = false; - ActiveIdHasBeenEditedThisFrame = false; - ActiveIdAllowNavDirFlags = 0x00; - ActiveIdBlockNavInputFlags = 0x00; - ActiveIdClickOffset = ImVec2(-1,-1); - ActiveIdWindow = NULL; - ActiveIdSource = ImGuiInputSource_None; - - ActiveIdPreviousFrame = 0; - ActiveIdPreviousFrameIsAlive = false; - ActiveIdPreviousFrameHasBeenEditedBefore = false; - ActiveIdPreviousFrameWindow = NULL; - - LastActiveId = 0; - LastActiveIdTimer = 0.0f; - - NavWindow = NULL; - NavId = NavActivateId = NavActivateDownId = NavActivatePressedId = NavInputId = 0; - NavJustTabbedId = NavJustMovedToId = NavJustMovedToMultiSelectScopeId = NavNextActivateId = 0; - NavInputSource = ImGuiInputSource_None; - NavScoringRectScreen = ImRect(); - NavScoringCount = 0; - NavWindowingTarget = NavWindowingTargetAnim = NavWindowingList = NULL; - NavWindowingTimer = NavWindowingHighlightAlpha = 0.0f; - NavWindowingToggleLayer = false; - NavLayer = ImGuiNavLayer_Main; - NavIdTabCounter = INT_MAX; - NavIdIsAlive = false; - NavMousePosDirty = false; - NavDisableHighlight = true; - NavDisableMouseHover = false; - NavAnyRequest = false; - NavInitRequest = false; - NavInitRequestFromMove = false; - NavInitResultId = 0; - NavMoveFromClampedRefRect = false; - NavMoveRequest = false; - NavMoveRequestFlags = 0; - NavMoveRequestForward = ImGuiNavForward_None; - NavMoveDir = NavMoveDirLast = NavMoveClipDir = ImGuiDir_None; - - FocusRequestCurrWindow = FocusRequestNextWindow = NULL; - FocusRequestCurrCounterAll = FocusRequestCurrCounterTab = INT_MAX; - FocusRequestNextCounterAll = FocusRequestNextCounterTab = INT_MAX; - FocusTabPressed = false; - - DimBgRatio = 0.0f; - BackgroundDrawList._OwnerName = "##Background"; // Give it a name for debugging - ForegroundDrawList._OwnerName = "##Foreground"; // Give it a name for debugging - MouseCursor = ImGuiMouseCursor_Arrow; - - DragDropActive = DragDropWithinSourceOrTarget = false; - DragDropSourceFlags = 0; - DragDropSourceFrameCount = -1; - DragDropMouseButton = -1; - DragDropTargetId = 0; - DragDropAcceptFlags = 0; - DragDropAcceptIdCurrRectSurface = 0.0f; - DragDropAcceptIdPrev = DragDropAcceptIdCurr = 0; - DragDropAcceptFrameCount = -1; - memset(DragDropPayloadBufLocal, 0, sizeof(DragDropPayloadBufLocal)); - - CurrentTabBar = NULL; - - LastValidMousePos = ImVec2(0.0f, 0.0f); - TempInputTextId = 0; - ColorEditOptions = ImGuiColorEditFlags__OptionsDefault; - ColorEditLastHue = 0.0f; - ColorEditLastColor[0] = ColorEditLastColor[1] = ColorEditLastColor[2] = FLT_MAX; - DragCurrentAccumDirty = false; - DragCurrentAccum = 0.0f; - DragSpeedDefaultRatio = 1.0f / 100.0f; - ScrollbarClickDeltaToGrabCenter = 0.0f; - TooltipOverrideCount = 0; - - MultiSelectScopeId = 0; - - PlatformImePos = PlatformImeLastPos = ImVec2(FLT_MAX, FLT_MAX); - - SettingsLoaded = false; - SettingsDirtyTimer = 0.0f; - - LogEnabled = false; - LogType = ImGuiLogType_None; - LogFile = NULL; - LogLinePosY = FLT_MAX; - LogLineFirstItem = false; - LogDepthRef = 0; - LogDepthToExpand = LogDepthToExpandDefault = 2; - - DebugItemPickerActive = false; - DebugItemPickerBreakID = 0; - - memset(FramerateSecPerFrame, 0, sizeof(FramerateSecPerFrame)); - FramerateSecPerFrameIdx = 0; - FramerateSecPerFrameAccum = 0.0f; - WantCaptureMouseNextFrame = WantCaptureKeyboardNextFrame = WantTextInputNextFrame = -1; - memset(TempBuffer, 0, sizeof(TempBuffer)); - } + ImGuiContext(ImFontAtlas* shared_font_atlas); }; //----------------------------------------------------------------------------- -// ImGuiWindow +// [SECTION] ImGuiWindowTempData, ImGuiWindow //----------------------------------------------------------------------------- // Transient per-window data, reset at the beginning of the frame. This used to be called ImGuiDrawContext, hence the DC variable name in ImGuiWindow. -// FIXME: That's theory, in practice the delimitation between ImGuiWindow and ImGuiWindowTempData is quite tenuous and could be reconsidered. +// (That's theory, in practice the delimitation between ImGuiWindow and ImGuiWindowTempData is quite tenuous and could be reconsidered..) +// (This doesn't need a constructor because we zero-clear it as part of ImGuiWindow and all frame-temporary data are setup on Begin) struct IMGUI_API ImGuiWindowTempData { + // Layout ImVec2 CursorPos; // Current emitting position, in absolute coordinates. ImVec2 CursorPosPrevLine; ImVec2 CursorStartPos; // Initial position after Begin(), generally ~ window position + WindowPadding. - ImVec2 CursorMaxPos; // Used to implicitly calculate the size of our contents, always growing during the frame. Used to calculate window->ContentSize at the beginning of next frame + ImVec2 CursorMaxPos; // Used to implicitly calculate ContentSize at the beginning of next frame, for scrolling range and auto-resize. Always growing during the frame. + ImVec2 IdealMaxPos; // Used to implicitly calculate ContentSizeIdeal at the beginning of next frame, for auto-resize only. Always growing during the frame. ImVec2 CurrLineSize; ImVec2 PrevLineSize; float CurrLineTextBaseOffset; // Baseline offset (0.0f by default on a new line, generally == style.FramePadding.y when a framed item has been added). float PrevLineTextBaseOffset; - int TreeDepth; // Current tree depth. - ImU32 TreeMayJumpToParentOnPopMask; // Store a copy of !g.NavIdIsAlive for TreeDepth 0..31.. Could be turned into a ImU64 if necessary. - ImGuiID LastItemId; // ID for last item - ImGuiItemStatusFlags LastItemStatusFlags; // Status flags for last item (see ImGuiItemStatusFlags_) - ImRect LastItemRect; // Interaction rect for last item - ImRect LastItemDisplayRect; // End-user display rect for last item (only valid if LastItemStatusFlags & ImGuiItemStatusFlags_HasDisplayRect) + bool IsSameLine; + bool IsSetPos; + ImVec1 Indent; // Indentation / start position from left of window (increased by TreePush/TreePop, etc.) + ImVec1 ColumnsOffset; // Offset to the current column (if ColumnsCurrent > 0). FIXME: This and the above should be a stack to allow use cases like Tree->Column->Tree. Need revamp columns API. + ImVec1 GroupOffset; + ImVec2 CursorStartPosLossyness;// Record the loss of precision of CursorStartPos due to really large scrolling amount. This is used by clipper to compensate and fix the most common use case of large scroll area. + + // Keyboard/Gamepad navigation ImGuiNavLayer NavLayerCurrent; // Current layer, 0..31 (we currently only use 0..1) - int NavLayerCurrentMask; // = (1 << NavLayerCurrent) used by ItemAdd prior to clipping. - int NavLayerActiveMask; // Which layer have been written to (result from previous frame) - int NavLayerActiveMaskNext; // Which layer have been written to (buffer for current frame) + short NavLayersActiveMask; // Which layers have been written to (result from previous frame) + short NavLayersActiveMaskNext;// Which layers have been written to (accumulator for current frame) + bool NavIsScrollPushableX; // Set when current work location may be scrolled horizontally when moving left / right. This is generally always true UNLESS within a column. bool NavHideHighlightOneFrame; - bool NavHasScroll; // Set when scrolling can be used (ScrollMax > 0.0f) + bool NavWindowHasScrollY; // Set per window when scrolling can be used (== ScrollMax.y > 0.0f) + + // Miscellaneous bool MenuBarAppending; // FIXME: Remove this ImVec2 MenuBarOffset; // MenuBarOffset.x is sort of equivalent of a per-layer CursorPos.x, saved/restored as we switch to the menu bar. The only situation when MenuBarOffset.y is > 0 if when (SafeAreaPadding.y > FramePadding.y), often used on TVs. + ImGuiMenuColumns MenuColumns; // Simplified columns storage for menu items measurement + int TreeDepth; // Current tree depth. + ImU32 TreeHasStackDataDepthMask; // Store whether given depth has ImGuiTreeNodeStackData data. Could be turned into a ImU64 if necessary. ImVector ChildWindows; ImGuiStorage* StateStorage; // Current persistent per-window storage (store e.g. tree node open/close state) + ImGuiOldColumns* CurrentColumns; // Current columns set + int CurrentTableIdx; // Current table index (into g.Tables) ImGuiLayoutType LayoutType; ImGuiLayoutType ParentLayoutType; // Layout type of parent window at the time of Begin() - int FocusCounterAll; // Counter for focus/tabbing system. Start at -1 and increase as assigned via FocusableItemRegister() (FIXME-NAV: Needs redesign) - int FocusCounterTab; // (same, but only count widgets which you can Tab through) + ImU32 ModalDimBgColor; + // Status flags + ImGuiItemStatusFlags WindowItemStatusFlags; + ImGuiItemStatusFlags ChildItemStatusFlags; + ImGuiItemStatusFlags DockTabItemStatusFlags; + ImRect DockTabItemRect; + + // Local parameters stacks // We store the current settings outside of the vectors to increase memory locality (reduce cache misses). The vectors are rarely modified. Also it allows us to not heap allocate for short-lived windows which are not using those settings. - ImGuiItemFlags ItemFlags; // == ItemFlagsStack.back() [empty == ImGuiItemFlags_Default] - float ItemWidth; // == ItemWidthStack.back(). 0.0: default, >0.0: width in pixels, <0.0: align xx pixels to the right of window - float TextWrapPos; // == TextWrapPosStack.back() [empty == -1.0f] - ImVectorItemFlagsStack; - ImVector ItemWidthStack; - ImVector TextWrapPosStack; - ImVectorGroupStack; - short StackSizesBackup[6]; // Store size of various stacks for asserting - - ImVec1 Indent; // Indentation / start position from left of window (increased by TreePush/TreePop, etc.) - ImVec1 GroupOffset; - ImVec1 ColumnsOffset; // Offset to the current column (if ColumnsCurrent > 0). FIXME: This and the above should be a stack to allow use cases like Tree->Column->Tree. Need revamp columns API. - ImGuiColumns* CurrentColumns; // Current columns set - - ImGuiWindowTempData() - { - CursorPos = CursorPosPrevLine = CursorStartPos = CursorMaxPos = ImVec2(0.0f, 0.0f); - CurrLineSize = PrevLineSize = ImVec2(0.0f, 0.0f); - CurrLineTextBaseOffset = PrevLineTextBaseOffset = 0.0f; - TreeDepth = 0; - TreeMayJumpToParentOnPopMask = 0x00; - LastItemId = 0; - LastItemStatusFlags = 0; - LastItemRect = LastItemDisplayRect = ImRect(); - NavLayerActiveMask = NavLayerActiveMaskNext = 0x00; - NavLayerCurrent = ImGuiNavLayer_Main; - NavLayerCurrentMask = (1 << ImGuiNavLayer_Main); - NavHideHighlightOneFrame = false; - NavHasScroll = false; - MenuBarAppending = false; - MenuBarOffset = ImVec2(0.0f, 0.0f); - StateStorage = NULL; - LayoutType = ParentLayoutType = ImGuiLayoutType_Vertical; - FocusCounterAll = FocusCounterTab = -1; - - ItemFlags = ImGuiItemFlags_Default_; - ItemWidth = 0.0f; - TextWrapPos = -1.0f; - memset(StackSizesBackup, 0, sizeof(StackSizesBackup)); - - Indent = ImVec1(0.0f); - GroupOffset = ImVec1(0.0f); - ColumnsOffset = ImVec1(0.0f); - CurrentColumns = NULL; - } + float ItemWidth; // Current item width (>0.0: width in pixels, <0.0: align xx pixels to the right of window). + float TextWrapPos; // Current text wrap pos. + ImVector ItemWidthStack; // Store item widths to restore (attention: .back() is not == ItemWidth) + ImVector TextWrapPosStack; // Store text wrap pos to restore (attention: .back() is not == TextWrapPos) }; // Storage for one window struct IMGUI_API ImGuiWindow { - char* Name; - ImGuiID ID; // == ImHash(Name) - ImGuiWindowFlags Flags; // See enum ImGuiWindowFlags_ + ImGuiContext* Ctx; // Parent UI context (needs to be set explicitly by parent). + char* Name; // Window name, owned by the window. + ImGuiID ID; // == ImHashStr(Name) + ImGuiWindowFlags Flags, FlagsPreviousFrame; // See enum ImGuiWindowFlags_ + ImGuiChildFlags ChildFlags; // Set when window is a child window. See enum ImGuiChildFlags_ + ImGuiWindowClass WindowClass; // Advanced users only. Set with SetNextWindowClass() + ImGuiViewportP* Viewport; // Always set in Begin(). Inactive windows may have a NULL value here if their viewport was discarded. + ImGuiID ViewportId; // We backup the viewport id (since the viewport may disappear or never be created if the window is inactive) + ImVec2 ViewportPos; // We backup the viewport position (since the viewport may disappear or never be created if the window is inactive) + int ViewportAllowPlatformMonitorExtend; // Reset to -1 every frame (index is guaranteed to be valid between NewFrame..EndFrame), only used in the Appearing frame of a tooltip/popup to enforce clamping to a given monitor ImVec2 Pos; // Position (always rounded-up to nearest pixel) ImVec2 Size; // Current size (==SizeFull or collapsed title bar size) ImVec2 SizeFull; // Size when non collapsed ImVec2 ContentSize; // Size of contents/scrollable client area (calculated from the extents reach of the cursor) from previous frame. Does not include window decoration or window padding. + ImVec2 ContentSizeIdeal; ImVec2 ContentSizeExplicit; // Size of contents/scrollable client area explicitly request by the user via SetNextWindowContentSize(). ImVec2 WindowPadding; // Window padding at the time of Begin(). - float WindowRounding; // Window rounding at the time of Begin(). + float WindowRounding; // Window rounding at the time of Begin(). May be clamped lower to avoid rendering artifacts with title bar, menu bar etc. float WindowBorderSize; // Window border size at the time of Begin(). + float TitleBarHeight, MenuBarHeight; // Note that those used to be function before 2024/05/28. If you have old code calling TitleBarHeight() you can change it to TitleBarHeight. + float DecoOuterSizeX1, DecoOuterSizeY1; // Left/Up offsets. Sum of non-scrolling outer decorations (X1 generally == 0.0f. Y1 generally = TitleBarHeight + MenuBarHeight). Locked during Begin(). + float DecoOuterSizeX2, DecoOuterSizeY2; // Right/Down offsets (X2 generally == ScrollbarSize.x, Y2 == ScrollbarSizes.y). + float DecoInnerSizeX1, DecoInnerSizeY1; // Applied AFTER/OVER InnerRect. Specialized for Tables as they use specialized form of clipping and frozen rows/columns are inside InnerRect (and not part of regular decoration sizes). int NameBufLen; // Size of buffer storing Name. May be larger than strlen(Name)! ImGuiID MoveId; // == window->GetID("#MOVE") + ImGuiID TabId; // == window->GetID("#TAB") ImGuiID ChildId; // ID of corresponding item in parent window (for navigation to return from child window to parent window) + ImGuiID PopupId; // ID in the popup stack when this window is used as a popup/menu (because we use generic Name/ID for recycling) ImVec2 Scroll; ImVec2 ScrollMax; ImVec2 ScrollTarget; // target scroll position. stored as cursor position with scrolling canceled out, so the highest point is always 0.0f. (FLT_MAX for no change) ImVec2 ScrollTargetCenterRatio; // 0.0f = scroll so that target position is at top, 0.5f = scroll so that target position is centered - ImVec2 ScrollbarSizes; // Size taken by scrollbars on each axis + ImVec2 ScrollTargetEdgeSnapDist; // 0.0f = no snapping, >0.0f snapping threshold + ImVec2 ScrollbarSizes; // Size taken by each scrollbars on their smaller axis. Pay attention! ScrollbarSizes.x == width of the vertical scrollbar, ScrollbarSizes.y = height of the horizontal scrollbar. bool ScrollbarX, ScrollbarY; // Are scrollbars visible? + bool ScrollbarXStabilizeEnabled; // Was ScrollbarX previously auto-stabilized? + ImU8 ScrollbarXStabilizeToggledHistory; // Used to stabilize scrollbar visibility in case of feedback loops + bool ViewportOwned; bool Active; // Set to true on Begin(), unless Collapsed bool WasActive; bool WriteAccessed; // Set to true when any widget access the current window bool Collapsed; // Set when collapsing window to become only title-bar bool WantCollapseToggle; bool SkipItems; // Set when items can safely be all clipped (e.g. window not visible or collapsed) + bool SkipRefresh; // [EXPERIMENTAL] Reuse previous frame drawn contents, Begin() returns false. bool Appearing; // Set during the frame where the window is appearing (or re-appearing) - bool Hidden; // Do not display (== (HiddenFrames*** > 0)) + bool Hidden; // Do not display (== HiddenFrames*** > 0) + bool IsFallbackWindow; // Set on the "Debug##Default" window. + bool IsExplicitChild; // Set when passed _ChildWindow, left to false by BeginDocked() bool HasCloseButton; // Set when the window has a close button (p_open != NULL) + signed char ResizeBorderHovered; // Current border being hovered for resize (-1: none, otherwise 0-3) signed char ResizeBorderHeld; // Current border being held for resize (-1: none, otherwise 0-3) short BeginCount; // Number of Begin() during the current frame (generally 0 or 1, 1+ if appending via multiple Begin/End pairs) - short BeginOrderWithinParent; // Order within immediate parent window, if we are a child window. Otherwise 0. - short BeginOrderWithinContext; // Order within entire imgui context. This is mostly used for debugging submission order related issues. - ImGuiID PopupId; // ID in the popup stack when this window is used as a popup/menu (because we use generic Name/ID for recycling) + short BeginCountPreviousFrame; // Number of Begin() during the previous frame + short BeginOrderWithinParent; // Begin() order within immediate parent window, if we are a child window. Otherwise 0. + short BeginOrderWithinContext; // Begin() order within entire imgui context. This is mostly used for debugging submission order related issues. + short FocusOrder; // Order within WindowsFocusOrder[], altered when windows are focused. ImS8 AutoFitFramesX, AutoFitFramesY; - ImS8 AutoFitChildAxises; bool AutoFitOnlyGrows; ImGuiDir AutoPosLastDirection; - int HiddenFramesCanSkipItems; // Hide the window for N frames - int HiddenFramesCannotSkipItems; // Hide the window for N frames while allowing items to be submitted so we can measure their size - ImGuiCond SetWindowPosAllowFlags; // store acceptable condition flags for SetNextWindowPos() use. - ImGuiCond SetWindowSizeAllowFlags; // store acceptable condition flags for SetNextWindowSize() use. - ImGuiCond SetWindowCollapsedAllowFlags; // store acceptable condition flags for SetNextWindowCollapsed() use. + ImS8 HiddenFramesCanSkipItems; // Hide the window for N frames + ImS8 HiddenFramesCannotSkipItems; // Hide the window for N frames while allowing items to be submitted so we can measure their size + ImS8 HiddenFramesForRenderOnly; // Hide the window until frame N at Render() time only + ImS8 DisableInputsFrames; // Disable window interactions for N frames + ImGuiCond SetWindowPosAllowFlags : 8; // store acceptable condition flags for SetNextWindowPos() use. + ImGuiCond SetWindowSizeAllowFlags : 8; // store acceptable condition flags for SetNextWindowSize() use. + ImGuiCond SetWindowCollapsedAllowFlags : 8; // store acceptable condition flags for SetNextWindowCollapsed() use. + ImGuiCond SetWindowDockAllowFlags : 8; // store acceptable condition flags for SetNextWindowDock() use. ImVec2 SetWindowPosVal; // store window position when using a non-zero Pivot (position set needs to be processed when we know the window size) - ImVec2 SetWindowPosPivot; // store window pivot for positioning. ImVec2(0,0) when positioning from top-left corner; ImVec2(0.5f,0.5f) for centering; ImVec2(1,1) for bottom right. + ImVec2 SetWindowPosPivot; // store window pivot for positioning. ImVec2(0, 0) when positioning from top-left corner; ImVec2(0.5f, 0.5f) for centering; ImVec2(1, 1) for bottom right. ImVector IDStack; // ID stack. ID are hashes seeded with the value at the top of the stack. (In theory this should be in the TempData structure) ImGuiWindowTempData DC; // Temporary per-window data, reset at the beginning of the frame. This used to be called ImGuiDrawContext, hence the "DC" variable name. - // The best way to understand what those rectangles are is to use the 'Metrics -> Tools -> Show windows rectangles' viewer. + // The best way to understand what those rectangles are is to use the 'Metrics->Tools->Show Windows Rectangles' viewer. // The main 'OuterRect', omitted as a field, is window->Rect(). ImRect OuterRectClipped; // == Window->Rect() just after setup in Begin(). == window->Rect() for root window. ImRect InnerRect; // Inner rectangle (omit title bar, menu bar, scroll bar) ImRect InnerClipRect; // == InnerRect shrunk by WindowPadding*0.5f on each side, clipped within viewport or parent clip rect. - ImRect WorkRect; // Cover the whole scrolling region, shrunk by WindowPadding*1.0f on each side. This is meant to replace ContentsRegionRect over time (from 1.71+ onward). + ImRect WorkRect; // Initially covers the whole scrolling region. Reduced by containers e.g columns/tables when active. Shrunk by WindowPadding*1.0f on each side. This is meant to replace ContentRegionRect over time (from 1.71+ onward). + ImRect ParentWorkRect; // Backup of WorkRect before entering a container such as columns/tables. Used by e.g. SpanAllColumns functions to easily access. Stacked containers are responsible for maintaining this. // FIXME-WORKRECT: Could be a stack? ImRect ClipRect; // Current clipping/scissoring rectangle, evolve as we are using PushClipRect(), etc. == DrawList->clip_rect_stack.back(). - ImRect ContentsRegionRect; // FIXME: This is currently confusing/misleading. It is essentially WorkRect but not handling of scrolling. We currently rely on it as right/bottom aligned sizing operation need some size to rely on. + ImRect ContentRegionRect; // FIXME: This is currently confusing/misleading. It is essentially WorkRect but not handling of scrolling. We currently rely on it as right/bottom aligned sizing operation need some size to rely on. + ImVec2ih HitTestHoleSize; // Define an optional rectangular hole where mouse will pass-through the window. + ImVec2ih HitTestHoleOffset; int LastFrameActive; // Last frame number the window was Active. + int LastFrameJustFocused; // Last frame number the window was made Focused. float LastTimeActive; // Last timestamp the window was Active (using float as we don't need high precision there) float ItemWidthDefault; - ImGuiMenuColumns MenuColumns; // Simplified columns storage for menu items ImGuiStorage StateStorage; - ImVector ColumnsStorage; + ImVector ColumnsStorage; float FontWindowScale; // User scale multiplier per-window, via SetWindowFontScale() - int SettingsIdx; // Index into SettingsWindow[] (indices are always valid as we only grow the array from the back) + float FontWindowScaleParents; + float FontDpiScale; + float FontRefSize; // This is a copy of window->CalcFontSize() at the time of Begin(), trying to phase out CalcFontSize() especially as it may be called on non-current window. + int SettingsOffset; // Offset into SettingsWindows[] (offsets are always valid as we only grow the array from the back) ImDrawList* DrawList; // == &DrawListInst (for backward compatibility reason with code using imgui_internal.h we keep this a pointer) ImDrawList DrawListInst; - ImGuiWindow* ParentWindow; // If we are a child _or_ popup window, this is pointing to our parent. Otherwise NULL. - ImGuiWindow* RootWindow; // Point to ourself or first ancestor that is not a child window. + ImGuiWindow* ParentWindow; // If we are a child _or_ popup _or_ docked window, this is pointing to our parent. Otherwise NULL. + ImGuiWindow* ParentWindowInBeginStack; + ImGuiWindow* RootWindow; // Point to ourself or first ancestor that is not a child window. Doesn't cross through popups/dock nodes. + ImGuiWindow* RootWindowPopupTree; // Point to ourself or first ancestor that is not a child window. Cross through popups parent<>child. + ImGuiWindow* RootWindowDockTree; // Point to ourself or first ancestor that is not a child window. Cross through dock nodes. ImGuiWindow* RootWindowForTitleBarHighlight; // Point to ourself or first ancestor which will display TitleBgActive color when this window is active. ImGuiWindow* RootWindowForNav; // Point to ourself or first ancestor which doesn't have the NavFlattened flag. + ImGuiWindow* ParentWindowForFocusRoute; // Set to manual link a window to its logical parent so that Shortcut() chain are honoerd (e.g. Tool linked to Document) ImGuiWindow* NavLastChildNavWindow; // When going to the menu bar, we remember the child window we came from. (This could probably be made implicit if we kept g.Windows sorted by last focused including child window.) ImGuiID NavLastIds[ImGuiNavLayer_COUNT]; // Last known NavId for this window, per layer (0/1) ImRect NavRectRel[ImGuiNavLayer_COUNT]; // Reference rectangle, in window relative space + ImVec2 NavPreferredScoringPosRel[ImGuiNavLayer_COUNT]; // Preferred X/Y position updated when moving on a given axis, reset to FLT_MAX. + ImGuiID NavRootFocusScopeId; // Focus Scope ID at the time of Begin() - bool MemoryCompacted; - int MemoryDrawListIdxCapacity; + int MemoryDrawListIdxCapacity; // Backup of last idx/vtx count, so when waking up the window we can preallocate and avoid iterative alloc/copy int MemoryDrawListVtxCapacity; + bool MemoryCompacted; // Set when window extraneous data have been garbage collected + + // Docking + bool DockIsActive :1; // When docking artifacts are actually visible. When this is set, DockNode is guaranteed to be != NULL. ~~ (DockNode != NULL) && (DockNode->Windows.Size > 1). + bool DockNodeIsVisible :1; + bool DockTabIsVisible :1; // Is our window visible this frame? ~~ is the corresponding tab selected? + bool DockTabWantClose :1; + short DockOrder; // Order of the last time the window was visible within its DockNode. This is used to reorder windows that are reappearing on the same frame. Same value between windows that were active and windows that were none are possible. + ImGuiWindowDockStyle DockStyle; + ImGuiDockNode* DockNode; // Which node are we docked into. Important: Prefer testing DockIsActive in many cases as this will still be set when the dock node is hidden. + ImGuiDockNode* DockNodeAsHost; // Which node are we owning (for parent windows) + ImGuiID DockId; // Backup of last valid DockNode->ID, so single window remember their dock node id even when they are not bound any more public: ImGuiWindow(ImGuiContext* context, const char* name); @@ -1368,35 +2838,18 @@ public: ImGuiID GetID(const char* str, const char* str_end = NULL); ImGuiID GetID(const void* ptr); ImGuiID GetID(int n); - ImGuiID GetIDNoKeepAlive(const char* str, const char* str_end = NULL); - ImGuiID GetIDNoKeepAlive(const void* ptr); - ImGuiID GetIDNoKeepAlive(int n); + ImGuiID GetIDFromPos(const ImVec2& p_abs); ImGuiID GetIDFromRectangle(const ImRect& r_abs); - // We don't use g.FontSize because the window may be != g.CurrentWidow. - ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x+Size.x, Pos.y+Size.y); } - float CalcFontSize() const { ImGuiContext& g = *GImGui; float scale = g.FontBaseSize * FontWindowScale; if (ParentWindow) scale *= ParentWindow->FontWindowScale; return scale; } - float TitleBarHeight() const { ImGuiContext& g = *GImGui; return (Flags & ImGuiWindowFlags_NoTitleBar) ? 0.0f : CalcFontSize() + g.Style.FramePadding.y * 2.0f; } - ImRect TitleBarRect() const { return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight())); } - float MenuBarHeight() const { ImGuiContext& g = *GImGui; return (Flags & ImGuiWindowFlags_MenuBar) ? DC.MenuBarOffset.y + CalcFontSize() + g.Style.FramePadding.y * 2.0f : 0.0f; } - ImRect MenuBarRect() const { float y1 = Pos.y + TitleBarHeight(); return ImRect(Pos.x, y1, Pos.x + SizeFull.x, y1 + MenuBarHeight()); } -}; - -// Backup and restore just enough data to be able to use IsItemHovered() on item A after another B in the same window has overwritten the data. -struct ImGuiItemHoveredDataBackup -{ - ImGuiID LastItemId; - ImGuiItemStatusFlags LastItemStatusFlags; - ImRect LastItemRect; - ImRect LastItemDisplayRect; - - ImGuiItemHoveredDataBackup() { Backup(); } - void Backup() { ImGuiWindow* window = GImGui->CurrentWindow; LastItemId = window->DC.LastItemId; LastItemStatusFlags = window->DC.LastItemStatusFlags; LastItemRect = window->DC.LastItemRect; LastItemDisplayRect = window->DC.LastItemDisplayRect; } - void Restore() const { ImGuiWindow* window = GImGui->CurrentWindow; window->DC.LastItemId = LastItemId; window->DC.LastItemStatusFlags = LastItemStatusFlags; window->DC.LastItemRect = LastItemRect; window->DC.LastItemDisplayRect = LastItemDisplayRect; } + // We don't use g.FontSize because the window may be != g.CurrentWindow. + ImRect Rect() const { return ImRect(Pos.x, Pos.y, Pos.x + Size.x, Pos.y + Size.y); } + float CalcFontSize() const { ImGuiContext& g = *Ctx; return g.FontBaseSize * FontWindowScale * FontDpiScale * FontWindowScaleParents; } + ImRect TitleBarRect() const { return ImRect(Pos, ImVec2(Pos.x + SizeFull.x, Pos.y + TitleBarHeight)); } + ImRect MenuBarRect() const { float y1 = Pos.y + TitleBarHeight; return ImRect(Pos.x, y1, Pos.x + SizeFull.x, y1 + MenuBarHeight); } }; //----------------------------------------------------------------------------- -// Tab bar, tab item +// [SECTION] Tab bar, Tab item support //----------------------------------------------------------------------------- // Extend ImGuiTabBarFlags_ @@ -1404,129 +2857,490 @@ enum ImGuiTabBarFlagsPrivate_ { ImGuiTabBarFlags_DockNode = 1 << 20, // Part of a dock node [we don't use this in the master branch but it facilitate branch syncing to keep this around] ImGuiTabBarFlags_IsFocused = 1 << 21, - ImGuiTabBarFlags_SaveSettings = 1 << 22 // FIXME: Settings are handled by the docking system, this only request the tab bar to mark settings dirty when reordering tabs + ImGuiTabBarFlags_SaveSettings = 1 << 22, // FIXME: Settings are handled by the docking system, this only request the tab bar to mark settings dirty when reordering tabs }; // Extend ImGuiTabItemFlags_ enum ImGuiTabItemFlagsPrivate_ { - ImGuiTabItemFlags_NoCloseButton = 1 << 20 // Store whether p_open is set or not, which we need to recompute WidthContents during layout. + ImGuiTabItemFlags_SectionMask_ = ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing, + ImGuiTabItemFlags_NoCloseButton = 1 << 20, // Track whether p_open was set or not (we'll need this info on the next frame to recompute ContentWidth during layout) + ImGuiTabItemFlags_Button = 1 << 21, // Used by TabItemButton, change the tab item behavior to mimic a button + ImGuiTabItemFlags_Invisible = 1 << 22, // To reserve space e.g. with ImGuiTabItemFlags_Leading + ImGuiTabItemFlags_Unsorted = 1 << 23, // [Docking] Trailing tabs with the _Unsorted flag will be sorted based on the DockOrder of their Window. }; -// Storage for one active tab item (sizeof() 26~32 bytes) +// Storage for one active tab item (sizeof() 48 bytes) struct ImGuiTabItem { ImGuiID ID; ImGuiTabItemFlags Flags; + ImGuiWindow* Window; // When TabItem is part of a DockNode's TabBar, we hold on to a window. int LastFrameVisible; int LastFrameSelected; // This allows us to infer an ordered list of the last activated tabs with little maintenance - int NameOffset; // When Window==NULL, offset to name within parent ImGuiTabBar::TabsNames float Offset; // Position relative to beginning of tab float Width; // Width currently displayed - float WidthContents; // Width of actual contents, stored during BeginTabItem() call + float ContentWidth; // Width of label, stored during BeginTabItem() call + float RequestedWidth; // Width optionally requested by caller, -1.0f is unused + ImS32 NameOffset; // When Window==NULL, offset to name within parent ImGuiTabBar::TabsNames + ImS16 BeginOrder; // BeginTabItem() order, used to re-order tabs after toggling ImGuiTabBarFlags_Reorderable + ImS16 IndexDuringLayout; // Index only used during TabBarLayout(). Tabs gets reordered so 'Tabs[n].IndexDuringLayout == n' but may mismatch during additions. + bool WantClose; // Marked as closed by SetTabItemClosed() - ImGuiTabItem() { ID = Flags = 0; LastFrameVisible = LastFrameSelected = -1; NameOffset = -1; Offset = Width = WidthContents = 0.0f; } + ImGuiTabItem() { memset(this, 0, sizeof(*this)); LastFrameVisible = LastFrameSelected = -1; RequestedWidth = -1.0f; NameOffset = -1; BeginOrder = IndexDuringLayout = -1; } }; -// Storage for a tab bar (sizeof() 92~96 bytes) -struct ImGuiTabBar +// Storage for a tab bar (sizeof() 160 bytes) +struct IMGUI_API ImGuiTabBar { + ImGuiWindow* Window; ImVector Tabs; + ImGuiTabBarFlags Flags; ImGuiID ID; // Zero for tab-bars used by docking - ImGuiID SelectedTabId; // Selected tab - ImGuiID NextSelectedTabId; + ImGuiID SelectedTabId; // Selected tab/window + ImGuiID NextSelectedTabId; // Next selected tab/window. Will also trigger a scrolling animation ImGuiID VisibleTabId; // Can occasionally be != SelectedTabId (e.g. when previewing contents for CTRL+TAB preview) int CurrFrameVisible; int PrevFrameVisible; ImRect BarRect; - float LastTabContentHeight; // Record the height of contents submitted below the tab bar - float OffsetMax; // Distance from BarRect.Min.x, locked during layout - float OffsetMaxIdeal; // Ideal offset if all tabs were visible and not clipped - float OffsetNextTab; // Distance from BarRect.Min.x, incremented with each BeginTabItem() call, not used if ImGuiTabBarFlags_Reorderable if set. + float CurrTabsContentsHeight; + float PrevTabsContentsHeight; // Record the height of contents submitted below the tab bar + float WidthAllTabs; // Actual width of all tabs (locked during layout) + float WidthAllTabsIdeal; // Ideal width if all tabs were visible and not clipped float ScrollingAnim; float ScrollingTarget; float ScrollingTargetDistToVisibility; float ScrollingSpeed; - ImGuiTabBarFlags Flags; + float ScrollingRectMinX; + float ScrollingRectMaxX; + float SeparatorMinX; + float SeparatorMaxX; ImGuiID ReorderRequestTabId; - ImS8 ReorderRequestDir; + ImS16 ReorderRequestOffset; + ImS8 BeginCount; bool WantLayout; bool VisibleTabWasSubmitted; - short LastTabItemIdx; // For BeginTabItem()/EndTabItem() + bool TabsAddedNew; // Set to true when a new tab item or button has been added to the tab bar during last frame + ImS16 TabsActiveCount; // Number of tabs submitted this frame. + ImS16 LastTabItemIdx; // Index of last BeginTabItem() tab for use by EndTabItem() + float ItemSpacingY; ImVec2 FramePadding; // style.FramePadding locked at the time of BeginTabBar() + ImVec2 BackupCursorPos; ImGuiTextBuffer TabsNames; // For non-docking tab bar we re-append names in a contiguous buffer. ImGuiTabBar(); - int GetTabOrder(const ImGuiTabItem* tab) const { return Tabs.index_from_ptr(tab); } - const char* GetTabName(const ImGuiTabItem* tab) const - { - IM_ASSERT(tab->NameOffset != -1 && tab->NameOffset < TabsNames.Buf.Size); - return TabsNames.Buf.Data + tab->NameOffset; - } }; //----------------------------------------------------------------------------- -// Internal API -// No guarantee of forward compatibility here. +// [SECTION] Table support +//----------------------------------------------------------------------------- + +#define IM_COL32_DISABLE IM_COL32(0,0,0,1) // Special sentinel code which cannot be used as a regular color. +#define IMGUI_TABLE_MAX_COLUMNS 512 // May be further lifted + +// Our current column maximum is 64 but we may raise that in the future. +typedef ImS16 ImGuiTableColumnIdx; +typedef ImU16 ImGuiTableDrawChannelIdx; + +// [Internal] sizeof() ~ 112 +// We use the terminology "Enabled" to refer to a column that is not Hidden by user/api. +// We use the terminology "Clipped" to refer to a column that is out of sight because of scrolling/clipping. +// This is in contrast with some user-facing api such as IsItemVisible() / IsRectVisible() which use "Visible" to mean "not clipped". +struct ImGuiTableColumn +{ + ImGuiTableColumnFlags Flags; // Flags after some patching (not directly same as provided by user). See ImGuiTableColumnFlags_ + float WidthGiven; // Final/actual width visible == (MaxX - MinX), locked in TableUpdateLayout(). May be > WidthRequest to honor minimum width, may be < WidthRequest to honor shrinking columns down in tight space. + float MinX; // Absolute positions + float MaxX; + float WidthRequest; // Master width absolute value when !(Flags & _WidthStretch). When Stretch this is derived every frame from StretchWeight in TableUpdateLayout() + float WidthAuto; // Automatic width + float WidthMax; // Maximum width (FIXME: overwritten by each instance) + float StretchWeight; // Master width weight when (Flags & _WidthStretch). Often around ~1.0f initially. + float InitStretchWeightOrWidth; // Value passed to TableSetupColumn(). For Width it is a content width (_without padding_). + ImRect ClipRect; // Clipping rectangle for the column + ImGuiID UserID; // Optional, value passed to TableSetupColumn() + float WorkMinX; // Contents region min ~(MinX + CellPaddingX + CellSpacingX1) == cursor start position when entering column + float WorkMaxX; // Contents region max ~(MaxX - CellPaddingX - CellSpacingX2) + float ItemWidth; // Current item width for the column, preserved across rows + float ContentMaxXFrozen; // Contents maximum position for frozen rows (apart from headers), from which we can infer content width. + float ContentMaxXUnfrozen; + float ContentMaxXHeadersUsed; // Contents maximum position for headers rows (regardless of freezing). TableHeader() automatically softclip itself + report ideal desired size, to avoid creating extraneous draw calls + float ContentMaxXHeadersIdeal; + ImS16 NameOffset; // Offset into parent ColumnsNames[] + ImGuiTableColumnIdx DisplayOrder; // Index within Table's IndexToDisplayOrder[] (column may be reordered by users) + ImGuiTableColumnIdx IndexWithinEnabledSet; // Index within enabled/visible set (<= IndexToDisplayOrder) + ImGuiTableColumnIdx PrevEnabledColumn; // Index of prev enabled/visible column within Columns[], -1 if first enabled/visible column + ImGuiTableColumnIdx NextEnabledColumn; // Index of next enabled/visible column within Columns[], -1 if last enabled/visible column + ImGuiTableColumnIdx SortOrder; // Index of this column within sort specs, -1 if not sorting on this column, 0 for single-sort, may be >0 on multi-sort + ImGuiTableDrawChannelIdx DrawChannelCurrent; // Index within DrawSplitter.Channels[] + ImGuiTableDrawChannelIdx DrawChannelFrozen; // Draw channels for frozen rows (often headers) + ImGuiTableDrawChannelIdx DrawChannelUnfrozen; // Draw channels for unfrozen rows + bool IsEnabled; // IsUserEnabled && (Flags & ImGuiTableColumnFlags_Disabled) == 0 + bool IsUserEnabled; // Is the column not marked Hidden by the user? (unrelated to being off view, e.g. clipped by scrolling). + bool IsUserEnabledNextFrame; + bool IsVisibleX; // Is actually in view (e.g. overlapping the host window clipping rectangle, not scrolled). + bool IsVisibleY; + bool IsRequestOutput; // Return value for TableSetColumnIndex() / TableNextColumn(): whether we request user to output contents or not. + bool IsSkipItems; // Do we want item submissions to this column to be completely ignored (no layout will happen). + bool IsPreserveWidthAuto; + ImS8 NavLayerCurrent; // ImGuiNavLayer in 1 byte + ImU8 AutoFitQueue; // Queue of 8 values for the next 8 frames to request auto-fit + ImU8 CannotSkipItemsQueue; // Queue of 8 values for the next 8 frames to disable Clipped/SkipItem + ImU8 SortDirection : 2; // ImGuiSortDirection_Ascending or ImGuiSortDirection_Descending + ImU8 SortDirectionsAvailCount : 2; // Number of available sort directions (0 to 3) + ImU8 SortDirectionsAvailMask : 4; // Mask of available sort directions (1-bit each) + ImU8 SortDirectionsAvailList; // Ordered list of available sort directions (2-bits each, total 8-bits) + + ImGuiTableColumn() + { + memset(this, 0, sizeof(*this)); + StretchWeight = WidthRequest = -1.0f; + NameOffset = -1; + DisplayOrder = IndexWithinEnabledSet = -1; + PrevEnabledColumn = NextEnabledColumn = -1; + SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + DrawChannelCurrent = DrawChannelFrozen = DrawChannelUnfrozen = (ImU8)-1; + } +}; + +// Transient cell data stored per row. +// sizeof() ~ 6 bytes +struct ImGuiTableCellData +{ + ImU32 BgColor; // Actual color + ImGuiTableColumnIdx Column; // Column number +}; + +// Parameters for TableAngledHeadersRowEx() +// This may end up being refactored for more general purpose. +// sizeof() ~ 12 bytes +struct ImGuiTableHeaderData +{ + ImGuiTableColumnIdx Index; // Column index + ImU32 TextColor; + ImU32 BgColor0; + ImU32 BgColor1; +}; + +// Per-instance data that needs preserving across frames (seemingly most others do not need to be preserved aside from debug needs. Does that means they could be moved to ImGuiTableTempData?) +// sizeof() ~ 24 bytes +struct ImGuiTableInstanceData +{ + ImGuiID TableInstanceID; + float LastOuterHeight; // Outer height from last frame + float LastTopHeadersRowHeight; // Height of first consecutive header rows from last frame (FIXME: this is used assuming consecutive headers are in same frozen set) + float LastFrozenHeight; // Height of frozen section from last frame + int HoveredRowLast; // Index of row which was hovered last frame. + int HoveredRowNext; // Index of row hovered this frame, set after encountering it. + + ImGuiTableInstanceData() { TableInstanceID = 0; LastOuterHeight = LastTopHeadersRowHeight = LastFrozenHeight = 0.0f; HoveredRowLast = HoveredRowNext = -1; } +}; + +// sizeof() ~ 592 bytes + heap allocs described in TableBeginInitMemory() +struct IMGUI_API ImGuiTable +{ + ImGuiID ID; + ImGuiTableFlags Flags; + void* RawData; // Single allocation to hold Columns[], DisplayOrderToIndex[], and RowCellData[] + ImGuiTableTempData* TempData; // Transient data while table is active. Point within g.CurrentTableStack[] + ImSpan Columns; // Point within RawData[] + ImSpan DisplayOrderToIndex; // Point within RawData[]. Store display order of columns (when not reordered, the values are 0...Count-1) + ImSpan RowCellData; // Point within RawData[]. Store cells background requests for current row. + ImBitArrayPtr EnabledMaskByDisplayOrder; // Column DisplayOrder -> IsEnabled map + ImBitArrayPtr EnabledMaskByIndex; // Column Index -> IsEnabled map (== not hidden by user/api) in a format adequate for iterating column without touching cold data + ImBitArrayPtr VisibleMaskByIndex; // Column Index -> IsVisibleX|IsVisibleY map (== not hidden by user/api && not hidden by scrolling/cliprect) + ImGuiTableFlags SettingsLoadedFlags; // Which data were loaded from the .ini file (e.g. when order is not altered we won't save order) + int SettingsOffset; // Offset in g.SettingsTables + int LastFrameActive; + int ColumnsCount; // Number of columns declared in BeginTable() + int CurrentRow; + int CurrentColumn; + ImS16 InstanceCurrent; // Count of BeginTable() calls with same ID in the same frame (generally 0). This is a little bit similar to BeginCount for a window, but multiple tables with the same ID are multiple tables, they are just synced. + ImS16 InstanceInteracted; // Mark which instance (generally 0) of the same ID is being interacted with + float RowPosY1; + float RowPosY2; + float RowMinHeight; // Height submitted to TableNextRow() + float RowCellPaddingY; // Top and bottom padding. Reloaded during row change. + float RowTextBaseline; + float RowIndentOffsetX; + ImGuiTableRowFlags RowFlags : 16; // Current row flags, see ImGuiTableRowFlags_ + ImGuiTableRowFlags LastRowFlags : 16; + int RowBgColorCounter; // Counter for alternating background colors (can be fast-forwarded by e.g clipper), not same as CurrentRow because header rows typically don't increase this. + ImU32 RowBgColor[2]; // Background color override for current row. + ImU32 BorderColorStrong; + ImU32 BorderColorLight; + float BorderX1; + float BorderX2; + float HostIndentX; + float MinColumnWidth; + float OuterPaddingX; + float CellPaddingX; // Padding from each borders. Locked in BeginTable()/Layout. + float CellSpacingX1; // Spacing between non-bordered cells. Locked in BeginTable()/Layout. + float CellSpacingX2; + float InnerWidth; // User value passed to BeginTable(), see comments at the top of BeginTable() for details. + float ColumnsGivenWidth; // Sum of current column width + float ColumnsAutoFitWidth; // Sum of ideal column width in order nothing to be clipped, used for auto-fitting and content width submission in outer window + float ColumnsStretchSumWeights; // Sum of weight of all enabled stretching columns + float ResizedColumnNextWidth; + float ResizeLockMinContentsX2; // Lock minimum contents width while resizing down in order to not create feedback loops. But we allow growing the table. + float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + float AngledHeadersHeight; // Set by TableAngledHeadersRow(), used in TableUpdateLayout() + float AngledHeadersSlope; // Set by TableAngledHeadersRow(), used in TableUpdateLayout() + ImRect OuterRect; // Note: for non-scrolling table, OuterRect.Max.y is often FLT_MAX until EndTable(), unless a height has been specified in BeginTable(). + ImRect InnerRect; // InnerRect but without decoration. As with OuterRect, for non-scrolling tables, InnerRect.Max.y is " + ImRect WorkRect; + ImRect InnerClipRect; + ImRect BgClipRect; // We use this to cpu-clip cell background color fill, evolve during the frame as we cross frozen rows boundaries + ImRect Bg0ClipRectForDrawCmd; // Actual ImDrawCmd clip rect for BG0/1 channel. This tends to be == OuterWindow->ClipRect at BeginTable() because output in BG0/BG1 is cpu-clipped + ImRect Bg2ClipRectForDrawCmd; // Actual ImDrawCmd clip rect for BG2 channel. This tends to be a correct, tight-fit, because output to BG2 are done by widgets relying on regular ClipRect. + ImRect HostClipRect; // This is used to check if we can eventually merge our columns draw calls into the current draw call of the current window. + ImRect HostBackupInnerClipRect; // Backup of InnerWindow->ClipRect during PushTableBackground()/PopTableBackground() + ImGuiWindow* OuterWindow; // Parent window for the table + ImGuiWindow* InnerWindow; // Window holding the table data (== OuterWindow or a child window) + ImGuiTextBuffer ColumnsNames; // Contiguous buffer holding columns names + ImDrawListSplitter* DrawSplitter; // Shortcut to TempData->DrawSplitter while in table. Isolate draw commands per columns to avoid switching clip rect constantly + ImGuiTableInstanceData InstanceDataFirst; + ImVector InstanceDataExtra; // FIXME-OPT: Using a small-vector pattern would be good. + ImGuiTableColumnSortSpecs SortSpecsSingle; + ImVector SortSpecsMulti; // FIXME-OPT: Using a small-vector pattern would be good. + ImGuiTableSortSpecs SortSpecs; // Public facing sorts specs, this is what we return in TableGetSortSpecs() + ImGuiTableColumnIdx SortSpecsCount; + ImGuiTableColumnIdx ColumnsEnabledCount; // Number of enabled columns (<= ColumnsCount) + ImGuiTableColumnIdx ColumnsEnabledFixedCount; // Number of enabled columns using fixed width (<= ColumnsCount) + ImGuiTableColumnIdx DeclColumnsCount; // Count calls to TableSetupColumn() + ImGuiTableColumnIdx AngledHeadersCount; // Count columns with angled headers + ImGuiTableColumnIdx HoveredColumnBody; // Index of column whose visible region is being hovered. Important: == ColumnsCount when hovering empty region after the right-most column! + ImGuiTableColumnIdx HoveredColumnBorder; // Index of column whose right-border is being hovered (for resizing). + ImGuiTableColumnIdx HighlightColumnHeader; // Index of column which should be highlighted. + ImGuiTableColumnIdx AutoFitSingleColumn; // Index of single column requesting auto-fit. + ImGuiTableColumnIdx ResizedColumn; // Index of column being resized. Reset when InstanceCurrent==0. + ImGuiTableColumnIdx LastResizedColumn; // Index of column being resized from previous frame. + ImGuiTableColumnIdx HeldHeaderColumn; // Index of column header being held. + ImGuiTableColumnIdx ReorderColumn; // Index of column being reordered. (not cleared) + ImGuiTableColumnIdx ReorderColumnDir; // -1 or +1 + ImGuiTableColumnIdx LeftMostEnabledColumn; // Index of left-most non-hidden column. + ImGuiTableColumnIdx RightMostEnabledColumn; // Index of right-most non-hidden column. + ImGuiTableColumnIdx LeftMostStretchedColumn; // Index of left-most stretched column. + ImGuiTableColumnIdx RightMostStretchedColumn; // Index of right-most stretched column. + ImGuiTableColumnIdx ContextPopupColumn; // Column right-clicked on, of -1 if opening context menu from a neutral/empty spot + ImGuiTableColumnIdx FreezeRowsRequest; // Requested frozen rows count + ImGuiTableColumnIdx FreezeRowsCount; // Actual frozen row count (== FreezeRowsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx FreezeColumnsRequest; // Requested frozen columns count + ImGuiTableColumnIdx FreezeColumnsCount; // Actual frozen columns count (== FreezeColumnsRequest, or == 0 when no scrolling offset) + ImGuiTableColumnIdx RowCellDataCurrent; // Index of current RowCellData[] entry in current row + ImGuiTableDrawChannelIdx DummyDrawChannel; // Redirect non-visible columns here. + ImGuiTableDrawChannelIdx Bg2DrawChannelCurrent; // For Selectable() and other widgets drawing across columns after the freezing line. Index within DrawSplitter.Channels[] + ImGuiTableDrawChannelIdx Bg2DrawChannelUnfrozen; + ImS8 NavLayer; // ImGuiNavLayer at the time of BeginTable(). + bool IsLayoutLocked; // Set by TableUpdateLayout() which is called when beginning the first row. + bool IsInsideRow; // Set when inside TableBeginRow()/TableEndRow(). + bool IsInitializing; + bool IsSortSpecsDirty; + bool IsUsingHeaders; // Set when the first row had the ImGuiTableRowFlags_Headers flag. + bool IsContextPopupOpen; // Set when default context menu is open (also see: ContextPopupColumn, InstanceInteracted). + bool DisableDefaultContextMenu; // Disable default context menu contents. You may submit your own using TableBeginContextMenuPopup()/EndPopup() + bool IsSettingsRequestLoad; + bool IsSettingsDirty; // Set when table settings have changed and needs to be reported into ImGuiTableSetttings data. + bool IsDefaultDisplayOrder; // Set when display order is unchanged from default (DisplayOrder contains 0...Count-1) + bool IsResetAllRequest; + bool IsResetDisplayOrderRequest; + bool IsUnfrozenRows; // Set when we got past the frozen row. + bool IsDefaultSizingPolicy; // Set if user didn't explicitly set a sizing policy in BeginTable() + bool IsActiveIdAliveBeforeTable; + bool IsActiveIdInTable; + bool HasScrollbarYCurr; // Whether ANY instance of this table had a vertical scrollbar during the current frame. + bool HasScrollbarYPrev; // Whether ANY instance of this table had a vertical scrollbar during the previous. + bool MemoryCompacted; + bool HostSkipItems; // Backup of InnerWindow->SkipItem at the end of BeginTable(), because we will overwrite InnerWindow->SkipItem on a per-column basis + + ImGuiTable() { memset(this, 0, sizeof(*this)); LastFrameActive = -1; } + ~ImGuiTable() { IM_FREE(RawData); } +}; + +// Transient data that are only needed between BeginTable() and EndTable(), those buffers are shared (1 per level of stacked table). +// - Accessing those requires chasing an extra pointer so for very frequently used data we leave them in the main table structure. +// - We also leave out of this structure data that tend to be particularly useful for debugging/metrics. +// FIXME-TABLE: more transient data could be stored in a stacked ImGuiTableTempData: e.g. SortSpecs. +// sizeof() ~ 136 bytes. +struct IMGUI_API ImGuiTableTempData +{ + int TableIndex; // Index in g.Tables.Buf[] pool + float LastTimeActive; // Last timestamp this structure was used + float AngledHeadersExtraWidth; // Used in EndTable() + ImVector AngledHeadersRequests; // Used in TableAngledHeadersRow() + + ImVec2 UserOuterSize; // outer_size.x passed to BeginTable() + ImDrawListSplitter DrawSplitter; + + ImRect HostBackupWorkRect; // Backup of InnerWindow->WorkRect at the end of BeginTable() + ImRect HostBackupParentWorkRect; // Backup of InnerWindow->ParentWorkRect at the end of BeginTable() + ImVec2 HostBackupPrevLineSize; // Backup of InnerWindow->DC.PrevLineSize at the end of BeginTable() + ImVec2 HostBackupCurrLineSize; // Backup of InnerWindow->DC.CurrLineSize at the end of BeginTable() + ImVec2 HostBackupCursorMaxPos; // Backup of InnerWindow->DC.CursorMaxPos at the end of BeginTable() + ImVec1 HostBackupColumnsOffset; // Backup of OuterWindow->DC.ColumnsOffset at the end of BeginTable() + float HostBackupItemWidth; // Backup of OuterWindow->DC.ItemWidth at the end of BeginTable() + int HostBackupItemWidthStackSize;//Backup of OuterWindow->DC.ItemWidthStack.Size at the end of BeginTable() + + ImGuiTableTempData() { memset(this, 0, sizeof(*this)); LastTimeActive = -1.0f; } +}; + +// sizeof() ~ 16 +struct ImGuiTableColumnSettings +{ + float WidthOrWeight; + ImGuiID UserID; + ImGuiTableColumnIdx Index; + ImGuiTableColumnIdx DisplayOrder; + ImGuiTableColumnIdx SortOrder; + ImU8 SortDirection : 2; + ImS8 IsEnabled : 2; // "Visible" in ini file + ImU8 IsStretch : 1; + + ImGuiTableColumnSettings() + { + WidthOrWeight = 0.0f; + UserID = 0; + Index = -1; + DisplayOrder = SortOrder = -1; + SortDirection = ImGuiSortDirection_None; + IsEnabled = -1; + IsStretch = 0; + } +}; + +// This is designed to be stored in a single ImChunkStream (1 header followed by N ImGuiTableColumnSettings, etc.) +struct ImGuiTableSettings +{ + ImGuiID ID; // Set to 0 to invalidate/delete the setting + ImGuiTableFlags SaveFlags; // Indicate data we want to save using the Resizable/Reorderable/Sortable/Hideable flags (could be using its own flags..) + float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. + ImGuiTableColumnIdx ColumnsCount; + ImGuiTableColumnIdx ColumnsCountMax; // Maximum number of columns this settings instance can store, we can recycle a settings instance with lower number of columns but not higher + bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + + ImGuiTableSettings() { memset(this, 0, sizeof(*this)); } + ImGuiTableColumnSettings* GetColumnSettings() { return (ImGuiTableColumnSettings*)(this + 1); } +}; + +//----------------------------------------------------------------------------- +// [SECTION] ImGui internal API +// No guarantee of forward compatibility here! //----------------------------------------------------------------------------- namespace ImGui { + // Windows // We should always have a CurrentWindow in the stack (there is an implicit "Debug" window) - // If this ever crash because g.CurrentWindow is NULL it means that either + // If this ever crashes because g.CurrentWindow is NULL, it means that either: // - ImGui::NewFrame() has never been called, which is illegal. // - You are calling ImGui functions after ImGui::EndFrame()/ImGui::Render() and before the next ImGui::NewFrame(), which is also illegal. + IMGUI_API ImGuiIO& GetIO(ImGuiContext* ctx); + IMGUI_API ImGuiPlatformIO& GetPlatformIO(ImGuiContext* ctx); inline ImGuiWindow* GetCurrentWindowRead() { ImGuiContext& g = *GImGui; return g.CurrentWindow; } inline ImGuiWindow* GetCurrentWindow() { ImGuiContext& g = *GImGui; g.CurrentWindow->WriteAccessed = true; return g.CurrentWindow; } IMGUI_API ImGuiWindow* FindWindowByID(ImGuiID id); IMGUI_API ImGuiWindow* FindWindowByName(const char* name); - IMGUI_API void FocusWindow(ImGuiWindow* window); - IMGUI_API void FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window); - IMGUI_API void BringWindowToFocusFront(ImGuiWindow* window); - IMGUI_API void BringWindowToDisplayFront(ImGuiWindow* window); - IMGUI_API void BringWindowToDisplayBack(ImGuiWindow* window); IMGUI_API void UpdateWindowParentAndRootLinks(ImGuiWindow* window, ImGuiWindowFlags flags, ImGuiWindow* parent_window); - IMGUI_API ImVec2 CalcWindowExpectedSize(ImGuiWindow* window); - IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent); + IMGUI_API void UpdateWindowSkipRefresh(ImGuiWindow* window); + IMGUI_API ImVec2 CalcWindowNextAutoFitSize(ImGuiWindow* window); + IMGUI_API bool IsWindowChildOf(ImGuiWindow* window, ImGuiWindow* potential_parent, bool popup_hierarchy, bool dock_hierarchy); + IMGUI_API bool IsWindowWithinBeginStackOf(ImGuiWindow* window, ImGuiWindow* potential_parent); + IMGUI_API bool IsWindowAbove(ImGuiWindow* potential_above, ImGuiWindow* potential_below); IMGUI_API bool IsWindowNavFocusable(ImGuiWindow* window); - IMGUI_API ImRect GetWindowAllowedExtentRect(ImGuiWindow* window); IMGUI_API void SetWindowPos(ImGuiWindow* window, const ImVec2& pos, ImGuiCond cond = 0); IMGUI_API void SetWindowSize(ImGuiWindow* window, const ImVec2& size, ImGuiCond cond = 0); IMGUI_API void SetWindowCollapsed(ImGuiWindow* window, bool collapsed, ImGuiCond cond = 0); - IMGUI_API void GcCompactTransientWindowBuffers(ImGuiWindow* window); - IMGUI_API void GcAwakeTransientWindowBuffers(ImGuiWindow* window); + IMGUI_API void SetWindowHitTestHole(ImGuiWindow* window, const ImVec2& pos, const ImVec2& size); + IMGUI_API void SetWindowHiddenAndSkipItemsForCurrentFrame(ImGuiWindow* window); + inline void SetWindowParentWindowForFocusRoute(ImGuiWindow* window, ImGuiWindow* parent_window) { window->ParentWindowForFocusRoute = parent_window; } // You may also use SetNextWindowClass()'s FocusRouteParentWindowId field. + inline ImRect WindowRectAbsToRel(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x - off.x, r.Min.y - off.y, r.Max.x - off.x, r.Max.y - off.y); } + inline ImRect WindowRectRelToAbs(ImGuiWindow* window, const ImRect& r) { ImVec2 off = window->DC.CursorStartPos; return ImRect(r.Min.x + off.x, r.Min.y + off.y, r.Max.x + off.x, r.Max.y + off.y); } + inline ImVec2 WindowPosAbsToRel(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x - off.x, p.y - off.y); } + inline ImVec2 WindowPosRelToAbs(ImGuiWindow* window, const ImVec2& p) { ImVec2 off = window->DC.CursorStartPos; return ImVec2(p.x + off.x, p.y + off.y); } + // Windows: Display Order and Focus Order + IMGUI_API void FocusWindow(ImGuiWindow* window, ImGuiFocusRequestFlags flags = 0); + IMGUI_API void FocusTopMostWindowUnderOne(ImGuiWindow* under_this_window, ImGuiWindow* ignore_window, ImGuiViewport* filter_viewport, ImGuiFocusRequestFlags flags); + IMGUI_API void BringWindowToFocusFront(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayFront(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayBack(ImGuiWindow* window); + IMGUI_API void BringWindowToDisplayBehind(ImGuiWindow* window, ImGuiWindow* above_window); + IMGUI_API int FindWindowDisplayIndex(ImGuiWindow* window); + IMGUI_API ImGuiWindow* FindBottomMostVisibleWindowWithinBeginStack(ImGuiWindow* window); + + // Windows: Idle, Refresh Policies [EXPERIMENTAL] + IMGUI_API void SetNextWindowRefreshPolicy(ImGuiWindowRefreshFlags flags); + + // Fonts, drawing IMGUI_API void SetCurrentFont(ImFont* font); inline ImFont* GetDefaultFont() { ImGuiContext& g = *GImGui; return g.IO.FontDefault ? g.IO.FontDefault : g.IO.Fonts->Fonts[0]; } - inline ImDrawList* GetForegroundDrawList(ImGuiWindow*) { ImGuiContext& g = *GImGui; return &g.ForegroundDrawList; } // This seemingly unnecessary wrapper simplifies compatibility between the 'master' and 'docking' branches. + IMGUI_API void PushPasswordFont(); + inline ImDrawList* GetForegroundDrawList(ImGuiWindow* window) { return GetForegroundDrawList(window->Viewport); } + IMGUI_API void AddDrawListToDrawDataEx(ImDrawData* draw_data, ImVector* out_list, ImDrawList* draw_list); // Init - IMGUI_API void Initialize(ImGuiContext* context); - IMGUI_API void Shutdown(ImGuiContext* context); // Since 1.60 this is a _private_ function. You can call DestroyContext() to destroy the context created by CreateContext(). + IMGUI_API void Initialize(); + IMGUI_API void Shutdown(); // Since 1.60 this is a _private_ function. You can call DestroyContext() to destroy the context created by CreateContext(). // NewFrame + IMGUI_API void UpdateInputEvents(bool trickle_fast_inputs); IMGUI_API void UpdateHoveredWindowAndCaptureFlags(); + IMGUI_API void FindHoveredWindowEx(const ImVec2& pos, bool find_first_and_in_any_viewport, ImGuiWindow** out_hovered_window, ImGuiWindow** out_hovered_window_under_moving_window); IMGUI_API void StartMouseMovingWindow(ImGuiWindow* window); + IMGUI_API void StartMouseMovingWindowOrNode(ImGuiWindow* window, ImGuiDockNode* node, bool undock); IMGUI_API void UpdateMouseMovingWindowNewFrame(); IMGUI_API void UpdateMouseMovingWindowEndFrame(); + // Generic context hooks + IMGUI_API ImGuiID AddContextHook(ImGuiContext* context, const ImGuiContextHook* hook); + IMGUI_API void RemoveContextHook(ImGuiContext* context, ImGuiID hook_to_remove); + IMGUI_API void CallContextHooks(ImGuiContext* context, ImGuiContextHookType type); + + // Viewports + IMGUI_API void TranslateWindowsInViewport(ImGuiViewportP* viewport, const ImVec2& old_pos, const ImVec2& new_pos, const ImVec2& old_size, const ImVec2& new_size); + IMGUI_API void ScaleWindowsInViewport(ImGuiViewportP* viewport, float scale); + IMGUI_API void DestroyPlatformWindow(ImGuiViewportP* viewport); + IMGUI_API void SetWindowViewport(ImGuiWindow* window, ImGuiViewportP* viewport); + IMGUI_API void SetCurrentViewport(ImGuiWindow* window, ImGuiViewportP* viewport); + IMGUI_API const ImGuiPlatformMonitor* GetViewportPlatformMonitor(ImGuiViewport* viewport); + IMGUI_API ImGuiViewportP* FindHoveredViewportFromPlatformWindowStack(const ImVec2& mouse_platform_pos); + // Settings IMGUI_API void MarkIniSettingsDirty(); IMGUI_API void MarkIniSettingsDirty(ImGuiWindow* window); - IMGUI_API ImGuiWindowSettings* CreateNewWindowSettings(const char* name); - IMGUI_API ImGuiWindowSettings* FindWindowSettings(ImGuiID id); - IMGUI_API ImGuiWindowSettings* FindOrCreateWindowSettings(const char* name); + IMGUI_API void ClearIniSettings(); + IMGUI_API void AddSettingsHandler(const ImGuiSettingsHandler* handler); + IMGUI_API void RemoveSettingsHandler(const char* type_name); IMGUI_API ImGuiSettingsHandler* FindSettingsHandler(const char* type_name); + // Settings - Windows + IMGUI_API ImGuiWindowSettings* CreateNewWindowSettings(const char* name); + IMGUI_API ImGuiWindowSettings* FindWindowSettingsByID(ImGuiID id); + IMGUI_API ImGuiWindowSettings* FindWindowSettingsByWindow(ImGuiWindow* window); + IMGUI_API void ClearWindowSettings(const char* name); + + // Localization + IMGUI_API void LocalizeRegisterEntries(const ImGuiLocEntry* entries, int count); + inline const char* LocalizeGetMsg(ImGuiLocKey key) { ImGuiContext& g = *GImGui; const char* msg = g.LocalizationTable[key]; return msg ? msg : "*Missing Text*"; } + // Scrolling - IMGUI_API void SetScrollX(ImGuiWindow* window, float new_scroll_x); - IMGUI_API void SetScrollY(ImGuiWindow* window, float new_scroll_y); - IMGUI_API void SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio = 0.5f); - IMGUI_API void SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio = 0.5f); - IMGUI_API ImVec2 ScrollToBringRectIntoView(ImGuiWindow* window, const ImRect& item_rect); + IMGUI_API void SetScrollX(ImGuiWindow* window, float scroll_x); + IMGUI_API void SetScrollY(ImGuiWindow* window, float scroll_y); + IMGUI_API void SetScrollFromPosX(ImGuiWindow* window, float local_x, float center_x_ratio); + IMGUI_API void SetScrollFromPosY(ImGuiWindow* window, float local_y, float center_y_ratio); + + // Early work-in-progress API (ScrollToItem() will become public) + IMGUI_API void ScrollToItem(ImGuiScrollFlags flags = 0); + IMGUI_API void ScrollToRect(ImGuiWindow* window, const ImRect& rect, ImGuiScrollFlags flags = 0); + IMGUI_API ImVec2 ScrollToRectEx(ImGuiWindow* window, const ImRect& rect, ImGuiScrollFlags flags = 0); +//#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void ScrollToBringRectIntoView(ImGuiWindow* window, const ImRect& rect) { ScrollToRect(window, rect, ImGuiScrollFlags_KeepVisibleEdgeY); } +//#endif // Basic Accessors - inline ImGuiID GetItemID() { ImGuiContext& g = *GImGui; return g.CurrentWindow->DC.LastItemId; } + inline ImGuiItemStatusFlags GetItemStatusFlags() { ImGuiContext& g = *GImGui; return g.LastItemData.StatusFlags; } + inline ImGuiItemFlags GetItemFlags() { ImGuiContext& g = *GImGui; return g.LastItemData.ItemFlags; } inline ImGuiID GetActiveID() { ImGuiContext& g = *GImGui; return g.ActiveId; } inline ImGuiID GetFocusID() { ImGuiContext& g = *GImGui; return g.NavId; } IMGUI_API void SetActiveID(ImGuiID id, ImGuiWindow* window); @@ -1535,155 +3349,452 @@ namespace ImGui IMGUI_API ImGuiID GetHoveredID(); IMGUI_API void SetHoveredID(ImGuiID id); IMGUI_API void KeepAliveID(ImGuiID id); - IMGUI_API void MarkItemEdited(ImGuiID id); - IMGUI_API void PushOverrideID(ImGuiID id); + IMGUI_API void MarkItemEdited(ImGuiID id); // Mark data associated to given item as "edited", used by IsItemDeactivatedAfterEdit() function. + IMGUI_API void PushOverrideID(ImGuiID id); // Push given value as-is at the top of the ID stack (whereas PushID combines old and new hashes) + IMGUI_API ImGuiID GetIDWithSeed(const char* str_id_begin, const char* str_id_end, ImGuiID seed); + IMGUI_API ImGuiID GetIDWithSeed(int n, ImGuiID seed); // Basic Helpers for widget code - IMGUI_API void ItemSize(const ImVec2& size, float text_offset_y = 0.0f); - IMGUI_API void ItemSize(const ImRect& bb, float text_offset_y = 0.0f); - IMGUI_API bool ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb = NULL); - IMGUI_API bool ItemHoverable(const ImRect& bb, ImGuiID id); - IMGUI_API bool IsClippedEx(const ImRect& bb, ImGuiID id, bool clip_even_when_logged); - IMGUI_API bool FocusableItemRegister(ImGuiWindow* window, ImGuiID id); // Return true if focus is requested - IMGUI_API void FocusableItemUnregister(ImGuiWindow* window); + IMGUI_API void ItemSize(const ImVec2& size, float text_baseline_y = -1.0f); + inline void ItemSize(const ImRect& bb, float text_baseline_y = -1.0f) { ItemSize(bb.GetSize(), text_baseline_y); } // FIXME: This is a misleading API since we expect CursorPos to be bb.Min. + IMGUI_API bool ItemAdd(const ImRect& bb, ImGuiID id, const ImRect* nav_bb = NULL, ImGuiItemFlags extra_flags = 0); + IMGUI_API bool ItemHoverable(const ImRect& bb, ImGuiID id, ImGuiItemFlags item_flags); + IMGUI_API bool IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags = 0); + IMGUI_API bool IsClippedEx(const ImRect& bb, ImGuiID id); + IMGUI_API void SetLastItemData(ImGuiID item_id, ImGuiItemFlags item_flags, ImGuiItemStatusFlags status_flags, const ImRect& item_rect); IMGUI_API ImVec2 CalcItemSize(ImVec2 size, float default_w, float default_h); IMGUI_API float CalcWrapWidthForPos(const ImVec2& pos, float wrap_pos_x); IMGUI_API void PushMultiItemsWidths(int components, float width_full); - IMGUI_API void PushItemFlag(ImGuiItemFlags option, bool enabled); - IMGUI_API void PopItemFlag(); - IMGUI_API bool IsItemToggledSelection(); // Was the last item selection toggled? (after Selectable(), TreeNode() etc. We only returns toggle _event_ in order to handle clipping correctly) - IMGUI_API ImVec2 GetContentRegionMaxAbs(); IMGUI_API void ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess); - // Logging/Capture - IMGUI_API void LogBegin(ImGuiLogType type, int auto_open_depth); // -> BeginCapture() when we design v2 api, for now stay under the radar by using the old name. - IMGUI_API void LogToBuffer(int auto_open_depth = -1); // Start logging/capturing to internal buffer + // Parameter stacks (shared) + IMGUI_API const ImGuiStyleVarInfo* GetStyleVarInfo(ImGuiStyleVar idx); + IMGUI_API void BeginDisabledOverrideReenable(); + IMGUI_API void EndDisabledOverrideReenable(); - // Popups, Modals, Tooltips - IMGUI_API void OpenPopupEx(ImGuiID id); + // Logging/Capture + IMGUI_API void LogBegin(ImGuiLogFlags flags, int auto_open_depth); // -> BeginCapture() when we design v2 api, for now stay under the radar by using the old name. + IMGUI_API void LogToBuffer(int auto_open_depth = -1); // Start logging/capturing to internal buffer + IMGUI_API void LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end = NULL); + IMGUI_API void LogSetNextTextDecoration(const char* prefix, const char* suffix); + + // Childs + IMGUI_API bool BeginChildEx(const char* name, ImGuiID id, const ImVec2& size_arg, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags); + + // Popups, Modals + IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_window_flags); + IMGUI_API bool BeginPopupMenuEx(ImGuiID id, const char* label, ImGuiWindowFlags extra_window_flags); + IMGUI_API void OpenPopupEx(ImGuiID id, ImGuiPopupFlags popup_flags = ImGuiPopupFlags_None); IMGUI_API void ClosePopupToLevel(int remaining, bool restore_focus_to_window_under_popup); IMGUI_API void ClosePopupsOverWindow(ImGuiWindow* ref_window, bool restore_focus_to_window_under_popup); - IMGUI_API bool IsPopupOpen(ImGuiID id); // Test for id within current popup stack level (currently begin-ed into); this doesn't scan the whole popup stack! - IMGUI_API bool BeginPopupEx(ImGuiID id, ImGuiWindowFlags extra_flags); - IMGUI_API void BeginTooltipEx(ImGuiWindowFlags extra_flags, bool override_previous_tooltip = true); + IMGUI_API void ClosePopupsExceptModals(); + IMGUI_API bool IsPopupOpen(ImGuiID id, ImGuiPopupFlags popup_flags); + IMGUI_API ImRect GetPopupAllowedExtentRect(ImGuiWindow* window); IMGUI_API ImGuiWindow* GetTopMostPopupModal(); + IMGUI_API ImGuiWindow* GetTopMostAndVisiblePopupModal(); + IMGUI_API ImGuiWindow* FindBlockingModal(ImGuiWindow* window); IMGUI_API ImVec2 FindBestWindowPosForPopup(ImGuiWindow* window); - IMGUI_API ImVec2 FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy = ImGuiPopupPositionPolicy_Default); + IMGUI_API ImVec2 FindBestWindowPosForPopupEx(const ImVec2& ref_pos, const ImVec2& size, ImGuiDir* last_dir, const ImRect& r_outer, const ImRect& r_avoid, ImGuiPopupPositionPolicy policy); - // Navigation + // Tooltips + IMGUI_API bool BeginTooltipEx(ImGuiTooltipFlags tooltip_flags, ImGuiWindowFlags extra_window_flags); + IMGUI_API bool BeginTooltipHidden(); + + // Menus + IMGUI_API bool BeginViewportSideBar(const char* name, ImGuiViewport* viewport, ImGuiDir dir, float size, ImGuiWindowFlags window_flags); + IMGUI_API bool BeginMenuEx(const char* label, const char* icon, bool enabled = true); + IMGUI_API bool MenuItemEx(const char* label, const char* icon, const char* shortcut = NULL, bool selected = false, bool enabled = true); + + // Combos + IMGUI_API bool BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags); + IMGUI_API bool BeginComboPreview(); + IMGUI_API void EndComboPreview(); + + // Keyboard/Gamepad Navigation IMGUI_API void NavInitWindow(ImGuiWindow* window, bool force_reinit); + IMGUI_API void NavInitRequestApplyResult(); IMGUI_API bool NavMoveRequestButNoResultYet(); + IMGUI_API void NavMoveRequestSubmit(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); + IMGUI_API void NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, ImGuiNavMoveFlags move_flags, ImGuiScrollFlags scroll_flags); + IMGUI_API void NavMoveRequestResolveWithLastItem(ImGuiNavItemData* result); + IMGUI_API void NavMoveRequestResolveWithPastTreeNode(ImGuiNavItemData* result, ImGuiTreeNodeStackData* tree_node_data); IMGUI_API void NavMoveRequestCancel(); - IMGUI_API void NavMoveRequestForward(ImGuiDir move_dir, ImGuiDir clip_dir, const ImRect& bb_rel, ImGuiNavMoveFlags move_flags); + IMGUI_API void NavMoveRequestApplyResult(); IMGUI_API void NavMoveRequestTryWrapping(ImGuiWindow* window, ImGuiNavMoveFlags move_flags); - IMGUI_API float GetNavInputAmount(ImGuiNavInput n, ImGuiInputReadMode mode); - IMGUI_API ImVec2 GetNavInputAmount2d(ImGuiNavDirSourceFlags dir_sources, ImGuiInputReadMode mode, float slow_factor = 0.0f, float fast_factor = 0.0f); - IMGUI_API int CalcTypematicPressedRepeatAmount(float t, float t_prev, float repeat_delay, float repeat_rate); - IMGUI_API void ActivateItem(ImGuiID id); // Remotely activate a button, checkbox, tree node etc. given its unique ID. activation is queued and processed on the next frame when the item is encountered again. - IMGUI_API void SetNavID(ImGuiID id, int nav_layer); - IMGUI_API void SetNavIDWithRectRel(ImGuiID id, int nav_layer, const ImRect& rect_rel); + IMGUI_API void NavHighlightActivated(ImGuiID id); + IMGUI_API void NavClearPreferredPosForAxis(ImGuiAxis axis); + IMGUI_API void SetNavCursorVisibleAfterMove(); + IMGUI_API void NavUpdateCurrentWindowIsScrollPushableX(); + IMGUI_API void SetNavWindow(ImGuiWindow* window); + IMGUI_API void SetNavID(ImGuiID id, ImGuiNavLayer nav_layer, ImGuiID focus_scope_id, const ImRect& rect_rel); + IMGUI_API void SetNavFocusScope(ImGuiID focus_scope_id); + + // Focus/Activation + // This should be part of a larger set of API: FocusItem(offset = -1), FocusItemByID(id), ActivateItem(offset = -1), ActivateItemByID(id) etc. which are + // much harder to design and implement than expected. I have a couple of private branches on this matter but it's not simple. For now implementing the easy ones. + IMGUI_API void FocusItem(); // Focus last item (no selection/activation). + IMGUI_API void ActivateItemByID(ImGuiID id); // Activate an item by ID (button, checkbox, tree node etc.). Activation is queued and processed on the next frame when the item is encountered again. // Inputs - IMGUI_API bool IsMouseDragPastThreshold(int button, float lock_threshold = -1.0f); - inline bool IsKeyPressedMap(ImGuiKey key, bool repeat = true) { const int key_index = GImGui->IO.KeyMap[key]; return (key_index >= 0) ? IsKeyPressed(key_index, repeat) : false; } - inline bool IsNavInputDown(ImGuiNavInput n) { return GImGui->IO.NavInputs[n] > 0.0f; } - inline bool IsNavInputPressed(ImGuiNavInput n, ImGuiInputReadMode mode) { return GetNavInputAmount(n, mode) > 0.0f; } - inline bool IsNavInputPressedAnyOfTwo(ImGuiNavInput n1, ImGuiNavInput n2, ImGuiInputReadMode mode) { return (GetNavInputAmount(n1, mode) + GetNavInputAmount(n2, mode)) > 0.0f; } + // FIXME: Eventually we should aim to move e.g. IsActiveIdUsingKey() into IsKeyXXX functions. + inline bool IsNamedKey(ImGuiKey key) { return key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END; } + inline bool IsNamedKeyOrMod(ImGuiKey key) { return (key >= ImGuiKey_NamedKey_BEGIN && key < ImGuiKey_NamedKey_END) || key == ImGuiMod_Ctrl || key == ImGuiMod_Shift || key == ImGuiMod_Alt || key == ImGuiMod_Super; } + inline bool IsLegacyKey(ImGuiKey key) { return key >= ImGuiKey_LegacyNativeKey_BEGIN && key < ImGuiKey_LegacyNativeKey_END; } + inline bool IsKeyboardKey(ImGuiKey key) { return key >= ImGuiKey_Keyboard_BEGIN && key < ImGuiKey_Keyboard_END; } + inline bool IsGamepadKey(ImGuiKey key) { return key >= ImGuiKey_Gamepad_BEGIN && key < ImGuiKey_Gamepad_END; } + inline bool IsMouseKey(ImGuiKey key) { return key >= ImGuiKey_Mouse_BEGIN && key < ImGuiKey_Mouse_END; } + inline bool IsAliasKey(ImGuiKey key) { return key >= ImGuiKey_Aliases_BEGIN && key < ImGuiKey_Aliases_END; } + inline bool IsLRModKey(ImGuiKey key) { return key >= ImGuiKey_LeftCtrl && key <= ImGuiKey_RightSuper; } + ImGuiKeyChord FixupKeyChord(ImGuiKeyChord key_chord); + inline ImGuiKey ConvertSingleModFlagToKey(ImGuiKey key) + { + if (key == ImGuiMod_Ctrl) return ImGuiKey_ReservedForModCtrl; + if (key == ImGuiMod_Shift) return ImGuiKey_ReservedForModShift; + if (key == ImGuiMod_Alt) return ImGuiKey_ReservedForModAlt; + if (key == ImGuiMod_Super) return ImGuiKey_ReservedForModSuper; + return key; + } + + IMGUI_API ImGuiKeyData* GetKeyData(ImGuiContext* ctx, ImGuiKey key); + inline ImGuiKeyData* GetKeyData(ImGuiKey key) { ImGuiContext& g = *GImGui; return GetKeyData(&g, key); } + IMGUI_API const char* GetKeyChordName(ImGuiKeyChord key_chord); + inline ImGuiKey MouseButtonToKey(ImGuiMouseButton button) { IM_ASSERT(button >= 0 && button < ImGuiMouseButton_COUNT); return (ImGuiKey)(ImGuiKey_MouseLeft + button); } + IMGUI_API bool IsMouseDragPastThreshold(ImGuiMouseButton button, float lock_threshold = -1.0f); + IMGUI_API ImVec2 GetKeyMagnitude2d(ImGuiKey key_left, ImGuiKey key_right, ImGuiKey key_up, ImGuiKey key_down); + IMGUI_API float GetNavTweakPressedAmount(ImGuiAxis axis); + IMGUI_API int CalcTypematicRepeatAmount(float t0, float t1, float repeat_delay, float repeat_rate); + IMGUI_API void GetTypematicRepeatRate(ImGuiInputFlags flags, float* repeat_delay, float* repeat_rate); + IMGUI_API void TeleportMousePos(const ImVec2& pos); + IMGUI_API void SetActiveIdUsingAllKeyboardKeys(); + inline bool IsActiveIdUsingNavDir(ImGuiDir dir) { ImGuiContext& g = *GImGui; return (g.ActiveIdUsingNavDirMask & (1 << dir)) != 0; } + + // [EXPERIMENTAL] Low-Level: Key/Input Ownership + // - The idea is that instead of "eating" a given input, we can link to an owner id. + // - Ownership is most often claimed as a result of reacting to a press/down event (but occasionally may be claimed ahead). + // - Input queries can then read input by specifying ImGuiKeyOwner_Any (== 0), ImGuiKeyOwner_NoOwner (== -1) or a custom ID. + // - Legacy input queries (without specifying an owner or _Any or _None) are equivalent to using ImGuiKeyOwner_Any (== 0). + // - Input ownership is automatically released on the frame after a key is released. Therefore: + // - for ownership registration happening as a result of a down/press event, the SetKeyOwner() call may be done once (common case). + // - for ownership registration happening ahead of a down/press event, the SetKeyOwner() call needs to be made every frame (happens if e.g. claiming ownership on hover). + // - SetItemKeyOwner() is a shortcut for common simple case. A custom widget will probably want to call SetKeyOwner() multiple times directly based on its interaction state. + // - This is marked experimental because not all widgets are fully honoring the Set/Test idioms. We will need to move forward step by step. + // Please open a GitHub Issue to submit your usage scenario or if there's a use case you need solved. + IMGUI_API ImGuiID GetKeyOwner(ImGuiKey key); + IMGUI_API void SetKeyOwner(ImGuiKey key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + IMGUI_API void SetKeyOwnersForKeyChord(ImGuiKeyChord key, ImGuiID owner_id, ImGuiInputFlags flags = 0); + IMGUI_API void SetItemKeyOwner(ImGuiKey key, ImGuiInputFlags flags); // Set key owner to last item if it is hovered or active. Equivalent to 'if (IsItemHovered() || IsItemActive()) { SetKeyOwner(key, GetItemID());'. + IMGUI_API bool TestKeyOwner(ImGuiKey key, ImGuiID owner_id); // Test that key is either not owned, either owned by 'owner_id' + inline ImGuiKeyOwnerData* GetKeyOwnerData(ImGuiContext* ctx, ImGuiKey key) { if (key & ImGuiMod_Mask_) key = ConvertSingleModFlagToKey(key); IM_ASSERT(IsNamedKey(key)); return &ctx->KeysOwnerData[key - ImGuiKey_NamedKey_BEGIN]; } + + // [EXPERIMENTAL] High-Level: Input Access functions w/ support for Key/Input Ownership + // - Important: legacy IsKeyPressed(ImGuiKey, bool repeat=true) _DEFAULTS_ to repeat, new IsKeyPressed() requires _EXPLICIT_ ImGuiInputFlags_Repeat flag. + // - Expected to be later promoted to public API, the prototypes are designed to replace existing ones (since owner_id can default to Any == 0) + // - Specifying a value for 'ImGuiID owner' will test that EITHER the key is NOT owned (UNLESS locked), EITHER the key is owned by 'owner'. + // Legacy functions use ImGuiKeyOwner_Any meaning that they typically ignore ownership, unless a call to SetKeyOwner() explicitly used ImGuiInputFlags_LockThisFrame or ImGuiInputFlags_LockUntilRelease. + // - Binding generators may want to ignore those for now, or suffix them with Ex() until we decide if this gets moved into public API. + IMGUI_API bool IsKeyDown(ImGuiKey key, ImGuiID owner_id); + IMGUI_API bool IsKeyPressed(ImGuiKey key, ImGuiInputFlags flags, ImGuiID owner_id = 0); // Important: when transitioning from old to new IsKeyPressed(): old API has "bool repeat = true", so would default to repeat. New API requiress explicit ImGuiInputFlags_Repeat. + IMGUI_API bool IsKeyReleased(ImGuiKey key, ImGuiID owner_id); + IMGUI_API bool IsKeyChordPressed(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id = 0); + IMGUI_API bool IsMouseDown(ImGuiMouseButton button, ImGuiID owner_id); + IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, ImGuiInputFlags flags, ImGuiID owner_id = 0); + IMGUI_API bool IsMouseReleased(ImGuiMouseButton button, ImGuiID owner_id); + IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button, ImGuiID owner_id); + + // Shortcut Testing & Routing + // - Set Shortcut() and SetNextItemShortcut() in imgui.h + // - When a policy (except for ImGuiInputFlags_RouteAlways *) is set, Shortcut() will register itself with SetShortcutRouting(), + // allowing the system to decide where to route the input among other route-aware calls. + // (* using ImGuiInputFlags_RouteAlways is roughly equivalent to calling IsKeyChordPressed(key) and bypassing route registration and check) + // - When using one of the routing option: + // - The default route is ImGuiInputFlags_RouteFocused (accept inputs if window is in focus stack. Deep-most focused window takes inputs. ActiveId takes inputs over deep-most focused window.) + // - Routes are requested given a chord (key + modifiers) and a routing policy. + // - Routes are resolved during NewFrame(): if keyboard modifiers are matching current ones: SetKeyOwner() is called + route is granted for the frame. + // - Each route may be granted to a single owner. When multiple requests are made we have policies to select the winning route (e.g. deep most window). + // - Multiple read sites may use the same owner id can all access the granted route. + // - When owner_id is 0 we use the current Focus Scope ID as a owner ID in order to identify our location. + // - You can chain two unrelated windows in the focus stack using SetWindowParentWindowForFocusRoute() + // e.g. if you have a tool window associated to a document, and you want document shortcuts to run when the tool is focused. + IMGUI_API bool Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id); + IMGUI_API bool SetShortcutRouting(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID owner_id); // owner_id needs to be explicit and cannot be 0 + IMGUI_API bool TestShortcutRouting(ImGuiKeyChord key_chord, ImGuiID owner_id); + IMGUI_API ImGuiKeyRoutingData* GetShortcutRoutingData(ImGuiKeyChord key_chord); + + // Docking + // (some functions are only declared in imgui.cpp, see Docking section) + IMGUI_API void DockContextInitialize(ImGuiContext* ctx); + IMGUI_API void DockContextShutdown(ImGuiContext* ctx); + IMGUI_API void DockContextClearNodes(ImGuiContext* ctx, ImGuiID root_id, bool clear_settings_refs); // Use root_id==0 to clear all + IMGUI_API void DockContextRebuildNodes(ImGuiContext* ctx); + IMGUI_API void DockContextNewFrameUpdateUndocking(ImGuiContext* ctx); + IMGUI_API void DockContextNewFrameUpdateDocking(ImGuiContext* ctx); + IMGUI_API void DockContextEndFrame(ImGuiContext* ctx); + IMGUI_API ImGuiID DockContextGenNodeID(ImGuiContext* ctx); + IMGUI_API void DockContextQueueDock(ImGuiContext* ctx, ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload, ImGuiDir split_dir, float split_ratio, bool split_outer); + IMGUI_API void DockContextQueueUndockWindow(ImGuiContext* ctx, ImGuiWindow* window); + IMGUI_API void DockContextQueueUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); + IMGUI_API void DockContextProcessUndockWindow(ImGuiContext* ctx, ImGuiWindow* window, bool clear_persistent_docking_ref = true); + IMGUI_API void DockContextProcessUndockNode(ImGuiContext* ctx, ImGuiDockNode* node); + IMGUI_API bool DockContextCalcDropPosForDocking(ImGuiWindow* target, ImGuiDockNode* target_node, ImGuiWindow* payload_window, ImGuiDockNode* payload_node, ImGuiDir split_dir, bool split_outer, ImVec2* out_pos); + IMGUI_API ImGuiDockNode*DockContextFindNodeByID(ImGuiContext* ctx, ImGuiID id); + IMGUI_API void DockNodeWindowMenuHandler_Default(ImGuiContext* ctx, ImGuiDockNode* node, ImGuiTabBar* tab_bar); + IMGUI_API bool DockNodeBeginAmendTabBar(ImGuiDockNode* node); + IMGUI_API void DockNodeEndAmendTabBar(); + inline ImGuiDockNode* DockNodeGetRootNode(ImGuiDockNode* node) { while (node->ParentNode) node = node->ParentNode; return node; } + inline bool DockNodeIsInHierarchyOf(ImGuiDockNode* node, ImGuiDockNode* parent) { while (node) { if (node == parent) return true; node = node->ParentNode; } return false; } + inline int DockNodeGetDepth(const ImGuiDockNode* node) { int depth = 0; while (node->ParentNode) { node = node->ParentNode; depth++; } return depth; } + inline ImGuiID DockNodeGetWindowMenuButtonId(const ImGuiDockNode* node) { return ImHashStr("#COLLAPSE", 0, node->ID); } + inline ImGuiDockNode* GetWindowDockNode() { ImGuiContext& g = *GImGui; return g.CurrentWindow->DockNode; } + IMGUI_API bool GetWindowAlwaysWantOwnTabBar(ImGuiWindow* window); + IMGUI_API void BeginDocked(ImGuiWindow* window, bool* p_open); + IMGUI_API void BeginDockableDragDropSource(ImGuiWindow* window); + IMGUI_API void BeginDockableDragDropTarget(ImGuiWindow* window); + IMGUI_API void SetWindowDock(ImGuiWindow* window, ImGuiID dock_id, ImGuiCond cond); + + // Docking - Builder function needs to be generally called before the node is used/submitted. + // - The DockBuilderXXX functions are designed to _eventually_ become a public API, but it is too early to expose it and guarantee stability. + // - Do not hold on ImGuiDockNode* pointers! They may be invalidated by any split/merge/remove operation and every frame. + // - To create a DockSpace() node, make sure to set the ImGuiDockNodeFlags_DockSpace flag when calling DockBuilderAddNode(). + // You can create dockspace nodes (attached to a window) _or_ floating nodes (carry its own window) with this API. + // - DockBuilderSplitNode() create 2 child nodes within 1 node. The initial node becomes a parent node. + // - If you intend to split the node immediately after creation using DockBuilderSplitNode(), make sure + // to call DockBuilderSetNodeSize() beforehand. If you don't, the resulting split sizes may not be reliable. + // - Call DockBuilderFinish() after you are done. + IMGUI_API void DockBuilderDockWindow(const char* window_name, ImGuiID node_id); + IMGUI_API ImGuiDockNode*DockBuilderGetNode(ImGuiID node_id); + inline ImGuiDockNode* DockBuilderGetCentralNode(ImGuiID node_id) { ImGuiDockNode* node = DockBuilderGetNode(node_id); if (!node) return NULL; return DockNodeGetRootNode(node)->CentralNode; } + IMGUI_API ImGuiID DockBuilderAddNode(ImGuiID node_id = 0, ImGuiDockNodeFlags flags = 0); + IMGUI_API void DockBuilderRemoveNode(ImGuiID node_id); // Remove node and all its child, undock all windows + IMGUI_API void DockBuilderRemoveNodeDockedWindows(ImGuiID node_id, bool clear_settings_refs = true); + IMGUI_API void DockBuilderRemoveNodeChildNodes(ImGuiID node_id); // Remove all split/hierarchy. All remaining docked windows will be re-docked to the remaining root node (node_id). + IMGUI_API void DockBuilderSetNodePos(ImGuiID node_id, ImVec2 pos); + IMGUI_API void DockBuilderSetNodeSize(ImGuiID node_id, ImVec2 size); + IMGUI_API ImGuiID DockBuilderSplitNode(ImGuiID node_id, ImGuiDir split_dir, float size_ratio_for_node_at_dir, ImGuiID* out_id_at_dir, ImGuiID* out_id_at_opposite_dir); // Create 2 child nodes in this parent node. + IMGUI_API void DockBuilderCopyDockSpace(ImGuiID src_dockspace_id, ImGuiID dst_dockspace_id, ImVector* in_window_remap_pairs); + IMGUI_API void DockBuilderCopyNode(ImGuiID src_node_id, ImGuiID dst_node_id, ImVector* out_node_remap_pairs); + IMGUI_API void DockBuilderCopyWindowSettings(const char* src_name, const char* dst_name); + IMGUI_API void DockBuilderFinish(ImGuiID node_id); + + // [EXPERIMENTAL] Focus Scope + // This is generally used to identify a unique input location (for e.g. a selection set) + // There is one per window (automatically set in Begin), but: + // - Selection patterns generally need to react (e.g. clear a selection) when landing on one item of the set. + // So in order to identify a set multiple lists in same window may each need a focus scope. + // If you imagine an hypothetical BeginSelectionGroup()/EndSelectionGroup() api, it would likely call PushFocusScope()/EndFocusScope() + // - Shortcut routing also use focus scope as a default location identifier if an owner is not provided. + // We don't use the ID Stack for this as it is common to want them separate. + IMGUI_API void PushFocusScope(ImGuiID id); + IMGUI_API void PopFocusScope(); + inline ImGuiID GetCurrentFocusScope() { ImGuiContext& g = *GImGui; return g.CurrentFocusScopeId; } // Focus scope we are outputting into, set by PushFocusScope() // Drag and Drop + IMGUI_API bool IsDragDropActive(); IMGUI_API bool BeginDragDropTargetCustom(const ImRect& bb, ImGuiID id); IMGUI_API void ClearDragDrop(); IMGUI_API bool IsDragDropPayloadBeingAccepted(); + IMGUI_API void RenderDragDropTargetRect(const ImRect& bb, const ImRect& item_clip_rect); - // New Columns API (FIXME-WIP) - IMGUI_API void BeginColumns(const char* str_id, int count, ImGuiColumnsFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns(). - IMGUI_API void EndColumns(); // close columns + // Typing-Select API + // (provide Windows Explorer style "select items by typing partial name" + "cycle through items by typing same letter" feature) + // (this is currently not documented nor used by main library, but should work. See "widgets_typingselect" in imgui_test_suite for usage code. Please let us know if you use this!) + IMGUI_API ImGuiTypingSelectRequest* GetTypingSelectRequest(ImGuiTypingSelectFlags flags = ImGuiTypingSelectFlags_None); + IMGUI_API int TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); + IMGUI_API int TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx); + IMGUI_API int TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data); + + // Box-Select API + IMGUI_API bool BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags); + IMGUI_API void EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags); + + // Multi-Select API + IMGUI_API void MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags); + IMGUI_API void MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed); + IMGUI_API void MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected); + IMGUI_API void MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item); + inline ImGuiBoxSelectState* GetBoxSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.BoxSelectState.ID == id && g.BoxSelectState.IsActive) ? &g.BoxSelectState : NULL; } + inline ImGuiMultiSelectState* GetMultiSelectState(ImGuiID id) { ImGuiContext& g = *GImGui; return g.MultiSelectStorage.GetByKey(id); } + + // Internal Columns API (this is not exposed because we will encourage transitioning to the Tables API) + IMGUI_API void SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect); + IMGUI_API void BeginColumns(const char* str_id, int count, ImGuiOldColumnFlags flags = 0); // setup number of columns. use an identifier to distinguish multiple column sets. close with EndColumns(). + IMGUI_API void EndColumns(); // close columns IMGUI_API void PushColumnClipRect(int column_index); IMGUI_API void PushColumnsBackground(); IMGUI_API void PopColumnsBackground(); IMGUI_API ImGuiID GetColumnsID(const char* str_id, int count); - IMGUI_API ImGuiColumns* FindOrCreateColumns(ImGuiWindow* window, ImGuiID id); - IMGUI_API float GetColumnOffsetFromNorm(const ImGuiColumns* columns, float offset_norm); - IMGUI_API float GetColumnNormFromOffset(const ImGuiColumns* columns, float offset); + IMGUI_API ImGuiOldColumns* FindOrCreateColumns(ImGuiWindow* window, ImGuiID id); + IMGUI_API float GetColumnOffsetFromNorm(const ImGuiOldColumns* columns, float offset_norm); + IMGUI_API float GetColumnNormFromOffset(const ImGuiOldColumns* columns, float offset); + + // Tables: Candidates for public API + IMGUI_API void TableOpenContextMenu(int column_n = -1); + IMGUI_API void TableSetColumnWidth(int column_n, float width); + IMGUI_API void TableSetColumnSortDirection(int column_n, ImGuiSortDirection sort_direction, bool append_to_sort_specs); + IMGUI_API int TableGetHoveredRow(); // Retrieve *PREVIOUS FRAME* hovered row. This difference with TableGetHoveredColumn() is the reason why this is not public yet. + IMGUI_API float TableGetHeaderRowHeight(); + IMGUI_API float TableGetHeaderAngledMaxLabelWidth(); + IMGUI_API void TablePushBackgroundChannel(); + IMGUI_API void TablePopBackgroundChannel(); + IMGUI_API void TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label_width, const ImGuiTableHeaderData* data, int data_count); + + // Tables: Internals + inline ImGuiTable* GetCurrentTable() { ImGuiContext& g = *GImGui; return g.CurrentTable; } + IMGUI_API ImGuiTable* TableFindByID(ImGuiID id); + IMGUI_API bool BeginTableEx(const char* name, ImGuiID id, int columns_count, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0, 0), float inner_width = 0.0f); + IMGUI_API void TableBeginInitMemory(ImGuiTable* table, int columns_count); + IMGUI_API void TableBeginApplyRequests(ImGuiTable* table); + IMGUI_API void TableSetupDrawChannels(ImGuiTable* table); + IMGUI_API void TableUpdateLayout(ImGuiTable* table); + IMGUI_API void TableUpdateBorders(ImGuiTable* table); + IMGUI_API void TableUpdateColumnsWeightFromWidth(ImGuiTable* table); + IMGUI_API void TableDrawBorders(ImGuiTable* table); + IMGUI_API void TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display); + IMGUI_API bool TableBeginContextMenuPopup(ImGuiTable* table); + IMGUI_API void TableMergeDrawChannels(ImGuiTable* table); + inline ImGuiTableInstanceData* TableGetInstanceData(ImGuiTable* table, int instance_no) { if (instance_no == 0) return &table->InstanceDataFirst; return &table->InstanceDataExtra[instance_no - 1]; } + inline ImGuiID TableGetInstanceID(ImGuiTable* table, int instance_no) { return TableGetInstanceData(table, instance_no)->TableInstanceID; } + IMGUI_API void TableSortSpecsSanitize(ImGuiTable* table); + IMGUI_API void TableSortSpecsBuild(ImGuiTable* table); + IMGUI_API ImGuiSortDirection TableGetColumnNextSortDirection(ImGuiTableColumn* column); + IMGUI_API void TableFixColumnSortDirection(ImGuiTable* table, ImGuiTableColumn* column); + IMGUI_API float TableGetColumnWidthAuto(ImGuiTable* table, ImGuiTableColumn* column); + IMGUI_API void TableBeginRow(ImGuiTable* table); + IMGUI_API void TableEndRow(ImGuiTable* table); + IMGUI_API void TableBeginCell(ImGuiTable* table, int column_n); + IMGUI_API void TableEndCell(ImGuiTable* table); + IMGUI_API ImRect TableGetCellBgRect(const ImGuiTable* table, int column_n); + IMGUI_API const char* TableGetColumnName(const ImGuiTable* table, int column_n); + IMGUI_API ImGuiID TableGetColumnResizeID(ImGuiTable* table, int column_n, int instance_no = 0); + IMGUI_API float TableCalcMaxColumnWidth(const ImGuiTable* table, int column_n); + IMGUI_API void TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n); + IMGUI_API void TableSetColumnWidthAutoAll(ImGuiTable* table); + IMGUI_API void TableRemove(ImGuiTable* table); + IMGUI_API void TableGcCompactTransientBuffers(ImGuiTable* table); + IMGUI_API void TableGcCompactTransientBuffers(ImGuiTableTempData* table); + IMGUI_API void TableGcCompactSettings(); + + // Tables: Settings + IMGUI_API void TableLoadSettings(ImGuiTable* table); + IMGUI_API void TableSaveSettings(ImGuiTable* table); + IMGUI_API void TableResetSettings(ImGuiTable* table); + IMGUI_API ImGuiTableSettings* TableGetBoundSettings(ImGuiTable* table); + IMGUI_API void TableSettingsAddSettingsHandler(); + IMGUI_API ImGuiTableSettings* TableSettingsCreate(ImGuiID id, int columns_count); + IMGUI_API ImGuiTableSettings* TableSettingsFindByID(ImGuiID id); // Tab Bars + inline ImGuiTabBar* GetCurrentTabBar() { ImGuiContext& g = *GImGui; return g.CurrentTabBar; } IMGUI_API bool BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& bb, ImGuiTabBarFlags flags); IMGUI_API ImGuiTabItem* TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id); + IMGUI_API ImGuiTabItem* TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order); + IMGUI_API ImGuiTabItem* TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar); + IMGUI_API ImGuiTabItem* TabBarGetCurrentTab(ImGuiTabBar* tab_bar); + inline int TabBarGetTabOrder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { return tab_bar->Tabs.index_from_ptr(tab); } + IMGUI_API const char* TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window); IMGUI_API void TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id); IMGUI_API void TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); - IMGUI_API void TabBarQueueChangeTabOrder(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, int dir); - IMGUI_API bool TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags); - IMGUI_API ImVec2 TabItemCalcSize(const char* label, bool has_close_button); + IMGUI_API void TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + IMGUI_API void TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name); + IMGUI_API void TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset); + IMGUI_API void TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, ImVec2 mouse_pos); + IMGUI_API bool TabBarProcessReorder(ImGuiTabBar* tab_bar); + IMGUI_API bool TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window); + IMGUI_API void TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width); + IMGUI_API ImVec2 TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker); + IMGUI_API ImVec2 TabItemCalcSize(ImGuiWindow* window); IMGUI_API void TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col); - IMGUI_API bool TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id); + IMGUI_API void TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped); // Render helpers // AVOID USING OUTSIDE OF IMGUI.CPP! NOT FOR PUBLIC CONSUMPTION. THOSE FUNCTIONS ARE A MESS. THEIR SIGNATURE AND BEHAVIOR WILL CHANGE, THEY NEED TO BE REFACTORED INTO SOMETHING DECENT. // NB: All position are in absolute pixels coordinates (we are never using window coordinates internally) IMGUI_API void RenderText(ImVec2 pos, const char* text, const char* text_end = NULL, bool hide_text_after_hash = true); IMGUI_API void RenderTextWrapped(ImVec2 pos, const char* text, const char* text_end, float wrap_width); - IMGUI_API void RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0,0), const ImRect* clip_rect = NULL); + IMGUI_API void RenderTextClipped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL); IMGUI_API void RenderTextClippedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = NULL); IMGUI_API void RenderTextEllipsis(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, float clip_max_x, float ellipsis_max_x, const char* text, const char* text_end, const ImVec2* text_size_if_known); - IMGUI_API void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool border = true, float rounding = 0.0f); + IMGUI_API void RenderFrame(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, bool borders = true, float rounding = 0.0f); IMGUI_API void RenderFrameBorder(ImVec2 p_min, ImVec2 p_max, float rounding = 0.0f); - IMGUI_API void RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, int rounding_corners_flags = ~0); - IMGUI_API void RenderCheckMark(ImVec2 pos, ImU32 col, float sz); - IMGUI_API void RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavHighlightFlags flags = ImGuiNavHighlightFlags_TypeDefault); // Navigation highlight + IMGUI_API void RenderColorRectWithAlphaCheckerboard(ImDrawList* draw_list, ImVec2 p_min, ImVec2 p_max, ImU32 fill_col, float grid_step, ImVec2 grid_off, float rounding = 0.0f, ImDrawFlags flags = 0); + IMGUI_API void RenderNavCursor(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFlags flags = ImGuiNavRenderCursorFlags_None); // Navigation highlight +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + inline void RenderNavHighlight(const ImRect& bb, ImGuiID id, ImGuiNavRenderCursorFlags flags = ImGuiNavRenderCursorFlags_None) { RenderNavCursor(bb, id, flags); } // Renamed in 1.91.4 +#endif IMGUI_API const char* FindRenderedTextEnd(const char* text, const char* text_end = NULL); // Find the optional ## from which we stop displaying text. - IMGUI_API void LogRenderedText(const ImVec2* ref_pos, const char* text, const char* text_end = NULL); + IMGUI_API void RenderMouseCursor(ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow); // Render helpers (those functions don't access any ImGui state!) IMGUI_API void RenderArrow(ImDrawList* draw_list, ImVec2 pos, ImU32 col, ImGuiDir dir, float scale = 1.0f); IMGUI_API void RenderBullet(ImDrawList* draw_list, ImVec2 pos, ImU32 col); - IMGUI_API void RenderMouseCursor(ImDrawList* draw_list, ImVec2 pos, float scale, ImGuiMouseCursor mouse_cursor, ImU32 col_fill, ImU32 col_border, ImU32 col_shadow); + IMGUI_API void RenderCheckMark(ImDrawList* draw_list, ImVec2 pos, ImU32 col, float sz); IMGUI_API void RenderArrowPointingAt(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, ImGuiDir direction, ImU32 col); + IMGUI_API void RenderArrowDockMenu(ImDrawList* draw_list, ImVec2 p_min, float sz, ImU32 col); IMGUI_API void RenderRectFilledRangeH(ImDrawList* draw_list, const ImRect& rect, ImU32 col, float x_start_norm, float x_end_norm, float rounding); - -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - // 2019/06/07: Updating prototypes of some of the internal functions. Leaving those for reference for a short while. - inline void RenderArrow(ImVec2 pos, ImGuiDir dir, float scale=1.0f) { ImGuiWindow* window = GetCurrentWindow(); RenderArrow(window->DrawList, pos, GetColorU32(ImGuiCol_Text), dir, scale); } - inline void RenderBullet(ImVec2 pos) { ImGuiWindow* window = GetCurrentWindow(); RenderBullet(window->DrawList, pos, GetColorU32(ImGuiCol_Text)); } -#endif + IMGUI_API void RenderRectFilledWithHole(ImDrawList* draw_list, const ImRect& outer, const ImRect& inner, ImU32 col, float rounding); + IMGUI_API ImDrawFlags CalcRoundingFlagsForRectInRect(const ImRect& r_in, const ImRect& r_outer, float threshold); // Widgets IMGUI_API void TextEx(const char* text, const char* text_end = NULL, ImGuiTextFlags flags = 0); - IMGUI_API bool ButtonEx(const char* label, const ImVec2& size_arg = ImVec2(0,0), ImGuiButtonFlags flags = 0); + IMGUI_API bool ButtonEx(const char* label, const ImVec2& size_arg = ImVec2(0, 0), ImGuiButtonFlags flags = 0); + IMGUI_API bool ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size_arg, ImGuiButtonFlags flags = 0); + IMGUI_API bool ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags = 0); + IMGUI_API void SeparatorEx(ImGuiSeparatorFlags flags, float thickness = 1.0f); + IMGUI_API void SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_width); + IMGUI_API bool CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value); + IMGUI_API bool CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value); + + // Widgets: Window Decorations IMGUI_API bool CloseButton(ImGuiID id, const ImVec2& pos); - IMGUI_API bool CollapseButton(ImGuiID id, const ImVec2& pos); - IMGUI_API bool ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size_arg, ImGuiButtonFlags flags); + IMGUI_API bool CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node); IMGUI_API void Scrollbar(ImGuiAxis axis); - IMGUI_API bool ScrollbarEx(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* p_scroll_v, float avail_v, float contents_v, ImDrawCornerFlags rounding_corners); - IMGUI_API ImGuiID GetScrollbarID(ImGuiWindow* window, ImGuiAxis axis); - IMGUI_API void SeparatorEx(ImGuiSeparatorFlags flags); + IMGUI_API bool ScrollbarEx(const ImRect& bb, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 avail_v, ImS64 contents_v, ImDrawFlags draw_rounding_flags = 0); + IMGUI_API ImRect GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis); + IMGUI_API ImGuiID GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis); + IMGUI_API ImGuiID GetWindowResizeCornerID(ImGuiWindow* window, int n); // 0..3: corners + IMGUI_API ImGuiID GetWindowResizeBorderID(ImGuiWindow* window, ImGuiDir dir); // Widgets low-level behaviors IMGUI_API bool ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags = 0); - IMGUI_API bool DragBehavior(ImGuiID id, ImGuiDataType data_type, void* v, float v_speed, const void* v_min, const void* v_max, const char* format, float power, ImGuiDragFlags flags); - IMGUI_API bool SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* v, const void* v_min, const void* v_max, const char* format, float power, ImGuiSliderFlags flags, ImRect* out_grab_bb); - IMGUI_API bool SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend = 0.0f, float hover_visibility_delay = 0.0f); + IMGUI_API bool DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags); + IMGUI_API bool SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); + IMGUI_API bool SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend = 0.0f, float hover_visibility_delay = 0.0f, ImU32 bg_col = 0); + + // Widgets: Tree Nodes IMGUI_API bool TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end = NULL); - IMGUI_API bool TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags = 0); // Consume previous SetNextItemOpen() data, if any. May return true when logging IMGUI_API void TreePushOverrideID(ImGuiID id); + IMGUI_API bool TreeNodeGetOpen(ImGuiID storage_id); + IMGUI_API void TreeNodeSetOpen(ImGuiID storage_id, bool open); + IMGUI_API bool TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags); // Return open state. Consume previous SetNextItemOpen() data, if any. May return true when logging. // Template functions are instantiated in imgui_widgets.cpp for a finite number of types. // To use them externally (for custom widget) you may need an "extern template" statement in your code in order to link to existing instances and silence Clang warnings (see #2036). // e.g. " extern template IMGUI_API float RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, float v); " - template IMGUI_API bool DragBehaviorT(ImGuiDataType data_type, T* v, float v_speed, T v_min, T v_max, const char* format, float power, ImGuiDragFlags flags); - template IMGUI_API bool SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, T* v, T v_min, T v_max, const char* format, float power, ImGuiSliderFlags flags, ImRect* out_grab_bb); - template IMGUI_API float SliderCalcRatioFromValueT(ImGuiDataType data_type, T v, T v_min, T v_max, float power, float linear_zero_pos); - template IMGUI_API T RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, T v); + template IMGUI_API float ScaleRatioFromValueT(ImGuiDataType data_type, T v, T v_min, T v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_size); + template IMGUI_API T ScaleValueFromRatioT(ImGuiDataType data_type, float t, T v_min, T v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_size); + template IMGUI_API bool DragBehaviorT(ImGuiDataType data_type, T* v, float v_speed, T v_min, T v_max, const char* format, ImGuiSliderFlags flags); + template IMGUI_API bool SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, T* v, T v_min, T v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb); + template IMGUI_API T RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, T v); + template IMGUI_API bool CheckboxFlagsT(const char* label, T* flags, T flags_value); // Data type helpers IMGUI_API const ImGuiDataTypeInfo* DataTypeGetInfo(ImGuiDataType data_type); - IMGUI_API int DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* data_ptr, const char* format); - IMGUI_API void DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, void* arg_1, const void* arg_2); - IMGUI_API bool DataTypeApplyOpFromText(const char* buf, const char* initial_value_buf, ImGuiDataType data_type, void* data_ptr, const char* format); + IMGUI_API int DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format); + IMGUI_API void DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg_1, const void* arg_2); + IMGUI_API bool DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty = NULL); + IMGUI_API int DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2); + IMGUI_API bool DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max); + IMGUI_API bool DataTypeIsZero(ImGuiDataType data_type, const void* p_data); // InputText IMGUI_API bool InputTextEx(const char* label, const char* hint, char* buf, int buf_size, const ImVec2& size_arg, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback = NULL, void* user_data = NULL); - IMGUI_API bool TempInputTextScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* data_ptr, const char* format); - inline bool TempInputTextIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.ActiveId == id && g.TempInputTextId == id); } + IMGUI_API void InputTextDeactivateHook(ImGuiID id); + IMGUI_API bool TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags); + IMGUI_API bool TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min = NULL, const void* p_clamp_max = NULL); + inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.ActiveId == id && g.TempInputId == id); } + inline ImGuiInputTextState* GetInputTextState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.InputTextState.ID == id) ? &g.InputTextState : NULL; } // Get input text state if active + IMGUI_API void SetNextItemRefVal(ImGuiDataType data_type, void* p_data); + inline bool IsItemActiveAsInputText() { ImGuiContext& g = *GImGui; return g.ActiveId != 0 && g.ActiveId == g.LastItemData.ID && g.InputTextState.ID == g.LastItemData.ID; } // This may be useful to apply workaround that a based on distinguish whenever an item is active as a text input field. // Color IMGUI_API void ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags flags); @@ -1691,55 +3802,130 @@ namespace ImGui IMGUI_API void ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags); // Plot - IMGUI_API void PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 frame_size); + IMGUI_API int PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg); // Shade functions (write over already created vertices) IMGUI_API void ShadeVertsLinearColorGradientKeepAlpha(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, ImVec2 gradient_p0, ImVec2 gradient_p1, ImU32 col0, ImU32 col1); IMGUI_API void ShadeVertsLinearUV(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& a, const ImVec2& b, const ImVec2& uv_a, const ImVec2& uv_b, bool clamp); + IMGUI_API void ShadeVertsTransformPos(ImDrawList* draw_list, int vert_start_idx, int vert_end_idx, const ImVec2& pivot_in, float cos_a, float sin_a, const ImVec2& pivot_out); + + // Garbage collection + IMGUI_API void GcCompactTransientMiscBuffers(); + IMGUI_API void GcCompactTransientWindowBuffers(ImGuiWindow* window); + IMGUI_API void GcAwakeTransientWindowBuffers(ImGuiWindow* window); + + // Error handling, State Recovery + IMGUI_API bool ErrorLog(const char* msg); + IMGUI_API void ErrorRecoveryStoreState(ImGuiErrorRecoveryState* state_out); + IMGUI_API void ErrorRecoveryTryToRecoverState(const ImGuiErrorRecoveryState* state_in); + IMGUI_API void ErrorRecoveryTryToRecoverWindowState(const ImGuiErrorRecoveryState* state_in); + IMGUI_API void ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + IMGUI_API void ErrorCheckEndFrameFinalizeErrorTooltip(); + IMGUI_API bool BeginErrorTooltip(); + IMGUI_API void EndErrorTooltip(); // Debug Tools - inline void DebugStartItemPicker() { GImGui->DebugItemPickerActive = true; } + IMGUI_API void DebugAllocHook(ImGuiDebugAllocInfo* info, int frame_count, void* ptr, size_t size); // size >= 0 : alloc, size = -1 : free + IMGUI_API void DebugDrawCursorPos(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugDrawLineExtents(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugDrawItemRect(ImU32 col = IM_COL32(255, 0, 0, 255)); + IMGUI_API void DebugTextUnformattedWithLocateItem(const char* line_begin, const char* line_end); + IMGUI_API void DebugLocateItem(ImGuiID target_id); // Call sparingly: only 1 at the same time! + IMGUI_API void DebugLocateItemOnHover(ImGuiID target_id); // Only call on reaction to a mouse Hover: because only 1 at the same time! + IMGUI_API void DebugLocateItemResolveWithLastItem(); + IMGUI_API void DebugBreakClearData(); + IMGUI_API bool DebugBreakButton(const char* label, const char* description_of_location); + IMGUI_API void DebugBreakButtonTooltip(bool keyboard_only, const char* description_of_location); + IMGUI_API void ShowFontAtlas(ImFontAtlas* atlas); + IMGUI_API void DebugHookIdInfo(ImGuiID id, ImGuiDataType data_type, const void* data_id, const void* data_id_end); + IMGUI_API void DebugNodeColumns(ImGuiOldColumns* columns); + IMGUI_API void DebugNodeDockNode(ImGuiDockNode* node, const char* label); + IMGUI_API void DebugNodeDrawList(ImGuiWindow* window, ImGuiViewportP* viewport, const ImDrawList* draw_list, const char* label); + IMGUI_API void DebugNodeDrawCmdShowMeshAndBoundingBox(ImDrawList* out_draw_list, const ImDrawList* draw_list, const ImDrawCmd* draw_cmd, bool show_mesh, bool show_aabb); + IMGUI_API void DebugNodeFont(ImFont* font); + IMGUI_API void DebugNodeFontGlyph(ImFont* font, const ImFontGlyph* glyph); + IMGUI_API void DebugNodeStorage(ImGuiStorage* storage, const char* label); + IMGUI_API void DebugNodeTabBar(ImGuiTabBar* tab_bar, const char* label); + IMGUI_API void DebugNodeTable(ImGuiTable* table); + IMGUI_API void DebugNodeTableSettings(ImGuiTableSettings* settings); + IMGUI_API void DebugNodeInputTextState(ImGuiInputTextState* state); + IMGUI_API void DebugNodeTypingSelectState(ImGuiTypingSelectState* state); + IMGUI_API void DebugNodeMultiSelectState(ImGuiMultiSelectState* state); + IMGUI_API void DebugNodeWindow(ImGuiWindow* window, const char* label); + IMGUI_API void DebugNodeWindowSettings(ImGuiWindowSettings* settings); + IMGUI_API void DebugNodeWindowsList(ImVector* windows, const char* label); + IMGUI_API void DebugNodeWindowsListByBeginStackParent(ImGuiWindow** windows, int windows_size, ImGuiWindow* parent_in_begin_stack); + IMGUI_API void DebugNodeViewport(ImGuiViewportP* viewport); + IMGUI_API void DebugNodePlatformMonitor(ImGuiPlatformMonitor* monitor, const char* label, int idx); + IMGUI_API void DebugRenderKeyboardPreview(ImDrawList* draw_list); + IMGUI_API void DebugRenderViewportThumbnail(ImDrawList* draw_list, ImGuiViewportP* viewport, const ImRect& bb); + + // Obsolete functions +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + //inline void SetItemUsingMouseWheel() { SetItemKeyOwner(ImGuiKey_MouseWheelY); } // Changed in 1.89 + //inline bool TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags = 0) { return TreeNodeUpdateNextOpen(id, flags); } // Renamed in 1.89 + //inline bool IsKeyPressedMap(ImGuiKey key, bool repeat = true) { IM_ASSERT(IsNamedKey(key)); return IsKeyPressed(key, repeat); } // Removed in 1.87: Mapping from named key is always identity! + + // Refactored focus/nav/tabbing system in 1.82 and 1.84. If you have old/custom copy-and-pasted widgets which used FocusableItemRegister(): + // (Old) IMGUI_VERSION_NUM < 18209: using 'ItemAdd(....)' and 'bool tab_focused = FocusableItemRegister(...)' + // (Old) IMGUI_VERSION_NUM >= 18209: using 'ItemAdd(..., ImGuiItemAddFlags_Focusable)' and 'bool tab_focused = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Focused) != 0' + // (New) IMGUI_VERSION_NUM >= 18413: using 'ItemAdd(..., ImGuiItemFlags_Inputable)' and 'bool tab_focused = (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))' + //inline bool FocusableItemRegister(ImGuiWindow* window, ImGuiID id) // -> pass ImGuiItemAddFlags_Inputable flag to ItemAdd() + //inline void FocusableItemUnregister(ImGuiWindow* window) // -> unnecessary: TempInputText() uses ImGuiInputTextFlags_MergedItem +#endif } // namespace ImGui -// ImFontAtlas internals -IMGUI_API bool ImFontAtlasBuildWithStbTruetype(ImFontAtlas* atlas); -IMGUI_API void ImFontAtlasBuildRegisterDefaultCustomRects(ImFontAtlas* atlas); -IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* font_config, float ascent, float descent); -IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque); -IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas* atlas); -IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor); -IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride); -// Debug Tools -// Use 'Metrics->Tools->Item Picker' to break into the call-stack of a specific item. -#ifndef IM_DEBUG_BREAK -#if defined(__clang__) -#define IM_DEBUG_BREAK() __builtin_debugtrap() -#elif defined (_MSC_VER) -#define IM_DEBUG_BREAK() __debugbreak() -#else -#define IM_DEBUG_BREAK() IM_ASSERT(0) // It is expected that you define IM_DEBUG_BREAK() into something that will break nicely in a debugger! +//----------------------------------------------------------------------------- +// [SECTION] ImFontAtlas internal API +//----------------------------------------------------------------------------- + +// This structure is likely to evolve as we add support for incremental atlas updates. +// Conceptually this could be in ImGuiPlatformIO, but we are far from ready to make this public. +struct ImFontBuilderIO +{ + bool (*FontBuilder_Build)(ImFontAtlas* atlas); +}; + +// Helper for font builder +#ifdef IMGUI_ENABLE_STB_TRUETYPE +IMGUI_API const ImFontBuilderIO* ImFontAtlasGetBuilderForStbTruetype(); #endif -#endif // #ifndef IM_DEBUG_BREAK +IMGUI_API void ImFontAtlasUpdateSourcesPointers(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildInit(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildSetupFont(ImFontAtlas* atlas, ImFont* font, ImFontConfig* src, float ascent, float descent); +IMGUI_API void ImFontAtlasBuildPackCustomRects(ImFontAtlas* atlas, void* stbrp_context_opaque); +IMGUI_API void ImFontAtlasBuildFinish(ImFontAtlas* atlas); +IMGUI_API void ImFontAtlasBuildRender8bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned char in_marker_pixel_value); +IMGUI_API void ImFontAtlasBuildRender32bppRectFromString(ImFontAtlas* atlas, int x, int y, int w, int h, const char* in_str, char in_marker_char, unsigned int in_marker_pixel_value); +IMGUI_API void ImFontAtlasBuildMultiplyCalcLookupTable(unsigned char out_table[256], float in_multiply_factor); +IMGUI_API void ImFontAtlasBuildMultiplyRectAlpha8(const unsigned char table[256], unsigned char* pixels, int x, int y, int w, int h, int stride); +IMGUI_API void ImFontAtlasBuildGetOversampleFactors(const ImFontConfig* src, int* out_oversample_h, int* out_oversample_v); + +IMGUI_API bool ImFontAtlasGetMouseCursorTexData(ImFontAtlas* atlas, ImGuiMouseCursor cursor_type, ImVec2* out_offset, ImVec2* out_size, ImVec2 out_uv_border[2], ImVec2 out_uv_fill[2]); + +//----------------------------------------------------------------------------- +// [SECTION] Test Engine specific hooks (imgui_test_engine) +//----------------------------------------------------------------------------- -// Test Engine Hooks (imgui_tests) -//#define IMGUI_ENABLE_TEST_ENGINE #ifdef IMGUI_ENABLE_TEST_ENGINE -extern void ImGuiTestEngineHook_PreNewFrame(ImGuiContext* ctx); -extern void ImGuiTestEngineHook_PostNewFrame(ImGuiContext* ctx); -extern void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ctx, const ImRect& bb, ImGuiID id); -extern void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ctx, ImGuiID id, const char* label, ImGuiItemStatusFlags flags); -extern void ImGuiTestEngineHook_Log(ImGuiContext* ctx, const char* fmt, ...); -#define IMGUI_TEST_ENGINE_ITEM_ADD(_BB, _ID) ImGuiTestEngineHook_ItemAdd(&g, _BB, _ID) // Register item bounding box -#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID, _LABEL, _FLAGS) ImGuiTestEngineHook_ItemInfo(&g, _ID, _LABEL, _FLAGS) // Register item label and status flags (optional) -#define IMGUI_TEST_ENGINE_LOG(_FMT, ...) ImGuiTestEngineHook_Log(&g, _FMT, __VA_ARGS__) // Custom log entry from user land into test log +extern void ImGuiTestEngineHook_ItemAdd(ImGuiContext* ctx, ImGuiID id, const ImRect& bb, const ImGuiLastItemData* item_data); // item_data may be NULL +extern void ImGuiTestEngineHook_ItemInfo(ImGuiContext* ctx, ImGuiID id, const char* label, ImGuiItemStatusFlags flags); +extern void ImGuiTestEngineHook_Log(ImGuiContext* ctx, const char* fmt, ...); +extern const char* ImGuiTestEngine_FindItemDebugLabel(ImGuiContext* ctx, ImGuiID id); + +// In IMGUI_VERSION_NUM >= 18934: changed IMGUI_TEST_ENGINE_ITEM_ADD(bb,id) to IMGUI_TEST_ENGINE_ITEM_ADD(id,bb,item_data); +#define IMGUI_TEST_ENGINE_ITEM_ADD(_ID,_BB,_ITEM_DATA) if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemAdd(&g, _ID, _BB, _ITEM_DATA) // Register item bounding box +#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS) if (g.TestEngineHookItems) ImGuiTestEngineHook_ItemInfo(&g, _ID, _LABEL, _FLAGS) // Register item label and status flags (optional) +#define IMGUI_TEST_ENGINE_LOG(_FMT,...) ImGuiTestEngineHook_Log(&g, _FMT, __VA_ARGS__) // Custom log entry from user land into test log #else -#define IMGUI_TEST_ENGINE_ITEM_ADD(_BB, _ID) do { } while (0) -#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID, _LABEL, _FLAGS) do { } while (0) -#define IMGUI_TEST_ENGINE_LOG(_FMT, ...) do { } while (0) +#define IMGUI_TEST_ENGINE_ITEM_ADD(_BB,_ID) ((void)0) +#define IMGUI_TEST_ENGINE_ITEM_INFO(_ID,_LABEL,_FLAGS) ((void)g) #endif +//----------------------------------------------------------------------------- + #if defined(__clang__) #pragma clang diagnostic pop #elif defined(__GNUC__) @@ -1749,3 +3935,5 @@ extern void ImGuiTestEngineHook_Log(ImGuiContext* ctx, const cha #ifdef _MSC_VER #pragma warning (pop) #endif + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_tables.cpp b/imgui/imgui_tables.cpp new file mode 100644 index 00000000..c98aea2d --- /dev/null +++ b/imgui/imgui_tables.cpp @@ -0,0 +1,4525 @@ +// dear imgui, v1.91b +// (tables and columns code) + +/* + +Index of this file: + +// [SECTION] Commentary +// [SECTION] Header mess +// [SECTION] Tables: Main code +// [SECTION] Tables: Simple accessors +// [SECTION] Tables: Row changes +// [SECTION] Tables: Columns changes +// [SECTION] Tables: Columns width management +// [SECTION] Tables: Drawing +// [SECTION] Tables: Sorting +// [SECTION] Tables: Headers +// [SECTION] Tables: Context Menu +// [SECTION] Tables: Settings (.ini data) +// [SECTION] Tables: Garbage Collection +// [SECTION] Tables: Debugging +// [SECTION] Columns, BeginColumns, EndColumns, etc. + +*/ + +// Navigating this file: +// - In Visual Studio: CTRL+comma ("Edit.GoToAll") can follow symbols inside comments, whereas CTRL+F12 ("Edit.GoToImplementation") cannot. +// - In Visual Studio w/ Visual Assist installed: ALT+G ("VAssistX.GoToImplementation") can also follow symbols inside comments. +// - In VS Code, CLion, etc.: CTRL+click can follow symbols inside comments. + +//----------------------------------------------------------------------------- +// [SECTION] Commentary +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Typical tables call flow: (root level is generally public API): +//----------------------------------------------------------------------------- +// - BeginTable() user begin into a table +// | BeginChild() - (if ScrollX/ScrollY is set) +// | TableBeginInitMemory() - first time table is used +// | TableResetSettings() - on settings reset +// | TableLoadSettings() - on settings load +// | TableBeginApplyRequests() - apply queued resizing/reordering/hiding requests +// | - TableSetColumnWidth() - apply resizing width (for mouse resize, often requested by previous frame) +// | - TableUpdateColumnsWeightFromWidth()- recompute columns weights (of stretch columns) from their respective width +// - TableSetupColumn() user submit columns details (optional) +// - TableSetupScrollFreeze() user submit scroll freeze information (optional) +//----------------------------------------------------------------------------- +// - TableUpdateLayout() [Internal] followup to BeginTable(): setup everything: widths, columns positions, clipping rectangles. Automatically called by the FIRST call to TableNextRow() or TableHeadersRow(). +// | TableSetupDrawChannels() - setup ImDrawList channels +// | TableUpdateBorders() - detect hovering columns for resize, ahead of contents submission +// | TableBeginContextMenuPopup() +// | - TableDrawDefaultContextMenu() - draw right-click context menu contents +//----------------------------------------------------------------------------- +// - TableHeadersRow() or TableHeader() user submit a headers row (optional) +// | TableSortSpecsClickColumn() - when left-clicked: alter sort order and sort direction +// | TableOpenContextMenu() - when right-clicked: trigger opening of the default context menu +// - TableGetSortSpecs() user queries updated sort specs (optional, generally after submitting headers) +// - TableNextRow() user begin into a new row (also automatically called by TableHeadersRow()) +// | TableEndRow() - finish existing row +// | TableBeginRow() - add a new row +// - TableSetColumnIndex() / TableNextColumn() user begin into a cell +// | TableEndCell() - close existing column/cell +// | TableBeginCell() - enter into current column/cell +// - [...] user emit contents +//----------------------------------------------------------------------------- +// - EndTable() user ends the table +// | TableDrawBorders() - draw outer borders, inner vertical borders +// | TableMergeDrawChannels() - merge draw channels if clipping isn't required +// | EndChild() - (if ScrollX/ScrollY is set) +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// TABLE SIZING +//----------------------------------------------------------------------------- +// (Read carefully because this is subtle but it does make sense!) +//----------------------------------------------------------------------------- +// About 'outer_size': +// Its meaning needs to differ slightly depending on if we are using ScrollX/ScrollY flags. +// Default value is ImVec2(0.0f, 0.0f). +// X +// - outer_size.x <= 0.0f -> Right-align from window/work-rect right-most edge. With -FLT_MIN or 0.0f will align exactly on right-most edge. +// - outer_size.x > 0.0f -> Set Fixed width. +// Y with ScrollX/ScrollY disabled: we output table directly in current window +// - outer_size.y < 0.0f -> Bottom-align (but will auto extend, unless _NoHostExtendY is set). Not meaningful if parent window can vertically scroll. +// - outer_size.y = 0.0f -> No minimum height (but will auto extend, unless _NoHostExtendY is set) +// - outer_size.y > 0.0f -> Set Minimum height (but will auto extend, unless _NoHostExtendY is set) +// Y with ScrollX/ScrollY enabled: using a child window for scrolling +// - outer_size.y < 0.0f -> Bottom-align. Not meaningful if parent window can vertically scroll. +// - outer_size.y = 0.0f -> Bottom-align, consistent with BeginChild(). Not recommended unless table is last item in parent window. +// - outer_size.y > 0.0f -> Set Exact height. Recommended when using Scrolling on any axis. +//----------------------------------------------------------------------------- +// Outer size is also affected by the NoHostExtendX/NoHostExtendY flags. +// Important to note how the two flags have slightly different behaviors! +// - ImGuiTableFlags_NoHostExtendX -> Make outer width auto-fit to columns (overriding outer_size.x value). Only available when ScrollX/ScrollY are disabled and Stretch columns are not used. +// - ImGuiTableFlags_NoHostExtendY -> Make outer height stop exactly at outer_size.y (prevent auto-extending table past the limit). Only available when ScrollX/ScrollY is disabled. Data below the limit will be clipped and not visible. +// In theory ImGuiTableFlags_NoHostExtendY could be the default and any non-scrolling tables with outer_size.y != 0.0f would use exact height. +// This would be consistent but perhaps less useful and more confusing (as vertically clipped items are not useful and not easily noticeable). +//----------------------------------------------------------------------------- +// About 'inner_width': +// With ScrollX disabled: +// - inner_width -> *ignored* +// With ScrollX enabled: +// - inner_width < 0.0f -> *illegal* fit in known width (right align from outer_size.x) <-- weird +// - inner_width = 0.0f -> fit in outer_width: Fixed size columns will take space they need (if avail, otherwise shrink down), Stretch columns becomes Fixed columns. +// - inner_width > 0.0f -> override scrolling width, generally to be larger than outer_size.x. Fixed column take space they need (if avail, otherwise shrink down), Stretch columns share remaining space! +//----------------------------------------------------------------------------- +// Details: +// - If you want to use Stretch columns with ScrollX, you generally need to specify 'inner_width' otherwise the concept +// of "available space" doesn't make sense. +// - Even if not really useful, we allow 'inner_width < outer_size.x' for consistency and to facilitate understanding +// of what the value does. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// COLUMNS SIZING POLICIES +// (Reference: ImGuiTableFlags_SizingXXX flags and ImGuiTableColumnFlags_WidthXXX flags) +//----------------------------------------------------------------------------- +// About overriding column sizing policy and width/weight with TableSetupColumn(): +// We use a default parameter of -1 for 'init_width'/'init_weight'. +// - with ImGuiTableColumnFlags_WidthFixed, init_width <= 0 (default) --> width is automatic +// - with ImGuiTableColumnFlags_WidthFixed, init_width > 0 (explicit) --> width is custom +// - with ImGuiTableColumnFlags_WidthStretch, init_weight <= 0 (default) --> weight is 1.0f +// - with ImGuiTableColumnFlags_WidthStretch, init_weight > 0 (explicit) --> weight is custom +// Widths are specified _without_ CellPadding. If you specify a width of 100.0f, the column will be cover (100.0f + Padding * 2.0f) +// and you can fit a 100.0f wide item in it without clipping and with padding honored. +//----------------------------------------------------------------------------- +// About default sizing policy (if you don't specify a ImGuiTableColumnFlags_WidthXXXX flag) +// - with Table policy ImGuiTableFlags_SizingFixedFit --> default Column policy is ImGuiTableColumnFlags_WidthFixed, default Width is equal to contents width +// - with Table policy ImGuiTableFlags_SizingFixedSame --> default Column policy is ImGuiTableColumnFlags_WidthFixed, default Width is max of all contents width +// - with Table policy ImGuiTableFlags_SizingStretchSame --> default Column policy is ImGuiTableColumnFlags_WidthStretch, default Weight is 1.0f +// - with Table policy ImGuiTableFlags_SizingStretchWeight --> default Column policy is ImGuiTableColumnFlags_WidthStretch, default Weight is proportional to contents +// Default Width and default Weight can be overridden when calling TableSetupColumn(). +//----------------------------------------------------------------------------- +// About mixing Fixed/Auto and Stretch columns together: +// - the typical use of mixing sizing policies is: any number of LEADING Fixed columns, followed by one or two TRAILING Stretch columns. +// - using mixed policies with ScrollX does not make much sense, as using Stretch columns with ScrollX does not make much sense in the first place! +// that is, unless 'inner_width' is passed to BeginTable() to explicitly provide a total width to layout columns in. +// - when using ImGuiTableFlags_SizingFixedSame with mixed columns, only the Fixed/Auto columns will match their widths to the width of the maximum contents. +// - when using ImGuiTableFlags_SizingStretchSame with mixed columns, only the Stretch columns will match their weights/widths. +//----------------------------------------------------------------------------- +// About using column width: +// If a column is manually resizable or has a width specified with TableSetupColumn(): +// - you may use GetContentRegionAvail().x to query the width available in a given column. +// - right-side alignment features such as SetNextItemWidth(-x) or PushItemWidth(-x) will rely on this width. +// If the column is not resizable and has no width specified with TableSetupColumn(): +// - its width will be automatic and be set to the max of items submitted. +// - therefore you generally cannot have ALL items of the columns use e.g. SetNextItemWidth(-FLT_MIN). +// - but if the column has one or more items of known/fixed size, this will become the reference width used by SetNextItemWidth(-FLT_MIN). +//----------------------------------------------------------------------------- + + +//----------------------------------------------------------------------------- +// TABLES CLIPPING/CULLING +//----------------------------------------------------------------------------- +// About clipping/culling of Rows in Tables: +// - For large numbers of rows, it is recommended you use ImGuiListClipper to submit only visible rows. +// ImGuiListClipper is reliant on the fact that rows are of equal height. +// See 'Demo->Tables->Vertical Scrolling' or 'Demo->Tables->Advanced' for a demo of using the clipper. +// - Note that auto-resizing columns don't play well with using the clipper. +// By default a table with _ScrollX but without _Resizable will have column auto-resize. +// So, if you want to use the clipper, make sure to either enable _Resizable, either setup columns width explicitly with _WidthFixed. +//----------------------------------------------------------------------------- +// About clipping/culling of Columns in Tables: +// - Both TableSetColumnIndex() and TableNextColumn() return true when the column is visible or performing +// width measurements. Otherwise, you may skip submitting the contents of a cell/column, BUT ONLY if you know +// it is not going to contribute to row height. +// In many situations, you may skip submitting contents for every column but one (e.g. the first one). +// - Case A: column is not hidden by user, and at least partially in sight (most common case). +// - Case B: column is clipped / out of sight (because of scrolling or parent ClipRect): TableNextColumn() return false as a hint but we still allow layout output. +// - Case C: column is hidden explicitly by the user (e.g. via the context menu, or _DefaultHide column flag, etc.). +// +// [A] [B] [C] +// TableNextColumn(): true false false -> [userland] when TableNextColumn() / TableSetColumnIndex() returns false, user can skip submitting items but only if the column doesn't contribute to row height. +// SkipItems: false false true -> [internal] when SkipItems is true, most widgets will early out if submitted, resulting is no layout output. +// ClipRect: normal zero-width zero-width -> [internal] when ClipRect is zero, ItemAdd() will return false and most widgets will early out mid-way. +// ImDrawList output: normal dummy dummy -> [internal] when using the dummy channel, ImDrawList submissions (if any) will be wasted (because cliprect is zero-width anyway). +// +// - We need to distinguish those cases because non-hidden columns that are clipped outside of scrolling bounds should still contribute their height to the row. +// However, in the majority of cases, the contribution to row height is the same for all columns, or the tallest cells are known by the programmer. +//----------------------------------------------------------------------------- +// About clipping/culling of whole Tables: +// - Scrolling tables with a known outer size can be clipped earlier as BeginTable() will return false. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// [SECTION] Header mess +//----------------------------------------------------------------------------- + +#if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) +#define _CRT_SECURE_NO_WARNINGS +#endif + +#ifndef IMGUI_DEFINE_MATH_OPERATORS +#define IMGUI_DEFINE_MATH_OPERATORS +#endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_internal.h" + +// System includes +#include // intptr_t + +// Visual Studio warnings +#ifdef _MSC_VER +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). +#endif + +// Clang/GCC warnings with -Weverything +#if defined(__clang__) +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! +#endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat" // warning: format specifies type 'int' but the argument has type 'unsigned int' +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label +#elif defined(__GNUC__) +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wstrict-overflow" +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#endif + +//----------------------------------------------------------------------------- +// [SECTION] Tables: Main code +//----------------------------------------------------------------------------- +// - TableFixFlags() [Internal] +// - TableFindByID() [Internal] +// - BeginTable() +// - BeginTableEx() [Internal] +// - TableBeginInitMemory() [Internal] +// - TableBeginApplyRequests() [Internal] +// - TableSetupColumnFlags() [Internal] +// - TableUpdateLayout() [Internal] +// - TableUpdateBorders() [Internal] +// - EndTable() +// - TableSetupColumn() +// - TableSetupScrollFreeze() +//----------------------------------------------------------------------------- + +// Configuration +static const int TABLE_DRAW_CHANNEL_BG0 = 0; +static const int TABLE_DRAW_CHANNEL_BG2_FROZEN = 1; +static const int TABLE_DRAW_CHANNEL_NOCLIP = 2; // When using ImGuiTableFlags_NoClip (this becomes the last visible channel) +static const float TABLE_BORDER_SIZE = 1.0f; // FIXME-TABLE: Currently hard-coded because of clipping assumptions with outer borders rendering. +static const float TABLE_RESIZE_SEPARATOR_HALF_THICKNESS = 4.0f; // Extend outside inner borders. +static const float TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER = 0.06f; // Delay/timer before making the hover feedback (color+cursor) visible because tables/columns tends to be more cramped. + +// Helper +inline ImGuiTableFlags TableFixFlags(ImGuiTableFlags flags, ImGuiWindow* outer_window) +{ + // Adjust flags: set default sizing policy + if ((flags & ImGuiTableFlags_SizingMask_) == 0) + flags |= ((flags & ImGuiTableFlags_ScrollX) || (outer_window->Flags & ImGuiWindowFlags_AlwaysAutoResize)) ? ImGuiTableFlags_SizingFixedFit : ImGuiTableFlags_SizingStretchSame; + + // Adjust flags: enable NoKeepColumnsVisible when using ImGuiTableFlags_SizingFixedSame + if ((flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableFlags_NoKeepColumnsVisible; + + // Adjust flags: enforce borders when resizable + if (flags & ImGuiTableFlags_Resizable) + flags |= ImGuiTableFlags_BordersInnerV; + + // Adjust flags: disable NoHostExtendX/NoHostExtendY if we have any scrolling going on + if (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) + flags &= ~(ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_NoHostExtendY); + + // Adjust flags: NoBordersInBodyUntilResize takes priority over NoBordersInBody + if (flags & ImGuiTableFlags_NoBordersInBodyUntilResize) + flags &= ~ImGuiTableFlags_NoBordersInBody; + + // Adjust flags: disable saved settings if there's nothing to save + if ((flags & (ImGuiTableFlags_Resizable | ImGuiTableFlags_Hideable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Sortable)) == 0) + flags |= ImGuiTableFlags_NoSavedSettings; + + // Inherit _NoSavedSettings from top-level window (child windows always have _NoSavedSettings set) + if (outer_window->RootWindow->Flags & ImGuiWindowFlags_NoSavedSettings) + flags |= ImGuiTableFlags_NoSavedSettings; + + return flags; +} + +ImGuiTable* ImGui::TableFindByID(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + return g.Tables.GetByKey(id); +} + +// Read about "TABLE SIZING" at the top of this file. +bool ImGui::BeginTable(const char* str_id, int columns_count, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width) +{ + ImGuiID id = GetID(str_id); + return BeginTableEx(str_id, id, columns_count, flags, outer_size, inner_width); +} + +bool ImGui::BeginTableEx(const char* name, ImGuiID id, int columns_count, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* outer_window = GetCurrentWindow(); + if (outer_window->SkipItems) // Consistent with other tables + beneficial side effect that assert on miscalling EndTable() will be more visible. + return false; + + // Sanity checks + IM_ASSERT(columns_count > 0 && columns_count < IMGUI_TABLE_MAX_COLUMNS); + if (flags & ImGuiTableFlags_ScrollX) + IM_ASSERT(inner_width >= 0.0f); + + // If an outer size is specified ahead we will be able to early out when not visible. Exact clipping criteria may evolve. + // FIXME: coarse clipping because access to table data causes two issues: + // - instance numbers varying/unstable. may not be a direct problem for users, but could make outside access broken or confusing, e.g. TestEngine. + // - can't implement support for ImGuiChildFlags_ResizeY as we need to somehow pull the height data from somewhere. this also needs stable instance numbers. + // The side-effects of accessing table data on coarse clip would be: + // - always reserving the pooled ImGuiTable data ahead for a fully clipped table (minor IMHO). Also the 'outer_window_is_measuring_size' criteria may already be defeating this in some situations. + // - always performing the GetOrAddByKey() O(log N) query in g.Tables.Map[]. + const bool use_child_window = (flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) != 0; + const ImVec2 avail_size = GetContentRegionAvail(); + const ImVec2 actual_outer_size = ImTrunc(CalcItemSize(outer_size, ImMax(avail_size.x, 1.0f), use_child_window ? ImMax(avail_size.y, 1.0f) : 0.0f)); + const ImRect outer_rect(outer_window->DC.CursorPos, outer_window->DC.CursorPos + actual_outer_size); + const bool outer_window_is_measuring_size = (outer_window->AutoFitFramesX > 0) || (outer_window->AutoFitFramesY > 0); // Doesn't apply to AlwaysAutoResize windows! + if (use_child_window && IsClippedEx(outer_rect, 0) && !outer_window_is_measuring_size) + { + ItemSize(outer_rect); + ItemAdd(outer_rect, id); + g.NextWindowData.ClearFlags(); + return false; + } + + // [DEBUG] Debug break requested by user + if (g.DebugBreakInTable == id) + IM_DEBUG_BREAK(); + + // Acquire storage for the table + ImGuiTable* table = g.Tables.GetOrAddByKey(id); + + // Acquire temporary buffers + const int table_idx = g.Tables.GetIndex(table); + if (++g.TablesTempDataStacked > g.TablesTempData.Size) + g.TablesTempData.resize(g.TablesTempDataStacked, ImGuiTableTempData()); + ImGuiTableTempData* temp_data = table->TempData = &g.TablesTempData[g.TablesTempDataStacked - 1]; + temp_data->TableIndex = table_idx; + table->DrawSplitter = &table->TempData->DrawSplitter; + table->DrawSplitter->Clear(); + + // Fix flags + table->IsDefaultSizingPolicy = (flags & ImGuiTableFlags_SizingMask_) == 0; + flags = TableFixFlags(flags, outer_window); + + // Initialize + const int previous_frame_active = table->LastFrameActive; + const int instance_no = (previous_frame_active != g.FrameCount) ? 0 : table->InstanceCurrent + 1; + const ImGuiTableFlags previous_flags = table->Flags; + table->ID = id; + table->Flags = flags; + table->LastFrameActive = g.FrameCount; + table->OuterWindow = table->InnerWindow = outer_window; + table->ColumnsCount = columns_count; + table->IsLayoutLocked = false; + table->InnerWidth = inner_width; + table->NavLayer = (ImS8)outer_window->DC.NavLayerCurrent; + temp_data->UserOuterSize = outer_size; + + // Instance data (for instance 0, TableID == TableInstanceID) + ImGuiID instance_id; + table->InstanceCurrent = (ImS16)instance_no; + if (instance_no > 0) + { + IM_ASSERT(table->ColumnsCount == columns_count && "BeginTable(): Cannot change columns count mid-frame while preserving same ID"); + if (table->InstanceDataExtra.Size < instance_no) + table->InstanceDataExtra.push_back(ImGuiTableInstanceData()); + instance_id = GetIDWithSeed(instance_no, GetIDWithSeed("##Instances", NULL, id)); // Push "##Instances" followed by (int)instance_no in ID stack. + } + else + { + instance_id = id; + } + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + table_instance->TableInstanceID = instance_id; + + // When not using a child window, WorkRect.Max will grow as we append contents. + if (use_child_window) + { + // Ensure no vertical scrollbar appears if we only want horizontal one, to make flag consistent + // (we have no other way to disable vertical scrollbar of a window while keeping the horizontal one showing) + ImVec2 override_content_size(FLT_MAX, FLT_MAX); + if ((flags & ImGuiTableFlags_ScrollX) && !(flags & ImGuiTableFlags_ScrollY)) + override_content_size.y = FLT_MIN; + + // Ensure specified width (when not specified, Stretched columns will act as if the width == OuterWidth and + // never lead to any scrolling). We don't handle inner_width < 0.0f, we could potentially use it to right-align + // based on the right side of the child window work rect, which would require knowing ahead if we are going to + // have decoration taking horizontal spaces (typically a vertical scrollbar). + if ((flags & ImGuiTableFlags_ScrollX) && inner_width > 0.0f) + override_content_size.x = inner_width; + + if (override_content_size.x != FLT_MAX || override_content_size.y != FLT_MAX) + SetNextWindowContentSize(ImVec2(override_content_size.x != FLT_MAX ? override_content_size.x : 0.0f, override_content_size.y != FLT_MAX ? override_content_size.y : 0.0f)); + + // Reset scroll if we are reactivating it + if ((previous_flags & (ImGuiTableFlags_ScrollX | ImGuiTableFlags_ScrollY)) == 0) + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasScroll) == 0) + SetNextWindowScroll(ImVec2(0.0f, 0.0f)); + + // Create scrolling region (without border and zero window padding) + ImGuiChildFlags child_child_flags = (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasChildFlags) ? g.NextWindowData.ChildFlags : ImGuiChildFlags_None; + ImGuiWindowFlags child_window_flags = (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasWindowFlags) ? g.NextWindowData.WindowFlags : ImGuiWindowFlags_None; + if (flags & ImGuiTableFlags_ScrollX) + child_window_flags |= ImGuiWindowFlags_HorizontalScrollbar; + BeginChildEx(name, instance_id, outer_rect.GetSize(), child_child_flags, child_window_flags); + table->InnerWindow = g.CurrentWindow; + table->WorkRect = table->InnerWindow->WorkRect; + table->OuterRect = table->InnerWindow->Rect(); + table->InnerRect = table->InnerWindow->InnerRect; + IM_ASSERT(table->InnerWindow->WindowPadding.x == 0.0f && table->InnerWindow->WindowPadding.y == 0.0f && table->InnerWindow->WindowBorderSize == 0.0f); + + // Allow submitting when host is measuring + if (table->InnerWindow->SkipItems && outer_window_is_measuring_size) + table->InnerWindow->SkipItems = false; + + // When using multiple instances, ensure they have the same amount of horizontal decorations (aka vertical scrollbar) so stretched columns can be aligned) + if (instance_no == 0) + { + table->HasScrollbarYPrev = table->HasScrollbarYCurr; + table->HasScrollbarYCurr = false; + } + table->HasScrollbarYCurr |= table->InnerWindow->ScrollbarY; + } + else + { + // For non-scrolling tables, WorkRect == OuterRect == InnerRect. + // But at this point we do NOT have a correct value for .Max.y (unless a height has been explicitly passed in). It will only be updated in EndTable(). + table->WorkRect = table->OuterRect = table->InnerRect = outer_rect; + table->HasScrollbarYPrev = table->HasScrollbarYCurr = false; + } + + // Push a standardized ID for both child-using and not-child-using tables + PushOverrideID(id); + if (instance_no > 0) + PushOverrideID(instance_id); // FIXME: Somehow this is not resolved by stack-tool, even tho GetIDWithSeed() submitted the symbol. + + // Backup a copy of host window members we will modify + ImGuiWindow* inner_window = table->InnerWindow; + table->HostIndentX = inner_window->DC.Indent.x; + table->HostClipRect = inner_window->ClipRect; + table->HostSkipItems = inner_window->SkipItems; + temp_data->HostBackupWorkRect = inner_window->WorkRect; + temp_data->HostBackupParentWorkRect = inner_window->ParentWorkRect; + temp_data->HostBackupColumnsOffset = outer_window->DC.ColumnsOffset; + temp_data->HostBackupPrevLineSize = inner_window->DC.PrevLineSize; + temp_data->HostBackupCurrLineSize = inner_window->DC.CurrLineSize; + temp_data->HostBackupCursorMaxPos = inner_window->DC.CursorMaxPos; + temp_data->HostBackupItemWidth = outer_window->DC.ItemWidth; + temp_data->HostBackupItemWidthStackSize = outer_window->DC.ItemWidthStack.Size; + inner_window->DC.PrevLineSize = inner_window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + + // Make borders not overlap our contents by offsetting HostClipRect (#6765, #7428, #3752) + // (we normally shouldn't alter HostClipRect as we rely on TableMergeDrawChannels() expanding non-clipped column toward the + // limits of that rectangle, in order for ImDrawListSplitter::Merge() to merge the draw commands. However since the overlap + // problem only affect scrolling tables in this case we can get away with doing it without extra cost). + if (inner_window != outer_window) + { + // FIXME: Because inner_window's Scrollbar doesn't know about border size, since it's not encoded in window->WindowBorderSize, + // it already overlaps it and doesn't need an extra offset. Ideally we should be able to pass custom border size with + // different x/y values to BeginChild(). + if (flags & ImGuiTableFlags_BordersOuterV) + { + table->HostClipRect.Min.x = ImMin(table->HostClipRect.Min.x + TABLE_BORDER_SIZE, table->HostClipRect.Max.x); + if (inner_window->DecoOuterSizeX2 == 0.0f) + table->HostClipRect.Max.x = ImMax(table->HostClipRect.Max.x - TABLE_BORDER_SIZE, table->HostClipRect.Min.x); + } + if (flags & ImGuiTableFlags_BordersOuterH) + { + table->HostClipRect.Min.y = ImMin(table->HostClipRect.Min.y + TABLE_BORDER_SIZE, table->HostClipRect.Max.y); + if (inner_window->DecoOuterSizeY2 == 0.0f) + table->HostClipRect.Max.y = ImMax(table->HostClipRect.Max.y - TABLE_BORDER_SIZE, table->HostClipRect.Min.y); + } + } + + // Padding and Spacing + // - None ........Content..... Pad .....Content........ + // - PadOuter | Pad ..Content..... Pad .....Content.. Pad | + // - PadInner ........Content.. Pad | Pad ..Content........ + // - PadOuter+PadInner | Pad ..Content.. Pad | Pad ..Content.. Pad | + const bool pad_outer_x = (flags & ImGuiTableFlags_NoPadOuterX) ? false : (flags & ImGuiTableFlags_PadOuterX) ? true : (flags & ImGuiTableFlags_BordersOuterV) != 0; + const bool pad_inner_x = (flags & ImGuiTableFlags_NoPadInnerX) ? false : true; + const float inner_spacing_for_border = (flags & ImGuiTableFlags_BordersInnerV) ? TABLE_BORDER_SIZE : 0.0f; + const float inner_spacing_explicit = (pad_inner_x && (flags & ImGuiTableFlags_BordersInnerV) == 0) ? g.Style.CellPadding.x : 0.0f; + const float inner_padding_explicit = (pad_inner_x && (flags & ImGuiTableFlags_BordersInnerV) != 0) ? g.Style.CellPadding.x : 0.0f; + table->CellSpacingX1 = inner_spacing_explicit + inner_spacing_for_border; + table->CellSpacingX2 = inner_spacing_explicit; + table->CellPaddingX = inner_padding_explicit; + + const float outer_padding_for_border = (flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f; + const float outer_padding_explicit = pad_outer_x ? g.Style.CellPadding.x : 0.0f; + table->OuterPaddingX = (outer_padding_for_border + outer_padding_explicit) - table->CellPaddingX; + + table->CurrentColumn = -1; + table->CurrentRow = -1; + table->RowBgColorCounter = 0; + table->LastRowFlags = ImGuiTableRowFlags_None; + table->InnerClipRect = (inner_window == outer_window) ? table->WorkRect : inner_window->ClipRect; + table->InnerClipRect.ClipWith(table->WorkRect); // We need this to honor inner_width + table->InnerClipRect.ClipWithFull(table->HostClipRect); + table->InnerClipRect.Max.y = (flags & ImGuiTableFlags_NoHostExtendY) ? ImMin(table->InnerClipRect.Max.y, inner_window->WorkRect.Max.y) : table->HostClipRect.Max.y; + + table->RowPosY1 = table->RowPosY2 = table->WorkRect.Min.y; // This is needed somehow + table->RowTextBaseline = 0.0f; // This will be cleared again by TableBeginRow() + table->RowCellPaddingY = 0.0f; + table->FreezeRowsRequest = table->FreezeRowsCount = 0; // This will be setup by TableSetupScrollFreeze(), if any + table->FreezeColumnsRequest = table->FreezeColumnsCount = 0; + table->IsUnfrozenRows = true; + table->DeclColumnsCount = table->AngledHeadersCount = 0; + if (previous_frame_active + 1 < g.FrameCount) + table->IsActiveIdInTable = false; + table->AngledHeadersHeight = 0.0f; + temp_data->AngledHeadersExtraWidth = 0.0f; + + // Using opaque colors facilitate overlapping lines of the grid, otherwise we'd need to improve TableDrawBorders() + table->BorderColorStrong = GetColorU32(ImGuiCol_TableBorderStrong); + table->BorderColorLight = GetColorU32(ImGuiCol_TableBorderLight); + + // Make table current + g.CurrentTable = table; + outer_window->DC.NavIsScrollPushableX = false; // Shortcut for NavUpdateCurrentWindowIsScrollPushableX(); + outer_window->DC.CurrentTableIdx = table_idx; + if (inner_window != outer_window) // So EndChild() within the inner window can restore the table properly. + inner_window->DC.CurrentTableIdx = table_idx; + + if ((previous_flags & ImGuiTableFlags_Reorderable) && (flags & ImGuiTableFlags_Reorderable) == 0) + table->IsResetDisplayOrderRequest = true; + + // Mark as used to avoid GC + if (table_idx >= g.TablesLastTimeActive.Size) + g.TablesLastTimeActive.resize(table_idx + 1, -1.0f); + g.TablesLastTimeActive[table_idx] = (float)g.Time; + temp_data->LastTimeActive = (float)g.Time; + table->MemoryCompacted = false; + + // Setup memory buffer (clear data if columns count changed) + ImGuiTableColumn* old_columns_to_preserve = NULL; + void* old_columns_raw_data = NULL; + const int old_columns_count = table->Columns.size(); + if (old_columns_count != 0 && old_columns_count != columns_count) + { + // Attempt to preserve width on column count change (#4046) + old_columns_to_preserve = table->Columns.Data; + old_columns_raw_data = table->RawData; + table->RawData = NULL; + } + if (table->RawData == NULL) + { + TableBeginInitMemory(table, columns_count); + table->IsInitializing = table->IsSettingsRequestLoad = true; + } + if (table->IsResetAllRequest) + TableResetSettings(table); + if (table->IsInitializing) + { + // Initialize + table->SettingsOffset = -1; + table->IsSortSpecsDirty = true; + table->IsSettingsDirty = true; // Records itself into .ini file even when in default state (#7934) + table->InstanceInteracted = -1; + table->ContextPopupColumn = -1; + table->ReorderColumn = table->ResizedColumn = table->LastResizedColumn = -1; + table->AutoFitSingleColumn = -1; + table->HoveredColumnBody = table->HoveredColumnBorder = -1; + for (int n = 0; n < columns_count; n++) + { + ImGuiTableColumn* column = &table->Columns[n]; + if (old_columns_to_preserve && n < old_columns_count) + { + // FIXME: We don't attempt to preserve column order in this path. + *column = old_columns_to_preserve[n]; + } + else + { + float width_auto = column->WidthAuto; + *column = ImGuiTableColumn(); + column->WidthAuto = width_auto; + column->IsPreserveWidthAuto = true; // Preserve WidthAuto when reinitializing a live table: not technically necessary but remove a visible flicker + column->IsEnabled = column->IsUserEnabled = column->IsUserEnabledNextFrame = true; + } + column->DisplayOrder = table->DisplayOrderToIndex[n] = (ImGuiTableColumnIdx)n; + } + } + if (old_columns_raw_data) + IM_FREE(old_columns_raw_data); + + // Load settings + if (table->IsSettingsRequestLoad) + TableLoadSettings(table); + + // Handle DPI/font resize + // This is designed to facilitate DPI changes with the assumption that e.g. style.CellPadding has been scaled as well. + // It will also react to changing fonts with mixed results. It doesn't need to be perfect but merely provide a decent transition. + // FIXME-DPI: Provide consistent standards for reference size. Perhaps using g.CurrentDpiScale would be more self explanatory. + // This is will lead us to non-rounded WidthRequest in columns, which should work but is a poorly tested path. + const float new_ref_scale_unit = g.FontSize; // g.Font->GetCharAdvance('A') ? + if (table->RefScale != 0.0f && table->RefScale != new_ref_scale_unit) + { + const float scale_factor = new_ref_scale_unit / table->RefScale; + //IMGUI_DEBUG_PRINT("[table] %08X RefScaleUnit %.3f -> %.3f, scaling width by %.3f\n", table->ID, table->RefScaleUnit, new_ref_scale_unit, scale_factor); + for (int n = 0; n < columns_count; n++) + table->Columns[n].WidthRequest = table->Columns[n].WidthRequest * scale_factor; + } + table->RefScale = new_ref_scale_unit; + + // Disable output until user calls TableNextRow() or TableNextColumn() leading to the TableUpdateLayout() call.. + // This is not strictly necessary but will reduce cases were "out of table" output will be misleading to the user. + // Because we cannot safely assert in EndTable() when no rows have been created, this seems like our best option. + inner_window->SkipItems = true; + + // Clear names + // At this point the ->NameOffset field of each column will be invalid until TableUpdateLayout() or the first call to TableSetupColumn() + if (table->ColumnsNames.Buf.Size > 0) + table->ColumnsNames.Buf.resize(0); + + // Apply queued resizing/reordering/hiding requests + TableBeginApplyRequests(table); + + return true; +} + +// For reference, the average total _allocation count_ for a table is: +// + 0 (for ImGuiTable instance, we are pooling allocations in g.Tables[]) +// + 1 (for table->RawData allocated below) +// + 1 (for table->ColumnsNames, if names are used) +// Shared allocations for the maximum number of simultaneously nested tables (generally a very small number) +// + 1 (for table->Splitter._Channels) +// + 2 * active_channels_count (for ImDrawCmd and ImDrawIdx buffers inside channels) +// Where active_channels_count is variable but often == columns_count or == columns_count + 1, see TableSetupDrawChannels() for details. +// Unused channels don't perform their +2 allocations. +void ImGui::TableBeginInitMemory(ImGuiTable* table, int columns_count) +{ + // Allocate single buffer for our arrays + const int columns_bit_array_size = (int)ImBitArrayGetStorageSizeInBytes(columns_count); + ImSpanAllocator<6> span_allocator; + span_allocator.Reserve(0, columns_count * sizeof(ImGuiTableColumn)); + span_allocator.Reserve(1, columns_count * sizeof(ImGuiTableColumnIdx)); + span_allocator.Reserve(2, columns_count * sizeof(ImGuiTableCellData), 4); + for (int n = 3; n < 6; n++) + span_allocator.Reserve(n, columns_bit_array_size); + table->RawData = IM_ALLOC(span_allocator.GetArenaSizeInBytes()); + memset(table->RawData, 0, span_allocator.GetArenaSizeInBytes()); + span_allocator.SetArenaBasePtr(table->RawData); + span_allocator.GetSpan(0, &table->Columns); + span_allocator.GetSpan(1, &table->DisplayOrderToIndex); + span_allocator.GetSpan(2, &table->RowCellData); + table->EnabledMaskByDisplayOrder = (ImU32*)span_allocator.GetSpanPtrBegin(3); + table->EnabledMaskByIndex = (ImU32*)span_allocator.GetSpanPtrBegin(4); + table->VisibleMaskByIndex = (ImU32*)span_allocator.GetSpanPtrBegin(5); +} + +// Apply queued resizing/reordering/hiding requests +void ImGui::TableBeginApplyRequests(ImGuiTable* table) +{ + // Handle resizing request + // (We process this in the TableBegin() of the first instance of each table) + // FIXME-TABLE: Contains columns if our work area doesn't allow for scrolling? + if (table->InstanceCurrent == 0) + { + if (table->ResizedColumn != -1 && table->ResizedColumnNextWidth != FLT_MAX) + TableSetColumnWidth(table->ResizedColumn, table->ResizedColumnNextWidth); + table->LastResizedColumn = table->ResizedColumn; + table->ResizedColumnNextWidth = FLT_MAX; + table->ResizedColumn = -1; + + // Process auto-fit for single column, which is a special case for stretch columns and fixed columns with FixedSame policy. + // FIXME-TABLE: Would be nice to redistribute available stretch space accordingly to other weights, instead of giving it all to siblings. + if (table->AutoFitSingleColumn != -1) + { + TableSetColumnWidth(table->AutoFitSingleColumn, table->Columns[table->AutoFitSingleColumn].WidthAuto); + table->AutoFitSingleColumn = -1; + } + } + + // Handle reordering request + // Note: we don't clear ReorderColumn after handling the request. + if (table->InstanceCurrent == 0) + { + if (table->HeldHeaderColumn == -1 && table->ReorderColumn != -1) + table->ReorderColumn = -1; + table->HeldHeaderColumn = -1; + if (table->ReorderColumn != -1 && table->ReorderColumnDir != 0) + { + // We need to handle reordering across hidden columns. + // In the configuration below, moving C to the right of E will lead to: + // ... C [D] E ---> ... [D] E C (Column name/index) + // ... 2 3 4 ... 2 3 4 (Display order) + const int reorder_dir = table->ReorderColumnDir; + IM_ASSERT(reorder_dir == -1 || reorder_dir == +1); + IM_ASSERT(table->Flags & ImGuiTableFlags_Reorderable); + ImGuiTableColumn* src_column = &table->Columns[table->ReorderColumn]; + ImGuiTableColumn* dst_column = &table->Columns[(reorder_dir == -1) ? src_column->PrevEnabledColumn : src_column->NextEnabledColumn]; + IM_UNUSED(dst_column); + const int src_order = src_column->DisplayOrder; + const int dst_order = dst_column->DisplayOrder; + src_column->DisplayOrder = (ImGuiTableColumnIdx)dst_order; + for (int order_n = src_order + reorder_dir; order_n != dst_order + reorder_dir; order_n += reorder_dir) + table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder -= (ImGuiTableColumnIdx)reorder_dir; + IM_ASSERT(dst_column->DisplayOrder == dst_order - reorder_dir); + + // Display order is stored in both columns->IndexDisplayOrder and table->DisplayOrder[]. Rebuild later from the former. + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->DisplayOrderToIndex[table->Columns[column_n].DisplayOrder] = (ImGuiTableColumnIdx)column_n; + table->ReorderColumnDir = 0; + table->IsSettingsDirty = true; + } + } + + // Handle display order reset request + if (table->IsResetDisplayOrderRequest) + { + for (int n = 0; n < table->ColumnsCount; n++) + table->DisplayOrderToIndex[n] = table->Columns[n].DisplayOrder = (ImGuiTableColumnIdx)n; + table->IsResetDisplayOrderRequest = false; + table->IsSettingsDirty = true; + } +} + +// Adjust flags: default width mode + stretch columns are not allowed when auto extending +static void TableSetupColumnFlags(ImGuiTable* table, ImGuiTableColumn* column, ImGuiTableColumnFlags flags_in) +{ + ImGuiTableColumnFlags flags = flags_in; + + // Sizing Policy + if ((flags & ImGuiTableColumnFlags_WidthMask_) == 0) + { + const ImGuiTableFlags table_sizing_policy = (table->Flags & ImGuiTableFlags_SizingMask_); + if (table_sizing_policy == ImGuiTableFlags_SizingFixedFit || table_sizing_policy == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableColumnFlags_WidthFixed; + else + flags |= ImGuiTableColumnFlags_WidthStretch; + } + else + { + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags_WidthMask_)); // Check that only 1 of each set is used. + } + + // Resize + if ((table->Flags & ImGuiTableFlags_Resizable) == 0) + flags |= ImGuiTableColumnFlags_NoResize; + + // Sorting + if ((flags & ImGuiTableColumnFlags_NoSortAscending) && (flags & ImGuiTableColumnFlags_NoSortDescending)) + flags |= ImGuiTableColumnFlags_NoSort; + + // Indentation + if ((flags & ImGuiTableColumnFlags_IndentMask_) == 0) + flags |= (table->Columns.index_from_ptr(column) == 0) ? ImGuiTableColumnFlags_IndentEnable : ImGuiTableColumnFlags_IndentDisable; + + // Alignment + //if ((flags & ImGuiTableColumnFlags_AlignMask_) == 0) + // flags |= ImGuiTableColumnFlags_AlignCenter; + //IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiTableColumnFlags_AlignMask_)); // Check that only 1 of each set is used. + + // Preserve status flags + column->Flags = flags | (column->Flags & ImGuiTableColumnFlags_StatusMask_); + + // Build an ordered list of available sort directions + column->SortDirectionsAvailCount = column->SortDirectionsAvailMask = column->SortDirectionsAvailList = 0; + if (table->Flags & ImGuiTableFlags_Sortable) + { + int count = 0, mask = 0, list = 0; + if ((flags & ImGuiTableColumnFlags_PreferSortAscending) != 0 && (flags & ImGuiTableColumnFlags_NoSortAscending) == 0) { mask |= 1 << ImGuiSortDirection_Ascending; list |= ImGuiSortDirection_Ascending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortDescending) != 0 && (flags & ImGuiTableColumnFlags_NoSortDescending) == 0) { mask |= 1 << ImGuiSortDirection_Descending; list |= ImGuiSortDirection_Descending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortAscending) == 0 && (flags & ImGuiTableColumnFlags_NoSortAscending) == 0) { mask |= 1 << ImGuiSortDirection_Ascending; list |= ImGuiSortDirection_Ascending << (count << 1); count++; } + if ((flags & ImGuiTableColumnFlags_PreferSortDescending) == 0 && (flags & ImGuiTableColumnFlags_NoSortDescending) == 0) { mask |= 1 << ImGuiSortDirection_Descending; list |= ImGuiSortDirection_Descending << (count << 1); count++; } + if ((table->Flags & ImGuiTableFlags_SortTristate) || count == 0) { mask |= 1 << ImGuiSortDirection_None; count++; } + column->SortDirectionsAvailList = (ImU8)list; + column->SortDirectionsAvailMask = (ImU8)mask; + column->SortDirectionsAvailCount = (ImU8)count; + ImGui::TableFixColumnSortDirection(table, column); + } +} + +// Layout columns for the frame. This is in essence the followup to BeginTable() and this is our largest function. +// Runs on the first call to TableNextRow(), to give a chance for TableSetupColumn() and other TableSetupXXXXX() functions to be called first. +// FIXME-TABLE: Our width (and therefore our WorkRect) will be minimal in the first frame for _WidthAuto columns. +// Increase feedback side-effect with widgets relying on WorkRect.Max.x... Maybe provide a default distribution for _WidthAuto columns? +void ImGui::TableUpdateLayout(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(table->IsLayoutLocked == false); + + const ImGuiTableFlags table_sizing_policy = (table->Flags & ImGuiTableFlags_SizingMask_); + table->IsDefaultDisplayOrder = true; + table->ColumnsEnabledCount = 0; + ImBitArrayClearAllBits(table->EnabledMaskByIndex, table->ColumnsCount); + ImBitArrayClearAllBits(table->EnabledMaskByDisplayOrder, table->ColumnsCount); + table->LeftMostEnabledColumn = -1; + table->MinColumnWidth = ImMax(1.0f, g.Style.FramePadding.x * 1.0f); // g.Style.ColumnsMinSpacing; // FIXME-TABLE + + // [Part 1] Apply/lock Enabled and Order states. Calculate auto/ideal width for columns. Count fixed/stretch columns. + // Process columns in their visible orders as we are building the Prev/Next indices. + int count_fixed = 0; // Number of columns that have fixed sizing policies + int count_stretch = 0; // Number of columns that have stretch sizing policies + int prev_visible_column_idx = -1; + bool has_auto_fit_request = false; + bool has_resizable = false; + float stretch_sum_width_auto = 0.0f; + float fixed_max_width_auto = 0.0f; + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + if (column_n != order_n) + table->IsDefaultDisplayOrder = false; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Clear column setup if not submitted by user. Currently we make it mandatory to call TableSetupColumn() every frame. + // It would easily work without but we're not ready to guarantee it since e.g. names need resubmission anyway. + // We take a slight shortcut but in theory we could be calling TableSetupColumn() here with dummy values, it should yield the same effect. + if (table->DeclColumnsCount <= column_n) + { + TableSetupColumnFlags(table, column, ImGuiTableColumnFlags_None); + column->NameOffset = -1; + column->UserID = 0; + column->InitStretchWeightOrWidth = -1.0f; + } + + // Update Enabled state, mark settings and sort specs dirty + if (!(table->Flags & ImGuiTableFlags_Hideable) || (column->Flags & ImGuiTableColumnFlags_NoHide)) + column->IsUserEnabledNextFrame = true; + if (column->IsUserEnabled != column->IsUserEnabledNextFrame) + { + column->IsUserEnabled = column->IsUserEnabledNextFrame; + table->IsSettingsDirty = true; + } + column->IsEnabled = column->IsUserEnabled && (column->Flags & ImGuiTableColumnFlags_Disabled) == 0; + + if (column->SortOrder != -1 && !column->IsEnabled) + table->IsSortSpecsDirty = true; + if (column->SortOrder > 0 && !(table->Flags & ImGuiTableFlags_SortMulti)) + table->IsSortSpecsDirty = true; + + // Auto-fit unsized columns + const bool start_auto_fit = (column->Flags & ImGuiTableColumnFlags_WidthFixed) ? (column->WidthRequest < 0.0f) : (column->StretchWeight < 0.0f); + if (start_auto_fit) + column->AutoFitQueue = column->CannotSkipItemsQueue = (1 << 3) - 1; // Fit for three frames + + if (!column->IsEnabled) + { + column->IndexWithinEnabledSet = -1; + continue; + } + + // Mark as enabled and link to previous/next enabled column + column->PrevEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; + column->NextEnabledColumn = -1; + if (prev_visible_column_idx != -1) + table->Columns[prev_visible_column_idx].NextEnabledColumn = (ImGuiTableColumnIdx)column_n; + else + table->LeftMostEnabledColumn = (ImGuiTableColumnIdx)column_n; + column->IndexWithinEnabledSet = table->ColumnsEnabledCount++; + ImBitArraySetBit(table->EnabledMaskByIndex, column_n); + ImBitArraySetBit(table->EnabledMaskByDisplayOrder, column->DisplayOrder); + prev_visible_column_idx = column_n; + IM_ASSERT(column->IndexWithinEnabledSet <= column->DisplayOrder); + + // Calculate ideal/auto column width (that's the width required for all contents to be visible without clipping) + // Combine width from regular rows + width from headers unless requested not to. + if (!column->IsPreserveWidthAuto && table->InstanceCurrent == 0) + column->WidthAuto = TableGetColumnWidthAuto(table, column); + + // Non-resizable columns keep their requested width (apply user value regardless of IsPreserveWidthAuto) + const bool column_is_resizable = (column->Flags & ImGuiTableColumnFlags_NoResize) == 0; + if (column_is_resizable) + has_resizable = true; + if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && column->InitStretchWeightOrWidth > 0.0f && !column_is_resizable) + column->WidthAuto = column->InitStretchWeightOrWidth; + + if (column->AutoFitQueue != 0x00) + has_auto_fit_request = true; + if (column->Flags & ImGuiTableColumnFlags_WidthStretch) + { + stretch_sum_width_auto += column->WidthAuto; + count_stretch++; + } + else + { + fixed_max_width_auto = ImMax(fixed_max_width_auto, column->WidthAuto); + count_fixed++; + } + } + if ((table->Flags & ImGuiTableFlags_Sortable) && table->SortSpecsCount == 0 && !(table->Flags & ImGuiTableFlags_SortTristate)) + table->IsSortSpecsDirty = true; + table->RightMostEnabledColumn = (ImGuiTableColumnIdx)prev_visible_column_idx; + IM_ASSERT(table->LeftMostEnabledColumn >= 0 && table->RightMostEnabledColumn >= 0); + + // [Part 2] Disable child window clipping while fitting columns. This is not strictly necessary but makes it possible to avoid + // the column fitting having to wait until the first visible frame of the child container (may or not be a good thing). Also see #6510. + // FIXME-TABLE: for always auto-resizing columns may not want to do that all the time. + if (has_auto_fit_request && table->OuterWindow != table->InnerWindow) + table->InnerWindow->SkipItems = false; + if (has_auto_fit_request) + table->IsSettingsDirty = true; + + // [Part 3] Fix column flags and record a few extra information. + float sum_width_requests = 0.0f; // Sum of all width for fixed and auto-resize columns, excluding width contributed by Stretch columns but including spacing/padding. + float stretch_sum_weights = 0.0f; // Sum of all weights for stretch columns. + table->LeftMostStretchedColumn = table->RightMostStretchedColumn = -1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + const bool column_is_resizable = (column->Flags & ImGuiTableColumnFlags_NoResize) == 0; + if (column->Flags & ImGuiTableColumnFlags_WidthFixed) + { + // Apply same widths policy + float width_auto = column->WidthAuto; + if (table_sizing_policy == ImGuiTableFlags_SizingFixedSame && (column->AutoFitQueue != 0x00 || !column_is_resizable)) + width_auto = fixed_max_width_auto; + + // Apply automatic width + // Latch initial size for fixed columns and update it constantly for auto-resizing column (unless clipped!) + if (column->AutoFitQueue != 0x00) + column->WidthRequest = width_auto; + else if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && !column_is_resizable && column->IsRequestOutput) + column->WidthRequest = width_auto; + + // FIXME-TABLE: Increase minimum size during init frame to avoid biasing auto-fitting widgets + // (e.g. TextWrapped) too much. Otherwise what tends to happen is that TextWrapped would output a very + // large height (= first frame scrollbar display very off + clipper would skip lots of items). + // This is merely making the side-effect less extreme, but doesn't properly fixes it. + // FIXME: Move this to ->WidthGiven to avoid temporary lossyless? + // FIXME: This break IsPreserveWidthAuto from not flickering if the stored WidthAuto was smaller. + if (column->AutoFitQueue > 0x01 && table->IsInitializing && !column->IsPreserveWidthAuto) + column->WidthRequest = ImMax(column->WidthRequest, table->MinColumnWidth * 4.0f); // FIXME-TABLE: Another constant/scale? + sum_width_requests += column->WidthRequest; + } + else + { + // Initialize stretch weight + if (column->AutoFitQueue != 0x00 || column->StretchWeight < 0.0f || !column_is_resizable) + { + if (column->InitStretchWeightOrWidth > 0.0f) + column->StretchWeight = column->InitStretchWeightOrWidth; + else if (table_sizing_policy == ImGuiTableFlags_SizingStretchProp) + column->StretchWeight = (column->WidthAuto / stretch_sum_width_auto) * count_stretch; + else + column->StretchWeight = 1.0f; + } + + stretch_sum_weights += column->StretchWeight; + if (table->LeftMostStretchedColumn == -1 || table->Columns[table->LeftMostStretchedColumn].DisplayOrder > column->DisplayOrder) + table->LeftMostStretchedColumn = (ImGuiTableColumnIdx)column_n; + if (table->RightMostStretchedColumn == -1 || table->Columns[table->RightMostStretchedColumn].DisplayOrder < column->DisplayOrder) + table->RightMostStretchedColumn = (ImGuiTableColumnIdx)column_n; + } + column->IsPreserveWidthAuto = false; + sum_width_requests += table->CellPaddingX * 2.0f; + } + table->ColumnsEnabledFixedCount = (ImGuiTableColumnIdx)count_fixed; + table->ColumnsStretchSumWeights = stretch_sum_weights; + + // [Part 4] Apply final widths based on requested widths + const ImRect work_rect = table->WorkRect; + const float width_spacings = (table->OuterPaddingX * 2.0f) + (table->CellSpacingX1 + table->CellSpacingX2) * (table->ColumnsEnabledCount - 1); + const float width_removed = (table->HasScrollbarYPrev && !table->InnerWindow->ScrollbarY) ? g.Style.ScrollbarSize : 0.0f; // To synchronize decoration width of synced tables with mismatching scrollbar state (#5920) + const float width_avail = ImMax(1.0f, (((table->Flags & ImGuiTableFlags_ScrollX) && table->InnerWidth == 0.0f) ? table->InnerClipRect.GetWidth() : work_rect.GetWidth()) - width_removed); + const float width_avail_for_stretched_columns = width_avail - width_spacings - sum_width_requests; + float width_remaining_for_stretched_columns = width_avail_for_stretched_columns; + table->ColumnsGivenWidth = width_spacings + (table->CellPaddingX * 2.0f) * table->ColumnsEnabledCount; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Allocate width for stretched/weighted columns (StretchWeight gets converted into WidthRequest) + if (column->Flags & ImGuiTableColumnFlags_WidthStretch) + { + float weight_ratio = column->StretchWeight / stretch_sum_weights; + column->WidthRequest = IM_TRUNC(ImMax(width_avail_for_stretched_columns * weight_ratio, table->MinColumnWidth) + 0.01f); + width_remaining_for_stretched_columns -= column->WidthRequest; + } + + // [Resize Rule 1] The right-most Visible column is not resizable if there is at least one Stretch column + // See additional comments in TableSetColumnWidth(). + if (column->NextEnabledColumn == -1 && table->LeftMostStretchedColumn != -1) + column->Flags |= ImGuiTableColumnFlags_NoDirectResize_; + + // Assign final width, record width in case we will need to shrink + column->WidthGiven = ImTrunc(ImMax(column->WidthRequest, table->MinColumnWidth)); + table->ColumnsGivenWidth += column->WidthGiven; + } + + // [Part 5] Redistribute stretch remainder width due to rounding (remainder width is < 1.0f * number of Stretch column). + // Using right-to-left distribution (more likely to match resizing cursor). + if (width_remaining_for_stretched_columns >= 1.0f && !(table->Flags & ImGuiTableFlags_PreciseWidths)) + for (int order_n = table->ColumnsCount - 1; stretch_sum_weights > 0.0f && width_remaining_for_stretched_columns >= 1.0f && order_n >= 0; order_n--) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + continue; + ImGuiTableColumn* column = &table->Columns[table->DisplayOrderToIndex[order_n]]; + if (!(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + column->WidthRequest += 1.0f; + column->WidthGiven += 1.0f; + width_remaining_for_stretched_columns -= 1.0f; + } + + // Determine if table is hovered which will be used to flag columns as hovered. + // - In principle we'd like to use the equivalent of IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem), + // but because our item is partially submitted at this point we use ItemHoverable() and a workaround (temporarily + // clear ActiveId, which is equivalent to the change provided by _AllowWhenBLockedByActiveItem). + // - This allows columns to be marked as hovered when e.g. clicking a button inside the column, or using drag and drop. + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + table_instance->HoveredRowLast = table_instance->HoveredRowNext; + table_instance->HoveredRowNext = -1; + table->HoveredColumnBody = table->HoveredColumnBorder = -1; + const ImRect mouse_hit_rect(table->OuterRect.Min.x, table->OuterRect.Min.y, table->OuterRect.Max.x, ImMax(table->OuterRect.Max.y, table->OuterRect.Min.y + table_instance->LastOuterHeight)); + const ImGuiID backup_active_id = g.ActiveId; + g.ActiveId = 0; + const bool is_hovering_table = ItemHoverable(mouse_hit_rect, 0, ImGuiItemFlags_None); + g.ActiveId = backup_active_id; + + // Determine skewed MousePos.x to support angled headers. + float mouse_skewed_x = g.IO.MousePos.x; + if (table->AngledHeadersHeight > 0.0f) + if (g.IO.MousePos.y >= table->OuterRect.Min.y && g.IO.MousePos.y <= table->OuterRect.Min.y + table->AngledHeadersHeight) + mouse_skewed_x += ImTrunc((table->OuterRect.Min.y + table->AngledHeadersHeight - g.IO.MousePos.y) * table->AngledHeadersSlope); + + // [Part 6] Setup final position, offset, skip/clip states and clipping rectangles, detect hovered column + // Process columns in their visible orders as we are comparing the visible order and adjusting host_clip_rect while looping. + int visible_n = 0; + bool has_at_least_one_column_requesting_output = false; + bool offset_x_frozen = (table->FreezeColumnsCount > 0); + float offset_x = ((table->FreezeColumnsCount > 0) ? table->OuterRect.Min.x : work_rect.Min.x) + table->OuterPaddingX - table->CellSpacingX1; + ImRect host_clip_rect = table->InnerClipRect; + //host_clip_rect.Max.x += table->CellPaddingX + table->CellSpacingX2; + ImBitArrayClearAllBits(table->VisibleMaskByIndex, table->ColumnsCount); + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Initial nav layer: using FreezeRowsCount, NOT FreezeRowsRequest, so Header line changes layer when frozen + column->NavLayerCurrent = (ImS8)(table->FreezeRowsCount > 0 ? ImGuiNavLayer_Menu : (ImGuiNavLayer)table->NavLayer); + + if (offset_x_frozen && table->FreezeColumnsCount == visible_n) + { + offset_x += work_rect.Min.x - table->OuterRect.Min.x; + offset_x_frozen = false; + } + + // Clear status flags + column->Flags &= ~ImGuiTableColumnFlags_StatusMask_; + + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + { + // Hidden column: clear a few fields and we are done with it for the remainder of the function. + // We set a zero-width clip rect but set Min.y/Max.y properly to not interfere with the clipper. + column->MinX = column->MaxX = column->WorkMinX = column->ClipRect.Min.x = column->ClipRect.Max.x = offset_x; + column->WidthGiven = 0.0f; + column->ClipRect.Min.y = work_rect.Min.y; + column->ClipRect.Max.y = FLT_MAX; + column->ClipRect.ClipWithFull(host_clip_rect); + column->IsVisibleX = column->IsVisibleY = column->IsRequestOutput = false; + column->IsSkipItems = true; + column->ItemWidth = 1.0f; + continue; + } + + // Lock start position + column->MinX = offset_x; + + // Lock width based on start position and minimum/maximum width for this position + column->WidthMax = TableCalcMaxColumnWidth(table, column_n); + column->WidthGiven = ImMin(column->WidthGiven, column->WidthMax); + column->WidthGiven = ImMax(column->WidthGiven, ImMin(column->WidthRequest, table->MinColumnWidth)); + column->MaxX = offset_x + column->WidthGiven + table->CellSpacingX1 + table->CellSpacingX2 + table->CellPaddingX * 2.0f; + + // Lock other positions + // - ClipRect.Min.x: Because merging draw commands doesn't compare min boundaries, we make ClipRect.Min.x match left bounds to be consistent regardless of merging. + // - ClipRect.Max.x: using WorkMaxX instead of MaxX (aka including padding) makes things more consistent when resizing down, tho slightly detrimental to visibility in very-small column. + // - ClipRect.Max.x: using MaxX makes it easier for header to receive hover highlight with no discontinuity and display sorting arrow. + // - FIXME-TABLE: We want equal width columns to have equal (ClipRect.Max.x - WorkMinX) width, which means ClipRect.max.x cannot stray off host_clip_rect.Max.x else right-most column may appear shorter. + const float previous_instance_work_min_x = column->WorkMinX; + column->WorkMinX = column->MinX + table->CellPaddingX + table->CellSpacingX1; + column->WorkMaxX = column->MaxX - table->CellPaddingX - table->CellSpacingX2; // Expected max + column->ItemWidth = ImTrunc(column->WidthGiven * 0.65f); + column->ClipRect.Min.x = column->MinX; + column->ClipRect.Min.y = work_rect.Min.y; + column->ClipRect.Max.x = column->MaxX; //column->WorkMaxX; + column->ClipRect.Max.y = FLT_MAX; + column->ClipRect.ClipWithFull(host_clip_rect); + + // Mark column as Clipped (not in sight) + // Note that scrolling tables (where inner_window != outer_window) handle Y clipped earlier in BeginTable() so IsVisibleY really only applies to non-scrolling tables. + // FIXME-TABLE: Because InnerClipRect.Max.y is conservatively ==outer_window->ClipRect.Max.y, we never can mark columns _Above_ the scroll line as not IsVisibleY. + // Taking advantage of LastOuterHeight would yield good results there... + // FIXME-TABLE: Y clipping is disabled because it effectively means not submitting will reduce contents width which is fed to outer_window->DC.CursorMaxPos.x, + // and this may be used (e.g. typically by outer_window using AlwaysAutoResize or outer_window's horizontal scrollbar, but could be something else). + // Possible solution to preserve last known content width for clipped column. Test 'table_reported_size' fails when enabling Y clipping and window is resized small. + column->IsVisibleX = (column->ClipRect.Max.x > column->ClipRect.Min.x); + column->IsVisibleY = true; // (column->ClipRect.Max.y > column->ClipRect.Min.y); + const bool is_visible = column->IsVisibleX; //&& column->IsVisibleY; + if (is_visible) + ImBitArraySetBit(table->VisibleMaskByIndex, column_n); + + // Mark column as requesting output from user. Note that fixed + non-resizable sets are auto-fitting at all times and therefore always request output. + column->IsRequestOutput = is_visible || column->AutoFitQueue != 0 || column->CannotSkipItemsQueue != 0; + + // Mark column as SkipItems (ignoring all items/layout) + // (table->HostSkipItems is a copy of inner_window->SkipItems before we cleared it above in Part 2) + column->IsSkipItems = !column->IsEnabled || table->HostSkipItems; + if (column->IsSkipItems) + IM_ASSERT(!is_visible); + if (column->IsRequestOutput && !column->IsSkipItems) + has_at_least_one_column_requesting_output = true; + + // Update status flags + column->Flags |= ImGuiTableColumnFlags_IsEnabled; + if (is_visible) + column->Flags |= ImGuiTableColumnFlags_IsVisible; + if (column->SortOrder != -1) + column->Flags |= ImGuiTableColumnFlags_IsSorted; + + // Detect hovered column + if (is_hovering_table && mouse_skewed_x >= column->ClipRect.Min.x && mouse_skewed_x < column->ClipRect.Max.x) + { + column->Flags |= ImGuiTableColumnFlags_IsHovered; + table->HoveredColumnBody = (ImGuiTableColumnIdx)column_n; + } + + // Alignment + // FIXME-TABLE: This align based on the whole column width, not per-cell, and therefore isn't useful in + // many cases (to be able to honor this we might be able to store a log of cells width, per row, for + // visible rows, but nav/programmatic scroll would have visible artifacts.) + //if (column->Flags & ImGuiTableColumnFlags_AlignRight) + // column->WorkMinX = ImMax(column->WorkMinX, column->MaxX - column->ContentWidthRowsUnfrozen); + //else if (column->Flags & ImGuiTableColumnFlags_AlignCenter) + // column->WorkMinX = ImLerp(column->WorkMinX, ImMax(column->StartX, column->MaxX - column->ContentWidthRowsUnfrozen), 0.5f); + + // Reset content width variables + if (table->InstanceCurrent == 0) + { + column->ContentMaxXFrozen = column->WorkMinX; + column->ContentMaxXUnfrozen = column->WorkMinX; + column->ContentMaxXHeadersUsed = column->WorkMinX; + column->ContentMaxXHeadersIdeal = column->WorkMinX; + } + else + { + // As we store an absolute value to make per-cell updates faster, we need to offset values used for width computation. + const float offset_from_previous_instance = column->WorkMinX - previous_instance_work_min_x; + column->ContentMaxXFrozen += offset_from_previous_instance; + column->ContentMaxXUnfrozen += offset_from_previous_instance; + column->ContentMaxXHeadersUsed += offset_from_previous_instance; + column->ContentMaxXHeadersIdeal += offset_from_previous_instance; + } + + // Don't decrement auto-fit counters until container window got a chance to submit its items + if (table->HostSkipItems == false && table->InstanceCurrent == 0) + { + column->AutoFitQueue >>= 1; + column->CannotSkipItemsQueue >>= 1; + } + + if (visible_n < table->FreezeColumnsCount) + host_clip_rect.Min.x = ImClamp(column->MaxX + TABLE_BORDER_SIZE, host_clip_rect.Min.x, host_clip_rect.Max.x); + + offset_x += column->WidthGiven + table->CellSpacingX1 + table->CellSpacingX2 + table->CellPaddingX * 2.0f; + visible_n++; + } + + // In case the table is visible (e.g. decorations) but all columns clipped, we keep a column visible. + // Else if give no chance to a clipper-savy user to submit rows and therefore total contents height used by scrollbar. + if (has_at_least_one_column_requesting_output == false) + { + table->Columns[table->LeftMostEnabledColumn].IsRequestOutput = true; + table->Columns[table->LeftMostEnabledColumn].IsSkipItems = false; + } + + // [Part 7] Detect/store when we are hovering the unused space after the right-most column (so e.g. context menus can react on it) + // Clear Resizable flag if none of our column are actually resizable (either via an explicit _NoResize flag, either + // because of using _WidthAuto/_WidthStretch). This will hide the resizing option from the context menu. + const float unused_x1 = ImMax(table->WorkRect.Min.x, table->Columns[table->RightMostEnabledColumn].ClipRect.Max.x); + if (is_hovering_table && table->HoveredColumnBody == -1) + if (mouse_skewed_x >= unused_x1) + table->HoveredColumnBody = (ImGuiTableColumnIdx)table->ColumnsCount; + if (has_resizable == false && (table->Flags & ImGuiTableFlags_Resizable)) + table->Flags &= ~ImGuiTableFlags_Resizable; + + table->IsActiveIdAliveBeforeTable = (g.ActiveIdIsAlive != 0); + + // [Part 8] Lock actual OuterRect/WorkRect right-most position. + // This is done late to handle the case of fixed-columns tables not claiming more widths that they need. + // Because of this we are careful with uses of WorkRect and InnerClipRect before this point. + if (table->RightMostStretchedColumn != -1) + table->Flags &= ~ImGuiTableFlags_NoHostExtendX; + if (table->Flags & ImGuiTableFlags_NoHostExtendX) + { + table->OuterRect.Max.x = table->WorkRect.Max.x = unused_x1; + table->InnerClipRect.Max.x = ImMin(table->InnerClipRect.Max.x, unused_x1); + } + table->InnerWindow->ParentWorkRect = table->WorkRect; + table->BorderX1 = table->InnerClipRect.Min.x; + table->BorderX2 = table->InnerClipRect.Max.x; + + // Setup window's WorkRect.Max.y for GetContentRegionAvail(). Other values will be updated in each TableBeginCell() call. + float window_content_max_y; + if (table->Flags & ImGuiTableFlags_NoHostExtendY) + window_content_max_y = table->OuterRect.Max.y; + else + window_content_max_y = ImMax(table->InnerWindow->ContentRegionRect.Max.y, (table->Flags & ImGuiTableFlags_ScrollY) ? 0.0f : table->OuterRect.Max.y); + table->InnerWindow->WorkRect.Max.y = ImClamp(window_content_max_y - g.Style.CellPadding.y, table->InnerWindow->WorkRect.Min.y, table->InnerWindow->WorkRect.Max.y); + + // [Part 9] Allocate draw channels and setup background cliprect + TableSetupDrawChannels(table); + + // [Part 10] Hit testing on borders + if (table->Flags & ImGuiTableFlags_Resizable) + TableUpdateBorders(table); + table_instance->LastTopHeadersRowHeight = 0.0f; + table->IsLayoutLocked = true; + table->IsUsingHeaders = false; + + // Highlight header + table->HighlightColumnHeader = -1; + if (table->IsContextPopupOpen && table->ContextPopupColumn != -1 && table->InstanceInteracted == table->InstanceCurrent) + table->HighlightColumnHeader = table->ContextPopupColumn; + else if ((table->Flags & ImGuiTableFlags_HighlightHoveredColumn) && table->HoveredColumnBody != -1 && table->HoveredColumnBody != table->ColumnsCount && table->HoveredColumnBorder == -1) + if (g.ActiveId == 0 || (table->IsActiveIdInTable || g.DragDropActive)) + table->HighlightColumnHeader = table->HoveredColumnBody; + + // [Part 11] Default context menu + // - To append to this menu: you can call TableBeginContextMenuPopup()/.../EndPopup(). + // - To modify or replace this: set table->IsContextPopupNoDefaultContents = true, then call TableBeginContextMenuPopup()/.../EndPopup(). + // - You may call TableDrawDefaultContextMenu() with selected flags to display specific sections of the default menu, + // e.g. TableDrawDefaultContextMenu(table, table->Flags & ~ImGuiTableFlags_Hideable) will display everything EXCEPT columns visibility options. + if (table->DisableDefaultContextMenu == false && TableBeginContextMenuPopup(table)) + { + TableDrawDefaultContextMenu(table, table->Flags); + EndPopup(); + } + + // [Part 12] Sanitize and build sort specs before we have a chance to use them for display. + // This path will only be exercised when sort specs are modified before header rows (e.g. init or visibility change) + if (table->IsSortSpecsDirty && (table->Flags & ImGuiTableFlags_Sortable)) + TableSortSpecsBuild(table); + + // [Part 13] Setup inner window decoration size (for scrolling / nav tracking to properly take account of frozen rows/columns) + if (table->FreezeColumnsRequest > 0) + table->InnerWindow->DecoInnerSizeX1 = table->Columns[table->DisplayOrderToIndex[table->FreezeColumnsRequest - 1]].MaxX - table->OuterRect.Min.x; + if (table->FreezeRowsRequest > 0) + table->InnerWindow->DecoInnerSizeY1 = table_instance->LastFrozenHeight; + table_instance->LastFrozenHeight = 0.0f; + + // Initial state + ImGuiWindow* inner_window = table->InnerWindow; + if (table->Flags & ImGuiTableFlags_NoClip) + table->DrawSplitter->SetCurrentChannel(inner_window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP); + else + inner_window->DrawList->PushClipRect(inner_window->InnerClipRect.Min, inner_window->InnerClipRect.Max, false); // FIXME: use table->InnerClipRect? +} + +// Process hit-testing on resizing borders. Actual size change will be applied in EndTable() +// - Set table->HoveredColumnBorder with a short delay/timer to reduce visual feedback noise. +void ImGui::TableUpdateBorders(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(table->Flags & ImGuiTableFlags_Resizable); + + // At this point OuterRect height may be zero or under actual final height, so we rely on temporal coherency and + // use the final height from last frame. Because this is only affecting _interaction_ with columns, it is not + // really problematic (whereas the actual visual will be displayed in EndTable() and using the current frame height). + // Actual columns highlight/render will be performed in EndTable() and not be affected. + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + const float hit_half_width = ImTrunc(TABLE_RESIZE_SEPARATOR_HALF_THICKNESS * g.CurrentDpiScale); + const float hit_y1 = (table->FreezeRowsCount >= 1 ? table->OuterRect.Min.y : table->WorkRect.Min.y) + table->AngledHeadersHeight; + const float hit_y2_body = ImMax(table->OuterRect.Max.y, hit_y1 + table_instance->LastOuterHeight - table->AngledHeadersHeight); + const float hit_y2_head = hit_y1 + table_instance->LastTopHeadersRowHeight; + + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + continue; + + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->Flags & (ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoDirectResize_)) + continue; + + // ImGuiTableFlags_NoBordersInBodyUntilResize will be honored in TableDrawBorders() + const float border_y2_hit = (table->Flags & ImGuiTableFlags_NoBordersInBody) ? hit_y2_head : hit_y2_body; + if ((table->Flags & ImGuiTableFlags_NoBordersInBody) && table->IsUsingHeaders == false) + continue; + + if (!column->IsVisibleX && table->LastResizedColumn != column_n) + continue; + + ImGuiID column_id = TableGetColumnResizeID(table, column_n, table->InstanceCurrent); + ImRect hit_rect(column->MaxX - hit_half_width, hit_y1, column->MaxX + hit_half_width, border_y2_hit); + ItemAdd(hit_rect, column_id, NULL, ImGuiItemFlags_NoNav); + //GetForegroundDrawList()->AddRect(hit_rect.Min, hit_rect.Max, IM_COL32(255, 0, 0, 100)); + + bool hovered = false, held = false; + bool pressed = ButtonBehavior(hit_rect, column_id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnDoubleClick | ImGuiButtonFlags_NoNavFocus); + if (pressed && IsMouseDoubleClicked(0)) + { + TableSetColumnWidthAutoSingle(table, column_n); + ClearActiveID(); + held = false; + } + if (held) + { + if (table->LastResizedColumn == -1) + table->ResizeLockMinContentsX2 = table->RightMostEnabledColumn != -1 ? table->Columns[table->RightMostEnabledColumn].MaxX : -FLT_MAX; + table->ResizedColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + } + if ((hovered && g.HoveredIdTimer > TABLE_RESIZE_SEPARATOR_FEEDBACK_TIMER) || held) + { + table->HoveredColumnBorder = (ImGuiTableColumnIdx)column_n; + SetMouseCursor(ImGuiMouseCursor_ResizeEW); + } + } +} + +void ImGui::EndTable() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "EndTable() call should only be done while in BeginTable() scope!"); + return; + } + + // This assert would be very useful to catch a common error... unfortunately it would probably trigger in some + // cases, and for consistency user may sometimes output empty tables (and still benefit from e.g. outer border) + //IM_ASSERT(table->IsLayoutLocked && "Table unused: never called TableNextRow(), is that the intent?"); + + // If the user never got to call TableNextRow() or TableNextColumn(), we call layout ourselves to ensure all our + // code paths are consistent (instead of just hoping that TableBegin/TableEnd will work), get borders drawn, etc. + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + const ImGuiTableFlags flags = table->Flags; + ImGuiWindow* inner_window = table->InnerWindow; + ImGuiWindow* outer_window = table->OuterWindow; + ImGuiTableTempData* temp_data = table->TempData; + IM_ASSERT(inner_window == g.CurrentWindow); + IM_ASSERT(outer_window == inner_window || outer_window == inner_window->ParentWindow); + + if (table->IsInsideRow) + TableEndRow(table); + + // Context menu in columns body + if (flags & ImGuiTableFlags_ContextMenuInBody) + if (table->HoveredColumnBody != -1 && !IsAnyItemHovered() && IsMouseReleased(ImGuiMouseButton_Right)) + TableOpenContextMenu((int)table->HoveredColumnBody); + + // Finalize table height + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + inner_window->DC.PrevLineSize = temp_data->HostBackupPrevLineSize; + inner_window->DC.CurrLineSize = temp_data->HostBackupCurrLineSize; + inner_window->DC.CursorMaxPos = temp_data->HostBackupCursorMaxPos; + const float inner_content_max_y = table->RowPosY2; + IM_ASSERT(table->RowPosY2 == inner_window->DC.CursorPos.y); + if (inner_window != outer_window) + inner_window->DC.CursorMaxPos.y = inner_content_max_y; + else if (!(flags & ImGuiTableFlags_NoHostExtendY)) + table->OuterRect.Max.y = table->InnerRect.Max.y = ImMax(table->OuterRect.Max.y, inner_content_max_y); // Patch OuterRect/InnerRect height + table->WorkRect.Max.y = ImMax(table->WorkRect.Max.y, table->OuterRect.Max.y); + table_instance->LastOuterHeight = table->OuterRect.GetHeight(); + + // Setup inner scrolling range + // FIXME: This ideally should be done earlier, in BeginTable() SetNextWindowContentSize call, just like writing to inner_window->DC.CursorMaxPos.y, + // but since the later is likely to be impossible to do we'd rather update both axes together. + if (table->Flags & ImGuiTableFlags_ScrollX) + { + const float outer_padding_for_border = (table->Flags & ImGuiTableFlags_BordersOuterV) ? TABLE_BORDER_SIZE : 0.0f; + float max_pos_x = table->InnerWindow->DC.CursorMaxPos.x; + if (table->RightMostEnabledColumn != -1) + max_pos_x = ImMax(max_pos_x, table->Columns[table->RightMostEnabledColumn].WorkMaxX + table->CellPaddingX + table->OuterPaddingX - outer_padding_for_border); + if (table->ResizedColumn != -1) + max_pos_x = ImMax(max_pos_x, table->ResizeLockMinContentsX2); + table->InnerWindow->DC.CursorMaxPos.x = max_pos_x + table->TempData->AngledHeadersExtraWidth; + } + + // Pop clipping rect + if (!(flags & ImGuiTableFlags_NoClip)) + inner_window->DrawList->PopClipRect(); + inner_window->ClipRect = inner_window->DrawList->_ClipRectStack.back(); + + // Draw borders + if ((flags & ImGuiTableFlags_Borders) != 0) + TableDrawBorders(table); + +#if 0 + // Strip out dummy channel draw calls + // We have no way to prevent user submitting direct ImDrawList calls into a hidden column (but ImGui:: calls will be clipped out) + // Pros: remove draw calls which will have no effect. since they'll have zero-size cliprect they may be early out anyway. + // Cons: making it harder for users watching metrics/debugger to spot the wasted vertices. + if (table->DummyDrawChannel != (ImGuiTableColumnIdx)-1) + { + ImDrawChannel* dummy_channel = &table->DrawSplitter._Channels[table->DummyDrawChannel]; + dummy_channel->_CmdBuffer.resize(0); + dummy_channel->_IdxBuffer.resize(0); + } +#endif + + // Flatten channels and merge draw calls + ImDrawListSplitter* splitter = table->DrawSplitter; + splitter->SetCurrentChannel(inner_window->DrawList, 0); + if ((table->Flags & ImGuiTableFlags_NoClip) == 0) + TableMergeDrawChannels(table); + splitter->Merge(inner_window->DrawList); + + // Update ColumnsAutoFitWidth to get us ahead for host using our size to auto-resize without waiting for next BeginTable() + float auto_fit_width_for_fixed = 0.0f; + float auto_fit_width_for_stretched = 0.0f; + float auto_fit_width_for_stretched_min = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + float column_width_request = ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && !(column->Flags & ImGuiTableColumnFlags_NoResize)) ? column->WidthRequest : TableGetColumnWidthAuto(table, column); + if (column->Flags & ImGuiTableColumnFlags_WidthFixed) + auto_fit_width_for_fixed += column_width_request; + else + auto_fit_width_for_stretched += column_width_request; + if ((column->Flags & ImGuiTableColumnFlags_WidthStretch) && (column->Flags & ImGuiTableColumnFlags_NoResize) != 0) + auto_fit_width_for_stretched_min = ImMax(auto_fit_width_for_stretched_min, column_width_request / (column->StretchWeight / table->ColumnsStretchSumWeights)); + } + const float width_spacings = (table->OuterPaddingX * 2.0f) + (table->CellSpacingX1 + table->CellSpacingX2) * (table->ColumnsEnabledCount - 1); + table->ColumnsAutoFitWidth = width_spacings + (table->CellPaddingX * 2.0f) * table->ColumnsEnabledCount + auto_fit_width_for_fixed + ImMax(auto_fit_width_for_stretched, auto_fit_width_for_stretched_min); + + // Update scroll + if ((table->Flags & ImGuiTableFlags_ScrollX) == 0 && inner_window != outer_window) + { + inner_window->Scroll.x = 0.0f; + } + else if (table->LastResizedColumn != -1 && table->ResizedColumn == -1 && inner_window->ScrollbarX && table->InstanceInteracted == table->InstanceCurrent) + { + // When releasing a column being resized, scroll to keep the resulting column in sight + const float neighbor_width_to_keep_visible = table->MinColumnWidth + table->CellPaddingX * 2.0f; + ImGuiTableColumn* column = &table->Columns[table->LastResizedColumn]; + if (column->MaxX < table->InnerClipRect.Min.x) + SetScrollFromPosX(inner_window, column->MaxX - inner_window->Pos.x - neighbor_width_to_keep_visible, 1.0f); + else if (column->MaxX > table->InnerClipRect.Max.x) + SetScrollFromPosX(inner_window, column->MaxX - inner_window->Pos.x + neighbor_width_to_keep_visible, 1.0f); + } + + // Apply resizing/dragging at the end of the frame + if (table->ResizedColumn != -1 && table->InstanceCurrent == table->InstanceInteracted) + { + ImGuiTableColumn* column = &table->Columns[table->ResizedColumn]; + const float new_x2 = (g.IO.MousePos.x - g.ActiveIdClickOffset.x + ImTrunc(TABLE_RESIZE_SEPARATOR_HALF_THICKNESS * g.CurrentDpiScale)); + const float new_width = ImTrunc(new_x2 - column->MinX - table->CellSpacingX1 - table->CellPaddingX * 2.0f); + table->ResizedColumnNextWidth = new_width; + } + + table->IsActiveIdInTable = (g.ActiveIdIsAlive != 0 && table->IsActiveIdAliveBeforeTable == false); + + // Pop from id stack + IM_ASSERT_USER_ERROR(inner_window->IDStack.back() == table_instance->TableInstanceID, "Mismatching PushID/PopID!"); + IM_ASSERT_USER_ERROR(outer_window->DC.ItemWidthStack.Size >= temp_data->HostBackupItemWidthStackSize, "Too many PopItemWidth!"); + if (table->InstanceCurrent > 0) + PopID(); + PopID(); + + // Restore window data that we modified + const ImVec2 backup_outer_max_pos = outer_window->DC.CursorMaxPos; + inner_window->WorkRect = temp_data->HostBackupWorkRect; + inner_window->ParentWorkRect = temp_data->HostBackupParentWorkRect; + inner_window->SkipItems = table->HostSkipItems; + outer_window->DC.CursorPos = table->OuterRect.Min; + outer_window->DC.ItemWidth = temp_data->HostBackupItemWidth; + outer_window->DC.ItemWidthStack.Size = temp_data->HostBackupItemWidthStackSize; + outer_window->DC.ColumnsOffset = temp_data->HostBackupColumnsOffset; + + // Layout in outer window + // (FIXME: To allow auto-fit and allow desirable effect of SameLine() we dissociate 'used' vs 'ideal' size by overriding + // CursorPosPrevLine and CursorMaxPos manually. That should be a more general layout feature, see same problem e.g. #3414) + if (inner_window != outer_window) + { + short backup_nav_layers_active_mask = inner_window->DC.NavLayersActiveMask; + inner_window->DC.NavLayersActiveMask |= 1 << table->NavLayer; // So empty table don't appear to navigate differently. + g.CurrentTable = NULL; // To avoid error recovery recursing + EndChild(); + g.CurrentTable = table; + inner_window->DC.NavLayersActiveMask = backup_nav_layers_active_mask; + } + else + { + ItemSize(table->OuterRect.GetSize()); + ItemAdd(table->OuterRect, 0); + } + + // Override declared contents width/height to enable auto-resize while not needlessly adding a scrollbar + if (table->Flags & ImGuiTableFlags_NoHostExtendX) + { + // FIXME-TABLE: Could we remove this section? + // ColumnsAutoFitWidth may be one frame ahead here since for Fixed+NoResize is calculated from latest contents + IM_ASSERT((table->Flags & ImGuiTableFlags_ScrollX) == 0); + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, table->OuterRect.Min.x + table->ColumnsAutoFitWidth); + } + else if (temp_data->UserOuterSize.x <= 0.0f) + { + // Some references for this: #7651 + tests "table_reported_size", "table_reported_size_outer" equivalent Y block + // - Checking for ImGuiTableFlags_ScrollX/ScrollY flag makes us a frame ahead when disabling those flags. + // - FIXME-TABLE: Would make sense to pre-compute expected scrollbar visibility/sizes to generally save a frame of feedback. + const float inner_content_max_x = table->OuterRect.Min.x + table->ColumnsAutoFitWidth; // Slightly misleading name but used for code symmetry with inner_content_max_y + const float decoration_size = table->TempData->AngledHeadersExtraWidth + ((table->Flags & ImGuiTableFlags_ScrollY) ? inner_window->ScrollbarSizes.x : 0.0f); + outer_window->DC.IdealMaxPos.x = ImMax(outer_window->DC.IdealMaxPos.x, inner_content_max_x + decoration_size - temp_data->UserOuterSize.x); + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, ImMin(table->OuterRect.Max.x, inner_content_max_x + decoration_size)); + } + else + { + outer_window->DC.CursorMaxPos.x = ImMax(backup_outer_max_pos.x, table->OuterRect.Max.x); + } + if (temp_data->UserOuterSize.y <= 0.0f) + { + const float decoration_size = (table->Flags & ImGuiTableFlags_ScrollX) ? inner_window->ScrollbarSizes.y : 0.0f; + outer_window->DC.IdealMaxPos.y = ImMax(outer_window->DC.IdealMaxPos.y, inner_content_max_y + decoration_size - temp_data->UserOuterSize.y); + outer_window->DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, ImMin(table->OuterRect.Max.y, inner_content_max_y + decoration_size)); + } + else + { + // OuterRect.Max.y may already have been pushed downward from the initial value (unless ImGuiTableFlags_NoHostExtendY is set) + outer_window->DC.CursorMaxPos.y = ImMax(backup_outer_max_pos.y, table->OuterRect.Max.y); + } + + // Save settings + if (table->IsSettingsDirty) + TableSaveSettings(table); + table->IsInitializing = false; + + // Clear or restore current table, if any + IM_ASSERT(g.CurrentWindow == outer_window && g.CurrentTable == table); + IM_ASSERT(g.TablesTempDataStacked > 0); + temp_data = (--g.TablesTempDataStacked > 0) ? &g.TablesTempData[g.TablesTempDataStacked - 1] : NULL; + g.CurrentTable = temp_data ? g.Tables.GetByIndex(temp_data->TableIndex) : NULL; + if (g.CurrentTable) + { + g.CurrentTable->TempData = temp_data; + g.CurrentTable->DrawSplitter = &temp_data->DrawSplitter; + } + outer_window->DC.CurrentTableIdx = g.CurrentTable ? g.Tables.GetIndex(g.CurrentTable) : -1; + NavUpdateCurrentWindowIsScrollPushableX(); +} + +// Called in TableSetupColumn() when initializing and in TableLoadSettings() for defaults before applying stored settings. +// 'init_mask' specify which fields to initialize. +static void TableInitColumnDefaults(ImGuiTable* table, ImGuiTableColumn* column, ImGuiTableColumnFlags init_mask) +{ + ImGuiTableColumnFlags flags = column->Flags; + if (init_mask & ImGuiTableFlags_Resizable) + { + float init_width_or_weight = column->InitStretchWeightOrWidth; + column->WidthRequest = ((flags & ImGuiTableColumnFlags_WidthFixed) && init_width_or_weight > 0.0f) ? init_width_or_weight : -1.0f; + column->StretchWeight = (init_width_or_weight > 0.0f && (flags & ImGuiTableColumnFlags_WidthStretch)) ? init_width_or_weight : -1.0f; + if (init_width_or_weight > 0.0f) // Disable auto-fit if an explicit width/weight has been specified + column->AutoFitQueue = 0x00; + } + if (init_mask & ImGuiTableFlags_Reorderable) + column->DisplayOrder = (ImGuiTableColumnIdx)table->Columns.index_from_ptr(column); + if (init_mask & ImGuiTableFlags_Hideable) + column->IsUserEnabled = column->IsUserEnabledNextFrame = (flags & ImGuiTableColumnFlags_DefaultHide) ? 0 : 1; + if (init_mask & ImGuiTableFlags_Sortable) + { + // Multiple columns using _DefaultSort will be reassigned unique SortOrder values when building the sort specs. + column->SortOrder = (flags & ImGuiTableColumnFlags_DefaultSort) ? 0 : -1; + column->SortDirection = (flags & ImGuiTableColumnFlags_DefaultSort) ? ((flags & ImGuiTableColumnFlags_PreferSortDescending) ? (ImS8)ImGuiSortDirection_Descending : (ImU8)(ImGuiSortDirection_Ascending)) : (ImS8)ImGuiSortDirection_None; + } +} + +// See "COLUMNS SIZING POLICIES" comments at the top of this file +// If (init_width_or_weight <= 0.0f) it is ignored +void ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags, float init_width_or_weight, ImGuiID user_id) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + IM_ASSERT(table->IsLayoutLocked == false && "Need to call TableSetupColumn() before first row!"); + IM_ASSERT((flags & ImGuiTableColumnFlags_StatusMask_) == 0 && "Illegal to pass StatusMask values to TableSetupColumn()"); + if (table->DeclColumnsCount >= table->ColumnsCount) + { + IM_ASSERT_USER_ERROR(table->DeclColumnsCount < table->ColumnsCount, "Called TableSetupColumn() too many times!"); + return; + } + + ImGuiTableColumn* column = &table->Columns[table->DeclColumnsCount]; + table->DeclColumnsCount++; + + // Assert when passing a width or weight if policy is entirely left to default, to avoid storing width into weight and vice-versa. + // Give a grace to users of ImGuiTableFlags_ScrollX. + if (table->IsDefaultSizingPolicy && (flags & ImGuiTableColumnFlags_WidthMask_) == 0 && (flags & ImGuiTableFlags_ScrollX) == 0) + IM_ASSERT(init_width_or_weight <= 0.0f && "Can only specify width/weight if sizing policy is set explicitly in either Table or Column."); + + // When passing a width automatically enforce WidthFixed policy + // (whereas TableSetupColumnFlags would default to WidthAuto if table is not resizable) + if ((flags & ImGuiTableColumnFlags_WidthMask_) == 0 && init_width_or_weight > 0.0f) + if ((table->Flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedFit || (table->Flags & ImGuiTableFlags_SizingMask_) == ImGuiTableFlags_SizingFixedSame) + flags |= ImGuiTableColumnFlags_WidthFixed; + if (flags & ImGuiTableColumnFlags_AngledHeader) + { + flags |= ImGuiTableColumnFlags_NoHeaderLabel; + table->AngledHeadersCount++; + } + + TableSetupColumnFlags(table, column, flags); + column->UserID = user_id; + flags = column->Flags; + + // Initialize defaults + column->InitStretchWeightOrWidth = init_width_or_weight; + if (table->IsInitializing) + { + ImGuiTableFlags init_flags = ~table->SettingsLoadedFlags; + if (column->WidthRequest < 0.0f && column->StretchWeight < 0.0f) + init_flags |= ImGuiTableFlags_Resizable; + TableInitColumnDefaults(table, column, init_flags); + } + + // Store name (append with zero-terminator in contiguous buffer) + // FIXME: If we recorded the number of \n in names we could compute header row height + column->NameOffset = -1; + if (label != NULL && label[0] != 0) + { + column->NameOffset = (ImS16)table->ColumnsNames.size(); + table->ColumnsNames.append(label, label + ImStrlen(label) + 1); + } +} + +// [Public] +void ImGui::TableSetupScrollFreeze(int columns, int rows) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + IM_ASSERT(table->IsLayoutLocked == false && "Need to call TableSetupColumn() before first row!"); + IM_ASSERT(columns >= 0 && columns < IMGUI_TABLE_MAX_COLUMNS); + IM_ASSERT(rows >= 0 && rows < 128); // Arbitrary limit + + table->FreezeColumnsRequest = (table->Flags & ImGuiTableFlags_ScrollX) ? (ImGuiTableColumnIdx)ImMin(columns, table->ColumnsCount) : 0; + table->FreezeColumnsCount = (table->InnerWindow->Scroll.x != 0.0f) ? table->FreezeColumnsRequest : 0; + table->FreezeRowsRequest = (table->Flags & ImGuiTableFlags_ScrollY) ? (ImGuiTableColumnIdx)rows : 0; + table->FreezeRowsCount = (table->InnerWindow->Scroll.y != 0.0f) ? table->FreezeRowsRequest : 0; + table->IsUnfrozenRows = (table->FreezeRowsCount == 0); // Make sure this is set before TableUpdateLayout() so ImGuiListClipper can benefit from it.b + + // Ensure frozen columns are ordered in their section. We still allow multiple frozen columns to be reordered. + // FIXME-TABLE: This work for preserving 2143 into 21|43. How about 4321 turning into 21|43? (preserve relative order in each section) + for (int column_n = 0; column_n < table->FreezeColumnsRequest; column_n++) + { + int order_n = table->DisplayOrderToIndex[column_n]; + if (order_n != column_n && order_n >= table->FreezeColumnsRequest) + { + ImSwap(table->Columns[table->DisplayOrderToIndex[order_n]].DisplayOrder, table->Columns[table->DisplayOrderToIndex[column_n]].DisplayOrder); + ImSwap(table->DisplayOrderToIndex[order_n], table->DisplayOrderToIndex[column_n]); + } + } +} + +//----------------------------------------------------------------------------- +// [SECTION] Tables: Simple accessors +//----------------------------------------------------------------------------- +// - TableGetColumnCount() +// - TableGetColumnName() +// - TableGetColumnName() [Internal] +// - TableSetColumnEnabled() +// - TableGetColumnFlags() +// - TableGetCellBgRect() [Internal] +// - TableGetColumnResizeID() [Internal] +// - TableGetHoveredColumn() [Internal] +// - TableGetHoveredRow() [Internal] +// - TableSetBgColor() +//----------------------------------------------------------------------------- + +int ImGui::TableGetColumnCount() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + return table ? table->ColumnsCount : 0; +} + +const char* ImGui::TableGetColumnName(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return NULL; + if (column_n < 0) + column_n = table->CurrentColumn; + return TableGetColumnName(table, column_n); +} + +const char* ImGui::TableGetColumnName(const ImGuiTable* table, int column_n) +{ + if (table->IsLayoutLocked == false && column_n >= table->DeclColumnsCount) + return ""; // NameOffset is invalid at this point + const ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->NameOffset == -1) + return ""; + return &table->ColumnsNames.Buf[column->NameOffset]; +} + +// Change user accessible enabled/disabled state of a column (often perceived as "showing/hiding" from users point of view) +// Note that end-user can use the context menu to change this themselves (right-click in headers, or right-click in columns body with ImGuiTableFlags_ContextMenuInBody) +// - Require table to have the ImGuiTableFlags_Hideable flag because we are manipulating user accessible state. +// - Request will be applied during next layout, which happens on the first call to TableNextRow() after BeginTable(). +// - For the getter you can test (TableGetColumnFlags() & ImGuiTableColumnFlags_IsEnabled) != 0. +// - Alternative: the ImGuiTableColumnFlags_Disabled is an overriding/master disable flag which will also hide the column from context menu. +void ImGui::TableSetColumnEnabled(int column_n, bool enabled) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + IM_ASSERT(table->Flags & ImGuiTableFlags_Hideable); // See comments above + if (column_n < 0) + column_n = table->CurrentColumn; + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiTableColumn* column = &table->Columns[column_n]; + column->IsUserEnabledNextFrame = enabled; +} + +// We allow querying for an extra column in order to poll the IsHovered state of the right-most section +ImGuiTableColumnFlags ImGui::TableGetColumnFlags(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return ImGuiTableColumnFlags_None; + if (column_n < 0) + column_n = table->CurrentColumn; + if (column_n == table->ColumnsCount) + return (table->HoveredColumnBody == column_n) ? ImGuiTableColumnFlags_IsHovered : ImGuiTableColumnFlags_None; + return table->Columns[column_n].Flags; +} + +// Return the cell rectangle based on currently known height. +// - Important: we generally don't know our row height until the end of the row, so Max.y will be incorrect in many situations. +// The only case where this is correct is if we provided a min_row_height to TableNextRow() and don't go below it, or in TableEndRow() when we locked that height. +// - Important: if ImGuiTableFlags_PadOuterX is set but ImGuiTableFlags_PadInnerX is not set, the outer-most left and right +// columns report a small offset so their CellBgRect can extend up to the outer border. +// FIXME: But the rendering code in TableEndRow() nullifies that with clamping required for scrolling. +ImRect ImGui::TableGetCellBgRect(const ImGuiTable* table, int column_n) +{ + const ImGuiTableColumn* column = &table->Columns[column_n]; + float x1 = column->MinX; + float x2 = column->MaxX; + //if (column->PrevEnabledColumn == -1) + // x1 -= table->OuterPaddingX; + //if (column->NextEnabledColumn == -1) + // x2 += table->OuterPaddingX; + x1 = ImMax(x1, table->WorkRect.Min.x); + x2 = ImMin(x2, table->WorkRect.Max.x); + return ImRect(x1, table->RowPosY1, x2, table->RowPosY2); +} + +// Return the resizing ID for the right-side of the given column. +ImGuiID ImGui::TableGetColumnResizeID(ImGuiTable* table, int column_n, int instance_no) +{ + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiID instance_id = TableGetInstanceID(table, instance_no); + return instance_id + 1 + column_n; // FIXME: #6140: still not ideal +} + +// Return -1 when table is not hovered. return columns_count if hovering the unused space at the right of the right-most visible column. +int ImGui::TableGetHoveredColumn() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return -1; + return (int)table->HoveredColumnBody; +} + +// Return -1 when table is not hovered. Return maxrow+1 if in table but below last submitted row. +// *IMPORTANT* Unlike TableGetHoveredColumn(), this has a one frame latency in updating the value. +// This difference with is the reason why this is not public yet. +int ImGui::TableGetHoveredRow() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return -1; + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + return (int)table_instance->HoveredRowLast; +} + +void ImGui::TableSetBgColor(ImGuiTableBgTarget target, ImU32 color, int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(target != ImGuiTableBgTarget_None); + + if (color == IM_COL32_DISABLE) + color = 0; + + // We cannot draw neither the cell or row background immediately as we don't know the row height at this point in time. + switch (target) + { + case ImGuiTableBgTarget_CellBg: + { + if (table->RowPosY1 > table->InnerClipRect.Max.y) // Discard + return; + if (column_n == -1) + column_n = table->CurrentColumn; + if (!IM_BITARRAY_TESTBIT(table->VisibleMaskByIndex, column_n)) + return; + if (table->RowCellDataCurrent < 0 || table->RowCellData[table->RowCellDataCurrent].Column != column_n) + table->RowCellDataCurrent++; + ImGuiTableCellData* cell_data = &table->RowCellData[table->RowCellDataCurrent]; + cell_data->BgColor = color; + cell_data->Column = (ImGuiTableColumnIdx)column_n; + break; + } + case ImGuiTableBgTarget_RowBg0: + case ImGuiTableBgTarget_RowBg1: + { + if (table->RowPosY1 > table->InnerClipRect.Max.y) // Discard + return; + IM_ASSERT(column_n == -1); + int bg_idx = (target == ImGuiTableBgTarget_RowBg1) ? 1 : 0; + table->RowBgColor[bg_idx] = color; + break; + } + default: + IM_ASSERT(0); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Row changes +//------------------------------------------------------------------------- +// - TableGetRowIndex() +// - TableNextRow() +// - TableBeginRow() [Internal] +// - TableEndRow() [Internal] +//------------------------------------------------------------------------- + +// [Public] Note: for row coloring we use ->RowBgColorCounter which is the same value without counting header rows +int ImGui::TableGetRowIndex() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return 0; + return table->CurrentRow; +} + +// [Public] Starts into the first cell of a new row +void ImGui::TableNextRow(ImGuiTableRowFlags row_flags, float row_min_height) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + if (table->IsInsideRow) + TableEndRow(table); + + table->LastRowFlags = table->RowFlags; + table->RowFlags = row_flags; + table->RowCellPaddingY = g.Style.CellPadding.y; + table->RowMinHeight = row_min_height; + TableBeginRow(table); + + // We honor min_row_height requested by user, but cannot guarantee per-row maximum height, + // because that would essentially require a unique clipping rectangle per-cell. + table->RowPosY2 += table->RowCellPaddingY * 2.0f; + table->RowPosY2 = ImMax(table->RowPosY2, table->RowPosY1 + row_min_height); + + // Disable output until user calls TableNextColumn() + table->InnerWindow->SkipItems = true; +} + +// [Internal] Only called by TableNextRow() +void ImGui::TableBeginRow(ImGuiTable* table) +{ + ImGuiWindow* window = table->InnerWindow; + IM_ASSERT(!table->IsInsideRow); + + // New row + table->CurrentRow++; + table->CurrentColumn = -1; + table->RowBgColor[0] = table->RowBgColor[1] = IM_COL32_DISABLE; + table->RowCellDataCurrent = -1; + table->IsInsideRow = true; + + // Begin frozen rows + float next_y1 = table->RowPosY2; + if (table->CurrentRow == 0 && table->FreezeRowsCount > 0) + next_y1 = window->DC.CursorPos.y = table->OuterRect.Min.y; + + table->RowPosY1 = table->RowPosY2 = next_y1; + table->RowTextBaseline = 0.0f; + table->RowIndentOffsetX = window->DC.Indent.x - table->HostIndentX; // Lock indent + + window->DC.PrevLineTextBaseOffset = 0.0f; + window->DC.CursorPosPrevLine = ImVec2(window->DC.CursorPos.x, window->DC.CursorPos.y + table->RowCellPaddingY); // This allows users to call SameLine() to share LineSize between columns. + window->DC.PrevLineSize = window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); // This allows users to call SameLine() to share LineSize between columns, and to call it from first column too. + window->DC.IsSameLine = window->DC.IsSetPos = false; + window->DC.CursorMaxPos.y = next_y1; + + // Making the header BG color non-transparent will allow us to overlay it multiple times when handling smooth dragging. + if (table->RowFlags & ImGuiTableRowFlags_Headers) + { + TableSetBgColor(ImGuiTableBgTarget_RowBg0, GetColorU32(ImGuiCol_TableHeaderBg)); + if (table->CurrentRow == 0) + table->IsUsingHeaders = true; + } +} + +// [Internal] Called by TableNextRow() +void ImGui::TableEndRow(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(window == table->InnerWindow); + IM_ASSERT(table->IsInsideRow); + + if (table->CurrentColumn != -1) + TableEndCell(table); + + // Logging + if (g.LogEnabled) + LogRenderedText(NULL, "|"); + + // Position cursor at the bottom of our row so it can be used for e.g. clipping calculation. However it is + // likely that the next call to TableBeginCell() will reposition the cursor to take account of vertical padding. + window->DC.CursorPos.y = table->RowPosY2; + + // Row background fill + const float bg_y1 = table->RowPosY1; + const float bg_y2 = table->RowPosY2; + const bool unfreeze_rows_actual = (table->CurrentRow + 1 == table->FreezeRowsCount); + const bool unfreeze_rows_request = (table->CurrentRow + 1 == table->FreezeRowsRequest); + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + if ((table->RowFlags & ImGuiTableRowFlags_Headers) && (table->CurrentRow == 0 || (table->LastRowFlags & ImGuiTableRowFlags_Headers))) + table_instance->LastTopHeadersRowHeight += bg_y2 - bg_y1; + + const bool is_visible = (bg_y2 >= table->InnerClipRect.Min.y && bg_y1 <= table->InnerClipRect.Max.y); + if (is_visible) + { + // Update data for TableGetHoveredRow() + if (table->HoveredColumnBody != -1 && g.IO.MousePos.y >= bg_y1 && g.IO.MousePos.y < bg_y2 && table_instance->HoveredRowNext < 0) + table_instance->HoveredRowNext = table->CurrentRow; + + // Decide of background color for the row + ImU32 bg_col0 = 0; + ImU32 bg_col1 = 0; + if (table->RowBgColor[0] != IM_COL32_DISABLE) + bg_col0 = table->RowBgColor[0]; + else if (table->Flags & ImGuiTableFlags_RowBg) + bg_col0 = GetColorU32((table->RowBgColorCounter & 1) ? ImGuiCol_TableRowBgAlt : ImGuiCol_TableRowBg); + if (table->RowBgColor[1] != IM_COL32_DISABLE) + bg_col1 = table->RowBgColor[1]; + + // Decide of top border color + ImU32 top_border_col = 0; + const float border_size = TABLE_BORDER_SIZE; + if (table->CurrentRow > 0 && (table->Flags & ImGuiTableFlags_BordersInnerH)) + top_border_col = (table->LastRowFlags & ImGuiTableRowFlags_Headers) ? table->BorderColorStrong : table->BorderColorLight; + + const bool draw_cell_bg_color = table->RowCellDataCurrent >= 0; + const bool draw_strong_bottom_border = unfreeze_rows_actual; + if ((bg_col0 | bg_col1 | top_border_col) != 0 || draw_strong_bottom_border || draw_cell_bg_color) + { + // In theory we could call SetWindowClipRectBeforeSetChannel() but since we know TableEndRow() is + // always followed by a change of clipping rectangle we perform the smallest overwrite possible here. + if ((table->Flags & ImGuiTableFlags_NoClip) == 0) + window->DrawList->_CmdHeader.ClipRect = table->Bg0ClipRectForDrawCmd.ToVec4(); + table->DrawSplitter->SetCurrentChannel(window->DrawList, TABLE_DRAW_CHANNEL_BG0); + } + + // Draw row background + // We soft/cpu clip this so all backgrounds and borders can share the same clipping rectangle + if (bg_col0 || bg_col1) + { + ImRect row_rect(table->WorkRect.Min.x, bg_y1, table->WorkRect.Max.x, bg_y2); + row_rect.ClipWith(table->BgClipRect); + if (bg_col0 != 0 && row_rect.Min.y < row_rect.Max.y) + window->DrawList->AddRectFilled(row_rect.Min, row_rect.Max, bg_col0); + if (bg_col1 != 0 && row_rect.Min.y < row_rect.Max.y) + window->DrawList->AddRectFilled(row_rect.Min, row_rect.Max, bg_col1); + } + + // Draw cell background color + if (draw_cell_bg_color) + { + ImGuiTableCellData* cell_data_end = &table->RowCellData[table->RowCellDataCurrent]; + for (ImGuiTableCellData* cell_data = &table->RowCellData[0]; cell_data <= cell_data_end; cell_data++) + { + // As we render the BG here we need to clip things (for layout we would not) + // FIXME: This cancels the OuterPadding addition done by TableGetCellBgRect(), need to keep it while rendering correctly while scrolling. + const ImGuiTableColumn* column = &table->Columns[cell_data->Column]; + ImRect cell_bg_rect = TableGetCellBgRect(table, cell_data->Column); + cell_bg_rect.ClipWith(table->BgClipRect); + cell_bg_rect.Min.x = ImMax(cell_bg_rect.Min.x, column->ClipRect.Min.x); // So that first column after frozen one gets clipped when scrolling + cell_bg_rect.Max.x = ImMin(cell_bg_rect.Max.x, column->MaxX); + if (cell_bg_rect.Min.y < cell_bg_rect.Max.y) + window->DrawList->AddRectFilled(cell_bg_rect.Min, cell_bg_rect.Max, cell_data->BgColor); + } + } + + // Draw top border + if (top_border_col && bg_y1 >= table->BgClipRect.Min.y && bg_y1 < table->BgClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y1), ImVec2(table->BorderX2, bg_y1), top_border_col, border_size); + + // Draw bottom border at the row unfreezing mark (always strong) + if (draw_strong_bottom_border && bg_y2 >= table->BgClipRect.Min.y && bg_y2 < table->BgClipRect.Max.y) + window->DrawList->AddLine(ImVec2(table->BorderX1, bg_y2), ImVec2(table->BorderX2, bg_y2), table->BorderColorStrong, border_size); + } + + // End frozen rows (when we are past the last frozen row line, teleport cursor and alter clipping rectangle) + // We need to do that in TableEndRow() instead of TableBeginRow() so the list clipper can mark end of row and + // get the new cursor position. + if (unfreeze_rows_request) + { + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->Columns[column_n].NavLayerCurrent = table->NavLayer; + const float y0 = ImMax(table->RowPosY2 + 1, table->InnerClipRect.Min.y); + table_instance->LastFrozenHeight = y0 - table->OuterRect.Min.y; + + if (unfreeze_rows_actual) + { + IM_ASSERT(table->IsUnfrozenRows == false); + table->IsUnfrozenRows = true; + + // BgClipRect starts as table->InnerClipRect, reduce it now and make BgClipRectForDrawCmd == BgClipRect + table->BgClipRect.Min.y = table->Bg2ClipRectForDrawCmd.Min.y = ImMin(y0, table->InnerClipRect.Max.y); + table->BgClipRect.Max.y = table->Bg2ClipRectForDrawCmd.Max.y = table->InnerClipRect.Max.y; + table->Bg2DrawChannelCurrent = table->Bg2DrawChannelUnfrozen; + IM_ASSERT(table->Bg2ClipRectForDrawCmd.Min.y <= table->Bg2ClipRectForDrawCmd.Max.y); + + float row_height = table->RowPosY2 - table->RowPosY1; + table->RowPosY2 = window->DC.CursorPos.y = table->WorkRect.Min.y + table->RowPosY2 - table->OuterRect.Min.y; + table->RowPosY1 = table->RowPosY2 - row_height; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + column->DrawChannelCurrent = column->DrawChannelUnfrozen; + column->ClipRect.Min.y = table->Bg2ClipRectForDrawCmd.Min.y; + } + + // Update cliprect ahead of TableBeginCell() so clipper can access to new ClipRect->Min.y + SetWindowClipRectBeforeSetChannel(window, table->Columns[0].ClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, table->Columns[0].DrawChannelCurrent); + } + } + + if (!(table->RowFlags & ImGuiTableRowFlags_Headers)) + table->RowBgColorCounter++; + table->IsInsideRow = false; +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Columns changes +//------------------------------------------------------------------------- +// - TableGetColumnIndex() +// - TableSetColumnIndex() +// - TableNextColumn() +// - TableBeginCell() [Internal] +// - TableEndCell() [Internal] +//------------------------------------------------------------------------- + +int ImGui::TableGetColumnIndex() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return 0; + return table->CurrentColumn; +} + +// [Public] Append into a specific column +bool ImGui::TableSetColumnIndex(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + + if (table->CurrentColumn != column_n) + { + if (table->CurrentColumn != -1) + TableEndCell(table); + if ((column_n >= 0 && column_n < table->ColumnsCount) == false) + { + IM_ASSERT_USER_ERROR(column_n >= 0 && column_n < table->ColumnsCount, "TableSetColumnIndex() invalid column index!"); + return false; + } + TableBeginCell(table, column_n); + } + + // Return whether the column is visible. User may choose to skip submitting items based on this return value, + // however they shouldn't skip submitting for columns that may have the tallest contribution to row height. + return table->Columns[column_n].IsRequestOutput; +} + +// [Public] Append into the next column, wrap and create a new row when already on last column +bool ImGui::TableNextColumn() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (!table) + return false; + + if (table->IsInsideRow && table->CurrentColumn + 1 < table->ColumnsCount) + { + if (table->CurrentColumn != -1) + TableEndCell(table); + TableBeginCell(table, table->CurrentColumn + 1); + } + else + { + TableNextRow(); + TableBeginCell(table, 0); + } + + // Return whether the column is visible. User may choose to skip submitting items based on this return value, + // however they shouldn't skip submitting for columns that may have the tallest contribution to row height. + return table->Columns[table->CurrentColumn].IsRequestOutput; +} + + +// [Internal] Called by TableSetColumnIndex()/TableNextColumn() +// This is called very frequently, so we need to be mindful of unnecessary overhead. +// FIXME-TABLE FIXME-OPT: Could probably shortcut some things for non-active or clipped columns. +void ImGui::TableBeginCell(ImGuiTable* table, int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTableColumn* column = &table->Columns[column_n]; + ImGuiWindow* window = table->InnerWindow; + table->CurrentColumn = column_n; + + // Start position is roughly ~~ CellRect.Min + CellPadding + Indent + float start_x = column->WorkMinX; + if (column->Flags & ImGuiTableColumnFlags_IndentEnable) + start_x += table->RowIndentOffsetX; // ~~ += window.DC.Indent.x - table->HostIndentX, except we locked it for the row. + + window->DC.CursorPos.x = start_x; + window->DC.CursorPos.y = table->RowPosY1 + table->RowCellPaddingY; + window->DC.CursorMaxPos.x = window->DC.CursorPos.x; + window->DC.ColumnsOffset.x = start_x - window->Pos.x - window->DC.Indent.x; // FIXME-WORKRECT + window->DC.CursorPosPrevLine.x = window->DC.CursorPos.x; // PrevLine.y is preserved. This allows users to call SameLine() to share LineSize between columns. + window->DC.CurrLineTextBaseOffset = table->RowTextBaseline; + window->DC.NavLayerCurrent = (ImGuiNavLayer)column->NavLayerCurrent; + + // Note how WorkRect.Max.y is only set once during layout + window->WorkRect.Min.y = window->DC.CursorPos.y; + window->WorkRect.Min.x = column->WorkMinX; + window->WorkRect.Max.x = column->WorkMaxX; + window->DC.ItemWidth = column->ItemWidth; + + window->SkipItems = column->IsSkipItems; + if (column->IsSkipItems) + { + g.LastItemData.ID = 0; + g.LastItemData.StatusFlags = 0; + } + + if (table->Flags & ImGuiTableFlags_NoClip) + { + // FIXME: if we end up drawing all borders/bg in EndTable, could remove this and just assert that channel hasn't changed. + table->DrawSplitter->SetCurrentChannel(window->DrawList, TABLE_DRAW_CHANNEL_NOCLIP); + //IM_ASSERT(table->DrawSplitter._Current == TABLE_DRAW_CHANNEL_NOCLIP); + } + else + { + // FIXME-TABLE: Could avoid this if draw channel is dummy channel? + SetWindowClipRectBeforeSetChannel(window, column->ClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent); + } + + // Logging + if (g.LogEnabled && !column->IsSkipItems) + { + LogRenderedText(&window->DC.CursorPos, "|"); + g.LogLinePosY = FLT_MAX; + } +} + +// [Internal] Called by TableNextRow()/TableSetColumnIndex()/TableNextColumn() +void ImGui::TableEndCell(ImGuiTable* table) +{ + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + ImGuiWindow* window = table->InnerWindow; + + if (window->DC.IsSetPos) + ErrorCheckUsingSetCursorPosToExtendParentBoundaries(); + + // Report maximum position so we can infer content size per column. + float* p_max_pos_x; + if (table->RowFlags & ImGuiTableRowFlags_Headers) + p_max_pos_x = &column->ContentMaxXHeadersUsed; // Useful in case user submit contents in header row that is not a TableHeader() call + else + p_max_pos_x = table->IsUnfrozenRows ? &column->ContentMaxXUnfrozen : &column->ContentMaxXFrozen; + *p_max_pos_x = ImMax(*p_max_pos_x, window->DC.CursorMaxPos.x); + if (column->IsEnabled) + table->RowPosY2 = ImMax(table->RowPosY2, window->DC.CursorMaxPos.y + table->RowCellPaddingY); + column->ItemWidth = window->DC.ItemWidth; + + // Propagate text baseline for the entire row + // FIXME-TABLE: Here we propagate text baseline from the last line of the cell.. instead of the first one. + table->RowTextBaseline = ImMax(table->RowTextBaseline, window->DC.PrevLineTextBaseOffset); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Columns width management +//------------------------------------------------------------------------- +// - TableGetMaxColumnWidth() [Internal] +// - TableGetColumnWidthAuto() [Internal] +// - TableSetColumnWidth() +// - TableSetColumnWidthAutoSingle() [Internal] +// - TableSetColumnWidthAutoAll() [Internal] +// - TableUpdateColumnsWeightFromWidth() [Internal] +//------------------------------------------------------------------------- +// Note that actual columns widths are computed in TableUpdateLayout(). +//------------------------------------------------------------------------- + +// Maximum column content width given current layout. Use column->MinX so this value differs on a per-column basis. +float ImGui::TableCalcMaxColumnWidth(const ImGuiTable* table, int column_n) +{ + const ImGuiTableColumn* column = &table->Columns[column_n]; + float max_width = FLT_MAX; + const float min_column_distance = table->MinColumnWidth + table->CellPaddingX * 2.0f + table->CellSpacingX1 + table->CellSpacingX2; + if (table->Flags & ImGuiTableFlags_ScrollX) + { + // Frozen columns can't reach beyond visible width else scrolling will naturally break. + // (we use DisplayOrder as within a set of multiple frozen column reordering is possible) + if (column->DisplayOrder < table->FreezeColumnsRequest) + { + max_width = (table->InnerClipRect.Max.x - (table->FreezeColumnsRequest - column->DisplayOrder) * min_column_distance) - column->MinX; + max_width = max_width - table->OuterPaddingX - table->CellPaddingX - table->CellSpacingX2; + } + } + else if ((table->Flags & ImGuiTableFlags_NoKeepColumnsVisible) == 0) + { + // If horizontal scrolling if disabled, we apply a final lossless shrinking of columns in order to make + // sure they are all visible. Because of this we also know that all of the columns will always fit in + // table->WorkRect and therefore in table->InnerRect (because ScrollX is off) + // FIXME-TABLE: This is solved incorrectly but also quite a difficult problem to fix as we also want ClipRect width to match. + // See "table_width_distrib" and "table_width_keep_visible" tests + max_width = table->WorkRect.Max.x - (table->ColumnsEnabledCount - column->IndexWithinEnabledSet - 1) * min_column_distance - column->MinX; + //max_width -= table->CellSpacingX1; + max_width -= table->CellSpacingX2; + max_width -= table->CellPaddingX * 2.0f; + max_width -= table->OuterPaddingX; + } + return max_width; +} + +// Note this is meant to be stored in column->WidthAuto, please generally use the WidthAuto field +float ImGui::TableGetColumnWidthAuto(ImGuiTable* table, ImGuiTableColumn* column) +{ + const float content_width_body = ImMax(column->ContentMaxXFrozen, column->ContentMaxXUnfrozen) - column->WorkMinX; + const float content_width_headers = column->ContentMaxXHeadersIdeal - column->WorkMinX; + float width_auto = content_width_body; + if (!(column->Flags & ImGuiTableColumnFlags_NoHeaderWidth)) + width_auto = ImMax(width_auto, content_width_headers); + + // Non-resizable fixed columns preserve their requested width + if ((column->Flags & ImGuiTableColumnFlags_WidthFixed) && column->InitStretchWeightOrWidth > 0.0f) + if (!(table->Flags & ImGuiTableFlags_Resizable) || (column->Flags & ImGuiTableColumnFlags_NoResize)) + width_auto = column->InitStretchWeightOrWidth; + + return ImMax(width_auto, table->MinColumnWidth); +} + +// 'width' = inner column width, without padding +void ImGui::TableSetColumnWidth(int column_n, float width) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL && table->IsLayoutLocked == false); + IM_ASSERT(column_n >= 0 && column_n < table->ColumnsCount); + ImGuiTableColumn* column_0 = &table->Columns[column_n]; + float column_0_width = width; + + // Apply constraints early + // Compare both requested and actual given width to avoid overwriting requested width when column is stuck (minimum size, bounded) + IM_ASSERT(table->MinColumnWidth > 0.0f); + const float min_width = table->MinColumnWidth; + const float max_width = ImMax(min_width, column_0->WidthMax); // Don't use TableCalcMaxColumnWidth() here as it would rely on MinX from last instance (#7933) + column_0_width = ImClamp(column_0_width, min_width, max_width); + if (column_0->WidthGiven == column_0_width || column_0->WidthRequest == column_0_width) + return; + + //IMGUI_DEBUG_PRINT("TableSetColumnWidth(%d, %.1f->%.1f)\n", column_0_idx, column_0->WidthGiven, column_0_width); + ImGuiTableColumn* column_1 = (column_0->NextEnabledColumn != -1) ? &table->Columns[column_0->NextEnabledColumn] : NULL; + + // In this surprisingly not simple because of how we support mixing Fixed and multiple Stretch columns. + // - All fixed: easy. + // - All stretch: easy. + // - One or more fixed + one stretch: easy. + // - One or more fixed + more than one stretch: tricky. + // Qt when manual resize is enabled only supports a single _trailing_ stretch column, we support more cases here. + + // When forwarding resize from Wn| to Fn+1| we need to be considerate of the _NoResize flag on Fn+1. + // FIXME-TABLE: Find a way to rewrite all of this so interactions feel more consistent for the user. + // Scenarios: + // - F1 F2 F3 resize from F1| or F2| --> ok: alter ->WidthRequested of Fixed column. Subsequent columns will be offset. + // - F1 F2 F3 resize from F3| --> ok: alter ->WidthRequested of Fixed column. If active, ScrollX extent can be altered. + // - F1 F2 W3 resize from F1| or F2| --> ok: alter ->WidthRequested of Fixed column. If active, ScrollX extent can be altered, but it doesn't make much sense as the Stretch column will always be minimal size. + // - F1 F2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 W2 W3 resize from W1| or W2| --> ok + // - W1 W2 W3 resize from W3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 F2 F3 resize from F3| --> ok: no-op (disabled by Resize Rule 1) + // - W1 F2 resize from F2| --> ok: no-op (disabled by Resize Rule 1) + // - W1 W2 F3 resize from W1| or W2| --> ok + // - W1 F2 W3 resize from W1| or F2| --> ok + // - F1 W2 F3 resize from W2| --> ok + // - F1 W3 F2 resize from W3| --> ok + // - W1 F2 F3 resize from W1| --> ok: equivalent to resizing |F2. F3 will not move. + // - W1 F2 F3 resize from F2| --> ok + // All resizes from a Wx columns are locking other columns. + + // Possible improvements: + // - W1 W2 W3 resize W1| --> to not be stuck, both W2 and W3 would stretch down. Seems possible to fix. Would be most beneficial to simplify resize of all-weighted columns. + // - W3 F1 F2 resize W3| --> to not be stuck past F1|, both F1 and F2 would need to stretch down, which would be lossy or ambiguous. Seems hard to fix. + + // [Resize Rule 1] Can't resize from right of right-most visible column if there is any Stretch column. Implemented in TableUpdateLayout(). + + // If we have all Fixed columns OR resizing a Fixed column that doesn't come after a Stretch one, we can do an offsetting resize. + // This is the preferred resize path + if (column_0->Flags & ImGuiTableColumnFlags_WidthFixed) + if (!column_1 || table->LeftMostStretchedColumn == -1 || table->Columns[table->LeftMostStretchedColumn].DisplayOrder >= column_0->DisplayOrder) + { + column_0->WidthRequest = column_0_width; + table->IsSettingsDirty = true; + return; + } + + // We can also use previous column if there's no next one (this is used when doing an auto-fit on the right-most stretch column) + if (column_1 == NULL) + column_1 = (column_0->PrevEnabledColumn != -1) ? &table->Columns[column_0->PrevEnabledColumn] : NULL; + if (column_1 == NULL) + return; + + // Resizing from right-side of a Stretch column before a Fixed column forward sizing to left-side of fixed column. + // (old_a + old_b == new_a + new_b) --> (new_a == old_a + old_b - new_b) + float column_1_width = ImMax(column_1->WidthRequest - (column_0_width - column_0->WidthRequest), min_width); + column_0_width = column_0->WidthRequest + column_1->WidthRequest - column_1_width; + IM_ASSERT(column_0_width > 0.0f && column_1_width > 0.0f); + column_0->WidthRequest = column_0_width; + column_1->WidthRequest = column_1_width; + if ((column_0->Flags | column_1->Flags) & ImGuiTableColumnFlags_WidthStretch) + TableUpdateColumnsWeightFromWidth(table); + table->IsSettingsDirty = true; +} + +// Disable clipping then auto-fit, will take 2 frames +// (we don't take a shortcut for unclipped columns to reduce inconsistencies when e.g. resizing multiple columns) +void ImGui::TableSetColumnWidthAutoSingle(ImGuiTable* table, int column_n) +{ + // Single auto width uses auto-fit + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled) + return; + column->CannotSkipItemsQueue = (1 << 0); + table->AutoFitSingleColumn = (ImGuiTableColumnIdx)column_n; +} + +void ImGui::TableSetColumnWidthAutoAll(ImGuiTable* table) +{ + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled && !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) // Cannot reset weight of hidden stretch column + continue; + column->CannotSkipItemsQueue = (1 << 0); + column->AutoFitQueue = (1 << 1); + } +} + +void ImGui::TableUpdateColumnsWeightFromWidth(ImGuiTable* table) +{ + IM_ASSERT(table->LeftMostStretchedColumn != -1 && table->RightMostStretchedColumn != -1); + + // Measure existing quantities + float visible_weight = 0.0f; + float visible_width = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled || !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + IM_ASSERT(column->StretchWeight > 0.0f); + visible_weight += column->StretchWeight; + visible_width += column->WidthRequest; + } + IM_ASSERT(visible_weight > 0.0f && visible_width > 0.0f); + + // Apply new weights + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (!column->IsEnabled || !(column->Flags & ImGuiTableColumnFlags_WidthStretch)) + continue; + column->StretchWeight = (column->WidthRequest / visible_width) * visible_weight; + IM_ASSERT(column->StretchWeight > 0.0f); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Drawing +//------------------------------------------------------------------------- +// - TablePushBackgroundChannel() [Internal] +// - TablePopBackgroundChannel() [Internal] +// - TableSetupDrawChannels() [Internal] +// - TableMergeDrawChannels() [Internal] +// - TableGetColumnBorderCol() [Internal] +// - TableDrawBorders() [Internal] +//------------------------------------------------------------------------- + +// Bg2 is used by Selectable (and possibly other widgets) to render to the background. +// Unlike our Bg0/1 channel which we uses for RowBg/CellBg/Borders and where we guarantee all shapes to be CPU-clipped, the Bg2 channel being widgets-facing will rely on regular ClipRect. +void ImGui::TablePushBackgroundChannel() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + + // Optimization: avoid SetCurrentChannel() + PushClipRect() + table->HostBackupInnerClipRect = window->ClipRect; + SetWindowClipRectBeforeSetChannel(window, table->Bg2ClipRectForDrawCmd); + table->DrawSplitter->SetCurrentChannel(window->DrawList, table->Bg2DrawChannelCurrent); +} + +void ImGui::TablePopBackgroundChannel() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiTable* table = g.CurrentTable; + ImGuiTableColumn* column = &table->Columns[table->CurrentColumn]; + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + SetWindowClipRectBeforeSetChannel(window, table->HostBackupInnerClipRect); + table->DrawSplitter->SetCurrentChannel(window->DrawList, column->DrawChannelCurrent); +} + +// Allocate draw channels. Called by TableUpdateLayout() +// - We allocate them following storage order instead of display order so reordering columns won't needlessly +// increase overall dormant memory cost. +// - We isolate headers draw commands in their own channels instead of just altering clip rects. +// This is in order to facilitate merging of draw commands. +// - After crossing FreezeRowsCount, all columns see their current draw channel changed to a second set of channels. +// - We only use the dummy draw channel so we can push a null clipping rectangle into it without affecting other +// channels, while simplifying per-row/per-cell overhead. It will be empty and discarded when merged. +// - We allocate 1 or 2 background draw channels. This is because we know TablePushBackgroundChannel() is only used for +// horizontal spanning. If we allowed vertical spanning we'd need one background draw channel per merge group (1-4). +// Draw channel allocation (before merging): +// - NoClip --> 2+D+1 channels: bg0/1 + bg2 + foreground (same clip rect == always 1 draw call) +// - Clip --> 2+D+N channels +// - FreezeRows --> 2+D+N*2 (unless scrolling value is zero) +// - FreezeRows || FreezeColunns --> 3+D+N*2 (unless scrolling value is zero) +// Where D is 1 if any column is clipped or hidden (dummy channel) otherwise 0. +void ImGui::TableSetupDrawChannels(ImGuiTable* table) +{ + const int freeze_row_multiplier = (table->FreezeRowsCount > 0) ? 2 : 1; + const int channels_for_row = (table->Flags & ImGuiTableFlags_NoClip) ? 1 : table->ColumnsEnabledCount; + const int channels_for_bg = 1 + 1 * freeze_row_multiplier; + const int channels_for_dummy = (table->ColumnsEnabledCount < table->ColumnsCount || (memcmp(table->VisibleMaskByIndex, table->EnabledMaskByIndex, ImBitArrayGetStorageSizeInBytes(table->ColumnsCount)) != 0)) ? +1 : 0; + const int channels_total = channels_for_bg + (channels_for_row * freeze_row_multiplier) + channels_for_dummy; + table->DrawSplitter->Split(table->InnerWindow->DrawList, channels_total); + table->DummyDrawChannel = (ImGuiTableDrawChannelIdx)((channels_for_dummy > 0) ? channels_total - 1 : -1); + table->Bg2DrawChannelCurrent = TABLE_DRAW_CHANNEL_BG2_FROZEN; + table->Bg2DrawChannelUnfrozen = (ImGuiTableDrawChannelIdx)((table->FreezeRowsCount > 0) ? 2 + channels_for_row : TABLE_DRAW_CHANNEL_BG2_FROZEN); + + int draw_channel_current = 2; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->IsVisibleX && column->IsVisibleY) + { + column->DrawChannelFrozen = (ImGuiTableDrawChannelIdx)(draw_channel_current); + column->DrawChannelUnfrozen = (ImGuiTableDrawChannelIdx)(draw_channel_current + (table->FreezeRowsCount > 0 ? channels_for_row + 1 : 0)); + if (!(table->Flags & ImGuiTableFlags_NoClip)) + draw_channel_current++; + } + else + { + column->DrawChannelFrozen = column->DrawChannelUnfrozen = table->DummyDrawChannel; + } + column->DrawChannelCurrent = column->DrawChannelFrozen; + } + + // Initial draw cmd starts with a BgClipRect that matches the one of its host, to facilitate merge draw commands by default. + // All our cell highlight are manually clipped with BgClipRect. When unfreezing it will be made smaller to fit scrolling rect. + // (This technically isn't part of setting up draw channels, but is reasonably related to be done here) + table->BgClipRect = table->InnerClipRect; + table->Bg0ClipRectForDrawCmd = table->OuterWindow->ClipRect; + table->Bg2ClipRectForDrawCmd = table->HostClipRect; + IM_ASSERT(table->BgClipRect.Min.y <= table->BgClipRect.Max.y); +} + +// This function reorder draw channels based on matching clip rectangle, to facilitate merging them. Called by EndTable(). +// For simplicity we call it TableMergeDrawChannels() but in fact it only reorder channels + overwrite ClipRect, +// actual merging is done by table->DrawSplitter.Merge() which is called right after TableMergeDrawChannels(). +// +// Columns where the contents didn't stray off their local clip rectangle can be merged. To achieve +// this we merge their clip rect and make them contiguous in the channel list, so they can be merged +// by the call to DrawSplitter.Merge() following to the call to this function. +// We reorder draw commands by arranging them into a maximum of 4 distinct groups: +// +// 1 group: 2 groups: 2 groups: 4 groups: +// [ 0. ] no freeze [ 0. ] row freeze [ 01 ] col freeze [ 01 ] row+col freeze +// [ .. ] or no scroll [ 2. ] and v-scroll [ .. ] and h-scroll [ 23 ] and v+h-scroll +// +// Each column itself can use 1 channel (row freeze disabled) or 2 channels (row freeze enabled). +// When the contents of a column didn't stray off its limit, we move its channels into the corresponding group +// based on its position (within frozen rows/columns groups or not). +// At the end of the operation our 1-4 groups will each have a ImDrawCmd using the same ClipRect. +// This function assume that each column are pointing to a distinct draw channel, +// otherwise merge_group->ChannelsCount will not match set bit count of merge_group->ChannelsMask. +// +// Column channels will not be merged into one of the 1-4 groups in the following cases: +// - The contents stray off its clipping rectangle (we only compare the MaxX value, not the MinX value). +// Direct ImDrawList calls won't be taken into account by default, if you use them make sure the ImGui:: bounds +// matches, by e.g. calling SetCursorScreenPos(). +// - The channel uses more than one draw command itself. We drop all our attempt at merging stuff here.. +// we could do better but it's going to be rare and probably not worth the hassle. +// Columns for which the draw channel(s) haven't been merged with other will use their own ImDrawCmd. +// +// This function is particularly tricky to understand.. take a breath. +void ImGui::TableMergeDrawChannels(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + ImDrawListSplitter* splitter = table->DrawSplitter; + const bool has_freeze_v = (table->FreezeRowsCount > 0); + const bool has_freeze_h = (table->FreezeColumnsCount > 0); + IM_ASSERT(splitter->_Current == 0); + + // Track which groups we are going to attempt to merge, and which channels goes into each group. + struct MergeGroup + { + ImRect ClipRect; + int ChannelsCount = 0; + ImBitArrayPtr ChannelsMask = NULL; + }; + int merge_group_mask = 0x00; + MergeGroup merge_groups[4]; + + // Use a reusable temp buffer for the merge masks as they are dynamically sized. + const int max_draw_channels = (4 + table->ColumnsCount * 2); + const int size_for_masks_bitarrays_one = (int)ImBitArrayGetStorageSizeInBytes(max_draw_channels); + g.TempBuffer.reserve(size_for_masks_bitarrays_one * 5); + memset(g.TempBuffer.Data, 0, size_for_masks_bitarrays_one * 5); + for (int n = 0; n < IM_ARRAYSIZE(merge_groups); n++) + merge_groups[n].ChannelsMask = (ImBitArrayPtr)(void*)(g.TempBuffer.Data + (size_for_masks_bitarrays_one * n)); + ImBitArrayPtr remaining_mask = (ImBitArrayPtr)(void*)(g.TempBuffer.Data + (size_for_masks_bitarrays_one * 4)); + + // 1. Scan channels and take note of those which can be merged + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + if (!IM_BITARRAY_TESTBIT(table->VisibleMaskByIndex, column_n)) + continue; + ImGuiTableColumn* column = &table->Columns[column_n]; + + const int merge_group_sub_count = has_freeze_v ? 2 : 1; + for (int merge_group_sub_n = 0; merge_group_sub_n < merge_group_sub_count; merge_group_sub_n++) + { + const int channel_no = (merge_group_sub_n == 0) ? column->DrawChannelFrozen : column->DrawChannelUnfrozen; + + // Don't attempt to merge if there are multiple draw calls within the column + ImDrawChannel* src_channel = &splitter->_Channels[channel_no]; + if (src_channel->_CmdBuffer.Size > 0 && src_channel->_CmdBuffer.back().ElemCount == 0 && src_channel->_CmdBuffer.back().UserCallback == NULL) // Equivalent of PopUnusedDrawCmd() + src_channel->_CmdBuffer.pop_back(); + if (src_channel->_CmdBuffer.Size != 1) + continue; + + // Find out the width of this merge group and check if it will fit in our column + // (note that we assume that rendering didn't stray on the left direction. we should need a CursorMinPos to detect it) + if (!(column->Flags & ImGuiTableColumnFlags_NoClip)) + { + float content_max_x; + if (!has_freeze_v) + content_max_x = ImMax(column->ContentMaxXUnfrozen, column->ContentMaxXHeadersUsed); // No row freeze + else if (merge_group_sub_n == 0) + content_max_x = ImMax(column->ContentMaxXFrozen, column->ContentMaxXHeadersUsed); // Row freeze: use width before freeze + else + content_max_x = column->ContentMaxXUnfrozen; // Row freeze: use width after freeze + if (content_max_x > column->ClipRect.Max.x) + continue; + } + + const int merge_group_n = (has_freeze_h && column_n < table->FreezeColumnsCount ? 0 : 1) + (has_freeze_v && merge_group_sub_n == 0 ? 0 : 2); + IM_ASSERT(channel_no < max_draw_channels); + MergeGroup* merge_group = &merge_groups[merge_group_n]; + if (merge_group->ChannelsCount == 0) + merge_group->ClipRect = ImRect(+FLT_MAX, +FLT_MAX, -FLT_MAX, -FLT_MAX); + ImBitArraySetBit(merge_group->ChannelsMask, channel_no); + merge_group->ChannelsCount++; + merge_group->ClipRect.Add(src_channel->_CmdBuffer[0].ClipRect); + merge_group_mask |= (1 << merge_group_n); + } + + // Invalidate current draw channel + // (we don't clear DrawChannelFrozen/DrawChannelUnfrozen solely to facilitate debugging/later inspection of data) + column->DrawChannelCurrent = (ImGuiTableDrawChannelIdx)-1; + } + + // [DEBUG] Display merge groups +#if 0 + if (g.IO.KeyShift) + for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++) + { + MergeGroup* merge_group = &merge_groups[merge_group_n]; + if (merge_group->ChannelsCount == 0) + continue; + char buf[32]; + ImFormatString(buf, 32, "MG%d:%d", merge_group_n, merge_group->ChannelsCount); + ImVec2 text_pos = merge_group->ClipRect.Min + ImVec2(4, 4); + ImVec2 text_size = CalcTextSize(buf, NULL); + GetForegroundDrawList()->AddRectFilled(text_pos, text_pos + text_size, IM_COL32(0, 0, 0, 255)); + GetForegroundDrawList()->AddText(text_pos, IM_COL32(255, 255, 0, 255), buf, NULL); + GetForegroundDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 255, 0, 255)); + } +#endif + + // 2. Rewrite channel list in our preferred order + if (merge_group_mask != 0) + { + // We skip channel 0 (Bg0/Bg1) and 1 (Bg2 frozen) from the shuffling since they won't move - see channels allocation in TableSetupDrawChannels(). + const int LEADING_DRAW_CHANNELS = 2; + g.DrawChannelsTempMergeBuffer.resize(splitter->_Count - LEADING_DRAW_CHANNELS); // Use shared temporary storage so the allocation gets amortized + ImDrawChannel* dst_tmp = g.DrawChannelsTempMergeBuffer.Data; + ImBitArraySetBitRange(remaining_mask, LEADING_DRAW_CHANNELS, splitter->_Count); + ImBitArrayClearBit(remaining_mask, table->Bg2DrawChannelUnfrozen); + IM_ASSERT(has_freeze_v == false || table->Bg2DrawChannelUnfrozen != TABLE_DRAW_CHANNEL_BG2_FROZEN); + int remaining_count = splitter->_Count - (has_freeze_v ? LEADING_DRAW_CHANNELS + 1 : LEADING_DRAW_CHANNELS); + //ImRect host_rect = (table->InnerWindow == table->OuterWindow) ? table->InnerClipRect : table->HostClipRect; + ImRect host_rect = table->HostClipRect; + for (int merge_group_n = 0; merge_group_n < IM_ARRAYSIZE(merge_groups); merge_group_n++) + { + if (int merge_channels_count = merge_groups[merge_group_n].ChannelsCount) + { + MergeGroup* merge_group = &merge_groups[merge_group_n]; + ImRect merge_clip_rect = merge_group->ClipRect; + + // Extend outer-most clip limits to match those of host, so draw calls can be merged even if + // outer-most columns have some outer padding offsetting them from their parent ClipRect. + // The principal cases this is dealing with are: + // - On a same-window table (not scrolling = single group), all fitting columns ClipRect -> will extend and match host ClipRect -> will merge + // - Columns can use padding and have left-most ClipRect.Min.x and right-most ClipRect.Max.x != from host ClipRect -> will extend and match host ClipRect -> will merge + // FIXME-TABLE FIXME-WORKRECT: We are wasting a merge opportunity on tables without scrolling if column doesn't fit + // within host clip rect, solely because of the half-padding difference between window->WorkRect and window->InnerClipRect. + if ((merge_group_n & 1) == 0 || !has_freeze_h) + merge_clip_rect.Min.x = ImMin(merge_clip_rect.Min.x, host_rect.Min.x); + if ((merge_group_n & 2) == 0 || !has_freeze_v) + merge_clip_rect.Min.y = ImMin(merge_clip_rect.Min.y, host_rect.Min.y); + if ((merge_group_n & 1) != 0) + merge_clip_rect.Max.x = ImMax(merge_clip_rect.Max.x, host_rect.Max.x); + if ((merge_group_n & 2) != 0 && (table->Flags & ImGuiTableFlags_NoHostExtendY) == 0) + merge_clip_rect.Max.y = ImMax(merge_clip_rect.Max.y, host_rect.Max.y); + //GetForegroundDrawList()->AddRect(merge_group->ClipRect.Min, merge_group->ClipRect.Max, IM_COL32(255, 0, 0, 200), 0.0f, 0, 1.0f); // [DEBUG] + //GetForegroundDrawList()->AddLine(merge_group->ClipRect.Min, merge_clip_rect.Min, IM_COL32(255, 100, 0, 200)); + //GetForegroundDrawList()->AddLine(merge_group->ClipRect.Max, merge_clip_rect.Max, IM_COL32(255, 100, 0, 200)); + remaining_count -= merge_group->ChannelsCount; + for (int n = 0; n < (size_for_masks_bitarrays_one >> 2); n++) + remaining_mask[n] &= ~merge_group->ChannelsMask[n]; + for (int n = 0; n < splitter->_Count && merge_channels_count != 0; n++) + { + // Copy + overwrite new clip rect + if (!IM_BITARRAY_TESTBIT(merge_group->ChannelsMask, n)) + continue; + IM_BITARRAY_CLEARBIT(merge_group->ChannelsMask, n); + merge_channels_count--; + + ImDrawChannel* channel = &splitter->_Channels[n]; + IM_ASSERT(channel->_CmdBuffer.Size == 1 && merge_clip_rect.Contains(ImRect(channel->_CmdBuffer[0].ClipRect))); + channel->_CmdBuffer[0].ClipRect = merge_clip_rect.ToVec4(); + memcpy(dst_tmp++, channel, sizeof(ImDrawChannel)); + } + } + + // Make sure Bg2DrawChannelUnfrozen appears in the middle of our groups (whereas Bg0/Bg1 and Bg2 frozen are fixed to 0 and 1) + if (merge_group_n == 1 && has_freeze_v) + memcpy(dst_tmp++, &splitter->_Channels[table->Bg2DrawChannelUnfrozen], sizeof(ImDrawChannel)); + } + + // Append unmergeable channels that we didn't reorder at the end of the list + for (int n = 0; n < splitter->_Count && remaining_count != 0; n++) + { + if (!IM_BITARRAY_TESTBIT(remaining_mask, n)) + continue; + ImDrawChannel* channel = &splitter->_Channels[n]; + memcpy(dst_tmp++, channel, sizeof(ImDrawChannel)); + remaining_count--; + } + IM_ASSERT(dst_tmp == g.DrawChannelsTempMergeBuffer.Data + g.DrawChannelsTempMergeBuffer.Size); + memcpy(splitter->_Channels.Data + LEADING_DRAW_CHANNELS, g.DrawChannelsTempMergeBuffer.Data, (splitter->_Count - LEADING_DRAW_CHANNELS) * sizeof(ImDrawChannel)); + } +} + +static ImU32 TableGetColumnBorderCol(ImGuiTable* table, int order_n, int column_n) +{ + const bool is_hovered = (table->HoveredColumnBorder == column_n); + const bool is_resized = (table->ResizedColumn == column_n) && (table->InstanceInteracted == table->InstanceCurrent); + const bool is_frozen_separator = (table->FreezeColumnsCount == order_n + 1); + if (is_resized || is_hovered) + return ImGui::GetColorU32(is_resized ? ImGuiCol_SeparatorActive : ImGuiCol_SeparatorHovered); + if (is_frozen_separator || (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize))) + return table->BorderColorStrong; + return table->BorderColorLight; +} + +// FIXME-TABLE: This is a mess, need to redesign how we render borders (as some are also done in TableEndRow) +void ImGui::TableDrawBorders(ImGuiTable* table) +{ + ImGuiWindow* inner_window = table->InnerWindow; + if (!table->OuterWindow->ClipRect.Overlaps(table->OuterRect)) + return; + + ImDrawList* inner_drawlist = inner_window->DrawList; + table->DrawSplitter->SetCurrentChannel(inner_drawlist, TABLE_DRAW_CHANNEL_BG0); + inner_drawlist->PushClipRect(table->Bg0ClipRectForDrawCmd.Min, table->Bg0ClipRectForDrawCmd.Max, false); + + // Draw inner border and resizing feedback + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + const float border_size = TABLE_BORDER_SIZE; + const float draw_y1 = ImMax(table->InnerRect.Min.y, (table->FreezeRowsCount >= 1 ? table->InnerRect.Min.y : table->WorkRect.Min.y) + table->AngledHeadersHeight) + ((table->Flags & ImGuiTableFlags_BordersOuterH) ? 1.0f : 0.0f); + const float draw_y2_body = table->InnerRect.Max.y; + const float draw_y2_head = table->IsUsingHeaders ? ImMin(table->InnerRect.Max.y, (table->FreezeRowsCount >= 1 ? table->InnerRect.Min.y : table->WorkRect.Min.y) + table_instance->LastTopHeadersRowHeight) : draw_y1; + if (table->Flags & ImGuiTableFlags_BordersInnerV) + { + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + { + if (!IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + continue; + + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + const bool is_hovered = (table->HoveredColumnBorder == column_n); + const bool is_resized = (table->ResizedColumn == column_n) && (table->InstanceInteracted == table->InstanceCurrent); + const bool is_resizable = (column->Flags & (ImGuiTableColumnFlags_NoResize | ImGuiTableColumnFlags_NoDirectResize_)) == 0; + const bool is_frozen_separator = (table->FreezeColumnsCount == order_n + 1); + if (column->MaxX > table->InnerClipRect.Max.x && !is_resized) + continue; + + // Decide whether right-most column is visible + if (column->NextEnabledColumn == -1 && !is_resizable) + if ((table->Flags & ImGuiTableFlags_SizingMask_) != ImGuiTableFlags_SizingFixedSame || (table->Flags & ImGuiTableFlags_NoHostExtendX)) + continue; + if (column->MaxX <= column->ClipRect.Min.x) // FIXME-TABLE FIXME-STYLE: Assume BorderSize==1, this is problematic if we want to increase the border size.. + continue; + + // Draw in outer window so right-most column won't be clipped + // Always draw full height border when being resized/hovered, or on the delimitation of frozen column scrolling. + float draw_y2 = (is_hovered || is_resized || is_frozen_separator || (table->Flags & (ImGuiTableFlags_NoBordersInBody | ImGuiTableFlags_NoBordersInBodyUntilResize)) == 0) ? draw_y2_body : draw_y2_head; + if (draw_y2 > draw_y1) + inner_drawlist->AddLine(ImVec2(column->MaxX, draw_y1), ImVec2(column->MaxX, draw_y2), TableGetColumnBorderCol(table, order_n, column_n), border_size); + } + } + + // Draw outer border + // FIXME: could use AddRect or explicit VLine/HLine helper? + if (table->Flags & ImGuiTableFlags_BordersOuter) + { + // Display outer border offset by 1 which is a simple way to display it without adding an extra draw call + // (Without the offset, in outer_window it would be rendered behind cells, because child windows are above their + // parent. In inner_window, it won't reach out over scrollbars. Another weird solution would be to display part + // of it in inner window, and the part that's over scrollbars in the outer window..) + // Either solution currently won't allow us to use a larger border size: the border would clipped. + const ImRect outer_border = table->OuterRect; + const ImU32 outer_col = table->BorderColorStrong; + if ((table->Flags & ImGuiTableFlags_BordersOuter) == ImGuiTableFlags_BordersOuter) + { + inner_drawlist->AddRect(outer_border.Min, outer_border.Max, outer_col, 0.0f, 0, border_size); + } + else if (table->Flags & ImGuiTableFlags_BordersOuterV) + { + inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Min.x, outer_border.Max.y), outer_col, border_size); + inner_drawlist->AddLine(ImVec2(outer_border.Max.x, outer_border.Min.y), outer_border.Max, outer_col, border_size); + } + else if (table->Flags & ImGuiTableFlags_BordersOuterH) + { + inner_drawlist->AddLine(outer_border.Min, ImVec2(outer_border.Max.x, outer_border.Min.y), outer_col, border_size); + inner_drawlist->AddLine(ImVec2(outer_border.Min.x, outer_border.Max.y), outer_border.Max, outer_col, border_size); + } + } + if ((table->Flags & ImGuiTableFlags_BordersInnerH) && table->RowPosY2 < table->OuterRect.Max.y) + { + // Draw bottom-most row border between it is above outer border. + const float border_y = table->RowPosY2; + if (border_y >= table->BgClipRect.Min.y && border_y < table->BgClipRect.Max.y) + inner_drawlist->AddLine(ImVec2(table->BorderX1, border_y), ImVec2(table->BorderX2, border_y), table->BorderColorLight, border_size); + } + + inner_drawlist->PopClipRect(); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Sorting +//------------------------------------------------------------------------- +// - TableGetSortSpecs() +// - TableFixColumnSortDirection() [Internal] +// - TableGetColumnNextSortDirection() [Internal] +// - TableSetColumnSortDirection() [Internal] +// - TableSortSpecsSanitize() [Internal] +// - TableSortSpecsBuild() [Internal] +//------------------------------------------------------------------------- + +// Return NULL if no sort specs (most often when ImGuiTableFlags_Sortable is not set) +// When 'sort_specs->SpecsDirty == true' you should sort your data. It will be true when sorting specs have +// changed since last call, or the first time. Make sure to set 'SpecsDirty = false' after sorting, +// else you may wastefully sort your data every frame! +// Lifetime: don't hold on this pointer over multiple frames or past any subsequent call to BeginTable()! +ImGuiTableSortSpecs* ImGui::TableGetSortSpecs() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + IM_ASSERT(table != NULL); + + if (!(table->Flags & ImGuiTableFlags_Sortable)) + return NULL; + + // Require layout (in case TableHeadersRow() hasn't been called) as it may alter IsSortSpecsDirty in some paths. + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + TableSortSpecsBuild(table); + return &table->SortSpecs; +} + +static inline ImGuiSortDirection TableGetColumnAvailSortDirection(ImGuiTableColumn* column, int n) +{ + IM_ASSERT(n < column->SortDirectionsAvailCount); + return (ImGuiSortDirection)((column->SortDirectionsAvailList >> (n << 1)) & 0x03); +} + +// Fix sort direction if currently set on a value which is unavailable (e.g. activating NoSortAscending/NoSortDescending) +void ImGui::TableFixColumnSortDirection(ImGuiTable* table, ImGuiTableColumn* column) +{ + if (column->SortOrder == -1 || (column->SortDirectionsAvailMask & (1 << column->SortDirection)) != 0) + return; + column->SortDirection = (ImU8)TableGetColumnAvailSortDirection(column, 0); + table->IsSortSpecsDirty = true; +} + +// Calculate next sort direction that would be set after clicking the column +// - If the PreferSortDescending flag is set, we will default to a Descending direction on the first click. +// - Note that the PreferSortAscending flag is never checked, it is essentially the default and therefore a no-op. +IM_STATIC_ASSERT(ImGuiSortDirection_None == 0 && ImGuiSortDirection_Ascending == 1 && ImGuiSortDirection_Descending == 2); +ImGuiSortDirection ImGui::TableGetColumnNextSortDirection(ImGuiTableColumn* column) +{ + IM_ASSERT(column->SortDirectionsAvailCount > 0); + if (column->SortOrder == -1) + return TableGetColumnAvailSortDirection(column, 0); + for (int n = 0; n < 3; n++) + if (column->SortDirection == TableGetColumnAvailSortDirection(column, n)) + return TableGetColumnAvailSortDirection(column, (n + 1) % column->SortDirectionsAvailCount); + IM_ASSERT(0); + return ImGuiSortDirection_None; +} + +// Note that the NoSortAscending/NoSortDescending flags are processed in TableSortSpecsSanitize(), and they may change/revert +// the value of SortDirection. We could technically also do it here but it would be unnecessary and duplicate code. +void ImGui::TableSetColumnSortDirection(int column_n, ImGuiSortDirection sort_direction, bool append_to_sort_specs) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + + if (!(table->Flags & ImGuiTableFlags_SortMulti)) + append_to_sort_specs = false; + if (!(table->Flags & ImGuiTableFlags_SortTristate)) + IM_ASSERT(sort_direction != ImGuiSortDirection_None); + + ImGuiTableColumnIdx sort_order_max = 0; + if (append_to_sort_specs) + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + sort_order_max = ImMax(sort_order_max, table->Columns[other_column_n].SortOrder); + + ImGuiTableColumn* column = &table->Columns[column_n]; + column->SortDirection = (ImU8)sort_direction; + if (column->SortDirection == ImGuiSortDirection_None) + column->SortOrder = -1; + else if (column->SortOrder == -1 || !append_to_sort_specs) + column->SortOrder = append_to_sort_specs ? sort_order_max + 1 : 0; + + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + { + ImGuiTableColumn* other_column = &table->Columns[other_column_n]; + if (other_column != column && !append_to_sort_specs) + other_column->SortOrder = -1; + TableFixColumnSortDirection(table, other_column); + } + table->IsSettingsDirty = true; + table->IsSortSpecsDirty = true; +} + +void ImGui::TableSortSpecsSanitize(ImGuiTable* table) +{ + IM_ASSERT(table->Flags & ImGuiTableFlags_Sortable); + + // Clear SortOrder from hidden column and verify that there's no gap or duplicate. + int sort_order_count = 0; + ImU64 sort_order_mask = 0x00; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->SortOrder != -1 && !column->IsEnabled) + column->SortOrder = -1; + if (column->SortOrder == -1) + continue; + sort_order_count++; + sort_order_mask |= ((ImU64)1 << column->SortOrder); + IM_ASSERT(sort_order_count < (int)sizeof(sort_order_mask) * 8); + } + + const bool need_fix_linearize = ((ImU64)1 << sort_order_count) != (sort_order_mask + 1); + const bool need_fix_single_sort_order = (sort_order_count > 1) && !(table->Flags & ImGuiTableFlags_SortMulti); + if (need_fix_linearize || need_fix_single_sort_order) + { + ImU64 fixed_mask = 0x00; + for (int sort_n = 0; sort_n < sort_order_count; sort_n++) + { + // Fix: Rewrite sort order fields if needed so they have no gap or duplicate. + // (e.g. SortOrder 0 disappeared, SortOrder 1..2 exists --> rewrite then as SortOrder 0..1) + int column_with_smallest_sort_order = -1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if ((fixed_mask & ((ImU64)1 << (ImU64)column_n)) == 0 && table->Columns[column_n].SortOrder != -1) + if (column_with_smallest_sort_order == -1 || table->Columns[column_n].SortOrder < table->Columns[column_with_smallest_sort_order].SortOrder) + column_with_smallest_sort_order = column_n; + IM_ASSERT(column_with_smallest_sort_order != -1); + fixed_mask |= ((ImU64)1 << column_with_smallest_sort_order); + table->Columns[column_with_smallest_sort_order].SortOrder = (ImGuiTableColumnIdx)sort_n; + + // Fix: Make sure only one column has a SortOrder if ImGuiTableFlags_MultiSortable is not set. + if (need_fix_single_sort_order) + { + sort_order_count = 1; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (column_n != column_with_smallest_sort_order) + table->Columns[column_n].SortOrder = -1; + break; + } + } + } + + // Fallback default sort order (if no column with the ImGuiTableColumnFlags_DefaultSort flag) + if (sort_order_count == 0 && !(table->Flags & ImGuiTableFlags_SortTristate)) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->IsEnabled && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + sort_order_count = 1; + column->SortOrder = 0; + column->SortDirection = (ImU8)TableGetColumnAvailSortDirection(column, 0); + break; + } + } + + table->SortSpecsCount = (ImGuiTableColumnIdx)sort_order_count; +} + +void ImGui::TableSortSpecsBuild(ImGuiTable* table) +{ + bool dirty = table->IsSortSpecsDirty; + if (dirty) + { + TableSortSpecsSanitize(table); + table->SortSpecsMulti.resize(table->SortSpecsCount <= 1 ? 0 : table->SortSpecsCount); + table->SortSpecs.SpecsDirty = true; // Mark as dirty for user + table->IsSortSpecsDirty = false; // Mark as not dirty for us + } + + // Write output + // May be able to move all SortSpecs data from table (48 bytes) to ImGuiTableTempData if we decide to write it back on every BeginTable() + ImGuiTableColumnSortSpecs* sort_specs = (table->SortSpecsCount == 0) ? NULL : (table->SortSpecsCount == 1) ? &table->SortSpecsSingle : table->SortSpecsMulti.Data; + if (dirty && sort_specs != NULL) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + if (column->SortOrder == -1) + continue; + IM_ASSERT(column->SortOrder < table->SortSpecsCount); + ImGuiTableColumnSortSpecs* sort_spec = &sort_specs[column->SortOrder]; + sort_spec->ColumnUserID = column->UserID; + sort_spec->ColumnIndex = (ImGuiTableColumnIdx)column_n; + sort_spec->SortOrder = (ImGuiTableColumnIdx)column->SortOrder; + sort_spec->SortDirection = (ImGuiSortDirection)column->SortDirection; + } + + table->SortSpecs.Specs = sort_specs; + table->SortSpecs.SpecsCount = table->SortSpecsCount; +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Headers +//------------------------------------------------------------------------- +// - TableGetHeaderRowHeight() [Internal] +// - TableGetHeaderAngledMaxLabelWidth() [Internal] +// - TableHeadersRow() +// - TableHeader() +// - TableAngledHeadersRow() +// - TableAngledHeadersRowEx() [Internal] +//------------------------------------------------------------------------- + +float ImGui::TableGetHeaderRowHeight() +{ + // Caring for a minor edge case: + // Calculate row height, for the unlikely case that some labels may be taller than others. + // If we didn't do that, uneven header height would highlight but smaller one before the tallest wouldn't catch input for all height. + // In your custom header row you may omit this all together and just call TableNextRow() without a height... + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + float row_height = g.FontSize; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + if ((table->Columns[column_n].Flags & ImGuiTableColumnFlags_NoHeaderLabel) == 0) + row_height = ImMax(row_height, CalcTextSize(TableGetColumnName(table, column_n)).y); + return row_height + g.Style.CellPadding.y * 2.0f; +} + +float ImGui::TableGetHeaderAngledMaxLabelWidth() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + float width = 0.0f; + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByIndex, column_n)) + if (table->Columns[column_n].Flags & ImGuiTableColumnFlags_AngledHeader) + width = ImMax(width, CalcTextSize(TableGetColumnName(table, column_n), NULL, true).x); + return width + g.Style.CellPadding.y * 2.0f; // Swap padding +} + +// [Public] This is a helper to output TableHeader() calls based on the column names declared in TableSetupColumn(). +// The intent is that advanced users willing to create customized headers would not need to use this helper +// and can create their own! For example: TableHeader() may be preceded by Checkbox() or other custom widgets. +// See 'Demo->Tables->Custom headers' for a demonstration of implementing a custom version of this. +// This code is intentionally written to not make much use of internal functions, to give you better direction +// if you need to write your own. +// FIXME-TABLE: TableOpenContextMenu() and TableGetHeaderRowHeight() are not public. +void ImGui::TableHeadersRow() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + + // Call layout if not already done. This is automatically done by TableNextRow: we do it here _only_ to make + // it easier to debug-step in TableUpdateLayout(). Your own version of this function doesn't need this. + if (!table->IsLayoutLocked) + TableUpdateLayout(table); + + // Open row + const float row_height = TableGetHeaderRowHeight(); + TableNextRow(ImGuiTableRowFlags_Headers, row_height); + const float row_y1 = GetCursorScreenPos().y; + if (table->HostSkipItems) // Merely an optimization, you may skip in your own code. + return; + + const int columns_count = TableGetColumnCount(); + for (int column_n = 0; column_n < columns_count; column_n++) + { + if (!TableSetColumnIndex(column_n)) + continue; + + // Push an id to allow empty/unnamed headers. This is also idiomatic as it ensure there is a consistent ID path to access columns (for e.g. automation) + const char* name = (TableGetColumnFlags(column_n) & ImGuiTableColumnFlags_NoHeaderLabel) ? "" : TableGetColumnName(column_n); + PushID(column_n); + TableHeader(name); + PopID(); + } + + // Allow opening popup from the right-most section after the last column. + ImVec2 mouse_pos = ImGui::GetMousePos(); + if (IsMouseReleased(1) && TableGetHoveredColumn() == columns_count) + if (mouse_pos.y >= row_y1 && mouse_pos.y < row_y1 + row_height) + TableOpenContextMenu(columns_count); // Will open a non-column-specific popup. +} + +// Emit a column header (text + optional sort order) +// We cpu-clip text here so that all columns headers can be merged into a same draw call. +// Note that because of how we cpu-clip and display sorting indicators, you _cannot_ use SameLine() after a TableHeader() +void ImGui::TableHeader(const char* label) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTable* table = g.CurrentTable; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + + IM_ASSERT(table->CurrentColumn != -1); + const int column_n = table->CurrentColumn; + ImGuiTableColumn* column = &table->Columns[column_n]; + + // Label + if (label == NULL) + label = ""; + const char* label_end = FindRenderedTextEnd(label); + ImVec2 label_size = CalcTextSize(label, label_end, true); + ImVec2 label_pos = window->DC.CursorPos; + + // If we already got a row height, there's use that. + // FIXME-TABLE: Padding problem if the correct outer-padding CellBgRect strays off our ClipRect? + ImRect cell_r = TableGetCellBgRect(table, column_n); + float label_height = ImMax(label_size.y, table->RowMinHeight - table->RowCellPaddingY * 2.0f); + + // Calculate ideal size for sort order arrow + float w_arrow = 0.0f; + float w_sort_text = 0.0f; + bool sort_arrow = false; + char sort_order_suf[4] = ""; + const float ARROW_SCALE = 0.65f; + if ((table->Flags & ImGuiTableFlags_Sortable) && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + w_arrow = ImTrunc(g.FontSize * ARROW_SCALE + g.Style.FramePadding.x); + if (column->SortOrder != -1) + sort_arrow = true; + if (column->SortOrder > 0) + { + ImFormatString(sort_order_suf, IM_ARRAYSIZE(sort_order_suf), "%d", column->SortOrder + 1); + w_sort_text = g.Style.ItemInnerSpacing.x + CalcTextSize(sort_order_suf).x; + } + } + + // We feed our unclipped width to the column without writing on CursorMaxPos, so that column is still considered for merging. + float max_pos_x = label_pos.x + label_size.x + w_sort_text + w_arrow; + column->ContentMaxXHeadersUsed = ImMax(column->ContentMaxXHeadersUsed, sort_arrow ? cell_r.Max.x : ImMin(max_pos_x, cell_r.Max.x)); + column->ContentMaxXHeadersIdeal = ImMax(column->ContentMaxXHeadersIdeal, max_pos_x); + + // Keep header highlighted when context menu is open. + ImGuiID id = window->GetID(label); + ImRect bb(cell_r.Min.x, cell_r.Min.y, cell_r.Max.x, ImMax(cell_r.Max.y, cell_r.Min.y + label_height + g.Style.CellPadding.y * 2.0f)); + ItemSize(ImVec2(0.0f, label_height)); // Don't declare unclipped width, it'll be fed ContentMaxPosHeadersIdeal + if (!ItemAdd(bb, id)) + return; + + //GetForegroundDrawList()->AddRect(cell_r.Min, cell_r.Max, IM_COL32(255, 0, 0, 255)); // [DEBUG] + //GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(255, 0, 0, 255)); // [DEBUG] + + // Using AllowOverlap mode because we cover the whole cell, and we want user to be able to submit subsequent items. + const bool highlight = (table->HighlightColumnHeader == column_n); + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_AllowOverlap); + if (held || hovered || highlight) + { + const ImU32 col = GetColorU32(held ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + //RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + TableSetBgColor(ImGuiTableBgTarget_CellBg, col, table->CurrentColumn); + } + else + { + // Submit single cell bg color in the case we didn't submit a full header row + if ((table->RowFlags & ImGuiTableRowFlags_Headers) == 0) + TableSetBgColor(ImGuiTableBgTarget_CellBg, GetColorU32(ImGuiCol_TableHeaderBg), table->CurrentColumn); + } + RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding); + if (held) + table->HeldHeaderColumn = (ImGuiTableColumnIdx)column_n; + window->DC.CursorPos.y -= g.Style.ItemSpacing.y * 0.5f; + + // Drag and drop to re-order columns. + // FIXME-TABLE: Scroll request while reordering a column and it lands out of the scrolling zone. + if (held && (table->Flags & ImGuiTableFlags_Reorderable) && IsMouseDragging(0) && !g.DragDropActive) + { + // While moving a column it will jump on the other side of the mouse, so we also test for MouseDelta.x + table->ReorderColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + + // We don't reorder: through the frozen<>unfrozen line, or through a column that is marked with ImGuiTableColumnFlags_NoReorder. + if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < cell_r.Min.x) + if (ImGuiTableColumn* prev_column = (column->PrevEnabledColumn != -1) ? &table->Columns[column->PrevEnabledColumn] : NULL) + if (!((column->Flags | prev_column->Flags) & ImGuiTableColumnFlags_NoReorder)) + if ((column->IndexWithinEnabledSet < table->FreezeColumnsRequest) == (prev_column->IndexWithinEnabledSet < table->FreezeColumnsRequest)) + table->ReorderColumnDir = -1; + if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > cell_r.Max.x) + if (ImGuiTableColumn* next_column = (column->NextEnabledColumn != -1) ? &table->Columns[column->NextEnabledColumn] : NULL) + if (!((column->Flags | next_column->Flags) & ImGuiTableColumnFlags_NoReorder)) + if ((column->IndexWithinEnabledSet < table->FreezeColumnsRequest) == (next_column->IndexWithinEnabledSet < table->FreezeColumnsRequest)) + table->ReorderColumnDir = +1; + } + + // Sort order arrow + const float ellipsis_max = ImMax(cell_r.Max.x - w_arrow - w_sort_text, label_pos.x); + if ((table->Flags & ImGuiTableFlags_Sortable) && !(column->Flags & ImGuiTableColumnFlags_NoSort)) + { + if (column->SortOrder != -1) + { + float x = ImMax(cell_r.Min.x, cell_r.Max.x - w_arrow - w_sort_text); + float y = label_pos.y; + if (column->SortOrder > 0) + { + PushStyleColor(ImGuiCol_Text, GetColorU32(ImGuiCol_Text, 0.70f)); + RenderText(ImVec2(x + g.Style.ItemInnerSpacing.x, y), sort_order_suf); + PopStyleColor(); + x += w_sort_text; + } + RenderArrow(window->DrawList, ImVec2(x, y), GetColorU32(ImGuiCol_Text), column->SortDirection == ImGuiSortDirection_Ascending ? ImGuiDir_Up : ImGuiDir_Down, ARROW_SCALE); + } + + // Handle clicking on column header to adjust Sort Order + if (pressed && table->ReorderColumn != column_n) + { + ImGuiSortDirection sort_direction = TableGetColumnNextSortDirection(column); + TableSetColumnSortDirection(column_n, sort_direction, g.IO.KeyShift); + } + } + + // Render clipped label. Clipping here ensure that in the majority of situations, all our header cells will + // be merged into a single draw call. + //window->DrawList->AddCircleFilled(ImVec2(ellipsis_max, label_pos.y), 40, IM_COL32_WHITE); + RenderTextEllipsis(window->DrawList, label_pos, ImVec2(ellipsis_max, label_pos.y + label_height + g.Style.FramePadding.y), ellipsis_max, ellipsis_max, label, label_end, &label_size); + + const bool text_clipped = label_size.x > (ellipsis_max - label_pos.x); + if (text_clipped && hovered && g.ActiveId == 0) + SetItemTooltip("%.*s", (int)(label_end - label), label); + + // We don't use BeginPopupContextItem() because we want the popup to stay up even after the column is hidden + if (IsMouseReleased(1) && IsItemHovered()) + TableOpenContextMenu(column_n); +} + +// Unlike TableHeadersRow() it is not expected that you can reimplement or customize this with custom widgets. +// FIXME: No hit-testing/button on the angled header. +void ImGui::TableAngledHeadersRow() +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + ImGuiTableTempData* temp_data = table->TempData; + temp_data->AngledHeadersRequests.resize(0); + temp_data->AngledHeadersRequests.reserve(table->ColumnsEnabledCount); + + // Which column needs highlight? + const ImGuiID row_id = GetID("##AngledHeaders"); + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, table->InstanceCurrent); + int highlight_column_n = table->HighlightColumnHeader; + if (highlight_column_n == -1 && table->HoveredColumnBody != -1) + if (table_instance->HoveredRowLast == 0 && table->HoveredColumnBorder == -1 && (g.ActiveId == 0 || g.ActiveId == row_id || (table->IsActiveIdInTable || g.DragDropActive))) + highlight_column_n = table->HoveredColumnBody; + + // Build up request + ImU32 col_header_bg = GetColorU32(ImGuiCol_TableHeaderBg); + ImU32 col_text = GetColorU32(ImGuiCol_Text); + for (int order_n = 0; order_n < table->ColumnsCount; order_n++) + if (IM_BITARRAY_TESTBIT(table->EnabledMaskByDisplayOrder, order_n)) + { + const int column_n = table->DisplayOrderToIndex[order_n]; + ImGuiTableColumn* column = &table->Columns[column_n]; + if ((column->Flags & ImGuiTableColumnFlags_AngledHeader) == 0) // Note: can't rely on ImGuiTableColumnFlags_IsVisible test here. + continue; + ImGuiTableHeaderData request = { (ImGuiTableColumnIdx)column_n, col_text, col_header_bg, (column_n == highlight_column_n) ? GetColorU32(ImGuiCol_Header) : 0 }; + temp_data->AngledHeadersRequests.push_back(request); + } + + // Render row + TableAngledHeadersRowEx(row_id, g.Style.TableAngledHeadersAngle, 0.0f, temp_data->AngledHeadersRequests.Data, temp_data->AngledHeadersRequests.Size); +} + +// Important: data must be fed left to right +void ImGui::TableAngledHeadersRowEx(ImGuiID row_id, float angle, float max_label_width, const ImGuiTableHeaderData* data, int data_count) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + ImGuiWindow* window = g.CurrentWindow; + ImDrawList* draw_list = window->DrawList; + if (table == NULL) + { + IM_ASSERT_USER_ERROR(table != NULL, "Call should only be done while in BeginTable() scope!"); + return; + } + IM_ASSERT(table->CurrentRow == -1 && "Must be first row"); + + if (max_label_width == 0.0f) + max_label_width = TableGetHeaderAngledMaxLabelWidth(); + + // Angle argument expressed in (-IM_PI/2 .. +IM_PI/2) as it is easier to think about for user. + const bool flip_label = (angle < 0.0f); + angle -= IM_PI * 0.5f; + const float cos_a = ImCos(angle); + const float sin_a = ImSin(angle); + const float label_cos_a = flip_label ? ImCos(angle + IM_PI) : cos_a; + const float label_sin_a = flip_label ? ImSin(angle + IM_PI) : sin_a; + const ImVec2 unit_right = ImVec2(cos_a, sin_a); + + // Calculate our base metrics and set angled headers data _before_ the first call to TableNextRow() + // FIXME-STYLE: Would it be better for user to submit 'max_label_width' or 'row_height' ? One can be derived from the other. + const float header_height = g.FontSize + g.Style.CellPadding.x * 2.0f; + const float row_height = ImTrunc(ImFabs(ImRotate(ImVec2(max_label_width, flip_label ? +header_height : -header_height), cos_a, sin_a).y)); + table->AngledHeadersHeight = row_height; + table->AngledHeadersSlope = (sin_a != 0.0f) ? (cos_a / sin_a) : 0.0f; + const ImVec2 header_angled_vector = unit_right * (row_height / -sin_a); // vector from bottom-left to top-left, and from bottom-right to top-right + + // Declare row, override and draw our own background + TableNextRow(ImGuiTableRowFlags_Headers, row_height); + TableNextColumn(); + const ImRect row_r(table->WorkRect.Min.x, table->BgClipRect.Min.y, table->WorkRect.Max.x, table->RowPosY2); + table->DrawSplitter->SetCurrentChannel(draw_list, TABLE_DRAW_CHANNEL_BG0); + float clip_rect_min_x = table->BgClipRect.Min.x; + if (table->FreezeColumnsCount > 0) + clip_rect_min_x = ImMax(clip_rect_min_x, table->Columns[table->FreezeColumnsCount - 1].MaxX); + TableSetBgColor(ImGuiTableBgTarget_RowBg0, 0); // Cancel + PushClipRect(table->BgClipRect.Min, table->BgClipRect.Max, false); // Span all columns + draw_list->AddRectFilled(ImVec2(table->BgClipRect.Min.x, row_r.Min.y), ImVec2(table->BgClipRect.Max.x, row_r.Max.y), GetColorU32(ImGuiCol_TableHeaderBg, 0.25f)); // FIXME-STYLE: Change row background with an arbitrary color. + PushClipRect(ImVec2(clip_rect_min_x, table->BgClipRect.Min.y), table->BgClipRect.Max, true); // Span all columns + + ButtonBehavior(row_r, row_id, NULL, NULL); + KeepAliveID(row_id); + + const float ascent_scaled = g.Font->Ascent * g.FontScale; // FIXME: Standardize those scaling factors better + const float line_off_for_ascent_x = (ImMax((g.FontSize - ascent_scaled) * 0.5f, 0.0f) / -sin_a) * (flip_label ? -1.0f : 1.0f); + const ImVec2 padding = g.Style.CellPadding; // We will always use swapped component + const ImVec2 align = g.Style.TableAngledHeadersTextAlign; + + // Draw background and labels in first pass, then all borders. + float max_x = -FLT_MAX; + for (int pass = 0; pass < 2; pass++) + for (int order_n = 0; order_n < data_count; order_n++) + { + const ImGuiTableHeaderData* request = &data[order_n]; + const int column_n = request->Index; + ImGuiTableColumn* column = &table->Columns[column_n]; + + ImVec2 bg_shape[4]; + bg_shape[0] = ImVec2(column->MaxX, row_r.Max.y); + bg_shape[1] = ImVec2(column->MinX, row_r.Max.y); + bg_shape[2] = bg_shape[1] + header_angled_vector; + bg_shape[3] = bg_shape[0] + header_angled_vector; + if (pass == 0) + { + // Draw shape + draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], request->BgColor0); + draw_list->AddQuadFilled(bg_shape[0], bg_shape[1], bg_shape[2], bg_shape[3], request->BgColor1); // Optional highlight + max_x = ImMax(max_x, bg_shape[3].x); + + // Draw label + // - First draw at an offset where RenderTextXXX() function won't meddle with applying current ClipRect, then transform to final offset. + // - Handle multiple lines manually, as we want each lines to follow on the horizontal border, rather than see a whole block rotated. + const char* label_name = TableGetColumnName(table, column_n); + const char* label_name_end = FindRenderedTextEnd(label_name); + const float line_off_step_x = (g.FontSize / -sin_a); + const int label_lines = ImTextCountLines(label_name, label_name_end); + + // Left<>Right alignment + float line_off_curr_x = flip_label ? (label_lines - 1) * line_off_step_x : 0.0f; + float line_off_for_align_x = ImMax((((column->MaxX - column->MinX) - padding.x * 2.0f) - (label_lines * line_off_step_x)), 0.0f) * align.x; + line_off_curr_x += line_off_for_align_x - line_off_for_ascent_x; + + // Register header width + column->ContentMaxXHeadersUsed = column->ContentMaxXHeadersIdeal = column->WorkMinX + ImCeil(label_lines * line_off_step_x - line_off_for_align_x); + + while (label_name < label_name_end) + { + const char* label_name_eol = strchr(label_name, '\n'); + if (label_name_eol == NULL) + label_name_eol = label_name_end; + + // FIXME: Individual line clipping for right-most column is broken for negative angles. + ImVec2 label_size = CalcTextSize(label_name, label_name_eol); + float clip_width = max_label_width - padding.y; // Using padding.y*2.0f would be symmetrical but hide more text. + float clip_height = ImMin(label_size.y, column->ClipRect.Max.x - column->WorkMinX - line_off_curr_x); + ImRect clip_r(window->ClipRect.Min, window->ClipRect.Min + ImVec2(clip_width, clip_height)); + int vtx_idx_begin = draw_list->_VtxCurrentIdx; + PushStyleColor(ImGuiCol_Text, request->TextColor); + RenderTextEllipsis(draw_list, clip_r.Min, clip_r.Max, clip_r.Max.x, clip_r.Max.x, label_name, label_name_eol, &label_size); + PopStyleColor(); + int vtx_idx_end = draw_list->_VtxCurrentIdx; + + // Up<>Down alignment + const float available_space = ImMax(clip_width - label_size.x + ImAbs(padding.x * cos_a) * 2.0f - ImAbs(padding.y * sin_a) * 2.0f, 0.0f); + const float vertical_offset = available_space * align.y * (flip_label ? -1.0f : 1.0f); + + // Rotate and offset label + ImVec2 pivot_in = ImVec2(window->ClipRect.Min.x - vertical_offset, window->ClipRect.Min.y + label_size.y); + ImVec2 pivot_out = ImVec2(column->WorkMinX, row_r.Max.y); + line_off_curr_x += flip_label ? -line_off_step_x : line_off_step_x; + pivot_out += unit_right * padding.y; + if (flip_label) + pivot_out += unit_right * (clip_width - ImMax(0.0f, clip_width - label_size.x)); + pivot_out.x += flip_label ? line_off_curr_x + line_off_step_x : line_off_curr_x; + ShadeVertsTransformPos(draw_list, vtx_idx_begin, vtx_idx_end, pivot_in, label_cos_a, label_sin_a, pivot_out); // Rotate and offset + //if (g.IO.KeyShift) { ImDrawList* fg_dl = GetForegroundDrawList(); vtx_idx_begin = fg_dl->_VtxCurrentIdx; fg_dl->AddRect(clip_r.Min, clip_r.Max, IM_COL32(0, 255, 0, 255), 0.0f, 0, 1.0f); ShadeVertsTransformPos(fg_dl, vtx_idx_begin, fg_dl->_VtxCurrentIdx, pivot_in, label_cos_a, label_sin_a, pivot_out); } + + label_name = label_name_eol + 1; + } + } + if (pass == 1) + { + // Draw border + draw_list->AddLine(bg_shape[0], bg_shape[3], TableGetColumnBorderCol(table, order_n, column_n)); + } + } + PopClipRect(); + PopClipRect(); + table->TempData->AngledHeadersExtraWidth = ImMax(0.0f, max_x - table->Columns[table->RightMostEnabledColumn].MaxX); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Context Menu +//------------------------------------------------------------------------- +// - TableOpenContextMenu() [Internal] +// - TableBeginContextMenuPopup() [Internal] +// - TableDrawDefaultContextMenu() [Internal] +//------------------------------------------------------------------------- + +// Use -1 to open menu not specific to a given column. +void ImGui::TableOpenContextMenu(int column_n) +{ + ImGuiContext& g = *GImGui; + ImGuiTable* table = g.CurrentTable; + if (column_n == -1 && table->CurrentColumn != -1) // When called within a column automatically use this one (for consistency) + column_n = table->CurrentColumn; + if (column_n == table->ColumnsCount) // To facilitate using with TableGetHoveredColumn() + column_n = -1; + IM_ASSERT(column_n >= -1 && column_n < table->ColumnsCount); + if (table->Flags & (ImGuiTableFlags_Resizable | ImGuiTableFlags_Reorderable | ImGuiTableFlags_Hideable)) + { + table->IsContextPopupOpen = true; + table->ContextPopupColumn = (ImGuiTableColumnIdx)column_n; + table->InstanceInteracted = table->InstanceCurrent; + const ImGuiID context_menu_id = ImHashStr("##ContextMenu", 0, table->ID); + OpenPopupEx(context_menu_id, ImGuiPopupFlags_None); + } +} + +bool ImGui::TableBeginContextMenuPopup(ImGuiTable* table) +{ + if (!table->IsContextPopupOpen || table->InstanceCurrent != table->InstanceInteracted) + return false; + const ImGuiID context_menu_id = ImHashStr("##ContextMenu", 0, table->ID); + if (BeginPopupEx(context_menu_id, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings)) + return true; + table->IsContextPopupOpen = false; + return false; +} + +// Output context menu into current window (generally a popup) +// FIXME-TABLE: Ideally this should be writable by the user. Full programmatic access to that data? +// Sections to display are pulled from 'flags_for_section_to_display', which is typically == table->Flags. +// - ImGuiTableFlags_Resizable -> display Sizing menu items +// - ImGuiTableFlags_Reorderable -> display "Reset Order" +////- ImGuiTableFlags_Sortable -> display sorting options (disabled) +// - ImGuiTableFlags_Hideable -> display columns visibility menu items +// It means if you have a custom context menus you can call this section and omit some sections, and add your own. +void ImGui::TableDrawDefaultContextMenu(ImGuiTable* table, ImGuiTableFlags flags_for_section_to_display) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + bool want_separator = false; + const int column_n = (table->ContextPopupColumn >= 0 && table->ContextPopupColumn < table->ColumnsCount) ? table->ContextPopupColumn : -1; + ImGuiTableColumn* column = (column_n != -1) ? &table->Columns[column_n] : NULL; + + // Sizing + if (flags_for_section_to_display & ImGuiTableFlags_Resizable) + { + if (column != NULL) + { + const bool can_resize = !(column->Flags & ImGuiTableColumnFlags_NoResize) && column->IsEnabled; + if (MenuItem(LocalizeGetMsg(ImGuiLocKey_TableSizeOne), NULL, false, can_resize)) // "###SizeOne" + TableSetColumnWidthAutoSingle(table, column_n); + } + + const char* size_all_desc; + if (table->ColumnsEnabledFixedCount == table->ColumnsEnabledCount && (table->Flags & ImGuiTableFlags_SizingMask_) != ImGuiTableFlags_SizingFixedSame) + size_all_desc = LocalizeGetMsg(ImGuiLocKey_TableSizeAllFit); // "###SizeAll" All fixed + else + size_all_desc = LocalizeGetMsg(ImGuiLocKey_TableSizeAllDefault); // "###SizeAll" All stretch or mixed + if (MenuItem(size_all_desc, NULL)) + TableSetColumnWidthAutoAll(table); + want_separator = true; + } + + // Ordering + if (flags_for_section_to_display & ImGuiTableFlags_Reorderable) + { + if (MenuItem(LocalizeGetMsg(ImGuiLocKey_TableResetOrder), NULL, false, !table->IsDefaultDisplayOrder)) + table->IsResetDisplayOrderRequest = true; + want_separator = true; + } + + // Reset all (should work but seems unnecessary/noisy to expose?) + //if (MenuItem("Reset all")) + // table->IsResetAllRequest = true; + + // Sorting + // (modify TableOpenContextMenu() to add _Sortable flag if enabling this) +#if 0 + if ((flags_for_section_to_display & ImGuiTableFlags_Sortable) && column != NULL && (column->Flags & ImGuiTableColumnFlags_NoSort) == 0) + { + if (want_separator) + Separator(); + want_separator = true; + + bool append_to_sort_specs = g.IO.KeyShift; + if (MenuItem("Sort in Ascending Order", NULL, column->SortOrder != -1 && column->SortDirection == ImGuiSortDirection_Ascending, (column->Flags & ImGuiTableColumnFlags_NoSortAscending) == 0)) + TableSetColumnSortDirection(table, column_n, ImGuiSortDirection_Ascending, append_to_sort_specs); + if (MenuItem("Sort in Descending Order", NULL, column->SortOrder != -1 && column->SortDirection == ImGuiSortDirection_Descending, (column->Flags & ImGuiTableColumnFlags_NoSortDescending) == 0)) + TableSetColumnSortDirection(table, column_n, ImGuiSortDirection_Descending, append_to_sort_specs); + } +#endif + + // Hiding / Visibility + if (flags_for_section_to_display & ImGuiTableFlags_Hideable) + { + if (want_separator) + Separator(); + want_separator = true; + + PushItemFlag(ImGuiItemFlags_AutoClosePopups, false); + for (int other_column_n = 0; other_column_n < table->ColumnsCount; other_column_n++) + { + ImGuiTableColumn* other_column = &table->Columns[other_column_n]; + if (other_column->Flags & ImGuiTableColumnFlags_Disabled) + continue; + + const char* name = TableGetColumnName(table, other_column_n); + if (name == NULL || name[0] == 0) + name = ""; + + // Make sure we can't hide the last active column + bool menu_item_active = (other_column->Flags & ImGuiTableColumnFlags_NoHide) ? false : true; + if (other_column->IsUserEnabled && table->ColumnsEnabledCount <= 1) + menu_item_active = false; + if (MenuItem(name, NULL, other_column->IsUserEnabled, menu_item_active)) + other_column->IsUserEnabledNextFrame = !other_column->IsUserEnabled; + } + PopItemFlag(); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Settings (.ini data) +//------------------------------------------------------------------------- +// FIXME: The binding/finding/creating flow are too confusing. +//------------------------------------------------------------------------- +// - TableSettingsInit() [Internal] +// - TableSettingsCalcChunkSize() [Internal] +// - TableSettingsCreate() [Internal] +// - TableSettingsFindByID() [Internal] +// - TableGetBoundSettings() [Internal] +// - TableResetSettings() +// - TableSaveSettings() [Internal] +// - TableLoadSettings() [Internal] +// - TableSettingsHandler_ClearAll() [Internal] +// - TableSettingsHandler_ApplyAll() [Internal] +// - TableSettingsHandler_ReadOpen() [Internal] +// - TableSettingsHandler_ReadLine() [Internal] +// - TableSettingsHandler_WriteAll() [Internal] +// - TableSettingsInstallHandler() [Internal] +//------------------------------------------------------------------------- +// [Init] 1: TableSettingsHandler_ReadXXXX() Load and parse .ini file into TableSettings. +// [Main] 2: TableLoadSettings() When table is created, bind Table to TableSettings, serialize TableSettings data into Table. +// [Main] 3: TableSaveSettings() When table properties are modified, serialize Table data into bound or new TableSettings, mark .ini as dirty. +// [Main] 4: TableSettingsHandler_WriteAll() When .ini file is dirty (which can come from other source), save TableSettings into .ini file. +//------------------------------------------------------------------------- + +// Clear and initialize empty settings instance +static void TableSettingsInit(ImGuiTableSettings* settings, ImGuiID id, int columns_count, int columns_count_max) +{ + IM_PLACEMENT_NEW(settings) ImGuiTableSettings(); + ImGuiTableColumnSettings* settings_column = settings->GetColumnSettings(); + for (int n = 0; n < columns_count_max; n++, settings_column++) + IM_PLACEMENT_NEW(settings_column) ImGuiTableColumnSettings(); + settings->ID = id; + settings->ColumnsCount = (ImGuiTableColumnIdx)columns_count; + settings->ColumnsCountMax = (ImGuiTableColumnIdx)columns_count_max; + settings->WantApply = true; +} + +static size_t TableSettingsCalcChunkSize(int columns_count) +{ + return sizeof(ImGuiTableSettings) + (size_t)columns_count * sizeof(ImGuiTableColumnSettings); +} + +ImGuiTableSettings* ImGui::TableSettingsCreate(ImGuiID id, int columns_count) +{ + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.alloc_chunk(TableSettingsCalcChunkSize(columns_count)); + TableSettingsInit(settings, id, columns_count, columns_count); + return settings; +} + +// Find existing settings +ImGuiTableSettings* ImGui::TableSettingsFindByID(ImGuiID id) +{ + // FIXME-OPT: Might want to store a lookup map for this? + ImGuiContext& g = *GImGui; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID == id) + return settings; + return NULL; +} + +// Get settings for a given table, NULL if none +ImGuiTableSettings* ImGui::TableGetBoundSettings(ImGuiTable* table) +{ + if (table->SettingsOffset != -1) + { + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = g.SettingsTables.ptr_from_offset(table->SettingsOffset); + IM_ASSERT(settings->ID == table->ID); + if (settings->ColumnsCountMax >= table->ColumnsCount) + return settings; // OK + settings->ID = 0; // Invalidate storage, we won't fit because of a count change + } + return NULL; +} + +// Restore initial state of table (with or without saved settings) +void ImGui::TableResetSettings(ImGuiTable* table) +{ + table->IsInitializing = table->IsSettingsDirty = true; + table->IsResetAllRequest = false; + table->IsSettingsRequestLoad = false; // Don't reload from ini + table->SettingsLoadedFlags = ImGuiTableFlags_None; // Mark as nothing loaded so our initialized data becomes authoritative +} + +void ImGui::TableSaveSettings(ImGuiTable* table) +{ + table->IsSettingsDirty = false; + if (table->Flags & ImGuiTableFlags_NoSavedSettings) + return; + + // Bind or create settings data + ImGuiContext& g = *GImGui; + ImGuiTableSettings* settings = TableGetBoundSettings(table); + if (settings == NULL) + { + settings = TableSettingsCreate(table->ID, table->ColumnsCount); + table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); + } + settings->ColumnsCount = (ImGuiTableColumnIdx)table->ColumnsCount; + + // Serialize ImGuiTable/ImGuiTableColumn into ImGuiTableSettings/ImGuiTableColumnSettings + IM_ASSERT(settings->ID == table->ID); + IM_ASSERT(settings->ColumnsCount == table->ColumnsCount && settings->ColumnsCountMax >= settings->ColumnsCount); + ImGuiTableColumn* column = table->Columns.Data; + ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); + + bool save_ref_scale = false; + settings->SaveFlags = ImGuiTableFlags_None; + for (int n = 0; n < table->ColumnsCount; n++, column++, column_settings++) + { + const float width_or_weight = (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? column->StretchWeight : column->WidthRequest; + column_settings->WidthOrWeight = width_or_weight; + column_settings->Index = (ImGuiTableColumnIdx)n; + column_settings->DisplayOrder = column->DisplayOrder; + column_settings->SortOrder = column->SortOrder; + column_settings->SortDirection = column->SortDirection; + column_settings->IsEnabled = column->IsUserEnabled; + column_settings->IsStretch = (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? 1 : 0; + if ((column->Flags & ImGuiTableColumnFlags_WidthStretch) == 0) + save_ref_scale = true; + + // We skip saving some data in the .ini file when they are unnecessary to restore our state. + // Note that fixed width where initial width was derived from auto-fit will always be saved as InitStretchWeightOrWidth will be 0.0f. + // FIXME-TABLE: We don't have logic to easily compare SortOrder to DefaultSortOrder yet so it's always saved when present. + if (width_or_weight != column->InitStretchWeightOrWidth) + settings->SaveFlags |= ImGuiTableFlags_Resizable; + if (column->DisplayOrder != n) + settings->SaveFlags |= ImGuiTableFlags_Reorderable; + if (column->SortOrder != -1) + settings->SaveFlags |= ImGuiTableFlags_Sortable; + if (column->IsUserEnabled != ((column->Flags & ImGuiTableColumnFlags_DefaultHide) == 0)) + settings->SaveFlags |= ImGuiTableFlags_Hideable; + } + settings->SaveFlags &= table->Flags; + settings->RefScale = save_ref_scale ? table->RefScale : 0.0f; + + MarkIniSettingsDirty(); +} + +void ImGui::TableLoadSettings(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + table->IsSettingsRequestLoad = false; + if (table->Flags & ImGuiTableFlags_NoSavedSettings) + return; + + // Bind settings + ImGuiTableSettings* settings; + if (table->SettingsOffset == -1) + { + settings = TableSettingsFindByID(table->ID); + if (settings == NULL) + return; + if (settings->ColumnsCount != table->ColumnsCount) // Allow settings if columns count changed. We could otherwise decide to return... + table->IsSettingsDirty = true; + table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); + } + else + { + settings = TableGetBoundSettings(table); + } + + table->SettingsLoadedFlags = settings->SaveFlags; + table->RefScale = settings->RefScale; + + // Initialize default columns settings + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + { + ImGuiTableColumn* column = &table->Columns[column_n]; + TableInitColumnDefaults(table, column, ~0); + column->AutoFitQueue = 0x00; + } + + // Serialize ImGuiTableSettings/ImGuiTableColumnSettings into ImGuiTable/ImGuiTableColumn + ImGuiTableColumnSettings* column_settings = settings->GetColumnSettings(); + ImU64 display_order_mask = 0; + for (int data_n = 0; data_n < settings->ColumnsCount; data_n++, column_settings++) + { + int column_n = column_settings->Index; + if (column_n < 0 || column_n >= table->ColumnsCount) + continue; + + ImGuiTableColumn* column = &table->Columns[column_n]; + if (settings->SaveFlags & ImGuiTableFlags_Resizable) + { + if (column_settings->IsStretch) + column->StretchWeight = column_settings->WidthOrWeight; + else + column->WidthRequest = column_settings->WidthOrWeight; + } + if (settings->SaveFlags & ImGuiTableFlags_Reorderable) + column->DisplayOrder = column_settings->DisplayOrder; + display_order_mask |= (ImU64)1 << column->DisplayOrder; + if ((settings->SaveFlags & ImGuiTableFlags_Hideable) && column_settings->IsEnabled != -1) + column->IsUserEnabled = column->IsUserEnabledNextFrame = column_settings->IsEnabled == 1; + column->SortOrder = column_settings->SortOrder; + column->SortDirection = column_settings->SortDirection; + } + + // Validate and fix invalid display order data + const ImU64 expected_display_order_mask = (settings->ColumnsCount == 64) ? ~0 : ((ImU64)1 << settings->ColumnsCount) - 1; + if (display_order_mask != expected_display_order_mask) + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->Columns[column_n].DisplayOrder = (ImGuiTableColumnIdx)column_n; + + // Rebuild index + for (int column_n = 0; column_n < table->ColumnsCount; column_n++) + table->DisplayOrderToIndex[table->Columns[column_n].DisplayOrder] = (ImGuiTableColumnIdx)column_n; +} + +static void TableSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Tables.GetMapSize(); i++) + if (ImGuiTable* table = g.Tables.TryGetMapData(i)) + table->SettingsOffset = -1; + g.SettingsTables.clear(); +} + +// Apply to existing windows (if any) +static void TableSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) +{ + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Tables.GetMapSize(); i++) + if (ImGuiTable* table = g.Tables.TryGetMapData(i)) + { + table->IsSettingsRequestLoad = true; + table->SettingsOffset = -1; + } +} + +static void* TableSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) +{ + ImGuiID id = 0; + int columns_count = 0; + if (sscanf(name, "0x%08X,%d", &id, &columns_count) < 2) + return NULL; + + if (ImGuiTableSettings* settings = ImGui::TableSettingsFindByID(id)) + { + if (settings->ColumnsCountMax >= columns_count) + { + TableSettingsInit(settings, id, columns_count, settings->ColumnsCountMax); // Recycle + return settings; + } + settings->ID = 0; // Invalidate storage, we won't fit because of a count change + } + return ImGui::TableSettingsCreate(id, columns_count); +} + +static void TableSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line) +{ + // "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v" + ImGuiTableSettings* settings = (ImGuiTableSettings*)entry; + float f = 0.0f; + int column_n = 0, r = 0, n = 0; + + if (sscanf(line, "RefScale=%f", &f) == 1) { settings->RefScale = f; return; } + + if (sscanf(line, "Column %d%n", &column_n, &r) == 1) + { + if (column_n < 0 || column_n >= settings->ColumnsCount) + return; + line = ImStrSkipBlank(line + r); + char c = 0; + ImGuiTableColumnSettings* column = settings->GetColumnSettings() + column_n; + column->Index = (ImGuiTableColumnIdx)column_n; + if (sscanf(line, "UserID=0x%08X%n", (ImU32*)&n, &r)==1) { line = ImStrSkipBlank(line + r); column->UserID = (ImGuiID)n; } + if (sscanf(line, "Width=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->WidthOrWeight = (float)n; column->IsStretch = 0; settings->SaveFlags |= ImGuiTableFlags_Resizable; } + if (sscanf(line, "Weight=%f%n", &f, &r) == 1) { line = ImStrSkipBlank(line + r); column->WidthOrWeight = f; column->IsStretch = 1; settings->SaveFlags |= ImGuiTableFlags_Resizable; } + if (sscanf(line, "Visible=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->IsEnabled = (ImU8)n; settings->SaveFlags |= ImGuiTableFlags_Hideable; } + if (sscanf(line, "Order=%d%n", &n, &r) == 1) { line = ImStrSkipBlank(line + r); column->DisplayOrder = (ImGuiTableColumnIdx)n; settings->SaveFlags |= ImGuiTableFlags_Reorderable; } + if (sscanf(line, "Sort=%d%c%n", &n, &c, &r) == 2) { line = ImStrSkipBlank(line + r); column->SortOrder = (ImGuiTableColumnIdx)n; column->SortDirection = (c == '^') ? ImGuiSortDirection_Descending : ImGuiSortDirection_Ascending; settings->SaveFlags |= ImGuiTableFlags_Sortable; } + } +} + +static void TableSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* buf) +{ + ImGuiContext& g = *ctx; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + { + if (settings->ID == 0) // Skip ditched settings + continue; + + // TableSaveSettings() may clear some of those flags when we establish that the data can be stripped + // (e.g. Order was unchanged) + const bool save_size = (settings->SaveFlags & ImGuiTableFlags_Resizable) != 0; + const bool save_visible = (settings->SaveFlags & ImGuiTableFlags_Hideable) != 0; + const bool save_order = (settings->SaveFlags & ImGuiTableFlags_Reorderable) != 0; + const bool save_sort = (settings->SaveFlags & ImGuiTableFlags_Sortable) != 0; + // We need to save the [Table] entry even if all the bools are false, since this records a table with "default settings". + + buf->reserve(buf->size() + 30 + settings->ColumnsCount * 50); // ballpark reserve + buf->appendf("[%s][0x%08X,%d]\n", handler->TypeName, settings->ID, settings->ColumnsCount); + if (settings->RefScale != 0.0f) + buf->appendf("RefScale=%g\n", settings->RefScale); + ImGuiTableColumnSettings* column = settings->GetColumnSettings(); + for (int column_n = 0; column_n < settings->ColumnsCount; column_n++, column++) + { + // "Column 0 UserID=0x42AD2D21 Width=100 Visible=1 Order=0 Sort=0v" + bool save_column = column->UserID != 0 || save_size || save_visible || save_order || (save_sort && column->SortOrder != -1); + if (!save_column) + continue; + buf->appendf("Column %-2d", column_n); + if (column->UserID != 0) { buf->appendf(" UserID=%08X", column->UserID); } + if (save_size && column->IsStretch) { buf->appendf(" Weight=%.4f", column->WidthOrWeight); } + if (save_size && !column->IsStretch) { buf->appendf(" Width=%d", (int)column->WidthOrWeight); } + if (save_visible) { buf->appendf(" Visible=%d", column->IsEnabled); } + if (save_order) { buf->appendf(" Order=%d", column->DisplayOrder); } + if (save_sort && column->SortOrder != -1) { buf->appendf(" Sort=%d%c", column->SortOrder, (column->SortDirection == ImGuiSortDirection_Ascending) ? 'v' : '^'); } + buf->append("\n"); + } + buf->append("\n"); + } +} + +void ImGui::TableSettingsAddSettingsHandler() +{ + ImGuiSettingsHandler ini_handler; + ini_handler.TypeName = "Table"; + ini_handler.TypeHash = ImHashStr("Table"); + ini_handler.ClearAllFn = TableSettingsHandler_ClearAll; + ini_handler.ReadOpenFn = TableSettingsHandler_ReadOpen; + ini_handler.ReadLineFn = TableSettingsHandler_ReadLine; + ini_handler.ApplyAllFn = TableSettingsHandler_ApplyAll; + ini_handler.WriteAllFn = TableSettingsHandler_WriteAll; + AddSettingsHandler(&ini_handler); +} + +//------------------------------------------------------------------------- +// [SECTION] Tables: Garbage Collection +//------------------------------------------------------------------------- +// - TableRemove() [Internal] +// - TableGcCompactTransientBuffers() [Internal] +// - TableGcCompactSettings() [Internal] +//------------------------------------------------------------------------- + +// Remove Table (currently only used by TestEngine) +void ImGui::TableRemove(ImGuiTable* table) +{ + //IMGUI_DEBUG_PRINT("TableRemove() id=0x%08X\n", table->ID); + ImGuiContext& g = *GImGui; + int table_idx = g.Tables.GetIndex(table); + //memset(table->RawData.Data, 0, table->RawData.size_in_bytes()); + //memset(table, 0, sizeof(ImGuiTable)); + g.Tables.Remove(table->ID, table); + g.TablesLastTimeActive[table_idx] = -1.0f; +} + +// Free up/compact internal Table buffers for when it gets unused +void ImGui::TableGcCompactTransientBuffers(ImGuiTable* table) +{ + //IMGUI_DEBUG_PRINT("TableGcCompactTransientBuffers() id=0x%08X\n", table->ID); + ImGuiContext& g = *GImGui; + IM_ASSERT(table->MemoryCompacted == false); + table->SortSpecs.Specs = NULL; + table->SortSpecsMulti.clear(); + table->IsSortSpecsDirty = true; // FIXME: In theory shouldn't have to leak into user performing a sort on resume. + table->ColumnsNames.clear(); + table->MemoryCompacted = true; + for (int n = 0; n < table->ColumnsCount; n++) + table->Columns[n].NameOffset = -1; + g.TablesLastTimeActive[g.Tables.GetIndex(table)] = -1.0f; +} + +void ImGui::TableGcCompactTransientBuffers(ImGuiTableTempData* temp_data) +{ + temp_data->DrawSplitter.ClearFreeMemory(); + temp_data->LastTimeActive = -1.0f; +} + +// Compact and remove unused settings data (currently only used by TestEngine) +void ImGui::TableGcCompactSettings() +{ + ImGuiContext& g = *GImGui; + int required_memory = 0; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID != 0) + required_memory += (int)TableSettingsCalcChunkSize(settings->ColumnsCount); + if (required_memory == g.SettingsTables.Buf.Size) + return; + ImChunkStream new_chunk_stream; + new_chunk_stream.Buf.reserve(required_memory); + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + if (settings->ID != 0) + memcpy(new_chunk_stream.alloc_chunk(TableSettingsCalcChunkSize(settings->ColumnsCount)), settings, TableSettingsCalcChunkSize(settings->ColumnsCount)); + g.SettingsTables.swap(new_chunk_stream); +} + + +//------------------------------------------------------------------------- +// [SECTION] Tables: Debugging +//------------------------------------------------------------------------- +// - DebugNodeTable() [Internal] +//------------------------------------------------------------------------- + +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + +static const char* DebugNodeTableGetSizingPolicyDesc(ImGuiTableFlags sizing_policy) +{ + sizing_policy &= ImGuiTableFlags_SizingMask_; + if (sizing_policy == ImGuiTableFlags_SizingFixedFit) { return "FixedFit"; } + if (sizing_policy == ImGuiTableFlags_SizingFixedSame) { return "FixedSame"; } + if (sizing_policy == ImGuiTableFlags_SizingStretchProp) { return "StretchProp"; } + if (sizing_policy == ImGuiTableFlags_SizingStretchSame) { return "StretchSame"; } + return "N/A"; +} + +void ImGui::DebugNodeTable(ImGuiTable* table) +{ + ImGuiContext& g = *GImGui; + const bool is_active = (table->LastFrameActive >= g.FrameCount - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode(table, "Table 0x%08X (%d columns, in '%s')%s", table->ID, table->ColumnsCount, table->OuterWindow->Name, is_active ? "" : " *Inactive*"); + if (!is_active) { PopStyleColor(); } + if (IsItemHovered()) + GetForegroundDrawList()->AddRect(table->OuterRect.Min, table->OuterRect.Max, IM_COL32(255, 255, 0, 255)); + if (IsItemVisible() && table->HoveredColumnBody != -1) + GetForegroundDrawList()->AddRect(GetItemRectMin(), GetItemRectMax(), IM_COL32(255, 255, 0, 255)); + if (!open) + return; + if (table->InstanceCurrent > 0) + Text("** %d instances of same table! Some data below will refer to last instance.", table->InstanceCurrent + 1); + if (g.IO.ConfigDebugIsDebuggerPresent) + { + if (DebugBreakButton("**DebugBreak**", "in BeginTable()")) + g.DebugBreakInTable = table->ID; + SameLine(); + } + + bool clear_settings = SmallButton("Clear settings"); + BulletText("OuterRect: Pos: (%.1f,%.1f) Size: (%.1f,%.1f) Sizing: '%s'", table->OuterRect.Min.x, table->OuterRect.Min.y, table->OuterRect.GetWidth(), table->OuterRect.GetHeight(), DebugNodeTableGetSizingPolicyDesc(table->Flags)); + BulletText("ColumnsGivenWidth: %.1f, ColumnsAutoFitWidth: %.1f, InnerWidth: %.1f%s", table->ColumnsGivenWidth, table->ColumnsAutoFitWidth, table->InnerWidth, table->InnerWidth == 0.0f ? " (auto)" : ""); + BulletText("CellPaddingX: %.1f, CellSpacingX: %.1f/%.1f, OuterPaddingX: %.1f", table->CellPaddingX, table->CellSpacingX1, table->CellSpacingX2, table->OuterPaddingX); + BulletText("HoveredColumnBody: %d, HoveredColumnBorder: %d", table->HoveredColumnBody, table->HoveredColumnBorder); + BulletText("ResizedColumn: %d, ReorderColumn: %d, HeldHeaderColumn: %d", table->ResizedColumn, table->ReorderColumn, table->HeldHeaderColumn); + for (int n = 0; n < table->InstanceCurrent + 1; n++) + { + ImGuiTableInstanceData* table_instance = TableGetInstanceData(table, n); + BulletText("Instance %d: HoveredRow: %d, LastOuterHeight: %.2f", n, table_instance->HoveredRowLast, table_instance->LastOuterHeight); + } + //BulletText("BgDrawChannels: %d/%d", 0, table->BgDrawChannelUnfrozen); + float sum_weights = 0.0f; + for (int n = 0; n < table->ColumnsCount; n++) + if (table->Columns[n].Flags & ImGuiTableColumnFlags_WidthStretch) + sum_weights += table->Columns[n].StretchWeight; + for (int n = 0; n < table->ColumnsCount; n++) + { + ImGuiTableColumn* column = &table->Columns[n]; + const char* name = TableGetColumnName(table, n); + char buf[512]; + ImFormatString(buf, IM_ARRAYSIZE(buf), + "Column %d order %d '%s': offset %+.2f to %+.2f%s\n" + "Enabled: %d, VisibleX/Y: %d/%d, RequestOutput: %d, SkipItems: %d, DrawChannels: %d,%d\n" + "WidthGiven: %.1f, Request/Auto: %.1f/%.1f, StretchWeight: %.3f (%.1f%%)\n" + "MinX: %.1f, MaxX: %.1f (%+.1f), ClipRect: %.1f to %.1f (+%.1f)\n" + "ContentWidth: %.1f,%.1f, HeadersUsed/Ideal %.1f/%.1f\n" + "Sort: %d%s, UserID: 0x%08X, Flags: 0x%04X: %s%s%s..", + n, column->DisplayOrder, name, column->MinX - table->WorkRect.Min.x, column->MaxX - table->WorkRect.Min.x, (n < table->FreezeColumnsRequest) ? " (Frozen)" : "", + column->IsEnabled, column->IsVisibleX, column->IsVisibleY, column->IsRequestOutput, column->IsSkipItems, column->DrawChannelFrozen, column->DrawChannelUnfrozen, + column->WidthGiven, column->WidthRequest, column->WidthAuto, column->StretchWeight, column->StretchWeight > 0.0f ? (column->StretchWeight / sum_weights) * 100.0f : 0.0f, + column->MinX, column->MaxX, column->MaxX - column->MinX, column->ClipRect.Min.x, column->ClipRect.Max.x, column->ClipRect.Max.x - column->ClipRect.Min.x, + column->ContentMaxXFrozen - column->WorkMinX, column->ContentMaxXUnfrozen - column->WorkMinX, column->ContentMaxXHeadersUsed - column->WorkMinX, column->ContentMaxXHeadersIdeal - column->WorkMinX, + column->SortOrder, (column->SortDirection == ImGuiSortDirection_Ascending) ? " (Asc)" : (column->SortDirection == ImGuiSortDirection_Descending) ? " (Des)" : "", column->UserID, column->Flags, + (column->Flags & ImGuiTableColumnFlags_WidthStretch) ? "WidthStretch " : "", + (column->Flags & ImGuiTableColumnFlags_WidthFixed) ? "WidthFixed " : "", + (column->Flags & ImGuiTableColumnFlags_NoResize) ? "NoResize " : ""); + Bullet(); + Selectable(buf); + if (IsItemHovered()) + { + ImRect r(column->MinX, table->OuterRect.Min.y, column->MaxX, table->OuterRect.Max.y); + GetForegroundDrawList()->AddRect(r.Min, r.Max, IM_COL32(255, 255, 0, 255)); + } + } + if (ImGuiTableSettings* settings = TableGetBoundSettings(table)) + DebugNodeTableSettings(settings); + if (clear_settings) + table->IsResetAllRequest = true; + TreePop(); +} + +void ImGui::DebugNodeTableSettings(ImGuiTableSettings* settings) +{ + if (!TreeNode((void*)(intptr_t)settings->ID, "Settings 0x%08X (%d columns)", settings->ID, settings->ColumnsCount)) + return; + BulletText("SaveFlags: 0x%08X", settings->SaveFlags); + BulletText("ColumnsCount: %d (max %d)", settings->ColumnsCount, settings->ColumnsCountMax); + for (int n = 0; n < settings->ColumnsCount; n++) + { + ImGuiTableColumnSettings* column_settings = &settings->GetColumnSettings()[n]; + ImGuiSortDirection sort_dir = (column_settings->SortOrder != -1) ? (ImGuiSortDirection)column_settings->SortDirection : ImGuiSortDirection_None; + BulletText("Column %d Order %d SortOrder %d %s Vis %d %s %7.3f UserID 0x%08X", + n, column_settings->DisplayOrder, column_settings->SortOrder, + (sort_dir == ImGuiSortDirection_Ascending) ? "Asc" : (sort_dir == ImGuiSortDirection_Descending) ? "Des" : "---", + column_settings->IsEnabled, column_settings->IsStretch ? "Weight" : "Width ", column_settings->WidthOrWeight, column_settings->UserID); + } + TreePop(); +} + +#else // #ifndef IMGUI_DISABLE_DEBUG_TOOLS + +void ImGui::DebugNodeTable(ImGuiTable*) {} +void ImGui::DebugNodeTableSettings(ImGuiTableSettings*) {} + +#endif + + +//------------------------------------------------------------------------- +// [SECTION] Columns, BeginColumns, EndColumns, etc. +// (This is a legacy API, prefer using BeginTable/EndTable!) +//------------------------------------------------------------------------- +// FIXME: sizing is lossy when columns width is very small (default width may turn negative etc.) +//------------------------------------------------------------------------- +// - SetWindowClipRectBeforeSetChannel() [Internal] +// - GetColumnIndex() +// - GetColumnsCount() +// - GetColumnOffset() +// - GetColumnWidth() +// - SetColumnOffset() +// - SetColumnWidth() +// - PushColumnClipRect() [Internal] +// - PushColumnsBackground() [Internal] +// - PopColumnsBackground() [Internal] +// - FindOrCreateColumns() [Internal] +// - GetColumnsID() [Internal] +// - BeginColumns() +// - NextColumn() +// - EndColumns() +// - Columns() +//------------------------------------------------------------------------- + +// [Internal] Small optimization to avoid calls to PopClipRect/SetCurrentChannel/PushClipRect in sequences, +// they would meddle many times with the underlying ImDrawCmd. +// Instead, we do a preemptive overwrite of clipping rectangle _without_ altering the command-buffer and let +// the subsequent single call to SetCurrentChannel() does it things once. +void ImGui::SetWindowClipRectBeforeSetChannel(ImGuiWindow* window, const ImRect& clip_rect) +{ + ImVec4 clip_rect_vec4 = clip_rect.ToVec4(); + window->ClipRect = clip_rect; + window->DrawList->_CmdHeader.ClipRect = clip_rect_vec4; + window->DrawList->_ClipRectStack.Data[window->DrawList->_ClipRectStack.Size - 1] = clip_rect_vec4; +} + +int ImGui::GetColumnIndex() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CurrentColumns ? window->DC.CurrentColumns->Current : 0; +} + +int ImGui::GetColumnsCount() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + return window->DC.CurrentColumns ? window->DC.CurrentColumns->Count : 1; +} + +float ImGui::GetColumnOffsetFromNorm(const ImGuiOldColumns* columns, float offset_norm) +{ + return offset_norm * (columns->OffMaxX - columns->OffMinX); +} + +float ImGui::GetColumnNormFromOffset(const ImGuiOldColumns* columns, float offset) +{ + return offset / (columns->OffMaxX - columns->OffMinX); +} + +static const float COLUMNS_HIT_RECT_HALF_THICKNESS = 4.0f; + +static float GetDraggedColumnOffset(ImGuiOldColumns* columns, int column_index) +{ + // Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing + // window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning. + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT(column_index > 0); // We are not supposed to drag column 0. + IM_ASSERT(g.ActiveId == columns->ID + ImGuiID(column_index)); + + float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + ImTrunc(COLUMNS_HIT_RECT_HALF_THICKNESS * g.CurrentDpiScale) - window->Pos.x; + x = ImMax(x, ImGui::GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing); + if ((columns->Flags & ImGuiOldColumnFlags_NoPreserveWidths)) + x = ImMin(x, ImGui::GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing); + + return x; +} + +float ImGui::GetColumnOffset(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns == NULL) + return 0.0f; + + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); + + const float t = columns->Columns[column_index].OffsetNorm; + const float x_offset = ImLerp(columns->OffMinX, columns->OffMaxX, t); + return x_offset; +} + +static float GetColumnWidthEx(ImGuiOldColumns* columns, int column_index, bool before_resize = false) +{ + if (column_index < 0) + column_index = columns->Current; + + float offset_norm; + if (before_resize) + offset_norm = columns->Columns[column_index + 1].OffsetNormBeforeResize - columns->Columns[column_index].OffsetNormBeforeResize; + else + offset_norm = columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm; + return ImGui::GetColumnOffsetFromNorm(columns, offset_norm); +} + +float ImGui::GetColumnWidth(int column_index) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns == NULL) + return GetContentRegionAvail().x; + + if (column_index < 0) + column_index = columns->Current; + return GetColumnOffsetFromNorm(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm); +} + +void ImGui::SetColumnOffset(int column_index, float offset) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + IM_ASSERT(column_index < columns->Columns.Size); + + const bool preserve_width = !(columns->Flags & ImGuiOldColumnFlags_NoPreserveWidths) && (column_index < columns->Count - 1); + const float width = preserve_width ? GetColumnWidthEx(columns, column_index, columns->IsBeingResized) : 0.0f; + + if (!(columns->Flags & ImGuiOldColumnFlags_NoForceWithinWindow)) + offset = ImMin(offset, columns->OffMaxX - g.Style.ColumnsMinSpacing * (columns->Count - column_index)); + columns->Columns[column_index].OffsetNorm = GetColumnNormFromOffset(columns, offset - columns->OffMinX); + + if (preserve_width) + SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width)); +} + +void ImGui::SetColumnWidth(int column_index, float width) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + if (column_index < 0) + column_index = columns->Current; + SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width); +} + +void ImGui::PushColumnClipRect(int column_index) +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (column_index < 0) + column_index = columns->Current; + + ImGuiOldColumnData* column = &columns->Columns[column_index]; + PushClipRect(column->ClipRect.Min, column->ClipRect.Max, false); +} + +// Get into the columns background draw command (which is generally the same draw command as before we called BeginColumns) +void ImGui::PushColumnsBackground() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns->Count == 1) + return; + + // Optimization: avoid SetCurrentChannel() + PushClipRect() + columns->HostBackupClipRect = window->ClipRect; + SetWindowClipRectBeforeSetChannel(window, columns->HostInitialClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, 0); +} + +void ImGui::PopColumnsBackground() +{ + ImGuiWindow* window = GetCurrentWindowRead(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns->Count == 1) + return; + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + SetWindowClipRectBeforeSetChannel(window, columns->HostBackupClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, columns->Current + 1); +} + +ImGuiOldColumns* ImGui::FindOrCreateColumns(ImGuiWindow* window, ImGuiID id) +{ + // We have few columns per window so for now we don't need bother much with turning this into a faster lookup. + for (int n = 0; n < window->ColumnsStorage.Size; n++) + if (window->ColumnsStorage[n].ID == id) + return &window->ColumnsStorage[n]; + + window->ColumnsStorage.push_back(ImGuiOldColumns()); + ImGuiOldColumns* columns = &window->ColumnsStorage.back(); + columns->ID = id; + return columns; +} + +ImGuiID ImGui::GetColumnsID(const char* str_id, int columns_count) +{ + ImGuiWindow* window = GetCurrentWindow(); + + // Differentiate column ID with an arbitrary prefix for cases where users name their columns set the same as another widget. + // In addition, when an identifier isn't explicitly provided we include the number of columns in the hash to make it uniquer. + PushID(0x11223347 + (str_id ? 0 : columns_count)); + ImGuiID id = window->GetID(str_id ? str_id : "columns"); + PopID(); + + return id; +} + +void ImGui::BeginColumns(const char* str_id, int columns_count, ImGuiOldColumnFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + + IM_ASSERT(columns_count >= 1); + IM_ASSERT(window->DC.CurrentColumns == NULL); // Nested columns are currently not supported + + // Acquire storage for the columns set + ImGuiID id = GetColumnsID(str_id, columns_count); + ImGuiOldColumns* columns = FindOrCreateColumns(window, id); + IM_ASSERT(columns->ID == id); + columns->Current = 0; + columns->Count = columns_count; + columns->Flags = flags; + window->DC.CurrentColumns = columns; + window->DC.NavIsScrollPushableX = false; // Shortcut for NavUpdateCurrentWindowIsScrollPushableX(); + + columns->HostCursorPosY = window->DC.CursorPos.y; + columns->HostCursorMaxPosX = window->DC.CursorMaxPos.x; + columns->HostInitialClipRect = window->ClipRect; + columns->HostBackupParentWorkRect = window->ParentWorkRect; + window->ParentWorkRect = window->WorkRect; + + // Set state for first column + // We aim so that the right-most column will have the same clipping width as other after being clipped by parent ClipRect + const float column_padding = g.Style.ItemSpacing.x; + const float half_clip_extend_x = ImTrunc(ImMax(window->WindowPadding.x * 0.5f, window->WindowBorderSize)); + const float max_1 = window->WorkRect.Max.x + column_padding - ImMax(column_padding - window->WindowPadding.x, 0.0f); + const float max_2 = window->WorkRect.Max.x + half_clip_extend_x; + columns->OffMinX = window->DC.Indent.x - column_padding + ImMax(column_padding - window->WindowPadding.x, 0.0f); + columns->OffMaxX = ImMax(ImMin(max_1, max_2) - window->Pos.x, columns->OffMinX + 1.0f); + columns->LineMinY = columns->LineMaxY = window->DC.CursorPos.y; + + // Clear data if columns count changed + if (columns->Columns.Size != 0 && columns->Columns.Size != columns_count + 1) + columns->Columns.resize(0); + + // Initialize default widths + columns->IsFirstFrame = (columns->Columns.Size == 0); + if (columns->Columns.Size == 0) + { + columns->Columns.reserve(columns_count + 1); + for (int n = 0; n < columns_count + 1; n++) + { + ImGuiOldColumnData column; + column.OffsetNorm = n / (float)columns_count; + columns->Columns.push_back(column); + } + } + + for (int n = 0; n < columns_count; n++) + { + // Compute clipping rectangle + ImGuiOldColumnData* column = &columns->Columns[n]; + float clip_x1 = IM_ROUND(window->Pos.x + GetColumnOffset(n)); + float clip_x2 = IM_ROUND(window->Pos.x + GetColumnOffset(n + 1) - 1.0f); + column->ClipRect = ImRect(clip_x1, -FLT_MAX, clip_x2, +FLT_MAX); + column->ClipRect.ClipWithFull(window->ClipRect); + } + + if (columns->Count > 1) + { + columns->Splitter.Split(window->DrawList, 1 + columns->Count); + columns->Splitter.SetCurrentChannel(window->DrawList, 1); + PushColumnClipRect(0); + } + + // We don't generally store Indent.x inside ColumnsOffset because it may be manipulated by the user. + float offset_0 = GetColumnOffset(columns->Current); + float offset_1 = GetColumnOffset(columns->Current + 1); + float width = offset_1 - offset_0; + PushItemWidth(width * 0.65f); + window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; + window->WorkRect.Max.y = window->ContentRegionRect.Max.y; +} + +void ImGui::NextColumn() +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems || window->DC.CurrentColumns == NULL) + return; + + ImGuiContext& g = *GImGui; + ImGuiOldColumns* columns = window->DC.CurrentColumns; + + if (columns->Count == 1) + { + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + IM_ASSERT(columns->Current == 0); + return; + } + + // Next column + if (++columns->Current == columns->Count) + columns->Current = 0; + + PopItemWidth(); + + // Optimization: avoid PopClipRect() + SetCurrentChannel() + PushClipRect() + // (which would needlessly attempt to update commands in the wrong channel, then pop or overwrite them), + ImGuiOldColumnData* column = &columns->Columns[columns->Current]; + SetWindowClipRectBeforeSetChannel(window, column->ClipRect); + columns->Splitter.SetCurrentChannel(window->DrawList, columns->Current + 1); + + const float column_padding = g.Style.ItemSpacing.x; + columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y); + if (columns->Current > 0) + { + // Columns 1+ ignore IndentX (by canceling it out) + // FIXME-COLUMNS: Unnecessary, could be locked? + window->DC.ColumnsOffset.x = GetColumnOffset(columns->Current) - window->DC.Indent.x + column_padding; + } + else + { + // New row/line: column 0 honor IndentX. + window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); + window->DC.IsSameLine = false; + columns->LineMinY = columns->LineMaxY; + } + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + window->DC.CursorPos.y = columns->LineMinY; + window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); + window->DC.CurrLineTextBaseOffset = 0.0f; + + // FIXME-COLUMNS: Share code with BeginColumns() - move code on columns setup. + float offset_0 = GetColumnOffset(columns->Current); + float offset_1 = GetColumnOffset(columns->Current + 1); + float width = offset_1 - offset_0; + PushItemWidth(width * 0.65f); + window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; +} + +void ImGui::EndColumns() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = GetCurrentWindow(); + ImGuiOldColumns* columns = window->DC.CurrentColumns; + IM_ASSERT(columns != NULL); + + PopItemWidth(); + if (columns->Count > 1) + { + PopClipRect(); + columns->Splitter.Merge(window->DrawList); + } + + const ImGuiOldColumnFlags flags = columns->Flags; + columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y); + window->DC.CursorPos.y = columns->LineMaxY; + if (!(flags & ImGuiOldColumnFlags_GrowParentContentsSize)) + window->DC.CursorMaxPos.x = columns->HostCursorMaxPosX; // Restore cursor max pos, as columns don't grow parent + + // Draw columns borders and handle resize + // The IsBeingResized flag ensure we preserve pre-resize columns width so back-and-forth are not lossy + bool is_being_resized = false; + if (!(flags & ImGuiOldColumnFlags_NoBorder) && !window->SkipItems) + { + // We clip Y boundaries CPU side because very long triangles are mishandled by some GPU drivers. + const float y1 = ImMax(columns->HostCursorPosY, window->ClipRect.Min.y); + const float y2 = ImMin(window->DC.CursorPos.y, window->ClipRect.Max.y); + int dragging_column = -1; + for (int n = 1; n < columns->Count; n++) + { + ImGuiOldColumnData* column = &columns->Columns[n]; + float x = window->Pos.x + GetColumnOffset(n); + const ImGuiID column_id = columns->ID + ImGuiID(n); + const float column_hit_hw = ImTrunc(COLUMNS_HIT_RECT_HALF_THICKNESS * g.CurrentDpiScale); + const ImRect column_hit_rect(ImVec2(x - column_hit_hw, y1), ImVec2(x + column_hit_hw, y2)); + if (!ItemAdd(column_hit_rect, column_id, NULL, ImGuiItemFlags_NoNav)) + continue; + + bool hovered = false, held = false; + if (!(flags & ImGuiOldColumnFlags_NoResize)) + { + ButtonBehavior(column_hit_rect, column_id, &hovered, &held); + if (hovered || held) + SetMouseCursor(ImGuiMouseCursor_ResizeEW); + if (held && !(column->Flags & ImGuiOldColumnFlags_NoResize)) + dragging_column = n; + } + + // Draw column + const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); + const float xi = IM_TRUNC(x); + window->DrawList->AddLine(ImVec2(xi, y1 + 1.0f), ImVec2(xi, y2), col); + } + + // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame. + if (dragging_column != -1) + { + if (!columns->IsBeingResized) + for (int n = 0; n < columns->Count + 1; n++) + columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm; + columns->IsBeingResized = is_being_resized = true; + float x = GetDraggedColumnOffset(columns, dragging_column); + SetColumnOffset(dragging_column, x); + } + } + columns->IsBeingResized = is_being_resized; + + window->WorkRect = window->ParentWorkRect; + window->ParentWorkRect = columns->HostBackupParentWorkRect; + window->DC.CurrentColumns = NULL; + window->DC.ColumnsOffset.x = 0.0f; + window->DC.CursorPos.x = IM_TRUNC(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); + NavUpdateCurrentWindowIsScrollPushableX(); +} + +void ImGui::Columns(int columns_count, const char* id, bool borders) +{ + ImGuiWindow* window = GetCurrentWindow(); + IM_ASSERT(columns_count >= 1); + + ImGuiOldColumnFlags flags = (borders ? 0 : ImGuiOldColumnFlags_NoBorder); + //flags |= ImGuiOldColumnFlags_NoPreserveWidths; // NB: Legacy behavior + ImGuiOldColumns* columns = window->DC.CurrentColumns; + if (columns != NULL && columns->Count == columns_count && columns->Flags == flags) + return; + + if (columns != NULL) + EndColumns(); + + if (columns_count != 1) + BeginColumns(id, columns_count, flags); +} + +//------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imgui_widgets.cpp b/imgui/imgui_widgets.cpp index d9ca11e5..0fd9cbb3 100644 --- a/imgui/imgui_widgets.cpp +++ b/imgui/imgui_widgets.cpp @@ -1,4 +1,4 @@ -// dear imgui, v1.73 +// dear imgui, v1.91b // (widgets code) /* @@ -18,6 +18,10 @@ Index of this file: // [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. // [SECTION] Widgets: TreeNode, CollapsingHeader, etc. // [SECTION] Widgets: Selectable +// [SECTION] Widgets: Typing-Select support +// [SECTION] Widgets: Box-Select support +// [SECTION] Widgets: Multi-Select support +// [SECTION] Widgets: Multi-Select helpers // [SECTION] Widgets: ListBox // [SECTION] Widgets: PlotLines, PlotHistogram // [SECTION] Widgets: Value helpers @@ -28,57 +32,76 @@ Index of this file: */ -#ifdef DBG_NEW -#undef new -#endif - #if defined(_MSC_VER) && !defined(_CRT_SECURE_NO_WARNINGS) #define _CRT_SECURE_NO_WARNINGS #endif -#include "imgui.h" #ifndef IMGUI_DEFINE_MATH_OPERATORS #define IMGUI_DEFINE_MATH_OPERATORS #endif + +#include "imgui.h" +#ifndef IMGUI_DISABLE #include "imgui_internal.h" -#include // toupper -#if defined(_MSC_VER) && _MSC_VER <= 1500 // MSVC 2008 or earlier -#include // intptr_t -#else +// System includes #include // intptr_t -#endif -#include +//------------------------------------------------------------------------- +// Warnings +//------------------------------------------------------------------------- // Visual Studio warnings #ifdef _MSC_VER -#pragma warning (disable: 4127) // condition expression is constant -#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#pragma warning (disable: 4127) // condition expression is constant +#pragma warning (disable: 4996) // 'This function or variable may be unsafe': strcpy, strdup, sprintf, vsnprintf, sscanf, fopen +#if defined(_MSC_VER) && _MSC_VER >= 1922 // MSVC 2019 16.2 or later +#pragma warning (disable: 5054) // operator '|': deprecated between enumerations of different types +#endif +#pragma warning (disable: 26451) // [Static Analyzer] Arithmetic overflow : Using operator 'xxx' on a 4 byte value and then casting the result to a 8 byte value. Cast the value to the wider type before calling operator 'xxx' to avoid overflow(io.2). +#pragma warning (disable: 26812) // [Static Analyzer] The enum type 'xxx' is unscoped. Prefer 'enum class' over 'enum' (Enum.3). #endif // Clang/GCC warnings with -Weverything #if defined(__clang__) -#pragma clang diagnostic ignored "-Wold-style-cast" // warning : use of old-style cast // yes, they are more terse. -#pragma clang diagnostic ignored "-Wfloat-equal" // warning : comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. -#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning : format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. -#pragma clang diagnostic ignored "-Wsign-conversion" // warning : implicit conversion changes signedness // -#if __has_warning("-Wzero-as-null-pointer-constant") -#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning : zero as null pointer constant // some standard header variations use #define NULL 0 -#endif -#if __has_warning("-Wdouble-promotion") -#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#if __has_warning("-Wunknown-warning-option") +#pragma clang diagnostic ignored "-Wunknown-warning-option" // warning: unknown warning group 'xxx' // not all warnings are known by all Clang versions and they tend to be rename-happy.. so ignoring warnings triggers new warnings on some configuration. Great! #endif +#pragma clang diagnostic ignored "-Wunknown-pragmas" // warning: unknown warning group 'xxx' +#pragma clang diagnostic ignored "-Wold-style-cast" // warning: use of old-style cast // yes, they are more terse. +#pragma clang diagnostic ignored "-Wfloat-equal" // warning: comparing floating point with == or != is unsafe // storing and comparing against same constants (typically 0.0f) is ok. +#pragma clang diagnostic ignored "-Wformat" // warning: format specifies type 'int' but the argument has type 'unsigned int' +#pragma clang diagnostic ignored "-Wformat-nonliteral" // warning: format string is not a string literal // passing non-literal to vsnformat(). yes, user passing incorrect format strings can crash the code. +#pragma clang diagnostic ignored "-Wsign-conversion" // warning: implicit conversion changes signedness +#pragma clang diagnostic ignored "-Wunused-macros" // warning: macro is not used // we define snprintf/vsnprintf on Windows so they are available, but not always used. +#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant" // warning: zero as null pointer constant // some standard header variations use #define NULL 0 +#pragma clang diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function // using printf() is a misery with this as C++ va_arg ellipsis changes float to double. +#pragma clang diagnostic ignored "-Wenum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') +#pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision +#pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access +#pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type +#pragma clang diagnostic ignored "-Wswitch-default" // warning: 'switch' missing 'default' label #elif defined(__GNUC__) -#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind -#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked -#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wpragmas" // warning: unknown option after '#pragma GCC diagnostic' kind +#pragma GCC diagnostic ignored "-Wfloat-equal" // warning: comparing floating-point with '==' or '!=' is unsafe +#pragma GCC diagnostic ignored "-Wformat" // warning: format '%p' expects argument of type 'int'/'void*', but argument X has type 'unsigned int'/'ImGuiWindow*' +#pragma GCC diagnostic ignored "-Wformat-nonliteral" // warning: format not a string literal, format string not checked +#pragma GCC diagnostic ignored "-Wdeprecated-enum-enum-conversion" // warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma GCC diagnostic ignored "-Wdouble-promotion" // warning: implicit conversion from 'float' to 'double' when passing argument to function +#pragma GCC diagnostic ignored "-Wstrict-overflow" // warning: assuming signed overflow does not occur when simplifying division / ..when changing X +- C1 cmp C2 to X cmp C2 -+ C1 +#pragma GCC diagnostic ignored "-Wclass-memaccess" // [__GNUC__ >= 8] warning: 'memset/memcpy' clearing/writing an object of type 'xxxx' with no trivial copy-assignment; use assignment or value-initialization instead +#pragma GCC diagnostic ignored "-Wcast-qual" // warning: cast from type 'const xxxx *' to type 'xxxx *' casts away qualifiers #endif //------------------------------------------------------------------------- // Data //------------------------------------------------------------------------- +// Widgets +static const float DRAGDROP_HOLD_TO_OPEN_TIMER = 0.70f; // Time for drag-hold to activate items accepting the ImGuiButtonFlags_PressedOnDragDropHold button behavior. +static const float DRAG_MOUSE_THRESHOLD_FACTOR = 0.50f; // Multiplier for the default value of io.MouseDragThreshold to make DragFloat/DragInt react faster to mouse drags. + // Those MIN/MAX values are not define because we need to point to them static const signed char IM_S8_MIN = -128; static const signed char IM_S8_MAX = 127; @@ -111,13 +134,14 @@ static const ImU64 IM_U64_MAX = (2ULL * 9223372036854775807LL + 1); //------------------------------------------------------------------------- // For InputTextEx() -static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data); -static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); -static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); +static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard = false); +static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end); +static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining = NULL, ImVec2* out_offset = NULL, bool stop_on_new_line = false); //------------------------------------------------------------------------- // [SECTION] Widgets: Text, etc. //------------------------------------------------------------------------- +// - TextEx() [Internal] // - TextUnformatted() // - Text() // - TextV() @@ -138,17 +162,35 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return; - ImGuiContext& g = *GImGui; - IM_ASSERT(text != NULL); + + // Accept null ranges + if (text == text_end) + text = text_end = ""; + + // Calculate length const char* text_begin = text; if (text_end == NULL) - text_end = text + strlen(text); // FIXME-OPT + text_end = text + ImStrlen(text); // FIXME-OPT const ImVec2 text_pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); const float wrap_pos_x = window->DC.TextWrapPos; const bool wrap_enabled = (wrap_pos_x >= 0.0f); - if (text_end - text > 2000 && !wrap_enabled) + if (text_end - text <= 2000 || wrap_enabled) + { + // Common case + const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; + const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); + + ImRect bb(text_pos, text_pos + text_size); + ItemSize(text_size, 0.0f); + if (!ItemAdd(bb, 0)) + return; + + // Render (we don't hide text after ## in this end-user function) + RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width); + } + else { // Long text! // Perform manual coarse clipping to optimize for long multi-line text @@ -157,7 +199,7 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) // - We use memchr(), pay attention that well optimized versions of those str/mem functions are much faster than a casually written loop. const char* line = text; const float line_height = GetTextLineHeight(); - ImVec2 text_size(0,0); + ImVec2 text_size(0, 0); // Lines to skip (can't skip when logging text) ImVec2 pos = text_pos; @@ -169,7 +211,7 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) int lines_skipped = 0; while (line < text_end && lines_skipped < lines_skippable) { - const char* line_end = (const char*)memchr(line, '\n', text_end - line); + const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line); if (!line_end) line_end = text_end; if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0) @@ -187,10 +229,10 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) ImRect line_rect(pos, pos + ImVec2(FLT_MAX, line_height)); while (line < text_end) { - if (IsClippedEx(line_rect, 0, false)) + if (IsClippedEx(line_rect, 0)) break; - const char* line_end = (const char*)memchr(line, '\n', text_end - line); + const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line); if (!line_end) line_end = text_end; text_size.x = ImMax(text_size.x, CalcTextSize(line, line_end).x); @@ -205,7 +247,7 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) int lines_skipped = 0; while (line < text_end) { - const char* line_end = (const char*)memchr(line, '\n', text_end - line); + const char* line_end = (const char*)ImMemchr(line, '\n', text_end - line); if (!line_end) line_end = text_end; if ((flags & ImGuiTextFlags_NoWidthForLargeClippedText) == 0) @@ -218,22 +260,9 @@ void ImGui::TextEx(const char* text, const char* text_end, ImGuiTextFlags flags) text_size.y = (pos - text_pos).y; ImRect bb(text_pos, text_pos + text_size); - ItemSize(text_size); + ItemSize(text_size, 0.0f); ItemAdd(bb, 0); } - else - { - const float wrap_width = wrap_enabled ? CalcWrapWidthForPos(window->DC.CursorPos, wrap_pos_x) : 0.0f; - const ImVec2 text_size = CalcTextSize(text_begin, text_end, false, wrap_width); - - ImRect bb(text_pos, text_pos + text_size); - ItemSize(text_size); - if (!ItemAdd(bb, 0)) - return; - - // Render (we don't hide text after ## in this end-user function) - RenderTextWrapped(bb.Min, text_begin, text_end, wrap_width); - } } void ImGui::TextUnformatted(const char* text, const char* text_end) @@ -255,9 +284,9 @@ void ImGui::TextV(const char* fmt, va_list args) if (window->SkipItems) return; - ImGuiContext& g = *GImGui; - const char* text_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - TextEx(g.TempBuffer, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); + const char* text, *text_end; + ImFormatStringToTempBufferV(&text, &text_end, fmt, args); + TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText); } void ImGui::TextColored(const ImVec4& col, const char* fmt, ...) @@ -285,7 +314,8 @@ void ImGui::TextDisabled(const char* fmt, ...) void ImGui::TextDisabledV(const char* fmt, va_list args) { - PushStyleColor(ImGuiCol_Text, GImGui->Style.Colors[ImGuiCol_TextDisabled]); + ImGuiContext& g = *GImGui; + PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); TextV(fmt, args); PopStyleColor(); } @@ -300,8 +330,8 @@ void ImGui::TextWrapped(const char* fmt, ...) void ImGui::TextWrappedV(const char* fmt, va_list args) { - ImGuiWindow* window = GetCurrentWindow(); - bool need_backup = (window->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set + ImGuiContext& g = *GImGui; + const bool need_backup = (g.CurrentWindow->DC.TextWrapPos < 0.0f); // Keep existing wrap position if one is already set if (need_backup) PushTextWrapPos(0.0f); TextV(fmt, args); @@ -328,17 +358,20 @@ void ImGui::LabelTextV(const char* label, const char* fmt, va_list args) const ImGuiStyle& style = g.Style; const float w = CalcItemWidth(); + const char* value_text_begin, *value_text_end; + ImFormatStringToTempBufferV(&value_text_begin, &value_text_end, fmt, args); + const ImVec2 value_size = CalcTextSize(value_text_begin, value_text_end, false); const ImVec2 label_size = CalcTextSize(label, NULL, true); - const ImRect value_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2)); - const ImRect total_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x : 0.0f), style.FramePadding.y*2) + label_size); + + const ImVec2 pos = window->DC.CursorPos; + const ImRect value_bb(pos, pos + ImVec2(w, value_size.y + style.FramePadding.y * 2)); + const ImRect total_bb(pos, pos + ImVec2(w + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), ImMax(value_size.y, label_size.y) + style.FramePadding.y * 2)); ItemSize(total_bb, style.FramePadding.y); if (!ItemAdd(total_bb, 0)) return; // Render - const char* value_text_begin = &g.TempBuffer[0]; - const char* value_text_end = value_text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - RenderTextClipped(value_bb.Min, value_bb.Max, value_text_begin, value_text_end, NULL, ImVec2(0.0f,0.5f)); + RenderTextClipped(value_bb.Min + style.FramePadding, value_bb.Max, value_text_begin, value_text_end, &value_size, ImVec2(0.0f, 0.0f)); if (label_size.x > 0.0f) RenderText(ImVec2(value_bb.Max.x + style.ItemInnerSpacing.x, value_bb.Min.y + style.FramePadding.y), label); } @@ -361,20 +394,21 @@ void ImGui::BulletTextV(const char* fmt, va_list args) ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const char* text_begin = g.TempBuffer; - const char* text_end = text_begin + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); + const char* text_begin, *text_end; + ImFormatStringToTempBufferV(&text_begin, &text_end, fmt, args); const ImVec2 label_size = CalcTextSize(text_begin, text_end, false); - const float text_base_offset_y = ImMax(0.0f, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it - const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y*2), g.FontSize); - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x*2) : 0.0f), ImMax(line_height, label_size.y))); // Empty text doesn't add padding - ItemSize(bb); + const ImVec2 total_size = ImVec2(g.FontSize + (label_size.x > 0.0f ? (label_size.x + style.FramePadding.x * 2) : 0.0f), label_size.y); // Empty text doesn't add padding + ImVec2 pos = window->DC.CursorPos; + pos.y += window->DC.CurrLineTextBaseOffset; + ItemSize(total_size, 0.0f); + const ImRect bb(pos, pos + total_size); if (!ItemAdd(bb, 0)) return; // Render ImU32 text_col = GetColorU32(ImGuiCol_Text); - RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f), text_col); - RenderText(bb.Min+ImVec2(g.FontSize + style.FramePadding.x*2, text_base_offset_y), text_begin, text_end, false); + RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, g.FontSize * 0.5f), text_col); + RenderText(bb.Min + ImVec2(g.FontSize + style.FramePadding.x * 2, 0.0f), text_begin, text_end, false); } //------------------------------------------------------------------------- @@ -387,15 +421,19 @@ void ImGui::BulletTextV(const char* fmt, va_list args) // - ArrowButton() // - CloseButton() [Internal] // - CollapseButton() [Internal] -// - ScrollbarEx() [Internal] +// - GetWindowScrollbarID() [Internal] +// - GetWindowScrollbarRect() [Internal] // - Scrollbar() [Internal] +// - ScrollbarEx() [Internal] // - Image() // - ImageButton() // - Checkbox() +// - CheckboxFlagsT() [Internal] // - CheckboxFlags() // - RadioButton() // - ProgressBar() // - Bullet() +// - Hyperlink() //------------------------------------------------------------------------- // The ButtonBehavior() function is key to many interactions and used by many/most widgets. @@ -441,7 +479,7 @@ void ImGui::BulletTextV(const char* fmt, va_list args) // - PressedOnDragDropHold can generally be associated with any flag. // - PressedOnDoubleClick can be associated by PressedOnClickRelease/PressedOnRelease, in which case the second release event won't be reported. //------------------------------------------------------------------------------------------------------------------------------------------------ -// The behavior of the return-value changes when ImGuiButtonFlags_Repeat is set: +// The behavior of the return-value changes when ImGuiItemFlags_ButtonRepeat is set: // Repeat+ Repeat+ Repeat+ Repeat+ // PressedOnClickRelease PressedOnClick PressedOnRelease PressedOnDoubleClick //------------------------------------------------------------------------------------------------------------------------------------------------- @@ -452,39 +490,46 @@ void ImGui::BulletTextV(const char* fmt, va_list args) // Frame N + RepeatDelay + RepeatRate*N true true - true //------------------------------------------------------------------------------------------------------------------------------------------------- +// - FIXME: For refactor we could output flags, incl mouse hovered vs nav keyboard vs nav triggered etc. +// And better standardize how widgets use 'GetColor32((held && hovered) ? ... : hovered ? ...)' vs 'GetColor32(held ? ... : hovered ? ...);' +// For mouse feedback we typically prefer the 'held && hovered' test, but for nav feedback not always. Outputting hovered=true on Activation may be misleading. +// - Since v1.91.2 (Sept 2024) we included io.ConfigDebugHighlightIdConflicts feature. +// One idiom which was previously valid which will now emit a warning is when using multiple overlayed ButtonBehavior() +// with same ID and different MouseButton (see #8030). You can fix it by: +// (1) switching to use a single ButtonBehavior() with multiple _MouseButton flags. +// or (2) surrounding those calls with PushItemFlag(ImGuiItemFlags_AllowDuplicateId, true); ... PopItemFlag() bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool* out_held, ImGuiButtonFlags flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = GetCurrentWindow(); - if (flags & ImGuiButtonFlags_Disabled) - { - if (out_hovered) *out_hovered = false; - if (out_held) *out_held = false; - if (g.ActiveId == id) ClearActiveID(); - return false; - } + // Default behavior inherited from item flags + // Note that _both_ ButtonFlags and ItemFlags are valid sources, so copy one into the item_flags and only check that. + ImGuiItemFlags item_flags = (g.LastItemData.ID == id ? g.LastItemData.ItemFlags : g.CurrentItemFlags); + if (flags & ImGuiButtonFlags_AllowOverlap) + item_flags |= ImGuiItemFlags_AllowOverlap; - // Default behavior requires click+release on same spot - if ((flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_PressedOnRelease | ImGuiButtonFlags_PressedOnDoubleClick)) == 0) - flags |= ImGuiButtonFlags_PressedOnClickRelease; + // Default only reacts to left mouse button + if ((flags & ImGuiButtonFlags_MouseButtonMask_) == 0) + flags |= ImGuiButtonFlags_MouseButtonLeft; + + // Default behavior requires click + release inside bounding box + if ((flags & ImGuiButtonFlags_PressedOnMask_) == 0) + flags |= (item_flags & ImGuiItemFlags_ButtonRepeat) ? ImGuiButtonFlags_PressedOnClick : ImGuiButtonFlags_PressedOnDefault_; ImGuiWindow* backup_hovered_window = g.HoveredWindow; - const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredRootWindow == window; + const bool flatten_hovered_children = (flags & ImGuiButtonFlags_FlattenChildren) && g.HoveredWindow && g.HoveredWindow->RootWindowDockTree == window->RootWindowDockTree; if (flatten_hovered_children) g.HoveredWindow = window; #ifdef IMGUI_ENABLE_TEST_ENGINE - if (id != 0 && window->DC.LastItemId != id) - ImGuiTestEngineHook_ItemAdd(&g, bb, id); + // Alternate registration spot, for when caller didn't use ItemAdd() + if (g.LastItemData.ID != id) + IMGUI_TEST_ENGINE_ITEM_ADD(id, bb, NULL); #endif bool pressed = false; - bool hovered = ItemHoverable(bb, id); - - // Drag source doesn't report as hovered - if (hovered && g.DragDropActive && g.DragDropPayload.SourceId == id && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoDisableHover)) - hovered = false; + bool hovered = ItemHoverable(bb, id, item_flags); // Special mode for Drag and Drop where holding button pressed for a long time while dragging another item triggers the button if (g.DragDropActive && (flags & ImGuiButtonFlags_PressedOnDragDropHold) && !(g.DragDropSourceFlags & ImGuiDragDropFlags_SourceNoHoldToOpenOthers)) @@ -492,9 +537,10 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool { hovered = true; SetHoveredID(id); - if (CalcTypematicPressedRepeatAmount(g.HoveredIdTimer + 0.0001f, g.HoveredIdTimer + 0.0001f - g.IO.DeltaTime, 0.01f, 0.70f)) // FIXME: Our formula for CalcTypematicPressedRepeatAmount() is fishy + if (g.HoveredIdTimer - g.IO.DeltaTime <= DRAGDROP_HOLD_TO_OPEN_TIMER && g.HoveredIdTimer >= DRAGDROP_HOLD_TO_OPEN_TIMER) { pressed = true; + g.DragDropHoldJustPressedId = id; FocusWindow(window); } } @@ -502,105 +548,173 @@ bool ImGui::ButtonBehavior(const ImRect& bb, ImGuiID id, bool* out_hovered, bool if (flatten_hovered_children) g.HoveredWindow = backup_hovered_window; - // AllowOverlap mode (rarely used) requires previous frame HoveredId to be null or to match. This allows using patterns where a later submitted widget overlaps a previous one. - if (hovered && (flags & ImGuiButtonFlags_AllowItemOverlap) && (g.HoveredIdPreviousFrame != id && g.HoveredIdPreviousFrame != 0)) - hovered = false; - - // Mouse + // Mouse handling + const ImGuiID test_owner_id = (flags & ImGuiButtonFlags_NoTestKeyOwner) ? ImGuiKeyOwner_Any : id; if (hovered) { - if (!(flags & ImGuiButtonFlags_NoKeyModifiers) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt)) + IM_ASSERT(id != 0); // Lazily check inside rare path. + + // Poll mouse buttons + // - 'mouse_button_clicked' is generally carried into ActiveIdMouseButton when setting ActiveId. + // - Technically we only need some values in one code path, but since this is gated by hovered test this is fine. + int mouse_button_clicked = -1; + int mouse_button_released = -1; + for (int button = 0; button < 3; button++) + if (flags & (ImGuiButtonFlags_MouseButtonLeft << button)) // Handle ImGuiButtonFlags_MouseButtonRight and ImGuiButtonFlags_MouseButtonMiddle here. + { + if (IsMouseClicked(button, ImGuiInputFlags_None, test_owner_id) && mouse_button_clicked == -1) { mouse_button_clicked = button; } + if (IsMouseReleased(button, test_owner_id) && mouse_button_released == -1) { mouse_button_released = button; } + } + + // Process initial action + const bool mods_ok = !(flags & ImGuiButtonFlags_NoKeyModsAllowed) || (!g.IO.KeyCtrl && !g.IO.KeyShift && !g.IO.KeyAlt); + if (mods_ok) { - if ((flags & ImGuiButtonFlags_PressedOnClickRelease) && g.IO.MouseClicked[0]) + if (mouse_button_clicked != -1 && g.ActiveId != id) { - SetActiveID(id, window); - if (!(flags & ImGuiButtonFlags_NoNavFocus)) - SetFocusID(id, window); - FocusWindow(window); - } - if (((flags & ImGuiButtonFlags_PressedOnClick) && g.IO.MouseClicked[0]) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDoubleClicked[0])) - { - pressed = true; - if (flags & ImGuiButtonFlags_NoHoldingActiveID) - ClearActiveID(); - else - SetActiveID(id, window); // Hold on ID - FocusWindow(window); - } - if ((flags & ImGuiButtonFlags_PressedOnRelease) && g.IO.MouseReleased[0]) - { - if (!((flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay)) // Repeat mode trumps + if (!(flags & ImGuiButtonFlags_NoSetKeyOwner)) + SetKeyOwner(MouseButtonToKey(mouse_button_clicked), id); + if (flags & (ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnClickReleaseAnywhere)) + { + SetActiveID(id, window); + g.ActiveIdMouseButton = mouse_button_clicked; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + { + SetFocusID(id, window); + FocusWindow(window); + } + else + { + FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child + } + } + if ((flags & ImGuiButtonFlags_PressedOnClick) || ((flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseClickedCount[mouse_button_clicked] == 2)) + { pressed = true; - ClearActiveID(); + if (flags & ImGuiButtonFlags_NoHoldingActiveId) + ClearActiveID(); + else + SetActiveID(id, window); // Hold on ID + g.ActiveIdMouseButton = mouse_button_clicked; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + { + SetFocusID(id, window); + FocusWindow(window); + } + else + { + FocusWindow(window, ImGuiFocusRequestFlags_RestoreFocusedChild); // Still need to focus and bring to front, but try to avoid losing NavId when navigating a child + } + } + } + if (flags & ImGuiButtonFlags_PressedOnRelease) + { + if (mouse_button_released != -1) + { + const bool has_repeated_at_least_once = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button_released] >= g.IO.KeyRepeatDelay; // Repeat mode trumps on release behavior + if (!has_repeated_at_least_once) + pressed = true; + if (!(flags & ImGuiButtonFlags_NoNavFocus)) + SetFocusID(id, window); // FIXME: Lack of FocusWindow() call here is inconsistent with other paths. Research why. + ClearActiveID(); + } } // 'Repeat' mode acts when held regardless of _PressedOn flags (see table above). // Relies on repeat logic of IsMouseClicked() but we may as well do it ourselves if we end up exposing finer RepeatDelay/RepeatRate settings. - if ((flags & ImGuiButtonFlags_Repeat) && g.ActiveId == id && g.IO.MouseDownDuration[0] > 0.0f && IsMouseClicked(0, true)) - pressed = true; + if (g.ActiveId == id && (item_flags & ImGuiItemFlags_ButtonRepeat)) + if (g.IO.MouseDownDuration[g.ActiveIdMouseButton] > 0.0f && IsMouseClicked(g.ActiveIdMouseButton, ImGuiInputFlags_Repeat, test_owner_id)) + pressed = true; } - if (pressed) - g.NavDisableHighlight = true; + if (pressed && g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; } - // Gamepad/Keyboard navigation - // We report navigated item as hovered but we don't set g.HoveredId to not interfere with mouse. - if (g.NavId == id && !g.NavDisableHighlight && g.NavDisableMouseHover && (g.ActiveId == 0 || g.ActiveId == id || g.ActiveId == window->MoveId)) - if (!(flags & ImGuiButtonFlags_NoHoveredOnNav)) + // Keyboard/Gamepad navigation handling + // We report navigated and navigation-activated items as hovered but we don't set g.HoveredId to not interfere with mouse. + if (g.NavId == id && g.NavCursorVisible && g.NavHighlightItemUnderNav) + if (!(flags & ImGuiButtonFlags_NoHoveredOnFocus)) hovered = true; - if (g.NavActivateDownId == id) { bool nav_activated_by_code = (g.NavActivateId == id); - bool nav_activated_by_inputs = IsNavInputPressed(ImGuiNavInput_Activate, (flags & ImGuiButtonFlags_Repeat) ? ImGuiInputReadMode_Repeat : ImGuiInputReadMode_Pressed); + bool nav_activated_by_inputs = (g.NavActivatePressedId == id); + if (!nav_activated_by_inputs && (item_flags & ImGuiItemFlags_ButtonRepeat)) + { + // Avoid pressing multiple keys from triggering excessive amount of repeat events + const ImGuiKeyData* key1 = GetKeyData(ImGuiKey_Space); + const ImGuiKeyData* key2 = GetKeyData(ImGuiKey_Enter); + const ImGuiKeyData* key3 = GetKeyData(ImGuiKey_NavGamepadActivate); + const float t1 = ImMax(ImMax(key1->DownDuration, key2->DownDuration), key3->DownDuration); + nav_activated_by_inputs = CalcTypematicRepeatAmount(t1 - g.IO.DeltaTime, t1, g.IO.KeyRepeatDelay, g.IO.KeyRepeatRate) > 0; + } if (nav_activated_by_code || nav_activated_by_inputs) - pressed = true; - if (nav_activated_by_code || nav_activated_by_inputs || g.ActiveId == id) { // Set active id so it can be queried by user via IsItemActive(), equivalent of holding the mouse button. - g.NavActivateId = id; // This is so SetActiveId assign a Nav source + pressed = true; SetActiveID(id, window); - if ((nav_activated_by_code || nav_activated_by_inputs) && !(flags & ImGuiButtonFlags_NoNavFocus)) + g.ActiveIdSource = g.NavInputSource; + if (!(flags & ImGuiButtonFlags_NoNavFocus) && !(g.NavActivateFlags & ImGuiActivateFlags_FromShortcut)) SetFocusID(id, window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right) | (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + if (g.NavActivateFlags & ImGuiActivateFlags_FromShortcut) + g.ActiveIdFromShortcut = true; } } + // Process while held bool held = false; if (g.ActiveId == id) { - if (pressed) - g.ActiveIdHasBeenPressedBefore = true; if (g.ActiveIdSource == ImGuiInputSource_Mouse) { if (g.ActiveIdIsJustActivated) g.ActiveIdClickOffset = g.IO.MousePos - bb.Min; - if (g.IO.MouseDown[0]) + + const int mouse_button = g.ActiveIdMouseButton; + if (mouse_button == -1) + { + // Fallback for the rare situation were g.ActiveId was set programmatically or from another widget (e.g. #6304). + ClearActiveID(); + } + else if (IsMouseDown(mouse_button, test_owner_id)) { held = true; } else { - if (hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) && !g.DragDropActive) + bool release_in = hovered && (flags & ImGuiButtonFlags_PressedOnClickRelease) != 0; + bool release_anywhere = (flags & ImGuiButtonFlags_PressedOnClickReleaseAnywhere) != 0; + if ((release_in || release_anywhere) && !g.DragDropActive) { - bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseDownWasDoubleClick[0]; - bool is_repeating_already = (flags & ImGuiButtonFlags_Repeat) && g.IO.MouseDownDurationPrev[0] >= g.IO.KeyRepeatDelay; // Repeat mode trumps - if (!is_double_click_release && !is_repeating_already) + // Report as pressed when releasing the mouse (this is the most common path) + bool is_double_click_release = (flags & ImGuiButtonFlags_PressedOnDoubleClick) && g.IO.MouseReleased[mouse_button] && g.IO.MouseClickedLastCount[mouse_button] == 2; + bool is_repeating_already = (item_flags & ImGuiItemFlags_ButtonRepeat) && g.IO.MouseDownDurationPrev[mouse_button] >= g.IO.KeyRepeatDelay; // Repeat mode trumps + bool is_button_avail_or_owned = TestKeyOwner(MouseButtonToKey(mouse_button), test_owner_id); + if (!is_double_click_release && !is_repeating_already && is_button_avail_or_owned) pressed = true; } ClearActiveID(); } - if (!(flags & ImGuiButtonFlags_NoNavFocus)) - g.NavDisableHighlight = true; + if (!(flags & ImGuiButtonFlags_NoNavFocus) && g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; } - else if (g.ActiveIdSource == ImGuiInputSource_Nav) + else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) { - if (g.NavActivateDownId != id) + // When activated using Nav, we hold on the ActiveID until activation button is released + if (g.NavActivateDownId == id) + held = true; // hovered == true not true as we are already likely hovered on direct activation. + else ClearActiveID(); } + if (pressed) + g.ActiveIdHasBeenPressedBefore = true; } + // Activation highlight (this may be a remote activation) + if (g.NavHighlightActivatedId == id) + hovered = true; + if (out_hovered) *out_hovered = hovered; if (out_held) *out_held = held; @@ -628,28 +742,29 @@ bool ImGui::ButtonEx(const char* label, const ImVec2& size_arg, ImGuiButtonFlags if (!ItemAdd(bb, id)) return false; - if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat) - flags |= ImGuiButtonFlags_Repeat; bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); // Render const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); - RenderNavHighlight(bb, id); + RenderNavCursor(bb, id); RenderFrame(bb.Min, bb.Max, col, true, style.FrameRounding); + + if (g.LogEnabled) + LogSetNextTextDecoration("[", "]"); RenderTextClipped(bb.Min + style.FramePadding, bb.Max - style.FramePadding, label, NULL, &label_size, style.ButtonTextAlign, &bb); // Automatically close popups //if (pressed && !(flags & ImGuiButtonFlags_DontClosePopups) && (window->Flags & ImGuiWindowFlags_Popup)) // CloseCurrentPopup(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.LastItemStatusFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } bool ImGui::Button(const char* label, const ImVec2& size_arg) { - return ButtonEx(label, size_arg, 0); + return ButtonEx(label, size_arg, ImGuiButtonFlags_None); } // Small buttons fits within text without additional vertical spacing. @@ -665,8 +780,9 @@ bool ImGui::SmallButton(const char* label) // Tip: use ImGui::PushID()/PopID() to push indices or pointers in the ID stack. // Then you can keep 'str_id' empty or the same for all your buttons (instead of creating a string based on a non-string id) -bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg) +bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg, ImGuiButtonFlags flags) { + ImGuiContext& g = *GImGui; ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; @@ -678,110 +794,169 @@ bool ImGui::InvisibleButton(const char* str_id, const ImVec2& size_arg) ImVec2 size = CalcItemSize(size_arg, 0.0f, 0.0f); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); ItemSize(size); - if (!ItemAdd(bb, id)) + if (!ItemAdd(bb, id, NULL, (flags & ImGuiButtonFlags_EnableNav) ? ImGuiItemFlags_None : ImGuiItemFlags_NoNav)) return false; bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held); + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); + RenderNavCursor(bb, id); + IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); return pressed; } bool ImGui::ArrowButtonEx(const char* str_id, ImGuiDir dir, ImVec2 size, ImGuiButtonFlags flags) { + ImGuiContext& g = *GImGui; ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; - ImGuiContext& g = *GImGui; const ImGuiID id = window->GetID(str_id); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); const float default_size = GetFrameHeight(); - ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); + ItemSize(size, (size.y >= default_size) ? g.Style.FramePadding.y : -1.0f); if (!ItemAdd(bb, id)) return false; - if (window->DC.ItemFlags & ImGuiItemFlags_ButtonRepeat) - flags |= ImGuiButtonFlags_Repeat; - bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); // Render const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); const ImU32 text_col = GetColorU32(ImGuiCol_Text); - RenderNavHighlight(bb, id); + RenderNavCursor(bb, id); RenderFrame(bb.Min, bb.Max, bg_col, true, g.Style.FrameRounding); RenderArrow(window->DrawList, bb.Min + ImVec2(ImMax(0.0f, (size.x - g.FontSize) * 0.5f), ImMax(0.0f, (size.y - g.FontSize) * 0.5f)), text_col, dir); + IMGUI_TEST_ENGINE_ITEM_INFO(id, str_id, g.LastItemData.StatusFlags); return pressed; } bool ImGui::ArrowButton(const char* str_id, ImGuiDir dir) { float sz = GetFrameHeight(); - return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), 0); + return ArrowButtonEx(str_id, dir, ImVec2(sz, sz), ImGuiButtonFlags_None); } // Button to close a window -bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos)//, float size) +bool ImGui::CloseButton(ImGuiID id, const ImVec2& pos) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - // We intentionally allow interaction when clipped so that a mechanical Alt,Right,Validate sequence close a window. - // (this isn't the regular behavior of buttons, but it doesn't affect the user much because navigation tends to keep items visible). - const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); - bool is_clipped = !ItemAdd(bb, id); + // Tweak 1: Shrink hit-testing area if button covers an abnormally large proportion of the visible region. That's in order to facilitate moving the window away. (#3825) + // This may better be applied as a general hit-rect reduction mechanism for all widgets to ensure the area to move window is always accessible? + const ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize)); + ImRect bb_interact = bb; + const float area_to_visible_ratio = window->OuterRectClipped.GetArea() / bb.GetArea(); + if (area_to_visible_ratio < 1.5f) + bb_interact.Expand(ImTrunc(bb_interact.GetSize() * -0.25f)); + + // Tweak 2: We intentionally allow interaction when clipped so that a mechanical Alt,Right,Activate sequence can always close a window. + // (this isn't the common behavior of buttons, but it doesn't affect the user because navigation tends to keep items visible in scrolling layer). + bool is_clipped = !ItemAdd(bb_interact, id); bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held); + bool pressed = ButtonBehavior(bb_interact, id, &hovered, &held); if (is_clipped) return pressed; // Render - ImU32 col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); - ImVec2 center = bb.GetCenter(); + ImU32 bg_col = GetColorU32(held ? ImGuiCol_ButtonActive : ImGuiCol_ButtonHovered); if (hovered) - window->DrawList->AddCircleFilled(center, ImMax(2.0f, g.FontSize * 0.5f + 1.0f), col, 12); - - float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f; + window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col); + RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact); ImU32 cross_col = GetColorU32(ImGuiCol_Text); - center -= ImVec2(0.5f, 0.5f); - window->DrawList->AddLine(center + ImVec2(+cross_extent,+cross_extent), center + ImVec2(-cross_extent,-cross_extent), cross_col, 1.0f); - window->DrawList->AddLine(center + ImVec2(+cross_extent,-cross_extent), center + ImVec2(-cross_extent,+cross_extent), cross_col, 1.0f); + ImVec2 cross_center = bb.GetCenter() - ImVec2(0.5f, 0.5f); + float cross_extent = g.FontSize * 0.5f * 0.7071f - 1.0f; + window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, +cross_extent), cross_center + ImVec2(-cross_extent, -cross_extent), cross_col, 1.0f); + window->DrawList->AddLine(cross_center + ImVec2(+cross_extent, -cross_extent), cross_center + ImVec2(-cross_extent, +cross_extent), cross_col, 1.0f); return pressed; } -bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos) +// The Collapse button also functions as a Dock Menu button. +bool ImGui::CollapseButton(ImGuiID id, const ImVec2& pos, ImGuiDockNode* dock_node) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize) + g.Style.FramePadding * 2.0f); - ItemAdd(bb, id); + ImRect bb(pos, pos + ImVec2(g.FontSize, g.FontSize)); + bool is_clipped = !ItemAdd(bb, id); bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_None); + if (is_clipped) + return pressed; // Render + //bool is_dock_menu = (window->DockNodeAsHost && !window->Collapsed); ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); ImU32 text_col = GetColorU32(ImGuiCol_Text); - ImVec2 center = bb.GetCenter(); if (hovered || held) - window->DrawList->AddCircleFilled(center/*+ ImVec2(0.0f, -0.5f)*/, g.FontSize * 0.5f + 1.0f, bg_col, 12); - RenderArrow(window->DrawList, bb.Min + g.Style.FramePadding, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); + window->DrawList->AddRectFilled(bb.Min, bb.Max, bg_col); + RenderNavCursor(bb, id, ImGuiNavRenderCursorFlags_Compact); + + if (dock_node) + RenderArrowDockMenu(window->DrawList, bb.Min, g.FontSize, text_col); + else + RenderArrow(window->DrawList, bb.Min, text_col, window->Collapsed ? ImGuiDir_Right : ImGuiDir_Down, 1.0f); // Switch to moving the window after mouse is moved beyond the initial drag threshold - if (IsItemActive() && IsMouseDragging()) - StartMouseMovingWindow(window); + if (IsItemActive() && IsMouseDragging(0)) + StartMouseMovingWindowOrNode(window, dock_node, true); // Undock from window/collapse menu button return pressed; } -ImGuiID ImGui::GetScrollbarID(ImGuiWindow* window, ImGuiAxis axis) +ImGuiID ImGui::GetWindowScrollbarID(ImGuiWindow* window, ImGuiAxis axis) { - return window->GetIDNoKeepAlive(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY"); + return window->GetID(axis == ImGuiAxis_X ? "#SCROLLX" : "#SCROLLY"); +} + +// Return scrollbar rectangle, must only be called for corresponding axis if window->ScrollbarX/Y is set. +ImRect ImGui::GetWindowScrollbarRect(ImGuiWindow* window, ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + const ImRect outer_rect = window->Rect(); + const ImRect inner_rect = window->InnerRect; + const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; // (ScrollbarSizes.x = width of Y scrollbar; ScrollbarSizes.y = height of X scrollbar) + IM_ASSERT(scrollbar_size >= 0.0f); + const float border_size = IM_ROUND(window->WindowBorderSize * 0.5f); + const float border_top = (window->Flags & ImGuiWindowFlags_MenuBar) ? IM_ROUND(g.Style.FrameBorderSize * 0.5f) : 0.0f; + if (axis == ImGuiAxis_X) + return ImRect(inner_rect.Min.x + border_size, ImMax(outer_rect.Min.y + border_size, outer_rect.Max.y - border_size - scrollbar_size), inner_rect.Max.x - border_size, outer_rect.Max.y - border_size); + else + return ImRect(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y + border_top, outer_rect.Max.x - border_size, inner_rect.Max.y - border_size); +} + +void ImGui::Scrollbar(ImGuiAxis axis) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + const ImGuiID id = GetWindowScrollbarID(window, axis); + + // Calculate scrollbar bounding box + ImRect bb = GetWindowScrollbarRect(window, axis); + ImDrawFlags rounding_corners = ImDrawFlags_RoundCornersNone; + if (axis == ImGuiAxis_X) + { + rounding_corners |= ImDrawFlags_RoundCornersBottomLeft; + if (!window->ScrollbarY) + rounding_corners |= ImDrawFlags_RoundCornersBottomRight; + } + else + { + if ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) + rounding_corners |= ImDrawFlags_RoundCornersTopRight; + if (!window->ScrollbarX) + rounding_corners |= ImDrawFlags_RoundCornersBottomRight; + } + float size_visible = window->InnerRect.Max[axis] - window->InnerRect.Min[axis]; + float size_contents = window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f; + ImS64 scroll = (ImS64)window->Scroll[axis]; + ScrollbarEx(bb, id, axis, &scroll, (ImS64)size_visible, (ImS64)size_contents, rounding_corners); + window->Scroll[axis] = (float)scroll; } // Vertical/Horizontal scrollbar @@ -790,7 +965,7 @@ ImGuiID ImGui::GetScrollbarID(ImGuiWindow* window, ImGuiAxis axis) // - We store values as normalized ratio and in a form that allows the window content to change while we are holding on a scrollbar // - We handle both horizontal and vertical scrollbars, which makes the terminology not ideal. // Still, the code should probably be made simpler.. -bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, float* p_scroll_v, float size_avail_v, float size_contents_v, ImDrawCornerFlags rounding_corners) +bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, ImS64* p_scroll_v, ImS64 size_visible_v, ImS64 size_contents_v, ImDrawFlags draw_rounding_flags) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; @@ -802,77 +977,89 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, floa if (bb_frame_width <= 0.0f || bb_frame_height <= 0.0f) return false; - // When we are too small, start hiding and disabling the grab (this reduce visual noise on very small window and facilitate using the resize grab) + // When we are too small, start hiding and disabling the grab (this reduce visual noise on very small window and facilitate using the window resize grab) float alpha = 1.0f; - if ((axis == ImGuiAxis_Y) && bb_frame_height < g.FontSize + g.Style.FramePadding.y * 2.0f) - alpha = ImSaturate((bb_frame_height - g.FontSize) / (g.Style.FramePadding.y * 2.0f)); + if ((axis == ImGuiAxis_Y) && bb_frame_height < bb_frame_width) + alpha = ImSaturate(bb_frame_height / ImMax(bb_frame_width * 2.0f, 1.0f)); if (alpha <= 0.0f) return false; const ImGuiStyle& style = g.Style; const bool allow_interaction = (alpha >= 1.0f); - const bool horizontal = (axis == ImGuiAxis_X); ImRect bb = bb_frame; - bb.Expand(ImVec2(-ImClamp((float)(int)((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp((float)(int)((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f))); + bb.Expand(ImVec2(-ImClamp(IM_TRUNC((bb_frame_width - 2.0f) * 0.5f), 0.0f, 3.0f), -ImClamp(IM_TRUNC((bb_frame_height - 2.0f) * 0.5f), 0.0f, 3.0f))); // V denote the main, longer axis of the scrollbar (= height for a vertical scrollbar) - const float scrollbar_size_v = horizontal ? bb.GetWidth() : bb.GetHeight(); + const float scrollbar_size_v = (axis == ImGuiAxis_X) ? bb.GetWidth() : bb.GetHeight(); // Calculate the height of our grabbable box. It generally represent the amount visible (vs the total scrollable amount) // But we maintain a minimum size in pixel to allow for the user to still aim inside. - IM_ASSERT(ImMax(size_contents_v, size_avail_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers. - const float win_size_v = ImMax(ImMax(size_contents_v, size_avail_v), 1.0f); - const float grab_h_pixels = ImClamp(scrollbar_size_v * (size_avail_v / win_size_v), style.GrabMinSize, scrollbar_size_v); + IM_ASSERT(ImMax(size_contents_v, size_visible_v) > 0.0f); // Adding this assert to check if the ImMax(XXX,1.0f) is still needed. PLEASE CONTACT ME if this triggers. + const ImS64 win_size_v = ImMax(ImMax(size_contents_v, size_visible_v), (ImS64)1); + const float grab_h_minsize = ImMin(bb.GetSize()[axis], style.GrabMinSize); + const float grab_h_pixels = ImClamp(scrollbar_size_v * ((float)size_visible_v / (float)win_size_v), grab_h_minsize, scrollbar_size_v); const float grab_h_norm = grab_h_pixels / scrollbar_size_v; // Handle input right away. None of the code of Begin() is relying on scrolling position before calling Scrollbar(). bool held = false; bool hovered = false; + ItemAdd(bb_frame, id, NULL, ImGuiItemFlags_NoNav); ButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags_NoNavFocus); - float scroll_max = ImMax(1.0f, size_contents_v - size_avail_v); - float scroll_ratio = ImSaturate(*p_scroll_v / scroll_max); - float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; + const ImS64 scroll_max = ImMax((ImS64)1, size_contents_v - size_visible_v); + float scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max); + float grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; // Grab position in normalized space if (held && allow_interaction && grab_h_norm < 1.0f) { - float scrollbar_pos_v = horizontal ? bb.Min.x : bb.Min.y; - float mouse_pos_v = horizontal ? g.IO.MousePos.x : g.IO.MousePos.y; + const float scrollbar_pos_v = bb.Min[axis]; + const float mouse_pos_v = g.IO.MousePos[axis]; // Click position in scrollbar normalized space (0.0f->1.0f) const float clicked_v_norm = ImSaturate((mouse_pos_v - scrollbar_pos_v) / scrollbar_size_v); - SetHoveredID(id); - bool seek_absolute = false; + const int held_dir = (clicked_v_norm < grab_v_norm) ? -1 : (clicked_v_norm > grab_v_norm + grab_h_norm) ? +1 : 0; if (g.ActiveIdIsJustActivated) { - // On initial click calculate the distance between mouse and the center of the grab - seek_absolute = (clicked_v_norm < grab_v_norm || clicked_v_norm > grab_v_norm + grab_h_norm); - if (seek_absolute) - g.ScrollbarClickDeltaToGrabCenter = 0.0f; - else - g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; + // On initial click when held_dir == 0 (clicked over grab): calculate the distance between mouse and the center of the grab + const bool scroll_to_clicked_location = (g.IO.ConfigScrollbarScrollByPage == false || g.IO.KeyShift || held_dir == 0); + g.ScrollbarSeekMode = scroll_to_clicked_location ? 0 : (short)held_dir; + g.ScrollbarClickDeltaToGrabCenter = (held_dir == 0 && !g.IO.KeyShift) ? clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f : 0.0f; } - // Apply scroll + // Apply scroll (p_scroll_v will generally point on one member of window->Scroll) // It is ok to modify Scroll here because we are being called in Begin() after the calculation of ContentSize and before setting up our starting position - const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm)); - *p_scroll_v = (float)(int)(0.5f + scroll_v_norm * scroll_max);//(win_size_contents_v - win_size_v)); + if (g.ScrollbarSeekMode == 0) + { + // Absolute seeking + const float scroll_v_norm = ImSaturate((clicked_v_norm - g.ScrollbarClickDeltaToGrabCenter - grab_h_norm * 0.5f) / (1.0f - grab_h_norm)); + *p_scroll_v = (ImS64)(scroll_v_norm * scroll_max); + } + else + { + // Page by page + if (IsMouseClicked(ImGuiMouseButton_Left, ImGuiInputFlags_Repeat) && held_dir == g.ScrollbarSeekMode) + { + float page_dir = (g.ScrollbarSeekMode > 0.0f) ? +1.0f : -1.0f; + *p_scroll_v = ImClamp(*p_scroll_v + (ImS64)(page_dir * size_visible_v), (ImS64)0, scroll_max); + } + } // Update values for rendering - scroll_ratio = ImSaturate(*p_scroll_v / scroll_max); + scroll_ratio = ImSaturate((float)*p_scroll_v / (float)scroll_max); grab_v_norm = scroll_ratio * (scrollbar_size_v - grab_h_pixels) / scrollbar_size_v; - // Update distance to grab now that we have seeked and saturated - if (seek_absolute) - g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; + // Update distance to grab now that we have seek'ed and saturated + //if (seek_absolute) + // g.ScrollbarClickDeltaToGrabCenter = clicked_v_norm - grab_v_norm - grab_h_norm * 0.5f; } // Render - window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, GetColorU32(ImGuiCol_ScrollbarBg), window->WindowRounding, rounding_corners); + const ImU32 bg_col = GetColorU32(ImGuiCol_ScrollbarBg); const ImU32 grab_col = GetColorU32(held ? ImGuiCol_ScrollbarGrabActive : hovered ? ImGuiCol_ScrollbarGrabHovered : ImGuiCol_ScrollbarGrab, alpha); + window->DrawList->AddRectFilled(bb_frame.Min, bb_frame.Max, bg_col, window->WindowRounding, draw_rounding_flags); ImRect grab_rect; - if (horizontal) + if (axis == ImGuiAxis_X) grab_rect = ImRect(ImLerp(bb.Min.x, bb.Max.x, grab_v_norm), bb.Min.y, ImLerp(bb.Min.x, bb.Max.x, grab_v_norm) + grab_h_pixels, bb.Max.y); else grab_rect = ImRect(bb.Min.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm), bb.Max.x, ImLerp(bb.Min.y, bb.Max.y, grab_v_norm) + grab_h_pixels); @@ -881,102 +1068,108 @@ bool ImGui::ScrollbarEx(const ImRect& bb_frame, ImGuiID id, ImGuiAxis axis, floa return held; } -void ImGui::Scrollbar(ImGuiAxis axis) +// - Read about ImTextureID here: https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples +// - 'uv0' and 'uv1' are texture coordinates. Read about them from the same link above. +void ImGui::ImageWithBg(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) { ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - - const ImGuiID id = GetScrollbarID(window, axis); - KeepAliveID(id); - - // Calculate scrollbar bounding box - const ImRect outer_rect = window->Rect(); - const ImRect inner_rect = window->InnerRect; - const float border_size = window->WindowBorderSize; - const float scrollbar_size = window->ScrollbarSizes[axis ^ 1]; - IM_ASSERT(scrollbar_size > 0.0f); - const float other_scrollbar_size = window->ScrollbarSizes[axis]; - ImDrawCornerFlags rounding_corners = (other_scrollbar_size <= 0.0f) ? ImDrawCornerFlags_BotRight : 0; - ImRect bb; - if (axis == ImGuiAxis_X) - { - bb.Min = ImVec2(inner_rect.Min.x, ImMax(outer_rect.Min.y, outer_rect.Max.y - border_size - scrollbar_size)); - bb.Max = ImVec2(inner_rect.Max.x, outer_rect.Max.y); - rounding_corners |= ImDrawCornerFlags_BotLeft; - } - else - { - bb.Min = ImVec2(ImMax(outer_rect.Min.x, outer_rect.Max.x - border_size - scrollbar_size), inner_rect.Min.y); - bb.Max = ImVec2(outer_rect.Max.x, window->InnerRect.Max.y); - rounding_corners |= ((window->Flags & ImGuiWindowFlags_NoTitleBar) && !(window->Flags & ImGuiWindowFlags_MenuBar)) ? ImDrawCornerFlags_TopRight : 0; - } - ScrollbarEx(bb, id, axis, &window->Scroll[axis], inner_rect.Max[axis] - inner_rect.Min[axis], window->ContentSize[axis] + window->WindowPadding[axis] * 2.0f, rounding_corners); -} - -void ImGui::Image(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) -{ ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return; - ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); - if (border_col.w > 0.0f) - bb.Max += ImVec2(2, 2); + const ImVec2 padding(g.Style.ImageBorderSize, g.Style.ImageBorderSize); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f); ItemSize(bb); if (!ItemAdd(bb, 0)) return; - if (border_col.w > 0.0f) - { - window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(border_col), 0.0f); - window->DrawList->AddImage(user_texture_id, bb.Min + ImVec2(1, 1), bb.Max - ImVec2(1, 1), uv0, uv1, GetColorU32(tint_col)); - } - else - { - window->DrawList->AddImage(user_texture_id, bb.Min, bb.Max, uv0, uv1, GetColorU32(tint_col)); - } + // Render + if (g.Style.ImageBorderSize > 0.0f) + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_Border), 0.0f, ImDrawFlags_None, g.Style.ImageBorderSize); + if (bg_col.w > 0.0f) + window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col)); + window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); } -// frame_padding < 0: uses FramePadding from style (default) -// frame_padding = 0: no framing -// frame_padding > 0: set framing size -// The color used are the button colors. -bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col) +void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1) { + ImageWithBg(user_texture_id, image_size, uv0, uv1); +} + +// 1.91.9 (February 2025) removed 'tint_col' and 'border_col' parameters, made border size not depend on color value. (#8131, #8238) +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +void ImGui::Image(ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& tint_col, const ImVec4& border_col) +{ + ImGuiContext& g = *GImGui; + PushStyleVar(ImGuiStyleVar_ImageBorderSize, (border_col.w > 0.0f) ? ImMax(1.0f, g.Style.ImageBorderSize) : 0.0f); // Preserve legacy behavior where border is always visible when border_col's Alpha is >0.0f + PushStyleColor(ImGuiCol_Border, border_col); + ImageWithBg(user_texture_id, image_size, uv0, uv1, ImVec4(0, 0, 0, 0), tint_col); + PopStyleColor(); + PopStyleVar(); +} +#endif + +bool ImGui::ImageButtonEx(ImGuiID id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col, ImGuiButtonFlags flags) +{ + ImGuiContext& g = *GImGui; ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; - ImGuiContext& g = *GImGui; - const ImGuiStyle& style = g.Style; - - // Default to using texture ID as ID. User can still push string/integer prefixes. - // We could hash the size/uv to create a unique ID but that would prevent the user from animating UV. - PushID((void*)(intptr_t)user_texture_id); - const ImGuiID id = window->GetID("#image"); - PopID(); - - const ImVec2 padding = (frame_padding >= 0) ? ImVec2((float)frame_padding, (float)frame_padding) : style.FramePadding; - const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size + padding * 2); - const ImRect image_bb(window->DC.CursorPos + padding, window->DC.CursorPos + padding + size); + const ImVec2 padding = g.Style.FramePadding; + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + image_size + padding * 2.0f); ItemSize(bb); if (!ItemAdd(bb, id)) return false; bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held); + bool pressed = ButtonBehavior(bb, id, &hovered, &held, flags); // Render const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); - RenderNavHighlight(bb, id); - RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, style.FrameRounding)); + RenderNavCursor(bb, id); + RenderFrame(bb.Min, bb.Max, col, true, ImClamp((float)ImMin(padding.x, padding.y), 0.0f, g.Style.FrameRounding)); if (bg_col.w > 0.0f) - window->DrawList->AddRectFilled(image_bb.Min, image_bb.Max, GetColorU32(bg_col)); - window->DrawList->AddImage(user_texture_id, image_bb.Min, image_bb.Max, uv0, uv1, GetColorU32(tint_col)); + window->DrawList->AddRectFilled(bb.Min + padding, bb.Max - padding, GetColorU32(bg_col)); + window->DrawList->AddImage(user_texture_id, bb.Min + padding, bb.Max - padding, uv0, uv1, GetColorU32(tint_col)); return pressed; } +// - ImageButton() adds style.FramePadding*2.0f to provided size. This is in order to facilitate fitting an image in a button. +// - ImageButton() draws a background based on regular Button() color + optionally an inner background if specified. (#8165) // FIXME: Maybe that's not the best design? +bool ImGui::ImageButton(const char* str_id, ImTextureID user_texture_id, const ImVec2& image_size, const ImVec2& uv0, const ImVec2& uv1, const ImVec4& bg_col, const ImVec4& tint_col) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + return ImageButtonEx(window->GetID(str_id), user_texture_id, image_size, uv0, uv1, bg_col, tint_col); +} + +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS +// Legacy API obsoleted in 1.89. Two differences with new ImageButton() +// - old ImageButton() used ImTextureID as item id (created issue with multiple buttons with same image, transient texture id values, opaque computation of ID) +// - new ImageButton() requires an explicit 'const char* str_id' +// - old ImageButton() had frame_padding' override argument. +// - new ImageButton() always use style.FramePadding. +/* +bool ImGui::ImageButton(ImTextureID user_texture_id, const ImVec2& size, const ImVec2& uv0, const ImVec2& uv1, int frame_padding, const ImVec4& bg_col, const ImVec4& tint_col) +{ + // Default to using texture ID as ID. User can still push string/integer prefixes. + PushID((ImTextureID)(intptr_t)user_texture_id); + if (frame_padding >= 0) + PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2((float)frame_padding, (float)frame_padding)); + bool ret = ImageButton("", user_texture_id, size, uv0, uv1, bg_col, tint_col); + if (frame_padding >= 0) + PopStyleVar(); + PopID(); + return ret; +} +*/ +#endif // #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + bool ImGui::Checkbox(const char* label, bool* v) { ImGuiWindow* window = GetCurrentWindow(); @@ -992,57 +1185,113 @@ bool ImGui::Checkbox(const char* label, bool* v) const ImVec2 pos = window->DC.CursorPos; const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 2.0f)); ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, id)) - return false; + const bool is_visible = ItemAdd(total_bb, id); + const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (!is_visible) + if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(total_bb)) // Extra layer of "no logic clip" for box-select support + { + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); + return false; + } + + // Range-Selection/Multi-selection support (header) + bool checked = *v; + if (is_multi_select) + MultiSelectItemHeader(id, &checked, NULL); bool hovered, held; bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); - if (pressed) + + // Range-Selection/Multi-selection support (footer) + if (is_multi_select) + MultiSelectItemFooter(id, &checked, &pressed); + else if (pressed) + checked = !checked; + + if (*v != checked) { - *v = !(*v); + *v = checked; + pressed = true; // return value MarkItemEdited(id); } const ImRect check_bb(pos, pos + ImVec2(square_sz, square_sz)); - RenderNavHighlight(total_bb, id); - RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); - ImU32 check_col = GetColorU32(ImGuiCol_CheckMark); - if (window->DC.ItemFlags & ImGuiItemFlags_MixedValue) + const bool mixed_value = (g.LastItemData.ItemFlags & ImGuiItemFlags_MixedValue) != 0; + if (is_visible) { - // Undocumented tristate/mixed/indeterminate checkbox (#2644) - ImVec2 pad(ImMax(1.0f, (float)(int)(square_sz / 3.6f)), ImMax(1.0f, (float)(int)(square_sz / 3.6f))); - window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); + RenderNavCursor(total_bb, id); + RenderFrame(check_bb.Min, check_bb.Max, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), true, style.FrameRounding); + ImU32 check_col = GetColorU32(ImGuiCol_CheckMark); + if (mixed_value) + { + // Undocumented tristate/mixed/indeterminate checkbox (#2644) + // This may seem awkwardly designed because the aim is to make ImGuiItemFlags_MixedValue supported by all widgets (not just checkbox) + ImVec2 pad(ImMax(1.0f, IM_TRUNC(square_sz / 3.6f)), ImMax(1.0f, IM_TRUNC(square_sz / 3.6f))); + window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, style.FrameRounding); + } + else if (*v) + { + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); + RenderCheckMark(window->DrawList, check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad * 2.0f); + } } - else if (*v) - { - const float pad = ImMax(1.0f, (float)(int)(square_sz / 6.0f)); - RenderCheckMark(check_bb.Min + ImVec2(pad, pad), check_col, square_sz - pad*2.0f); - } - + const ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); if (g.LogEnabled) - LogRenderedText(&total_bb.Min, *v ? "[x]" : "[ ]"); - if (label_size.x > 0.0f) - RenderText(ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y), label); + LogRenderedText(&label_pos, mixed_value ? "[~]" : *v ? "[x]" : "[ ]"); + if (is_visible && label_size.x > 0.0f) + RenderText(label_pos, label); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); return pressed; } -bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) +template +bool ImGui::CheckboxFlagsT(const char* label, T* flags, T flags_value) { - bool v = ((*flags & flags_value) == flags_value); - bool pressed = Checkbox(label, &v); + bool all_on = (*flags & flags_value) == flags_value; + bool any_on = (*flags & flags_value) != 0; + bool pressed; + if (!all_on && any_on) + { + ImGuiContext& g = *GImGui; + g.NextItemData.ItemFlags |= ImGuiItemFlags_MixedValue; + pressed = Checkbox(label, &all_on); + } + else + { + pressed = Checkbox(label, &all_on); + + } if (pressed) { - if (v) + if (all_on) *flags |= flags_value; else *flags &= ~flags_value; } - return pressed; } +bool ImGui::CheckboxFlags(const char* label, int* flags, int flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, unsigned int* flags, unsigned int flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, ImS64* flags, ImS64 flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + +bool ImGui::CheckboxFlags(const char* label, ImU64* flags, ImU64 flags_value) +{ + return CheckboxFlagsT(label, flags, flags_value); +} + bool ImGui::RadioButton(const char* label, bool active) { ImGuiWindow* window = GetCurrentWindow(); @@ -1063,8 +1312,8 @@ bool ImGui::RadioButton(const char* label, bool active) return false; ImVec2 center = check_bb.GetCenter(); - center.x = (float)(int)center.x + 0.5f; - center.y = (float)(int)center.y + 0.5f; + center.x = IM_ROUND(center.x); + center.y = IM_ROUND(center.y); const float radius = (square_sz - 1.0f) * 0.5f; bool hovered, held; @@ -1072,25 +1321,28 @@ bool ImGui::RadioButton(const char* label, bool active) if (pressed) MarkItemEdited(id); - RenderNavHighlight(total_bb, id); - window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), 16); + RenderNavCursor(total_bb, id); + const int num_segment = window->DrawList->_CalcCircleAutoSegmentCount(radius); + window->DrawList->AddCircleFilled(center, radius, GetColorU32((held && hovered) ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg), num_segment); if (active) { - const float pad = ImMax(1.0f, (float)(int)(square_sz / 6.0f)); - window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark), 16); + const float pad = ImMax(1.0f, IM_TRUNC(square_sz / 6.0f)); + window->DrawList->AddCircleFilled(center, radius - pad, GetColorU32(ImGuiCol_CheckMark)); } if (style.FrameBorderSize > 0.0f) { - window->DrawList->AddCircle(center + ImVec2(1,1), radius, GetColorU32(ImGuiCol_BorderShadow), 16, style.FrameBorderSize); - window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), 16, style.FrameBorderSize); + window->DrawList->AddCircle(center + ImVec2(1, 1), radius, GetColorU32(ImGuiCol_BorderShadow), num_segment, style.FrameBorderSize); + window->DrawList->AddCircle(center, radius, GetColorU32(ImGuiCol_Border), num_segment, style.FrameBorderSize); } + ImVec2 label_pos = ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y); if (g.LogEnabled) - LogRenderedText(&total_bb.Min, active ? "(x)" : "( )"); + LogRenderedText(&label_pos, active ? "(x)" : "( )"); if (label_size.x > 0.0f) - RenderText(ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y), label); + RenderText(label_pos, label); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); return pressed; } @@ -1114,30 +1366,53 @@ void ImGui::ProgressBar(float fraction, const ImVec2& size_arg, const char* over const ImGuiStyle& style = g.Style; ImVec2 pos = window->DC.CursorPos; - ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y*2.0f); + ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), g.FontSize + style.FramePadding.y * 2.0f); ImRect bb(pos, pos + size); ItemSize(size, style.FramePadding.y); if (!ItemAdd(bb, 0)) return; - // Render - fraction = ImSaturate(fraction); - RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); - bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); - const ImVec2 fill_br = ImVec2(ImLerp(bb.Min.x, bb.Max.x, fraction), bb.Max.y); - RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), 0.0f, fraction, style.FrameRounding); + // Fraction < 0.0f will display an indeterminate progress bar animation + // The value must be animated along with time, so e.g. passing '-1.0f * ImGui::GetTime()' as fraction works. + const bool is_indeterminate = (fraction < 0.0f); + if (!is_indeterminate) + fraction = ImSaturate(fraction); - // Default displaying the fraction as percentage string, but user can override it - char overlay_buf[32]; - if (!overlay) + // Out of courtesy we accept a NaN fraction without crashing + float fill_n0 = 0.0f; + float fill_n1 = (fraction == fraction) ? fraction : 0.0f; + + if (is_indeterminate) { - ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction*100+0.01f); - overlay = overlay_buf; + const float fill_width_n = 0.2f; + fill_n0 = ImFmod(-fraction, 1.0f) * (1.0f + fill_width_n) - fill_width_n; + fill_n1 = ImSaturate(fill_n0 + fill_width_n); + fill_n0 = ImSaturate(fill_n0); } - ImVec2 overlay_size = CalcTextSize(overlay, NULL); - if (overlay_size.x > 0.0f) - RenderTextClipped(ImVec2(ImClamp(fill_br.x + style.ItemSpacing.x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f,0.5f), &bb); + // Render + RenderFrame(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); + bb.Expand(ImVec2(-style.FrameBorderSize, -style.FrameBorderSize)); + RenderRectFilledRangeH(window->DrawList, bb, GetColorU32(ImGuiCol_PlotHistogram), fill_n0, fill_n1, style.FrameRounding); + + // Default displaying the fraction as percentage string, but user can override it + // Don't display text for indeterminate bars by default + char overlay_buf[32]; + if (!is_indeterminate || overlay != NULL) + { + if (!overlay) + { + ImFormatString(overlay_buf, IM_ARRAYSIZE(overlay_buf), "%.0f%%", fraction * 100 + 0.01f); + overlay = overlay_buf; + } + + ImVec2 overlay_size = CalcTextSize(overlay, NULL); + if (overlay_size.x > 0.0f) + { + float text_x = is_indeterminate ? (bb.Min.x + bb.Max.x - overlay_size.x) * 0.5f : ImLerp(bb.Min.x, bb.Max.x, fill_n1) + style.ItemSpacing.x; + RenderTextClipped(ImVec2(ImClamp(text_x, bb.Min.x, bb.Max.x - overlay_size.x - style.ItemInnerSpacing.x), bb.Min.y), bb.Max, overlay, NULL, &overlay_size, ImVec2(0.0f, 0.5f), &bb); + } + } } void ImGui::Bullet() @@ -1148,21 +1423,94 @@ void ImGui::Bullet() ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + g.Style.FramePadding.y*2), g.FontSize); + const float line_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), g.FontSize); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(g.FontSize, line_height)); ItemSize(bb); if (!ItemAdd(bb, 0)) { - SameLine(0, style.FramePadding.x*2); + SameLine(0, style.FramePadding.x * 2); return; } // Render and stay on same line ImU32 text_col = GetColorU32(ImGuiCol_Text); - RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize*0.5f, line_height*0.5f), text_col); + RenderBullet(window->DrawList, bb.Min + ImVec2(style.FramePadding.x + g.FontSize * 0.5f, line_height * 0.5f), text_col); SameLine(0, style.FramePadding.x * 2.0f); } +// This is provided as a convenience for being an often requested feature. +// FIXME-STYLE: we delayed adding as there is a larger plan to revamp the styling system. +// Because of this we currently don't provide many styling options for this widget +// (e.g. hovered/active colors are automatically inferred from a single color). +bool ImGui::TextLink(const char* label) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return false; + + ImGuiContext& g = *GImGui; + const ImGuiID id = window->GetID(label); + const char* label_end = FindRenderedTextEnd(label); + + ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + ImVec2 size = CalcTextSize(label, label_end, true); + ImRect bb(pos, pos + size); + ItemSize(size, 0.0f); + if (!ItemAdd(bb, id)) + return false; + + bool hovered, held; + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + RenderNavCursor(bb, id); + + if (hovered) + SetMouseCursor(ImGuiMouseCursor_Hand); + + ImVec4 text_colf = g.Style.Colors[ImGuiCol_TextLink]; + ImVec4 line_colf = text_colf; + { + // FIXME-STYLE: Read comments above. This widget is NOT written in the same style as some earlier widgets, + // as we are currently experimenting/planning a different styling system. + float h, s, v; + ColorConvertRGBtoHSV(text_colf.x, text_colf.y, text_colf.z, h, s, v); + if (held || hovered) + { + v = ImSaturate(v + (held ? 0.4f : 0.3f)); + h = ImFmod(h + 0.02f, 1.0f); + } + ColorConvertHSVtoRGB(h, s, v, text_colf.x, text_colf.y, text_colf.z); + v = ImSaturate(v - 0.20f); + ColorConvertHSVtoRGB(h, s, v, line_colf.x, line_colf.y, line_colf.z); + } + + float line_y = bb.Max.y + ImFloor(g.Font->Descent * g.FontScale * 0.20f); + window->DrawList->AddLine(ImVec2(bb.Min.x, line_y), ImVec2(bb.Max.x, line_y), GetColorU32(line_colf)); // FIXME-TEXT: Underline mode. + + PushStyleColor(ImGuiCol_Text, GetColorU32(text_colf)); + RenderText(bb.Min, label, label_end); + PopStyleColor(); + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; +} + +void ImGui::TextLinkOpenURL(const char* label, const char* url) +{ + ImGuiContext& g = *GImGui; + if (url == NULL) + url = label; + if (TextLink(label)) + if (g.PlatformIO.Platform_OpenInShellFn != NULL) + g.PlatformIO.Platform_OpenInShellFn(&g, url); + SetItemTooltip(LocalizeGetMsg(ImGuiLocKey_OpenLink_s), url); // It is more reassuring for user to _always_ display URL when we same as label + if (BeginPopupContextItem()) + { + if (MenuItem(LocalizeGetMsg(ImGuiLocKey_CopyLink))) + SetClipboardText(url); + EndPopup(); + } +} + //------------------------------------------------------------------------- // [SECTION] Widgets: Low-level Layout helpers //------------------------------------------------------------------------- @@ -1181,7 +1529,7 @@ void ImGui::Spacing() ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return; - ItemSize(ImVec2(0,0)); + ItemSize(ImVec2(0, 0)); } void ImGui::Dummy(const ImVec2& size) @@ -1204,8 +1552,9 @@ void ImGui::NewLine() ImGuiContext& g = *GImGui; const ImGuiLayoutType backup_layout_type = window->DC.LayoutType; window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.IsSameLine = false; if (window->DC.CurrLineSize.y > 0.0f) // In the event that we are on a line with items that is smaller that FontSize high, we will preserve its height. - ItemSize(ImVec2(0,0)); + ItemSize(ImVec2(0, 0)); else ItemSize(ImVec2(0.0f, g.FontSize)); window->DC.LayoutType = backup_layout_type; @@ -1223,7 +1572,9 @@ void ImGui::AlignTextToFramePadding() } // Horizontal/vertical separating line -void ImGui::SeparatorEx(ImGuiSeparatorFlags flags) +// FIXME: Surprisingly, this seemingly trivial widget is a victim of many different legacy/tricky layout issues. +// Note how thickness == 1.0f is handled specifically as not moving CursorPos by 'thickness', but other values are. +void ImGui::SeparatorEx(ImGuiSeparatorFlags flags, float thickness) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -1231,54 +1582,54 @@ void ImGui::SeparatorEx(ImGuiSeparatorFlags flags) ImGuiContext& g = *GImGui; IM_ASSERT(ImIsPowerOfTwo(flags & (ImGuiSeparatorFlags_Horizontal | ImGuiSeparatorFlags_Vertical))); // Check that only 1 option is selected + IM_ASSERT(thickness > 0.0f); - float thickness_draw = 1.0f; - float thickness_layout = 0.0f; if (flags & ImGuiSeparatorFlags_Vertical) { - // Vertical separator, for menu bars (use current line height). Not exposed because it is misleading and it doesn't have an effect on regular layout. + // Vertical separator, for menu bars (use current line height). float y1 = window->DC.CursorPos.y; float y2 = window->DC.CursorPos.y + window->DC.CurrLineSize.y; - const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness_draw, y2)); - ItemSize(ImVec2(thickness_layout, 0.0f)); + const ImRect bb(ImVec2(window->DC.CursorPos.x, y1), ImVec2(window->DC.CursorPos.x + thickness, y2)); + ItemSize(ImVec2(thickness, 0.0f)); if (!ItemAdd(bb, 0)) return; // Draw - window->DrawList->AddLine(ImVec2(bb.Min.x, bb.Min.y), ImVec2(bb.Min.x, bb.Max.y), GetColorU32(ImGuiCol_Separator)); + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator)); if (g.LogEnabled) LogText(" |"); } else if (flags & ImGuiSeparatorFlags_Horizontal) { // Horizontal Separator - float x1 = window->Pos.x; - float x2 = window->Pos.x + window->Size.x; - if (!window->DC.GroupStack.empty()) - x1 += window->DC.Indent.x; + float x1 = window->DC.CursorPos.x; + float x2 = window->WorkRect.Max.x; - ImGuiColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL; + // Preserve legacy behavior inside Columns() + // Before Tables API happened, we relied on Separator() to span all columns of a Columns() set. + // We currently don't need to provide the same feature for tables because tables naturally have border features. + ImGuiOldColumns* columns = (flags & ImGuiSeparatorFlags_SpanAllColumns) ? window->DC.CurrentColumns : NULL; if (columns) - PushColumnsBackground(); - - // We don't provide our width to the layout so that it doesn't get feed back into AutoFit - const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness_draw)); - ItemSize(ImVec2(0.0f, thickness_layout)); - if (!ItemAdd(bb, 0)) { - if (columns) - { - PopColumnsBackground(); - columns->LineMinY = window->DC.CursorPos.y; - } - return; + x1 = window->Pos.x + window->DC.Indent.x; // Used to be Pos.x before 2023/10/03 + x2 = window->Pos.x + window->Size.x; + PushColumnsBackground(); } - // Draw - window->DrawList->AddLine(bb.Min, ImVec2(bb.Max.x, bb.Min.y), GetColorU32(ImGuiCol_Separator)); - if (g.LogEnabled) - LogRenderedText(&bb.Min, "--------------------------------"); + // We don't provide our width to the layout so that it doesn't get feed back into AutoFit + // FIXME: This prevents ->CursorMaxPos based bounding box evaluation from working (e.g. TableEndCell) + const float thickness_for_layout = (thickness == 1.0f) ? 0.0f : thickness; // FIXME: See 1.70/1.71 Separator() change: makes legacy 1-px separator not affect layout yet. Should change. + const ImRect bb(ImVec2(x1, window->DC.CursorPos.y), ImVec2(x2, window->DC.CursorPos.y + thickness)); + ItemSize(ImVec2(0.0f, thickness_for_layout)); + if (ItemAdd(bb, 0)) + { + // Draw + window->DrawList->AddRectFilled(bb.Min, bb.Max, GetColorU32(ImGuiCol_Separator)); + if (g.LogEnabled) + LogRenderedText(&bb.Min, "--------------------------------\n"); + + } if (columns) { PopColumnsBackground(); @@ -1294,40 +1645,113 @@ void ImGui::Separator() if (window->SkipItems) return; - // Those flags should eventually be overridable by the user + // Those flags should eventually be configurable by the user + // FIXME: We cannot g.Style.SeparatorTextBorderSize for thickness as it relates to SeparatorText() which is a decorated separator, not defaulting to 1.0f. ImGuiSeparatorFlags flags = (window->DC.LayoutType == ImGuiLayoutType_Horizontal) ? ImGuiSeparatorFlags_Vertical : ImGuiSeparatorFlags_Horizontal; - flags |= ImGuiSeparatorFlags_SpanAllColumns; - SeparatorEx(flags); + + // Only applies to legacy Columns() api as they relied on Separator() a lot. + if (window->DC.CurrentColumns) + flags |= ImGuiSeparatorFlags_SpanAllColumns; + + SeparatorEx(flags, 1.0f); +} + +void ImGui::SeparatorTextEx(ImGuiID id, const char* label, const char* label_end, float extra_w) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiStyle& style = g.Style; + + const ImVec2 label_size = CalcTextSize(label, label_end, false); + const ImVec2 pos = window->DC.CursorPos; + const ImVec2 padding = style.SeparatorTextPadding; + + const float separator_thickness = style.SeparatorTextBorderSize; + const ImVec2 min_size(label_size.x + extra_w + padding.x * 2.0f, ImMax(label_size.y + padding.y * 2.0f, separator_thickness)); + const ImRect bb(pos, ImVec2(window->WorkRect.Max.x, pos.y + min_size.y)); + const float text_baseline_y = ImTrunc((bb.GetHeight() - label_size.y) * style.SeparatorTextAlign.y + 0.99999f); //ImMax(padding.y, ImTrunc((style.SeparatorTextSize - label_size.y) * 0.5f)); + ItemSize(min_size, text_baseline_y); + if (!ItemAdd(bb, id)) + return; + + const float sep1_x1 = pos.x; + const float sep2_x2 = bb.Max.x; + const float seps_y = ImTrunc((bb.Min.y + bb.Max.y) * 0.5f + 0.99999f); + + const float label_avail_w = ImMax(0.0f, sep2_x2 - sep1_x1 - padding.x * 2.0f); + const ImVec2 label_pos(pos.x + padding.x + ImMax(0.0f, (label_avail_w - label_size.x - extra_w) * style.SeparatorTextAlign.x), pos.y + text_baseline_y); // FIXME-ALIGN + + // This allows using SameLine() to position something in the 'extra_w' + window->DC.CursorPosPrevLine.x = label_pos.x + label_size.x; + + const ImU32 separator_col = GetColorU32(ImGuiCol_Separator); + if (label_size.x > 0.0f) + { + const float sep1_x2 = label_pos.x - style.ItemSpacing.x; + const float sep2_x1 = label_pos.x + label_size.x + extra_w + style.ItemSpacing.x; + if (sep1_x2 > sep1_x1 && separator_thickness > 0.0f) + window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep1_x2, seps_y), separator_col, separator_thickness); + if (sep2_x2 > sep2_x1 && separator_thickness > 0.0f) + window->DrawList->AddLine(ImVec2(sep2_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness); + if (g.LogEnabled) + LogSetNextTextDecoration("---", NULL); + RenderTextEllipsis(window->DrawList, label_pos, ImVec2(bb.Max.x, bb.Max.y + style.ItemSpacing.y), bb.Max.x, bb.Max.x, label, label_end, &label_size); + } + else + { + if (g.LogEnabled) + LogText("---"); + if (separator_thickness > 0.0f) + window->DrawList->AddLine(ImVec2(sep1_x1, seps_y), ImVec2(sep2_x2, seps_y), separator_col, separator_thickness); + } +} + +void ImGui::SeparatorText(const char* label) +{ + ImGuiWindow* window = GetCurrentWindow(); + if (window->SkipItems) + return; + + // The SeparatorText() vs SeparatorTextEx() distinction is designed to be considerate that we may want: + // - allow separator-text to be draggable items (would require a stable ID + a noticeable highlight) + // - this high-level entry point to allow formatting? (which in turns may require ID separate from formatted string) + // - because of this we probably can't turn 'const char* label' into 'const char* fmt, ...' + // Otherwise, we can decide that users wanting to drag this would layout a dedicated drag-item, + // and then we can turn this into a format function. + SeparatorTextEx(0, label, FindRenderedTextEnd(label), 0.0f); } // Using 'hover_visibility_delay' allows us to hide the highlight and mouse cursor for a short time, which can be convenient to reduce visual noise. -bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend, float hover_visibility_delay) +bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float* size1, float* size2, float min_size1, float min_size2, float hover_extend, float hover_visibility_delay, ImU32 bg_col) { ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - const ImGuiItemFlags item_flags_backup = window->DC.ItemFlags; - window->DC.ItemFlags |= ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus; - bool item_add = ItemAdd(bb, id); - window->DC.ItemFlags = item_flags_backup; - if (!item_add) + if (!ItemAdd(bb, id, NULL, ImGuiItemFlags_NoNav)) return false; + // FIXME: AFAIK the only leftover reason for passing ImGuiButtonFlags_AllowOverlap here is + // to allow caller of SplitterBehavior() to call SetItemAllowOverlap() after the item. + // Nowadays we would instead want to use SetNextItemAllowOverlap() before the item. + ImGuiButtonFlags button_flags = ImGuiButtonFlags_FlattenChildren; +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + button_flags |= ImGuiButtonFlags_AllowOverlap; +#endif + bool hovered, held; ImRect bb_interact = bb; bb_interact.Expand(axis == ImGuiAxis_Y ? ImVec2(0.0f, hover_extend) : ImVec2(hover_extend, 0.0f)); - ButtonBehavior(bb_interact, id, &hovered, &held, ImGuiButtonFlags_FlattenChildren | ImGuiButtonFlags_AllowItemOverlap); - if (g.ActiveId != id) - SetItemAllowOverlap(); + ButtonBehavior(bb_interact, id, &hovered, &held, button_flags); + if (hovered) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredRect; // for IsItemHovered(), because bb_interact is larger than bb - if (held || (g.HoveredId == id && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay)) + if (held || (hovered && g.HoveredIdPreviousFrame == id && g.HoveredIdTimer >= hover_visibility_delay)) SetMouseCursor(axis == ImGuiAxis_Y ? ImGuiMouseCursor_ResizeNS : ImGuiMouseCursor_ResizeEW); ImRect bb_render = bb; if (held) { - ImVec2 mouse_delta_2d = g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min; - float mouse_delta = (axis == ImGuiAxis_Y) ? mouse_delta_2d.y : mouse_delta_2d.x; + float mouse_delta = (g.IO.MousePos - g.ActiveIdClickOffset - bb_interact.Min)[axis]; // Minimum pane size float size_1_maximum_delta = ImMax(0.0f, *size1 - min_size1); @@ -1340,20 +1764,18 @@ bool ImGui::SplitterBehavior(const ImRect& bb, ImGuiID id, ImGuiAxis axis, float // Apply resize if (mouse_delta != 0.0f) { - if (mouse_delta < 0.0f) - IM_ASSERT(*size1 + mouse_delta >= min_size1); - if (mouse_delta > 0.0f) - IM_ASSERT(*size2 - mouse_delta >= min_size2); - *size1 += mouse_delta; - *size2 -= mouse_delta; + *size1 = ImMax(*size1 + mouse_delta, min_size1); + *size2 = ImMax(*size2 - mouse_delta, min_size2); bb_render.Translate((axis == ImGuiAxis_X) ? ImVec2(mouse_delta, 0.0f) : ImVec2(0.0f, mouse_delta)); MarkItemEdited(id); } } - // Render + // Render at new position + if (bg_col & IM_COL32_A_MASK) + window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, bg_col, 0.0f); const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : (hovered && g.HoveredIdTimer >= hover_visibility_delay) ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); - window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, g.Style.FrameRounding); + window->DrawList->AddRectFilled(bb_render.Min, bb_render.Max, col, 0.0f); return held; } @@ -1368,11 +1790,13 @@ static int IMGUI_CDECL ShrinkWidthItemComparer(const void* lhs, const void* rhs) } // Shrink excess width from a set of item, by removing width from the larger items first. +// Set items Width to -1.0f to disable shrinking this item. void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_excess) { if (count == 1) { - items[0].Width = ImMax(items[0].Width - width_excess, 1.0f); + if (items[0].Width >= 0.0f) + items[0].Width = ImMax(items[0].Width - width_excess, 1.0f); return; } ImQsort(items, (size_t)count, sizeof(ImGuiShrinkWidthItem), ShrinkWidthItemComparer); @@ -1381,33 +1805,42 @@ void ImGui::ShrinkWidths(ImGuiShrinkWidthItem* items, int count, float width_exc { while (count_same_width < count && items[0].Width <= items[count_same_width].Width) count_same_width++; - float max_width_to_remove_per_item = (count_same_width < count) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f); + float max_width_to_remove_per_item = (count_same_width < count && items[count_same_width].Width >= 0.0f) ? (items[0].Width - items[count_same_width].Width) : (items[0].Width - 1.0f); + if (max_width_to_remove_per_item <= 0.0f) + break; float width_to_remove_per_item = ImMin(width_excess / count_same_width, max_width_to_remove_per_item); for (int item_n = 0; item_n < count_same_width; item_n++) items[item_n].Width -= width_to_remove_per_item; width_excess -= width_to_remove_per_item * count_same_width; } - // Round width and redistribute remainder left-to-right (could make it an option of the function?) + // Round width and redistribute remainder // Ensure that e.g. the right-most tab of a shrunk tab-bar always reaches exactly at the same distance from the right-most edge of the tab bar separator. width_excess = 0.0f; for (int n = 0; n < count; n++) { - float width_rounded = ImFloor(items[n].Width); + float width_rounded = ImTrunc(items[n].Width); width_excess += items[n].Width - width_rounded; items[n].Width = width_rounded; } - if (width_excess > 0.0f) - for (int n = 0; n < count; n++) - if (items[n].Index < (int)(width_excess + 0.01f)) - items[n].Width += 1.0f; + while (width_excess > 0.0f) + for (int n = 0; n < count && width_excess > 0.0f; n++) + { + float width_to_add = ImMin(items[n].InitialWidth - items[n].Width, 1.0f); + items[n].Width += width_to_add; + width_excess -= width_to_add; + } } //------------------------------------------------------------------------- // [SECTION] Widgets: ComboBox //------------------------------------------------------------------------- +// - CalcMaxPopupHeightFromItemCount() [Internal] // - BeginCombo() +// - BeginComboPopup() [Internal] // - EndCombo() +// - BeginComboPreview() [Internal] +// - EndComboPreview() [Internal] // - Combo() //------------------------------------------------------------------------- @@ -1421,99 +1854,135 @@ static float CalcMaxPopupHeightFromItemCount(int items_count) bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboFlags flags) { - // Always consume the SetNextWindowSizeConstraint() call in our early return paths ImGuiContext& g = *GImGui; - bool has_window_size_constraint = (g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint) != 0; - g.NextWindowData.Flags &= ~ImGuiNextWindowDataFlags_HasSizeConstraint; - ImGuiWindow* window = GetCurrentWindow(); + + ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags; + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values if (window->SkipItems) return false; - IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together - const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); + IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together + if (flags & ImGuiComboFlags_WidthFitPreview) + IM_ASSERT((flags & (ImGuiComboFlags_NoPreview | (ImGuiComboFlags)ImGuiComboFlags_CustomPreview)) == 0); const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight(); const ImVec2 label_size = CalcTextSize(label, NULL, true); - const float expected_w = CalcItemWidth(); - const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : expected_w; - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f)); - const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + const float preview_width = ((flags & ImGuiComboFlags_WidthFitPreview) && (preview_value != NULL)) ? CalcTextSize(preview_value, NULL, true).x : 0.0f; + const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : ((flags & ImGuiComboFlags_WidthFitPreview) ? (arrow_size + preview_width + style.FramePadding.x * 2.0f) : CalcItemWidth()); + const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); + const ImRect total_bb(bb.Min, bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, id, &frame_bb)) + if (!ItemAdd(total_bb, id, &bb)) return false; + // Open on click bool hovered, held; - bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held); - bool popup_open = IsPopupOpen(id); + bool pressed = ButtonBehavior(bb, id, &hovered, &held); + const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, id); + bool popup_open = IsPopupOpen(popup_id, ImGuiPopupFlags_None); + if (pressed && !popup_open) + { + OpenPopupEx(popup_id, ImGuiPopupFlags_None); + popup_open = true; + } + // Render shape const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); - const float value_x2 = ImMax(frame_bb.Min.x, frame_bb.Max.x - arrow_size); - RenderNavHighlight(frame_bb, id); + const float value_x2 = ImMax(bb.Min.x, bb.Max.x - arrow_size); + RenderNavCursor(bb, id); if (!(flags & ImGuiComboFlags_NoPreview)) - window->DrawList->AddRectFilled(frame_bb.Min, ImVec2(value_x2, frame_bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Left); + window->DrawList->AddRectFilled(bb.Min, ImVec2(value_x2, bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft); if (!(flags & ImGuiComboFlags_NoArrowButton)) { ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button); ImU32 text_col = GetColorU32(ImGuiCol_Text); - window->DrawList->AddRectFilled(ImVec2(value_x2, frame_bb.Min.y), frame_bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawCornerFlags_All : ImDrawCornerFlags_Right); - if (value_x2 + arrow_size - style.FramePadding.x <= frame_bb.Max.x) - RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f); + window->DrawList->AddRectFilled(ImVec2(value_x2, bb.Min.y), bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight); + if (value_x2 + arrow_size - style.FramePadding.x <= bb.Max.x) + RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f); } - RenderFrameBorder(frame_bb.Min, frame_bb.Max, style.FrameRounding); - if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview)) - RenderTextClipped(frame_bb.Min + style.FramePadding, ImVec2(value_x2, frame_bb.Max.y), preview_value, NULL, NULL, ImVec2(0.0f,0.0f)); - if (label_size.x > 0) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + RenderFrameBorder(bb.Min, bb.Max, style.FrameRounding); - if ((pressed || g.NavActivateId == id) && !popup_open) + // Custom preview + if (flags & ImGuiComboFlags_CustomPreview) { - if (window->DC.NavLayerCurrent == 0) - window->NavLastIds[0] = id; - OpenPopupEx(id); - popup_open = true; + g.ComboPreviewData.PreviewRect = ImRect(bb.Min.x, bb.Min.y, value_x2, bb.Max.y); + IM_ASSERT(preview_value == NULL || preview_value[0] == 0); + preview_value = NULL; } + // Render preview and label + if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview)) + { + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); + RenderTextClipped(bb.Min + style.FramePadding, ImVec2(value_x2, bb.Max.y), preview_value, NULL, NULL); + } + if (label_size.x > 0) + RenderText(ImVec2(bb.Max.x + style.ItemInnerSpacing.x, bb.Min.y + style.FramePadding.y), label); + if (!popup_open) return false; - if (has_window_size_constraint) + g.NextWindowData.HasFlags = backup_next_window_data_flags; + return BeginComboPopup(popup_id, bb, flags); +} + +bool ImGui::BeginComboPopup(ImGuiID popup_id, const ImRect& bb, ImGuiComboFlags flags) +{ + ImGuiContext& g = *GImGui; + if (!IsPopupOpen(popup_id, ImGuiPopupFlags_None)) + { + g.NextWindowData.ClearFlags(); + return false; + } + + // Set popup size + float w = bb.GetWidth(); + if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint) { - g.NextWindowData.Flags |= ImGuiNextWindowDataFlags_HasSizeConstraint; g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w); } else { if ((flags & ImGuiComboFlags_HeightMask_) == 0) flags |= ImGuiComboFlags_HeightRegular; - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one int popup_max_height_in_items = -1; if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8; else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4; else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20; - SetNextWindowSizeConstraints(ImVec2(w, 0.0f), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); + ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX); + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size + constraint_min.x = w; + if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f) + constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items); + SetNextWindowSizeConstraints(constraint_min, constraint_max); } + // This is essentially a specialized version of BeginPopupEx() char name[16]; - ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth + ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginComboDepth); // Recycle windows based on depth - // Peak into expected window size so we can position it + // Set position given a custom constraint (peak into expected window size so we can position it) + // FIXME: This might be easier to express with an hypothetical SetNextWindowPosConstraints() function? + // FIXME: This might be moved to Begin() or at least around the same spot where Tooltips and other Popups are calling FindBestWindowPosForPopupEx()? if (ImGuiWindow* popup_window = FindWindowByName(name)) if (popup_window->WasActive) { - ImVec2 size_expected = CalcWindowExpectedSize(popup_window); - if (flags & ImGuiComboFlags_PopupAlignLeft) - popup_window->AutoPosLastDirection = ImGuiDir_Left; - ImRect r_outer = GetWindowAllowedExtentRect(popup_window); - ImVec2 pos = FindBestWindowPosForPopupEx(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, frame_bb, ImGuiPopupPositionPolicy_ComboBox); + // Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us. + ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window); + popup_window->AutoPosLastDirection = (flags & ImGuiComboFlags_PopupAlignLeft) ? ImGuiDir_Left : ImGuiDir_Down; // Left = "Below, Toward Left", Down = "Below, Toward Right (default)" + ImRect r_outer = GetPopupAllowedExtentRect(popup_window); + ImVec2 pos = FindBestWindowPosForPopupEx(bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, bb, ImGuiPopupPositionPolicy_ComboBox); SetNextWindowPos(pos); } - // Horizontally align ourselves with the framed text - ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings; - PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(style.FramePadding.x, style.WindowPadding.y)); + // We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx() + ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove; + PushStyleVarX(ImGuiStyleVar_WindowPadding, g.Style.FramePadding.x); // Horizontally align ourselves with the framed text bool ret = Begin(name, NULL, window_flags); PopStyleVar(); if (!ret) @@ -1522,27 +1991,80 @@ bool ImGui::BeginCombo(const char* label, const char* preview_value, ImGuiComboF IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above return false; } + g.BeginComboDepth++; return true; } void ImGui::EndCombo() { + ImGuiContext& g = *GImGui; EndPopup(); + g.BeginComboDepth--; } -// Getter for the old Combo() API: const char*[] -static bool Items_ArrayGetter(void* data, int idx, const char** out_text) +// Call directly after the BeginCombo/EndCombo block. The preview is designed to only host non-interactive elements +// (Experimental, see GitHub issues: #1658, #4168) +bool ImGui::BeginComboPreview() { - const char* const* items = (const char* const*)data; - if (out_text) - *out_text = items[idx]; + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiComboPreviewData* preview_data = &g.ComboPreviewData; + + if (window->SkipItems || !(g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible)) + return false; + IM_ASSERT(g.LastItemData.Rect.Min.x == preview_data->PreviewRect.Min.x && g.LastItemData.Rect.Min.y == preview_data->PreviewRect.Min.y); // Didn't call after BeginCombo/EndCombo block or forgot to pass ImGuiComboFlags_CustomPreview flag? + if (!window->ClipRect.Overlaps(preview_data->PreviewRect)) // Narrower test (optional) + return false; + + // FIXME: This could be contained in a PushWorkRect() api + preview_data->BackupCursorPos = window->DC.CursorPos; + preview_data->BackupCursorMaxPos = window->DC.CursorMaxPos; + preview_data->BackupCursorPosPrevLine = window->DC.CursorPosPrevLine; + preview_data->BackupPrevLineTextBaseOffset = window->DC.PrevLineTextBaseOffset; + preview_data->BackupLayout = window->DC.LayoutType; + window->DC.CursorPos = preview_data->PreviewRect.Min + g.Style.FramePadding; + window->DC.CursorMaxPos = window->DC.CursorPos; + window->DC.LayoutType = ImGuiLayoutType_Horizontal; + window->DC.IsSameLine = false; + PushClipRect(preview_data->PreviewRect.Min, preview_data->PreviewRect.Max, true); + return true; } -// Getter for the old Combo() API: "item1\0item2\0item3\0" -static bool Items_SingleStringGetter(void* data, int idx, const char** out_text) +void ImGui::EndComboPreview() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiComboPreviewData* preview_data = &g.ComboPreviewData; + + // FIXME: Using CursorMaxPos approximation instead of correct AABB which we will store in ImDrawCmd in the future + ImDrawList* draw_list = window->DrawList; + if (window->DC.CursorMaxPos.x < preview_data->PreviewRect.Max.x && window->DC.CursorMaxPos.y < preview_data->PreviewRect.Max.y) + if (draw_list->CmdBuffer.Size > 1) // Unlikely case that the PushClipRect() didn't create a command + { + draw_list->_CmdHeader.ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 1].ClipRect = draw_list->CmdBuffer[draw_list->CmdBuffer.Size - 2].ClipRect; + draw_list->_TryMergeDrawCmds(); + } + PopClipRect(); + window->DC.CursorPos = preview_data->BackupCursorPos; + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, preview_data->BackupCursorMaxPos); + window->DC.CursorPosPrevLine = preview_data->BackupCursorPosPrevLine; + window->DC.PrevLineTextBaseOffset = preview_data->BackupPrevLineTextBaseOffset; + window->DC.LayoutType = preview_data->BackupLayout; + window->DC.IsSameLine = false; + preview_data->PreviewRect = ImRect(); +} + +// Getter for the old Combo() API: const char*[] +static const char* Items_ArrayGetter(void* data, int idx) +{ + const char* const* items = (const char* const*)data; + return items[idx]; +} + +// Getter for the old Combo() API: "item1\0item2\0item3\0" +static const char* Items_SingleStringGetter(void* data, int idx) { - // FIXME-OPT: we could pre-compute the indices to fasten this. But only 1 active combo means the waste is limited. const char* items_separated_by_zeros = (const char*)data; int items_count = 0; const char* p = items_separated_by_zeros; @@ -1550,54 +2072,57 @@ static bool Items_SingleStringGetter(void* data, int idx, const char** out_text) { if (idx == items_count) break; - p += strlen(p) + 1; + p += ImStrlen(p) + 1; items_count++; } - if (!*p) - return false; - if (out_text) - *out_text = p; - return true; + return *p ? p : NULL; } // Old API, prefer using BeginCombo() nowadays if you can. -bool ImGui::Combo(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int popup_max_height_in_items) +bool ImGui::Combo(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int popup_max_height_in_items) { ImGuiContext& g = *GImGui; // Call the getter to obtain the preview string which is a parameter to BeginCombo() const char* preview_value = NULL; if (*current_item >= 0 && *current_item < items_count) - items_getter(data, *current_item, &preview_value); + preview_value = getter(user_data, *current_item); // The old Combo() API exposed "popup_max_height_in_items". The new more general BeginCombo() API doesn't have/need it, but we emulate it here. - if (popup_max_height_in_items != -1 && !(g.NextWindowData.Flags & ImGuiNextWindowDataFlags_HasSizeConstraint)) - SetNextWindowSizeConstraints(ImVec2(0,0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); + if (popup_max_height_in_items != -1 && !(g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)) + SetNextWindowSizeConstraints(ImVec2(0, 0), ImVec2(FLT_MAX, CalcMaxPopupHeightFromItemCount(popup_max_height_in_items))); if (!BeginCombo(label, preview_value, ImGuiComboFlags_None)) return false; // Display items - // FIXME-OPT: Use clipper (but we need to disable it on the appearing frame to make sure our call to SetItemDefaultFocus() is processed) bool value_changed = false; - for (int i = 0; i < items_count; i++) - { - PushID((void*)(intptr_t)i); - const bool item_selected = (i == *current_item); - const char* item_text; - if (!items_getter(data, i, &item_text)) - item_text = "*Unknown item*"; - if (Selectable(item_text, item_selected)) + ImGuiListClipper clipper; + clipper.Begin(items_count); + clipper.IncludeItemByIndex(*current_item); + while (clipper.Step()) + for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - value_changed = true; - *current_item = i; + const char* item_text = getter(user_data, i); + if (item_text == NULL) + item_text = "*Unknown item*"; + + PushID(i); + const bool item_selected = (i == *current_item); + if (Selectable(item_text, item_selected) && *current_item != i) + { + value_changed = true; + *current_item = i; + } + if (item_selected) + SetItemDefaultFocus(); + PopID(); } - if (item_selected) - SetItemDefaultFocus(); - PopID(); - } EndCombo(); + if (value_changed) + MarkItemEdited(g.LastItemData.ID); + return value_changed; } @@ -1615,99 +2140,102 @@ bool ImGui::Combo(const char* label, int* current_item, const char* items_separa const char* p = items_separated_by_zeros; // FIXME-OPT: Avoid computing this, or at least only when combo is open while (*p) { - p += strlen(p) + 1; + p += ImStrlen(p) + 1; items_count++; } bool value_changed = Combo(label, current_item, Items_SingleStringGetter, (void*)items_separated_by_zeros, items_count, height_in_items); return value_changed; } +#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS + +struct ImGuiGetNameFromIndexOldToNewCallbackData { void* UserData; bool (*OldCallback)(void*, int, const char**); }; +static const char* ImGuiGetNameFromIndexOldToNewCallback(void* user_data, int idx) +{ + ImGuiGetNameFromIndexOldToNewCallbackData* data = (ImGuiGetNameFromIndexOldToNewCallbackData*)user_data; + const char* s = NULL; + data->OldCallback(data->UserData, idx, &s); + return s; +} + +bool ImGui::ListBox(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int height_in_items) +{ + ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter }; + return ListBox(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, height_in_items); +} +bool ImGui::Combo(const char* label, int* current_item, bool (*old_getter)(void*, int, const char**), void* user_data, int items_count, int popup_max_height_in_items) +{ + ImGuiGetNameFromIndexOldToNewCallbackData old_to_new_data = { user_data, old_getter }; + return Combo(label, current_item, ImGuiGetNameFromIndexOldToNewCallback, &old_to_new_data, items_count, popup_max_height_in_items); +} + +#endif + //------------------------------------------------------------------------- // [SECTION] Data Type and Data Formatting Helpers [Internal] //------------------------------------------------------------------------- -// - PatchFormatStringFloatToInt() // - DataTypeGetInfo() // - DataTypeFormatString() // - DataTypeApplyOp() -// - DataTypeApplyOpFromText() +// - DataTypeApplyFromText() +// - DataTypeCompare() +// - DataTypeClamp() // - GetMinimumStepAtDecimalPrecision // - RoundScalarWithFormat<>() //------------------------------------------------------------------------- static const ImGuiDataTypeInfo GDataTypeInfo[] = { - { sizeof(char), "%d", "%d" }, // ImGuiDataType_S8 - { sizeof(unsigned char), "%u", "%u" }, - { sizeof(short), "%d", "%d" }, // ImGuiDataType_S16 - { sizeof(unsigned short), "%u", "%u" }, - { sizeof(int), "%d", "%d" }, // ImGuiDataType_S32 - { sizeof(unsigned int), "%u", "%u" }, + { sizeof(char), "S8", "%d", "%d" }, // ImGuiDataType_S8 + { sizeof(unsigned char), "U8", "%u", "%u" }, + { sizeof(short), "S16", "%d", "%d" }, // ImGuiDataType_S16 + { sizeof(unsigned short), "U16", "%u", "%u" }, + { sizeof(int), "S32", "%d", "%d" }, // ImGuiDataType_S32 + { sizeof(unsigned int), "U32", "%u", "%u" }, #ifdef _MSC_VER - { sizeof(ImS64), "%I64d","%I64d" }, // ImGuiDataType_S64 - { sizeof(ImU64), "%I64u","%I64u" }, + { sizeof(ImS64), "S64", "%I64d","%I64d" }, // ImGuiDataType_S64 + { sizeof(ImU64), "U64", "%I64u","%I64u" }, #else - { sizeof(ImS64), "%lld", "%lld" }, // ImGuiDataType_S64 - { sizeof(ImU64), "%llu", "%llu" }, + { sizeof(ImS64), "S64", "%lld", "%lld" }, // ImGuiDataType_S64 + { sizeof(ImU64), "U64", "%llu", "%llu" }, #endif - { sizeof(float), "%f", "%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg) - { sizeof(double), "%f", "%lf" }, // ImGuiDataType_Double + { sizeof(float), "float", "%.3f","%f" }, // ImGuiDataType_Float (float are promoted to double in va_arg) + { sizeof(double), "double","%f", "%lf" }, // ImGuiDataType_Double + { sizeof(bool), "bool", "%d", "%d" }, // ImGuiDataType_Bool + { 0, "char*","%s", "%s" }, // ImGuiDataType_String }; IM_STATIC_ASSERT(IM_ARRAYSIZE(GDataTypeInfo) == ImGuiDataType_COUNT); -// FIXME-LEGACY: Prior to 1.61 our DragInt() function internally used floats and because of this the compile-time default value for format was "%.0f". -// Even though we changed the compile-time default, we expect users to have carried %f around, which would break the display of DragInt() calls. -// To honor backward compatibility we are rewriting the format string, unless IMGUI_DISABLE_OBSOLETE_FUNCTIONS is enabled. What could possibly go wrong?! -static const char* PatchFormatStringFloatToInt(const char* fmt) -{ - if (fmt[0] == '%' && fmt[1] == '.' && fmt[2] == '0' && fmt[3] == 'f' && fmt[4] == 0) // Fast legacy path for "%.0f" which is expected to be the most common case. - return "%d"; - const char* fmt_start = ImParseFormatFindStart(fmt); // Find % (if any, and ignore %%) - const char* fmt_end = ImParseFormatFindEnd(fmt_start); // Find end of format specifier, which itself is an exercise of confidence/recklessness (because snprintf is dependent on libc or user). - if (fmt_end > fmt_start && fmt_end[-1] == 'f') - { -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS - if (fmt_start == fmt && fmt_end[0] == 0) - return "%d"; - ImGuiContext& g = *GImGui; - ImFormatString(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), "%.*s%%d%s", (int)(fmt_start - fmt), fmt, fmt_end); // Honor leading and trailing decorations, but lose alignment/precision. - return g.TempBuffer; -#else - IM_ASSERT(0 && "DragInt(): Invalid format string!"); // Old versions used a default parameter of "%.0f", please replace with e.g. "%d" -#endif - } - return fmt; -} - const ImGuiDataTypeInfo* ImGui::DataTypeGetInfo(ImGuiDataType data_type) { IM_ASSERT(data_type >= 0 && data_type < ImGuiDataType_COUNT); return &GDataTypeInfo[data_type]; } -int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* data_ptr, const char* format) +int ImGui::DataTypeFormatString(char* buf, int buf_size, ImGuiDataType data_type, const void* p_data, const char* format) { // Signedness doesn't matter when pushing integer arguments if (data_type == ImGuiDataType_S32 || data_type == ImGuiDataType_U32) - return ImFormatString(buf, buf_size, format, *(const ImU32*)data_ptr); + return ImFormatString(buf, buf_size, format, *(const ImU32*)p_data); if (data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64) - return ImFormatString(buf, buf_size, format, *(const ImU64*)data_ptr); + return ImFormatString(buf, buf_size, format, *(const ImU64*)p_data); if (data_type == ImGuiDataType_Float) - return ImFormatString(buf, buf_size, format, *(const float*)data_ptr); + return ImFormatString(buf, buf_size, format, *(const float*)p_data); if (data_type == ImGuiDataType_Double) - return ImFormatString(buf, buf_size, format, *(const double*)data_ptr); + return ImFormatString(buf, buf_size, format, *(const double*)p_data); if (data_type == ImGuiDataType_S8) - return ImFormatString(buf, buf_size, format, *(const ImS8*)data_ptr); + return ImFormatString(buf, buf_size, format, *(const ImS8*)p_data); if (data_type == ImGuiDataType_U8) - return ImFormatString(buf, buf_size, format, *(const ImU8*)data_ptr); + return ImFormatString(buf, buf_size, format, *(const ImU8*)p_data); if (data_type == ImGuiDataType_S16) - return ImFormatString(buf, buf_size, format, *(const ImS16*)data_ptr); + return ImFormatString(buf, buf_size, format, *(const ImS16*)p_data); if (data_type == ImGuiDataType_U16) - return ImFormatString(buf, buf_size, format, *(const ImU16*)data_ptr); + return ImFormatString(buf, buf_size, format, *(const ImU16*)p_data); IM_ASSERT(0); return 0; } -void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, void* arg1, const void* arg2) +void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, const void* arg1, const void* arg2) { IM_ASSERT(op == '+' || op == '-'); switch (data_type) @@ -1759,105 +2287,116 @@ void ImGui::DataTypeApplyOp(ImGuiDataType data_type, int op, void* output, void* // User can input math operators (e.g. +100) to edit a numerical values. // NB: This is _not_ a full expression evaluator. We should probably add one and replace this dumb mess.. -bool ImGui::DataTypeApplyOpFromText(const char* buf, const char* initial_value_buf, ImGuiDataType data_type, void* data_ptr, const char* format) +bool ImGui::DataTypeApplyFromText(const char* buf, ImGuiDataType data_type, void* p_data, const char* format, void* p_data_when_empty) { + // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all. + const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type); + ImGuiDataTypeStorage data_backup; + memcpy(&data_backup, p_data, type_info->Size); + while (ImCharIsBlankA(*buf)) buf++; - - // We don't support '-' op because it would conflict with inputing negative value. - // Instead you can use +-100 to subtract from an existing value - char op = buf[0]; - if (op == '+' || op == '*' || op == '/') - { - buf++; - while (ImCharIsBlankA(*buf)) - buf++; - } - else - { - op = 0; - } if (!buf[0]) + { + if (p_data_when_empty != NULL) + { + memcpy(p_data, p_data_when_empty, type_info->Size); + return memcmp(&data_backup, p_data, type_info->Size) != 0; + } return false; + } - // Copy the value in an opaque buffer so we can compare at the end of the function if it changed at all. - IM_ASSERT(data_type < ImGuiDataType_COUNT); - int data_backup[2]; - const ImGuiDataTypeInfo* type_info = ImGui::DataTypeGetInfo(data_type); - IM_ASSERT(type_info->Size <= sizeof(data_backup)); - memcpy(data_backup, data_ptr, type_info->Size); - - if (format == NULL) + // Sanitize format + // - For float/double we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in, so force them into %f and %lf + // - In theory could treat empty format as using default, but this would only cover rare/bizarre case of using InputScalar() + integer + format string without %. + char format_sanitized[32]; + if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) format = type_info->ScanFmt; - - // FIXME-LEGACY: The aim is to remove those operators and write a proper expression evaluator at some point.. - int arg1i = 0; - if (data_type == ImGuiDataType_S32) - { - int* v = (int*)data_ptr; - int arg0i = *v; - float arg1f = 0.0f; - if (op && sscanf(initial_value_buf, format, &arg0i) < 1) - return false; - // Store operand in a float so we can use fractional value for multipliers (*1.1), but constant always parsed as integer so we can fit big integers (e.g. 2000000003) past float precision - if (op == '+') { if (sscanf(buf, "%d", &arg1i)) *v = (int)(arg0i + arg1i); } // Add (use "+-" to subtract) - else if (op == '*') { if (sscanf(buf, "%f", &arg1f)) *v = (int)(arg0i * arg1f); } // Multiply - else if (op == '/') { if (sscanf(buf, "%f", &arg1f) && arg1f != 0.0f) *v = (int)(arg0i / arg1f); } // Divide - else { if (sscanf(buf, format, &arg1i) == 1) *v = arg1i; } // Assign constant - } - else if (data_type == ImGuiDataType_Float) - { - // For floats we have to ignore format with precision (e.g. "%.2f") because sscanf doesn't take them in - format = "%f"; - float* v = (float*)data_ptr; - float arg0f = *v, arg1f = 0.0f; - if (op && sscanf(initial_value_buf, format, &arg0f) < 1) - return false; - if (sscanf(buf, format, &arg1f) < 1) - return false; - if (op == '+') { *v = arg0f + arg1f; } // Add (use "+-" to subtract) - else if (op == '*') { *v = arg0f * arg1f; } // Multiply - else if (op == '/') { if (arg1f != 0.0f) *v = arg0f / arg1f; } // Divide - else { *v = arg1f; } // Assign constant - } - else if (data_type == ImGuiDataType_Double) - { - format = "%lf"; // scanf differentiate float/double unlike printf which forces everything to double because of ellipsis - double* v = (double*)data_ptr; - double arg0f = *v, arg1f = 0.0; - if (op && sscanf(initial_value_buf, format, &arg0f) < 1) - return false; - if (sscanf(buf, format, &arg1f) < 1) - return false; - if (op == '+') { *v = arg0f + arg1f; } // Add (use "+-" to subtract) - else if (op == '*') { *v = arg0f * arg1f; } // Multiply - else if (op == '/') { if (arg1f != 0.0f) *v = arg0f / arg1f; } // Divide - else { *v = arg1f; } // Assign constant - } - else if (data_type == ImGuiDataType_U32 || data_type == ImGuiDataType_S64 || data_type == ImGuiDataType_U64) - { - // All other types assign constant - // We don't bother handling support for legacy operators since they are a little too crappy. Instead we will later implement a proper expression evaluator in the future. - sscanf(buf, format, data_ptr); - } else + format = ImParseFormatSanitizeForScanning(format, format_sanitized, IM_ARRAYSIZE(format_sanitized)); + + // Small types need a 32-bit buffer to receive the result from scanf() + int v32 = 0; + if (sscanf(buf, format, type_info->Size >= 4 ? p_data : &v32) < 1) + return false; + if (type_info->Size < 4) { - // Small types need a 32-bit buffer to receive the result from scanf() - int v32; - sscanf(buf, format, &v32); if (data_type == ImGuiDataType_S8) - *(ImS8*)data_ptr = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX); + *(ImS8*)p_data = (ImS8)ImClamp(v32, (int)IM_S8_MIN, (int)IM_S8_MAX); else if (data_type == ImGuiDataType_U8) - *(ImU8*)data_ptr = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX); + *(ImU8*)p_data = (ImU8)ImClamp(v32, (int)IM_U8_MIN, (int)IM_U8_MAX); else if (data_type == ImGuiDataType_S16) - *(ImS16*)data_ptr = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX); + *(ImS16*)p_data = (ImS16)ImClamp(v32, (int)IM_S16_MIN, (int)IM_S16_MAX); else if (data_type == ImGuiDataType_U16) - *(ImU16*)data_ptr = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX); + *(ImU16*)p_data = (ImU16)ImClamp(v32, (int)IM_U16_MIN, (int)IM_U16_MAX); else IM_ASSERT(0); } - return memcmp(data_backup, data_ptr, type_info->Size) != 0; + return memcmp(&data_backup, p_data, type_info->Size) != 0; +} + +template +static int DataTypeCompareT(const T* lhs, const T* rhs) +{ + if (*lhs < *rhs) return -1; + if (*lhs > *rhs) return +1; + return 0; +} + +int ImGui::DataTypeCompare(ImGuiDataType data_type, const void* arg_1, const void* arg_2) +{ + switch (data_type) + { + case ImGuiDataType_S8: return DataTypeCompareT((const ImS8* )arg_1, (const ImS8* )arg_2); + case ImGuiDataType_U8: return DataTypeCompareT((const ImU8* )arg_1, (const ImU8* )arg_2); + case ImGuiDataType_S16: return DataTypeCompareT((const ImS16* )arg_1, (const ImS16* )arg_2); + case ImGuiDataType_U16: return DataTypeCompareT((const ImU16* )arg_1, (const ImU16* )arg_2); + case ImGuiDataType_S32: return DataTypeCompareT((const ImS32* )arg_1, (const ImS32* )arg_2); + case ImGuiDataType_U32: return DataTypeCompareT((const ImU32* )arg_1, (const ImU32* )arg_2); + case ImGuiDataType_S64: return DataTypeCompareT((const ImS64* )arg_1, (const ImS64* )arg_2); + case ImGuiDataType_U64: return DataTypeCompareT((const ImU64* )arg_1, (const ImU64* )arg_2); + case ImGuiDataType_Float: return DataTypeCompareT((const float* )arg_1, (const float* )arg_2); + case ImGuiDataType_Double: return DataTypeCompareT((const double*)arg_1, (const double*)arg_2); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return 0; +} + +template +static bool DataTypeClampT(T* v, const T* v_min, const T* v_max) +{ + // Clamp, both sides are optional, return true if modified + if (v_min && *v < *v_min) { *v = *v_min; return true; } + if (v_max && *v > *v_max) { *v = *v_max; return true; } + return false; +} + +bool ImGui::DataTypeClamp(ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max) +{ + switch (data_type) + { + case ImGuiDataType_S8: return DataTypeClampT((ImS8* )p_data, (const ImS8* )p_min, (const ImS8* )p_max); + case ImGuiDataType_U8: return DataTypeClampT((ImU8* )p_data, (const ImU8* )p_min, (const ImU8* )p_max); + case ImGuiDataType_S16: return DataTypeClampT((ImS16* )p_data, (const ImS16* )p_min, (const ImS16* )p_max); + case ImGuiDataType_U16: return DataTypeClampT((ImU16* )p_data, (const ImU16* )p_min, (const ImU16* )p_max); + case ImGuiDataType_S32: return DataTypeClampT((ImS32* )p_data, (const ImS32* )p_min, (const ImS32* )p_max); + case ImGuiDataType_U32: return DataTypeClampT((ImU32* )p_data, (const ImU32* )p_min, (const ImU32* )p_max); + case ImGuiDataType_S64: return DataTypeClampT((ImS64* )p_data, (const ImS64* )p_min, (const ImS64* )p_max); + case ImGuiDataType_U64: return DataTypeClampT((ImU64* )p_data, (const ImU64* )p_min, (const ImU64* )p_max); + case ImGuiDataType_Float: return DataTypeClampT((float* )p_data, (const float* )p_min, (const float* )p_max); + case ImGuiDataType_Double: return DataTypeClampT((double*)p_data, (const double*)p_min, (const double*)p_max); + case ImGuiDataType_COUNT: break; + } + IM_ASSERT(0); + return false; +} + +bool ImGui::DataTypeIsZero(ImGuiDataType data_type, const void* p_data) +{ + ImGuiContext& g = *GImGui; + return DataTypeCompare(data_type, p_data, &g.DataTypeZeroValue) == 0; } static float GetMinimumStepAtDecimalPrecision(int decimal_precision) @@ -1869,33 +2408,27 @@ static float GetMinimumStepAtDecimalPrecision(int decimal_precision) } template -static const char* ImAtoi(const char* src, TYPE* output) -{ - int negative = 0; - if (*src == '-') { negative = 1; src++; } - if (*src == '+') { src++; } - TYPE v = 0; - while (*src >= '0' && *src <= '9') - v = (v * 10) + (*src++ - '0'); - *output = negative ? -v : v; - return src; -} - -template TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, TYPE v) { + IM_UNUSED(data_type); + IM_ASSERT(data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double); const char* fmt_start = ImParseFormatFindStart(format); if (fmt_start[0] != '%' || fmt_start[1] == '%') // Don't apply if the value is not visible in the format string return v; + + // Sanitize format + char fmt_sanitized[32]; + ImParseFormatSanitizeForPrinting(fmt_start, fmt_sanitized, IM_ARRAYSIZE(fmt_sanitized)); + fmt_start = fmt_sanitized; + + // Format value with our rounding, and read back char v_str[64]; ImFormatString(v_str, IM_ARRAYSIZE(v_str), fmt_start, v); const char* p = v_str; while (*p == ' ') p++; - if (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) - v = (TYPE)ImAtof(p); - else - ImAtoi(p, (SIGNEDTYPE*)&v); + v = (TYPE)ImAtof(p); + return v; } @@ -1920,35 +2453,36 @@ TYPE ImGui::RoundScalarWithFormatT(const char* format, ImGuiDataType data_type, // This is called by DragBehavior() when the widget is active (held by mouse or being manipulated with Nav controls) template -bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, float power, ImGuiDragFlags flags) +bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags) { ImGuiContext& g = *GImGui; - const ImGuiAxis axis = (flags & ImGuiDragFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; - const bool is_decimal = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); - const bool is_clamped = (v_min < v_max); - const bool is_power = (power != 1.0f && is_decimal && is_clamped && (v_max - v_min < FLT_MAX)); - const bool is_locked = (v_min > v_max); - if (is_locked) - return false; + const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; + const bool is_bounded = (v_min < v_max) || ((v_min == v_max) && (v_min != 0.0f || (flags & ImGuiSliderFlags_ClampZeroRange))); + const bool is_wrapped = is_bounded && (flags & ImGuiSliderFlags_WrapAround); + const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); // Default tweak speed - if (v_speed == 0.0f && is_clamped && (v_max - v_min < FLT_MAX)) + if (v_speed == 0.0f && is_bounded && (v_max - v_min < FLT_MAX)) v_speed = (float)((v_max - v_min) * g.DragSpeedDefaultRatio); // Inputs accumulates into g.DragCurrentAccum, which is flushed into the current value as soon as it makes a difference with our precision settings float adjust_delta = 0.0f; - if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && g.IO.MouseDragMaxDistanceSqr[0] > 1.0f*1.0f) + if (g.ActiveIdSource == ImGuiInputSource_Mouse && IsMousePosValid() && IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) { adjust_delta = g.IO.MouseDelta[axis]; - if (g.IO.KeyAlt) + if (g.IO.KeyAlt && !(flags & ImGuiSliderFlags_NoSpeedTweaks)) adjust_delta *= 1.0f / 100.0f; - if (g.IO.KeyShift) + if (g.IO.KeyShift && !(flags & ImGuiSliderFlags_NoSpeedTweaks)) adjust_delta *= 10.0f; } - else if (g.ActiveIdSource == ImGuiInputSource_Nav) + else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) { - int decimal_precision = is_decimal ? ImParseFormatPrecision(format, 3) : 0; - adjust_delta = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 1.0f / 10.0f, 10.0f)[axis]; + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; + const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow); + const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast); + const float tweak_factor = (flags & ImGuiSliderFlags_NoSpeedTweaks) ? 1.0f : tweak_slow ? 1.0f / 10.0f : tweak_fast ? 10.0f : 1.0f; + adjust_delta = GetNavTweakPressedAmount(axis) * tweak_factor; v_speed = ImMax(v_speed, GetMinimumStepAtDecimalPrecision(decimal_precision)); } adjust_delta *= v_speed; @@ -1957,12 +2491,15 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const if (axis == ImGuiAxis_Y) adjust_delta = -adjust_delta; + // For logarithmic use our range is effectively 0..1 so scale the delta into that range + if (is_logarithmic && (v_max - v_min < FLT_MAX) && ((v_max - v_min) > 0.000001f)) // Epsilon to avoid /0 + adjust_delta /= (float)(v_max - v_min); + // Clear current value on activation // Avoid altering values and clamping when we are _already_ past the limits and heading in the same direction, so e.g. if range is 0..255, current value is 300 and we are pushing to the right side, keep the 300. - bool is_just_activated = g.ActiveIdIsJustActivated; - bool is_already_past_limits_and_pushing_outward = is_clamped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f)); - bool is_drag_direction_change_with_power = is_power && ((adjust_delta < 0 && g.DragCurrentAccum > 0) || (adjust_delta > 0 && g.DragCurrentAccum < 0)); - if (is_just_activated || is_already_past_limits_and_pushing_outward || is_drag_direction_change_with_power) + const bool is_just_activated = g.ActiveIdIsJustActivated; + const bool is_already_past_limits_and_pushing_outward = is_bounded && !is_wrapped && ((*v >= v_max && adjust_delta > 0.0f) || (*v <= v_min && adjust_delta < 0.0f)); + if (is_just_activated || is_already_past_limits_and_pushing_outward) { g.DragCurrentAccum = 0.0f; g.DragCurrentAccumDirty = false; @@ -1979,13 +2516,19 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const TYPE v_cur = *v; FLOATTYPE v_old_ref_for_accum_remainder = (FLOATTYPE)0.0f; - if (is_power) + float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true + const float zero_deadzone_halfsize = 0.0f; // Drag widgets have no deadzone (as it doesn't make sense) + if (is_logarithmic) { - // Offset + round to user desired precision, with a curve on the v_min..v_max range to get more precision on one side of the range - FLOATTYPE v_old_norm_curved = ImPow((FLOATTYPE)(v_cur - v_min) / (FLOATTYPE)(v_max - v_min), (FLOATTYPE)1.0f / power); - FLOATTYPE v_new_norm_curved = v_old_norm_curved + (g.DragCurrentAccum / (v_max - v_min)); - v_cur = v_min + (SIGNEDTYPE)ImPow(ImSaturate((float)v_new_norm_curved), power) * (v_max - v_min); - v_old_ref_for_accum_remainder = v_old_norm_curved; + // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound. + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; + logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision); + + // Convert to parametric space, apply delta, convert back + float v_old_parametric = ScaleRatioFromValueT(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + float v_new_parametric = v_old_parametric + g.DragCurrentAccum; + v_cur = ScaleValueFromRatioT(data_type, v_new_parametric, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + v_old_ref_for_accum_remainder = v_old_parametric; } else { @@ -1993,14 +2536,16 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const } // Round to user desired precision based on format string - v_cur = RoundScalarWithFormatT(format, data_type, v_cur); + if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_cur = RoundScalarWithFormatT(format, data_type, v_cur); // Preserve remainder after rounding has been applied. This also allow slow tweaking of values. g.DragCurrentAccumDirty = false; - if (is_power) + if (is_logarithmic) { - FLOATTYPE v_cur_norm_curved = ImPow((FLOATTYPE)(v_cur - v_min) / (FLOATTYPE)(v_max - v_min), (FLOATTYPE)1.0f / power); - g.DragCurrentAccum -= (float)(v_cur_norm_curved - v_old_ref_for_accum_remainder); + // Convert to parametric space, apply delta, convert back + float v_new_parametric = ScaleRatioFromValueT(data_type, v_cur, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + g.DragCurrentAccum -= (float)(v_new_parametric - v_old_ref_for_accum_remainder); } else { @@ -2011,13 +2556,24 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const if (v_cur == (TYPE)-0) v_cur = (TYPE)0; - // Clamp values (+ handle overflow/wrap-around for integer types) - if (*v != v_cur && is_clamped) + if (*v != v_cur && is_bounded) { - if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_decimal)) - v_cur = v_min; - if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_decimal)) - v_cur = v_max; + if (is_wrapped) + { + // Wrap values + if (v_cur < v_min) + v_cur += v_max - v_min + (is_floating_point ? 0 : 1); + if (v_cur > v_max) + v_cur -= v_max - v_min + (is_floating_point ? 0 : 1); + } + else + { + // Clamp values + handle overflow/wrap-around for integer types. + if (v_cur < v_min || (v_cur > *v && adjust_delta < 0.0f && !is_floating_point)) + v_cur = v_min; + if (v_cur > v_max || (v_cur < *v && adjust_delta > 0.0f && !is_floating_point)) + v_cur = v_max; + } } // Apply result @@ -2027,112 +2583,145 @@ bool ImGui::DragBehaviorT(ImGuiDataType data_type, TYPE* v, float v_speed, const return true; } -bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* v, float v_speed, const void* v_min, const void* v_max, const char* format, float power, ImGuiDragFlags flags) +bool ImGui::DragBehavior(ImGuiID id, ImGuiDataType data_type, void* p_v, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { + // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. + IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); + ImGuiContext& g = *GImGui; if (g.ActiveId == id) { + // Those are the things we can do easily outside the DragBehaviorT<> template, saves code generation. if (g.ActiveIdSource == ImGuiInputSource_Mouse && !g.IO.MouseDown[0]) ClearActiveID(); - else if (g.ActiveIdSource == ImGuiInputSource_Nav && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) + else if ((g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) && g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) ClearActiveID(); } if (g.ActiveId != id) return false; + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) + return false; switch (data_type) { - case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)v; bool r = DragBehaviorT(ImGuiDataType_S32, &v32, v_speed, v_min ? *(const ImS8*) v_min : IM_S8_MIN, v_max ? *(const ImS8*)v_max : IM_S8_MAX, format, power, flags); if (r) *(ImS8*)v = (ImS8)v32; return r; } - case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)v; bool r = DragBehaviorT(ImGuiDataType_U32, &v32, v_speed, v_min ? *(const ImU8*) v_min : IM_U8_MIN, v_max ? *(const ImU8*)v_max : IM_U8_MAX, format, power, flags); if (r) *(ImU8*)v = (ImU8)v32; return r; } - case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)v; bool r = DragBehaviorT(ImGuiDataType_S32, &v32, v_speed, v_min ? *(const ImS16*)v_min : IM_S16_MIN, v_max ? *(const ImS16*)v_max : IM_S16_MAX, format, power, flags); if (r) *(ImS16*)v = (ImS16)v32; return r; } - case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)v; bool r = DragBehaviorT(ImGuiDataType_U32, &v32, v_speed, v_min ? *(const ImU16*)v_min : IM_U16_MIN, v_max ? *(const ImU16*)v_max : IM_U16_MAX, format, power, flags); if (r) *(ImU16*)v = (ImU16)v32; return r; } - case ImGuiDataType_S32: return DragBehaviorT(data_type, (ImS32*)v, v_speed, v_min ? *(const ImS32* )v_min : IM_S32_MIN, v_max ? *(const ImS32* )v_max : IM_S32_MAX, format, power, flags); - case ImGuiDataType_U32: return DragBehaviorT(data_type, (ImU32*)v, v_speed, v_min ? *(const ImU32* )v_min : IM_U32_MIN, v_max ? *(const ImU32* )v_max : IM_U32_MAX, format, power, flags); - case ImGuiDataType_S64: return DragBehaviorT(data_type, (ImS64*)v, v_speed, v_min ? *(const ImS64* )v_min : IM_S64_MIN, v_max ? *(const ImS64* )v_max : IM_S64_MAX, format, power, flags); - case ImGuiDataType_U64: return DragBehaviorT(data_type, (ImU64*)v, v_speed, v_min ? *(const ImU64* )v_min : IM_U64_MIN, v_max ? *(const ImU64* )v_max : IM_U64_MAX, format, power, flags); - case ImGuiDataType_Float: return DragBehaviorT(data_type, (float*)v, v_speed, v_min ? *(const float* )v_min : -FLT_MAX, v_max ? *(const float* )v_max : FLT_MAX, format, power, flags); - case ImGuiDataType_Double: return DragBehaviorT(data_type, (double*)v, v_speed, v_min ? *(const double*)v_min : -DBL_MAX, v_max ? *(const double*)v_max : DBL_MAX, format, power, flags); + case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = DragBehaviorT(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS8*) p_min : IM_S8_MIN, p_max ? *(const ImS8*)p_max : IM_S8_MAX, format, flags); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } + case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = DragBehaviorT(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU8*) p_min : IM_U8_MIN, p_max ? *(const ImU8*)p_max : IM_U8_MAX, format, flags); if (r) *(ImU8*)p_v = (ImU8)v32; return r; } + case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = DragBehaviorT(ImGuiDataType_S32, &v32, v_speed, p_min ? *(const ImS16*)p_min : IM_S16_MIN, p_max ? *(const ImS16*)p_max : IM_S16_MAX, format, flags); if (r) *(ImS16*)p_v = (ImS16)v32; return r; } + case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = DragBehaviorT(ImGuiDataType_U32, &v32, v_speed, p_min ? *(const ImU16*)p_min : IM_U16_MIN, p_max ? *(const ImU16*)p_max : IM_U16_MAX, format, flags); if (r) *(ImU16*)p_v = (ImU16)v32; return r; } + case ImGuiDataType_S32: return DragBehaviorT(data_type, (ImS32*)p_v, v_speed, p_min ? *(const ImS32* )p_min : IM_S32_MIN, p_max ? *(const ImS32* )p_max : IM_S32_MAX, format, flags); + case ImGuiDataType_U32: return DragBehaviorT(data_type, (ImU32*)p_v, v_speed, p_min ? *(const ImU32* )p_min : IM_U32_MIN, p_max ? *(const ImU32* )p_max : IM_U32_MAX, format, flags); + case ImGuiDataType_S64: return DragBehaviorT(data_type, (ImS64*)p_v, v_speed, p_min ? *(const ImS64* )p_min : IM_S64_MIN, p_max ? *(const ImS64* )p_max : IM_S64_MAX, format, flags); + case ImGuiDataType_U64: return DragBehaviorT(data_type, (ImU64*)p_v, v_speed, p_min ? *(const ImU64* )p_min : IM_U64_MIN, p_max ? *(const ImU64* )p_max : IM_U64_MAX, format, flags); + case ImGuiDataType_Float: return DragBehaviorT(data_type, (float*)p_v, v_speed, p_min ? *(const float* )p_min : -FLT_MAX, p_max ? *(const float* )p_max : FLT_MAX, format, flags); + case ImGuiDataType_Double: return DragBehaviorT(data_type, (double*)p_v, v_speed, p_min ? *(const double*)p_min : -DBL_MAX, p_max ? *(const double*)p_max : DBL_MAX, format, flags); case ImGuiDataType_COUNT: break; } IM_ASSERT(0); return false; } -bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* v, float v_speed, const void* v_min, const void* v_max, const char* format, float power) +// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a Drag widget, p_min and p_max are optional. +// Read code of e.g. DragFloat(), DragInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::DragScalar(const char* label, ImGuiDataType data_type, void* p_data, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; - if (power != 1.0f) - IM_ASSERT(v_min != NULL && v_max != NULL); // When using a power curve the drag needs to have known bounds - ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); const float w = CalcItemWidth(); + const ImVec2 label_size = CalcTextSize(label, NULL, true); - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f)); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, id, &frame_bb)) + if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) return false; // Default format string when passing NULL if (format == NULL) format = DataTypeGetInfo(data_type)->PrintFmt; - else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.) - format = PatchFormatStringFloatToInt(format); - // Tabbing or CTRL-clicking on Drag turns it into an input box - const bool hovered = ItemHoverable(frame_bb, id); - bool temp_input_is_active = TempInputTextIsActive(id); - bool temp_input_start = false; + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags); + bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); if (!temp_input_is_active) { - const bool focus_requested = FocusableItemRegister(window, id); - const bool clicked = (hovered && g.IO.MouseClicked[0]); - const bool double_clicked = (hovered && g.IO.MouseDoubleClicked[0]); - if (focus_requested || clicked || double_clicked || g.NavActivateId == id || g.NavInputId == id) + // Tabbing or CTRL+click on Drag turns it into an InputText + const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); + const bool double_clicked = (hovered && g.IO.MouseClickedCount[0] == 2 && TestKeyOwner(ImGuiKey_MouseLeft, id)); + const bool make_active = (clicked || double_clicked || g.NavActivateId == id); + if (make_active && (clicked || double_clicked)) + SetKeyOwner(ImGuiKey_MouseLeft, id); + if (make_active && temp_input_allowed) + if ((clicked && g.IO.KeyCtrl) || double_clicked || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) + temp_input_is_active = true; + + // (Optional) simple click (without moving) turns Drag into an InputText + if (g.IO.ConfigDragClickToInputText && temp_input_allowed && !temp_input_is_active) + if (g.ActiveId == id && hovered && g.IO.MouseReleased[0] && !IsMouseDragPastThreshold(0, g.IO.MouseDragThreshold * DRAG_MOUSE_THRESHOLD_FACTOR)) + { + g.NavActivateId = id; + g.NavActivateFlags = ImGuiActivateFlags_PreferInput; + temp_input_is_active = true; + } + + // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert) + if (make_active) + memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size); + + if (make_active && !temp_input_is_active) { SetActiveID(id, window); SetFocusID(id, window); FocusWindow(window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); - if (focus_requested || (clicked && g.IO.KeyCtrl) || double_clicked || g.NavInputId == id) - { - temp_input_start = true; - FocusableItemUnregister(window); - } + g.ActiveIdUsingNavDirMask = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); } } - if (temp_input_is_active || temp_input_start) - return TempInputTextScalar(frame_bb, id, label, data_type, v, format); + + if (temp_input_is_active) + { + // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp) + bool clamp_enabled = false; + if ((flags & ImGuiSliderFlags_ClampOnInput) && (p_min != NULL || p_max != NULL)) + { + const int clamp_range_dir = (p_min != NULL && p_max != NULL) ? DataTypeCompare(data_type, p_min, p_max) : 0; // -1 when *p_min < *p_max, == 0 when *p_min == *p_max + if (p_min == NULL || p_max == NULL || clamp_range_dir < 0) + clamp_enabled = true; + else if (clamp_range_dir == 0) + clamp_enabled = DataTypeIsZero(data_type, p_min) ? ((flags & ImGuiSliderFlags_ClampZeroRange) != 0) : true; + } + return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL); + } // Draw frame - const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); - RenderNavHighlight(frame_bb, id); + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavCursor(frame_bb, id); RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, style.FrameRounding); // Drag behavior - const bool value_changed = DragBehavior(id, data_type, v, v_speed, v_min, v_max, format, power, ImGuiDragFlags_None); + const bool value_changed = DragBehavior(id, data_type, p_data, v_speed, p_min, p_max, format, flags); if (value_changed) MarkItemEdited(id); // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. char value_buf[64]; - const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, v, format); + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); if (label_size.x > 0.0f) RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); return value_changed; } -bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* v, int components, float v_speed, const void* v_min, const void* v_max, const char* format, float power) +bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, float v_speed, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -2149,10 +2738,10 @@ bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* v, int PushID(i); if (i > 0) SameLine(0, g.Style.ItemInnerSpacing.x); - value_changed |= DragScalar("", data_type, v, v_speed, v_min, v_max, format, power); + value_changed |= DragScalar("", data_type, p_data, v_speed, p_min, p_max, format, flags); PopID(); PopItemWidth(); - v = (void*)((char*)v + type_size); + p_data = (void*)((char*)p_data + type_size); } PopID(); @@ -2167,27 +2756,28 @@ bool ImGui::DragScalarN(const char* label, ImGuiDataType data_type, void* v, int return value_changed; } -bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, float power) +bool ImGui::DragFloat(const char* label, float* v, float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, power); + return DragScalar(label, ImGuiDataType_Float, v, v_speed, &v_min, &v_max, format, flags); } -bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, float power) +bool ImGui::DragFloat2(const char* label, float v[2], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, power); + return DragScalarN(label, ImGuiDataType_Float, v, 2, v_speed, &v_min, &v_max, format, flags); } -bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, float power) +bool ImGui::DragFloat3(const char* label, float v[3], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, power); + return DragScalarN(label, ImGuiDataType_Float, v, 3, v_speed, &v_min, &v_max, format, flags); } -bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, float power) +bool ImGui::DragFloat4(const char* label, float v[4], float v_speed, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, power); + return DragScalarN(label, ImGuiDataType_Float, v, 4, v_speed, &v_min, &v_max, format, flags); } -bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, float power) +// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this. +bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_current_max, float v_speed, float v_min, float v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -2198,41 +2788,50 @@ bool ImGui::DragFloatRange2(const char* label, float* v_current_min, float* v_cu BeginGroup(); PushMultiItemsWidths(2, CalcItemWidth()); - bool value_changed = DragFloat("##min", v_current_min, v_speed, (v_min >= v_max) ? -FLT_MAX : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), format, power); + float min_min = (v_min >= v_max) ? -FLT_MAX : v_min; + float min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max); + ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0); + bool value_changed = DragScalar("##min", ImGuiDataType_Float, v_current_min, v_speed, &min_min, &min_max, format, min_flags); PopItemWidth(); SameLine(0, g.Style.ItemInnerSpacing.x); - value_changed |= DragFloat("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? FLT_MAX : v_max, format_max ? format_max : format, power); + + float max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min); + float max_max = (v_min >= v_max) ? FLT_MAX : v_max; + ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0); + value_changed |= DragScalar("##max", ImGuiDataType_Float, v_current_max, v_speed, &max_min, &max_max, format_max ? format_max : format, max_flags); PopItemWidth(); SameLine(0, g.Style.ItemInnerSpacing.x); TextEx(label, FindRenderedTextEnd(label)); EndGroup(); PopID(); + return value_changed; } // NB: v_speed is float to allow adjusting the drag speed with more precision -bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format) +bool ImGui::DragInt(const char* label, int* v, float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format); + return DragScalar(label, ImGuiDataType_S32, v, v_speed, &v_min, &v_max, format, flags); } -bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format) +bool ImGui::DragInt2(const char* label, int v[2], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format); + return DragScalarN(label, ImGuiDataType_S32, v, 2, v_speed, &v_min, &v_max, format, flags); } -bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format) +bool ImGui::DragInt3(const char* label, int v[3], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format); + return DragScalarN(label, ImGuiDataType_S32, v, 3, v_speed, &v_min, &v_max, format, flags); } -bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format) +bool ImGui::DragInt4(const char* label, int v[4], float v_speed, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format); + return DragScalarN(label, ImGuiDataType_S32, v, 4, v_speed, &v_min, &v_max, format, flags); } -bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max) +// NB: You likely want to specify the ImGuiSliderFlags_AlwaysClamp when using this. +bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_max, float v_speed, int v_min, int v_max, const char* format, const char* format_max, ImGuiSliderFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -2243,10 +2842,17 @@ bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_ BeginGroup(); PushMultiItemsWidths(2, CalcItemWidth()); - bool value_changed = DragInt("##min", v_current_min, v_speed, (v_min >= v_max) ? INT_MIN : v_min, (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max), format); + int min_min = (v_min >= v_max) ? INT_MIN : v_min; + int min_max = (v_min >= v_max) ? *v_current_max : ImMin(v_max, *v_current_max); + ImGuiSliderFlags min_flags = flags | ((min_min == min_max) ? ImGuiSliderFlags_ReadOnly : 0); + bool value_changed = DragInt("##min", v_current_min, v_speed, min_min, min_max, format, min_flags); PopItemWidth(); SameLine(0, g.Style.ItemInnerSpacing.x); - value_changed |= DragInt("##max", v_current_max, v_speed, (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min), (v_min >= v_max) ? INT_MAX : v_max, format_max ? format_max : format); + + int max_min = (v_min >= v_max) ? *v_current_min : ImMax(v_min, *v_current_min); + int max_max = (v_min >= v_max) ? INT_MAX : v_max; + ImGuiSliderFlags max_flags = flags | ((max_min == max_max) ? ImGuiSliderFlags_ReadOnly : 0); + value_changed |= DragInt("##max", v_current_max, v_speed, max_min, max_max, format_max ? format_max : format, max_flags); PopItemWidth(); SameLine(0, g.Style.ItemInnerSpacing.x); @@ -2260,6 +2866,8 @@ bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_ //------------------------------------------------------------------------- // [SECTION] Widgets: SliderScalar, SliderFloat, SliderInt, etc. //------------------------------------------------------------------------- +// - ScaleRatioFromValueT<> [Internal] +// - ScaleValueFromRatioT<> [Internal] // - SliderBehaviorT<>() [Internal] // - SliderBehavior() [Internal] // - SliderScalar() @@ -2278,67 +2886,161 @@ bool ImGui::DragIntRange2(const char* label, int* v_current_min, int* v_current_ // - VSliderInt() //------------------------------------------------------------------------- -template -float ImGui::SliderCalcRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, float power, float linear_zero_pos) +// Convert a value v in the output space of a slider into a parametric position on the slider itself (the logical opposite of ScaleValueFromRatioT) +template +float ImGui::ScaleRatioFromValueT(ImGuiDataType data_type, TYPE v, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize) { if (v_min == v_max) return 0.0f; + IM_UNUSED(data_type); - const bool is_power = (power != 1.0f) && (data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double); const TYPE v_clamped = (v_min < v_max) ? ImClamp(v, v_min, v_max) : ImClamp(v, v_max, v_min); - if (is_power) + if (is_logarithmic) { - if (v_clamped < 0.0f) + bool flipped = v_max < v_min; + + if (flipped) // Handle the case where the range is backwards + ImSwap(v_min, v_max); + + // Fudge min/max to avoid getting close to log(0) + FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min; + FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max; + + // Awkward special cases - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon) + if ((v_min == 0.0f) && (v_max < 0.0f)) + v_min_fudged = -logarithmic_zero_epsilon; + else if ((v_max == 0.0f) && (v_min < 0.0f)) + v_max_fudged = -logarithmic_zero_epsilon; + + float result; + if (v_clamped <= v_min_fudged) + result = 0.0f; // Workaround for values that are in-range but below our fudge + else if (v_clamped >= v_max_fudged) + result = 1.0f; // Workaround for values that are in-range but above our fudge + else if ((v_min * v_max) < 0.0f) // Range crosses zero, so split into two portions { - const float f = 1.0f - (float)((v_clamped - v_min) / (ImMin((TYPE)0, v_max) - v_min)); - return (1.0f - ImPow(f, 1.0f/power)) * linear_zero_pos; + float zero_point_center = (-(float)v_min) / ((float)v_max - (float)v_min); // The zero point in parametric space. There's an argument we should take the logarithmic nature into account when calculating this, but for now this should do (and the most common case of a symmetrical range works fine) + float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize; + float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize; + if (v == 0.0f) + result = zero_point_center; // Special case for exactly zero + else if (v < 0.0f) + result = (1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(-v_min_fudged / logarithmic_zero_epsilon))) * zero_point_snap_L; + else + result = zero_point_snap_R + ((float)(ImLog((FLOATTYPE)v_clamped / logarithmic_zero_epsilon) / ImLog(v_max_fudged / logarithmic_zero_epsilon)) * (1.0f - zero_point_snap_R)); } + else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider + result = 1.0f - (float)(ImLog(-(FLOATTYPE)v_clamped / -v_max_fudged) / ImLog(-v_min_fudged / -v_max_fudged)); else + result = (float)(ImLog((FLOATTYPE)v_clamped / v_min_fudged) / ImLog(v_max_fudged / v_min_fudged)); + + return flipped ? (1.0f - result) : result; + } + else + { + // Linear slider + return (float)((FLOATTYPE)(SIGNEDTYPE)(v_clamped - v_min) / (FLOATTYPE)(SIGNEDTYPE)(v_max - v_min)); + } +} + +// Convert a parametric position on a slider into a value v in the output space (the logical opposite of ScaleRatioFromValueT) +template +TYPE ImGui::ScaleValueFromRatioT(ImGuiDataType data_type, float t, TYPE v_min, TYPE v_max, bool is_logarithmic, float logarithmic_zero_epsilon, float zero_deadzone_halfsize) +{ + // We special-case the extents because otherwise our logarithmic fudging can lead to "mathematically correct" + // but non-intuitive behaviors like a fully-left slider not actually reaching the minimum value. Also generally simpler. + if (t <= 0.0f || v_min == v_max) + return v_min; + if (t >= 1.0f) + return v_max; + + TYPE result = (TYPE)0; + if (is_logarithmic) + { + // Fudge min/max to avoid getting silly results close to zero + FLOATTYPE v_min_fudged = (ImAbs((FLOATTYPE)v_min) < logarithmic_zero_epsilon) ? ((v_min < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_min; + FLOATTYPE v_max_fudged = (ImAbs((FLOATTYPE)v_max) < logarithmic_zero_epsilon) ? ((v_max < 0.0f) ? -logarithmic_zero_epsilon : logarithmic_zero_epsilon) : (FLOATTYPE)v_max; + + const bool flipped = v_max < v_min; // Check if range is "backwards" + if (flipped) + ImSwap(v_min_fudged, v_max_fudged); + + // Awkward special case - we need ranges of the form (-100 .. 0) to convert to (-100 .. -epsilon), not (-100 .. epsilon) + if ((v_max == 0.0f) && (v_min < 0.0f)) + v_max_fudged = -logarithmic_zero_epsilon; + + float t_with_flip = flipped ? (1.0f - t) : t; // t, but flipped if necessary to account for us flipping the range + + if ((v_min * v_max) < 0.0f) // Range crosses zero, so we have to do this in two parts { - const float f = (float)((v_clamped - ImMax((TYPE)0, v_min)) / (v_max - ImMax((TYPE)0, v_min))); - return linear_zero_pos + ImPow(f, 1.0f/power) * (1.0f - linear_zero_pos); + float zero_point_center = (-(float)ImMin(v_min, v_max)) / ImAbs((float)v_max - (float)v_min); // The zero point in parametric space + float zero_point_snap_L = zero_point_center - zero_deadzone_halfsize; + float zero_point_snap_R = zero_point_center + zero_deadzone_halfsize; + if (t_with_flip >= zero_point_snap_L && t_with_flip <= zero_point_snap_R) + result = (TYPE)0.0f; // Special case to make getting exactly zero possible (the epsilon prevents it otherwise) + else if (t_with_flip < zero_point_center) + result = (TYPE)-(logarithmic_zero_epsilon * ImPow(-v_min_fudged / logarithmic_zero_epsilon, (FLOATTYPE)(1.0f - (t_with_flip / zero_point_snap_L)))); + else + result = (TYPE)(logarithmic_zero_epsilon * ImPow(v_max_fudged / logarithmic_zero_epsilon, (FLOATTYPE)((t_with_flip - zero_point_snap_R) / (1.0f - zero_point_snap_R)))); + } + else if ((v_min < 0.0f) || (v_max < 0.0f)) // Entirely negative slider + result = (TYPE)-(-v_max_fudged * ImPow(-v_min_fudged / -v_max_fudged, (FLOATTYPE)(1.0f - t_with_flip))); + else + result = (TYPE)(v_min_fudged * ImPow(v_max_fudged / v_min_fudged, (FLOATTYPE)t_with_flip)); + } + else + { + // Linear slider + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + if (is_floating_point) + { + result = ImLerp(v_min, v_max, t); + } + else if (t < 1.0) + { + // - For integer values we want the clicking position to match the grab box so we round above + // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property.. + // - Not doing a *1.0 multiply at the end of a range as it tends to be lossy. While absolute aiming at a large s64/u64 + // range is going to be imprecise anyway, with this check we at least make the edge values matches expected limits. + FLOATTYPE v_new_off_f = (SIGNEDTYPE)(v_max - v_min) * t; + result = (TYPE)((SIGNEDTYPE)v_min + (SIGNEDTYPE)(v_new_off_f + (FLOATTYPE)(v_min > v_max ? -0.5 : 0.5))); } } - // Linear slider - return (float)((FLOATTYPE)(v_clamped - v_min) / (FLOATTYPE)(v_max - v_min)); + return result; } -// FIXME: Move some of the code into SliderBehavior(). Current responsability is larger than what the equivalent DragBehaviorT<> does, we also do some rendering, etc. +// FIXME: Try to move more of the code into shared SliderBehavior() template -bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v, const TYPE v_min, const TYPE v_max, const char* format, float power, ImGuiSliderFlags flags, ImRect* out_grab_bb) +bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, TYPE* v, const TYPE v_min, const TYPE v_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb) { ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const ImGuiAxis axis = (flags & ImGuiSliderFlags_Vertical) ? ImGuiAxis_Y : ImGuiAxis_X; - const bool is_decimal = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); - const bool is_power = (power != 1.0f) && is_decimal; + const bool is_logarithmic = (flags & ImGuiSliderFlags_Logarithmic) != 0; + const bool is_floating_point = (data_type == ImGuiDataType_Float) || (data_type == ImGuiDataType_Double); + const float v_range_f = (float)(v_min < v_max ? v_max - v_min : v_min - v_max); // We don't need high precision for what we do with it. - const float grab_padding = 2.0f; + // Calculate bounds + const float grab_padding = 2.0f; // FIXME: Should be part of style. const float slider_sz = (bb.Max[axis] - bb.Min[axis]) - grab_padding * 2.0f; float grab_sz = style.GrabMinSize; - SIGNEDTYPE v_range = (v_min < v_max ? v_max - v_min : v_min - v_max); - if (!is_decimal && v_range >= 0) // v_range < 0 may happen on integer overflows - grab_sz = ImMax((float)(slider_sz / (v_range + 1)), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit + if (!is_floating_point && v_range_f >= 0.0f) // v_range_f < 0 may happen on integer overflows + grab_sz = ImMax(slider_sz / (v_range_f + 1), style.GrabMinSize); // For integer sliders: if possible have the grab size represent 1 unit grab_sz = ImMin(grab_sz, slider_sz); const float slider_usable_sz = slider_sz - grab_sz; const float slider_usable_pos_min = bb.Min[axis] + grab_padding + grab_sz * 0.5f; const float slider_usable_pos_max = bb.Max[axis] - grab_padding - grab_sz * 0.5f; - // For power curve sliders that cross over sign boundary we want the curve to be symmetric around 0.0f - float linear_zero_pos; // 0.0->1.0f - if (is_power && v_min * v_max < 0.0f) + float logarithmic_zero_epsilon = 0.0f; // Only valid when is_logarithmic is true + float zero_deadzone_halfsize = 0.0f; // Only valid when is_logarithmic is true + if (is_logarithmic) { - // Different sign - const FLOATTYPE linear_dist_min_to_0 = ImPow(v_min >= 0 ? (FLOATTYPE)v_min : -(FLOATTYPE)v_min, (FLOATTYPE)1.0f / power); - const FLOATTYPE linear_dist_max_to_0 = ImPow(v_max >= 0 ? (FLOATTYPE)v_max : -(FLOATTYPE)v_max, (FLOATTYPE)1.0f / power); - linear_zero_pos = (float)(linear_dist_min_to_0 / (linear_dist_min_to_0 + linear_dist_max_to_0)); - } - else - { - // Same sign - linear_zero_pos = v_min < 0.0f ? 1.0f : 0.0f; + // When using logarithmic sliders, we need to clamp to avoid hitting zero, but our choice of clamp value greatly affects slider precision. We attempt to use the specified precision to estimate a good lower bound. + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 1; + logarithmic_zero_epsilon = ImPow(0.1f, (float)decimal_precision); + zero_deadzone_halfsize = (style.LogSliderDeadzone * 0.5f) / ImMax(slider_usable_sz, 1.0f); } // Process interacting with the slider @@ -2356,95 +3058,103 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ else { const float mouse_abs_pos = g.IO.MousePos[axis]; - clicked_t = (slider_usable_sz > 0.0f) ? ImClamp((mouse_abs_pos - slider_usable_pos_min) / slider_usable_sz, 0.0f, 1.0f) : 0.0f; + if (g.ActiveIdIsJustActivated) + { + float grab_t = ScaleRatioFromValueT(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + if (axis == ImGuiAxis_Y) + grab_t = 1.0f - grab_t; + const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); + const bool clicked_around_grab = (mouse_abs_pos >= grab_pos - grab_sz * 0.5f - 1.0f) && (mouse_abs_pos <= grab_pos + grab_sz * 0.5f + 1.0f); // No harm being extra generous here. + g.SliderGrabClickOffset = (clicked_around_grab && is_floating_point) ? mouse_abs_pos - grab_pos : 0.0f; + } + if (slider_usable_sz > 0.0f) + clicked_t = ImSaturate((mouse_abs_pos - g.SliderGrabClickOffset - slider_usable_pos_min) / slider_usable_sz); if (axis == ImGuiAxis_Y) clicked_t = 1.0f - clicked_t; set_new_value = true; } } - else if (g.ActiveIdSource == ImGuiInputSource_Nav) + else if (g.ActiveIdSource == ImGuiInputSource_Keyboard || g.ActiveIdSource == ImGuiInputSource_Gamepad) { - const ImVec2 delta2 = GetNavInputAmount2d(ImGuiNavDirSourceFlags_Keyboard | ImGuiNavDirSourceFlags_PadDPad, ImGuiInputReadMode_RepeatFast, 0.0f, 0.0f); - float delta = (axis == ImGuiAxis_X) ? delta2.x : -delta2.y; + if (g.ActiveIdIsJustActivated) + { + g.SliderCurrentAccum = 0.0f; // Reset any stored nav delta upon activation + g.SliderCurrentAccumDirty = false; + } + + float input_delta = (axis == ImGuiAxis_X) ? GetNavTweakPressedAmount(axis) : -GetNavTweakPressedAmount(axis); + if (input_delta != 0.0f) + { + const bool tweak_slow = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakSlow : ImGuiKey_NavKeyboardTweakSlow); + const bool tweak_fast = IsKeyDown((g.NavInputSource == ImGuiInputSource_Gamepad) ? ImGuiKey_NavGamepadTweakFast : ImGuiKey_NavKeyboardTweakFast); + const int decimal_precision = is_floating_point ? ImParseFormatPrecision(format, 3) : 0; + if (decimal_precision > 0) + { + input_delta /= 100.0f; // Keyboard/Gamepad tweak speeds in % of slider bounds + if (tweak_slow) + input_delta /= 10.0f; + } + else + { + if ((v_range_f >= -100.0f && v_range_f <= 100.0f && v_range_f != 0.0f) || tweak_slow) + input_delta = ((input_delta < 0.0f) ? -1.0f : +1.0f) / v_range_f; // Keyboard/Gamepad tweak speeds in integer steps + else + input_delta /= 100.0f; + } + if (tweak_fast) + input_delta *= 10.0f; + + g.SliderCurrentAccum += input_delta; + g.SliderCurrentAccumDirty = true; + } + + float delta = g.SliderCurrentAccum; if (g.NavActivatePressedId == id && !g.ActiveIdIsJustActivated) { ClearActiveID(); } - else if (delta != 0.0f) + else if (g.SliderCurrentAccumDirty) { - clicked_t = SliderCalcRatioFromValueT(data_type, *v, v_min, v_max, power, linear_zero_pos); - const int decimal_precision = is_decimal ? ImParseFormatPrecision(format, 3) : 0; - if ((decimal_precision > 0) || is_power) - { - delta /= 100.0f; // Gamepad/keyboard tweak speeds in % of slider bounds - if (IsNavInputDown(ImGuiNavInput_TweakSlow)) - delta /= 10.0f; - } - else - { - if ((v_range >= -100.0f && v_range <= 100.0f) || IsNavInputDown(ImGuiNavInput_TweakSlow)) - delta = ((delta < 0.0f) ? -1.0f : +1.0f) / (float)v_range; // Gamepad/keyboard tweak speeds in integer steps - else - delta /= 100.0f; - } - if (IsNavInputDown(ImGuiNavInput_TweakFast)) - delta *= 10.0f; - set_new_value = true; + clicked_t = ScaleRatioFromValueT(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + if ((clicked_t >= 1.0f && delta > 0.0f) || (clicked_t <= 0.0f && delta < 0.0f)) // This is to avoid applying the saturation when already past the limits + { set_new_value = false; + g.SliderCurrentAccum = 0.0f; // If pushing up against the limits, don't continue to accumulate + } else + { + set_new_value = true; + float old_clicked_t = clicked_t; clicked_t = ImSaturate(clicked_t + delta); + + // Calculate what our "new" clicked_t will be, and thus how far we actually moved the slider, and subtract this from the accumulator + TYPE v_new = ScaleValueFromRatioT(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_new = RoundScalarWithFormatT(format, data_type, v_new); + float new_clicked_t = ScaleRatioFromValueT(data_type, v_new, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); + + if (delta > 0) + g.SliderCurrentAccum -= ImMin(new_clicked_t - old_clicked_t, delta); + else + g.SliderCurrentAccum -= ImMax(new_clicked_t - old_clicked_t, delta); + } + + g.SliderCurrentAccumDirty = false; } } + if (set_new_value) + if ((g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) || (flags & ImGuiSliderFlags_ReadOnly)) + set_new_value = false; + if (set_new_value) { - TYPE v_new; - if (is_power) - { - // Account for power curve scale on both sides of the zero - if (clicked_t < linear_zero_pos) - { - // Negative: rescale to the negative range before powering - float a = 1.0f - (clicked_t / linear_zero_pos); - a = ImPow(a, power); - v_new = ImLerp(ImMin(v_max, (TYPE)0), v_min, a); - } - else - { - // Positive: rescale to the positive range before powering - float a; - if (ImFabs(linear_zero_pos - 1.0f) > 1.e-6f) - a = (clicked_t - linear_zero_pos) / (1.0f - linear_zero_pos); - else - a = clicked_t; - a = ImPow(a, power); - v_new = ImLerp(ImMax(v_min, (TYPE)0), v_max, a); - } - } - else - { - // Linear slider - if (is_decimal) - { - v_new = ImLerp(v_min, v_max, clicked_t); - } - else - { - // For integer values we want the clicking position to match the grab box so we round above - // This code is carefully tuned to work with large values (e.g. high ranges of U64) while preserving this property.. - FLOATTYPE v_new_off_f = (v_max - v_min) * clicked_t; - TYPE v_new_off_floor = (TYPE)(v_new_off_f); - TYPE v_new_off_round = (TYPE)(v_new_off_f + (FLOATTYPE)0.5); - if (!is_decimal && v_new_off_floor < v_new_off_round) - v_new = v_min + v_new_off_round; - else - v_new = v_min + v_new_off_floor; - } - } + TYPE v_new = ScaleValueFromRatioT(data_type, clicked_t, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); // Round to user desired precision based on format string - v_new = RoundScalarWithFormatT(format, data_type, v_new); + if (is_floating_point && !(flags & ImGuiSliderFlags_NoRoundToFormat)) + v_new = RoundScalarWithFormatT(format, data_type, v_new); // Apply result if (*v != v_new) @@ -2462,7 +3172,7 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ else { // Output grab position so it can be displayed by the caller - float grab_t = SliderCalcRatioFromValueT(data_type, *v, v_min, v_max, power, linear_zero_pos); + float grab_t = ScaleRatioFromValueT(data_type, *v, v_min, v_max, is_logarithmic, logarithmic_zero_epsilon, zero_deadzone_halfsize); if (axis == ImGuiAxis_Y) grab_t = 1.0f - grab_t; const float grab_pos = ImLerp(slider_usable_pos_min, slider_usable_pos_max, grab_t); @@ -2475,42 +3185,48 @@ bool ImGui::SliderBehaviorT(const ImRect& bb, ImGuiID id, ImGuiDataType data_typ return value_changed; } -// For 32-bits and larger types, slider bounds are limited to half the natural type range. +// For 32-bit and larger types, slider bounds are limited to half the natural type range. // So e.g. an integer Slider between INT_MAX-10 and INT_MAX will fail, but an integer Slider between INT_MAX/2-10 and INT_MAX/2 will be ok. // It would be possible to lift that limitation with some work but it doesn't seem to be worth it for sliders. -bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* v, const void* v_min, const void* v_max, const char* format, float power, ImGuiSliderFlags flags, ImRect* out_grab_bb) +bool ImGui::SliderBehavior(const ImRect& bb, ImGuiID id, ImGuiDataType data_type, void* p_v, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags, ImRect* out_grab_bb) { + // Read imgui.cpp "API BREAKING CHANGES" section for 1.78 if you hit this assert. + IM_ASSERT((flags == 1 || (flags & ImGuiSliderFlags_InvalidMask_) == 0) && "Invalid ImGuiSliderFlags flags! Has the legacy 'float power' argument been mistakenly cast to flags? Call function with ImGuiSliderFlags_Logarithmic flags instead."); + IM_ASSERT((flags & ImGuiSliderFlags_WrapAround) == 0); // Not supported by SliderXXX(), only by DragXXX() + switch (data_type) { - case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)v_min, *(const ImS8*)v_max, format, power, flags, out_grab_bb); if (r) *(ImS8*)v = (ImS8)v32; return r; } - case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)v_min, *(const ImU8*)v_max, format, power, flags, out_grab_bb); if (r) *(ImU8*)v = (ImU8)v32; return r; } - case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)v_min, *(const ImS16*)v_max, format, power, flags, out_grab_bb); if (r) *(ImS16*)v = (ImS16)v32; return r; } - case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)v_min, *(const ImU16*)v_max, format, power, flags, out_grab_bb); if (r) *(ImU16*)v = (ImU16)v32; return r; } + case ImGuiDataType_S8: { ImS32 v32 = (ImS32)*(ImS8*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS8*)p_min, *(const ImS8*)p_max, format, flags, out_grab_bb); if (r) *(ImS8*)p_v = (ImS8)v32; return r; } + case ImGuiDataType_U8: { ImU32 v32 = (ImU32)*(ImU8*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_U32, &v32, *(const ImU8*)p_min, *(const ImU8*)p_max, format, flags, out_grab_bb); if (r) *(ImU8*)p_v = (ImU8)v32; return r; } + case ImGuiDataType_S16: { ImS32 v32 = (ImS32)*(ImS16*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_S32, &v32, *(const ImS16*)p_min, *(const ImS16*)p_max, format, flags, out_grab_bb); if (r) *(ImS16*)p_v = (ImS16)v32; return r; } + case ImGuiDataType_U16: { ImU32 v32 = (ImU32)*(ImU16*)p_v; bool r = SliderBehaviorT(bb, id, ImGuiDataType_U32, &v32, *(const ImU16*)p_min, *(const ImU16*)p_max, format, flags, out_grab_bb); if (r) *(ImU16*)p_v = (ImU16)v32; return r; } case ImGuiDataType_S32: - IM_ASSERT(*(const ImS32*)v_min >= IM_S32_MIN/2 && *(const ImS32*)v_max <= IM_S32_MAX/2); - return SliderBehaviorT(bb, id, data_type, (ImS32*)v, *(const ImS32*)v_min, *(const ImS32*)v_max, format, power, flags, out_grab_bb); + IM_ASSERT(*(const ImS32*)p_min >= IM_S32_MIN / 2 && *(const ImS32*)p_max <= IM_S32_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImS32*)p_v, *(const ImS32*)p_min, *(const ImS32*)p_max, format, flags, out_grab_bb); case ImGuiDataType_U32: - IM_ASSERT(*(const ImU32*)v_max <= IM_U32_MAX/2); - return SliderBehaviorT(bb, id, data_type, (ImU32*)v, *(const ImU32*)v_min, *(const ImU32*)v_max, format, power, flags, out_grab_bb); + IM_ASSERT(*(const ImU32*)p_max <= IM_U32_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImU32*)p_v, *(const ImU32*)p_min, *(const ImU32*)p_max, format, flags, out_grab_bb); case ImGuiDataType_S64: - IM_ASSERT(*(const ImS64*)v_min >= IM_S64_MIN/2 && *(const ImS64*)v_max <= IM_S64_MAX/2); - return SliderBehaviorT(bb, id, data_type, (ImS64*)v, *(const ImS64*)v_min, *(const ImS64*)v_max, format, power, flags, out_grab_bb); + IM_ASSERT(*(const ImS64*)p_min >= IM_S64_MIN / 2 && *(const ImS64*)p_max <= IM_S64_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImS64*)p_v, *(const ImS64*)p_min, *(const ImS64*)p_max, format, flags, out_grab_bb); case ImGuiDataType_U64: - IM_ASSERT(*(const ImU64*)v_max <= IM_U64_MAX/2); - return SliderBehaviorT(bb, id, data_type, (ImU64*)v, *(const ImU64*)v_min, *(const ImU64*)v_max, format, power, flags, out_grab_bb); + IM_ASSERT(*(const ImU64*)p_max <= IM_U64_MAX / 2); + return SliderBehaviorT(bb, id, data_type, (ImU64*)p_v, *(const ImU64*)p_min, *(const ImU64*)p_max, format, flags, out_grab_bb); case ImGuiDataType_Float: - IM_ASSERT(*(const float*)v_min >= -FLT_MAX/2.0f && *(const float*)v_max <= FLT_MAX/2.0f); - return SliderBehaviorT(bb, id, data_type, (float*)v, *(const float*)v_min, *(const float*)v_max, format, power, flags, out_grab_bb); + IM_ASSERT(*(const float*)p_min >= -FLT_MAX / 2.0f && *(const float*)p_max <= FLT_MAX / 2.0f); + return SliderBehaviorT(bb, id, data_type, (float*)p_v, *(const float*)p_min, *(const float*)p_max, format, flags, out_grab_bb); case ImGuiDataType_Double: - IM_ASSERT(*(const double*)v_min >= -DBL_MAX/2.0f && *(const double*)v_max <= DBL_MAX/2.0f); - return SliderBehaviorT(bb, id, data_type, (double*)v, *(const double*)v_min, *(const double*)v_max, format, power, flags, out_grab_bb); + IM_ASSERT(*(const double*)p_min >= -DBL_MAX / 2.0f && *(const double*)p_max <= DBL_MAX / 2.0f); + return SliderBehaviorT(bb, id, data_type, (double*)p_v, *(const double*)p_min, *(const double*)p_max, format, flags, out_grab_bb); case ImGuiDataType_COUNT: break; } IM_ASSERT(0); return false; } -bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* v, const void* v_min, const void* v_max, const char* format, float power) +// Note: p_data, p_min and p_max are _pointers_ to a memory address holding the data. For a slider, they are all required. +// Read code of e.g. SliderFloat(), SliderInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -2522,51 +3238,59 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* v, co const float w = CalcItemWidth(); const ImVec2 label_size = CalcTextSize(label, NULL, true); - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y*2.0f)); + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f)); const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); + const bool temp_input_allowed = (flags & ImGuiSliderFlags_NoInput) == 0; ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, id, &frame_bb)) + if (!ItemAdd(total_bb, id, &frame_bb, temp_input_allowed ? ImGuiItemFlags_Inputable : 0)) return false; // Default format string when passing NULL if (format == NULL) format = DataTypeGetInfo(data_type)->PrintFmt; - else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.) - format = PatchFormatStringFloatToInt(format); - // Tabbing or CTRL-clicking on Slider turns it into an input box - const bool hovered = ItemHoverable(frame_bb, id); - bool temp_input_is_active = TempInputTextIsActive(id); - bool temp_input_start = false; + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags); + bool temp_input_is_active = temp_input_allowed && TempInputIsActive(id); if (!temp_input_is_active) { - const bool focus_requested = FocusableItemRegister(window, id); - const bool clicked = (hovered && g.IO.MouseClicked[0]); - if (focus_requested || clicked || g.NavActivateId == id || g.NavInputId == id) + // Tabbing or CTRL+click on Slider turns it into an input box + const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); + const bool make_active = (clicked || g.NavActivateId == id); + if (make_active && clicked) + SetKeyOwner(ImGuiKey_MouseLeft, id); + if (make_active && temp_input_allowed) + if ((clicked && g.IO.KeyCtrl) || (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput))) + temp_input_is_active = true; + + // Store initial value (not used by main lib but available as a convenience but some mods e.g. to revert) + if (make_active) + memcpy(&g.ActiveIdValueOnActivation, p_data, DataTypeGetInfo(data_type)->Size); + + if (make_active && !temp_input_is_active) { SetActiveID(id, window); SetFocusID(id, window); FocusWindow(window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); - if (focus_requested || (clicked && g.IO.KeyCtrl) || g.NavInputId == id) - { - temp_input_start = true; - FocusableItemUnregister(window); - } + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); } } - if (temp_input_is_active || temp_input_start) - return TempInputTextScalar(frame_bb, id, label, data_type, v, format); + + if (temp_input_is_active) + { + // Only clamp CTRL+Click input when ImGuiSliderFlags_ClampOnInput is set (generally via ImGuiSliderFlags_AlwaysClamp) + const bool clamp_enabled = (flags & ImGuiSliderFlags_ClampOnInput) != 0; + return TempInputScalar(frame_bb, id, label, data_type, p_data, format, clamp_enabled ? p_min : NULL, clamp_enabled ? p_max : NULL); + } // Draw frame - const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); - RenderNavHighlight(frame_bb, id); + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavCursor(frame_bb, id); RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding); // Slider behavior ImRect grab_bb; - const bool value_changed = SliderBehavior(frame_bb, id, data_type, v, v_min, v_max, format, power, ImGuiSliderFlags_None, &grab_bb); + const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags, &grab_bb); if (value_changed) MarkItemEdited(id); @@ -2576,18 +3300,20 @@ bool ImGui::SliderScalar(const char* label, ImGuiDataType data_type, void* v, co // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. char value_buf[64]; - const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, v, format); - RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f,0.5f)); + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + if (g.LogEnabled) + LogSetNextTextDecoration("{", "}"); + RenderTextClipped(frame_bb.Min, frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.5f)); if (label_size.x > 0.0f) RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (temp_input_allowed ? ImGuiItemStatusFlags_Inputable : 0)); return value_changed; } // Add multiple sliders on 1 line for compact edition of multiple components -bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, float power) +bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* v_min, const void* v_max, const char* format, ImGuiSliderFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -2604,7 +3330,7 @@ bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, i PushID(i); if (i > 0) SameLine(0, g.Style.ItemInnerSpacing.x); - value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, power); + value_changed |= SliderScalar("", data_type, v, v_min, v_max, format, flags); PopID(); PopItemWidth(); v = (void*)((char*)v + type_size); @@ -2622,57 +3348,58 @@ bool ImGui::SliderScalarN(const char* label, ImGuiDataType data_type, void* v, i return value_changed; } -bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, float power) +bool ImGui::SliderFloat(const char* label, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, power); + return SliderScalar(label, ImGuiDataType_Float, v, &v_min, &v_max, format, flags); } -bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, float power) +bool ImGui::SliderFloat2(const char* label, float v[2], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, power); + return SliderScalarN(label, ImGuiDataType_Float, v, 2, &v_min, &v_max, format, flags); } -bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, float power) +bool ImGui::SliderFloat3(const char* label, float v[3], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, power); + return SliderScalarN(label, ImGuiDataType_Float, v, 3, &v_min, &v_max, format, flags); } -bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, float power) +bool ImGui::SliderFloat4(const char* label, float v[4], float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, power); + return SliderScalarN(label, ImGuiDataType_Float, v, 4, &v_min, &v_max, format, flags); } -bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format) +bool ImGui::SliderAngle(const char* label, float* v_rad, float v_degrees_min, float v_degrees_max, const char* format, ImGuiSliderFlags flags) { if (format == NULL) format = "%.0f deg"; - float v_deg = (*v_rad) * 360.0f / (2*IM_PI); - bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, 1.0f); - *v_rad = v_deg * (2*IM_PI) / 360.0f; + float v_deg = (*v_rad) * 360.0f / (2 * IM_PI); + bool value_changed = SliderFloat(label, &v_deg, v_degrees_min, v_degrees_max, format, flags); + if (value_changed) + *v_rad = v_deg * (2 * IM_PI) / 360.0f; return value_changed; } -bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format) +bool ImGui::SliderInt(const char* label, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format); + return SliderScalar(label, ImGuiDataType_S32, v, &v_min, &v_max, format, flags); } -bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format) +bool ImGui::SliderInt2(const char* label, int v[2], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format); + return SliderScalarN(label, ImGuiDataType_S32, v, 2, &v_min, &v_max, format, flags); } -bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format) +bool ImGui::SliderInt3(const char* label, int v[3], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format); + return SliderScalarN(label, ImGuiDataType_S32, v, 3, &v_min, &v_max, format, flags); } -bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format) +bool ImGui::SliderInt4(const char* label, int v[4], int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format); + return SliderScalarN(label, ImGuiDataType_S32, v, 4, &v_min, &v_max, format, flags); } -bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* v, const void* v_min, const void* v_max, const char* format, float power) +bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType data_type, void* p_data, const void* p_min, const void* p_max, const char* format, ImGuiSliderFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -2693,26 +3420,27 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d // Default format string when passing NULL if (format == NULL) format = DataTypeGetInfo(data_type)->PrintFmt; - else if (data_type == ImGuiDataType_S32 && strcmp(format, "%d") != 0) // (FIXME-LEGACY: Patch old "%.0f" format string to use "%d", read function more details.) - format = PatchFormatStringFloatToInt(format); - const bool hovered = ItemHoverable(frame_bb, id); - if ((hovered && g.IO.MouseClicked[0]) || g.NavActivateId == id || g.NavInputId == id) + const bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags); + const bool clicked = hovered && IsMouseClicked(0, ImGuiInputFlags_None, id); + if (clicked || g.NavActivateId == id) { + if (clicked) + SetKeyOwner(ImGuiKey_MouseLeft, id); SetActiveID(id, window); SetFocusID(id, window); FocusWindow(window); - g.ActiveIdAllowNavDirFlags = (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); } // Draw frame - const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : g.HoveredId == id ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); - RenderNavHighlight(frame_bb, id); + const ImU32 frame_col = GetColorU32(g.ActiveId == id ? ImGuiCol_FrameBgActive : hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg); + RenderNavCursor(frame_bb, id); RenderFrame(frame_bb.Min, frame_bb.Max, frame_col, true, g.Style.FrameRounding); // Slider behavior ImRect grab_bb; - const bool value_changed = SliderBehavior(frame_bb, id, data_type, v, v_min, v_max, format, power, ImGuiSliderFlags_Vertical, &grab_bb); + const bool value_changed = SliderBehavior(frame_bb, id, data_type, p_data, p_min, p_max, format, flags | ImGuiSliderFlags_Vertical, &grab_bb); if (value_changed) MarkItemEdited(id); @@ -2723,22 +3451,22 @@ bool ImGui::VSliderScalar(const char* label, const ImVec2& size, ImGuiDataType d // Display value using user-provided display format so user can add prefix/suffix/decorations to the value. // For the vertical slider we allow centered text to overlap the frame padding char value_buf[64]; - const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, v, format); - RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f,0.0f)); + const char* value_buf_end = value_buf + DataTypeFormatString(value_buf, IM_ARRAYSIZE(value_buf), data_type, p_data, format); + RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, value_buf, value_buf_end, NULL, ImVec2(0.5f, 0.0f)); if (label_size.x > 0.0f) RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); return value_changed; } -bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, float power) +bool ImGui::VSliderFloat(const char* label, const ImVec2& size, float* v, float v_min, float v_max, const char* format, ImGuiSliderFlags flags) { - return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, power); + return VSliderScalar(label, size, ImGuiDataType_Float, v, &v_min, &v_max, format, flags); } -bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format) +bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, int v_max, const char* format, ImGuiSliderFlags flags) { - return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format); + return VSliderScalar(label, size, ImGuiDataType_S32, v, &v_min, &v_max, format, flags); } //------------------------------------------------------------------------- @@ -2747,6 +3475,8 @@ bool ImGui::VSliderInt(const char* label, const ImVec2& size, int* v, int v_min, // - ImParseFormatFindStart() [Internal] // - ImParseFormatFindEnd() [Internal] // - ImParseFormatTrimDecorations() [Internal] +// - ImParseFormatSanitizeForPrinting() [Internal] +// - ImParseFormatSanitizeForScanning() [Internal] // - ImParseFormatPrecision() [Internal] // - TempInputTextScalar() [Internal] // - InputScalar() @@ -2794,7 +3524,7 @@ const char* ImParseFormatFindEnd(const char* fmt) } // Extract the format out of a format string with leading or trailing decorations -// fmt = "blah blah" -> return fmt +// fmt = "blah blah" -> return "" // fmt = "%.3f" -> return fmt // fmt = "hello %.3f" -> return fmt + 6 // fmt = "%.3f hello" -> return buf written with "%.3f" @@ -2802,7 +3532,7 @@ const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_ { const char* fmt_start = ImParseFormatFindStart(fmt); if (fmt_start[0] != '%') - return fmt; + return ""; const char* fmt_end = ImParseFormatFindEnd(fmt_start); if (fmt_end[0] == 0) // If we only have leading decoration, we don't need to copy the data. return fmt_start; @@ -2810,6 +3540,57 @@ const char* ImParseFormatTrimDecorations(const char* fmt, char* buf, size_t buf_ return buf; } +// Sanitize format +// - Zero terminate so extra characters after format (e.g. "%f123") don't confuse atof/atoi +// - stb_sprintf.h supports several new modifiers which format numbers in a way that also makes them incompatible atof/atoi. +void ImParseFormatSanitizeForPrinting(const char* fmt_in, char* fmt_out, size_t fmt_out_size) +{ + const char* fmt_end = ImParseFormatFindEnd(fmt_in); + IM_UNUSED(fmt_out_size); + IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you! + while (fmt_in < fmt_end) + { + char c = *fmt_in++; + if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '. + *(fmt_out++) = c; + } + *fmt_out = 0; // Zero-terminate +} + +// - For scanning we need to remove all width and precision fields and flags "%+3.7f" -> "%f". BUT don't strip types like "%I64d" which includes digits. ! "%07I64d" -> "%I64d" +const char* ImParseFormatSanitizeForScanning(const char* fmt_in, char* fmt_out, size_t fmt_out_size) +{ + const char* fmt_end = ImParseFormatFindEnd(fmt_in); + const char* fmt_out_begin = fmt_out; + IM_UNUSED(fmt_out_size); + IM_ASSERT((size_t)(fmt_end - fmt_in + 1) < fmt_out_size); // Format is too long, let us know if this happens to you! + bool has_type = false; + while (fmt_in < fmt_end) + { + char c = *fmt_in++; + if (!has_type && ((c >= '0' && c <= '9') || c == '.' || c == '+' || c == '#')) + continue; + has_type |= ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); // Stop skipping digits + if (c != '\'' && c != '$' && c != '_') // Custom flags provided by stb_sprintf.h. POSIX 2008 also supports '. + *(fmt_out++) = c; + } + *fmt_out = 0; // Zero-terminate + return fmt_out_begin; +} + +template +static const char* ImAtoi(const char* src, TYPE* output) +{ + int negative = 0; + if (*src == '-') { negative = 1; src++; } + if (*src == '+') { src++; } + TYPE v = 0; + while (*src >= '0' && *src <= '9') + v = (v * 10) + (*src++ - '0'); + *output = negative ? -v : v; + return src; +} + // Parse display precision back from the display format string // FIXME: This is still used by some navigation code path to infer a minimum tweak step, but we should aim to rework widgets so it isn't needed. int ImParseFormatPrecision(const char* fmt, int default_precision) @@ -2836,42 +3617,84 @@ int ImParseFormatPrecision(const char* fmt, int default_precision) // Create text input in place of another active widget (e.g. used when doing a CTRL+Click on drag/slider widgets) // FIXME: Facilitate using this in variety of other situations. -bool ImGui::TempInputTextScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* data_ptr, const char* format) +// FIXME: Among other things, setting ImGuiItemFlags_AllowDuplicateId in LastItemData is currently correct but +// the expected relationship between TempInputXXX functions and LastItemData is a little fishy. +bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, int buf_size, ImGuiInputTextFlags flags) { - ImGuiContext& g = *GImGui; - // On the first frame, g.TempInputTextId == 0, then on subsequent frames it becomes == id. // We clear ActiveID on the first frame to allow the InputText() taking it back. - const bool init = (g.TempInputTextId != id); + ImGuiContext& g = *GImGui; + const bool init = (g.TempInputId != id); if (init) ClearActiveID(); - char fmt_buf[32]; - char data_buf[32]; - format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf)); - DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, data_ptr, format); - ImStrTrimBlanks(data_buf); - g.CurrentWindow->DC.CursorPos = bb.Min; - ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoMarkEdited; - flags |= ((data_type == ImGuiDataType_Float || data_type == ImGuiDataType_Double) ? ImGuiInputTextFlags_CharsScientific : ImGuiInputTextFlags_CharsDecimal); - bool value_changed = InputTextEx(label, NULL, data_buf, IM_ARRAYSIZE(data_buf), bb.GetSize(), flags); + g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId; + bool value_changed = InputTextEx(label, NULL, buf, buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_MergedItem); if (init) { // First frame we started displaying the InputText widget, we expect it to take the active id. IM_ASSERT(g.ActiveId == id); - g.TempInputTextId = g.ActiveId; + g.TempInputId = g.ActiveId; } - if (value_changed) + return value_changed; +} + +// Note that Drag/Slider functions are only forwarding the min/max values clamping values if the ImGuiSliderFlags_AlwaysClamp flag is set! +// This is intended: this way we allow CTRL+Click manual input to set a value out of bounds, for maximum flexibility. +// However this may not be ideal for all uses, as some user code may break on out of bound values. +bool ImGui::TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min, const void* p_clamp_max) +{ + // FIXME: May need to clarify display behavior if format doesn't contain %. + // "%d" -> "%d" / "There are %d items" -> "%d" / "items" -> "%d" (fallback). Also see #6405 + ImGuiContext& g = *GImGui; + const ImGuiDataTypeInfo* type_info = DataTypeGetInfo(data_type); + char fmt_buf[32]; + char data_buf[32]; + format = ImParseFormatTrimDecorations(format, fmt_buf, IM_ARRAYSIZE(fmt_buf)); + if (format[0] == 0) + format = type_info->PrintFmt; + DataTypeFormatString(data_buf, IM_ARRAYSIZE(data_buf), data_type, p_data, format); + ImStrTrimBlanks(data_buf); + + ImGuiInputTextFlags flags = ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint; + g.LastItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; // Because TempInputText() uses ImGuiInputTextFlags_MergedItem it doesn't submit a new item, so we poke LastItemData. + bool value_changed = false; + if (TempInputText(bb, id, label, data_buf, IM_ARRAYSIZE(data_buf), flags)) { - value_changed = DataTypeApplyOpFromText(data_buf, g.InputTextState.InitialTextA.Data, data_type, data_ptr, NULL); + // Backup old value + size_t data_type_size = type_info->Size; + ImGuiDataTypeStorage data_backup; + memcpy(&data_backup, p_data, data_type_size); + + // Apply new value (or operations) then clamp + DataTypeApplyFromText(data_buf, data_type, p_data, format, NULL); + if (p_clamp_min || p_clamp_max) + { + if (p_clamp_min && p_clamp_max && DataTypeCompare(data_type, p_clamp_min, p_clamp_max) > 0) + ImSwap(p_clamp_min, p_clamp_max); + DataTypeClamp(data_type, p_data, p_clamp_min, p_clamp_max); + } + + // Only mark as edited if new value is different + g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited; + value_changed = memcmp(&data_backup, p_data, data_type_size) != 0; if (value_changed) MarkItemEdited(id); } return value_changed; } -bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* data_ptr, const void* step, const void* step_fast, const char* format, ImGuiInputTextFlags flags) +void ImGui::SetNextItemRefVal(ImGuiDataType data_type, void* p_data) +{ + ImGuiContext& g = *GImGui; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasRefVal; + memcpy(&g.NextItemData.RefVal, p_data, DataTypeGetInfo(data_type)->Size); +} + +// Note: p_data, p_step, p_step_fast are _pointers_ to a memory address holding the data. For an Input widget, p_step and p_step_fast are optional. +// Read code of e.g. InputFloat(), InputInt() etc. or examples in 'Demo->Widgets->Data Types' to understand how to use this function directly. +bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -2879,20 +3702,31 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* data_p ImGuiContext& g = *GImGui; ImGuiStyle& style = g.Style; + IM_ASSERT((flags & ImGuiInputTextFlags_EnterReturnsTrue) == 0); // Not supported by InputScalar(). Please open an issue if you this would be useful to you. Otherwise use IsItemDeactivatedAfterEdit()! if (format == NULL) format = DataTypeGetInfo(data_type)->PrintFmt; + void* p_data_default = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasRefVal) ? &g.NextItemData.RefVal : &g.DataTypeZeroValue; + char buf[64]; - DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, data_ptr, format); + if ((flags & ImGuiInputTextFlags_DisplayEmptyRefVal) && DataTypeCompare(data_type, p_data, p_data_default) == 0) + buf[0] = 0; + else + DataTypeFormatString(buf, IM_ARRAYSIZE(buf), data_type, p_data, format); + + // Disable the MarkItemEdited() call in InputText but keep ImGuiItemStatusFlags_Edited. + // We call MarkItemEdited() ourselves by comparing the actual data rather than the string. + g.NextItemData.ItemFlags |= ImGuiItemFlags_NoMarkEdited; + flags |= ImGuiInputTextFlags_AutoSelectAll | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint; bool value_changed = false; - if ((flags & (ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsScientific)) == 0) - flags |= ImGuiInputTextFlags_CharsDecimal; - flags |= ImGuiInputTextFlags_AutoSelectAll; - flags |= ImGuiInputTextFlags_NoMarkEdited; // We call MarkItemEdited() ourselve by comparing the actual data rather than the string. - - if (step != NULL) + if (p_step == NULL) + { + if (InputText(label, buf, IM_ARRAYSIZE(buf), flags)) + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); + } + else { const float button_size = GetFrameHeight(); @@ -2900,26 +3734,30 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* data_p PushID(label); SetNextItemWidth(ImMax(1.0f, CalcItemWidth() - (button_size + style.ItemInnerSpacing.x) * 2)); if (InputText("", buf, IM_ARRAYSIZE(buf), flags)) // PushId(label) + "" gives us the expected ID from outside point of view - value_changed = DataTypeApplyOpFromText(buf, g.InputTextState.InitialTextA.Data, data_type, data_ptr, format); + value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL); + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable); // Step buttons const ImVec2 backup_frame_padding = style.FramePadding; style.FramePadding.x = style.FramePadding.y; - ImGuiButtonFlags button_flags = ImGuiButtonFlags_Repeat | ImGuiButtonFlags_DontClosePopups; if (flags & ImGuiInputTextFlags_ReadOnly) - button_flags |= ImGuiButtonFlags_Disabled; + BeginDisabled(); + PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); SameLine(0, style.ItemInnerSpacing.x); - if (ButtonEx("-", ImVec2(button_size, button_size), button_flags)) + if (ButtonEx("-", ImVec2(button_size, button_size))) { - DataTypeApplyOp(data_type, '-', data_ptr, data_ptr, g.IO.KeyCtrl && step_fast ? step_fast : step); + DataTypeApplyOp(data_type, '-', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); value_changed = true; } SameLine(0, style.ItemInnerSpacing.x); - if (ButtonEx("+", ImVec2(button_size, button_size), button_flags)) + if (ButtonEx("+", ImVec2(button_size, button_size))) { - DataTypeApplyOp(data_type, '+', data_ptr, data_ptr, g.IO.KeyCtrl && step_fast ? step_fast : step); + DataTypeApplyOp(data_type, '+', p_data, p_data, g.IO.KeyCtrl && p_step_fast ? p_step_fast : p_step); value_changed = true; } + PopItemFlag(); + if (flags & ImGuiInputTextFlags_ReadOnly) + EndDisabled(); const char* label_end = FindRenderedTextEnd(label); if (label != label_end) @@ -2932,18 +3770,15 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* data_p PopID(); EndGroup(); } - else - { - if (InputText(label, buf, IM_ARRAYSIZE(buf), flags)) - value_changed = DataTypeApplyOpFromText(buf, g.InputTextState.InitialTextA.Data, data_type, data_ptr, format); - } + + g.LastItemData.ItemFlags &= ~ImGuiItemFlags_NoMarkEdited; if (value_changed) - MarkItemEdited(window->DC.LastItemId); + MarkItemEdited(g.LastItemData.ID); return value_changed; } -bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* v, int components, const void* step, const void* step_fast, const char* format, ImGuiInputTextFlags flags) +bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* p_data, int components, const void* p_step, const void* p_step_fast, const char* format, ImGuiInputTextFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -2960,10 +3795,10 @@ bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* v, in PushID(i); if (i > 0) SameLine(0, g.Style.ItemInnerSpacing.x); - value_changed |= InputScalar("", data_type, v, step, step_fast, format, flags); + value_changed |= InputScalar("", data_type, p_data, p_step, p_step_fast, format, flags); PopID(); PopItemWidth(); - v = (void*)((char*)v + type_size); + p_data = (void*)((char*)p_data + type_size); } PopID(); @@ -2980,8 +3815,7 @@ bool ImGui::InputScalarN(const char* label, ImGuiDataType data_type, void* v, in bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, const char* format, ImGuiInputTextFlags flags) { - flags |= ImGuiInputTextFlags_CharsScientific; - return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step>0.0f ? &step : NULL), (void*)(step_fast>0.0f ? &step_fast : NULL), format, flags); + return InputScalar(label, ImGuiDataType_Float, (void*)v, (void*)(step > 0.0f ? &step : NULL), (void*)(step_fast > 0.0f ? &step_fast : NULL), format, flags); } bool ImGui::InputFloat2(const char* label, float v[2], const char* format, ImGuiInputTextFlags flags) @@ -2999,46 +3833,11 @@ bool ImGui::InputFloat4(const char* label, float v[4], const char* format, ImGui return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags); } -// Prefer using "const char* format" directly, which is more flexible and consistent with other API. -#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS -bool ImGui::InputFloat(const char* label, float* v, float step, float step_fast, int decimal_precision, ImGuiInputTextFlags flags) -{ - char format[16] = "%f"; - if (decimal_precision >= 0) - ImFormatString(format, IM_ARRAYSIZE(format), "%%.%df", decimal_precision); - return InputFloat(label, v, step, step_fast, format, flags); -} - -bool ImGui::InputFloat2(const char* label, float v[2], int decimal_precision, ImGuiInputTextFlags flags) -{ - char format[16] = "%f"; - if (decimal_precision >= 0) - ImFormatString(format, IM_ARRAYSIZE(format), "%%.%df", decimal_precision); - return InputScalarN(label, ImGuiDataType_Float, v, 2, NULL, NULL, format, flags); -} - -bool ImGui::InputFloat3(const char* label, float v[3], int decimal_precision, ImGuiInputTextFlags flags) -{ - char format[16] = "%f"; - if (decimal_precision >= 0) - ImFormatString(format, IM_ARRAYSIZE(format), "%%.%df", decimal_precision); - return InputScalarN(label, ImGuiDataType_Float, v, 3, NULL, NULL, format, flags); -} - -bool ImGui::InputFloat4(const char* label, float v[4], int decimal_precision, ImGuiInputTextFlags flags) -{ - char format[16] = "%f"; - if (decimal_precision >= 0) - ImFormatString(format, IM_ARRAYSIZE(format), "%%.%df", decimal_precision); - return InputScalarN(label, ImGuiDataType_Float, v, 4, NULL, NULL, format, flags); -} -#endif // IMGUI_DISABLE_OBSOLETE_FUNCTIONS - bool ImGui::InputInt(const char* label, int* v, int step, int step_fast, ImGuiInputTextFlags flags) { // Hexadecimal input provided as a convenience but the flag name is awkward. Typically you'd use InputText() to parse your own data, if you want to handle prefixes. const char* format = (flags & ImGuiInputTextFlags_CharsHexadecimal) ? "%08X" : "%d"; - return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step>0 ? &step : NULL), (void*)(step_fast>0 ? &step_fast : NULL), format, flags); + return InputScalar(label, ImGuiDataType_S32, (void*)v, (void*)(step > 0 ? &step : NULL), (void*)(step_fast > 0 ? &step_fast : NULL), format, flags); } bool ImGui::InputInt2(const char* label, int v[2], ImGuiInputTextFlags flags) @@ -3058,23 +3857,32 @@ bool ImGui::InputInt4(const char* label, int v[4], ImGuiInputTextFlags flags) bool ImGui::InputDouble(const char* label, double* v, double step, double step_fast, const char* format, ImGuiInputTextFlags flags) { - flags |= ImGuiInputTextFlags_CharsScientific; - return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step>0.0 ? &step : NULL), (void*)(step_fast>0.0 ? &step_fast : NULL), format, flags); + return InputScalar(label, ImGuiDataType_Double, (void*)v, (void*)(step > 0.0 ? &step : NULL), (void*)(step_fast > 0.0 ? &step_fast : NULL), format, flags); } //------------------------------------------------------------------------- // [SECTION] Widgets: InputText, InputTextMultiline, InputTextWithHint //------------------------------------------------------------------------- +// - imstb_textedit.h include // - InputText() // - InputTextWithHint() // - InputTextMultiline() +// - InputTextGetCharInfo() [Internal] +// - InputTextReindexLines() [Internal] +// - InputTextReindexLinesRange() [Internal] // - InputTextEx() [Internal] +// - DebugNodeInputTextState() [Internal] //------------------------------------------------------------------------- +namespace ImStb +{ +#include "imstb_textedit.h" +} + bool ImGui::InputText(const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) { IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() - return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0,0), flags, callback, user_data); + return InputTextEx(label, NULL, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); } bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, const ImVec2& size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) @@ -3084,38 +3892,50 @@ bool ImGui::InputTextMultiline(const char* label, char* buf, size_t buf_size, co bool ImGui::InputTextWithHint(const char* label, const char* hint, char* buf, size_t buf_size, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) { - IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() + IM_ASSERT(!(flags & ImGuiInputTextFlags_Multiline)); // call InputTextMultiline() or InputTextEx() manually if you need multi-line + hint. return InputTextEx(label, hint, buf, (int)buf_size, ImVec2(0, 0), flags, callback, user_data); } +// This is only used in the path where the multiline widget is inactivate. static int InputTextCalcTextLenAndLineCount(const char* text_begin, const char** out_text_end) { int line_count = 0; const char* s = text_begin; - while (char c = *s++) // We are only matching for \n so we can ignore UTF-8 decoding - if (c == '\n') - line_count++; - s--; - if (s[0] != '\n' && s[0] != '\r') + while (true) + { + const char* s_eol = strchr(s, '\n'); line_count++; + if (s_eol == NULL) + { + s = s + ImStrlen(s); + break; + } + s = s_eol + 1; + } *out_text_end = s; return line_count; } -static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line) +// FIXME: Ideally we'd share code with ImFont::CalcTextSizeA() +static ImVec2 InputTextCalcTextSize(ImGuiContext* ctx, const char* text_begin, const char* text_end, const char** remaining, ImVec2* out_offset, bool stop_on_new_line) { - ImGuiContext& g = *GImGui; + ImGuiContext& g = *ctx; ImFont* font = g.Font; const float line_height = g.FontSize; const float scale = line_height / font->FontSize; - ImVec2 text_size = ImVec2(0,0); + ImVec2 text_size = ImVec2(0, 0); float line_width = 0.0f; - const ImWchar* s = text_begin; + const char* s = text_begin; while (s < text_end) { - unsigned int c = (unsigned int)(*s++); + unsigned int c = (unsigned int)*s; + if (c < 0x80) + s += 1; + else + s += ImTextCharFromUtf8(&c, s, text_end); + if (c == '\n') { text_size.x = ImMax(text_size.x, line_width); @@ -3128,7 +3948,7 @@ static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* t if (c == '\r') continue; - const float char_width = font->GetCharAdvance((ImWchar)c) * scale; + const float char_width = ((int)c < font->IndexAdvanceX.Size ? font->IndexAdvanceX.Data[c] : font->FallbackAdvanceX) * scale; line_width += char_width; } @@ -3148,19 +3968,21 @@ static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* t } // Wrapper for stb_textedit.h to edit text (our wrapper is for: statically sized buffer, single-line, wchar characters. InputText converts between UTF-8 and wchar) +// With our UTF-8 use of stb_textedit: +// - STB_TEXTEDIT_GETCHAR is nothing more than a a "GETBYTE". It's only used to compare to ascii or to copy blocks of text so we are fine. +// - One exception is the STB_TEXTEDIT_IS_SPACE feature which would expect a full char in order to handle full-width space such as 0x3000 (see ImCharIsBlankW). +// - ...but we don't use that feature. namespace ImStb { - -static int STB_TEXTEDIT_STRINGLEN(const STB_TEXTEDIT_STRING* obj) { return obj->CurLenW; } -static ImWchar STB_TEXTEDIT_GETCHAR(const STB_TEXTEDIT_STRING* obj, int idx) { return obj->TextW[idx]; } -static float STB_TEXTEDIT_GETWIDTH(STB_TEXTEDIT_STRING* obj, int line_start_idx, int char_idx) { ImWchar c = obj->TextW[line_start_idx + char_idx]; if (c == '\n') return STB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *GImGui; return g.Font->GetCharAdvance(c) * (g.FontSize / g.Font->FontSize); } -static int STB_TEXTEDIT_KEYTOTEXT(int key) { return key >= 0x10000 ? 0 : key; } -static ImWchar STB_TEXTEDIT_NEWLINE = '\n'; -static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* obj, int line_start_idx) +static int STB_TEXTEDIT_STRINGLEN(const ImGuiInputTextState* obj) { return obj->TextLen; } +static char STB_TEXTEDIT_GETCHAR(const ImGuiInputTextState* obj, int idx) { IM_ASSERT(idx <= obj->TextLen); return obj->TextSrc[idx]; } +static float STB_TEXTEDIT_GETWIDTH(ImGuiInputTextState* obj, int line_start_idx, int char_idx) { unsigned int c; ImTextCharFromUtf8(&c, obj->TextSrc + line_start_idx + char_idx, obj->TextSrc + obj->TextLen); if ((ImWchar)c == '\n') return IMSTB_TEXTEDIT_GETWIDTH_NEWLINE; ImGuiContext& g = *obj->Ctx; return g.Font->GetCharAdvance((ImWchar)c) * g.FontScale; } +static char STB_TEXTEDIT_NEWLINE = '\n'; +static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, ImGuiInputTextState* obj, int line_start_idx) { - const ImWchar* text = obj->TextW.Data; - const ImWchar* text_remaining = NULL; - const ImVec2 size = InputTextCalcTextSizeW(text + line_start_idx, text + obj->CurLenW, &text_remaining, NULL, true); + const char* text = obj->TextSrc; + const char* text_remaining = NULL; + const ImVec2 size = InputTextCalcTextSize(obj->Ctx, text + line_start_idx, text + obj->TextLen, &text_remaining, NULL, true); r->x0 = 0.0f; r->x1 = size.x; r->baseline_y_delta = size.y; @@ -3169,60 +3991,137 @@ static void STB_TEXTEDIT_LAYOUTROW(StbTexteditRow* r, STB_TEXTEDIT_STRING* ob r->num_chars = (int)(text_remaining - (text + line_start_idx)); } -static bool is_separator(unsigned int c) { return ImCharIsBlankW(c) || c==',' || c==';' || c=='(' || c==')' || c=='{' || c=='}' || c=='[' || c==']' || c=='|'; } -static int is_word_boundary_from_right(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (is_separator( obj->TextW[idx-1] ) && !is_separator( obj->TextW[idx] ) ) : 1; } -static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx--; while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) idx--; return idx < 0 ? 0 : idx; } -#ifdef __APPLE__ // FIXME: Move setting to IO structure -static int is_word_boundary_from_left(STB_TEXTEDIT_STRING* obj, int idx) { return idx > 0 ? (!is_separator( obj->TextW[idx-1] ) && is_separator( obj->TextW[idx] ) ) : 1; } -static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_left(obj, idx)) idx++; return idx > len ? len : idx; } -#else -static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(STB_TEXTEDIT_STRING* obj, int idx) { idx++; int len = obj->CurLenW; while (idx < len && !is_word_boundary_from_right(obj, idx)) idx++; return idx > len ? len : idx; } -#endif -#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h -#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL +#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL +#define IMSTB_TEXTEDIT_GETPREVCHARINDEX IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL -static void STB_TEXTEDIT_DELETECHARS(STB_TEXTEDIT_STRING* obj, int pos, int n) +static int IMSTB_TEXTEDIT_GETNEXTCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) { - ImWchar* dst = obj->TextW.Data + pos; - - // We maintain our buffer length in both UTF-8 and wchar formats - obj->CurLenA -= ImTextCountUtf8BytesFromStr(dst, dst + n); - obj->CurLenW -= n; - - // Offset remaining text (FIXME-OPT: Use memmove) - const ImWchar* src = obj->TextW.Data + pos + n; - while (ImWchar c = *src++) - *dst++ = c; - *dst = '\0'; + if (idx >= obj->TextLen) + return obj->TextLen + 1; + unsigned int c; + return idx + ImTextCharFromUtf8(&c, obj->TextSrc + idx, obj->TextSrc + obj->TextLen); } -static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING* obj, int pos, const ImWchar* new_text, int new_text_len) +static int IMSTB_TEXTEDIT_GETPREVCHARINDEX_IMPL(ImGuiInputTextState* obj, int idx) { - const bool is_resizable = (obj->UserFlags & ImGuiInputTextFlags_CallbackResize) != 0; - const int text_len = obj->CurLenW; + if (idx <= 0) + return -1; + const char* p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, obj->TextSrc + idx); + return (int)(p - obj->TextSrc); +} + +static bool ImCharIsSeparatorW(unsigned int c) +{ + static const unsigned int separator_list[] = + { + ',', 0x3001, '.', 0x3002, ';', 0xFF1B, '(', 0xFF08, ')', 0xFF09, '{', 0xFF5B, '}', 0xFF5D, + '[', 0x300C, ']', 0x300D, '|', 0xFF5C, '!', 0xFF01, '\\', 0xFFE5, '/', 0x30FB, 0xFF0F, + '\n', '\r', + }; + for (unsigned int separator : separator_list) + if (c == separator) + return true; + return false; +} + +static int is_word_boundary_from_right(ImGuiInputTextState* obj, int idx) +{ + // When ImGuiInputTextFlags_Password is set, we don't want actions such as CTRL+Arrow to leak the fact that underlying data are blanks or separators. + if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) + return 0; + + const char* curr_p = obj->TextSrc + idx; + const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p); + unsigned int curr_c; ImTextCharFromUtf8(&curr_c, curr_p, obj->TextSrc + obj->TextLen); + unsigned int prev_c; ImTextCharFromUtf8(&prev_c, prev_p, obj->TextSrc + obj->TextLen); + + bool prev_white = ImCharIsBlankW(prev_c); + bool prev_separ = ImCharIsSeparatorW(prev_c); + bool curr_white = ImCharIsBlankW(curr_c); + bool curr_separ = ImCharIsSeparatorW(curr_c); + return ((prev_white || prev_separ) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); +} +static int is_word_boundary_from_left(ImGuiInputTextState* obj, int idx) +{ + if ((obj->Flags & ImGuiInputTextFlags_Password) || idx <= 0) + return 0; + + const char* curr_p = obj->TextSrc + idx; + const char* prev_p = ImTextFindPreviousUtf8Codepoint(obj->TextSrc, curr_p); + unsigned int prev_c; ImTextCharFromUtf8(&prev_c, curr_p, obj->TextSrc + obj->TextLen); + unsigned int curr_c; ImTextCharFromUtf8(&curr_c, prev_p, obj->TextSrc + obj->TextLen); + + bool prev_white = ImCharIsBlankW(prev_c); + bool prev_separ = ImCharIsSeparatorW(prev_c); + bool curr_white = ImCharIsBlankW(curr_c); + bool curr_separ = ImCharIsSeparatorW(curr_c); + return ((prev_white) && !(curr_separ || curr_white)) || (curr_separ && !prev_separ); +} +static int STB_TEXTEDIT_MOVEWORDLEFT_IMPL(ImGuiInputTextState* obj, int idx) +{ + idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx); + while (idx >= 0 && !is_word_boundary_from_right(obj, idx)) + idx = IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx); + return idx < 0 ? 0 : idx; +} +static int STB_TEXTEDIT_MOVEWORDRIGHT_MAC(ImGuiInputTextState* obj, int idx) +{ + int len = obj->TextLen; + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + while (idx < len && !is_word_boundary_from_left(obj, idx)) + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + return idx > len ? len : idx; +} +static int STB_TEXTEDIT_MOVEWORDRIGHT_WIN(ImGuiInputTextState* obj, int idx) +{ + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + int len = obj->TextLen; + while (idx < len && !is_word_boundary_from_right(obj, idx)) + idx = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx); + return idx > len ? len : idx; +} +static int STB_TEXTEDIT_MOVEWORDRIGHT_IMPL(ImGuiInputTextState* obj, int idx) { ImGuiContext& g = *obj->Ctx; if (g.IO.ConfigMacOSXBehaviors) return STB_TEXTEDIT_MOVEWORDRIGHT_MAC(obj, idx); else return STB_TEXTEDIT_MOVEWORDRIGHT_WIN(obj, idx); } +#define STB_TEXTEDIT_MOVEWORDLEFT STB_TEXTEDIT_MOVEWORDLEFT_IMPL // They need to be #define for stb_textedit.h +#define STB_TEXTEDIT_MOVEWORDRIGHT STB_TEXTEDIT_MOVEWORDRIGHT_IMPL + +static void STB_TEXTEDIT_DELETECHARS(ImGuiInputTextState* obj, int pos, int n) +{ + // Offset remaining text (+ copy zero terminator) + IM_ASSERT(obj->TextSrc == obj->TextA.Data); + char* dst = obj->TextA.Data + pos; + char* src = obj->TextA.Data + pos + n; + memmove(dst, src, obj->TextLen - n - pos + 1); + obj->Edited = true; + obj->TextLen -= n; +} + +static bool STB_TEXTEDIT_INSERTCHARS(ImGuiInputTextState* obj, int pos, const char* new_text, int new_text_len) +{ + const bool is_resizable = (obj->Flags & ImGuiInputTextFlags_CallbackResize) != 0; + const int text_len = obj->TextLen; IM_ASSERT(pos <= text_len); - const int new_text_len_utf8 = ImTextCountUtf8BytesFromStr(new_text, new_text + new_text_len); - if (!is_resizable && (new_text_len_utf8 + obj->CurLenA + 1 > obj->BufCapacityA)) + if (!is_resizable && (new_text_len + obj->TextLen + 1 > obj->BufCapacity)) return false; // Grow internal buffer if needed - if (new_text_len + text_len + 1 > obj->TextW.Size) + IM_ASSERT(obj->TextSrc == obj->TextA.Data); + if (new_text_len + text_len + 1 > obj->TextA.Size) { if (!is_resizable) return false; - IM_ASSERT(text_len < obj->TextW.Size); - obj->TextW.resize(text_len + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1); + obj->TextA.resize(text_len + ImClamp(new_text_len, 32, ImMax(256, new_text_len)) + 1); + obj->TextSrc = obj->TextA.Data; } - ImWchar* text = obj->TextW.Data; + char* text = obj->TextA.Data; if (pos != text_len) - memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos) * sizeof(ImWchar)); - memcpy(text + pos, new_text, (size_t)new_text_len * sizeof(ImWchar)); + memmove(text + pos + new_text_len, text + pos, (size_t)(text_len - pos)); + memcpy(text + pos, new_text, (size_t)new_text_len); - obj->CurLenW += new_text_len; - obj->CurLenA += new_text_len_utf8; - obj->TextW[obj->CurLenW] = '\0'; + obj->Edited = true; + obj->TextLen += new_text_len; + obj->TextA[obj->TextLen] = '\0'; return true; } @@ -3242,38 +4141,95 @@ static bool STB_TEXTEDIT_INSERTCHARS(STB_TEXTEDIT_STRING* obj, int pos, const Im #define STB_TEXTEDIT_K_REDO 0x20000B // keyboard input to perform redo #define STB_TEXTEDIT_K_WORDLEFT 0x20000C // keyboard input to move cursor left one word #define STB_TEXTEDIT_K_WORDRIGHT 0x20000D // keyboard input to move cursor right one word +#define STB_TEXTEDIT_K_PGUP 0x20000E // keyboard input to move cursor up a page +#define STB_TEXTEDIT_K_PGDOWN 0x20000F // keyboard input to move cursor down a page #define STB_TEXTEDIT_K_SHIFT 0x400000 -#define STB_TEXTEDIT_IMPLEMENTATION +#define IMSTB_TEXTEDIT_IMPLEMENTATION +#define IMSTB_TEXTEDIT_memmove memmove #include "imstb_textedit.h" +// stb_textedit internally allows for a single undo record to do addition and deletion, but somehow, calling +// the stb_textedit_paste() function creates two separate records, so we perform it manually. (FIXME: Report to nothings/stb?) +static void stb_textedit_replace(ImGuiInputTextState* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len) +{ + stb_text_makeundo_replace(str, state, 0, str->TextLen, text_len); + ImStb::STB_TEXTEDIT_DELETECHARS(str, 0, str->TextLen); + state->cursor = state->select_start = state->select_end = 0; + if (text_len <= 0) + return; + if (ImStb::STB_TEXTEDIT_INSERTCHARS(str, 0, text, text_len)) + { + state->cursor = state->select_start = state->select_end = text_len; + state->has_preferred_x = 0; + return; + } + IM_ASSERT(0); // Failed to insert character, normally shouldn't happen because of how we currently use stb_textedit_replace() +} + +} // namespace ImStb + +// We added an extra indirection where 'Stb' is heap-allocated, in order facilitate the work of bindings generators. +ImGuiInputTextState::ImGuiInputTextState() +{ + memset(this, 0, sizeof(*this)); + Stb = IM_NEW(ImStbTexteditState); + memset(Stb, 0, sizeof(*Stb)); +} + +ImGuiInputTextState::~ImGuiInputTextState() +{ + IM_DELETE(Stb); } void ImGuiInputTextState::OnKeyPressed(int key) { - stb_textedit_key(this, &Stb, key); + stb_textedit_key(this, Stb, key); CursorFollow = true; CursorAnimReset(); } +void ImGuiInputTextState::OnCharPressed(unsigned int c) +{ + // Convert the key to a UTF8 byte sequence. + // The changes we had to make to stb_textedit_key made it very much UTF-8 specific which is not too great. + char utf8[5]; + ImTextCharToUtf8(utf8, c); + stb_textedit_text(this, Stb, utf8, (int)ImStrlen(utf8)); + CursorFollow = true; + CursorAnimReset(); +} + +// Those functions are not inlined in imgui_internal.h, allowing us to hide ImStbTexteditState from that header. +void ImGuiInputTextState::CursorAnimReset() { CursorAnim = -0.30f; } // After a user-input the cursor stays on for a while without blinking +void ImGuiInputTextState::CursorClamp() { Stb->cursor = ImMin(Stb->cursor, TextLen); Stb->select_start = ImMin(Stb->select_start, TextLen); Stb->select_end = ImMin(Stb->select_end, TextLen); } +bool ImGuiInputTextState::HasSelection() const { return Stb->select_start != Stb->select_end; } +void ImGuiInputTextState::ClearSelection() { Stb->select_start = Stb->select_end = Stb->cursor; } +int ImGuiInputTextState::GetCursorPos() const { return Stb->cursor; } +int ImGuiInputTextState::GetSelectionStart() const { return Stb->select_start; } +int ImGuiInputTextState::GetSelectionEnd() const { return Stb->select_end; } +void ImGuiInputTextState::SelectAll() { Stb->select_start = 0; Stb->cursor = Stb->select_end = TextLen; Stb->has_preferred_x = 0; } +void ImGuiInputTextState::ReloadUserBufAndSelectAll() { WantReloadUserBuf = true; ReloadSelectionStart = 0; ReloadSelectionEnd = INT_MAX; } +void ImGuiInputTextState::ReloadUserBufAndKeepSelection() { WantReloadUserBuf = true; ReloadSelectionStart = Stb->select_start; ReloadSelectionEnd = Stb->select_end; } +void ImGuiInputTextState::ReloadUserBufAndMoveToEnd() { WantReloadUserBuf = true; ReloadSelectionStart = ReloadSelectionEnd = INT_MAX; } + ImGuiInputTextCallbackData::ImGuiInputTextCallbackData() { memset(this, 0, sizeof(*this)); } -// Public API to manipulate UTF-8 text -// We expose UTF-8 to the user (unlike the STB_TEXTEDIT_* functions which are manipulating wchar) +// Public API to manipulate UTF-8 text from within a callback. // FIXME: The existence of this rarely exercised code path is a bit of a nuisance. +// Historically they existed because STB_TEXTEDIT_INSERTCHARS() etc. worked on our ImWchar +// buffer, but nowadays they both work on UTF-8 data. Should aim to merge both. void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count) { IM_ASSERT(pos + bytes_count <= BufTextLen); char* dst = Buf + pos; const char* src = Buf + pos + bytes_count; - while (char c = *src++) - *dst++ = c; - *dst = '\0'; + memmove(dst, src, BufTextLen - bytes_count - pos + 1); - if (CursorPos + bytes_count >= pos) + if (CursorPos >= pos + bytes_count) CursorPos -= bytes_count; else if (CursorPos >= pos) CursorPos = pos; @@ -3284,22 +4240,27 @@ void ImGuiInputTextCallbackData::DeleteChars(int pos, int bytes_count) void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, const char* new_text_end) { + // Accept null ranges + if (new_text == new_text_end) + return; + + // Grow internal buffer if needed const bool is_resizable = (Flags & ImGuiInputTextFlags_CallbackResize) != 0; - const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)strlen(new_text); + const int new_text_len = new_text_end ? (int)(new_text_end - new_text) : (int)ImStrlen(new_text); if (new_text_len + BufTextLen >= BufSize) { if (!is_resizable) return; - // Contrary to STB_TEXTEDIT_INSERTCHARS() this is working in the UTF8 buffer, hence the midly similar code (until we remove the U16 buffer alltogether!) - ImGuiContext& g = *GImGui; + ImGuiContext& g = *Ctx; ImGuiInputTextState* edit_state = &g.InputTextState; IM_ASSERT(edit_state->ID != 0 && g.ActiveId == edit_state->ID); IM_ASSERT(Buf == edit_state->TextA.Data); int new_buf_size = BufTextLen + ImClamp(new_text_len * 4, 32, ImMax(256, new_text_len)) + 1; - edit_state->TextA.reserve(new_buf_size + 1); + edit_state->TextA.resize(new_buf_size + 1); + edit_state->TextSrc = edit_state->TextA.Data; Buf = edit_state->TextA.Data; - BufSize = edit_state->BufCapacityA = new_buf_size; + BufSize = edit_state->BufCapacity = new_buf_size; } if (BufTextLen != pos) @@ -3314,54 +4275,116 @@ void ImGuiInputTextCallbackData::InsertChars(int pos, const char* new_text, cons BufTextLen += new_text_len; } +void ImGui::PushPasswordFont() +{ + ImGuiContext& g = *GImGui; + ImFont* in_font = g.Font; + ImFont* out_font = &g.InputTextPasswordFont; + ImFontGlyph* glyph = in_font->FindGlyph('*'); + out_font->FontSize = in_font->FontSize; + out_font->Scale = in_font->Scale; + out_font->Ascent = in_font->Ascent; + out_font->Descent = in_font->Descent; + out_font->ContainerAtlas = in_font->ContainerAtlas; + out_font->FallbackGlyph = glyph; + out_font->FallbackAdvanceX = glyph->AdvanceX; + IM_ASSERT(out_font->Glyphs.Size == 0 && out_font->IndexAdvanceX.Size == 0 && out_font->IndexLookup.Size == 0); + PushFont(out_font); +} + // Return false to discard a character. -static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data) +static bool InputTextFilterCharacter(ImGuiContext* ctx, unsigned int* p_char, ImGuiInputTextFlags flags, ImGuiInputTextCallback callback, void* user_data, bool input_source_is_clipboard) { unsigned int c = *p_char; // Filter non-printable (NB: isprint is unreliable! see #2467) + bool apply_named_filters = true; if (c < 0x20) { bool pass = false; - pass |= (c == '\n' && (flags & ImGuiInputTextFlags_Multiline)); - pass |= (c == '\t' && (flags & ImGuiInputTextFlags_AllowTabInput)); + pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; // Note that an Enter KEY will emit \r and be ignored (we poll for KEY in InputText() code) + if (c == '\n' && input_source_is_clipboard && (flags & ImGuiInputTextFlags_Multiline) == 0) // In single line mode, replace \n with a space + { + c = *p_char = ' '; + pass = true; + } + pass |= (c == '\n') && (flags & ImGuiInputTextFlags_Multiline) != 0; + pass |= (c == '\t') && (flags & ImGuiInputTextFlags_AllowTabInput) != 0; if (!pass) return false; + apply_named_filters = false; // Override named filters below so newline and tabs can still be inserted. + } + + if (input_source_is_clipboard == false) + { + // We ignore Ascii representation of delete (emitted from Backspace on OSX, see #2578, #2817) + if (c == 127) + return false; + + // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME) + if (c >= 0xE000 && c <= 0xF8FF) + return false; } - // Filter private Unicode range. GLFW on OSX seems to send private characters for special keys like arrow keys (FIXME) - if (c >= 0xE000 && c <= 0xF8FF) + // Filter Unicode ranges we are not handling in this build + if (c > IM_UNICODE_CODEPOINT_MAX) return false; // Generic named filters - if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific)) + if (apply_named_filters && (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase | ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint))) { + // The libc allows overriding locale, with e.g. 'setlocale(LC_NUMERIC, "de_DE.UTF-8");' which affect the output/input of printf/scanf to use e.g. ',' instead of '.'. + // The standard mandate that programs starts in the "C" locale where the decimal point is '.'. + // We don't really intend to provide widespread support for it, but out of empathy for people stuck with using odd API, we support the bare minimum aka overriding the decimal point. + // Change the default decimal_point with: + // ImGui::GetPlatformIO()->Platform_LocaleDecimalPoint = *localeconv()->decimal_point; + // Users of non-default decimal point (in particular ',') may be affected by word-selection logic (is_word_boundary_from_right/is_word_boundary_from_left) functions. + ImGuiContext& g = *ctx; + const unsigned c_decimal_point = (unsigned int)g.PlatformIO.Platform_LocaleDecimalPoint; + if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | (ImGuiInputTextFlags)ImGuiInputTextFlags_LocalizeDecimalPoint)) + if (c == '.' || c == ',') + c = c_decimal_point; + + // Full-width -> half-width conversion for numeric fields (https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block) + // While this is mostly convenient, this has the side-effect for uninformed users accidentally inputting full-width characters that they may + // scratch their head as to why it works in numerical fields vs in generic text fields it would require support in the font. + if (flags & (ImGuiInputTextFlags_CharsDecimal | ImGuiInputTextFlags_CharsScientific | ImGuiInputTextFlags_CharsHexadecimal)) + if (c >= 0xFF01 && c <= 0xFF5E) + c = c - 0xFF01 + 0x21; + + // Allow 0-9 . - + * / if (flags & ImGuiInputTextFlags_CharsDecimal) - if (!(c >= '0' && c <= '9') && (c != '.') && (c != '-') && (c != '+') && (c != '*') && (c != '/')) + if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/')) return false; + // Allow 0-9 . - + * / e E if (flags & ImGuiInputTextFlags_CharsScientific) - if (!(c >= '0' && c <= '9') && (c != '.') && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E')) + if (!(c >= '0' && c <= '9') && (c != c_decimal_point) && (c != '-') && (c != '+') && (c != '*') && (c != '/') && (c != 'e') && (c != 'E')) return false; + // Allow 0-9 a-F A-F if (flags & ImGuiInputTextFlags_CharsHexadecimal) if (!(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F')) return false; + // Turn a-z into A-Z if (flags & ImGuiInputTextFlags_CharsUppercase) if (c >= 'a' && c <= 'z') - *p_char = (c += (unsigned int)('A'-'a')); + c += (unsigned int)('A' - 'a'); if (flags & ImGuiInputTextFlags_CharsNoBlank) if (ImCharIsBlankW(c)) return false; + + *p_char = c; } // Custom callback filter if (flags & ImGuiInputTextFlags_CallbackCharFilter) { + ImGuiContext& g = *GImGui; ImGuiInputTextCallbackData callback_data; - memset(&callback_data, 0, sizeof(ImGuiInputTextCallbackData)); + callback_data.Ctx = &g; callback_data.EventFlag = ImGuiInputTextFlags_CallbackCharFilter; callback_data.EventChar = (ImWchar)c; callback_data.Flags = flags; @@ -3376,6 +4399,57 @@ static bool InputTextFilterCharacter(unsigned int* p_char, ImGuiInputTextFlags f return true; } +// Find the shortest single replacement we can make to get from old_buf to new_buf +// Note that this doesn't directly alter state->TextA, state->TextLen. They are expected to be made valid separately. +// FIXME: Ideally we should transition toward (1) making InsertChars()/DeleteChars() update undo-stack (2) discourage (and keep reconcile) or obsolete (and remove reconcile) accessing buffer directly. +static void InputTextReconcileUndoState(ImGuiInputTextState* state, const char* old_buf, int old_length, const char* new_buf, int new_length) +{ + const int shorter_length = ImMin(old_length, new_length); + int first_diff; + for (first_diff = 0; first_diff < shorter_length; first_diff++) + if (old_buf[first_diff] != new_buf[first_diff]) + break; + if (first_diff == old_length && first_diff == new_length) + return; + + int old_last_diff = old_length - 1; + int new_last_diff = new_length - 1; + for (; old_last_diff >= first_diff && new_last_diff >= first_diff; old_last_diff--, new_last_diff--) + if (old_buf[old_last_diff] != new_buf[new_last_diff]) + break; + + const int insert_len = new_last_diff - first_diff + 1; + const int delete_len = old_last_diff - first_diff + 1; + if (insert_len > 0 || delete_len > 0) + if (IMSTB_TEXTEDIT_CHARTYPE* p = stb_text_createundo(&state->Stb->undostate, first_diff, delete_len, insert_len)) + for (int i = 0; i < delete_len; i++) + p[i] = old_buf[first_diff + i]; +} + +// As InputText() retain textual data and we currently provide a path for user to not retain it (via local variables) +// we need some form of hook to reapply data back to user buffer on deactivation frame. (#4714) +// It would be more desirable that we discourage users from taking advantage of the "user not retaining data" trick, +// but that more likely be attractive when we do have _NoLiveEdit flag available. +void ImGui::InputTextDeactivateHook(ImGuiID id) +{ + ImGuiContext& g = *GImGui; + ImGuiInputTextState* state = &g.InputTextState; + if (id == 0 || state->ID != id) + return; + g.InputTextDeactivatedState.ID = state->ID; + if (state->Flags & ImGuiInputTextFlags_ReadOnly) + { + g.InputTextDeactivatedState.TextA.resize(0); // In theory this data won't be used, but clear to be neat. + } + else + { + IM_ASSERT(state->TextA.Data != 0); + IM_ASSERT(state->TextA[state->TextLen] == 0); + g.InputTextDeactivatedState.TextA.resize(state->TextLen + 1); + memcpy(g.InputTextDeactivatedState.TextA.Data, state->TextA.Data, state->TextLen + 1); + } +} + // Edit a string of text // - buf_size account for the zero-terminator, so a buf_size of 6 can hold "Hello" but not "Hello!". // This is so we can easily call InputText() on static arrays using ARRAYSIZE() and to match @@ -3390,8 +4464,10 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (window->SkipItems) return false; + IM_ASSERT(buf != NULL && buf_size >= 0); IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackHistory) && (flags & ImGuiInputTextFlags_Multiline))); // Can't use both together (they both use up/down keys) IM_ASSERT(!((flags & ImGuiInputTextFlags_CallbackCompletion) && (flags & ImGuiInputTextFlags_AllowTabInput))); // Can't use both together (they both use tab key) + IM_ASSERT(!((flags & ImGuiInputTextFlags_ElideLeft) && (flags & ImGuiInputTextFlags_Multiline))); // Multiline will not work with left-trimming ImGuiContext& g = *GImGui; ImGuiIO& io = g.IO; @@ -3399,6 +4475,82 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const bool RENDER_SELECTION_WHEN_INACTIVE = false; const bool is_multiline = (flags & ImGuiInputTextFlags_Multiline) != 0; + + if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope (including the scrollbar) + BeginGroup(); + const ImGuiID id = window->GetID(label); + const ImVec2 label_size = CalcTextSize(label, NULL, true); + const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y * 2.0f); // Arbitrary default of 8 lines high for multi-line + const ImVec2 total_size = ImVec2(frame_size.x + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), frame_size.y); + + const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); + const ImRect total_bb(frame_bb.Min, frame_bb.Min + total_size); + + ImGuiWindow* draw_window = window; + ImVec2 inner_size = frame_size; + ImGuiLastItemData item_data_backup; + if (is_multiline) + { + ImVec2 backup_pos = window->DC.CursorPos; + ItemSize(total_bb, style.FramePadding.y); + if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) + { + EndGroup(); + return false; + } + item_data_backup = g.LastItemData; + window->DC.CursorPos = backup_pos; + + // Prevent NavActivation from Tabbing when our widget accepts Tab inputs: this allows cycling through widgets without stopping. + if (g.NavActivateId == id && (g.NavActivateFlags & ImGuiActivateFlags_FromTabbing) && (flags & ImGuiInputTextFlags_AllowTabInput)) + g.NavActivateId = 0; + + // Prevent NavActivate reactivating in BeginChild() when we are already active. + const ImGuiID backup_activate_id = g.NavActivateId; + if (g.ActiveId == id) // Prevent reactivation + g.NavActivateId = 0; + + // We reproduce the contents of BeginChildFrame() in order to provide 'label' so our window internal data are easier to read/debug. + PushStyleColor(ImGuiCol_ChildBg, style.Colors[ImGuiCol_FrameBg]); + PushStyleVar(ImGuiStyleVar_ChildRounding, style.FrameRounding); + PushStyleVar(ImGuiStyleVar_ChildBorderSize, style.FrameBorderSize); + PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); // Ensure no clip rect so mouse hover can reach FramePadding edges + bool child_visible = BeginChildEx(label, id, frame_bb.GetSize(), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoMove); + g.NavActivateId = backup_activate_id; + PopStyleVar(3); + PopStyleColor(); + if (!child_visible) + { + EndChild(); + EndGroup(); + return false; + } + draw_window = g.CurrentWindow; // Child window + draw_window->DC.NavLayersActiveMaskNext |= (1 << draw_window->DC.NavLayerCurrent); // This is to ensure that EndChild() will display a navigation highlight so we can "enter" into it. + draw_window->DC.CursorPos += style.FramePadding; + inner_size.x -= draw_window->ScrollbarSizes.x; + } + else + { + // Support for internal ImGuiInputTextFlags_MergedItem flag, which could be redesigned as an ItemFlags if needed (with test performed in ItemAdd) + ItemSize(total_bb, style.FramePadding.y); + if (!(flags & ImGuiInputTextFlags_MergedItem)) + if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_Inputable)) + return false; + } + + // Ensure mouse cursor is set even after switching to keyboard/gamepad mode. May generalize further? (#6417) + bool hovered = ItemHoverable(frame_bb, id, g.LastItemData.ItemFlags | ImGuiItemFlags_NoNavDisableMouseHover); + if (hovered) + SetMouseCursor(ImGuiMouseCursor_TextInput); + if (hovered && g.NavHighlightItemUnderNav) + hovered = false; + + // We are only allowed to access the state if we are already the active widget. + ImGuiInputTextState* state = GetInputTextState(id); + + if (g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) + flags |= ImGuiInputTextFlags_ReadOnly; const bool is_readonly = (flags & ImGuiInputTextFlags_ReadOnly) != 0; const bool is_password = (flags & ImGuiInputTextFlags_Password) != 0; const bool is_undoable = (flags & ImGuiInputTextFlags_NoUndoRedo) == 0; @@ -3406,118 +4558,135 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (is_resizable) IM_ASSERT(callback != NULL); // Must provide a callback if you set the ImGuiInputTextFlags_CallbackResize flag! - if (is_multiline) // Open group before calling GetID() because groups tracks id created within their scope, - BeginGroup(); - const ImGuiID id = window->GetID(label); - const ImVec2 label_size = CalcTextSize(label, NULL, true); - ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), (is_multiline ? g.FontSize * 8.0f : label_size.y) + style.FramePadding.y*2.0f); // Arbitrary default of 8 lines high for multi-line - const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + size); - const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? (style.ItemInnerSpacing.x + label_size.x) : 0.0f, 0.0f)); - - ImGuiWindow* draw_window = window; - if (is_multiline) - { - if (!ItemAdd(total_bb, id, &frame_bb)) - { - ItemSize(total_bb, style.FramePadding.y); - EndGroup(); - return false; - } - if (!BeginChildFrame(id, frame_bb.GetSize())) - { - EndChildFrame(); - EndGroup(); - return false; - } - draw_window = GetCurrentWindow(); - draw_window->DC.NavLayerActiveMaskNext |= draw_window->DC.NavLayerCurrentMask; // This is to ensure that EndChild() will display a navigation highlight - size.x -= draw_window->ScrollbarSizes.x; - } - else - { - ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, id, &frame_bb)) - return false; - } - const bool hovered = ItemHoverable(frame_bb, id); - if (hovered) - g.MouseCursor = ImGuiMouseCursor_TextInput; - - // NB: we are only allowed to access 'edit_state' if we are the active widget. - ImGuiInputTextState* state = NULL; - if (g.InputTextState.ID == id) - state = &g.InputTextState; - - const bool focus_requested = FocusableItemRegister(window, id); - const bool focus_requested_by_code = focus_requested && (g.FocusRequestCurrWindow == window && g.FocusRequestCurrCounterAll == window->DC.FocusCounterAll); - const bool focus_requested_by_tab = focus_requested && !focus_requested_by_code; + const bool input_requested_by_nav = (g.ActiveId != id) && ((g.NavActivateId == id) && ((g.NavActivateFlags & ImGuiActivateFlags_PreferInput) || (g.NavInputSource == ImGuiInputSource_Keyboard))); const bool user_clicked = hovered && io.MouseClicked[0]; - const bool user_nav_input_start = (g.ActiveId != id) && ((g.NavInputId == id) || (g.NavActivateId == id && g.NavInputSource == ImGuiInputSource_NavKeyboard)); - const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetScrollbarID(draw_window, ImGuiAxis_Y); - const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetScrollbarID(draw_window, ImGuiAxis_Y); - + const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == GetWindowScrollbarID(draw_window, ImGuiAxis_Y); + const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == GetWindowScrollbarID(draw_window, ImGuiAxis_Y); bool clear_active_id = false; - bool select_all = (g.ActiveId != id) && ((flags & ImGuiInputTextFlags_AutoSelectAll) != 0 || user_nav_input_start) && (!is_multiline); + bool select_all = false; - const bool init_make_active = (focus_requested || user_clicked || user_scroll_finish || user_nav_input_start); + float scroll_y = is_multiline ? draw_window->Scroll.y : FLT_MAX; + + const bool init_reload_from_user_buf = (state != NULL && state->WantReloadUserBuf); + const bool init_changed_specs = (state != NULL && state->Stb->single_line != !is_multiline); // state != NULL means its our state. + const bool init_make_active = (user_clicked || user_scroll_finish || input_requested_by_nav); const bool init_state = (init_make_active || user_scroll_active); - if (init_state && g.ActiveId != id) + if (init_reload_from_user_buf) + { + int new_len = (int)ImStrlen(buf); + IM_ASSERT(new_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?"); + state->WantReloadUserBuf = false; + InputTextReconcileUndoState(state, state->TextA.Data, state->TextLen, buf, new_len); + state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string. + state->TextLen = new_len; + memcpy(state->TextA.Data, buf, state->TextLen + 1); + state->Stb->select_start = state->ReloadSelectionStart; + state->Stb->cursor = state->Stb->select_end = state->ReloadSelectionEnd; + state->CursorClamp(); + } + else if ((init_state && g.ActiveId != id) || init_changed_specs) { // Access state even if we don't own it yet. state = &g.InputTextState; state->CursorAnimReset(); - // Take a copy of the initial buffer value (both in original UTF-8 format and converted to wchar) - // From the moment we focused we are ignoring the content of 'buf' (unless we are in read-only mode) - const int buf_len = (int)strlen(buf); - state->InitialTextA.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string. - memcpy(state->InitialTextA.Data, buf, buf_len + 1); + // Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714) + InputTextDeactivateHook(state->ID); - // Start edition - const char* buf_end = NULL; - state->TextW.resize(buf_size + 1); // wchar count <= UTF-8 count. we use +1 to make sure that .Data is always pointing to at least an empty string. - state->TextA.resize(0); - state->TextAIsValid = false; // TextA is not valid yet (we will display buf until then) - state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, buf_size, buf, NULL, &buf_end); - state->CurLenA = (int)(buf_end - buf); // We can't get the result from ImStrncpy() above because it is not UTF-8 aware. Here we'll cut off malformed UTF-8. + // Take a copy of the initial buffer value. + // From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode) + const int buf_len = (int)ImStrlen(buf); + IM_ASSERT(buf_len + 1 <= buf_size && "Is your input buffer properly zero-terminated?"); + state->TextToRevertTo.resize(buf_len + 1); // UTF-8. we use +1 to make sure that .Data is always pointing to at least an empty string. + memcpy(state->TextToRevertTo.Data, buf, buf_len + 1); // Preserve cursor position and undo/redo stack if we come back to same widget - // FIXME: For non-readonly widgets we might be able to require that TextAIsValid && TextA == buf ? (untested) and discard undo stack if user buffer has changed. - const bool recycle_state = (state->ID == id); - if (recycle_state) + // FIXME: Since we reworked this on 2022/06, may want to differentiate recycle_cursor vs recycle_undostate? + bool recycle_state = (state->ID == id && !init_changed_specs); + if (recycle_state && (state->TextLen != buf_len || (state->TextA.Data == NULL || strncmp(state->TextA.Data, buf, buf_len) != 0))) + recycle_state = false; + + // Start edition + state->ID = id; + state->TextLen = buf_len; + if (!is_readonly) { - // Recycle existing cursor/selection/undo stack but clamp position - // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler. - state->CursorClamp(); + state->TextA.resize(buf_size + 1); // we use +1 to make sure that .Data is always pointing to at least an empty string. + memcpy(state->TextA.Data, buf, state->TextLen + 1); } + + // Find initial scroll position for right alignment + state->Scroll = ImVec2(0.0f, 0.0f); + if (flags & ImGuiInputTextFlags_ElideLeft) + state->Scroll.x += ImMax(0.0f, CalcTextSize(buf).x - frame_size.x + style.FramePadding.x * 2.0f); + + // Recycle existing cursor/selection/undo stack but clamp position + // Note a single mouse click will override the cursor/position immediately by calling stb_textedit_click handler. + if (recycle_state) + state->CursorClamp(); else + stb_textedit_initialize_state(state->Stb, !is_multiline); + + if (!is_multiline) { - state->ID = id; - state->ScrollX = 0.0f; - stb_textedit_initialize_state(&state->Stb, !is_multiline); - if (!is_multiline && focus_requested_by_code) + if (flags & ImGuiInputTextFlags_AutoSelectAll) + select_all = true; + if (input_requested_by_nav && (!recycle_state || !(g.NavActivateFlags & ImGuiActivateFlags_TryToPreserveState))) + select_all = true; + if (user_clicked && io.KeyCtrl) select_all = true; } - if (flags & ImGuiInputTextFlags_AlwaysInsertMode) - state->Stb.insert_mode = 1; - if (!is_multiline && (focus_requested_by_tab || (user_clicked && io.KeyCtrl))) - select_all = true; + + if (flags & ImGuiInputTextFlags_AlwaysOverwrite) + state->Stb->insert_mode = 1; // stb field name is indeed incorrect (see #2863) } + const bool is_osx = io.ConfigMacOSXBehaviors; if (g.ActiveId != id && init_make_active) { IM_ASSERT(state && state->ID == id); SetActiveID(id, window); SetFocusID(id, window); FocusWindow(window); - IM_ASSERT(ImGuiNavInput_COUNT < 32); - g.ActiveIdBlockNavInputFlags = (1 << ImGuiNavInput_Cancel); - if (flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_AllowTabInput)) // Disable keyboard tabbing out as we will use the \t character. - g.ActiveIdBlockNavInputFlags |= (1 << ImGuiNavInput_KeyTab_); - if (!is_multiline && !(flags & ImGuiInputTextFlags_CallbackHistory)) - g.ActiveIdAllowNavDirFlags = ((1 << ImGuiDir_Up) | (1 << ImGuiDir_Down)); } + if (g.ActiveId == id) + { + // Declare some inputs, the other are registered and polled via Shortcut() routing system. + // FIXME: The reason we don't use Shortcut() is we would need a routing flag to specify multiple mods, or to all mods combinaison into individual shortcuts. + const ImGuiKey always_owned_keys[] = { ImGuiKey_LeftArrow, ImGuiKey_RightArrow, ImGuiKey_Enter, ImGuiKey_KeypadEnter, ImGuiKey_Delete, ImGuiKey_Backspace, ImGuiKey_Home, ImGuiKey_End }; + for (ImGuiKey key : always_owned_keys) + SetKeyOwner(key, id); + if (user_clicked) + SetKeyOwner(ImGuiKey_MouseLeft, id); + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Left) | (1 << ImGuiDir_Right); + if (is_multiline || (flags & ImGuiInputTextFlags_CallbackHistory)) + { + g.ActiveIdUsingNavDirMask |= (1 << ImGuiDir_Up) | (1 << ImGuiDir_Down); + SetKeyOwner(ImGuiKey_UpArrow, id); + SetKeyOwner(ImGuiKey_DownArrow, id); + } + if (is_multiline) + { + SetKeyOwner(ImGuiKey_PageUp, id); + SetKeyOwner(ImGuiKey_PageDown, id); + } + // FIXME: May be a problem to always steal Alt on OSX, would ideally still allow an uninterrupted Alt down-up to toggle menu + if (is_osx) + SetKeyOwner(ImGuiMod_Alt, id); + + // Expose scroll in a manner that is agnostic to us using a child window + if (is_multiline && state != NULL) + state->Scroll.y = draw_window->Scroll.y; + + // Read-only mode always ever read from source buffer. Refresh TextLen when active. + if (is_readonly && state != NULL) + state->TextLen = (int)ImStrlen(buf); + //if (is_readonly && state != NULL) + // state->TextA.clear(); // Uncomment to facilitate debugging, but we otherwise prefer to keep/amortize th allocation. + } + if (state != NULL) + state->TextSrc = is_readonly ? buf : state->TextA.Data; // We have an edge case if ActiveId was set through another widget (e.g. widget being swapped), clear id immediately (don't wait until the end of the function) if (g.ActiveId == id && state == NULL) @@ -3528,117 +4697,127 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ clear_active_id = true; // Lock the decision of whether we are going to take the path displaying the cursor or selection - const bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active); - bool render_selection = state && state->HasSelection() && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor); + bool render_cursor = (g.ActiveId == id) || (state && user_scroll_active); + bool render_selection = state && (state->HasSelection() || select_all) && (RENDER_SELECTION_WHEN_INACTIVE || render_cursor); bool value_changed = false; - bool enter_pressed = false; - - // When read-only we always use the live data passed to the function - // FIXME-OPT: Because our selection/cursor code currently needs the wide text we need to convert it when active, which is not ideal :( - if (is_readonly && state != NULL && (render_cursor || render_selection)) - { - const char* buf_end = NULL; - state->TextW.resize(buf_size + 1); - state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, buf, NULL, &buf_end); - state->CurLenA = (int)(buf_end - buf); - state->CursorClamp(); - render_selection &= state->HasSelection(); - } + bool validated = false; // Select the buffer to render. - const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state && state->TextAIsValid; - const bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); + const bool buf_display_from_state = (render_cursor || render_selection || g.ActiveId == id) && !is_readonly && state; + bool is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); // Password pushes a temporary font with only a fallback glyph if (is_password && !is_displaying_hint) - { - const ImFontGlyph* glyph = g.Font->FindGlyph('*'); - ImFont* password_font = &g.InputTextPasswordFont; - password_font->FontSize = g.Font->FontSize; - password_font->Scale = g.Font->Scale; - password_font->DisplayOffset = g.Font->DisplayOffset; - password_font->Ascent = g.Font->Ascent; - password_font->Descent = g.Font->Descent; - password_font->ContainerAtlas = g.Font->ContainerAtlas; - password_font->FallbackGlyph = glyph; - password_font->FallbackAdvanceX = glyph->AdvanceX; - IM_ASSERT(password_font->Glyphs.empty() && password_font->IndexAdvanceX.empty() && password_font->IndexLookup.empty()); - PushFont(password_font); - } + PushPasswordFont(); // Process mouse inputs and character inputs - int backup_current_text_length = 0; if (g.ActiveId == id) { IM_ASSERT(state != NULL); - backup_current_text_length = state->CurLenA; - state->BufCapacityA = buf_size; - state->UserFlags = flags; - state->UserCallback = callback; - state->UserCallbackData = callback_user_data; + state->Edited = false; + state->BufCapacity = buf_size; + state->Flags = flags; // Although we are active we don't prevent mouse from hovering other elements unless we are interacting right now with the widget. // Down the line we should have a cleaner library-wide concept of Selected vs Active. g.ActiveIdAllowOverlap = !io.MouseDown[0]; - g.WantTextInputNextFrame = 1; // Edit in progress - const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->ScrollX; - const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y - style.FramePadding.y) : (g.FontSize*0.5f)); + const float mouse_x = (io.MousePos.x - frame_bb.Min.x - style.FramePadding.x) + state->Scroll.x; + const float mouse_y = (is_multiline ? (io.MousePos.y - draw_window->DC.CursorPos.y) : (g.FontSize * 0.5f)); - const bool is_osx = io.ConfigMacOSXBehaviors; - if (select_all || (hovered && !is_osx && io.MouseDoubleClicked[0])) + if (select_all) { state->SelectAll(); state->SelectedAllMouseLock = true; } - else if (hovered && is_osx && io.MouseDoubleClicked[0]) + else if (hovered && io.MouseClickedCount[0] >= 2 && !io.KeyShift) { - // Double-click select a word only, OS X style (by simulating keystrokes) - state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT); - state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); + stb_textedit_click(state, state->Stb, mouse_x, mouse_y); + const int multiclick_count = (io.MouseClickedCount[0] - 2); + if ((multiclick_count % 2) == 0) + { + // Double-click: Select word + // We always use the "Mac" word advance for double-click select vs CTRL+Right which use the platform dependent variant: + // FIXME: There are likely many ways to improve this behavior, but there's no "right" behavior (depends on use-case, software, OS) + const bool is_bol = (state->Stb->cursor == 0) || ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor - 1) == '\n'; + if (STB_TEXT_HAS_SELECTION(state->Stb) || !is_bol) + state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT); + //state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); + if (!STB_TEXT_HAS_SELECTION(state->Stb)) + ImStb::stb_textedit_prep_selection_at_cursor(state->Stb); + state->Stb->cursor = ImStb::STB_TEXTEDIT_MOVEWORDRIGHT_MAC(state, state->Stb->cursor); + state->Stb->select_end = state->Stb->cursor; + ImStb::stb_textedit_clamp(state, state->Stb); + } + else + { + // Triple-click: Select line + const bool is_eol = ImStb::STB_TEXTEDIT_GETCHAR(state, state->Stb->cursor) == '\n'; + state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART); + state->OnKeyPressed(STB_TEXTEDIT_K_LINEEND | STB_TEXTEDIT_K_SHIFT); + state->OnKeyPressed(STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT); + if (!is_eol && is_multiline) + { + ImSwap(state->Stb->select_start, state->Stb->select_end); + state->Stb->cursor = state->Stb->select_end; + } + state->CursorFollow = false; + } + state->CursorAnimReset(); } else if (io.MouseClicked[0] && !state->SelectedAllMouseLock) { if (hovered) { - stb_textedit_click(state, &state->Stb, mouse_x, mouse_y); + if (io.KeyShift) + stb_textedit_drag(state, state->Stb, mouse_x, mouse_y); + else + stb_textedit_click(state, state->Stb, mouse_x, mouse_y); state->CursorAnimReset(); } } else if (io.MouseDown[0] && !state->SelectedAllMouseLock && (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f)) { - stb_textedit_drag(state, &state->Stb, mouse_x, mouse_y); + stb_textedit_drag(state, state->Stb, mouse_x, mouse_y); state->CursorAnimReset(); state->CursorFollow = true; } if (state->SelectedAllMouseLock && !io.MouseDown[0]) state->SelectedAllMouseLock = false; - // It is ill-defined whether the back-end needs to send a \t character when pressing the TAB keys. - // Win32 and GLFW naturally do it but not SDL. - const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeySuper); - if ((flags & ImGuiInputTextFlags_AllowTabInput) && IsKeyPressedMap(ImGuiKey_Tab) && !ignore_char_inputs && !io.KeyShift && !is_readonly) - if (!io.InputQueueCharacters.contains('\t')) + // We expect backends to emit a Tab key but some also emit a Tab character which we ignore (#2467, #1336) + // (For Tab and Enter: Win32/SFML/Allegro are sending both keys and chars, GLFW and SDL are only sending keys. For Space they all send all threes) + if ((flags & ImGuiInputTextFlags_AllowTabInput) && !is_readonly) + { + if (Shortcut(ImGuiKey_Tab, ImGuiInputFlags_Repeat, id)) { unsigned int c = '\t'; // Insert TAB - if (InputTextFilterCharacter(&c, flags, callback, callback_user_data)) - state->OnKeyPressed((int)c); + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) + state->OnCharPressed(c); } + // FIXME: Implement Shift+Tab + /* + if (Shortcut(ImGuiKey_Tab | ImGuiMod_Shift, ImGuiInputFlags_Repeat, id)) + { + } + */ + } // Process regular text input (before we check for Return because using some IME will effectively send a Return?) // We ignore CTRL inputs, but need to allow ALT+CTRL as some keyboards (e.g. German) use AltGR (which _is_ Alt+Ctrl) to input certain characters. + const bool ignore_char_inputs = (io.KeyCtrl && !io.KeyAlt) || (is_osx && io.KeyCtrl); if (io.InputQueueCharacters.Size > 0) { - if (!ignore_char_inputs && !is_readonly && !user_nav_input_start) + if (!ignore_char_inputs && !is_readonly && !input_requested_by_nav) for (int n = 0; n < io.InputQueueCharacters.Size; n++) { // Insert character if they pass filtering unsigned int c = (unsigned int)io.InputQueueCharacters[n]; - if (c == '\t' && io.KeyShift) + if (c == '\t') // Skip Tab, see above. continue; - if (InputTextFilterCharacter(&c, flags, callback, callback_user_data)) - state->OnKeyPressed((int)c); + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) + state->OnCharPressed(c); } // Consume characters @@ -3647,67 +4826,110 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } // Process other shortcuts/key-presses - bool cancel_edit = false; + bool revert_edit = false; if (g.ActiveId == id && !g.ActiveIdIsJustActivated && !clear_active_id) { IM_ASSERT(state != NULL); + + const int row_count_per_page = ImMax((int)((inner_size.y - style.FramePadding.y) / g.FontSize), 1); + state->Stb->row_count_per_page = row_count_per_page; + const int k_mask = (io.KeyShift ? STB_TEXTEDIT_K_SHIFT : 0); - const bool is_osx = io.ConfigMacOSXBehaviors; - const bool is_shortcut_key = (is_osx ? (io.KeySuper && !io.KeyCtrl) : (io.KeyCtrl && !io.KeySuper)) && !io.KeyAlt && !io.KeyShift; // OS X style: Shortcuts using Cmd/Super instead of Ctrl - const bool is_osx_shift_shortcut = is_osx && io.KeySuper && io.KeyShift && !io.KeyCtrl && !io.KeyAlt; const bool is_wordmove_key_down = is_osx ? io.KeyAlt : io.KeyCtrl; // OS X style: Text editing cursor movement using Alt instead of Ctrl - const bool is_startend_key_down = is_osx && io.KeySuper && !io.KeyCtrl && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End - const bool is_ctrl_key_only = io.KeyCtrl && !io.KeyShift && !io.KeyAlt && !io.KeySuper; - const bool is_shift_key_only = io.KeyShift && !io.KeyCtrl && !io.KeyAlt && !io.KeySuper; + const bool is_startend_key_down = is_osx && io.KeyCtrl && !io.KeySuper && !io.KeyAlt; // OS X style: Line/Text Start and End using Cmd+Arrows instead of Home/End - const bool is_cut = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_X)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Delete))) && !is_readonly && !is_password && (!is_multiline || state->HasSelection()); - const bool is_copy = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_C)) || (is_ctrl_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_password && (!is_multiline || state->HasSelection()); - const bool is_paste = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_V)) || (is_shift_key_only && IsKeyPressedMap(ImGuiKey_Insert))) && !is_readonly; - const bool is_undo = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_Z)) && !is_readonly && is_undoable); - const bool is_redo = ((is_shortcut_key && IsKeyPressedMap(ImGuiKey_Y)) || (is_osx_shift_shortcut && IsKeyPressedMap(ImGuiKey_Z))) && !is_readonly && is_undoable; + // Using Shortcut() with ImGuiInputFlags_RouteFocused (default policy) to allow routing operations for other code (e.g. calling window trying to use CTRL+A and CTRL+B: former would be handled by InputText) + // Otherwise we could simply assume that we own the keys as we are active. + const ImGuiInputFlags f_repeat = ImGuiInputFlags_Repeat; + const bool is_cut = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_X, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Delete, f_repeat, id)) && !is_readonly && !is_password && (!is_multiline || state->HasSelection()); + const bool is_copy = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_C, 0, id) || Shortcut(ImGuiMod_Ctrl | ImGuiKey_Insert, 0, id)) && !is_password && (!is_multiline || state->HasSelection()); + const bool is_paste = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_V, f_repeat, id) || Shortcut(ImGuiMod_Shift | ImGuiKey_Insert, f_repeat, id)) && !is_readonly; + const bool is_undo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable; + const bool is_redo = (Shortcut(ImGuiMod_Ctrl | ImGuiKey_Y, f_repeat, id) || Shortcut(ImGuiMod_Ctrl | ImGuiMod_Shift | ImGuiKey_Z, f_repeat, id)) && !is_readonly && is_undoable; + const bool is_select_all = Shortcut(ImGuiMod_Ctrl | ImGuiKey_A, 0, id); - if (IsKeyPressedMap(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); } - else if (IsKeyPressedMap(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); } - else if (IsKeyPressedMap(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); } - else if (IsKeyPressedMap(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); } - else if (IsKeyPressedMap(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); } - else if (IsKeyPressedMap(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); } - else if (IsKeyPressedMap(ImGuiKey_Delete) && !is_readonly) { state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); } - else if (IsKeyPressedMap(ImGuiKey_Backspace) && !is_readonly) + // We allow validate/cancel with Nav source (gamepad) to makes it easier to undo an accidental NavInput press with no keyboard wired, but otherwise it isn't very useful. + const bool nav_gamepad_active = (io.ConfigFlags & ImGuiConfigFlags_NavEnableGamepad) != 0 && (io.BackendFlags & ImGuiBackendFlags_HasGamepad) != 0; + const bool is_enter_pressed = IsKeyPressed(ImGuiKey_Enter, true) || IsKeyPressed(ImGuiKey_KeypadEnter, true); + const bool is_gamepad_validate = nav_gamepad_active && (IsKeyPressed(ImGuiKey_NavGamepadActivate, false) || IsKeyPressed(ImGuiKey_NavGamepadInput, false)); + const bool is_cancel = Shortcut(ImGuiKey_Escape, f_repeat, id) || (nav_gamepad_active && Shortcut(ImGuiKey_NavGamepadCancel, f_repeat, id)); + + // FIXME: Should use more Shortcut() and reduce IsKeyPressed()+SetKeyOwner(), but requires modifiers combination to be taken account of. + // FIXME-OSX: Missing support for Alt(option)+Right/Left = go to end of line, or next line if already in end of line. + if (IsKeyPressed(ImGuiKey_LeftArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINESTART : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_LEFT) | k_mask); } + else if (IsKeyPressed(ImGuiKey_RightArrow)) { state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_LINEEND : is_wordmove_key_down ? STB_TEXTEDIT_K_WORDRIGHT : STB_TEXTEDIT_K_RIGHT) | k_mask); } + else if (IsKeyPressed(ImGuiKey_UpArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMax(draw_window->Scroll.y - g.FontSize, 0.0f)); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTSTART : STB_TEXTEDIT_K_UP) | k_mask); } + else if (IsKeyPressed(ImGuiKey_DownArrow) && is_multiline) { if (io.KeyCtrl) SetScrollY(draw_window, ImMin(draw_window->Scroll.y + g.FontSize, GetScrollMaxY())); else state->OnKeyPressed((is_startend_key_down ? STB_TEXTEDIT_K_TEXTEND : STB_TEXTEDIT_K_DOWN) | k_mask); } + else if (IsKeyPressed(ImGuiKey_PageUp) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGUP | k_mask); scroll_y -= row_count_per_page * g.FontSize; } + else if (IsKeyPressed(ImGuiKey_PageDown) && is_multiline) { state->OnKeyPressed(STB_TEXTEDIT_K_PGDOWN | k_mask); scroll_y += row_count_per_page * g.FontSize; } + else if (IsKeyPressed(ImGuiKey_Home)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTSTART | k_mask : STB_TEXTEDIT_K_LINESTART | k_mask); } + else if (IsKeyPressed(ImGuiKey_End)) { state->OnKeyPressed(io.KeyCtrl ? STB_TEXTEDIT_K_TEXTEND | k_mask : STB_TEXTEDIT_K_LINEEND | k_mask); } + else if (IsKeyPressed(ImGuiKey_Delete) && !is_readonly && !is_cut) + { + if (!state->HasSelection()) + { + // OSX doesn't seem to have Super+Delete to delete until end-of-line, so we don't emulate that (as opposed to Super+Backspace) + if (is_wordmove_key_down) + state->OnKeyPressed(STB_TEXTEDIT_K_WORDRIGHT | STB_TEXTEDIT_K_SHIFT); + } + state->OnKeyPressed(STB_TEXTEDIT_K_DELETE | k_mask); + } + else if (IsKeyPressed(ImGuiKey_Backspace) && !is_readonly) { if (!state->HasSelection()) { if (is_wordmove_key_down) - state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT|STB_TEXTEDIT_K_SHIFT); - else if (is_osx && io.KeySuper && !io.KeyAlt && !io.KeyCtrl) - state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART|STB_TEXTEDIT_K_SHIFT); + state->OnKeyPressed(STB_TEXTEDIT_K_WORDLEFT | STB_TEXTEDIT_K_SHIFT); + else if (is_osx && io.KeyCtrl && !io.KeyAlt && !io.KeySuper) + state->OnKeyPressed(STB_TEXTEDIT_K_LINESTART | STB_TEXTEDIT_K_SHIFT); } state->OnKeyPressed(STB_TEXTEDIT_K_BACKSPACE | k_mask); } - else if (IsKeyPressedMap(ImGuiKey_Enter) || IsKeyPressedMap(ImGuiKey_KeyPadEnter)) + else if (is_enter_pressed || is_gamepad_validate) { + // Determine if we turn Enter into a \n character bool ctrl_enter_for_new_line = (flags & ImGuiInputTextFlags_CtrlEnterForNewLine) != 0; - if (!is_multiline || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl)) + if (!is_multiline || is_gamepad_validate || (ctrl_enter_for_new_line && !io.KeyCtrl) || (!ctrl_enter_for_new_line && io.KeyCtrl)) { - enter_pressed = clear_active_id = true; + validated = true; + if (io.ConfigInputTextEnterKeepActive && !is_multiline) + state->SelectAll(); // No need to scroll + else + clear_active_id = true; } else if (!is_readonly) { unsigned int c = '\n'; // Insert new line - if (InputTextFilterCharacter(&c, flags, callback, callback_user_data)) - state->OnKeyPressed((int)c); + if (InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data)) + state->OnCharPressed(c); } } - else if (IsKeyPressedMap(ImGuiKey_Escape)) + else if (is_cancel) { - clear_active_id = cancel_edit = true; + if (flags & ImGuiInputTextFlags_EscapeClearsAll) + { + if (buf[0] != 0) + { + revert_edit = true; + } + else + { + render_cursor = render_selection = false; + clear_active_id = true; + } + } + else + { + clear_active_id = revert_edit = true; + render_cursor = render_selection = false; + } } else if (is_undo || is_redo) { state->OnKeyPressed(is_undo ? STB_TEXTEDIT_K_UNDO : STB_TEXTEDIT_K_REDO); state->ClearSelection(); } - else if (is_shortcut_key && IsKeyPressedMap(ImGuiKey_A)) + else if (is_select_all) { state->SelectAll(); state->CursorFollow = true; @@ -3715,22 +4937,22 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ else if (is_cut || is_copy) { // Cut, Copy - if (io.SetClipboardTextFn) + if (g.PlatformIO.Platform_SetClipboardTextFn != NULL) { - const int ib = state->HasSelection() ? ImMin(state->Stb.select_start, state->Stb.select_end) : 0; - const int ie = state->HasSelection() ? ImMax(state->Stb.select_start, state->Stb.select_end) : state->CurLenW; - const int clipboard_data_len = ImTextCountUtf8BytesFromStr(state->TextW.Data + ib, state->TextW.Data + ie) + 1; - char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char)); - ImTextStrToUtf8(clipboard_data, clipboard_data_len, state->TextW.Data + ib, state->TextW.Data + ie); - SetClipboardText(clipboard_data); - MemFree(clipboard_data); + // SetClipboardText() only takes null terminated strings + state->TextSrc may point to read-only user buffer, so we need to make a copy. + const int ib = state->HasSelection() ? ImMin(state->Stb->select_start, state->Stb->select_end) : 0; + const int ie = state->HasSelection() ? ImMax(state->Stb->select_start, state->Stb->select_end) : state->TextLen; + g.TempBuffer.reserve(ie - ib + 1); + memcpy(g.TempBuffer.Data, state->TextSrc + ib, ie - ib); + g.TempBuffer.Data[ie - ib] = 0; + SetClipboardText(g.TempBuffer.Data); } if (is_cut) { if (!state->HasSelection()) state->SelectAll(); state->CursorFollow = true; - stb_textedit_cut(state, &state->Stb); + stb_textedit_cut(state, state->Stb); } } else if (is_paste) @@ -3738,26 +4960,28 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ if (const char* clipboard = GetClipboardText()) { // Filter pasted buffer - const int clipboard_len = (int)strlen(clipboard); - ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len+1) * sizeof(ImWchar)); - int clipboard_filtered_len = 0; - for (const char* s = clipboard; *s; ) + const int clipboard_len = (int)ImStrlen(clipboard); + ImVector clipboard_filtered; + clipboard_filtered.reserve(clipboard_len + 1); + for (const char* s = clipboard; *s != 0; ) { unsigned int c; - s += ImTextCharFromUtf8(&c, s, NULL); - if (c == 0) - break; - if (c >= 0x10000 || !InputTextFilterCharacter(&c, flags, callback, callback_user_data)) + int in_len = ImTextCharFromUtf8(&c, s, NULL); + s += in_len; + if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, true)) continue; - clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c; + char c_utf8[5]; + ImTextCharToUtf8(c_utf8, c); + int out_len = (int)ImStrlen(c_utf8); + clipboard_filtered.resize(clipboard_filtered.Size + out_len); + memcpy(clipboard_filtered.Data + clipboard_filtered.Size - out_len, c_utf8, out_len); } - clipboard_filtered[clipboard_filtered_len] = 0; - if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation + if (clipboard_filtered.Size > 0) // If everything was filtered, ignore the pasting operation { - stb_textedit_paste(state, &state->Stb, clipboard_filtered, clipboard_filtered_len); + clipboard_filtered.push_back(0); + stb_textedit_paste(state, state->Stb, clipboard_filtered.Data, clipboard_filtered.Size - 1); state->CursorFollow = true; } - MemFree(clipboard_filtered); } } @@ -3766,156 +4990,193 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } // Process callbacks and apply result back to user's buffer. + const char* apply_new_text = NULL; + int apply_new_text_length = 0; if (g.ActiveId == id) { IM_ASSERT(state != NULL); - const char* apply_new_text = NULL; - int apply_new_text_length = 0; - if (cancel_edit) + if (revert_edit && !is_readonly) { - // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents. - if (!is_readonly && strcmp(buf, state->InitialTextA.Data) != 0) + if (flags & ImGuiInputTextFlags_EscapeClearsAll) { - apply_new_text = state->InitialTextA.Data; - apply_new_text_length = state->InitialTextA.Size - 1; + // Clear input + IM_ASSERT(buf[0] != 0); + apply_new_text = ""; + apply_new_text_length = 0; + value_changed = true; + IMSTB_TEXTEDIT_CHARTYPE empty_string; + stb_textedit_replace(state, state->Stb, &empty_string, 0); + } + else if (strcmp(buf, state->TextToRevertTo.Data) != 0) + { + apply_new_text = state->TextToRevertTo.Data; + apply_new_text_length = state->TextToRevertTo.Size - 1; + + // Restore initial value. Only return true if restoring to the initial value changes the current buffer contents. + // Push records into the undo stack so we can CTRL+Z the revert operation itself + value_changed = true; + stb_textedit_replace(state, state->Stb, state->TextToRevertTo.Data, state->TextToRevertTo.Size - 1); } } - // When using 'ImGuiInputTextFlags_EnterReturnsTrue' as a special case we reapply the live buffer back to the input buffer before clearing ActiveId, even though strictly speaking it wasn't modified on this frame. - // If we didn't do that, code like InputInt() with ImGuiInputTextFlags_EnterReturnsTrue would fail. Also this allows the user to use InputText() with ImGuiInputTextFlags_EnterReturnsTrue without maintaining any user-side storage. - bool apply_edit_back_to_user_buffer = !cancel_edit || (enter_pressed && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); + // FIXME-OPT: We always reapply the live buffer back to the input buffer before clearing ActiveId, + // even though strictly speaking it wasn't modified on this frame. Should mark dirty state from the stb_textedit callbacks. + // If we do that, need to ensure that as special case, 'validated == true' also writes back. + // This also allows the user to use InputText() without maintaining any user-side storage. + // (please note that if you use this property along ImGuiInputTextFlags_CallbackResize you can end up with your temporary string object + // unnecessarily allocating once a frame, either store your string data, either if you don't then don't use ImGuiInputTextFlags_CallbackResize). + const bool apply_edit_back_to_user_buffer = true;// !revert_edit || (validated && (flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0); if (apply_edit_back_to_user_buffer) { - // Apply new value immediately - copy modified buffer back + // Apply current edited text immediately. // Note that as soon as the input box is active, the in-widget value gets priority over any underlying modification of the input buffer - // FIXME: We actually always render 'buf' when calling DrawList->AddText, making the comment above incorrect. - // FIXME-OPT: CPU waste to do this every time the widget is active, should mark dirty state from the stb_textedit callbacks. - if (!is_readonly) - { - state->TextAIsValid = true; - state->TextA.resize(state->TextW.Size * 4 + 1); - ImTextStrToUtf8(state->TextA.Data, state->TextA.Size, state->TextW.Data, NULL); - } // User callback - if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackAlways)) != 0) + if ((flags & (ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory | ImGuiInputTextFlags_CallbackEdit | ImGuiInputTextFlags_CallbackAlways)) != 0) { IM_ASSERT(callback != NULL); // The reason we specify the usage semantic (Completion/History) is that Completion needs to disable keyboard TABBING at the moment. ImGuiInputTextFlags event_flag = 0; - ImGuiKey event_key = ImGuiKey_COUNT; - if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && IsKeyPressedMap(ImGuiKey_Tab)) + ImGuiKey event_key = ImGuiKey_None; + if ((flags & ImGuiInputTextFlags_CallbackCompletion) != 0 && Shortcut(ImGuiKey_Tab, 0, id)) { event_flag = ImGuiInputTextFlags_CallbackCompletion; event_key = ImGuiKey_Tab; } - else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_UpArrow)) + else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_UpArrow)) { event_flag = ImGuiInputTextFlags_CallbackHistory; event_key = ImGuiKey_UpArrow; } - else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressedMap(ImGuiKey_DownArrow)) + else if ((flags & ImGuiInputTextFlags_CallbackHistory) != 0 && IsKeyPressed(ImGuiKey_DownArrow)) { event_flag = ImGuiInputTextFlags_CallbackHistory; event_key = ImGuiKey_DownArrow; } + else if ((flags & ImGuiInputTextFlags_CallbackEdit) && state->Edited) + { + event_flag = ImGuiInputTextFlags_CallbackEdit; + } else if (flags & ImGuiInputTextFlags_CallbackAlways) + { event_flag = ImGuiInputTextFlags_CallbackAlways; + } if (event_flag) { ImGuiInputTextCallbackData callback_data; - memset(&callback_data, 0, sizeof(ImGuiInputTextCallbackData)); + callback_data.Ctx = &g; callback_data.EventFlag = event_flag; callback_data.Flags = flags; callback_data.UserData = callback_user_data; + // FIXME-OPT: Undo stack reconcile needs a backup of the data until we rework API, see #7925 + char* callback_buf = is_readonly ? buf : state->TextA.Data; + IM_ASSERT(callback_buf == state->TextSrc); + state->CallbackTextBackup.resize(state->TextLen + 1); + memcpy(state->CallbackTextBackup.Data, callback_buf, state->TextLen + 1); + callback_data.EventKey = event_key; - callback_data.Buf = state->TextA.Data; - callback_data.BufTextLen = state->CurLenA; - callback_data.BufSize = state->BufCapacityA; + callback_data.Buf = callback_buf; + callback_data.BufTextLen = state->TextLen; + callback_data.BufSize = state->BufCapacity; callback_data.BufDirty = false; - // We have to convert from wchar-positions to UTF-8-positions, which can be pretty slow (an incentive to ditch the ImWchar buffer, see https://github.com/nothings/stb/issues/188) - ImWchar* text = state->TextW.Data; - const int utf8_cursor_pos = callback_data.CursorPos = ImTextCountUtf8BytesFromStr(text, text + state->Stb.cursor); - const int utf8_selection_start = callback_data.SelectionStart = ImTextCountUtf8BytesFromStr(text, text + state->Stb.select_start); - const int utf8_selection_end = callback_data.SelectionEnd = ImTextCountUtf8BytesFromStr(text, text + state->Stb.select_end); + const int utf8_cursor_pos = callback_data.CursorPos = state->Stb->cursor; + const int utf8_selection_start = callback_data.SelectionStart = state->Stb->select_start; + const int utf8_selection_end = callback_data.SelectionEnd = state->Stb->select_end; // Call user code callback(&callback_data); // Read back what user may have modified - IM_ASSERT(callback_data.Buf == state->TextA.Data); // Invalid to modify those fields - IM_ASSERT(callback_data.BufSize == state->BufCapacityA); + callback_buf = is_readonly ? buf : state->TextA.Data; // Pointer may have been invalidated by a resize callback + IM_ASSERT(callback_data.Buf == callback_buf); // Invalid to modify those fields + IM_ASSERT(callback_data.BufSize == state->BufCapacity); IM_ASSERT(callback_data.Flags == flags); - if (callback_data.CursorPos != utf8_cursor_pos) { state->Stb.cursor = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.CursorPos); state->CursorFollow = true; } - if (callback_data.SelectionStart != utf8_selection_start) { state->Stb.select_start = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionStart); } - if (callback_data.SelectionEnd != utf8_selection_end) { state->Stb.select_end = ImTextCountCharsFromUtf8(callback_data.Buf, callback_data.Buf + callback_data.SelectionEnd); } - if (callback_data.BufDirty) + const bool buf_dirty = callback_data.BufDirty; + if (callback_data.CursorPos != utf8_cursor_pos || buf_dirty) { state->Stb->cursor = callback_data.CursorPos; state->CursorFollow = true; } + if (callback_data.SelectionStart != utf8_selection_start || buf_dirty) { state->Stb->select_start = (callback_data.SelectionStart == callback_data.CursorPos) ? state->Stb->cursor : callback_data.SelectionStart; } + if (callback_data.SelectionEnd != utf8_selection_end || buf_dirty) { state->Stb->select_end = (callback_data.SelectionEnd == callback_data.SelectionStart) ? state->Stb->select_start : callback_data.SelectionEnd; } + if (buf_dirty) { - IM_ASSERT(callback_data.BufTextLen == (int)strlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! - if (callback_data.BufTextLen > backup_current_text_length && is_resizable) - state->TextW.resize(state->TextW.Size + (callback_data.BufTextLen - backup_current_text_length)); - state->CurLenW = ImTextStrFromUtf8(state->TextW.Data, state->TextW.Size, callback_data.Buf, NULL); - state->CurLenA = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() + // Callback may update buffer and thus set buf_dirty even in read-only mode. + IM_ASSERT(callback_data.BufTextLen == (int)ImStrlen(callback_data.Buf)); // You need to maintain BufTextLen if you change the text! + InputTextReconcileUndoState(state, state->CallbackTextBackup.Data, state->CallbackTextBackup.Size - 1, callback_data.Buf, callback_data.BufTextLen); + state->TextLen = callback_data.BufTextLen; // Assume correct length and valid UTF-8 from user, saves us an extra strlen() state->CursorAnimReset(); } } } // Will copy result string if modified - if (!is_readonly && strcmp(state->TextA.Data, buf) != 0) + if (!is_readonly && strcmp(state->TextSrc, buf) != 0) { - apply_new_text = state->TextA.Data; - apply_new_text_length = state->CurLenA; + apply_new_text = state->TextSrc; + apply_new_text_length = state->TextLen; + value_changed = true; } } + } - // Copy result to user buffer - if (apply_new_text) + // Handle reapplying final data on deactivation (see InputTextDeactivateHook() for details) + if (g.InputTextDeactivatedState.ID == id) + { + if (g.ActiveId != id && IsItemDeactivatedAfterEdit() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0) { - IM_ASSERT(apply_new_text_length >= 0); - if (backup_current_text_length != apply_new_text_length && is_resizable) - { - ImGuiInputTextCallbackData callback_data; - callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize; - callback_data.Flags = flags; - callback_data.Buf = buf; - callback_data.BufTextLen = apply_new_text_length; - callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1); - callback_data.UserData = callback_user_data; - callback(&callback_data); - buf = callback_data.Buf; - buf_size = callback_data.BufSize; - apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1); - IM_ASSERT(apply_new_text_length <= buf_size); - } - - // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size. - ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size)); + apply_new_text = g.InputTextDeactivatedState.TextA.Data; + apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1; value_changed = true; + //IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text); } + g.InputTextDeactivatedState.ID = 0; + } - // Clear temporary user storage - state->UserFlags = 0; - state->UserCallback = NULL; - state->UserCallbackData = NULL; + // Copy result to user buffer. This can currently only happen when (g.ActiveId == id) + if (apply_new_text != NULL) + { + //// We cannot test for 'backup_current_text_length != apply_new_text_length' here because we have no guarantee that the size + //// of our owned buffer matches the size of the string object held by the user, and by design we allow InputText() to be used + //// without any storage on user's side. + IM_ASSERT(apply_new_text_length >= 0); + if (is_resizable) + { + ImGuiInputTextCallbackData callback_data; + callback_data.Ctx = &g; + callback_data.EventFlag = ImGuiInputTextFlags_CallbackResize; + callback_data.Flags = flags; + callback_data.Buf = buf; + callback_data.BufTextLen = apply_new_text_length; + callback_data.BufSize = ImMax(buf_size, apply_new_text_length + 1); + callback_data.UserData = callback_user_data; + callback(&callback_data); + buf = callback_data.Buf; + buf_size = callback_data.BufSize; + apply_new_text_length = ImMin(callback_data.BufTextLen, buf_size - 1); + IM_ASSERT(apply_new_text_length <= buf_size); + } + //IMGUI_DEBUG_PRINT("InputText(\"%s\"): apply_new_text length %d\n", label, apply_new_text_length); + + // If the underlying buffer resize was denied or not carried to the next frame, apply_new_text_length+1 may be >= buf_size. + ImStrncpy(buf, apply_new_text, ImMin(apply_new_text_length + 1, buf_size)); } // Release active ID at the end of the function (so e.g. pressing Return still does a final application of the value) - if (clear_active_id && g.ActiveId == id) + // Otherwise request text input ahead for next frame. + if (g.ActiveId == id && clear_active_id) ClearActiveID(); + else if (g.ActiveId == id) + g.WantTextInputNextFrame = 1; // Render frame if (!is_multiline) { - RenderNavHighlight(frame_bb, id); + RenderNavCursor(frame_bb, id); RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); } - const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + size.x, frame_bb.Min.y + size.y); // Not using frame_bb.Max because we have adjusted size + const ImVec4 clip_rect(frame_bb.Min.x, frame_bb.Min.y, frame_bb.Min.x + inner_size.x, frame_bb.Min.y + inner_size.y); // Not using frame_bb.Max because we have adjusted size ImVec2 draw_pos = is_multiline ? draw_window->DC.CursorPos : frame_bb.Min + style.FramePadding; ImVec2 text_size(0.0f, 0.0f); @@ -3925,10 +5186,22 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ const int buf_display_max_length = 2 * 1024 * 1024; const char* buf_display = buf_display_from_state ? state->TextA.Data : buf; //-V595 const char* buf_display_end = NULL; // We have specialized paths below for setting the length + + // Display hint when contents is empty + // At this point we need to handle the possibility that a callback could have modified the underlying buffer (#8368) + const bool new_is_displaying_hint = (hint != NULL && (buf_display_from_state ? state->TextA.Data : buf)[0] == 0); + if (new_is_displaying_hint != is_displaying_hint) + { + if (is_password && !is_displaying_hint) + PopFont(); + is_displaying_hint = new_is_displaying_hint; + if (is_password && !is_displaying_hint) + PushPasswordFont(); + } if (is_displaying_hint) { buf_display = hint; - buf_display_end = hint + strlen(hint); + buf_display_end = hint + ImStrlen(hint); } // Render text. We currently only render selection when the widget is active or while scrolling. @@ -3937,7 +5210,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { IM_ASSERT(state != NULL); if (!is_displaying_hint) - buf_display_end = buf_display + state->CurLenA; + buf_display_end = buf_display + state->TextLen; // Render text (with cursor and selection) // This is going to be messy. We need to: @@ -3946,57 +5219,45 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // - Measure text height (for scrollbar) // We are attempting to do most of that in **one main pass** to minimize the computation cost (non-negligible for large amount of text) + 2nd pass for selection rendering (we could merge them by an extra refactoring effort) // FIXME: This should occur on buf_display but we'd need to maintain cursor/select_start/select_end for UTF-8. - const ImWchar* text_begin = state->TextW.Data; + const char* text_begin = buf_display; + const char* text_end = text_begin + state->TextLen; ImVec2 cursor_offset, select_start_offset; { - // Find lines numbers straddling 'cursor' (slot 0) and 'select_start' (slot 1) positions. - const ImWchar* searches_input_ptr[2] = { NULL, NULL }; - int searches_result_line_no[2] = { -1000, -1000 }; - int searches_remaining = 0; - if (render_cursor) - { - searches_input_ptr[0] = text_begin + state->Stb.cursor; - searches_result_line_no[0] = -1; - searches_remaining++; - } - if (render_selection) - { - searches_input_ptr[1] = text_begin + ImMin(state->Stb.select_start, state->Stb.select_end); - searches_result_line_no[1] = -1; - searches_remaining++; - } + // Find lines numbers straddling cursor and selection min position + int cursor_line_no = render_cursor ? -1 : -1000; + int selmin_line_no = render_selection ? -1 : -1000; + const char* cursor_ptr = render_cursor ? text_begin + state->Stb->cursor : NULL; + const char* selmin_ptr = render_selection ? text_begin + ImMin(state->Stb->select_start, state->Stb->select_end) : NULL; - // Iterate all lines to find our line numbers - // In multi-line mode, we never exit the loop until all lines are counted, so add one extra to the searches_remaining counter. - searches_remaining += is_multiline ? 1 : 0; - int line_count = 0; - //for (const ImWchar* s = text_begin; (s = (const ImWchar*)wcschr((const wchar_t*)s, (wchar_t)'\n')) != NULL; s++) // FIXME-OPT: Could use this when wchar_t are 16-bits - for (const ImWchar* s = text_begin; *s != 0; s++) - if (*s == '\n') + // Count lines and find line number for cursor and selection ends + int line_count = 1; + if (is_multiline) + { + for (const char* s = text_begin; (s = (const char*)ImMemchr(s, '\n', (size_t)(text_end - s))) != NULL; s++) { + if (cursor_line_no == -1 && s >= cursor_ptr) { cursor_line_no = line_count; } + if (selmin_line_no == -1 && s >= selmin_ptr) { selmin_line_no = line_count; } line_count++; - if (searches_result_line_no[0] == -1 && s >= searches_input_ptr[0]) { searches_result_line_no[0] = line_count; if (--searches_remaining <= 0) break; } - if (searches_result_line_no[1] == -1 && s >= searches_input_ptr[1]) { searches_result_line_no[1] = line_count; if (--searches_remaining <= 0) break; } } - line_count++; - if (searches_result_line_no[0] == -1) - searches_result_line_no[0] = line_count; - if (searches_result_line_no[1] == -1) - searches_result_line_no[1] = line_count; + } + if (cursor_line_no == -1) + cursor_line_no = line_count; + if (selmin_line_no == -1) + selmin_line_no = line_count; // Calculate 2d position by finding the beginning of the line and measuring distance - cursor_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[0], text_begin), searches_input_ptr[0]).x; - cursor_offset.y = searches_result_line_no[0] * g.FontSize; - if (searches_result_line_no[1] >= 0) + cursor_offset.x = InputTextCalcTextSize(&g, ImStrbol(cursor_ptr, text_begin), cursor_ptr).x; + cursor_offset.y = cursor_line_no * g.FontSize; + if (selmin_line_no >= 0) { - select_start_offset.x = InputTextCalcTextSizeW(ImStrbolW(searches_input_ptr[1], text_begin), searches_input_ptr[1]).x; - select_start_offset.y = searches_result_line_no[1] * g.FontSize; + select_start_offset.x = InputTextCalcTextSize(&g, ImStrbol(selmin_ptr, text_begin), selmin_ptr).x; + select_start_offset.y = selmin_line_no * g.FontSize; } // Store text height (note that we haven't calculated text width at all, see GitHub issues #383, #1224) if (is_multiline) - text_size = ImVec2(size.x, line_count * g.FontSize); + text_size = ImVec2(inner_size.x, line_count * g.FontSize); } // Scroll @@ -4005,25 +5266,28 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ // Horizontal scroll in chunks of quarter width if (!(flags & ImGuiInputTextFlags_NoHorizontalScroll)) { - const float scroll_increment_x = size.x * 0.25f; - if (cursor_offset.x < state->ScrollX) - state->ScrollX = (float)(int)ImMax(0.0f, cursor_offset.x - scroll_increment_x); - else if (cursor_offset.x - size.x >= state->ScrollX) - state->ScrollX = (float)(int)(cursor_offset.x - size.x + scroll_increment_x); + const float scroll_increment_x = inner_size.x * 0.25f; + const float visible_width = inner_size.x - style.FramePadding.x; + if (cursor_offset.x < state->Scroll.x) + state->Scroll.x = IM_TRUNC(ImMax(0.0f, cursor_offset.x - scroll_increment_x)); + else if (cursor_offset.x - visible_width >= state->Scroll.x) + state->Scroll.x = IM_TRUNC(cursor_offset.x - visible_width + scroll_increment_x); } else { - state->ScrollX = 0.0f; + state->Scroll.y = 0.0f; } // Vertical scroll if (is_multiline) { - float scroll_y = draw_window->Scroll.y; + // Test if cursor is vertically visible if (cursor_offset.y - g.FontSize < scroll_y) scroll_y = ImMax(0.0f, cursor_offset.y - g.FontSize); - else if (cursor_offset.y - size.y >= scroll_y) - scroll_y = cursor_offset.y - size.y; + else if (cursor_offset.y - (inner_size.y - style.FramePadding.y * 2.0f) >= scroll_y) + scroll_y = cursor_offset.y - inner_size.y + style.FramePadding.y * 2.0f; + const float scroll_max_y = ImMax((text_size.y + style.FramePadding.y * 2.0f) - inner_size.y, 0.0f); + scroll_y = ImClamp(scroll_y, 0.0f, scroll_max_y); draw_pos.y += (draw_window->Scroll.y - scroll_y); // Manipulate cursor pos immediately avoid a frame of lag draw_window->Scroll.y = scroll_y; } @@ -4032,43 +5296,41 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ } // Draw selection - const ImVec2 draw_scroll = ImVec2(state->ScrollX, 0.0f); + const ImVec2 draw_scroll = ImVec2(state->Scroll.x, 0.0f); if (render_selection) { - const ImWchar* text_selected_begin = text_begin + ImMin(state->Stb.select_start, state->Stb.select_end); - const ImWchar* text_selected_end = text_begin + ImMax(state->Stb.select_start, state->Stb.select_end); + const char* text_selected_begin = text_begin + ImMin(state->Stb->select_start, state->Stb->select_end); + const char* text_selected_end = text_begin + ImMax(state->Stb->select_start, state->Stb->select_end); ImU32 bg_color = GetColorU32(ImGuiCol_TextSelectedBg, render_cursor ? 1.0f : 0.6f); // FIXME: current code flow mandate that render_cursor is always true here, we are leaving the transparent one for tests. float bg_offy_up = is_multiline ? 0.0f : -1.0f; // FIXME: those offsets should be part of the style? they don't play so well with multi-line selection. float bg_offy_dn = is_multiline ? 0.0f : 2.0f; ImVec2 rect_pos = draw_pos + select_start_offset - draw_scroll; - for (const ImWchar* p = text_selected_begin; p < text_selected_end; ) + for (const char* p = text_selected_begin; p < text_selected_end; ) { if (rect_pos.y > clip_rect.w + g.FontSize) break; if (rect_pos.y < clip_rect.y) { - //p = (const ImWchar*)wmemchr((const wchar_t*)p, '\n', text_selected_end - p); // FIXME-OPT: Could use this when wchar_t are 16-bits - //p = p ? p + 1 : text_selected_end; - while (p < text_selected_end) - if (*p++ == '\n') - break; + p = (const char*)ImMemchr((void*)p, '\n', text_selected_end - p); + p = p ? p + 1 : text_selected_end; } else { - ImVec2 rect_size = InputTextCalcTextSizeW(p, text_selected_end, &p, NULL, true); - if (rect_size.x <= 0.0f) rect_size.x = (float)(int)(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines - ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos +ImVec2(rect_size.x, bg_offy_dn)); + ImVec2 rect_size = InputTextCalcTextSize(&g, p, text_selected_end, &p, NULL, true); + if (rect_size.x <= 0.0f) rect_size.x = IM_TRUNC(g.Font->GetCharAdvance((ImWchar)' ') * 0.50f); // So we can see selected empty lines + ImRect rect(rect_pos + ImVec2(0.0f, bg_offy_up - g.FontSize), rect_pos + ImVec2(rect_size.x, bg_offy_dn)); rect.ClipWith(clip_rect); if (rect.Overlaps(clip_rect)) draw_window->DrawList->AddRectFilled(rect.Min, rect.Max, bg_color); + rect_pos.x = draw_pos.x - draw_scroll.x; } - rect_pos.x = draw_pos.x - draw_scroll.x; rect_pos.y += g.FontSize; } } // We test for 'buf_display_max_length' as a way to avoid some pathological cases (e.g. single-line 1 MB string) which would make ImDrawList crash. + // FIXME-OPT: Multiline could submit a smaller amount of contents to AddText() since we already iterated through it. if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) { ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); @@ -4080,60 +5342,124 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_ { state->CursorAnim += io.DeltaTime; bool cursor_is_visible = (!g.IO.ConfigInputTextCursorBlink) || (state->CursorAnim <= 0.0f) || ImFmod(state->CursorAnim, 1.20f) <= 0.80f; - ImVec2 cursor_screen_pos = draw_pos + cursor_offset - draw_scroll; + ImVec2 cursor_screen_pos = ImTrunc(draw_pos + cursor_offset - draw_scroll); ImRect cursor_screen_rect(cursor_screen_pos.x, cursor_screen_pos.y - g.FontSize + 0.5f, cursor_screen_pos.x + 1.0f, cursor_screen_pos.y - 1.5f); if (cursor_is_visible && cursor_screen_rect.Overlaps(clip_rect)) draw_window->DrawList->AddLine(cursor_screen_rect.Min, cursor_screen_rect.GetBL(), GetColorU32(ImGuiCol_Text)); // Notify OS of text input position for advanced IME (-1 x offset so that Windows IME can cover our cursor. Bit of an extra nicety.) if (!is_readonly) - g.PlatformImePos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize); + { + g.PlatformImeData.WantVisible = true; + g.PlatformImeData.InputPos = ImVec2(cursor_screen_pos.x - 1.0f, cursor_screen_pos.y - g.FontSize); + g.PlatformImeData.InputLineHeight = g.FontSize; + g.PlatformImeViewport = window->Viewport->ID; + } } } else { // Render text only (no selection, no cursor) if (is_multiline) - text_size = ImVec2(size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width + text_size = ImVec2(inner_size.x, InputTextCalcTextLenAndLineCount(buf_display, &buf_display_end) * g.FontSize); // We don't need width else if (!is_displaying_hint && g.ActiveId == id) - buf_display_end = buf_display + state->CurLenA; + buf_display_end = buf_display + state->TextLen; else if (!is_displaying_hint) - buf_display_end = buf_display + strlen(buf_display); + buf_display_end = buf_display + ImStrlen(buf_display); if (is_multiline || (buf_display_end - buf_display) < buf_display_max_length) { - ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); - draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); - } - } + // Find render position for right alignment + if (flags & ImGuiInputTextFlags_ElideLeft) + draw_pos.x = ImMin(draw_pos.x, frame_bb.Max.x - CalcTextSize(buf_display, NULL).x - style.FramePadding.x); - if (is_multiline) - { - Dummy(text_size + ImVec2(0.0f, g.FontSize)); // Always add room to scroll an extra line - EndChildFrame(); - EndGroup(); + const ImVec2 draw_scroll = /*state ? ImVec2(state->Scroll.x, 0.0f) :*/ ImVec2(0.0f, 0.0f); // Preserve scroll when inactive? + ImU32 col = GetColorU32(is_displaying_hint ? ImGuiCol_TextDisabled : ImGuiCol_Text); + draw_window->DrawList->AddText(g.Font, g.FontSize, draw_pos - draw_scroll, col, buf_display, buf_display_end, 0.0f, is_multiline ? NULL : &clip_rect); + } } if (is_password && !is_displaying_hint) PopFont(); + if (is_multiline) + { + // For focus requests to work on our multiline we need to ensure our child ItemAdd() call specifies the ImGuiItemFlags_Inputable (see #4761, #7870)... + Dummy(ImVec2(text_size.x, text_size.y + style.FramePadding.y)); + g.NextItemData.ItemFlags |= (ImGuiItemFlags)ImGuiItemFlags_Inputable | ImGuiItemFlags_NoTabStop; + EndChild(); + item_data_backup.StatusFlags |= (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredWindow); + + // ...and then we need to undo the group overriding last item data, which gets a bit messy as EndGroup() tries to forward scrollbar being active... + // FIXME: This quite messy/tricky, should attempt to get rid of the child window. + EndGroup(); + if (g.LastItemData.ID == 0 || g.LastItemData.ID != GetWindowScrollbarID(draw_window, ImGuiAxis_Y)) + { + g.LastItemData.ID = id; + g.LastItemData.ItemFlags = item_data_backup.ItemFlags; + g.LastItemData.StatusFlags = item_data_backup.StatusFlags; + } + } + if (state) + state->TextSrc = NULL; + // Log as text - if (g.LogEnabled && !(is_password && !is_displaying_hint)) + if (g.LogEnabled && (!is_password || is_displaying_hint)) + { + LogSetNextTextDecoration("{", "}"); LogRenderedText(&draw_pos, buf_display, buf_display_end); + } if (label_size.x > 0) RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); - if (value_changed && !(flags & ImGuiInputTextFlags_NoMarkEdited)) + if (value_changed) MarkItemEdited(id); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Inputable); if ((flags & ImGuiInputTextFlags_EnterReturnsTrue) != 0) - return enter_pressed; + return validated; else return value_changed; } +void ImGui::DebugNodeInputTextState(ImGuiInputTextState* state) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + ImGuiContext& g = *GImGui; + ImStb::STB_TexteditState* stb_state = state->Stb; + ImStb::StbUndoState* undo_state = &stb_state->undostate; + Text("ID: 0x%08X, ActiveID: 0x%08X", state->ID, g.ActiveId); + DebugLocateItemOnHover(state->ID); + Text("CurLenA: %d, Cursor: %d, Selection: %d..%d", state->TextLen, stb_state->cursor, stb_state->select_start, stb_state->select_end); + Text("BufCapacityA: %d", state->BufCapacity); + Text("(Internal Buffer: TextA Size: %d, Capacity: %d)", state->TextA.Size, state->TextA.Capacity); + Text("has_preferred_x: %d (%.2f)", stb_state->has_preferred_x, stb_state->preferred_x); + Text("undo_point: %d, redo_point: %d, undo_char_point: %d, redo_char_point: %d", undo_state->undo_point, undo_state->redo_point, undo_state->undo_char_point, undo_state->redo_char_point); + if (BeginChild("undopoints", ImVec2(0.0f, GetTextLineHeight() * 10), ImGuiChildFlags_Borders | ImGuiChildFlags_ResizeY)) // Visualize undo state + { + PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + for (int n = 0; n < IMSTB_TEXTEDIT_UNDOSTATECOUNT; n++) + { + ImStb::StbUndoRecord* undo_rec = &undo_state->undo_rec[n]; + const char undo_rec_type = (n < undo_state->undo_point) ? 'u' : (n >= undo_state->redo_point) ? 'r' : ' '; + if (undo_rec_type == ' ') + BeginDisabled(); + const int buf_preview_len = (undo_rec_type != ' ' && undo_rec->char_storage != -1) ? undo_rec->insert_length : 0; + const char* buf_preview_str = undo_state->undo_char + undo_rec->char_storage; + Text("%c [%02d] where %03d, insert %03d, delete %03d, char_storage %03d \"%.*s\"", + undo_rec_type, n, undo_rec->where, undo_rec->insert_length, undo_rec->delete_length, undo_rec->char_storage, buf_preview_len, buf_preview_str); + if (undo_rec_type == ' ') + EndDisabled(); + } + PopStyleVar(); + } + EndChild(); +#else + IM_UNUSED(state); +#endif +} + //------------------------------------------------------------------------- // [SECTION] Widgets: ColorEdit, ColorPicker, ColorButton, etc. //------------------------------------------------------------------------- @@ -4154,9 +5480,37 @@ bool ImGui::ColorEdit3(const char* label, float col[3], ImGuiColorEditFlags flag return ColorEdit4(label, col, flags | ImGuiColorEditFlags_NoAlpha); } +static void ColorEditRestoreH(const float* col, float* H) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ColorEditCurrentID != 0); + if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0))) + return; + *H = g.ColorEditSavedHue; +} + +// ColorEdit supports RGB and HSV inputs. In case of RGB input resulting color may have undefined hue and/or saturation. +// Since widget displays both RGB and HSV values we must preserve hue and saturation to prevent these values resetting. +static void ColorEditRestoreHS(const float* col, float* H, float* S, float* V) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.ColorEditCurrentID != 0); + if (g.ColorEditSavedID != g.ColorEditCurrentID || g.ColorEditSavedColor != ImGui::ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0))) + return; + + // When S == 0, H is undefined. + // When H == 1 it wraps around to 0. + if (*S == 0.0f || (*H == 0.0f && g.ColorEditSavedHue == 1)) + *H = g.ColorEditSavedHue; + + // When V == 0, S is undefined. + if (*V == 0.0f) + *S = g.ColorEditSavedSat; +} + // Edit colors components (each component in 0.0f..1.0f range). // See enum ImGuiColorEditFlags_ for available options. e.g. Only access 3 floats if ImGuiColorEditFlags_NoAlpha flag is set. -// With typical options: Left-click on colored square to open color picker. Right-click to open option menu. CTRL-Click over input fields to edit them and TAB to go to next item. +// With typical options: Left-click on color square to open color picker. Right-click to open option menu. CTRL+Click over input fields to edit them and TAB to go to next item. bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flags) { ImGuiWindow* window = GetCurrentWindow(); @@ -4166,40 +5520,44 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const float square_sz = GetFrameHeight(); - const float w_full = CalcItemWidth(); - const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); - const float w_inputs = w_full - w_button; const char* label_display_end = FindRenderedTextEnd(label); + float w_full = CalcItemWidth(); g.NextItemData.ClearFlags(); BeginGroup(); PushID(label); + const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0); + if (set_current_color_edit_id) + g.ColorEditCurrentID = window->IDStack.back(); // If we're not showing any slider there's no point in doing any HSV conversions const ImGuiColorEditFlags flags_untouched = flags; if (flags & ImGuiColorEditFlags_NoInputs) - flags = (flags & (~ImGuiColorEditFlags__DisplayMask)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions; + flags = (flags & (~ImGuiColorEditFlags_DisplayMask_)) | ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_NoOptions; // Context menu: display and modify options (before defaults are applied) if (!(flags & ImGuiColorEditFlags_NoOptions)) ColorEditOptionsPopup(col, flags); // Read stored options - if (!(flags & ImGuiColorEditFlags__DisplayMask)) - flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DisplayMask); - if (!(flags & ImGuiColorEditFlags__DataTypeMask)) - flags |= (g.ColorEditOptions & ImGuiColorEditFlags__DataTypeMask); - if (!(flags & ImGuiColorEditFlags__PickerMask)) - flags |= (g.ColorEditOptions & ImGuiColorEditFlags__PickerMask); - if (!(flags & ImGuiColorEditFlags__InputMask)) - flags |= (g.ColorEditOptions & ImGuiColorEditFlags__InputMask); - flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags__DisplayMask | ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags__InputMask)); - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__DisplayMask)); // Check that only 1 is selected - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__InputMask)); // Check that only 1 is selected + if (!(flags & ImGuiColorEditFlags_DisplayMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DisplayMask_); + if (!(flags & ImGuiColorEditFlags_DataTypeMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_DataTypeMask_); + if (!(flags & ImGuiColorEditFlags_PickerMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_); + if (!(flags & ImGuiColorEditFlags_InputMask_)) + flags |= (g.ColorEditOptions & ImGuiColorEditFlags_InputMask_); + flags |= (g.ColorEditOptions & ~(ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_)); + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check that only 1 is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected const bool alpha = (flags & ImGuiColorEditFlags_NoAlpha) == 0; const bool hdr = (flags & ImGuiColorEditFlags_HDR) != 0; const int components = alpha ? 4 : 3; + const float w_button = (flags & ImGuiColorEditFlags_NoSmallPreview) ? 0.0f : (square_sz + style.ItemInnerSpacing.x); + const float w_inputs = ImMax(w_full - w_button, 1.0f); + w_full = w_inputs + w_button; // Convert to the formats we need float f[4] = { col[0], col[1], col[2], alpha ? col[3] : 1.0f }; @@ -4207,10 +5565,9 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); else if ((flags & ImGuiColorEditFlags_InputRGB) && (flags & ImGuiColorEditFlags_DisplayHSV)) { - // Hue is lost when converting from greyscale rgb (saturation=0). Restore it. + // Hue is lost when converting from grayscale rgb (saturation=0). Restore it. ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); - if (f[1] == 0 && memcmp(g.ColorEditLastColor, col, sizeof(float) * 3) == 0) - f[0] = g.ColorEditLastHue; + ColorEditRestoreHS(col, &f[0], &f[1], &f[2]); } int i[4] = { IM_F32_TO_INT8_UNBOUND(f[0]), IM_F32_TO_INT8_UNBOUND(f[1]), IM_F32_TO_INT8_UNBOUND(f[2]), IM_F32_TO_INT8_UNBOUND(f[3]) }; @@ -4224,10 +5581,9 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag if ((flags & (ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_DisplayHSV)) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) { // RGB/HSV 0..255 Sliders - const float w_item_one = ImMax(1.0f, (float)(int)((w_inputs - (style.ItemInnerSpacing.x) * (components-1)) / (float)components)); - const float w_item_last = ImMax(1.0f, (float)(int)(w_inputs - (w_item_one + style.ItemInnerSpacing.x) * (components-1))); + const float w_items = w_inputs - style.ItemInnerSpacing.x * (components - 1); - const bool hide_prefix = (w_item_one <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); + const bool hide_prefix = (IM_TRUNC(w_items / components) <= CalcTextSize((flags & ImGuiColorEditFlags_Float) ? "M:0.000" : "M:000").x); static const char* ids[4] = { "##X", "##Y", "##Z", "##W" }; static const char* fmt_table_int[3][4] = { @@ -4243,25 +5599,27 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag }; const int fmt_idx = hide_prefix ? 0 : (flags & ImGuiColorEditFlags_DisplayHSV) ? 2 : 1; + float prev_split = 0.0f; for (int n = 0; n < components; n++) { if (n > 0) SameLine(0, style.ItemInnerSpacing.x); - SetNextItemWidth((n + 1 < components) ? w_item_one : w_item_last); + float next_split = IM_TRUNC(w_items * (n + 1) / components); + SetNextItemWidth(ImMax(next_split - prev_split, 1.0f)); + prev_split = next_split; - // Disable Hue edit when Saturation is zero - const bool disable_hue_edit = (n == 0 && (flags & ImGuiColorEditFlags_DisplayHSV) && i[1] == 0); + // FIXME: When ImGuiColorEditFlags_HDR flag is passed HS values snap in weird ways when SV values go below 0. if (flags & ImGuiColorEditFlags_Float) { - value_changed |= DragFloat(ids[n], &f[n], 1.0f/255.0f, disable_hue_edit ? +FLT_MAX : 0.0f, disable_hue_edit ? -FLT_MAX : hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); + value_changed |= DragFloat(ids[n], &f[n], 1.0f / 255.0f, 0.0f, hdr ? 0.0f : 1.0f, fmt_table_float[fmt_idx][n]); value_changed_as_float |= value_changed; } else { - value_changed |= DragInt(ids[n], &i[n], 1.0f, disable_hue_edit ? INT_MAX : 0, disable_hue_edit ? INT_MIN : hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); + value_changed |= DragInt(ids[n], &i[n], 1.0f, 0, hdr ? 0 : 255, fmt_table_int[fmt_idx][n]); } if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); } } else if ((flags & ImGuiColorEditFlags_DisplayHex) != 0 && (flags & ImGuiColorEditFlags_NoInputs) == 0) @@ -4269,24 +5627,27 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag // RGB Hexadecimal Input char buf[64]; if (alpha) - ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255), ImClamp(i[3],0,255)); + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255), ImClamp(i[3], 0, 255)); else - ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0],0,255), ImClamp(i[1],0,255), ImClamp(i[2],0,255)); + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", ImClamp(i[0], 0, 255), ImClamp(i[1], 0, 255), ImClamp(i[2], 0, 255)); SetNextItemWidth(w_inputs); - if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_CharsUppercase)) + if (InputText("##Text", buf, IM_ARRAYSIZE(buf), ImGuiInputTextFlags_CharsUppercase)) { value_changed = true; char* p = buf; while (*p == '#' || ImCharIsBlankA(*p)) p++; - i[0] = i[1] = i[2] = i[3] = 0; + i[0] = i[1] = i[2] = 0; + i[3] = 0xFF; // alpha default to 255 is not parsed by scanf (e.g. inputting #FFFFFF omitting alpha) + int r; if (alpha) - sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned) + r = sscanf(p, "%02X%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2], (unsigned int*)&i[3]); // Treat at unsigned (%X is unsigned) else - sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]); + r = sscanf(p, "%02X%02X%02X", (unsigned int*)&i[0], (unsigned int*)&i[1], (unsigned int*)&i[2]); + IM_UNUSED(r); // Fixes C6031: Return value ignored: 'sscanf'. } if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); } ImGuiWindow* picker_active_window = NULL; @@ -4303,31 +5664,37 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag // Store current color and open a picker g.ColorPickerRef = col_v4; OpenPopup("picker"); - SetNextWindowPos(window->DC.LastItemRect.GetBL() + ImVec2(-1,style.ItemSpacing.y)); + SetNextWindowPos(g.LastItemData.Rect.GetBL() + ImVec2(0.0f, style.ItemSpacing.y)); } } if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); if (BeginPopup("picker")) { - picker_active_window = g.CurrentWindow; - if (label != label_display_end) + if (g.CurrentWindow->BeginCount == 1) { - TextEx(label, label_display_end); - Spacing(); + picker_active_window = g.CurrentWindow; + if (label != label_display_end) + { + TextEx(label, label_display_end); + Spacing(); + } + ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; + ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; + SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? + value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); } - ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__PickerMask | ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar; - ImGuiColorEditFlags picker_flags = (flags_untouched & picker_flags_to_forward) | ImGuiColorEditFlags__DisplayMask | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_AlphaPreviewHalf; - SetNextItemWidth(square_sz * 12.0f); // Use 256 + bar sizes? - value_changed |= ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x); EndPopup(); } } if (label != label_display_end && !(flags & ImGuiColorEditFlags_NoLabel)) { - window->DC.CursorPos = ImVec2(pos.x + w_full + style.ItemInnerSpacing.x, pos.y + style.FramePadding.y); + // Position not necessarily next to last submitted button (e.g. if style.ColorButtonPosition == ImGuiDir_Left), + // but we need to use SameLine() to setup baseline correctly. Might want to refactor SameLine() to simplify this. + SameLine(0.0f, style.ItemInnerSpacing.x); + window->DC.CursorPos.x = pos.x + ((flags & ImGuiColorEditFlags_NoInputs) ? w_button : w_full + style.ItemInnerSpacing.x); TextEx(label, label_display_end); } @@ -4339,9 +5706,11 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag f[n] = i[n] / 255.0f; if ((flags & ImGuiColorEditFlags_DisplayHSV) && (flags & ImGuiColorEditFlags_InputRGB)) { - g.ColorEditLastHue = f[0]; + g.ColorEditSavedHue = f[0]; + g.ColorEditSavedSat = f[1]; ColorConvertHSVtoRGB(f[0], f[1], f[2], f[0], f[1], f[2]); - memcpy(g.ColorEditLastColor, f, sizeof(float) * 3); + g.ColorEditSavedID = g.ColorEditCurrentID; + g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(f[0], f[1], f[2], 0)); } if ((flags & ImGuiColorEditFlags_DisplayRGB) && (flags & ImGuiColorEditFlags_InputHSV)) ColorConvertRGBtoHSV(f[0], f[1], f[2], f[0], f[1], f[2]); @@ -4353,17 +5722,19 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag col[3] = f[3]; } + if (set_current_color_edit_id) + g.ColorEditCurrentID = 0; PopID(); EndGroup(); // Drag and Drop Target // NB: The flag test is merely an optional micro-optimization, BeginDragDropTarget() does the same test. - if ((window->DC.LastItemStatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget()) + if ((g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) && !(g.LastItemData.ItemFlags & ImGuiItemFlags_ReadOnly) && !(flags & ImGuiColorEditFlags_NoDragDrop) && BeginDragDropTarget()) { bool accepted_drag_drop = false; if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_3F)) { - memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 + memcpy((float*)col, payload->Data, sizeof(float) * 3); // Preserve alpha if any //-V512 //-V1086 value_changed = accepted_drag_drop = true; } if (const ImGuiPayload* payload = AcceptDragDropPayload(IMGUI_PAYLOAD_TYPE_COLOR_4F)) @@ -4380,10 +5751,10 @@ bool ImGui::ColorEdit4(const char* label, float col[4], ImGuiColorEditFlags flag // When picker is being actively used, use its active id so IsItemActive() will function on ColorEdit4(). if (picker_active_window && g.ActiveId != 0 && g.ActiveIdWindow == picker_active_window) - window->DC.LastItemId = g.ActiveId; + g.LastItemData.ID = g.ActiveId; - if (value_changed) - MarkItemEdited(window->DC.LastItemId); + if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId + MarkItemEdited(g.LastItemData.ID); return value_changed; } @@ -4397,52 +5768,6 @@ bool ImGui::ColorPicker3(const char* label, float col[3], ImGuiColorEditFlags fl return true; } -static inline ImU32 ImAlphaBlendColor(ImU32 col_a, ImU32 col_b) -{ - float t = ((col_b >> IM_COL32_A_SHIFT) & 0xFF) / 255.f; - int r = ImLerp((int)(col_a >> IM_COL32_R_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_R_SHIFT) & 0xFF, t); - int g = ImLerp((int)(col_a >> IM_COL32_G_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_G_SHIFT) & 0xFF, t); - int b = ImLerp((int)(col_a >> IM_COL32_B_SHIFT) & 0xFF, (int)(col_b >> IM_COL32_B_SHIFT) & 0xFF, t); - return IM_COL32(r, g, b, 0xFF); -} - -// Helper for ColorPicker4() -// NB: This is rather brittle and will show artifact when rounding this enabled if rounded corners overlap multiple cells. Caller currently responsible for avoiding that. -// I spent a non reasonable amount of time trying to getting this right for ColorButton with rounding+anti-aliasing+ImGuiColorEditFlags_HalfAlphaPreview flag + various grid sizes and offsets, and eventually gave up... probably more reasonable to disable rounding alltogether. -void ImGui::RenderColorRectWithAlphaCheckerboard(ImVec2 p_min, ImVec2 p_max, ImU32 col, float grid_step, ImVec2 grid_off, float rounding, int rounding_corners_flags) -{ - ImGuiWindow* window = GetCurrentWindow(); - if (((col & IM_COL32_A_MASK) >> IM_COL32_A_SHIFT) < 0xFF) - { - ImU32 col_bg1 = GetColorU32(ImAlphaBlendColor(IM_COL32(204,204,204,255), col)); - ImU32 col_bg2 = GetColorU32(ImAlphaBlendColor(IM_COL32(128,128,128,255), col)); - window->DrawList->AddRectFilled(p_min, p_max, col_bg1, rounding, rounding_corners_flags); - - int yi = 0; - for (float y = p_min.y + grid_off.y; y < p_max.y; y += grid_step, yi++) - { - float y1 = ImClamp(y, p_min.y, p_max.y), y2 = ImMin(y + grid_step, p_max.y); - if (y2 <= y1) - continue; - for (float x = p_min.x + grid_off.x + (yi & 1) * grid_step; x < p_max.x; x += grid_step * 2.0f) - { - float x1 = ImClamp(x, p_min.x, p_max.x), x2 = ImMin(x + grid_step, p_max.x); - if (x2 <= x1) - continue; - int rounding_corners_flags_cell = 0; - if (y1 <= p_min.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_TopRight; } - if (y2 >= p_max.y) { if (x1 <= p_min.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotLeft; if (x2 >= p_max.x) rounding_corners_flags_cell |= ImDrawCornerFlags_BotRight; } - rounding_corners_flags_cell &= rounding_corners_flags; - window->DrawList->AddRectFilled(ImVec2(x1,y1), ImVec2(x2,y2), col_bg2, rounding_corners_flags_cell ? rounding : 0.0f, rounding_corners_flags_cell); - } - } - } - else - { - window->DrawList->AddRectFilled(p_min, p_max, col, rounding, rounding_corners_flags); - } -} - // Helper for ColorPicker4() static void RenderArrowsForVerticalBar(ImDrawList* draw_list, ImVec2 pos, ImVec2 half_sz, float bar_w, float alpha) { @@ -4469,9 +5794,13 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl ImGuiIO& io = g.IO; const float width = CalcItemWidth(); + const bool is_readonly = ((g.NextItemData.ItemFlags | g.CurrentItemFlags) & ImGuiItemFlags_ReadOnly) != 0; g.NextItemData.ClearFlags(); PushID(label); + const bool set_current_color_edit_id = (g.ColorEditCurrentID == 0); + if (set_current_color_edit_id) + g.ColorEditCurrentID = window->IDStack.back(); BeginGroup(); if (!(flags & ImGuiColorEditFlags_NoSidePreview)) @@ -4482,12 +5811,12 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl ColorPickerOptionsPopup(col, flags); // Read stored options - if (!(flags & ImGuiColorEditFlags__PickerMask)) - flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__PickerMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__PickerMask; - if (!(flags & ImGuiColorEditFlags__InputMask)) - flags |= ((g.ColorEditOptions & ImGuiColorEditFlags__InputMask) ? g.ColorEditOptions : ImGuiColorEditFlags__OptionsDefault) & ImGuiColorEditFlags__InputMask; - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__PickerMask)); // Check that only 1 is selected - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__InputMask)); // Check that only 1 is selected + if (!(flags & ImGuiColorEditFlags_PickerMask_)) + flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_PickerMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_PickerMask_; + if (!(flags & ImGuiColorEditFlags_InputMask_)) + flags |= ((g.ColorEditOptions & ImGuiColorEditFlags_InputMask_) ? g.ColorEditOptions : ImGuiColorEditFlags_DefaultOptions_) & ImGuiColorEditFlags_InputMask_; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check that only 1 is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check that only 1 is selected if (!(flags & ImGuiColorEditFlags_NoOptions)) flags |= (g.ColorEditOptions & ImGuiColorEditFlags_AlphaBar); @@ -4500,7 +5829,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl float sv_picker_size = ImMax(bars_width * 1, width - (alpha_bar ? 2 : 1) * (bars_width + style.ItemInnerSpacing.x)); // Saturation/Value picking box float bar0_pos_x = picker_pos.x + sv_picker_size + style.ItemInnerSpacing.x; float bar1_pos_x = bar0_pos_x + bars_width + style.ItemInnerSpacing.x; - float bars_triangles_half_sz = (float)(int)(bars_width * 0.20f); + float bars_triangles_half_sz = IM_TRUNC(bars_width * 0.20f); float backup_initial_col[4]; memcpy(backup_initial_col, col, components * sizeof(float)); @@ -4508,7 +5837,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl float wheel_thickness = sv_picker_size * 0.08f; float wheel_r_outer = sv_picker_size * 0.50f; float wheel_r_inner = wheel_r_outer - wheel_thickness; - ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size*0.5f); + ImVec2 wheel_center(picker_pos.x + (sv_picker_size + bars_width)*0.5f, picker_pos.y + sv_picker_size * 0.5f); // Note: the triangle is displayed rotated with triangle_pa pointing to Hue, but most coordinates stays unrotated for logic. float triangle_r = wheel_r_inner - (int)(sv_picker_size * 0.027f); @@ -4520,10 +5849,9 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl float R = col[0], G = col[1], B = col[2]; if (flags & ImGuiColorEditFlags_InputRGB) { - // Hue is lost when converting from greyscale rgb (saturation=0). Restore it. + // Hue is lost when converting from grayscale rgb (saturation=0). Restore it. ColorConvertRGBtoHSV(R, G, B, H, S, V); - if (S == 0 && memcmp(g.ColorEditLastColor, col, sizeof(float) * 3) == 0) - H = g.ColorEditLastHue; + ColorEditRestoreHS(col, &H, &S, &V); } else if (flags & ImGuiColorEditFlags_InputHSV) { @@ -4537,15 +5865,15 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl { // Hue wheel + SV triangle logic InvisibleButton("hsv", ImVec2(sv_picker_size + style.ItemInnerSpacing.x + bars_width, sv_picker_size)); - if (IsItemActive()) + if (IsItemActive() && !is_readonly) { ImVec2 initial_off = g.IO.MouseClickedPos[0] - wheel_center; ImVec2 current_off = g.IO.MousePos - wheel_center; float initial_dist2 = ImLengthSqr(initial_off); - if (initial_dist2 >= (wheel_r_inner-1)*(wheel_r_inner-1) && initial_dist2 <= (wheel_r_outer+1)*(wheel_r_outer+1)) + if (initial_dist2 >= (wheel_r_inner - 1) * (wheel_r_inner - 1) && initial_dist2 <= (wheel_r_outer + 1) * (wheel_r_outer + 1)) { // Interactive with Hue wheel - H = ImAtan2(current_off.y, current_off.x) / IM_PI*0.5f; + H = ImAtan2(current_off.y, current_off.x) / IM_PI * 0.5f; if (H < 0.0f) H += 1.0f; value_changed = value_changed_h = true; @@ -4566,27 +5894,28 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl } } if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); } else if (flags & ImGuiColorEditFlags_PickerHueBar) { // SV rectangle logic InvisibleButton("sv", ImVec2(sv_picker_size, sv_picker_size)); - if (IsItemActive()) + if (IsItemActive() && !is_readonly) { - S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size-1)); - V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + S = ImSaturate((io.MousePos.x - picker_pos.x) / (sv_picker_size - 1)); + V = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); + ColorEditRestoreH(col, &H); // Greatly reduces hue jitter and reset to 0 when hue == 255 and color is rapidly modified using SV square. value_changed = value_changed_sv = true; } if (!(flags & ImGuiColorEditFlags_NoOptions)) - OpenPopupOnItemClick("context"); + OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight); // Hue bar logic SetCursorScreenPos(ImVec2(bar0_pos_x, picker_pos.y)); InvisibleButton("hue", ImVec2(bars_width, sv_picker_size)); - if (IsItemActive()) + if (IsItemActive() && !is_readonly) { - H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + H = ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); value_changed = value_changed_h = true; } } @@ -4598,7 +5927,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl InvisibleButton("alpha", ImVec2(bars_width, sv_picker_size)); if (IsItemActive()) { - col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size-1)); + col[3] = 1.0f - ImSaturate((io.MousePos.y - picker_pos.y) / (sv_picker_size - 1)); value_changed = true; } } @@ -4628,7 +5957,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl if ((flags & ImGuiColorEditFlags_NoLabel)) Text("Current"); - ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip; + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoTooltip; ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)); if (ref_col != NULL) { @@ -4649,9 +5978,11 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl { if (flags & ImGuiColorEditFlags_InputRGB) { - ColorConvertHSVtoRGB(H >= 1.0f ? H - 10 * 1e-6f : H, S > 0.0f ? S : 10*1e-6f, V > 0.0f ? V : 1e-6f, col[0], col[1], col[2]); - g.ColorEditLastHue = H; - memcpy(g.ColorEditLastColor, col, sizeof(float) * 3); + ColorConvertHSVtoRGB(H, S, V, col[0], col[1], col[2]); + g.ColorEditSavedHue = H; + g.ColorEditSavedSat = S; + g.ColorEditSavedID = g.ColorEditCurrentID; + g.ColorEditSavedColor = ColorConvertFloat4ToU32(ImVec4(col[0], col[1], col[2], 0)); } else if (flags & ImGuiColorEditFlags_InputHSV) { @@ -4666,19 +5997,19 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl if ((flags & ImGuiColorEditFlags_NoInputs) == 0) { PushItemWidth((alpha_bar ? bar1_pos_x : bar0_pos_x) + bars_width - picker_pos.x); - ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags__DataTypeMask | ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf; + ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaMask_ | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoSmallPreview; ImGuiColorEditFlags sub_flags = (flags & sub_flags_to_forward) | ImGuiColorEditFlags_NoPicker; - if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags__DisplayMask) == 0) + if (flags & ImGuiColorEditFlags_DisplayRGB || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) if (ColorEdit4("##rgb", col, sub_flags | ImGuiColorEditFlags_DisplayRGB)) { - // FIXME: Hackily differenciating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget. + // FIXME: Hackily differentiating using the DragInt (ActiveId != 0 && !ActiveIdAllowOverlap) vs. using the InputText or DropTarget. // For the later we don't want to run the hue-wrap canceling code. If you are well versed in HSV picker please provide your input! (See #2050) value_changed_fix_hue_wrap = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap); value_changed = true; } - if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags__DisplayMask) == 0) + if (flags & ImGuiColorEditFlags_DisplayHSV || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) value_changed |= ColorEdit4("##hsv", col, sub_flags | ImGuiColorEditFlags_DisplayHSV); - if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags__DisplayMask) == 0) + if (flags & ImGuiColorEditFlags_DisplayHex || (flags & ImGuiColorEditFlags_DisplayMask_) == 0) value_changed |= ColorEdit4("##hex", col, sub_flags | ImGuiColorEditFlags_DisplayHex); PopItemWidth(); } @@ -4705,8 +6036,7 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl G = col[1]; B = col[2]; ColorConvertRGBtoHSV(R, G, B, H, S, V); - if (S == 0 && memcmp(g.ColorEditLastColor, col, sizeof(float) * 3) == 0) // Fix local Hue as display below will use it immediately. - H = g.ColorEditLastHue; + ColorEditRestoreHS(col, &H, &S, &V); // Fix local Hue as display below will use it immediately. } else if (flags & ImGuiColorEditFlags_InputHSV) { @@ -4740,23 +6070,23 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl const float a1 = (n+1.0f)/6.0f * 2.0f * IM_PI + aeps; const int vert_start_idx = draw_list->VtxBuffer.Size; draw_list->PathArcTo(wheel_center, (wheel_r_inner + wheel_r_outer)*0.5f, a0, a1, segment_per_arc); - draw_list->PathStroke(col_white, false, wheel_thickness); + draw_list->PathStroke(col_white, 0, wheel_thickness); const int vert_end_idx = draw_list->VtxBuffer.Size; // Paint colors over existing vertices ImVec2 gradient_p0(wheel_center.x + ImCos(a0) * wheel_r_inner, wheel_center.y + ImSin(a0) * wheel_r_inner); ImVec2 gradient_p1(wheel_center.x + ImCos(a1) * wheel_r_inner, wheel_center.y + ImSin(a1) * wheel_r_inner); - ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n+1]); + ShadeVertsLinearColorGradientKeepAlpha(draw_list, vert_start_idx, vert_end_idx, gradient_p0, gradient_p1, col_hues[n], col_hues[n + 1]); } // Render Cursor + preview on Hue Wheel float cos_hue_angle = ImCos(H * 2.0f * IM_PI); float sin_hue_angle = ImSin(H * 2.0f * IM_PI); - ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner+wheel_r_outer)*0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner+wheel_r_outer)*0.5f); + ImVec2 hue_cursor_pos(wheel_center.x + cos_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f, wheel_center.y + sin_hue_angle * (wheel_r_inner + wheel_r_outer) * 0.5f); float hue_cursor_rad = value_changed_h ? wheel_thickness * 0.65f : wheel_thickness * 0.55f; - int hue_cursor_segments = ImClamp((int)(hue_cursor_rad / 1.4f), 9, 32); + int hue_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(hue_cursor_rad); // Lock segment count so the +1 one matches others. draw_list->AddCircleFilled(hue_cursor_pos, hue_cursor_rad, hue_color32, hue_cursor_segments); - draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad+1, col_midgrey, hue_cursor_segments); + draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad + 1, col_midgrey, hue_cursor_segments); draw_list->AddCircle(hue_cursor_pos, hue_cursor_rad, col_white, hue_cursor_segments); // Render SV triangle (rotated according to hue) @@ -4764,13 +6094,10 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl ImVec2 trb = wheel_center + ImRotate(triangle_pb, cos_hue_angle, sin_hue_angle); ImVec2 trc = wheel_center + ImRotate(triangle_pc, cos_hue_angle, sin_hue_angle); ImVec2 uv_white = GetFontTexUvWhitePixel(); - draw_list->PrimReserve(6, 6); + draw_list->PrimReserve(3, 3); draw_list->PrimVtx(tra, uv_white, hue_color32); - draw_list->PrimVtx(trb, uv_white, hue_color32); + draw_list->PrimVtx(trb, uv_white, col_black); draw_list->PrimVtx(trc, uv_white, col_white); - draw_list->PrimVtx(tra, uv_white, 0); - draw_list->PrimVtx(trb, uv_white, col_white); - draw_list->PrimVtx(trc, uv_white, 0); draw_list->AddTriangle(tra, trb, trc, col_midgrey, 1.5f); sv_cursor_pos = ImLerp(ImLerp(trc, tra, ImSaturate(S)), trb, ImSaturate(1 - V)); } @@ -4780,31 +6107,32 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), col_white, hue_color32, hue_color32, col_white); draw_list->AddRectFilledMultiColor(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0, 0, col_black, col_black); RenderFrameBorder(picker_pos, picker_pos + ImVec2(sv_picker_size, sv_picker_size), 0.0f); - sv_cursor_pos.x = ImClamp((float)(int)(picker_pos.x + ImSaturate(S) * sv_picker_size + 0.5f), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much - sv_cursor_pos.y = ImClamp((float)(int)(picker_pos.y + ImSaturate(1 - V) * sv_picker_size + 0.5f), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); + sv_cursor_pos.x = ImClamp(IM_ROUND(picker_pos.x + ImSaturate(S) * sv_picker_size), picker_pos.x + 2, picker_pos.x + sv_picker_size - 2); // Sneakily prevent the circle to stick out too much + sv_cursor_pos.y = ImClamp(IM_ROUND(picker_pos.y + ImSaturate(1 - V) * sv_picker_size), picker_pos.y + 2, picker_pos.y + sv_picker_size - 2); // Render Hue Bar for (int i = 0; i < 6; ++i) draw_list->AddRectFilledMultiColor(ImVec2(bar0_pos_x, picker_pos.y + i * (sv_picker_size / 6)), ImVec2(bar0_pos_x + bars_width, picker_pos.y + (i + 1) * (sv_picker_size / 6)), col_hues[i], col_hues[i], col_hues[i + 1], col_hues[i + 1]); - float bar0_line_y = (float)(int)(picker_pos.y + H * sv_picker_size + 0.5f); + float bar0_line_y = IM_ROUND(picker_pos.y + H * sv_picker_size); RenderFrameBorder(ImVec2(bar0_pos_x, picker_pos.y), ImVec2(bar0_pos_x + bars_width, picker_pos.y + sv_picker_size), 0.0f); RenderArrowsForVerticalBar(draw_list, ImVec2(bar0_pos_x - 1, bar0_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha); } // Render cursor/preview circle (clamp S/V within 0..1 range because floating points colors may lead HSV values to be out of range) - float sv_cursor_rad = value_changed_sv ? 10.0f : 6.0f; - draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, 12); - draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad+1, col_midgrey, 12); - draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, 12); + float sv_cursor_rad = value_changed_sv ? wheel_thickness * 0.55f : wheel_thickness * 0.40f; + int sv_cursor_segments = draw_list->_CalcCircleAutoSegmentCount(sv_cursor_rad); // Lock segment count so the +1 one matches others. + draw_list->AddCircleFilled(sv_cursor_pos, sv_cursor_rad, user_col32_striped_of_alpha, sv_cursor_segments); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad + 1, col_midgrey, sv_cursor_segments); + draw_list->AddCircle(sv_cursor_pos, sv_cursor_rad, col_white, sv_cursor_segments); // Render alpha bar if (alpha_bar) { float alpha = ImSaturate(col[3]); ImRect bar1_bb(bar1_pos_x, picker_pos.y, bar1_pos_x + bars_width, picker_pos.y + sv_picker_size); - RenderColorRectWithAlphaCheckerboard(bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); + RenderColorRectWithAlphaCheckerboard(draw_list, bar1_bb.Min, bar1_bb.Max, 0, bar1_bb.GetWidth() / 2.0f, ImVec2(0.0f, 0.0f)); draw_list->AddRectFilledMultiColor(bar1_bb.Min, bar1_bb.Max, user_col32_striped_of_alpha, user_col32_striped_of_alpha, user_col32_striped_of_alpha & ~IM_COL32_A_MASK, user_col32_striped_of_alpha & ~IM_COL32_A_MASK); - float bar1_line_y = (float)(int)(picker_pos.y + (1.0f - alpha) * sv_picker_size + 0.5f); + float bar1_line_y = IM_ROUND(picker_pos.y + (1.0f - alpha) * sv_picker_size); RenderFrameBorder(bar1_bb.Min, bar1_bb.Max, 0.0f); RenderArrowsForVerticalBar(draw_list, ImVec2(bar1_pos_x - 1, bar1_line_y), ImVec2(bars_triangles_half_sz + 1, bars_triangles_half_sz), bars_width + 2.0f, style.Alpha); } @@ -4813,19 +6141,21 @@ bool ImGui::ColorPicker4(const char* label, float col[4], ImGuiColorEditFlags fl if (value_changed && memcmp(backup_initial_col, col, components * sizeof(float)) == 0) value_changed = false; - if (value_changed) - MarkItemEdited(window->DC.LastItemId); + if (value_changed && g.LastItemData.ID != 0) // In case of ID collision, the second EndGroup() won't catch g.ActiveId + MarkItemEdited(g.LastItemData.ID); + if (set_current_color_edit_id) + g.ColorEditCurrentID = 0; PopID(); return value_changed; } -// A little colored square. Return true when clicked. +// A little color square. Return true when clicked. // FIXME: May want to display/ignore the alpha component in the color display? Yet show it in the tooltip. // 'desc_id' is not called 'label' because we don't display it next to the button, but only in the tooltip. // Note that 'col' may be encoded in HSV if ImGuiColorEditFlags_InputHSV is set. -bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, ImVec2 size) +bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFlags flags, const ImVec2& size_arg) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -4833,11 +6163,8 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl ImGuiContext& g = *GImGui; const ImGuiID id = window->GetID(desc_id); - float default_size = GetFrameHeight(); - if (size.x == 0.0f) - size.x = default_size; - if (size.y == 0.0f) - size.y = default_size; + const float default_size = GetFrameHeight(); + const ImVec2 size(size_arg.x == 0.0f ? default_size : size_arg.x, size_arg.y == 0.0f ? default_size : size_arg.y); const ImRect bb(window->DC.CursorPos, window->DC.CursorPos + size); ItemSize(bb, (size.y >= default_size) ? g.Style.FramePadding.y : 0.0f); if (!ItemAdd(bb, id)) @@ -4846,8 +6173,8 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held); - if (flags & ImGuiColorEditFlags_NoAlpha) - flags &= ~(ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf); + if (flags & (ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaOpaque)) + flags &= ~(ImGuiColorEditFlags_AlphaNoBg | ImGuiColorEditFlags_AlphaPreviewHalf); ImVec4 col_rgb = col; if (flags & ImGuiColorEditFlags_InputHSV) @@ -4857,28 +6184,38 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl float grid_step = ImMin(size.x, size.y) / 2.99f; float rounding = ImMin(g.Style.FrameRounding, grid_step * 0.5f); ImRect bb_inner = bb; - float off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts. - bb_inner.Expand(off); + float off = 0.0f; + if ((flags & ImGuiColorEditFlags_NoBorder) == 0) + { + off = -0.75f; // The border (using Col_FrameBg) tends to look off when color is near-opaque and rounding is enabled. This offset seemed like a good middle ground to reduce those artifacts. + bb_inner.Expand(off); + } if ((flags & ImGuiColorEditFlags_AlphaPreviewHalf) && col_rgb.w < 1.0f) { - float mid_x = (float)(int)((bb_inner.Min.x + bb_inner.Max.x) * 0.5f + 0.5f); - RenderColorRectWithAlphaCheckerboard(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawCornerFlags_TopRight| ImDrawCornerFlags_BotRight); - window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawCornerFlags_TopLeft|ImDrawCornerFlags_BotLeft); + float mid_x = IM_ROUND((bb_inner.Min.x + bb_inner.Max.x) * 0.5f); + if ((flags & ImGuiColorEditFlags_AlphaNoBg) == 0) + RenderColorRectWithAlphaCheckerboard(window->DrawList, ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), grid_step, ImVec2(-grid_step + off, off), rounding, ImDrawFlags_RoundCornersRight); + else + window->DrawList->AddRectFilled(ImVec2(bb_inner.Min.x + grid_step, bb_inner.Min.y), bb_inner.Max, GetColorU32(col_rgb), rounding, ImDrawFlags_RoundCornersRight); + window->DrawList->AddRectFilled(bb_inner.Min, ImVec2(mid_x, bb_inner.Max.y), GetColorU32(col_rgb_without_alpha), rounding, ImDrawFlags_RoundCornersLeft); } else { // Because GetColorU32() multiplies by the global style Alpha and we don't want to display a checkerboard if the source code had no alpha - ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaPreview) ? col_rgb : col_rgb_without_alpha; - if (col_source.w < 1.0f) - RenderColorRectWithAlphaCheckerboard(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); + ImVec4 col_source = (flags & ImGuiColorEditFlags_AlphaOpaque) ? col_rgb_without_alpha : col_rgb; + if (col_source.w < 1.0f && (flags & ImGuiColorEditFlags_AlphaNoBg) == 0) + RenderColorRectWithAlphaCheckerboard(window->DrawList, bb_inner.Min, bb_inner.Max, GetColorU32(col_source), grid_step, ImVec2(off, off), rounding); else - window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding, ImDrawCornerFlags_All); + window->DrawList->AddRectFilled(bb_inner.Min, bb_inner.Max, GetColorU32(col_source), rounding); + } + RenderNavCursor(bb, id); + if ((flags & ImGuiColorEditFlags_NoBorder) == 0) + { + if (g.Style.FrameBorderSize > 0.0f) + RenderFrameBorder(bb.Min, bb.Max, rounding); + else + window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color buttons are often in need of some sort of border } - RenderNavHighlight(bb, id); - if (g.Style.FrameBorderSize > 0.0f) - RenderFrameBorder(bb.Min, bb.Max, rounding); - else - window->DrawList->AddRect(bb.Min, bb.Max, GetColorU32(ImGuiCol_FrameBg), rounding); // Color button are often in need of some sort of border // Drag and Drop Source // NB: The ActiveId test is merely an optional micro-optimization, BeginDragDropSource() does the same test. @@ -4895,8 +6232,8 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl } // Tooltip - if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered) - ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)); + if (!(flags & ImGuiColorEditFlags_NoTooltip) && hovered && IsItemHovered(ImGuiHoveredFlags_ForTooltip)) + ColorTooltip(desc_id, &col.x, flags & (ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_)); return pressed; } @@ -4905,18 +6242,18 @@ bool ImGui::ColorButton(const char* desc_id, const ImVec4& col, ImGuiColorEditFl void ImGui::SetColorEditOptions(ImGuiColorEditFlags flags) { ImGuiContext& g = *GImGui; - if ((flags & ImGuiColorEditFlags__DisplayMask) == 0) - flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DisplayMask; - if ((flags & ImGuiColorEditFlags__DataTypeMask) == 0) - flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__DataTypeMask; - if ((flags & ImGuiColorEditFlags__PickerMask) == 0) - flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__PickerMask; - if ((flags & ImGuiColorEditFlags__InputMask) == 0) - flags |= ImGuiColorEditFlags__OptionsDefault & ImGuiColorEditFlags__InputMask; - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__DisplayMask)); // Check only 1 option is selected - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__DataTypeMask)); // Check only 1 option is selected - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__PickerMask)); // Check only 1 option is selected - IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags__InputMask)); // Check only 1 option is selected + if ((flags & ImGuiColorEditFlags_DisplayMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DisplayMask_; + if ((flags & ImGuiColorEditFlags_DataTypeMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_DataTypeMask_; + if ((flags & ImGuiColorEditFlags_PickerMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_PickerMask_; + if ((flags & ImGuiColorEditFlags_InputMask_) == 0) + flags |= ImGuiColorEditFlags_DefaultOptions_ & ImGuiColorEditFlags_InputMask_; + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DisplayMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_DataTypeMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_PickerMask_)); // Check only 1 option is selected + IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiColorEditFlags_InputMask_)); // Check only 1 option is selected g.ColorEditOptions = flags; } @@ -4925,7 +6262,8 @@ void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags { ImGuiContext& g = *GImGui; - BeginTooltipEx(0, true); + if (!BeginTooltipEx(ImGuiTooltipFlags_OverridePrevious, ImGuiWindowFlags_None)) + return; const char* text_end = text ? FindRenderedTextEnd(text, NULL) : text; if (text_end > text) { @@ -4936,9 +6274,10 @@ void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags ImVec2 sz(g.FontSize * 3 + g.Style.FramePadding.y * 2, g.FontSize * 3 + g.Style.FramePadding.y * 2); ImVec4 cf(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]); int cr = IM_F32_TO_INT8_SAT(col[0]), cg = IM_F32_TO_INT8_SAT(col[1]), cb = IM_F32_TO_INT8_SAT(col[2]), ca = (flags & ImGuiColorEditFlags_NoAlpha) ? 255 : IM_F32_TO_INT8_SAT(col[3]); - ColorButton("##preview", cf, (flags & (ImGuiColorEditFlags__InputMask | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaPreview | ImGuiColorEditFlags_AlphaPreviewHalf)) | ImGuiColorEditFlags_NoTooltip, sz); + ImGuiColorEditFlags flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_AlphaMask_; + ColorButton("##preview", cf, (flags & flags_to_forward) | ImGuiColorEditFlags_NoTooltip, sz); SameLine(); - if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags__InputMask)) + if ((flags & ImGuiColorEditFlags_InputRGB) || !(flags & ImGuiColorEditFlags_InputMask_)) { if (flags & ImGuiColorEditFlags_NoAlpha) Text("#%02X%02X%02X\nR: %d, G: %d, B: %d\n(%.3f, %.3f, %.3f)", cr, cg, cb, cr, cg, cb, col[0], col[1], col[2]); @@ -4957,28 +6296,30 @@ void ImGui::ColorTooltip(const char* text, const float* col, ImGuiColorEditFlags void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) { - bool allow_opt_inputs = !(flags & ImGuiColorEditFlags__DisplayMask); - bool allow_opt_datatype = !(flags & ImGuiColorEditFlags__DataTypeMask); + bool allow_opt_inputs = !(flags & ImGuiColorEditFlags_DisplayMask_); + bool allow_opt_datatype = !(flags & ImGuiColorEditFlags_DataTypeMask_); if ((!allow_opt_inputs && !allow_opt_datatype) || !BeginPopup("context")) return; + ImGuiContext& g = *GImGui; + PushItemFlag(ImGuiItemFlags_NoMarkEdited, true); ImGuiColorEditFlags opts = g.ColorEditOptions; if (allow_opt_inputs) { - if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags__DisplayMask) | ImGuiColorEditFlags_DisplayRGB; - if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags__DisplayMask) | ImGuiColorEditFlags_DisplayHSV; - if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags__DisplayMask) | ImGuiColorEditFlags_DisplayHex; + if (RadioButton("RGB", (opts & ImGuiColorEditFlags_DisplayRGB) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayRGB; + if (RadioButton("HSV", (opts & ImGuiColorEditFlags_DisplayHSV) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHSV; + if (RadioButton("Hex", (opts & ImGuiColorEditFlags_DisplayHex) != 0)) opts = (opts & ~ImGuiColorEditFlags_DisplayMask_) | ImGuiColorEditFlags_DisplayHex; } if (allow_opt_datatype) { if (allow_opt_inputs) Separator(); - if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Uint8; - if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags__DataTypeMask) | ImGuiColorEditFlags_Float; + if (RadioButton("0..255", (opts & ImGuiColorEditFlags_Uint8) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Uint8; + if (RadioButton("0.00..1.00", (opts & ImGuiColorEditFlags_Float) != 0)) opts = (opts & ~ImGuiColorEditFlags_DataTypeMask_) | ImGuiColorEditFlags_Float; } if (allow_opt_inputs || allow_opt_datatype) Separator(); - if (Button("Copy as..", ImVec2(-1,0))) + if (Button("Copy as..", ImVec2(-1, 0))) OpenPopup("Copy"); if (BeginPopup("Copy")) { @@ -4990,26 +6331,32 @@ void ImGui::ColorEditOptionsPopup(const float* col, ImGuiColorEditFlags flags) ImFormatString(buf, IM_ARRAYSIZE(buf), "(%d,%d,%d,%d)", cr, cg, cb, ca); if (Selectable(buf)) SetClipboardText(buf); - if (flags & ImGuiColorEditFlags_NoAlpha) - ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X", cr, cg, cb); - else - ImFormatString(buf, IM_ARRAYSIZE(buf), "0x%02X%02X%02X%02X", cr, cg, cb, ca); + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X", cr, cg, cb); if (Selectable(buf)) SetClipboardText(buf); + if (!(flags & ImGuiColorEditFlags_NoAlpha)) + { + ImFormatString(buf, IM_ARRAYSIZE(buf), "#%02X%02X%02X%02X", cr, cg, cb, ca); + if (Selectable(buf)) + SetClipboardText(buf); + } EndPopup(); } g.ColorEditOptions = opts; + PopItemFlag(); EndPopup(); } void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags flags) { - bool allow_opt_picker = !(flags & ImGuiColorEditFlags__PickerMask); + bool allow_opt_picker = !(flags & ImGuiColorEditFlags_PickerMask_); bool allow_opt_alpha_bar = !(flags & ImGuiColorEditFlags_NoAlpha) && !(flags & ImGuiColorEditFlags_AlphaBar); if ((!allow_opt_picker && !allow_opt_alpha_bar) || !BeginPopup("context")) return; + ImGuiContext& g = *GImGui; + PushItemFlag(ImGuiItemFlags_NoMarkEdited, true); if (allow_opt_picker) { ImVec2 picker_size(g.FontSize * 8, ImMax(g.FontSize * 8 - (GetFrameHeight() + g.Style.ItemInnerSpacing.x), 1.0f)); // FIXME: Picker size copied from main picker function @@ -5019,16 +6366,16 @@ void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags fl // Draw small/thumbnail version of each picker type (over an invisible button for selection) if (picker_type > 0) Separator(); PushID(picker_type); - ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs|ImGuiColorEditFlags_NoOptions|ImGuiColorEditFlags_NoLabel|ImGuiColorEditFlags_NoSidePreview|(flags & ImGuiColorEditFlags_NoAlpha); + ImGuiColorEditFlags picker_flags = ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoOptions | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview | (flags & ImGuiColorEditFlags_NoAlpha); if (picker_type == 0) picker_flags |= ImGuiColorEditFlags_PickerHueBar; if (picker_type == 1) picker_flags |= ImGuiColorEditFlags_PickerHueWheel; ImVec2 backup_pos = GetCursorScreenPos(); if (Selectable("##selectable", false, 0, picker_size)) // By default, Selectable() is closing popup - g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags__PickerMask) | (picker_flags & ImGuiColorEditFlags__PickerMask); + g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_); SetCursorScreenPos(backup_pos); - ImVec4 dummy_ref_col; - memcpy(&dummy_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4)); - ColorPicker4("##dummypicker", &dummy_ref_col.x, picker_flags); + ImVec4 previewing_ref_col; + memcpy(&previewing_ref_col, ref_col, sizeof(float) * ((picker_flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4)); + ColorPicker4("##previewing_picker", &previewing_ref_col.x, picker_flags); PopID(); } PopItemWidth(); @@ -5036,8 +6383,9 @@ void ImGui::ColorPickerOptionsPopup(const float* ref_col, ImGuiColorEditFlags fl if (allow_opt_alpha_bar) { if (allow_opt_picker) Separator(); - CheckboxFlags("Alpha Bar", (unsigned int*)&g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); + CheckboxFlags("Alpha Bar", &g.ColorEditOptions, ImGuiColorEditFlags_AlphaBar); } + PopItemFlag(); EndPopup(); } @@ -5079,7 +6427,8 @@ bool ImGui::TreeNode(const char* label) ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; - return TreeNodeBehavior(window->GetID(label), 0, label, NULL); + ImGuiID id = window->GetID(label); + return TreeNodeBehavior(id, ImGuiTreeNodeFlags_None, label, NULL); } bool ImGui::TreeNodeV(const char* str_id, const char* fmt, va_list args) @@ -5097,8 +6446,8 @@ bool ImGui::TreeNodeEx(const char* label, ImGuiTreeNodeFlags flags) ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; - - return TreeNodeBehavior(window->GetID(label), flags, label, NULL); + ImGuiID id = window->GetID(label); + return TreeNodeBehavior(id, flags, label, NULL); } bool ImGui::TreeNodeEx(const char* str_id, ImGuiTreeNodeFlags flags, const char* fmt, ...) @@ -5125,9 +6474,10 @@ bool ImGui::TreeNodeExV(const char* str_id, ImGuiTreeNodeFlags flags, const char if (window->SkipItems) return false; - ImGuiContext& g = *GImGui; - const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - return TreeNodeBehavior(window->GetID(str_id), flags, g.TempBuffer, label_end); + ImGuiID id = window->GetID(str_id); + const char* label, *label_end; + ImFormatStringToTempBufferV(&label, &label_end, fmt, args); + return TreeNodeBehavior(id, flags, label, label_end); } bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char* fmt, va_list args) @@ -5136,37 +6486,52 @@ bool ImGui::TreeNodeExV(const void* ptr_id, ImGuiTreeNodeFlags flags, const char if (window->SkipItems) return false; - ImGuiContext& g = *GImGui; - const char* label_end = g.TempBuffer + ImFormatStringV(g.TempBuffer, IM_ARRAYSIZE(g.TempBuffer), fmt, args); - return TreeNodeBehavior(window->GetID(ptr_id), flags, g.TempBuffer, label_end); + ImGuiID id = window->GetID(ptr_id); + const char* label, *label_end; + ImFormatStringToTempBufferV(&label, &label_end, fmt, args); + return TreeNodeBehavior(id, flags, label, label_end); } -bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags) +bool ImGui::TreeNodeGetOpen(ImGuiID storage_id) +{ + ImGuiContext& g = *GImGui; + ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage; + return storage->GetInt(storage_id, 0) != 0; +} + +void ImGui::TreeNodeSetOpen(ImGuiID storage_id, bool open) +{ + ImGuiContext& g = *GImGui; + ImGuiStorage* storage = g.CurrentWindow->DC.StateStorage; + storage->SetInt(storage_id, open ? 1 : 0); +} + +bool ImGui::TreeNodeUpdateNextOpen(ImGuiID storage_id, ImGuiTreeNodeFlags flags) { if (flags & ImGuiTreeNodeFlags_Leaf) return true; - // We only write to the tree storage if the user clicks (or explicitly use the SetNextItemOpen function) + // We only write to the tree storage if the user clicks, or explicitly use the SetNextItemOpen function ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; ImGuiStorage* storage = window->DC.StateStorage; bool is_open; - if (g.NextItemData.Flags & ImGuiNextItemDataFlags_HasOpen) + if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasOpen) { if (g.NextItemData.OpenCond & ImGuiCond_Always) { is_open = g.NextItemData.OpenVal; - storage->SetInt(id, is_open); + TreeNodeSetOpen(storage_id, is_open); } else { // We treat ImGuiCond_Once and ImGuiCond_FirstUseEver the same because tree node state are not saved persistently. - const int stored_value = storage->GetInt(id, -1); + const int stored_value = storage->GetInt(storage_id, -1); if (stored_value == -1) { is_open = g.NextItemData.OpenVal; - storage->SetInt(id, is_open); + TreeNodeSetOpen(storage_id, is_open); } else { @@ -5176,7 +6541,7 @@ bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags) } else { - is_open = storage->GetInt(id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; + is_open = storage->GetInt(storage_id, (flags & ImGuiTreeNodeFlags_DefaultOpen) ? 1 : 0) != 0; } // When logging is enabled, we automatically expand tree nodes (but *NOT* collapsing headers.. seems like sensible behavior). @@ -5187,6 +6552,23 @@ bool ImGui::TreeNodeBehaviorIsOpen(ImGuiID id, ImGuiTreeNodeFlags flags) return is_open; } +// Store ImGuiTreeNodeStackData for just submitted node. +// Currently only supports 32 level deep and we are fine with (1 << Depth) overflowing into a zero, easy to increase. +static void TreeNodeStoreStackData(ImGuiTreeNodeFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + g.TreeNodeStack.resize(g.TreeNodeStack.Size + 1); + ImGuiTreeNodeStackData* tree_node_data = &g.TreeNodeStack.back(); + tree_node_data->ID = g.LastItemData.ID; + tree_node_data->TreeFlags = flags; + tree_node_data->ItemFlags = g.LastItemData.ItemFlags; + tree_node_data->NavRect = g.LastItemData.NavRect; + window->DC.TreeHasStackDataDepthMask |= (1 << window->DC.TreeDepth); +} + +// When using public API, currently 'id == storage_id' is always true, but we separate the values to facilitate advanced user code doing storage queries outside of UI loop. bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* label, const char* label_end) { ImGuiWindow* window = GetCurrentWindow(); @@ -5196,166 +6578,258 @@ bool ImGui::TreeNodeBehavior(ImGuiID id, ImGuiTreeNodeFlags flags, const char* l ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const bool display_frame = (flags & ImGuiTreeNodeFlags_Framed) != 0; - const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, 0.0f); + const ImVec2 padding = (display_frame || (flags & ImGuiTreeNodeFlags_FramePadding)) ? style.FramePadding : ImVec2(style.FramePadding.x, ImMin(window->DC.CurrLineTextBaseOffset, style.FramePadding.y)); if (!label_end) label_end = FindRenderedTextEnd(label); const ImVec2 label_size = CalcTextSize(label, label_end, false); + const float text_offset_x = g.FontSize + (display_frame ? padding.x * 3 : padding.x * 2); // Collapsing arrow width + Spacing + const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it + const float text_width = g.FontSize + label_size.x + padding.x * 2; // Include collapsing arrow + // We vertically grow up to current line height up the typical widget height. - const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y*2), label_size.y + padding.y*2); + const float frame_height = ImMax(ImMin(window->DC.CurrLineSize.y, g.FontSize + style.FramePadding.y * 2), label_size.y + padding.y * 2); + const bool span_all_columns = (flags & ImGuiTreeNodeFlags_SpanAllColumns) != 0 && (g.CurrentTable != NULL); + const bool span_all_columns_label = (flags & ImGuiTreeNodeFlags_LabelSpanAllColumns) != 0 && (g.CurrentTable != NULL); ImRect frame_bb; - frame_bb.Min.x = (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; + frame_bb.Min.x = span_all_columns ? window->ParentWorkRect.Min.x : (flags & ImGuiTreeNodeFlags_SpanFullWidth) ? window->WorkRect.Min.x : window->DC.CursorPos.x; frame_bb.Min.y = window->DC.CursorPos.y; - frame_bb.Max.x = window->WorkRect.Max.x; + frame_bb.Max.x = span_all_columns ? window->ParentWorkRect.Max.x : (flags & ImGuiTreeNodeFlags_SpanLabelWidth) ? window->DC.CursorPos.x + text_width + padding.x : window->WorkRect.Max.x; frame_bb.Max.y = window->DC.CursorPos.y + frame_height; if (display_frame) { - // Framed header expand a little outside the default padding, to the edge of InnerClipRect - // (FIXME: May remove this at some point and make InnerClipRect align with WindowPadding.x instead of WindowPadding.x*0.5f) - frame_bb.Min.x -= (float)(int)(window->WindowPadding.x * 0.5f - 1.0f); - frame_bb.Max.x += (float)(int)(window->WindowPadding.x * 0.5f); + const float outer_extend = IM_TRUNC(window->WindowPadding.x * 0.5f); // Framed header expand a little outside of current limits + frame_bb.Min.x -= outer_extend; + frame_bb.Max.x += outer_extend; } - const float text_offset_x = g.FontSize + (display_frame ? padding.x*3 : padding.x*2); // Collapser arrow width + Spacing - const float text_offset_y = ImMax(padding.y, window->DC.CurrLineTextBaseOffset); // Latch before ItemSize changes it - const float text_width = g.FontSize + (label_size.x > 0.0f ? label_size.x + padding.x*2 : 0.0f); // Include collapser ImVec2 text_pos(window->DC.CursorPos.x + text_offset_x, window->DC.CursorPos.y + text_offset_y); - ItemSize(ImVec2(text_width, frame_height), text_offset_y); + ItemSize(ImVec2(text_width, frame_height), padding.y); // For regular tree nodes, we arbitrary allow to click past 2 worth of ItemSpacing ImRect interact_bb = frame_bb; - if (!display_frame && (flags & (ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth)) == 0) - interact_bb.Max.x = frame_bb.Min.x + text_width + style.ItemSpacing.x * 2.0f; - - // Store a flag for the current depth to tell if we will allow closing this node when navigating one of its child. - // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). - // This is currently only support 32 level deep and we are fine with (1 << Depth) overflowing into a zero. - const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0; - bool is_open = TreeNodeBehaviorIsOpen(id, flags); - if (is_open && !g.NavIdIsAlive && (flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) - window->DC.TreeMayJumpToParentOnPopMask |= (1 << window->DC.TreeDepth); + if ((flags & (ImGuiTreeNodeFlags_Framed | ImGuiTreeNodeFlags_SpanAvailWidth | ImGuiTreeNodeFlags_SpanFullWidth | ImGuiTreeNodeFlags_SpanLabelWidth | ImGuiTreeNodeFlags_SpanAllColumns)) == 0) + interact_bb.Max.x = frame_bb.Min.x + text_width + (label_size.x > 0.0f ? style.ItemSpacing.x * 2.0f : 0.0f); - bool item_add = ItemAdd(interact_bb, id); - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; - window->DC.LastItemDisplayRect = frame_bb; + // Compute open and multi-select states before ItemAdd() as it clear NextItem data. + ImGuiID storage_id = (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasStorageID) ? g.NextItemData.StorageId : id; + bool is_open = TreeNodeUpdateNextOpen(storage_id, flags); - if (!item_add) + bool is_visible; + if (span_all_columns || span_all_columns_label) { + // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. + const float backup_clip_rect_min_x = window->ClipRect.Min.x; + const float backup_clip_rect_max_x = window->ClipRect.Max.x; + window->ClipRect.Min.x = window->ParentWorkRect.Min.x; + window->ClipRect.Max.x = window->ParentWorkRect.Max.x; + is_visible = ItemAdd(interact_bb, id); + window->ClipRect.Min.x = backup_clip_rect_min_x; + window->ClipRect.Max.x = backup_clip_rect_max_x; + } + else + { + is_visible = ItemAdd(interact_bb, id); + } + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDisplayRect; + g.LastItemData.DisplayRect = frame_bb; + + // If a NavLeft request is happening and ImGuiTreeNodeFlags_NavLeftJumpsBackHere enabled: + // Store data for the current depth to allow returning to this node from any child item. + // For this purpose we essentially compare if g.NavIdIsAlive went from 0 to 1 between TreeNode() and TreePop(). + // It will become tempting to enable ImGuiTreeNodeFlags_NavLeftJumpsBackHere by default or move it to ImGuiStyle. + bool store_tree_node_stack_data = false; + if (!(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) + { + if ((flags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) && is_open && !g.NavIdIsAlive) + if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) + store_tree_node_stack_data = true; + } + + const bool is_leaf = (flags & ImGuiTreeNodeFlags_Leaf) != 0; + if (!is_visible) + { + if (store_tree_node_stack_data && is_open) + TreeNodeStoreStackData(flags); // Call before TreePushOverrideID() if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) TreePushOverrideID(id); - IMGUI_TEST_ENGINE_ITEM_INFO(window->DC.LastItemId, label, window->DC.ItemFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); return is_open; } - // Flags that affects opening behavior: - // - 0 (default) .................... single-click anywhere to open - // - OpenOnDoubleClick .............. double-click anywhere to open - // - OpenOnArrow .................... single-click on arrow to open - // - OpenOnDoubleClick|OpenOnArrow .. single-click on arrow or double-click anywhere to open - ImGuiButtonFlags button_flags = ImGuiButtonFlags_NoKeyModifiers; - if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) - button_flags |= ImGuiButtonFlags_AllowItemOverlap; - if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) - button_flags |= ImGuiButtonFlags_PressedOnDoubleClick | ((flags & ImGuiTreeNodeFlags_OpenOnArrow) ? ImGuiButtonFlags_PressedOnClickRelease : 0); + if (span_all_columns || span_all_columns_label) + { + TablePushBackgroundChannel(); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; + g.LastItemData.ClipRect = window->ClipRect; + } + + ImGuiButtonFlags button_flags = ImGuiTreeNodeFlags_None; + if ((flags & ImGuiTreeNodeFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) + button_flags |= ImGuiButtonFlags_AllowOverlap; if (!is_leaf) button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; + // We allow clicking on the arrow section with keyboard modifiers held, in order to easily + // allow browsing a tree while preserving selection with code implementing multi-selection patterns. + // When clicking on the rest of the tree node we always disallow keyboard modifiers. + const float arrow_hit_x1 = (text_pos.x - text_offset_x) - style.TouchExtraPadding.x; + const float arrow_hit_x2 = (text_pos.x - text_offset_x) + (g.FontSize + padding.x * 2.0f) + style.TouchExtraPadding.x; + const bool is_mouse_x_over_arrow = (g.IO.MousePos.x >= arrow_hit_x1 && g.IO.MousePos.x < arrow_hit_x2); + + const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (is_multi_select) // We absolutely need to distinguish open vs select so _OpenOnArrow comes by default + flags |= (flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 ? ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick : ImGuiTreeNodeFlags_OpenOnArrow; + + // Open behaviors can be altered with the _OpenOnArrow and _OnOnDoubleClick flags. + // Some alteration have subtle effects (e.g. toggle on MouseUp vs MouseDown events) due to requirements for multi-selection and drag and drop support. + // - Single-click on label = Toggle on MouseUp (default, when _OpenOnArrow=0) + // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=0) + // - Single-click on arrow = Toggle on MouseDown (when _OpenOnArrow=1) + // - Double-click on label = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1) + // - Double-click on arrow = Toggle on MouseDoubleClick (when _OpenOnDoubleClick=1 and _OpenOnArrow=0) + // It is rather standard that arrow click react on Down rather than Up. + // We set ImGuiButtonFlags_PressedOnClickRelease on OpenOnDoubleClick because we want the item to be active on the initial MouseDown in order for drag and drop to work. + if (is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_PressedOnClick; + else if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) + button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + bool selected = (flags & ImGuiTreeNodeFlags_Selected) != 0; const bool was_selected = selected; + // Multi-selection support (header) + if (is_multi_select) + { + // Handle multi-select + alter button flags for it + MultiSelectItemHeader(id, &selected, &button_flags); + if (is_mouse_x_over_arrow) + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; + } + else + { + if (window != g.HoveredWindow || !is_mouse_x_over_arrow) + button_flags |= ImGuiButtonFlags_NoKeyModsAllowed; + } + bool hovered, held; bool pressed = ButtonBehavior(interact_bb, id, &hovered, &held, button_flags); bool toggled = false; if (!is_leaf) { - if (pressed) + if (pressed && g.DragDropHoldJustPressedId != id) { - toggled = !(flags & (ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) || (g.NavActivateId == id); + if ((flags & ImGuiTreeNodeFlags_OpenOnMask_) == 0 || (g.NavActivateId == id && !is_multi_select)) + toggled = true; // Single click if (flags & ImGuiTreeNodeFlags_OpenOnArrow) - toggled |= IsMouseHoveringRect(interact_bb.Min, ImVec2(interact_bb.Min.x + text_offset_x, interact_bb.Max.y)) && (!g.NavDisableMouseHover); - if (flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) - toggled |= g.IO.MouseDoubleClicked[0]; - if (g.DragDropActive && is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. - toggled = false; + toggled |= is_mouse_x_over_arrow && !g.NavHighlightItemUnderNav; // Lightweight equivalent of IsMouseHoveringRect() since ButtonBehavior() already did the job + if ((flags & ImGuiTreeNodeFlags_OpenOnDoubleClick) && g.IO.MouseClickedCount[0] == 2) + toggled = true; // Double click + } + else if (pressed && g.DragDropHoldJustPressedId == id) + { + IM_ASSERT(button_flags & ImGuiButtonFlags_PressedOnDragDropHold); + if (!is_open) // When using Drag and Drop "hold to open" we keep the node highlighted after opening, but never close it again. + toggled = true; + else + pressed = false; // Cancel press so it doesn't trigger selection. } - if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Left && is_open) + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Left && is_open) { toggled = true; + NavClearPreferredPosForAxis(ImGuiAxis_X); NavMoveRequestCancel(); } - if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right && !is_open) // If there's something upcoming on the line we may want to give it the priority? { toggled = true; + NavClearPreferredPosForAxis(ImGuiAxis_X); NavMoveRequestCancel(); } if (toggled) { is_open = !is_open; - window->DC.StateStorage->SetInt(id, is_open); + window->DC.StateStorage->SetInt(storage_id, is_open); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledOpen; } } - if (flags & ImGuiTreeNodeFlags_AllowItemOverlap) - SetItemAllowOverlap(); - // In this branch, TreeNodeBehavior() cannot toggle the selection so this will never trigger. - if (selected != was_selected) //-V547 - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + // Multi-selection support (footer) + if (is_multi_select) + { + bool pressed_copy = pressed && !toggled; + MultiSelectItemFooter(id, &selected, &pressed_copy); + if (pressed) + SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, interact_bb); + } + + if (selected != was_selected) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render - const ImU32 text_col = GetColorU32(ImGuiCol_Text); - ImGuiNavHighlightFlags nav_highlight_flags = ImGuiNavHighlightFlags_TypeThin; - if (display_frame) { - // Framed type - const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); - RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding); - RenderNavHighlight(frame_bb, id, nav_highlight_flags); - if (flags & ImGuiTreeNodeFlags_Bullet) - RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col); - else if (!is_leaf) - RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 1.0f); - else // Leaf without bullet, left-adjusted text - text_pos.x -= text_offset_x; - if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton) - frame_bb.Max.x -= g.FontSize + style.FramePadding.x; - if (g.LogEnabled) + const ImU32 text_col = GetColorU32(ImGuiCol_Text); + ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact; + if (is_multi_select) + nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle + if (display_frame) { - // NB: '##' is normally used to hide text (as a library-wide feature), so we need to specify the text range to make sure the ## aren't stripped out here. - const char log_prefix[] = "\n##"; - const char log_suffix[] = "##"; - LogRenderedText(&text_pos, log_prefix, log_prefix+3); - RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); - LogRenderedText(&text_pos, log_suffix, log_suffix+2); + // Framed type + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, true, style.FrameRounding); + RenderNavCursor(frame_bb, id, nav_render_cursor_flags); + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.60f, text_pos.y + g.FontSize * 0.5f), text_col); + else if (!is_leaf) + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 1.0f); + else // Leaf without bullet, left-adjusted text + text_pos.x -= text_offset_x - padding.x; + if (flags & ImGuiTreeNodeFlags_ClipLabelForTrailingButton) + frame_bb.Max.x -= g.FontSize + style.FramePadding.x; + if (g.LogEnabled) + LogSetNextTextDecoration("###", "###"); } else { + // Unframed typed for tree nodes + if (hovered || selected) + { + const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false); + } + RenderNavCursor(frame_bb, id, nav_render_cursor_flags); + if (flags & ImGuiTreeNodeFlags_Bullet) + RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col); + else if (!is_leaf) + RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ((flags & ImGuiTreeNodeFlags_UpsideDownArrow) ? ImGuiDir_Up : ImGuiDir_Down) : ImGuiDir_Right, 0.70f); + if (g.LogEnabled) + LogSetNextTextDecoration(">", NULL); + } + + if (span_all_columns && !span_all_columns_label) + TablePopBackgroundChannel(); + + // Label + if (display_frame) RenderTextClipped(text_pos, frame_bb.Max, label, label_end, &label_size); - } - } - else - { - // Unframed typed for tree nodes - if (hovered || selected) - { - const ImU32 bg_col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); - RenderFrame(frame_bb.Min, frame_bb.Max, bg_col, false); - RenderNavHighlight(frame_bb, id, nav_highlight_flags); - } - if (flags & ImGuiTreeNodeFlags_Bullet) - RenderBullet(window->DrawList, ImVec2(text_pos.x - text_offset_x * 0.5f, text_pos.y + g.FontSize * 0.5f), text_col); - else if (!is_leaf) - RenderArrow(window->DrawList, ImVec2(text_pos.x - text_offset_x + padding.x, text_pos.y + g.FontSize * 0.15f), text_col, is_open ? ImGuiDir_Down : ImGuiDir_Right, 0.70f); - if (g.LogEnabled) - LogRenderedText(&text_pos, ">"); - RenderText(text_pos, label, label_end, false); + else + RenderText(text_pos, label, label_end, false); + + if (span_all_columns_label) + TablePopBackgroundChannel(); } + if (store_tree_node_stack_data && is_open) + TreeNodeStoreStackData(flags); // Call before TreePushOverrideID() if (is_open && !(flags & ImGuiTreeNodeFlags_NoTreePushOnOpen)) - TreePushOverrideID(id); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); + TreePushOverrideID(id); // Could use TreePush(label) but this avoid computing twice + + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | (is_leaf ? 0 : ImGuiItemStatusFlags_Openable) | (is_open ? ImGuiItemStatusFlags_Opened : 0)); return is_open; } @@ -5364,7 +6838,7 @@ void ImGui::TreePush(const char* str_id) ImGuiWindow* window = GetCurrentWindow(); Indent(); window->DC.TreeDepth++; - PushID(str_id ? str_id : "#TreePush"); + PushID(str_id); } void ImGui::TreePush(const void* ptr_id) @@ -5372,15 +6846,16 @@ void ImGui::TreePush(const void* ptr_id) ImGuiWindow* window = GetCurrentWindow(); Indent(); window->DC.TreeDepth++; - PushID(ptr_id ? ptr_id : (const void*)"#TreePush"); + PushID(ptr_id); } void ImGui::TreePushOverrideID(ImGuiID id) { - ImGuiWindow* window = GetCurrentWindow(); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; Indent(); window->DC.TreeDepth++; - window->IDStack.push_back(id); + PushOverrideID(id); } void ImGui::TreePop() @@ -5392,14 +6867,19 @@ void ImGui::TreePop() window->DC.TreeDepth--; ImU32 tree_depth_mask = (1 << window->DC.TreeDepth); - // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled) - if (g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) - if (g.NavIdIsAlive && (window->DC.TreeMayJumpToParentOnPopMask & tree_depth_mask)) + if (window->DC.TreeHasStackDataDepthMask & tree_depth_mask) // Only set during request + { + ImGuiTreeNodeStackData* data = &g.TreeNodeStack.back(); + IM_ASSERT(data->ID == window->IDStack.back()); + if (data->TreeFlags & ImGuiTreeNodeFlags_NavLeftJumpsBackHere) { - SetNavID(window->IDStack.back(), g.NavLayer); - NavMoveRequestCancel(); + // Handle Left arrow to move to parent tree node (when ImGuiTreeNodeFlags_NavLeftJumpsBackHere is enabled) + if (g.NavIdIsAlive && g.NavMoveDir == ImGuiDir_Left && g.NavWindow == window && NavMoveRequestButNoResultYet()) + NavMoveRequestResolveWithPastTreeNode(&g.NavMoveResultLocal, data); } - window->DC.TreeMayJumpToParentOnPopMask &= tree_depth_mask - 1; + g.TreeNodeStack.pop_back(); + window->DC.TreeHasStackDataDepthMask &= ~tree_depth_mask; + } IM_ASSERT(window->IDStack.Size > 1); // There should always be 1 element in the IDStack (pushed during window creation). If this triggers you called TreePop/PopID too much. PopID(); @@ -5418,9 +6898,19 @@ void ImGui::SetNextItemOpen(bool is_open, ImGuiCond cond) ImGuiContext& g = *GImGui; if (g.CurrentWindow->SkipItems) return; - g.NextItemData.Flags |= ImGuiNextItemDataFlags_HasOpen; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasOpen; g.NextItemData.OpenVal = is_open; - g.NextItemData.OpenCond = cond ? cond : ImGuiCond_Always; + g.NextItemData.OpenCond = (ImU8)(cond ? cond : ImGuiCond_Always); +} + +// Set next TreeNode/CollapsingHeader storage id. +void ImGui::SetNextItemStorageID(ImGuiID storage_id) +{ + ImGuiContext& g = *GImGui; + if (g.CurrentWindow->SkipItems) + return; + g.NextItemData.HasFlags |= ImGuiNextItemDataFlags_HasStorageID; + g.NextItemData.StorageId = storage_id; } // CollapsingHeader returns true when opened but do not indent nor push into the ID stack (because of the ImGuiTreeNodeFlags_NoTreePushOnOpen flag). @@ -5430,35 +6920,42 @@ bool ImGui::CollapsingHeader(const char* label, ImGuiTreeNodeFlags flags) ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; - - return TreeNodeBehavior(window->GetID(label), flags | ImGuiTreeNodeFlags_CollapsingHeader, label); + ImGuiID id = window->GetID(label); + return TreeNodeBehavior(id, flags | ImGuiTreeNodeFlags_CollapsingHeader, label); } -bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags flags) +// p_visible == NULL : regular collapsing header +// p_visible != NULL && *p_visible == true : show a small close button on the corner of the header, clicking the button will set *p_visible = false +// p_visible != NULL && *p_visible == false : do not show the header at all +// Do not mistake this with the Open state of the header itself, which you can adjust with SetNextItemOpen() or ImGuiTreeNodeFlags_DefaultOpen. +bool ImGui::CollapsingHeader(const char* label, bool* p_visible, ImGuiTreeNodeFlags flags) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; - if (p_open && !*p_open) + if (p_visible && !*p_visible) return false; ImGuiID id = window->GetID(label); - flags |= ImGuiTreeNodeFlags_CollapsingHeader | (p_open ? ImGuiTreeNodeFlags_AllowItemOverlap | ImGuiTreeNodeFlags_ClipLabelForTrailingButton : 0); + flags |= ImGuiTreeNodeFlags_CollapsingHeader; + if (p_visible) + flags |= ImGuiTreeNodeFlags_AllowOverlap | (ImGuiTreeNodeFlags)ImGuiTreeNodeFlags_ClipLabelForTrailingButton; bool is_open = TreeNodeBehavior(id, flags, label); - if (p_open) + if (p_visible != NULL) { // Create a small overlapping close button // FIXME: We can evolve this into user accessible helpers to add extra buttons on title bars, headers, etc. // FIXME: CloseButton can overlap into text, need find a way to clip the text somehow. ImGuiContext& g = *GImGui; - ImGuiItemHoveredDataBackup last_item_backup; + ImGuiLastItemData last_item_backup = g.LastItemData; float button_size = g.FontSize; - float button_x = ImMax(window->DC.LastItemRect.Min.x, window->DC.LastItemRect.Max.x - g.Style.FramePadding.x * 2.0f - button_size); - float button_y = window->DC.LastItemRect.Min.y; - if (CloseButton(window->GetID((void*)((intptr_t)id + 1)), ImVec2(button_x, button_y))) - *p_open = false; - last_item_backup.Restore(); + float button_x = ImMax(g.LastItemData.Rect.Min.x, g.LastItemData.Rect.Max.x - g.Style.FramePadding.x - button_size); + float button_y = g.LastItemData.Rect.Min.y + g.Style.FramePadding.y; + ImGuiID close_button_id = GetIDWithSeed("#CLOSE", NULL, id); + if (CloseButton(close_button_id, ImVec2(button_x, button_y))) + *p_visible = false; + g.LastItemData = last_item_backup; } return is_open; @@ -5470,8 +6967,10 @@ bool ImGui::CollapsingHeader(const char* label, bool* p_open, ImGuiTreeNodeFlags // - Selectable() //------------------------------------------------------------------------- -// Tip: pass a non-visible label (e.g. "##dummy") then you can use the space to draw other text or image. +// Tip: pass a non-visible label (e.g. "##hello") then you can use the space to draw other text or image. // But you need to make sure the ID is unique, e.g. enclose calls in PushID/PopID or use ##unique_id. +// With this scheme, ImGuiSelectableFlags_SpanAllColumns and ImGuiSelectableFlags_AllowOverlap are also frequently used flags. +// FIXME: Selectable() with (size.x == 0.0f) and (SelectableTextAlign.x > 0.0f) followed by SameLine() is currently not supported. bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) { ImGuiWindow* window = GetCurrentWindow(); @@ -5481,116 +6980,176 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.CurrentColumns) // FIXME-OPT: Avoid if vertically clipped. - PushColumnsBackground(); - + // Submit label or explicit size to ItemSize(), whereas ItemAdd() will submit a larger/spanning rectangle. ImGuiID id = window->GetID(label); ImVec2 label_size = CalcTextSize(label, NULL, true); ImVec2 size(size_arg.x != 0.0f ? size_arg.x : label_size.x, size_arg.y != 0.0f ? size_arg.y : label_size.y); ImVec2 pos = window->DC.CursorPos; pos.y += window->DC.CurrLineTextBaseOffset; - ImRect bb_inner(pos, pos + size); - ItemSize(size); + ItemSize(size, 0.0f); - // Fill horizontal space. - ImVec2 window_padding = window->WindowPadding; - float max_x = (flags & ImGuiSelectableFlags_SpanAllColumns) ? GetWindowContentRegionMax().x : GetContentRegionMax().x; - float w_draw = ImMax(label_size.x, window->Pos.x + max_x - window_padding.x - pos.x); - ImVec2 size_draw((size_arg.x != 0 && !(flags & ImGuiSelectableFlags_DrawFillAvailWidth)) ? size_arg.x : w_draw, size_arg.y != 0.0f ? size_arg.y : size.y); - ImRect bb(pos, pos + size_draw); - if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_DrawFillAvailWidth)) - bb.Max.x += window_padding.x; + // Fill horizontal space + // We don't support (size < 0.0f) in Selectable() because the ItemSpacing extension would make explicitly right-aligned sizes not visibly match other widgets. + const bool span_all_columns = (flags & ImGuiSelectableFlags_SpanAllColumns) != 0; + const float min_x = span_all_columns ? window->ParentWorkRect.Min.x : pos.x; + const float max_x = span_all_columns ? window->ParentWorkRect.Max.x : window->WorkRect.Max.x; + if (size_arg.x == 0.0f || (flags & ImGuiSelectableFlags_SpanAvailWidth)) + size.x = ImMax(label_size.x, max_x - min_x); - // Selectables are tightly packed together so we extend the box to cover spacing between selectable. - const float spacing_x = style.ItemSpacing.x; - const float spacing_y = style.ItemSpacing.y; - const float spacing_L = (float)(int)(spacing_x * 0.50f); - const float spacing_U = (float)(int)(spacing_y * 0.50f); - bb.Min.x -= spacing_L; - bb.Min.y -= spacing_U; - bb.Max.x += (spacing_x - spacing_L); - bb.Max.y += (spacing_y - spacing_U); - - bool item_add; - if (flags & ImGuiSelectableFlags_Disabled) + // Selectables are meant to be tightly packed together with no click-gap, so we extend their box to cover spacing between selectable. + // FIXME: Not part of layout so not included in clipper calculation, but ItemSize currently doesn't allow offsetting CursorPos. + ImRect bb(min_x, pos.y, min_x + size.x, pos.y + size.y); + if ((flags & ImGuiSelectableFlags_NoPadWithHalfSpacing) == 0) { - ImGuiItemFlags backup_item_flags = window->DC.ItemFlags; - window->DC.ItemFlags |= ImGuiItemFlags_Disabled | ImGuiItemFlags_NoNavDefaultFocus; - item_add = ItemAdd(bb, id); - window->DC.ItemFlags = backup_item_flags; + const float spacing_x = span_all_columns ? 0.0f : style.ItemSpacing.x; + const float spacing_y = style.ItemSpacing.y; + const float spacing_L = IM_TRUNC(spacing_x * 0.50f); + const float spacing_U = IM_TRUNC(spacing_y * 0.50f); + bb.Min.x -= spacing_L; + bb.Min.y -= spacing_U; + bb.Max.x += (spacing_x - spacing_L); + bb.Max.y += (spacing_y - spacing_U); + } + //if (g.IO.KeyCtrl) { GetForegroundDrawList()->AddRect(bb.Min, bb.Max, IM_COL32(0, 255, 0, 255)); } + + const bool disabled_item = (flags & ImGuiSelectableFlags_Disabled) != 0; + const ImGuiItemFlags extra_item_flags = disabled_item ? (ImGuiItemFlags)ImGuiItemFlags_Disabled : ImGuiItemFlags_None; + bool is_visible; + if (span_all_columns) + { + // Modify ClipRect for the ItemAdd(), faster than doing a PushColumnsBackground/PushTableBackgroundChannel for every Selectable.. + const float backup_clip_rect_min_x = window->ClipRect.Min.x; + const float backup_clip_rect_max_x = window->ClipRect.Max.x; + window->ClipRect.Min.x = window->ParentWorkRect.Min.x; + window->ClipRect.Max.x = window->ParentWorkRect.Max.x; + is_visible = ItemAdd(bb, id, NULL, extra_item_flags); + window->ClipRect.Min.x = backup_clip_rect_min_x; + window->ClipRect.Max.x = backup_clip_rect_max_x; } else { - item_add = ItemAdd(bb, id); + is_visible = ItemAdd(bb, id, NULL, extra_item_flags); } - if (!item_add) + + const bool is_multi_select = (g.LastItemData.ItemFlags & ImGuiItemFlags_IsMultiSelect) != 0; + if (!is_visible) + if (!is_multi_select || !g.BoxSelectState.UnclipMode || !g.BoxSelectState.UnclipRect.Overlaps(bb)) // Extra layer of "no logic clip" for box-select support (would be more overhead to add to ItemAdd) + return false; + + const bool disabled_global = (g.CurrentItemFlags & ImGuiItemFlags_Disabled) != 0; + if (disabled_item && !disabled_global) // Only testing this as an optimization + BeginDisabled(); + + // FIXME: We can standardize the behavior of those two, we could also keep the fast path of override ClipRect + full push on render only, + // which would be advantageous since most selectable are not selected. + if (span_all_columns) { - if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.CurrentColumns) - PopColumnsBackground(); - return false; + if (g.CurrentTable) + TablePushBackgroundChannel(); + else if (window->DC.CurrentColumns) + PushColumnsBackground(); + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasClipRect; + g.LastItemData.ClipRect = window->ClipRect; } // We use NoHoldingActiveID on menus so user can click and _hold_ on a menu then drag to browse child entries ImGuiButtonFlags button_flags = 0; - if (flags & ImGuiSelectableFlags_NoHoldingActiveID) button_flags |= ImGuiButtonFlags_NoHoldingActiveID; - if (flags & ImGuiSelectableFlags_PressedOnClick) button_flags |= ImGuiButtonFlags_PressedOnClick; - if (flags & ImGuiSelectableFlags_PressedOnRelease) button_flags |= ImGuiButtonFlags_PressedOnRelease; - if (flags & ImGuiSelectableFlags_Disabled) button_flags |= ImGuiButtonFlags_Disabled; - if (flags & ImGuiSelectableFlags_AllowDoubleClick) button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; - if (flags & ImGuiSelectableFlags_AllowItemOverlap) button_flags |= ImGuiButtonFlags_AllowItemOverlap; - - if (flags & ImGuiSelectableFlags_Disabled) - selected = false; + if (flags & ImGuiSelectableFlags_NoHoldingActiveID) { button_flags |= ImGuiButtonFlags_NoHoldingActiveId; } + if (flags & ImGuiSelectableFlags_NoSetKeyOwner) { button_flags |= ImGuiButtonFlags_NoSetKeyOwner; } + if (flags & ImGuiSelectableFlags_SelectOnClick) { button_flags |= ImGuiButtonFlags_PressedOnClick; } + if (flags & ImGuiSelectableFlags_SelectOnRelease) { button_flags |= ImGuiButtonFlags_PressedOnRelease; } + if (flags & ImGuiSelectableFlags_AllowDoubleClick) { button_flags |= ImGuiButtonFlags_PressedOnClickRelease | ImGuiButtonFlags_PressedOnDoubleClick; } + if ((flags & ImGuiSelectableFlags_AllowOverlap) || (g.LastItemData.ItemFlags & ImGuiItemFlags_AllowOverlap)) { button_flags |= ImGuiButtonFlags_AllowOverlap; } + // Multi-selection support (header) const bool was_selected = selected; + if (is_multi_select) + { + // Handle multi-select + alter button flags for it + MultiSelectItemHeader(id, &selected, &button_flags); + } + bool hovered, held; bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); - // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with gamepad/keyboard + // Multi-selection support (footer) + if (is_multi_select) + { + MultiSelectItemFooter(id, &selected, &pressed); + } + else + { + // Auto-select when moved into + // - This will be more fully fleshed in the range-select branch + // - This is not exposed as it won't nicely work with some user side handling of shift/control + // - We cannot do 'if (g.NavJustMovedToId != id) { selected = false; pressed = was_selected; }' for two reasons + // - (1) it would require focus scope to be set, need exposing PushFocusScope() or equivalent (e.g. BeginSelection() calling PushFocusScope()) + // - (2) usage will fail with clipped items + // The multi-select API aim to fix those issues, e.g. may be replaced with a BeginSelection() API. + if ((flags & ImGuiSelectableFlags_SelectOnNav) && g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == g.CurrentFocusScopeId) + if (g.NavJustMovedToId == id) + selected = pressed = true; + } + + // Update NavId when clicking or when Hovering (this doesn't happen on most widgets), so navigation can be resumed with keyboard/gamepad if (pressed || (hovered && (flags & ImGuiSelectableFlags_SetNavIdOnHover))) { - if (!g.NavDisableMouseHover && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) + if (!g.NavHighlightItemUnderNav && g.NavWindow == window && g.NavLayer == window->DC.NavLayerCurrent) { - g.NavDisableHighlight = true; - SetNavID(id, window->DC.NavLayerCurrent); + SetNavID(id, window->DC.NavLayerCurrent, g.CurrentFocusScopeId, WindowRectAbsToRel(window, bb)); // (bb == NavRect) + if (g.IO.ConfigNavCursorVisibleAuto) + g.NavCursorVisible = false; } } if (pressed) MarkItemEdited(id); - if (flags & ImGuiSelectableFlags_AllowItemOverlap) - SetItemAllowOverlap(); - - // In this branch, Selectable() cannot toggle the selection so this will never trigger. - if (selected != was_selected) //-V547 - window->DC.LastItemStatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + if (selected != was_selected) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; // Render - if (held && (flags & ImGuiSelectableFlags_DrawHoveredWhenHeld)) - hovered = true; - if (hovered || selected) + if (is_visible) { - const ImU32 col = GetColorU32((held && hovered) ? ImGuiCol_HeaderActive : hovered ? ImGuiCol_HeaderHovered : ImGuiCol_Header); - RenderFrame(bb.Min, bb.Max, col, false, 0.0f); - RenderNavHighlight(bb, id, ImGuiNavHighlightFlags_TypeThin | ImGuiNavHighlightFlags_NoRounding); + const bool highlighted = hovered || (flags & ImGuiSelectableFlags_Highlight); + if (highlighted || selected) + { + // Between 1.91.0 and 1.91.4 we made selected Selectable use an arbitrary lerp between _Header and _HeaderHovered. Removed that now. (#8106) + ImU32 col = GetColorU32((held && highlighted) ? ImGuiCol_HeaderActive : highlighted ? ImGuiCol_HeaderHovered : ImGuiCol_Header); + RenderFrame(bb.Min, bb.Max, col, false, 0.0f); + } + if (g.NavId == id) + { + ImGuiNavRenderCursorFlags nav_render_cursor_flags = ImGuiNavRenderCursorFlags_Compact | ImGuiNavRenderCursorFlags_NoRounding; + if (is_multi_select) + nav_render_cursor_flags |= ImGuiNavRenderCursorFlags_AlwaysDraw; // Always show the nav rectangle + RenderNavCursor(bb, id, nav_render_cursor_flags); + } } - if ((flags & ImGuiSelectableFlags_SpanAllColumns) && window->DC.CurrentColumns) + if (span_all_columns) { - PopColumnsBackground(); - bb.Max.x -= (GetContentRegionMax().x - max_x); + if (g.CurrentTable) + TablePopBackgroundChannel(); + else if (window->DC.CurrentColumns) + PopColumnsBackground(); } - if (flags & ImGuiSelectableFlags_Disabled) PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); - RenderTextClipped(bb_inner.Min, bb_inner.Max, label, NULL, &label_size, style.SelectableTextAlign, &bb); - if (flags & ImGuiSelectableFlags_Disabled) PopStyleColor(); + // Text stays at the submission position. Alignment/clipping extents ignore SpanAllColumns. + if (is_visible) + RenderTextClipped(pos, ImVec2(ImMin(pos.x + size.x, window->WorkRect.Max.x), pos.y + size.y), label, NULL, &label_size, style.SelectableTextAlign, &bb); // Automatically close popups - if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_DontClosePopups) && !(window->DC.ItemFlags & ImGuiItemFlags_SelectableDontClosePopup)) + if (pressed && (window->Flags & ImGuiWindowFlags_Popup) && !(flags & ImGuiSelectableFlags_NoAutoClosePopups) && (g.LastItemData.ItemFlags & ImGuiItemFlags_AutoClosePopups)) CloseCurrentPopup(); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags); - return pressed; + if (disabled_item && !disabled_global) + EndDisabled(); + + // Selectable() always returns a pressed state! + // Users of BeginMultiSelect()/EndMultiSelect() scope: you may call ImGui::IsItemToggledSelection() to retrieve + // selection toggle, only useful if you need that state updated (e.g. for rendering purpose) before reaching EndMultiSelect(). + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); + return pressed; //-V1020 } bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags flags, const ImVec2& size_arg) @@ -5603,21 +7162,1145 @@ bool ImGui::Selectable(const char* label, bool* p_selected, ImGuiSelectableFlags return false; } + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Typing-Select support +//------------------------------------------------------------------------- + +// [Experimental] Currently not exposed in public API. +// Consume character inputs and return search request, if any. +// This would typically only be called on the focused window or location you want to grab inputs for, e.g. +// if (ImGui::IsWindowFocused(...)) +// if (ImGuiTypingSelectRequest* req = ImGui::GetTypingSelectRequest()) +// focus_idx = ImGui::TypingSelectFindMatch(req, my_items.size(), [](void*, int n) { return my_items[n]->Name; }, &my_items, -1); +// However the code is written in a way where calling it from multiple locations is safe (e.g. to obtain buffer). +ImGuiTypingSelectRequest* ImGui::GetTypingSelectRequest(ImGuiTypingSelectFlags flags) +{ + ImGuiContext& g = *GImGui; + ImGuiTypingSelectState* data = &g.TypingSelectState; + ImGuiTypingSelectRequest* out_request = &data->Request; + + // Clear buffer + const float TYPING_SELECT_RESET_TIMER = 1.80f; // FIXME: Potentially move to IO config. + const int TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK = 4; // Lock single char matching when repeating same char 4 times + if (data->SearchBuffer[0] != 0) + { + bool clear_buffer = false; + clear_buffer |= (g.NavFocusScopeId != data->FocusScope); + clear_buffer |= (data->LastRequestTime + TYPING_SELECT_RESET_TIMER < g.Time); + clear_buffer |= g.NavAnyRequest; + clear_buffer |= g.ActiveId != 0 && g.NavActivateId == 0; // Allow temporary SPACE activation to not interfere + clear_buffer |= IsKeyPressed(ImGuiKey_Escape) || IsKeyPressed(ImGuiKey_Enter); + clear_buffer |= IsKeyPressed(ImGuiKey_Backspace) && (flags & ImGuiTypingSelectFlags_AllowBackspace) == 0; + //if (clear_buffer) { IMGUI_DEBUG_LOG("GetTypingSelectRequest(): Clear SearchBuffer.\n"); } + if (clear_buffer) + data->Clear(); + } + + // Append to buffer + const int buffer_max_len = IM_ARRAYSIZE(data->SearchBuffer) - 1; + int buffer_len = (int)ImStrlen(data->SearchBuffer); + bool select_request = false; + for (ImWchar w : g.IO.InputQueueCharacters) + { + const int w_len = ImTextCountUtf8BytesFromStr(&w, &w + 1); + if (w < 32 || (buffer_len == 0 && ImCharIsBlankW(w)) || (buffer_len + w_len > buffer_max_len)) // Ignore leading blanks + continue; + char w_buf[5]; + ImTextCharToUtf8(w_buf, (unsigned int)w); + if (data->SingleCharModeLock && w_len == out_request->SingleCharSize && memcmp(w_buf, data->SearchBuffer, w_len) == 0) + { + select_request = true; // Same character: don't need to append to buffer. + continue; + } + if (data->SingleCharModeLock) + { + data->Clear(); // Different character: clear + buffer_len = 0; + } + memcpy(data->SearchBuffer + buffer_len, w_buf, w_len + 1); // Append + buffer_len += w_len; + select_request = true; + } + g.IO.InputQueueCharacters.resize(0); + + // Handle backspace + if ((flags & ImGuiTypingSelectFlags_AllowBackspace) && IsKeyPressed(ImGuiKey_Backspace, ImGuiInputFlags_Repeat)) + { + char* p = (char*)(void*)ImTextFindPreviousUtf8Codepoint(data->SearchBuffer, data->SearchBuffer + buffer_len); + *p = 0; + buffer_len = (int)(p - data->SearchBuffer); + } + + // Return request if any + if (buffer_len == 0) + return NULL; + if (select_request) + { + data->FocusScope = g.NavFocusScopeId; + data->LastRequestFrame = g.FrameCount; + data->LastRequestTime = (float)g.Time; + } + out_request->Flags = flags; + out_request->SearchBufferLen = buffer_len; + out_request->SearchBuffer = data->SearchBuffer; + out_request->SelectRequest = (data->LastRequestFrame == g.FrameCount); + out_request->SingleCharMode = false; + out_request->SingleCharSize = 0; + + // Calculate if buffer contains the same character repeated. + // - This can be used to implement a special search mode on first character. + // - Performed on UTF-8 codepoint for correctness. + // - SingleCharMode is always set for first input character, because it usually leads to a "next". + if (flags & ImGuiTypingSelectFlags_AllowSingleCharMode) + { + const char* buf_begin = out_request->SearchBuffer; + const char* buf_end = out_request->SearchBuffer + out_request->SearchBufferLen; + const int c0_len = ImTextCountUtf8BytesFromChar(buf_begin, buf_end); + const char* p = buf_begin + c0_len; + for (; p < buf_end; p += c0_len) + if (memcmp(buf_begin, p, (size_t)c0_len) != 0) + break; + const int single_char_count = (p == buf_end) ? (out_request->SearchBufferLen / c0_len) : 0; + out_request->SingleCharMode = (single_char_count > 0 || data->SingleCharModeLock); + out_request->SingleCharSize = (ImS8)c0_len; + data->SingleCharModeLock |= (single_char_count >= TYPING_SELECT_SINGLE_CHAR_COUNT_FOR_LOCK); // From now on we stop search matching to lock to single char mode. + } + + return out_request; +} + +static int ImStrimatchlen(const char* s1, const char* s1_end, const char* s2) +{ + int match_len = 0; + while (s1 < s1_end && ImToUpper(*s1++) == ImToUpper(*s2++)) + match_len++; + return match_len; +} + +// Default handler for finding a result for typing-select. You may implement your own. +// You might want to display a tooltip to visualize the current request SearchBuffer +// When SingleCharMode is set: +// - it is better to NOT display a tooltip of other on-screen display indicator. +// - the index of the currently focused item is required. +// if your SetNextItemSelectionUserData() values are indices, you can obtain it from ImGuiMultiSelectIO::NavIdItem, otherwise from g.NavLastValidSelectionUserData. +int ImGui::TypingSelectFindMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx) +{ + if (req == NULL || req->SelectRequest == false) // Support NULL parameter so both calls can be done from same spot. + return -1; + int idx = -1; + if (req->SingleCharMode && (req->Flags & ImGuiTypingSelectFlags_AllowSingleCharMode)) + idx = TypingSelectFindNextSingleCharMatch(req, items_count, get_item_name_func, user_data, nav_item_idx); + else + idx = TypingSelectFindBestLeadingMatch(req, items_count, get_item_name_func, user_data); + if (idx != -1) + SetNavCursorVisibleAfterMove(); + return idx; +} + +// Special handling when a single character is repeated: perform search on a single letter and goes to next. +int ImGui::TypingSelectFindNextSingleCharMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data, int nav_item_idx) +{ + // FIXME: Assume selection user data is index. Would be extremely practical. + //if (nav_item_idx == -1) + // nav_item_idx = (int)g.NavLastValidSelectionUserData; + + int first_match_idx = -1; + bool return_next_match = false; + for (int idx = 0; idx < items_count; idx++) + { + const char* item_name = get_item_name_func(user_data, idx); + if (ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SingleCharSize, item_name) < req->SingleCharSize) + continue; + if (return_next_match) // Return next matching item after current item. + return idx; + if (first_match_idx == -1 && nav_item_idx == -1) // Return first match immediately if we don't have a nav_item_idx value. + return idx; + if (first_match_idx == -1) // Record first match for wrapping. + first_match_idx = idx; + if (nav_item_idx == idx) // Record that we encountering nav_item so we can return next match. + return_next_match = true; + } + return first_match_idx; // First result +} + +int ImGui::TypingSelectFindBestLeadingMatch(ImGuiTypingSelectRequest* req, int items_count, const char* (*get_item_name_func)(void*, int), void* user_data) +{ + int longest_match_idx = -1; + int longest_match_len = 0; + for (int idx = 0; idx < items_count; idx++) + { + const char* item_name = get_item_name_func(user_data, idx); + const int match_len = ImStrimatchlen(req->SearchBuffer, req->SearchBuffer + req->SearchBufferLen, item_name); + if (match_len <= longest_match_len) + continue; + longest_match_idx = idx; + longest_match_len = match_len; + if (match_len == req->SearchBufferLen) + break; + } + return longest_match_idx; +} + +void ImGui::DebugNodeTypingSelectState(ImGuiTypingSelectState* data) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + Text("SearchBuffer = \"%s\"", data->SearchBuffer); + Text("SingleCharMode = %d, Size = %d, Lock = %d", data->Request.SingleCharMode, data->Request.SingleCharSize, data->SingleCharModeLock); + Text("LastRequest = time: %.2f, frame: %d", data->LastRequestTime, data->LastRequestFrame); +#else + IM_UNUSED(data); +#endif +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Box-Select support +// This has been extracted away from Multi-Select logic in the hope that it could eventually be used elsewhere, but hasn't been yet. +//------------------------------------------------------------------------- +// Extra logic in MultiSelectItemFooter() and ImGuiListClipper::Step() +//------------------------------------------------------------------------- +// - BoxSelectPreStartDrag() [Internal] +// - BoxSelectActivateDrag() [Internal] +// - BoxSelectDeactivateDrag() [Internal] +// - BoxSelectScrollWithMouseDrag() [Internal] +// - BeginBoxSelect() [Internal] +// - EndBoxSelect() [Internal] +//------------------------------------------------------------------------- + +// Call on the initial click. +static void BoxSelectPreStartDrag(ImGuiID id, ImGuiSelectionUserData clicked_item) +{ + ImGuiContext& g = *GImGui; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + bs->ID = id; + bs->IsStarting = true; // Consider starting box-select. + bs->IsStartedFromVoid = (clicked_item == ImGuiSelectionUserData_Invalid); + bs->IsStartedSetNavIdOnce = bs->IsStartedFromVoid; + bs->KeyMods = g.IO.KeyMods; + bs->StartPosRel = bs->EndPosRel = ImGui::WindowPosAbsToRel(g.CurrentWindow, g.IO.MousePos); + bs->ScrollAccum = ImVec2(0.0f, 0.0f); +} + +static void BoxSelectActivateDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Activate\n", bs->ID); + bs->IsActive = true; + bs->Window = window; + bs->IsStarting = false; + ImGui::SetActiveID(bs->ID, window); + ImGui::SetActiveIdUsingAllKeyboardKeys(); + if (bs->IsStartedFromVoid && (bs->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0) + bs->RequestClear = true; +} + +static void BoxSelectDeactivateDrag(ImGuiBoxSelectState* bs) +{ + ImGuiContext& g = *GImGui; + bs->IsActive = bs->IsStarting = false; + if (g.ActiveId == bs->ID) + { + IMGUI_DEBUG_LOG_SELECTION("[selection] BeginBoxSelect() 0X%08X: Deactivate\n", bs->ID); + ImGui::ClearActiveID(); + } + bs->ID = 0; +} + +static void BoxSelectScrollWithMouseDrag(ImGuiBoxSelectState* bs, ImGuiWindow* window, const ImRect& inner_r) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(bs->Window == window); + for (int n = 0; n < 2; n++) // each axis + { + const float mouse_pos = g.IO.MousePos[n]; + const float dist = (mouse_pos > inner_r.Max[n]) ? mouse_pos - inner_r.Max[n] : (mouse_pos < inner_r.Min[n]) ? mouse_pos - inner_r.Min[n] : 0.0f; + const float scroll_curr = window->Scroll[n]; + if (dist == 0.0f || (dist < 0.0f && scroll_curr < 0.0f) || (dist > 0.0f && scroll_curr >= window->ScrollMax[n])) + continue; + + const float speed_multiplier = ImLinearRemapClamp(g.FontSize, g.FontSize * 5.0f, 1.0f, 4.0f, ImAbs(dist)); // x1 to x4 depending on distance + const float scroll_step = g.FontSize * 35.0f * speed_multiplier * ImSign(dist) * g.IO.DeltaTime; + bs->ScrollAccum[n] += scroll_step; + + // Accumulate into a stored value so we can handle high-framerate + const float scroll_step_i = ImFloor(bs->ScrollAccum[n]); + if (scroll_step_i == 0.0f) + continue; + if (n == 0) + ImGui::SetScrollX(window, scroll_curr + scroll_step_i); + else + ImGui::SetScrollY(window, scroll_curr + scroll_step_i); + bs->ScrollAccum[n] -= scroll_step_i; + } +} + +bool ImGui::BeginBoxSelect(const ImRect& scope_rect, ImGuiWindow* window, ImGuiID box_select_id, ImGuiMultiSelectFlags ms_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + KeepAliveID(box_select_id); + if (bs->ID != box_select_id) + return false; + + // IsStarting is set by MultiSelectItemFooter() when considering a possible box-select. We validate it here and lock geometry. + bs->UnclipMode = false; + bs->RequestClear = false; + if (bs->IsStarting && IsMouseDragPastThreshold(0)) + BoxSelectActivateDrag(bs, window); + else if ((bs->IsStarting || bs->IsActive) && g.IO.MouseDown[0] == false) + BoxSelectDeactivateDrag(bs); + if (!bs->IsActive) + return false; + + // Current frame absolute prev/current rectangles are used to toggle selection. + // They are derived from positions relative to scrolling space. + ImVec2 start_pos_abs = WindowPosRelToAbs(window, bs->StartPosRel); + ImVec2 prev_end_pos_abs = WindowPosRelToAbs(window, bs->EndPosRel); // Clamped already + ImVec2 curr_end_pos_abs = g.IO.MousePos; + if (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) // Box-select scrolling only happens with ScopeWindow + curr_end_pos_abs = ImClamp(curr_end_pos_abs, scope_rect.Min, scope_rect.Max); + bs->BoxSelectRectPrev.Min = ImMin(start_pos_abs, prev_end_pos_abs); + bs->BoxSelectRectPrev.Max = ImMax(start_pos_abs, prev_end_pos_abs); + bs->BoxSelectRectCurr.Min = ImMin(start_pos_abs, curr_end_pos_abs); + bs->BoxSelectRectCurr.Max = ImMax(start_pos_abs, curr_end_pos_abs); + + // Box-select 2D mode detects horizontal changes (vertical ones are already picked by Clipper) + // Storing an extra rect used by widgets supporting box-select. + if (ms_flags & ImGuiMultiSelectFlags_BoxSelect2d) + if (bs->BoxSelectRectPrev.Min.x != bs->BoxSelectRectCurr.Min.x || bs->BoxSelectRectPrev.Max.x != bs->BoxSelectRectCurr.Max.x) + { + bs->UnclipMode = true; + bs->UnclipRect = bs->BoxSelectRectPrev; // FIXME-OPT: UnclipRect x coordinates could be intersection of Prev and Curr rect on X axis. + bs->UnclipRect.Add(bs->BoxSelectRectCurr); + } + + //GetForegroundDrawList()->AddRect(bs->UnclipRect.Min, bs->UnclipRect.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); + //GetForegroundDrawList()->AddRect(bs->BoxSelectRectPrev.Min, bs->BoxSelectRectPrev.Max, IM_COL32(255,0,0,200), 0.0f, 0, 3.0f); + //GetForegroundDrawList()->AddRect(bs->BoxSelectRectCurr.Min, bs->BoxSelectRectCurr.Max, IM_COL32(0,255,0,200), 0.0f, 0, 1.0f); + return true; +} + +void ImGui::EndBoxSelect(const ImRect& scope_rect, ImGuiMultiSelectFlags ms_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + ImGuiBoxSelectState* bs = &g.BoxSelectState; + IM_ASSERT(bs->IsActive); + bs->UnclipMode = false; + + // Render selection rectangle + bs->EndPosRel = WindowPosAbsToRel(window, ImClamp(g.IO.MousePos, scope_rect.Min, scope_rect.Max)); // Clamp stored position according to current scrolling view + ImRect box_select_r = bs->BoxSelectRectCurr; + box_select_r.ClipWith(scope_rect); + window->DrawList->AddRectFilled(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_SeparatorHovered, 0.30f)); // FIXME-MULTISELECT: Styling + window->DrawList->AddRect(box_select_r.Min, box_select_r.Max, GetColorU32(ImGuiCol_NavCursor)); // FIXME-MULTISELECT: Styling + + // Scroll + const bool enable_scroll = (ms_flags & ImGuiMultiSelectFlags_ScopeWindow) && (ms_flags & ImGuiMultiSelectFlags_BoxSelectNoScroll) == 0; + if (enable_scroll) + { + ImRect scroll_r = scope_rect; + scroll_r.Expand(-g.FontSize); + //GetForegroundDrawList()->AddRect(scroll_r.Min, scroll_r.Max, IM_COL32(0, 255, 0, 255)); + if (!scroll_r.Contains(g.IO.MousePos)) + BoxSelectScrollWithMouseDrag(bs, window, scroll_r); + } +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Select support +//------------------------------------------------------------------------- +// - DebugLogMultiSelectRequests() [Internal] +// - CalcScopeRect() [Internal] +// - BeginMultiSelect() +// - EndMultiSelect() +// - SetNextItemSelectionUserData() +// - MultiSelectItemHeader() [Internal] +// - MultiSelectItemFooter() [Internal] +// - DebugNodeMultiSelectState() [Internal] +//------------------------------------------------------------------------- + +static void DebugLogMultiSelectRequests(const char* function, const ImGuiMultiSelectIO* io) +{ + ImGuiContext& g = *GImGui; + IM_UNUSED(function); + for (const ImGuiSelectionRequest& req : io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetAll %d (= %s)\n", function, req.Selected, req.Selected ? "SelectAll" : "Clear"); + if (req.Type == ImGuiSelectionRequestType_SetRange) IMGUI_DEBUG_LOG_SELECTION("[selection] %s: Request: SetRange %" IM_PRId64 "..%" IM_PRId64 " (0x%" IM_PRIX64 "..0x%" IM_PRIX64 ") = %d (dir %d)\n", function, req.RangeFirstItem, req.RangeLastItem, req.RangeFirstItem, req.RangeLastItem, req.Selected, req.RangeDirection); + } +} + +static ImRect CalcScopeRect(ImGuiMultiSelectTempData* ms, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) + { + // Warning: this depends on CursorMaxPos so it means to be called by EndMultiSelect() only + return ImRect(ms->ScopeRectMin, ImMax(window->DC.CursorMaxPos, ms->ScopeRectMin)); + } + else + { + // When a table, pull HostClipRect, which allows us to predict ClipRect before first row/layout is performed. (#7970) + ImRect scope_rect = window->InnerClipRect; + if (g.CurrentTable != NULL) + scope_rect = g.CurrentTable->HostClipRect; + + // Add inner table decoration (#7821) // FIXME: Why not baking in InnerClipRect? + scope_rect.Min = ImMin(scope_rect.Min + ImVec2(window->DecoInnerSizeX1, window->DecoInnerSizeY1), scope_rect.Max); + return scope_rect; + } +} + +// Return ImGuiMultiSelectIO structure. +// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). +// Passing 'selection_size' and 'items_count' parameters is currently optional. +// - 'selection_size' is useful to disable some shortcut routing: e.g. ImGuiMultiSelectFlags_ClearOnEscape won't claim Escape key when selection_size 0, +// allowing a first press to clear selection THEN the second press to leave child window and return to parent. +// - 'items_count' is stored in ImGuiMultiSelectIO which makes it a convenient way to pass the information to your ApplyRequest() handler (but you may pass it differently). +// - If they are costly for you to compute (e.g. external intrusive selection without maintaining size), you may avoid them and pass -1. +// - If you can easily tell if your selection is empty or not, you may pass 0/1, or you may enable ImGuiMultiSelectFlags_ClearOnEscape flag dynamically. +ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int selection_size, int items_count) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + if (++g.MultiSelectTempDataStacked > g.MultiSelectTempData.Size) + g.MultiSelectTempData.resize(g.MultiSelectTempDataStacked, ImGuiMultiSelectTempData()); + ImGuiMultiSelectTempData* ms = &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1]; + IM_STATIC_ASSERT(offsetof(ImGuiMultiSelectTempData, IO) == 0); // Clear() relies on that. + g.CurrentMultiSelect = ms; + if ((flags & (ImGuiMultiSelectFlags_ScopeWindow | ImGuiMultiSelectFlags_ScopeRect)) == 0) + flags |= ImGuiMultiSelectFlags_ScopeWindow; + if (flags & ImGuiMultiSelectFlags_SingleSelect) + flags &= ~(ImGuiMultiSelectFlags_BoxSelect2d | ImGuiMultiSelectFlags_BoxSelect1d); + if (flags & ImGuiMultiSelectFlags_BoxSelect2d) + flags &= ~ImGuiMultiSelectFlags_BoxSelect1d; + + // FIXME: Workaround to the fact we override CursorMaxPos, meaning size measurement are lost. (#8250) + // They should perhaps be stacked properly? + if (ImGuiTable* table = g.CurrentTable) + if (table->CurrentColumn != -1) + TableEndCell(table); // This is currently safe to call multiple time. If that properly is lost we can extract the "save measurement" part of it. + + // FIXME: BeginFocusScope() + const ImGuiID id = window->IDStack.back(); + ms->Clear(); + ms->FocusScopeId = id; + ms->Flags = flags; + ms->IsFocused = (ms->FocusScopeId == g.NavFocusScopeId); + ms->BackupCursorMaxPos = window->DC.CursorMaxPos; + ms->ScopeRectMin = window->DC.CursorMaxPos = window->DC.CursorPos; + PushFocusScope(ms->FocusScopeId); + if (flags & ImGuiMultiSelectFlags_ScopeWindow) // Mark parent child window as navigable into, with highlight. Assume user will always submit interactive items. + window->DC.NavLayersActiveMask |= 1 << ImGuiNavLayer_Main; + + // Use copy of keyboard mods at the time of the request, otherwise we would requires mods to be held for an extra frame. + ms->KeyMods = g.NavJustMovedToId ? (g.NavJustMovedToIsTabbing ? 0 : g.NavJustMovedToKeyMods) : g.IO.KeyMods; + if (flags & ImGuiMultiSelectFlags_NoRangeSelect) + ms->KeyMods &= ~ImGuiMod_Shift; + + // Bind storage + ImGuiMultiSelectState* storage = g.MultiSelectStorage.GetOrAddByKey(id); + storage->ID = id; + storage->LastFrameActive = g.FrameCount; + storage->LastSelectionSize = selection_size; + storage->Window = window; + ms->Storage = storage; + + // Output to user + ms->IO.Requests.resize(0); + ms->IO.RangeSrcItem = storage->RangeSrcItem; + ms->IO.NavIdItem = storage->NavIdItem; + ms->IO.NavIdSelected = (storage->NavIdSelected == 1) ? true : false; + ms->IO.ItemsCount = items_count; + + // Clear when using Navigation to move within the scope + // (we compare FocusScopeId so it possible to use multiple selections inside a same window) + bool request_clear = false; + bool request_select_all = false; + if (g.NavJustMovedToId != 0 && g.NavJustMovedToFocusScopeId == ms->FocusScopeId && g.NavJustMovedToHasSelectionData) + { + if (ms->KeyMods & ImGuiMod_Shift) + ms->IsKeyboardSetRange = true; + if (ms->IsKeyboardSetRange) + IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid); // Not ready -> could clear? + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) + request_clear = true; + } + else if (g.NavJustMovedFromFocusScopeId == ms->FocusScopeId) + { + // Also clear on leaving scope (may be optional?) + if ((ms->KeyMods & (ImGuiMod_Ctrl | ImGuiMod_Shift)) == 0 && (flags & (ImGuiMultiSelectFlags_NoAutoClear | ImGuiMultiSelectFlags_NoAutoSelect)) == 0) + request_clear = true; + } + + // Box-select handling: update active state. + ImGuiBoxSelectState* bs = &g.BoxSelectState; + if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) + { + ms->BoxSelectId = GetID("##BoxSelect"); + if (BeginBoxSelect(CalcScopeRect(ms, window), window, ms->BoxSelectId, flags)) + request_clear |= bs->RequestClear; + } + + if (ms->IsFocused) + { + // Shortcut: Clear selection (Escape) + // - Only claim shortcut if selection is not empty, allowing further presses on Escape to e.g. leave current child window. + // - Box select also handle Escape and needs to pass an id to bypass ActiveIdUsingAllKeyboardKeys lock. + if (flags & ImGuiMultiSelectFlags_ClearOnEscape) + { + if (selection_size != 0 || bs->IsActive) + if (Shortcut(ImGuiKey_Escape, ImGuiInputFlags_None, bs->IsActive ? bs->ID : 0)) + { + request_clear = true; + if (bs->IsActive) + BoxSelectDeactivateDrag(bs); + } + } + + // Shortcut: Select all (CTRL+A) + if (!(flags & ImGuiMultiSelectFlags_SingleSelect) && !(flags & ImGuiMultiSelectFlags_NoSelectAll)) + if (Shortcut(ImGuiMod_Ctrl | ImGuiKey_A)) + request_select_all = true; + } + + if (request_clear || request_select_all) + { + MultiSelectAddSetAll(ms, request_select_all); + if (!request_select_all) + storage->LastSelectionSize = 0; + } + ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; + ms->LastSubmittedItem = ImGuiSelectionUserData_Invalid; + + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) + DebugLogMultiSelectRequests("BeginMultiSelect", &ms->IO); + + return &ms->IO; +} + +// Return updated ImGuiMultiSelectIO structure. +// Lifetime: don't hold on ImGuiMultiSelectIO* pointers over multiple frames or past any subsequent call to BeginMultiSelect() or EndMultiSelect(). +ImGuiMultiSelectIO* ImGui::EndMultiSelect() +{ + ImGuiContext& g = *GImGui; + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; + ImGuiMultiSelectState* storage = ms->Storage; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT_USER_ERROR(ms->FocusScopeId == g.CurrentFocusScopeId, "EndMultiSelect() FocusScope mismatch!"); + IM_ASSERT(g.CurrentMultiSelect != NULL && storage->Window == g.CurrentWindow); + IM_ASSERT(g.MultiSelectTempDataStacked > 0 && &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] == g.CurrentMultiSelect); + + ImRect scope_rect = CalcScopeRect(ms, window); + if (ms->IsFocused) + { + // We currently don't allow user code to modify RangeSrcItem by writing to BeginIO's version, but that would be an easy change here. + if (ms->IO.RangeSrcReset || (ms->RangeSrcPassedBy == false && ms->IO.RangeSrcItem != ImGuiSelectionUserData_Invalid)) // Can't read storage->RangeSrcItem here -> we want the state at begining of the scope (see tests for easy failure) + { + IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset RangeSrcItem.\n"); // Will set be to NavId. + storage->RangeSrcItem = ImGuiSelectionUserData_Invalid; + } + if (ms->NavIdPassedBy == false && storage->NavIdItem != ImGuiSelectionUserData_Invalid) + { + IMGUI_DEBUG_LOG_SELECTION("[selection] EndMultiSelect: Reset NavIdItem.\n"); + storage->NavIdItem = ImGuiSelectionUserData_Invalid; + storage->NavIdSelected = -1; + } + + if ((ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) && GetBoxSelectState(ms->BoxSelectId)) + EndBoxSelect(scope_rect, ms->Flags); + } + + if (ms->IsEndIO == false) + ms->IO.Requests.resize(0); + + // Clear selection when clicking void? + // We specifically test for IsMouseDragPastThreshold(0) == false to allow box-selection! + // The InnerRect test is necessary for non-child/decorated windows. + bool scope_hovered = IsWindowHovered() && window->InnerRect.Contains(g.IO.MousePos); + if (scope_hovered && (ms->Flags & ImGuiMultiSelectFlags_ScopeRect)) + scope_hovered &= scope_rect.Contains(g.IO.MousePos); + if (scope_hovered && g.HoveredId == 0 && g.ActiveId == 0) + { + if (ms->Flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) + { + if (!g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && g.IO.MouseClickedCount[0] == 1) + { + BoxSelectPreStartDrag(ms->BoxSelectId, ImGuiSelectionUserData_Invalid); + FocusWindow(window, ImGuiFocusRequestFlags_UnlessBelowModal); + SetHoveredID(ms->BoxSelectId); + if (ms->Flags & ImGuiMultiSelectFlags_ScopeRect) + SetNavID(0, ImGuiNavLayer_Main, ms->FocusScopeId, ImRect(g.IO.MousePos, g.IO.MousePos)); // Automatically switch FocusScope for initial click from void to box-select. + } + } + + if (ms->Flags & ImGuiMultiSelectFlags_ClearOnClickVoid) + if (IsMouseReleased(0) && IsMouseDragPastThreshold(0) == false && g.IO.KeyMods == ImGuiMod_None) + MultiSelectAddSetAll(ms, false); + } + + // Courtesy nav wrapping helper flag + if (ms->Flags & ImGuiMultiSelectFlags_NavWrapX) + { + IM_ASSERT(ms->Flags & ImGuiMultiSelectFlags_ScopeWindow); // Only supported at window scope + ImGui::NavMoveRequestTryWrapping(ImGui::GetCurrentWindow(), ImGuiNavMoveFlags_WrapX); + } + + // Unwind + window->DC.CursorMaxPos = ImMax(ms->BackupCursorMaxPos, window->DC.CursorMaxPos); + PopFocusScope(); + + if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) + DebugLogMultiSelectRequests("EndMultiSelect", &ms->IO); + + ms->FocusScopeId = 0; + ms->Flags = ImGuiMultiSelectFlags_None; + g.CurrentMultiSelect = (--g.MultiSelectTempDataStacked > 0) ? &g.MultiSelectTempData[g.MultiSelectTempDataStacked - 1] : NULL; + + return &ms->IO; +} + +void ImGui::SetNextItemSelectionUserData(ImGuiSelectionUserData selection_user_data) +{ + // Note that flags will be cleared by ItemAdd(), so it's only useful for Navigation code! + // This designed so widgets can also cheaply set this before calling ItemAdd(), so we are not tied to MultiSelect api. + ImGuiContext& g = *GImGui; + g.NextItemData.SelectionUserData = selection_user_data; + g.NextItemData.FocusScopeId = g.CurrentFocusScopeId; + + if (ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect) + { + // Auto updating RangeSrcPassedBy for cases were clipper is not used (done before ItemAdd() clipping) + g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData | ImGuiItemFlags_IsMultiSelect; + if (ms->IO.RangeSrcItem == selection_user_data) + ms->RangeSrcPassedBy = true; + } + else + { + g.NextItemData.ItemFlags |= ImGuiItemFlags_HasSelectionUserData; + } +} + +// In charge of: +// - Applying SetAll for submitted items. +// - Applying SetRange for submitted items and record end points. +// - Altering button behavior flags to facilitate use with drag and drop. +void ImGui::MultiSelectItemHeader(ImGuiID id, bool* p_selected, ImGuiButtonFlags* p_button_flags) +{ + ImGuiContext& g = *GImGui; + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; + + bool selected = *p_selected; + if (ms->IsFocused) + { + ImGuiMultiSelectState* storage = ms->Storage; + ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; + IM_ASSERT(g.NextItemData.FocusScopeId == g.CurrentFocusScopeId && "Forgot to call SetNextItemSelectionUserData() prior to item, required in BeginMultiSelect()/EndMultiSelect() scope"); + + // Apply SetAll (Clear/SelectAll) requests requested by BeginMultiSelect(). + // This is only useful if the user hasn't processed them already, and this only works if the user isn't using the clipper. + // If you are using a clipper you need to process the SetAll request after calling BeginMultiSelect() + if (ms->LoopRequestSetAll != -1) + selected = (ms->LoopRequestSetAll == 1); + + // When using SHIFT+Nav: because it can incur scrolling we cannot afford a frame of lag with the selection highlight (otherwise scrolling would happen before selection) + // For this to work, we need someone to set 'RangeSrcPassedBy = true' at some point (either clipper either SetNextItemSelectionUserData() function) + if (ms->IsKeyboardSetRange) + { + IM_ASSERT(id != 0 && (ms->KeyMods & ImGuiMod_Shift) != 0); + const bool is_range_dst = (ms->RangeDstPassedBy == false) && g.NavJustMovedToId == id; // Assume that g.NavJustMovedToId is not clipped. + if (is_range_dst) + ms->RangeDstPassedBy = true; + if (is_range_dst && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) // If we don't have RangeSrc, assign RangeSrc = RangeDst + { + storage->RangeSrcItem = item_data; + storage->RangeSelected = selected ? 1 : 0; + } + const bool is_range_src = storage->RangeSrcItem == item_data; + if (is_range_src || is_range_dst || ms->RangeSrcPassedBy != ms->RangeDstPassedBy) + { + // Apply range-select value to visible items + IM_ASSERT(storage->RangeSrcItem != ImGuiSelectionUserData_Invalid && storage->RangeSelected != -1); + selected = (storage->RangeSelected != 0); + } + else if ((ms->KeyMods & ImGuiMod_Ctrl) == 0 && (ms->Flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) + { + // Clear other items + selected = false; + } + } + *p_selected = selected; + } + + // Alter button behavior flags + // To handle drag and drop of multiple items we need to avoid clearing selection on click. + // Enabling this test makes actions using CTRL+SHIFT delay their effect on MouseUp which is annoying, but it allows drag and drop of multiple items. + if (p_button_flags != NULL) + { + ImGuiButtonFlags button_flags = *p_button_flags; + button_flags |= ImGuiButtonFlags_NoHoveredOnFocus; + if ((!selected || (g.ActiveId == id && g.ActiveIdHasBeenPressedBefore)) && !(ms->Flags & ImGuiMultiSelectFlags_SelectOnClickRelease)) + button_flags = (button_flags | ImGuiButtonFlags_PressedOnClick) & ~ImGuiButtonFlags_PressedOnClickRelease; + else + button_flags |= ImGuiButtonFlags_PressedOnClickRelease; + *p_button_flags = button_flags; + } +} + +// In charge of: +// - Auto-select on navigation. +// - Box-select toggle handling. +// - Right-click handling. +// - Altering selection based on Ctrl/Shift modifiers, both for keyboard and mouse. +// - Record current selection state for RangeSrc +// This is all rather complex, best to run and refer to "widgets_multiselect_xxx" tests in imgui_test_suite. +void ImGui::MultiSelectItemFooter(ImGuiID id, bool* p_selected, bool* p_pressed) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + + bool selected = *p_selected; + bool pressed = *p_pressed; + ImGuiMultiSelectTempData* ms = g.CurrentMultiSelect; + ImGuiMultiSelectState* storage = ms->Storage; + if (pressed) + ms->IsFocused = true; + + bool hovered = false; + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_HoveredRect) + hovered = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + if (!ms->IsFocused && !hovered) + return; + + ImGuiSelectionUserData item_data = g.NextItemData.SelectionUserData; + + ImGuiMultiSelectFlags flags = ms->Flags; + const bool is_singleselect = (flags & ImGuiMultiSelectFlags_SingleSelect) != 0; + bool is_ctrl = (ms->KeyMods & ImGuiMod_Ctrl) != 0; + bool is_shift = (ms->KeyMods & ImGuiMod_Shift) != 0; + + bool apply_to_range_src = false; + + if (g.NavId == id && storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) + apply_to_range_src = true; + if (ms->IsEndIO == false) + { + ms->IO.Requests.resize(0); + ms->IsEndIO = true; + } + + // Auto-select as you navigate a list + if (g.NavJustMovedToId == id) + { + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + if (is_ctrl && is_shift) + pressed = true; + else if (!is_ctrl) + selected = pressed = true; + } + else + { + // With NoAutoSelect, using Shift+keyboard performs a write/copy + if (is_shift) + pressed = true; + else if (!is_ctrl) + apply_to_range_src = true; // Since if (pressed) {} main block is not running we update this + } + } + + if (apply_to_range_src) + { + storage->RangeSrcItem = item_data; + storage->RangeSelected = selected; // Will be updated at the end of this function anyway. + } + + // Box-select toggle handling + if (ms->BoxSelectId != 0) + if (ImGuiBoxSelectState* bs = GetBoxSelectState(ms->BoxSelectId)) + { + const bool rect_overlap_curr = bs->BoxSelectRectCurr.Overlaps(g.LastItemData.Rect); + const bool rect_overlap_prev = bs->BoxSelectRectPrev.Overlaps(g.LastItemData.Rect); + if ((rect_overlap_curr && !rect_overlap_prev && !selected) || (rect_overlap_prev && !rect_overlap_curr)) + { + if (storage->LastSelectionSize <= 0 && bs->IsStartedSetNavIdOnce) + { + pressed = true; // First item act as a pressed: code below will emit selection request and set NavId (whatever we emit here will be overridden anyway) + bs->IsStartedSetNavIdOnce = false; + } + else + { + selected = !selected; + MultiSelectAddSetRange(ms, selected, +1, item_data, item_data); + } + storage->LastSelectionSize = ImMax(storage->LastSelectionSize + 1, 1); + } + } + + // Right-click handling. + // FIXME-MULTISELECT: Currently filtered out by ImGuiMultiSelectFlags_NoAutoSelect but maybe should be moved to Selectable(). See https://github.com/ocornut/imgui/pull/5816 + if (hovered && IsMouseClicked(1) && (flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + if (g.ActiveId != 0 && g.ActiveId != id) + ClearActiveID(); + SetFocusID(id, window); + if (!pressed && !selected) + { + pressed = true; + is_ctrl = is_shift = false; + } + } + + // Unlike Space, Enter doesn't alter selection (but can still return a press) unless current item is not selected. + // The later, "unless current item is not select", may become optional? It seems like a better default if Enter doesn't necessarily open something + // (unlike e.g. Windows explorer). For use case where Enter always open something, we might decide to make this optional? + const bool enter_pressed = pressed && (g.NavActivateId == id) && (g.NavActivateFlags & ImGuiActivateFlags_PreferInput); + + // Alter selection + if (pressed && (!enter_pressed || !selected)) + { + // Box-select + ImGuiInputSource input_source = (g.NavJustMovedToId == id || g.NavActivateId == id) ? g.NavInputSource : ImGuiInputSource_Mouse; + if (flags & (ImGuiMultiSelectFlags_BoxSelect1d | ImGuiMultiSelectFlags_BoxSelect2d)) + if (selected == false && !g.BoxSelectState.IsActive && !g.BoxSelectState.IsStarting && input_source == ImGuiInputSource_Mouse && g.IO.MouseClickedCount[0] == 1) + BoxSelectPreStartDrag(ms->BoxSelectId, item_data); + + //---------------------------------------------------------------------------------------- + // ACTION | Begin | Pressed/Activated | End + //---------------------------------------------------------------------------------------- + // Keys Navigated: | Clear | Src=item, Sel=1 SetRange 1 + // Keys Navigated: Ctrl | n/a | n/a + // Keys Navigated: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1 + // Keys Navigated: Ctrl+Shift | n/a | Dst=item, Sel=Src => Clear + SetRange Src-Dst + // Keys Activated: | n/a | Src=item, Sel=1 => Clear + SetRange 1 + // Keys Activated: Ctrl | n/a | Src=item, Sel=!Sel => SetSange 1 + // Keys Activated: Shift | n/a | Dst=item, Sel=1 => Clear + SetSange 1 + //---------------------------------------------------------------------------------------- + // Mouse Pressed: | n/a | Src=item, Sel=1, => Clear + SetRange 1 + // Mouse Pressed: Ctrl | n/a | Src=item, Sel=!Sel => SetRange 1 + // Mouse Pressed: Shift | n/a | Dst=item, Sel=1, => Clear + SetRange 1 + // Mouse Pressed: Ctrl+Shift | n/a | Dst=item, Sel=!Sel => SetRange Src-Dst + //---------------------------------------------------------------------------------------- + + if ((flags & ImGuiMultiSelectFlags_NoAutoClear) == 0) + { + bool request_clear = false; + if (is_singleselect) + request_clear = true; + else if ((input_source == ImGuiInputSource_Mouse || g.NavActivateId == id) && !is_ctrl) + request_clear = (flags & ImGuiMultiSelectFlags_NoAutoClearOnReselect) ? !selected : true; + else if ((input_source == ImGuiInputSource_Keyboard || input_source == ImGuiInputSource_Gamepad) && is_shift && !is_ctrl) + request_clear = true; // With is_shift==false the RequestClear was done in BeginIO, not necessary to do again. + if (request_clear) + MultiSelectAddSetAll(ms, false); + } + + int range_direction; + bool range_selected; + if (is_shift && !is_singleselect) + { + //IM_ASSERT(storage->HasRangeSrc && storage->HasRangeValue); + if (storage->RangeSrcItem == ImGuiSelectionUserData_Invalid) + storage->RangeSrcItem = item_data; + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + { + // Shift+Arrow always select + // Ctrl+Shift+Arrow copy source selection state (already stored by BeginMultiSelect() in storage->RangeSelected) + range_selected = (is_ctrl && storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + } + else + { + // Shift+Arrow copy source selection state + // Shift+Click always copy from target selection state + if (ms->IsKeyboardSetRange) + range_selected = (storage->RangeSelected != -1) ? (storage->RangeSelected != 0) : true; + else + range_selected = !selected; + } + range_direction = ms->RangeSrcPassedBy ? +1 : -1; + } + else + { + // Ctrl inverts selection, otherwise always select + if ((flags & ImGuiMultiSelectFlags_NoAutoSelect) == 0) + selected = is_ctrl ? !selected : true; + else + selected = !selected; + storage->RangeSrcItem = item_data; + range_selected = selected; + range_direction = +1; + } + MultiSelectAddSetRange(ms, range_selected, range_direction, storage->RangeSrcItem, item_data); + } + + // Update/store the selection state of the Source item (used by CTRL+SHIFT, when Source is unselected we perform a range unselect) + if (storage->RangeSrcItem == item_data) + storage->RangeSelected = selected ? 1 : 0; + + // Update/store the selection state of focused item + if (g.NavId == id) + { + storage->NavIdItem = item_data; + storage->NavIdSelected = selected ? 1 : 0; + } + if (storage->NavIdItem == item_data) + ms->NavIdPassedBy = true; + ms->LastSubmittedItem = item_data; + + *p_selected = selected; + *p_pressed = pressed; +} + +void ImGui::MultiSelectAddSetAll(ImGuiMultiSelectTempData* ms, bool selected) +{ + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetAll, selected, 0, ImGuiSelectionUserData_Invalid, ImGuiSelectionUserData_Invalid }; + ms->IO.Requests.resize(0); // Can always clear previous requests + ms->IO.Requests.push_back(req); // Add new request +} + +void ImGui::MultiSelectAddSetRange(ImGuiMultiSelectTempData* ms, bool selected, int range_dir, ImGuiSelectionUserData first_item, ImGuiSelectionUserData last_item) +{ + // Merge contiguous spans into same request (unless NoRangeSelect is set which guarantees single-item ranges) + if (ms->IO.Requests.Size > 0 && first_item == last_item && (ms->Flags & ImGuiMultiSelectFlags_NoRangeSelect) == 0) + { + ImGuiSelectionRequest* prev = &ms->IO.Requests.Data[ms->IO.Requests.Size - 1]; + if (prev->Type == ImGuiSelectionRequestType_SetRange && prev->RangeLastItem == ms->LastSubmittedItem && prev->Selected == selected) + { + prev->RangeLastItem = last_item; + return; + } + } + + ImGuiSelectionRequest req = { ImGuiSelectionRequestType_SetRange, selected, (ImS8)range_dir, (range_dir > 0) ? first_item : last_item, (range_dir > 0) ? last_item : first_item }; + ms->IO.Requests.push_back(req); // Add new request +} + +void ImGui::DebugNodeMultiSelectState(ImGuiMultiSelectState* storage) +{ +#ifndef IMGUI_DISABLE_DEBUG_TOOLS + const bool is_active = (storage->LastFrameActive >= GetFrameCount() - 2); // Note that fully clipped early out scrolling tables will appear as inactive here. + if (!is_active) { PushStyleColor(ImGuiCol_Text, GetStyleColorVec4(ImGuiCol_TextDisabled)); } + bool open = TreeNode((void*)(intptr_t)storage->ID, "MultiSelect 0x%08X in '%s'%s", storage->ID, storage->Window ? storage->Window->Name : "N/A", is_active ? "" : " *Inactive*"); + if (!is_active) { PopStyleColor(); } + if (!open) + return; + Text("RangeSrcItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), RangeSelected = %d", storage->RangeSrcItem, storage->RangeSrcItem, storage->RangeSelected); + Text("NavIdItem = %" IM_PRId64 " (0x%" IM_PRIX64 "), NavIdSelected = %d", storage->NavIdItem, storage->NavIdItem, storage->NavIdSelected); + Text("LastSelectionSize = %d", storage->LastSelectionSize); // Provided by user + TreePop(); +#else + IM_UNUSED(storage); +#endif +} + +//------------------------------------------------------------------------- +// [SECTION] Widgets: Multi-Select helpers +//------------------------------------------------------------------------- +// - ImGuiSelectionBasicStorage +// - ImGuiSelectionExternalStorage +//------------------------------------------------------------------------- + +ImGuiSelectionBasicStorage::ImGuiSelectionBasicStorage() +{ + Size = 0; + PreserveOrder = false; + UserData = NULL; + AdapterIndexToStorageId = [](ImGuiSelectionBasicStorage*, int idx) { return (ImGuiID)idx; }; + _SelectionOrder = 1; // Always >0 +} + +void ImGuiSelectionBasicStorage::Clear() +{ + Size = 0; + _SelectionOrder = 1; // Always >0 + _Storage.Data.resize(0); +} + +void ImGuiSelectionBasicStorage::Swap(ImGuiSelectionBasicStorage& r) +{ + ImSwap(Size, r.Size); + ImSwap(_SelectionOrder, r._SelectionOrder); + _Storage.Data.swap(r._Storage.Data); +} + +bool ImGuiSelectionBasicStorage::Contains(ImGuiID id) const +{ + return _Storage.GetInt(id, 0) != 0; +} + +static int IMGUI_CDECL PairComparerByValueInt(const void* lhs, const void* rhs) +{ + int lhs_v = ((const ImGuiStoragePair*)lhs)->val_i; + int rhs_v = ((const ImGuiStoragePair*)rhs)->val_i; + return (lhs_v > rhs_v ? +1 : lhs_v < rhs_v ? -1 : 0); +} + +// GetNextSelectedItem() is an abstraction allowing us to change our underlying actual storage system without impacting user. +// (e.g. store unselected vs compact down, compact down on demand, use raw ImVector instead of ImGuiStorage...) +bool ImGuiSelectionBasicStorage::GetNextSelectedItem(void** opaque_it, ImGuiID* out_id) +{ + ImGuiStoragePair* it = (ImGuiStoragePair*)*opaque_it; + ImGuiStoragePair* it_end = _Storage.Data.Data + _Storage.Data.Size; + if (PreserveOrder && it == NULL && it_end != NULL) + ImQsort(_Storage.Data.Data, (size_t)_Storage.Data.Size, sizeof(ImGuiStoragePair), PairComparerByValueInt); // ~ImGuiStorage::BuildSortByValueInt() + if (it == NULL) + it = _Storage.Data.Data; + IM_ASSERT(it >= _Storage.Data.Data && it <= it_end); + if (it != it_end) + while (it->val_i == 0 && it < it_end) + it++; + const bool has_more = (it != it_end); + *opaque_it = has_more ? (void**)(it + 1) : (void**)(it); + *out_id = has_more ? it->key : 0; + if (PreserveOrder && !has_more) + _Storage.BuildSortByKey(); + return has_more; +} + +void ImGuiSelectionBasicStorage::SetItemSelected(ImGuiID id, bool selected) +{ + int* p_int = _Storage.GetIntRef(id, 0); + if (selected && *p_int == 0) { *p_int = _SelectionOrder++; Size++; } + else if (!selected && *p_int != 0) { *p_int = 0; Size--; } +} + +// Optimized for batch edits (with same value of 'selected') +static void ImGuiSelectionBasicStorage_BatchSetItemSelected(ImGuiSelectionBasicStorage* selection, ImGuiID id, bool selected, int size_before_amends, int selection_order) +{ + ImGuiStorage* storage = &selection->_Storage; + ImGuiStoragePair* it = ImLowerBound(storage->Data.Data, storage->Data.Data + size_before_amends, id); + const bool is_contained = (it != storage->Data.Data + size_before_amends) && (it->key == id); + if (selected == (is_contained && it->val_i != 0)) + return; + if (selected && !is_contained) + storage->Data.push_back(ImGuiStoragePair(id, selection_order)); // Push unsorted at end of vector, will be sorted in SelectionMultiAmendsFinish() + else if (is_contained) + it->val_i = selected ? selection_order : 0; // Modify in-place. + selection->Size += selected ? +1 : -1; +} + +static void ImGuiSelectionBasicStorage_BatchFinish(ImGuiSelectionBasicStorage* selection, bool selected, int size_before_amends) +{ + ImGuiStorage* storage = &selection->_Storage; + if (selected && selection->Size != size_before_amends) + storage->BuildSortByKey(); // When done selecting: sort everything +} + +// Apply requests coming from BeginMultiSelect() and EndMultiSelect(). +// - Enable 'Demo->Tools->Debug Log->Selection' to see selection requests as they happen. +// - Honoring SetRange requests requires that you can iterate/interpolate between RangeFirstItem and RangeLastItem. +// - In this demo we often submit indices to SetNextItemSelectionUserData() + store the same indices in persistent selection. +// - Your code may do differently. If you store pointers or objects ID in ImGuiSelectionUserData you may need to perform +// a lookup in order to have some way to iterate/interpolate between two items. +// - A full-featured application is likely to allow search/filtering which is likely to lead to using indices +// and constructing a view index <> object id/ptr data structure anyway. +// WHEN YOUR APPLICATION SETTLES ON A CHOICE, YOU WILL PROBABLY PREFER TO GET RID OF THIS UNNECESSARY 'ImGuiSelectionBasicStorage' INDIRECTION LOGIC. +// Notice that with the simplest adapter (using indices everywhere), all functions return their parameters. +// The most simple implementation (using indices everywhere) would look like: +// for (ImGuiSelectionRequest& req : ms_io->Requests) +// { +// if (req.Type == ImGuiSelectionRequestType_SetAll) { Clear(); if (req.Selected) { for (int n = 0; n < items_count; n++) { SetItemSelected(n, true); } } +// if (req.Type == ImGuiSelectionRequestType_SetRange) { for (int n = (int)ms_io->RangeFirstItem; n <= (int)ms_io->RangeLastItem; n++) { SetItemSelected(n, ms_io->Selected); } } +// } +void ImGuiSelectionBasicStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) +{ + // For convenience we obtain ItemsCount as passed to BeginMultiSelect(), which is optional. + // It makes sense when using ImGuiSelectionBasicStorage to simply pass your items count to BeginMultiSelect(). + // Other scheme may handle SetAll differently. + IM_ASSERT(ms_io->ItemsCount != -1 && "Missing value for items_count in BeginMultiSelect() call!"); + IM_ASSERT(AdapterIndexToStorageId != NULL); + + // This is optimized/specialized to cope with very large selections (e.g. 100k+ items) + // - A simpler version could call SetItemSelected() directly instead of ImGuiSelectionBasicStorage_BatchSetItemSelected() + ImGuiSelectionBasicStorage_BatchFinish(). + // - Optimized select can append unsorted, then sort in a second pass. Optimized unselect can clear in-place then compact in a second pass. + // - A more optimal version wouldn't even use ImGuiStorage but directly a ImVector to reduce bandwidth, but this is a reasonable trade off to reuse code. + // - There are many ways this could be better optimized. The worse case scenario being: using BoxSelect2d in a grid, box-select scrolling down while wiggling + // left and right: it affects coarse clipping + can emit multiple SetRange with 1 item each.) + // FIXME-OPT: For each block of consecutive SetRange request: + // - add all requests to a sorted list, store ID, selected, offset in ImGuiStorage. + // - rewrite sorted storage a single time. + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) + { + Clear(); + if (req.Selected) + { + _Storage.Data.reserve(ms_io->ItemsCount); + const int size_before_amends = _Storage.Data.Size; + for (int idx = 0; idx < ms_io->ItemsCount; idx++, _SelectionOrder++) + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, _SelectionOrder); + ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); + } + } + else if (req.Type == ImGuiSelectionRequestType_SetRange) + { + const int selection_changes = (int)req.RangeLastItem - (int)req.RangeFirstItem + 1; + //ImGuiContext& g = *GImGui; IMGUI_DEBUG_LOG_SELECTION("Req %d/%d: set %d to %d\n", ms_io->Requests.index_from_ptr(&req), ms_io->Requests.Size, selection_changes, req.Selected); + if (selection_changes == 1 || (selection_changes < Size / 100)) + { + // Multiple sorted insertion + copy likely to be faster. + // Technically we could do a single copy with a little more work (sort sequential SetRange requests) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + SetItemSelected(GetStorageIdFromIndex(idx), req.Selected); + } + else + { + // Append insertion + single sort likely be faster. + // Use req.RangeDirection to set order field so that shift+clicking from 1 to 5 is different than shift+clicking from 5 to 1 + const int size_before_amends = _Storage.Data.Size; + int selection_order = _SelectionOrder + ((req.RangeDirection < 0) ? selection_changes - 1 : 0); + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++, selection_order += req.RangeDirection) + ImGuiSelectionBasicStorage_BatchSetItemSelected(this, GetStorageIdFromIndex(idx), req.Selected, size_before_amends, selection_order); + if (req.Selected) + _SelectionOrder += selection_changes; + ImGuiSelectionBasicStorage_BatchFinish(this, req.Selected, size_before_amends); + } + } + } +} + +//------------------------------------------------------------------------- + +ImGuiSelectionExternalStorage::ImGuiSelectionExternalStorage() +{ + UserData = NULL; + AdapterSetItemSelected = NULL; +} + +// Apply requests coming from BeginMultiSelect() and EndMultiSelect(). +// We also pull 'ms_io->ItemsCount' as passed for BeginMultiSelect() for consistency with ImGuiSelectionBasicStorage +// This makes no assumption about underlying storage. +void ImGuiSelectionExternalStorage::ApplyRequests(ImGuiMultiSelectIO* ms_io) +{ + IM_ASSERT(AdapterSetItemSelected); + for (ImGuiSelectionRequest& req : ms_io->Requests) + { + if (req.Type == ImGuiSelectionRequestType_SetAll) + for (int idx = 0; idx < ms_io->ItemsCount; idx++) + AdapterSetItemSelected(this, idx, req.Selected); + if (req.Type == ImGuiSelectionRequestType_SetRange) + for (int idx = (int)req.RangeFirstItem; idx <= (int)req.RangeLastItem; idx++) + AdapterSetItemSelected(this, idx, req.Selected); + } +} + //------------------------------------------------------------------------- // [SECTION] Widgets: ListBox //------------------------------------------------------------------------- +// - BeginListBox() +// - EndListBox() // - ListBox() -// - ListBoxHeader() -// - ListBoxFooter() -//------------------------------------------------------------------------- -// FIXME: This is an old API. We should redesign some of it, rename ListBoxHeader->BeginListBox, ListBoxFooter->EndListBox -// and promote using them over existing ListBox() functions, similarly to change with combo boxes. //------------------------------------------------------------------------- -// FIXME: In principle this function should be called BeginListBox(). We should rename it after re-evaluating if we want to keep the same signature. -// Helper to calculate the size of a listbox and display a label on the right. -// Tip: To have a list filling the entire window width, PushItemWidth(-1) and pass an non-visible label e.g. "##empty" -bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg) +// This is essentially a thin wrapper to using BeginChild/EndChild with the ImGuiChildFlags_FrameStyle flag for stylistic changes + displaying a label. +// This handle some subtleties with capturing info from the label. +// If you don't need a label you can pretty much directly use ImGui::BeginChild() with ImGuiChildFlags_FrameStyle. +// Tip: To have a list filling the entire window width, use size.x = -FLT_MIN and pass an non-visible label e.g. "##empty" +// Tip: If your vertical size is calculated from an item count (e.g. 10 * item_height) consider adding a fractional part to facilitate seeing scrolling boundaries (e.g. 10.5f * item_height). +bool ImGui::BeginListBox(const char* label, const ImVec2& size_arg) { ImGuiContext& g = *GImGui; ImGuiWindow* window = GetCurrentWindow(); @@ -5628,63 +8311,45 @@ bool ImGui::ListBoxHeader(const char* label, const ImVec2& size_arg) const ImGuiID id = GetID(label); const ImVec2 label_size = CalcTextSize(label, NULL, true); - // Size default to hold ~7 items. Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. - ImVec2 size = CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.4f + style.ItemSpacing.y); + // Size default to hold ~7.25 items. + // Fractional number of items helps seeing that we can scroll down/up without looking at scrollbar. + ImVec2 size = ImTrunc(CalcItemSize(size_arg, CalcItemWidth(), GetTextLineHeightWithSpacing() * 7.25f + style.FramePadding.y * 2.0f)); ImVec2 frame_size = ImVec2(size.x, ImMax(size.y, label_size.y)); ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); ImRect bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f)); - window->DC.LastItemRect = bb; // Forward storage for ListBoxFooter.. dodgy. g.NextItemData.ClearFlags(); if (!IsRectVisible(bb.Min, bb.Max)) { ItemSize(bb.GetSize(), style.FramePadding.y); ItemAdd(bb, 0, &frame_bb); + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values return false; } + // FIXME-OPT: We could omit the BeginGroup() if label_size.x == 0.0f but would need to omit the EndGroup() as well. BeginGroup(); - if (label_size.x > 0) - RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label); + if (label_size.x > 0.0f) + { + ImVec2 label_pos = ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y); + RenderText(label_pos, label); + window->DC.CursorMaxPos = ImMax(window->DC.CursorMaxPos, label_pos + label_size); + AlignTextToFramePadding(); + } - BeginChildFrame(id, frame_bb.GetSize()); + BeginChild(id, frame_bb.GetSize(), ImGuiChildFlags_FrameStyle); return true; } -// FIXME: In principle this function should be called EndListBox(). We should rename it after re-evaluating if we want to keep the same signature. -bool ImGui::ListBoxHeader(const char* label, int items_count, int height_in_items) +void ImGui::EndListBox() { - // Size default to hold ~7.25 items. - // We add +25% worth of item height to allow the user to see at a glance if there are more items up/down, without looking at the scrollbar. - // We don't add this extra bit if items_count <= height_in_items. It is slightly dodgy, because it means a dynamic list of items will make the widget resize occasionally when it crosses that size. - // I am expecting that someone will come and complain about this behavior in a remote future, then we can advise on a better solution. - if (height_in_items < 0) - height_in_items = ImMin(items_count, 7); - const ImGuiStyle& style = GetStyle(); - float height_in_items_f = (height_in_items < items_count) ? (height_in_items + 0.25f) : (height_in_items + 0.00f); + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + IM_ASSERT((window->Flags & ImGuiWindowFlags_ChildWindow) && "Mismatched BeginListBox/EndListBox calls. Did you test the return value of BeginListBox?"); + IM_UNUSED(window); - // We include ItemSpacing.y so that a list sized for the exact number of items doesn't make a scrollbar appears. We could also enforce that by passing a flag to BeginChild(). - ImVec2 size; - size.x = 0.0f; - size.y = ImFloor(GetTextLineHeightWithSpacing() * height_in_items_f + style.FramePadding.y * 2.0f); - return ListBoxHeader(label, size); -} - -// FIXME: In principle this function should be called EndListBox(). We should rename it after re-evaluating if we want to keep the same signature. -void ImGui::ListBoxFooter() -{ - ImGuiWindow* parent_window = GetCurrentWindow()->ParentWindow; - const ImRect bb = parent_window->DC.LastItemRect; - const ImGuiStyle& style = GetStyle(); - - EndChildFrame(); - - // Redeclare item size so that it includes the label (we have stored the full size in LastItemRect) - // We call SameLine() to restore DC.CurrentLine* data - SameLine(); - parent_window->DC.CursorPos = bb.Min; - ItemSize(bb, style.FramePadding.y); - EndGroup(); + EndChild(); + EndGroup(); // This is only required to be able to do IsItemXXX query on the whole ListBox including label } bool ImGui::ListBox(const char* label, int* current_item, const char* const items[], int items_count, int height_items) @@ -5693,24 +8358,36 @@ bool ImGui::ListBox(const char* label, int* current_item, const char* const item return value_changed; } -bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(void*, int, const char**), void* data, int items_count, int height_in_items) +// This is merely a helper around BeginListBox(), EndListBox(). +// Considering using those directly to submit custom data or store selection differently. +bool ImGui::ListBox(const char* label, int* current_item, const char* (*getter)(void* user_data, int idx), void* user_data, int items_count, int height_in_items) { - if (!ListBoxHeader(label, items_count, height_in_items)) + ImGuiContext& g = *GImGui; + + // Calculate size from "height_in_items" + if (height_in_items < 0) + height_in_items = ImMin(items_count, 7); + float height_in_items_f = height_in_items + 0.25f; + ImVec2 size(0.0f, ImTrunc(GetTextLineHeightWithSpacing() * height_in_items_f + g.Style.FramePadding.y * 2.0f)); + + if (!BeginListBox(label, size)) return false; - // Assume all items have even height (= 1 line of text). If you need items of different or variable sizes you can create a custom version of ListBox() in your code without using the clipper. - ImGuiContext& g = *GImGui; + // Assume all items have even height (= 1 line of text). If you need items of different height, + // you can create a custom version of ListBox() in your code without using the clipper. bool value_changed = false; - ImGuiListClipper clipper(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. + ImGuiListClipper clipper; + clipper.Begin(items_count, GetTextLineHeightWithSpacing()); // We know exactly our line height here so we pass it as a minor optimization, but generally you don't need to. + clipper.IncludeItemByIndex(*current_item); while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - const bool item_selected = (i == *current_item); - const char* item_text; - if (!items_getter(data, i, &item_text)) + const char* item_text = getter(user_data, i); + if (item_text == NULL) item_text = "*Unknown item*"; PushID(i); + const bool item_selected = (i == *current_item); if (Selectable(item_text, item_selected)) { *current_item = i; @@ -5720,9 +8397,10 @@ bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(v SetItemDefaultFocus(); PopID(); } - ListBoxFooter(); + EndListBox(); + if (value_changed) - MarkItemEdited(g.CurrentWindow->DC.LastItemId); + MarkItemEdited(g.LastItemData.ID); return value_changed; } @@ -5734,30 +8412,33 @@ bool ImGui::ListBox(const char* label, int* current_item, bool (*items_getter)(v // - PlotLines() // - PlotHistogram() //------------------------------------------------------------------------- +// Plot/Graph widgets are not very good. +// Consider writing your own, or using a third-party one, see: +// - ImPlot https://github.com/epezent/implot +// - others https://github.com/ocornut/imgui/wiki/Useful-Extensions +//------------------------------------------------------------------------- -void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, ImVec2 frame_size) +int ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_getter)(void* data, int idx), void* data, int values_count, int values_offset, const char* overlay_text, float scale_min, float scale_max, const ImVec2& size_arg) { + ImGuiContext& g = *GImGui; ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) - return; + return -1; - ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); const ImVec2 label_size = CalcTextSize(label, NULL, true); - if (frame_size.x == 0.0f) - frame_size.x = CalcItemWidth(); - if (frame_size.y == 0.0f) - frame_size.y = label_size.y + (style.FramePadding.y * 2); + const ImVec2 frame_size = CalcItemSize(size_arg, CalcItemWidth(), label_size.y + style.FramePadding.y * 2.0f); const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + frame_size); const ImRect inner_bb(frame_bb.Min + style.FramePadding, frame_bb.Max - style.FramePadding); const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0)); ItemSize(total_bb, style.FramePadding.y); - if (!ItemAdd(total_bb, 0, &frame_bb)) - return; - const bool hovered = ItemHoverable(frame_bb, id); + if (!ItemAdd(total_bb, id, &frame_bb, ImGuiItemFlags_NoNav)) + return -1; + bool hovered; + ButtonBehavior(frame_bb, id, &hovered, NULL); // Determine scale from values if not specified if (scale_min == FLT_MAX || scale_max == FLT_MAX) @@ -5781,13 +8462,13 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge RenderFrame(frame_bb.Min, frame_bb.Max, GetColorU32(ImGuiCol_FrameBg), true, style.FrameRounding); const int values_count_min = (plot_type == ImGuiPlotType_Lines) ? 2 : 1; + int idx_hovered = -1; if (values_count >= values_count_min) { int res_w = ImMin((int)frame_size.x, values_count) + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); int item_count = values_count + ((plot_type == ImGuiPlotType_Lines) ? -1 : 0); // Tooltip on hover - int v_hovered = -1; if (hovered && inner_bb.Contains(g.IO.MousePos)) { const float t = ImClamp((g.IO.MousePos.x - inner_bb.Min.x) / (inner_bb.Max.x - inner_bb.Min.x), 0.0f, 0.9999f); @@ -5797,10 +8478,10 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge const float v0 = values_getter(data, (v_idx + values_offset) % values_count); const float v1 = values_getter(data, (v_idx + 1 + values_offset) % values_count); if (plot_type == ImGuiPlotType_Lines) - SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx+1, v1); + SetTooltip("%d: %8.4g\n%d: %8.4g", v_idx, v0, v_idx + 1, v1); else if (plot_type == ImGuiPlotType_Histogram) SetTooltip("%d: %8.4g", v_idx, v0); - v_hovered = v_idx; + idx_hovered = v_idx; } const float t_step = 1.0f / (float)res_w; @@ -5809,7 +8490,7 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge float v0 = values_getter(data, (0 + values_offset) % values_count); float t0 = 0.0f; ImVec2 tp0 = ImVec2( t0, 1.0f - ImSaturate((v0 - scale_min) * inv_scale) ); // Point in the normalized space of our target rectangle - float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (-scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands + float histogram_zero_line_t = (scale_min * scale_max < 0.0f) ? (1 + scale_min * inv_scale) : (scale_min < 0.0f ? 0.0f : 1.0f); // Where does the zero line stands const ImU32 col_base = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLines : ImGuiCol_PlotHistogram); const ImU32 col_hovered = GetColorU32((plot_type == ImGuiPlotType_Lines) ? ImGuiCol_PlotLinesHovered : ImGuiCol_PlotHistogramHovered); @@ -5827,13 +8508,13 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge ImVec2 pos1 = ImLerp(inner_bb.Min, inner_bb.Max, (plot_type == ImGuiPlotType_Lines) ? tp1 : ImVec2(tp1.x, histogram_zero_line_t)); if (plot_type == ImGuiPlotType_Lines) { - window->DrawList->AddLine(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base); + window->DrawList->AddLine(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); } else if (plot_type == ImGuiPlotType_Histogram) { if (pos1.x >= pos0.x + 2.0f) pos1.x -= 1.0f; - window->DrawList->AddRectFilled(pos0, pos1, v_hovered == v1_idx ? col_hovered : col_base); + window->DrawList->AddRectFilled(pos0, pos1, idx_hovered == v1_idx ? col_hovered : col_base); } t0 = t1; @@ -5843,10 +8524,14 @@ void ImGui::PlotEx(ImGuiPlotType plot_type, const char* label, float (*values_ge // Text overlay if (overlay_text) - RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f,0.0f)); + RenderTextClipped(ImVec2(frame_bb.Min.x, frame_bb.Min.y + style.FramePadding.y), frame_bb.Max, overlay_text, NULL, NULL, ImVec2(0.5f, 0.0f)); if (label_size.x > 0.0f) RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, inner_bb.Min.y), label); + + // Return hovered index or -1 if none are hovered. + // This is currently not exposed in the public API because we need a larger redesign of the whole thing, but in the short-term we are making it available in PlotEx(). + return idx_hovered; } struct ImGuiPlotArrayGetterData @@ -5926,89 +8611,57 @@ void ImGui::Value(const char* prefix, float v, const char* float_format) // [SECTION] MenuItem, BeginMenu, EndMenu, etc. //------------------------------------------------------------------------- // - ImGuiMenuColumns [Internal] -// - BeginMainMenuBar() -// - EndMainMenuBar() // - BeginMenuBar() // - EndMenuBar() +// - BeginMainMenuBar() +// - EndMainMenuBar() // - BeginMenu() // - EndMenu() +// - MenuItemEx() [Internal] // - MenuItem() //------------------------------------------------------------------------- // Helpers for internal use -ImGuiMenuColumns::ImGuiMenuColumns() +void ImGuiMenuColumns::Update(float spacing, bool window_reappearing) { - Spacing = Width = NextWidth = 0.0f; - memset(Pos, 0, sizeof(Pos)); - memset(NextWidths, 0, sizeof(NextWidths)); + if (window_reappearing) + memset(Widths, 0, sizeof(Widths)); + Spacing = (ImU16)spacing; + CalcNextTotalWidth(true); + memset(Widths, 0, sizeof(Widths)); + TotalWidth = NextTotalWidth; + NextTotalWidth = 0; } -void ImGuiMenuColumns::Update(int count, float spacing, bool clear) +void ImGuiMenuColumns::CalcNextTotalWidth(bool update_offsets) { - IM_ASSERT(count == IM_ARRAYSIZE(Pos)); - IM_UNUSED(count); - Width = NextWidth = 0.0f; - Spacing = spacing; - if (clear) - memset(NextWidths, 0, sizeof(NextWidths)); - for (int i = 0; i < IM_ARRAYSIZE(Pos); i++) + ImU16 offset = 0; + bool want_spacing = false; + for (int i = 0; i < IM_ARRAYSIZE(Widths); i++) { - if (i > 0 && NextWidths[i] > 0.0f) - Width += Spacing; - Pos[i] = (float)(int)Width; - Width += NextWidths[i]; - NextWidths[i] = 0.0f; + ImU16 width = Widths[i]; + if (want_spacing && width > 0) + offset += Spacing; + want_spacing |= (width > 0); + if (update_offsets) + { + if (i == 1) { OffsetLabel = offset; } + if (i == 2) { OffsetShortcut = offset; } + if (i == 3) { OffsetMark = offset; } + } + offset += width; } + NextTotalWidth = offset; } -float ImGuiMenuColumns::DeclColumns(float w0, float w1, float w2) // not using va_arg because they promote float to double +float ImGuiMenuColumns::DeclColumns(float w_icon, float w_label, float w_shortcut, float w_mark) { - NextWidth = 0.0f; - NextWidths[0] = ImMax(NextWidths[0], w0); - NextWidths[1] = ImMax(NextWidths[1], w1); - NextWidths[2] = ImMax(NextWidths[2], w2); - for (int i = 0; i < IM_ARRAYSIZE(Pos); i++) - NextWidth += NextWidths[i] + ((i > 0 && NextWidths[i] > 0.0f) ? Spacing : 0.0f); - return ImMax(Width, NextWidth); -} - -float ImGuiMenuColumns::CalcExtraSpace(float avail_w) -{ - return ImMax(0.0f, avail_w - Width); -} - -// For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set. -bool ImGui::BeginMainMenuBar() -{ - ImGuiContext& g = *GImGui; - g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f)); - SetNextWindowPos(ImVec2(0.0f, 0.0f)); - SetNextWindowSize(ImVec2(g.IO.DisplaySize.x, g.NextWindowData.MenuBarOffsetMinVal.y + g.FontBaseSize + g.Style.FramePadding.y)); - PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); - PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0,0)); - ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; - bool is_open = Begin("##MainMenuBar", NULL, window_flags) && BeginMenuBar(); - PopStyleVar(2); - g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f); - if (!is_open) - { - End(); - return false; - } - return true; //-V1020 -} - -void ImGui::EndMainMenuBar() -{ - EndMenuBar(); - - // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window - // FIXME: With this strategy we won't be able to restore a NULL focus. - ImGuiContext& g = *GImGui; - if (g.CurrentWindow == g.NavWindow && g.NavLayer == 0 && !g.NavAnyRequest) - FocusTopMostWindowUnderOne(g.NavWindow, NULL); - - End(); + Widths[0] = ImMax(Widths[0], (ImU16)w_icon); + Widths[1] = ImMax(Widths[1], (ImU16)w_label); + Widths[2] = ImMax(Widths[2], (ImU16)w_shortcut); + Widths[3] = ImMax(Widths[3], (ImU16)w_mark); + CalcNextTotalWidth(false); + return (float)ImMax(TotalWidth, NextTotalWidth); } // FIXME: Provided a rectangle perhaps e.g. a BeginMenuBarEx() could be used anywhere.. @@ -6025,19 +8678,22 @@ bool ImGui::BeginMenuBar() IM_ASSERT(!window->DC.MenuBarAppending); BeginGroup(); // Backup position on layer 0 // FIXME: Misleading to use a group for that backup/restore - PushID("##menubar"); + PushID("##MenuBar"); // We don't clip with current window clipping rectangle as it is already set to the area below. However we clip with window full rect. // We remove 1 worth of rounding to Max.x to that text in long menus and small windows don't tend to display over the lower-right rounded area, which looks particularly glitchy. + const float border_top = ImMax(IM_ROUND(window->WindowBorderSize * 0.5f - window->TitleBarHeight), 0.0f); + const float border_half = IM_ROUND(window->WindowBorderSize * 0.5f); ImRect bar_rect = window->MenuBarRect(); - ImRect clip_rect(ImFloor(bar_rect.Min.x + 0.5f), ImFloor(bar_rect.Min.y + window->WindowBorderSize + 0.5f), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - window->WindowRounding) + 0.5f), ImFloor(bar_rect.Max.y + 0.5f)); + ImRect clip_rect(ImFloor(bar_rect.Min.x + border_half), ImFloor(bar_rect.Min.y + border_top), ImFloor(ImMax(bar_rect.Min.x, bar_rect.Max.x - ImMax(window->WindowRounding, border_half))), ImFloor(bar_rect.Max.y)); clip_rect.ClipWith(window->OuterRectClipped); PushClipRect(clip_rect.Min, clip_rect.Max, false); - window->DC.CursorPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y); + // We overwrite CursorMaxPos because BeginGroup sets it to CursorPos (essentially the .EmitItem hack in EndMenuBar() would need something analogous here, maybe a BeginGroupEx() with flags). + window->DC.CursorPos = window->DC.CursorMaxPos = ImVec2(bar_rect.Min.x + window->DC.MenuBarOffset.x, bar_rect.Min.y + window->DC.MenuBarOffset.y); window->DC.LayoutType = ImGuiLayoutType_Horizontal; + window->DC.IsSameLine = false; window->DC.NavLayerCurrent = ImGuiNavLayer_Menu; - window->DC.NavLayerCurrentMask = (1 << ImGuiNavLayer_Menu); window->DC.MenuBarAppending = true; AlignTextToFramePadding(); return true; @@ -6050,41 +8706,165 @@ void ImGui::EndMenuBar() return; ImGuiContext& g = *GImGui; + IM_MSVC_WARNING_SUPPRESS(6011); // Static Analysis false positive "warning C6011: Dereferencing NULL pointer 'window'" + IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); + IM_ASSERT(window->DC.MenuBarAppending); + // Nav: When a move request within one of our child menu failed, capture the request to navigate among our siblings. if (NavMoveRequestButNoResultYet() && (g.NavMoveDir == ImGuiDir_Left || g.NavMoveDir == ImGuiDir_Right) && (g.NavWindow->Flags & ImGuiWindowFlags_ChildMenu)) { + // Try to find out if the request is for one of our child menu ImGuiWindow* nav_earliest_child = g.NavWindow; while (nav_earliest_child->ParentWindow && (nav_earliest_child->ParentWindow->Flags & ImGuiWindowFlags_ChildMenu)) nav_earliest_child = nav_earliest_child->ParentWindow; - if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && g.NavMoveRequestForward == ImGuiNavForward_None) + if (nav_earliest_child->ParentWindow == window && nav_earliest_child->DC.ParentLayoutType == ImGuiLayoutType_Horizontal && (g.NavMoveFlags & ImGuiNavMoveFlags_Forwarded) == 0) { // To do so we claim focus back, restore NavId and then process the movement request for yet another frame. - // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth the hassle/cost) + // This involve a one-frame delay which isn't very problematic in this situation. We could remove it by scoring in advance for multiple window (probably not worth bothering) const ImGuiNavLayer layer = ImGuiNavLayer_Menu; - IM_ASSERT(window->DC.NavLayerActiveMaskNext & (1 << layer)); // Sanity check + IM_ASSERT(window->DC.NavLayersActiveMaskNext & (1 << layer)); // Sanity check (FIXME: Seems unnecessary) FocusWindow(window); - SetNavIDWithRectRel(window->NavLastIds[layer], layer, window->NavRectRel[layer]); - g.NavLayer = layer; - g.NavDisableHighlight = true; // Hide highlight for the current frame so we don't see the intermediary selection. - g.NavMoveRequestForward = ImGuiNavForward_ForwardQueued; - NavMoveRequestCancel(); + SetNavID(window->NavLastIds[layer], layer, 0, window->NavRectRel[layer]); + // FIXME-NAV: How to deal with this when not using g.IO.ConfigNavCursorVisibleAuto? + if (g.NavCursorVisible) + { + g.NavCursorVisible = false; // Hide nav cursor for the current frame so we don't see the intermediary selection. Will be set again + g.NavCursorHideFrames = 2; + } + g.NavHighlightItemUnderNav = g.NavMousePosDirty = true; + NavMoveRequestForward(g.NavMoveDir, g.NavMoveClipDir, g.NavMoveFlags, g.NavMoveScrollFlags); // Repeat } } - IM_ASSERT(window->Flags & ImGuiWindowFlags_MenuBar); - IM_ASSERT(window->DC.MenuBarAppending); PopClipRect(); PopID(); - window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->MenuBarRect().Min.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos. - window->DC.GroupStack.back().EmitItem = false; - EndGroup(); // Restore position on layer 0 + window->DC.MenuBarOffset.x = window->DC.CursorPos.x - window->Pos.x; // Save horizontal position so next append can reuse it. This is kinda equivalent to a per-layer CursorPos. + + // FIXME: Extremely confusing, cleanup by (a) working on WorkRect stack system (b) not using a Group confusingly here. + ImGuiGroupData& group_data = g.GroupStack.back(); + group_data.EmitItem = false; + ImVec2 restore_cursor_max_pos = group_data.BackupCursorMaxPos; + window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, window->DC.CursorMaxPos.x - window->Scroll.x); // Convert ideal extents for scrolling layer equivalent. + EndGroup(); // Restore position on layer 0 // FIXME: Misleading to use a group for that backup/restore window->DC.LayoutType = ImGuiLayoutType_Vertical; + window->DC.IsSameLine = false; window->DC.NavLayerCurrent = ImGuiNavLayer_Main; - window->DC.NavLayerCurrentMask = (1 << ImGuiNavLayer_Main); window->DC.MenuBarAppending = false; + window->DC.CursorMaxPos = restore_cursor_max_pos; } -bool ImGui::BeginMenu(const char* label, bool enabled) +// Important: calling order matters! +// FIXME: Somehow overlapping with docking tech. +// FIXME: The "rect-cut" aspect of this could be formalized into a lower-level helper (rect-cut: https://halt.software/dead-simple-layouts) +bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, ImGuiDir dir, float axis_size, ImGuiWindowFlags window_flags) +{ + IM_ASSERT(dir != ImGuiDir_None); + + ImGuiWindow* bar_window = FindWindowByName(name); + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)(viewport_p ? viewport_p : GetMainViewport()); + if (bar_window == NULL || bar_window->BeginCount == 0) + { + // Calculate and set window size/position + ImRect avail_rect = viewport->GetBuildWorkRect(); + ImGuiAxis axis = (dir == ImGuiDir_Up || dir == ImGuiDir_Down) ? ImGuiAxis_Y : ImGuiAxis_X; + ImVec2 pos = avail_rect.Min; + if (dir == ImGuiDir_Right || dir == ImGuiDir_Down) + pos[axis] = avail_rect.Max[axis] - axis_size; + ImVec2 size = avail_rect.GetSize(); + size[axis] = axis_size; + SetNextWindowPos(pos); + SetNextWindowSize(size); + + // Report our size into work area (for next frame) using actual window size + if (dir == ImGuiDir_Up || dir == ImGuiDir_Left) + viewport->BuildWorkInsetMin[axis] += axis_size; + else if (dir == ImGuiDir_Down || dir == ImGuiDir_Right) + viewport->BuildWorkInsetMax[axis] += axis_size; + } + + window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDocking; + SetNextWindowViewport(viewport->ID); // Enforce viewport so we don't create our own viewport when ImGuiConfigFlags_ViewportsNoMerge is set. + PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); + PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint + bool is_open = Begin(name, NULL, window_flags); + PopStyleVar(2); + + return is_open; +} + +bool ImGui::BeginMainMenuBar() +{ + ImGuiContext& g = *GImGui; + ImGuiViewportP* viewport = (ImGuiViewportP*)(void*)GetMainViewport(); + + // Notify of viewport change so GetFrameHeight() can be accurate in case of DPI change + SetCurrentViewport(NULL, viewport); + + // For the main menu bar, which cannot be moved, we honor g.Style.DisplaySafeAreaPadding to ensure text can be visible on a TV set. + // FIXME: This could be generalized as an opt-in way to clamp window->DC.CursorStartPos to avoid SafeArea? + // FIXME: Consider removing support for safe area down the line... it's messy. Nowadays consoles have support for TV calibration in OS settings. + g.NextWindowData.MenuBarOffsetMinVal = ImVec2(g.Style.DisplaySafeAreaPadding.x, ImMax(g.Style.DisplaySafeAreaPadding.y - g.Style.FramePadding.y, 0.0f)); + ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_MenuBar; + float height = GetFrameHeight(); + bool is_open = BeginViewportSideBar("##MainMenuBar", viewport, ImGuiDir_Up, height, window_flags); + g.NextWindowData.MenuBarOffsetMinVal = ImVec2(0.0f, 0.0f); + if (!is_open) + { + End(); + return false; + } + + // Temporarily disable _NoSavedSettings, in the off-chance that tables or child windows submitted within the menu-bar may want to use settings. (#8356) + g.CurrentWindow->Flags &= ~ImGuiWindowFlags_NoSavedSettings; + BeginMenuBar(); + return is_open; +} + +void ImGui::EndMainMenuBar() +{ + ImGuiContext& g = *GImGui; + if (!g.CurrentWindow->DC.MenuBarAppending) + { + IM_ASSERT_USER_ERROR(0, "Calling EndMainMenuBar() not from a menu-bar!"); // Not technically testing that it is the main menu bar + return; + } + + EndMenuBar(); + g.CurrentWindow->Flags |= ImGuiWindowFlags_NoSavedSettings; // Restore _NoSavedSettings (#8356) + + // When the user has left the menu layer (typically: closed menus through activation of an item), we restore focus to the previous window + // FIXME: With this strategy we won't be able to restore a NULL focus. + if (g.CurrentWindow == g.NavWindow && g.NavLayer == ImGuiNavLayer_Main && !g.NavAnyRequest && g.ActiveId == 0) + FocusTopMostWindowUnderOne(g.NavWindow, NULL, NULL, ImGuiFocusRequestFlags_UnlessBelowModal | ImGuiFocusRequestFlags_RestoreFocusedChild); + + End(); +} + +static bool IsRootOfOpenMenuSet() +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if ((g.OpenPopupStack.Size <= g.BeginPopupStack.Size) || (window->Flags & ImGuiWindowFlags_ChildMenu)) + return false; + + // Initially we used 'upper_popup->OpenParentId == window->IDStack.back()' to differentiate multiple menu sets from each others + // (e.g. inside menu bar vs loose menu items) based on parent ID. + // This would however prevent the use of e.g. PushID() user code submitting menus. + // Previously this worked between popup and a first child menu because the first child menu always had the _ChildWindow flag, + // making hovering on parent popup possible while first child menu was focused - but this was generally a bug with other side effects. + // Instead we don't treat Popup specifically (in order to consistently support menu features in them), maybe the first child menu of a Popup + // doesn't have the _ChildWindow flag, and we rely on this IsRootOfOpenMenuSet() check to allow hovering between root window/popup and first child menu. + // In the end, lack of ID check made it so we could no longer differentiate between separate menu sets. To compensate for that, we at least check parent window nav layer. + // This fixes the most common case of menu opening on hover when moving between window content and menu bar. Multiple different menu sets in same nav layer would still + // open on hover, but that should be a lesser problem, because if such menus are close in proximity in window content then it won't feel weird and if they are far apart + // it likely won't be a problem anyone runs into. + const ImGuiPopupData* upper_popup = &g.OpenPopupStack[g.BeginPopupStack.Size]; + if (window->DC.NavLayerCurrent != upper_popup->ParentNavLayer) + return false; + return upper_popup->Window && (upper_popup->Window->Flags & ImGuiWindowFlags_ChildMenu) && ImGui::IsWindowChildOf(upper_popup->Window, window, true, false); +} + +bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -6093,88 +8873,137 @@ bool ImGui::BeginMenu(const char* label, bool enabled) ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); + bool menu_is_open = IsPopupOpen(id, ImGuiPopupFlags_None); + + // Sub-menus are ChildWindow so that mouse can be hovering across them (otherwise top-most popup menu would steal focus and not allow hovering on parent menu) + // The first menu in a hierarchy isn't so hovering doesn't get across (otherwise e.g. resizing borders with ImGuiButtonFlags_FlattenChildren would react), but top-most BeginMenu() will bypass that limitation. + ImGuiWindowFlags window_flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus; + if (window->Flags & ImGuiWindowFlags_ChildMenu) + window_flags |= ImGuiWindowFlags_ChildWindow; + + // If a menu with same the ID was already submitted, we will append to it, matching the behavior of Begin(). + // We are relying on a O(N) search - so O(N log N) over the frame - which seems like the most efficient for the expected small amount of BeginMenu() calls per frame. + // If somehow this is ever becoming a problem we can switch to use e.g. ImGuiStorage mapping key to last frame used. + if (g.MenusIdSubmittedThisFrame.contains(id)) + { + if (menu_is_open) + menu_is_open = BeginPopupMenuEx(id, label, window_flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + else + g.NextWindowData.ClearFlags(); // we behave like Begin() and need to consume those values + return menu_is_open; + } + + // Tag menu as used. Next time BeginMenu() with same ID is called it will append to existing menu + g.MenusIdSubmittedThisFrame.push_back(id); ImVec2 label_size = CalcTextSize(label, NULL, true); - bool pressed; - bool menu_is_open = IsPopupOpen(id); - bool menuset_is_open = !(window->Flags & ImGuiWindowFlags_Popup) && (g.OpenPopupStack.Size > g.BeginPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].OpenParentId == window->IDStack.back()); - ImGuiWindow* backed_nav_window = g.NavWindow; + // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent without always being a Child window) + // This is only done for items for the menu set and not the full parent window. + const bool menuset_is_open = IsRootOfOpenMenuSet(); if (menuset_is_open) - g.NavWindow = window; // Odd hack to allow hovering across menus of a same menu-set (otherwise we wouldn't be able to hover parent) + PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true); // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu, - // However the final position is going to be different! It is choosen by FindBestWindowPosForPopup(). + // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup(). // e.g. Menus tend to overlap each other horizontally to amplify relative Z-ordering. ImVec2 popup_pos, pos = window->DC.CursorPos; + PushID(label); + if (!enabled) + BeginDisabled(); + const ImGuiMenuColumns* offsets = &window->DC.MenuColumns; + bool pressed; + + // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. + const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SelectOnClick | ImGuiSelectableFlags_NoAutoClosePopups; if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) { - // Menu inside an horizontal menu bar + // Menu inside a horizontal menu bar // Selectable extend their highlight by half ItemSpacing in each direction. // For ChildMenu, the popup position will be overwritten by the call to FindBestWindowPosForPopup() in Begin() - popup_pos = ImVec2(pos.x - 1.0f - (float)(int)(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight()); - window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * 0.5f); - PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y)); + popup_pos = ImVec2(pos.x - 1.0f - IM_TRUNC(style.ItemSpacing.x * 0.5f), pos.y - style.FramePadding.y + window->MenuBarHeight); + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f); + PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f); float w = label_size.x; - pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_PressedOnClick | ImGuiSelectableFlags_DontClosePopups | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f)); + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + pressed = Selectable("", menu_is_open, selectable_flags, ImVec2(w, label_size.y)); + LogSetNextTextDecoration("[", "]"); + RenderText(text_pos, label); PopStyleVar(); - window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). } else { - // Menu inside a menu + // Menu inside a regular/vertical menu + // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. + // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. popup_pos = ImVec2(pos.x, pos.y - style.WindowPadding.y); - float w = window->MenuColumns.DeclColumns(label_size.x, 0.0f, (float)(int)(g.FontSize * 1.20f)); // Feedback to next frame - float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w); - pressed = Selectable(label, menu_is_open, ImGuiSelectableFlags_NoHoldingActiveID | ImGuiSelectableFlags_PressedOnClick | ImGuiSelectableFlags_DontClosePopups | ImGuiSelectableFlags_DrawFillAvailWidth | (!enabled ? ImGuiSelectableFlags_Disabled : 0), ImVec2(w, 0.0f)); - ImU32 text_col = GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled); - RenderArrow(window->DrawList, pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.30f, 0.0f), text_col, ImGuiDir_Right); + float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; + float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); + float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, 0.0f, checkmark_w); // Feedback to next frame + float extra_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + pressed = Selectable("", menu_is_open, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); + LogSetNextTextDecoration("", ">"); + RenderText(text_pos, label); + if (icon_w > 0.0f) + RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); + RenderArrow(window->DrawList, pos + ImVec2(offsets->OffsetMark + extra_w + g.FontSize * 0.30f, 0.0f), GetColorU32(ImGuiCol_Text), ImGuiDir_Right); } + if (!enabled) + EndDisabled(); - const bool hovered = enabled && ItemHoverable(window->DC.LastItemRect, id); + const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav; if (menuset_is_open) - g.NavWindow = backed_nav_window; + PopItemFlag(); bool want_open = false; + bool want_open_nav_init = false; bool want_close = false; if (window->DC.LayoutType == ImGuiLayoutType_Vertical) // (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) { // Close menu when not hovering it anymore unless we are moving roughly in the direction of the menu // Implement http://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown to avoid using timers, so menus feels more reactive. - bool moving_toward_other_child_menu = false; - - ImGuiWindow* child_menu_window = (g.BeginPopupStack.Size < g.OpenPopupStack.Size && g.OpenPopupStack[g.BeginPopupStack.Size].SourceWindow == window) ? g.OpenPopupStack[g.BeginPopupStack.Size].Window : NULL; - if (g.HoveredWindow == window && child_menu_window != NULL && !(window->Flags & ImGuiWindowFlags_MenuBar)) + bool moving_toward_child_menu = false; + ImGuiPopupData* child_popup = (g.BeginPopupStack.Size < g.OpenPopupStack.Size) ? &g.OpenPopupStack[g.BeginPopupStack.Size] : NULL; // Popup candidate (testing below) + ImGuiWindow* child_menu_window = (child_popup && child_popup->Window && child_popup->Window->ParentWindow == window) ? child_popup->Window : NULL; + if (g.HoveredWindow == window && child_menu_window != NULL) { - // FIXME-DPI: Values should be derived from a master "scale" factor. - ImRect next_window_rect = child_menu_window->Rect(); - ImVec2 ta = g.IO.MousePos - g.IO.MouseDelta; - ImVec2 tb = (window->Pos.x < child_menu_window->Pos.x) ? next_window_rect.GetTL() : next_window_rect.GetTR(); - ImVec2 tc = (window->Pos.x < child_menu_window->Pos.x) ? next_window_rect.GetBL() : next_window_rect.GetBR(); - float extra = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, 5.0f, 30.0f); // add a bit of extra slack. - ta.x += (window->Pos.x < child_menu_window->Pos.x) ? -0.5f : +0.5f; // to avoid numerical issues - tb.y = ta.y + ImMax((tb.y - extra) - ta.y, -100.0f); // triangle is maximum 200 high to limit the slope and the bias toward large sub-menus // FIXME: Multiply by fb_scale? - tc.y = ta.y + ImMin((tc.y + extra) - ta.y, +100.0f); - moving_toward_other_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); - //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_within_opened_triangle ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG] + const float ref_unit = g.FontSize; // FIXME-DPI + const float child_dir = (window->Pos.x < child_menu_window->Pos.x) ? 1.0f : -1.0f; + const ImRect next_window_rect = child_menu_window->Rect(); + ImVec2 ta = (g.IO.MousePos - g.IO.MouseDelta); + ImVec2 tb = (child_dir > 0.0f) ? next_window_rect.GetTL() : next_window_rect.GetTR(); + ImVec2 tc = (child_dir > 0.0f) ? next_window_rect.GetBL() : next_window_rect.GetBR(); + const float pad_farmost_h = ImClamp(ImFabs(ta.x - tb.x) * 0.30f, ref_unit * 0.5f, ref_unit * 2.5f); // Add a bit of extra slack. + ta.x += child_dir * -0.5f; + tb.x += child_dir * ref_unit; + tc.x += child_dir * ref_unit; + tb.y = ta.y + ImMax((tb.y - pad_farmost_h) - ta.y, -ref_unit * 8.0f); // Triangle has maximum height to limit the slope and the bias toward large sub-menus + tc.y = ta.y + ImMin((tc.y + pad_farmost_h) - ta.y, +ref_unit * 8.0f); + moving_toward_child_menu = ImTriangleContainsPoint(ta, tb, tc, g.IO.MousePos); + //GetForegroundDrawList()->AddTriangleFilled(ta, tb, tc, moving_toward_child_menu ? IM_COL32(0,128,0,128) : IM_COL32(128,0,0,128)); // [DEBUG] } - if (menu_is_open && !hovered && g.HoveredWindow == window && g.HoveredIdPreviousFrame != 0 && g.HoveredIdPreviousFrame != id && !moving_toward_other_child_menu) + + // The 'HovereWindow == window' check creates an inconsistency (e.g. moving away from menu slowly tends to hit same window, whereas moving away fast does not) + // But we also need to not close the top-menu menu when moving over void. Perhaps we should extend the triangle check to a larger polygon. + // (Remember to test this on BeginPopup("A")->BeginMenu("B") sequence which behaves slightly differently as B isn't a Child of A and hovering isn't shared.) + if (menu_is_open && !hovered && g.HoveredWindow == window && !moving_toward_child_menu && !g.NavHighlightItemUnderNav && g.ActiveId == 0) want_close = true; - if (!menu_is_open && hovered && pressed) // Click to open + // Open + // (note: at this point 'hovered' actually includes the NavDisableMouseHover == false test) + if (!menu_is_open && pressed) // Click/activate to open want_open = true; - else if (!menu_is_open && hovered && !moving_toward_other_child_menu) // Hover to open + else if (!menu_is_open && hovered && !moving_toward_child_menu) // Hover to open want_open = true; - - if (g.NavActivateId == id) + else if (!menu_is_open && hovered && g.HoveredIdTimer >= 0.30f && g.MouseStationaryTimer >= 0.30f) // Hover to open (timer fallback) + want_open = true; + if (g.NavId == id && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open { - want_close = menu_is_open; - want_open = !menu_is_open; - } - if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Right) // Nav-Right to open - { - want_open = true; + want_open = want_open_nav_init = true; NavMoveRequestCancel(); + SetNavCursorVisibleAfterMove(); } } else @@ -6189,7 +9018,7 @@ bool ImGui::BeginMenu(const char* label, bool enabled) { want_open = true; } - else if (g.NavId == id && g.NavMoveRequest && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open + else if (g.NavId == id && g.NavMoveDir == ImGuiDir_Down) // Nav-Down to open { want_open = true; NavMoveRequestCancel(); @@ -6198,52 +9027,79 @@ bool ImGui::BeginMenu(const char* label, bool enabled) if (!enabled) // explicitly close if an open menu becomes disabled, facilitate users code a lot in pattern such as 'if (BeginMenu("options", has_object)) { ..use object.. }' want_close = true; - if (want_close && IsPopupOpen(id)) + if (want_close && IsPopupOpen(id, ImGuiPopupFlags_None)) ClosePopupToLevel(g.BeginPopupStack.Size, true); - IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0)); + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Openable | (menu_is_open ? ImGuiItemStatusFlags_Opened : 0)); + PopID(); - if (!menu_is_open && want_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size) + if (want_open && !menu_is_open && g.OpenPopupStack.Size > g.BeginPopupStack.Size) { - // Don't recycle same menu level in the same frame, first close the other menu and yield for a frame. + // Don't reopen/recycle same menu level in the same frame if it is a different menu ID, first close the other menu and yield for a frame. OpenPopup(label); - return false; } - - menu_is_open |= want_open; - if (want_open) - OpenPopup(label); + else if (want_open) + { + menu_is_open = true; + OpenPopup(label, ImGuiPopupFlags_NoReopen);// | (want_open_nav_init ? ImGuiPopupFlags_NoReopenAlwaysNavInit : 0)); + } if (menu_is_open) { - // Sub-menus are ChildWindow so that mouse can be hovering across them (otherwise top-most popup menu would steal focus and not allow hovering on parent menu) - SetNextWindowPos(popup_pos, ImGuiCond_Always); - ImGuiWindowFlags flags = ImGuiWindowFlags_ChildMenu | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNavFocus; - if (window->Flags & (ImGuiWindowFlags_Popup|ImGuiWindowFlags_ChildMenu)) - flags |= ImGuiWindowFlags_ChildWindow; - menu_is_open = BeginPopupEx(id, flags); // menu_is_open can be 'false' when the popup is completely clipped (e.g. zero size display) + ImGuiLastItemData last_item_in_parent = g.LastItemData; + SetNextWindowPos(popup_pos, ImGuiCond_Always); // Note: misleading: the value will serve as reference for FindBestWindowPosForPopup(), not actual pos. + PushStyleVar(ImGuiStyleVar_ChildRounding, style.PopupRounding); // First level will use _PopupRounding, subsequent will use _ChildRounding + menu_is_open = BeginPopupMenuEx(id, label, window_flags); // menu_is_open may be 'false' when the popup is completely clipped (e.g. zero size display) + PopStyleVar(); + if (menu_is_open) + { + // Implement what ImGuiPopupFlags_NoReopenAlwaysNavInit would do: + // Perform an init request in the case the popup was already open (via a previous mouse hover) + if (want_open && want_open_nav_init && !g.NavInitRequest) + { + FocusWindow(g.CurrentWindow, ImGuiFocusRequestFlags_UnlessBelowModal); + NavInitWindow(g.CurrentWindow, false); + } + + // Restore LastItemData so IsItemXXXX functions can work after BeginMenu()/EndMenu() + // (This fixes using IsItemClicked() and IsItemHovered(), but IsItemHovered() also relies on its support for ImGuiItemFlags_NoWindowHoverableCheck) + g.LastItemData = last_item_in_parent; + if (g.HoveredWindow == window) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + } + } + else + { + g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values } return menu_is_open; } +bool ImGui::BeginMenu(const char* label, bool enabled) +{ + return BeginMenuEx(label, NULL, enabled); +} + void ImGui::EndMenu() { - // Nav: When a left move request _within our child menu_ failed, close ourselves (the _parent_ menu). - // A menu doesn't close itself because EndMenuBar() wants the catch the last Left<>Right inputs. - // However, it means that with the current code, a BeginMenu() from outside another menu or a menu-bar won't be closable with the Left direction. + // Nav: When a left move request our menu failed, close ourselves. ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; - if (g.NavWindow && g.NavWindow->ParentWindow == window && g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet() && window->DC.LayoutType == ImGuiLayoutType_Vertical) - { - ClosePopupToLevel(g.BeginPopupStack.Size, true); - NavMoveRequestCancel(); - } + IM_ASSERT(window->Flags & ImGuiWindowFlags_Popup); // Mismatched BeginMenu()/EndMenu() calls + ImGuiWindow* parent_window = window->ParentWindow; // Should always be != NULL is we passed assert. + if (window->BeginCount == window->BeginCountPreviousFrame) + if (g.NavMoveDir == ImGuiDir_Left && NavMoveRequestButNoResultYet()) + if (g.NavWindow && (g.NavWindow->RootWindowForNav == window) && parent_window->DC.LayoutType == ImGuiLayoutType_Vertical) + { + ClosePopupToLevel(g.BeginPopupStack.Size - 1, true); + NavMoveRequestCancel(); + } EndPopup(); } -bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) +bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut, bool selected, bool enabled) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) @@ -6254,44 +9110,80 @@ bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, boo ImVec2 pos = window->DC.CursorPos; ImVec2 label_size = CalcTextSize(label, NULL, true); + // See BeginMenuEx() for comments about this. + const bool menuset_is_open = IsRootOfOpenMenuSet(); + if (menuset_is_open) + PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true); + // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73), // but I am unsure whether this should be kept at all. For now moved it to be an opt-in feature used by menus only. - ImGuiSelectableFlags flags = ImGuiSelectableFlags_PressedOnRelease | ImGuiSelectableFlags_SetNavIdOnHover | (enabled ? 0 : ImGuiSelectableFlags_Disabled); bool pressed; + PushID(label); + if (!enabled) + BeginDisabled(); + + // We use ImGuiSelectableFlags_NoSetKeyOwner to allow down on one menu item, move, up on another. + const ImGuiSelectableFlags selectable_flags = ImGuiSelectableFlags_SelectOnRelease | ImGuiSelectableFlags_NoSetKeyOwner | ImGuiSelectableFlags_SetNavIdOnHover; + const ImGuiMenuColumns* offsets = &window->DC.MenuColumns; if (window->DC.LayoutType == ImGuiLayoutType_Horizontal) { // Mimic the exact layout spacing of BeginMenu() to allow MenuItem() inside a menu bar, which is a little misleading but may be useful - // Note that in this situation we render neither the shortcut neither the selected tick mark + // Note that in this situation: we don't render the shortcut, we render a highlight instead of the selected tick mark. float w = label_size.x; - window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * 0.5f); - PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(style.ItemSpacing.x * 2.0f, style.ItemSpacing.y)); - pressed = Selectable(label, false, flags, ImVec2(w, 0.0f)); + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * 0.5f); + ImVec2 text_pos(window->DC.CursorPos.x + offsets->OffsetLabel, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset); + PushStyleVarX(ImGuiStyleVar_ItemSpacing, style.ItemSpacing.x * 2.0f); + pressed = Selectable("", selected, selectable_flags, ImVec2(w, 0.0f)); PopStyleVar(); - window->DC.CursorPos.x += (float)(int)(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) + RenderText(text_pos, label); + window->DC.CursorPos.x += IM_TRUNC(style.ItemSpacing.x * (-1.0f + 0.5f)); // -1 spacing to compensate the spacing added when Selectable() did a SameLine(). It would also work to call SameLine() ourselves after the PopStyleVar(). } else { - ImVec2 shortcut_size = shortcut ? CalcTextSize(shortcut, NULL) : ImVec2(0.0f, 0.0f); - float w = window->MenuColumns.DeclColumns(label_size.x, shortcut_size.x, (float)(int)(g.FontSize * 1.20f)); // Feedback for next frame - float extra_w = ImMax(0.0f, GetContentRegionAvail().x - w); - pressed = Selectable(label, false, flags | ImGuiSelectableFlags_DrawFillAvailWidth, ImVec2(w, 0.0f)); - if (shortcut_size.x > 0.0f) + // Menu item inside a vertical menu + // (In a typical menu window where all items are BeginMenu() or MenuItem() calls, extra_w will always be 0.0f. + // Only when they are other items sticking out we're going to add spacing, yet only register minimum width into the layout system. + float icon_w = (icon && icon[0]) ? CalcTextSize(icon, NULL).x : 0.0f; + float shortcut_w = (shortcut && shortcut[0]) ? CalcTextSize(shortcut, NULL).x : 0.0f; + float checkmark_w = IM_TRUNC(g.FontSize * 1.20f); + float min_w = window->DC.MenuColumns.DeclColumns(icon_w, label_size.x, shortcut_w, checkmark_w); // Feedback for next frame + float stretch_w = ImMax(0.0f, GetContentRegionAvail().x - min_w); + pressed = Selectable("", false, selectable_flags | ImGuiSelectableFlags_SpanAvailWidth, ImVec2(min_w, label_size.y)); + if (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) { - PushStyleColor(ImGuiCol_Text, g.Style.Colors[ImGuiCol_TextDisabled]); - RenderText(pos + ImVec2(window->MenuColumns.Pos[1] + extra_w, 0.0f), shortcut, NULL, false); - PopStyleColor(); + RenderText(pos + ImVec2(offsets->OffsetLabel, 0.0f), label); + if (icon_w > 0.0f) + RenderText(pos + ImVec2(offsets->OffsetIcon, 0.0f), icon); + if (shortcut_w > 0.0f) + { + PushStyleColor(ImGuiCol_Text, style.Colors[ImGuiCol_TextDisabled]); + LogSetNextTextDecoration("(", ")"); + RenderText(pos + ImVec2(offsets->OffsetShortcut + stretch_w, 0.0f), shortcut, NULL, false); + PopStyleColor(); + } + if (selected) + RenderCheckMark(window->DrawList, pos + ImVec2(offsets->OffsetMark + stretch_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(ImGuiCol_Text), g.FontSize * 0.866f); } - if (selected) - RenderCheckMark(pos + ImVec2(window->MenuColumns.Pos[2] + extra_w + g.FontSize * 0.40f, g.FontSize * 0.134f * 0.5f), GetColorU32(enabled ? ImGuiCol_Text : ImGuiCol_TextDisabled), g.FontSize * 0.866f); } + IMGUI_TEST_ENGINE_ITEM_INFO(g.LastItemData.ID, label, g.LastItemData.StatusFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0)); + if (!enabled) + EndDisabled(); + PopID(); + if (menuset_is_open) + PopItemFlag(); - IMGUI_TEST_ENGINE_ITEM_INFO(window->DC.LastItemId, label, window->DC.ItemFlags | ImGuiItemStatusFlags_Checkable | (selected ? ImGuiItemStatusFlags_Checked : 0)); return pressed; } +bool ImGui::MenuItem(const char* label, const char* shortcut, bool selected, bool enabled) +{ + return MenuItemEx(label, NULL, shortcut, selected, enabled); +} + bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, bool enabled) { - if (MenuItem(label, shortcut, p_selected ? *p_selected : false, enabled)) + if (MenuItemEx(label, NULL, shortcut, p_selected ? *p_selected : false, enabled)) { if (p_selected) *p_selected = !*p_selected; @@ -6303,9 +9195,6 @@ bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, //------------------------------------------------------------------------- // [SECTION] Widgets: BeginTabBar, EndTabBar, etc. //------------------------------------------------------------------------- -// [BETA API] API may evolve! This code has been extracted out of the Docking branch, -// and some of the construct which are not used in Master may be left here to facilitate merging. -//------------------------------------------------------------------------- // - BeginTabBar() // - BeginTabBarEx() [Internal] // - EndTabBar() @@ -6313,46 +9202,71 @@ bool ImGui::MenuItem(const char* label, const char* shortcut, bool* p_selected, // - TabBarCalcTabID() [Internal] // - TabBarCalcMaxTabWidth() [Internal] // - TabBarFindTabById() [Internal] +// - TabBarFindTabByOrder() [Internal] +// - TabBarFindMostRecentlySelectedTabForActiveWindow() [Internal] +// - TabBarGetCurrentTab() [Internal] +// - TabBarGetTabName() [Internal] +// - TabBarAddTab() [Internal] // - TabBarRemoveTab() [Internal] // - TabBarCloseTab() [Internal] -// - TabBarScrollClamp()v +// - TabBarScrollClamp() [Internal] // - TabBarScrollToTab() [Internal] -// - TabBarQueueChangeTabOrder() [Internal] +// - TabBarQueueFocus() [Internal] +// - TabBarQueueReorder() [Internal] +// - TabBarProcessReorderFromMousePos() [Internal] +// - TabBarProcessReorder() [Internal] // - TabBarScrollingButtons() [Internal] // - TabBarTabListPopupButton() [Internal] //------------------------------------------------------------------------- +struct ImGuiTabBarSection +{ + int TabCount; // Number of tabs in this section. + float Width; // Sum of width of tabs in this section (after shrinking down) + float Spacing; // Horizontal spacing at the end of the section. + + ImGuiTabBarSection() { memset(this, 0, sizeof(*this)); } +}; + namespace ImGui { static void TabBarLayout(ImGuiTabBar* tab_bar); - static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label); + static ImU32 TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window); static float TabBarCalcMaxTabWidth(); static float TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling); - static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab); + static void TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections); static ImGuiTabItem* TabBarScrollingButtons(ImGuiTabBar* tab_bar); static ImGuiTabItem* TabBarTabListPopupButton(ImGuiTabBar* tab_bar); } ImGuiTabBar::ImGuiTabBar() { - ID = 0; - SelectedTabId = NextSelectedTabId = VisibleTabId = 0; + memset(this, 0, sizeof(*this)); CurrFrameVisible = PrevFrameVisible = -1; - LastTabContentHeight = 0.0f; - OffsetMax = OffsetMaxIdeal = OffsetNextTab = 0.0f; - ScrollingAnim = ScrollingTarget = ScrollingTargetDistToVisibility = ScrollingSpeed = 0.0f; - Flags = ImGuiTabBarFlags_None; - ReorderRequestTabId = 0; - ReorderRequestDir = 0; - WantLayout = VisibleTabWasSubmitted = false; LastTabItemIdx = -1; } -static int IMGUI_CDECL TabItemComparerByVisibleOffset(const void* lhs, const void* rhs) +static inline int TabItemGetSectionIdx(const ImGuiTabItem* tab) +{ + return (tab->Flags & ImGuiTabItemFlags_Leading) ? 0 : (tab->Flags & ImGuiTabItemFlags_Trailing) ? 2 : 1; +} + +static int IMGUI_CDECL TabItemComparerBySection(const void* lhs, const void* rhs) { const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; - return (int)(a->Offset - b->Offset); + const int a_section = TabItemGetSectionIdx(a); + const int b_section = TabItemGetSectionIdx(b); + if (a_section != b_section) + return a_section - b_section; + return (int)(a->IndexDuringLayout - b->IndexDuringLayout); +} + +static int IMGUI_CDECL TabItemComparerByBeginOrder(const void* lhs, const void* rhs) +{ + const ImGuiTabItem* a = (const ImGuiTabItem*)lhs; + const ImGuiTabItem* b = (const ImGuiTabItem*)rhs; + return (int)(a->BeginOrder - b->BeginOrder); } static ImGuiTabBar* GetTabBarFromTabBarRef(const ImGuiPtrOrIndex& ref) @@ -6380,7 +9294,11 @@ bool ImGui::BeginTabBar(const char* str_id, ImGuiTabBarFlags flags) ImGuiTabBar* tab_bar = g.TabBars.GetOrAddByKey(id); ImRect tab_bar_bb = ImRect(window->DC.CursorPos.x, window->DC.CursorPos.y, window->WorkRect.Max.x, window->DC.CursorPos.y + g.FontSize + g.Style.FramePadding.y * 2); tab_bar->ID = id; - return BeginTabBarEx(tab_bar, tab_bar_bb, flags | ImGuiTabBarFlags_IsFocused); + tab_bar->SeparatorMinX = tab_bar->BarRect.Min.x - IM_TRUNC(window->WindowPadding.x * 0.5f); + tab_bar->SeparatorMaxX = tab_bar->BarRect.Max.x + IM_TRUNC(window->WindowPadding.x * 0.5f); + //if (g.NavWindow && IsWindowChildOf(g.NavWindow, window, false, false)) + flags |= ImGuiTabBarFlags_IsFocused; + return BeginTabBarEx(tab_bar, tab_bar_bb, flags); } bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImGuiTabBarFlags flags) @@ -6390,24 +9308,29 @@ bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImG if (window->SkipItems) return false; + IM_ASSERT(tab_bar->ID != 0); if ((flags & ImGuiTabBarFlags_DockNode) == 0) PushOverrideID(tab_bar->ID); // Add to stack g.CurrentTabBarStack.push_back(GetTabBarRefFromTabBar(tab_bar)); g.CurrentTabBar = tab_bar; + tab_bar->Window = window; + // Append with multiple BeginTabBar()/EndTabBar() pairs. + tab_bar->BackupCursorPos = window->DC.CursorPos; if (tab_bar->CurrFrameVisible == g.FrameCount) { - //IMGUI_DEBUG_LOG("BeginTabBarEx already called this frame\n", g.FrameCount); - IM_ASSERT(0); + window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); + tab_bar->BeginCount++; return true; } - // When toggling back from ordered to manually-reorderable, shuffle tabs to enforce the last visible order. - // Otherwise, the most recently inserted tabs would move at the end of visible list which can be a little too confusing or magic for the user. - if ((flags & ImGuiTabBarFlags_Reorderable) && !(tab_bar->Flags & ImGuiTabBarFlags_Reorderable) && tab_bar->Tabs.Size > 1 && tab_bar->PrevFrameVisible != -1) - ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByVisibleOffset); + // Ensure correct ordering when toggling ImGuiTabBarFlags_Reorderable flag, or when a new tab was added while being not reorderable + if ((flags & ImGuiTabBarFlags_Reorderable) != (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (tab_bar->TabsAddedNew && !(flags & ImGuiTabBarFlags_Reorderable))) + if ((flags & ImGuiTabBarFlags_DockNode) == 0) // FIXME: TabBar with DockNode can now be hybrid + ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerByBeginOrder); + tab_bar->TabsAddedNew = false; // Flags if ((flags & ImGuiTabBarFlags_FittingPolicyMask_) == 0) @@ -6418,19 +9341,24 @@ bool ImGui::BeginTabBarEx(ImGuiTabBar* tab_bar, const ImRect& tab_bar_bb, ImG tab_bar->WantLayout = true; // Layout will be done on the first call to ItemTab() tab_bar->PrevFrameVisible = tab_bar->CurrFrameVisible; tab_bar->CurrFrameVisible = g.FrameCount; + tab_bar->PrevTabsContentsHeight = tab_bar->CurrTabsContentsHeight; + tab_bar->CurrTabsContentsHeight = 0.0f; + tab_bar->ItemSpacingY = g.Style.ItemSpacing.y; tab_bar->FramePadding = g.Style.FramePadding; + tab_bar->TabsActiveCount = 0; + tab_bar->LastTabItemIdx = -1; + tab_bar->BeginCount = 1; - // Layout - ItemSize(ImVec2(tab_bar->OffsetMaxIdeal, tab_bar->BarRect.GetHeight())); - window->DC.CursorPos.x = tab_bar->BarRect.Min.x; + // Set cursor pos in a way which only be used in the off-chance the user erroneously submits item before BeginTabItem(): items will overlap + window->DC.CursorPos = ImVec2(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.y + tab_bar->ItemSpacingY); // Draw separator - const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive); - const float y = tab_bar->BarRect.Max.y - 1.0f; + // (it would be misleading to draw this in EndTabBar() suggesting that it may be drawn over tabs, as tab bar are appendable) + const ImU32 col = GetColorU32((flags & ImGuiTabBarFlags_IsFocused) ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected); + if (g.Style.TabBarBorderSize > 0.0f) { - const float separator_min_x = tab_bar->BarRect.Min.x - ImFloor(window->WindowPadding.x * 0.5f); - const float separator_max_x = tab_bar->BarRect.Max.x + ImFloor(window->WindowPadding.x * 0.5f); - window->DrawList->AddLine(ImVec2(separator_min_x, y), ImVec2(separator_max_x, y), col, 1.0f); + const float y = tab_bar->BarRect.Max.y; + window->DrawList->AddRectFilled(ImVec2(tab_bar->SeparatorMinX, y - g.Style.TabBarBorderSize), ImVec2(tab_bar->SeparatorMaxX, y), col); } return true; } @@ -6445,19 +9373,29 @@ void ImGui::EndTabBar() ImGuiTabBar* tab_bar = g.CurrentTabBar; if (tab_bar == NULL) { - IM_ASSERT(tab_bar != NULL && "Mismatched BeginTabBar()/EndTabBar()!"); - return; // FIXME-ERRORHANDLING + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Mismatched BeginTabBar()/EndTabBar()!"); + return; } + + // Fallback in case no TabItem have been submitted if (tab_bar->WantLayout) TabBarLayout(tab_bar); // Restore the last visible height if no tab is visible, this reduce vertical flicker/movement when a tabs gets removed without calling SetTabItemClosed(). const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); if (tab_bar->VisibleTabWasSubmitted || tab_bar->VisibleTabId == 0 || tab_bar_appearing) - tab_bar->LastTabContentHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, 0.0f); + { + tab_bar->CurrTabsContentsHeight = ImMax(window->DC.CursorPos.y - tab_bar->BarRect.Max.y, tab_bar->CurrTabsContentsHeight); + window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->CurrTabsContentsHeight; + } else - window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->LastTabContentHeight; + { + window->DC.CursorPos.y = tab_bar->BarRect.Max.y + tab_bar->PrevTabsContentsHeight; + } + if (tab_bar->BeginCount > 1) + window->DC.CursorPos = tab_bar->BackupCursorPos; + tab_bar->LastTabItemIdx = -1; if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0) PopID(); @@ -6465,6 +9403,12 @@ void ImGui::EndTabBar() g.CurrentTabBar = g.CurrentTabBarStack.empty() ? NULL : GetTabBarFromTabBarRef(g.CurrentTabBarStack.back()); } +// Scrolling happens only in the central section (leading/trailing sections are not scrolling) +static float TabBarCalcScrollableWidth(ImGuiTabBar* tab_bar, ImGuiTabBarSection* sections) +{ + return tab_bar->BarRect.GetWidth() - sections[0].Width - sections[2].Width - sections[1].Spacing; +} + // This is called only once a frame before by the first call to ItemTab() // The reason we're not calling it in BeginTabBar() is to leave a chance to the user to call the SetTabItemClosed() functions. static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) @@ -6472,149 +9416,222 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) ImGuiContext& g = *GImGui; tab_bar->WantLayout = false; - // Garbage collect + // Garbage collect by compacting list + // Detect if we need to sort out tab list (e.g. in rare case where a tab changed section) int tab_dst_n = 0; + bool need_sort_by_section = false; + ImGuiTabBarSection sections[3]; // Layout sections: Leading, Central, Trailing for (int tab_src_n = 0; tab_src_n < tab_bar->Tabs.Size; tab_src_n++) { ImGuiTabItem* tab = &tab_bar->Tabs[tab_src_n]; - if (tab->LastFrameVisible < tab_bar->PrevFrameVisible) + if (tab->LastFrameVisible < tab_bar->PrevFrameVisible || tab->WantClose) { - if (tab->ID == tab_bar->SelectedTabId) - tab_bar->SelectedTabId = 0; + // Remove tab + if (tab_bar->VisibleTabId == tab->ID) { tab_bar->VisibleTabId = 0; } + if (tab_bar->SelectedTabId == tab->ID) { tab_bar->SelectedTabId = 0; } + if (tab_bar->NextSelectedTabId == tab->ID) { tab_bar->NextSelectedTabId = 0; } continue; } if (tab_dst_n != tab_src_n) tab_bar->Tabs[tab_dst_n] = tab_bar->Tabs[tab_src_n]; + + tab = &tab_bar->Tabs[tab_dst_n]; + tab->IndexDuringLayout = (ImS16)tab_dst_n; + + // We will need sorting if tabs have changed section (e.g. moved from one of Leading/Central/Trailing to another) + int curr_tab_section_n = TabItemGetSectionIdx(tab); + if (tab_dst_n > 0) + { + ImGuiTabItem* prev_tab = &tab_bar->Tabs[tab_dst_n - 1]; + int prev_tab_section_n = TabItemGetSectionIdx(prev_tab); + if (curr_tab_section_n == 0 && prev_tab_section_n != 0) + need_sort_by_section = true; + if (prev_tab_section_n == 2 && curr_tab_section_n != 2) + need_sort_by_section = true; + } + + sections[curr_tab_section_n].TabCount++; tab_dst_n++; } if (tab_bar->Tabs.Size != tab_dst_n) tab_bar->Tabs.resize(tab_dst_n); + if (need_sort_by_section) + ImQsort(tab_bar->Tabs.Data, tab_bar->Tabs.Size, sizeof(ImGuiTabItem), TabItemComparerBySection); + + // Calculate spacing between sections + sections[0].Spacing = sections[0].TabCount > 0 && (sections[1].TabCount + sections[2].TabCount) > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; + sections[1].Spacing = sections[1].TabCount > 0 && sections[2].TabCount > 0 ? g.Style.ItemInnerSpacing.x : 0.0f; + // Setup next selected tab - ImGuiID scroll_track_selected_tab_id = 0; + ImGuiID scroll_to_tab_id = 0; if (tab_bar->NextSelectedTabId) { tab_bar->SelectedTabId = tab_bar->NextSelectedTabId; tab_bar->NextSelectedTabId = 0; - scroll_track_selected_tab_id = tab_bar->SelectedTabId; + scroll_to_tab_id = tab_bar->SelectedTabId; } // Process order change request (we could probably process it when requested but it's just saner to do it in a single spot). if (tab_bar->ReorderRequestTabId != 0) { - if (ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId)) - { - //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools - int tab2_order = tab_bar->GetTabOrder(tab1) + tab_bar->ReorderRequestDir; - if (tab2_order >= 0 && tab2_order < tab_bar->Tabs.Size) - { - ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order]; - ImGuiTabItem item_tmp = *tab1; - *tab1 = *tab2; - *tab2 = item_tmp; - if (tab2->ID == tab_bar->SelectedTabId) - scroll_track_selected_tab_id = tab2->ID; - tab1 = tab2 = NULL; - } - if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings) - MarkIniSettingsDirty(); - } + if (TabBarProcessReorder(tab_bar)) + if (tab_bar->ReorderRequestTabId == tab_bar->SelectedTabId) + scroll_to_tab_id = tab_bar->ReorderRequestTabId; tab_bar->ReorderRequestTabId = 0; } // Tab List Popup (will alter tab_bar->BarRect and therefore the available width!) const bool tab_list_popup_button = (tab_bar->Flags & ImGuiTabBarFlags_TabListPopupButton) != 0; if (tab_list_popup_button) - if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Max.x! - scroll_track_selected_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; + if (ImGuiTabItem* tab_to_select = TabBarTabListPopupButton(tab_bar)) // NB: Will alter BarRect.Min.x! + scroll_to_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; - // Compute ideal widths + // Leading/Trailing tabs will be shrink only if central one aren't visible anymore, so layout the shrink data as: leading, trailing, central + // (whereas our tabs are stored as: leading, central, trailing) + int shrink_buffer_indexes[3] = { 0, sections[0].TabCount + sections[2].TabCount, sections[0].TabCount }; g.ShrinkWidthBuffer.resize(tab_bar->Tabs.Size); - float width_total_contents = 0.0f; + + // Compute ideal tabs widths + store them into shrink buffer ImGuiTabItem* most_recently_selected_tab = NULL; + int curr_section_n = -1; bool found_selected_tab_id = false; for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) { ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; IM_ASSERT(tab->LastFrameVisible >= tab_bar->PrevFrameVisible); - if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) + if ((most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) && !(tab->Flags & ImGuiTabItemFlags_Button)) most_recently_selected_tab = tab; if (tab->ID == tab_bar->SelectedTabId) found_selected_tab_id = true; + if (scroll_to_tab_id == 0 && g.NavJustMovedToId == tab->ID) + scroll_to_tab_id = tab->ID; // Refresh tab width immediately, otherwise changes of style e.g. style.FramePadding.x would noticeably lag in the tab bar. // Additionally, when using TabBarAddTab() to manipulate tab bar order we occasionally insert new tabs that don't have a width yet, // and we cannot wait for the next BeginTabItem() call. We cannot compute this width within TabBarAddTab() because font size depends on the active window. - const char* tab_name = tab_bar->GetTabName(tab); - const bool has_close_button = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) ? false : true; - tab->WidthContents = TabItemCalcSize(tab_name, has_close_button).x; + const char* tab_name = TabBarGetTabName(tab_bar, tab); + const bool has_close_button_or_unsaved_marker = (tab->Flags & ImGuiTabItemFlags_NoCloseButton) == 0 || (tab->Flags & ImGuiTabItemFlags_UnsavedDocument); + tab->ContentWidth = (tab->RequestedWidth >= 0.0f) ? tab->RequestedWidth : TabItemCalcSize(tab_name, has_close_button_or_unsaved_marker).x; - width_total_contents += (tab_n > 0 ? g.Style.ItemInnerSpacing.x : 0.0f) + tab->WidthContents; + int section_n = TabItemGetSectionIdx(tab); + ImGuiTabBarSection* section = §ions[section_n]; + section->Width += tab->ContentWidth + (section_n == curr_section_n ? g.Style.ItemInnerSpacing.x : 0.0f); + curr_section_n = section_n; // Store data so we can build an array sorted by width if we need to shrink tabs down - g.ShrinkWidthBuffer[tab_n].Index = tab_n; - g.ShrinkWidthBuffer[tab_n].Width = tab->WidthContents; + IM_MSVC_WARNING_SUPPRESS(6385); + ImGuiShrinkWidthItem* shrink_width_item = &g.ShrinkWidthBuffer[shrink_buffer_indexes[section_n]++]; + shrink_width_item->Index = tab_n; + shrink_width_item->Width = shrink_width_item->InitialWidth = tab->ContentWidth; + tab->Width = ImMax(tab->ContentWidth, 1.0f); } - // Compute width - const float initial_offset_x = 0.0f; // g.Style.ItemInnerSpacing.x; - const float width_avail = ImMax(tab_bar->BarRect.GetWidth() - initial_offset_x, 0.0f); - float width_excess = (width_avail < width_total_contents) ? (width_total_contents - width_avail) : 0.0f; - if (width_excess > 0.0f && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown)) - { - // If we don't have enough room, resize down the largest tabs first - ShrinkWidths(g.ShrinkWidthBuffer.Data, g.ShrinkWidthBuffer.Size, width_excess); - for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) - tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index].Width = (float)(int)g.ShrinkWidthBuffer[tab_n].Width; - } - else - { - const float tab_max_width = TabBarCalcMaxTabWidth(); - for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + // Compute total ideal width (used for e.g. auto-resizing a window) + tab_bar->WidthAllTabsIdeal = 0.0f; + for (int section_n = 0; section_n < 3; section_n++) + tab_bar->WidthAllTabsIdeal += sections[section_n].Width + sections[section_n].Spacing; + + // Horizontal scrolling buttons + // (note that TabBarScrollButtons() will alter BarRect.Max.x) + if ((tab_bar->WidthAllTabsIdeal > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll)) + if (ImGuiTabItem* scroll_and_select_tab = TabBarScrollingButtons(tab_bar)) { - ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; - tab->Width = ImMin(tab->WidthContents, tab_max_width); - IM_ASSERT(tab->Width > 0.0f); + scroll_to_tab_id = scroll_and_select_tab->ID; + if ((scroll_and_select_tab->Flags & ImGuiTabItemFlags_Button) == 0) + tab_bar->SelectedTabId = scroll_to_tab_id; + } + + // Shrink widths if full tabs don't fit in their allocated space + float section_0_w = sections[0].Width + sections[0].Spacing; + float section_1_w = sections[1].Width + sections[1].Spacing; + float section_2_w = sections[2].Width + sections[2].Spacing; + bool central_section_is_visible = (section_0_w + section_2_w) < tab_bar->BarRect.GetWidth(); + float width_excess; + if (central_section_is_visible) + width_excess = ImMax(section_1_w - (tab_bar->BarRect.GetWidth() - section_0_w - section_2_w), 0.0f); // Excess used to shrink central section + else + width_excess = (section_0_w + section_2_w) - tab_bar->BarRect.GetWidth(); // Excess used to shrink leading/trailing section + + // With ImGuiTabBarFlags_FittingPolicyScroll policy, we will only shrink leading/trailing if the central section is not visible anymore + if (width_excess >= 1.0f && ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyResizeDown) || !central_section_is_visible)) + { + int shrink_data_count = (central_section_is_visible ? sections[1].TabCount : sections[0].TabCount + sections[2].TabCount); + int shrink_data_offset = (central_section_is_visible ? sections[0].TabCount + sections[2].TabCount : 0); + ShrinkWidths(g.ShrinkWidthBuffer.Data + shrink_data_offset, shrink_data_count, width_excess); + + // Apply shrunk values into tabs and sections + for (int tab_n = shrink_data_offset; tab_n < shrink_data_offset + shrink_data_count; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[g.ShrinkWidthBuffer[tab_n].Index]; + float shrinked_width = IM_TRUNC(g.ShrinkWidthBuffer[tab_n].Width); + if (shrinked_width < 0.0f) + continue; + + shrinked_width = ImMax(1.0f, shrinked_width); + int section_n = TabItemGetSectionIdx(tab); + sections[section_n].Width -= (tab->Width - shrinked_width); + tab->Width = shrinked_width; } } // Layout all active tabs - float offset_x = initial_offset_x; - float offset_x_ideal = offset_x; - tab_bar->OffsetNextTab = offset_x; // This is used by non-reorderable tab bar where the submission order is always honored. - for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + int section_tab_index = 0; + float tab_offset = 0.0f; + tab_bar->WidthAllTabs = 0.0f; + for (int section_n = 0; section_n < 3; section_n++) { - ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; - tab->Offset = offset_x; - if (scroll_track_selected_tab_id == 0 && g.NavJustMovedToId == tab->ID) - scroll_track_selected_tab_id = tab->ID; - offset_x += tab->Width + g.Style.ItemInnerSpacing.x; - offset_x_ideal += tab->WidthContents + g.Style.ItemInnerSpacing.x; - } - tab_bar->OffsetMax = ImMax(offset_x - g.Style.ItemInnerSpacing.x, 0.0f); - tab_bar->OffsetMaxIdeal = ImMax(offset_x_ideal - g.Style.ItemInnerSpacing.x, 0.0f); + ImGuiTabBarSection* section = §ions[section_n]; + if (section_n == 2) + tab_offset = ImMin(ImMax(0.0f, tab_bar->BarRect.GetWidth() - section->Width), tab_offset); - // Horizontal scrolling buttons - const bool scrolling_buttons = (tab_bar->OffsetMax > tab_bar->BarRect.GetWidth() && tab_bar->Tabs.Size > 1) && !(tab_bar->Flags & ImGuiTabBarFlags_NoTabListScrollingButtons) && (tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll); - if (scrolling_buttons) - if (ImGuiTabItem* tab_to_select = TabBarScrollingButtons(tab_bar)) // NB: Will alter BarRect.Max.x! - scroll_track_selected_tab_id = tab_bar->SelectedTabId = tab_to_select->ID; + for (int tab_n = 0; tab_n < section->TabCount; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[section_tab_index + tab_n]; + tab->Offset = tab_offset; + tab->NameOffset = -1; + tab_offset += tab->Width + (tab_n < section->TabCount - 1 ? g.Style.ItemInnerSpacing.x : 0.0f); + } + tab_bar->WidthAllTabs += ImMax(section->Width + section->Spacing, 0.0f); + tab_offset += section->Spacing; + section_tab_index += section->TabCount; + } + + // Clear name buffers + tab_bar->TabsNames.Buf.resize(0); // If we have lost the selected tab, select the next most recently active one if (found_selected_tab_id == false) tab_bar->SelectedTabId = 0; if (tab_bar->SelectedTabId == 0 && tab_bar->NextSelectedTabId == 0 && most_recently_selected_tab != NULL) - scroll_track_selected_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID; + scroll_to_tab_id = tab_bar->SelectedTabId = most_recently_selected_tab->ID; // Lock in visible tab tab_bar->VisibleTabId = tab_bar->SelectedTabId; tab_bar->VisibleTabWasSubmitted = false; + // CTRL+TAB can override visible tab temporarily + if (g.NavWindowingTarget != NULL && g.NavWindowingTarget->DockNode && g.NavWindowingTarget->DockNode->TabBar == tab_bar) + tab_bar->VisibleTabId = scroll_to_tab_id = g.NavWindowingTarget->TabId; + + // Apply request requests + if (scroll_to_tab_id != 0) + TabBarScrollToTab(tab_bar, scroll_to_tab_id, sections); + else if ((tab_bar->Flags & ImGuiTabBarFlags_FittingPolicyScroll) && IsMouseHoveringRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max, true) && IsWindowContentHoverable(g.CurrentWindow)) + { + const float wheel = g.IO.MouseWheelRequestAxisSwap ? g.IO.MouseWheel : g.IO.MouseWheelH; + const ImGuiKey wheel_key = g.IO.MouseWheelRequestAxisSwap ? ImGuiKey_MouseWheelY : ImGuiKey_MouseWheelX; + if (TestKeyOwner(wheel_key, tab_bar->ID) && wheel != 0.0f) + { + const float scroll_step = wheel * TabBarCalcScrollableWidth(tab_bar, sections) / 3.0f; + tab_bar->ScrollingTargetDistToVisibility = 0.0f; + tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget - scroll_step); + } + SetKeyOwner(wheel_key, tab_bar->ID); + } + // Update scrolling - if (scroll_track_selected_tab_id) - if (ImGuiTabItem* scroll_track_selected_tab = TabBarFindTabByID(tab_bar, scroll_track_selected_tab_id)) - TabBarScrollToTab(tab_bar, scroll_track_selected_tab); tab_bar->ScrollingAnim = TabBarScrollClamp(tab_bar, tab_bar->ScrollingAnim); tab_bar->ScrollingTarget = TabBarScrollClamp(tab_bar, tab_bar->ScrollingTarget); if (tab_bar->ScrollingAnim != tab_bar->ScrollingTarget) @@ -6630,18 +9647,24 @@ static void ImGui::TabBarLayout(ImGuiTabBar* tab_bar) { tab_bar->ScrollingSpeed = 0.0f; } + tab_bar->ScrollingRectMinX = tab_bar->BarRect.Min.x + sections[0].Width + sections[0].Spacing; + tab_bar->ScrollingRectMaxX = tab_bar->BarRect.Max.x - sections[2].Width - sections[1].Spacing; - // Clear name buffers - if ((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0) - tab_bar->TabsNames.Buf.resize(0); + // Actual layout in host window (we don't do it in BeginTabBar() so as not to waste an extra frame) + ImGuiWindow* window = g.CurrentWindow; + window->DC.CursorPos = tab_bar->BarRect.Min; + ItemSize(ImVec2(tab_bar->WidthAllTabs, tab_bar->BarRect.GetHeight()), tab_bar->FramePadding.y); + window->DC.IdealMaxPos.x = ImMax(window->DC.IdealMaxPos.x, tab_bar->BarRect.Min.x + tab_bar->WidthAllTabsIdeal); } -// Dockables uses Name/ID in the global namespace. Non-dockable items use the ID stack. -static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label) +// Dockable windows uses Name/ID in the global namespace. Non-dockable items use the ID stack. +static ImU32 ImGui::TabBarCalcTabID(ImGuiTabBar* tab_bar, const char* label, ImGuiWindow* docked_window) { - if (tab_bar->Flags & ImGuiTabBarFlags_DockNode) + if (docked_window != NULL) { - ImGuiID id = ImHashStr(label); + IM_UNUSED(tab_bar); + IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode); + ImGuiID id = docked_window->TabId; KeepAliveID(id); return id; } @@ -6667,7 +9690,67 @@ ImGuiTabItem* ImGui::TabBarFindTabByID(ImGuiTabBar* tab_bar, ImGuiID tab_id) return NULL; } -// The *TabId fields be already set by the docking system _before_ the actual TabItem was created, so we clear them regardless. +// Order = visible order, not submission order! (which is tab->BeginOrder) +ImGuiTabItem* ImGui::TabBarFindTabByOrder(ImGuiTabBar* tab_bar, int order) +{ + if (order < 0 || order >= tab_bar->Tabs.Size) + return NULL; + return &tab_bar->Tabs[order]; +} + +// FIXME: See references to #2304 in TODO.txt +ImGuiTabItem* ImGui::TabBarFindMostRecentlySelectedTabForActiveWindow(ImGuiTabBar* tab_bar) +{ + ImGuiTabItem* most_recently_selected_tab = NULL; + for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) + { + ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; + if (most_recently_selected_tab == NULL || most_recently_selected_tab->LastFrameSelected < tab->LastFrameSelected) + if (tab->Window && tab->Window->WasActive) + most_recently_selected_tab = tab; + } + return most_recently_selected_tab; +} + +ImGuiTabItem* ImGui::TabBarGetCurrentTab(ImGuiTabBar* tab_bar) +{ + if (tab_bar->LastTabItemIdx < 0 || tab_bar->LastTabItemIdx >= tab_bar->Tabs.Size) + return NULL; + return &tab_bar->Tabs[tab_bar->LastTabItemIdx]; +} + +const char* ImGui::TabBarGetTabName(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) +{ + if (tab->Window) + return tab->Window->Name; + if (tab->NameOffset == -1) + return "N/A"; + IM_ASSERT(tab->NameOffset < tab_bar->TabsNames.Buf.Size); + return tab_bar->TabsNames.Buf.Data + tab->NameOffset; +} + +// The purpose of this call is to register tab in advance so we can control their order at the time they appear. +// Otherwise calling this is unnecessary as tabs are appending as needed by the BeginTabItem() function. +void ImGui::TabBarAddTab(ImGuiTabBar* tab_bar, ImGuiTabItemFlags tab_flags, ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(TabBarFindTabByID(tab_bar, window->TabId) == NULL); + IM_ASSERT(g.CurrentTabBar != tab_bar); // Can't work while the tab bar is active as our tab doesn't have an X offset yet, in theory we could/should test something like (tab_bar->CurrFrameVisible < g.FrameCount) but we'd need to solve why triggers the commented early-out assert in BeginTabBarEx() (probably dock node going from implicit to explicit in same frame) + + if (!window->HasCloseButton) + tab_flags |= ImGuiTabItemFlags_NoCloseButton; // Set _NoCloseButton immediately because it will be used for first-frame width calculation. + + ImGuiTabItem new_tab; + new_tab.ID = window->TabId; + new_tab.Flags = tab_flags; + new_tab.LastFrameVisible = tab_bar->CurrFrameVisible; // Required so BeginTabBar() doesn't ditch the tab + if (new_tab.LastFrameVisible == -1) + new_tab.LastFrameVisible = g.FrameCount - 1; + new_tab.Window = window; // Required so tab bar layout can compute the tab width before tab submission + tab_bar->Tabs.push_back(new_tab); +} + +// The *TabId fields are already set by the docking system _before_ the actual TabItem was created, so we clear them regardless. void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) { if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) @@ -6680,52 +9763,153 @@ void ImGui::TabBarRemoveTab(ImGuiTabBar* tab_bar, ImGuiID tab_id) // Called on manual closure attempt void ImGui::TabBarCloseTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { - if ((tab_bar->VisibleTabId == tab->ID) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument)) + if (tab->Flags & ImGuiTabItemFlags_Button) + return; // A button appended with TabItemButton(). + + if ((tab->Flags & (ImGuiTabItemFlags_UnsavedDocument | ImGuiTabItemFlags_NoAssumedClosure)) == 0) { // This will remove a frame of lag for selecting another tab on closure. // However we don't run it in the case where the 'Unsaved' flag is set, so user gets a chance to fully undo the closure - tab->LastFrameVisible = -1; - tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0; + tab->WantClose = true; + if (tab_bar->VisibleTabId == tab->ID) + { + tab->LastFrameVisible = -1; + tab_bar->SelectedTabId = tab_bar->NextSelectedTabId = 0; + } } - else if ((tab_bar->VisibleTabId != tab->ID) && (tab->Flags & ImGuiTabItemFlags_UnsavedDocument)) + else { - // Actually select before expecting closure - tab_bar->NextSelectedTabId = tab->ID; + // Actually select before expecting closure attempt (on an UnsavedDocument tab user is expect to e.g. show a popup) + if (tab_bar->VisibleTabId != tab->ID) + TabBarQueueFocus(tab_bar, tab); } } static float ImGui::TabBarScrollClamp(ImGuiTabBar* tab_bar, float scrolling) { - scrolling = ImMin(scrolling, tab_bar->OffsetMax - tab_bar->BarRect.GetWidth()); + scrolling = ImMin(scrolling, tab_bar->WidthAllTabs - tab_bar->BarRect.GetWidth()); return ImMax(scrolling, 0.0f); } -static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) +// Note: we may scroll to tab that are not selected! e.g. using keyboard arrow keys +static void ImGui::TabBarScrollToTab(ImGuiTabBar* tab_bar, ImGuiID tab_id, ImGuiTabBarSection* sections) { + ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id); + if (tab == NULL) + return; + if (tab->Flags & ImGuiTabItemFlags_SectionMask_) + return; + ImGuiContext& g = *GImGui; float margin = g.FontSize * 1.0f; // When to scroll to make Tab N+1 visible always make a bit of N visible to suggest more scrolling area (since we don't have a scrollbar) - int order = tab_bar->GetTabOrder(tab); - float tab_x1 = tab->Offset + (order > 0 ? -margin : 0.0f); - float tab_x2 = tab->Offset + tab->Width + (order + 1 < tab_bar->Tabs.Size ? margin : 1.0f); + int order = TabBarGetTabOrder(tab_bar, tab); + + // Scrolling happens only in the central section (leading/trailing sections are not scrolling) + float scrollable_width = TabBarCalcScrollableWidth(tab_bar, sections); + + // We make all tabs positions all relative Sections[0].Width to make code simpler + float tab_x1 = tab->Offset - sections[0].Width + (order > sections[0].TabCount - 1 ? -margin : 0.0f); + float tab_x2 = tab->Offset - sections[0].Width + tab->Width + (order + 1 < tab_bar->Tabs.Size - sections[2].TabCount ? margin : 1.0f); tab_bar->ScrollingTargetDistToVisibility = 0.0f; - if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= tab_bar->BarRect.GetWidth())) + if (tab_bar->ScrollingTarget > tab_x1 || (tab_x2 - tab_x1 >= scrollable_width)) { + // Scroll to the left tab_bar->ScrollingTargetDistToVisibility = ImMax(tab_bar->ScrollingAnim - tab_x2, 0.0f); tab_bar->ScrollingTarget = tab_x1; } - else if (tab_bar->ScrollingTarget < tab_x2 - tab_bar->BarRect.GetWidth()) + else if (tab_bar->ScrollingTarget < tab_x2 - scrollable_width) { - tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - tab_bar->BarRect.GetWidth()) - tab_bar->ScrollingAnim, 0.0f); - tab_bar->ScrollingTarget = tab_x2 - tab_bar->BarRect.GetWidth(); + // Scroll to the right + tab_bar->ScrollingTargetDistToVisibility = ImMax((tab_x1 - scrollable_width) - tab_bar->ScrollingAnim, 0.0f); + tab_bar->ScrollingTarget = tab_x2 - scrollable_width; } } -void ImGui::TabBarQueueChangeTabOrder(ImGuiTabBar* tab_bar, const ImGuiTabItem* tab, int dir) +void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, ImGuiTabItem* tab) { - IM_ASSERT(dir == -1 || dir == +1); + tab_bar->NextSelectedTabId = tab->ID; +} + +void ImGui::TabBarQueueFocus(ImGuiTabBar* tab_bar, const char* tab_name) +{ + IM_ASSERT((tab_bar->Flags & ImGuiTabBarFlags_DockNode) == 0); // Only supported for manual/explicit tab bars + ImGuiID tab_id = TabBarCalcTabID(tab_bar, tab_name, NULL); + tab_bar->NextSelectedTabId = tab_id; +} + +void ImGui::TabBarQueueReorder(ImGuiTabBar* tab_bar, ImGuiTabItem* tab, int offset) +{ + IM_ASSERT(offset != 0); IM_ASSERT(tab_bar->ReorderRequestTabId == 0); tab_bar->ReorderRequestTabId = tab->ID; - tab_bar->ReorderRequestDir = (ImS8)dir; + tab_bar->ReorderRequestOffset = (ImS16)offset; +} + +void ImGui::TabBarQueueReorderFromMousePos(ImGuiTabBar* tab_bar, ImGuiTabItem* src_tab, ImVec2 mouse_pos) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(tab_bar->ReorderRequestTabId == 0); + if ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) == 0) + return; + + const bool is_central_section = (src_tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; + const float bar_offset = tab_bar->BarRect.Min.x - (is_central_section ? tab_bar->ScrollingTarget : 0); + + // Count number of contiguous tabs we are crossing over + const int dir = (bar_offset + src_tab->Offset) > mouse_pos.x ? -1 : +1; + const int src_idx = tab_bar->Tabs.index_from_ptr(src_tab); + int dst_idx = src_idx; + for (int i = src_idx; i >= 0 && i < tab_bar->Tabs.Size; i += dir) + { + // Reordered tabs must share the same section + const ImGuiTabItem* dst_tab = &tab_bar->Tabs[i]; + if (dst_tab->Flags & ImGuiTabItemFlags_NoReorder) + break; + if ((dst_tab->Flags & ImGuiTabItemFlags_SectionMask_) != (src_tab->Flags & ImGuiTabItemFlags_SectionMask_)) + break; + dst_idx = i; + + // Include spacing after tab, so when mouse cursor is between tabs we would not continue checking further tabs that are not hovered. + const float x1 = bar_offset + dst_tab->Offset - g.Style.ItemInnerSpacing.x; + const float x2 = bar_offset + dst_tab->Offset + dst_tab->Width + g.Style.ItemInnerSpacing.x; + //GetForegroundDrawList()->AddRect(ImVec2(x1, tab_bar->BarRect.Min.y), ImVec2(x2, tab_bar->BarRect.Max.y), IM_COL32(255, 0, 0, 255)); + if ((dir < 0 && mouse_pos.x > x1) || (dir > 0 && mouse_pos.x < x2)) + break; + } + + if (dst_idx != src_idx) + TabBarQueueReorder(tab_bar, src_tab, dst_idx - src_idx); +} + +bool ImGui::TabBarProcessReorder(ImGuiTabBar* tab_bar) +{ + ImGuiTabItem* tab1 = TabBarFindTabByID(tab_bar, tab_bar->ReorderRequestTabId); + if (tab1 == NULL || (tab1->Flags & ImGuiTabItemFlags_NoReorder)) + return false; + + //IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_Reorderable); // <- this may happen when using debug tools + int tab2_order = TabBarGetTabOrder(tab_bar, tab1) + tab_bar->ReorderRequestOffset; + if (tab2_order < 0 || tab2_order >= tab_bar->Tabs.Size) + return false; + + // Reordered tabs must share the same section + // (Note: TabBarQueueReorderFromMousePos() also has a similar test but since we allow direct calls to TabBarQueueReorder() we do it here too) + ImGuiTabItem* tab2 = &tab_bar->Tabs[tab2_order]; + if (tab2->Flags & ImGuiTabItemFlags_NoReorder) + return false; + if ((tab1->Flags & ImGuiTabItemFlags_SectionMask_) != (tab2->Flags & ImGuiTabItemFlags_SectionMask_)) + return false; + + ImGuiTabItem item_tmp = *tab1; + ImGuiTabItem* src_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 + 1 : tab2; + ImGuiTabItem* dst_tab = (tab_bar->ReorderRequestOffset > 0) ? tab1 : tab2 + 1; + const int move_count = (tab_bar->ReorderRequestOffset > 0) ? tab_bar->ReorderRequestOffset : -tab_bar->ReorderRequestOffset; + memmove(dst_tab, src_tab, move_count * sizeof(ImGuiTabItem)); + *tab2 = item_tmp; + + if (tab_bar->Flags & ImGuiTabBarFlags_SaveSettings) + MarkIniSettingsDirty(); + return true; } static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar) @@ -6739,47 +9923,56 @@ static ImGuiTabItem* ImGui::TabBarScrollingButtons(ImGuiTabBar* tab_bar) const ImVec2 backup_cursor_pos = window->DC.CursorPos; //window->DrawList->AddRect(ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width, tab_bar->BarRect.Min.y), ImVec2(tab_bar->BarRect.Max.x, tab_bar->BarRect.Max.y), IM_COL32(255,0,0,255)); - const ImRect avail_bar_rect = tab_bar->BarRect; - bool want_clip_rect = !avail_bar_rect.Contains(ImRect(window->DC.CursorPos, window->DC.CursorPos + ImVec2(scrolling_buttons_width, 0.0f))); - if (want_clip_rect) - PushClipRect(tab_bar->BarRect.Min, tab_bar->BarRect.Max + ImVec2(g.Style.ItemInnerSpacing.x, 0.0f), true); - - ImGuiTabItem* tab_to_select = NULL; - int select_dir = 0; ImVec4 arrow_col = g.Style.Colors[ImGuiCol_Text]; arrow_col.w *= 0.5f; PushStyleColor(ImGuiCol_Text, arrow_col); PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); + PushItemFlag(ImGuiItemFlags_ButtonRepeat, true); const float backup_repeat_delay = g.IO.KeyRepeatDelay; const float backup_repeat_rate = g.IO.KeyRepeatRate; g.IO.KeyRepeatDelay = 0.250f; g.IO.KeyRepeatRate = 0.200f; - window->DC.CursorPos = ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width, tab_bar->BarRect.Min.y); - if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat)) + float x = ImMax(tab_bar->BarRect.Min.x, tab_bar->BarRect.Max.x - scrolling_buttons_width); + window->DC.CursorPos = ImVec2(x, tab_bar->BarRect.Min.y); + if (ArrowButtonEx("##<", ImGuiDir_Left, arrow_button_size, ImGuiButtonFlags_PressedOnClick)) select_dir = -1; - window->DC.CursorPos = ImVec2(tab_bar->BarRect.Max.x - scrolling_buttons_width + arrow_button_size.x, tab_bar->BarRect.Min.y); - if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_Repeat)) + window->DC.CursorPos = ImVec2(x + arrow_button_size.x, tab_bar->BarRect.Min.y); + if (ArrowButtonEx("##>", ImGuiDir_Right, arrow_button_size, ImGuiButtonFlags_PressedOnClick)) select_dir = +1; + PopItemFlag(); PopStyleColor(2); g.IO.KeyRepeatRate = backup_repeat_rate; g.IO.KeyRepeatDelay = backup_repeat_delay; - if (want_clip_rect) - PopClipRect(); - + ImGuiTabItem* tab_to_scroll_to = NULL; if (select_dir != 0) if (ImGuiTabItem* tab_item = TabBarFindTabByID(tab_bar, tab_bar->SelectedTabId)) { - int selected_order = tab_bar->GetTabOrder(tab_item); + int selected_order = TabBarGetTabOrder(tab_bar, tab_item); int target_order = selected_order + select_dir; - tab_to_select = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order]; // If we are at the end of the list, still scroll to make our tab visible + + // Skip tab item buttons until another tab item is found or end is reached + while (tab_to_scroll_to == NULL) + { + // If we are at the end of the list, still scroll to make our tab visible + tab_to_scroll_to = &tab_bar->Tabs[(target_order >= 0 && target_order < tab_bar->Tabs.Size) ? target_order : selected_order]; + + // Cross through buttons + // (even if first/last item is a button, return it so we can update the scroll) + if (tab_to_scroll_to->Flags & ImGuiTabItemFlags_Button) + { + target_order += select_dir; + selected_order += select_dir; + tab_to_scroll_to = (target_order < 0 || target_order >= tab_bar->Tabs.Size) ? tab_to_scroll_to : NULL; + } + } } window->DC.CursorPos = backup_cursor_pos; tab_bar->BarRect.Max.x -= scrolling_buttons_width + 1.0f; - return tab_to_select; + return tab_to_scroll_to; } static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) @@ -6797,7 +9990,7 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) arrow_col.w *= 0.5f; PushStyleColor(ImGuiCol_Text, arrow_col); PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); - bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview); + bool open = BeginCombo("##v", NULL, ImGuiComboFlags_NoPreview | ImGuiComboFlags_HeightLargest); PopStyleColor(2); ImGuiTabItem* tab_to_select = NULL; @@ -6806,7 +9999,10 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) for (int tab_n = 0; tab_n < tab_bar->Tabs.Size; tab_n++) { ImGuiTabItem* tab = &tab_bar->Tabs[tab_n]; - const char* tab_name = tab_bar->GetTabName(tab); + if (tab->Flags & ImGuiTabItemFlags_Button) + continue; + + const char* tab_name = TabBarGetTabName(tab_bar, tab); if (Selectable(tab_name, tab_bar->SelectedTabId == tab->ID)) tab_to_select = tab; } @@ -6820,11 +10016,9 @@ static ImGuiTabItem* ImGui::TabBarTabListPopupButton(ImGuiTabBar* tab_bar) //------------------------------------------------------------------------- // [SECTION] Widgets: BeginTabItem, EndTabItem, etc. //------------------------------------------------------------------------- -// [BETA API] API may evolve! This code has been extracted out of the Docking branch, -// and some of the construct which are not used in Master may be left here to facilitate merging. -//------------------------------------------------------------------------- // - BeginTabItem() // - EndTabItem() +// - TabItemButton() // - TabItemEx() [Internal] // - SetTabItemClosed() // - TabItemCalcSize() [Internal] @@ -6842,10 +10036,12 @@ bool ImGui::BeginTabItem(const char* label, bool* p_open, ImGuiTabItemFlags f ImGuiTabBar* tab_bar = g.CurrentTabBar; if (tab_bar == NULL) { - IM_ASSERT(tab_bar && "Needs to be called between BeginTabBar() and EndTabBar()!"); - return false; // FIXME-ERRORHANDLING + IM_ASSERT_USER_ERROR(tab_bar, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return false; } - bool ret = TabItemEx(tab_bar, label, p_open, flags); + IM_ASSERT((flags & ImGuiTabItemFlags_Button) == 0); // BeginTabItem() Can't be used with button flags, use TabItemButton() instead! + + bool ret = TabItemEx(tab_bar, label, p_open, flags, NULL); if (ret && !(flags & ImGuiTabItemFlags_NoPushId)) { ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx]; @@ -6864,40 +10060,82 @@ void ImGui::EndTabItem() ImGuiTabBar* tab_bar = g.CurrentTabBar; if (tab_bar == NULL) { - IM_ASSERT(tab_bar != NULL && "Needs to be called between BeginTabBar() and EndTabBar()!"); + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); return; } IM_ASSERT(tab_bar->LastTabItemIdx >= 0); ImGuiTabItem* tab = &tab_bar->Tabs[tab_bar->LastTabItemIdx]; if (!(tab->Flags & ImGuiTabItemFlags_NoPushId)) - window->IDStack.pop_back(); + PopID(); } -bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags) +bool ImGui::TabItemButton(const char* label, ImGuiTabItemFlags flags) { - // Layout whole tab bar if not already done - if (tab_bar->WantLayout) - TabBarLayout(tab_bar); - ImGuiContext& g = *GImGui; ImGuiWindow* window = g.CurrentWindow; if (window->SkipItems) return false; - const ImGuiStyle& style = g.Style; - const ImGuiID id = TabBarCalcTabID(tab_bar, label); + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return false; + } + return TabItemEx(tab_bar, label, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder, NULL); +} - // If the user called us with *p_open == false, we early out and don't render. We make a dummy call to ItemAdd() so that attempts to use a contextual popup menu with an implicit ID won't use an older ID. +void ImGui::TabItemSpacing(const char* str_id, ImGuiTabItemFlags flags, float width) +{ + ImGuiContext& g = *GImGui; + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return; + + ImGuiTabBar* tab_bar = g.CurrentTabBar; + if (tab_bar == NULL) + { + IM_ASSERT_USER_ERROR(tab_bar != NULL, "Needs to be called between BeginTabBar() and EndTabBar()!"); + return; + } + SetNextItemWidth(width); + TabItemEx(tab_bar, str_id, NULL, flags | ImGuiTabItemFlags_Button | ImGuiTabItemFlags_NoReorder | ImGuiTabItemFlags_Invisible, NULL); +} + +bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, ImGuiTabItemFlags flags, ImGuiWindow* docked_window) +{ + // Layout whole tab bar if not already done + ImGuiContext& g = *GImGui; + if (tab_bar->WantLayout) + { + ImGuiNextItemData backup_next_item_data = g.NextItemData; + TabBarLayout(tab_bar); + g.NextItemData = backup_next_item_data; + } + ImGuiWindow* window = g.CurrentWindow; + if (window->SkipItems) + return false; + + const ImGuiStyle& style = g.Style; + const ImGuiID id = TabBarCalcTabID(tab_bar, label, docked_window); + + // If the user called us with *p_open == false, we early out and don't render. + // We make a call to ItemAdd() so that attempts to use a contextual popup menu with an implicit ID won't use an older ID. + IMGUI_TEST_ENGINE_ITEM_INFO(id, label, g.LastItemData.StatusFlags); if (p_open && !*p_open) { - PushItemFlag(ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus, true); - ItemAdd(ImRect(), id); - PopItemFlag(); + ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav); return false; } - // Calculate tab contents size - ImVec2 size = TabItemCalcSize(label, p_open != NULL); + IM_ASSERT(!p_open || !(flags & ImGuiTabItemFlags_Button)); + IM_ASSERT((flags & (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)) != (ImGuiTabItemFlags_Leading | ImGuiTabItemFlags_Trailing)); // Can't use both Leading and Trailing + + // Store into ImGuiTabItemFlags_NoCloseButton, also honor ImGuiTabItemFlags_NoCloseButton passed by user (although not documented) + if (flags & ImGuiTabItemFlags_NoCloseButton) + p_open = NULL; + else if (p_open == NULL) + flags |= ImGuiTabItemFlags_NoCloseButton; // Acquire tab data ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, id); @@ -6907,55 +10145,70 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, tab_bar->Tabs.push_back(ImGuiTabItem()); tab = &tab_bar->Tabs.back(); tab->ID = id; - tab->Width = size.x; - tab_is_new = true; + tab_bar->TabsAddedNew = tab_is_new = true; } - tab_bar->LastTabItemIdx = (short)tab_bar->Tabs.index_from_ptr(tab); - tab->WidthContents = size.x; + tab_bar->LastTabItemIdx = (ImS16)tab_bar->Tabs.index_from_ptr(tab); - if (p_open == NULL) - flags |= ImGuiTabItemFlags_NoCloseButton; + // Calculate tab contents size + ImVec2 size = TabItemCalcSize(label, (p_open != NULL) || (flags & ImGuiTabItemFlags_UnsavedDocument)); + tab->RequestedWidth = -1.0f; + if (g.NextItemData.HasFlags & ImGuiNextItemDataFlags_HasWidth) + size.x = tab->RequestedWidth = g.NextItemData.Width; + if (tab_is_new) + tab->Width = ImMax(1.0f, size.x); + tab->ContentWidth = size.x; + tab->BeginOrder = tab_bar->TabsActiveCount++; const bool tab_bar_appearing = (tab_bar->PrevFrameVisible + 1 < g.FrameCount); const bool tab_bar_focused = (tab_bar->Flags & ImGuiTabBarFlags_IsFocused) != 0; const bool tab_appearing = (tab->LastFrameVisible + 1 < g.FrameCount); + const bool tab_just_unsaved = (flags & ImGuiTabItemFlags_UnsavedDocument) && !(tab->Flags & ImGuiTabItemFlags_UnsavedDocument); + const bool is_tab_button = (flags & ImGuiTabItemFlags_Button) != 0; tab->LastFrameVisible = g.FrameCount; tab->Flags = flags; + tab->Window = docked_window; - // Append name with zero-terminator - tab->NameOffset = tab_bar->TabsNames.size(); - tab_bar->TabsNames.append(label, label + strlen(label) + 1); - - // If we are not reorderable, always reset offset based on submission order. - // (We already handled layout and sizing using the previous known order, but sizing is not affected by order!) - if (!tab_appearing && !(tab_bar->Flags & ImGuiTabBarFlags_Reorderable)) + // Append name _WITH_ the zero-terminator + // (regular tabs are permitted in a DockNode tab bar, but window tabs not permitted in a non-DockNode tab bar) + if (docked_window != NULL) { - tab->Offset = tab_bar->OffsetNextTab; - tab_bar->OffsetNextTab += tab->Width + g.Style.ItemInnerSpacing.x; + IM_ASSERT(tab_bar->Flags & ImGuiTabBarFlags_DockNode); + tab->NameOffset = -1; + } + else + { + tab->NameOffset = (ImS32)tab_bar->TabsNames.size(); + tab_bar->TabsNames.append(label, label + ImStrlen(label) + 1); } // Update selected tab - if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0) - if (!tab_bar_appearing || tab_bar->SelectedTabId == 0) - tab_bar->NextSelectedTabId = id; // New tabs gets activated - if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // SetSelected can only be passed on explicit tab bar - tab_bar->NextSelectedTabId = id; + if (!is_tab_button) + { + if (tab_appearing && (tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs) && tab_bar->NextSelectedTabId == 0) + if (!tab_bar_appearing || tab_bar->SelectedTabId == 0) + TabBarQueueFocus(tab_bar, tab); // New tabs gets activated + if ((flags & ImGuiTabItemFlags_SetSelected) && (tab_bar->SelectedTabId != id)) // _SetSelected can only be passed on explicit tab bar + TabBarQueueFocus(tab_bar, tab); + } // Lock visibility + // (Note: tab_contents_visible != tab_selected... because CTRL+TAB operations may preview some tabs without selecting them!) bool tab_contents_visible = (tab_bar->VisibleTabId == id); if (tab_contents_visible) tab_bar->VisibleTabWasSubmitted = true; // On the very first frame of a tab bar we let first tab contents be visible to minimize appearing glitches - if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing) + if (!tab_contents_visible && tab_bar->SelectedTabId == 0 && tab_bar_appearing && docked_window == NULL) if (tab_bar->Tabs.Size == 1 && !(tab_bar->Flags & ImGuiTabBarFlags_AutoSelectNewTabs)) tab_contents_visible = true; - if (tab_appearing && !(tab_bar_appearing && !tab_is_new)) + // Note that tab_is_new is not necessarily the same as tab_appearing! When a tab bar stops being submitted + // and then gets submitted again, the tabs will have 'tab_appearing=true' but 'tab_is_new=false'. + if (tab_appearing && (!tab_bar_appearing || tab_is_new)) { - PushItemFlag(ImGuiItemFlags_NoNav | ImGuiItemFlags_NoNavDefaultFocus, true); - ItemAdd(ImRect(), id); - PopItemFlag(); + ItemAdd(ImRect(), id, NULL, ImGuiItemFlags_NoNav); + if (is_tab_button) + return false; return tab_contents_visible; } @@ -6966,15 +10219,19 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, const ImVec2 backup_main_cursor_pos = window->DC.CursorPos; // Layout + const bool is_central_section = (tab->Flags & ImGuiTabItemFlags_SectionMask_) == 0; size.x = tab->Width; - window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2((float)(int)tab->Offset - tab_bar->ScrollingAnim, 0.0f); + if (is_central_section) + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(IM_TRUNC(tab->Offset - tab_bar->ScrollingAnim), 0.0f); + else + window->DC.CursorPos = tab_bar->BarRect.Min + ImVec2(tab->Offset, 0.0f); ImVec2 pos = window->DC.CursorPos; ImRect bb(pos, pos + size); // We don't have CPU clipping primitives to clip the CloseButton (until it becomes a texture), so need to add an extra draw call (temporary in the case of vertical animation) - bool want_clip_rect = (bb.Min.x < tab_bar->BarRect.Min.x) || (bb.Max.x > tab_bar->BarRect.Max.x); + const bool want_clip_rect = is_central_section && (bb.Min.x < tab_bar->ScrollingRectMinX || bb.Max.x > tab_bar->ScrollingRectMaxX); if (want_clip_rect) - PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->BarRect.Min.x), bb.Min.y - 1), ImVec2(tab_bar->BarRect.Max.x, bb.Max.y), true); + PushClipRect(ImVec2(ImMax(bb.Min.x, tab_bar->ScrollingRectMinX), bb.Min.y - 1), ImVec2(tab_bar->ScrollingRectMaxX, bb.Max.y), true); ImVec2 backup_cursor_max_pos = window->DC.CursorMaxPos; ItemSize(bb.GetSize(), style.FramePadding.y); @@ -6989,69 +10246,156 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, } // Click to Select a tab - ImGuiButtonFlags button_flags = (ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_AllowItemOverlap); - if (g.DragDropActive) + ImGuiButtonFlags button_flags = ((is_tab_button ? ImGuiButtonFlags_PressedOnClickRelease : ImGuiButtonFlags_PressedOnClick) | ImGuiButtonFlags_AllowOverlap); + if (g.DragDropActive && !g.DragDropPayload.IsDataType(IMGUI_PAYLOAD_TYPE_WINDOW)) // FIXME: May be an opt-in property of the payload to disable this button_flags |= ImGuiButtonFlags_PressedOnDragDropHold; - bool hovered, held; - bool pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); - if (pressed) - tab_bar->NextSelectedTabId = id; - hovered |= (g.HoveredId == id); + bool hovered, held, pressed; + if (flags & ImGuiTabItemFlags_Invisible) + hovered = held = pressed = false; + else + pressed = ButtonBehavior(bb, id, &hovered, &held, button_flags); + if (pressed && !is_tab_button) + TabBarQueueFocus(tab_bar, tab); - // Allow the close button to overlap unless we are dragging (in which case we don't want any overlapping tabs to be hovered) - if (!held) - SetItemAllowOverlap(); + // Transfer active id window so the active id is not owned by the dock host (as StartMouseMovingWindow() + // will only do it on the drag). This allows FocusWindow() to be more conservative in how it clears active id. + if (held && docked_window && g.ActiveId == id && g.ActiveIdIsJustActivated) + g.ActiveIdWindow = docked_window; - // Drag and drop: re-order tabs - if (held && !tab_appearing && IsMouseDragging(0)) + // Drag and drop a single floating window node moves it + ImGuiDockNode* node = docked_window ? docked_window->DockNode : NULL; + const bool single_floating_window_node = node && node->IsFloatingNode() && (node->Windows.Size == 1); + if (held && single_floating_window_node && IsMouseDragging(0, 0.0f)) { - if (!g.DragDropActive && (tab_bar->Flags & ImGuiTabBarFlags_Reorderable)) + // Move + StartMouseMovingWindow(docked_window); + } + else if (held && !tab_appearing && IsMouseDragging(0)) + { + // Drag and drop: re-order tabs + int drag_dir = 0; + float drag_distance_from_edge_x = 0.0f; + if (!g.DragDropActive && ((tab_bar->Flags & ImGuiTabBarFlags_Reorderable) || (docked_window != NULL))) { // While moving a tab it will jump on the other side of the mouse, so we also test for MouseDelta.x if (g.IO.MouseDelta.x < 0.0f && g.IO.MousePos.x < bb.Min.x) { - if (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) - TabBarQueueChangeTabOrder(tab_bar, tab, -1); + drag_dir = -1; + drag_distance_from_edge_x = bb.Min.x - g.IO.MousePos.x; + TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); } else if (g.IO.MouseDelta.x > 0.0f && g.IO.MousePos.x > bb.Max.x) { - if (tab_bar->Flags & ImGuiTabBarFlags_Reorderable) - TabBarQueueChangeTabOrder(tab_bar, tab, +1); + drag_dir = +1; + drag_distance_from_edge_x = g.IO.MousePos.x - bb.Max.x; + TabBarQueueReorderFromMousePos(tab_bar, tab, g.IO.MousePos); + } + } + + // Extract a Dockable window out of it's tab bar + const bool can_undock = docked_window != NULL && !(docked_window->Flags & ImGuiWindowFlags_NoMove) && !(node->MergedFlags & ImGuiDockNodeFlags_NoUndocking); + if (can_undock) + { + // We use a variable threshold to distinguish dragging tabs within a tab bar and extracting them out of the tab bar + bool undocking_tab = (g.DragDropActive && g.DragDropPayload.SourceId == id); + if (!undocking_tab) //&& (!g.IO.ConfigDockingWithShift || g.IO.KeyShift) + { + float threshold_base = g.FontSize; + float threshold_x = (threshold_base * 2.2f); + float threshold_y = (threshold_base * 1.5f) + ImClamp((ImFabs(g.IO.MouseDragMaxDistanceAbs[0].x) - threshold_base * 2.0f) * 0.20f, 0.0f, threshold_base * 4.0f); + //GetForegroundDrawList()->AddRect(ImVec2(bb.Min.x - threshold_x, bb.Min.y - threshold_y), ImVec2(bb.Max.x + threshold_x, bb.Max.y + threshold_y), IM_COL32_WHITE); // [DEBUG] + + float distance_from_edge_y = ImMax(bb.Min.y - g.IO.MousePos.y, g.IO.MousePos.y - bb.Max.y); + if (distance_from_edge_y >= threshold_y) + undocking_tab = true; + if (drag_distance_from_edge_x > threshold_x) + if ((drag_dir < 0 && TabBarGetTabOrder(tab_bar, tab) == 0) || (drag_dir > 0 && TabBarGetTabOrder(tab_bar, tab) == tab_bar->Tabs.Size - 1)) + undocking_tab = true; + } + + if (undocking_tab) + { + // Undock + // FIXME: refactor to share more code with e.g. StartMouseMovingWindow + DockContextQueueUndockWindow(&g, docked_window); + g.MovingWindow = docked_window; + SetActiveID(g.MovingWindow->MoveId, g.MovingWindow); + g.ActiveIdClickOffset -= g.MovingWindow->Pos - bb.Min; + g.ActiveIdNoClearOnFocusLoss = true; + SetActiveIdUsingAllKeyboardKeys(); } } } #if 0 - if (hovered && g.HoveredIdNotActiveTimer > 0.50f && bb.GetWidth() < tab->WidthContents) + if (hovered && g.HoveredIdNotActiveTimer > TOOLTIP_DELAY && bb.GetWidth() < tab->ContentWidth) { // Enlarge tab display when hovering - bb.Max.x = bb.Min.x + (float)(int)ImLerp(bb.GetWidth(), tab->WidthContents, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f)); + bb.Max.x = bb.Min.x + IM_TRUNC(ImLerp(bb.GetWidth(), tab->ContentWidth, ImSaturate((g.HoveredIdNotActiveTimer - 0.40f) * 6.0f))); display_draw_list = GetForegroundDrawList(window); TabItemBackground(display_draw_list, bb, flags, GetColorU32(ImGuiCol_TitleBgActive)); } #endif // Render tab shape - ImDrawList* display_draw_list = window->DrawList; - const ImU32 tab_col = GetColorU32((held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabActive : ImGuiCol_TabUnfocusedActive) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabUnfocused)); - TabItemBackground(display_draw_list, bb, flags, tab_col); - RenderNavHighlight(bb, id); - - // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget. - const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); - if (hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1))) - tab_bar->NextSelectedTabId = id; - - if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) - flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; - - // Render tab label, process close button - const ImGuiID close_button_id = p_open ? window->GetID((void*)((intptr_t)id + 1)) : 0; - bool just_closed = TabItemLabelAndCloseButton(display_draw_list, bb, flags, tab_bar->FramePadding, label, id, close_button_id); - if (just_closed && p_open != NULL) + const bool is_visible = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_Visible) && !(flags & ImGuiTabItemFlags_Invisible); + if (is_visible) { - *p_open = false; - TabBarCloseTab(tab_bar, tab); + ImDrawList* display_draw_list = window->DrawList; + const ImU32 tab_col = GetColorU32((held || hovered) ? ImGuiCol_TabHovered : tab_contents_visible ? (tab_bar_focused ? ImGuiCol_TabSelected : ImGuiCol_TabDimmedSelected) : (tab_bar_focused ? ImGuiCol_Tab : ImGuiCol_TabDimmed)); + TabItemBackground(display_draw_list, bb, flags, tab_col); + if (tab_contents_visible && (tab_bar->Flags & ImGuiTabBarFlags_DrawSelectedOverline) && style.TabBarOverlineSize > 0.0f) + { + // Might be moved to TabItemBackground() ? + ImVec2 tl = bb.GetTL() + ImVec2(0, 1.0f * g.CurrentDpiScale); + ImVec2 tr = bb.GetTR() + ImVec2(0, 1.0f * g.CurrentDpiScale); + ImU32 overline_col = GetColorU32(tab_bar_focused ? ImGuiCol_TabSelectedOverline : ImGuiCol_TabDimmedSelectedOverline); + if (style.TabRounding > 0.0f) + { + float rounding = style.TabRounding; + display_draw_list->PathArcToFast(tl + ImVec2(+rounding, +rounding), rounding, 7, 9); + display_draw_list->PathArcToFast(tr + ImVec2(-rounding, +rounding), rounding, 9, 11); + display_draw_list->PathStroke(overline_col, 0, style.TabBarOverlineSize); + } + else + { + display_draw_list->AddLine(tl - ImVec2(0.5f, 0.5f), tr - ImVec2(0.5f, 0.5f), overline_col, style.TabBarOverlineSize); + } + } + RenderNavCursor(bb, id); + + // Select with right mouse button. This is so the common idiom for context menu automatically highlight the current widget. + const bool hovered_unblocked = IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByPopup); + if (tab_bar->SelectedTabId != tab->ID && hovered_unblocked && (IsMouseClicked(1) || IsMouseReleased(1)) && !is_tab_button) + TabBarQueueFocus(tab_bar, tab); + + if (tab_bar->Flags & ImGuiTabBarFlags_NoCloseWithMiddleMouseButton) + flags |= ImGuiTabItemFlags_NoCloseWithMiddleMouseButton; + + // Render tab label, process close button + const ImGuiID close_button_id = p_open ? GetIDWithSeed("#CLOSE", NULL, docked_window ? docked_window->ID : id) : 0; + bool just_closed; + bool text_clipped; + TabItemLabelAndCloseButton(display_draw_list, bb, tab_just_unsaved ? (flags & ~ImGuiTabItemFlags_UnsavedDocument) : flags, tab_bar->FramePadding, label, id, close_button_id, tab_contents_visible, &just_closed, &text_clipped); + if (just_closed && p_open != NULL) + { + *p_open = false; + TabBarCloseTab(tab_bar, tab); + } + + // Forward Hovered state so IsItemHovered() after Begin() can work (even though we are technically hovering our parent) + // That state is copied to window->DockTabItemStatusFlags by our caller. + if (docked_window && (hovered || g.HoveredId == close_button_id)) + g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_HoveredWindow; + + // Tooltip + // (Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer-> seems ok) + // (We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar, which g.HoveredId ignores) + // FIXME: This is a mess. + // FIXME: We may want disabled tab to still display the tooltip? + if (text_clipped && g.HoveredId == id && !held) + if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip) && !(tab->Flags & ImGuiTabItemFlags_NoTooltip)) + SetItemTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); } // Restore main window position so user can draw there @@ -7059,17 +10403,15 @@ bool ImGui::TabItemEx(ImGuiTabBar* tab_bar, const char* label, bool* p_open, PopClipRect(); window->DC.CursorPos = backup_main_cursor_pos; - // Tooltip (FIXME: Won't work over the close button because ItemOverlap systems messes up with HoveredIdTimer) - // We test IsItemHovered() to discard e.g. when another item is active or drag and drop over the tab bar (which g.HoveredId ignores) - if (g.HoveredId == id && !held && g.HoveredIdNotActiveTimer > 0.50f && IsItemHovered()) - if (!(tab_bar->Flags & ImGuiTabBarFlags_NoTooltip)) - SetTooltip("%.*s", (int)(FindRenderedTextEnd(label) - label), label); - + IM_ASSERT(!is_tab_button || !(tab_bar->SelectedTabId == tab->ID && is_tab_button)); // TabItemButton should not be selected + if (is_tab_button) + return pressed; return tab_contents_visible; } // [Public] This is call is 100% optional but it allows to remove some one-frame glitches when a tab has been unexpectedly removed. -// To use it to need to call the function SetTabItemClosed() after BeginTabBar() and before any call to BeginTabItem() +// To use it to need to call the function SetTabItemClosed() between BeginTabBar() and EndTabBar(). +// Tabs closed by the close button will automatically be flagged to avoid this issue. void ImGui::SetTabItemClosed(const char* label) { ImGuiContext& g = *GImGui; @@ -7077,24 +10419,39 @@ void ImGui::SetTabItemClosed(const char* label) if (is_within_manual_tab_bar) { ImGuiTabBar* tab_bar = g.CurrentTabBar; - IM_ASSERT(tab_bar->WantLayout); // Needs to be called AFTER BeginTabBar() and BEFORE the first call to BeginTabItem() - ImGuiID tab_id = TabBarCalcTabID(tab_bar, label); - TabBarRemoveTab(tab_bar, tab_id); + ImGuiID tab_id = TabBarCalcTabID(tab_bar, label, NULL); + if (ImGuiTabItem* tab = TabBarFindTabByID(tab_bar, tab_id)) + tab->WantClose = true; // Will be processed by next call to TabBarLayout() + } + else if (ImGuiWindow* window = FindWindowByName(label)) + { + if (window->DockIsActive) + if (ImGuiDockNode* node = window->DockNode) + { + ImGuiID tab_id = TabBarCalcTabID(node->TabBar, label, window); + TabBarRemoveTab(node->TabBar, tab_id); + window->DockTabWantClose = true; + } } } -ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button) +ImVec2 ImGui::TabItemCalcSize(const char* label, bool has_close_button_or_unsaved_marker) { ImGuiContext& g = *GImGui; ImVec2 label_size = CalcTextSize(label, NULL, true); ImVec2 size = ImVec2(label_size.x + g.Style.FramePadding.x, label_size.y + g.Style.FramePadding.y * 2.0f); - if (has_close_button) + if (has_close_button_or_unsaved_marker) size.x += g.Style.FramePadding.x + (g.Style.ItemInnerSpacing.x + g.FontSize); // We use Y intentionally to fit the close button circle. else size.x += g.Style.FramePadding.x + 1.0f; return ImVec2(ImMin(size.x, TabBarCalcMaxTabWidth()), size.y); } +ImVec2 ImGui::TabItemCalcSize(ImGuiWindow* window) +{ + return TabItemCalcSize(window->Name, window->HasCloseButton || (window->Flags & ImGuiWindowFlags_UnsavedDocument)); +} + void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImU32 col) { // While rendering tabs, we trim 1 pixel off the top of our bounding box so they can fit within a regular frame height while looking "detached" from it. @@ -7102,9 +10459,9 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI const float width = bb.GetWidth(); IM_UNUSED(flags); IM_ASSERT(width > 0.0f); - const float rounding = ImMax(0.0f, ImMin(g.Style.TabRounding, width * 0.5f - 1.0f)); + const float rounding = ImMax(0.0f, ImMin((flags & ImGuiTabItemFlags_Button) ? g.Style.FrameRounding : g.Style.TabRounding, width * 0.5f - 1.0f)); const float y1 = bb.Min.y + 1.0f; - const float y2 = bb.Max.y - 1.0f; + const float y2 = bb.Max.y - g.Style.TabBarBorderSize; draw_list->PathLineTo(ImVec2(bb.Min.x, y2)); draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding, y1 + rounding), rounding, 6, 9); draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding, y1 + rounding), rounding, 9, 12); @@ -7116,512 +10473,137 @@ void ImGui::TabItemBackground(ImDrawList* draw_list, const ImRect& bb, ImGuiTabI draw_list->PathArcToFast(ImVec2(bb.Min.x + rounding + 0.5f, y1 + rounding + 0.5f), rounding, 6, 9); draw_list->PathArcToFast(ImVec2(bb.Max.x - rounding - 0.5f, y1 + rounding + 0.5f), rounding, 9, 12); draw_list->PathLineTo(ImVec2(bb.Max.x - 0.5f, y2)); - draw_list->PathStroke(GetColorU32(ImGuiCol_Border), false, g.Style.TabBorderSize); + draw_list->PathStroke(GetColorU32(ImGuiCol_Border), 0, g.Style.TabBorderSize); } } // Render text label (with custom clipping) + Unsaved Document marker + Close Button logic // We tend to lock style.FramePadding for a given tab-bar, hence the 'frame_padding' parameter. -bool ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id) +void ImGui::TabItemLabelAndCloseButton(ImDrawList* draw_list, const ImRect& bb, ImGuiTabItemFlags flags, ImVec2 frame_padding, const char* label, ImGuiID tab_id, ImGuiID close_button_id, bool is_contents_visible, bool* out_just_closed, bool* out_text_clipped) { ImGuiContext& g = *GImGui; ImVec2 label_size = CalcTextSize(label, NULL, true); + + if (out_just_closed) + *out_just_closed = false; + if (out_text_clipped) + *out_text_clipped = false; + if (bb.GetWidth() <= 1.0f) - return false; + return; + + // In Style V2 we'll have full override of all colors per state (e.g. focused, selected) + // But right now if you want to alter text color of tabs this is what you need to do. +#if 0 + const float backup_alpha = g.Style.Alpha; + if (!is_contents_visible) + g.Style.Alpha *= 0.7f; +#endif // Render text label (with clipping + alpha gradient) + unsaved marker - const char* TAB_UNSAVED_MARKER = "*"; ImRect text_pixel_clip_bb(bb.Min.x + frame_padding.x, bb.Min.y + frame_padding.y, bb.Max.x - frame_padding.x, bb.Max.y); - if (flags & ImGuiTabItemFlags_UnsavedDocument) - { - text_pixel_clip_bb.Max.x -= CalcTextSize(TAB_UNSAVED_MARKER, NULL, false).x; - ImVec2 unsaved_marker_pos(ImMin(bb.Min.x + frame_padding.x + label_size.x + 2, text_pixel_clip_bb.Max.x), bb.Min.y + frame_padding.y + (float)(int)(-g.FontSize * 0.25f)); - RenderTextClippedEx(draw_list, unsaved_marker_pos, bb.Max - frame_padding, TAB_UNSAVED_MARKER, NULL, NULL); - } ImRect text_ellipsis_clip_bb = text_pixel_clip_bb; - // Close Button + // Return clipped state ignoring the close button + if (out_text_clipped) + { + *out_text_clipped = (text_ellipsis_clip_bb.Min.x + label_size.x) > text_pixel_clip_bb.Max.x; + //draw_list->AddCircle(text_ellipsis_clip_bb.Min, 3.0f, *out_text_clipped ? IM_COL32(255, 0, 0, 255) : IM_COL32(0, 255, 0, 255)); + } + + const float button_sz = g.FontSize; + const ImVec2 button_pos(ImMax(bb.Min.x, bb.Max.x - frame_padding.x - button_sz), bb.Min.y + frame_padding.y); + + // Close Button & Unsaved Marker // We are relying on a subtle and confusing distinction between 'hovered' and 'g.HoveredId' which happens because we are using ImGuiButtonFlags_AllowOverlapMode + SetItemAllowOverlap() // 'hovered' will be true when hovering the Tab but NOT when hovering the close button // 'g.HoveredId==id' will be true when hovering the Tab including when hovering the close button // 'g.ActiveId==close_button_id' will be true when we are holding on the close button, in which case both hovered booleans are false bool close_button_pressed = false; bool close_button_visible = false; + bool is_hovered = g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == tab_id || g.ActiveId == close_button_id; // Any interaction account for this too. + if (close_button_id != 0) - if (g.HoveredId == tab_id || g.HoveredId == close_button_id || g.ActiveId == close_button_id) - close_button_visible = true; - if (close_button_visible) { - ImGuiItemHoveredDataBackup last_item_backup; - const float close_button_sz = g.FontSize; - PushStyleVar(ImGuiStyleVar_FramePadding, frame_padding); - if (CloseButton(close_button_id, ImVec2(bb.Max.x - frame_padding.x * 2.0f - close_button_sz, bb.Min.y))) + if (is_contents_visible) + close_button_visible = (g.Style.TabCloseButtonMinWidthSelected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthSelected)); + else + close_button_visible = (g.Style.TabCloseButtonMinWidthUnselected < 0.0f) ? true : (is_hovered && bb.GetWidth() >= ImMax(button_sz, g.Style.TabCloseButtonMinWidthUnselected)); + } + + // When tabs/document is unsaved, the unsaved marker takes priority over the close button. + const bool unsaved_marker_visible = (flags & ImGuiTabItemFlags_UnsavedDocument) != 0 && (button_pos.x + button_sz <= bb.Max.x) && (!close_button_visible || !is_hovered); + if (unsaved_marker_visible) + { + const ImRect bullet_bb(button_pos, button_pos + ImVec2(button_sz, button_sz)); + RenderBullet(draw_list, bullet_bb.GetCenter(), GetColorU32(ImGuiCol_Text)); + } + else if (close_button_visible) + { + ImGuiLastItemData last_item_backup = g.LastItemData; + if (CloseButton(close_button_id, button_pos)) close_button_pressed = true; - PopStyleVar(); - last_item_backup.Restore(); + g.LastItemData = last_item_backup; // Close with middle mouse button - if (!(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2)) + if (is_hovered && !(flags & ImGuiTabItemFlags_NoCloseWithMiddleMouseButton) && IsMouseClicked(2)) close_button_pressed = true; - - text_pixel_clip_bb.Max.x -= close_button_sz; } + // This is all rather complicated + // (the main idea is that because the close button only appears on hover, we don't want it to alter the ellipsis position) + // FIXME: if FramePadding is noticeably large, ellipsis_max_x will be wrong here (e.g. #3497), maybe for consistency that parameter of RenderTextEllipsis() shouldn't exist.. float ellipsis_max_x = close_button_visible ? text_pixel_clip_bb.Max.x : bb.Max.x - 1.0f; + if (close_button_visible || unsaved_marker_visible) + { + text_pixel_clip_bb.Max.x -= close_button_visible ? (button_sz) : (button_sz * 0.80f); + text_ellipsis_clip_bb.Max.x -= unsaved_marker_visible ? (button_sz * 0.80f) : 0.0f; + ellipsis_max_x = text_pixel_clip_bb.Max.x; + } + LogSetNextTextDecoration("/", "\\"); RenderTextEllipsis(draw_list, text_ellipsis_clip_bb.Min, text_ellipsis_clip_bb.Max, text_pixel_clip_bb.Max.x, ellipsis_max_x, label, NULL, &label_size); - return close_button_pressed; +#if 0 + if (!is_contents_visible) + g.Style.Alpha = backup_alpha; +#endif + + if (out_just_closed) + *out_just_closed = close_button_pressed; } //------------------------------------------------------------------------- -// [SECTION] Widgets: Columns, BeginColumns, EndColumns, etc. -// In the current version, Columns are very weak. Needs to be replaced with a more full-featured system. -//------------------------------------------------------------------------- -// - GetColumnIndex() -// - GetColumnCount() -// - GetColumnOffset() -// - GetColumnWidth() -// - SetColumnOffset() -// - SetColumnWidth() -// - PushColumnClipRect() [Internal] -// - PushColumnsBackground() [Internal] -// - PopColumnsBackground() [Internal] -// - FindOrCreateColumns() [Internal] -// - GetColumnsID() [Internal] -// - BeginColumns() -// - NextColumn() -// - EndColumns() -// - Columns() -//------------------------------------------------------------------------- - -int ImGui::GetColumnIndex() -{ - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.CurrentColumns ? window->DC.CurrentColumns->Current : 0; -} - -int ImGui::GetColumnsCount() -{ - ImGuiWindow* window = GetCurrentWindowRead(); - return window->DC.CurrentColumns ? window->DC.CurrentColumns->Count : 1; -} - -float ImGui::GetColumnOffsetFromNorm(const ImGuiColumns* columns, float offset_norm) -{ - return offset_norm * (columns->OffMaxX - columns->OffMinX); -} - -float ImGui::GetColumnNormFromOffset(const ImGuiColumns* columns, float offset) -{ - return offset / (columns->OffMaxX - columns->OffMinX); -} - -static const float COLUMNS_HIT_RECT_HALF_WIDTH = 4.0f; - -static float GetDraggedColumnOffset(ImGuiColumns* columns, int column_index) -{ - // Active (dragged) column always follow mouse. The reason we need this is that dragging a column to the right edge of an auto-resizing - // window creates a feedback loop because we store normalized positions. So while dragging we enforce absolute positioning. - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - IM_ASSERT(column_index > 0); // We are not supposed to drag column 0. - IM_ASSERT(g.ActiveId == columns->ID + ImGuiID(column_index)); - - float x = g.IO.MousePos.x - g.ActiveIdClickOffset.x + COLUMNS_HIT_RECT_HALF_WIDTH - window->Pos.x; - x = ImMax(x, ImGui::GetColumnOffset(column_index - 1) + g.Style.ColumnsMinSpacing); - if ((columns->Flags & ImGuiColumnsFlags_NoPreserveWidths)) - x = ImMin(x, ImGui::GetColumnOffset(column_index + 1) - g.Style.ColumnsMinSpacing); - - return x; -} - -float ImGui::GetColumnOffset(int column_index) -{ - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumns* columns = window->DC.CurrentColumns; - if (columns == NULL) - return 0.0f; - - if (column_index < 0) - column_index = columns->Current; - IM_ASSERT(column_index < columns->Columns.Size); - - const float t = columns->Columns[column_index].OffsetNorm; - const float x_offset = ImLerp(columns->OffMinX, columns->OffMaxX, t); - return x_offset; -} - -static float GetColumnWidthEx(ImGuiColumns* columns, int column_index, bool before_resize = false) -{ - if (column_index < 0) - column_index = columns->Current; - - float offset_norm; - if (before_resize) - offset_norm = columns->Columns[column_index + 1].OffsetNormBeforeResize - columns->Columns[column_index].OffsetNormBeforeResize; - else - offset_norm = columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm; - return ImGui::GetColumnOffsetFromNorm(columns, offset_norm); -} - -float ImGui::GetColumnWidth(int column_index) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - ImGuiColumns* columns = window->DC.CurrentColumns; - if (columns == NULL) - return GetContentRegionAvail().x; - - if (column_index < 0) - column_index = columns->Current; - return GetColumnOffsetFromNorm(columns, columns->Columns[column_index + 1].OffsetNorm - columns->Columns[column_index].OffsetNorm); -} - -void ImGui::SetColumnOffset(int column_index, float offset) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - ImGuiColumns* columns = window->DC.CurrentColumns; - IM_ASSERT(columns != NULL); - - if (column_index < 0) - column_index = columns->Current; - IM_ASSERT(column_index < columns->Columns.Size); - - const bool preserve_width = !(columns->Flags & ImGuiColumnsFlags_NoPreserveWidths) && (column_index < columns->Count-1); - const float width = preserve_width ? GetColumnWidthEx(columns, column_index, columns->IsBeingResized) : 0.0f; - - if (!(columns->Flags & ImGuiColumnsFlags_NoForceWithinWindow)) - offset = ImMin(offset, columns->OffMaxX - g.Style.ColumnsMinSpacing * (columns->Count - column_index)); - columns->Columns[column_index].OffsetNorm = GetColumnNormFromOffset(columns, offset - columns->OffMinX); - - if (preserve_width) - SetColumnOffset(column_index + 1, offset + ImMax(g.Style.ColumnsMinSpacing, width)); -} - -void ImGui::SetColumnWidth(int column_index, float width) -{ - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumns* columns = window->DC.CurrentColumns; - IM_ASSERT(columns != NULL); - - if (column_index < 0) - column_index = columns->Current; - SetColumnOffset(column_index + 1, GetColumnOffset(column_index) + width); -} - -void ImGui::PushColumnClipRect(int column_index) -{ - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumns* columns = window->DC.CurrentColumns; - if (column_index < 0) - column_index = columns->Current; - - ImGuiColumnData* column = &columns->Columns[column_index]; - PushClipRect(column->ClipRect.Min, column->ClipRect.Max, false); -} - -// Get into the columns background draw command (which is generally the same draw command as before we called BeginColumns) -void ImGui::PushColumnsBackground() -{ - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumns* columns = window->DC.CurrentColumns; - if (columns->Count == 1) - return; - window->DrawList->ChannelsSetCurrent(0); - int cmd_size = window->DrawList->CmdBuffer.Size; - PushClipRect(columns->HostClipRect.Min, columns->HostClipRect.Max, false); - IM_UNUSED(cmd_size); - IM_ASSERT(cmd_size == window->DrawList->CmdBuffer.Size); // Being in channel 0 this should not have created an ImDrawCmd -} - -void ImGui::PopColumnsBackground() -{ - ImGuiWindow* window = GetCurrentWindowRead(); - ImGuiColumns* columns = window->DC.CurrentColumns; - if (columns->Count == 1) - return; - window->DrawList->ChannelsSetCurrent(columns->Current + 1); - PopClipRect(); -} - -ImGuiColumns* ImGui::FindOrCreateColumns(ImGuiWindow* window, ImGuiID id) -{ - // We have few columns per window so for now we don't need bother much with turning this into a faster lookup. - for (int n = 0; n < window->ColumnsStorage.Size; n++) - if (window->ColumnsStorage[n].ID == id) - return &window->ColumnsStorage[n]; - - window->ColumnsStorage.push_back(ImGuiColumns()); - ImGuiColumns* columns = &window->ColumnsStorage.back(); - columns->ID = id; - return columns; -} - -ImGuiID ImGui::GetColumnsID(const char* str_id, int columns_count) -{ - ImGuiWindow* window = GetCurrentWindow(); - - // Differentiate column ID with an arbitrary prefix for cases where users name their columns set the same as another widget. - // In addition, when an identifier isn't explicitly provided we include the number of columns in the hash to make it uniquer. - PushID(0x11223347 + (str_id ? 0 : columns_count)); - ImGuiID id = window->GetID(str_id ? str_id : "columns"); - PopID(); - - return id; -} - -void ImGui::BeginColumns(const char* str_id, int columns_count, ImGuiColumnsFlags flags) -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = GetCurrentWindow(); - - IM_ASSERT(columns_count >= 1); - IM_ASSERT(window->DC.CurrentColumns == NULL); // Nested columns are currently not supported - - // Acquire storage for the columns set - ImGuiID id = GetColumnsID(str_id, columns_count); - ImGuiColumns* columns = FindOrCreateColumns(window, id); - IM_ASSERT(columns->ID == id); - columns->Current = 0; - columns->Count = columns_count; - columns->Flags = flags; - window->DC.CurrentColumns = columns; - - columns->HostCursorPosY = window->DC.CursorPos.y; - columns->HostCursorMaxPosX = window->DC.CursorMaxPos.x; - columns->HostClipRect = window->ClipRect; - columns->HostWorkRect = window->WorkRect; - - // Set state for first column - // We aim so that the right-most column will have the same clipping width as other after being clipped by parent ClipRect - const float column_padding = g.Style.ItemSpacing.x; - const float half_clip_extend_x = ImFloor(ImMax(window->WindowPadding.x * 0.5f, window->WindowBorderSize)); - const float max_1 = window->WorkRect.Max.x + column_padding - ImMax(column_padding - window->WindowPadding.x, 0.0f); - const float max_2 = window->WorkRect.Max.x + half_clip_extend_x; - columns->OffMinX = window->DC.Indent.x - column_padding + ImMax(column_padding - window->WindowPadding.x, 0.0f); - columns->OffMaxX = ImMax(ImMin(max_1, max_2) - window->Pos.x, columns->OffMinX + 1.0f); - columns->LineMinY = columns->LineMaxY = window->DC.CursorPos.y; - - // Clear data if columns count changed - if (columns->Columns.Size != 0 && columns->Columns.Size != columns_count + 1) - columns->Columns.resize(0); - - // Initialize default widths - columns->IsFirstFrame = (columns->Columns.Size == 0); - if (columns->Columns.Size == 0) - { - columns->Columns.reserve(columns_count + 1); - for (int n = 0; n < columns_count + 1; n++) - { - ImGuiColumnData column; - column.OffsetNorm = n / (float)columns_count; - columns->Columns.push_back(column); - } - } - - for (int n = 0; n < columns_count; n++) - { - // Compute clipping rectangle - ImGuiColumnData* column = &columns->Columns[n]; - float clip_x1 = ImFloor(0.5f + window->Pos.x + GetColumnOffset(n)); - float clip_x2 = ImFloor(0.5f + window->Pos.x + GetColumnOffset(n + 1) - 1.0f); - column->ClipRect = ImRect(clip_x1, -FLT_MAX, clip_x2, +FLT_MAX); - column->ClipRect.ClipWith(window->ClipRect); - } - - if (columns->Count > 1) - { - window->DrawList->ChannelsSplit(1 + columns->Count); - window->DrawList->ChannelsSetCurrent(1); - PushColumnClipRect(0); - } - - // We don't generally store Indent.x inside ColumnsOffset because it may be manipulated by the user. - float offset_0 = GetColumnOffset(columns->Current); - float offset_1 = GetColumnOffset(columns->Current + 1); - float width = offset_1 - offset_0; - PushItemWidth(width * 0.65f); - window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); - window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); - window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; -} - -void ImGui::NextColumn() -{ - ImGuiWindow* window = GetCurrentWindow(); - if (window->SkipItems || window->DC.CurrentColumns == NULL) - return; - - ImGuiContext& g = *GImGui; - ImGuiColumns* columns = window->DC.CurrentColumns; - - if (columns->Count == 1) - { - window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); - IM_ASSERT(columns->Current == 0); - return; - } - PopItemWidth(); - PopClipRect(); - - const float column_padding = g.Style.ItemSpacing.x; - columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y); - if (++columns->Current < columns->Count) - { - // Columns 1+ ignore IndentX (by canceling it out) - // FIXME-COLUMNS: Unnecessary, could be locked? - window->DC.ColumnsOffset.x = GetColumnOffset(columns->Current) - window->DC.Indent.x + column_padding; - window->DrawList->ChannelsSetCurrent(columns->Current + 1); - } - else - { - // New row/line - // Column 0 honor IndentX - window->DC.ColumnsOffset.x = ImMax(column_padding - window->WindowPadding.x, 0.0f); - window->DrawList->ChannelsSetCurrent(1); - columns->Current = 0; - columns->LineMinY = columns->LineMaxY; - } - window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); - window->DC.CursorPos.y = columns->LineMinY; - window->DC.CurrLineSize = ImVec2(0.0f, 0.0f); - window->DC.CurrLineTextBaseOffset = 0.0f; - - PushColumnClipRect(columns->Current); // FIXME-COLUMNS: Could it be an overwrite? - - // FIXME-COLUMNS: Share code with BeginColumns() - move code on columns setup. - float offset_0 = GetColumnOffset(columns->Current); - float offset_1 = GetColumnOffset(columns->Current + 1); - float width = offset_1 - offset_0; - PushItemWidth(width * 0.65f); - window->WorkRect.Max.x = window->Pos.x + offset_1 - column_padding; -} - -void ImGui::EndColumns() -{ - ImGuiContext& g = *GImGui; - ImGuiWindow* window = GetCurrentWindow(); - ImGuiColumns* columns = window->DC.CurrentColumns; - IM_ASSERT(columns != NULL); - - PopItemWidth(); - if (columns->Count > 1) - { - PopClipRect(); - window->DrawList->ChannelsMerge(); - } - - const ImGuiColumnsFlags flags = columns->Flags; - columns->LineMaxY = ImMax(columns->LineMaxY, window->DC.CursorPos.y); - window->DC.CursorPos.y = columns->LineMaxY; - if (!(flags & ImGuiColumnsFlags_GrowParentContentsSize)) - window->DC.CursorMaxPos.x = columns->HostCursorMaxPosX; // Restore cursor max pos, as columns don't grow parent - - // Draw columns borders and handle resize - // The IsBeingResized flag ensure we preserve pre-resize columns width so back-and-forth are not lossy - bool is_being_resized = false; - if (!(flags & ImGuiColumnsFlags_NoBorder) && !window->SkipItems) - { - // We clip Y boundaries CPU side because very long triangles are mishandled by some GPU drivers. - const float y1 = ImMax(columns->HostCursorPosY, window->ClipRect.Min.y); - const float y2 = ImMin(window->DC.CursorPos.y, window->ClipRect.Max.y); - int dragging_column = -1; - for (int n = 1; n < columns->Count; n++) - { - ImGuiColumnData* column = &columns->Columns[n]; - float x = window->Pos.x + GetColumnOffset(n); - const ImGuiID column_id = columns->ID + ImGuiID(n); - const float column_hit_hw = COLUMNS_HIT_RECT_HALF_WIDTH; - const ImRect column_hit_rect(ImVec2(x - column_hit_hw, y1), ImVec2(x + column_hit_hw, y2)); - KeepAliveID(column_id); - if (IsClippedEx(column_hit_rect, column_id, false)) - continue; - - bool hovered = false, held = false; - if (!(flags & ImGuiColumnsFlags_NoResize)) - { - ButtonBehavior(column_hit_rect, column_id, &hovered, &held); - if (hovered || held) - g.MouseCursor = ImGuiMouseCursor_ResizeEW; - if (held && !(column->Flags & ImGuiColumnsFlags_NoResize)) - dragging_column = n; - } - - // Draw column - const ImU32 col = GetColorU32(held ? ImGuiCol_SeparatorActive : hovered ? ImGuiCol_SeparatorHovered : ImGuiCol_Separator); - const float xi = (float)(int)x; - window->DrawList->AddLine(ImVec2(xi, y1 + 1.0f), ImVec2(xi, y2), col); - } - - // Apply dragging after drawing the column lines, so our rendered lines are in sync with how items were displayed during the frame. - if (dragging_column != -1) - { - if (!columns->IsBeingResized) - for (int n = 0; n < columns->Count + 1; n++) - columns->Columns[n].OffsetNormBeforeResize = columns->Columns[n].OffsetNorm; - columns->IsBeingResized = is_being_resized = true; - float x = GetDraggedColumnOffset(columns, dragging_column); - SetColumnOffset(dragging_column, x); - } - } - columns->IsBeingResized = is_being_resized; - - window->WorkRect = columns->HostWorkRect; - window->DC.CurrentColumns = NULL; - window->DC.ColumnsOffset.x = 0.0f; - window->DC.CursorPos.x = (float)(int)(window->Pos.x + window->DC.Indent.x + window->DC.ColumnsOffset.x); -} - -// [2018-03: This is currently the only public API, while we are working on making BeginColumns/EndColumns user-facing] -void ImGui::Columns(int columns_count, const char* id, bool border) -{ - ImGuiWindow* window = GetCurrentWindow(); - IM_ASSERT(columns_count >= 1); - - ImGuiColumnsFlags flags = (border ? 0 : ImGuiColumnsFlags_NoBorder); - //flags |= ImGuiColumnsFlags_NoPreserveWidths; // NB: Legacy behavior - ImGuiColumns* columns = window->DC.CurrentColumns; - if (columns != NULL && columns->Count == columns_count && columns->Flags == flags) - return; - - if (columns != NULL) - EndColumns(); - - if (columns_count != 1) - BeginColumns(id, columns_count, flags); -} - -//------------------------------------------------------------------------- - +// MaSzyna additions // by zfedoran, https://github.com/ocornut/imgui/issues/1901 +//------------------------------------------------------------------------- -bool ImGui::BufferingBar( const char* label, float value, const ImVec2& size_arg, const ImU32& bg_col, const ImU32& fg_col ) { +bool ImGui::BufferingBar(const char* label, float value, const ImVec2& size_arg, const ImU32& bg_col, const ImU32& fg_col) +{ ImGuiWindow* window = GetCurrentWindow(); - if( window->SkipItems ) + if (window->SkipItems) return false; ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID( label ); + const ImGuiID id = window->GetID(label); ImVec2 pos = window->DC.CursorPos; ImVec2 size = size_arg; size.x -= style.FramePadding.x * 2; - const ImRect bb( pos, ImVec2( pos.x + size.x, pos.y + size.y ) ); - ItemSize( bb, style.FramePadding.y ); - if( !ItemAdd( bb, id ) ) + const ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y)); + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(bb, id)) return false; // Render - const float circleStart = std::max( size.x * 0.7f, size.x - 30.f ); + const float circleStart = ImMax(size.x * 0.7f, size.x - 30.f); const float circleEnd = size.x; const float circleWidth = circleEnd - circleStart; - window->DrawList->AddRectFilled( bb.Min, ImVec2( pos.x + circleStart, bb.Max.y ), bg_col ); - window->DrawList->AddRectFilled( bb.Min, ImVec2( pos.x + circleStart * value, bb.Max.y ), fg_col ); + window->DrawList->AddRectFilled(bb.Min, ImVec2(pos.x + circleStart, bb.Max.y), bg_col); + window->DrawList->AddRectFilled(bb.Min, ImVec2(pos.x + circleStart * value, bb.Max.y), fg_col); const float t = g.FrameCount * 0.1f; // g.Time; const float r = size.y / 2; @@ -7631,54 +10613,58 @@ bool ImGui::BufferingBar( const char* label, float value, const ImVec2& size_arg const float b = speed * 0.333f; const float c = speed * 0.666f; - const float o1 = ( circleWidth + r ) * ( t + a - speed * (int)( ( t + a ) / speed ) ) / speed; - const float o2 = ( circleWidth + r ) * ( t + b - speed * (int)( ( t + b ) / speed ) ) / speed; - const float o3 = ( circleWidth + r ) * ( t + c - speed * (int)( ( t + c ) / speed ) ) / speed; + const float o1 = (circleWidth + r) * (t + a - speed * (int)((t + a) / speed)) / speed; + const float o2 = (circleWidth + r) * (t + b - speed * (int)((t + b) / speed)) / speed; + const float o3 = (circleWidth + r) * (t + c - speed * (int)((t + c) / speed)) / speed; - window->DrawList->AddCircleFilled( ImVec2( pos.x + circleEnd - o1, bb.Min.y + r ), r, bg_col ); - window->DrawList->AddCircleFilled( ImVec2( pos.x + circleEnd - o2, bb.Min.y + r ), r, bg_col ); - window->DrawList->AddCircleFilled( ImVec2( pos.x + circleEnd - o3, bb.Min.y + r ), r, bg_col ); + window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o1, bb.Min.y + r), r, bg_col); + window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o2, bb.Min.y + r), r, bg_col); + window->DrawList->AddCircleFilled(ImVec2(pos.x + circleEnd - o3, bb.Min.y + r), r, bg_col); return true; } -bool ImGui::Spinner( const char* label, float radius, int thickness, const ImU32& color ) { +bool ImGui::Spinner(const char* label, float radius, int thickness, const ImU32& color) +{ ImGuiWindow* window = GetCurrentWindow(); - if( window->SkipItems ) + if (window->SkipItems) return false; ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; - const ImGuiID id = window->GetID( label ); + const ImGuiID id = window->GetID(label); ImVec2 pos = window->DC.CursorPos; - ImVec2 size( ( radius ) * 2, ( radius + style.FramePadding.y ) * 2 ); + ImVec2 size((radius) * 2, (radius + style.FramePadding.y) * 2); - const ImRect bb( pos, ImVec2( pos.x + size.x, pos.y + size.y ) ); - ItemSize( bb, style.FramePadding.y ); - if( !ItemAdd( bb, id ) ) + const ImRect bb(pos, ImVec2(pos.x + size.x, pos.y + size.y)); + ItemSize(bb, style.FramePadding.y); + if (!ItemAdd(bb, id)) return false; // Render window->DrawList->PathClear(); - auto progress { g.FrameCount * 0.1f }; + float const progress = g.FrameCount * 0.1f; int num_segments = 30; - int start = abs( ImSin( progress * 1.8f )*( num_segments - 5 ) ); + int start = (int)ImFabs(ImSin(progress * 1.8f) * (num_segments - 5)); - const float a_min = IM_PI * 2.0f * ( (float)start ) / (float)num_segments; - const float a_max = IM_PI * 2.0f * ( (float)num_segments - 3 ) / (float)num_segments; + const float a_min = IM_PI * 2.0f * ((float)start) / (float)num_segments; + const float a_max = IM_PI * 2.0f * ((float)num_segments - 3) / (float)num_segments; - const ImVec2 centre = ImVec2( pos.x + radius, pos.y + radius + style.FramePadding.y ); + const ImVec2 centre = ImVec2(pos.x + radius, pos.y + radius + style.FramePadding.y); - for( int i = 0; i < num_segments; i++ ) { - const float a = a_min + ( (float)i / (float)num_segments ) * ( a_max - a_min ); - window->DrawList->PathLineTo( ImVec2( centre.x + ImCos( a + progress * 8 ) * radius, - centre.y + ImSin( a + progress * 8 ) * radius ) ); + for (int i = 0; i < num_segments; i++) + { + const float a = a_min + ((float)i / (float)num_segments) * (a_max - a_min); + window->DrawList->PathLineTo(ImVec2(centre.x + ImCos(a + progress * 8) * radius, centre.y + ImSin(a + progress * 8) * radius)); } - window->DrawList->PathStroke( color, false, thickness ); + window->DrawList->PathStroke(color, 0, (float)thickness); return true; } + + +#endif // #ifndef IMGUI_DISABLE diff --git a/imgui/imstb_rectpack.h b/imgui/imstb_rectpack.h index ff2a85df..f6917e7a 100644 --- a/imgui/imstb_rectpack.h +++ b/imgui/imstb_rectpack.h @@ -1,15 +1,19 @@ // [DEAR IMGUI] -// This is a slightly modified version of stb_rect_pack.h 1.00. -// Those changes would need to be pushed into nothings/stb: -// - Added STBRP__CDECL +// This is a slightly modified version of stb_rect_pack.h 1.01. // Grep for [DEAR IMGUI] to find the changes. - -// stb_rect_pack.h - v1.00 - public domain - rectangle packing +// +// stb_rect_pack.h - v1.01 - public domain - rectangle packing // Sean Barrett 2014 // // Useful for e.g. packing rectangular textures into an atlas. // Does not do rotation. // +// Before #including, +// +// #define STB_RECT_PACK_IMPLEMENTATION +// +// in the file that you want to have the implementation. +// // Not necessarily the awesomest packing method, but better than // the totally naive one in stb_truetype (which is primarily what // this is meant to replace). @@ -34,13 +38,14 @@ // Minor features // Martins Mozeiko // github:IntellectualKitty -// +// // Bugfixes / warning fixes // Jeremy Jaussaud // Fabian Giesen // // Version history: // +// 1.01 (2021-07-11) always use large rect mode, expose STBRP__MAXVAL in public section // 1.00 (2019-02-25) avoid small space waste; gracefully fail too-wide rectangles // 0.99 (2019-02-07) warning fixes // 0.11 (2017-03-03) return packing success/fail result @@ -81,11 +86,10 @@ typedef struct stbrp_context stbrp_context; typedef struct stbrp_node stbrp_node; typedef struct stbrp_rect stbrp_rect; -#ifdef STBRP_LARGE_RECTS typedef int stbrp_coord; -#else -typedef unsigned short stbrp_coord; -#endif + +#define STBRP__MAXVAL 0x7fffffff +// Mostly for internal use, but this is the maximum supported coordinate value. STBRP_DEF int stbrp_pack_rects (stbrp_context *context, stbrp_rect *rects, int num_rects); // Assign packed locations to rectangles. The rectangles are of type @@ -213,10 +217,9 @@ struct stbrp_context #define STBRP_ASSERT assert #endif -// [DEAR IMGUI] Added STBRP__CDECL #ifdef _MSC_VER #define STBRP__NOTUSED(v) (void)(v) -#define STBRP__CDECL __cdecl +#define STBRP__CDECL __cdecl #else #define STBRP__NOTUSED(v) (void)sizeof(v) #define STBRP__CDECL @@ -262,9 +265,6 @@ STBRP_DEF void stbrp_setup_allow_out_of_mem(stbrp_context *context, int allow_ou STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, stbrp_node *nodes, int num_nodes) { int i; -#ifndef STBRP_LARGE_RECTS - STBRP_ASSERT(width <= 0xffff && height <= 0xffff); -#endif for (i=0; i < num_nodes-1; ++i) nodes[i].next = &nodes[i+1]; @@ -283,11 +283,7 @@ STBRP_DEF void stbrp_init_target(stbrp_context *context, int width, int height, context->extra[0].y = 0; context->extra[0].next = &context->extra[1]; context->extra[1].x = (stbrp_coord) width; -#ifdef STBRP_LARGE_RECTS context->extra[1].y = (1<<30); -#else - context->extra[1].y = 65535; -#endif context->extra[1].next = NULL; } @@ -433,7 +429,7 @@ static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int widt if (y <= best_y) { if (y < best_y || waste < best_waste || (waste==best_waste && xpos < best_x)) { best_x = xpos; - STBRP_ASSERT(y <= best_y); + //STBRP_ASSERT(y <= best_y); [DEAR IMGUI] best_y = y; best_waste = waste; best = prev; @@ -441,7 +437,7 @@ static stbrp__findresult stbrp__skyline_find_best_pos(stbrp_context *c, int widt } } tail = tail->next; - } + } } fr.prev_link = best; @@ -529,7 +525,6 @@ static stbrp__findresult stbrp__skyline_pack_rectangle(stbrp_context *context, i return res; } -// [DEAR IMGUI] Added STBRP__CDECL static int STBRP__CDECL rect_height_compare(const void *a, const void *b) { const stbrp_rect *p = (const stbrp_rect *) a; @@ -541,7 +536,6 @@ static int STBRP__CDECL rect_height_compare(const void *a, const void *b) return (p->w > q->w) ? -1 : (p->w < q->w); } -// [DEAR IMGUI] Added STBRP__CDECL static int STBRP__CDECL rect_original_order(const void *a, const void *b) { const stbrp_rect *p = (const stbrp_rect *) a; @@ -549,12 +543,6 @@ static int STBRP__CDECL rect_original_order(const void *a, const void *b) return (p->was_packed < q->was_packed) ? -1 : (p->was_packed > q->was_packed); } -#ifdef STBRP_LARGE_RECTS -#define STBRP__MAXVAL 0xffffffff -#else -#define STBRP__MAXVAL 0xffff -#endif - STBRP_DEF int stbrp_pack_rects(stbrp_context *context, stbrp_rect *rects, int num_rects) { int i, all_rects_packed = 1; @@ -602,38 +590,38 @@ This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------ ALTERNATIVE A - MIT License Copyright (c) 2017 Sean Barrett -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ ALTERNATIVE B - Public Domain (www.unlicense.org) This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -software, either in source code form or as a compiled binary, for any purpose, +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors of this -software dedicate any and all copyright interest in the software to the public -domain. We make this dedication for the benefit of the public at large and to -the detriment of our heirs and successors. We intend this dedication to be an -overt act of relinquishment in perpetuity of all present and future rights to +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ */ diff --git a/imgui/imstb_textedit.h b/imgui/imstb_textedit.h index d7fcbd62..b7a761c8 100644 --- a/imgui/imstb_textedit.h +++ b/imgui/imstb_textedit.h @@ -1,10 +1,14 @@ -// [DEAR IMGUI] -// This is a slightly modified version of stb_textedit.h 1.13. +// [DEAR IMGUI] +// This is a slightly modified version of stb_textedit.h 1.14. // Those changes would need to be pushed into nothings/stb: // - Fix in stb_textedit_discard_redo (see https://github.com/nothings/stb/issues/321) +// - Fix in stb_textedit_find_charpos to handle last line (see https://github.com/ocornut/imgui/issues/6000 + #6783) +// - Added name to struct or it may be forward declared in our code. +// - Added UTF-8 support (see https://github.com/nothings/stb/issues/188 + https://github.com/ocornut/imgui/pull/7925) // Grep for [DEAR IMGUI] to find the changes. +// - Also renamed macros used or defined outside of IMSTB_TEXTEDIT_IMPLEMENTATION block from STB_TEXTEDIT_* to IMSTB_TEXTEDIT_* -// stb_textedit.h - v1.13 - public domain - Sean Barrett +// stb_textedit.h - v1.14 - public domain - Sean Barrett // Development of this library was sponsored by RAD Game Tools // // This C header file implements the guts of a multi-line text-editing @@ -19,7 +23,7 @@ // texts, as its performance does not scale and it has limited undo). // // Non-trivial behaviors are modelled after Windows text controls. -// +// // // LICENSE // @@ -29,16 +33,17 @@ // DEPENDENCIES // // Uses the C runtime function 'memmove', which you can override -// by defining STB_TEXTEDIT_memmove before the implementation. +// by defining IMSTB_TEXTEDIT_memmove before the implementation. // Uses no other functions. Performs no runtime allocations. // // // VERSION HISTORY // +// 1.14 (2021-07-11) page up/down, various fixes // 1.13 (2019-02-07) fix bug in undo size management // 1.12 (2018-01-29) user can change STB_TEXTEDIT_KEYTYPE, fix redo to avoid crash // 1.11 (2017-03-03) fix HOME on last line, dragging off single-line textfield -// 1.10 (2016-10-25) supress warnings about casting away const with -Wcast-qual +// 1.10 (2016-10-25) suppress warnings about casting away const with -Wcast-qual // 1.9 (2016-08-27) customizable move-by-word // 1.8 (2016-04-02) better keyboard handling when mouse button is down // 1.7 (2015-09-13) change y range handling in case baseline is non-0 @@ -58,6 +63,7 @@ // Ulf Winklemann: move-by-word in 1.1 // Fabian Giesen: secondary key inputs in 1.5 // Martins Mozeiko: STB_TEXTEDIT_memmove in 1.6 +// Louis Schnellbach: page up/down in 1.14 // // Bugfixes: // Scott Graham @@ -93,8 +99,8 @@ // moderate sizes. The undo system does no memory allocations, so // it grows STB_TexteditState by the worst-case storage which is (in bytes): // -// [4 + 3 * sizeof(STB_TEXTEDIT_POSITIONTYPE)] * STB_TEXTEDIT_UNDOSTATE_COUNT -// + sizeof(STB_TEXTEDIT_CHARTYPE) * STB_TEXTEDIT_UNDOCHAR_COUNT +// [4 + 3 * sizeof(STB_TEXTEDIT_POSITIONTYPE)] * STB_TEXTEDIT_UNDOSTATECOUNT +// + sizeof(STB_TEXTEDIT_CHARTYPE) * STB_TEXTEDIT_UNDOCHARCOUNT // // // Implementation mode: @@ -148,6 +154,8 @@ // STB_TEXTEDIT_K_RIGHT keyboard input to move cursor right // STB_TEXTEDIT_K_UP keyboard input to move cursor up // STB_TEXTEDIT_K_DOWN keyboard input to move cursor down +// STB_TEXTEDIT_K_PGUP keyboard input to move cursor up a page +// STB_TEXTEDIT_K_PGDOWN keyboard input to move cursor down a page // STB_TEXTEDIT_K_LINESTART keyboard input to move cursor to start of line // e.g. HOME // STB_TEXTEDIT_K_LINEEND keyboard input to move cursor to end of line // e.g. END // STB_TEXTEDIT_K_TEXTSTART keyboard input to move cursor to start of text // e.g. ctrl-HOME @@ -170,14 +178,10 @@ // STB_TEXTEDIT_K_TEXTSTART2 secondary keyboard input to move cursor to start of text // STB_TEXTEDIT_K_TEXTEND2 secondary keyboard input to move cursor to end of text // -// Todo: -// STB_TEXTEDIT_K_PGUP keyboard input to move cursor up a page -// STB_TEXTEDIT_K_PGDOWN keyboard input to move cursor down a page -// // Keyboard input must be encoded as a single integer value; e.g. a character code // and some bitflags that represent shift states. to simplify the interface, SHIFT must // be a bitflag, so we can test the shifted state of cursor movements to allow selection, -// i.e. (STB_TEXTED_K_RIGHT|STB_TEXTEDIT_K_SHIFT) should be shifted right-arrow. +// i.e. (STB_TEXTEDIT_K_RIGHT|STB_TEXTEDIT_K_SHIFT) should be shifted right-arrow. // // You can encode other things, such as CONTROL or ALT, in additional bits, and // then test for their presence in e.g. STB_TEXTEDIT_K_WORDLEFT. For example, @@ -207,6 +211,7 @@ // int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) // int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) // void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXEDIT_KEYTYPE key) +// void stb_textedit_text(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int text_len) // // Each of these functions potentially updates the string and updates the // state. @@ -219,20 +224,20 @@ // call this with the mouse x,y on a mouse down; it will update the cursor // and reset the selection start/end to the cursor point. the x,y must // be relative to the text widget, with (0,0) being the top left. -// +// // drag: // call this with the mouse x,y on a mouse drag/up; it will update the // cursor and the selection end point -// +// // cut: // call this to delete the current selection; returns true if there was // one. you should FIRST copy the current selection to the system paste buffer. // (To copy, just copy the current selection out of the string yourself.) -// +// // paste: // call this to paste text at the current cursor point or over the current // selection if there is one. -// +// // key: // call this for keyboard inputs sent to the textfield. you can use it // for "key down" events or for "translated" key events. if you need to @@ -241,9 +246,14 @@ // various definitions like STB_TEXTEDIT_K_LEFT have the is-key-event bit // set, and make STB_TEXTEDIT_KEYTOCHAR check that the is-key-event bit is // clear. STB_TEXTEDIT_KEYTYPE defaults to int, but you can #define it to -// anything other type you wante before including. +// anything other type you want before including. +// if the STB_TEXTEDIT_KEYTOTEXT function is defined, selected keys are +// transformed into text and stb_textedit_text() is automatically called. +// +// text: [DEAR IMGUI] added 2024-09 +// call this to text inputs sent to the textfield. +// // -// // When rendering, you can read the cursor position and selection state from // the STB_TexteditState. // @@ -273,8 +283,8 @@ //// //// -#ifndef INCLUDE_STB_TEXTEDIT_H -#define INCLUDE_STB_TEXTEDIT_H +#ifndef INCLUDE_IMSTB_TEXTEDIT_H +#define INCLUDE_IMSTB_TEXTEDIT_H //////////////////////////////////////////////////////////////////////// // @@ -285,38 +295,38 @@ // and undo state. // -#ifndef STB_TEXTEDIT_UNDOSTATECOUNT -#define STB_TEXTEDIT_UNDOSTATECOUNT 99 +#ifndef IMSTB_TEXTEDIT_UNDOSTATECOUNT +#define IMSTB_TEXTEDIT_UNDOSTATECOUNT 99 #endif -#ifndef STB_TEXTEDIT_UNDOCHARCOUNT -#define STB_TEXTEDIT_UNDOCHARCOUNT 999 +#ifndef IMSTB_TEXTEDIT_UNDOCHARCOUNT +#define IMSTB_TEXTEDIT_UNDOCHARCOUNT 999 #endif -#ifndef STB_TEXTEDIT_CHARTYPE -#define STB_TEXTEDIT_CHARTYPE int +#ifndef IMSTB_TEXTEDIT_CHARTYPE +#define IMSTB_TEXTEDIT_CHARTYPE int #endif -#ifndef STB_TEXTEDIT_POSITIONTYPE -#define STB_TEXTEDIT_POSITIONTYPE int +#ifndef IMSTB_TEXTEDIT_POSITIONTYPE +#define IMSTB_TEXTEDIT_POSITIONTYPE int #endif typedef struct { // private data - STB_TEXTEDIT_POSITIONTYPE where; - STB_TEXTEDIT_POSITIONTYPE insert_length; - STB_TEXTEDIT_POSITIONTYPE delete_length; + IMSTB_TEXTEDIT_POSITIONTYPE where; + IMSTB_TEXTEDIT_POSITIONTYPE insert_length; + IMSTB_TEXTEDIT_POSITIONTYPE delete_length; int char_storage; } StbUndoRecord; typedef struct { // private data - StbUndoRecord undo_rec [STB_TEXTEDIT_UNDOSTATECOUNT]; - STB_TEXTEDIT_CHARTYPE undo_char[STB_TEXTEDIT_UNDOCHARCOUNT]; + StbUndoRecord undo_rec [IMSTB_TEXTEDIT_UNDOSTATECOUNT]; + IMSTB_TEXTEDIT_CHARTYPE undo_char[IMSTB_TEXTEDIT_UNDOCHARCOUNT]; short undo_point, redo_point; int undo_char_point, redo_char_point; } StbUndoState; -typedef struct +typedef struct STB_TexteditState { ///////////////////// // @@ -337,6 +347,10 @@ typedef struct // each textfield keeps its own insert mode state. to keep an app-wide // insert mode, copy this value in/out of the app state + int row_count_per_page; + // page size in number of row. + // this value MUST be set to >0 for pageup or pagedown in multilines documents. + ///////////////////// // // private data @@ -366,7 +380,7 @@ typedef struct float ymin,ymax; // height of row above and below baseline int num_chars; } StbTexteditRow; -#endif //INCLUDE_STB_TEXTEDIT_H +#endif //INCLUDE_IMSTB_TEXTEDIT_H //////////////////////////////////////////////////////////////////////////// @@ -379,11 +393,11 @@ typedef struct // implementation isn't include-guarded, since it might have indirectly // included just the "header" portion -#ifdef STB_TEXTEDIT_IMPLEMENTATION +#ifdef IMSTB_TEXTEDIT_IMPLEMENTATION -#ifndef STB_TEXTEDIT_memmove +#ifndef IMSTB_TEXTEDIT_memmove #include -#define STB_TEXTEDIT_memmove memmove +#define IMSTB_TEXTEDIT_memmove memmove #endif @@ -393,7 +407,7 @@ typedef struct // // traverse the layout to locate the nearest character to a display position -static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y) +static int stb_text_locate_coord(IMSTB_TEXTEDIT_STRING *str, float x, float y) { StbTexteditRow r; int n = STB_TEXTEDIT_STRINGLEN(str); @@ -432,13 +446,13 @@ static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y) if (x < r.x1) { // search characters in row for one that straddles 'x' prev_x = r.x0; - for (k=0; k < r.num_chars; ++k) { + for (k=0; k < r.num_chars; k = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k) - i) { float w = STB_TEXTEDIT_GETWIDTH(str, i, k); if (x < prev_x+w) { if (x < prev_x+w/2) return k+i; else - return k+i+1; + return IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, i + k); } prev_x += w; } @@ -453,7 +467,7 @@ static int stb_text_locate_coord(STB_TEXTEDIT_STRING *str, float x, float y) } // API click: on mouse down, move the cursor to the clicked location, and reset the selection -static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +static void stb_textedit_click(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) { // In single-line mode, just always make y = 0. This lets the drag keep working if the mouse // goes off the top or bottom of the text @@ -471,7 +485,7 @@ static void stb_textedit_click(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat } // API drag: on mouse drag, move the cursor and selection endpoint to the clicked location -static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) +static void stb_textedit_drag(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, float x, float y) { int p = 0; @@ -497,11 +511,11 @@ static void stb_textedit_drag(STB_TEXTEDIT_STRING *str, STB_TexteditState *state // // forward declarations -static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); -static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state); -static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length); +static void stb_text_undo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_redo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state); +static void stb_text_makeundo_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length); static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int length); -static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length); +static void stb_text_makeundo_replace(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length); typedef struct { @@ -513,36 +527,21 @@ typedef struct // find the x/y location of a character, and remember info about the previous row in // case we get a move-up event (for page up, we'll have to rescan) -static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *str, int n, int single_line) +static void stb_textedit_find_charpos(StbFindState *find, IMSTB_TEXTEDIT_STRING *str, int n, int single_line) { StbTexteditRow r; int prev_start = 0; int z = STB_TEXTEDIT_STRINGLEN(str); int i=0, first; - if (n == z) { - // if it's at the end, then find the last line -- simpler than trying to - // explicitly handle this case in the regular code - if (single_line) { - STB_TEXTEDIT_LAYOUTROW(&r, str, 0); - find->y = 0; - find->first_char = 0; - find->length = z; - find->height = r.ymax - r.ymin; - find->x = r.x1; - } else { - find->y = 0; - find->x = 0; - find->height = 1; - while (i < z) { - STB_TEXTEDIT_LAYOUTROW(&r, str, i); - prev_start = i; - i += r.num_chars; - } - find->first_char = i; - find->length = 0; - find->prev_first = prev_start; - } + if (n == z && single_line) { + // special case if it's at the end (may not be needed?) + STB_TEXTEDIT_LAYOUTROW(&r, str, 0); + find->y = 0; + find->first_char = 0; + find->length = z; + find->height = r.ymax - r.ymin; + find->x = r.x1; return; } @@ -553,9 +552,16 @@ static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *s STB_TEXTEDIT_LAYOUTROW(&r, str, i); if (n < i + r.num_chars) break; + if (i + r.num_chars == z && z > 0 && STB_TEXTEDIT_GETCHAR(str, z - 1) != STB_TEXTEDIT_NEWLINE) // [DEAR IMGUI] special handling for last line + break; // [DEAR IMGUI] prev_start = i; i += r.num_chars; find->y += r.baseline_y_delta; + if (i == z) // [DEAR IMGUI] + { + r.num_chars = 0; // [DEAR IMGUI] + break; // [DEAR IMGUI] + } } find->first_char = first = i; @@ -565,14 +571,14 @@ static void stb_textedit_find_charpos(StbFindState *find, STB_TEXTEDIT_STRING *s // now scan to find xpos find->x = r.x0; - for (i=0; first+i < n; ++i) + for (i=0; first+i < n; i = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, first + i) - first) find->x += STB_TEXTEDIT_GETWIDTH(str, first, i); } #define STB_TEXT_HAS_SELECTION(s) ((s)->select_start != (s)->select_end) // make the selection/cursor state valid if client altered the string -static void stb_textedit_clamp(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_textedit_clamp(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { int n = STB_TEXTEDIT_STRINGLEN(str); if (STB_TEXT_HAS_SELECTION(state)) { @@ -586,7 +592,7 @@ static void stb_textedit_clamp(STB_TEXTEDIT_STRING *str, STB_TexteditState *stat } // delete characters while updating undo -static void stb_textedit_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int len) +static void stb_textedit_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int len) { stb_text_makeundo_delete(str, state, where, len); STB_TEXTEDIT_DELETECHARS(str, where, len); @@ -594,7 +600,7 @@ static void stb_textedit_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *sta } // delete the section -static void stb_textedit_delete_selection(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_textedit_delete_selection(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { stb_textedit_clamp(str, state); if (STB_TEXT_HAS_SELECTION(state)) { @@ -631,7 +637,7 @@ static void stb_textedit_move_to_first(STB_TexteditState *state) } // move cursor to last character of selection -static void stb_textedit_move_to_last(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_textedit_move_to_last(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { if (STB_TEXT_HAS_SELECTION(state)) { stb_textedit_sortselection(state); @@ -642,14 +648,25 @@ static void stb_textedit_move_to_last(STB_TEXTEDIT_STRING *str, STB_TexteditStat } } +// [DEAR IMGUI] +// Functions must be implemented for UTF8 support +// Code in this file that uses those functions is modified for [DEAR IMGUI] and deviates from the original stb_textedit. +// There is not necessarily a '[DEAR IMGUI]' at the usage sites. +#ifndef IMSTB_TEXTEDIT_GETPREVCHARINDEX +#define IMSTB_TEXTEDIT_GETPREVCHARINDEX(obj, idx) (idx - 1) +#endif +#ifndef IMSTB_TEXTEDIT_GETNEXTCHARINDEX +#define IMSTB_TEXTEDIT_GETNEXTCHARINDEX(obj, idx) (idx + 1) +#endif + #ifdef STB_TEXTEDIT_IS_SPACE -static int is_word_boundary( STB_TEXTEDIT_STRING *str, int idx ) +static int is_word_boundary( IMSTB_TEXTEDIT_STRING *str, int idx ) { return idx > 0 ? (STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(str,idx-1) ) && !STB_TEXTEDIT_IS_SPACE( STB_TEXTEDIT_GETCHAR(str, idx) ) ) : 1; } #ifndef STB_TEXTEDIT_MOVEWORDLEFT -static int stb_textedit_move_to_word_previous( STB_TEXTEDIT_STRING *str, int c ) +static int stb_textedit_move_to_word_previous( IMSTB_TEXTEDIT_STRING *str, int c ) { --c; // always move at least one character while( c >= 0 && !is_word_boundary( str, c ) ) @@ -664,7 +681,7 @@ static int stb_textedit_move_to_word_previous( STB_TEXTEDIT_STRING *str, int c ) #endif #ifndef STB_TEXTEDIT_MOVEWORDRIGHT -static int stb_textedit_move_to_word_next( STB_TEXTEDIT_STRING *str, int c ) +static int stb_textedit_move_to_word_next( IMSTB_TEXTEDIT_STRING *str, int c ) { const int len = STB_TEXTEDIT_STRINGLEN(str); ++c; // always move at least one character @@ -691,7 +708,7 @@ static void stb_textedit_prep_selection_at_cursor(STB_TexteditState *state) } // API cut: delete selection -static int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static int stb_textedit_cut(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { if (STB_TEXT_HAS_SELECTION(state)) { stb_textedit_delete_selection(str,state); // implicitly clamps @@ -702,7 +719,7 @@ static int stb_textedit_cut(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) } // API paste: replace existing selection with passed-in text -static int stb_textedit_paste_internal(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE *text, int len) +static int stb_textedit_paste_internal(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, IMSTB_TEXTEDIT_CHARTYPE *text, int len) { // if there's a selection, the paste should delete it stb_textedit_clamp(str, state); @@ -714,9 +731,7 @@ static int stb_textedit_paste_internal(STB_TEXTEDIT_STRING *str, STB_TexteditSta state->has_preferred_x = 0; return 1; } - // remove the undo since we didn't actually insert the characters - if (state->undostate.undo_point) - --state->undostate.undo_point; + // note: paste failure will leave deleted selection, may be restored with an undo (see https://github.com/nothings/stb/issues/734 for details) return 0; } @@ -724,36 +739,44 @@ static int stb_textedit_paste_internal(STB_TEXTEDIT_STRING *str, STB_TexteditSta #define STB_TEXTEDIT_KEYTYPE int #endif +// [DEAR IMGUI] Added stb_textedit_text(), extracted out and called by stb_textedit_key() for backward compatibility. +static void stb_textedit_text(IMSTB_TEXTEDIT_STRING* str, STB_TexteditState* state, const IMSTB_TEXTEDIT_CHARTYPE* text, int text_len) +{ + // can't add newline in single-line mode + if (text[0] == '\n' && state->single_line) + return; + + if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { + stb_text_makeundo_replace(str, state, state->cursor, 1, 1); + STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) { + state->cursor += text_len; + state->has_preferred_x = 0; + } + } + else { + stb_textedit_delete_selection(str, state); // implicitly clamps + if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, text, text_len)) { + stb_text_makeundo_insert(state, state->cursor, text_len); + state->cursor += text_len; + state->has_preferred_x = 0; + } + } +} + // API key: process a keyboard input -static void stb_textedit_key(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key) +static void stb_textedit_key(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_KEYTYPE key) { retry: switch (key) { default: { +#ifdef STB_TEXTEDIT_KEYTOTEXT int c = STB_TEXTEDIT_KEYTOTEXT(key); if (c > 0) { - STB_TEXTEDIT_CHARTYPE ch = (STB_TEXTEDIT_CHARTYPE) c; - - // can't add newline in single-line mode - if (c == '\n' && state->single_line) - break; - - if (state->insert_mode && !STB_TEXT_HAS_SELECTION(state) && state->cursor < STB_TEXTEDIT_STRINGLEN(str)) { - stb_text_makeundo_replace(str, state, state->cursor, 1, 1); - STB_TEXTEDIT_DELETECHARS(str, state->cursor, 1); - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { - ++state->cursor; - state->has_preferred_x = 0; - } - } else { - stb_textedit_delete_selection(str,state); // implicitly clamps - if (STB_TEXTEDIT_INSERTCHARS(str, state->cursor, &ch, 1)) { - stb_text_makeundo_insert(state, state->cursor, 1); - ++state->cursor; - state->has_preferred_x = 0; - } - } + IMSTB_TEXTEDIT_CHARTYPE ch = (IMSTB_TEXTEDIT_CHARTYPE)c; + stb_textedit_text(str, state, &ch, 1); } +#endif break; } @@ -762,7 +785,7 @@ retry: state->insert_mode = !state->insert_mode; break; #endif - + case STB_TEXTEDIT_K_UNDO: stb_text_undo(str, state); state->has_preferred_x = 0; @@ -777,9 +800,9 @@ retry: // if currently there's a selection, move cursor to start of selection if (STB_TEXT_HAS_SELECTION(state)) stb_textedit_move_to_first(state); - else + else if (state->cursor > 0) - --state->cursor; + state->cursor = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); state->has_preferred_x = 0; break; @@ -788,7 +811,7 @@ retry: if (STB_TEXT_HAS_SELECTION(state)) stb_textedit_move_to_last(str, state); else - ++state->cursor; + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); stb_textedit_clamp(str, state); state->has_preferred_x = 0; break; @@ -798,7 +821,7 @@ retry: stb_textedit_prep_selection_at_cursor(state); // move selection left if (state->select_end > 0) - --state->select_end; + state->select_end = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->select_end); state->cursor = state->select_end; state->has_preferred_x = 0; break; @@ -826,7 +849,7 @@ retry: #ifdef STB_TEXTEDIT_MOVEWORDRIGHT case STB_TEXTEDIT_K_WORDRIGHT: - if (STB_TEXT_HAS_SELECTION(state)) + if (STB_TEXT_HAS_SELECTION(state)) stb_textedit_move_to_last(str, state); else { state->cursor = STB_TEXTEDIT_MOVEWORDRIGHT(str, state->cursor); @@ -848,19 +871,23 @@ retry: case STB_TEXTEDIT_K_RIGHT | STB_TEXTEDIT_K_SHIFT: stb_textedit_prep_selection_at_cursor(state); // move selection right - ++state->select_end; + state->select_end = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->select_end); stb_textedit_clamp(str, state); state->cursor = state->select_end; state->has_preferred_x = 0; break; case STB_TEXTEDIT_K_DOWN: - case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT: { + case STB_TEXTEDIT_K_DOWN | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_PGDOWN: + case STB_TEXTEDIT_K_PGDOWN | STB_TEXTEDIT_K_SHIFT: { StbFindState find; StbTexteditRow row; - int i, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + int i, j, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGDOWN; + int row_count = is_page ? state->row_count_per_page : 1; - if (state->single_line) { + if (!is_page && state->single_line) { // on windows, up&down in single-line behave like left&right key = STB_TEXTEDIT_K_RIGHT | (key & STB_TEXTEDIT_K_SHIFT); goto retry; @@ -869,30 +896,38 @@ retry: if (sel) stb_textedit_prep_selection_at_cursor(state); else if (STB_TEXT_HAS_SELECTION(state)) - stb_textedit_move_to_last(str,state); + stb_textedit_move_to_last(str, state); // compute current position of cursor point stb_textedit_clamp(str, state); stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); - // now find character position down a row - if (find.length) { - float goal_x = state->has_preferred_x ? state->preferred_x : find.x; - float x; + for (j = 0; j < row_count; ++j) { + float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x; int start = find.first_char + find.length; + + if (find.length == 0) + break; + + // [DEAR IMGUI] + // going down while being on the last line shouldn't bring us to that line end + if (STB_TEXTEDIT_GETCHAR(str, find.first_char + find.length - 1) != STB_TEXTEDIT_NEWLINE) + break; + + // now find character position down a row state->cursor = start; STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); x = row.x0; for (i=0; i < row.num_chars; ++i) { float dx = STB_TEXTEDIT_GETWIDTH(str, start, i); - #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE - if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + #ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE) break; #endif x += dx; if (x > goal_x) break; - ++state->cursor; + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); } stb_textedit_clamp(str, state); @@ -901,17 +936,25 @@ retry: if (sel) state->select_end = state->cursor; + + // go to next line + find.first_char = find.first_char + find.length; + find.length = row.num_chars; } break; } - + case STB_TEXTEDIT_K_UP: - case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT: { + case STB_TEXTEDIT_K_UP | STB_TEXTEDIT_K_SHIFT: + case STB_TEXTEDIT_K_PGUP: + case STB_TEXTEDIT_K_PGUP | STB_TEXTEDIT_K_SHIFT: { StbFindState find; StbTexteditRow row; - int i, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + int i, j, prev_scan, sel = (key & STB_TEXTEDIT_K_SHIFT) != 0; + int is_page = (key & ~STB_TEXTEDIT_K_SHIFT) == STB_TEXTEDIT_K_PGUP; + int row_count = is_page ? state->row_count_per_page : 1; - if (state->single_line) { + if (!is_page && state->single_line) { // on windows, up&down become left&right key = STB_TEXTEDIT_K_LEFT | (key & STB_TEXTEDIT_K_SHIFT); goto retry; @@ -926,24 +969,27 @@ retry: stb_textedit_clamp(str, state); stb_textedit_find_charpos(&find, str, state->cursor, state->single_line); - // can only go up if there's a previous row - if (find.prev_first != find.first_char) { + for (j = 0; j < row_count; ++j) { + float x, goal_x = state->has_preferred_x ? state->preferred_x : find.x; + + // can only go up if there's a previous row + if (find.prev_first == find.first_char) + break; + // now find character position up a row - float goal_x = state->has_preferred_x ? state->preferred_x : find.x; - float x; state->cursor = find.prev_first; STB_TEXTEDIT_LAYOUTROW(&row, str, state->cursor); x = row.x0; for (i=0; i < row.num_chars; ++i) { float dx = STB_TEXTEDIT_GETWIDTH(str, find.prev_first, i); - #ifdef STB_TEXTEDIT_GETWIDTH_NEWLINE - if (dx == STB_TEXTEDIT_GETWIDTH_NEWLINE) + #ifdef IMSTB_TEXTEDIT_GETWIDTH_NEWLINE + if (dx == IMSTB_TEXTEDIT_GETWIDTH_NEWLINE) break; #endif x += dx; if (x > goal_x) break; - ++state->cursor; + state->cursor = IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor); } stb_textedit_clamp(str, state); @@ -952,6 +998,14 @@ retry: if (sel) state->select_end = state->cursor; + + // go to previous line + // (we need to scan previous line the hard way. maybe we could expose this as a new API function?) + prev_scan = find.prev_first > 0 ? find.prev_first - 1 : 0; + while (prev_scan > 0 && STB_TEXTEDIT_GETCHAR(str, prev_scan - 1) != STB_TEXTEDIT_NEWLINE) + --prev_scan; + find.first_char = find.prev_first; + find.prev_first = prev_scan; } break; } @@ -963,7 +1017,7 @@ retry: else { int n = STB_TEXTEDIT_STRINGLEN(str); if (state->cursor < n) - stb_textedit_delete(str, state, state->cursor, 1); + stb_textedit_delete(str, state, state->cursor, IMSTB_TEXTEDIT_GETNEXTCHARINDEX(str, state->cursor) - state->cursor); } state->has_preferred_x = 0; break; @@ -975,13 +1029,14 @@ retry: else { stb_textedit_clamp(str, state); if (state->cursor > 0) { - stb_textedit_delete(str, state, state->cursor-1, 1); - --state->cursor; + int prev = IMSTB_TEXTEDIT_GETPREVCHARINDEX(str, state->cursor); + stb_textedit_delete(str, state, prev, state->cursor - prev); + state->cursor = prev; } } state->has_preferred_x = 0; break; - + #ifdef STB_TEXTEDIT_K_TEXTSTART2 case STB_TEXTEDIT_K_TEXTSTART2: #endif @@ -998,7 +1053,7 @@ retry: state->select_start = state->select_end = 0; state->has_preferred_x = 0; break; - + #ifdef STB_TEXTEDIT_K_TEXTSTART2 case STB_TEXTEDIT_K_TEXTSTART2 | STB_TEXTEDIT_K_SHIFT: #endif @@ -1075,10 +1130,6 @@ retry: state->has_preferred_x = 0; break; } - -// @TODO: -// STB_TEXTEDIT_K_PGUP - move cursor up a page -// STB_TEXTEDIT_K_PGDOWN - move cursor down a page } } @@ -1090,8 +1141,8 @@ retry: static void stb_textedit_flush_redo(StbUndoState *state) { - state->redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; - state->redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; + state->redo_point = IMSTB_TEXTEDIT_UNDOSTATECOUNT; + state->redo_char_point = IMSTB_TEXTEDIT_UNDOCHARCOUNT; } // discard the oldest entry in the undo list @@ -1103,13 +1154,13 @@ static void stb_textedit_discard_undo(StbUndoState *state) int n = state->undo_rec[0].insert_length, i; // delete n characters from all other records state->undo_char_point -= n; - STB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) (state->undo_char_point*sizeof(STB_TEXTEDIT_CHARTYPE))); + IMSTB_TEXTEDIT_memmove(state->undo_char, state->undo_char + n, (size_t) (state->undo_char_point*sizeof(IMSTB_TEXTEDIT_CHARTYPE))); for (i=0; i < state->undo_point; ++i) if (state->undo_rec[i].char_storage >= 0) state->undo_rec[i].char_storage -= n; // @OPTIMIZE: get rid of char_storage and infer it } --state->undo_point; - STB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) (state->undo_point*sizeof(state->undo_rec[0]))); + IMSTB_TEXTEDIT_memmove(state->undo_rec, state->undo_rec+1, (size_t) (state->undo_point*sizeof(state->undo_rec[0]))); } } @@ -1119,7 +1170,7 @@ static void stb_textedit_discard_undo(StbUndoState *state) // fill up even though the undo buffer didn't static void stb_textedit_discard_redo(StbUndoState *state) { - int k = STB_TEXTEDIT_UNDOSTATECOUNT-1; + int k = IMSTB_TEXTEDIT_UNDOSTATECOUNT-1; if (state->redo_point <= k) { // if the k'th undo state has characters, clean those up @@ -1127,20 +1178,20 @@ static void stb_textedit_discard_redo(StbUndoState *state) int n = state->undo_rec[k].insert_length, i; // move the remaining redo character data to the end of the buffer state->redo_char_point += n; - STB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((STB_TEXTEDIT_UNDOCHARCOUNT - state->redo_char_point)*sizeof(STB_TEXTEDIT_CHARTYPE))); + IMSTB_TEXTEDIT_memmove(state->undo_char + state->redo_char_point, state->undo_char + state->redo_char_point-n, (size_t) ((IMSTB_TEXTEDIT_UNDOCHARCOUNT - state->redo_char_point)*sizeof(IMSTB_TEXTEDIT_CHARTYPE))); // adjust the position of all the other records to account for above memmove for (i=state->redo_point; i < k; ++i) if (state->undo_rec[i].char_storage >= 0) state->undo_rec[i].char_storage += n; } // now move all the redo records towards the end of the buffer; the first one is at 'redo_point' - // {DEAR IMGUI] - size_t move_size = (size_t)((STB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point - 1) * sizeof(state->undo_rec[0])); + // [DEAR IMGUI] + size_t move_size = (size_t)((IMSTB_TEXTEDIT_UNDOSTATECOUNT - state->redo_point - 1) * sizeof(state->undo_rec[0])); const char* buf_begin = (char*)state->undo_rec; (void)buf_begin; const char* buf_end = (char*)state->undo_rec + sizeof(state->undo_rec); (void)buf_end; IM_ASSERT(((char*)(state->undo_rec + state->redo_point)) >= buf_begin); IM_ASSERT(((char*)(state->undo_rec + state->redo_point + 1) + move_size) <= buf_end); - STB_TEXTEDIT_memmove(state->undo_rec + state->redo_point+1, state->undo_rec + state->redo_point, move_size); + IMSTB_TEXTEDIT_memmove(state->undo_rec + state->redo_point+1, state->undo_rec + state->redo_point, move_size); // now move redo_point to point to the new one ++state->redo_point; @@ -1154,32 +1205,32 @@ static StbUndoRecord *stb_text_create_undo_record(StbUndoState *state, int numch // if we have no free records, we have to make room, by sliding the // existing records down - if (state->undo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + if (state->undo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) stb_textedit_discard_undo(state); // if the characters to store won't possibly fit in the buffer, we can't undo - if (numchars > STB_TEXTEDIT_UNDOCHARCOUNT) { + if (numchars > IMSTB_TEXTEDIT_UNDOCHARCOUNT) { state->undo_point = 0; state->undo_char_point = 0; return NULL; } // if we don't have enough free characters in the buffer, we have to make room - while (state->undo_char_point + numchars > STB_TEXTEDIT_UNDOCHARCOUNT) + while (state->undo_char_point + numchars > IMSTB_TEXTEDIT_UNDOCHARCOUNT) stb_textedit_discard_undo(state); return &state->undo_rec[state->undo_point++]; } -static STB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, int insert_len, int delete_len) +static IMSTB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, int insert_len, int delete_len) { StbUndoRecord *r = stb_text_create_undo_record(state, insert_len); if (r == NULL) return NULL; r->where = pos; - r->insert_length = (STB_TEXTEDIT_POSITIONTYPE) insert_len; - r->delete_length = (STB_TEXTEDIT_POSITIONTYPE) delete_len; + r->insert_length = (IMSTB_TEXTEDIT_POSITIONTYPE) insert_len; + r->delete_length = (IMSTB_TEXTEDIT_POSITIONTYPE) delete_len; if (insert_len == 0) { r->char_storage = -1; @@ -1191,7 +1242,7 @@ static STB_TEXTEDIT_CHARTYPE *stb_text_createundo(StbUndoState *state, int pos, } } -static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_text_undo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { StbUndoState *s = &state->undostate; StbUndoRecord u, *r; @@ -1218,7 +1269,7 @@ static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) // characters stored for *undoing* don't leave room for redo // if the last is true, we have to bail - if (s->undo_char_point + u.delete_length >= STB_TEXTEDIT_UNDOCHARCOUNT) { + if (s->undo_char_point + u.delete_length >= IMSTB_TEXTEDIT_UNDOCHARCOUNT) { // the undo records take up too much character space; there's no space to store the redo characters r->insert_length = 0; } else { @@ -1227,7 +1278,7 @@ static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) // there's definitely room to store the characters eventually while (s->undo_char_point + u.delete_length > s->redo_char_point) { // should never happen: - if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + if (s->redo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) return; // there's currently not enough room, so discard a redo record stb_textedit_discard_redo(s); @@ -1259,11 +1310,11 @@ static void stb_text_undo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) s->redo_point--; } -static void stb_text_redo(STB_TEXTEDIT_STRING *str, STB_TexteditState *state) +static void stb_text_redo(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state) { StbUndoState *s = &state->undostate; StbUndoRecord *u, r; - if (s->redo_point == STB_TEXTEDIT_UNDOSTATECOUNT) + if (s->redo_point == IMSTB_TEXTEDIT_UNDOSTATECOUNT) return; // we need to do two things: apply the redo record, and create an undo record @@ -1315,20 +1366,20 @@ static void stb_text_makeundo_insert(STB_TexteditState *state, int where, int le stb_text_createundo(&state->undostate, where, 0, length); } -static void stb_text_makeundo_delete(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length) +static void stb_text_makeundo_delete(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int length) { int i; - STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); + IMSTB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, length, 0); if (p) { for (i=0; i < length; ++i) p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); } } -static void stb_text_makeundo_replace(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length) +static void stb_text_makeundo_replace(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, int where, int old_length, int new_length) { int i; - STB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); + IMSTB_TEXTEDIT_CHARTYPE *p = stb_text_createundo(&state->undostate, where, old_length, new_length); if (p) { for (i=0; i < old_length; ++i) p[i] = STB_TEXTEDIT_GETCHAR(str, where+i); @@ -1340,8 +1391,8 @@ static void stb_textedit_clear_state(STB_TexteditState *state, int is_single_lin { state->undostate.undo_point = 0; state->undostate.undo_char_point = 0; - state->undostate.redo_point = STB_TEXTEDIT_UNDOSTATECOUNT; - state->undostate.redo_char_point = STB_TEXTEDIT_UNDOCHARCOUNT; + state->undostate.redo_point = IMSTB_TEXTEDIT_UNDOSTATECOUNT; + state->undostate.redo_char_point = IMSTB_TEXTEDIT_UNDOCHARCOUNT; state->select_end = state->select_start = 0; state->cursor = 0; state->has_preferred_x = 0; @@ -1350,6 +1401,7 @@ static void stb_textedit_clear_state(STB_TexteditState *state, int is_single_lin state->initialized = 1; state->single_line = (unsigned char) is_single_line; state->insert_mode = 0; + state->row_count_per_page = 0; } // API initialize @@ -1363,16 +1415,16 @@ static void stb_textedit_initialize_state(STB_TexteditState *state, int is_singl #pragma GCC diagnostic ignored "-Wcast-qual" #endif -static int stb_textedit_paste(STB_TEXTEDIT_STRING *str, STB_TexteditState *state, STB_TEXTEDIT_CHARTYPE const *ctext, int len) +static int stb_textedit_paste(IMSTB_TEXTEDIT_STRING *str, STB_TexteditState *state, IMSTB_TEXTEDIT_CHARTYPE const *ctext, int len) { - return stb_textedit_paste_internal(str, state, (STB_TEXTEDIT_CHARTYPE *) ctext, len); + return stb_textedit_paste_internal(str, state, (IMSTB_TEXTEDIT_CHARTYPE *) ctext, len); } #if defined(__GNUC__) || defined(__clang__) #pragma GCC diagnostic pop #endif -#endif//STB_TEXTEDIT_IMPLEMENTATION +#endif//IMSTB_TEXTEDIT_IMPLEMENTATION /* ------------------------------------------------------------------------------ @@ -1380,38 +1432,38 @@ This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------ ALTERNATIVE A - MIT License Copyright (c) 2017 Sean Barrett -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ ALTERNATIVE B - Public Domain (www.unlicense.org) This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -software, either in source code form or as a compiled binary, for any purpose, +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors of this -software dedicate any and all copyright interest in the software to the public -domain. We make this dedication for the benefit of the public at large and to -the detriment of our heirs and successors. We intend this dedication to be an -overt act of relinquishment in perpetuity of all present and future rights to +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ */ diff --git a/imgui/imstb_truetype.h b/imgui/imstb_truetype.h index c1cdb180..976f09cb 100644 --- a/imgui/imstb_truetype.h +++ b/imgui/imstb_truetype.h @@ -1,10 +1,19 @@ -// [DEAR IMGUI] -// This is a slightly modified version of stb_truetype.h 1.20. +// [DEAR IMGUI] +// This is a slightly modified version of stb_truetype.h 1.26. // Mostly fixing for compiler and static analyzer warnings. // Grep for [DEAR IMGUI] to find the changes. -// stb_truetype.h - v1.20 - public domain -// authored from 2009-2016 by Sean Barrett / RAD Game Tools +// stb_truetype.h - v1.26 - public domain +// authored from 2009-2021 by Sean Barrett / RAD Game Tools +// +// ======================================================================= +// +// NO SECURITY GUARANTEE -- DO NOT USE THIS ON UNTRUSTED FONT FILES +// +// This library does no range checking of the offsets found in the file, +// meaning an attacker can use it to read arbitrary memory. +// +// ======================================================================= // // This library processes TrueType files: // parse files @@ -37,11 +46,11 @@ // Daniel Ribeiro Maciel // // Bug/warning reports/fixes: -// "Zer" on mollyrocket Fabian "ryg" Giesen -// Cass Everitt Martins Mozeiko -// stoiko (Haemimont Games) Cap Petschulat -// Brian Hook Omar Cornut -// Walter van Niftrik github:aloucks +// "Zer" on mollyrocket Fabian "ryg" Giesen github:NiLuJe +// Cass Everitt Martins Mozeiko github:aloucks +// stoiko (Haemimont Games) Cap Petschulat github:oyvindjam +// Brian Hook Omar Cornut github:vassvik +// Walter van Niftrik Ryan Griege // David Gow Peter LaValle // David Given Sergey Popov // Ivan-Assen Ivanov Giumo X. Clanjor @@ -49,11 +58,17 @@ // Johan Duparc Thomas Fields // Hou Qiming Derek Vinyard // Rob Loach Cort Stratton -// Kenney Phillis Jr. github:oyvindjam -// Brian Costabile github:vassvik -// +// Kenney Phillis Jr. Brian Costabile +// Ken Voskuil (kaesve) +// // VERSION HISTORY // +// 1.26 (2021-08-28) fix broken rasterizer +// 1.25 (2021-07-11) many fixes +// 1.24 (2020-02-05) fix warning +// 1.23 (2020-02-02) query SVG data for glyphs; query whole kerning table (but only kern not GPOS) +// 1.22 (2019-08-11) minimize missing-glyph duplication; fix kerning if both 'GPOS' and 'kern' are defined +// 1.21 (2019-02-25) fix warning // 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() // 1.19 (2018-02-11) GPOS kerning, STBTT_fmod // 1.18 (2018-01-29) add missing function @@ -212,7 +227,7 @@ // // Advancing for the next character: // Call GlyphHMetrics, and compute 'current_point += SF * advance'. -// +// // // ADVANCED USAGE // @@ -248,19 +263,6 @@ // recommend it. // // -// SOURCE STATISTICS (based on v0.6c, 2050 LOC) -// -// Documentation & header file 520 LOC \___ 660 LOC documentation -// Sample code 140 LOC / -// Truetype parsing 620 LOC ---- 620 LOC TrueType -// Software rasterization 240 LOC \. -// Curve tessellation 120 LOC \__ 550 LOC Bitmap creation -// Bitmap management 100 LOC / -// Baked bitmap interface 70 LOC / -// Font name matching & access 150 LOC ---- 150 -// C runtime library abstraction 60 LOC ---- 60 -// -// // PERFORMANCE MEASUREMENTS FOR 1.06: // // 32-bit 64-bit @@ -275,8 +277,8 @@ //// SAMPLE PROGRAMS //// // -// Incomplete text-in-3d-api example, which draws quads properly aligned to be lossless -// +// Incomplete text-in-3d-api example, which draws quads properly aligned to be lossless. +// See "tests/truetype_demo_win32.c" for a complete version. #if 0 #define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation #include "stb_truetype.h" @@ -302,6 +304,8 @@ void my_stbtt_initfont(void) void my_stbtt_print(float x, float y, char *text) { // assume orthographic projection with units = screen pixels, origin at top left + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, ftex); glBegin(GL_QUADS); @@ -309,10 +313,10 @@ void my_stbtt_print(float x, float y, char *text) if (*text >= 32 && *text < 128) { stbtt_aligned_quad q; stbtt_GetBakedQuad(cdata, 512,512, *text-32, &x,&y,&q,1);//1=opengl & d3d10+,0=d3d9 - glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y0); - glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y0); - glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y1); - glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y1); + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y0); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y0); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y1); + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y1); } ++text; } @@ -350,7 +354,7 @@ int main(int argc, char **argv) } return 0; } -#endif +#endif // // Output: // @@ -364,9 +368,9 @@ int main(int argc, char **argv) // :@@. M@M // @@@o@@@@ // :M@@V:@@. -// +// ////////////////////////////////////////////////////////////////////////////// -// +// // Complete program: print "Hello World!" banner, with bugs // #if 0 @@ -652,7 +656,7 @@ STBTT_DEF void stbtt_PackSetOversampling(stbtt_pack_context *spc, unsigned int h STBTT_DEF void stbtt_PackSetSkipMissingCodepoints(stbtt_pack_context *spc, int skip); // If skip != 0, this tells stb_truetype to skip any codepoints for which // there is no corresponding glyph. If skip=0, which is the default, then -// codepoints without a glyph recived the font's "missing character" glyph, +// codepoints without a glyph received the font's "missing character" glyph, // typically an empty box by convention. STBTT_DEF void stbtt_GetPackedQuad(const stbtt_packedchar *chardata, int pw, int ph, // same data as above @@ -667,7 +671,7 @@ STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, cons // Calling these functions in sequence is roughly equivalent to calling // stbtt_PackFontRanges(). If you more control over the packing of multiple // fonts, or if you want to pack custom data into a font texture, take a look -// at the source to of stbtt_PackFontRanges() and create a custom version +// at the source to of stbtt_PackFontRanges() and create a custom version // using these functions, e.g. call GatherRects multiple times, // building up a single array of rects, then call PackRects once, // then call RenderIntoRects repeatedly. This may result in a @@ -719,7 +723,7 @@ struct stbtt_fontinfo int numGlyphs; // number of glyphs, needed for range checking - int loca,head,glyf,hhea,hmtx,kern,gpos; // table locations as offset from start of .ttf + int loca,head,glyf,hhea,hmtx,kern,gpos,svg; // table locations as offset from start of .ttf int index_map; // a cmap mapping for our chosen character encoding int indexToLocFormat; // format needed to map from glyph index to glyph @@ -802,6 +806,18 @@ STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, STBTT_DEF int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); // as above, but takes one or more glyph indices for greater efficiency +typedef struct stbtt_kerningentry +{ + int glyph1; // use stbtt_FindGlyphIndex + int glyph2; + int advance; +} stbtt_kerningentry; + +STBTT_DEF int stbtt_GetKerningTableLength(const stbtt_fontinfo *info); +STBTT_DEF int stbtt_GetKerningTable(const stbtt_fontinfo *info, stbtt_kerningentry* table, int table_length); +// Retrieves a complete list of all of the kerning pairs provided by the font +// stbtt_GetKerningTable never writes more than table_length entries and returns how many entries it did write. +// The table will be sorted by (a.glyph1 == b.glyph1)?(a.glyph2 < b.glyph2):(a.glyph1 < b.glyph1) ////////////////////////////////////////////////////////////////////////////// // @@ -846,6 +862,12 @@ STBTT_DEF int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, s STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); // frees the data allocated above +STBTT_DEF unsigned char *stbtt_FindSVGDoc(const stbtt_fontinfo *info, int gl); +STBTT_DEF int stbtt_GetCodepointSVG(const stbtt_fontinfo *info, int unicode_codepoint, const char **svg); +STBTT_DEF int stbtt_GetGlyphSVG(const stbtt_fontinfo *info, int gl, const char **svg); +// fills svg with the character's SVG data. +// returns data size or 0 if SVG not found. + ////////////////////////////////////////////////////////////////////////////// // // BITMAP RENDERING @@ -975,7 +997,7 @@ STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, floa // and computing from that can allow drop-out prevention). // // The algorithm has not been optimized at all, so expect it to be slow -// if computing lots of characters or very large sizes. +// if computing lots of characters or very large sizes. @@ -1347,6 +1369,22 @@ static stbtt__buf stbtt__get_subrs(stbtt__buf cff, stbtt__buf fontdict) return stbtt__cff_get_index(&cff); } +// since most people won't use this, find this table the first time it's needed +static int stbtt__get_svg(stbtt_fontinfo *info) +{ + stbtt_uint32 t; + if (info->svg < 0) { + t = stbtt__find_table(info->data, info->fontstart, "SVG "); + if (t) { + stbtt_uint32 offset = ttULONG(info->data + t + 2); + info->svg = t + offset; + } else { + info->svg = 0; + } + } + return info->svg; +} + static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, int fontstart) { stbtt_uint32 cmap, t; @@ -1426,6 +1464,8 @@ static int stbtt_InitFont_internal(stbtt_fontinfo *info, unsigned char *data, in else info->numGlyphs = 0xffff; + info->svg = -1; + // find a cmap encoding table we understand *now* to avoid searching // later. (todo: could make this installable) // the same regardless of glyph. @@ -1509,12 +1549,12 @@ STBTT_DEF int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codep search += 2; { - stbtt_uint16 offset, start; + stbtt_uint16 offset, start, last; stbtt_uint16 item = (stbtt_uint16) ((search - endCount) >> 1); - STBTT_assert(unicode_codepoint <= ttUSHORT(data + endCount + 2*item)); start = ttUSHORT(data + index_map + 14 + segcount*2 + 2 + 2*item); - if (unicode_codepoint < start) + last = ttUSHORT(data + endCount + 2*item); + if (unicode_codepoint < start || unicode_codepoint > last) return 0; offset = ttUSHORT(data + index_map + 14 + segcount*6 + 2 + 2*item); @@ -1732,7 +1772,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s if (i != 0) num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); - // now start the new one + // now start the new one start_off = !(flags & 1); if (start_off) { // if we start off with an off-curve point, then when we need to find a point on the curve @@ -1774,7 +1814,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s } } num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); - } else if (numberOfContours == -1) { + } else if (numberOfContours < 0) { // Compound shapes. int more = 1; stbtt_uint8 *comp = data + g + 10; @@ -1785,7 +1825,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s int comp_num_verts = 0, i; stbtt_vertex *comp_verts = 0, *tmp = 0; float mtx[6] = {1,0,0,1,0,0}, m, n; - + flags = ttSHORT(comp); comp+=2; gidx = ttSHORT(comp); comp+=2; @@ -1815,7 +1855,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; } - + // Find transformation scales. m = (float) STBTT_sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); n = (float) STBTT_sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); @@ -1841,7 +1881,7 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s if (comp_verts) STBTT_free(comp_verts, info->userdata); return 0; } - if (num_vertices > 0) STBTT_memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); //-V595 + if (num_vertices > 0 && vertices) STBTT_memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); STBTT_memcpy(tmp+num_vertices, comp_verts, comp_num_verts*sizeof(stbtt_vertex)); if (vertices) STBTT_free(vertices, info->userdata); vertices = tmp; @@ -1851,9 +1891,6 @@ static int stbtt__GetGlyphShapeTT(const stbtt_fontinfo *info, int glyph_index, s // More components ? more = flags & (1<<5); } - } else if (numberOfContours < 0) { - // @TODO other compound variations? - STBTT_assert(0); } else { // numberOfCounters == 0, do nothing } @@ -1971,7 +2008,7 @@ static stbtt__buf stbtt__cid_get_glyph_subrs(const stbtt_fontinfo *info, int gly start = end; } } - if (fdselector == -1) stbtt__new_buf(NULL, 0); + if (fdselector == -1) return stbtt__new_buf(NULL, 0); // [DEAR IMGUI] fixed, see #6007 and nothings/stb#1422 return stbtt__get_subrs(info->cff, stbtt__cff_index_get(info->fontdicts, fdselector)); } @@ -2107,7 +2144,7 @@ static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, st subrs = stbtt__cid_get_glyph_subrs(info, glyph_index); has_subrs = 1; } - // fallthrough + // FALLTHROUGH case 0x1D: // callgsubr if (sp < 1) return STBTT__CSERR("call(g|)subr stack"); v = (int) s[--sp]; @@ -2212,7 +2249,7 @@ static int stbtt__run_charstring(const stbtt_fontinfo *info, int glyph_index, st } break; default: - if (b0 != 255 && b0 != 28 && (b0 < 32 || b0 > 254)) //-V560 + if (b0 != 255 && b0 != 28 && b0 < 32) return STBTT__CSERR("reserved operator"); // push immediate @@ -2282,7 +2319,49 @@ STBTT_DEF void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_inde } } -static int stbtt__GetGlyphKernInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +STBTT_DEF int stbtt_GetKerningTableLength(const stbtt_fontinfo *info) +{ + stbtt_uint8 *data = info->data + info->kern; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + return ttUSHORT(data+10); +} + +STBTT_DEF int stbtt_GetKerningTable(const stbtt_fontinfo *info, stbtt_kerningentry* table, int table_length) +{ + stbtt_uint8 *data = info->data + info->kern; + int k, length; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + length = ttUSHORT(data+10); + if (table_length < length) + length = table_length; + + for (k = 0; k < length; k++) + { + table[k].glyph1 = ttUSHORT(data+18+(k*6)); + table[k].glyph2 = ttUSHORT(data+20+(k*6)); + table[k].advance = ttSHORT(data+22+(k*6)); + } + + return length; +} + +static int stbtt__GetGlyphKernInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) { stbtt_uint8 *data = info->data + info->kern; stbtt_uint32 needle, straw; @@ -2312,245 +2391,225 @@ static int stbtt__GetGlyphKernInfoAdvance(const stbtt_fontinfo *info, int glyph return 0; } -static stbtt_int32 stbtt__GetCoverageIndex(stbtt_uint8 *coverageTable, int glyph) +static stbtt_int32 stbtt__GetCoverageIndex(stbtt_uint8 *coverageTable, int glyph) { - stbtt_uint16 coverageFormat = ttUSHORT(coverageTable); - switch(coverageFormat) { - case 1: { - stbtt_uint16 glyphCount = ttUSHORT(coverageTable + 2); + stbtt_uint16 coverageFormat = ttUSHORT(coverageTable); + switch (coverageFormat) { + case 1: { + stbtt_uint16 glyphCount = ttUSHORT(coverageTable + 2); - // Binary search. - stbtt_int32 l=0, r=glyphCount-1, m; - int straw, needle=glyph; - while (l <= r) { - stbtt_uint8 *glyphArray = coverageTable + 4; - stbtt_uint16 glyphID; - m = (l + r) >> 1; - glyphID = ttUSHORT(glyphArray + 2 * m); - straw = glyphID; - if (needle < straw) - r = m - 1; - else if (needle > straw) - l = m + 1; - else { - return m; - } + // Binary search. + stbtt_int32 l=0, r=glyphCount-1, m; + int straw, needle=glyph; + while (l <= r) { + stbtt_uint8 *glyphArray = coverageTable + 4; + stbtt_uint16 glyphID; + m = (l + r) >> 1; + glyphID = ttUSHORT(glyphArray + 2 * m); + straw = glyphID; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + return m; } - } break; + } + break; + } - case 2: { - stbtt_uint16 rangeCount = ttUSHORT(coverageTable + 2); - stbtt_uint8 *rangeArray = coverageTable + 4; + case 2: { + stbtt_uint16 rangeCount = ttUSHORT(coverageTable + 2); + stbtt_uint8 *rangeArray = coverageTable + 4; - // Binary search. - stbtt_int32 l=0, r=rangeCount-1, m; - int strawStart, strawEnd, needle=glyph; - while (l <= r) { - stbtt_uint8 *rangeRecord; - m = (l + r) >> 1; - rangeRecord = rangeArray + 6 * m; - strawStart = ttUSHORT(rangeRecord); - strawEnd = ttUSHORT(rangeRecord + 2); - if (needle < strawStart) - r = m - 1; - else if (needle > strawEnd) - l = m + 1; - else { - stbtt_uint16 startCoverageIndex = ttUSHORT(rangeRecord + 4); - return startCoverageIndex + glyph - strawStart; - } + // Binary search. + stbtt_int32 l=0, r=rangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *rangeRecord; + m = (l + r) >> 1; + rangeRecord = rangeArray + 6 * m; + strawStart = ttUSHORT(rangeRecord); + strawEnd = ttUSHORT(rangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else { + stbtt_uint16 startCoverageIndex = ttUSHORT(rangeRecord + 4); + return startCoverageIndex + glyph - strawStart; } - } break; + } + break; + } - default: { - // There are no other cases. - STBTT_assert(0); - } break; - } + default: return -1; // unsupported + } - return -1; + return -1; } static stbtt_int32 stbtt__GetGlyphClass(stbtt_uint8 *classDefTable, int glyph) { - stbtt_uint16 classDefFormat = ttUSHORT(classDefTable); - switch(classDefFormat) - { - case 1: { - stbtt_uint16 startGlyphID = ttUSHORT(classDefTable + 2); - stbtt_uint16 glyphCount = ttUSHORT(classDefTable + 4); - stbtt_uint8 *classDef1ValueArray = classDefTable + 6; + stbtt_uint16 classDefFormat = ttUSHORT(classDefTable); + switch (classDefFormat) + { + case 1: { + stbtt_uint16 startGlyphID = ttUSHORT(classDefTable + 2); + stbtt_uint16 glyphCount = ttUSHORT(classDefTable + 4); + stbtt_uint8 *classDef1ValueArray = classDefTable + 6; - if (glyph >= startGlyphID && glyph < startGlyphID + glyphCount) - return (stbtt_int32)ttUSHORT(classDef1ValueArray + 2 * (glyph - startGlyphID)); + if (glyph >= startGlyphID && glyph < startGlyphID + glyphCount) + return (stbtt_int32)ttUSHORT(classDef1ValueArray + 2 * (glyph - startGlyphID)); + break; + } - // [DEAR IMGUI] Commented to fix static analyzer warning - //classDefTable = classDef1ValueArray + 2 * glyphCount; - } break; + case 2: { + stbtt_uint16 classRangeCount = ttUSHORT(classDefTable + 2); + stbtt_uint8 *classRangeRecords = classDefTable + 4; - case 2: { - stbtt_uint16 classRangeCount = ttUSHORT(classDefTable + 2); - stbtt_uint8 *classRangeRecords = classDefTable + 4; + // Binary search. + stbtt_int32 l=0, r=classRangeCount-1, m; + int strawStart, strawEnd, needle=glyph; + while (l <= r) { + stbtt_uint8 *classRangeRecord; + m = (l + r) >> 1; + classRangeRecord = classRangeRecords + 6 * m; + strawStart = ttUSHORT(classRangeRecord); + strawEnd = ttUSHORT(classRangeRecord + 2); + if (needle < strawStart) + r = m - 1; + else if (needle > strawEnd) + l = m + 1; + else + return (stbtt_int32)ttUSHORT(classRangeRecord + 4); + } + break; + } - // Binary search. - stbtt_int32 l=0, r=classRangeCount-1, m; - int strawStart, strawEnd, needle=glyph; - while (l <= r) { - stbtt_uint8 *classRangeRecord; - m = (l + r) >> 1; - classRangeRecord = classRangeRecords + 6 * m; - strawStart = ttUSHORT(classRangeRecord); - strawEnd = ttUSHORT(classRangeRecord + 2); - if (needle < strawStart) - r = m - 1; - else if (needle > strawEnd) - l = m + 1; - else - return (stbtt_int32)ttUSHORT(classRangeRecord + 4); - } + default: + return -1; // Unsupported definition type, return an error. + } - // [DEAR IMGUI] Commented to fix static analyzer warning - //classDefTable = classRangeRecords + 6 * classRangeCount; - } break; - - default: { - // There are no other cases. - STBTT_assert(0); - } break; - } - - return -1; + // "All glyphs not assigned to a class fall into class 0". (OpenType spec) + return 0; } // Define to STBTT_assert(x) if you want to break on unimplemented formats. #define STBTT_GPOS_TODO_assert(x) -static stbtt_int32 stbtt__GetGlyphGPOSInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +static stbtt_int32 stbtt__GetGlyphGPOSInfoAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) { - stbtt_uint16 lookupListOffset; - stbtt_uint8 *lookupList; - stbtt_uint16 lookupCount; - stbtt_uint8 *data; - stbtt_int32 i; + stbtt_uint16 lookupListOffset; + stbtt_uint8 *lookupList; + stbtt_uint16 lookupCount; + stbtt_uint8 *data; + stbtt_int32 i, sti; - if (!info->gpos) return 0; + if (!info->gpos) return 0; - data = info->data + info->gpos; + data = info->data + info->gpos; - if (ttUSHORT(data+0) != 1) return 0; // Major version 1 - if (ttUSHORT(data+2) != 0) return 0; // Minor version 0 + if (ttUSHORT(data+0) != 1) return 0; // Major version 1 + if (ttUSHORT(data+2) != 0) return 0; // Minor version 0 - lookupListOffset = ttUSHORT(data+8); - lookupList = data + lookupListOffset; - lookupCount = ttUSHORT(lookupList); + lookupListOffset = ttUSHORT(data+8); + lookupList = data + lookupListOffset; + lookupCount = ttUSHORT(lookupList); - for (i=0; i= pairSetCount) return 0; - // Binary search. - while (l <= r) { - stbtt_uint16 secondGlyph; - stbtt_uint8 *pairValue; - m = (l + r) >> 1; - pairValue = pairValueArray + (2 + valueRecordPairSizeInBytes) * m; - secondGlyph = ttUSHORT(pairValue); - straw = secondGlyph; - if (needle < straw) - r = m - 1; - else if (needle > straw) - l = m + 1; - else { - stbtt_int16 xAdvance = ttSHORT(pairValue + 2); - return xAdvance; - } - } - } break; + needle=glyph2; + r=pairValueCount-1; + l=0; - case 2: { - stbtt_uint16 valueFormat1 = ttUSHORT(table + 4); - stbtt_uint16 valueFormat2 = ttUSHORT(table + 6); + // Binary search. + while (l <= r) { + stbtt_uint16 secondGlyph; + stbtt_uint8 *pairValue; + m = (l + r) >> 1; + pairValue = pairValueArray + (2 + valueRecordPairSizeInBytes) * m; + secondGlyph = ttUSHORT(pairValue); + straw = secondGlyph; + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else { + stbtt_int16 xAdvance = ttSHORT(pairValue + 2); + return xAdvance; + } + } + } else + return 0; + break; + } - stbtt_uint16 classDef1Offset = ttUSHORT(table + 8); - stbtt_uint16 classDef2Offset = ttUSHORT(table + 10); - int glyph1class = stbtt__GetGlyphClass(table + classDef1Offset, glyph1); - int glyph2class = stbtt__GetGlyphClass(table + classDef2Offset, glyph2); + case 2: { + stbtt_uint16 valueFormat1 = ttUSHORT(table + 4); + stbtt_uint16 valueFormat2 = ttUSHORT(table + 6); + if (valueFormat1 == 4 && valueFormat2 == 0) { // Support more formats? + stbtt_uint16 classDef1Offset = ttUSHORT(table + 8); + stbtt_uint16 classDef2Offset = ttUSHORT(table + 10); + int glyph1class = stbtt__GetGlyphClass(table + classDef1Offset, glyph1); + int glyph2class = stbtt__GetGlyphClass(table + classDef2Offset, glyph2); - stbtt_uint16 class1Count = ttUSHORT(table + 12); - stbtt_uint16 class2Count = ttUSHORT(table + 14); - STBTT_assert(glyph1class < class1Count); - STBTT_assert(glyph2class < class2Count); + stbtt_uint16 class1Count = ttUSHORT(table + 12); + stbtt_uint16 class2Count = ttUSHORT(table + 14); + stbtt_uint8 *class1Records, *class2Records; + stbtt_int16 xAdvance; - // TODO: Support more formats. - STBTT_GPOS_TODO_assert(valueFormat1 == 4); - if (valueFormat1 != 4) return 0; - STBTT_GPOS_TODO_assert(valueFormat2 == 0); - if (valueFormat2 != 0) return 0; + if (glyph1class < 0 || glyph1class >= class1Count) return 0; // malformed + if (glyph2class < 0 || glyph2class >= class2Count) return 0; // malformed - if (glyph1class >= 0 && glyph1class < class1Count && glyph2class >= 0 && glyph2class < class2Count) { - stbtt_uint8 *class1Records = table + 16; - stbtt_uint8 *class2Records = class1Records + 2 * (glyph1class * class2Count); - stbtt_int16 xAdvance = ttSHORT(class2Records + 2 * glyph2class); - return xAdvance; - } - } break; - - default: { - // There are no other cases. - STBTT_assert(0); - break; - }; - } - } - break; - }; + class1Records = table + 16; + class2Records = class1Records + 2 * (glyph1class * class2Count); + xAdvance = ttSHORT(class2Records + 2 * glyph2class); + return xAdvance; + } else + return 0; + break; + } default: - // TODO: Implement other stuff. - break; - } - } + return 0; // Unsupported position format + } + } + } - return 0; + return 0; } STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int g1, int g2) @@ -2559,8 +2618,7 @@ STBTT_DEF int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int g1, int if (info->gpos) xAdvance += stbtt__GetGlyphGPOSInfoAdvance(info, g1, g2); - - if (info->kern) + else if (info->kern) xAdvance += stbtt__GetGlyphKernInfoAdvance(info, g1, g2); return xAdvance; @@ -2621,6 +2679,45 @@ STBTT_DEF void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) STBTT_free(v, info->userdata); } +STBTT_DEF stbtt_uint8 *stbtt_FindSVGDoc(const stbtt_fontinfo *info, int gl) +{ + int i; + stbtt_uint8 *data = info->data; + stbtt_uint8 *svg_doc_list = data + stbtt__get_svg((stbtt_fontinfo *) info); + + int numEntries = ttUSHORT(svg_doc_list); + stbtt_uint8 *svg_docs = svg_doc_list + 2; + + for(i=0; i= ttUSHORT(svg_doc)) && (gl <= ttUSHORT(svg_doc + 2))) + return svg_doc; + } + return 0; +} + +STBTT_DEF int stbtt_GetGlyphSVG(const stbtt_fontinfo *info, int gl, const char **svg) +{ + stbtt_uint8 *data = info->data; + stbtt_uint8 *svg_doc; + + if (info->svg == 0) + return 0; + + svg_doc = stbtt_FindSVGDoc(info, gl); + if (svg_doc != NULL) { + *svg = (char *) data + info->svg + ttULONG(svg_doc + 4); + return ttULONG(svg_doc + 8); + } else { + return 0; + } +} + +STBTT_DEF int stbtt_GetCodepointSVG(const stbtt_fontinfo *info, int unicode_codepoint, const char **svg) +{ + return stbtt_GetGlyphSVG(info, stbtt_FindGlyphIndex(info, unicode_codepoint), svg); +} + ////////////////////////////////////////////////////////////////////////////// // // antialiasing software rasterizer @@ -2746,7 +2843,7 @@ static stbtt__active_edge *stbtt__new_active(stbtt__hheap *hh, stbtt__edge *e, i float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); STBTT_assert(z != NULL); if (!z) return z; - + // round dx down to avoid overshooting if (dxdy < 0) z->dx = -STBTT_ifloor(STBTT_FIX * -dxdy); @@ -2824,7 +2921,7 @@ static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__ac } } } - + e = e->next; } } @@ -2970,6 +3067,23 @@ static void stbtt__handle_clipped_edge(float *scanline, int x, stbtt__active_edg } } +static float stbtt__sized_trapezoid_area(float height, float top_width, float bottom_width) +{ + STBTT_assert(top_width >= 0); + STBTT_assert(bottom_width >= 0); + return (top_width + bottom_width) / 2.0f * height; +} + +static float stbtt__position_trapezoid_area(float height, float tx0, float tx1, float bx0, float bx1) +{ + return stbtt__sized_trapezoid_area(height, tx1 - tx0, bx1 - bx0); +} + +static float stbtt__sized_triangle_area(float height, float width) +{ + return height * width / 2; +} + static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, int len, stbtt__active_edge *e, float y_top) { float y_bottom = y_top+1; @@ -3024,13 +3138,13 @@ static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, float height; // simple case, only spans one pixel int x = (int) x_top; - height = sy1 - sy0; + height = (sy1 - sy0) * e->direction; STBTT_assert(x >= 0 && x < len); - scanline[x] += e->direction * (1-((x_top - x) + (x_bottom-x))/2) * height; - scanline_fill[x] += e->direction * height; // everything right of this pixel is filled + scanline[x] += stbtt__position_trapezoid_area(height, x_top, x+1.0f, x_bottom, x+1.0f); + scanline_fill[x] += height; // everything right of this pixel is filled } else { int x,x1,x2; - float y_crossing, step, sign, area; + float y_crossing, y_final, step, sign, area; // covers 2+ pixels if (x_top > x_bottom) { // flip scanline vertically; signed area is the same @@ -3042,32 +3156,83 @@ static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, dx = -dx; dy = -dy; t = x0, x0 = xb, xb = t; - // [DEAR IMGUI] Fix static analyzer warning - (void)dx; // [ImGui: fix static analyzer warning] } + STBTT_assert(dy >= 0); + STBTT_assert(dx >= 0); x1 = (int) x_top; x2 = (int) x_bottom; // compute intersection with y axis at x1+1 - y_crossing = (x1+1 - x0) * dy + y_top; + y_crossing = y_top + dy * (x1+1 - x0); + + // compute intersection with y axis at x2 + y_final = y_top + dy * (x2 - x0); + + // x1 x_top x2 x_bottom + // y_top +------|-----+------------+------------+--------|---+------------+ + // | | | | | | + // | | | | | | + // sy0 | Txxxxx|............|............|............|............| + // y_crossing | *xxxxx.......|............|............|............| + // | | xxxxx..|............|............|............| + // | | /- xx*xxxx........|............|............| + // | | dy < | xxxxxx..|............|............| + // y_final | | \- | xx*xxx.........|............| + // sy1 | | | | xxxxxB...|............| + // | | | | | | + // | | | | | | + // y_bottom +------------+------------+------------+------------+------------+ + // + // goal is to measure the area covered by '.' in each pixel + + // if x2 is right at the right edge of x1, y_crossing can blow up, github #1057 + // @TODO: maybe test against sy1 rather than y_bottom? + if (y_crossing > y_bottom) + y_crossing = y_bottom; sign = e->direction; - // area of the rectangle covered from y0..y_crossing - area = sign * (y_crossing-sy0); - // area of the triangle (x_top,y0), (x+1,y0), (x+1,y_crossing) - scanline[x1] += area * (1-((x_top - x1)+(x1+1-x1))/2); - step = sign * dy; + // area of the rectangle covered from sy0..y_crossing + area = sign * (y_crossing-sy0); + + // area of the triangle (x_top,sy0), (x1+1,sy0), (x1+1,y_crossing) + scanline[x1] += stbtt__sized_triangle_area(area, x1+1 - x_top); + + // check if final y_crossing is blown up; no test case for this + if (y_final > y_bottom) { + int denom = (x2 - (x1+1)); + y_final = y_bottom; + if (denom != 0) { // [DEAR IMGUI] Avoid div by zero (https://github.com/nothings/stb/issues/1316) + dy = (y_final - y_crossing ) / denom; // if denom=0, y_final = y_crossing, so y_final <= y_bottom + } + } + + // in second pixel, area covered by line segment found in first pixel + // is always a rectangle 1 wide * the height of that line segment; this + // is exactly what the variable 'area' stores. it also gets a contribution + // from the line segment within it. the THIRD pixel will get the first + // pixel's rectangle contribution, the second pixel's rectangle contribution, + // and its own contribution. the 'own contribution' is the same in every pixel except + // the leftmost and rightmost, a trapezoid that slides down in each pixel. + // the second pixel's contribution to the third pixel will be the + // rectangle 1 wide times the height change in the second pixel, which is dy. + + step = sign * dy * 1; // dy is dy/dx, change in y for every 1 change in x, + // which multiplied by 1-pixel-width is how much pixel area changes for each step in x + // so the area advances by 'step' every time + for (x = x1+1; x < x2; ++x) { - scanline[x] += area + step/2; + scanline[x] += area + step/2; // area of trapezoid is 1*step/2 area += step; } - y_crossing += dy * (x2 - (x1+1)); + STBTT_assert(STBTT_fabs(area) <= 1.01f); // accumulated error from area += step unless we round step down + STBTT_assert(sy1 > y_final-0.01f); - STBTT_assert(STBTT_fabs(area) <= 1.01f); - - scanline[x2] += area + sign * (1-((x2-x2)+(x_bottom-x2))/2) * (sy1-y_crossing); + // area covered in the last pixel is the rectangle from all the pixels to the left, + // plus the trapezoid filled by the line segment in this pixel all the way to the right edge + scanline[x2] += area + sign * stbtt__position_trapezoid_area(sy1-y_final, (float) x2, x2+1.0f, x_bottom, x2+1.0f); + // the rest of the line is filled based on the total height of the line segment in this pixel scanline_fill[x2] += sign * (sy1-sy0); } } else { @@ -3075,6 +3240,9 @@ static void stbtt__fill_active_edges_new(float *scanline, float *scanline_fill, // clipping logic. since this does not match the intended use // of this library, we use a different, very slow brute // force implementation + // note though that this does happen some of the time because + // x_top and x_bottom can be extrapolated at the top & bottom of + // the shape and actually lie outside the bounding box int x; for (x=0; x < len; ++x) { // cases: @@ -3554,7 +3722,7 @@ STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info { int ix0,iy0,ix1,iy1; stbtt__bitmap gbm; - stbtt_vertex *vertices; + stbtt_vertex *vertices; int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); if (scale_x == 0) scale_x = scale_y; @@ -3577,7 +3745,7 @@ STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info if (height) *height = gbm.h; if (xoff ) *xoff = ix0; if (yoff ) *yoff = iy0; - + if (gbm.w && gbm.h) { gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); if (gbm.pixels) { @@ -3588,7 +3756,7 @@ STBTT_DEF unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info } STBTT_free(vertices, info->userdata); return gbm.pixels; -} +} STBTT_DEF unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) { @@ -3600,7 +3768,7 @@ STBTT_DEF void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigne int ix0,iy0; stbtt_vertex *vertices; int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); - stbtt__bitmap gbm; + stbtt__bitmap gbm; stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); gbm.pixels = output; @@ -3622,7 +3790,7 @@ STBTT_DEF void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char * STBTT_DEF unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) { return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); -} +} STBTT_DEF void stbtt_MakeCodepointBitmapSubpixelPrefilter(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int oversample_x, int oversample_y, float *sub_x, float *sub_y, int codepoint) { @@ -3637,7 +3805,7 @@ STBTT_DEF void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, uns STBTT_DEF unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) { return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); -} +} STBTT_DEF void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) { @@ -3762,7 +3930,7 @@ static void stbrp_init_target(stbrp_context *con, int pw, int ph, stbrp_node *no con->y = 0; con->bottom_y = 0; STBTT__NOTUSED(nodes); - STBTT__NOTUSED(num_nodes); + STBTT__NOTUSED(num_nodes); } static void stbrp_pack_rects(stbrp_context *con, stbrp_rect *rects, int num_rects) @@ -3989,6 +4157,7 @@ static float stbtt__oversample_shift(int oversample) STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) { int i,j,k; + int missing_glyph_added = 0; k=0; for (i=0; i < num_ranges; ++i) { @@ -4000,7 +4169,7 @@ STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stb int x0,y0,x1,y1; int codepoint = ranges[i].array_of_unicode_codepoints == NULL ? ranges[i].first_unicode_codepoint_in_range + j : ranges[i].array_of_unicode_codepoints[j]; int glyph = stbtt_FindGlyphIndex(info, codepoint); - if (glyph == 0 && spc->skip_missing) { + if (glyph == 0 && (spc->skip_missing || missing_glyph_added)) { rects[k].w = rects[k].h = 0; } else { stbtt_GetGlyphBitmapBoxSubpixel(info,glyph, @@ -4010,6 +4179,8 @@ STBTT_DEF int stbtt_PackFontRangesGatherRects(stbtt_pack_context *spc, const stb &x0,&y0,&x1,&y1); rects[k].w = (stbrp_coord) (x1-x0 + spc->padding + spc->h_oversample-1); rects[k].h = (stbrp_coord) (y1-y0 + spc->padding + spc->v_oversample-1); + if (glyph == 0) + missing_glyph_added = 1; } ++k; } @@ -4044,7 +4215,7 @@ STBTT_DEF void stbtt_MakeGlyphBitmapSubpixelPrefilter(const stbtt_fontinfo *info // rects array must be big enough to accommodate all characters in the given ranges STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const stbtt_fontinfo *info, stbtt_pack_range *ranges, int num_ranges, stbrp_rect *rects) { - int i,j,k, return_value = 1; + int i,j,k, missing_glyph = -1, return_value = 1; // save current values int old_h_over = spc->h_oversample; @@ -4109,6 +4280,13 @@ STBTT_DEF int stbtt_PackFontRangesRenderIntoRects(stbtt_pack_context *spc, const bc->yoff = (float) y0 * recip_v + sub_y; bc->xoff2 = (x0 + r->w) * recip_h + sub_x; bc->yoff2 = (y0 + r->h) * recip_v + sub_y; + + if (glyph == 0) + missing_glyph = j; + } else if (spc->skip_missing) { + return_value = 0; + } else if (r->was_packed && r->w == 0 && r->h == 0 && missing_glyph >= 0) { + ranges[i].chardata_for_range[j] = ranges[i].chardata_for_range[missing_glyph]; } else { return_value = 0; // if any fail, report failure } @@ -4132,7 +4310,7 @@ STBTT_DEF void stbtt_PackFontRangesPackRects(stbtt_pack_context *spc, stbrp_rect STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char *fontdata, int font_index, stbtt_pack_range *ranges, int num_ranges) { stbtt_fontinfo info; - int i,j,n, return_value = 1; + int i, j, n, return_value; // [DEAR IMGUI] removed = 1; //stbrp_context *context = (stbrp_context *) spc->pack_info; stbrp_rect *rects; @@ -4147,7 +4325,7 @@ STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char n = 0; for (i=0; i < num_ranges; ++i) n += ranges[i].num_chars; - + rects = (stbrp_rect *) STBTT_malloc(sizeof(*rects) * n, spc->user_allocator_context); if (rects == NULL) return 0; @@ -4158,7 +4336,7 @@ STBTT_DEF int stbtt_PackFontRanges(stbtt_pack_context *spc, const unsigned char n = stbtt_PackFontRangesGatherRects(spc, &info, ranges, num_ranges, rects); stbtt_PackFontRangesPackRects(spc, rects, n); - + return_value = stbtt_PackFontRangesRenderIntoRects(spc, &info, ranges, num_ranges, rects); STBTT_free(rects, spc->user_allocator_context); @@ -4301,15 +4479,14 @@ static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex float y_frac; int winding = 0; - orig[0] = x; - //orig[1] = y; // [DEAR IMGUI] commmented double assignment - // make sure y never passes through a vertex of the shape y_frac = (float) STBTT_fmod(y, 1.0f); if (y_frac < 0.01f) y += 0.01f; else if (y_frac > 0.99f) y -= 0.01f; + + orig[0] = x; orig[1] = y; // test a ray from (-infinity,y) to (x,y) @@ -4319,7 +4496,7 @@ static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex int x1 = (int) verts[i ].x, y1 = (int) verts[i ].y; if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; - if (x_inter < x) + if (x_inter < x) winding += (y0 < y1) ? 1 : -1; } } @@ -4345,7 +4522,7 @@ static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex y1 = (int)verts[i ].y; if (y > STBTT_min(y0,y1) && y < STBTT_max(y0,y1) && x > STBTT_min(x0,x1)) { float x_inter = (y - y0) / (y1 - y0) * (x1-x0) + x0; - if (x_inter < x) + if (x_inter < x) winding += (y0 < y1) ? 1 : -1; } } else { @@ -4357,7 +4534,7 @@ static int stbtt__compute_crossings_x(float x, float y, int nverts, stbtt_vertex if (hits[1][0] < 0) winding += (hits[1][1] < 0 ? -1 : 1); } - } + } } } return winding; @@ -4371,35 +4548,35 @@ static float stbtt__cuberoot( float x ) return (float) STBTT_pow( x,1.0f/3.0f); } -// x^3 + c*x^2 + b*x + a = 0 +// x^3 + a*x^2 + b*x + c = 0 static int stbtt__solve_cubic(float a, float b, float c, float* r) { - float s = -a / 3; - float p = b - a*a / 3; - float q = a * (2*a*a - 9*b) / 27 + c; + float s = -a / 3; + float p = b - a*a / 3; + float q = a * (2*a*a - 9*b) / 27 + c; float p3 = p*p*p; - float d = q*q + 4*p3 / 27; - if (d >= 0) { - float z = (float) STBTT_sqrt(d); - float u = (-q + z) / 2; - float v = (-q - z) / 2; - u = stbtt__cuberoot(u); - v = stbtt__cuberoot(v); - r[0] = s + u + v; - return 1; - } else { - float u = (float) STBTT_sqrt(-p/3); - float v = (float) STBTT_acos(-STBTT_sqrt(-27/p3) * q / 2) / 3; // p3 must be negative, since d is negative - float m = (float) STBTT_cos(v); + float d = q*q + 4*p3 / 27; + if (d >= 0) { + float z = (float) STBTT_sqrt(d); + float u = (-q + z) / 2; + float v = (-q - z) / 2; + u = stbtt__cuberoot(u); + v = stbtt__cuberoot(v); + r[0] = s + u + v; + return 1; + } else { + float u = (float) STBTT_sqrt(-p/3); + float v = (float) STBTT_acos(-STBTT_sqrt(-27/p3) * q / 2) / 3; // p3 must be negative, since d is negative + float m = (float) STBTT_cos(v); float n = (float) STBTT_cos(v-3.141592/2)*1.732050808f; - r[0] = s + u * 2 * m; - r[1] = s - u * (m + n); - r[2] = s - u * (m - n); + r[0] = s + u * 2 * m; + r[1] = s - u * (m + n); + r[2] = s - u * (m - n); //STBTT_assert( STBTT_fabs(((r[0]+a)*r[0]+b)*r[0]+c) < 0.05f); // these asserts may not be safe at all scales, though they're in bezier t parameter units so maybe? //STBTT_assert( STBTT_fabs(((r[1]+a)*r[1]+b)*r[1]+c) < 0.05f); //STBTT_assert( STBTT_fabs(((r[2]+a)*r[2]+b)*r[2]+c) < 0.05f); - return 3; + return 3; } } @@ -4410,12 +4587,7 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc int w,h; unsigned char *data; - // if one scale is 0, use same scale for both - if (scale_x == 0) scale_x = scale_y; - if (scale_y == 0) { - if (scale_x == 0) return NULL; // if both scales are 0, return NULL - scale_y = scale_x; - } + if (scale == 0) return NULL; stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale, scale, 0.0f,0.0f, &ix0,&iy0,&ix1,&iy1); @@ -4438,7 +4610,7 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc // invert for y-downwards bitmaps scale_y = -scale_y; - + { int x,y,i,j; float *precompute; @@ -4481,18 +4653,17 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc for (i=0; i < num_verts; ++i) { float x0 = verts[i].x*scale_x, y0 = verts[i].y*scale_y; - // check against every point here rather than inside line/curve primitives -- @TODO: wrong if multiple 'moves' in a row produce a garbage point, and given culling, probably more efficient to do within line/curve - float dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); - if (dist2 < min_dist*min_dist) - min_dist = (float) STBTT_sqrt(dist2); - - if (verts[i].type == STBTT_vline) { + if (verts[i].type == STBTT_vline && precompute[i] != 0.0f) { float x1 = verts[i-1].x*scale_x, y1 = verts[i-1].y*scale_y; + float dist,dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + // coarse culling against bbox //if (sx > STBTT_min(x0,x1)-min_dist && sx < STBTT_max(x0,x1)+min_dist && // sy > STBTT_min(y0,y1)-min_dist && sy < STBTT_max(y0,y1)+min_dist) - float dist = (float) STBTT_fabs((x1-x0)*(y0-sy) - (y1-y0)*(x0-sx)) * precompute[i]; + dist = (float) STBTT_fabs((x1-x0)*(y0-sy) - (y1-y0)*(x0-sx)) * precompute[i]; STBTT_assert(i != 0); if (dist < min_dist) { // check position along line @@ -4519,7 +4690,8 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc float ax = x1-x0, ay = y1-y0; float bx = x0 - 2*x1 + x2, by = y0 - 2*y1 + y2; float mx = x0 - sx, my = y0 - sy; - float res[3],px,py,t,it; + float res[3] = {0.f,0.f,0.f}; + float px,py,t,it,dist2; float a_inv = precompute[i]; if (a_inv == 0.0) { // if a_inv is 0, it's 2nd degree so use quadratic formula float a = 3*(ax*bx + ay*by); @@ -4546,6 +4718,10 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc float d = (mx*ax+my*ay) * a_inv; num = stbtt__solve_cubic(b, c, d, res); } + dist2 = (x0-sx)*(x0-sx) + (y0-sy)*(y0-sy); + if (dist2 < min_dist*min_dist) + min_dist = (float) STBTT_sqrt(dist2); + if (num >= 1 && res[0] >= 0.0f && res[0] <= 1.0f) { t = res[0], it = 1.0f - t; px = it*it*x0 + 2*t*it*x1 + t*t*x2; @@ -4587,7 +4763,7 @@ STBTT_DEF unsigned char * stbtt_GetGlyphSDF(const stbtt_fontinfo *info, float sc STBTT_free(verts, info->userdata); } return data; -} +} STBTT_DEF unsigned char * stbtt_GetCodepointSDF(const stbtt_fontinfo *info, float scale, int codepoint, int padding, unsigned char onedge_value, float pixel_dist_scale, int *width, int *height, int *xoff, int *yoff) { @@ -4605,7 +4781,7 @@ STBTT_DEF void stbtt_FreeSDF(unsigned char *bitmap, void *userdata) // // check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string -static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, stbtt_int32 len1, stbtt_uint8 *s2, stbtt_int32 len2) +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, stbtt_int32 len1, stbtt_uint8 *s2, stbtt_int32 len2) { stbtt_int32 i=0; @@ -4644,7 +4820,7 @@ static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(stbtt_uint8 *s1, s return i; } -static int stbtt_CompareUTF8toUTF16_bigendian_internal(char *s1, int len1, char *s2, int len2) +static int stbtt_CompareUTF8toUTF16_bigendian_internal(char *s1, int len1, char *s2, int len2) { return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((stbtt_uint8*) s1, len1, (stbtt_uint8*) s2, len2); } @@ -4773,7 +4949,7 @@ STBTT_DEF int stbtt_BakeFontBitmap(const unsigned char *data, int offset, STBTT_DEF int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index) { - return stbtt_GetFontOffsetForIndex_internal((unsigned char *) data, index); + return stbtt_GetFontOffsetForIndex_internal((unsigned char *) data, index); } STBTT_DEF int stbtt_GetNumberOfFonts(const unsigned char *data) @@ -4805,6 +4981,12 @@ STBTT_DEF int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const // FULL VERSION HISTORY // +// 1.25 (2021-07-11) many fixes +// 1.24 (2020-02-05) fix warning +// 1.23 (2020-02-02) query SVG data for glyphs; query whole kerning table (but only kern not GPOS) +// 1.22 (2019-08-11) minimize missing-glyph duplication; fix kerning if both 'GPOS' and 'kern' are defined +// 1.21 (2019-02-25) fix warning +// 1.20 (2019-02-07) PackFontRange skips missing codepoints; GetScaleFontVMetrics() // 1.19 (2018-02-11) OpenType GPOS kerning (horizontal only), STBTT_fmod // 1.18 (2018-01-29) add missing function // 1.17 (2017-07-23) make more arguments const; doc fix @@ -4866,38 +5048,38 @@ This software is available under 2 licenses -- choose whichever you prefer. ------------------------------------------------------------------------------ ALTERNATIVE A - MIT License Copyright (c) 2017 Sean Barrett -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ ALTERNATIVE B - Public Domain (www.unlicense.org) This is free and unencumbered software released into the public domain. -Anyone is free to copy, modify, publish, use, compile, sell, or distribute this -software, either in source code form or as a compiled binary, for any purpose, +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. -In jurisdictions that recognize copyright laws, the author or authors of this -software dedicate any and all copyright interest in the software to the public -domain. We make this dedication for the benefit of the public at large and to -the detriment of our heirs and successors. We intend this dedication to be an -overt act of relinquishment in perpetuity of all present and future rights to +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------ */ diff --git a/lang/editor_pl.po b/lang/editor_pl.po new file mode 100644 index 00000000..58b30109 --- /dev/null +++ b/lang/editor_pl.po @@ -0,0 +1,5215 @@ +msgid "" +msgstr "" +"Language: pl_PL\n" +"Content-Type: text/plain; charset=UTF-8\n" + +# scenery editor: track tools + +msgid "Straights in the scenery" +msgstr "Proste w scenerii" + +msgid "Build" +msgstr "Budowa" + +msgid "Select Esc" +msgstr "Wybór Esc" + +msgid "Lay track L" +msgstr "Układaj tor L" + +msgid "Switch T" +msgstr "Rozjazd T" + +msgid "Edit and check (inspector)" +msgstr "Edycja i kontrola (inspektor)" + +msgid "Show the inspector" +msgstr "Pokaż inspektor" + +msgid "The curve can't be read from this path, edit it as a single path" +msgstr "Z tego odcinka nie da się odczytać łuku, edytuj go jako pojedynczy odcinek" + +msgid "end of the straight, or a vertex of a curve" +msgstr "koniec prostej albo wierzchołek łuku" + +msgid "next vertex of the curve" +msgstr "kolejny wierzchołek łuku" + +msgid "Laying track: LMB adds the points" +msgstr "Układanie toru: LPM dodaje punkty" + +msgid "Switch: drag to set its direction and side" +msgstr "Rozjazd: przeciągnij, aby ustawić kierunek i stronę" + +msgid "Switch: press and drag on the selected track" +msgstr "Rozjazd: wciśnij i przeciągnij na wybranym torze" + +msgid "Straight: shift around, Shift+click the points" +msgstr "Prosta: objazd, Shift+klik w punkty" + +msgid "New path from the free end" +msgstr "Nowy odcinek z wolnego końca" + +msgid "Straight: break, LMB on the straight" +msgstr "Prosta: załamanie, LPM na prostej" + +msgid "Straight: S-curve, LMB on the straight" +msgstr "Prosta: przesunięcie (esowanie), LPM na prostej" + +msgid "Straight: drag its ends or the middle" +msgstr "Prosta: przeciągaj końce lub środek" + +msgid "Select: LMB on a path" +msgstr "Wybór: LPM na odcinku" + +msgid "Curve: drag the vertices and the radius grips" +msgstr "Łuk: przeciągaj wierzchołki i uchwyty promienia" + +msgid "Switch: move and turn it with the gizmo" +msgstr "Rozjazd: przesuwaj i obracaj gizmem" + +msgid "Path: drag its points" +msgstr "Odcinek: przeciągaj jego punkty" + +msgid "Vertical profile: edit the grade line in the strip below" +msgstr "Profil podłużny: edytuj niweletę na pasku na dole" + +msgid "Speed check: LMB on a row shows the path" +msgstr "Kontrola prędkości: LPM na wierszu pokazuje odcinek" + +msgid "Infrastructure along the track" +msgstr "Infrastruktura wzdłuż toru" + +msgid "Track" +msgstr "Tor" + +msgid "LMB on a path in the view selects it. L lays new track, T puts a switch into the selected one." +msgstr "LPM na odcinku w widoku wybiera go. L układa nowy tor, T wstawia rozjazd w wybrany." + +msgid "New track" +msgstr "Nowy tor" + +msgid "Click the points the track goes through, the curves go at the inner points. Start or finish at a free end (magenta ring) to join it." +msgstr "Klikaj punkty, przez które ma iść tor, łuki powstaną w punktach pośrednich. Zacznij lub skończ na wolnym końcu (purpurowy pierścień), aby się z nim połączyć." + +msgid "Curve radius (m)" +msgstr "Promień łuku (m)" + +msgid "Speed (km/h)" +msgstr "Prędkość (km/h)" + +msgid "Transition curves and cant" +msgstr "Krzywe przejściowe i przechyłka" + +msgid "Longest path (m)" +msgstr "Najdłuższy odcinek (m)" + +msgid "Parameters of the selected track" +msgstr "Parametry wybranego toru" + +msgid "Switch template" +msgstr "Wzorzec rozjazdu" + +msgid "Press on the selected straight or curve where the switch starts and drag: along sets its direction, sideways its side" +msgstr "Wciśnij na wybranej prostej lub łuku w miejscu początku rozjazdu i przeciągnij: wzdłuż ustala kierunek, w bok stronę" + +msgid "Collect from the scenery" +msgstr "Zbierz ze scenerii" + +msgid "Select a path in the view, then build the route along the line, or set its start and end paths (switches are passed through)" +msgstr "Wybierz odcinek w widoku, potem zbuduj trasę wzdłuż linii albo wskaż jej odcinek początkowy i końcowy (rozjazdy są przechodzone)" + +msgid "Start: %s" +msgstr "Początek: %s" + +msgid "Route: %zu paths, %.2f m, %s ... %s" +msgstr "Trasa: %zu odcinków, %.2f m, %s ... %s" + +msgid "Vertical profile" +msgstr "Profil podłużny" + +msgid "Along the line from the selected path" +msgstr "Wzdłuż linii od wybranego odcinka" + +msgid "m each way" +msgstr "m w każdą stronę" + +msgid "Start: selected path" +msgstr "Początek: wybrany odcinek" + +msgid "End: selected path" +msgstr "Koniec: wybrany odcinek" + +msgid "Chainage of the start (m)" +msgstr "Kilometraż początku (m)" + +msgid "Design speed (km/h)" +msgstr "Prędkość projektowa (km/h)" + +msgid "Exaggeration" +msgstr "Przewyższenie" + +msgid "Shown:" +msgstr "Widoczne:" + +msgid "Terrain" +msgstr "Teren" + +msgid "Plan curvature" +msgstr "Krzywizna w planie" + +msgid "Fit the view" +msgstr "Dopasuj widok" + +msgid "Recognize from the track" +msgstr "Rozpoznaj z toru" + +msgid "Apply to the scenery" +msgstr "Zastosuj w scenerii" + +msgid "LMB on a path selects it, the inspector on the right shows what can be done with it" +msgstr "LPM na odcinku wybiera go, inspektor po prawej pokazuje, co można z nim zrobić" + +msgid "Lays new track through the clicked points, on the ground or from a free end of a track" +msgstr "Układa nowy tor przez klikane punkty, na terenie albo od wolnego końca toru" + +msgid "Puts a switch into the selected straight or curve: choose the template, then press and drag on the track" +msgstr "Wstawia rozjazd w wybraną prostą lub łuk: wybierz wzorzec, potem wciśnij i przeciągnij na torze" + +msgid "Keeps the start and the direction of the switch, takes the shape of the chosen template" +msgstr "Zachowuje początek i kierunek rozjazdu, przyjmuje kształt wybranego wzorca" + +msgid "Mirrors the switch: the diverging track goes to the other side" +msgstr "Odbija rozjazd: tor zwrotny przechodzi na drugą stronę" + +msgid "Speed limit of the path for the AI drivers and the timetable, -1: no limit of its own" +msgstr "Ograniczenie prędkości odcinka dla maszynistów AI i rozkładu, -1: bez własnego ograniczenia" + +msgid "Friction coefficient of the rails, 0.15 for the dry rails; lower values make the wheels slip" +msgstr "Współczynnik tarcia szyn, 0.15 dla suchych; mniejsze wartości powodują poślizg kół" + +msgid "Distance between the rail joints in metres, sets the rhythm of the wheel clatter; -1: the default" +msgstr "Odległość między stykami szyn w metrach, ustala rytm stukotu kół; -1: domyślna" + +msgid "Quality of the track, 20 is the nominal one; lower values give a rougher ride" +msgstr "Jakość toru, 20 to nominalna; mniejsze wartości dają mniej spokojną jazdę" + +msgid "0: track in order; any value adds jolts, 128: the vehicles derail on the path" +msgstr "0: tor sprawny; inne wartości dodają szarpnięcia, 128: pojazdy wykolejają się na odcinku" + +msgid "Surroundings of the path, they affect the sound of the passing vehicles" +msgstr "Otoczenie odcinka, wpływa na dźwięk przejeżdżających pojazdów" + +msgid "Gauge of the track (1.435 m for the standard gauge), width of the surface for the roads" +msgstr "Rozstaw toru (1.435 m dla normalnotorowego), szerokość nawierzchni dla dróg" + +msgid "Length in metres after which the texture of the rails and the trackbed repeats" +msgstr "Długość w metrach, po której powtarza się tekstura szyn i podsypki" + +msgid "Height of the trackbed edge below the rail top, in metres" +msgstr "Wysokość krawędzi podsypki poniżej główki szyny, w metrach" + +msgid "Width of the trackbed outside of the rail, in metres" +msgstr "Szerokość podsypki na zewnątrz szyny, w metrach" + +msgid "Horizontal reach of the slope at the outer edge of the trackbed, in metres" +msgstr "Poziomy zasięg skarpy przy zewnętrznej krawędzi podsypki, w metrach" + +msgid "Names of the track circuits (isolated) the path belongs to, separated by spaces.\nA name which doesn't exist yet makes a new circuit. Changes on leaving the field" +msgstr "Nazwy obwodów torowych (isolated), do których należy odcinek, oddzielone spacjami.\nNieistniejąca nazwa tworzy nowy obwód. Zmiana po opuszczeniu pola" + +msgid "State of the overhead line over the path: the trains coast through a section with no current,\nor lower the pantographs and keep to the speed limit given" +msgstr "Stan sieci trakcyjnej nad odcinkiem: pociągi przejeżdżają bez prądu przez odcinek beznapięciowy\nalbo opuszczają pantografy i trzymają się podanego ograniczenia" + +msgid "Repeats a model along the path, the sleepers in place of the drawn trackbed" +msgstr "Powtarza model wzdłuż odcinka, podkłady zamiast rysowanej podsypki" + +msgid "Shift of each model: sideways, along the path, up" +msgstr "Przesunięcie każdego modelu: w bok, wzdłuż odcinka, w górę" + +msgid "event0: a vehicle with a crew stands on the path, event1: runs along it towards its start, event2: towards its end.\neventall0/1/2: the same for any vehicle" +msgstr "event0: pojazd z obsadą stoi na odcinku, event1: jedzie po nim w stronę początku, event2: w stronę końca.\neventall0/1/2: to samo dla dowolnego pojazdu" + +msgid "Name of the event to add to the chosen list. An event which doesn't exist is kept, and reported" +msgstr "Nazwa zdarzenia do dodania do wybranej listy. Nieistniejące zdarzenie zostaje zachowane i zgłoszone" + +msgid "A dragged free end within this distance of another free end joins it on release" +msgstr "Przeciągany wolny koniec w tej odległości od innego wolnego końca łączy się z nim po puszczeniu" + +msgid "On joining, the end also takes over the direction of the other path, so the joint has no kink" +msgstr "Przy łączeniu koniec przejmuje też kierunek drugiego odcinka, żeby styk nie miał załamania" + +msgid "Dragging a joined end, or moving and turning the whole path or switch,\nmoves the ends of the neighbouring paths with it" +msgstr "Przeciąganie połączonego końca albo przesuwanie i obracanie całego odcinka lub rozjazdu\nprzesuwa razem z nim końce sąsiednich odcinków" + +msgid "Control vector of the start, relative to the start point; zero for a straight" +msgstr "Wektor sterujący początku, względem punktu początkowego; zero dla prostej" + +msgid "Control vector of the end, relative to the end point; zero for a straight" +msgstr "Wektor sterujący końca, względem punktu końcowego; zero dla prostej" + +msgid "Cant at the start and at the end as the roll angle of the track" +msgstr "Przechyłka na początku i na końcu jako kąt przechylenia toru" + +msgid "Radius stored with the path in the scenery; the shape itself comes from the control vectors" +msgstr "Promień zapisany z odcinkiem w scenerii; sam kształt wynika z wektorów sterujących" + +msgid "Removes the control vectors and the radius, the path becomes a straight between its ends" +msgstr "Usuwa wektory sterujące i promień, odcinek staje się prostą między swoimi końcami" + +msgid "The line through the selected path, from switch to switch or to a free end:\na vertex for each curve, the straights between them follow the vertices" +msgstr "Linia przez wybrany odcinek, od rozjazdu do rozjazdu lub do wolnego końca:\nwierzchołek dla każdego łuku, proste między nimi podążają za wierzchołkami" + +msgid "Only the curve under the selected path, between the straights adjoining it,\nwhich it takes over or gives back as its radius changes" +msgstr "Tylko łuk pod wybranym odcinkiem, między przylegającymi prostymi,\nktóre przejmuje lub oddaje przy zmianie promienia" + +msgid "Speed the cant, the transitions and the recommended radius are designed for" +msgstr "Prędkość, dla której projektowane są przechyłka, krzywe przejściowe i zalecany promień" + +msgid "Cubic parabola: the usual one on the Polish lines; clothoid: curvature growing linearly with the length" +msgstr "Parabola sześcienna: zwykła na polskich liniach; klotoida: krzywizna rośnie liniowo z długością" + +msgid "Curve made of two arcs of different radii, joined with a transition" +msgstr "Łuk z dwóch łuków o różnych promieniach, połączonych krzywą przejściową" + +msgid "Height of the outer rail above the inner one" +msgstr "Wysokość szyny zewnętrznej ponad wewnętrzną" + +msgid "For the loops: the curve turns by more than a half circle" +msgstr "Dla pętli: łuk skręca o więcej niż pół okręgu" + +msgid "Cant and transitions recommended for the design speed and the radius" +msgstr "Przechyłka i krzywe przejściowe zalecane dla prędkości projektowej i promienia" + +msgid "Largest change of the direction between the paths of one straight" +msgstr "Największa zmiana kierunku między odcinkami jednej prostej" + +msgid "Largest distance of the paths from the common axis of the straight" +msgstr "Największa odległość odcinków od wspólnej osi prostej" + +msgid "Curves of a larger radius count as straight" +msgstr "Łuki o większym promieniu liczą się jako proste" + +msgid "Shorter straights aren't listed" +msgstr "Krótsze proste nie są wypisywane" + +msgid "Radius of the curves made by the break and the S-curve; automatic: 1.5 times the minimum radius for the speed of the line" +msgstr "Promień łuków tworzonych przez załamanie i esowanie; automatyczny: 1.5 raza promień minimalny dla prędkości linii" + +msgid "Lengths of the transition curves from the speed of the paths and the limits of the curve design" +msgstr "Długości krzywych przejściowych z prędkości odcinków i ograniczeń projektowania łuków" + +msgid "Drag to change, Ctrl+click to type the value" +msgstr "Przeciągnij, aby zmienić, Ctrl+klik, aby wpisać wartość" + +msgid "Speed limit of the new paths, and the design speed of the transition curves and of the cant" +msgstr "Ograniczenie prędkości nowych odcinków oraz prędkość projektowa krzywych przejściowych i przechyłki" + +msgid "Lengths and the cant recommended for the speed and the radius; off: plain circular arcs with no cant" +msgstr "Długości i przechyłka zalecane dla prędkości i promienia; wyłączone: zwykłe łuki kołowe bez przechyłki" + +msgid "Takes the textures and the other parameters from the selected track.\nA track started or finished at a free end always takes them from the track it joins" +msgstr "Bierze tekstury i pozostałe parametry z wybranego toru.\nTor zaczęty lub zakończony na wolnym końcu zawsze bierze je z toru, do którego dochodzi" + +msgid "Looks for the switches of the scenery, to use their shapes as the templates" +msgstr "Szuka rozjazdów w scenerii, aby użyć ich kształtów jako wzorców" + +msgid "start of the new track, on the ground or at a free end" +msgstr "początek nowego toru, na terenie lub na wolnym końcu" + +msgid "stop laying" +msgstr "zakończ układanie" + +msgid "next point, a curve goes at it" +msgstr "następny punkt, w nim powstanie łuk" + +msgid "join and lay" +msgstr "połącz i ułóż" + +msgid "lay" +msgstr "ułóż" + +msgid "take back the point" +msgstr "cofnij punkt" + +msgid "cancel" +msgstr "anuluj" + +msgid "curve through the cursor, along the end: straight" +msgstr "łuk przez kursor, wzdłuż końca: prosta" + +msgid "straight" +msgstr "prosta" + +msgid "build" +msgstr "zbuduj" + +msgid "direction of the switch" +msgstr "kierunek rozjazdu" + +msgid "side of the diverging track" +msgstr "strona toru zwrotnego" + +msgid "on the selected straight or curve where the switch starts" +msgstr "na wybranej prostej lub łuku, gdzie zaczyna się rozjazd" + +msgid "put the switch away" +msgstr "odłóż rozjazd" + +msgid "select a path" +msgstr "wybierz odcinek" + +msgid "drag the points of the grade line, wheel zooms" +msgstr "przeciągaj punkty niwelety, kółko przybliża" + +msgid "show the path" +msgstr "pokaż odcinek" + +msgid "select a straight or a curve" +msgstr "wybierz prostą lub łuk" + +msgid "the ends or the middle" +msgstr "końce lub środek" + +msgid "break" +msgstr "załamanie" + +msgid "shift around" +msgstr "objazd" + +msgid "add to the set" +msgstr "dodaj do zestawu" + +msgid "new path" +msgstr "nowy odcinek" + +msgid "delete" +msgstr "usuń" + +msgid "move the switch" +msgstr "przesuń rozjazd" + +msgid "turn" +msgstr "obróć" + +msgid "leave the main track" +msgstr "zostaw tor zasadniczy" + +msgid "select a curve" +msgstr "wybierz łuk" + +msgid "vertex" +msgstr "wierzchołek" + +msgid "radius" +msgstr "promień" + +msgid "apply" +msgstr "zastosuj" + +msgid "release the vertex" +msgstr "puść wierzchołek" + +msgid "select a path or a point handle" +msgstr "wybierz odcinek lub uchwyt punktu" + +msgid "release the point" +msgstr "puść punkt" + +msgid "split the path under the cursor" +msgstr "podziel odcinek pod kursorem" + +msgid "top view" +msgstr "widok z góry" + +msgid "undo" +msgstr "cofnij" + +msgid "redo" +msgstr "ponów" + +msgid "lay, switch, straight, curve, profile, speed, infra" +msgstr "układanie, rozjazd, prosta, łuk, profil, prędkość, infrastruktura" + +msgid "Straight G" +msgstr "Prosta G" + +msgid "The whole straight through the selected path" +msgstr "Cała prosta przez wybrany odcinek" + +msgid "Curve C" +msgstr "Łuk C" + +msgid "Curves of the line through the selected path: radii, transitions, cant" +msgstr "Łuki linii przez wybrany odcinek: promienie, krzywe przejściowe, przechyłka" + +msgid "Path" +msgstr "Odcinek" + +msgid "Points, control vectors and parameters of the selected path" +msgstr "Punkty, wektory sterujące i parametry wybranego odcinka" + +msgid "Profile P" +msgstr "Profil P" + +msgid "Vertical profile (grade line) along the line" +msgstr "Profil podłużny (niweleta) wzdłuż linii" + +msgid "Speed V" +msgstr "Prędkość V" + +msgid "Speed limits of the paths against what their geometry allows" +msgstr "Ograniczenia prędkości odcinków wobec tego, na co pozwala ich geometria" + +msgid "Infra I" +msgstr "Infra I" + +msgid "Infrastructure bound to the track, which follows its changes" +msgstr "Infrastruktura powiązana z torem, podążająca za jego zmianami" + +msgid "Straight" +msgstr "Prosta" + +msgid "The whole straight through the selected path: drag its ends or the middle, break it, shift it (G)" +msgstr "Cała prosta przez wybrany odcinek: przeciągaj końce lub środek, załamuj, przesuwaj (G)" + +msgid "Curve" +msgstr "Łuk" + +msgid "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves (C)" +msgstr "Linia przez odcinek, od rozjazdu do rozjazdu: wierzchołki z promieniami, krzywymi przejściowymi i przechyłką łuków (C)" + +msgid "Switch" +msgstr "Rozjazd" + +msgid "Geometry of the switch from a template, side of the diverging track" +msgstr "Geometria rozjazdu ze wzorca, strona toru zwrotnego" + +msgid "Profile" +msgstr "Profil" + +msgid "Vertical profile (grade line) along the line (P)" +msgstr "Profil podłużny (niweleta) wzdłuż linii (P)" + +msgid "Speed" +msgstr "Prędkość" + +msgid "Speed limits of the paths against the speed their geometry allows (V)" +msgstr "Ograniczenia prędkości odcinków wobec prędkości dopuszczalnej przez geometrię (V)" + +msgid "Infra" +msgstr "Infra" + +msgid "Infrastructure along the track, bound to follow its changes (I)" +msgstr "Infrastruktura wzdłuż toru, powiązana tak, by podążała za jego zmianami (I)" + +msgid "Select a path in the view first" +msgstr "Najpierw wybierz odcinek w widoku" + +msgid "The selected path isn't a switch" +msgstr "Wybrany odcinek nie jest rozjazdem" + +msgid "The selected path isn't straight" +msgstr "Wybrany odcinek nie jest prosty" + +msgid "Switches and paths of other kinds aren't a part of a line" +msgstr "Rozjazdy i odcinki innych rodzajów nie są częścią linii" + +msgid "The selected path is straight, the single curve mode needs a curve" +msgstr "Wybrany odcinek jest prosty, tryb pojedynczego łuku wymaga łuku" + +msgid "normal" +msgstr "zwykły" + +msgid "switch" +msgstr "rozjazd" + +msgid "cross" +msgstr "skrzyżowanie" + +msgid "turntable" +msgstr "obrotnica" + +msgid "tributary" +msgstr "dopływ" + +# scenery editor: vertical profile + +msgid "Point" +msgstr "Punkt" + +msgid "Points" +msgstr "Punkty" + +msgid "Remarks" +msgstr "Uwagi" + +msgid "Keeps the elevation of the joint and takes over the grade of the adjoining track,\nwith a vertical curve which starts right at the joint" +msgstr "Zachowuje wysokość styku i przejmuje pochylenie przylegającego toru,\nz łukiem pionowym zaczynającym się zaraz na styku" + +msgid "Join the adjoining track at the start" +msgstr "Dowiąż do przylegającego toru na początku" + +msgid "Join the adjoining track at the end" +msgstr "Dowiąż do przylegającego toru na końcu" + +msgid "Limits" +msgstr "Ograniczenia" + +msgid "Automatic R at all points" +msgstr "Automatyczny R we wszystkich punktach" + +msgid "Required R for %.0f km/h: %.0f m" +msgstr "Wymagany R dla %.0f km/h: %.0f m" + +msgid "%d errors, %d warnings: see the Remarks in the strip" +msgstr "Błędy: %d, ostrzeżenia: %d - zobacz Uwagi na dolnym pasku" + +msgid "%d warnings: see the Remarks in the strip" +msgstr "Ostrzeżenia: %d - zobacz Uwagi na dolnym pasku" + +msgid "The grade line meets the limits" +msgstr "Niweleta spełnia ograniczenia" + +msgid "Largest departure from the track: %.3f m" +msgstr "Największa odchyłka od toru: %.3f m" + +msgid "Sets the points of the grade line from the existing track" +msgstr "Ustawia punkty niwelety na podstawie istniejącego toru" + +msgid "Sets the heights of the track along the route to the grade line, Ctrl+Z takes it back" +msgstr "Ustawia wysokości toru wzdłuż trasy według niwelety, Ctrl+Z cofa" + +msgid "LMB on a remark shows its place" +msgstr "LPM na uwadze pokazuje jej miejsce" + +msgid "sag" +msgstr "wklęsły" + +msgid "crest" +msgstr "wypukły" + +msgid "No point selected. Click a point of the grade line in the plot, or double click the line to add one." +msgstr "Nie wybrano punktu. Kliknij punkt niwelety na wykresie albo kliknij dwukrotnie linię, aby dodać nowy." + +msgid "Point %d: joint with the adjoining track" +msgstr "Punkt %d: styk z przylegającym torem" + +msgid "Grade %+.2f -> %+.2f per mille (%s)" +msgstr "Pochylenie %+.2f -> %+.2f promila (%s)" + +msgid "Placed automatically, moves with the neighbouring point; untick the joining in the inspector to edit the end freely" +msgstr "Ustawiany automatycznie, porusza się z sąsiednim punktem; odznacz dowiązanie w inspektorze, aby swobodnie edytować koniec" + +msgid "Point %d" +msgstr "Punkt %d" + +msgid ", joined to the adjoining track" +msgstr ", dowiązany do przylegającego toru" + +msgid ", end of the route" +msgstr ", koniec trasy" + +msgid "Elevation %.3f m" +msgstr "Wysokość %.3f m" + +msgid "auto" +msgstr "auto" + +msgid "R follows the limits: the required radius where the change of grade needs a curve; 0: no curve" +msgstr "R wynika z ograniczeń: wymagany promień tam, gdzie zmiana pochylenia wymaga łuku; 0: bez łuku" + +msgid "Change of grade %+.2f per mille, %s" +msgstr "Zmiana pochylenia %+.2f promila, %s" + +msgid "Tangent T %.2f m, sagitta f %.3f m" +msgstr "Styczna T %.2f m, strzałka f %.3f m" + +msgid "Add a point after" +msgstr "Dodaj punkt za" + +msgid "Remove Del" +msgstr "Usuń Del" + +msgid " joint" +msgstr " styk" + +msgid "heights x%.0f" +msgstr "wysokości x%.0f" + +msgid "Exaggeration of the heights against the lengths, also Ctrl+wheel over the plot" +msgstr "Przewyższenie wysokości względem długości, także Ctrl+kółko nad wykresem" + +msgid "Fit" +msgstr "Dopasuj" + +msgid "Shows the whole route" +msgstr "Pokazuje całą trasę" + +msgid "before %+.2f per mille" +msgstr "przed %+.2f promila" + +msgid "after %+.2f per mille" +msgstr "za %+.2f promila" + +msgid "plan: curvature (up: left)" +msgstr "plan: krzywizna (w górę: w lewo)" + +msgid "Grade line %.3f m, %+.2f per mille" +msgstr "Niweleta %.3f m, %+.2f promila" + +msgid "Track %.3f m (%+.3f), %+.2f per mille" +msgstr "Tor %.3f m (%+.3f), %+.2f promila" + +msgid "Terrain %.3f m, %s %.2f m" +msgstr "Teren %.3f m, %s %.2f m" + +msgid "fill" +msgstr "nasyp" + +msgid "cut" +msgstr "wykop" + +msgid " (switch, tilted as a whole with its branch)" +msgstr " (rozjazd, pochylany w całości z odgałęzieniem)" + +msgid "Maximum grade (per mille)" +msgstr "Największe pochylenie (promile)" + +msgid "Maximum grade over switches (per mille)" +msgstr "Największe pochylenie na rozjazdach (promile)" + +msgid "Vertical curve R >= k*V^2, k" +msgstr "Łuk pionowy R >= k*V^2, k" + +msgid "Vertical curve R min (m)" +msgstr "Najmniejszy R łuku pionowego (m)" + +msgid "Vertical curve needed above a change of (per mille)" +msgstr "Łuk pionowy potrzebny powyżej zmiany o (promile)" + +msgid "Constant grade between curves at least (m)" +msgstr "Stałe pochylenie między łukami co najmniej (m)" + +msgid "Tolerance at fixed points (m)" +msgstr "Tolerancja w punktach stałych (m)" + +msgid "Chainage (m)" +msgstr "Kilometraż (m)" + +msgid "Elevation (m)" +msgstr "Wysokość (m)" + +msgid "Grade before (per mille)" +msgstr "Pochylenie przed (promile)" + +msgid "Grade after (per mille)" +msgstr "Pochylenie za (promile)" + +msgid "Vertical curve R (m)" +msgstr "R łuku pionowego (m)" + +msgid "Grade line" +msgstr "Niweleta" + +msgid "Tangents" +msgstr "Styczne" + +msgid "Switches" +msgstr "Rozjazdy" + +msgid "wheel: zoom Ctrl+wheel: heights RMB/MMB drag: pan double click on the line: add a point" +msgstr "kółko: przybliżenie Ctrl+kółko: wysokości PPM/ŚPM: przesuwanie dwuklik na linii: nowy punkt" + +msgid "Ctrl: only the height Shift: keep the grade before" +msgstr "Ctrl: tylko wysokość Shift: zachowaj pochylenie przed" + +msgid "drag up or down: radius of the vertical curve" +msgstr "przeciągnij w górę lub w dół: promień łuku pionowego" + +msgid "drag: move the point Ctrl+drag: only the height Shift+drag: keep the grade before Del: remove" +msgstr "przeciągnij: przesuń punkt Ctrl: tylko wysokość Shift: zachowaj pochylenie przed Del: usuń" + +msgid "double click: add a point here" +msgstr "dwuklik: dodaj tu punkt" + +# scenery editor: track modes + +msgid "The selected path isn't straight: C edits it as a curve, Esc goes back to Select" +msgstr "Wybrany odcinek nie jest prosty: C edytuje go jako łuk, Esc wraca do Wyboru" + +msgid "Switches aren't a part of a line: T edits the switch, Esc goes back to Select" +msgstr "Rozjazdy nie są częścią linii: T edytuje rozjazd, Esc wraca do Wyboru" + +msgid "The track tools are in the Track window on the right: choose the mode at its top, or press its key in the view." +msgstr "Narzędzia torów są w oknie Tor po prawej: wybierz tryb u jego góry albo naciśnij jego klawisz w widoku." + +msgid "Show the Track window" +msgstr "Pokaż okno Tor" + +msgid "Tracks" +msgstr "Tory" + +msgid "Track window" +msgstr "Okno Tor" + +msgid "The selected path isn't straight: LMB on a straight" +msgstr "Wybrany odcinek nie jest prosty: LPM na prostej" + +msgid "Switches aren't a part of a line: LMB on a plain path" +msgstr "Rozjazdy nie są częścią linii: LPM na zwykłym odcinku" + +msgid "Select: drag the points of the path" +msgstr "Wybór: przeciągaj punkty odcinka" + +msgid "LMB on a path in the view selects it" +msgstr "LPM na odcinku w widoku wybiera go" + +msgid "Selected switch" +msgstr "Wybrany rozjazd" + +msgid "back to Select" +msgstr "powrót do Wyboru" + +msgid "select the straight or the curve for the switch" +msgstr "wybierz prostą lub łuk dla rozjazdu" + +msgid "move the selected switch" +msgstr "przesuń wybrany rozjazd" + +msgid "on the selected track where the switch starts" +msgstr "na wybranym torze, gdzie zaczyna się rozjazd" + +msgid "select another track" +msgstr "wybierz inny tor" + +msgid "modes: lay, switch, straight, curve, profile, speed, infra" +msgstr "tryby: układanie, rozjazd, prosta, łuk, profil, prędkość, infrastruktura" + +msgid "Select" +msgstr "Wybór" + +msgid "LMB on a path selects it: its points, control vectors and parameters" +msgstr "LPM na odcinku wybiera go: jego punkty, wektory sterujące i parametry" + +msgid "Lay track" +msgstr "Układanie" + +msgid "Puts switches into the selected straight or curve, edits the geometry of the selected switch" +msgstr "Wstawia rozjazdy w wybraną prostą lub łuk, edytuje geometrię wybranego rozjazdu" + +msgid "The whole straight through the selected path: drag its ends or the middle, break it, shift it" +msgstr "Cała prosta przez wybrany odcinek: przeciągaj końce lub środek, załamuj, przesuwaj" + +msgid "The line through the path, switch to switch: vertices with the radii, transitions and cant of the curves" +msgstr "Linia przez odcinek, od rozjazdu do rozjazdu: wierzchołki z promieniami, krzywymi przejściowymi i przechyłką łuków" + +msgid "Speed limits of the paths against the speed their geometry allows" +msgstr "Ograniczenia prędkości odcinków wobec prędkości dopuszczalnej przez geometrię" + +msgid "Infrastructure along the track, bound to follow its changes" +msgstr "Infrastruktura wzdłuż toru, powiązana tak, by podążała za jego zmianami" + +msgid "Track editor" +msgstr "Edytor torów" + +msgid "Structure gauge" +msgstr "Skrajnia" + +msgid "Checks which models enter the structure gauge of the tracks and the clearance over the roads (skrajnia budowli)" +msgstr "Sprawdza, które modele wchodzą w skrajnię budowli torów i w skrajnię nad drogami" + +msgid "Vertical profile: edit the grade line in the profile window" +msgstr "Profil podłużny: edytuj niweletę w oknie profilu" + +msgid "The geometry of this switch is degenerate, it can't be edited here" +msgstr "Geometria tego rozjazdu jest zdegenerowana, nie da się jej tu edytować" + +msgid "Length %.3f m, diverging %s, end offset %.3f m" +msgstr "Długość %.3f m, tor zwrotny %s, odsunięcie końca %.3f m" + +msgid "left" +msgstr "w lewo" + +msgid "right" +msgstr "w prawo" + +msgid "Angle 1:%.2f (%.4f deg), R in the entry %.0f m" +msgstr "Skos 1:%.2f (%.4f°), R na wejściu %.0f m" + +msgid "Too far from the new ends of the switch, left unjoined: " +msgstr "Za daleko od nowych końców rozjazdu, zostawione niepołączone: " + +msgid "Replace the geometry with the template" +msgstr "Zastąp geometrię wzorcem" + +msgid "Flip the side" +msgstr "Odwróć stronę" + +msgid "Velocity (km/h, -1: none)" +msgstr "Prędkość (km/h, -1: brak)" + +msgid "Ride and sound" +msgstr "Jazda i dźwięk" + +msgid "Friction" +msgstr "Tarcie" + +msgid "Sound distance" +msgstr "Odstęp stuków" + +msgid "Quality" +msgstr "Jakość" + +msgid "Damage flags" +msgstr "Flagi uszkodzeń" + +msgid "Width" +msgstr "Szerokość" + +msgid "Textures and trackbed" +msgstr "Tekstury i podsypka" + +msgid "The texture changes on Enter or leaving the field, \"none\" removes it" +msgstr "Tekstura zmienia się po Enter lub wyjściu z pola, \"none\" ją usuwa" + +msgid "Texture length" +msgstr "Długość tekstury" + +msgid "Trackbed height" +msgstr "Wysokość podsypki" + +msgid "Trackbed width" +msgstr "Szerokość podsypki" + +msgid "Trackbed slope" +msgstr "Skarpa podsypki" + +msgid "Track circuit, overhead line, sleepers, events" +msgstr "Odcinek izolowany, sieć, podkłady, zdarzenia" + +msgid "Track circuits" +msgstr "Odcinki izolowane" + +msgid "The track circuits can't change while vehicles stand on the path" +msgstr "Odcinki izolowane nie mogą się zmienić, gdy na odcinku stoją pojazdy" + +msgid "Speed with the pantographs lowered (km/h)" +msgstr "Prędkość z opuszczonymi pantografami (km/h)" + +msgid "Sleeper models" +msgstr "Modele podkładów" + +msgid "Give the sleeper model first" +msgstr "Najpierw podaj model podkładu" + +msgid "Sleeper model" +msgstr "Model podkładu" + +msgid "Path of the .e3d model, as in the scenery file" +msgstr "Ścieżka modelu .e3d, jak w pliku scenerii" + +msgid "Sleeper skin" +msgstr "Skóra podkładu" + +msgid "Replaceable skin of the model, \"none\": the one of the model" +msgstr "Wymienna skóra modelu, \"none\": ta z modelu" + +msgid "Sleeper spacing (m)" +msgstr "Rozstaw podkładów (m)" + +msgid "Distance between the sleepers along the path" +msgstr "Odległość między podkładami wzdłuż odcinka" + +msgid "Sleeper offset (m)" +msgstr "Przesunięcie podkładu (m)" + +msgid "Trackbed shift (m)" +msgstr "Przesunięcie podsypki (m)" + +msgid "Vertical shift of the drawn trackbed under the sleepers, negative lowers it" +msgstr "Pionowe przesunięcie rysowanej podsypki pod podkładami, ujemne ją obniża" + +msgid "Events" +msgstr "Zdarzenia" + +msgid "%s %s (by the name of the path)" +msgstr "%s %s (po nazwie odcinka)" + +msgid "%s %s (no such event)" +msgstr "%s %s (nie ma takiego zdarzenia)" + +msgid "Add" +msgstr "Dodaj" + +msgid "The events can't change while vehicles stand on the path" +msgstr "Zdarzenia nie mogą się zmienić, gdy na odcinku stoją pojazdy" + +msgid "No such events: %s" +msgstr "Nie ma takich zdarzeń: %s" + +msgid "No path selected" +msgstr "Nie zaznaczono odcinka" + +msgid "Editing of this path type isn't supported" +msgstr "Edycja tego typu odcinka nie jest obsługiwana" + +msgid "Geometry locked: %s" +msgstr "Geometria zablokowana: %s" + +msgid "Geometry" +msgstr "Geometria" + +msgid "Path %d" +msgstr "Odcinek %d" + +msgid "(+) joined with another track, (-) a free end" +msgstr "(+) połączony z innym torem, (-) wolny koniec" + +msgid "Start (%.2f, %.2f, %.2f) End (%.2f, %.2f, %.2f)" +msgstr "Początek (%.2f, %.2f, %.2f) Koniec (%.2f, %.2f, %.2f)" + +msgid "Drag to move, Ctrl+click to type the value" +msgstr "Przeciągnij, by zmienić, Ctrl+klik, by wpisać wartość" + +msgid "vector##point" +msgstr "wektor##point" + +msgid "Control vector, relative to its end; zero for a straight" +msgstr "Wektor kontrolny, względem swojego końca; zero dla prostej" + +msgid "Cant and radius" +msgstr "Przechyłka i promień" + +msgid "Roll (deg)" +msgstr "Przechył (°)" + +msgid "Radius" +msgstr "Promień" + +msgid "Straighten" +msgstr "Wyprostuj" + +msgid "Snapping of the ends" +msgstr "Przyciąganie końców" + +msgid "Snap radius (m)" +msgstr "Zasięg przyciągania (m)" + +msgid "Align with snapped path" +msgstr "Dopasuj kierunek do przyciągniętego" + +msgid "Drag connected ends" +msgstr "Ciągnij połączone końce" + +msgid "The fragment runs through %zu switches along their main tracks. They move as a whole with the line, so they have to fall on its straights" +msgstr "Fragment przechodzi przez %zu rozjazdów torem zasadniczym. Przesuwają się w całości z linią, więc muszą wypaść na jej prostych" + +msgid "Curve of " +msgstr "Łuk z " + +msgid " paths, turning " +msgstr " odcinków, kąt zwrotu " + +msgid " deg" +msgstr "°" + +msgid "Line of %zu paths, %.0f m, %zu vertices" +msgstr "Linia z %zu odcinków, %.0f m, %zu wierzchołków" + +msgid "The fragment changed since it was selected, select it again" +msgstr "Fragment zmienił się od zaznaczenia, zaznacz go ponownie" + +msgid "The adjoining straights can't be cut at the ends of the curve" +msgstr "Przyległych prostych nie da się przyciąć na końcach łuku" + +msgid "Re-laid " +msgstr "Ułożono na nowo " + +msgid " paths" +msgstr " odcinków" + +msgid ", added " +msgstr ", dodano " + +msgid "The switch " +msgstr "Rozjazd " + +msgid " doesn't fit on the designed line" +msgstr " nie mieści się na projektowanej linii" + +msgid "The switch %s would leave the designed line by %.2f m at its other end: it lies in a curve of the design. Move the vertices so that the switch falls on a straight" +msgstr "Rozjazd %s odszedłby od projektowanej linii o %.2f m na drugim końcu: leży w łuku projektu. Przesuń wierzchołki tak, by rozjazd wypadł na prostej" + +msgid "The paths between the switches can't be read as a chain" +msgstr "Odcinków między rozjazdami nie da się odczytać jako ciągu" + +msgid "The designed line between the switches is too short for the paths there" +msgstr "Projektowana linia między rozjazdami jest za krótka na tamtejsze odcinki" + +msgid "Whole line" +msgstr "Cała linia" + +msgid "Single curve" +msgstr "Pojedynczy łuk" + +msgid "%zu paths, %.2f m" +msgstr "%zu odcinków, %.2f m" + +msgid "Reset design" +msgstr "Resetuj projekt" + +msgid "Reads the curves of the line from the scenery again, dropping the changes not applied" +msgstr "Odczytuje łuki linii ze scenerii od nowa, porzucając niezastosowane zmiany" + +msgid "Design speed and limits" +msgstr "Prędkość projektowa i normy" + +msgid "First path: %s" +msgstr "Pierwszy odcinek: %s" + +msgid "Use selected##routefrom" +msgstr "Użyj zaznaczonego##routefrom" + +msgid "Last path: %s" +msgstr "Ostatni odcinek: %s" + +msgid "Use selected##routeto" +msgstr "Użyj zaznaczonego##routeto" + +msgid "Rail axes distance (mm)" +msgstr "Rozstaw osi szyn (mm)" + +msgid "Unbalanced acceleration (m/s2)" +msgstr "Przyspieszenie niezrównoważone (m/s2)" + +msgid "Maximum cant (mm)" +msgstr "Największa przechyłka (mm)" + +msgid "Cant change rate (mm/s)" +msgstr "Prędkość zmiany przechyłki (mm/s)" + +msgid "Cant ramp 1:(k*V), k" +msgstr "Rampa przechyłkowa 1:(k*V), k" + +msgid "Unbalanced acc. change rate (m/s3)" +msgstr "Przyrost przysp. niezrównoważonego (m/s3)" + +msgid "Straight between curves (s of travel)" +msgstr "Prosta między łukami (s jazdy)" + +msgid "Add vertex" +msgstr "Dodaj wierzchołek" + +msgid "Remove vertex" +msgstr "Usuń wierzchołek" + +msgid "LMB unfolds the values of the vertex, the same as a click on it in the view" +msgstr "LPM rozwija wartości wierzchołka, tak samo jak kliknięcie go w widoku" + +msgid "At the intersection of the end tangents" +msgstr "Na przecięciu stycznych końców" + +msgid "Distance from the start (m)" +msgstr "Odległość od początku (m)" + +msgid "Distance from the end (m)" +msgstr "Odległość od końca (m)" + +msgid "Position (x, z)" +msgstr "Położenie (x, z)" + +msgid "Radius R1 (m)" +msgstr "Promień R1 (m)" + +msgid "Radius R (m)" +msgstr "Promień R (m)" + +msgid "Compound curve (two radii)" +msgstr "Łuk koszowy (dwa promienie)" + +msgid "Radius R2 (m)" +msgstr "Promień R2 (m)" + +msgid "Transition R1-R2 (m)" +msgstr "Przejściowa R1-R2 (m)" + +msgid "Share of R1" +msgstr "Udział R1" + +msgid "Transition in (m)" +msgstr "Przejściowa na wejściu (m)" + +msgid "Transition out (m)" +msgstr "Przejściowa na wyjściu (m)" + +msgid "Cant (mm)" +msgstr "Przechyłka (mm)" + +msgid "Turn the longer way (over 180 deg)" +msgstr "Skręt dłuższą drogą (ponad 180°)" + +msgid "Apply recommended for V" +msgstr "Zalecane dla V" + +msgid "For %.0f km/h: R min %.0f m, cant %.0f mm (equilibrium %.0f, min %.0f), transition %.0f m" +msgstr "Dla %.0f km/h: R min %.0f m, przechyłka %.0f mm (równowagowa %.0f, min %.0f), przejściowa %.0f m" + +msgid "Deflection %.4f deg, tangents %.2f / %.2f m, arc %.2f m" +msgstr "Kąt zwrotu %.4f°, styczne %.2f / %.2f m, łuk %.2f m" + +msgid "Unbalanced acc. %.3f m/s2, cant rate %.1f / %.1f mm/s, acc. rate %.3f / %.3f m/s3" +msgstr "Przysp. niezrównoważone %.3f m/s2, zmiana przechyłki %.1f / %.1f mm/s, przyrost przysp. %.3f / %.3f m/s3" + +msgid "Length %.2f m (now %.2f m), %zu elements" +msgstr "Długość %.2f m (teraz %.2f m), %zu elementów" + +msgid "Paths: %zu in the fragment, %zu needed%s" +msgstr "Odcinki: %zu we fragmencie, potrzeba %zu%s" + +msgid " (copies of the neighbours will be added)" +msgstr " (zostaną dodane kopie sąsiednich)" + +msgid "Takes over %.2f m of the straight at the start, %.2f m at the end" +msgstr "Zajmuje %.2f m prostej na początku, %.2f m na końcu" + +msgid "Elements" +msgstr "Elementy" + +msgid "Straight available beyond the ends: %.2f / %.2f m" +msgstr "Prosta dostępna za końcami: %.2f / %.2f m" + +msgid "Correct the errors to apply the design" +msgstr "Popraw błędy, by zastosować projekt" + +msgid "Straights recognized from chains of collinear straight paths.\nLMB: select a path to see the straight it belongs to" +msgstr "Proste rozpoznane z ciągów współliniowych prostych odcinków.\nLPM: zaznacz odcinek, by zobaczyć jego prostą" + +msgid "Angle tolerance (deg)" +msgstr "Tolerancja kąta (°)" + +msgid "Offset tolerance (m)" +msgstr "Tolerancja odsunięcia (m)" + +msgid "Treat as straight above R (m)" +msgstr "Traktuj jako prostą powyżej R (m)" + +msgid "Minimum length (m)" +msgstr "Najmniejsza długość (m)" + +msgid "Recognize straights in the scenery" +msgstr "Rozpoznaj proste w scenerii" + +msgid "%zu straights" +msgstr "%zu prostych" + +msgid "path name" +msgstr "nazwa odcinka" + +msgid "Break" +msgstr "Złam" + +msgid "Turns the rest of the straight from a point, a curve goes at the break (Ctrl+drag in the view does the same)" +msgstr "Obraca resztę prostej od punktu, w załomie wstawia łuk (to samo robi Ctrl+przeciągnięcie w widoku)" + +msgid "S-curve" +msgstr "Esownica" + +msgid "Shifts the rest of the straight sideways through two curves" +msgstr "Przesuwa resztę prostej w bok przez dwa łuki" + +msgid "Leave the group" +msgstr "Opuść grupę" + +msgid "Move with others" +msgstr "Przesuwaj z innymi" + +msgid "Straights of a group move together and stay parallel; Alt+click in the view adds another one" +msgstr "Proste z grupy przesuwają się razem i zostają równoległe; Alt+klik w widoku dodaje kolejną" + +msgid "Curve R (m, 0: automatic)" +msgstr "R łuku (m, 0: automatycznie)" + +msgid "Transitions from the line speed" +msgstr "Przejściowe z prędkości linii" + +msgid "Transition (m)" +msgstr "Przejściowa (m)" + +msgid "Group: %zu straights move together" +msgstr "Grupa: %zu prostych przesuwa się razem" + +msgid "Clear" +msgstr "Wyczyść" + +msgid "Exact values" +msgstr "Dokładne wartości" + +msgid "Start (x, y, z)" +msgstr "Początek (x, y, z)" + +msgid "End (x, y, z)" +msgstr "Koniec (x, y, z)" + +msgid "Apply ends" +msgstr "Zastosuj końce" + +msgid "Length (m)" +msgstr "Długość (m)" + +msgid "Set from the start##length" +msgstr "Ustaw od początku##length" + +msgid "Azimuth (deg)" +msgstr "Azymut (°)" + +msgid "Turn around the start" +msgstr "Obróć wokół początku" + +msgid "Placing of the switch cancelled" +msgstr "Wstawianie rozjazdu anulowane" + +msgid "The curve would run through a switch" +msgstr "Łuk przechodziłby przez rozjazd" + +msgid "The curve can't be fitted" +msgstr "Nie da się dopasować łuku" + +msgid "Done, select a curve to adjust it in the Curve mode" +msgstr "Gotowe, zaznacz łuk, by poprawić go w trybie Łuk" + +msgid "The track can't be deleted while vehicles stand on it" +msgstr "Toru nie można usunąć, gdy stoją na nim pojazdy" + +msgid "The switch is replaced by its main track, Shift+Del removes all of it" +msgstr "Rozjazd zastąpiono jego torem zasadniczym, Shift+Del usuwa całość" + +msgid "Continue from the free end" +msgstr "Dalej od wolnego końca" + +msgid "Length (m)##extend" +msgstr "Długość (m)##extend" + +msgid "Length of the next piece, along the curve for the curves" +msgstr "Długość kolejnego odcinka, dla łuków mierzona po łuku" + +msgid "Radius of the curves added" +msgstr "Promień dodawanych łuków" + +msgid "Straight##extend" +msgstr "Prosta##extend" + +msgid "Adds a straight at the free end; the new piece gets selected, so the next click goes on from it" +msgstr "Dokłada prostą na wolnym końcu; nowy odcinek zostaje zaznaczony, więc kolejne kliknięcie idzie dalej od niego" + +msgid "Curve left##extend" +msgstr "Łuk w lewo##extend" + +msgid "Curve right##extend" +msgstr "Łuk w prawo##extend" + +msgid "Freehand" +msgstr "Z ręki" + +msgid "Dragging the red ring of a free end draws a curve through the cursor; off: it draws a straight along the end" +msgstr "Przeciągnięcie czerwonego pierścienia wolnego końca rysuje łuk przez kursor; wyłączone: rysuje prostą wzdłuż końca" + +msgid "Two points lie on one another" +msgstr "Dwa punkty leżą na sobie" + +msgid "The track can't be laid through these points" +msgstr "Nie da się ułożyć toru przez te punkty" + +msgid "Click at least two points" +msgstr "Kliknij co najmniej dwa punkty" + +msgid "Laid %zu paths, %.1f m" +msgstr "Ułożono %zu odcinków, %.1f m" + +msgid "The curvature changes abruptly where the track joins, there's no room for a transition curve" +msgstr "Na styku z torem krzywizna zmienia się skokowo, nie ma miejsca na krzywą przejściową" + +msgid "Compound curve: R %.0f m meets R %.0f m with no transition curve between them" +msgstr "Łuk koszowy: R %.0f m styka się z R %.0f m bez krzywej przejściowej między nimi" + +msgid "More settings" +msgstr "Więcej ustawień" + +msgid "Texture of the rails" +msgstr "Tekstura szyn" + +msgid "Texture of the ballast" +msgstr "Tekstura podsypki" + +msgid "The scenery isn't opened for editing: new track won't be saved" +msgstr "Sceneria nie jest otwarta do edycji: nowy tor nie zostanie zapisany" + +msgid "A double slip goes only into a straight crossing another one" +msgstr "Rozjazd krzyżowy wstawia się tylko w prostą przecinającą inną" + +msgid "The selected track is neither a recognized straight nor a curve" +msgstr "Zaznaczony tor nie jest ani rozpoznaną prostą, ani łukiem" + +msgid "The curve can't be followed" +msgstr "Nie da się prześledzić łuku" + +msgid "The straight is %.1f m long, too short for the %.1f m switch" +msgstr "Prosta ma %.1f m, za mało na rozjazd %.1f m" + +msgid "The switch would overlap another switch, move it onto plain track" +msgstr "Rozjazd nachodziłby na inny rozjazd, przesuń go na zwykły tor" + +msgid "Switch " +msgstr "Rozjazd " + +msgid " inserted" +msgstr " wstawiony" + +msgid "The switch couldn't be created from this template" +msgstr "Nie udało się utworzyć rozjazdu z tego wzorca" + +msgid "Scenery: " +msgstr "Sceneria: " + +msgid "No straight crossing the selected one near the click" +msgstr "W pobliżu kliknięcia żadna prosta nie przecina zaznaczonej" + +msgid "The straights cross at an angle unsuitable for a double slip" +msgstr "Proste przecinają się pod kątem nieodpowiednim dla rozjazdu krzyżowego" + +msgid "Not enough straight track around the crossing" +msgstr "Za mało prostego toru wokół skrzyżowania" + +msgid "R reduced to " +msgstr "R zmniejszony do " + +msgid " m, the straights end " +msgstr " m, proste kończą się " + +msgid " m from the crossing" +msgstr " m od skrzyżowania" + +msgid "The double slip isn't complete, it can't be replaced" +msgstr "Rozjazd krzyżowy jest niekompletny, nie da się go zastąpić" + +msgid "The double slip geometry isn't recognized" +msgstr "Nie rozpoznano geometrii rozjazdu krzyżowego" + +msgid "R limited to " +msgstr "R ograniczony do " + +msgid " m, a neighbouring path isn't straight" +msgstr " m, sąsiedni odcinek nie jest prosty" + +msgid " m to fit the straights" +msgstr " m, by zmieścić się na prostych" + +msgid "The curve is %.1f m long, too short for the %.1f m switch" +msgstr "Łuk ma %.1f m, za mało na rozjazd %.1f m" + +msgid " inserted into the curve" +msgstr " wstawiony w łuk" + +msgid "flat" +msgstr "płasko" + +msgid "mountains" +msgstr "góry" + +msgid "Texture 1 (rails/surface)" +msgstr "Tekstura 1 (szyny/nawierzchnia)" + +msgid "Texture 2 (trackbed/side)" +msgstr "Tekstura 2 (podsypka/bok)" + +msgid "Rails P1-P2" +msgstr "Szyny P1-P2" + +msgid "Rails P3-P4" +msgstr "Szyny P3-P4" + +msgid "Cubic parabola" +msgstr "Parabola 3. stopnia" + +msgid "Clothoid" +msgstr "Klotoida" + +msgid "transition" +msgstr "przejściowa" + +msgid "LMB" +msgstr "LPM" + +msgid "LMB on a free end" +msgstr "LPM na wolnym końcu" + +msgid "leave the axis" +msgstr "zejście z osi" + +msgid "shifted" +msgstr "przesunięta" + +msgid "Drag" +msgstr "Przeciągnij" + +msgid "Release" +msgstr "Puść" + +msgid "straight along the end, to a free end: a curve joining it" +msgstr "prosta wzdłuż końca, do wolnego końca: łuk łączący" + +msgid "Drag along" +msgstr "Ciągnij wzdłuż" + +msgid "Drag sideways" +msgstr "Ciągnij w bok" + +msgid "LMB on a track" +msgstr "LPM na torze" + +msgid "choose the straight or the curve to put the switch in" +msgstr "wybierz prostą lub łuk, w który wstawić rozjazd" + +msgid "remove the switch, its main track stays" +msgstr "usuwa rozjazd, tor zasadniczy zostaje" + +msgid "1. List below" +msgstr "1. Lista niżej" + +msgid "choose the type of the switch" +msgstr "wybierz typ rozjazdu" + +msgid "2. Press+drag" +msgstr "2. Wciśnij i ciągnij" + +msgid "on the selected track where the switch starts: along sets the direction, sideways the side" +msgstr "na zaznaczonym torze, gdzie zaczyna się rozjazd: wzdłuż ustala kierunek, w bok stronę" + +msgid "LMB elsewhere" +msgstr "LPM gdzie indziej" + +msgid "choose another track" +msgstr "wybiera inny tor" + +msgid "loads the grade line of its line" +msgstr "wczytuje niweletę jego linii" + +msgid "Drag a point" +msgstr "Ciągnij punkt" + +msgid "in the profile window: changes the grade" +msgstr "w oknie profilu: zmienia pochylenie" + +msgid "Double click" +msgstr "Podwójny klik" + +msgid "on the line: adds a point" +msgstr "na linii: dodaje punkt" + +msgid "Apply" +msgstr "Zastosuj" + +msgid "writes the heights into the tracks" +msgstr "zapisuje wysokości w torach" + +msgid "LMB on a row" +msgstr "LPM na wierszu" + +msgid "LMB on the straight" +msgstr "LPM na prostej" + +msgid "where it breaks, then drag the handle to turn the rest" +msgstr "w miejscu załomu, potem ciągnij uchwyt, by obrócić resztę" + +msgid "where the shift starts, then drag the handle: sideways the shift, along its length" +msgstr "gdzie zaczyna się przesunięcie, potem ciągnij uchwyt: w bok przesunięcie, wzdłuż jego długość" + +msgid "LMB on a straight track" +msgstr "LPM na prostym torze" + +msgid "selects the whole straight" +msgstr "zaznacza całą prostą" + +msgid "Drag a square" +msgstr "Ciągnij kwadrat" + +msgid "moves an end, the curves at it follow" +msgstr "przesuwa koniec, łuki przy nim podążają" + +msgid "Drag the diamond" +msgstr "Ciągnij romb" + +msgid "shifts the straight sideways" +msgstr "przesuwa prostą w bok" + +msgid "Near another straight" +msgstr "Przy innej prostej" + +msgid "keep shifting: snaps parallel at %.2f m" +msgstr "ciągnij dalej: przyleganie równoległe na %.2f m" + +msgid "snaps parallel at %.2f m" +msgstr "przyleganie równoległe na %.2f m" + +msgid "close: parallel snap at %.2f m" +msgstr "blisko: przyleganie równoległe na %.2f m" + +msgid "parallel %.2f m" +msgstr "równolegle %.2f m" + +msgid "parallel at %.2f m" +msgstr "równolegle na %.2f m" + +msgid "snap at %.2f m (%.1f°)" +msgstr "przyleganie na %.2f m (%.1f°)" + +msgid "snaps parallel at 4.00 / 4.50 / 4.75 m" +msgstr "łapie równolegle na 4.00 / 4.50 / 4.75 m" + +msgid "shifts sideways; close: snaps parallel at 4.00 / 4.50 / 4.75 m" +msgstr "przesuwa w bok; blisko: łapie równolegle na 4.00 / 4.50 / 4.75 m" + +msgid "Nearby straight: the diamond snaps parallel at 4.00 / 4.50 / 4.75 m" +msgstr "Sąsiednia prosta: romb łapie równolegle na 4.00 / 4.50 / 4.75 m" + +msgid "close: %.2f m %.1f°" +msgstr "blisko: %.2f m %.1f°" + +msgid "Drag: shift the straight sideways — close: parallel snap at %.2f m" +msgstr "Ciągnij: przesuń prostą w bok — blisko: przyleganie równoległe na %.2f m" + +msgid "Drag: shift the straight sideways — close: %.2f m %.1f°" +msgstr "Ciągnij: przesuń prostą w bok — blisko: %.2f m %.1f°" + +msgid "Drag: shift the straight sideways — snaps parallel at %.2f m" +msgstr "Ciągnij: przesuń prostą w bok — przyleganie równoległe na %.2f m" + +msgid "Ctrl+drag" +msgstr "Ctrl+ciągnij" + +msgid "breaks the straight" +msgstr "łamie prostą" + +msgid "Shift+click x4" +msgstr "Shift+klik x4" + +msgid "shifts a piece around an obstacle" +msgstr "przesuwa kawałek, omijając przeszkodę" + +msgid "Alt+click" +msgstr "Alt+klik" + +msgid "adds another straight to move together" +msgstr "dodaje kolejną prostą do wspólnego przesuwania" + +msgid "loads its line: the curves become vertices W1, W2..." +msgstr "wczytuje jego linię: łuki stają się wierzchołkami W1, W2..." + +msgid "Drag yellow" +msgstr "Ciągnij żółty" + +msgid "moves a vertex, the curve follows" +msgstr "przesuwa wierzchołek, łuk podąża" + +msgid "Drag green" +msgstr "Ciągnij zielony" + +msgid "changes the radius" +msgstr "zmienia promień" + +msgid "writes the design into the tracks" +msgstr "zapisuje projekt w torach" + +msgid "selects it" +msgstr "zaznacza go" + +msgid "lay a new track" +msgstr "ułóż nowy tor" + +msgid "moves the point" +msgstr "przesuwa punkt" + +msgid "releases the point" +msgstr "puszcza punkt" + +msgid "LMB on a handle" +msgstr "LPM na uchwycie" + +msgid "picks a point: an end or a control vector" +msgstr "wybiera punkt: koniec lub wektor kontrolny" + +msgid "moves the whole track, W turns it" +msgstr "przesuwa cały tor, W obraca" + +msgid "Drag the red ring" +msgstr "Ciągnij czerwony pierścień" + +msgid "leads a new track out of a free end, or use Continue below" +msgstr "wyprowadza nowy tor z wolnego końca, albo użyj „Dalej” niżej" + +msgid "deletes the track" +msgstr "usuwa tor" + +msgid "Design read from the scenery file " +msgstr "Projekt odczytany z pliku scenerii " + +msgid ", largest departure from the track %.3f m" +msgstr ", największa odchyłka od toru %.3f m" + +msgid "The scenery wasn't opened for editing, the design can't be kept in its files" +msgstr "Sceneria nie została otwarta do edycji, projektu nie da się zachować w jej plikach" + +msgid "The design will be removed from " +msgstr "Projekt zostanie usunięty z " + +msgid " on save" +msgstr " przy zapisie" + +msgid "The route of the stored design isn't in the scenery any more (paths moved or removed)" +msgstr "Trasy zapisanego projektu nie ma już w scenerii (odcinki przesunięte lub usunięte)" + +msgid "The path can't carry a profile" +msgstr "Ten odcinek nie może mieć profilu" + +msgid "Grade line recognized from the track: %zu points, largest departure %.3f m" +msgstr "Niweleta rozpoznana z toru: %zu punktów, największa odchyłka %.3f m" + +msgid "Correct the errors to apply the grade line" +msgstr "Popraw błędy, by zastosować niweletę" + +msgid "The grade line can't be applied, " +msgstr "Nie da się zastosować niwelety, " + +msgid "Grade line applied: %zu paths changed, %zu added, largest departure %.3f m" +msgstr "Niweleta zastosowana: zmieniono %zu odcinków, dodano %zu, największa odchyłka %.3f m" + +msgid "Design kept in %s%s" +msgstr "Projekt zachowany w %s%s" + +msgid ", changes go there on save" +msgstr ", zmiany trafią tam przy zapisie" + +msgid "Forget the design" +msgstr "Porzuć projekt" + +msgid "The design goes to the scenery file of the route on save" +msgstr "Projekt trafi przy zapisie do pliku scenerii z tą trasą" + +msgid "Designs kept in the scenery (%zu)" +msgstr "Projekty zachowane w scenerii (%zu)" + +msgid "Open" +msgstr "Otwórz" + +msgid "%s %.1f m, %zu points, from (%.1f, %.1f) to (%.1f, %.1f)" +msgstr "%s %.1f m, %zu punktów, od (%.1f, %.1f) do (%.1f, %.1f)" + +msgid " auto" +msgstr " auto" + +msgid "Select a track first" +msgstr "Najpierw zaznacz tor" + +msgid "%zu paths checked, %d with the speed above what the geometry allows, %d switch branch warnings" +msgstr "Sprawdzono %zu odcinków, %d z prędkością ponad dopuszczalną przez geometrię, %d ostrzeżeń dla torów zwrotnych" + +msgid "Nothing changed" +msgstr "Nic się nie zmieniło" + +msgid "Speed limits of %zu paths lowered to the safe ones, Ctrl+Z brings them back" +msgstr "Obniżono ograniczenia %zu odcinków do bezpiecznych, Ctrl+Z je przywraca" + +msgid "Speed limits of the paths against the speed their geometry allows:\nradius and cant, cant ramps, curves without the transitions, switch branches" +msgstr "Ograniczenia prędkości odcinków wobec prędkości dopuszczalnej przez geometrię:\npromień i przechyłka, rampy przechyłkowe, łuki bez przejściowych, tory zwrotne" + +msgid "Line through the selected path" +msgstr "Linia przez zaznaczony odcinek" + +msgid "Whole scenery" +msgstr "Cała sceneria" + +msgid "Checking %zu / %zu" +msgstr "Sprawdzanie %zu / %zu" + +msgid "Cancel" +msgstr "Anuluj" + +msgid "Check" +msgstr "Sprawdź" + +msgid "Switch branches (m/s2)" +msgstr "Tory zwrotne (m/s2)" + +msgid "Unbalanced acceleration allowed on the diverging tracks of the switches, which have no cant.\n0.65 m/s2 gives 40 km/h for R 190 m and 100 km/h for R 1200 m" +msgstr "Dopuszczalne przyspieszenie niezrównoważone na torach zwrotnych rozjazdów, które nie mają przechyłki.\n0,65 m/s2 daje 40 km/h dla R 190 m i 100 km/h dla R 1200 m" + +msgid "Reference length (m)" +msgstr "Długość odniesienia (m)" + +msgid "Length over which an abrupt change of the curvature or the cant is felt,\nabout the length of a car. Shorter: stricter for the curves without the transitions" +msgstr "Długość, na której odczuwa się nagłą zmianę krzywizny lub przechyłki,\nmniej więcej długość wagonu. Krótsza: surowiej dla łuków bez przejściowych" + +msgid "Change the limits, then Check again" +msgstr "Zmień normy i sprawdź ponownie" + +msgid "The scenery changed since the check" +msgstr "Sceneria zmieniła się od sprawdzenia" + +msgid "Paths without a speed limit of their own run at the line speed,\nthose are listed whenever the geometry limits the speed at all" +msgstr "Odcinki bez własnego ograniczenia jadą z prędkością linii,\nsą wypisywane, gdy tylko geometria w ogóle ogranicza prędkość" + +msgid "Within their limits too" +msgstr "Także mieszczące się w ograniczeniu" + +msgid "Set the safe speed on %zu paths" +msgstr "Ustaw bezpieczną prędkość na %zu odcinkach" + +msgid "Lowers the speed limit of each listed path to its safe speed, rounded down to 10 km/h (5 km/h below 40).\nThe limits of the switch branches are only warnings and aren't applied here, the button of a single path does.\nA switch has one velocity for both its tracks: negative limits only the diverging one, the main one keeping no limit.\nWhen the main track needs a limit of its own, the lower of the two applies to both" +msgstr "Obniża ograniczenie każdego wypisanego odcinka do bezpiecznej prędkości, zaokrąglonej w dół do 10 km/h (5 km/h poniżej 40).\nOgraniczenia torów zwrotnych są tylko ostrzeżeniami i nie są tu stosowane, robi to przycisk pojedynczego odcinka.\nRozjazd ma jedną prędkość dla obu torów: ujemna ogranicza tylko zwrotny, zasadniczy zostaje bez ograniczenia.\nGdy tor zasadniczy potrzebuje własnego ograniczenia, niższe z dwóch obowiązuje oba" + +msgid "any" +msgstr "dowolna" + +msgid "Set the safe speed on this path" +msgstr "Ustaw bezpieczną prędkość na tym odcinku" + +msgid "Set" +msgstr "Ustaw" + +msgid "Open the vertical profile of a route first" +msgstr "Najpierw otwórz profil podłużny trasy" + +msgid "The scenery has no paths the editor can handle" +msgstr "Sceneria nie ma odcinków obsługiwanych przez edytor" + +msgid "Select a path first" +msgstr "Najpierw zaznacz odcinek" + +msgid "%d object(s) found along %d path(s)" +msgstr "Znaleziono obiektów: %d wzdłuż odcinków: %d" + +msgid "%d object(s) bound, they follow the changes of the track. The bindings are kept in the scenery files on save" +msgstr "Powiązano obiektów: %d, podążają za zmianami toru. Powiązania są zapisywane w plikach scenerii" + +msgid "Bound objects follow the changes of the track" +msgstr "Powiązane obiekty podążają za zmianami toru" + +msgid "Bound objects keep their chainage along the path, the offset from its axis and the height above the rail top;\nmodels and includes turn with the path and stay upright. The move is a part of the undo step of the change." +msgstr "Powiązane obiekty zachowują kilometraż wzdłuż odcinka, odsunięcie od osi i wysokość nad główką szyny;\nmodele i include obracają się z odcinkiem i zostają pionowo. Przesunięcie jest częścią kroku cofania zmiany." + +msgid "Show in 3D" +msgstr "Pokaż w 3D" + +msgid "Objects bound in the scenery: %d" +msgstr "Obiekty powiązane w scenerii: %d" + +msgid "(%d stored binding(s) not found, kept as they are)" +msgstr "(nie znaleziono zapisanych powiązań: %d, zostają bez zmian)" + +msgid "Reach each way, m" +msgstr "Zasięg w każdą stronę, m" + +msgid "Corridor each side of the axis, m" +msgstr "Pas z każdej strony osi, m" + +msgid "Rules for names of the files" +msgstr "Reguły dla nazw plików" + +msgid "Parts of the names, separated by spaces; the first rule which matches decides" +msgstr "Fragmenty nazw rozdzielone spacjami; decyduje pierwsza pasująca reguła" + +msgid "Default rules" +msgstr "Reguły domyślne" + +msgid "Includes are matched by the template name and its //$e description, models by the file and node name;\ntraction is always the catenary, memory cells and event launchers the logic" +msgstr "Include są dopasowywane po nazwie szablonu i opisie //$e, modele po nazwie pliku i węzła;\ntraction to zawsze sieć, komórki pamięci i wyzwalacze zdarzeń to logika" + +msgid "Find objects" +msgstr "Znajdź obiekty" + +msgid "Bind the chosen (%d)" +msgstr "Powiąż wybrane (%d)" + +msgid "All" +msgstr "Wszystkie" + +msgid "None" +msgstr "Żadne" + +msgid "all##g" +msgstr "wszystkie##g" + +msgid "none##g" +msgstr "żadne##g" + +msgid "[bound]" +msgstr "[powiązany]" + +msgid "%s, %s\n%.2f m %s of the axis, %.2f m above the rail top\n%s" +msgstr "%s, %s\n%.2f m %s od osi, %.2f m nad główką szyny\n%s" + +msgid "Bound objects along the scope" +msgstr "Powiązane obiekty w zasięgu" + +msgid "%d of %d" +msgstr "%d z %d" + +msgid "Unbind all of these" +msgstr "Odwiąż wszystkie te" + +msgid "unbind" +msgstr "odwiąż" + +msgid " (no path to follow)" +msgstr " (brak odcinka do śledzenia)" + +msgid "Selected path" +msgstr "Zaznaczony odcinek" + +msgid "Half-width" +msgstr "Półszerokość" + +msgid "Height" +msgstr "Wysokość" + +msgid "Insert a point after this one" +msgstr "Wstaw punkt po tym" + +msgid "Show along the selected path" +msgstr "Pokaż wzdłuż zaznaczonego odcinka" + +msgid "Translucent tunnel of the gauge 500 m each way along the line of the selected path.\nWidened in the curves under 250 m and tilted on the cant, from 20 m (inner side)\nand 26 m (outer side) ahead of the curve, as in the PKP PLK standard, volume II." +msgstr "Półprzezroczysty tunel skrajni 500 m w każdą stronę wzdłuż linii zaznaczonego odcinka.\nPoszerzony w łukach poniżej 250 m i pochylony na przechyłce, od 20 m (strona wewnętrzna)\ni 26 m (strona zewnętrzna) przed łukiem, jak w standardzie PKP PLK, tom II." + +msgid "GPL: unified structure gauge, widened only in the curves under 250 m.\nG1, G2, GA, GB, GC: limit (GSZ) and nominal (NSZ) installation gauges, widened by 3750/R in all curves." +msgstr "GPL: ujednolicona skrajnia budowli, poszerzana tylko w łukach poniżej 250 m.\nG1, G2, GA, GB, GC: graniczne (GSZ) i nominalne (NSZ) skrajnie instalacyjne, poszerzane o 3750/R we wszystkich łukach." + +msgid "Clearance over the roads, from 0.5 m beyond the edges of the carriageway" +msgstr "Skrajnia nad drogami, od 0,5 m poza krawędziami jezdni" + +msgid "Stops the scan and keeps the models found so far" +msgstr "Przerywa skanowanie i zachowuje znalezione dotąd modele" + +msgid "Find the violations on the whole map" +msgstr "Znajdź naruszenia na całej mapie" + +msgid "Checks the models along all tracks of the scenery against the chosen gauge\nand lists them, the deepest first.\nRuns in the background, the list fills up as it goes" +msgstr "Sprawdza modele wzdłuż wszystkich torów scenerii względem wybranej skrajni\ni wypisuje je, najgłębsze najpierw.\nDziała w tle, lista zapełnia się na bieżąco" + +msgid "The scenery changed since the scan (%s)" +msgstr "Sceneria zmieniła się od skanowania (%s)" + +msgid "No model enters the gauge on the map" +msgstr "Żaden model nie wchodzi w skrajnię na mapie" + +msgid "The gauge is clear along this line" +msgstr "Skrajnia na tej linii jest wolna" + +msgid "%zu models enter the gauge%s" +msgstr "Modeli wchodzących w skrajnię: %zu%s" + +msgid " on the map" +msgstr " na mapie" + +msgid "Outline of %s (right half)" +msgstr "Obrys %s (prawa połowa)" + +msgid "Metres from the edge of the carriageway and above the road, bottom up." +msgstr "Metry od krawędzi jezdni i nad drogą, od dołu." + +msgid "Restore the starting outlines" +msgstr "Przywróć obrysy początkowe" + +msgid "Edit" +msgstr "Edycja" + +msgid "Analysis" +msgstr "Analiza" + + +msgid "The adjoining straights can't be cut at the ends of the curve: the start misses by %.1f m, the end by %.1f m" +msgstr "Nie da się przyciąć przyległych prostych na końcach łuku: początek chybia o %.1f m, koniec o %.1f m" + +msgid "No curve here: LMB on a curve" +msgstr "Tu nie ma łuku: LPM na łuku" + +msgid "LMB on a curve" +msgstr "LPM na łuku" + +msgid "loads it with the straights at its ends" +msgstr "wczytuje go razem z prostymi na końcach" + +msgid "Curve %d of the S-curve" +msgstr "Łuk %d esownicy" + +msgid "Details" +msgstr "Szczegóły" + + +msgid "There's no ground under the route" +msgstr "Pod trasą nie ma terenu" + +msgid "Grade line fitted to the ground: %zu points, embankment up to %.2f m, cutting up to %.2f m" +msgstr "Niweleta dopasowana do terenu: %zu punktów, nasyp do %.2f m, wykop do %.2f m" + +msgid "Fit to the ground" +msgstr "Dopasuj do terenu" + +msgid "Places the breaks of the grade line along the ground under the route,\nwithin the limits and with the least earthworks; the ground itself stays as it is" +msgstr "Rozmieszcza załamania niwelety wzdłuż terenu pod trasą,\nw granicach norm i z najmniejszymi robotami ziemnymi; sam teren się nie zmienia" + +msgid "m off the ground##fittolerance" +msgstr "m od terenu##fittolerance" + +msgid "How far the grade line may depart from the ground before it breaks;\nmore gives fewer breaks and higher embankments and deeper cuttings" +msgstr "Jak daleko niweleta może odejść od terenu, zanim się załamie;\nwięcej to mniej załamań, ale wyższe nasypy i głębsze wykopy" + +msgid "There's no editor terrain to shape" +msgstr "Nie ma terenu edytora do przekształcenia" + +msgid "The editor terrain doesn't reach the route, or it's shaped already" +msgstr "Teren edytora nie sięga trasy albo jest już przekształcony" + +msgid "Terrain shaped at %zu points: embankment up to %.2f m, cutting up to %.2f m" +msgstr "Teren przekształcony w %zu punktach: nasyp do %.2f m, wykop do %.2f m" + +msgid "The terrain is back as it was before the shaping" +msgstr "Teren przywrócony do stanu sprzed przekształcenia" + +msgid "The shaped terrain isn't loaded any more" +msgstr "Przekształcony teren nie jest już wczytany" + +msgid "Shape the terrain to the grade line" +msgstr "Przekształć teren pod niweletę" + +msgid "Only the editor terrain changes: a formation under the track and slopes to the ground; the triangles and models of the scenery stay as they are." +msgstr "Zmienia się tylko teren edytora: korona podtorza pod torem i skarpy do terenu; trójkąty i modele scenerii zostają bez zmian." + +msgid "Formation below the rail top (m)" +msgstr "Korona podtorza pod główką szyny (m)" + +msgid "Half width of the formation (m)" +msgstr "Połowa szerokości korony (m)" + +msgid "From the axis of the route to the edge of the formation;\nfor two tracks add the distance between them" +msgstr "Od osi trasy do krawędzi korony;\ndla dwóch torów dodaj rozstaw między nimi" + +msgid "Slopes 1 : n, n" +msgstr "Skarpy 1 : n, n" + +msgid "Slopes reach at most (m)" +msgstr "Skarpy sięgają najwyżej (m)" + +msgid "Correct the errors of the grade line first" +msgstr "Najpierw popraw błędy niwelety" + +msgid "Shape the terrain" +msgstr "Przekształć teren" + +msgid "Restore the terrain" +msgstr "Przywróć teren" + +msgid "Puts back the terrain as it was before the last shaping" +msgstr "Przywraca teren sprzed ostatniego przekształcenia" + +msgid "There's no editor terrain to shape: convert the scenery ground first" +msgstr "Nie ma terenu edytora do przekształcenia: najpierw zamień grunt scenerii" + +msgid "The chosen materials have no triangles" +msgstr "Wybrane materiały nie mają trójkątów" + +msgid "Ground converted: %zu chunks of editor terrain made, %zu shapes taken out of the scene, %zu triangle nodes go from the scenery files on save" +msgstr "Grunt zamieniony: %zu chunków terenu edytora, %zu brył zdjętych ze sceny, %zu węzłów trójkątów zniknie z plików scenerii przy zapisie" + +msgid ", %zu chunks kept as there was editor terrain already" +msgstr ", %zu chunków pominiętych, bo był tam już teren edytora" + +msgid ", %zu nodes stay as their files can't be rewritten" +msgstr ", %zu węzłów zostaje, bo ich plików nie da się zapisać" + +msgid "Convert the scenery ground to editor terrain..." +msgstr "Zamień grunt scenerii na teren edytora..." + +msgid "Replaces the triangles of the chosen materials with editor terrain of the same shape,\nwhich can be shaped and sculpted; the scenery files lose these triangles on save" +msgstr "Zastępuje trójkąty wybranych materiałów terenem edytora o tym samym kształcie,\nktóry można przekształcać i rzeźbić; pliki scenerii tracą te trójkąty przy zapisie" + +msgid "Materials of the scenery triangles: tick the ones which are the ground" +msgstr "Materiały trójkątów scenerii: zaznacz te, które są gruntem" + +msgid "The scenery has no triangles" +msgstr "Sceneria nie ma trójkątów" + +msgid "%s (%zu triangles)" +msgstr "%s (%zu trójkątów)" + +msgid "The terrain gets a single texture: that of the material which covers the most. Until saved, loading the scenery again takes it all back." +msgstr "Teren dostaje jedną teksturę: materiału, który pokrywa najwięcej. Do zapisu ponowne wczytanie scenerii cofa wszystko." + +msgid "Tick at least one material" +msgstr "Zaznacz co najmniej jeden materiał" + +msgid "Convert" +msgstr "Zamień" + +msgid "Sculpt mode (LMB smooth)" +msgstr "Rzeźbienie (LPM wygładza)" + +msgid "Sculpt mode (LMB raise / Shift = lower)" +msgstr "Rzeźbienie (LPM podnosi / Shift obniża)" + +msgid "Raise / lower" +msgstr "Podnoś / obniżaj" + +msgid "Smooth" +msgstr "Wygładzaj" + +msgid "Evens out the ground under the brush: bumps and sharp edges go, the shape stays.\nThe strength sets how fast" +msgstr "Wyrównuje teren pod pędzlem: znikają nierówności i ostre krawędzie, kształt zostaje.\nSiła ustala, jak szybko" + +msgid "Rounding of the edges (m)" +msgstr "Zaokrąglenie krawędzi (m)" + +msgid "Length over which the slope bends into the formation and into the ground; 0: sharp edges" +msgstr "Długość, na której skarpa przechodzi w koronę i w teren; 0: ostre krawędzie" + +msgid "Sculpting..." +msgstr "Rzeźbienie..." + +msgid "Opens the terrain tools: raising, lowering and smoothing with a brush" +msgstr "Otwiera narzędzia terenu: podnoszenie, obniżanie i wygładzanie pędzlem" + +# scenery editor: track editor, selection, joints, parallel track, hectometre posts, fouling points + +msgid " (branch)" +msgstr " (zwrotny)" + +msgid " (main)" +msgstr " (zasadniczy)" + +msgid "%s set on %zu paths, Ctrl+Z takes it back" +msgstr "%s ustawiono na %zu odcinkach, Ctrl+Z cofa" + +msgid "%zu fouling point markers moved to their places, Ctrl+Z takes it back" +msgstr "Przestawiono %zu wskaźników ukresu na ich miejsca, Ctrl+Z cofa" + +msgid "%zu fouling point markers placed, Ctrl+Z takes them back" +msgstr "Postawiono %zu wskaźników ukresu, Ctrl+Z je usuwa" + +msgid "%zu found, the first %zu listed" +msgstr "Znaleziono %zu, pokazano pierwsze %zu" + +msgid "%zu hectometre posts moved to their places, Ctrl+Z takes it back" +msgstr "Przestawiono %zu słupków hektometrowych na ich miejsca, Ctrl+Z cofa" + +msgid "%zu hectometre posts placed, Ctrl+Z takes them back" +msgstr "Postawiono %zu słupków hektometrowych, Ctrl+Z je usuwa" + +msgid "%zu joints fixed, Ctrl+Z takes them back" +msgstr "Naprawiono %zu styków, Ctrl+Z cofa" + +msgid "%zu paths checked: %d gaps, %d steps, %d kinks, %d cant jumps, %d grade breaks" +msgstr "Sprawdzono %zu odcinków: %d szczelin, %d uskoków, %d załamań, %d skoków przechyłki, %d załamań pochylenia" + +msgid "%zu paths deleted, Ctrl+Z brings them back" +msgstr "Usunięto %zu odcinków, Ctrl+Z je przywraca" + +msgid "%zu paths selected, %.1f m" +msgstr "Zaznaczono %zu odcinków, %.1f m" + +msgid "%zu switches, %zu fouling points: %d without a marker, %d markers standing wrong" +msgstr "%zu rozjazdów, %zu ukresów: %d bez wskaźnika, %d wskaźników stoi źle" + +msgid "(of the direction of the line, see the preview)" +msgstr "(względem kierunku linii, patrz podgląd)" + +msgid ", %d free ends" +msgstr ", %d wolnych końców" + +msgid ", %zu in the files which can't be changed are left as they are" +msgstr ", %zu w plikach, których nie można zmieniać, zostaje bez zmian" + +msgid ", %zu with vehicles on them or in the files which can't be changed are left" +msgstr ", %zu z pojazdami albo w plikach, których nie można zmieniać, zostaje" + +msgid "3.5 m on the straight track; more on the curves, where the cars lean out" +msgstr "3,5 m na prostej; na łukach więcej, bo wagony się wychylają" + +msgid "A free end closer than this to the end of another path is a gap: the trains don't go across it.\nThe simulation joins the ends no more than 2 cm apart" +msgstr "Wolny koniec bliżej niż tyle od końca innego odcinka to szczelina: pociągi przez nią nie przejadą.\nSymulacja łączy końce oddalone najwyżej o 2 cm" + +msgid "A marker off by more (m) is wrong" +msgstr "Wskaźnik dalej niż tyle (m) stoi źle" + +msgid "A post standing off by more (m) is wrong" +msgstr "Słupek przesunięty o więcej (m) stoi źle" + +msgid "A switch has no line of its own: select a plain path" +msgstr "Rozjazd nie ma własnej linii: zaznacz zwykły odcinek" + +msgid "Add to the selection" +msgstr "Dodaj do zaznaczenia" + +msgid "Along the line each way (m)" +msgstr "Wzdłuż linii w każdą stronę (m)" + +msgid "Along the line of the selected path" +msgstr "Wzdłuż linii zaznaczonego odcinka" + +msgid "Around the camera" +msgstr "Wokół kamery" + +msgid "Around the camera (m)" +msgstr "Wokół kamery (m)" + +msgid "Build %zu paths, %.1f m" +msgstr "Zbuduj %zu odcinków, %.1f m" + +msgid "Cant jump (mm)" +msgstr "Skok przechyłki (mm)" + +msgid "Circuits" +msgstr "Obwody" + +msgid "Copy the name" +msgstr "Kopiuj nazwę" + +msgid "Delete" +msgstr "Usuń" + +msgid "Delete %zu paths" +msgstr "Usuń %zu odcinków" + +msgid "Ends of the paths which almost meet, steps, kinks, jumps of the cant and of the grade at the joints" +msgstr "Końce odcinków, które prawie się stykają, uskoki, załamania, skoki przechyłki i pochylenia na stykach" + +msgid "Ends of the paths which almost meet, steps, kinks,\njumps of the cant and of the grade at the joints" +msgstr "Końce odcinków, które prawie się stykają, uskoki, załamania,\nskoki przechyłki i pochylenia na stykach" + +msgid "Environment" +msgstr "Otoczenie" + +msgid "Find a track by its name (Ctrl+F)" +msgstr "Znajdź tor po nazwie (Ctrl+F)" + +msgid "Fix" +msgstr "Napraw" + +msgid "Fix %zu listed" +msgstr "Napraw %zu z listy" + +msgid "Fix this one" +msgstr "Napraw ten" + +msgid "Fouling point markers" +msgstr "Wskaźniki ukresu (W17)" + +msgid "Free ends too" +msgstr "Także wolne końce" + +msgid "From the axis (m)" +msgstr "Od osi toru (m)" + +msgid "Gap up to (m)" +msgstr "Szczelina do (m)" + +msgid "Gaps: the free end goes to the other end, along it. Steps: the end goes to the other one.\nKinks and grade breaks: the end is turned in line with the other path. Cant: the cant of the other path.\nThe plain path moves, a switch stays as it is. One Ctrl+Z takes it all back" +msgstr "Szczeliny: wolny koniec idzie do drugiego końca, w jego kierunku. Uskoki: koniec idzie do drugiego.\nZałamania kierunku i pochylenia: koniec obraca się w linię z drugim odcinkiem. Przechyłka: taka jak na drugim odcinku.\nRusza się zwykły odcinek, rozjazd zostaje. Jedno Ctrl+Z cofa wszystko" + +msgid "Grade break (per mille)" +msgstr "Załamanie pochylenia (‰)" + +msgid "Hectometre posts" +msgstr "Słupki hektometrowe" + +msgid "Hectometre posts from here" +msgstr "Słupki hektometrowe od tego miejsca" + +msgid "Joints" +msgstr "Styki" + +msgid "Joints: LMB on a row shows the place" +msgstr "Styki: LPM na wierszu pokazuje miejsce" + +msgid "Kilometrage read from %zu posts standing along the line" +msgstr "Kilometraż odczytany z %zu słupków stojących przy linii" + +msgid "Kilometrage read from the post %s, check which way it grows" +msgstr "Kilometraż odczytany ze słupka %s, sprawdź, w którą stronę rośnie" + +msgid "Kilometre at the point (km)" +msgstr "Kilometr w tym miejscu (km)" + +msgid "Kink (deg)" +msgstr "Załamanie (st.)" + +msgid "Left" +msgstr "Lewa" + +msgid "Line %.0f m, km %s - %s: %d posts to place, %zu standing, %d of them wrong" +msgstr "Linia %.0f m, km %s - %s: %d słupków do postawienia, %zu stoi, %d z nich źle" + +msgid "Look at it" +msgstr "Pokaż" + +msgid "Marker" +msgstr "Wskaźnik" + +msgid "Move %d wrong markers to their places" +msgstr "Przestaw %d źle stojących wskaźników" + +msgid "Move %d wrong posts to their places" +msgstr "Przestaw %d źle stojących słupków" + +msgid "No line through this path" +msgstr "Przez ten odcinek nie biegnie żadna linia" + +msgid "No track of that name" +msgstr "Nie ma toru o takiej nazwie" + +msgid "Nothing could be fixed: the paths are switches or in the files which can't be changed" +msgstr "Nic nie dało się naprawić: to rozjazdy albo odcinki w plikach, których nie można zmieniać" + +msgid "Parallel track" +msgstr "Tor równoległy" + +msgid "Parallel" +msgstr "Równoległy" + +msgid "First select a straight path (LMB). Then a second track is laid beside it.\nAlong the line the curves are included: R minus or plus the spacing, same transitions and cant." +msgstr "Najpierw zaznacz odcinek prosty (LPM). Potem drugi tor kładzie się obok.\nWzdłuż linii łuki wchodzą: R minus albo plus rozstaw, te same krzywe przejściowe i przechyłka." + +msgid "First select a straight path in the view." +msgstr "Najpierw zaznacz odcinek prosty w widoku." + +msgid "The selected path isn't a straight. Select a straight first." +msgstr "Zaznaczony odcinek nie jest prostą. Najpierw zaznacz prostą." + +msgid "First select a straight. Creates a new track beside it; the selected path stays." +msgstr "Najpierw zaznacz prostą. Tworzy nowy tor obok; zaznaczony odcinek zostaje." + +msgid "Two paths selected: set the spacing of the other in Select. The first must be a straight." +msgstr "Dwa odcinki zaznaczone: rozstaw drugiego ustawiasz w Wyborze. Pierwszy musi być prostą." + +msgid "First selected must be a straight. It stays; the other is moved to this spacing." +msgstr "Pierwszy zaznaczony musi być prostą. On zostaje; drugi jest przestawiany na ten rozstaw." + +msgid "Lay %zu paths, %.1f m" +msgstr "Ułóż %zu odcinków, %.1f m" + +msgid "first select a straight, then a second track beside it in the panel" +msgstr "najpierw zaznacz prostą, potem drugi tor obok niej w panelu" + +msgid "Make parallel" +msgstr "Ustaw równolegle" + +msgid "%s stays. %s is moved to this spacing, taking its curves." +msgstr "%s zostaje. %s jest przestawiany na ten rozstaw, z jego łukami." + +msgid "Now %.2f m on the right" +msgstr "Teraz %.2f m po prawej" + +msgid "Now %.2f m on the left" +msgstr "Teraz %.2f m po lewej" + +msgid "Set %s to %.2f m" +msgstr "Ustaw %s na %.2f m" + +msgid "%s set parallel to %s, %.2f m away; Ctrl+Z takes it back" +msgstr "%s ustawiony równolegle do %s, rozstaw %.2f m; Ctrl+Z cofa" + +msgid "Two selected paths: set the scope to this path, so the other takes its shape at the spacing" +msgstr "Dwa zaznaczone odcinki: zakres na ten odcinek, wtedy drugi bierze jego kształt w rozstawie" + +msgid "Parallel track of %zu paths, %.1f m, %.2f m away; Ctrl+Z takes it back" +msgstr "Tor równoległy z %zu odcinków, %.1f m, w rozstawie %.2f m; Ctrl+Z cofa" + +msgid "Place %d markers" +msgstr "Postaw %d wskaźników" + +msgid "Place %d posts" +msgstr "Postaw %d słupków" + +msgid "Posts" +msgstr "Słupki" + +msgid "RMB on a track" +msgstr "PPM na torze" + +msgid "Rails" +msgstr "Szyny" + +msgid "Read from the posts" +msgstr "Odczytaj ze słupków" + +msgid "Right" +msgstr "Prawa" + +msgid "Second track alongside, curves included: radius R minus or plus the spacing,\nthe same transitions and cant, heights on the plane of the cant" +msgstr "Drugi tor obok, także na łukach: promień R minus albo plus rozstaw,\nte same krzywe przejściowe i przechyłka, wysokości na płaszczyźnie przechyłki" + +msgid "Select a path of the line, or RMB on it: Hectometre posts" +msgstr "Zaznacz odcinek linii albo PPM na nim: Słupki hektometrowe" + +msgid "Select a switch, or check around the camera" +msgstr "Zaznacz rozjazd albo sprawdź wokół kamery" + +msgid "Shift+LMB" +msgstr "Shift+LPM" + +msgid "Side, looking where it grows" +msgstr "Strona, patrząc w kierunku wzrostu" + +msgid "Spacing (m)" +msgstr "Rozstaw (m)" + +msgid "Spacing of the axes (m)" +msgstr "Odległość osi torów (m)" + +msgid "Speed check of the line" +msgstr "Sprawdź prędkość linii" + +msgid "Split here" +msgstr "Podziel tutaj" + +msgid "Step (m)" +msgstr "Uskok (m)" + +msgid "Take out of the selection" +msgstr "Usuń z zaznaczenia" + +msgid "Templates can be placed only in a scenery opened for editing (-edit)" +msgstr "Szablony można stawiać tylko w scenerii otwartej do edycji (-edit)" + +msgid "The active layer can't take them" +msgstr "Aktywna warstwa ich nie przyjmie" + +msgid "The active layer can't take them: " +msgstr "Aktywna warstwa ich nie przyjmie: " + +msgid "The file of the active layer would receive them with a rotation or a scale in effect, pick another layer" +msgstr "Plik aktywnej warstwy dostałby je z obrotem albo skalą, wybierz inną warstwę" + +msgid "The gizmo moves them together, W turns them; Shift+LMB adds or takes out a path" +msgstr "Gizmo przesuwa je razem, W obraca; Shift+LPM dodaje albo usuwa odcinek" + +msgid "The kilometrage grows" +msgstr "Kilometraż rośnie" + +msgid "The line, up to the switches" +msgstr "Linia, do rozjazdów" + +msgid "The numbers of the %zu posts along the line don't go by 100 m, the kilometrage wasn't read from them" +msgstr "Numery %zu słupków przy linii nie idą co 100 m, kilometrażu z nich nie odczytano" + +msgid "The scenery isn't opened for editing, the new track won't be saved" +msgstr "Sceneria nie jest otwarta do edycji, nowy tor nie zostanie zapisany" + +msgid "The selected switch" +msgstr "Zaznaczony rozjazd" + +msgid "The spacing is larger than the radius of %s" +msgstr "Rozstaw jest większy niż promień odcinka %s" + +msgid "This path" +msgstr "Ten odcinek" + +msgid "Trackbed" +msgstr "Podsypka" + +msgid "Turned by (deg)" +msgstr "Obrót (st.)" + +msgid "Velocity" +msgstr "Prędkość" + +msgid "Vertical profile of the line" +msgstr "Profil podłużny linii" + +msgid "W17 %.1f m off" +msgstr "W17 o %.1f m za daleko" + +msgid "W17 where the axes of the tracks meeting at a switch are the spacing apart,\nhalf way between them" +msgstr "W17 tam, gdzie osie torów zbiegających się na rozjeździe są o rozstaw od siebie,\nw połowie międzytorza" + +msgid "Whole straight" +msgstr "Cała prosta" + +msgid "adds a track to the selection, drag: a box" +msgstr "dodaje tor do zaznaczenia, przeciągnięcie: prostokąt" + +msgid "against it" +msgstr "przeciwnie" + +msgid "along the line" +msgstr "zgodnie z linią" + +msgid "cant jump" +msgstr "skok przechyłki" + +msgid "fixed" +msgstr "naprawiono" + +msgid "free end" +msgstr "wolny koniec" + +msgid "gap" +msgstr "szczelina" + +msgid "grade break" +msgstr "załamanie pochylenia" + +msgid "joins the ends, aligns them" +msgstr "łączy końce, wyrównuje je" + +msgid "kink" +msgstr "załamanie" + +msgid "km %s stands %+.1f m off##%u" +msgstr "km %s stoi %+.1f m od miejsca##%u" + +msgid "km %s, %+.1f m off" +msgstr "km %s, %+.1f m od miejsca" + +msgid "modes: lay, switch, straight, curve, profile, speed, joints, infra" +msgstr "tryby: układanie, rozjazd, prosta, łuk, profil, prędkość, styki, infrastruktura" + +msgid "show the place" +msgstr "pokazuje miejsce" + +msgid "step" +msgstr "uskok" + +msgid "texture of the rails" +msgstr "tekstura szyn" + +msgid "texture of the trackbed" +msgstr "tekstura podsypki" + +msgid "there's no active layer" +msgstr "nie ma aktywnej warstwy" + +msgid "track circuits, separated by spaces" +msgstr "obwody torowe, oddzielone spacjami" + +msgid "type the length" +msgstr "wpisz długość" + +msgid "type the length of the next leg, Enter adds the point" +msgstr "wpisz długość następnego odcinka, Enter dodaje punkt" + +msgid "type the radius, a space, the length" +msgstr "wpisz promień, spację, długość" + +msgid "type the shift, + to the left" +msgstr "wpisz przesunięcie, + w lewo" + +msgid "type the turn in degrees, + to the left" +msgstr "wpisz obrót w stopniach, + w lewo" + +msgid "what can be done with it" +msgstr "co można z nim zrobić" + +msgid "Vehicle on the track" +msgstr "Pojazd na torze" + +msgid "A vehicle to drive, placed where RMB on the track was clicked, or in the middle of the path.\nIt isn't written to the scenery files" +msgstr "Pojazd do jazdy, stawiany tam, gdzie kliknięto PPM na torze, albo w połowie odcinka.\nNie jest zapisywany do plików scenerii" + +msgid "search: name, series, skin" +msgstr "szukaj: nazwa, seria, malowanie" + +msgid "Only the ones with a cab" +msgstr "Tylko z kabiną" + +msgid "With a driver, to drive" +msgstr "Z maszynistą, do jazdy" + +msgid "Nobody, parked" +msgstr "Bez obsady, odstawiony" + +msgid "Pick a vehicle from the list" +msgstr "Wybierz pojazd z listy" + +msgid "Place" +msgstr "Postaw" + +msgid "Place and drive" +msgstr "Postaw i jedź" + +msgid "The vehicle couldn't be placed, the log says why" +msgstr "Nie udało się postawić pojazdu, przyczyna jest w logu" + +msgid "%s stands on %s. It isn't saved with the scenery" +msgstr "%s stoi na %s. Nie zapisuje się razem ze scenerią" + +msgid "Place a vehicle here" +msgstr "Postaw tu pojazd" + +msgid "Map" +msgstr "Mapa" + +msgid "Orthophoto" +msgstr "Ortofotomapa" + +msgid "Orthophoto settings..." +msgstr "Ustawienia ortofotomapy..." + +msgid "Aerial imagery of geoportal.gov.pl under the scenery, laid out by the origin of the scenery" +msgstr "Zdjęcia lotnicze z geoportal.gov.pl pod scenerią, ułożone według punktu odniesienia scenerii" + +msgid "in the scenery file: Y (easting) %.0f m, X (northing) %.0f m" +msgstr "w pliku scenerii: Y (wschód) %.0f m, X (północ) %.0f m" + +msgid "Use it" +msgstr "Użyj" + +msgid "Model along the track" +msgstr "Model wzdłuż toru" + +msgid "A model bent to follow the track, or repeated along it: platforms, walls, fences, barriers, lamps" +msgstr "Model wygięty wzdłuż toru albo powtarzany przy nim: perony, mury, płoty, bariery, latarnie" + +msgid "By this track:" +msgstr "Przy tym torze:" + +msgid "New one" +msgstr "Nowy" + +msgid "model, e.g. peronowe/1k96w200m" +msgstr "model, np. peronowe/1k96w200m" + +msgid "search the models" +msgstr "szukaj modeli" + +msgid "Skin" +msgstr "Skóra" + +msgid "The model is %.2f m long along the track" +msgstr "Model ma %.2f m długości wzdłuż toru" + +msgid "The model has no triangles to lay, or it can't be loaded" +msgstr "Model nie ma trójkątów do ułożenia albo nie da się go wczytać" + +msgid "Bent along the track" +msgstr "Wygięty wzdłuż toru" + +msgid "Repeated" +msgstr "Powtarzany" + +msgid "Copy length (m, 0: the model's)" +msgstr "Długość kopii (m, 0: jak model)" + +msgid "Step (m, 0: the model's length)" +msgstr "Krok (m, 0: długość modelu)" + +msgid "Sideways (m, + left)" +msgstr "W bok (m, + w lewo)" + +msgid "Up (m)" +msgstr "W górę (m)" + +msgid "Axis of the model along the track" +msgstr "Oś modelu wzdłuż toru" + +msgid "Turned around" +msgstr "Odwrócony" + +msgid "Mirrored" +msgstr "Lustrzany" + +msgid "Leans with the cant" +msgstr "Przechyla się z przechyłką" + +msgid "Along" +msgstr "Wzdłuż" + +msgid "this path" +msgstr "tego odcinka" + +msgid "the line to the switches" +msgstr "linii do rozjazdów" + +msgid "through the switches" +msgstr "przez rozjazdy" + +msgid "From (m)" +msgstr "Od (m)" + +msgid "To (m, -1: the end)" +msgstr "Do (m, -1: koniec)" + +msgid "LMB where it starts, the distance from the track sets the offset; drag to where it ends" +msgstr "LPM tam, gdzie się zaczyna (odległość od toru ustawia przesunięcie), przeciągnij do końca" + +msgid "Mark it in the view" +msgstr "Zaznacz w widoku" + +msgid "line %.0f m" +msgstr "linia %.0f m" + +msgid "Lay it" +msgstr "Ułóż" + +msgid "Lay it again along the selected line" +msgstr "Ułóż ponownie wzdłuż zaznaczonej linii" + +msgid "%s laid along %.1f m, Ctrl+Z takes it back" +msgstr "%s ułożony na %.1f m, Ctrl+Z cofa" + +msgid "The scenery isn't opened for editing, it won't be saved" +msgstr "Sceneria nie jest otwarta do edycji, to nie zostanie zapisane" + +msgid "type the shift, + to the right" +msgstr "wpisz przesunięcie, + w prawo" + +msgid "type the turn in degrees, + to the right" +msgstr "wpisz obrót w stopniach, + w prawo" + +msgid "Sideways (m, + right)" +msgstr "W bok (m, + w prawo)" + +msgid "model origin" +msgstr "początek modelu" + +msgid "side facing the track" +msgstr "strona od toru" + +msgid "far side" +msgstr "dalsza strona" + +msgid "middle" +msgstr "środek" + +msgid "Up (m, from the rail top)" +msgstr "W górę (m, od główki szyny)" + +msgid "bottom" +msgstr "spód" + +msgid "top" +msgstr "wierzch" + +msgid "Platform edge:" +msgstr "Krawędź peronu:" + +msgid "The edge of the platform %.3f m from the axis of the track, its top this high over the rail top" +msgstr "Krawędź peronu %.3f m od osi toru, wierzch tyle nad główką szyny" + +msgid "Parameters of the template" +msgstr "Parametry szablonu" + +msgid "The location and the rotation are zeros, the template is laid out along the track" +msgstr "Położenie i obrót są zerami, szablon układa się wzdłuż toru" + +msgid "top of the edge by the track" +msgstr "wierzch krawędzi od toru" + +msgid "Edge to the track" +msgstr "Krawędź do toru" + +msgid "The model is turned so its -x side faces the track, the way the platforms and the walls of the scenery are made" +msgstr "Model jest obrócony stroną -x do toru, tak jak są zrobione perony i mury w scenerii" + +msgid "Length" +msgstr "Długość" + +msgid "as the original" +msgstr "jak oryginał" + +msgid "given" +msgstr "podana" + +msgid "the whole stretch" +msgstr "cały zakres" + +msgid "%.1f m: %d copies of %.2f m (the original %.2f m)" +msgstr "%.1f m: %d kopii po %.2f m (oryginał %.2f m)" + +msgid "%.1f m: %d copies every %.2f m" +msgstr "%.1f m: %d kopii co %.2f m" + +msgid "The line ends before that, it's cut there" +msgstr "Linia kończy się wcześniej, tam zostanie ucięte" + +msgid "Set back by the widening of the structure gauge in the curves" +msgstr "Odsunięty o poszerzenie skrajni na łukach" + +msgid "The offset grows by the widening of the gauge below the platforms: 3.75/R in the curves of R >= 250 m,\nmore in the sharper ones, and on the inner side of a canted curve by the cant times the height of the edge / 1.5;\nit begins 20 m (inner side) and 26 m (outer side) ahead of the curve, as in the gauge check" +msgstr "Przesunięcie rośnie o poszerzenie skrajni poniżej peronów: 3,75/R na łukach R >= 250 m,\nwięcej na ciaśniejszych, a po wewnętrznej stronie łuku z przechyłką o przechyłkę razy wysokość krawędzi / 1,5;\nzaczyna się 20 m (strona wewnętrzna) i 26 m (zewnętrzna) przed łukiem, jak w sprawdzaniu skrajni" + +msgid "On the ground" +msgstr "Na gruncie" + +msgid "Over the rail heads" +msgstr "Nad główką szyny" + +msgid "by (m)" +msgstr "o (m)" + +msgid "A marker higher or lower by more (m) is wrong" +msgstr "Wskaźnik wyżej lub niżej o więcej (m) stoi źle" + +msgid "W17 %.1f m off, %+.2f m high" +msgstr "W17 %.1f m od miejsca, %+.2f m wysokości" + +msgid "alternately" +msgstr "naprzemiennie" + +msgid "The even hectometres on the right, the odd ones on the left" +msgstr "Parzyste hektometry po prawej, nieparzyste po lewej" + +msgid "Objects" +msgstr "Obiekty" + +msgid "Along the track: hectometre posts, fouling point markers, a vehicle to drive" +msgstr "Przy torze: słupki hektometrowe, wskaźniki ukresu, pojazd do jazdy" + +msgid "Model along the track: release where it ends" +msgstr "Model wzdłuż toru: puść tam, gdzie się kończy" + +msgid "Model along the track: LMB by the track where it starts, drag to the end" +msgstr "Model wzdłuż toru: LPM przy torze tam, gdzie się zaczyna, przeciągnij do końca" + +msgid "Objects along the track" +msgstr "Obiekty przy torze" + +msgid "LMB by the track" +msgstr "LPM przy torze" + +msgid "where it starts, the side of the track it goes on" +msgstr "tam, gdzie się zaczyna, po tej stronie toru" + +msgid "to where it ends, a click: one as long as the original" +msgstr "do końca; samo kliknięcie: jeden jak oryginał" + +msgid "lays it" +msgstr "układa" + +msgid "LMB away from the track" +msgstr "LPM z dala od toru" + +msgid "selects another track" +msgstr "zaznacza inny tor" + +msgid "the line the objects go along" +msgstr "linia, wzdłuż której idą obiekty" + +msgid "modes: lay, switch, straight, curve, objects, profile, speed, joints, infra" +msgstr "tryby: układanie, rozjazd, prosta, łuk, obiekty, profil, prędkość, styki, infrastruktura" + +msgid "Platforms and models" +msgstr "Perony i modele" + +msgid "Fouling points" +msgstr "Ukresy W17" + +msgid "Vehicle" +msgstr "Pojazd" + +msgid "LMB on a track: the line the objects go along" +msgstr "LPM na torze: linia, wzdłuż której idą obiekty" + +msgid "%zu duplicate hectometre posts removed, Ctrl+Z brings them back" +msgstr "Usunięto %zu zdublowanych słupków hektometrowych, Ctrl+Z je przywraca" + +msgid "Remove %d duplicate posts" +msgstr "Usuń %d zdublowanych słupków" + +msgid "Posts with a number another post nearer to its place already has" +msgstr "Słupki z numerem, który ma już inny słupek stojący bliżej swojego miejsca" + +msgid "km %s duplicate##%u" +msgstr "km %s zdublowany##%u" + +msgid "km %s stands %.1f m from its place##%u" +msgstr "km %s stoi %.1f m od swojego miejsca##%u" + +msgid "km %s duplicate" +msgstr "km %s zdublowany" + +msgid "km %s, %.1f m from its place" +msgstr "km %s, %.1f m od swojego miejsca" + +# scenery editor: what a click does + +msgid "Click: start from this free end, drag: lead a track out of it" +msgstr "Klik: zacznij od tego wolnego końca, przeciągnij: wyprowadź z niego tor" + +msgid "Click: join this end and lay" +msgstr "Klik: dołącz do tego końca i ułóż" + +msgid "Click: start of the new track" +msgstr "Klik: początek nowego toru" + +msgid "Click: next point" +msgstr "Klik: następny punkt" + +msgid "Press: where the model starts, drag to its end" +msgstr "Wciśnij: początek modelu, przeciągnij do jego końca" + +msgid "Drag: the tracks in a box join the selection" +msgstr "Przeciągnij: tory w ramce dołączą do zaznaczenia" + +msgid "Click: take out of the selection, drag: a box" +msgstr "Klik: usuń z zaznaczenia, przeciągnij: ramka" + +msgid "Click: add to the selection, drag: a box" +msgstr "Klik: dodaj do zaznaczenia, przeciągnij: ramka" + +msgid "Drag: the control vector, the curve follows" +msgstr "Przeciągnij: wektor kontrolny, krzywa podąża" + +msgid "Drag: the joint, the neighbours follow" +msgstr "Przeciągnij: styk, sąsiednie tory podążają" + +msgid "Drag: the end, dropped on a free end it joins it" +msgstr "Przeciągnij: koniec, upuszczony na wolny koniec łączy się z nim" + +msgid "Click: move this straight together with the others" +msgstr "Klik: przesuwaj tę prostą razem z pozostałymi" + +msgid "Press: break the straight here, drag to turn the rest" +msgstr "Wciśnij: załam prostą tutaj, przeciągnij, by obrócić resztę" + +msgid "Click: next point of the shift around an obstacle" +msgstr "Klik: następny punkt objazdu przeszkody" + +msgid "Click: break the straight here" +msgstr "Klik: załam prostą tutaj" + +msgid "Click: start of the shift" +msgstr "Klik: początek przesunięcia" + +msgid "Click: shift the straight sideways" +msgstr "Klik: przesuń prostą w bok" + +msgid "Click: move this end of the straight" +msgstr "Klik: przesuń ten koniec prostej" + +msgid "Click: move this vertex" +msgstr "Klik: przesuń ten wierzchołek" + +msgid "Click: change the radius of this curve" +msgstr "Klik: zmień promień tego łuku" + +msgid "Press and drag: a switch here, along sets its direction, sideways its side" +msgstr "Wciśnij i przeciągnij: rozjazd tutaj, wzdłuż ustala kierunek, w bok stronę" + +msgid "Click: the whole straight through this track" +msgstr "Klik: cała prosta przez ten tor" + +msgid "Click: the curve of this track with its straights" +msgstr "Klik: łuk tego toru z prostymi" + +msgid "Click: edit this switch" +msgstr "Klik: edytuj ten rozjazd" + +msgid "Click: choose this track, then the type of the switch" +msgstr "Klik: wybierz ten tor, potem typ rozjazdu" + +msgid "Click: put a switch into this track" +msgstr "Klik: wstaw rozjazd w ten tor" + +msgid "Click: the grade line through this track" +msgstr "Klik: niweleta przez ten tor" + +msgid "Click: the line the objects go along" +msgstr "Klik: linia, wzdłuż której idą obiekty" + +msgid "Click: select" +msgstr "Klik: zaznacz" + +msgid "Laying track: new path from the free end" +msgstr "Układanie toru: nowy odcinek z wolnego końca" + +msgid "Select: moving the point" +msgstr "Wybór: przesuwanie punktu" + +msgid "Switch: the selected one. Another: LMB on a track, then its type" +msgstr "Rozjazd: zaznaczony. Kolejny: LPM na torze, potem typ" + +msgid "Switch: choose its type from the list" +msgstr "Rozjazd: wybierz typ z listy" + +msgid "Drag from a free end" +msgstr "Przeciągnij z wolnego końca" + +msgid "straight along it, to another free end: a curve joining them" +msgstr "prosta wzdłuż niego, do innego wolnego końca: łuk łączący" + +msgid "moves the point, level" +msgstr "przesuwa punkt w poziomie" + +msgid "Over a free end" +msgstr "Nad wolnym końcem" + +msgid "the end joins it" +msgstr "koniec się z nim łączy" + +msgid "done" +msgstr "gotowe" + +msgid "moves it, an end dropped on a free end joins it" +msgstr "przesuwa go, koniec upuszczony na wolny koniec łączy się" + +msgid "Gizmo" +msgstr "Gizmo" + +msgid "moves the point along the axes" +msgstr "przesuwa punkt wzdłuż osi" + +msgid "L" +msgstr "L" + +msgid "new track, also from a free end" +msgstr "nowy tor, także z wolnego końca" + +msgid "Esc" +msgstr "Esc" + +msgid "deselects" +msgstr "odznacza" + +msgid "List below" +msgstr "Lista poniżej" + +msgid "choose the type of the switch to put in" +msgstr "wybierz typ rozjazdu do wstawienia" + +msgid "choose the straight or the curve" +msgstr "wybierz prostą albo łuk" + +msgid "Dragging from a free end draws a curve through the cursor; off: it draws a straight along the end" +msgstr "Przeciąganie z wolnego końca rysuje łuk przez kursor; wyłączone: prostą wzdłuż końca" + +msgid "Enter" +msgstr "Enter" + +msgid "lay up to the cursor" +msgstr "ułóż do kursora" + +msgid "Shift" +msgstr "Shift" + +msgid "straight on in the direction of the track" +msgstr "prosto w kierunku toru" + +msgid "straight, joins nothing" +msgstr "prosto, bez łączenia" + +msgid "moves the whole track" +msgstr "przesuwa cały tor" + +msgid "The gizmo moves them together; Shift+LMB adds or takes out a path" +msgstr "Gizmo przesuwa je razem; Shift+LPM dodaje albo usuwa odcinek" + +msgid "Double slip switches" +msgstr "Rozjazdy krzyżowe podwójne" + +msgid "Only at a crossing of two straights" +msgstr "Tylko na skrzyżowaniu dwóch prostych" + +msgid "Drag: shift the straight sideways" +msgstr "Przeciągnij: przesuń prostą w bok" + +msgid "Drag: move this end, the curves at it follow" +msgstr "Przeciągnij: przesuń ten koniec, łuki przy nim podążają" + +msgid "Drag: move this vertex, the curve follows" +msgstr "Przeciągnij: przesuń wierzchołek, łuk podąża" + +msgid "Drag: the radius of this curve" +msgstr "Przeciągnij: promień tego łuku" + +msgid "Compound curve (several radii)" +msgstr "Łuk koszowy (kilka promieni)" + +msgid "Curve made of arcs of different radii, each one its own path, joined with transitions" +msgstr "Łuk z kilku łuków o różnych promieniach, każdy osobnym odcinkiem, połączonych krzywymi przejściowymi" + +msgid "Share of R%d" +msgstr "Udział R%d" + +msgid "Radius R%d (m)" +msgstr "Promień R%d (m)" + +msgid "Transition R%d-R%d (m)" +msgstr "Przejściowa R%d-R%d (m)" + +msgid "Takes this radius out of the curve" +msgstr "Usuwa ten promień z łuku" + +msgid "Add a radius" +msgstr "Dodaj promień" + +msgid "Another arc after the last one, the angle of the curve is shared out by the shares" +msgstr "Kolejny łuk za ostatnim; kąt zwrotu dzielony według udziałów" + +msgid "Formation under the ballast of the track" +msgstr "Korona pod podsypką toru" + +msgid "The terrain goes as deep under the track as its ballast reaches and as wide as its foot,\nread from each path of the route; off: the values given below" +msgstr "Teren schodzi pod torem tak głęboko, jak sięga jego podsypka, i tak szeroko, jak jej podstawa,\nodczytane z każdego odcinka trasy; wyłączone: wartości podane niżej" + +msgid "Ballast %.2f m deep, its foot %.2f m from the axis" +msgstr "Podsypka %.2f m, jej podstawa %.2f m od osi" + +msgid "Formation below the track (m)" +msgstr "Korona poniżej toru (m)" + +msgid "Name##switchname" +msgstr "Nazwa##switchname" + +msgid "Name of the next switch, its drive events are named after it; empty: the next free z1, z2...; a number at the end goes up with each switch" +msgstr "Nazwa następnej zwrotnicy, od niej nazywają się zdarzenia napędu; puste: kolejna wolna z1, z2...; liczba na końcu rośnie z każdą zwrotnicą" + +msgid "No switch drives in scenery/tory/napedy" +msgstr "Brak napędów zwrotnicowych w scenery/tory/napedy" + +msgid "Drive" +msgstr "Napęd" + +msgid "Drive of the switch from scenery/tory/napedy: its events + and - switch it with the keys by the switch and in the map during the game; none: the switch can't be switched" +msgstr "Napęd zwrotnicy ze scenery/tory/napedy: jego zdarzenia + i - przestawiają ją klawiszami przy zwrotnicy i w podglądzie mapy podczas gry; brak: zwrotnicy nie da się przestawić" + +msgid "diverging side" +msgstr "od toru zwrotnego" + +msgid "other side" +msgstr "z drugiej strony" + +msgid "The drive couldn't be placed: " +msgstr "Nie udało się wstawić napędu: " + +msgid "Switch %s with the drive %s: events %s+ and %s- switch it, also in the map" +msgstr "Zwrotnica %s z napędem %s: zdarzenia %s+ i %s- ją przestawiają, także w mapie" + +msgid "Drive: events %s+ and %s- switch it, also in the map" +msgstr "Napęd: zdarzenia %s+ i %s- ją przestawiają, także w mapie" + +msgid "No drive: the switch can't be switched during the game" +msgstr "Brak napędu: zwrotnicy nie da się przestawić podczas gry" + +msgid "Put the drive by the switch" +msgstr "Postaw napęd przy zwrotnicy" + +msgid "no drive" +msgstr "bez napędu" + +msgid "The drive %s has no variant for a %s switch with the drive on the %s" +msgstr "Napęd %s nie ma wariantu dla rozjazdu %s z napędem z %s" + +msgid "No switch drives: no template is described with category: switch drive" +msgstr "Brak napędów: żaden wzorzec nie ma opisu z category: switch drive" + +msgid "Templates described with category: switch drive, the side of the switch and of the drive, and a parameter with the role track for the name of the switch.\nIts events + and - switch it with the keys by the switch and in the map during the game" +msgstr "Wzorce opisane jako category: switch drive, ze stroną rozjazdu i napędu oraz parametrem o roli track na nazwę zwrotnicy.\nIch zdarzenia + i - przestawiają zwrotnicę klawiszami przy niej i w podglądzie mapy podczas gry" + +msgid "Track editor: the window with its modes opens or closes" +msgstr "Edytor torów: otwiera albo zamyka okno z jego trybami" + +msgid "Look along" +msgstr "Szukaj wzdłuż" + +msgid "Route of the vertical profile" +msgstr "Trasa profilu podłużnego" + +msgid "%d read by the paths" +msgstr "%d odczytywanych przez tory" + +msgid "The memory cell of the object is read by a getvalues event of a path, listed by the path, inside a multiple\nevent or written loose as :event. The object is bound to the thread of that path, not to the nearest one" +msgstr "Komórkę pamięci obiektu odczytuje zdarzenie getvalues toru: wpisane w torze, wewnątrz zdarzenia multiple\nalbo dopisane luzem jako :event. Obiekt zostaje przypisany do nitki tego toru, nie do najbliższej" + +msgid "%d of them nearer to another path" +msgstr "w tym %d bliżej innego toru" + +msgid "By the distance alone these would go to the wrong thread. Check them in 3D: the dashed line goes to the nearer path" +msgstr "Po samej odległości trafiłyby do złej nitki. Sprawdź je w 3D: przerywana linia prowadzi do bliższego toru" + +msgid "%d signal(s) not read by any path" +msgstr "%d sygnalizator(ów) bez odczytu przez tor" + +msgid "The signal has a memory cell, but no event of a path reads it: the vehicles won't see it.\nBound to the nearest path" +msgstr "Sygnalizator ma komórkę pamięci, ale żadne zdarzenie toru jej nie odczytuje: pojazdy go nie zobaczą.\nPrzypisany do najbliższego toru" + +msgid "Read by a path" +msgstr "Odczytywane przez tor" + +msgid "Nearer to another path" +msgstr "Bliżej innego toru" + +msgid "Not read" +msgstr "Bez odczytu" + +msgid "\nRead by the path %s: event %s, getvalues of the memory cell %s" +msgstr "\nOdczytywany przez tor %s: zdarzenie %s, getvalues komórki pamięci %s" + +msgid " (written loose, the path takes it by the name)" +msgstr " (dopisane luzem, tor bierze je po nazwie)" + +msgid "\nThe path %s is nearer, but it doesn't read the object" +msgstr "\nTor %s jest bliżej, ale nie odczytuje obiektu" + +msgid "\nNo path reads the memory cell of the signal" +msgstr "\nŻaden tor nie odczytuje komórki pamięci sygnalizatora" + +msgid "\nClick to look at it" +msgstr "\nKliknij, żeby go obejrzeć" + +msgid "read by %s" +msgstr "odczyt: %s" + +msgid "(nearer: %s)" +msgstr "(bliżej: %s)" + +msgid "not read" +msgstr "bez odczytu" + +msgid "; %d left out: they stand by the scope, but the paths which read them are elsewhere" +msgstr "; pominięto %d: stoją przy zakresie, ale tory, które je odczytują, są gdzie indziej" + +msgid "Name: %s" +msgstr "Nazwa: %s" + +msgid "(can't be changed: %s)" +msgstr "(nie można zmienić: %s)" + +msgid "Renamed from %s" +msgstr "Zmieniono z %s" + +msgid "Name##trackname" +msgstr "Nazwa##trackname" + +msgid "Enter renames the path, Esc while typing takes the text back.\nThe events, memory cells and the map of the game find the path by its name" +msgstr "Enter zmienia nazwę toru, Esc w trakcie pisania przywraca tekst.\nZdarzenia, komórki pamięci i mapa w grze znajdują tor po nazwie" + +msgid "Rename anyway" +msgstr "Zmień mimo to" + +msgid "Rename" +msgstr "Zmień nazwę" + +msgid "Keep the old" +msgstr "Zostaw starą" + +msgid "The path needs a name" +msgstr "Tor musi mieć nazwę" + +msgid "The name can't hold blanks, quotes, semicolons or //" +msgstr "Nazwa nie może zawierać spacji, cudzysłowów, średników ani //" + +msgid "Another path has this name" +msgstr "Inny tor ma już tę nazwę" + +msgid "Saved as %s: the scenery keeps the names in lower case" +msgstr "Zapisana jako %s: sceneria trzyma nazwy małymi literami" + +msgid "The new name goes to %d memory cell(s) and %d include(s), the drives of a switch among them" +msgstr "Nowa nazwa trafi do %d komórek pamięci i %d includów, w tym napędów zwrotnicy" + +msgid "These events aim at the path by the old name and the editor doesn't write them: %s" +msgstr "Te zdarzenia wskazują tor starą nazwą, a edytor ich nie zapisuje: %s" + +msgid "These events belong to the path by the old name: %s" +msgstr "Te zdarzenia należą do toru przez starą nazwę: %s" + +msgid "They work until the scenery is loaded again; then they need the new name in their files" +msgstr "Działają do ponownego wczytania scenerii; potem trzeba im wpisać nową nazwę w plikach" + +msgid "(no name)" +msgstr "(bez nazwy)" + +msgid "Turntable" +msgstr "Obrotnica" + +msgid "Places a turntable, leads tracks out of it and fits the tracks around to it" +msgstr "Stawia obrotnicę, wyprowadza z niej tory i dopasowuje do niej tory dookoła" + +msgid "Turntable: drag to turn the bridge, release to place it" +msgstr "Obrotnica: przeciągnij, żeby obrócić most, puść, żeby postawić" + +msgid "Turntable: LMB around it leads a track out, on a free end fits that track" +msgstr "Obrotnica: LPM dookoła wyprowadza tor, na wolnym końcu dopasowuje ten tor" + +msgid "Turntable: LMB places a new one" +msgstr "Obrotnica: LPM stawia nową" + +msgid "It doesn't line up with any position of the turntable" +msgstr "Nie trafia w żadne położenie obrotnicy" + +msgid "Too close to the turntable for a curve of %.0f m or more" +msgstr "Za blisko obrotnicy na łuk o promieniu %.0f m lub większym" + +msgid "It points away from the turntable" +msgstr "Jest skierowany od obrotnicy" + +msgid "No free ends within %.0f m of the rim" +msgstr "Brak wolnych końców w promieniu %.0f m od krawędzi" + +msgid "%d track(s) led to the turntable" +msgstr "Doprowadzono %d tor(ów) do obrotnicy" + +msgid "The turntable has %d positions, %d track lines lead to it: the rest won't be reached" +msgstr "Obrotnica ma %d położeń, a dochodzi do niej %d linii torów: do reszty nie dojedzie" + +msgid "The name %s is taken by another path" +msgstr "Nazwę %s ma już inny tor" + +msgid "Turntable %s: click around it for the tracks out of it, or fit the tracks around. It turns after the scenery is saved and loaded again" +msgstr "Obrotnica %s: klikaj dookoła, żeby wyprowadzić tory, albo dopasuj tory wokół. Obraca się po zapisaniu i ponownym wczytaniu scenerii" + +msgid "Track out of the turntable at %.1f deg, %.1f m" +msgstr "Tor z obrotnicy pod kątem %.1f st., %.1f m" + +msgid "Click: lead this track to the turntable" +msgstr "Kliknij: doprowadź ten tor do obrotnicy" + +msgid "Click: a track out of the turntable, %.1f deg, %.1f m" +msgstr "Kliknij: tor z obrotnicy, %.1f st., %.1f m" + +msgid "Click: this turntable" +msgstr "Kliknij: ta obrotnica" + +msgid "Click: a turntable at the end of this track" +msgstr "Kliknij: obrotnica na końcu tego toru" + +msgid "Press: the centre of a new turntable, drag: the direction of its bridge" +msgstr "Wciśnij: środek nowej obrotnicy, przeciągnij: kierunek jej mostu" + +msgid "Turntable %s, bridge %.1f m" +msgstr "Obrotnica %s, most %.1f m" + +msgid "(positions set by the tracks around, up to %d)" +msgstr "(położenia według torów dookoła, do %d)" + +msgid "(%d positions fixed by the template)" +msgstr "(%d położeń ustalonych we wzorcu)" + +msgid "No turntable template takes this path as its track: the positions aren't known" +msgstr "Żaden wzorzec obrotnicy nie bierze tego toru: położenia nie są znane" + +msgid "Positions (%d)" +msgstr "Położenia (%d)" + +msgid "track" +msgstr "tor" + +msgid "track on the other end" +msgstr "tor na drugim końcu" + +msgid "no track" +msgstr "brak toru" + +msgid "Tracks around" +msgstr "Tory dookoła" + +msgid "Reach from the rim, m" +msgstr "Zasięg od krawędzi, m" + +msgid "Radius of the curves, m" +msgstr "Promień łuków, m" + +msgid "Angle step of a new track, deg (0: free)" +msgstr "Krok kąta nowego toru, st. (0: dowolny)" + +msgid "Click around the turntable: a track out of it, as long as the cursor is far.\nClick a free end near it: that track is led to the rim, straight or with a curve" +msgstr "Kliknij dookoła obrotnicy: tor z niej, tak długi, jak daleko jest kursor.\nKliknij wolny koniec w pobliżu: ten tor zostanie doprowadzony do krawędzi, prosto albo łukiem" + +msgid "Find the free ends around" +msgstr "Znajdź wolne końce dookoła" + +msgid "Lead the chosen to the turntable (%d)" +msgstr "Doprowadź wybrane do obrotnicy (%d)" + +msgid "%.1f deg, curve R %.0f m, %.1f m" +msgstr "%.1f st., łuk R %.0f m, %.1f m" + +msgid "%.1f deg, straight %.1f m" +msgstr "%.1f st., prosta %.1f m" + +msgid "in place of its last straight %s" +msgstr "w miejsce jego ostatniej prostej %s" + +msgid "This turntable can't be read" +msgstr "Nie da się odczytać tej obrotnicy" + +msgid "New turntable" +msgstr "Nowa obrotnica" + +msgid "No turntables: no template is described with category: turntable and its turntable: {length}" +msgstr "Brak obrotnic: żaden wzorzec nie ma opisu category: turntable z turntable: {length}" + +msgid "%s %.0f m, up to %d positions set by the tracks" +msgstr "%s %.0f m, do %d położeń według torów" + +msgid "%s %.0f m, %d fixed positions" +msgstr "%s %.0f m, %d stałych położeń" + +msgid "Name##turntablename" +msgstr "Nazwa##turntablename" + +msgid "Press where the centre goes and drag to turn the bridge.\nClick a free end of a track: the turntable lines up with it, at its end. Shift: no lining up" +msgstr "Wciśnij w miejscu środka i przeciągnij, żeby obrócić most.\nKliknij wolny koniec toru: obrotnica ustawi się w jego osi, na jego końcu. Shift: bez dopasowania" + +msgid "The scenery has its own ground under the route: convert it to editor terrain first" +msgstr "Sceneria ma pod trasą własny teren: najpierw zamień go na teren edytora" + +msgid "; %d chunk(s) of terrain made under the route" +msgstr "; utworzono %d kawałków terenu pod trasą" + +msgid "; %d chunk(s) left out: the scenery has its own ground there, convert it to shape it" +msgstr "; pominięto %d kawałków: sceneria ma tam własny teren, zamień go, żeby go kształtować" + +msgid "Make terrain where the route has none" +msgstr "Utwórz teren tam, gdzie trasa go nie ma" + +msgid "Chunks of editor terrain are made along the route where there's none, from the ground around,\nthen shaped. Where the scenery has its own ground triangles the chunk is left out: convert them instead" +msgstr "Wzdłuż trasy, tam gdzie go brak, powstają kawałki terenu edytora z wysokości otoczenia,\npotem są kształtowane. Tam, gdzie sceneria ma własne trójkąty terenu, kawałek jest pomijany: zamień je zamiast tego" + +msgid "Puts back the terrain as it was before the last shaping, and takes away the terrain it made" +msgstr "Przywraca teren sprzed ostatniego kształtowania i usuwa teren, który przy tym powstał" + +msgid "Click: start from this free end, drag: the curve of the switch goes on, Shift: straight" +msgstr "Kliknij: zacznij od tego wolnego końca, przeciągnij: łuk rozjazdu biegnie dalej, Shift: prosta" + +msgid "%s and %d vehicle(s) behind it stand on %s. It isn't saved with the scenery" +msgstr "%s i %d pojazd(ów) za nim stoją na %s. Nie zapisuje się ze scenerią" + +msgid "Add to the train" +msgstr "Dodaj do składu" + +msgid "Train: %d vehicle(s), the first one leads" +msgstr "Skład: %d pojazd(ów), pierwszy prowadzi" + +msgid "Turned around in the train" +msgstr "Obrócony w składzie" + +msgid "up" +msgstr "w górę" + +msgid "Empty the train" +msgstr "Wyczyść skład" + +msgid "Turn the train around" +msgstr "Obróć skład" + +msgid "the head goes where the arrow at the marker points" +msgstr "czoło jedzie tam, gdzie wskazuje strzałka przy znaczniku" + +msgid "Trainset turned around" +msgstr "Skład odwrócony" + +msgid "The first vehicle stays where the trainset starts and leads the other way,\nthe rest of the trainset stands behind it" +msgstr "Pierwszy pojazd zostaje w miejscu początku składu i prowadzi w drugą stronę,\nreszta składu stoi za nim" + +msgid "Click: take out the %zu joined paths, %.0f m" +msgstr "Kliknij: wyjmij %zu połączonych torów, %.0f m" + +msgid "Click: add the %zu joined paths, %.0f m" +msgstr "Kliknij: dodaj %zu połączonych torów, %.0f m" + +msgid "Spread along the joins: Ctrl+Shift+LMB on a path" +msgstr "Rozlewanie po połączeniach: Ctrl+Shift+LPM na torze" + +msgid "Stop before the switches" +msgstr "Zatrzymaj przed rozjazdami" + +msgid "Through the switches, along their main track" +msgstr "Przez rozjazdy, po ich torze zasadniczym" + +msgid "Through everything joined" +msgstr "Przez wszystko, co połączone" + +msgid "Only to the paths like the clicked one:" +msgstr "Tylko do torów takich jak kliknięty:" + +msgid "rails" +msgstr "szyny" + +msgid "trackbed" +msgstr "podsypka" + +msgid "speed" +msgstr "prędkość" + +msgid "Reach along the track, m (0: no limit)" +msgstr "Zasięg wzdłuż toru, m (0: bez limitu)" + +msgid "Holding Ctrl+Shift shows what the click takes; on a path of the set it takes them out" +msgstr "Trzymając Ctrl+Shift widać, co kliknięcie zabierze; na torze ze zbioru je wyjmuje" + +msgid "Grow by the neighbours" +msgstr "Poszerz o sąsiadów" + +msgid "Adds the paths joined to the set, by the rules of the spread" +msgstr "Dodaje tory połączone ze zbiorem, według zasad rozlewania" + +msgid "New scenery" +msgstr "Nowa sceneria" + +msgid "E %.3f km, N %.3f km (%.5f N, %.5f E)" +msgstr "E %.3f km, N %.3f km (%.5f N, %.5f E)" + +msgid "loading the map..." +msgstr "wczytywanie mapy..." + +msgid "The map service sent no image" +msgstr "Serwis mapy nie odesłał obrazu" + +msgid "No map: " +msgstr "Brak mapy: " + +msgid "Find the place on the map: the wheel zooms, dragging moves the map. A click sets the centre of the scenery, its point 0 0 0." +msgstr "Znajdź miejsce na mapie: kółko przybliża, przeciąganie przesuwa mapę. Kliknięcie ustala środek scenerii, jej punkt 0 0 0." + +msgid "file name, e.g. my_line" +msgstr "nazwa pliku, np. moja_linia" + +msgid "scenery/.scn" +msgstr "scenery/.scn" + +msgid "Lower case letters, digits, _ and - only" +msgstr "Tylko małe litery, cyfry, _ i -" + +msgid "%s exists already" +msgstr "%s już istnieje" + +msgid "title, written in the file" +msgstr "tytuł, wpisany do pliku" + +msgid "Centre of the scenery, PUWG 1992" +msgstr "Środek scenerii, PUWG 1992" + +msgid "E, km##east" +msgstr "E, km##east" + +msgid "N, km##north" +msgstr "N, km##north" + +msgid "latitude, longitude" +msgstr "szerokość, długość geograficzna" + +msgid "Show" +msgstr "Pokaż" + +msgid "Give the latitude and the longitude in degrees, e.g. 52.2297, 21.0122" +msgstr "Podaj szerokość i długość geograficzną w stopniach, np. 52.2297, 21.0122" + +msgid "Degrees, as given by the online maps; the map moves there" +msgstr "Stopnie, jak podają mapy internetowe; mapa się tam przesunie" + +msgid "%.5f N, %.5f E" +msgstr "%.5f N, %.5f E" + +msgid "The scenery keeps the centre as its geographic reference (//$g PUWG1992): the orthophoto of the editor lies under it, in place." +msgstr "Sceneria zapamiętuje środek jako odniesienie geograficzne (//$g PUWG1992): ortofotomapa edytora leży pod nią na swoim miejscu." + +msgid "Give the file a name" +msgstr "Nadaj plikowi nazwę" + +msgid "Click the centre on the map" +msgstr "Kliknij środek na mapie" + +msgid "Create and open in the editor" +msgstr "Utwórz i otwórz w edytorze" + +msgid "The file can't be written" +msgstr "Nie da się zapisać pliku" + +msgid "Map: (c) OpenStreetMap contributors, served by geoportal.gov.pl" +msgstr "Mapa: (c) współtwórcy OpenStreetMap, udostępniana przez geoportal.gov.pl" + +msgid "Along the selected path; dragged past its end the model goes on along the joined paths, at a switch along the branch nearer the cursor" +msgstr "Wzdłuż zaznaczonego toru; przeciągnięty za jego koniec model idzie dalej po połączonych torach, na rozjeździe gałęzią bliższą kursorowi" + +msgid "every (m, 0: its length)" +msgstr "co (m, 0: jego długość)" + +msgid "%.2f m %s of the axis, %.2f m over the rail top" +msgstr "%.2f m %s od osi, %.2f m nad główką szyny" + +msgid "Press beside the track where the model begins and drag along to where it ends; the side of the press is the side of the model. Past the end of the path it goes on along the joined ones. A click lays one copy." +msgstr "Wciśnij obok toru tam, gdzie model się zaczyna, i przeciągnij wzdłuż do jego końca; strona wciśnięcia to strona modelu. Za końcem toru idzie dalej po połączonych. Kliknięcie kładzie jedną kopię." + +msgid "Lay it along the selected path" +msgstr "Połóż wzdłuż zaznaczonego toru" + +msgid "New scenery..." +msgstr "Nowa sceneria..." + +msgid "The editor starts again with the wizard of a new scenery: its name and its centre on the map" +msgstr "Edytor uruchamia się od nowa z kreatorem nowej scenerii: jej nazwa i środek na mapie" + +msgid "New scenery##restart" +msgstr "Nowa sceneria##restart" + +msgid "The editor starts again, with the wizard of a new scenery." +msgstr "Edytor uruchomi się od nowa, z kreatorem nowej scenerii." + +msgid "What isn't saved of this scenery is lost." +msgstr "To, co nie jest zapisane w tej scenerii, przepadnie." + +msgid "Save and start the wizard" +msgstr "Zapisz i otwórz kreator" + +msgid "Start the wizard" +msgstr "Otwórz kreator" + +msgid "The editor couldn't be started again" +msgstr "Nie udało się ponownie uruchomić edytora" + +# scenery editor: models bent along the track, opening another scenery + +msgid "Bend along the track" +msgstr "Gięcie wzdłuż toru" + +msgid "Bend it along the track" +msgstr "Wygnij wzdłuż toru" + +msgid "Select a model or an include standing by a track" +msgstr "Zaznacz model albo include stojący przy torze" + +msgid "No track within %.0f m of the model" +msgstr "Brak toru w promieniu %.0f m od modelu" + +msgid "axis of the model along the track" +msgstr "oś modelu wzdłuż toru" + +msgid "the one nearest the track" +msgstr "najbliższa kierunkowi toru" + +msgid "Reference point: the model bends about it, the line through it keeps its length, the offsets are its place" +msgstr "Punkt odniesienia: wokół niego model się wygina, linia przez niego zachowuje długość, odsunięcia podają jego położenie" + +msgid "along the track" +msgstr "wzdłuż toru" + +msgid "across" +msgstr "w poprzek" + +msgid "start" +msgstr "początek" + +msgid "end" +msgstr "koniec" + +msgid "picked point" +msgstr "wskazany punkt" + +msgid "point (x, y, z of the model)" +msgstr "punkt (x, y, z modelu)" + +msgid "Pick the point on the model" +msgstr "Wskaż punkt na modelu" + +msgid "Click the point on the model..." +msgstr "Kliknij punkt na modelu..." + +msgid "The next click in the view sets the reference point where it lands on the model" +msgstr "Następne kliknięcie w widoku ustawia punkt odniesienia tam, gdzie trafi w model" + +msgid "Reference point %.2f %.2f %.2f in the model" +msgstr "Punkt odniesienia %.2f %.2f %.2f w modelu" + +msgid "The model stays where it stands and bends with the track; Ctrl+Z gives it back" +msgstr "Model zostaje tam, gdzie stoi, i wygina się z torem; Ctrl+Z go przywraca" + +msgid "The model stands %.1f deg off the track, bent it'll follow the track" +msgstr "Model stoi %.1f° w stosunku do toru, po wygięciu pójdzie wzdłuż toru" + +msgid "The model has no triangles to bend, or it can't be loaded" +msgstr "Model nie ma trójkątów do wygięcia albo nie da się go wczytać" + +msgid "The model lies before the start of the track" +msgstr "Model leży przed początkiem toru" + +msgid "%s bent along %.1f m of the track, Ctrl+Z takes it back" +msgstr "%s wygięty na %.1f m toru, Ctrl+Z cofa" + +msgid "%.2f m along the reference line, %d copies (the model is %.2f m)" +msgstr "%.2f m wzdłuż linii odniesienia, kopii: %d (model ma %.2f m)" + +msgid "from the track axis (m, + right)" +msgstr "od osi toru (m, + w prawo)" + +msgid "over the rail head (m)" +msgstr "nad główką szyny (m)" + +msgid "along the track (m)" +msgstr "wzdłuż toru (m)" + +msgid "length (m)" +msgstr "długość (m)" + +msgid "As long as the model" +msgstr "Długość modelu" + +msgid "Along the whole track" +msgstr "Na całym torze" + +msgid "Bent" +msgstr "Wygięty" + +msgid "copy every (m, 0: its length)" +msgstr "kopia co (m, 0: jej długość)" + +msgid "Done" +msgstr "Gotowe" + +msgid "Bent models by this track:" +msgstr "Wygięte modele przy tym torze:" + +msgid "Bent models nearby:" +msgstr "Wygięte modele w pobliżu:" + +msgid "New model along the selected track" +msgstr "Nowy model wzdłuż zaznaczonego toru" + +msgid "Open scenery..." +msgstr "Otwórz scenerię..." + +msgid "The editor starts again with another scenery of the scenery folder" +msgstr "Edytor uruchamia się ponownie z inną scenerią z folderu scenery" + +msgid "Open scenery##restart" +msgstr "Otwórz scenerię##restart" + +msgid "The editor starts again with the scenery chosen below." +msgstr "Edytor uruchomi się ponownie ze scenerią wybraną poniżej." + +msgid "search" +msgstr "szukaj" + +msgid "Save and open" +msgstr "Zapisz i otwórz" + +msgid "In the view: drag the blue cross sideways to set the offset, with Shift up and down, with Ctrl along the track" +msgstr "W widoku: przeciągnij niebieski celownik w bok, żeby ustawić odsunięcie, z Shift w górę i w dół, z Ctrl wzdłuż toru" + +msgid "In the view: drag the blue cross along the track and aside; Shift: height; Ctrl: only along" +msgstr "W widoku: przeciągnij niebieski celownik wzdłuż toru i w bok; Shift: wysokość; Ctrl: tylko wzdłuż" + +msgid "Drag: along the track and aside" +msgstr "Przeciągnij: wzdłuż toru i w bok" + +msgid "Shift: height Ctrl: only along the track" +msgstr "Shift: wysokość Ctrl: tylko wzdłuż toru" + +msgid "%.2f m from the track, %+.2f m along" +msgstr "%.2f m od toru, %+.2f m wzdłuż" + +msgid "Drag: offset from the track; Shift: height; Ctrl: along the track" +msgstr "Przeciągnij: odsunięcie od toru; Shift: wysokość; Ctrl: wzdłuż toru" + +msgid "%.2f m over the rail head" +msgstr "%.2f m nad główką szyny" + +msgid "%.2f m from the track axis" +msgstr "%.2f m od osi toru" + +msgid "%+.2f m along the track" +msgstr "%+.2f m wzdłuż toru" + +# scenery editor: general mode, toolset, layers, settings + +msgid "LMB: select node F: focus End: drop to ground Del: delete" +msgstr "LPM: zaznacz węzeł F: pokaż End: opuść na ziemię Del: usuń" + +msgid "LMB: copy the clicked model to the node bank" +msgstr "LPM: skopiuj kliknięty model do banku węzłów" + +msgid "LMB: insert a model at the cursor" +msgstr "LPM: wstaw model w miejscu kursora" + +msgid "Random model from set" +msgstr "Losowy model z zestawu" + +msgid "%zu templates in set%s" +msgstr "%zu szablonów w zestawie%s" + +msgid "Inserts the template selected in the node bank" +msgstr "Wstawia szablon zaznaczony w banku węzłów" + +msgid "Hold LMB: paint models along the cursor path" +msgstr "Przytrzymaj LPM: maluj modele wzdłuż drogi kursora" + +msgid "Node properties" +msgstr "Właściwości węzła" + +msgid "Array" +msgstr "Tablica" + +msgid "Ctrl+S" +msgstr "Ctrl+S" + +msgid "Ctrl+Shift+F11" +msgstr "Ctrl+Shift+F11" + +msgid "name" +msgstr "nazwa" + +msgid "(none)" +msgstr "(brak)" + +msgid "Name the events can refer to the node by.\nNodes without a name load quicker, leave it empty unless something needs it" +msgstr "Nazwa, po której zdarzenia mogą odwołać się do węzła.\nWęzły bez nazwy wczytują się szybciej, zostaw puste, jeśli nic jej nie potrzebuje" + +msgid "Transform" +msgstr "Przekształcenie" + +msgid "rotation (deg)" +msgstr "obrót (°)" + +msgid "scale (x,y,z)" +msgstr "skala (x,y,z)" + +msgid "reset rotation" +msgstr "zeruj obrót" + +msgid "reset scale" +msgstr "zeruj skalę" + +msgid "Parent Group" +msgstr "Grupa nadrzędna" + +msgid "Spacing" +msgstr "Odstęp" + +msgid "Paints the template selected in the node bank" +msgstr "Maluje szablonem zaznaczonym w banku węzłów" + +msgid "Reload node bank" +msgstr "Wczytaj bank węzłów ponownie" + +msgid "Search" +msgstr "Szukaj" + +msgid "Search node bank" +msgstr "Szukaj w banku węzłów" + +msgid "Scenery templates (.inc)" +msgstr "Szablony scenerii (.inc)" + +msgid "User sets" +msgstr "Zestawy użytkownika" + +msgid " (none yet, see Model sets...)" +msgstr " (jeszcze żadnych, zobacz Zestawy modeli...)" + +msgid "Node bank groups" +msgstr "Grupy banku węzłów" + +msgid "(empty)" +msgstr "(pusto)" + +msgid "Add selected" +msgstr "Dodaj zaznaczony" + +msgid "Adds the template selected in the node bank list" +msgstr "Dodaje szablon zaznaczony na liście banku węzłów" + +msgid "Remove" +msgstr "Usuń" + +msgid "Save as set" +msgstr "Zapisz jako zestaw" + +msgid "Model sets" +msgstr "Zestawy modeli" + +msgid "No sets yet" +msgstr "Brak zestawów" + +msgid "New" +msgstr "Nowy" + +msgid "Duplicate" +msgstr "Powiel" + +msgid "Delete set \"%s\"?" +msgstr "Usunąć zestaw \"%s\"?" + +msgid "Delete##confirm" +msgstr "Usuń##confirm" + +msgid "Create a set with \"New\", or use \"Save as set\" on a brush or area fill list." +msgstr "Utwórz zestaw przyciskiem \"Nowy\" albo użyj \"Zapisz jako zestaw\" na liście pędzla lub wypełniania obszaru." + +msgid "Sets can be chosen in Insert (random model), Brush and Area fill." +msgstr "Zestawy można wybrać we Wstawianiu (losowy model), Pędzlu i Wypełnianiu obszaru." + +msgid "(empty) select a template in the node bank and press \"Add selected\"" +msgstr "(pusto) zaznacz szablon w banku węzłów i naciśnij \"Dodaj zaznaczony\"" + +msgid "Remove all templates from this set?" +msgstr "Usunąć wszystkie szablony z tego zestawu?" + +msgid "Clear##confirm" +msgstr "Wyczyść##confirm" + +msgid "Add group" +msgstr "Dodaj grupę" + +msgid "Rotation" +msgstr "Obrót" + +msgid "Get rotation from last object" +msgstr "Obrót z ostatniego obiektu" + +msgid "Rotation value" +msgstr "Wartość obrotu" + +msgid "%.1f deg" +msgstr "%.1f°" + +msgid "include: %s" +msgstr "include: %s" + +msgid "The template takes no parameters." +msgstr "Szablon nie ma parametrów." + +msgid "Models of the template follow the changes at once, the rest of it once the scenery is saved and loaded again." +msgstr "Modele szablonu od razu idą za zmianami, reszta po zapisaniu i ponownym wczytaniu scenerii." + +msgid "Scenery layers are available in an edit session.\nStart the simulator with: -edit " +msgstr "Warstwy scenerii są dostępne w sesji edycji.\nUruchom symulator z: -edit " + +msgid "New..." +msgstr "Nowa..." + +msgid "New layer" +msgstr "Nowa warstwa" + +msgid "Restore" +msgstr "Przywróć" + +msgid "Remove layer" +msgstr "Usuń warstwę" + +msgid "Merge layer" +msgstr "Scal warstwę" + +msgid "Save" +msgstr "Zapisz" + +msgid "Writes changed nodes and layers to the scenery files (Ctrl+S)" +msgstr "Zapisuje zmienione węzły i warstwy do plików scenerii (Ctrl+S)" + +msgid "visible, locked, active, layer file" +msgstr "widoczna, zablokowana, aktywna, plik warstwy" + +msgid "Active layer: nodes created in the editor are placed in it" +msgstr "Aktywna warstwa: tu trafiają węzły tworzone w edytorze" + +msgid "File of the new layer, in the scenery directory:" +msgstr "Plik nowej warstwy, w katalogu scenerii:" + +msgid "name.scm" +msgstr "nazwa.scm" + +msgid "Create" +msgstr "Utwórz" + +msgid "Remove layer \"%s\" from the scenery?" +msgstr "Usunąć warstwę \"%s\" ze scenerii?" + +msgid "%d layer(s) it includes are removed with it." +msgstr "Razem z nią zostaną usunięte warstwy, które dołącza: %d." + +msgid "The layer holds:" +msgstr "Warstwa zawiera:" + +msgid "Make sure the rest of the scenery doesn't refer to its tracks, events or memory cells." +msgstr "Upewnij się, że reszta scenerii nie odwołuje się do jej torów, zdarzeń ani komórek pamięci." + +msgid "Move content of layer \"%s\" to:" +msgstr "Przenieś zawartość warstwy \"%s\" do:" + +msgid "no layer can take it" +msgstr "żadna warstwa nie może jej przyjąć" + +msgid "The text of the file is moved on save, the emptied file stays on the disk.\nMerged into the layer which includes it, the content stays where the include was.\nMerged into any other layer it's placed at its end, which changes the load order." +msgstr "Tekst pliku zostanie przeniesiony przy zapisie, opróżniony plik zostaje na dysku.\nScalona z warstwą, która ją dołącza, zawartość zostaje tam, gdzie był include.\nScalona z inną warstwą trafia na jej koniec, co zmienia kolejność wczytywania." + +msgid "Search templates" +msgstr "Szukaj szablonów" + +msgid "Described" +msgstr "Opisane" + +msgid "Used by the scenery" +msgstr "Użyte w scenerii" + +msgid "Rescan" +msgstr "Przeszukaj ponownie" + +msgid "Looks through the scenery directory again, for the templates added or changed outside of the editor" +msgstr "Przegląda katalog scenerii jeszcze raz, w poszukiwaniu szablonów dodanych lub zmienionych poza edytorem" + +msgid "%d template(s) in the scenery directory, %d described, %d ready for the node bank" +msgstr "Szablonów w katalogu scenerii: %d, opisanych: %d, gotowych do banku węzłów: %d" + +msgid "Not in the node bank: %s" +msgstr "Nie ma w banku węzłów: %s" + +msgid "param" +msgstr "param." + +msgid "role" +msgstr "rola" + +msgid "label" +msgstr "etykieta" + +msgid "default" +msgstr "domyślna" + +msgid "Complete: once saved, the template is offered by the node bank (Insert tab, Scenery templates)." +msgstr "Kompletny: po zapisaniu szablon pojawi się w banku węzłów (zakładka Wstaw, Szablony scenerii)." + +msgid "Not for the node bank yet: %s." +msgstr "Jeszcze nie do banku węzłów: %s." + +msgid "A template placed from the node bank gets the cursor location in the pos.* parameters, the rotation set in the\nInsert tab in rot.y, and a unique name (the default value followed by a random suffix) in the name parameter.\nThe other parameters receive their default values." +msgstr "Szablon wstawiony z banku obiektów dostaje położenie kursora w parametrach pos.*, obrót ustawiony w zakładce\nWstaw w rot.y, a w parametrze name niepowtarzalną nazwę (wartość domyślna z losowym przyrostkiem).\nPozostałe parametry dostają wartości domyślne." + +msgid "Suggest roles" +msgstr "Podpowiedz role" + +msgid "Sets roles of the parameters still marked as free, where the way the template uses them tells what they are" +msgstr "Ustawia role parametrów nadal oznaczonych jako wolne, gdy sposób ich użycia w szablonie mówi, czym są" + +msgid "Add parameter" +msgstr "Dodaj parametr" + +msgid "Save to file" +msgstr "Zapisz do pliku" + +msgid "Reload" +msgstr "Wczytaj ponownie" + +msgid "The .inc templates of the scenery directory. Describe a template and its parameters to add it to the node bank; the description is stored in the template file, as //$e comment lines." +msgstr "Szablony .inc z katalogu scenerii. Opisz szablon i jego parametry, żeby dodać go do banku węzłów; opis jest zapisywany w pliku szablonu jako linie komentarza //$e." + +msgid "The node bank offers the templates in a scenery opened for editing (-edit)." +msgstr "Bank węzłów oferuje szablony w scenerii otwartej do edycji (-edit)." + +msgid "Editor Settings" +msgstr "Ustawienia edytora" + +msgid "General" +msgstr "Ogólne" + +msgid "Camera movement" +msgstr "Ruch kamery" + +msgid "Transform gizmo (ImGuizmo)" +msgstr "Gizmo przekształceń (ImGuizmo)" + +msgid "Grid cells" +msgstr "Komórki siatki" + +msgid "Cell size (m)" +msgstr "Rozmiar komórki (m)" + +msgid "Base height (m)" +msgstr "Wysokość bazowa (m)" + +msgid "Texture (optional)" +msgstr "Tekstura (opcjonalnie)" + +msgid "Create flat terrain" +msgstr "Utwórz płaski teren" + +msgid "Chunks / side" +msgstr "Fragmenty / bok" + +msgid "Create chunked terrain" +msgstr "Utwórz teren z fragmentów" + +msgid "total %d x %d m, %d chunks" +msgstr "razem %d x %d m, fragmentów: %d" + +msgid "Chunk edit mode (LMB add neighbour / Shift = delete)" +msgstr "Tryb edycji fragmentów (LPM dodaje sąsiedni / Shift = usuń)" + +msgid "Grid chunks: %zu" +msgstr "Fragmenty siatki: %zu" + +msgid "Streaming (open world, follows camera)" +msgstr "Strumieniowanie (otwarty świat, za kamerą)" + +msgid "Stream radius" +msgstr "Promień strumieniowania" + +msgid "Persist edits to disk (16-bit)" +msgstr "Zapisuj zmiany na dysku (16-bit)" + +msgid "Stream terrain" +msgstr "Strumieniuj teren" + +msgid "Resident chunks: %zu (dir: %s)" +msgstr "Wczytane fragmenty: %zu (katalog: %s)" + +msgid "Patches: %zu" +msgstr "Łaty: %zu" + +msgid "Capture selected model as terrain" +msgstr "Zamień zaznaczony model na teren" + +msgid "Capture: select a model instance first" +msgstr "Zamiana: najpierw zaznacz instancję modelu" + +msgid "Brush radius" +msgstr "Promień pędzla" + +msgid "Brush strength" +msgstr "Siła pędzla" + +msgid "Raise centre" +msgstr "Podnieś środek" + +msgid "Lower centre" +msgstr "Obniż środek" + +msgid "Optimize (mesh simplification, all patches)" +msgstr "Optymalizacja (upraszczanie siatki, wszystkie łaty)" + +msgid "Flatness tol (m)" +msgstr "Tolerancja płaskości (m)" + +msgid "Auto-optimize after sculpt" +msgstr "Optymalizuj po rzeźbieniu" + +msgid "Optimize all" +msgstr "Optymalizuj wszystko" + +msgid "Full-res all" +msgstr "Pełna rozdzielczość wszędzie" + +msgid "Triangles: %zu / %zu" +msgstr "Trójkąty: %zu / %zu" + +msgid "Show orthophoto (geoportal.gov.pl)" +msgstr "Pokaż ortofotomapę (geoportal.gov.pl)" + +msgid "Scenery origin (0,0,0) in PUWG 1992 / EPSG:2180" +msgstr "Środek scenerii (0,0,0) w PUWG 1992 / EPSG:2180" + +msgid "X (northing, m)" +msgstr "X (northing, m)" + +msgid "Y (easting, m)" +msgstr "Y (easting, m)" + +msgid "Enter the origin of this scenery first" +msgstr "Najpierw podaj środek tej scenerii" + +msgid "Camera at X %.1f Y %.1f" +msgstr "Kamera w X %.1f Y %.1f" + +msgid "Distance (tiles)" +msgstr "Zasięg (kafle)" + +msgid "Fit to terrain" +msgstr "Dopasuj do terenu" + +msgid "Lays the imagery onto the ground geometry (terrain shapes, terrain tile models, editor terrain).\nPlaces without any ground use the height below." +msgstr "Kładzie zdjęcia na geometrii gruntu (kształty terenu, modele kafli terenu, teren edytora).\nMiejsca bez gruntu dostają wysokość podaną niżej." + +msgid "Refit" +msgstr "Dopasuj ponownie" + +msgid "Samples the ground again, after it was edited" +msgstr "Ponownie próbkuje grunt, po jego edycji" + +msgid "Lift above ground (m)" +msgstr "Uniesienie nad grunt (m)" + +msgid "Raises the fitted imagery above the ground. Keep it below the objects you want to see:\nin the scene everything lower than this ends up under the imagery." +msgstr "Unosi dopasowane zdjęcia nad grunt. Trzymaj je poniżej obiektów, które chcesz widzieć:\nw scenie wszystko, co jest niżej, znajdzie się pod zdjęciami." + +msgid "Height (m)" +msgstr "Wysokość (m)" + +msgid "Covered by objects and terrain" +msgstr "Zasłaniane przez obiekty i teren" + +msgid "Draws the imagery as part of the scene, so tracks, models and terrain in front of it hide it.\nIt is lit like the scene. Fully opaque, mouse placement lands on it as on any surface;\ntranslucent, it doesn't catch the mouse.\nNot available with the Better Renderer, which doesn't pick up geometry added in the editor." +msgstr "Rysuje zdjęcia jako część sceny, więc tory, modele i teren przed nimi je zasłaniają.\nJest oświetlone jak scena. W pełni kryjące łapie mysz przy wstawianiu jak każda powierzchnia;\nprzezroczyste nie łapie myszy.\nNiedostępne z Better Renderer, który nie widzi geometrii dodanej w edytorze." + +msgid "Opacity" +msgstr "Krycie" + +msgid "In the scene the opacity is part of the textures: the tiles are read again from the cache\nshortly after the slider stops, which takes a moment." +msgstr "W scenie krycie jest częścią tekstur: kafle są ponownie wczytywane z pamięci podręcznej\nchwilę po puszczeniu suwaka, co trwa moment." + +msgid "Newest" +msgstr "Najnowsze" + +msgid "4K tiles where available" +msgstr "Kafle 4K, gdzie są dostępne" + +msgid "4096 px tiles (~6 cm/px) from the high-resolution orthophoto, for the tiles around the camera.\nAreas it doesn't cover use the standard imagery. Each 4K tile takes ~85 MB of video memory." +msgstr "Kafle 4096 px (~6 cm/px) z ortofotomapy wysokiej rozdzielczości, dla kafli wokół kamery.\nTam, gdzie jej nie ma, zwykłe zdjęcia. Każdy kafel 4K zajmuje ~85 MB pamięci karty." + +msgid "Tiles: %d shown, %d loading, %d failed" +msgstr "Kafle: wyświetlane %d, wczytywane %d, nieudane %d" + +msgid "Retry" +msgstr "Ponów" + +msgid "This build has no HTTP client, only cached tiles are shown" +msgstr "Ten build nie ma klienta HTTP, widać tylko kafle z pamięci podręcznej" + +msgid "Cache: %s" +msgstr "Pamięć podręczna: %s" + +msgid "Clear cache" +msgstr "Wyczyść cache" + +msgid "Deletes downloaded tiles from disk. They are fetched again around the camera." +msgstr "Usuwa pobrane kafle z dysku. Zostaną ponownie pobrane wokół kamery." + +msgid "Imagery: GUGiK, geoportal.gov.pl" +msgstr "Zdjęcia: GUGiK, geoportal.gov.pl" + +msgid "Put all instances of the selected model on the ground" +msgstr "Postaw wszystkie instancje zaznaczonego modelu na ziemi" + +msgid "Select a model in the scene. Every instance of the same model file is moved straight up or down\nto the ground under it: terrain, large models and terrain made in the editor." +msgstr "Zaznacz model w scenie. Każda instancja tego samego pliku modelu zostanie przesunięta pionowo\nna grunt pod nią: teren, duże modele i teren zrobiony w edytorze." + +msgid "LMB: add outline point Backspace: remove last point" +msgstr "LPM: dodaj punkt obrysu Backspace: usuń ostatni punkt" + +msgid "Points: %zu Area: %.0f m2 (%.2f ha)" +msgstr "Punkty: %zu Powierzchnia: %.0f m2 (%.2f ha)" + +msgid "Remove last point" +msgstr "Usuń ostatni punkt" + +msgid "Clear outline" +msgstr "Wyczyść obrys" + +msgid "%zu templates in set" +msgstr "%zu szablonów w zestawie" + +msgid "Density (objects/ha)" +msgstr "Gęstość (obiektów/ha)" + +msgid "Min. spacing (m)" +msgstr "Min. odstęp (m)" + +msgid "Random rotation" +msgstr "Losowy obrót" + +msgid "Large models count as ground (terrain tiles)" +msgstr "Duże modele liczą się jako grunt (kafle terenu)" + +msgid "Estimated objects: %.0f" +msgstr "Szacowana liczba obiektów: %.0f" + +msgid "Fill area" +msgstr "Wypełnij obszar" + +msgid "Copies of the selected model, set out in a row" +msgstr "Kopie zaznaczonego modelu, ustawione w rzędzie" + +msgid "Fixed count" +msgstr "Stała liczba" + +msgid "Fit length" +msgstr "Dopasuj do długości" + +msgid "Count" +msgstr "Liczba" + +msgid "Number of the models in the array, the selected one included" +msgstr "Liczba modeli w tablicy, łącznie z zaznaczonym" + +msgid "As many copies are made as fit in this length, measured along the row" +msgstr "Powstaje tyle kopii, ile mieści się w tej długości, mierzonej wzdłuż rzędu" + +msgid "Relative offset" +msgstr "Odsunięcie względne" + +msgid "Distance between the copies as a part of the size of the model, along its axes: x, y (up), z.\n1 along an axis puts the copies end to end" +msgstr "Odległość między kopiami jako część rozmiaru modelu, wzdłuż jego osi: x, y (w górę), z.\n1 wzdłuż osi ustawia kopie jedna za drugą" + +msgid "x size" +msgstr "x rozmiaru" + +msgid "Constant offset" +msgstr "Odsunięcie stałe" + +msgid "Distance between the copies in metres, along the axes of the model: x, y (up), z.\nWith both offsets on the copies are set apart by their sum" +msgstr "Odległość między kopiami w metrach, wzdłuż osi modelu: x, y (w górę), z.\nPrzy obu odsunięciach kopie dzieli ich suma" + +msgid "Turn per copy (deg)" +msgstr "Obrót na kopię (°)" + +msgid "Each copy is turned by this much around the vertical axis against the one before it,\nand the row bends with them into an arc" +msgstr "Każda kopia jest obrócona o tyle wokół osi pionowej względem poprzedniej,\na rząd wygina się razem z nimi w łuk" + +msgid "Follow the ground" +msgstr "Za gruntem" + +msgid "The copies are put as high over the ground as the model is, instead of level with it.\nNot for the models large enough to count as ground themselves" +msgstr "Kopie stoją tak wysoko nad gruntem jak model, zamiast na jego poziomie.\nNie dla modeli tak dużych, że same liczą się jako grunt" + +msgid "Model %.2f x %.2f x %.2f m, %d in the array, %.2f m apart" +msgstr "Model %.2f x %.2f x %.2f m, w tablicy %d, co %.2f m" + +msgid "The array follows the settings" +msgstr "Tablica idzie za ustawieniami" + +msgid "The copies stay the way they are. Selecting or changing anything else does the same" +msgstr "Kopie zostają tak, jak są. To samo daje zaznaczenie lub zmiana czegokolwiek innego" + +msgid "Make array" +msgstr "Utwórz tablicę" + +msgid "Select a model to make an array of" +msgstr "Zaznacz model, z którego ma powstać tablica" + +msgid "Extent (m), wheel" +msgstr "Zasięg (m), kółko" + +msgid "Extent (m), wheel / MMB pan" +msgstr "Zasięg (m), kółko / przesuwanie kółkiem" + +msgid "Wheel zooms. Hold the wheel and drag to pan." +msgstr "Kółko przybliża. Wciśnij kółko i przeciągnij, żeby przesunąć widok." + +msgid "Top-down orthographic view" +msgstr "Widok z góry, rzut prostokątny" + +msgid "Hold the wheel" +msgstr "Przytrzymaj kółko" + +msgid "pans the view" +msgstr "przesuwa widok" + +msgid "Enabled" +msgstr "Włączone" + +msgid "Suspended while editing terrain" +msgstr "Wstrzymane podczas edycji terenu" + +msgid "Translate (Q)" +msgstr "Przesuń (Q)" + +msgid "Rotate (W)" +msgstr "Obróć (W)" + +msgid "Scale (E)" +msgstr "Skaluj (E)" + +msgid "Local space (R)" +msgstr "Układ lokalny (R)" + +msgid "Snap (hold Ctrl)" +msgstr "Krok (przytrzymaj Ctrl)" + +msgid "No node selected" +msgstr "Nic nie zaznaczono" + +msgid "Editor History" +msgstr "Historia edytora" + +msgid "Max history size" +msgstr "Maks. rozmiar historii" + +msgid "Max placement distance" +msgstr "Maks. odległość wstawiania" + +msgid "History (newest at end): %zu entries" +msgstr "Historia (najnowsze na końcu): %zu wpisów" + +msgid "Clear History" +msgstr "Wyczyść historię" + +msgid "Undo Selected" +msgstr "Cofnij zaznaczone" + +# scenery editor: modes, layers and messages + +msgid "Set##insert" +msgstr "Zestaw##insert" + +msgid "File" +msgstr "Plik" + +msgid "Start the simulator with -edit to save changes to the scenery files" +msgstr "Uruchom symulator z -edit , żeby zapisywać zmiany do plików scenerii" + +msgid "Export scenery dump" +msgstr "Eksportuj zrzut scenerii" + +msgid "Writes the whole scene to _export files next to the scenario, the scenery files are left as they are" +msgstr "Zapisuje całą scenę do plików _export obok scenariusza, pliki scenerii zostają bez zmian" + +msgid "Mode windows" +msgstr "Okna trybów" + +msgid "Toolset" +msgstr "Przybornik" + +msgid "Layers" +msgstr "Warstwy" + +msgid "Include database" +msgstr "Baza include'ów" + +msgid "Preset" +msgstr "Gotowy zestaw" + +msgid "Manual list" +msgstr "Lista ręczna" + +msgid "Insert" +msgstr "Wstaw" + +msgid "Brush" +msgstr "Pędzel" + +msgid "Area fill" +msgstr "Wypełnianie obszaru" + +msgid "Copy to bank" +msgstr "Kopiuj do banku" + +msgid "Random" +msgstr "Losowy" + +msgid "Fixed" +msgstr "Stały" + +msgid "Default (from template)" +msgstr "Domyślny (z szablonu)" + +msgid "Select a layer first" +msgstr "Najpierw zaznacz warstwę" + +msgid "Layer \"%s\" restored" +msgstr "Warstwa \"%s\" przywrócona" + +msgid "Remove..." +msgstr "Usuń..." + +msgid "Merge into..." +msgstr "Scal z..." + +msgid "The scenario file can't be merged into another layer" +msgstr "Pliku scenariusza nie da się scalić z inną warstwą" + +msgid "Layer \"%s\" can't take new nodes" +msgstr "Warstwa \"%s\" nie może przyjąć nowych węzłów" + +msgid "New layer, its file is created on save" +msgstr "Nowa warstwa, jej plik powstanie przy zapisie" + +msgid "Dropped from the scenery on save" +msgstr "Wypada ze scenerii przy zapisie" + +msgid "Scenery file" +msgstr "Plik scenerii" + +msgid "Layer \"%s\" created and made active. Its file is written on save." +msgstr "Warstwa \"%s\" utworzona i ustawiona jako aktywna. Jej plik zostanie zapisany przy zapisie." + +msgid "Layer \"%s\" removed. It can be restored until the scenery is saved." +msgstr "Warstwa \"%s\" usunięta. Można ją przywrócić, dopóki sceneria nie zostanie zapisana." + +msgid "Merge" +msgstr "Scal" + +msgid "Pick the target layer" +msgstr "Wybierz warstwę docelową" + +msgid "Layer \"%s\" merged into \"%s\". The files are changed on save." +msgstr "Warstwa \"%s\" scalona z \"%s\". Pliki zmienią się przy zapisie." + +msgid "WSAD (new)" +msgstr "WSAD (nowy)" + +msgid "Arrows (legacy)" +msgstr "Strzałki (stary)" + +msgid "The scenery wasn't opened for editing. Start the simulator with: -edit " +msgstr "Sceneria nie jest otwarta do edycji. Uruchom symulator z: -edit " + +msgid "Select a model in the scene first" +msgstr "Najpierw zaznacz model w scenie" + +msgid "Removed %zu objects of the last fill" +msgstr "Usunięto obiekty ostatniego wypełnienia: %zu" + +msgid "Outline needs at least 3 points" +msgstr "Obrys potrzebuje co najmniej 3 punktów" + +msgid "Selected model package is empty" +msgstr "Wybrany zestaw modeli jest pusty" + +msgid "Density too low for this area, nothing to place" +msgstr "Za mała gęstość na ten obszar, nie ma czego wstawić" + +msgid "Placed %zu of %zu objects" +msgstr "Wstawiono %zu z %zu obiektów" + +msgid "Model set" +msgstr "Zestaw modeli" + +msgid "Custom list" +msgstr "Własna lista" + +msgid "Undo last fill (%zu)" +msgstr "Cofnij ostatnie wypełnienie (%zu)" + +msgid "The offsets come to nothing, the copies would sit in the model" +msgstr "Odsunięcia dają zero, kopie siedziałyby w modelu" + +msgid "An array of one is the model itself, nothing to make" +msgstr "Tablica z jednego to sam model, nie ma czego tworzyć" + +msgid "The length takes no copy, nothing to make" +msgstr "Ta długość nie mieści żadnej kopii, nie ma czego tworzyć" + +msgid "Only %zu of %zu copies could be made" +msgstr "Udało się zrobić tylko %zu z %zu kopii" + +msgid "No copies with these settings" +msgstr "Przy tych ustawieniach nie ma kopii" + +msgid "The model and %zu copies; Ctrl+Z takes the array back" +msgstr "Model i kopie: %zu; Ctrl+Z cofa tablicę" + +msgid "Include selected, no gizmo under rotate or scale" +msgstr "Zaznaczony include, bez gizma przy obrocie lub skali" + +msgid "Include selected, axes as its template allows" +msgstr "Zaznaczony include, osie na tyle, na ile pozwala szablon" + +msgid "B: bend along %s" +msgstr "B: wygnij wzdłuż %s" + +msgid "Click: the reference point, where it lands on the model" +msgstr "Klik: punkt odniesienia tam, gdzie trafi w model" + +msgid "Drag: offset from the track" +msgstr "Przeciągnij: odsunięcie od toru" + +msgid "Shift: height Ctrl: along the track" +msgstr "Shift: wysokość Ctrl: wzdłuż toru" + +msgid "Click: edit the bent model" +msgstr "Klik: edytuj wygięty model" + +msgid "Bend it along the track (B)" +msgstr "Wygnij wzdłuż toru (B)" + +msgid "Select a model by a track, or click a bent one" +msgstr "Zaznacz model przy torze albo kliknij wygięty" + +msgid "LMB on a track: where the vehicle stands" +msgstr "LPM na torze: tam stanie pojazd" + +msgid "Platform" +msgstr "Peron" + +msgid "Puts the edge facing the track at the distance chosen from its axis, set back by the widening of the gauge in the curves.\nStands on the plane of the rail heads carried on to its edge: lower on the inner side of a canted curve,\nhigher on the outer one; on the inner side also set back by the cant times the height of the edge / 1.5" +msgstr "Ustawia krawędź od strony toru w wybranej odległości od jego osi, odsuniętą o poszerzenie skrajni na łukach.\nStoi na płaszczyźnie główek szyn przedłużonej do krawędzi: niżej po wewnętrznej stronie łuku z przechyłką,\nwyżej po zewnętrznej; po wewnętrznej dodatkowo odsunięty o przechyłkę razy wysokość krawędzi / 1,5" + +msgid "1.725 m: lines of out-of-gauge loads, older platforms" +msgstr "1,725 m: linie przesyłek z przekroczoną skrajnią, starsze perony" + +msgid "1.675 m: nominal by the 0.55 and 0.76 m platforms" +msgstr "1,675 m: nominalnie przy peronach 0,55 i 0,76 m" + +msgid "1.650 m: the least allowed in service" +msgstr "1,650 m: najmniej dopuszczalne w eksploatacji" + +msgid "edge of the platform from the axis" +msgstr "krawędź peronu od osi toru" + +# scenery editor: signals +msgid "Signals" +msgstr "Sygnalizatory" + +msgid "Signals by the track, from the templates of the scenery, with the event the train reads them by" +msgstr "Sygnalizatory przy torze, z szablonów scenerii, z eventem, którym pociąg je odczytuje" + +msgid "Signal: drag along the track, release to put it there" +msgstr "Sygnalizator: przeciągnij wzdłuż toru, puść, żeby go tam postawić" + +msgid "Signal: drag along the track sets the direction of the trains, release to place it" +msgstr "Sygnalizator: przeciąganie wzdłuż toru ustawia kierunek pociągów, puść, żeby go postawić" + +msgid "Signal: LMB by the track places it on that side" +msgstr "Sygnalizator: LPM przy torze stawia go po tej stronie" + +msgid "Signal: choose its type below" +msgstr "Sygnalizator: wybierz jego typ poniżej" + +msgid "moves the signal along the track" +msgstr "przesuwa sygnalizator wzdłuż toru" + +msgid "the trains it faces run that way" +msgstr "pociągi, do których jest zwrócony, jadą w tę stronę" + +msgid "LMB by a track" +msgstr "LPM przy torze" + +msgid "places the signal on that side, facing the trains which have it on their right" +msgstr "stawia sygnalizator po tej stronie, zwrócony do pociągów, które mają go po prawej" + +msgid "Press+drag along" +msgstr "Wciśnij i przeciągnij wzdłuż" + +msgid "the trains it faces run the way of the drag" +msgstr "pociągi, do których jest zwrócony, jadą w stronę przeciągnięcia" + +msgid "LMB on a signal" +msgstr "LPM na sygnalizatorze" + +msgid "Drag a signal" +msgstr "Przeciągnij sygnalizator" + +msgid "moves it along the track" +msgstr "przesuwa go wzdłuż toru" + +msgid "removes the selected signal with its event" +msgstr "usuwa zaznaczony sygnalizator razem z jego eventem" + +msgid "Click: select the signal %s, drag: move it along the track" +msgstr "Klik: zaznacz sygnalizator %s, przeciągnij: przesuń go wzdłuż toru" + +msgid "Choose the type of the signal in the Tracks window first" +msgstr "Najpierw wybierz typ sygnalizatora w oknie Tory" + +msgid "Semaphore" +msgstr "Semafor" + +msgid "Automatic block" +msgstr "SBL" + +msgid "Warning signal" +msgstr "Tarcza ostrzegawcza" + +msgid "Shunting signal" +msgstr "Tarcza manewrowa" + +msgid "Stop signal" +msgstr "Zaporowy" + +msgid "Substitute signal" +msgstr "Sygnał zastępczy" + +msgid "Repeater" +msgstr "Powtarzacz" + +msgid " on the right of the trains" +msgstr " po prawej pociągów" + +msgid " on the left of the trains" +msgstr " po lewej pociągów" + +msgid "Click: %s %s (%s)%s, the train doesn't read it" +msgstr "Klik: %s %s (%s)%s, pociąg go nie odczytuje" + +msgid "Drag along: the trains it faces run the way of the drag" +msgstr "Przeciągnij wzdłuż: pociągi, do których jest zwrócony, jadą w stronę przeciągnięcia" + +msgid "Click: %s %s (%s)%s, read by %s %s of %s" +msgstr "Klik: %s %s (%s)%s, pociąg odczytuje go przez %s %s toru %s" + +msgid "Signal %s moved" +msgstr "Sygnalizator %s przesunięty" + +msgid "Signal %s moved, read by %s %s of %s" +msgstr "Sygnalizator %s przesunięty, pociąg odczytuje go przez %s %s toru %s" + +msgid "Give the signal a name without spaces first" +msgstr "Najpierw nadaj sygnalizatorowi nazwę bez spacji" + +msgid "The signal couldn't be placed: " +msgstr "Nie udało się postawić sygnalizatora: " + +msgid "Signal %s placed (%s), the train doesn't read it" +msgstr "Sygnalizator %s postawiony (%s), pociąg go nie odczytuje" + +msgid "Signal %s placed (%s), but vehicles stand on the path: add %s to its %s by hand" +msgstr "Sygnalizator %s postawiony (%s), ale na torze stoją pojazdy: dopisz %s do jego %s ręcznie" + +msgid "Signal %s placed (%s), but the path can't take its event: %s" +msgstr "Sygnalizator %s postawiony (%s), ale tor nie przyjmie jego eventu: %s" + +msgid "Signal %s placed (%s), read by %s %s of %s. Its logic works after the scenery is saved and opened again" +msgstr "Sygnalizator %s postawiony (%s), pociąg odczytuje go przez %s %s toru %s. Jego logika zadziała po zapisaniu i ponownym otwarciu scenerii" + +msgid "Signal %s removed with its event %s, Ctrl+Z brings both back" +msgstr "Sygnalizator %s usunięty razem z eventem %s, Ctrl+Z przywraca oba" + +msgid "filter: name, w24, sbl..." +msgstr "filtr: nazwa, w24, sbl..." + +msgid "No template of this kind matches the filter" +msgstr "Żaden szablon tego rodzaju nie pasuje do filtra" + +msgid "%zu variants, e.g. %s" +msgstr "wariantów: %zu, np. %s" + +msgid "The train doesn't read it" +msgstr "Pociąg go nie odczytuje" + +msgid "The train reads it by an event of the path" +msgstr "Pociąg odczytuje go przez event toru" + +msgid "Variant of the chosen type. A head leaning to one side is swapped for its counterpart leaning towards the track on the other side" +msgstr "Wariant wybranego typu. Głowica odchylona w jedną stronę zostaje zamieniona na odpowiednik odchylony w stronę toru, gdy sygnalizator stanie po drugiej stronie" + +msgid "The train doesn't read it: it repeats what the signal ahead of it shows" +msgstr "Pociąg go nie odczytuje: powtarza to, co pokazuje sygnalizator przed nim" + +msgid "The train reads it by %s on the path it stands by" +msgstr "Pociąg odczytuje go przez %s na torze, przy którym stoi" + +msgid "Selected: %s (%s)" +msgstr "Zaznaczony: %s (%s)" + +msgid "Read by %s %s of %s" +msgstr "Odczytywany przez %s %s toru %s" + +msgid "No path has %s among its events: the train doesn't see it" +msgstr "Żaden tor nie ma %s wśród swoich eventów: pociąg go nie widzi" + +msgid "Name, plate and the linked signal: in the node properties. Del removes it with its event" +msgstr "Nazwa, tablica i powiązany sygnalizator: we właściwościach węzła. Del usuwa go razem z eventem" + +msgid "No signal templates: no template of the scenery directory is described with category: signal. tools/editor/signal_headers.py writes the descriptions into the signal templates." +msgstr "Brak szablonów sygnalizatorów: żaden szablon katalogu scenerii nie ma opisu z category: signal. tools/editor/signal_headers.py dopisuje opisy do szablonów sygnalizatorów." + +msgid "Name of the next signal; it counts up after each one placed. Its events and memory cell are named after it" +msgstr "Nazwa następnego sygnalizatora; po każdym postawionym rośnie o jeden. Od niej biorą nazwy jego eventy i komórka pamięci" + +msgid "auto: %s" +msgstr "auto: %s" + +msgid "Plate" +msgstr "Tablica" + +msgid "Texture of the plate from textures/tabl, e.g. a-12 or tob. Left empty: the one named like the signal, if there is one" +msgstr "Tekstura tablicy z textures/tabl, np. a-12 albo tob. Puste: ta nazwana tak jak sygnalizator, jeśli istnieje" + +msgid "no such texture" +msgstr "nie ma takiej tekstury" + +msgid "Linked signal" +msgstr "Powiązany sygnalizator" + +msgid "(p7) of the template: the warning signal a semaphore drives, the previous block signal, \"none\" for no link" +msgstr "(p7) szablonu: tarcza ostrzegawcza, którą steruje semafor, poprzedni semafor SBL, \"none\": bez powiązania" + +msgid "Distance of the mast from the axis of the track. 2.25 m is what the signals of the sceneries stand at" +msgstr "Odległość masztu od osi toru. Na 2,25 m stoją sygnalizatory w sceneriach" + +msgid "Over the rails (m)" +msgstr "Nad szynami (m)" + +msgid "A head for a gantry hangs over the axis of the track this high over the rail heads" +msgstr "Głowica na bramkę wisi nad osią toru tak wysoko nad główkami szyn" + +msgid " not read" +msgstr " nieodczytywany" + +msgid "Reset window layout" +msgstr "Przywróć układ okien" + +msgid "Selected:" +msgstr "Zaznaczono:" + +msgid "Tool options" +msgstr "Opcje narzędzia" + +msgid "Inspector" +msgstr "Inspektor" + +msgid "Scene" +msgstr "Scena" + +msgid "Node bank" +msgstr "Bank obiektów" + +msgid "Refresh" +msgstr "Odśwież" + +msgid "Filter: name or uuid" +msgstr "Filtr: nazwa lub uuid" + +msgid "%zu models" +msgstr "modeli: %zu" + +msgid "(no layer)" +msgstr "(bez warstwy)" + +msgid "Group" +msgstr "Grupa" + +msgid "Double click: fly to it" +msgstr "Dwuklik: kamera leci do niego" + +msgid "By the track" +msgstr "Przy torze" + +msgid "Checks" +msgstr "Kontrola" + +msgid "Infrastructure" +msgstr "Infrastruktura" + +msgid "Speed check" +msgstr "Kontrola prędkości" + +msgid "What the check finds is listed in its window" +msgstr "Wyniki kontroli są w jej oknie" + +msgid "Unpin: the dock hides at the edge of the window, its tabs show it over the view (Ctrl+Space: all docks)" +msgstr "Odepnij: dok chowa się przy krawędzi okna, jego zakładki wysuwają go nad widok (Ctrl+Spacja: wszystkie doki)" + +msgid "Pin: the dock goes back to its place" +msgstr "Przypnij: dok wraca na swoje miejsce" + +msgid "Unpin (hide at the edge)" +msgstr "Odepnij (schowaj przy krawędzi)" + +msgid "Unpin all docks" +msgstr "Odepnij wszystkie doki" + +msgid "Pin the docks back" +msgstr "Przypnij doki z powrotem" + +msgid "Previews" +msgstr "Podglądy" + +msgid "%d px" +msgstr "%d px" + +msgid "No previews yet: they're made by eu07 -generate-nodebank-previews" +msgstr "Brak podglądów: tworzy je eu07 -generate-nodebank-previews" + +msgid "Scenery templates (.inc) are listed with the previews off" +msgstr "Szablony scenerii (.inc) są na liście przy wyłączonych podglądach" + +msgid "not a model" +msgstr "to nie model" + +msgid "no preview" +msgstr "brak podglądu" + +msgid "Colours of the editor" +msgstr "Kolory edytora" + +msgid "Green, as the rest of the simulator" +msgstr "Zielone, jak reszta symulatora" + +msgid "Amber, of the editor" +msgstr "Bursztynowe, edytora" + +msgid "Surroundings" +msgstr "Otoczenie" + +msgid "Roads" +msgstr "Drogi" + +msgid "Sculpt" +msgstr "Rzeźbienie" + +msgid "Chunks" +msgstr "Kafle" + +msgid "Models around the line: select, insert, brush, area fill, copy to the node bank" +msgstr "Modele wokół linii: zaznaczanie, wstawianie, pędzel, wypełnianie obszaru, kopiowanie do banku" + +msgid "Tracks and switches, signals, objects along the track, checks of the line" +msgstr "Tory i rozjazdy, sygnalizatory, obiekty wzdłuż toru, kontrole linii" + +msgid "Roads and junctions, level crossings, the points where the traffic comes and goes" +msgstr "Drogi i skrzyżowania, przejazdy, punkty pojawiania się i znikania ruchu" + +msgid "Terrain patches and chunks, sculpting, streaming, orthophoto" +msgstr "Płaty i kafle terenu, rzeźbienie, strumieniowanie, ortofotomapa" + +msgid "LMB picks the models, as in the surroundings" +msgstr "LPM zaznacza modele, jak w otoczeniu" + +msgid "LMB raises the terrain under the brush, Shift+LMB lowers it" +msgstr "LPM podnosi teren pod pędzlem, Shift+LPM go obniża" + +msgid "LMB evens the terrain out under the brush" +msgstr "LPM wyrównuje teren pod pędzlem" + +msgid "LMB adds a chunk next to the clicked one, Shift+LMB deletes it" +msgstr "LPM dodaje kafel obok klikniętego, Shift+LPM go usuwa" + +msgid "A vehicle to drive, put on the selected track; it isn't written to the scenery files" +msgstr "Pojazd do jazdy, stawiany na zaznaczonym torze; nie trafia do plików scenerii" + +msgid "%zu groups taken apart" +msgstr "Rozgrupowano grup: %zu" + +msgid "%zu models copied; Ctrl+V puts them at the cursor" +msgstr "Skopiowano modeli: %zu; Ctrl+V wstawia je przy kursorze" + +msgid "%zu models grouped: a click on one selects them all; Ctrl+Shift+G takes the group apart" +msgstr "Zgrupowano modeli: %zu; klik na jednym zaznacza wszystkie, Ctrl+Shift+G rozgrupowuje" + +msgid "%zu models pasted; Ctrl+Z takes them back" +msgstr "Wklejono modeli: %zu; Ctrl+Z cofa" + +msgid "%zu models selected" +msgstr "Zaznaczono modeli: %zu" + +msgid "%zu models selected; Shift+LMB adds or takes one out" +msgstr "Zaznaczono modeli: %zu; Shift+LPM dodaje lub odejmuje model" + +msgid "Add a model to the selection, or take it out" +msgstr "Dodaje model do zaznaczenia albo go odejmuje" + +msgid "Add a track to the selection, drag: a box" +msgstr "Dodaje tor do zaznaczenia, przeciąganie: ramka" + +msgid "Area fill: take the last point of the outline back" +msgstr "Wypełnianie obszaru: cofnij ostatni punkt obrysu" + +msgid "Bend the selected along the track" +msgstr "Wygnij zaznaczony wzdłuż toru" + +msgid "Bends the selected model, e.g. a platform edge or a fence, along the nearest track; the settings are in the inspector" +msgstr "Wygina zaznaczony model, np. krawędź peronu albo płot, wzdłuż najbliższego toru; ustawienia są w inspektorze" + +msgid "Build: straight ahead" +msgstr "Budowa: prosto przed siebie" + +msgid "Camera" +msgstr "Kamera" + +msgid "Copies the selected models with their groups" +msgstr "Kopiuje zaznaczone modele razem z ich grupami" + +msgid "Copy" +msgstr "Kopiuj" + +msgid "Copy the selected models / paste them at the cursor" +msgstr "Kopiuj zaznaczone modele / wklej je przy kursorze" + +msgid "Ctrl held" +msgstr "Ctrl wciśnięty" + +msgid "Ctrl held while dragging" +msgstr "Ctrl wciśnięty przy przeciąganiu" + +msgid "Ctrl+RMB" +msgstr "Ctrl+PPM" + +msgid "Delete the selected" +msgstr "Usuń zaznaczone" + +msgid "Delete the selected tracks" +msgstr "Usuń zaznaczone tory" + +msgid "Deletes the selected models" +msgstr "Usuwa zaznaczone modele" + +msgid "Deletes what's selected: a model, tracks, a road piece or point, a signal, an include of a scenery template" +msgstr "Usuwa to, co zaznaczone: model, tory, kawałek lub punkt drogi, sygnalizator, include szablonu scenerii" + +msgid "Drop the selected onto the ground, each model of a group by itself" +msgstr "Opuść zaznaczone na ziemię, każdy model grupy osobno" + +msgid "Drop to the ground" +msgstr "Opuść na ziemię" + +msgid "Enter / Backspace" +msgstr "Enter / Backspace" + +msgid "Fields of work" +msgstr "Tryby pracy" + +msgid "Find a track by its name" +msgstr "Znajdź tor po nazwie" + +msgid "Finish the road being built" +msgstr "Zakończ budowaną drogę" + +msgid "Fly forwards, back, left, right (arrows in the legacy scheme)" +msgstr "Lot do przodu, do tyłu, w lewo, w prawo (strzałki w starym układzie)" + +msgid "Fly to the selected" +msgstr "Leć do zaznaczonego" + +msgid "Fly up / down (Page Up / Page Down in the legacy scheme)" +msgstr "Lot w górę / w dół (Page Up / Page Down w starym układzie)" + +msgid "Group the selected models / take their group apart" +msgstr "Grupuj zaznaczone modele / rozgrupuj ich grupę" + +msgid "Help" +msgstr "Pomoc" + +msgid "Keyboard shortcuts" +msgstr "Skróty klawiszowe" + +msgid "Lay track / switch / turntable" +msgstr "Układanie toru / rozjazd / obrotnica" + +msgid "Laying track: finish / take the last point back" +msgstr "Układanie toru: zakończ / cofnij ostatni punkt" + +msgid "Leave the editor" +msgstr "Wyjście z edytora" + +msgid "Local space" +msgstr "Lokalne osie" + +msgid "Log" +msgstr "Log" + +msgid "Look around" +msgstr "Rozglądanie się" + +msgid "Makes the change taken back again" +msgstr "Przywraca cofniętą zmianę" + +msgid "Menu of the selection at the cursor" +msgstr "Menu zaznaczenia przy kursorze" + +msgid "Nothing copied yet; Ctrl+C copies the selected models" +msgstr "Nic jeszcze nie skopiowano; Ctrl+C kopiuje zaznaczone modele" + +msgid "Nothing selected" +msgstr "Nic nie zaznaczono" + +msgid "Paste" +msgstr "Wklej" + +msgid "Paste here" +msgstr "Wklej tutaj" + +msgid "Profile / speed / joints / infrastructure" +msgstr "Profil / prędkość / styki / infrastruktura" + +msgid "Puts each selected model, also each model of their groups, down onto the terrain or the object under it" +msgstr "Opuszcza każdy zaznaczony model, także każdy model ich grup, na teren albo obiekt pod nim" + +msgid "Puts the copied models at the cursor, around it as they were around the selected one; Ctrl+RMB in the view pastes them where it was clicked" +msgstr "Wstawia skopiowane modele przy kursorze, rozłożone wokół niego tak jak wokół zaznaczonego; Ctrl+PPM w widoku wkleja je w klikniętym miejscu" + +msgid "Puts the copied models here, around this point as they were around the selected one" +msgstr "Wstawia skopiowane modele tutaj, rozłożone wokół tego punktu tak jak wokół zaznaczonego" + +msgid "Quit" +msgstr "Wyjście" + +msgid "RMB held, E / Q" +msgstr "PPM wciśnięty, E / Q" + +msgid "RMB held, W S A D" +msgstr "PPM wciśnięty, W S A D" + +msgid "RMB held, mouse" +msgstr "PPM wciśnięty, mysz" + +msgid "Redo" +msgstr "Ponów" + +msgid "Save the scenery" +msgstr "Zapisz scenerię" + +msgid "Screenshot" +msgstr "Zrzut ekranu" + +msgid "Sculpt: lower; chunks: delete one" +msgstr "Rzeźbienie: obniżanie; kafle: usunięcie kafla" + +msgid "Sculpt: raise; smooth: even out; chunks: add one" +msgstr "Rzeźbienie: podnoszenie; wygładzanie: wyrównanie; kafle: dodanie kafla" + +msgid "Select a model to copy" +msgstr "Zaznacz model do skopiowania" + +msgid "Select a model, or place one in the insert and brush modes" +msgstr "Zaznacz model albo postaw go w trybach wstawiania i pędzla" + +msgid "Select tool; cancels what's going on first" +msgstr "Narzędzie zaznaczania; najpierw przerywa to, co trwa" + +msgid "Select two models or more to group them: Shift+LMB adds one to the selection" +msgstr "Zaznacz co najmniej dwa modele, by je zgrupować: Shift+LPM dodaje model do zaznaczenia" + +msgid "Select, insert, brush, area fill, copy to bank" +msgstr "Zaznaczanie, wstawianie, pędzel, wypełnianie obszaru, kopiowanie do banku" + +msgid "Select: add a point, or take it out" +msgstr "Zaznaczanie: dodaj punkt albo go odejmij" + +msgid "Signals / objects along the track" +msgstr "Sygnalizatory / obiekty wzdłuż toru" + +msgid "Snap to the step set in the gizmo settings" +msgstr "Przyciąganie do kroku z ustawień gizma" + +msgid "Split the road at the cursor, or make a point there" +msgstr "Podziel drogę przy kursorze albo zrób tam punkt" + +msgid "Split the selected track at the cursor" +msgstr "Podziel zaznaczony tor przy kursorze" + +msgid "Straight / curve" +msgstr "Prosta / łuk" + +msgid "Takes the last change back" +msgstr "Cofa ostatnią zmianę" + +msgid "The camera flies to the selected model and looks at it" +msgstr "Kamera leci do zaznaczonego modelu i na niego patrzy" + +msgid "The handles along the axes of the selected model instead of the axes of the world" +msgstr "Uchwyty wzdłuż osi zaznaczonego modelu zamiast osi świata" + +msgid "The keys of the editor, for each field of work" +msgstr "Klawisze edytora w każdym trybie pracy" + +msgid "The models of the group move each by itself again" +msgstr "Modele grupy znów poruszają się osobno" + +msgid "The selected model isn't in a group" +msgstr "Zaznaczony model nie jest w grupie" + +msgid "The selected models move together from now on; a click on one selects them all" +msgstr "Zaznaczone modele poruszają się odtąd razem; klik na jednym zaznacza wszystkie" + +msgid "The selected models move together from now on; a click on one selects them all. Shift+LMB adds a model to the selection" +msgstr "Zaznaczone modele poruszają się odtąd razem; klik na jednym zaznacza wszystkie. Shift+LPM dodaje model do zaznaczenia" + +msgid "The view straight down without perspective, as on a map; again for the camera as it was" +msgstr "Widok z góry bez perspektywy, jak na mapie; ponownie: kamera jak była" + +msgid "This list of shortcuts" +msgstr "Ta lista skrótów" + +msgid "Top view, orthographic" +msgstr "Widok z góry, ortogonalny" + +msgid "Translate / rotate / scale" +msgstr "Przesuwanie / obrót / skala" + +msgid "Undo" +msgstr "Cofnij" + +msgid "Undo / redo" +msgstr "Cofnij / ponów" + +msgid "Ungroup" +msgstr "Rozgrupuj" + +msgid "Unpin all docks for the most of the view, or pin them back" +msgstr "Odepnij wszystkie doki dla większego widoku albo przypnij je z powrotem" + +msgid "What can be done with it" +msgstr "Co można z nim zrobić" + +msgid "What the handles of the gizmo on the selection do when dragged; Ctrl held snaps the values" +msgstr "Co robią uchwyty gizma na zaznaczeniu przy przeciąganiu; z Ctrl wartości są przyciągane" + +# scenery editor: view menu, weather and time + +msgid "View" +msgstr "Widok" + +msgid "Weather and time" +msgstr "Pogoda i czas" + +msgid "Time of day, day of the year, visibility, clouds and air temperature, as the scenery is seen in the editor" +msgstr "Godzina, dzień roku, widoczność, zachmurzenie i temperatura powietrza, z jakimi sceneria jest widoczna w edytorze" + +msgid "Time of day" +msgstr "Godzina" + +msgid "Day of the year" +msgstr "Dzień roku" + +msgid "Visibility" +msgstr "Widoczność" + +msgid "Clouds" +msgstr "Zachmurzenie" + +msgid "Up to 1 the sky clouds over, past it rain or snow falls, by the air temperature" +msgstr "Do 1 niebo się chmurzy, powyżej pada deszcz albo śnieg, zależnie od temperatury powietrza" + +msgid "Air temperature" +msgstr "Temperatura powietrza" + +msgid "Seen in the editor only; the scenery keeps its own time and weather" +msgstr "Widoczne tylko w edytorze; sceneria zachowuje własny czas i pogodę" + +msgid "Top view, orthographic (O)" +msgstr "Widok z góry, ortogonalny (O)" + +# scenery editor: tools over the 3d view + +msgid "Translate" +msgstr "Przesuwanie" + +msgid "Rotate" +msgstr "Obrót" + +msgid "Scale" +msgstr "Skala" + +msgid "Look along %s%s; from over the scenery it's the top view" +msgstr "Spójrz wzdłuż %s%s; znad scenerii to widok z góry" + +msgid "Show lanes" +msgstr "Pokaż pasy ruchu" + +msgid "The lanes of the roads drawn as the paths the vehicles take" +msgstr "Pasy ruchu dróg rysowane jako tory jazdy pojazdów" + +msgid "LMB: a road piece, a junction, a point where pieces meet, a level crossing or a traffic point" +msgstr "LPM: kawałek drogi, skrzyżowanie, punkt łączenia kawałków, przejazd kolejowy albo punkt ruchu" + +msgid "LMB: start a road, then each next point; on a loose end, the side of a road or a junction it's joined to them" +msgstr "LPM: początek drogi, potem każdy kolejny punkt; na wolnym końcu, boku drogi albo skrzyżowaniu droga zostaje do nich dołączona" + +msgid "Level crossings, and the points where vehicles appear on the roads or are taken off them" +msgstr "Przejazdy kolejowe oraz punkty, w których pojazdy pojawiają się na drogach albo są z nich zdejmowane" + +# scenery editor: node bank in two panes, models dragged to the sets + +msgid "Drop it on a model set, or on the list of the brush or the area fill" +msgstr "Upuść go na zestaw modeli albo na listę pędzla lub wypełniania obszaru" + +msgid "or drag models here from the node bank" +msgstr "albo przeciągnij tu modele z banku obiektów" + +# scenery editor: the toolbar with icons, the rows captioned + +msgid "Work modes" +msgstr "Tryby pracy" + +msgid "Tools" +msgstr "Narzędzia" + +msgid "Level crossing" +msgstr "Przejazd kolejowy" + +msgid "Spawn point" +msgstr "Punkt pojawiania" + +msgid "Removal point" +msgstr "Punkt znikania" + +msgid "Pedestrian crossing" +msgstr "Przejście dla pieszych" diff --git a/launcher/keymapper.cpp b/launcher/keymapper.cpp index 6ec882bd..a2486c9f 100644 --- a/launcher/keymapper.cpp +++ b/launcher/keymapper.cpp @@ -81,7 +81,7 @@ void ui::keymapper_panel::render_contents() label += "##" + std::to_string((int)binding.first); - ImGui::SameLine(ImGui::GetContentRegionAvailWidth() - 150); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 150); if (ImGui::Button(label.c_str(), ImVec2(150, 0))) { if (bind_active == binding.first) bind_active = user_command::none; diff --git a/launcher/launcheruilayer.cpp b/launcher/launcheruilayer.cpp index 91974bbc..45baccb0 100644 --- a/launcher/launcheruilayer.cpp +++ b/launcher/launcheruilayer.cpp @@ -11,8 +11,9 @@ launcher_ui::launcher_ui() : m_scenery_scanner(m_vehicles_bank), m_scenerylist_p add_external_panel(&m_scenerylist_panel); add_external_panel(&m_keymapper_panel); add_external_panel(&m_vehiclepicker_panel); + add_external_panel(&m_scenerywizard_panel); - open_panel(&m_scenerylist_panel); + open_panel(Global.editor_session ? static_cast(&m_scenerywizard_panel) : &m_scenerylist_panel); m_suppress_menu = true; } @@ -50,6 +51,9 @@ void launcher_ui::render_() if (ImGui::Button(STR_C("Keymapper"), topbar_button_size)) open_panel(&m_keymapper_panel); ImGui::SameLine(); + if (ImGui::Button(STR_C("New scenery"), topbar_button_size)) + open_panel(&m_scenerywizard_panel); + ImGui::SameLine(); if (ImGui::Button(STR_C("Quit"), topbar_button_size)) Application.queue_quit(false); } @@ -61,6 +65,7 @@ void launcher_ui::close_panels() m_scenerylist_panel.is_open = false; m_vehiclepicker_panel.is_open = false; m_keymapper_panel.is_open = false; + m_scenerywizard_panel.is_open = false; } void launcher_ui::open_panel(ui_panel* panel) diff --git a/launcher/launcheruilayer.h b/launcher/launcheruilayer.h index 91e36bbd..6c9cb937 100644 --- a/launcher/launcheruilayer.h +++ b/launcher/launcheruilayer.h @@ -15,6 +15,7 @@ http://mozilla.org/MPL/2.0/. #include "launcher/vehicle_picker.h" #include "launcher/textures_scanner.h" #include "launcher/scenery_scanner.h" +#include "launcher/scenery_wizard.h" class launcher_ui : public ui_layer { public: @@ -33,6 +34,7 @@ private: ui::scenerylist_panel m_scenerylist_panel; ui::keymapper_panel m_keymapper_panel; ui::vehiclepicker_panel m_vehiclepicker_panel; + ui::scenerywizard_panel m_scenerywizard_panel; ui_panel *m_current_panel; }; diff --git a/launcher/scenery_list.cpp b/launcher/scenery_list.cpp index 518669e5..d95c962b 100644 --- a/launcher/scenery_list.cpp +++ b/launcher/scenery_list.cpp @@ -105,10 +105,10 @@ void ui::scenerylist_panel::draw_scenery_image() tex.create(); if (tex.get_is_ready()) { - float avail_width = ImGui::GetContentRegionAvailWidth(); + float avail_width = ImGui::GetContentRegionAvail().x; float height = avail_width / tex.get_width() * tex.get_height(); - ImGui::Image(reinterpret_cast(tex.get_id()), ImVec2(avail_width, height), ImVec2(0, 1), ImVec2(1, 0)); + ImGui::Image((ImTextureID)(intptr_t)(tex.get_id()), ImVec2(avail_width, height), ImVec2(0, 1), ImVec2(1, 0)); } } } @@ -119,6 +119,10 @@ void ui::scenerylist_panel::draw_launch_box() ImGui::TextWrapped(selected_trainset->description.c_str()); + ImGui::Checkbox(STR_C("Trainset turned around"), &selected_trainset->reversed); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("The first vehicle stays where the trainset starts and leads the other way,\nthe rest of the trainset stands behind it")); + if (ImGui::Button(STR_C("Launch"), ImVec2(-1, 0))) { if (!launch_simulation()) ImGui::OpenPopup("missing_driver"); @@ -163,7 +167,7 @@ void ui::scenerylist_panel::add_replace_entry(const trainset_desc &trainset) set += trainset.name + " "; set += trainset.track + " "; set += std::to_string(trainset.offset) + " "; - set += std::to_string(trainset.velocity) + "\n"; + set += std::to_string(trainset.velocity) + (trainset.reversed ? " reversed\n" : "\n"); for (const auto &veh : trainset.vehicles) { if (!veh.skin) { ErrorLog("trainset contains invalid vehicle " + veh.name); @@ -190,7 +194,8 @@ void ui::scenerylist_panel::add_replace_entry(const trainset_desc &trainset) void ui::scenerylist_panel::draw_trainset_box() { - ImGuiListClipper clipper(selected_scenery->trainsets.size()); + ImGuiListClipper clipper; + clipper.Begin(selected_scenery->trainsets.size()); while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) draw_trainset(selected_scenery->trainsets[i]); } diff --git a/launcher/scenery_scanner.cpp b/launcher/scenery_scanner.cpp index 7bfa8a07..bcd18ac9 100644 --- a/launcher/scenery_scanner.cpp +++ b/launcher/scenery_scanner.cpp @@ -95,6 +95,10 @@ void scenery_scanner::parse_trainset(cParser &parser) parser >> trainset.name >> trainset.track >> trainset.offset >> trainset.velocity; parser.getTokens(); + if (parser.peek() == "reversed") { + trainset.reversed = true; + parser.getTokens(); + } while (parser.peek() == "node") { trainset.vehicles.emplace_back(); dynamic_desc &dyn = trainset.vehicles.back(); diff --git a/launcher/scenery_scanner.h b/launcher/scenery_scanner.h index 44854438..fcda27da 100644 --- a/launcher/scenery_scanner.h +++ b/launcher/scenery_scanner.h @@ -30,6 +30,7 @@ struct trainset_desc { float offset { 0.f }; float velocity { 0.f }; + bool reversed { false }; std::vector vehicles; }; diff --git a/launcher/scenery_wizard.cpp b/launcher/scenery_wizard.cpp new file mode 100644 index 00000000..01485203 --- /dev/null +++ b/launcher/scenery_wizard.cpp @@ -0,0 +1,385 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "launcher/scenery_wizard.h" +#include "application/application.h" +#include "editor/editorFormat.hpp" +#include "editor/editorOrthophoto.hpp" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/translation.h" + +#include "stb/stb_image.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +double constexpr kSemiMajor{6378137.0}; +double constexpr kFlattening{1.0 / 298.257222101}; +double constexpr kScale{0.9993}; +double constexpr kFalseEasting{500000.0}; +double constexpr kFalseNorthing{-5300000.0}; +double constexpr kCentralMeridian{19.0}; +glm::dvec2 const kPolandLow{140000.0, 120000.0}; +glm::dvec2 const kPolandHigh{890000.0, 800000.0}; +std::string const kMapService{"https://mapy.geoportal.gov.pl/wss/ext/OSM/BaseMap/service"}; + +double seconds() +{ + return std::chrono::duration(std::chrono::steady_clock::now().time_since_epoch()).count(); +} + +glm::dvec2 to_puwg(double const Latitude, double const Longitude) +{ + auto const e2{2.0 * kFlattening - kFlattening * kFlattening}; + auto const ep2{e2 / (1.0 - e2)}; + auto const phi{glm::radians(Latitude)}; + auto const sine{std::sin(phi)}; + auto const cosine{std::cos(phi)}; + auto const n{kSemiMajor / std::sqrt(1.0 - e2 * sine * sine)}; + auto const t{std::tan(phi) * std::tan(phi)}; + auto const c{ep2 * cosine * cosine}; + auto const a{glm::radians(Longitude - kCentralMeridian) * cosine}; + auto const m{kSemiMajor * ((1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0) * phi - (3.0 * e2 / 8.0 + 3.0 * e2 * e2 / 32.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(2.0 * phi) + + (15.0 * e2 * e2 / 256.0 + 45.0 * e2 * e2 * e2 / 1024.0) * std::sin(4.0 * phi) - (35.0 * e2 * e2 * e2 / 3072.0) * std::sin(6.0 * phi))}; + auto const x{kScale * n * (a + (1.0 - t + c) * std::pow(a, 3) / 6.0 + (5.0 - 18.0 * t + t * t + 72.0 * c - 58.0 * ep2) * std::pow(a, 5) / 120.0)}; + auto const y{kScale * (m + n * std::tan(phi) * (a * a / 2.0 + (5.0 - t + 9.0 * c + 4.0 * c * c) * std::pow(a, 4) / 24.0 + (61.0 - 58.0 * t + t * t + 600.0 * c - 330.0 * ep2) * std::pow(a, 6) / 720.0))}; + return {kFalseEasting + x, y + kFalseNorthing}; +} + +glm::dvec2 to_degrees(glm::dvec2 const &Point) +{ + auto const e2{2.0 * kFlattening - kFlattening * kFlattening}; + auto const ep2{e2 / (1.0 - e2)}; + auto const x{Point.x - kFalseEasting}; + auto const m{(Point.y - kFalseNorthing) / kScale}; + auto const mu{m / (kSemiMajor * (1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2 * e2 * e2 / 256.0))}; + auto const e1{(1.0 - std::sqrt(1.0 - e2)) / (1.0 + std::sqrt(1.0 - e2))}; + auto const phi1{mu + (3.0 * e1 / 2.0 - 27.0 * std::pow(e1, 3) / 32.0) * std::sin(2.0 * mu) + (21.0 * e1 * e1 / 16.0 - 55.0 * std::pow(e1, 4) / 32.0) * std::sin(4.0 * mu) + + (151.0 * std::pow(e1, 3) / 96.0) * std::sin(6.0 * mu) + (1097.0 * std::pow(e1, 4) / 512.0) * std::sin(8.0 * mu)}; + auto const sine{std::sin(phi1)}; + auto const c1{ep2 * std::cos(phi1) * std::cos(phi1)}; + auto const t1{std::tan(phi1) * std::tan(phi1)}; + auto const n1{kSemiMajor / std::sqrt(1.0 - e2 * sine * sine)}; + auto const r1{kSemiMajor * (1.0 - e2) / std::pow(1.0 - e2 * sine * sine, 1.5)}; + auto const d{x / (n1 * kScale)}; + auto const phi{phi1 - (n1 * std::tan(phi1) / r1) * (d * d / 2.0 - (5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * ep2) * std::pow(d, 4) / 24.0 + + (61.0 + 90.0 * t1 + 298.0 * c1 + 45.0 * t1 * t1 - 252.0 * ep2 - 3.0 * c1 * c1) * std::pow(d, 6) / 720.0)}; + auto const lambda{(d - (1.0 + 2.0 * t1 + c1) * std::pow(d, 3) / 6.0 + (5.0 - 2.0 * c1 + 28.0 * t1 - 3.0 * c1 * c1 + 8.0 * ep2 + 24.0 * t1 * t1) * std::pow(d, 5) / 120.0) / std::cos(phi1)}; + return {glm::degrees(phi), kCentralMeridian + glm::degrees(lambda)}; +} + +bool valid_name(std::string const &Name) +{ + return false == Name.empty() && std::all_of(Name.begin(), Name.end(), [](char const Character) { return (Character >= 'a' && Character <= 'z') || (Character >= '0' && Character <= '9') || Character == '_' || Character == '-'; }); +} + +} // namespace + +ui::scenerywizard_panel::scenerywizard_panel() : ui_panel(STR("New scenery"), false) {} + +ui::scenerywizard_panel::~scenerywizard_panel() +{ + if (m_request.valid()) + m_request.wait(); + if (m_texture != 0) + glDeleteTextures(1, &m_texture); +} + +void ui::scenerywizard_panel::render_contents() +{ + upload_map(); + auto const available{ImGui::GetContentRegionAvail()}; + auto const form{360.f * Global.ui_scale}; + glm::vec2 const map{std::max(200.f, available.x - form - ImGui::GetStyle().ItemSpacing.x), std::max(200.f, available.y)}; + ImGui::BeginChild("##wizardmap", ImVec2(map.x, map.y), true, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + render_map(map - glm::vec2{16.f}); + ImGui::EndChild(); + ImGui::SameLine(); + ImGui::BeginChild("##wizardform", ImVec2(0.f, map.y), true); + render_form(); + ImGui::EndChild(); +} + +void ui::scenerywizard_panel::render_map(glm::vec2 const &Size) +{ + auto const origin{ImGui::GetCursorScreenPos()}; + if (false == m_fitted) + { + m_fitted = true; + m_centre = (kPolandLow + kPolandHigh) * 0.5; + m_scale = std::max((kPolandHigh.x - kPolandLow.x) / Size.x, (kPolandHigh.y - kPolandLow.y) / Size.y); + } + ImGui::InvisibleButton("##map", ImVec2(Size.x, Size.y)); + auto const hovered{ImGui::IsItemHovered()}; + auto const active{ImGui::IsItemActive()}; + auto const &io{ImGui::GetIO()}; + glm::vec2 const mouse{io.MousePos.x, io.MousePos.y}; + glm::vec2 const middle{origin.x + Size.x * 0.5f, origin.y + Size.y * 0.5f}; + auto const to_world = [&](glm::vec2 const &Screen) { return glm::dvec2{m_centre.x + (Screen.x - middle.x) * m_scale, m_centre.y - (Screen.y - middle.y) * m_scale}; }; + auto const to_screen = [&](glm::dvec2 const &World) { return ImVec2(middle.x + static_cast((World.x - m_centre.x) / m_scale), middle.y - static_cast((World.y - m_centre.y) / m_scale)); }; + + if (hovered && io.MouseWheel != 0.f) + { + auto const under{to_world(mouse)}; + m_scale = std::clamp(m_scale * std::pow(0.8, static_cast(io.MouseWheel)), 0.25, 2000.0); + m_centre = {under.x - (mouse.x - middle.x) * m_scale, under.y + (mouse.y - middle.y) * m_scale}; + m_dirty = true; + m_changed = seconds(); + } + if (ImGui::IsItemActivated()) + { + m_pressed = mouse; + m_dragging = false; + } + if (active) + { + if (glm::length(mouse - m_pressed) > 4.f) + m_dragging = true; + if (m_dragging && (io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f)) + { + m_centre.x -= io.MouseDelta.x * m_scale; + m_centre.y += io.MouseDelta.y * m_scale; + m_dirty = true; + m_changed = seconds(); + } + } + if (ImGui::IsItemDeactivated() && false == m_dragging) + pick(to_world(mouse)); + + if (m_dirty && seconds() - m_changed > 0.25 && false == m_request.valid()) + request_map(Size); + + auto *drawlist{ImGui::GetWindowDrawList()}; + drawlist->PushClipRect(origin, ImVec2(origin.x + Size.x, origin.y + Size.y), true); + drawlist->AddRectFilled(origin, ImVec2(origin.x + Size.x, origin.y + Size.y), IM_COL32(200, 214, 222, 255)); + if (m_texture != 0) + drawlist->AddImage((ImTextureID)(static_cast(m_texture)), to_screen({m_image_low.x, m_image_high.y}), to_screen({m_image_high.x, m_image_low.y})); + if (m_picked) + { + auto const at{to_screen(m_point)}; + auto const colour{IM_COL32(220, 30, 30, 255)}; + drawlist->AddCircle(at, 10.f, colour, 24, 2.5f); + drawlist->AddLine(ImVec2(at.x - 16.f, at.y), ImVec2(at.x + 16.f, at.y), colour, 2.f); + drawlist->AddLine(ImVec2(at.x, at.y - 16.f), ImVec2(at.x, at.y + 16.f), colour, 2.f); + } + std::string info; + if (hovered) + { + auto const world{to_world(mouse)}; + auto const degrees{to_degrees(world)}; + info = format(STR_C("E %.3f km, N %.3f km (%.5f N, %.5f E)"), world.x / 1000.0, world.y / 1000.0, degrees.x, degrees.y); + } + if (m_request.valid()) + info += (info.empty() ? "" : " ") + std::string{STR_C("loading the map...")}; + else if (false == m_map_error.empty()) + info += (info.empty() ? "" : " ") + m_map_error; + if (false == info.empty()) + { + auto const size{ImGui::CalcTextSize(info.c_str())}; + drawlist->AddRectFilled(ImVec2(origin.x + 4.f, origin.y + 4.f), ImVec2(origin.x + 12.f + size.x, origin.y + 8.f + size.y), IM_COL32(0, 0, 0, 170), 4.f); + drawlist->AddText(ImVec2(origin.x + 8.f, origin.y + 6.f), IM_COL32(255, 255, 255, 240), info.c_str()); + } + drawlist->PopClipRect(); +} + +void ui::scenerywizard_panel::request_map(glm::vec2 const &Size) +{ + m_dirty = false; + auto const width{std::clamp(static_cast(Size.x), 64, 2048)}; + auto const height{std::clamp(static_cast(Size.y), 64, 2048)}; + glm::dvec2 const half{width * 0.5 * m_scale, height * 0.5 * m_scale}; + auto const low{m_centre - half}; + auto const high{m_centre + half}; + char bbox[160]; + std::snprintf(bbox, sizeof(bbox), "%.1f,%.1f,%.1f,%.1f", low.x, low.y, high.x, high.y); + auto const url{kMapService + "?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetMap&LAYERS=osm&STYLES=&SRS=EPSG:2180&BBOX=" + bbox + "&WIDTH=" + std::to_string(width) + "&HEIGHT=" + std::to_string(height) + "&FORMAT=image/png"}; + m_request = std::async(std::launch::async, [url, low, high, width, height]() { + map_result result; + result.low = low; + result.high = high; + std::vector data; + if (false == editor_http_get(url, data, result.error)) + return result; + int w{0}, h{0}, components{0}; + auto *pixels{stbi_load_from_memory(data.data(), static_cast(data.size()), &w, &h, &components, 4)}; + if (pixels == nullptr) + { + result.error = STR("The map service sent no image"); + return result; + } + result.width = w; + result.height = h; + result.pixels.assign(pixels, pixels + static_cast(w) * h * 4); + stbi_image_free(pixels); + return result; + }); +} + +void ui::scenerywizard_panel::upload_map() +{ + if (false == m_request.valid() || m_request.wait_for(std::chrono::seconds(0)) != std::future_status::ready) + return; + auto result{m_request.get()}; + if (false == result.error.empty() || result.pixels.empty()) + { + m_map_error = result.error.empty() ? std::string{STR("The map service sent no image")} : STR("No map: ") + result.error; + return; + } + m_map_error.clear(); + if (m_texture == 0) + glGenTextures(1, &m_texture); + glBindTexture(GL_TEXTURE_2D, m_texture); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, result.width, result.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, result.pixels.data()); + glBindTexture(GL_TEXTURE_2D, 0); + m_image_low = result.low; + m_image_high = result.high; +} + +void ui::scenerywizard_panel::pick(glm::dvec2 const &Point) +{ + m_point = Point; + m_picked = true; + auto const degrees{to_degrees(Point)}; + std::snprintf(m_latlon, sizeof(m_latlon), "%.6f, %.6f", degrees.x, degrees.y); +} + +void ui::scenerywizard_panel::render_form() +{ + ImGui::TextUnformatted(STR_C("New scenery")); + ImGui::Separator(); + ImGui::TextWrapped("%s", STR_C("Find the place on the map: the wheel zooms, dragging moves the map. A click sets the centre of the scenery, its point 0 0 0.")); + ImGui::Spacing(); + + ImGui::SetNextItemWidth(-1.f); + ImGui::InputTextWithHint("##name", STR_C("file name, e.g. my_line"), m_filename, sizeof(m_filename), ImGuiInputTextFlags_CharsNoBlank); + std::string name{m_filename}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + auto const path{Global.asCurrentSceneryPath + name + ".scn"}; + auto const named{valid_name(name)}; + auto const exists{named && std::filesystem::exists(path)}; + if (name.empty()) + ImGui::TextDisabled("%s", STR_C("scenery/.scn")); + else if (false == named) + ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), "%s", STR_C("Lower case letters, digits, _ and - only")); + else if (exists) + ImGui::TextColored(ImVec4(1.f, 0.45f, 0.35f, 1.f), STR_C("%s exists already"), path.c_str()); + else + ImGui::TextDisabled("%s", path.c_str()); + + ImGui::SetNextItemWidth(-1.f); + ImGui::InputTextWithHint("##title", STR_C("title, written in the file"), m_title, sizeof(m_title)); + ImGui::Spacing(); + + ImGui::TextUnformatted(STR_C("Centre of the scenery, PUWG 1992")); + double east{m_point.x / 1000.0}; + double north{m_point.y / 1000.0}; + ImGui::SetNextItemWidth(130.f); + auto changed{ImGui::InputDouble(STR_C("E, km##east"), &east, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::SetNextItemWidth(130.f); + changed |= ImGui::InputDouble(STR_C("N, km##north"), &north, 0.0, 0.0, "%.3f", ImGuiInputTextFlags_EnterReturnsTrue); + if (changed) + { + pick({east * 1000.0, north * 1000.0}); + m_centre = m_point; + m_dirty = true; + } + ImGui::SetNextItemWidth(-90.f); + auto const entered{ImGui::InputTextWithHint("##latlon", STR_C("latitude, longitude"), m_latlon, sizeof(m_latlon), ImGuiInputTextFlags_EnterReturnsTrue)}; + ImGui::SameLine(); + if (ImGui::Button(STR_C("Show")) || entered) + { + std::string text{m_latlon}; + std::replace(text.begin(), text.end(), ',', ' '); + std::istringstream words(text); + double latitude{0.0}, longitude{0.0}; + if (words >> latitude >> longitude && latitude > 45.0 && latitude < 58.0 && longitude > 10.0 && longitude < 28.0) + { + pick(to_puwg(latitude, longitude)); + m_centre = m_point; + m_scale = std::min(m_scale, 4.0); + m_dirty = true; + m_status.clear(); + } + else + m_status = STR("Give the latitude and the longitude in degrees, e.g. 52.2297, 21.0122"); + } + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", STR_C("Degrees, as given by the online maps; the map moves there")); + if (m_picked) + { + auto const degrees{to_degrees(m_point)}; + ImGui::TextDisabled(STR_C("%.5f N, %.5f E"), degrees.x, degrees.y); + } + ImGui::Spacing(); + ImGui::TextWrapped("%s", STR_C("The scenery keeps the centre as its geographic reference (//$g PUWG1992): the orthophoto of the editor lies under it, in place.")); + ImGui::Spacing(); + + auto const ready{named && false == exists && m_picked}; + if (false == ready) + ImGui::TextDisabled("%s", m_picked ? STR_C("Give the file a name") : STR_C("Click the centre on the map")); + if (ImGui::Button(STR_C("Create and open in the editor"), ImVec2(-1.f, 0.f)) && ready) + create_scenery(); + if (false == m_status.empty()) + ImGui::TextWrapped("%s", m_status.c_str()); + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + ImGui::TextWrapped("%s", STR_C("Map: (c) OpenStreetMap contributors, served by geoportal.gov.pl")); + ImGui::PopStyleColor(); +} + +bool ui::scenerywizard_panel::create_scenery() +{ + std::string name{m_filename}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + auto const file{name + ".scn"}; + std::ofstream output(Global.asCurrentSceneryPath + file, std::ios::binary); + if (false == output.is_open()) + { + m_status = STR("The file can't be written"); + return false; + } + auto const now{std::time(nullptr)}; + char date[32]; + std::strftime(date, sizeof(date), "%Y-%m-%d", std::localtime(&now)); + char reference[96]; + std::snprintf(reference, sizeof(reference), "//$g PUWG1992 %.3f %.3f", m_point.x / 1000.0, m_point.y / 1000.0); + output << reference << "\r\n"; + if (m_title[0] != '\0') + output << "// " << m_title << "\r\n"; + output << "// created in the editor " << date << "\r\n\r\n"; + output << "atmo 0.423 0.702 1.0 25000 25000 0.70 0.80 0.9 0 endatmo\r\n"; + output << "time 12:00 5:00 20:00 endtime\r\n\r\n"; + output << "FirstInit\r\n"; + output.close(); + WriteLog("Scenery wizard: created " + file + " at " + reference); + + Global.SceneryFile = file; + Global.local_start_vehicle = "ghostview"; + Global.editor_session = true; + Application.pop_mode(); + Application.push_mode(eu07_application::mode::scenarioloader); + return true; +} diff --git a/launcher/scenery_wizard.h b/launcher/scenery_wizard.h new file mode 100644 index 00000000..fb120c31 --- /dev/null +++ b/launcher/scenery_wizard.h @@ -0,0 +1,70 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "application/uilayer.h" + +#include +#include +#include +#include + +namespace ui +{ + +class scenerywizard_panel : public ui_panel +{ + public: + scenerywizard_panel(); + ~scenerywizard_panel() override; + + void render_contents() override; + + private: + struct map_result + { + glm::dvec2 low{0.0}; + glm::dvec2 high{0.0}; + int width{0}; + int height{0}; + std::vector pixels; + std::string error; + }; + + void render_map(glm::vec2 const &Size); + void render_form(); + void request_map(glm::vec2 const &Size); + void upload_map(); + void pick(glm::dvec2 const &Point); + bool create_scenery(); + + glm::dvec2 m_centre{515000.0, 460000.0}; + double m_scale{1000.0}; + bool m_fitted{false}; + double m_changed{0.0}; + bool m_dirty{true}; + glm::vec2 m_pressed{0.f}; + bool m_dragging{false}; + + std::future m_request; + unsigned int m_texture{0}; + glm::dvec2 m_image_low{0.0}; + glm::dvec2 m_image_high{0.0}; + std::string m_map_error; + + bool m_picked{false}; + glm::dvec2 m_point{0.0}; + char m_filename[64]{}; + char m_title[128]{}; + char m_latlon[64]{}; + std::string m_status; +}; + +} // namespace ui diff --git a/launcher/vehicle_picker.cpp b/launcher/vehicle_picker.cpp index 9fe0a29a..0ab862f0 100644 --- a/launcher/vehicle_picker.cpp +++ b/launcher/vehicle_picker.cpp @@ -78,7 +78,8 @@ void ui::vehiclepicker_panel::render_contents() } if (ImGui::BeginChild("box2")) { - ImGuiListClipper clipper(model_list.size()); + ImGuiListClipper clipper; + clipper.Begin(model_list.size()); while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { auto group = model_list[i]; @@ -109,7 +110,8 @@ void ui::vehiclepicker_panel::render_contents() } if (ImGui::BeginChild("box2")) { - ImGuiListClipper clipper(model_list.size()); + ImGuiListClipper clipper; + clipper.Begin(model_list.size()); while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { auto &desc = model_list[i]; @@ -141,7 +143,8 @@ void ui::vehiclepicker_panel::render_contents() skinset_list.end()); if (ImGui::BeginChild("box3")) { - ImGuiListClipper clipper(skinset_list.size()); + ImGuiListClipper clipper; + clipper.Begin(skinset_list.size()); while (clipper.Step()) for (int i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { auto skin = skinset_list[i]; diff --git a/model/AnimModel.cpp b/model/AnimModel.cpp index 1d808676..770790c1 100644 --- a/model/AnimModel.cpp +++ b/model/AnimModel.cpp @@ -251,12 +251,26 @@ TAnimModel::TAnimModel( scene::node_data const &Nodedata ) : basic_node( Nodedat bool TAnimModel::Init(std::string const &asName, std::string const &asReplacableTexture) { + m_skintoken = asReplacableTexture; if( asReplacableTexture.substr( 0, 1 ) == "*" ) { // od gwiazdki zaczynają się teksty na wyświetlaczach asText = asReplacableTexture.substr( 1, asReplacableTexture.length() - 1 ); // zapamiętanie tekstu } else if( asReplacableTexture != "none" ) { - m_materialdata.assign( asReplacableTexture ); + // the same skin is typically shared by lots of instances, and locating its files takes a couple dozen disk lookups. + // NOTE: only located skins are kept, so a missing one is still searched for each time, the way it used to be + static std::unordered_map skins; + auto const skinkey { Global.asCurrentTexturePath + '|' + asReplacableTexture }; + auto const lookup { skins.find( skinkey ) }; + if( lookup != skins.end() ) { + m_materialdata = lookup->second; + } + else { + m_materialdata.assign( asReplacableTexture ); + if( m_materialdata.replacable_skins[ 1 ] != null_handle ) { + skins.emplace( skinkey, m_materialdata ); + } + } } // TODO: redo the random timer initialization @@ -277,12 +291,9 @@ TAnimModel::is_keyword( std::string const &Token ) const { || Token == "notransition"; } -bool TAnimModel::Load(cParser *parser, bool ter) -{ // rozpoznanie wpisu modelu i ustawienie świateł - std::string name = parser->getToken(); - std::string texture = parser->getToken(false); - replace_slashes( name ); - replace_slashes( texture ); +// assigns the model and the replacable skin to the instance, and binds light and variant submodels of the model +void TAnimModel::assign_model( std::string &name, std::string const &texture, bool const ter ) +{ if (!Init( name, texture )) { if (name != "notload") @@ -303,30 +314,27 @@ bool TAnimModel::Load(cParser *parser, bool ter) } else { // wiązanie świateł, o ile model wczytany - LightsOn[0] = pModel->GetFromName("Light_On00"); - LightsOn[1] = pModel->GetFromName("Light_On01"); - LightsOn[2] = pModel->GetFromName("Light_On02"); - LightsOn[3] = pModel->GetFromName("Light_On03"); - LightsOn[4] = pModel->GetFromName("Light_On04"); - LightsOn[5] = pModel->GetFromName("Light_On05"); - LightsOn[6] = pModel->GetFromName("Light_On06"); - LightsOn[7] = pModel->GetFromName("Light_On07"); - LightsOff[0] = pModel->GetFromName("Light_Off00"); - LightsOff[1] = pModel->GetFromName("Light_Off01"); - LightsOff[2] = pModel->GetFromName("Light_Off02"); - LightsOff[3] = pModel->GetFromName("Light_Off03"); - LightsOff[4] = pModel->GetFromName("Light_Off04"); - LightsOff[5] = pModel->GetFromName("Light_Off05"); - LightsOff[6] = pModel->GetFromName("Light_Off06"); - LightsOff[7] = pModel->GetFromName("Light_Off07"); - sm_winter_variant = pModel->GetFromName("winter_variant"); - sm_spring_variant = pModel->GetFromName("spring_variant"); - sm_summer_variant = pModel->GetFromName("summer_variant"); - sm_autumn_variant = pModel->GetFromName("autumn_variant"); + // the submodels are located once for the model and shared by its instances + auto const &submodels { pModel->instance_parts() }; + LightsOn = submodels.lights_on; + LightsOff = submodels.lights_off; + sm_winter_variant = submodels.variants[ 0 ]; + sm_spring_variant = submodels.variants[ 1 ]; + sm_summer_variant = submodels.variants[ 2 ]; + sm_autumn_variant = submodels.variants[ 3 ]; } for (int i = 0; i < iMaxNumLights; ++i) if (LightsOn[i] || LightsOff[i]) // Ra: zlikwidowałem wymóg istnienia obu iNumLights = i + 1; +} + +bool TAnimModel::Load(cParser *parser, bool ter) +{ // rozpoznanie wpisu modelu i ustawienie świateł + std::string name = parser->getToken(); + std::string texture = parser->getToken(false); + replace_slashes( name ); + replace_slashes( texture ); + assign_model( name, texture, ter ); std::string token; do { @@ -398,6 +406,46 @@ bool TAnimModel::Load(cParser *parser, bool ter) return true; } +// sets up the instance from the values of a definition which was already taken apart, the way Load() does it from the text +void TAnimModel::Load( std::string const &Name, std::string const &Texture, TAnimModel const *Twin, glm::vec3 const *Angles, glm::vec3 const *Scale, bool const Transition ) +{ + if( Twin != nullptr + && Twin->pModel != nullptr + && ( Twin->m_materialdata.replacable_skins[ 1 ] != null_handle || Texture == "none" ) ) { + // the same model with the same skin was located for the twin already. + // NOTE: a model or a skin which couldn't be located is searched for and reported for each instance, like it always was + m_skintoken = Twin->m_skintoken; + m_materialdata = Twin->m_materialdata; + pModel = Twin->pModel; + LightsOn = Twin->LightsOn; + LightsOff = Twin->LightsOff; + sm_winter_variant = Twin->sm_winter_variant; + sm_spring_variant = Twin->sm_spring_variant; + sm_summer_variant = Twin->sm_summer_variant; + sm_autumn_variant = Twin->sm_autumn_variant; + iNumLights = Twin->iNumLights; + } + else { + auto name { Name }; + assign_model( name, Texture, false ); + } + // the optional blocks, each with the effect it has in Load() + if( Angles != nullptr ) { + vAngle = *Angles; + } + if( Scale != nullptr + && Scale->x > 0.0f && Scale->y > 0.0f && Scale->z > 0.0f ) { + m_scale.x *= Scale->x; + m_scale.y *= Scale->y; + m_scale.z *= Scale->z; + } + if( false == Transition ) { + m_transition = false; + } + + update_instanceable_flag(); +} + namespace { // returns true if this animation type mutates per-instance state on the shared // TSubModel tree, which would make batched rendering unsafe. Most animations @@ -806,13 +854,18 @@ TAnimModel::export_as_text_( std::ostream &Output ) const { } Output << modelfile << ' '; // texture - auto texturefile { ( - m_materialdata.replacable_skins[ 1 ] != null_handle ? - GfxRenderer->Material( m_materialdata.replacable_skins[ 1 ] )->GetName() : - "none" ) }; - if( texturefile.find( paths::textures ) == 0 ) { - // don't include 'textures/' in the path - texturefile.erase( 0, std::string{ paths::textures }.size() ); + // the skin goes out the way it came in. the materials it was turned into can't give it back: they don't tell + // a set of skins from a single one, a text for a display from no skin at all, or what a texture was to be generated from + auto texturefile { m_skintoken }; + if( texturefile.empty() ) { + texturefile = ( + m_materialdata.replacable_skins[ 1 ] != null_handle ? + GfxRenderer->Material( m_materialdata.replacable_skins[ 1 ] )->GetName() : + "none" ); + if( texturefile.find( paths::textures ) == 0 ) { + // don't include 'textures/' in the path + texturefile.erase( 0, std::string{ paths::textures }.size() ); + } } if( contains( texturefile, ' ' ) ) { Output << "\"" << texturefile << "\"" << ' '; @@ -827,6 +880,23 @@ TAnimModel::export_as_text_( std::ostream &Output ) const { Output << lsLights[ lightidx ] << ' '; } } + // colors given to the lights; goes right after the lights, the way it's read + if( std::any_of( std::begin( m_lightcolors ), std::begin( m_lightcolors ) + iNumLights, []( glm::vec3 const &Color ) { return Color.r >= 0.f; } ) ) { + Output << "lightcolors "; + for( int lightidx = 0; lightidx < iNumLights; ++lightidx ) { + auto const &color { m_lightcolors[ lightidx ] }; + if( color.r < 0.f ) { + // the light was left with the color the model gives it + Output << "-1 "; + continue; + } + auto const component = []( float const Value ) { return std::clamp( static_cast( std::lround( Value * 255.f ) ), 0, 255 ); }; + Output + << std::hex << std::setfill( '0' ) << std::setw( 6 ) + << ( component( color.r ) << 16 | component( color.g ) << 8 | component( color.b ) ) + << std::dec << std::setfill( ' ' ) << ' '; + } + } // potential light transition switch if( false == m_transition ) { Output << "notransition" << ' '; diff --git a/model/AnimModel.h b/model/AnimModel.h index 3c4b9f11..0087418d 100644 --- a/model/AnimModel.h +++ b/model/AnimModel.h @@ -46,7 +46,7 @@ class TAnimVocaloidFrame class basic_event; -class TAnimContainer : std::enable_shared_from_this +class TAnimContainer : public std::enable_shared_from_this { // opakowanie submodelu, określające animację egzemplarza - obsługiwane jako lista friend TAnimModel; @@ -118,6 +118,11 @@ public: static void AnimUpdate( double dt ); bool Init(std::string const &asName, std::string const &asReplacableTexture); bool Load(cParser *parser, bool ter = false); + // sets up the instance from the values of a definition which was already taken apart, the way Load(cParser *) does it + // from the text. Name, Texture: as that method gets them ready, lower case model name and forward slashes in both. + // Twin: instance set up for the same model and texture, to take them after instead of locating them again; can be nullptr. + // Angles, Scale: content of the optional blocks of the definition, nullptr for a block which isn't there + void Load( std::string const &Name, std::string const &Texture, TAnimModel const *Twin, glm::vec3 const *Angles, glm::vec3 const *Scale, bool const Transition ); std::shared_ptr AddContainer(std::string const &Name); std::shared_ptr GetContainer(std::string const &Name = ""); void LightSet( int const n, float const v ); @@ -201,6 +206,7 @@ public: material_data m_materialdata; std::string asText; // tekst dla wyświetlacza znakowego + std::string m_skintoken; // replacable skin the way the definition of the node gave it, kept for the export // TODO: wrap into a light state struct, remove fixed element count int iNumLights { 0 }; std::array LightsOn {}; // Ra: te wskaźniki powinny być w ramach TModel3d @@ -225,6 +231,8 @@ public: bool m_instanceable { false }; // helper: evaluates current state and updates m_instanceable accordingly. void update_instanceable_flag(); + // helper: assigns the model and the replacable skin to the instance, and binds light and variant submodels of the model + void assign_model( std::string &name, std::string const &texture, bool const ter ); // diagnostic counters (process-wide). Updated inside update_instanceable_flag() // so the renderer can surface load-time classification stats in the debug overlay. diff --git a/model/Model3d.cpp b/model/Model3d.cpp index b67663f9..66fc08c9 100644 --- a/model/Model3d.cpp +++ b/model/Model3d.cpp @@ -1624,6 +1624,23 @@ TSubModel *TModel3d::GetFromName(std::string const &Name) const return Root ? Root->GetFromName(Name) : nullptr; }; +// returns submodels which scenery instances of the model refer to by name, locating them on first use +TModel3d::instance_submodels const &TModel3d::instance_parts() +{ + if (false == m_instancesubmodelslocated) + { + for (std::size_t idx = 0; idx < m_instancesubmodels.lights_on.size(); ++idx) + { + auto const index{(idx < 10 ? "0" : "") + std::to_string(idx)}; + m_instancesubmodels.lights_on[idx] = GetFromName("Light_On" + index); + m_instancesubmodels.lights_off[idx] = GetFromName("Light_Off" + index); + } + m_instancesubmodels.variants = {GetFromName("winter_variant"), GetFromName("spring_variant"), GetFromName("summer_variant"), GetFromName("autumn_variant")}; + m_instancesubmodelslocated = true; + } + return m_instancesubmodels; +} + // locates particle source submodels and stores them on internal list nameoffset_sequence const &TModel3d::find_smoke_sources() { @@ -1978,8 +1995,121 @@ void TSubModel::deserialize(std::istream &s) m_rotation_init_done = true; } +namespace +{ +// read only stream buffer over the content of a whole e3d file kept in memory. +// reading the geometry of a model value by value from a file stream costs several times more than the read of the file itself +class e3d_memorybuffer : public std::streambuf +{ + public: + e3d_memorybuffer(char *Begin, char *End) + { + setg(Begin, Begin, End); + } + // data from the current read position on + unsigned char const *cursor() const + { + return reinterpret_cast(gptr()); + } + std::size_t available() const + { + return static_cast(egptr() - gptr()); + } + + protected: + pos_type seekoff(off_type Offset, std::ios_base::seekdir Direction, std::ios_base::openmode) override + { + off_type const size{egptr() - eback()}; + off_type const origin{Direction == std::ios_base::beg ? 0 : Direction == std::ios_base::cur ? (gptr() - eback()) + m_beyondend : size}; + off_type const position{origin + Offset}; + if (position < 0) + { + return pos_type(off_type(-1)); + } + // like a file stream it can be placed past the end of the data, where nothing more can be read + m_beyondend = std::max(position - size, 0); + setg(eback(), eback() + std::min(position, size), egptr()); + return pos_type(position); + } + pos_type seekpos(pos_type Position, std::ios_base::openmode Mode) override + { + return seekoff(off_type(Position), std::ios_base::beg, Mode); + } + + private: + off_type m_beyondend{0}; +}; + +// little endian values taken from memory the same way the sn_utils functions read them from a stream +std::uint16_t e3d_uint16(unsigned char const *&Data) +{ + std::uint16_t const value = static_cast(Data[1] << 8 | Data[0]); + Data += 2; + return value; +} +std::uint32_t e3d_uint32(unsigned char const *&Data) +{ + std::uint32_t const value = std::uint32_t{Data[3]} << 24 | std::uint32_t{Data[2]} << 16 | std::uint32_t{Data[1]} << 8 | std::uint32_t{Data[0]}; + Data += 4; + return value; +} +std::uint64_t e3d_uint64(unsigned char const *&Data) +{ + std::uint64_t const low{e3d_uint32(Data)}; + std::uint64_t const high{e3d_uint32(Data)}; + return high << 32 | low; +} +float e3d_float32(unsigned char const *&Data) +{ + return std::bit_cast(e3d_uint32(Data)); +} +glm::vec3 e3d_vec3(unsigned char const *&Data) +{ + glm::vec3 value; + value.x = e3d_float32(Data); + value.y = e3d_float32(Data); + value.z = e3d_float32(Data); + return value; +} +glm::vec4 e3d_vec4(unsigned char const *&Data) +{ + glm::vec4 value; + value.x = e3d_float32(Data); + value.y = e3d_float32(Data); + value.z = e3d_float32(Data); + value.w = e3d_float32(Data); + return value; +} +// counterparts of basic_vertex::deserialize() and basic_vertex::deserialize_packed() +void e3d_vertex(unsigned char const *&Data, gfx::basic_vertex &Vertex, bool const Tangent) +{ + Vertex.position = e3d_vec3(Data); + Vertex.normal = e3d_vec3(Data); + Vertex.texture.x = e3d_float32(Data); + Vertex.texture.y = e3d_float32(Data); + if (Tangent) + { + Vertex.tangent = e3d_vec4(Data); + } +} +void e3d_vertex_packed(unsigned char const *&Data, gfx::basic_vertex &Vertex, bool const Tangent) +{ + Vertex.position = glm::unpackHalf4x16(e3d_uint64(Data)); + Vertex.normal = glm::unpackSnorm3x10_1x2(e3d_uint32(Data)); + Vertex.texture.x = glm::unpackHalf1x16(e3d_uint16(Data)); + Vertex.texture.y = glm::unpackHalf1x16(e3d_uint16(Data)); + if (Tangent) + { + Vertex.tangent = glm::unpackSnorm3x10_1x2(e3d_uint32(Data)); + } +} +} // namespace + void TModel3d::deserialize(std::istream &s, size_t size, bool dynamic) { + // a model loaded by LoadFromBinFile() comes from memory, its geometry is taken straight from there + auto *const memory{dynamic_cast(s.rdbuf())}; + Root = nullptr; if (m_geometrybank == null_handle) { @@ -2066,6 +2196,67 @@ void TModel3d::deserialize(std::istream &s, size_t size, bool dynamic) if (hasuserdata) submodel.Userdata.resize(submodelgeometry.vertex_count); m_vertexcount += submodelgeometry.vertex_count; + if (memory != nullptr) + { + // geometry past the end of the file is left as it was made + auto const datasize{vertex_size * sizeof(float)}; + auto const vertexcount{std::min(submodel.Vertices.size(), (datasize > 0 ? memory->available() / datasize : 0))}; + if ((datasize > 0) && (vertexcount < submodel.Vertices.size())) + { + ErrorLog("Bad model: vertex data of sub-model " + std::to_string(submodeloffset.second) + " lies past the end of the file", logtype::model); + } + auto *data{memory->cursor()}; + switch (vertextype & 3) + { + case 0: + { + // legacy vnt0 format + for (std::size_t i = 0; i < vertexcount; ++i) + { + e3d_vertex(data, submodel.Vertices[i], hastangents); + if (hasuserdata) + submodel.Userdata[i].data = e3d_vec4(data); + if (submodel.eType < TP_ROTATOR) + { + // normal vectors debug routine + if (false == submodel.m_normalizenormals && std::abs(glm::length2(submodel.Vertices[i].normal) - 1.0f) > 0.01f) + { + submodel.m_normalizenormals = TSubModel::normalize; // we don't know if uniform scaling would suffice + WriteLog("Bad model: non-unit normal vector(s) encountered during sub-model geometry deserialization", logtype::model); + } + } + } + break; + } + case 1: + { + // expanded chunk formats + for (std::size_t i = 0; i < vertexcount; ++i) + { + e3d_vertex_packed(data, submodel.Vertices[i], hastangents); + if (hasuserdata) + submodel.Userdata[i].data = glm::unpackHalf4x16(e3d_uint64(data)); + } + break; + } + case 2: + { + // expanded chunk formats + for (std::size_t i = 0; i < vertexcount; ++i) + { + e3d_vertex(data, submodel.Vertices[i], hastangents); + if (hasuserdata) + submodel.Userdata[i].data = e3d_vec4(data); + } + break; + } + default: + { + break; + } + } + continue; + } switch (vertextype & 3) { case 0: @@ -2146,6 +2337,48 @@ void TModel3d::deserialize(std::istream &s, size_t size, bool dynamic) auto const &submodelgeometry{submodel.m_geometry}; submodel.Indices.resize(submodelgeometry.index_count); m_indexcount += submodelgeometry.index_count; + if (memory != nullptr) + { + // indices past the end of the file are left as zeroes + auto const indexcount{std::min(submodel.Indices.size(), (indexsize > 0 ? memory->available() / indexsize : 0))}; + if ((indexsize > 0) && (indexcount < submodel.Indices.size())) + { + ErrorLog("Bad model: index data of sub-model " + std::to_string(submodeloffset.second) + " lies past the end of the file", logtype::model); + } + auto *data{memory->cursor()}; + switch (indexsize) + { + case 1: + { + for (std::size_t i = 0; i < indexcount; ++i) + { + submodel.Indices[i] = *data++; + } + break; + } + case 2: + { + for (std::size_t i = 0; i < indexcount; ++i) + { + submodel.Indices[i] = e3d_uint16(data); + } + break; + } + case 4: + { + for (std::size_t i = 0; i < indexcount; ++i) + { + submodel.Indices[i] = e3d_uint32(data); + } + break; + } + default: + { + break; + } + } + continue; + } switch (indexsize) { case 1: @@ -2417,7 +2650,22 @@ void TModel3d::LoadFromBinFile(std::string const &FileName, bool dynamic) { // wczytanie modelu z pliku binarnego WriteLog("Loading binary format 3d model data from \"" + FileName + "\"...", logtype::model); - std::ifstream file(FileName, std::ios::binary); + // the file is read whole in one go and parsed from memory + std::unique_ptr filedata; + std::size_t filesize{0}; + { + std::ifstream input(FileName, std::ios::binary | std::ios::ate); + auto const length{input.tellg()}; + if (length > 0) + { + filedata.reset(new char[static_cast(length)]); + input.seekg(0); + input.read(filedata.get(), length); + filesize = static_cast(input.gcount()); + } + } + e3d_memorybuffer buffer(filedata.get(), filedata.get() + filesize); + std::istream file(&buffer); uint32_t type = sn_utils::ld_uint32(file); uint32_t size = sn_utils::ld_uint32(file) - 8; @@ -2425,7 +2673,6 @@ void TModel3d::LoadFromBinFile(std::string const &FileName, bool dynamic) if (type == MAKE_ID4('E', '3', 'D', '0')) { deserialize(file, size, dynamic); - file.close(); WriteLog("Finished loading 3d model data from \"" + FileName + "\"", logtype::model); } @@ -2433,7 +2680,6 @@ void TModel3d::LoadFromBinFile(std::string const &FileName, bool dynamic) { // throw std::runtime_error("e3d: unknown main chunk"); ErrorLog("Bad model: unknown main chunk in file \"" + FileName + "\"", logtype::model); - file.close(); } }; diff --git a/model/Model3d.h b/model/Model3d.h index 53e9084f..27565c20 100644 --- a/model/Model3d.h +++ b/model/Model3d.h @@ -272,6 +272,12 @@ class TModel3d friend opengl33_renderer; public: + // submodels which scenery instances of the model refer to by name + struct instance_submodels { + std::array lights_on {}; // Light_On00 - Light_On07 + std::array lights_off {}; // Light_Off00 - Light_Off07 + std::array variants {}; // winter_variant, spring_variant, summer_variant, autumn_variant + }; TSubModel *Root { nullptr }; // drzewo submodeli uint32_t iFlags { 0 }; // Ra: czy submodele mają przezroczyste tekstury public: // Ra: tymczasowo @@ -286,6 +292,8 @@ private: std::string asBinary; // nazwa pod którą zapisać model binarny std::string m_filename; nameoffset_sequence m_smokesources; // list of particle sources defined in the model + instance_submodels m_instancesubmodels; // filled on first use, see instance_parts() + bool m_instancesubmodelslocated { false }; public: TModel3d() = default; @@ -294,6 +302,9 @@ public: return Root ? Root->m_boundingradius : 0.f; } inline TSubModel * GetSMRoot() { return Root; }; TSubModel * GetFromName(std::string const &Name) const; + // returns submodels which scenery instances of the model refer to by name. the search for a name goes through the whole + // submodel tree, so it's done once for the model instead of once for each of its instances + instance_submodels const & instance_parts(); TSubModel * AddToNamed(const char *Name, TSubModel *SubModel); nameoffset_sequence const & find_smoke_sources(); void AddTo(TSubModel *tmp, TSubModel *SubModel); diff --git a/model/ResourceManager.h b/model/ResourceManager.h index c9f3cb0b..c9a1036d 100644 --- a/model/ResourceManager.h +++ b/model/ResourceManager.h @@ -67,6 +67,10 @@ public: std::chrono::steady_clock::time_point timestamp() const { return m_resourcetimestamp; } + // updates the time given to resources marked as used, without a sweep + void + refresh() { + m_resourcetimestamp = std::chrono::steady_clock::now(); } private: // members: diff --git a/model/Texture.cpp b/model/Texture.cpp index a4b9def4..f5b57ebb 100644 --- a/model/Texture.cpp +++ b/model/Texture.cpp @@ -30,6 +30,134 @@ http://mozilla.org/MPL/2.0/. #define EU07_DEFERRED_TEXTURE_UPLOAD +// texture data read by a worker thread. the texture itself is only touched by the main thread, +// which takes the data over when the texture is created on the gl side +struct texture_request { + std::string file; + std::size_t offset { 0 }; // start of the texture data in the file + std::size_t size { 0 }; // size of the texture data + // dds data with top-left origin is flipped by the worker + bool flip { false }; + std::uint32_t fourcc { 0 }; + int width { 0 }; + int height { 0 }; + int mapcount { 0 }; + int blocksize { 0 }; + // results, written by the thread reading the data + std::vector data; + bool failed { false }; + bool done { false }; // guarded by the mutex of the loader +}; + +namespace { + +// worker threads reading texture data from disk +class texture_loader { + +public: + static texture_loader &instance() { + static texture_loader loader; + return loader; } + + void submit( std::shared_ptr Request ) { + { + std::lock_guard lock( m_mutex ); + if( m_workers.empty() ) { + // reading the data is mostly waiting for the disk, a few threads are enough + auto const count { std::clamp( std::thread::hardware_concurrency(), 2, 5 ) - 1 }; + for( unsigned int idx = 0; idx < count; ++idx ) { + m_workers.emplace_back( [ this ]() { work(); } ); + } + } + m_queue.emplace_back( std::move( Request ) ); + } + m_queued.notify_one(); } + + // waits until data of the specified request is read. a request no worker has taken yet is carried out right away + void wait( std::shared_ptr const &Request ) { + std::unique_lock lock( m_mutex ); + if( Request->done ) { + return; + } + auto const lookup { std::find( std::begin( m_queue ), std::end( m_queue ), Request ) }; + if( lookup != std::end( m_queue ) ) { + m_queue.erase( lookup ); + lock.unlock(); + read( *Request ); + lock.lock(); + Request->done = true; + return; + } + m_read.wait( lock, [ &Request ]() { return Request->done; } ); } + + ~texture_loader() { + { + std::lock_guard lock( m_mutex ); + m_stop = true; + } + m_queued.notify_all(); + for( auto &worker : m_workers ) { + worker.join(); + } } + +private: + void work() { + std::unique_lock lock( m_mutex ); + while( true ) { + m_queued.wait( lock, [ this ]() { return m_stop || ( false == m_queue.empty() ); } ); + if( m_stop ) { + return; + } + auto request { m_queue.front() }; + m_queue.pop_front(); + lock.unlock(); + read( *request ); + lock.lock(); + request->done = true; + m_read.notify_all(); + } } + + // same as the reading part of opengl_texture::load_DDS() + static void read( texture_request &Request ) { + std::ifstream file( Request.file, std::ios::binary ); + if( false == file.is_open() ) { + Request.failed = true; + return; + } + file.seekg( Request.offset ); + Request.data.resize( Request.size ); + file.read( reinterpret_cast( Request.data.data() ), Request.size ); + + if( Request.flip ) { + char *mipmap = (char *)&Request.data[ 0 ]; + int mapcount = Request.mapcount, + width = Request.width, + height = Request.height; + while( mapcount ) { + if( Request.fourcc == FOURCC_DXT1 ) + flip_s3tc::flip_dxt1_image( mipmap, width, height ); + else if( Request.fourcc == FOURCC_DXT3 ) + flip_s3tc::flip_dxt23_image( mipmap, width, height ); + else if( Request.fourcc == FOURCC_DXT5 ) + flip_s3tc::flip_dxt45_image( mipmap, width, height ); + + mipmap += ( width + 3 ) / 4 * ( ( height + 3 ) / 4 ) * Request.blocksize; + width = std::max( width / 2, 4 ); + height = std::max( height / 2, 4 ); + --mapcount; + } + } } + + std::mutex m_mutex; + std::condition_variable m_queued; // wakes workers when requests are added + std::condition_variable m_read; // wakes the main thread waiting for a request + std::deque> m_queue; + std::vector m_workers; + bool m_stop { false }; +}; + +} // namespace + std::array opengl_texture::units = { 0 }; GLint opengl_texture::m_activeunit = -1; @@ -261,7 +389,9 @@ opengl_texture::load() { } // data state will be set by called loader, so we're all done here - if( data_state == resource_state::good ) { + // (a streamed texture has its description ready while the data is still being read) + if( ( data_state == resource_state::good ) + || ( ( data_state == resource_state::loading ) && ( request != nullptr ) ) ) { // verify texture size if( clamp_power_of_two(data_width) != data_width || clamp_power_of_two(data_height) != data_height ) { @@ -399,7 +529,7 @@ void opengl_texture::update_from_memory(size_t width, size_t height, const uint8 glDeleteTextures(1, &id); id = invalid_id; } - if (id == invalid_id) + if (id == invalid_id) // NOSONAR { data_width = width; data_height = height; @@ -579,6 +709,28 @@ opengl_texture::load_DDS() { data_state = resource_state::failed; return; } + if( true == Global.gfx_texture_streaming ) { + // the data is read by a worker thread, everything else about the texture is known already + request = std::make_shared(); + request->file = name + type; + request->offset = static_cast( file.tellg() ) + offset; + request->size = datasize; + request->flip = Global.dds_upper_origin; + request->fourcc = ddsd.ddpfPixelFormat.dwFourCC; + request->width = data_width; + request->height = data_height; + request->mapcount = data_mapcount; + request->blocksize = blockSize; + texture_loader::instance().submit( request ); + + data_components = + ddsd.ddpfPixelFormat.dwFourCC == FOURCC_DXT1 ? GL_RGB : GL_RGBA; + + data_state = resource_state::loading; + + return; + } + // reserve space and load texture data data.resize( datasize ); @@ -930,6 +1082,17 @@ opengl_texture::unbind(size_t unit) bool opengl_texture::create( bool const Static ) { + if( ( data_state == resource_state::none ) + && ( true == reload_on_use ) ) { + // the data was dropped along with the gl texture when the texture was far away + reload_on_use = false; + load(); + } + if( ( data_state == resource_state::loading ) + && ( request != nullptr ) ) { + complete_data(); + } + if( data_state != resource_state::good && !is_rendertarget ) { // don't bother until we have useful texture data // and it isn't rendertarget texture without loaded data @@ -1087,6 +1250,27 @@ opengl_texture::create( bool const Static ) { return true; } +// takes over the data read by a worker thread, waiting for it if needed +void +opengl_texture::complete_data() { + + if( request == nullptr ) { return; } + + texture_loader::instance().wait( request ); + auto const completed { std::move( request ) }; + + if( true == completed->failed ) { + data_state = resource_state::failed; + ErrorLog( "Bad texture: failed to load texture \"" + name + "\"" ); + // NOTE: temporary workaround for texture assignment errors, same as in load() + id = 0; + return; + } + data.swap( completed->data ); + data_state = resource_state::good; + size = data.size() / 1024; +} + // releases resources allocated on the opengl end, storing local copy if requested void opengl_texture::release() { @@ -1397,6 +1581,84 @@ texture_manager::delete_textures() { } } +// starts a scan for gl textures used only by vehicles far away +void +texture_manager::begin_release_scan() { + + m_releasescantime = std::chrono::steady_clock::now(); + // textures bound from now on are stamped with the time of this scan, the sweep may not be running + m_garbagecollector.refresh(); + if( m_neededtimes.size() < m_textures.size() ) { + m_neededtimes.resize( m_textures.size() ); + } +} + +// marks texture of a vehicle as needed if the vehicle is near, otherwise only as one used by vehicles +void +texture_manager::mark_as_needed( texture_handle const Texture, bool const Near ) { + + if( ( Texture <= 0 ) + || ( Texture >= static_cast( m_neededtimes.size() ) ) ) { + return; + } + auto &needed { m_neededtimes[ Texture ] }; + if( true == Near ) { + needed = m_releasescantime; + auto &texture { *m_textures[ Texture ].first }; + if( ( true == texture.reload_on_use ) + && ( texture.data_state == resource_state::none ) ) { + // the vehicle came back, the texture is read again before it's drawn (in the background with gfx.textures.streaming) + texture.reload_on_use = false; + texture.load(); + } + } + else if( needed == resource_timestamp() ) { + // from now on the texture can be released, if no vehicle near enough needs it for a while + needed = m_releasescantime; + } +} + +// releases gl textures of vehicles not needed nor drawn for a while +void +texture_manager::release_unneeded() { + + // the delay keeps textures of a vehicle moving back and forth around the release distance from going in and out + auto const delay { std::chrono::seconds{ 10 } }; + + auto releasecount { 0 }; + for( std::size_t index = 1; index < m_neededtimes.size(); ++index ) { + auto const needed { m_neededtimes[ index ] }; + if( ( needed == resource_timestamp() ) + || ( m_releasescantime - needed < delay ) ) { + continue; + } + auto &record { m_textures[ index ] }; + auto &texture { *record.first }; + if( ( texture.id == static_cast( -1 ) ) + || ( texture.id == 0 ) + || ( true == texture.is_static ) + || ( true == texture.is_rendertarget ) + || ( texture.type == "make:" ) + || ( texture.type == "internalsrc:" ) ) { + continue; + } + if( m_releasescantime - record.second < delay ) { + // drawn lately, the texture is also used by something else than far vehicles + continue; + } + texture.release(); + // without the resource sweep the data isn't kept after upload, it'll have to be read again + texture.reload_on_use = ( texture.data_state == resource_state::none ); + ++releasecount; + } + if( releasecount > 0 ) { + // released gl names can be given to new textures, the unit cache can't trust them + for( auto &unit : opengl_texture::units ) { + unit = -1; + } + } +} + // performs a resource sweep void texture_manager::update() { diff --git a/model/Texture.h b/model/Texture.h index 7b285ee3..0a4ae62e 100644 --- a/model/Texture.h +++ b/model/Texture.h @@ -17,6 +17,8 @@ http://mozilla.org/MPL/2.0/. #include "gl/ubo.h" #include "interfaces/ITexture.h" +struct texture_request; // texture data read by a worker thread, with gfx.textures.streaming + struct opengl_texture : public ITexture { static DDSURFACEDESC2 deserialize_ddsd(std::istream&); static DDCOLORKEY deserialize_ddck(std::istream&); @@ -113,7 +115,12 @@ public: void flip_vertical(); void gles_match_internalformat(GLuint format); + // takes over the data read by a worker thread, waiting for it if needed + void complete_data(); + // members + std::shared_ptr request; // data being read by a worker thread + bool reload_on_use = false; // the data was dropped along with the gl texture of a far texture, it's read again when the texture is needed bool is_static = false; // is excluded from garbage collection bool is_rendertarget = false; // is used as postfx rendertarget, without loaded data int samples = 1; @@ -161,6 +168,16 @@ public: // performs a resource sweep void update(); + // release of gl textures used only far away (gfx.textures.releasedistance). a scan is made of + // begin_release_scan(), mark_as_needed() for textures of each vehicle and release_unneeded() + void + begin_release_scan(); + // marks texture of a vehicle as needed if the vehicle is near, otherwise only as one used by vehicles + void + mark_as_needed( texture_handle const Texture, bool const Near ); + // releases gl textures of vehicles not needed nor drawn for a while + void + release_unneeded(); // debug performance string std::string info() const; @@ -193,6 +210,8 @@ private: texture_handle const npos { 0 }; // should be -1, but the rest of the code uses -1 for something else texturetimepointpair_sequence m_textures; index_map m_texturemappings; + std::vector m_neededtimes; // last time a vehicle near enough needed the texture, blank for textures not used by vehicles + resource_timestamp m_releasescantime; // time of the current release scan garbage_collector m_garbagecollector { m_textures, 600, 60, "texture" }; }; diff --git a/model/material.cpp b/model/material.cpp index 19fa0b37..dd7598ab 100644 --- a/model/material.cpp +++ b/model/material.cpp @@ -640,6 +640,28 @@ material_manager::create( std::string const &Filename, bool const Loadnow ) { return materialhandle; }; +material_handle +material_manager::create_from_text( std::string const &Name, std::string const &Definition ) { + + opengl_material material; + cParser materialparser( Definition, cParser::buffer_TEXT ); + if( false == material.deserialize( materialparser, true ) ) { + return null_handle; + } + material.name = Name; + try { + material.finalize( true ); + } + catch( gl::shader_exception const &e ) { + ErrorLog( "invalid shader: " + std::string( e.what() ) ); + return null_handle; + } + auto const materialhandle { static_cast( m_materials.size() ) }; + m_materials.emplace_back( std::move( material ) ); + // not listed under its name: the name isn't a file which could be asked for + return materialhandle; +} + void material_manager::on_weather_change() { diff --git a/model/material.h b/model/material.h index 7e04514c..f329a325 100644 --- a/model/material.h +++ b/model/material.h @@ -139,6 +139,9 @@ public: material_handle create( std::string const &Filename, bool const Loadnow ); + // material described by provided text (the content of a .mat file), not kept in a file. returns: handle to the material or null_handle + material_handle + create_from_text( std::string const &Name, std::string const &Definition ); opengl_material const & material( material_handle const Material ) const { return m_materials[ Material ]; } opengl_material & diff --git a/nodebank_previews_standalone.cpp b/nodebank_previews_standalone.cpp new file mode 100644 index 00000000..39884fd3 --- /dev/null +++ b/nodebank_previews_standalone.cpp @@ -0,0 +1,390 @@ +#include "stdafx.h" + +#include "model/AnimModel.h" +#include "rendering/renderer.h" +#include "scene/scenenode.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" +#include "editor/editorPreviews.hpp" +#include +#include + +// standalone generation of preview images of the models of the node bank of the scenery editor: +// -generate-nodebank-previews [-entries "node ... endmodel" ...] [-resolution ] [-margin ] [-no-shadows] [-software] +// each image is a square png on white background, saved in textures/previews/nodebank under the path of the model + +namespace +{ + +struct preview_settings +{ + std::vector entries; // node definitions to draw. the whole node bank if empty + int resolution{1024}; + int margin{64}; + bool shadows{true}; + bool software{false}; +}; + +// prints a line on the console the generator was started from, and puts it in the log +void report(std::string const &Text, bool const Error = false) +{ + std::fprintf((Error ? stderr : stdout), "%s\n", Text.c_str()); + std::fflush(Error ? stderr : stdout); + if (Error) + { + // goes to the log as well + ErrorLog(Text); + } + else + { + WriteLog(Text); + } +} + +bool parse_number(char const *Text, int &Value) +{ + try + { + std::size_t used{0}; + auto const value{std::stoi(Text, &used)}; + if (Text[used] != '\0') + { + return false; + } + Value = value; + return true; + } + catch (std::exception const &) + { + return false; + } +} + +bool parse_arguments(int const Argc, char *Argv[], preview_settings &Settings) +{ + for (int i = 2; i < Argc; ++i) + { + std::string const token{Argv[i]}; + if (token == "-entries") + { + while ((i + 1 < Argc) && (Argv[i + 1][0] != '-')) + { + Settings.entries.emplace_back(Argv[++i]); + } + } + else if (token == "-resolution") + { + if ((i + 1 >= Argc) || (false == parse_number(Argv[++i], Settings.resolution)) || (Settings.resolution < 16)) + { + report("-resolution needs a number of pixels, 16 or more", true); + return false; + } + } + else if (token == "-margin") + { + if ((i + 1 >= Argc) || (false == parse_number(Argv[++i], Settings.margin)) || (Settings.margin < 0)) + { + report("-margin needs a number of pixels, 0 or more", true); + return false; + } + } + else if (token == "-no-shadows") + { + Settings.shadows = false; + } + else if (token == "-software") + { + Settings.software = true; + } + else + { + report("unknown option " + token, true); + return false; + } + } + if (Settings.margin * 2 >= Settings.resolution) + { + report("the margin leaves no room for the model at this resolution", true); + return false; + } + return true; +} + +// node definitions of the node bank, read the way the node bank panel of the editor reads them +std::vector nodebank_entries() +{ + std::vector entries; + std::ifstream file("nodebank.txt", std::ios_base::in | std::ios_base::binary); + std::string line; + while (std::getline(file, line)) + { + if (line.size() < 4) + { + continue; + } + auto const nodestart{line.find("node")}; + if (nodestart == std::string::npos) + { + // group header + continue; + } + auto entry{line.substr(nodestart)}; + while ((false == entry.empty()) && ((entry.back() == '\r') || (entry.back() == '\n'))) + { + entry.pop_back(); + } + entries.emplace_back(entry); + } + return entries; +} + +// creates the hidden window with the gl context for the renderer. on a computer without a graphics driver able to provide +// gl 3.3 the software rasterizer of mesa can take over: on linux mesa picks it by itself, on windows its opengl32.dll +// (with the libraries it needs) is looked for in the mesa folder +GLFWwindow *create_context(bool const Software) +{ + if (Software) + { +#ifdef _WIN32 + auto const folder{std::filesystem::absolute("mesa")}; + ::SetDllDirectoryW(folder.wstring().c_str()); +#else + ::setenv("LIBGL_ALWAYS_SOFTWARE", "1", 1); +#endif + } + if (glfwInit() == GLFW_FALSE) + { + return nullptr; + } + glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_API); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE); + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); + if (Global.gfx_gldebug) + glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE); + auto *window{glfwCreateWindow(64, 64, "eu07 previews", nullptr, nullptr)}; + if (window == nullptr) + { + glfwTerminate(); + return nullptr; + } + glfwMakeContextCurrent(window); + if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) + { + glfwDestroyWindow(window); + glfwTerminate(); + return nullptr; + } + return window; +} + +bool save_png(std::string const &Path, std::vector const &Image, int const Size) +{ + std::error_code error; + std::filesystem::create_directories(std::filesystem::path(Path).parent_path(), error); + png_image png; + std::memset(&png, 0, sizeof(png_image)); + png.version = PNG_IMAGE_VERSION; + png.width = Size; + png.height = Size; + png.format = PNG_FORMAT_RGB; + return (png_image_write_to_file(&png, Path.c_str(), 0, Image.data(), Size * 3, nullptr) != 0); +} + +// draws the preview of a single node bank entry. returns: true on success +bool generate_preview(std::string const &Definition, preview_settings const &Settings, std::set &Saved, std::string &Outcome) +{ + // a line of the node bank can be given as it is, with the label in front of the definition + auto const nodestart{Definition.find("node")}; + if (nodestart == std::string::npos) + { + Outcome = "not a node definition"; + return false; + } + auto const entry{Definition.substr(nodestart)}; + cParser parser(entry, cParser::buffer_TEXT); + parser.getTokens(); // node + scene::node_data nodedata; + parser.getTokens(4); + parser >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type; + if (nodedata.name == "none") + { + nodedata.name.clear(); + } + if (nodedata.type != "model") + { + Outcome = "skipped, node of type " + nodedata.type + " isn't a model"; + return false; + } + // placement in the scenery is of no use here, the rotation however is a part of the look of the node + glm::dvec3 location; + glm::vec3 rotation{0.f}; + parser.getTokens(4); + parser >> location.x >> location.y >> location.z >> rotation.y; + // NOTE: instances are kept, as the rest of the simulator assumes scenery nodes live until the exit + auto *instance = new TAnimModel(nodedata); + instance->Angles(rotation); + instance->Load(&parser, false); + if (instance->pModel == nullptr) + { + Outcome = "model not found"; + return false; + } + instance->location(glm::dvec3{0.0}); + + // the model and skin, which give the name of the image, are the first two tokens after the rotation + cParser names(entry, cParser::buffer_TEXT); + names.getTokens(9, false); + auto const model{names.getToken()}; + auto const skin{names.getToken(false)}; + // another entry with the same model and skin, but other lights or angles, gets a number; the node bank of the editor + // counts the entries the same way to find the image + auto path{editor_previews::variant(editor_previews::preview_path(model, skin), Saved)}; + path += ".png"; + + std::vector image; + if (false == GfxRenderer->Render_Preview(instance, Settings.resolution, Settings.margin, Settings.shadows, image)) + { + Outcome = "nothing to draw"; + return false; + } + if (false == save_png(path, image, Settings.resolution)) + { + Outcome = "failed to save " + path; + return false; + } + Saved.emplace(path.substr(0, path.size() - 4)); + Outcome = path; + return true; +} + +} // namespace + +int generate_nodebank_previews(int Argc, char *Argv[]) +{ +#ifdef _WIN32 + // the simulator is a windows application, so the console it was started from has to be joined to report progress + if (::AttachConsole(ATTACH_PARENT_PROCESS)) + { + FILE *stream{nullptr}; + freopen_s(&stream, "CONOUT$", "w", stdout); + freopen_s(&stream, "CONOUT$", "w", stderr); + } +#endif + // settings of the simulator, for the paths and the graphics options which aren't set for the previews below + auto const inipath{user_config_path("eu07.ini")}; + if (false == inipath.empty() && std::filesystem::exists(inipath)) + { + Global.LoadIniFile(inipath.string()); + } + else + { + Global.LoadIniFile("eu07.ini"); + } + // the progress goes to the console, the log keeps the details + Global.iWriteLogEnabled &= ~2; + std::thread logservice(LogService); + + auto const finish = [&](int const Result) { + // give the log service time to write out what's left + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + Global.applicationQuitOrder = true; + logservice.join(); + return Result; + }; + + preview_settings settings; + if (false == parse_arguments(Argc, Argv, settings)) + { + report("usage: " + std::string(Argv[0]) + " -generate-nodebank-previews [-entries \"node ... endmodel\" ...] [-resolution ] [-margin ] [-no-shadows] [-software]", true); + return finish(1); + } + + // the full renderer, drawing offscreen + Global.GfxRenderer = "default"; + Global.LegacyRenderer = false; + Global.NvRenderer = false; + Global.BasicRenderer = false; + Global.gfx_usegles = false; + Global.gfx_skippipeline = false; + Global.gfx_postfx_motionblur_enabled = false; + Global.gfx_postfx_chromaticaberration_enabled = false; + Global.gfx_postfx_ssao_enabled = false; + Global.gfx_envmap_enabled = false; + Global.gfx_shadowmap_enabled = settings.shadows; + Global.shadowtune.map_size = std::max(Global.shadowtune.map_size, 2048u); + Global.iMultisampling = 0; + Global.vr = false; + Global.extra_viewports.clear(); + Global.VSync = false; + Global.fb_size = {64, 64}; + Global.window_size = {64, 64}; + Global.gfx_framebuffer_width = 64; + Global.gfx_framebuffer_height = 64; + Global.gfx_texture_streaming = false; + Global.gfx_texture_releasedistance = 0.f; + // summer daylight, which picks the seasonal variants of models and materials and keeps the lamps off + Global.Season = "summer:"; + Global.Overcast = 0.f; + Global.fLuminance = 1.0; + + auto *window{create_context(settings.software)}; + if ((window == nullptr) && (false == settings.software)) + { + report("no gl 3.3 context from the graphics driver, trying the software renderer"); + settings.software = true; + window = create_context(true); + } + if (window == nullptr) + { +#ifdef _WIN32 + report("failed to create gl 3.3 context. for computers without a capable graphics driver put opengl32.dll of mesa (with the libraries it comes with) in the mesa folder", true); +#else + report("failed to create gl 3.3 context. without a display run it through xvfb-run", true); +#endif + return finish(1); + } + GfxRenderer = gfx_renderer_factory::get_instance()->create("modern"); + if ((!GfxRenderer) || (false == GfxRenderer->Init(window))) + { + report("failed to initialize the renderer", true); + return finish(1); + } + report(std::string("renderer: ") + reinterpret_cast(glGetString(GL_RENDERER))); + + auto const entries{settings.entries.empty() ? nodebank_entries() : settings.entries}; + if (entries.empty()) + { + report("no entries to draw, nodebank.txt is missing or empty", true); + return finish(1); + } + + std::set saved; + auto failures{0}; + for (std::size_t idx = 0; idx < entries.size(); ++idx) + { + std::string outcome; + auto success{false}; + try + { + success = generate_preview(entries[idx], settings, saved, outcome); + } + catch (std::exception const &Error) + { + // a broken entry shouldn't stop the others + outcome = Error.what(); + } + if (false == success) + { + ++failures; + } + report("[" + std::to_string(idx + 1) + "/" + std::to_string(entries.size()) + "] " + entries[idx] + (success ? " -> " : ": ") + outcome, (false == success)); + } + report(std::to_string(saved.size()) + " previews saved in " + editor_previews::folder + (failures > 0 ? ", " + std::to_string(failures) + " entries failed" : "")); + + return finish(failures > 0 ? 1 : 0); +} diff --git a/ref/zstd/LICENSE b/ref/zstd/LICENSE new file mode 100644 index 00000000..75800288 --- /dev/null +++ b/ref/zstd/LICENSE @@ -0,0 +1,30 @@ +BSD License + +For Zstandard software + +Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook, nor Meta, nor the names of its contributors may + be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ref/zstd/README.txt b/ref/zstd/README.txt new file mode 100644 index 00000000..8426cb4e --- /dev/null +++ b/ref/zstd/README.txt @@ -0,0 +1,9 @@ +Zstandard 1.5.6 (https://github.com/facebook/zstd), BSD licence (see LICENSE). + +zstd.c is the single file amalgamation made with build/single_file_libs/combine.py from +zstd-in.c, without the dictionary builder and without multithreading: + + sed -e "s/^#ifndef __EMSCRIPTEN__$/#if 0/" -e "/dictBuilder/d" zstd-in.c > zstd-maszyna-in.c + python3 combine.py -r ../../lib -x legacy/zstd_legacy.h -o zstd.c zstd-maszyna-in.c + +Used by the heightmap terrain files (.tch). diff --git a/ref/zstd/zstd.c b/ref/zstd/zstd.c new file mode 100644 index 00000000..664ea2b2 --- /dev/null +++ b/ref/zstd/zstd.c @@ -0,0 +1,45229 @@ +/** + * \file zstd.c + * Single-file Zstandard library. + * + * Generate using: + * \code + * python combine.py -r ../../lib -x legacy/zstd_legacy.h -o zstd.c zstd-in.c + * \endcode + */ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ +/* + * Settings to bake for the single library file. + * + * Note: It's important that none of these affects 'zstd.h' (only the + * implementation files we're amalgamating). + * + * Note: MEM_MODULE stops xxhash redefining BYTE, U16, etc., which are also + * defined in mem.h (breaking C99 compatibility). + * + * Note: the undefs for xxHash allow Zstd's implementation to coincide with + * standalone xxHash usage (with global defines). + * + * Note: if you enable ZSTD_LEGACY_SUPPORT the combine.py script will need + * re-running without the "-x legacy/zstd_legacy.h" option (it excludes the + * legacy support at the source level). + * + * Note: multithreading is enabled for all platforms apart from Emscripten. + */ +#define DEBUGLEVEL 0 +#define MEM_MODULE +#undef XXH_NAMESPACE +#define XXH_NAMESPACE ZSTD_ +#undef XXH_PRIVATE_API +#define XXH_PRIVATE_API +#undef XXH_INLINE_ALL +#define XXH_INLINE_ALL +#define ZSTD_LEGACY_SUPPORT 0 +#if 0 /* single threaded build for MaSzyna */ +#define ZSTD_MULTITHREAD +#endif +#define ZSTD_TRACE 0 +/* TODO: Can't amalgamate ASM function */ +#define ZSTD_DISABLE_ASM 1 + +/* Include zstd_deps.h first with all the options we need enabled. */ +#define ZSTD_DEPS_NEED_MALLOC +#define ZSTD_DEPS_NEED_MATH64 +/**** start inlining common/zstd_deps.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/* This file provides common libc dependencies that zstd requires. + * The purpose is to allow replacing this file with a custom implementation + * to compile zstd without libc support. + */ + +/* Need: + * NULL + * INT_MAX + * UINT_MAX + * ZSTD_memcpy() + * ZSTD_memset() + * ZSTD_memmove() + */ +#ifndef ZSTD_DEPS_COMMON +#define ZSTD_DEPS_COMMON + +#include +#include +#include + +#if defined(__GNUC__) && __GNUC__ >= 4 +# define ZSTD_memcpy(d,s,l) __builtin_memcpy((d),(s),(l)) +# define ZSTD_memmove(d,s,l) __builtin_memmove((d),(s),(l)) +# define ZSTD_memset(p,v,l) __builtin_memset((p),(v),(l)) +#else +# define ZSTD_memcpy(d,s,l) memcpy((d),(s),(l)) +# define ZSTD_memmove(d,s,l) memmove((d),(s),(l)) +# define ZSTD_memset(p,v,l) memset((p),(v),(l)) +#endif + +#endif /* ZSTD_DEPS_COMMON */ + +/* Need: + * ZSTD_malloc() + * ZSTD_free() + * ZSTD_calloc() + */ +#ifdef ZSTD_DEPS_NEED_MALLOC +#ifndef ZSTD_DEPS_MALLOC +#define ZSTD_DEPS_MALLOC + +#include + +#define ZSTD_malloc(s) malloc(s) +#define ZSTD_calloc(n,s) calloc((n), (s)) +#define ZSTD_free(p) free((p)) + +#endif /* ZSTD_DEPS_MALLOC */ +#endif /* ZSTD_DEPS_NEED_MALLOC */ + +/* + * Provides 64-bit math support. + * Need: + * U64 ZSTD_div64(U64 dividend, U32 divisor) + */ +#ifdef ZSTD_DEPS_NEED_MATH64 +#ifndef ZSTD_DEPS_MATH64 +#define ZSTD_DEPS_MATH64 + +#define ZSTD_div64(dividend, divisor) ((dividend) / (divisor)) + +#endif /* ZSTD_DEPS_MATH64 */ +#endif /* ZSTD_DEPS_NEED_MATH64 */ + +/* Need: + * assert() + */ +#ifdef ZSTD_DEPS_NEED_ASSERT +#ifndef ZSTD_DEPS_ASSERT +#define ZSTD_DEPS_ASSERT + +#include + +#endif /* ZSTD_DEPS_ASSERT */ +#endif /* ZSTD_DEPS_NEED_ASSERT */ + +/* Need: + * ZSTD_DEBUG_PRINT() + */ +#ifdef ZSTD_DEPS_NEED_IO +#ifndef ZSTD_DEPS_IO +#define ZSTD_DEPS_IO + +#include +#define ZSTD_DEBUG_PRINT(...) fprintf(stderr, __VA_ARGS__) + +#endif /* ZSTD_DEPS_IO */ +#endif /* ZSTD_DEPS_NEED_IO */ + +/* Only requested when is known to be present. + * Need: + * intptr_t + */ +#ifdef ZSTD_DEPS_NEED_STDINT +#ifndef ZSTD_DEPS_STDINT +#define ZSTD_DEPS_STDINT + +#include + +#endif /* ZSTD_DEPS_STDINT */ +#endif /* ZSTD_DEPS_NEED_STDINT */ +/**** ended inlining common/zstd_deps.h ****/ + +/**** start inlining common/debug.c ****/ +/* ****************************************************************** + * debug + * Part of FSE library + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - Source repository : https://github.com/Cyan4973/FiniteStateEntropy + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + + +/* + * This module only hosts one global variable + * which can be used to dynamically influence the verbosity of traces, + * such as DEBUGLOG and RAWLOG + */ + +/**** start inlining debug.h ****/ +/* ****************************************************************** + * debug + * Part of FSE library + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - Source repository : https://github.com/Cyan4973/FiniteStateEntropy + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + + +/* + * The purpose of this header is to enable debug functions. + * They regroup assert(), DEBUGLOG() and RAWLOG() for run-time, + * and DEBUG_STATIC_ASSERT() for compile-time. + * + * By default, DEBUGLEVEL==0, which means run-time debug is disabled. + * + * Level 1 enables assert() only. + * Starting level 2, traces can be generated and pushed to stderr. + * The higher the level, the more verbose the traces. + * + * It's possible to dynamically adjust level using variable g_debug_level, + * which is only declared if DEBUGLEVEL>=2, + * and is a global variable, not multi-thread protected (use with care) + */ + +#ifndef DEBUG_H_12987983217 +#define DEBUG_H_12987983217 + +#if defined (__cplusplus) +extern "C" { +#endif + + +/* static assert is triggered at compile time, leaving no runtime artefact. + * static assert only works with compile-time constants. + * Also, this variant can only be used inside a function. */ +#define DEBUG_STATIC_ASSERT(c) (void)sizeof(char[(c) ? 1 : -1]) + + +/* DEBUGLEVEL is expected to be defined externally, + * typically through compiler command line. + * Value must be a number. */ +#ifndef DEBUGLEVEL +# define DEBUGLEVEL 0 +#endif + + +/* recommended values for DEBUGLEVEL : + * 0 : release mode, no debug, all run-time checks disabled + * 1 : enables assert() only, no display + * 2 : reserved, for currently active debug path + * 3 : events once per object lifetime (CCtx, CDict, etc.) + * 4 : events once per frame + * 5 : events once per block + * 6 : events once per sequence (verbose) + * 7+: events at every position (*very* verbose) + * + * It's generally inconvenient to output traces > 5. + * In which case, it's possible to selectively trigger high verbosity levels + * by modifying g_debug_level. + */ + +#if (DEBUGLEVEL>=1) +# define ZSTD_DEPS_NEED_ASSERT +/**** skipping file: zstd_deps.h ****/ +#else +# ifndef assert /* assert may be already defined, due to prior #include */ +# define assert(condition) ((void)0) /* disable assert (default) */ +# endif +#endif + +#if (DEBUGLEVEL>=2) +# define ZSTD_DEPS_NEED_IO +/**** skipping file: zstd_deps.h ****/ +extern int g_debuglevel; /* the variable is only declared, + it actually lives in debug.c, + and is shared by the whole process. + It's not thread-safe. + It's useful when enabling very verbose levels + on selective conditions (such as position in src) */ + +# define RAWLOG(l, ...) \ + do { \ + if (l<=g_debuglevel) { \ + ZSTD_DEBUG_PRINT(__VA_ARGS__); \ + } \ + } while (0) + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) +#define LINE_AS_STRING TOSTRING(__LINE__) + +# define DEBUGLOG(l, ...) \ + do { \ + if (l<=g_debuglevel) { \ + ZSTD_DEBUG_PRINT(__FILE__ ":" LINE_AS_STRING ": " __VA_ARGS__); \ + ZSTD_DEBUG_PRINT(" \n"); \ + } \ + } while (0) +#else +# define RAWLOG(l, ...) do { } while (0) /* disabled */ +# define DEBUGLOG(l, ...) do { } while (0) /* disabled */ +#endif + + +#if defined (__cplusplus) +} +#endif + +#endif /* DEBUG_H_12987983217 */ +/**** ended inlining debug.h ****/ + +#if !defined(ZSTD_LINUX_KERNEL) || (DEBUGLEVEL>=2) +/* We only use this when DEBUGLEVEL>=2, but we get -Werror=pedantic errors if a + * translation unit is empty. So remove this from Linux kernel builds, but + * otherwise just leave it in. + */ +int g_debuglevel = DEBUGLEVEL; +#endif +/**** ended inlining common/debug.c ****/ +/**** start inlining common/entropy_common.c ****/ +/* ****************************************************************** + * Common functions of New Generation Entropy library + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - FSE+HUF source repository : https://github.com/Cyan4973/FiniteStateEntropy + * - Public forum : https://groups.google.com/forum/#!forum/lz4c + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + +/* ************************************* +* Dependencies +***************************************/ +/**** start inlining mem.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef MEM_H_MODULE +#define MEM_H_MODULE + +#if defined (__cplusplus) +extern "C" { +#endif + +/*-**************************************** +* Dependencies +******************************************/ +#include /* size_t, ptrdiff_t */ +/**** start inlining compiler.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_COMPILER_H +#define ZSTD_COMPILER_H + +#include + +/**** start inlining portability_macros.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_PORTABILITY_MACROS_H +#define ZSTD_PORTABILITY_MACROS_H + +/** + * This header file contains macro definitions to support portability. + * This header is shared between C and ASM code, so it MUST only + * contain macro definitions. It MUST not contain any C code. + * + * This header ONLY defines macros to detect platforms/feature support. + * + */ + + +/* compat. with non-clang compilers */ +#ifndef __has_attribute + #define __has_attribute(x) 0 +#endif + +/* compat. with non-clang compilers */ +#ifndef __has_builtin +# define __has_builtin(x) 0 +#endif + +/* compat. with non-clang compilers */ +#ifndef __has_feature +# define __has_feature(x) 0 +#endif + +/* detects whether we are being compiled under msan */ +#ifndef ZSTD_MEMORY_SANITIZER +# if __has_feature(memory_sanitizer) +# define ZSTD_MEMORY_SANITIZER 1 +# else +# define ZSTD_MEMORY_SANITIZER 0 +# endif +#endif + +/* detects whether we are being compiled under asan */ +#ifndef ZSTD_ADDRESS_SANITIZER +# if __has_feature(address_sanitizer) +# define ZSTD_ADDRESS_SANITIZER 1 +# elif defined(__SANITIZE_ADDRESS__) +# define ZSTD_ADDRESS_SANITIZER 1 +# else +# define ZSTD_ADDRESS_SANITIZER 0 +# endif +#endif + +/* detects whether we are being compiled under dfsan */ +#ifndef ZSTD_DATAFLOW_SANITIZER +# if __has_feature(dataflow_sanitizer) +# define ZSTD_DATAFLOW_SANITIZER 1 +# else +# define ZSTD_DATAFLOW_SANITIZER 0 +# endif +#endif + +/* Mark the internal assembly functions as hidden */ +#ifdef __ELF__ +# define ZSTD_HIDE_ASM_FUNCTION(func) .hidden func +#elif defined(__APPLE__) +# define ZSTD_HIDE_ASM_FUNCTION(func) .private_extern func +#else +# define ZSTD_HIDE_ASM_FUNCTION(func) +#endif + +/* Enable runtime BMI2 dispatch based on the CPU. + * Enabled for clang & gcc >=4.8 on x86 when BMI2 isn't enabled by default. + */ +#ifndef DYNAMIC_BMI2 + #if ((defined(__clang__) && __has_attribute(__target__)) \ + || (defined(__GNUC__) \ + && (__GNUC__ >= 5 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 8)))) \ + && (defined(__x86_64__) || defined(_M_X64)) \ + && !defined(__BMI2__) + # define DYNAMIC_BMI2 1 + #else + # define DYNAMIC_BMI2 0 + #endif +#endif + +/** + * Only enable assembly for GNUC compatible compilers, + * because other platforms may not support GAS assembly syntax. + * + * Only enable assembly for Linux / MacOS, other platforms may + * work, but they haven't been tested. This could likely be + * extended to BSD systems. + * + * Disable assembly when MSAN is enabled, because MSAN requires + * 100% of code to be instrumented to work. + */ +#if defined(__GNUC__) +# if defined(__linux__) || defined(__linux) || defined(__APPLE__) +# if ZSTD_MEMORY_SANITIZER +# define ZSTD_ASM_SUPPORTED 0 +# elif ZSTD_DATAFLOW_SANITIZER +# define ZSTD_ASM_SUPPORTED 0 +# else +# define ZSTD_ASM_SUPPORTED 1 +# endif +# else +# define ZSTD_ASM_SUPPORTED 0 +# endif +#else +# define ZSTD_ASM_SUPPORTED 0 +#endif + +/** + * Determines whether we should enable assembly for x86-64 + * with BMI2. + * + * Enable if all of the following conditions hold: + * - ASM hasn't been explicitly disabled by defining ZSTD_DISABLE_ASM + * - Assembly is supported + * - We are compiling for x86-64 and either: + * - DYNAMIC_BMI2 is enabled + * - BMI2 is supported at compile time + */ +#if !defined(ZSTD_DISABLE_ASM) && \ + ZSTD_ASM_SUPPORTED && \ + defined(__x86_64__) && \ + (DYNAMIC_BMI2 || defined(__BMI2__)) +# define ZSTD_ENABLE_ASM_X86_64_BMI2 1 +#else +# define ZSTD_ENABLE_ASM_X86_64_BMI2 0 +#endif + +/* + * For x86 ELF targets, add .note.gnu.property section for Intel CET in + * assembly sources when CET is enabled. + * + * Additionally, any function that may be called indirectly must begin + * with ZSTD_CET_ENDBRANCH. + */ +#if defined(__ELF__) && (defined(__x86_64__) || defined(__i386__)) \ + && defined(__has_include) +# if __has_include() +# include +# define ZSTD_CET_ENDBRANCH _CET_ENDBR +# endif +#endif + +#ifndef ZSTD_CET_ENDBRANCH +# define ZSTD_CET_ENDBRANCH +#endif + +#endif /* ZSTD_PORTABILITY_MACROS_H */ +/**** ended inlining portability_macros.h ****/ + +/*-******************************************************* +* Compiler specifics +*********************************************************/ +/* force inlining */ + +#if !defined(ZSTD_NO_INLINE) +#if (defined(__GNUC__) && !defined(__STRICT_ANSI__)) || defined(__cplusplus) || defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* C99 */ +# define INLINE_KEYWORD inline +#else +# define INLINE_KEYWORD +#endif + +#if defined(__GNUC__) || defined(__ICCARM__) +# define FORCE_INLINE_ATTR __attribute__((always_inline)) +#elif defined(_MSC_VER) +# define FORCE_INLINE_ATTR __forceinline +#else +# define FORCE_INLINE_ATTR +#endif + +#else + +#define INLINE_KEYWORD +#define FORCE_INLINE_ATTR + +#endif + +/** + On MSVC qsort requires that functions passed into it use the __cdecl calling conversion(CC). + This explicitly marks such functions as __cdecl so that the code will still compile + if a CC other than __cdecl has been made the default. +*/ +#if defined(_MSC_VER) +# define WIN_CDECL __cdecl +#else +# define WIN_CDECL +#endif + +/* UNUSED_ATTR tells the compiler it is okay if the function is unused. */ +#if defined(__GNUC__) +# define UNUSED_ATTR __attribute__((unused)) +#else +# define UNUSED_ATTR +#endif + +/** + * FORCE_INLINE_TEMPLATE is used to define C "templates", which take constant + * parameters. They must be inlined for the compiler to eliminate the constant + * branches. + */ +#define FORCE_INLINE_TEMPLATE static INLINE_KEYWORD FORCE_INLINE_ATTR UNUSED_ATTR +/** + * HINT_INLINE is used to help the compiler generate better code. It is *not* + * used for "templates", so it can be tweaked based on the compilers + * performance. + * + * gcc-4.8 and gcc-4.9 have been shown to benefit from leaving off the + * always_inline attribute. + * + * clang up to 5.0.0 (trunk) benefit tremendously from the always_inline + * attribute. + */ +#if !defined(__clang__) && defined(__GNUC__) && __GNUC__ >= 4 && __GNUC_MINOR__ >= 8 && __GNUC__ < 5 +# define HINT_INLINE static INLINE_KEYWORD +#else +# define HINT_INLINE FORCE_INLINE_TEMPLATE +#endif + +/* "soft" inline : + * The compiler is free to select if it's a good idea to inline or not. + * The main objective is to silence compiler warnings + * when a defined function in included but not used. + * + * Note : this macro is prefixed `MEM_` because it used to be provided by `mem.h` unit. + * Updating the prefix is probably preferable, but requires a fairly large codemod, + * since this name is used everywhere. + */ +#ifndef MEM_STATIC /* already defined in Linux Kernel mem.h */ +#if defined(__GNUC__) +# define MEM_STATIC static __inline UNUSED_ATTR +#elif defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) +# define MEM_STATIC static inline +#elif defined(_MSC_VER) +# define MEM_STATIC static __inline +#else +# define MEM_STATIC static /* this version may generate warnings for unused static functions; disable the relevant warning */ +#endif +#endif + +/* force no inlining */ +#ifdef _MSC_VER +# define FORCE_NOINLINE static __declspec(noinline) +#else +# if defined(__GNUC__) || defined(__ICCARM__) +# define FORCE_NOINLINE static __attribute__((__noinline__)) +# else +# define FORCE_NOINLINE static +# endif +#endif + + +/* target attribute */ +#if defined(__GNUC__) || defined(__ICCARM__) +# define TARGET_ATTRIBUTE(target) __attribute__((__target__(target))) +#else +# define TARGET_ATTRIBUTE(target) +#endif + +/* Target attribute for BMI2 dynamic dispatch. + * Enable lzcnt, bmi, and bmi2. + * We test for bmi1 & bmi2. lzcnt is included in bmi1. + */ +#define BMI2_TARGET_ATTRIBUTE TARGET_ATTRIBUTE("lzcnt,bmi,bmi2") + +/* prefetch + * can be disabled, by declaring NO_PREFETCH build macro */ +#if defined(NO_PREFETCH) +# define PREFETCH_L1(ptr) do { (void)(ptr); } while (0) /* disabled */ +# define PREFETCH_L2(ptr) do { (void)(ptr); } while (0) /* disabled */ +#else +# if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_I86)) && !defined(_M_ARM64EC) /* _mm_prefetch() is not defined outside of x86/x64 */ +# include /* https://msdn.microsoft.com/fr-fr/library/84szxsww(v=vs.90).aspx */ +# define PREFETCH_L1(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T0) +# define PREFETCH_L2(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T1) +# elif defined(__GNUC__) && ( (__GNUC__ >= 4) || ( (__GNUC__ == 3) && (__GNUC_MINOR__ >= 1) ) ) +# define PREFETCH_L1(ptr) __builtin_prefetch((ptr), 0 /* rw==read */, 3 /* locality */) +# define PREFETCH_L2(ptr) __builtin_prefetch((ptr), 0 /* rw==read */, 2 /* locality */) +# elif defined(__aarch64__) +# define PREFETCH_L1(ptr) do { __asm__ __volatile__("prfm pldl1keep, %0" ::"Q"(*(ptr))); } while (0) +# define PREFETCH_L2(ptr) do { __asm__ __volatile__("prfm pldl2keep, %0" ::"Q"(*(ptr))); } while (0) +# else +# define PREFETCH_L1(ptr) do { (void)(ptr); } while (0) /* disabled */ +# define PREFETCH_L2(ptr) do { (void)(ptr); } while (0) /* disabled */ +# endif +#endif /* NO_PREFETCH */ + +#define CACHELINE_SIZE 64 + +#define PREFETCH_AREA(p, s) \ + do { \ + const char* const _ptr = (const char*)(p); \ + size_t const _size = (size_t)(s); \ + size_t _pos; \ + for (_pos=0; _pos<_size; _pos+=CACHELINE_SIZE) { \ + PREFETCH_L2(_ptr + _pos); \ + } \ + } while (0) + +/* vectorization + * older GCC (pre gcc-4.3 picked as the cutoff) uses a different syntax, + * and some compilers, like Intel ICC and MCST LCC, do not support it at all. */ +#if !defined(__INTEL_COMPILER) && !defined(__clang__) && defined(__GNUC__) && !defined(__LCC__) +# if (__GNUC__ == 4 && __GNUC_MINOR__ > 3) || (__GNUC__ >= 5) +# define DONT_VECTORIZE __attribute__((optimize("no-tree-vectorize"))) +# else +# define DONT_VECTORIZE _Pragma("GCC optimize(\"no-tree-vectorize\")") +# endif +#else +# define DONT_VECTORIZE +#endif + +/* Tell the compiler that a branch is likely or unlikely. + * Only use these macros if it causes the compiler to generate better code. + * If you can remove a LIKELY/UNLIKELY annotation without speed changes in gcc + * and clang, please do. + */ +#if defined(__GNUC__) +#define LIKELY(x) (__builtin_expect((x), 1)) +#define UNLIKELY(x) (__builtin_expect((x), 0)) +#else +#define LIKELY(x) (x) +#define UNLIKELY(x) (x) +#endif + +#if __has_builtin(__builtin_unreachable) || (defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5))) +# define ZSTD_UNREACHABLE do { assert(0), __builtin_unreachable(); } while (0) +#else +# define ZSTD_UNREACHABLE do { assert(0); } while (0) +#endif + +/* disable warnings */ +#ifdef _MSC_VER /* Visual Studio */ +# include /* For Visual 2005 */ +# pragma warning(disable : 4100) /* disable: C4100: unreferenced formal parameter */ +# pragma warning(disable : 4127) /* disable: C4127: conditional expression is constant */ +# pragma warning(disable : 4204) /* disable: C4204: non-constant aggregate initializer */ +# pragma warning(disable : 4214) /* disable: C4214: non-int bitfields */ +# pragma warning(disable : 4324) /* disable: C4324: padded structure */ +#endif + +/*Like DYNAMIC_BMI2 but for compile time determination of BMI2 support*/ +#ifndef STATIC_BMI2 +# if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_I86)) +# ifdef __AVX2__ //MSVC does not have a BMI2 specific flag, but every CPU that supports AVX2 also supports BMI2 +# define STATIC_BMI2 1 +# endif +# elif defined(__BMI2__) && defined(__x86_64__) && defined(__GNUC__) +# define STATIC_BMI2 1 +# endif +#endif + +#ifndef STATIC_BMI2 + #define STATIC_BMI2 0 +#endif + +/* compile time determination of SIMD support */ +#if !defined(ZSTD_NO_INTRINSICS) +# if defined(__SSE2__) || defined(_M_AMD64) || (defined (_M_IX86) && defined(_M_IX86_FP) && (_M_IX86_FP >= 2)) +# define ZSTD_ARCH_X86_SSE2 +# endif +# if defined(__ARM_NEON) || defined(_M_ARM64) +# define ZSTD_ARCH_ARM_NEON +# endif +# +# if defined(ZSTD_ARCH_X86_SSE2) +# include +# elif defined(ZSTD_ARCH_ARM_NEON) +# include +# endif +#endif + +/* C-language Attributes are added in C23. */ +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ > 201710L) && defined(__has_c_attribute) +# define ZSTD_HAS_C_ATTRIBUTE(x) __has_c_attribute(x) +#else +# define ZSTD_HAS_C_ATTRIBUTE(x) 0 +#endif + +/* Only use C++ attributes in C++. Some compilers report support for C++ + * attributes when compiling with C. + */ +#if defined(__cplusplus) && defined(__has_cpp_attribute) +# define ZSTD_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute(x) +#else +# define ZSTD_HAS_CPP_ATTRIBUTE(x) 0 +#endif + +/* Define ZSTD_FALLTHROUGH macro for annotating switch case with the 'fallthrough' attribute. + * - C23: https://en.cppreference.com/w/c/language/attributes/fallthrough + * - CPP17: https://en.cppreference.com/w/cpp/language/attributes/fallthrough + * - Else: __attribute__((__fallthrough__)) + */ +#ifndef ZSTD_FALLTHROUGH +# if ZSTD_HAS_C_ATTRIBUTE(fallthrough) +# define ZSTD_FALLTHROUGH [[fallthrough]] +# elif ZSTD_HAS_CPP_ATTRIBUTE(fallthrough) +# define ZSTD_FALLTHROUGH [[fallthrough]] +# elif __has_attribute(__fallthrough__) +/* Leading semicolon is to satisfy gcc-11 with -pedantic. Without the semicolon + * gcc complains about: a label can only be part of a statement and a declaration is not a statement. + */ +# define ZSTD_FALLTHROUGH ; __attribute__((__fallthrough__)) +# else +# define ZSTD_FALLTHROUGH +# endif +#endif + +/*-************************************************************** +* Alignment check +*****************************************************************/ + +/* this test was initially positioned in mem.h, + * but this file is removed (or replaced) for linux kernel + * so it's now hosted in compiler.h, + * which remains valid for both user & kernel spaces. + */ + +#ifndef ZSTD_ALIGNOF +# if defined(__GNUC__) || defined(_MSC_VER) +/* covers gcc, clang & MSVC */ +/* note : this section must come first, before C11, + * due to a limitation in the kernel source generator */ +# define ZSTD_ALIGNOF(T) __alignof(T) + +# elif defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) +/* C11 support */ +# include +# define ZSTD_ALIGNOF(T) alignof(T) + +# else +/* No known support for alignof() - imperfect backup */ +# define ZSTD_ALIGNOF(T) (sizeof(void*) < sizeof(T) ? sizeof(void*) : sizeof(T)) + +# endif +#endif /* ZSTD_ALIGNOF */ + +/*-************************************************************** +* Sanitizer +*****************************************************************/ + +/** + * Zstd relies on pointer overflow in its decompressor. + * We add this attribute to functions that rely on pointer overflow. + */ +#ifndef ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +# if __has_attribute(no_sanitize) +# if !defined(__clang__) && defined(__GNUC__) && __GNUC__ < 8 + /* gcc < 8 only has signed-integer-overlow which triggers on pointer overflow */ +# define ZSTD_ALLOW_POINTER_OVERFLOW_ATTR __attribute__((no_sanitize("signed-integer-overflow"))) +# else + /* older versions of clang [3.7, 5.0) will warn that pointer-overflow is ignored. */ +# define ZSTD_ALLOW_POINTER_OVERFLOW_ATTR __attribute__((no_sanitize("pointer-overflow"))) +# endif +# else +# define ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +# endif +#endif + +/** + * Helper function to perform a wrapped pointer difference without trigging + * UBSAN. + * + * @returns lhs - rhs with wrapping + */ +MEM_STATIC +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +ptrdiff_t ZSTD_wrappedPtrDiff(unsigned char const* lhs, unsigned char const* rhs) +{ + return lhs - rhs; +} + +/** + * Helper function to perform a wrapped pointer add without triggering UBSAN. + * + * @return ptr + add with wrapping + */ +MEM_STATIC +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +unsigned char const* ZSTD_wrappedPtrAdd(unsigned char const* ptr, ptrdiff_t add) +{ + return ptr + add; +} + +/** + * Helper function to perform a wrapped pointer subtraction without triggering + * UBSAN. + * + * @return ptr - sub with wrapping + */ +MEM_STATIC +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +unsigned char const* ZSTD_wrappedPtrSub(unsigned char const* ptr, ptrdiff_t sub) +{ + return ptr - sub; +} + +/** + * Helper function to add to a pointer that works around C's undefined behavior + * of adding 0 to NULL. + * + * @returns `ptr + add` except it defines `NULL + 0 == NULL`. + */ +MEM_STATIC +unsigned char* ZSTD_maybeNullPtrAdd(unsigned char* ptr, ptrdiff_t add) +{ + return add > 0 ? ptr + add : ptr; +} + +/* Issue #3240 reports an ASAN failure on an llvm-mingw build. Out of an + * abundance of caution, disable our custom poisoning on mingw. */ +#ifdef __MINGW32__ +#ifndef ZSTD_ASAN_DONT_POISON_WORKSPACE +#define ZSTD_ASAN_DONT_POISON_WORKSPACE 1 +#endif +#ifndef ZSTD_MSAN_DONT_POISON_WORKSPACE +#define ZSTD_MSAN_DONT_POISON_WORKSPACE 1 +#endif +#endif + +#if ZSTD_MEMORY_SANITIZER && !defined(ZSTD_MSAN_DONT_POISON_WORKSPACE) +/* Not all platforms that support msan provide sanitizers/msan_interface.h. + * We therefore declare the functions we need ourselves, rather than trying to + * include the header file... */ +#include /* size_t */ +#define ZSTD_DEPS_NEED_STDINT +/**** skipping file: zstd_deps.h ****/ + +/* Make memory region fully initialized (without changing its contents). */ +void __msan_unpoison(const volatile void *a, size_t size); + +/* Make memory region fully uninitialized (without changing its contents). + This is a legacy interface that does not update origin information. Use + __msan_allocated_memory() instead. */ +void __msan_poison(const volatile void *a, size_t size); + +/* Returns the offset of the first (at least partially) poisoned byte in the + memory range, or -1 if the whole range is good. */ +intptr_t __msan_test_shadow(const volatile void *x, size_t size); + +/* Print shadow and origin for the memory range to stderr in a human-readable + format. */ +void __msan_print_shadow(const volatile void *x, size_t size); +#endif + +#if ZSTD_ADDRESS_SANITIZER && !defined(ZSTD_ASAN_DONT_POISON_WORKSPACE) +/* Not all platforms that support asan provide sanitizers/asan_interface.h. + * We therefore declare the functions we need ourselves, rather than trying to + * include the header file... */ +#include /* size_t */ + +/** + * Marks a memory region ([addr, addr+size)) as unaddressable. + * + * This memory must be previously allocated by your program. Instrumented + * code is forbidden from accessing addresses in this region until it is + * unpoisoned. This function is not guaranteed to poison the entire region - + * it could poison only a subregion of [addr, addr+size) due to ASan + * alignment restrictions. + * + * \note This function is not thread-safe because no two threads can poison or + * unpoison memory in the same memory region simultaneously. + * + * \param addr Start of memory region. + * \param size Size of memory region. */ +void __asan_poison_memory_region(void const volatile *addr, size_t size); + +/** + * Marks a memory region ([addr, addr+size)) as addressable. + * + * This memory must be previously allocated by your program. Accessing + * addresses in this region is allowed until this region is poisoned again. + * This function could unpoison a super-region of [addr, addr+size) due + * to ASan alignment restrictions. + * + * \note This function is not thread-safe because no two threads can + * poison or unpoison memory in the same memory region simultaneously. + * + * \param addr Start of memory region. + * \param size Size of memory region. */ +void __asan_unpoison_memory_region(void const volatile *addr, size_t size); +#endif + +#endif /* ZSTD_COMPILER_H */ +/**** ended inlining compiler.h ****/ +/**** skipping file: debug.h ****/ +/**** skipping file: zstd_deps.h ****/ + + +/*-**************************************** +* Compiler specifics +******************************************/ +#if defined(_MSC_VER) /* Visual Studio */ +# include /* _byteswap_ulong */ +# include /* _byteswap_* */ +#endif + +/*-************************************************************** +* Basic Types +*****************************************************************/ +#if !defined (__VMS) && (defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# if defined(_AIX) +# include +# else +# include /* intptr_t */ +# endif + typedef uint8_t BYTE; + typedef uint8_t U8; + typedef int8_t S8; + typedef uint16_t U16; + typedef int16_t S16; + typedef uint32_t U32; + typedef int32_t S32; + typedef uint64_t U64; + typedef int64_t S64; +#else +# include +#if CHAR_BIT != 8 +# error "this implementation requires char to be exactly 8-bit type" +#endif + typedef unsigned char BYTE; + typedef unsigned char U8; + typedef signed char S8; +#if USHRT_MAX != 65535 +# error "this implementation requires short to be exactly 16-bit type" +#endif + typedef unsigned short U16; + typedef signed short S16; +#if UINT_MAX != 4294967295 +# error "this implementation requires int to be exactly 32-bit type" +#endif + typedef unsigned int U32; + typedef signed int S32; +/* note : there are no limits defined for long long type in C90. + * limits exist in C99, however, in such case, is preferred */ + typedef unsigned long long U64; + typedef signed long long S64; +#endif + + +/*-************************************************************** +* Memory I/O API +*****************************************************************/ +/*=== Static platform detection ===*/ +MEM_STATIC unsigned MEM_32bits(void); +MEM_STATIC unsigned MEM_64bits(void); +MEM_STATIC unsigned MEM_isLittleEndian(void); + +/*=== Native unaligned read/write ===*/ +MEM_STATIC U16 MEM_read16(const void* memPtr); +MEM_STATIC U32 MEM_read32(const void* memPtr); +MEM_STATIC U64 MEM_read64(const void* memPtr); +MEM_STATIC size_t MEM_readST(const void* memPtr); + +MEM_STATIC void MEM_write16(void* memPtr, U16 value); +MEM_STATIC void MEM_write32(void* memPtr, U32 value); +MEM_STATIC void MEM_write64(void* memPtr, U64 value); + +/*=== Little endian unaligned read/write ===*/ +MEM_STATIC U16 MEM_readLE16(const void* memPtr); +MEM_STATIC U32 MEM_readLE24(const void* memPtr); +MEM_STATIC U32 MEM_readLE32(const void* memPtr); +MEM_STATIC U64 MEM_readLE64(const void* memPtr); +MEM_STATIC size_t MEM_readLEST(const void* memPtr); + +MEM_STATIC void MEM_writeLE16(void* memPtr, U16 val); +MEM_STATIC void MEM_writeLE24(void* memPtr, U32 val); +MEM_STATIC void MEM_writeLE32(void* memPtr, U32 val32); +MEM_STATIC void MEM_writeLE64(void* memPtr, U64 val64); +MEM_STATIC void MEM_writeLEST(void* memPtr, size_t val); + +/*=== Big endian unaligned read/write ===*/ +MEM_STATIC U32 MEM_readBE32(const void* memPtr); +MEM_STATIC U64 MEM_readBE64(const void* memPtr); +MEM_STATIC size_t MEM_readBEST(const void* memPtr); + +MEM_STATIC void MEM_writeBE32(void* memPtr, U32 val32); +MEM_STATIC void MEM_writeBE64(void* memPtr, U64 val64); +MEM_STATIC void MEM_writeBEST(void* memPtr, size_t val); + +/*=== Byteswap ===*/ +MEM_STATIC U32 MEM_swap32(U32 in); +MEM_STATIC U64 MEM_swap64(U64 in); +MEM_STATIC size_t MEM_swapST(size_t in); + + +/*-************************************************************** +* Memory I/O Implementation +*****************************************************************/ +/* MEM_FORCE_MEMORY_ACCESS : For accessing unaligned memory: + * Method 0 : always use `memcpy()`. Safe and portable. + * Method 1 : Use compiler extension to set unaligned access. + * Method 2 : direct access. This method is portable but violate C standard. + * It can generate buggy code on targets depending on alignment. + * Default : method 1 if supported, else method 0 + */ +#ifndef MEM_FORCE_MEMORY_ACCESS /* can be defined externally, on command line for example */ +# ifdef __GNUC__ +# define MEM_FORCE_MEMORY_ACCESS 1 +# endif +#endif + +MEM_STATIC unsigned MEM_32bits(void) { return sizeof(size_t)==4; } +MEM_STATIC unsigned MEM_64bits(void) { return sizeof(size_t)==8; } + +MEM_STATIC unsigned MEM_isLittleEndian(void) +{ +#if defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + return 1; +#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + return 0; +#elif defined(__clang__) && __LITTLE_ENDIAN__ + return 1; +#elif defined(__clang__) && __BIG_ENDIAN__ + return 0; +#elif defined(_MSC_VER) && (_M_AMD64 || _M_IX86) + return 1; +#elif defined(__DMC__) && defined(_M_IX86) + return 1; +#else + const union { U32 u; BYTE c[4]; } one = { 1 }; /* don't use static : performance detrimental */ + return one.c[0]; +#endif +} + +#if defined(MEM_FORCE_MEMORY_ACCESS) && (MEM_FORCE_MEMORY_ACCESS==2) + +/* violates C standard, by lying on structure alignment. +Only use if no other choice to achieve best performance on target platform */ +MEM_STATIC U16 MEM_read16(const void* memPtr) { return *(const U16*) memPtr; } +MEM_STATIC U32 MEM_read32(const void* memPtr) { return *(const U32*) memPtr; } +MEM_STATIC U64 MEM_read64(const void* memPtr) { return *(const U64*) memPtr; } +MEM_STATIC size_t MEM_readST(const void* memPtr) { return *(const size_t*) memPtr; } + +MEM_STATIC void MEM_write16(void* memPtr, U16 value) { *(U16*)memPtr = value; } +MEM_STATIC void MEM_write32(void* memPtr, U32 value) { *(U32*)memPtr = value; } +MEM_STATIC void MEM_write64(void* memPtr, U64 value) { *(U64*)memPtr = value; } + +#elif defined(MEM_FORCE_MEMORY_ACCESS) && (MEM_FORCE_MEMORY_ACCESS==1) + +typedef __attribute__((aligned(1))) U16 unalign16; +typedef __attribute__((aligned(1))) U32 unalign32; +typedef __attribute__((aligned(1))) U64 unalign64; +typedef __attribute__((aligned(1))) size_t unalignArch; + +MEM_STATIC U16 MEM_read16(const void* ptr) { return *(const unalign16*)ptr; } +MEM_STATIC U32 MEM_read32(const void* ptr) { return *(const unalign32*)ptr; } +MEM_STATIC U64 MEM_read64(const void* ptr) { return *(const unalign64*)ptr; } +MEM_STATIC size_t MEM_readST(const void* ptr) { return *(const unalignArch*)ptr; } + +MEM_STATIC void MEM_write16(void* memPtr, U16 value) { *(unalign16*)memPtr = value; } +MEM_STATIC void MEM_write32(void* memPtr, U32 value) { *(unalign32*)memPtr = value; } +MEM_STATIC void MEM_write64(void* memPtr, U64 value) { *(unalign64*)memPtr = value; } + +#else + +/* default method, safe and standard. + can sometimes prove slower */ + +MEM_STATIC U16 MEM_read16(const void* memPtr) +{ + U16 val; ZSTD_memcpy(&val, memPtr, sizeof(val)); return val; +} + +MEM_STATIC U32 MEM_read32(const void* memPtr) +{ + U32 val; ZSTD_memcpy(&val, memPtr, sizeof(val)); return val; +} + +MEM_STATIC U64 MEM_read64(const void* memPtr) +{ + U64 val; ZSTD_memcpy(&val, memPtr, sizeof(val)); return val; +} + +MEM_STATIC size_t MEM_readST(const void* memPtr) +{ + size_t val; ZSTD_memcpy(&val, memPtr, sizeof(val)); return val; +} + +MEM_STATIC void MEM_write16(void* memPtr, U16 value) +{ + ZSTD_memcpy(memPtr, &value, sizeof(value)); +} + +MEM_STATIC void MEM_write32(void* memPtr, U32 value) +{ + ZSTD_memcpy(memPtr, &value, sizeof(value)); +} + +MEM_STATIC void MEM_write64(void* memPtr, U64 value) +{ + ZSTD_memcpy(memPtr, &value, sizeof(value)); +} + +#endif /* MEM_FORCE_MEMORY_ACCESS */ + +MEM_STATIC U32 MEM_swap32_fallback(U32 in) +{ + return ((in << 24) & 0xff000000 ) | + ((in << 8) & 0x00ff0000 ) | + ((in >> 8) & 0x0000ff00 ) | + ((in >> 24) & 0x000000ff ); +} + +MEM_STATIC U32 MEM_swap32(U32 in) +{ +#if defined(_MSC_VER) /* Visual Studio */ + return _byteswap_ulong(in); +#elif (defined (__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ >= 403)) \ + || (defined(__clang__) && __has_builtin(__builtin_bswap32)) + return __builtin_bswap32(in); +#else + return MEM_swap32_fallback(in); +#endif +} + +MEM_STATIC U64 MEM_swap64_fallback(U64 in) +{ + return ((in << 56) & 0xff00000000000000ULL) | + ((in << 40) & 0x00ff000000000000ULL) | + ((in << 24) & 0x0000ff0000000000ULL) | + ((in << 8) & 0x000000ff00000000ULL) | + ((in >> 8) & 0x00000000ff000000ULL) | + ((in >> 24) & 0x0000000000ff0000ULL) | + ((in >> 40) & 0x000000000000ff00ULL) | + ((in >> 56) & 0x00000000000000ffULL); +} + +MEM_STATIC U64 MEM_swap64(U64 in) +{ +#if defined(_MSC_VER) /* Visual Studio */ + return _byteswap_uint64(in); +#elif (defined (__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ >= 403)) \ + || (defined(__clang__) && __has_builtin(__builtin_bswap64)) + return __builtin_bswap64(in); +#else + return MEM_swap64_fallback(in); +#endif +} + +MEM_STATIC size_t MEM_swapST(size_t in) +{ + if (MEM_32bits()) + return (size_t)MEM_swap32((U32)in); + else + return (size_t)MEM_swap64((U64)in); +} + +/*=== Little endian r/w ===*/ + +MEM_STATIC U16 MEM_readLE16(const void* memPtr) +{ + if (MEM_isLittleEndian()) + return MEM_read16(memPtr); + else { + const BYTE* p = (const BYTE*)memPtr; + return (U16)(p[0] + (p[1]<<8)); + } +} + +MEM_STATIC void MEM_writeLE16(void* memPtr, U16 val) +{ + if (MEM_isLittleEndian()) { + MEM_write16(memPtr, val); + } else { + BYTE* p = (BYTE*)memPtr; + p[0] = (BYTE)val; + p[1] = (BYTE)(val>>8); + } +} + +MEM_STATIC U32 MEM_readLE24(const void* memPtr) +{ + return (U32)MEM_readLE16(memPtr) + ((U32)(((const BYTE*)memPtr)[2]) << 16); +} + +MEM_STATIC void MEM_writeLE24(void* memPtr, U32 val) +{ + MEM_writeLE16(memPtr, (U16)val); + ((BYTE*)memPtr)[2] = (BYTE)(val>>16); +} + +MEM_STATIC U32 MEM_readLE32(const void* memPtr) +{ + if (MEM_isLittleEndian()) + return MEM_read32(memPtr); + else + return MEM_swap32(MEM_read32(memPtr)); +} + +MEM_STATIC void MEM_writeLE32(void* memPtr, U32 val32) +{ + if (MEM_isLittleEndian()) + MEM_write32(memPtr, val32); + else + MEM_write32(memPtr, MEM_swap32(val32)); +} + +MEM_STATIC U64 MEM_readLE64(const void* memPtr) +{ + if (MEM_isLittleEndian()) + return MEM_read64(memPtr); + else + return MEM_swap64(MEM_read64(memPtr)); +} + +MEM_STATIC void MEM_writeLE64(void* memPtr, U64 val64) +{ + if (MEM_isLittleEndian()) + MEM_write64(memPtr, val64); + else + MEM_write64(memPtr, MEM_swap64(val64)); +} + +MEM_STATIC size_t MEM_readLEST(const void* memPtr) +{ + if (MEM_32bits()) + return (size_t)MEM_readLE32(memPtr); + else + return (size_t)MEM_readLE64(memPtr); +} + +MEM_STATIC void MEM_writeLEST(void* memPtr, size_t val) +{ + if (MEM_32bits()) + MEM_writeLE32(memPtr, (U32)val); + else + MEM_writeLE64(memPtr, (U64)val); +} + +/*=== Big endian r/w ===*/ + +MEM_STATIC U32 MEM_readBE32(const void* memPtr) +{ + if (MEM_isLittleEndian()) + return MEM_swap32(MEM_read32(memPtr)); + else + return MEM_read32(memPtr); +} + +MEM_STATIC void MEM_writeBE32(void* memPtr, U32 val32) +{ + if (MEM_isLittleEndian()) + MEM_write32(memPtr, MEM_swap32(val32)); + else + MEM_write32(memPtr, val32); +} + +MEM_STATIC U64 MEM_readBE64(const void* memPtr) +{ + if (MEM_isLittleEndian()) + return MEM_swap64(MEM_read64(memPtr)); + else + return MEM_read64(memPtr); +} + +MEM_STATIC void MEM_writeBE64(void* memPtr, U64 val64) +{ + if (MEM_isLittleEndian()) + MEM_write64(memPtr, MEM_swap64(val64)); + else + MEM_write64(memPtr, val64); +} + +MEM_STATIC size_t MEM_readBEST(const void* memPtr) +{ + if (MEM_32bits()) + return (size_t)MEM_readBE32(memPtr); + else + return (size_t)MEM_readBE64(memPtr); +} + +MEM_STATIC void MEM_writeBEST(void* memPtr, size_t val) +{ + if (MEM_32bits()) + MEM_writeBE32(memPtr, (U32)val); + else + MEM_writeBE64(memPtr, (U64)val); +} + +/* code only tested on 32 and 64 bits systems */ +MEM_STATIC void MEM_check(void) { DEBUG_STATIC_ASSERT((sizeof(size_t)==4) || (sizeof(size_t)==8)); } + + +#if defined (__cplusplus) +} +#endif + +#endif /* MEM_H_MODULE */ +/**** ended inlining mem.h ****/ +/**** start inlining error_private.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/* Note : this module is expected to remain private, do not expose it */ + +#ifndef ERROR_H_MODULE +#define ERROR_H_MODULE + +#if defined (__cplusplus) +extern "C" { +#endif + + +/* **************************************** +* Dependencies +******************************************/ +/**** start inlining ../zstd_errors.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_ERRORS_H_398273423 +#define ZSTD_ERRORS_H_398273423 + +#if defined (__cplusplus) +extern "C" { +#endif + +/*===== dependency =====*/ +#include /* size_t */ + + +/* ===== ZSTDERRORLIB_API : control library symbols visibility ===== */ +#ifndef ZSTDERRORLIB_VISIBLE + /* Backwards compatibility with old macro name */ +# ifdef ZSTDERRORLIB_VISIBILITY +# define ZSTDERRORLIB_VISIBLE ZSTDERRORLIB_VISIBILITY +# elif defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__) +# define ZSTDERRORLIB_VISIBLE __attribute__ ((visibility ("default"))) +# else +# define ZSTDERRORLIB_VISIBLE +# endif +#endif + +#ifndef ZSTDERRORLIB_HIDDEN +# if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__) +# define ZSTDERRORLIB_HIDDEN __attribute__ ((visibility ("hidden"))) +# else +# define ZSTDERRORLIB_HIDDEN +# endif +#endif + +#if defined(ZSTD_DLL_EXPORT) && (ZSTD_DLL_EXPORT==1) +# define ZSTDERRORLIB_API __declspec(dllexport) ZSTDERRORLIB_VISIBLE +#elif defined(ZSTD_DLL_IMPORT) && (ZSTD_DLL_IMPORT==1) +# define ZSTDERRORLIB_API __declspec(dllimport) ZSTDERRORLIB_VISIBLE /* It isn't required but allows to generate better code, saving a function pointer load from the IAT and an indirect jump.*/ +#else +# define ZSTDERRORLIB_API ZSTDERRORLIB_VISIBLE +#endif + +/*-********************************************* + * Error codes list + *-********************************************* + * Error codes _values_ are pinned down since v1.3.1 only. + * Therefore, don't rely on values if you may link to any version < v1.3.1. + * + * Only values < 100 are considered stable. + * + * note 1 : this API shall be used with static linking only. + * dynamic linking is not yet officially supported. + * note 2 : Prefer relying on the enum than on its value whenever possible + * This is the only supported way to use the error list < v1.3.1 + * note 3 : ZSTD_isError() is always correct, whatever the library version. + **********************************************/ +typedef enum { + ZSTD_error_no_error = 0, + ZSTD_error_GENERIC = 1, + ZSTD_error_prefix_unknown = 10, + ZSTD_error_version_unsupported = 12, + ZSTD_error_frameParameter_unsupported = 14, + ZSTD_error_frameParameter_windowTooLarge = 16, + ZSTD_error_corruption_detected = 20, + ZSTD_error_checksum_wrong = 22, + ZSTD_error_literals_headerWrong = 24, + ZSTD_error_dictionary_corrupted = 30, + ZSTD_error_dictionary_wrong = 32, + ZSTD_error_dictionaryCreation_failed = 34, + ZSTD_error_parameter_unsupported = 40, + ZSTD_error_parameter_combination_unsupported = 41, + ZSTD_error_parameter_outOfBound = 42, + ZSTD_error_tableLog_tooLarge = 44, + ZSTD_error_maxSymbolValue_tooLarge = 46, + ZSTD_error_maxSymbolValue_tooSmall = 48, + ZSTD_error_stabilityCondition_notRespected = 50, + ZSTD_error_stage_wrong = 60, + ZSTD_error_init_missing = 62, + ZSTD_error_memory_allocation = 64, + ZSTD_error_workSpace_tooSmall= 66, + ZSTD_error_dstSize_tooSmall = 70, + ZSTD_error_srcSize_wrong = 72, + ZSTD_error_dstBuffer_null = 74, + ZSTD_error_noForwardProgress_destFull = 80, + ZSTD_error_noForwardProgress_inputEmpty = 82, + /* following error codes are __NOT STABLE__, they can be removed or changed in future versions */ + ZSTD_error_frameIndex_tooLarge = 100, + ZSTD_error_seekableIO = 102, + ZSTD_error_dstBuffer_wrong = 104, + ZSTD_error_srcBuffer_wrong = 105, + ZSTD_error_sequenceProducer_failed = 106, + ZSTD_error_externalSequences_invalid = 107, + ZSTD_error_maxCode = 120 /* never EVER use this value directly, it can change in future versions! Use ZSTD_isError() instead */ +} ZSTD_ErrorCode; + +/*! ZSTD_getErrorCode() : + convert a `size_t` function result into a `ZSTD_ErrorCode` enum type, + which can be used to compare with enum list published above */ +ZSTDERRORLIB_API ZSTD_ErrorCode ZSTD_getErrorCode(size_t functionResult); +ZSTDERRORLIB_API const char* ZSTD_getErrorString(ZSTD_ErrorCode code); /**< Same as ZSTD_getErrorName, but using a `ZSTD_ErrorCode` enum argument */ + + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_ERRORS_H_398273423 */ +/**** ended inlining ../zstd_errors.h ****/ +/**** skipping file: compiler.h ****/ +/**** skipping file: debug.h ****/ +/**** skipping file: zstd_deps.h ****/ + + +/* **************************************** +* Compiler-specific +******************************************/ +#if defined(__GNUC__) +# define ERR_STATIC static __attribute__((unused)) +#elif defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) +# define ERR_STATIC static inline +#elif defined(_MSC_VER) +# define ERR_STATIC static __inline +#else +# define ERR_STATIC static /* this version may generate warnings for unused static functions; disable the relevant warning */ +#endif + + +/*-**************************************** +* Customization (error_public.h) +******************************************/ +typedef ZSTD_ErrorCode ERR_enum; +#define PREFIX(name) ZSTD_error_##name + + +/*-**************************************** +* Error codes handling +******************************************/ +#undef ERROR /* already defined on Visual Studio */ +#define ERROR(name) ZSTD_ERROR(name) +#define ZSTD_ERROR(name) ((size_t)-PREFIX(name)) + +ERR_STATIC unsigned ERR_isError(size_t code) { return (code > ERROR(maxCode)); } + +ERR_STATIC ERR_enum ERR_getErrorCode(size_t code) { if (!ERR_isError(code)) return (ERR_enum)0; return (ERR_enum) (0-code); } + +/* check and forward error code */ +#define CHECK_V_F(e, f) \ + size_t const e = f; \ + do { \ + if (ERR_isError(e)) \ + return e; \ + } while (0) +#define CHECK_F(f) do { CHECK_V_F(_var_err__, f); } while (0) + + +/*-**************************************** +* Error Strings +******************************************/ + +const char* ERR_getErrorString(ERR_enum code); /* error_private.c */ + +ERR_STATIC const char* ERR_getErrorName(size_t code) +{ + return ERR_getErrorString(ERR_getErrorCode(code)); +} + +/** + * Ignore: this is an internal helper. + * + * This is a helper function to help force C99-correctness during compilation. + * Under strict compilation modes, variadic macro arguments can't be empty. + * However, variadic function arguments can be. Using a function therefore lets + * us statically check that at least one (string) argument was passed, + * independent of the compilation flags. + */ +static INLINE_KEYWORD UNUSED_ATTR +void _force_has_format_string(const char *format, ...) { + (void)format; +} + +/** + * Ignore: this is an internal helper. + * + * We want to force this function invocation to be syntactically correct, but + * we don't want to force runtime evaluation of its arguments. + */ +#define _FORCE_HAS_FORMAT_STRING(...) \ + do { \ + if (0) { \ + _force_has_format_string(__VA_ARGS__); \ + } \ + } while (0) + +#define ERR_QUOTE(str) #str + +/** + * Return the specified error if the condition evaluates to true. + * + * In debug modes, prints additional information. + * In order to do that (particularly, printing the conditional that failed), + * this can't just wrap RETURN_ERROR(). + */ +#define RETURN_ERROR_IF(cond, err, ...) \ + do { \ + if (cond) { \ + RAWLOG(3, "%s:%d: ERROR!: check %s failed, returning %s", \ + __FILE__, __LINE__, ERR_QUOTE(cond), ERR_QUOTE(ERROR(err))); \ + _FORCE_HAS_FORMAT_STRING(__VA_ARGS__); \ + RAWLOG(3, ": " __VA_ARGS__); \ + RAWLOG(3, "\n"); \ + return ERROR(err); \ + } \ + } while (0) + +/** + * Unconditionally return the specified error. + * + * In debug modes, prints additional information. + */ +#define RETURN_ERROR(err, ...) \ + do { \ + RAWLOG(3, "%s:%d: ERROR!: unconditional check failed, returning %s", \ + __FILE__, __LINE__, ERR_QUOTE(ERROR(err))); \ + _FORCE_HAS_FORMAT_STRING(__VA_ARGS__); \ + RAWLOG(3, ": " __VA_ARGS__); \ + RAWLOG(3, "\n"); \ + return ERROR(err); \ + } while(0) + +/** + * If the provided expression evaluates to an error code, returns that error code. + * + * In debug modes, prints additional information. + */ +#define FORWARD_IF_ERROR(err, ...) \ + do { \ + size_t const err_code = (err); \ + if (ERR_isError(err_code)) { \ + RAWLOG(3, "%s:%d: ERROR!: forwarding error in %s: %s", \ + __FILE__, __LINE__, ERR_QUOTE(err), ERR_getErrorName(err_code)); \ + _FORCE_HAS_FORMAT_STRING(__VA_ARGS__); \ + RAWLOG(3, ": " __VA_ARGS__); \ + RAWLOG(3, "\n"); \ + return err_code; \ + } \ + } while(0) + +#if defined (__cplusplus) +} +#endif + +#endif /* ERROR_H_MODULE */ +/**** ended inlining error_private.h ****/ +#define FSE_STATIC_LINKING_ONLY /* FSE_MIN_TABLELOG */ +/**** start inlining fse.h ****/ +/* ****************************************************************** + * FSE : Finite State Entropy codec + * Public Prototypes declaration + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - Source repository : https://github.com/Cyan4973/FiniteStateEntropy + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + +#if defined (__cplusplus) +extern "C" { +#endif + +#ifndef FSE_H +#define FSE_H + + +/*-***************************************** +* Dependencies +******************************************/ +/**** skipping file: zstd_deps.h ****/ + + +/*-***************************************** +* FSE_PUBLIC_API : control library symbols visibility +******************************************/ +#if defined(FSE_DLL_EXPORT) && (FSE_DLL_EXPORT==1) && defined(__GNUC__) && (__GNUC__ >= 4) +# define FSE_PUBLIC_API __attribute__ ((visibility ("default"))) +#elif defined(FSE_DLL_EXPORT) && (FSE_DLL_EXPORT==1) /* Visual expected */ +# define FSE_PUBLIC_API __declspec(dllexport) +#elif defined(FSE_DLL_IMPORT) && (FSE_DLL_IMPORT==1) +# define FSE_PUBLIC_API __declspec(dllimport) /* It isn't required but allows to generate better code, saving a function pointer load from the IAT and an indirect jump.*/ +#else +# define FSE_PUBLIC_API +#endif + +/*------ Version ------*/ +#define FSE_VERSION_MAJOR 0 +#define FSE_VERSION_MINOR 9 +#define FSE_VERSION_RELEASE 0 + +#define FSE_LIB_VERSION FSE_VERSION_MAJOR.FSE_VERSION_MINOR.FSE_VERSION_RELEASE +#define FSE_QUOTE(str) #str +#define FSE_EXPAND_AND_QUOTE(str) FSE_QUOTE(str) +#define FSE_VERSION_STRING FSE_EXPAND_AND_QUOTE(FSE_LIB_VERSION) + +#define FSE_VERSION_NUMBER (FSE_VERSION_MAJOR *100*100 + FSE_VERSION_MINOR *100 + FSE_VERSION_RELEASE) +FSE_PUBLIC_API unsigned FSE_versionNumber(void); /**< library version number; to be used when checking dll version */ + + +/*-***************************************** +* Tool functions +******************************************/ +FSE_PUBLIC_API size_t FSE_compressBound(size_t size); /* maximum compressed size */ + +/* Error Management */ +FSE_PUBLIC_API unsigned FSE_isError(size_t code); /* tells if a return value is an error code */ +FSE_PUBLIC_API const char* FSE_getErrorName(size_t code); /* provides error code string (useful for debugging) */ + + +/*-***************************************** +* FSE detailed API +******************************************/ +/*! +FSE_compress() does the following: +1. count symbol occurrence from source[] into table count[] (see hist.h) +2. normalize counters so that sum(count[]) == Power_of_2 (2^tableLog) +3. save normalized counters to memory buffer using writeNCount() +4. build encoding table 'CTable' from normalized counters +5. encode the data stream using encoding table 'CTable' + +FSE_decompress() does the following: +1. read normalized counters with readNCount() +2. build decoding table 'DTable' from normalized counters +3. decode the data stream using decoding table 'DTable' + +The following API allows targeting specific sub-functions for advanced tasks. +For example, it's possible to compress several blocks using the same 'CTable', +or to save and provide normalized distribution using external method. +*/ + +/* *** COMPRESSION *** */ + +/*! FSE_optimalTableLog(): + dynamically downsize 'tableLog' when conditions are met. + It saves CPU time, by using smaller tables, while preserving or even improving compression ratio. + @return : recommended tableLog (necessarily <= 'maxTableLog') */ +FSE_PUBLIC_API unsigned FSE_optimalTableLog(unsigned maxTableLog, size_t srcSize, unsigned maxSymbolValue); + +/*! FSE_normalizeCount(): + normalize counts so that sum(count[]) == Power_of_2 (2^tableLog) + 'normalizedCounter' is a table of short, of minimum size (maxSymbolValue+1). + useLowProbCount is a boolean parameter which trades off compressed size for + faster header decoding. When it is set to 1, the compressed data will be slightly + smaller. And when it is set to 0, FSE_readNCount() and FSE_buildDTable() will be + faster. If you are compressing a small amount of data (< 2 KB) then useLowProbCount=0 + is a good default, since header deserialization makes a big speed difference. + Otherwise, useLowProbCount=1 is a good default, since the speed difference is small. + @return : tableLog, + or an errorCode, which can be tested using FSE_isError() */ +FSE_PUBLIC_API size_t FSE_normalizeCount(short* normalizedCounter, unsigned tableLog, + const unsigned* count, size_t srcSize, unsigned maxSymbolValue, unsigned useLowProbCount); + +/*! FSE_NCountWriteBound(): + Provides the maximum possible size of an FSE normalized table, given 'maxSymbolValue' and 'tableLog'. + Typically useful for allocation purpose. */ +FSE_PUBLIC_API size_t FSE_NCountWriteBound(unsigned maxSymbolValue, unsigned tableLog); + +/*! FSE_writeNCount(): + Compactly save 'normalizedCounter' into 'buffer'. + @return : size of the compressed table, + or an errorCode, which can be tested using FSE_isError(). */ +FSE_PUBLIC_API size_t FSE_writeNCount (void* buffer, size_t bufferSize, + const short* normalizedCounter, + unsigned maxSymbolValue, unsigned tableLog); + +/*! Constructor and Destructor of FSE_CTable. + Note that FSE_CTable size depends on 'tableLog' and 'maxSymbolValue' */ +typedef unsigned FSE_CTable; /* don't allocate that. It's only meant to be more restrictive than void* */ + +/*! FSE_buildCTable(): + Builds `ct`, which must be already allocated, using FSE_createCTable(). + @return : 0, or an errorCode, which can be tested using FSE_isError() */ +FSE_PUBLIC_API size_t FSE_buildCTable(FSE_CTable* ct, const short* normalizedCounter, unsigned maxSymbolValue, unsigned tableLog); + +/*! FSE_compress_usingCTable(): + Compress `src` using `ct` into `dst` which must be already allocated. + @return : size of compressed data (<= `dstCapacity`), + or 0 if compressed data could not fit into `dst`, + or an errorCode, which can be tested using FSE_isError() */ +FSE_PUBLIC_API size_t FSE_compress_usingCTable (void* dst, size_t dstCapacity, const void* src, size_t srcSize, const FSE_CTable* ct); + +/*! +Tutorial : +---------- +The first step is to count all symbols. FSE_count() does this job very fast. +Result will be saved into 'count', a table of unsigned int, which must be already allocated, and have 'maxSymbolValuePtr[0]+1' cells. +'src' is a table of bytes of size 'srcSize'. All values within 'src' MUST be <= maxSymbolValuePtr[0] +maxSymbolValuePtr[0] will be updated, with its real value (necessarily <= original value) +FSE_count() will return the number of occurrence of the most frequent symbol. +This can be used to know if there is a single symbol within 'src', and to quickly evaluate its compressibility. +If there is an error, the function will return an ErrorCode (which can be tested using FSE_isError()). + +The next step is to normalize the frequencies. +FSE_normalizeCount() will ensure that sum of frequencies is == 2 ^'tableLog'. +It also guarantees a minimum of 1 to any Symbol with frequency >= 1. +You can use 'tableLog'==0 to mean "use default tableLog value". +If you are unsure of which tableLog value to use, you can ask FSE_optimalTableLog(), +which will provide the optimal valid tableLog given sourceSize, maxSymbolValue, and a user-defined maximum (0 means "default"). + +The result of FSE_normalizeCount() will be saved into a table, +called 'normalizedCounter', which is a table of signed short. +'normalizedCounter' must be already allocated, and have at least 'maxSymbolValue+1' cells. +The return value is tableLog if everything proceeded as expected. +It is 0 if there is a single symbol within distribution. +If there is an error (ex: invalid tableLog value), the function will return an ErrorCode (which can be tested using FSE_isError()). + +'normalizedCounter' can be saved in a compact manner to a memory area using FSE_writeNCount(). +'buffer' must be already allocated. +For guaranteed success, buffer size must be at least FSE_headerBound(). +The result of the function is the number of bytes written into 'buffer'. +If there is an error, the function will return an ErrorCode (which can be tested using FSE_isError(); ex : buffer size too small). + +'normalizedCounter' can then be used to create the compression table 'CTable'. +The space required by 'CTable' must be already allocated, using FSE_createCTable(). +You can then use FSE_buildCTable() to fill 'CTable'. +If there is an error, both functions will return an ErrorCode (which can be tested using FSE_isError()). + +'CTable' can then be used to compress 'src', with FSE_compress_usingCTable(). +Similar to FSE_count(), the convention is that 'src' is assumed to be a table of char of size 'srcSize' +The function returns the size of compressed data (without header), necessarily <= `dstCapacity`. +If it returns '0', compressed data could not fit into 'dst'. +If there is an error, the function will return an ErrorCode (which can be tested using FSE_isError()). +*/ + + +/* *** DECOMPRESSION *** */ + +/*! FSE_readNCount(): + Read compactly saved 'normalizedCounter' from 'rBuffer'. + @return : size read from 'rBuffer', + or an errorCode, which can be tested using FSE_isError(). + maxSymbolValuePtr[0] and tableLogPtr[0] will also be updated with their respective values */ +FSE_PUBLIC_API size_t FSE_readNCount (short* normalizedCounter, + unsigned* maxSymbolValuePtr, unsigned* tableLogPtr, + const void* rBuffer, size_t rBuffSize); + +/*! FSE_readNCount_bmi2(): + * Same as FSE_readNCount() but pass bmi2=1 when your CPU supports BMI2 and 0 otherwise. + */ +FSE_PUBLIC_API size_t FSE_readNCount_bmi2(short* normalizedCounter, + unsigned* maxSymbolValuePtr, unsigned* tableLogPtr, + const void* rBuffer, size_t rBuffSize, int bmi2); + +typedef unsigned FSE_DTable; /* don't allocate that. It's just a way to be more restrictive than void* */ + +/*! +Tutorial : +---------- +(Note : these functions only decompress FSE-compressed blocks. + If block is uncompressed, use memcpy() instead + If block is a single repeated byte, use memset() instead ) + +The first step is to obtain the normalized frequencies of symbols. +This can be performed by FSE_readNCount() if it was saved using FSE_writeNCount(). +'normalizedCounter' must be already allocated, and have at least 'maxSymbolValuePtr[0]+1' cells of signed short. +In practice, that means it's necessary to know 'maxSymbolValue' beforehand, +or size the table to handle worst case situations (typically 256). +FSE_readNCount() will provide 'tableLog' and 'maxSymbolValue'. +The result of FSE_readNCount() is the number of bytes read from 'rBuffer'. +Note that 'rBufferSize' must be at least 4 bytes, even if useful information is less than that. +If there is an error, the function will return an error code, which can be tested using FSE_isError(). + +The next step is to build the decompression tables 'FSE_DTable' from 'normalizedCounter'. +This is performed by the function FSE_buildDTable(). +The space required by 'FSE_DTable' must be already allocated using FSE_createDTable(). +If there is an error, the function will return an error code, which can be tested using FSE_isError(). + +`FSE_DTable` can then be used to decompress `cSrc`, with FSE_decompress_usingDTable(). +`cSrcSize` must be strictly correct, otherwise decompression will fail. +FSE_decompress_usingDTable() result will tell how many bytes were regenerated (<=`dstCapacity`). +If there is an error, the function will return an error code, which can be tested using FSE_isError(). (ex: dst buffer too small) +*/ + +#endif /* FSE_H */ + + +#if defined(FSE_STATIC_LINKING_ONLY) && !defined(FSE_H_FSE_STATIC_LINKING_ONLY) +#define FSE_H_FSE_STATIC_LINKING_ONLY + +/* *** Dependency *** */ +/**** start inlining bitstream.h ****/ +/* ****************************************************************** + * bitstream + * Part of FSE library + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - Source repository : https://github.com/Cyan4973/FiniteStateEntropy + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ +#ifndef BITSTREAM_H_MODULE +#define BITSTREAM_H_MODULE + +#if defined (__cplusplus) +extern "C" { +#endif +/* +* This API consists of small unitary functions, which must be inlined for best performance. +* Since link-time-optimization is not available for all compilers, +* these functions are defined into a .h to be included. +*/ + +/*-**************************************** +* Dependencies +******************************************/ +/**** skipping file: mem.h ****/ +/**** skipping file: compiler.h ****/ +/**** skipping file: debug.h ****/ +/**** skipping file: error_private.h ****/ +/**** start inlining bits.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_BITS_H +#define ZSTD_BITS_H + +/**** skipping file: mem.h ****/ + +MEM_STATIC unsigned ZSTD_countTrailingZeros32_fallback(U32 val) +{ + assert(val != 0); + { + static const U32 DeBruijnBytePos[32] = {0, 1, 28, 2, 29, 14, 24, 3, + 30, 22, 20, 15, 25, 17, 4, 8, + 31, 27, 13, 23, 21, 19, 16, 7, + 26, 12, 18, 6, 11, 5, 10, 9}; + return DeBruijnBytePos[((U32) ((val & -(S32) val) * 0x077CB531U)) >> 27]; + } +} + +MEM_STATIC unsigned ZSTD_countTrailingZeros32(U32 val) +{ + assert(val != 0); +# if defined(_MSC_VER) +# if STATIC_BMI2 == 1 + return (unsigned)_tzcnt_u32(val); +# else + if (val != 0) { + unsigned long r; + _BitScanForward(&r, val); + return (unsigned)r; + } else { + /* Should not reach this code path */ + __assume(0); + } +# endif +# elif defined(__GNUC__) && (__GNUC__ >= 4) + return (unsigned)__builtin_ctz(val); +# else + return ZSTD_countTrailingZeros32_fallback(val); +# endif +} + +MEM_STATIC unsigned ZSTD_countLeadingZeros32_fallback(U32 val) { + assert(val != 0); + { + static const U32 DeBruijnClz[32] = {0, 9, 1, 10, 13, 21, 2, 29, + 11, 14, 16, 18, 22, 25, 3, 30, + 8, 12, 20, 28, 15, 17, 24, 7, + 19, 27, 23, 6, 26, 5, 4, 31}; + val |= val >> 1; + val |= val >> 2; + val |= val >> 4; + val |= val >> 8; + val |= val >> 16; + return 31 - DeBruijnClz[(val * 0x07C4ACDDU) >> 27]; + } +} + +MEM_STATIC unsigned ZSTD_countLeadingZeros32(U32 val) +{ + assert(val != 0); +# if defined(_MSC_VER) +# if STATIC_BMI2 == 1 + return (unsigned)_lzcnt_u32(val); +# else + if (val != 0) { + unsigned long r; + _BitScanReverse(&r, val); + return (unsigned)(31 - r); + } else { + /* Should not reach this code path */ + __assume(0); + } +# endif +# elif defined(__GNUC__) && (__GNUC__ >= 4) + return (unsigned)__builtin_clz(val); +# else + return ZSTD_countLeadingZeros32_fallback(val); +# endif +} + +MEM_STATIC unsigned ZSTD_countTrailingZeros64(U64 val) +{ + assert(val != 0); +# if defined(_MSC_VER) && defined(_WIN64) +# if STATIC_BMI2 == 1 + return (unsigned)_tzcnt_u64(val); +# else + if (val != 0) { + unsigned long r; + _BitScanForward64(&r, val); + return (unsigned)r; + } else { + /* Should not reach this code path */ + __assume(0); + } +# endif +# elif defined(__GNUC__) && (__GNUC__ >= 4) && defined(__LP64__) + return (unsigned)__builtin_ctzll(val); +# else + { + U32 mostSignificantWord = (U32)(val >> 32); + U32 leastSignificantWord = (U32)val; + if (leastSignificantWord == 0) { + return 32 + ZSTD_countTrailingZeros32(mostSignificantWord); + } else { + return ZSTD_countTrailingZeros32(leastSignificantWord); + } + } +# endif +} + +MEM_STATIC unsigned ZSTD_countLeadingZeros64(U64 val) +{ + assert(val != 0); +# if defined(_MSC_VER) && defined(_WIN64) +# if STATIC_BMI2 == 1 + return (unsigned)_lzcnt_u64(val); +# else + if (val != 0) { + unsigned long r; + _BitScanReverse64(&r, val); + return (unsigned)(63 - r); + } else { + /* Should not reach this code path */ + __assume(0); + } +# endif +# elif defined(__GNUC__) && (__GNUC__ >= 4) + return (unsigned)(__builtin_clzll(val)); +# else + { + U32 mostSignificantWord = (U32)(val >> 32); + U32 leastSignificantWord = (U32)val; + if (mostSignificantWord == 0) { + return 32 + ZSTD_countLeadingZeros32(leastSignificantWord); + } else { + return ZSTD_countLeadingZeros32(mostSignificantWord); + } + } +# endif +} + +MEM_STATIC unsigned ZSTD_NbCommonBytes(size_t val) +{ + if (MEM_isLittleEndian()) { + if (MEM_64bits()) { + return ZSTD_countTrailingZeros64((U64)val) >> 3; + } else { + return ZSTD_countTrailingZeros32((U32)val) >> 3; + } + } else { /* Big Endian CPU */ + if (MEM_64bits()) { + return ZSTD_countLeadingZeros64((U64)val) >> 3; + } else { + return ZSTD_countLeadingZeros32((U32)val) >> 3; + } + } +} + +MEM_STATIC unsigned ZSTD_highbit32(U32 val) /* compress, dictBuilder, decodeCorpus */ +{ + assert(val != 0); + return 31 - ZSTD_countLeadingZeros32(val); +} + +/* ZSTD_rotateRight_*(): + * Rotates a bitfield to the right by "count" bits. + * https://en.wikipedia.org/w/index.php?title=Circular_shift&oldid=991635599#Implementing_circular_shifts + */ +MEM_STATIC +U64 ZSTD_rotateRight_U64(U64 const value, U32 count) { + assert(count < 64); + count &= 0x3F; /* for fickle pattern recognition */ + return (value >> count) | (U64)(value << ((0U - count) & 0x3F)); +} + +MEM_STATIC +U32 ZSTD_rotateRight_U32(U32 const value, U32 count) { + assert(count < 32); + count &= 0x1F; /* for fickle pattern recognition */ + return (value >> count) | (U32)(value << ((0U - count) & 0x1F)); +} + +MEM_STATIC +U16 ZSTD_rotateRight_U16(U16 const value, U32 count) { + assert(count < 16); + count &= 0x0F; /* for fickle pattern recognition */ + return (value >> count) | (U16)(value << ((0U - count) & 0x0F)); +} + +#endif /* ZSTD_BITS_H */ +/**** ended inlining bits.h ****/ + + +/*========================================= +* Target specific +=========================================*/ +#ifndef ZSTD_NO_INTRINSICS +# if (defined(__BMI__) || defined(__BMI2__)) && defined(__GNUC__) +# include /* support for bextr (experimental)/bzhi */ +# elif defined(__ICCARM__) +# include +# endif +#endif + +#define STREAM_ACCUMULATOR_MIN_32 25 +#define STREAM_ACCUMULATOR_MIN_64 57 +#define STREAM_ACCUMULATOR_MIN ((U32)(MEM_32bits() ? STREAM_ACCUMULATOR_MIN_32 : STREAM_ACCUMULATOR_MIN_64)) + + +/*-****************************************** +* bitStream encoding API (write forward) +********************************************/ +/* bitStream can mix input from multiple sources. + * A critical property of these streams is that they encode and decode in **reverse** direction. + * So the first bit sequence you add will be the last to be read, like a LIFO stack. + */ +typedef struct { + size_t bitContainer; + unsigned bitPos; + char* startPtr; + char* ptr; + char* endPtr; +} BIT_CStream_t; + +MEM_STATIC size_t BIT_initCStream(BIT_CStream_t* bitC, void* dstBuffer, size_t dstCapacity); +MEM_STATIC void BIT_addBits(BIT_CStream_t* bitC, size_t value, unsigned nbBits); +MEM_STATIC void BIT_flushBits(BIT_CStream_t* bitC); +MEM_STATIC size_t BIT_closeCStream(BIT_CStream_t* bitC); + +/* Start with initCStream, providing the size of buffer to write into. +* bitStream will never write outside of this buffer. +* `dstCapacity` must be >= sizeof(bitD->bitContainer), otherwise @return will be an error code. +* +* bits are first added to a local register. +* Local register is size_t, hence 64-bits on 64-bits systems, or 32-bits on 32-bits systems. +* Writing data into memory is an explicit operation, performed by the flushBits function. +* Hence keep track how many bits are potentially stored into local register to avoid register overflow. +* After a flushBits, a maximum of 7 bits might still be stored into local register. +* +* Avoid storing elements of more than 24 bits if you want compatibility with 32-bits bitstream readers. +* +* Last operation is to close the bitStream. +* The function returns the final size of CStream in bytes. +* If data couldn't fit into `dstBuffer`, it will return a 0 ( == not storable) +*/ + + +/*-******************************************** +* bitStream decoding API (read backward) +**********************************************/ +typedef size_t BitContainerType; +typedef struct { + BitContainerType bitContainer; + unsigned bitsConsumed; + const char* ptr; + const char* start; + const char* limitPtr; +} BIT_DStream_t; + +typedef enum { BIT_DStream_unfinished = 0, /* fully refilled */ + BIT_DStream_endOfBuffer = 1, /* still some bits left in bitstream */ + BIT_DStream_completed = 2, /* bitstream entirely consumed, bit-exact */ + BIT_DStream_overflow = 3 /* user requested more bits than present in bitstream */ + } BIT_DStream_status; /* result of BIT_reloadDStream() */ + +MEM_STATIC size_t BIT_initDStream(BIT_DStream_t* bitD, const void* srcBuffer, size_t srcSize); +MEM_STATIC size_t BIT_readBits(BIT_DStream_t* bitD, unsigned nbBits); +MEM_STATIC BIT_DStream_status BIT_reloadDStream(BIT_DStream_t* bitD); +MEM_STATIC unsigned BIT_endOfDStream(const BIT_DStream_t* bitD); + + +/* Start by invoking BIT_initDStream(). +* A chunk of the bitStream is then stored into a local register. +* Local register size is 64-bits on 64-bits systems, 32-bits on 32-bits systems (BitContainerType). +* You can then retrieve bitFields stored into the local register, **in reverse order**. +* Local register is explicitly reloaded from memory by the BIT_reloadDStream() method. +* A reload guarantee a minimum of ((8*sizeof(bitD->bitContainer))-7) bits when its result is BIT_DStream_unfinished. +* Otherwise, it can be less than that, so proceed accordingly. +* Checking if DStream has reached its end can be performed with BIT_endOfDStream(). +*/ + + +/*-**************************************** +* unsafe API +******************************************/ +MEM_STATIC void BIT_addBitsFast(BIT_CStream_t* bitC, size_t value, unsigned nbBits); +/* faster, but works only if value is "clean", meaning all high bits above nbBits are 0 */ + +MEM_STATIC void BIT_flushBitsFast(BIT_CStream_t* bitC); +/* unsafe version; does not check buffer overflow */ + +MEM_STATIC size_t BIT_readBitsFast(BIT_DStream_t* bitD, unsigned nbBits); +/* faster, but works only if nbBits >= 1 */ + +/*===== Local Constants =====*/ +static const unsigned BIT_mask[] = { + 0, 1, 3, 7, 0xF, 0x1F, + 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, + 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF, 0x1FFFF, + 0x3FFFF, 0x7FFFF, 0xFFFFF, 0x1FFFFF, 0x3FFFFF, 0x7FFFFF, + 0xFFFFFF, 0x1FFFFFF, 0x3FFFFFF, 0x7FFFFFF, 0xFFFFFFF, 0x1FFFFFFF, + 0x3FFFFFFF, 0x7FFFFFFF}; /* up to 31 bits */ +#define BIT_MASK_SIZE (sizeof(BIT_mask) / sizeof(BIT_mask[0])) + +/*-************************************************************** +* bitStream encoding +****************************************************************/ +/*! BIT_initCStream() : + * `dstCapacity` must be > sizeof(size_t) + * @return : 0 if success, + * otherwise an error code (can be tested using ERR_isError()) */ +MEM_STATIC size_t BIT_initCStream(BIT_CStream_t* bitC, + void* startPtr, size_t dstCapacity) +{ + bitC->bitContainer = 0; + bitC->bitPos = 0; + bitC->startPtr = (char*)startPtr; + bitC->ptr = bitC->startPtr; + bitC->endPtr = bitC->startPtr + dstCapacity - sizeof(bitC->bitContainer); + if (dstCapacity <= sizeof(bitC->bitContainer)) return ERROR(dstSize_tooSmall); + return 0; +} + +FORCE_INLINE_TEMPLATE size_t BIT_getLowerBits(size_t bitContainer, U32 const nbBits) +{ +#if defined(STATIC_BMI2) && STATIC_BMI2 == 1 && !defined(ZSTD_NO_INTRINSICS) + return _bzhi_u64(bitContainer, nbBits); +#else + assert(nbBits < BIT_MASK_SIZE); + return bitContainer & BIT_mask[nbBits]; +#endif +} + +/*! BIT_addBits() : + * can add up to 31 bits into `bitC`. + * Note : does not check for register overflow ! */ +MEM_STATIC void BIT_addBits(BIT_CStream_t* bitC, + size_t value, unsigned nbBits) +{ + DEBUG_STATIC_ASSERT(BIT_MASK_SIZE == 32); + assert(nbBits < BIT_MASK_SIZE); + assert(nbBits + bitC->bitPos < sizeof(bitC->bitContainer) * 8); + bitC->bitContainer |= BIT_getLowerBits(value, nbBits) << bitC->bitPos; + bitC->bitPos += nbBits; +} + +/*! BIT_addBitsFast() : + * works only if `value` is _clean_, + * meaning all high bits above nbBits are 0 */ +MEM_STATIC void BIT_addBitsFast(BIT_CStream_t* bitC, + size_t value, unsigned nbBits) +{ + assert((value>>nbBits) == 0); + assert(nbBits + bitC->bitPos < sizeof(bitC->bitContainer) * 8); + bitC->bitContainer |= value << bitC->bitPos; + bitC->bitPos += nbBits; +} + +/*! BIT_flushBitsFast() : + * assumption : bitContainer has not overflowed + * unsafe version; does not check buffer overflow */ +MEM_STATIC void BIT_flushBitsFast(BIT_CStream_t* bitC) +{ + size_t const nbBytes = bitC->bitPos >> 3; + assert(bitC->bitPos < sizeof(bitC->bitContainer) * 8); + assert(bitC->ptr <= bitC->endPtr); + MEM_writeLEST(bitC->ptr, bitC->bitContainer); + bitC->ptr += nbBytes; + bitC->bitPos &= 7; + bitC->bitContainer >>= nbBytes*8; +} + +/*! BIT_flushBits() : + * assumption : bitContainer has not overflowed + * safe version; check for buffer overflow, and prevents it. + * note : does not signal buffer overflow. + * overflow will be revealed later on using BIT_closeCStream() */ +MEM_STATIC void BIT_flushBits(BIT_CStream_t* bitC) +{ + size_t const nbBytes = bitC->bitPos >> 3; + assert(bitC->bitPos < sizeof(bitC->bitContainer) * 8); + assert(bitC->ptr <= bitC->endPtr); + MEM_writeLEST(bitC->ptr, bitC->bitContainer); + bitC->ptr += nbBytes; + if (bitC->ptr > bitC->endPtr) bitC->ptr = bitC->endPtr; + bitC->bitPos &= 7; + bitC->bitContainer >>= nbBytes*8; +} + +/*! BIT_closeCStream() : + * @return : size of CStream, in bytes, + * or 0 if it could not fit into dstBuffer */ +MEM_STATIC size_t BIT_closeCStream(BIT_CStream_t* bitC) +{ + BIT_addBitsFast(bitC, 1, 1); /* endMark */ + BIT_flushBits(bitC); + if (bitC->ptr >= bitC->endPtr) return 0; /* overflow detected */ + return (bitC->ptr - bitC->startPtr) + (bitC->bitPos > 0); +} + + +/*-******************************************************** +* bitStream decoding +**********************************************************/ +/*! BIT_initDStream() : + * Initialize a BIT_DStream_t. + * `bitD` : a pointer to an already allocated BIT_DStream_t structure. + * `srcSize` must be the *exact* size of the bitStream, in bytes. + * @return : size of stream (== srcSize), or an errorCode if a problem is detected + */ +MEM_STATIC size_t BIT_initDStream(BIT_DStream_t* bitD, const void* srcBuffer, size_t srcSize) +{ + if (srcSize < 1) { ZSTD_memset(bitD, 0, sizeof(*bitD)); return ERROR(srcSize_wrong); } + + bitD->start = (const char*)srcBuffer; + bitD->limitPtr = bitD->start + sizeof(bitD->bitContainer); + + if (srcSize >= sizeof(bitD->bitContainer)) { /* normal case */ + bitD->ptr = (const char*)srcBuffer + srcSize - sizeof(bitD->bitContainer); + bitD->bitContainer = MEM_readLEST(bitD->ptr); + { BYTE const lastByte = ((const BYTE*)srcBuffer)[srcSize-1]; + bitD->bitsConsumed = lastByte ? 8 - ZSTD_highbit32(lastByte) : 0; /* ensures bitsConsumed is always set */ + if (lastByte == 0) return ERROR(GENERIC); /* endMark not present */ } + } else { + bitD->ptr = bitD->start; + bitD->bitContainer = *(const BYTE*)(bitD->start); + switch(srcSize) + { + case 7: bitD->bitContainer += (BitContainerType)(((const BYTE*)(srcBuffer))[6]) << (sizeof(bitD->bitContainer)*8 - 16); + ZSTD_FALLTHROUGH; + + case 6: bitD->bitContainer += (BitContainerType)(((const BYTE*)(srcBuffer))[5]) << (sizeof(bitD->bitContainer)*8 - 24); + ZSTD_FALLTHROUGH; + + case 5: bitD->bitContainer += (BitContainerType)(((const BYTE*)(srcBuffer))[4]) << (sizeof(bitD->bitContainer)*8 - 32); + ZSTD_FALLTHROUGH; + + case 4: bitD->bitContainer += (BitContainerType)(((const BYTE*)(srcBuffer))[3]) << 24; + ZSTD_FALLTHROUGH; + + case 3: bitD->bitContainer += (BitContainerType)(((const BYTE*)(srcBuffer))[2]) << 16; + ZSTD_FALLTHROUGH; + + case 2: bitD->bitContainer += (BitContainerType)(((const BYTE*)(srcBuffer))[1]) << 8; + ZSTD_FALLTHROUGH; + + default: break; + } + { BYTE const lastByte = ((const BYTE*)srcBuffer)[srcSize-1]; + bitD->bitsConsumed = lastByte ? 8 - ZSTD_highbit32(lastByte) : 0; + if (lastByte == 0) return ERROR(corruption_detected); /* endMark not present */ + } + bitD->bitsConsumed += (U32)(sizeof(bitD->bitContainer) - srcSize)*8; + } + + return srcSize; +} + +FORCE_INLINE_TEMPLATE size_t BIT_getUpperBits(BitContainerType bitContainer, U32 const start) +{ + return bitContainer >> start; +} + +FORCE_INLINE_TEMPLATE size_t BIT_getMiddleBits(BitContainerType bitContainer, U32 const start, U32 const nbBits) +{ + U32 const regMask = sizeof(bitContainer)*8 - 1; + /* if start > regMask, bitstream is corrupted, and result is undefined */ + assert(nbBits < BIT_MASK_SIZE); + /* x86 transform & ((1 << nbBits) - 1) to bzhi instruction, it is better + * than accessing memory. When bmi2 instruction is not present, we consider + * such cpus old (pre-Haswell, 2013) and their performance is not of that + * importance. + */ +#if defined(__x86_64__) || defined(_M_X86) + return (bitContainer >> (start & regMask)) & ((((U64)1) << nbBits) - 1); +#else + return (bitContainer >> (start & regMask)) & BIT_mask[nbBits]; +#endif +} + +/*! BIT_lookBits() : + * Provides next n bits from local register. + * local register is not modified. + * On 32-bits, maxNbBits==24. + * On 64-bits, maxNbBits==56. + * @return : value extracted */ +FORCE_INLINE_TEMPLATE size_t BIT_lookBits(const BIT_DStream_t* bitD, U32 nbBits) +{ + /* arbitrate between double-shift and shift+mask */ +#if 1 + /* if bitD->bitsConsumed + nbBits > sizeof(bitD->bitContainer)*8, + * bitstream is likely corrupted, and result is undefined */ + return BIT_getMiddleBits(bitD->bitContainer, (sizeof(bitD->bitContainer)*8) - bitD->bitsConsumed - nbBits, nbBits); +#else + /* this code path is slower on my os-x laptop */ + U32 const regMask = sizeof(bitD->bitContainer)*8 - 1; + return ((bitD->bitContainer << (bitD->bitsConsumed & regMask)) >> 1) >> ((regMask-nbBits) & regMask); +#endif +} + +/*! BIT_lookBitsFast() : + * unsafe version; only works if nbBits >= 1 */ +MEM_STATIC size_t BIT_lookBitsFast(const BIT_DStream_t* bitD, U32 nbBits) +{ + U32 const regMask = sizeof(bitD->bitContainer)*8 - 1; + assert(nbBits >= 1); + return (bitD->bitContainer << (bitD->bitsConsumed & regMask)) >> (((regMask+1)-nbBits) & regMask); +} + +FORCE_INLINE_TEMPLATE void BIT_skipBits(BIT_DStream_t* bitD, U32 nbBits) +{ + bitD->bitsConsumed += nbBits; +} + +/*! BIT_readBits() : + * Read (consume) next n bits from local register and update. + * Pay attention to not read more than nbBits contained into local register. + * @return : extracted value. */ +FORCE_INLINE_TEMPLATE size_t BIT_readBits(BIT_DStream_t* bitD, unsigned nbBits) +{ + size_t const value = BIT_lookBits(bitD, nbBits); + BIT_skipBits(bitD, nbBits); + return value; +} + +/*! BIT_readBitsFast() : + * unsafe version; only works if nbBits >= 1 */ +MEM_STATIC size_t BIT_readBitsFast(BIT_DStream_t* bitD, unsigned nbBits) +{ + size_t const value = BIT_lookBitsFast(bitD, nbBits); + assert(nbBits >= 1); + BIT_skipBits(bitD, nbBits); + return value; +} + +/*! BIT_reloadDStream_internal() : + * Simple variant of BIT_reloadDStream(), with two conditions: + * 1. bitstream is valid : bitsConsumed <= sizeof(bitD->bitContainer)*8 + * 2. look window is valid after shifted down : bitD->ptr >= bitD->start + */ +MEM_STATIC BIT_DStream_status BIT_reloadDStream_internal(BIT_DStream_t* bitD) +{ + assert(bitD->bitsConsumed <= sizeof(bitD->bitContainer)*8); + bitD->ptr -= bitD->bitsConsumed >> 3; + assert(bitD->ptr >= bitD->start); + bitD->bitsConsumed &= 7; + bitD->bitContainer = MEM_readLEST(bitD->ptr); + return BIT_DStream_unfinished; +} + +/*! BIT_reloadDStreamFast() : + * Similar to BIT_reloadDStream(), but with two differences: + * 1. bitsConsumed <= sizeof(bitD->bitContainer)*8 must hold! + * 2. Returns BIT_DStream_overflow when bitD->ptr < bitD->limitPtr, at this + * point you must use BIT_reloadDStream() to reload. + */ +MEM_STATIC BIT_DStream_status BIT_reloadDStreamFast(BIT_DStream_t* bitD) +{ + if (UNLIKELY(bitD->ptr < bitD->limitPtr)) + return BIT_DStream_overflow; + return BIT_reloadDStream_internal(bitD); +} + +/*! BIT_reloadDStream() : + * Refill `bitD` from buffer previously set in BIT_initDStream() . + * This function is safe, it guarantees it will not never beyond src buffer. + * @return : status of `BIT_DStream_t` internal register. + * when status == BIT_DStream_unfinished, internal register is filled with at least 25 or 57 bits */ +FORCE_INLINE_TEMPLATE BIT_DStream_status BIT_reloadDStream(BIT_DStream_t* bitD) +{ + /* note : once in overflow mode, a bitstream remains in this mode until it's reset */ + if (UNLIKELY(bitD->bitsConsumed > (sizeof(bitD->bitContainer)*8))) { + static const BitContainerType zeroFilled = 0; + bitD->ptr = (const char*)&zeroFilled; /* aliasing is allowed for char */ + /* overflow detected, erroneous scenario or end of stream: no update */ + return BIT_DStream_overflow; + } + + assert(bitD->ptr >= bitD->start); + + if (bitD->ptr >= bitD->limitPtr) { + return BIT_reloadDStream_internal(bitD); + } + if (bitD->ptr == bitD->start) { + /* reached end of bitStream => no update */ + if (bitD->bitsConsumed < sizeof(bitD->bitContainer)*8) return BIT_DStream_endOfBuffer; + return BIT_DStream_completed; + } + /* start < ptr < limitPtr => cautious update */ + { U32 nbBytes = bitD->bitsConsumed >> 3; + BIT_DStream_status result = BIT_DStream_unfinished; + if (bitD->ptr - nbBytes < bitD->start) { + nbBytes = (U32)(bitD->ptr - bitD->start); /* ptr > start */ + result = BIT_DStream_endOfBuffer; + } + bitD->ptr -= nbBytes; + bitD->bitsConsumed -= nbBytes*8; + bitD->bitContainer = MEM_readLEST(bitD->ptr); /* reminder : srcSize > sizeof(bitD->bitContainer), otherwise bitD->ptr == bitD->start */ + return result; + } +} + +/*! BIT_endOfDStream() : + * @return : 1 if DStream has _exactly_ reached its end (all bits consumed). + */ +MEM_STATIC unsigned BIT_endOfDStream(const BIT_DStream_t* DStream) +{ + return ((DStream->ptr == DStream->start) && (DStream->bitsConsumed == sizeof(DStream->bitContainer)*8)); +} + +#if defined (__cplusplus) +} +#endif + +#endif /* BITSTREAM_H_MODULE */ +/**** ended inlining bitstream.h ****/ + + +/* ***************************************** +* Static allocation +*******************************************/ +/* FSE buffer bounds */ +#define FSE_NCOUNTBOUND 512 +#define FSE_BLOCKBOUND(size) ((size) + ((size)>>7) + 4 /* fse states */ + sizeof(size_t) /* bitContainer */) +#define FSE_COMPRESSBOUND(size) (FSE_NCOUNTBOUND + FSE_BLOCKBOUND(size)) /* Macro version, useful for static allocation */ + +/* It is possible to statically allocate FSE CTable/DTable as a table of FSE_CTable/FSE_DTable using below macros */ +#define FSE_CTABLE_SIZE_U32(maxTableLog, maxSymbolValue) (1 + (1<<((maxTableLog)-1)) + (((maxSymbolValue)+1)*2)) +#define FSE_DTABLE_SIZE_U32(maxTableLog) (1 + (1<<(maxTableLog))) + +/* or use the size to malloc() space directly. Pay attention to alignment restrictions though */ +#define FSE_CTABLE_SIZE(maxTableLog, maxSymbolValue) (FSE_CTABLE_SIZE_U32(maxTableLog, maxSymbolValue) * sizeof(FSE_CTable)) +#define FSE_DTABLE_SIZE(maxTableLog) (FSE_DTABLE_SIZE_U32(maxTableLog) * sizeof(FSE_DTable)) + + +/* ***************************************** + * FSE advanced API + ***************************************** */ + +unsigned FSE_optimalTableLog_internal(unsigned maxTableLog, size_t srcSize, unsigned maxSymbolValue, unsigned minus); +/**< same as FSE_optimalTableLog(), which used `minus==2` */ + +size_t FSE_buildCTable_rle (FSE_CTable* ct, unsigned char symbolValue); +/**< build a fake FSE_CTable, designed to compress always the same symbolValue */ + +/* FSE_buildCTable_wksp() : + * Same as FSE_buildCTable(), but using an externally allocated scratch buffer (`workSpace`). + * `wkspSize` must be >= `FSE_BUILD_CTABLE_WORKSPACE_SIZE_U32(maxSymbolValue, tableLog)` of `unsigned`. + * See FSE_buildCTable_wksp() for breakdown of workspace usage. + */ +#define FSE_BUILD_CTABLE_WORKSPACE_SIZE_U32(maxSymbolValue, tableLog) (((maxSymbolValue + 2) + (1ull << (tableLog)))/2 + sizeof(U64)/sizeof(U32) /* additional 8 bytes for potential table overwrite */) +#define FSE_BUILD_CTABLE_WORKSPACE_SIZE(maxSymbolValue, tableLog) (sizeof(unsigned) * FSE_BUILD_CTABLE_WORKSPACE_SIZE_U32(maxSymbolValue, tableLog)) +size_t FSE_buildCTable_wksp(FSE_CTable* ct, const short* normalizedCounter, unsigned maxSymbolValue, unsigned tableLog, void* workSpace, size_t wkspSize); + +#define FSE_BUILD_DTABLE_WKSP_SIZE(maxTableLog, maxSymbolValue) (sizeof(short) * (maxSymbolValue + 1) + (1ULL << maxTableLog) + 8) +#define FSE_BUILD_DTABLE_WKSP_SIZE_U32(maxTableLog, maxSymbolValue) ((FSE_BUILD_DTABLE_WKSP_SIZE(maxTableLog, maxSymbolValue) + sizeof(unsigned) - 1) / sizeof(unsigned)) +FSE_PUBLIC_API size_t FSE_buildDTable_wksp(FSE_DTable* dt, const short* normalizedCounter, unsigned maxSymbolValue, unsigned tableLog, void* workSpace, size_t wkspSize); +/**< Same as FSE_buildDTable(), using an externally allocated `workspace` produced with `FSE_BUILD_DTABLE_WKSP_SIZE_U32(maxSymbolValue)` */ + +#define FSE_DECOMPRESS_WKSP_SIZE_U32(maxTableLog, maxSymbolValue) (FSE_DTABLE_SIZE_U32(maxTableLog) + 1 + FSE_BUILD_DTABLE_WKSP_SIZE_U32(maxTableLog, maxSymbolValue) + (FSE_MAX_SYMBOL_VALUE + 1) / 2 + 1) +#define FSE_DECOMPRESS_WKSP_SIZE(maxTableLog, maxSymbolValue) (FSE_DECOMPRESS_WKSP_SIZE_U32(maxTableLog, maxSymbolValue) * sizeof(unsigned)) +size_t FSE_decompress_wksp_bmi2(void* dst, size_t dstCapacity, const void* cSrc, size_t cSrcSize, unsigned maxLog, void* workSpace, size_t wkspSize, int bmi2); +/**< same as FSE_decompress(), using an externally allocated `workSpace` produced with `FSE_DECOMPRESS_WKSP_SIZE_U32(maxLog, maxSymbolValue)`. + * Set bmi2 to 1 if your CPU supports BMI2 or 0 if it doesn't */ + +typedef enum { + FSE_repeat_none, /**< Cannot use the previous table */ + FSE_repeat_check, /**< Can use the previous table but it must be checked */ + FSE_repeat_valid /**< Can use the previous table and it is assumed to be valid */ + } FSE_repeat; + +/* ***************************************** +* FSE symbol compression API +*******************************************/ +/*! + This API consists of small unitary functions, which highly benefit from being inlined. + Hence their body are included in next section. +*/ +typedef struct { + ptrdiff_t value; + const void* stateTable; + const void* symbolTT; + unsigned stateLog; +} FSE_CState_t; + +static void FSE_initCState(FSE_CState_t* CStatePtr, const FSE_CTable* ct); + +static void FSE_encodeSymbol(BIT_CStream_t* bitC, FSE_CState_t* CStatePtr, unsigned symbol); + +static void FSE_flushCState(BIT_CStream_t* bitC, const FSE_CState_t* CStatePtr); + +/**< +These functions are inner components of FSE_compress_usingCTable(). +They allow the creation of custom streams, mixing multiple tables and bit sources. + +A key property to keep in mind is that encoding and decoding are done **in reverse direction**. +So the first symbol you will encode is the last you will decode, like a LIFO stack. + +You will need a few variables to track your CStream. They are : + +FSE_CTable ct; // Provided by FSE_buildCTable() +BIT_CStream_t bitStream; // bitStream tracking structure +FSE_CState_t state; // State tracking structure (can have several) + + +The first thing to do is to init bitStream and state. + size_t errorCode = BIT_initCStream(&bitStream, dstBuffer, maxDstSize); + FSE_initCState(&state, ct); + +Note that BIT_initCStream() can produce an error code, so its result should be tested, using FSE_isError(); +You can then encode your input data, byte after byte. +FSE_encodeSymbol() outputs a maximum of 'tableLog' bits at a time. +Remember decoding will be done in reverse direction. + FSE_encodeByte(&bitStream, &state, symbol); + +At any time, you can also add any bit sequence. +Note : maximum allowed nbBits is 25, for compatibility with 32-bits decoders + BIT_addBits(&bitStream, bitField, nbBits); + +The above methods don't commit data to memory, they just store it into local register, for speed. +Local register size is 64-bits on 64-bits systems, 32-bits on 32-bits systems (size_t). +Writing data to memory is a manual operation, performed by the flushBits function. + BIT_flushBits(&bitStream); + +Your last FSE encoding operation shall be to flush your last state value(s). + FSE_flushState(&bitStream, &state); + +Finally, you must close the bitStream. +The function returns the size of CStream in bytes. +If data couldn't fit into dstBuffer, it will return a 0 ( == not compressible) +If there is an error, it returns an errorCode (which can be tested using FSE_isError()). + size_t size = BIT_closeCStream(&bitStream); +*/ + + +/* ***************************************** +* FSE symbol decompression API +*******************************************/ +typedef struct { + size_t state; + const void* table; /* precise table may vary, depending on U16 */ +} FSE_DState_t; + + +static void FSE_initDState(FSE_DState_t* DStatePtr, BIT_DStream_t* bitD, const FSE_DTable* dt); + +static unsigned char FSE_decodeSymbol(FSE_DState_t* DStatePtr, BIT_DStream_t* bitD); + +static unsigned FSE_endOfDState(const FSE_DState_t* DStatePtr); + +/**< +Let's now decompose FSE_decompress_usingDTable() into its unitary components. +You will decode FSE-encoded symbols from the bitStream, +and also any other bitFields you put in, **in reverse order**. + +You will need a few variables to track your bitStream. They are : + +BIT_DStream_t DStream; // Stream context +FSE_DState_t DState; // State context. Multiple ones are possible +FSE_DTable* DTablePtr; // Decoding table, provided by FSE_buildDTable() + +The first thing to do is to init the bitStream. + errorCode = BIT_initDStream(&DStream, srcBuffer, srcSize); + +You should then retrieve your initial state(s) +(in reverse flushing order if you have several ones) : + errorCode = FSE_initDState(&DState, &DStream, DTablePtr); + +You can then decode your data, symbol after symbol. +For information the maximum number of bits read by FSE_decodeSymbol() is 'tableLog'. +Keep in mind that symbols are decoded in reverse order, like a LIFO stack (last in, first out). + unsigned char symbol = FSE_decodeSymbol(&DState, &DStream); + +You can retrieve any bitfield you eventually stored into the bitStream (in reverse order) +Note : maximum allowed nbBits is 25, for 32-bits compatibility + size_t bitField = BIT_readBits(&DStream, nbBits); + +All above operations only read from local register (which size depends on size_t). +Refueling the register from memory is manually performed by the reload method. + endSignal = FSE_reloadDStream(&DStream); + +BIT_reloadDStream() result tells if there is still some more data to read from DStream. +BIT_DStream_unfinished : there is still some data left into the DStream. +BIT_DStream_endOfBuffer : Dstream reached end of buffer. Its container may no longer be completely filled. +BIT_DStream_completed : Dstream reached its exact end, corresponding in general to decompression completed. +BIT_DStream_tooFar : Dstream went too far. Decompression result is corrupted. + +When reaching end of buffer (BIT_DStream_endOfBuffer), progress slowly, notably if you decode multiple symbols per loop, +to properly detect the exact end of stream. +After each decoded symbol, check if DStream is fully consumed using this simple test : + BIT_reloadDStream(&DStream) >= BIT_DStream_completed + +When it's done, verify decompression is fully completed, by checking both DStream and the relevant states. +Checking if DStream has reached its end is performed by : + BIT_endOfDStream(&DStream); +Check also the states. There might be some symbols left there, if some high probability ones (>50%) are possible. + FSE_endOfDState(&DState); +*/ + + +/* ***************************************** +* FSE unsafe API +*******************************************/ +static unsigned char FSE_decodeSymbolFast(FSE_DState_t* DStatePtr, BIT_DStream_t* bitD); +/* faster, but works only if nbBits is always >= 1 (otherwise, result will be corrupted) */ + + +/* ***************************************** +* Implementation of inlined functions +*******************************************/ +typedef struct { + int deltaFindState; + U32 deltaNbBits; +} FSE_symbolCompressionTransform; /* total 8 bytes */ + +MEM_STATIC void FSE_initCState(FSE_CState_t* statePtr, const FSE_CTable* ct) +{ + const void* ptr = ct; + const U16* u16ptr = (const U16*) ptr; + const U32 tableLog = MEM_read16(ptr); + statePtr->value = (ptrdiff_t)1<stateTable = u16ptr+2; + statePtr->symbolTT = ct + 1 + (tableLog ? (1<<(tableLog-1)) : 1); + statePtr->stateLog = tableLog; +} + + +/*! FSE_initCState2() : +* Same as FSE_initCState(), but the first symbol to include (which will be the last to be read) +* uses the smallest state value possible, saving the cost of this symbol */ +MEM_STATIC void FSE_initCState2(FSE_CState_t* statePtr, const FSE_CTable* ct, U32 symbol) +{ + FSE_initCState(statePtr, ct); + { const FSE_symbolCompressionTransform symbolTT = ((const FSE_symbolCompressionTransform*)(statePtr->symbolTT))[symbol]; + const U16* stateTable = (const U16*)(statePtr->stateTable); + U32 nbBitsOut = (U32)((symbolTT.deltaNbBits + (1<<15)) >> 16); + statePtr->value = (nbBitsOut << 16) - symbolTT.deltaNbBits; + statePtr->value = stateTable[(statePtr->value >> nbBitsOut) + symbolTT.deltaFindState]; + } +} + +MEM_STATIC void FSE_encodeSymbol(BIT_CStream_t* bitC, FSE_CState_t* statePtr, unsigned symbol) +{ + FSE_symbolCompressionTransform const symbolTT = ((const FSE_symbolCompressionTransform*)(statePtr->symbolTT))[symbol]; + const U16* const stateTable = (const U16*)(statePtr->stateTable); + U32 const nbBitsOut = (U32)((statePtr->value + symbolTT.deltaNbBits) >> 16); + BIT_addBits(bitC, (size_t)statePtr->value, nbBitsOut); + statePtr->value = stateTable[ (statePtr->value >> nbBitsOut) + symbolTT.deltaFindState]; +} + +MEM_STATIC void FSE_flushCState(BIT_CStream_t* bitC, const FSE_CState_t* statePtr) +{ + BIT_addBits(bitC, (size_t)statePtr->value, statePtr->stateLog); + BIT_flushBits(bitC); +} + + +/* FSE_getMaxNbBits() : + * Approximate maximum cost of a symbol, in bits. + * Fractional get rounded up (i.e. a symbol with a normalized frequency of 3 gives the same result as a frequency of 2) + * note 1 : assume symbolValue is valid (<= maxSymbolValue) + * note 2 : if freq[symbolValue]==0, @return a fake cost of tableLog+1 bits */ +MEM_STATIC U32 FSE_getMaxNbBits(const void* symbolTTPtr, U32 symbolValue) +{ + const FSE_symbolCompressionTransform* symbolTT = (const FSE_symbolCompressionTransform*) symbolTTPtr; + return (symbolTT[symbolValue].deltaNbBits + ((1<<16)-1)) >> 16; +} + +/* FSE_bitCost() : + * Approximate symbol cost, as fractional value, using fixed-point format (accuracyLog fractional bits) + * note 1 : assume symbolValue is valid (<= maxSymbolValue) + * note 2 : if freq[symbolValue]==0, @return a fake cost of tableLog+1 bits */ +MEM_STATIC U32 FSE_bitCost(const void* symbolTTPtr, U32 tableLog, U32 symbolValue, U32 accuracyLog) +{ + const FSE_symbolCompressionTransform* symbolTT = (const FSE_symbolCompressionTransform*) symbolTTPtr; + U32 const minNbBits = symbolTT[symbolValue].deltaNbBits >> 16; + U32 const threshold = (minNbBits+1) << 16; + assert(tableLog < 16); + assert(accuracyLog < 31-tableLog); /* ensure enough room for renormalization double shift */ + { U32 const tableSize = 1 << tableLog; + U32 const deltaFromThreshold = threshold - (symbolTT[symbolValue].deltaNbBits + tableSize); + U32 const normalizedDeltaFromThreshold = (deltaFromThreshold << accuracyLog) >> tableLog; /* linear interpolation (very approximate) */ + U32 const bitMultiplier = 1 << accuracyLog; + assert(symbolTT[symbolValue].deltaNbBits + tableSize <= threshold); + assert(normalizedDeltaFromThreshold <= bitMultiplier); + return (minNbBits+1)*bitMultiplier - normalizedDeltaFromThreshold; + } +} + + +/* ====== Decompression ====== */ + +typedef struct { + U16 tableLog; + U16 fastMode; +} FSE_DTableHeader; /* sizeof U32 */ + +typedef struct +{ + unsigned short newState; + unsigned char symbol; + unsigned char nbBits; +} FSE_decode_t; /* size == U32 */ + +MEM_STATIC void FSE_initDState(FSE_DState_t* DStatePtr, BIT_DStream_t* bitD, const FSE_DTable* dt) +{ + const void* ptr = dt; + const FSE_DTableHeader* const DTableH = (const FSE_DTableHeader*)ptr; + DStatePtr->state = BIT_readBits(bitD, DTableH->tableLog); + BIT_reloadDStream(bitD); + DStatePtr->table = dt + 1; +} + +MEM_STATIC BYTE FSE_peekSymbol(const FSE_DState_t* DStatePtr) +{ + FSE_decode_t const DInfo = ((const FSE_decode_t*)(DStatePtr->table))[DStatePtr->state]; + return DInfo.symbol; +} + +MEM_STATIC void FSE_updateState(FSE_DState_t* DStatePtr, BIT_DStream_t* bitD) +{ + FSE_decode_t const DInfo = ((const FSE_decode_t*)(DStatePtr->table))[DStatePtr->state]; + U32 const nbBits = DInfo.nbBits; + size_t const lowBits = BIT_readBits(bitD, nbBits); + DStatePtr->state = DInfo.newState + lowBits; +} + +MEM_STATIC BYTE FSE_decodeSymbol(FSE_DState_t* DStatePtr, BIT_DStream_t* bitD) +{ + FSE_decode_t const DInfo = ((const FSE_decode_t*)(DStatePtr->table))[DStatePtr->state]; + U32 const nbBits = DInfo.nbBits; + BYTE const symbol = DInfo.symbol; + size_t const lowBits = BIT_readBits(bitD, nbBits); + + DStatePtr->state = DInfo.newState + lowBits; + return symbol; +} + +/*! FSE_decodeSymbolFast() : + unsafe, only works if no symbol has a probability > 50% */ +MEM_STATIC BYTE FSE_decodeSymbolFast(FSE_DState_t* DStatePtr, BIT_DStream_t* bitD) +{ + FSE_decode_t const DInfo = ((const FSE_decode_t*)(DStatePtr->table))[DStatePtr->state]; + U32 const nbBits = DInfo.nbBits; + BYTE const symbol = DInfo.symbol; + size_t const lowBits = BIT_readBitsFast(bitD, nbBits); + + DStatePtr->state = DInfo.newState + lowBits; + return symbol; +} + +MEM_STATIC unsigned FSE_endOfDState(const FSE_DState_t* DStatePtr) +{ + return DStatePtr->state == 0; +} + + + +#ifndef FSE_COMMONDEFS_ONLY + +/* ************************************************************** +* Tuning parameters +****************************************************************/ +/*!MEMORY_USAGE : +* Memory usage formula : N->2^N Bytes (examples : 10 -> 1KB; 12 -> 4KB ; 16 -> 64KB; 20 -> 1MB; etc.) +* Increasing memory usage improves compression ratio +* Reduced memory usage can improve speed, due to cache effect +* Recommended max value is 14, for 16KB, which nicely fits into Intel x86 L1 cache */ +#ifndef FSE_MAX_MEMORY_USAGE +# define FSE_MAX_MEMORY_USAGE 14 +#endif +#ifndef FSE_DEFAULT_MEMORY_USAGE +# define FSE_DEFAULT_MEMORY_USAGE 13 +#endif +#if (FSE_DEFAULT_MEMORY_USAGE > FSE_MAX_MEMORY_USAGE) +# error "FSE_DEFAULT_MEMORY_USAGE must be <= FSE_MAX_MEMORY_USAGE" +#endif + +/*!FSE_MAX_SYMBOL_VALUE : +* Maximum symbol value authorized. +* Required for proper stack allocation */ +#ifndef FSE_MAX_SYMBOL_VALUE +# define FSE_MAX_SYMBOL_VALUE 255 +#endif + +/* ************************************************************** +* template functions type & suffix +****************************************************************/ +#define FSE_FUNCTION_TYPE BYTE +#define FSE_FUNCTION_EXTENSION +#define FSE_DECODE_TYPE FSE_decode_t + + +#endif /* !FSE_COMMONDEFS_ONLY */ + + +/* *************************************************************** +* Constants +*****************************************************************/ +#define FSE_MAX_TABLELOG (FSE_MAX_MEMORY_USAGE-2) +#define FSE_MAX_TABLESIZE (1U< FSE_TABLELOG_ABSOLUTE_MAX +# error "FSE_MAX_TABLELOG > FSE_TABLELOG_ABSOLUTE_MAX is not supported" +#endif + +#define FSE_TABLESTEP(tableSize) (((tableSize)>>1) + ((tableSize)>>3) + 3) + + +#endif /* FSE_STATIC_LINKING_ONLY */ + + +#if defined (__cplusplus) +} +#endif +/**** ended inlining fse.h ****/ +/**** start inlining huf.h ****/ +/* ****************************************************************** + * huff0 huffman codec, + * part of Finite State Entropy library + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - Source repository : https://github.com/Cyan4973/FiniteStateEntropy + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + +#if defined (__cplusplus) +extern "C" { +#endif + +#ifndef HUF_H_298734234 +#define HUF_H_298734234 + +/* *** Dependencies *** */ +/**** skipping file: zstd_deps.h ****/ +/**** skipping file: mem.h ****/ +#define FSE_STATIC_LINKING_ONLY +/**** skipping file: fse.h ****/ + + +/* *** Tool functions *** */ +#define HUF_BLOCKSIZE_MAX (128 * 1024) /**< maximum input size for a single block compressed with HUF_compress */ +size_t HUF_compressBound(size_t size); /**< maximum compressed size (worst case) */ + +/* Error Management */ +unsigned HUF_isError(size_t code); /**< tells if a return value is an error code */ +const char* HUF_getErrorName(size_t code); /**< provides error code string (useful for debugging) */ + + +#define HUF_WORKSPACE_SIZE ((8 << 10) + 512 /* sorting scratch space */) +#define HUF_WORKSPACE_SIZE_U64 (HUF_WORKSPACE_SIZE / sizeof(U64)) + +/* *** Constants *** */ +#define HUF_TABLELOG_MAX 12 /* max runtime value of tableLog (due to static allocation); can be modified up to HUF_TABLELOG_ABSOLUTEMAX */ +#define HUF_TABLELOG_DEFAULT 11 /* default tableLog value when none specified */ +#define HUF_SYMBOLVALUE_MAX 255 + +#define HUF_TABLELOG_ABSOLUTEMAX 12 /* absolute limit of HUF_MAX_TABLELOG. Beyond that value, code does not work */ +#if (HUF_TABLELOG_MAX > HUF_TABLELOG_ABSOLUTEMAX) +# error "HUF_TABLELOG_MAX is too large !" +#endif + + +/* **************************************** +* Static allocation +******************************************/ +/* HUF buffer bounds */ +#define HUF_CTABLEBOUND 129 +#define HUF_BLOCKBOUND(size) (size + (size>>8) + 8) /* only true when incompressible is pre-filtered with fast heuristic */ +#define HUF_COMPRESSBOUND(size) (HUF_CTABLEBOUND + HUF_BLOCKBOUND(size)) /* Macro version, useful for static allocation */ + +/* static allocation of HUF's Compression Table */ +/* this is a private definition, just exposed for allocation and strict aliasing purpose. never EVER access its members directly */ +typedef size_t HUF_CElt; /* consider it an incomplete type */ +#define HUF_CTABLE_SIZE_ST(maxSymbolValue) ((maxSymbolValue)+2) /* Use tables of size_t, for proper alignment */ +#define HUF_CTABLE_SIZE(maxSymbolValue) (HUF_CTABLE_SIZE_ST(maxSymbolValue) * sizeof(size_t)) +#define HUF_CREATE_STATIC_CTABLE(name, maxSymbolValue) \ + HUF_CElt name[HUF_CTABLE_SIZE_ST(maxSymbolValue)] /* no final ; */ + +/* static allocation of HUF's DTable */ +typedef U32 HUF_DTable; +#define HUF_DTABLE_SIZE(maxTableLog) (1 + (1<<(maxTableLog))) +#define HUF_CREATE_STATIC_DTABLEX1(DTable, maxTableLog) \ + HUF_DTable DTable[HUF_DTABLE_SIZE((maxTableLog)-1)] = { ((U32)((maxTableLog)-1) * 0x01000001) } +#define HUF_CREATE_STATIC_DTABLEX2(DTable, maxTableLog) \ + HUF_DTable DTable[HUF_DTABLE_SIZE(maxTableLog)] = { ((U32)(maxTableLog) * 0x01000001) } + + +/* **************************************** +* Advanced decompression functions +******************************************/ + +/** + * Huffman flags bitset. + * For all flags, 0 is the default value. + */ +typedef enum { + /** + * If compiled with DYNAMIC_BMI2: Set flag only if the CPU supports BMI2 at runtime. + * Otherwise: Ignored. + */ + HUF_flags_bmi2 = (1 << 0), + /** + * If set: Test possible table depths to find the one that produces the smallest header + encoded size. + * If unset: Use heuristic to find the table depth. + */ + HUF_flags_optimalDepth = (1 << 1), + /** + * If set: If the previous table can encode the input, always reuse the previous table. + * If unset: If the previous table can encode the input, reuse the previous table if it results in a smaller output. + */ + HUF_flags_preferRepeat = (1 << 2), + /** + * If set: Sample the input and check if the sample is uncompressible, if it is then don't attempt to compress. + * If unset: Always histogram the entire input. + */ + HUF_flags_suspectUncompressible = (1 << 3), + /** + * If set: Don't use assembly implementations + * If unset: Allow using assembly implementations + */ + HUF_flags_disableAsm = (1 << 4), + /** + * If set: Don't use the fast decoding loop, always use the fallback decoding loop. + * If unset: Use the fast decoding loop when possible. + */ + HUF_flags_disableFast = (1 << 5) +} HUF_flags_e; + + +/* **************************************** + * HUF detailed API + * ****************************************/ +#define HUF_OPTIMAL_DEPTH_THRESHOLD ZSTD_btultra + +/*! HUF_compress() does the following: + * 1. count symbol occurrence from source[] into table count[] using FSE_count() (exposed within "fse.h") + * 2. (optional) refine tableLog using HUF_optimalTableLog() + * 3. build Huffman table from count using HUF_buildCTable() + * 4. save Huffman table to memory buffer using HUF_writeCTable() + * 5. encode the data stream using HUF_compress4X_usingCTable() + * + * The following API allows targeting specific sub-functions for advanced tasks. + * For example, it's possible to compress several blocks using the same 'CTable', + * or to save and regenerate 'CTable' using external methods. + */ +unsigned HUF_minTableLog(unsigned symbolCardinality); +unsigned HUF_cardinality(const unsigned* count, unsigned maxSymbolValue); +unsigned HUF_optimalTableLog(unsigned maxTableLog, size_t srcSize, unsigned maxSymbolValue, void* workSpace, + size_t wkspSize, HUF_CElt* table, const unsigned* count, int flags); /* table is used as scratch space for building and testing tables, not a return value */ +size_t HUF_writeCTable_wksp(void* dst, size_t maxDstSize, const HUF_CElt* CTable, unsigned maxSymbolValue, unsigned huffLog, void* workspace, size_t workspaceSize); +size_t HUF_compress4X_usingCTable(void* dst, size_t dstSize, const void* src, size_t srcSize, const HUF_CElt* CTable, int flags); +size_t HUF_estimateCompressedSize(const HUF_CElt* CTable, const unsigned* count, unsigned maxSymbolValue); +int HUF_validateCTable(const HUF_CElt* CTable, const unsigned* count, unsigned maxSymbolValue); + +typedef enum { + HUF_repeat_none, /**< Cannot use the previous table */ + HUF_repeat_check, /**< Can use the previous table but it must be checked. Note : The previous table must have been constructed by HUF_compress{1, 4}X_repeat */ + HUF_repeat_valid /**< Can use the previous table and it is assumed to be valid */ + } HUF_repeat; + +/** HUF_compress4X_repeat() : + * Same as HUF_compress4X_wksp(), but considers using hufTable if *repeat != HUF_repeat_none. + * If it uses hufTable it does not modify hufTable or repeat. + * If it doesn't, it sets *repeat = HUF_repeat_none, and it sets hufTable to the table used. + * If preferRepeat then the old table will always be used if valid. + * If suspectUncompressible then some sampling checks will be run to potentially skip huffman coding */ +size_t HUF_compress4X_repeat(void* dst, size_t dstSize, + const void* src, size_t srcSize, + unsigned maxSymbolValue, unsigned tableLog, + void* workSpace, size_t wkspSize, /**< `workSpace` must be aligned on 4-bytes boundaries, `wkspSize` must be >= HUF_WORKSPACE_SIZE */ + HUF_CElt* hufTable, HUF_repeat* repeat, int flags); + +/** HUF_buildCTable_wksp() : + * Same as HUF_buildCTable(), but using externally allocated scratch buffer. + * `workSpace` must be aligned on 4-bytes boundaries, and its size must be >= HUF_CTABLE_WORKSPACE_SIZE. + */ +#define HUF_CTABLE_WORKSPACE_SIZE_U32 ((4 * (HUF_SYMBOLVALUE_MAX + 1)) + 192) +#define HUF_CTABLE_WORKSPACE_SIZE (HUF_CTABLE_WORKSPACE_SIZE_U32 * sizeof(unsigned)) +size_t HUF_buildCTable_wksp (HUF_CElt* tree, + const unsigned* count, U32 maxSymbolValue, U32 maxNbBits, + void* workSpace, size_t wkspSize); + +/*! HUF_readStats() : + * Read compact Huffman tree, saved by HUF_writeCTable(). + * `huffWeight` is destination buffer. + * @return : size read from `src` , or an error Code . + * Note : Needed by HUF_readCTable() and HUF_readDTableXn() . */ +size_t HUF_readStats(BYTE* huffWeight, size_t hwSize, + U32* rankStats, U32* nbSymbolsPtr, U32* tableLogPtr, + const void* src, size_t srcSize); + +/*! HUF_readStats_wksp() : + * Same as HUF_readStats() but takes an external workspace which must be + * 4-byte aligned and its size must be >= HUF_READ_STATS_WORKSPACE_SIZE. + * If the CPU has BMI2 support, pass bmi2=1, otherwise pass bmi2=0. + */ +#define HUF_READ_STATS_WORKSPACE_SIZE_U32 FSE_DECOMPRESS_WKSP_SIZE_U32(6, HUF_TABLELOG_MAX-1) +#define HUF_READ_STATS_WORKSPACE_SIZE (HUF_READ_STATS_WORKSPACE_SIZE_U32 * sizeof(unsigned)) +size_t HUF_readStats_wksp(BYTE* huffWeight, size_t hwSize, + U32* rankStats, U32* nbSymbolsPtr, U32* tableLogPtr, + const void* src, size_t srcSize, + void* workspace, size_t wkspSize, + int flags); + +/** HUF_readCTable() : + * Loading a CTable saved with HUF_writeCTable() */ +size_t HUF_readCTable (HUF_CElt* CTable, unsigned* maxSymbolValuePtr, const void* src, size_t srcSize, unsigned *hasZeroWeights); + +/** HUF_getNbBitsFromCTable() : + * Read nbBits from CTable symbolTable, for symbol `symbolValue` presumed <= HUF_SYMBOLVALUE_MAX + * Note 1 : If symbolValue > HUF_readCTableHeader(symbolTable).maxSymbolValue, returns 0 + * Note 2 : is not inlined, as HUF_CElt definition is private + */ +U32 HUF_getNbBitsFromCTable(const HUF_CElt* symbolTable, U32 symbolValue); + +typedef struct { + BYTE tableLog; + BYTE maxSymbolValue; + BYTE unused[sizeof(size_t) - 2]; +} HUF_CTableHeader; + +/** HUF_readCTableHeader() : + * @returns The header from the CTable specifying the tableLog and the maxSymbolValue. + */ +HUF_CTableHeader HUF_readCTableHeader(HUF_CElt const* ctable); + +/* + * HUF_decompress() does the following: + * 1. select the decompression algorithm (X1, X2) based on pre-computed heuristics + * 2. build Huffman table from save, using HUF_readDTableX?() + * 3. decode 1 or 4 segments in parallel using HUF_decompress?X?_usingDTable() + */ + +/** HUF_selectDecoder() : + * Tells which decoder is likely to decode faster, + * based on a set of pre-computed metrics. + * @return : 0==HUF_decompress4X1, 1==HUF_decompress4X2 . + * Assumption : 0 < dstSize <= 128 KB */ +U32 HUF_selectDecoder (size_t dstSize, size_t cSrcSize); + +/** + * The minimum workspace size for the `workSpace` used in + * HUF_readDTableX1_wksp() and HUF_readDTableX2_wksp(). + * + * The space used depends on HUF_TABLELOG_MAX, ranging from ~1500 bytes when + * HUF_TABLE_LOG_MAX=12 to ~1850 bytes when HUF_TABLE_LOG_MAX=15. + * Buffer overflow errors may potentially occur if code modifications result in + * a required workspace size greater than that specified in the following + * macro. + */ +#define HUF_DECOMPRESS_WORKSPACE_SIZE ((2 << 10) + (1 << 9)) +#define HUF_DECOMPRESS_WORKSPACE_SIZE_U32 (HUF_DECOMPRESS_WORKSPACE_SIZE / sizeof(U32)) + + +/* ====================== */ +/* single stream variants */ +/* ====================== */ + +size_t HUF_compress1X_usingCTable(void* dst, size_t dstSize, const void* src, size_t srcSize, const HUF_CElt* CTable, int flags); +/** HUF_compress1X_repeat() : + * Same as HUF_compress1X_wksp(), but considers using hufTable if *repeat != HUF_repeat_none. + * If it uses hufTable it does not modify hufTable or repeat. + * If it doesn't, it sets *repeat = HUF_repeat_none, and it sets hufTable to the table used. + * If preferRepeat then the old table will always be used if valid. + * If suspectUncompressible then some sampling checks will be run to potentially skip huffman coding */ +size_t HUF_compress1X_repeat(void* dst, size_t dstSize, + const void* src, size_t srcSize, + unsigned maxSymbolValue, unsigned tableLog, + void* workSpace, size_t wkspSize, /**< `workSpace` must be aligned on 4-bytes boundaries, `wkspSize` must be >= HUF_WORKSPACE_SIZE */ + HUF_CElt* hufTable, HUF_repeat* repeat, int flags); + +size_t HUF_decompress1X_DCtx_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, const void* cSrc, size_t cSrcSize, void* workSpace, size_t wkspSize, int flags); +#ifndef HUF_FORCE_DECOMPRESS_X1 +size_t HUF_decompress1X2_DCtx_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, const void* cSrc, size_t cSrcSize, void* workSpace, size_t wkspSize, int flags); /**< double-symbols decoder */ +#endif + +/* BMI2 variants. + * If the CPU has BMI2 support, pass bmi2=1, otherwise pass bmi2=0. + */ +size_t HUF_decompress1X_usingDTable(void* dst, size_t maxDstSize, const void* cSrc, size_t cSrcSize, const HUF_DTable* DTable, int flags); +#ifndef HUF_FORCE_DECOMPRESS_X2 +size_t HUF_decompress1X1_DCtx_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, const void* cSrc, size_t cSrcSize, void* workSpace, size_t wkspSize, int flags); +#endif +size_t HUF_decompress4X_usingDTable(void* dst, size_t maxDstSize, const void* cSrc, size_t cSrcSize, const HUF_DTable* DTable, int flags); +size_t HUF_decompress4X_hufOnly_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, const void* cSrc, size_t cSrcSize, void* workSpace, size_t wkspSize, int flags); +#ifndef HUF_FORCE_DECOMPRESS_X2 +size_t HUF_readDTableX1_wksp(HUF_DTable* DTable, const void* src, size_t srcSize, void* workSpace, size_t wkspSize, int flags); +#endif +#ifndef HUF_FORCE_DECOMPRESS_X1 +size_t HUF_readDTableX2_wksp(HUF_DTable* DTable, const void* src, size_t srcSize, void* workSpace, size_t wkspSize, int flags); +#endif + +#endif /* HUF_H_298734234 */ + +#if defined (__cplusplus) +} +#endif +/**** ended inlining huf.h ****/ +/**** skipping file: bits.h ****/ + + +/*=== Version ===*/ +unsigned FSE_versionNumber(void) { return FSE_VERSION_NUMBER; } + + +/*=== Error Management ===*/ +unsigned FSE_isError(size_t code) { return ERR_isError(code); } +const char* FSE_getErrorName(size_t code) { return ERR_getErrorName(code); } + +unsigned HUF_isError(size_t code) { return ERR_isError(code); } +const char* HUF_getErrorName(size_t code) { return ERR_getErrorName(code); } + + +/*-************************************************************** +* FSE NCount encoding-decoding +****************************************************************/ +FORCE_INLINE_TEMPLATE +size_t FSE_readNCount_body(short* normalizedCounter, unsigned* maxSVPtr, unsigned* tableLogPtr, + const void* headerBuffer, size_t hbSize) +{ + const BYTE* const istart = (const BYTE*) headerBuffer; + const BYTE* const iend = istart + hbSize; + const BYTE* ip = istart; + int nbBits; + int remaining; + int threshold; + U32 bitStream; + int bitCount; + unsigned charnum = 0; + unsigned const maxSV1 = *maxSVPtr + 1; + int previous0 = 0; + + if (hbSize < 8) { + /* This function only works when hbSize >= 8 */ + char buffer[8] = {0}; + ZSTD_memcpy(buffer, headerBuffer, hbSize); + { size_t const countSize = FSE_readNCount(normalizedCounter, maxSVPtr, tableLogPtr, + buffer, sizeof(buffer)); + if (FSE_isError(countSize)) return countSize; + if (countSize > hbSize) return ERROR(corruption_detected); + return countSize; + } } + assert(hbSize >= 8); + + /* init */ + ZSTD_memset(normalizedCounter, 0, (*maxSVPtr+1) * sizeof(normalizedCounter[0])); /* all symbols not present in NCount have a frequency of 0 */ + bitStream = MEM_readLE32(ip); + nbBits = (bitStream & 0xF) + FSE_MIN_TABLELOG; /* extract tableLog */ + if (nbBits > FSE_TABLELOG_ABSOLUTE_MAX) return ERROR(tableLog_tooLarge); + bitStream >>= 4; + bitCount = 4; + *tableLogPtr = nbBits; + remaining = (1<> 1; + while (repeats >= 12) { + charnum += 3 * 12; + if (LIKELY(ip <= iend-7)) { + ip += 3; + } else { + bitCount -= (int)(8 * (iend - 7 - ip)); + bitCount &= 31; + ip = iend - 4; + } + bitStream = MEM_readLE32(ip) >> bitCount; + repeats = ZSTD_countTrailingZeros32(~bitStream | 0x80000000) >> 1; + } + charnum += 3 * repeats; + bitStream >>= 2 * repeats; + bitCount += 2 * repeats; + + /* Add the final repeat which isn't 0b11. */ + assert((bitStream & 3) < 3); + charnum += bitStream & 3; + bitCount += 2; + + /* This is an error, but break and return an error + * at the end, because returning out of a loop makes + * it harder for the compiler to optimize. + */ + if (charnum >= maxSV1) break; + + /* We don't need to set the normalized count to 0 + * because we already memset the whole buffer to 0. + */ + + if (LIKELY(ip <= iend-7) || (ip + (bitCount>>3) <= iend-4)) { + assert((bitCount >> 3) <= 3); /* For first condition to work */ + ip += bitCount>>3; + bitCount &= 7; + } else { + bitCount -= (int)(8 * (iend - 4 - ip)); + bitCount &= 31; + ip = iend - 4; + } + bitStream = MEM_readLE32(ip) >> bitCount; + } + { + int const max = (2*threshold-1) - remaining; + int count; + + if ((bitStream & (threshold-1)) < (U32)max) { + count = bitStream & (threshold-1); + bitCount += nbBits-1; + } else { + count = bitStream & (2*threshold-1); + if (count >= threshold) count -= max; + bitCount += nbBits; + } + + count--; /* extra accuracy */ + /* When it matters (small blocks), this is a + * predictable branch, because we don't use -1. + */ + if (count >= 0) { + remaining -= count; + } else { + assert(count == -1); + remaining += count; + } + normalizedCounter[charnum++] = (short)count; + previous0 = !count; + + assert(threshold > 1); + if (remaining < threshold) { + /* This branch can be folded into the + * threshold update condition because we + * know that threshold > 1. + */ + if (remaining <= 1) break; + nbBits = ZSTD_highbit32(remaining) + 1; + threshold = 1 << (nbBits - 1); + } + if (charnum >= maxSV1) break; + + if (LIKELY(ip <= iend-7) || (ip + (bitCount>>3) <= iend-4)) { + ip += bitCount>>3; + bitCount &= 7; + } else { + bitCount -= (int)(8 * (iend - 4 - ip)); + bitCount &= 31; + ip = iend - 4; + } + bitStream = MEM_readLE32(ip) >> bitCount; + } } + if (remaining != 1) return ERROR(corruption_detected); + /* Only possible when there are too many zeros. */ + if (charnum > maxSV1) return ERROR(maxSymbolValue_tooSmall); + if (bitCount > 32) return ERROR(corruption_detected); + *maxSVPtr = charnum-1; + + ip += (bitCount+7)>>3; + return ip-istart; +} + +/* Avoids the FORCE_INLINE of the _body() function. */ +static size_t FSE_readNCount_body_default( + short* normalizedCounter, unsigned* maxSVPtr, unsigned* tableLogPtr, + const void* headerBuffer, size_t hbSize) +{ + return FSE_readNCount_body(normalizedCounter, maxSVPtr, tableLogPtr, headerBuffer, hbSize); +} + +#if DYNAMIC_BMI2 +BMI2_TARGET_ATTRIBUTE static size_t FSE_readNCount_body_bmi2( + short* normalizedCounter, unsigned* maxSVPtr, unsigned* tableLogPtr, + const void* headerBuffer, size_t hbSize) +{ + return FSE_readNCount_body(normalizedCounter, maxSVPtr, tableLogPtr, headerBuffer, hbSize); +} +#endif + +size_t FSE_readNCount_bmi2( + short* normalizedCounter, unsigned* maxSVPtr, unsigned* tableLogPtr, + const void* headerBuffer, size_t hbSize, int bmi2) +{ +#if DYNAMIC_BMI2 + if (bmi2) { + return FSE_readNCount_body_bmi2(normalizedCounter, maxSVPtr, tableLogPtr, headerBuffer, hbSize); + } +#endif + (void)bmi2; + return FSE_readNCount_body_default(normalizedCounter, maxSVPtr, tableLogPtr, headerBuffer, hbSize); +} + +size_t FSE_readNCount( + short* normalizedCounter, unsigned* maxSVPtr, unsigned* tableLogPtr, + const void* headerBuffer, size_t hbSize) +{ + return FSE_readNCount_bmi2(normalizedCounter, maxSVPtr, tableLogPtr, headerBuffer, hbSize, /* bmi2 */ 0); +} + + +/*! HUF_readStats() : + Read compact Huffman tree, saved by HUF_writeCTable(). + `huffWeight` is destination buffer. + `rankStats` is assumed to be a table of at least HUF_TABLELOG_MAX U32. + @return : size read from `src` , or an error Code . + Note : Needed by HUF_readCTable() and HUF_readDTableX?() . +*/ +size_t HUF_readStats(BYTE* huffWeight, size_t hwSize, U32* rankStats, + U32* nbSymbolsPtr, U32* tableLogPtr, + const void* src, size_t srcSize) +{ + U32 wksp[HUF_READ_STATS_WORKSPACE_SIZE_U32]; + return HUF_readStats_wksp(huffWeight, hwSize, rankStats, nbSymbolsPtr, tableLogPtr, src, srcSize, wksp, sizeof(wksp), /* flags */ 0); +} + +FORCE_INLINE_TEMPLATE size_t +HUF_readStats_body(BYTE* huffWeight, size_t hwSize, U32* rankStats, + U32* nbSymbolsPtr, U32* tableLogPtr, + const void* src, size_t srcSize, + void* workSpace, size_t wkspSize, + int bmi2) +{ + U32 weightTotal; + const BYTE* ip = (const BYTE*) src; + size_t iSize; + size_t oSize; + + if (!srcSize) return ERROR(srcSize_wrong); + iSize = ip[0]; + /* ZSTD_memset(huffWeight, 0, hwSize); *//* is not necessary, even though some analyzer complain ... */ + + if (iSize >= 128) { /* special header */ + oSize = iSize - 127; + iSize = ((oSize+1)/2); + if (iSize+1 > srcSize) return ERROR(srcSize_wrong); + if (oSize >= hwSize) return ERROR(corruption_detected); + ip += 1; + { U32 n; + for (n=0; n> 4; + huffWeight[n+1] = ip[n/2] & 15; + } } } + else { /* header compressed with FSE (normal case) */ + if (iSize+1 > srcSize) return ERROR(srcSize_wrong); + /* max (hwSize-1) values decoded, as last one is implied */ + oSize = FSE_decompress_wksp_bmi2(huffWeight, hwSize-1, ip+1, iSize, 6, workSpace, wkspSize, bmi2); + if (FSE_isError(oSize)) return oSize; + } + + /* collect weight stats */ + ZSTD_memset(rankStats, 0, (HUF_TABLELOG_MAX + 1) * sizeof(U32)); + weightTotal = 0; + { U32 n; for (n=0; n HUF_TABLELOG_MAX) return ERROR(corruption_detected); + rankStats[huffWeight[n]]++; + weightTotal += (1 << huffWeight[n]) >> 1; + } } + if (weightTotal == 0) return ERROR(corruption_detected); + + /* get last non-null symbol weight (implied, total must be 2^n) */ + { U32 const tableLog = ZSTD_highbit32(weightTotal) + 1; + if (tableLog > HUF_TABLELOG_MAX) return ERROR(corruption_detected); + *tableLogPtr = tableLog; + /* determine last weight */ + { U32 const total = 1 << tableLog; + U32 const rest = total - weightTotal; + U32 const verif = 1 << ZSTD_highbit32(rest); + U32 const lastWeight = ZSTD_highbit32(rest) + 1; + if (verif != rest) return ERROR(corruption_detected); /* last value must be a clean power of 2 */ + huffWeight[oSize] = (BYTE)lastWeight; + rankStats[lastWeight]++; + } } + + /* check tree construction validity */ + if ((rankStats[1] < 2) || (rankStats[1] & 1)) return ERROR(corruption_detected); /* by construction : at least 2 elts of rank 1, must be even */ + + /* results */ + *nbSymbolsPtr = (U32)(oSize+1); + return iSize+1; +} + +/* Avoids the FORCE_INLINE of the _body() function. */ +static size_t HUF_readStats_body_default(BYTE* huffWeight, size_t hwSize, U32* rankStats, + U32* nbSymbolsPtr, U32* tableLogPtr, + const void* src, size_t srcSize, + void* workSpace, size_t wkspSize) +{ + return HUF_readStats_body(huffWeight, hwSize, rankStats, nbSymbolsPtr, tableLogPtr, src, srcSize, workSpace, wkspSize, 0); +} + +#if DYNAMIC_BMI2 +static BMI2_TARGET_ATTRIBUTE size_t HUF_readStats_body_bmi2(BYTE* huffWeight, size_t hwSize, U32* rankStats, + U32* nbSymbolsPtr, U32* tableLogPtr, + const void* src, size_t srcSize, + void* workSpace, size_t wkspSize) +{ + return HUF_readStats_body(huffWeight, hwSize, rankStats, nbSymbolsPtr, tableLogPtr, src, srcSize, workSpace, wkspSize, 1); +} +#endif + +size_t HUF_readStats_wksp(BYTE* huffWeight, size_t hwSize, U32* rankStats, + U32* nbSymbolsPtr, U32* tableLogPtr, + const void* src, size_t srcSize, + void* workSpace, size_t wkspSize, + int flags) +{ +#if DYNAMIC_BMI2 + if (flags & HUF_flags_bmi2) { + return HUF_readStats_body_bmi2(huffWeight, hwSize, rankStats, nbSymbolsPtr, tableLogPtr, src, srcSize, workSpace, wkspSize); + } +#endif + (void)flags; + return HUF_readStats_body_default(huffWeight, hwSize, rankStats, nbSymbolsPtr, tableLogPtr, src, srcSize, workSpace, wkspSize); +} +/**** ended inlining common/entropy_common.c ****/ +/**** start inlining common/error_private.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/* The purpose of this file is to have a single list of error strings embedded in binary */ + +/**** skipping file: error_private.h ****/ + +const char* ERR_getErrorString(ERR_enum code) +{ +#ifdef ZSTD_STRIP_ERROR_STRINGS + (void)code; + return "Error strings stripped"; +#else + static const char* const notErrorCode = "Unspecified error code"; + switch( code ) + { + case PREFIX(no_error): return "No error detected"; + case PREFIX(GENERIC): return "Error (generic)"; + case PREFIX(prefix_unknown): return "Unknown frame descriptor"; + case PREFIX(version_unsupported): return "Version not supported"; + case PREFIX(frameParameter_unsupported): return "Unsupported frame parameter"; + case PREFIX(frameParameter_windowTooLarge): return "Frame requires too much memory for decoding"; + case PREFIX(corruption_detected): return "Data corruption detected"; + case PREFIX(checksum_wrong): return "Restored data doesn't match checksum"; + case PREFIX(literals_headerWrong): return "Header of Literals' block doesn't respect format specification"; + case PREFIX(parameter_unsupported): return "Unsupported parameter"; + case PREFIX(parameter_combination_unsupported): return "Unsupported combination of parameters"; + case PREFIX(parameter_outOfBound): return "Parameter is out of bound"; + case PREFIX(init_missing): return "Context should be init first"; + case PREFIX(memory_allocation): return "Allocation error : not enough memory"; + case PREFIX(workSpace_tooSmall): return "workSpace buffer is not large enough"; + case PREFIX(stage_wrong): return "Operation not authorized at current processing stage"; + case PREFIX(tableLog_tooLarge): return "tableLog requires too much memory : unsupported"; + case PREFIX(maxSymbolValue_tooLarge): return "Unsupported max Symbol Value : too large"; + case PREFIX(maxSymbolValue_tooSmall): return "Specified maxSymbolValue is too small"; + case PREFIX(stabilityCondition_notRespected): return "pledged buffer stability condition is not respected"; + case PREFIX(dictionary_corrupted): return "Dictionary is corrupted"; + case PREFIX(dictionary_wrong): return "Dictionary mismatch"; + case PREFIX(dictionaryCreation_failed): return "Cannot create Dictionary from provided samples"; + case PREFIX(dstSize_tooSmall): return "Destination buffer is too small"; + case PREFIX(srcSize_wrong): return "Src size is incorrect"; + case PREFIX(dstBuffer_null): return "Operation on NULL destination buffer"; + case PREFIX(noForwardProgress_destFull): return "Operation made no progress over multiple calls, due to output buffer being full"; + case PREFIX(noForwardProgress_inputEmpty): return "Operation made no progress over multiple calls, due to input being empty"; + /* following error codes are not stable and may be removed or changed in a future version */ + case PREFIX(frameIndex_tooLarge): return "Frame index is too large"; + case PREFIX(seekableIO): return "An I/O error occurred when reading/seeking"; + case PREFIX(dstBuffer_wrong): return "Destination buffer is wrong"; + case PREFIX(srcBuffer_wrong): return "Source buffer is wrong"; + case PREFIX(sequenceProducer_failed): return "Block-level external sequence producer returned an error code"; + case PREFIX(externalSequences_invalid): return "External sequences are not valid"; + case PREFIX(maxCode): + default: return notErrorCode; + } +#endif +} +/**** ended inlining common/error_private.c ****/ +/**** start inlining common/fse_decompress.c ****/ +/* ****************************************************************** + * FSE : Finite State Entropy decoder + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - FSE source repository : https://github.com/Cyan4973/FiniteStateEntropy + * - Public forum : https://groups.google.com/forum/#!forum/lz4c + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + + +/* ************************************************************** +* Includes +****************************************************************/ +/**** skipping file: debug.h ****/ +/**** skipping file: bitstream.h ****/ +/**** skipping file: compiler.h ****/ +#define FSE_STATIC_LINKING_ONLY +/**** skipping file: fse.h ****/ +/**** skipping file: error_private.h ****/ +/**** skipping file: zstd_deps.h ****/ +/**** skipping file: bits.h ****/ + + +/* ************************************************************** +* Error Management +****************************************************************/ +#define FSE_isError ERR_isError +#define FSE_STATIC_ASSERT(c) DEBUG_STATIC_ASSERT(c) /* use only *after* variable declarations */ + + +/* ************************************************************** +* Templates +****************************************************************/ +/* + designed to be included + for type-specific functions (template emulation in C) + Objective is to write these functions only once, for improved maintenance +*/ + +/* safety checks */ +#ifndef FSE_FUNCTION_EXTENSION +# error "FSE_FUNCTION_EXTENSION must be defined" +#endif +#ifndef FSE_FUNCTION_TYPE +# error "FSE_FUNCTION_TYPE must be defined" +#endif + +/* Function names */ +#define FSE_CAT(X,Y) X##Y +#define FSE_FUNCTION_NAME(X,Y) FSE_CAT(X,Y) +#define FSE_TYPE_NAME(X,Y) FSE_CAT(X,Y) + +static size_t FSE_buildDTable_internal(FSE_DTable* dt, const short* normalizedCounter, unsigned maxSymbolValue, unsigned tableLog, void* workSpace, size_t wkspSize) +{ + void* const tdPtr = dt+1; /* because *dt is unsigned, 32-bits aligned on 32-bits */ + FSE_DECODE_TYPE* const tableDecode = (FSE_DECODE_TYPE*) (tdPtr); + U16* symbolNext = (U16*)workSpace; + BYTE* spread = (BYTE*)(symbolNext + maxSymbolValue + 1); + + U32 const maxSV1 = maxSymbolValue + 1; + U32 const tableSize = 1 << tableLog; + U32 highThreshold = tableSize-1; + + /* Sanity Checks */ + if (FSE_BUILD_DTABLE_WKSP_SIZE(tableLog, maxSymbolValue) > wkspSize) return ERROR(maxSymbolValue_tooLarge); + if (maxSymbolValue > FSE_MAX_SYMBOL_VALUE) return ERROR(maxSymbolValue_tooLarge); + if (tableLog > FSE_MAX_TABLELOG) return ERROR(tableLog_tooLarge); + + /* Init, lay down lowprob symbols */ + { FSE_DTableHeader DTableH; + DTableH.tableLog = (U16)tableLog; + DTableH.fastMode = 1; + { S16 const largeLimit= (S16)(1 << (tableLog-1)); + U32 s; + for (s=0; s= largeLimit) DTableH.fastMode=0; + symbolNext[s] = (U16)normalizedCounter[s]; + } } } + ZSTD_memcpy(dt, &DTableH, sizeof(DTableH)); + } + + /* Spread symbols */ + if (highThreshold == tableSize - 1) { + size_t const tableMask = tableSize-1; + size_t const step = FSE_TABLESTEP(tableSize); + /* First lay down the symbols in order. + * We use a uint64_t to lay down 8 bytes at a time. This reduces branch + * misses since small blocks generally have small table logs, so nearly + * all symbols have counts <= 8. We ensure we have 8 bytes at the end of + * our buffer to handle the over-write. + */ + { U64 const add = 0x0101010101010101ull; + size_t pos = 0; + U64 sv = 0; + U32 s; + for (s=0; s highThreshold) position = (position + step) & tableMask; /* lowprob area */ + } } + if (position!=0) return ERROR(GENERIC); /* position must reach all cells once, otherwise normalizedCounter is incorrect */ + } + + /* Build Decoding table */ + { U32 u; + for (u=0; u sizeof(bitD.bitContainer)*8) /* This test must be static */ + BIT_reloadDStream(&bitD); + + op[1] = FSE_GETSYMBOL(&state2); + + if (FSE_MAX_TABLELOG*4+7 > sizeof(bitD.bitContainer)*8) /* This test must be static */ + { if (BIT_reloadDStream(&bitD) > BIT_DStream_unfinished) { op+=2; break; } } + + op[2] = FSE_GETSYMBOL(&state1); + + if (FSE_MAX_TABLELOG*2+7 > sizeof(bitD.bitContainer)*8) /* This test must be static */ + BIT_reloadDStream(&bitD); + + op[3] = FSE_GETSYMBOL(&state2); + } + + /* tail */ + /* note : BIT_reloadDStream(&bitD) >= FSE_DStream_partiallyFilled; Ends at exactly BIT_DStream_completed */ + while (1) { + if (op>(omax-2)) return ERROR(dstSize_tooSmall); + *op++ = FSE_GETSYMBOL(&state1); + if (BIT_reloadDStream(&bitD)==BIT_DStream_overflow) { + *op++ = FSE_GETSYMBOL(&state2); + break; + } + + if (op>(omax-2)) return ERROR(dstSize_tooSmall); + *op++ = FSE_GETSYMBOL(&state2); + if (BIT_reloadDStream(&bitD)==BIT_DStream_overflow) { + *op++ = FSE_GETSYMBOL(&state1); + break; + } } + + assert(op >= ostart); + return (size_t)(op-ostart); +} + +typedef struct { + short ncount[FSE_MAX_SYMBOL_VALUE + 1]; +} FSE_DecompressWksp; + + +FORCE_INLINE_TEMPLATE size_t FSE_decompress_wksp_body( + void* dst, size_t dstCapacity, + const void* cSrc, size_t cSrcSize, + unsigned maxLog, void* workSpace, size_t wkspSize, + int bmi2) +{ + const BYTE* const istart = (const BYTE*)cSrc; + const BYTE* ip = istart; + unsigned tableLog; + unsigned maxSymbolValue = FSE_MAX_SYMBOL_VALUE; + FSE_DecompressWksp* const wksp = (FSE_DecompressWksp*)workSpace; + size_t const dtablePos = sizeof(FSE_DecompressWksp) / sizeof(FSE_DTable); + FSE_DTable* const dtable = (FSE_DTable*)workSpace + dtablePos; + + FSE_STATIC_ASSERT((FSE_MAX_SYMBOL_VALUE + 1) % 2 == 0); + if (wkspSize < sizeof(*wksp)) return ERROR(GENERIC); + + /* correct offset to dtable depends on this property */ + FSE_STATIC_ASSERT(sizeof(FSE_DecompressWksp) % sizeof(FSE_DTable) == 0); + + /* normal FSE decoding mode */ + { size_t const NCountLength = + FSE_readNCount_bmi2(wksp->ncount, &maxSymbolValue, &tableLog, istart, cSrcSize, bmi2); + if (FSE_isError(NCountLength)) return NCountLength; + if (tableLog > maxLog) return ERROR(tableLog_tooLarge); + assert(NCountLength <= cSrcSize); + ip += NCountLength; + cSrcSize -= NCountLength; + } + + if (FSE_DECOMPRESS_WKSP_SIZE(tableLog, maxSymbolValue) > wkspSize) return ERROR(tableLog_tooLarge); + assert(sizeof(*wksp) + FSE_DTABLE_SIZE(tableLog) <= wkspSize); + workSpace = (BYTE*)workSpace + sizeof(*wksp) + FSE_DTABLE_SIZE(tableLog); + wkspSize -= sizeof(*wksp) + FSE_DTABLE_SIZE(tableLog); + + CHECK_F( FSE_buildDTable_internal(dtable, wksp->ncount, maxSymbolValue, tableLog, workSpace, wkspSize) ); + + { + const void* ptr = dtable; + const FSE_DTableHeader* DTableH = (const FSE_DTableHeader*)ptr; + const U32 fastMode = DTableH->fastMode; + + /* select fast mode (static) */ + if (fastMode) return FSE_decompress_usingDTable_generic(dst, dstCapacity, ip, cSrcSize, dtable, 1); + return FSE_decompress_usingDTable_generic(dst, dstCapacity, ip, cSrcSize, dtable, 0); + } +} + +/* Avoids the FORCE_INLINE of the _body() function. */ +static size_t FSE_decompress_wksp_body_default(void* dst, size_t dstCapacity, const void* cSrc, size_t cSrcSize, unsigned maxLog, void* workSpace, size_t wkspSize) +{ + return FSE_decompress_wksp_body(dst, dstCapacity, cSrc, cSrcSize, maxLog, workSpace, wkspSize, 0); +} + +#if DYNAMIC_BMI2 +BMI2_TARGET_ATTRIBUTE static size_t FSE_decompress_wksp_body_bmi2(void* dst, size_t dstCapacity, const void* cSrc, size_t cSrcSize, unsigned maxLog, void* workSpace, size_t wkspSize) +{ + return FSE_decompress_wksp_body(dst, dstCapacity, cSrc, cSrcSize, maxLog, workSpace, wkspSize, 1); +} +#endif + +size_t FSE_decompress_wksp_bmi2(void* dst, size_t dstCapacity, const void* cSrc, size_t cSrcSize, unsigned maxLog, void* workSpace, size_t wkspSize, int bmi2) +{ +#if DYNAMIC_BMI2 + if (bmi2) { + return FSE_decompress_wksp_body_bmi2(dst, dstCapacity, cSrc, cSrcSize, maxLog, workSpace, wkspSize); + } +#endif + (void)bmi2; + return FSE_decompress_wksp_body_default(dst, dstCapacity, cSrc, cSrcSize, maxLog, workSpace, wkspSize); +} + +#endif /* FSE_COMMONDEFS_ONLY */ +/**** ended inlining common/fse_decompress.c ****/ +/**** start inlining common/threading.c ****/ +/** + * Copyright (c) 2016 Tino Reichardt + * All rights reserved. + * + * You can contact the author at: + * - zstdmt source repository: https://github.com/mcmilk/zstdmt + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/** + * This file will hold wrapper for systems, which do not support pthreads + */ + +/**** start inlining threading.h ****/ +/** + * Copyright (c) 2016 Tino Reichardt + * All rights reserved. + * + * You can contact the author at: + * - zstdmt source repository: https://github.com/mcmilk/zstdmt + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef THREADING_H_938743 +#define THREADING_H_938743 + +/**** skipping file: debug.h ****/ + +#if defined (__cplusplus) +extern "C" { +#endif + +#if defined(ZSTD_MULTITHREAD) && defined(_WIN32) + +/** + * Windows minimalist Pthread Wrapper + */ +#ifdef WINVER +# undef WINVER +#endif +#define WINVER 0x0600 + +#ifdef _WIN32_WINNT +# undef _WIN32_WINNT +#endif +#define _WIN32_WINNT 0x0600 + +#ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN +#endif + +#undef ERROR /* reported already defined on VS 2015 (Rich Geldreich) */ +#include +#undef ERROR +#define ERROR(name) ZSTD_ERROR(name) + + +/* mutex */ +#define ZSTD_pthread_mutex_t CRITICAL_SECTION +#define ZSTD_pthread_mutex_init(a, b) ((void)(b), InitializeCriticalSection((a)), 0) +#define ZSTD_pthread_mutex_destroy(a) DeleteCriticalSection((a)) +#define ZSTD_pthread_mutex_lock(a) EnterCriticalSection((a)) +#define ZSTD_pthread_mutex_unlock(a) LeaveCriticalSection((a)) + +/* condition variable */ +#define ZSTD_pthread_cond_t CONDITION_VARIABLE +#define ZSTD_pthread_cond_init(a, b) ((void)(b), InitializeConditionVariable((a)), 0) +#define ZSTD_pthread_cond_destroy(a) ((void)(a)) +#define ZSTD_pthread_cond_wait(a, b) SleepConditionVariableCS((a), (b), INFINITE) +#define ZSTD_pthread_cond_signal(a) WakeConditionVariable((a)) +#define ZSTD_pthread_cond_broadcast(a) WakeAllConditionVariable((a)) + +/* ZSTD_pthread_create() and ZSTD_pthread_join() */ +typedef HANDLE ZSTD_pthread_t; + +int ZSTD_pthread_create(ZSTD_pthread_t* thread, const void* unused, + void* (*start_routine) (void*), void* arg); + +int ZSTD_pthread_join(ZSTD_pthread_t thread); + +/** + * add here more wrappers as required + */ + + +#elif defined(ZSTD_MULTITHREAD) /* posix assumed ; need a better detection method */ +/* === POSIX Systems === */ +# include + +#if DEBUGLEVEL < 1 + +#define ZSTD_pthread_mutex_t pthread_mutex_t +#define ZSTD_pthread_mutex_init(a, b) pthread_mutex_init((a), (b)) +#define ZSTD_pthread_mutex_destroy(a) pthread_mutex_destroy((a)) +#define ZSTD_pthread_mutex_lock(a) pthread_mutex_lock((a)) +#define ZSTD_pthread_mutex_unlock(a) pthread_mutex_unlock((a)) + +#define ZSTD_pthread_cond_t pthread_cond_t +#define ZSTD_pthread_cond_init(a, b) pthread_cond_init((a), (b)) +#define ZSTD_pthread_cond_destroy(a) pthread_cond_destroy((a)) +#define ZSTD_pthread_cond_wait(a, b) pthread_cond_wait((a), (b)) +#define ZSTD_pthread_cond_signal(a) pthread_cond_signal((a)) +#define ZSTD_pthread_cond_broadcast(a) pthread_cond_broadcast((a)) + +#define ZSTD_pthread_t pthread_t +#define ZSTD_pthread_create(a, b, c, d) pthread_create((a), (b), (c), (d)) +#define ZSTD_pthread_join(a) pthread_join((a),NULL) + +#else /* DEBUGLEVEL >= 1 */ + +/* Debug implementation of threading. + * In this implementation we use pointers for mutexes and condition variables. + * This way, if we forget to init/destroy them the program will crash or ASAN + * will report leaks. + */ + +#define ZSTD_pthread_mutex_t pthread_mutex_t* +int ZSTD_pthread_mutex_init(ZSTD_pthread_mutex_t* mutex, pthread_mutexattr_t const* attr); +int ZSTD_pthread_mutex_destroy(ZSTD_pthread_mutex_t* mutex); +#define ZSTD_pthread_mutex_lock(a) pthread_mutex_lock(*(a)) +#define ZSTD_pthread_mutex_unlock(a) pthread_mutex_unlock(*(a)) + +#define ZSTD_pthread_cond_t pthread_cond_t* +int ZSTD_pthread_cond_init(ZSTD_pthread_cond_t* cond, pthread_condattr_t const* attr); +int ZSTD_pthread_cond_destroy(ZSTD_pthread_cond_t* cond); +#define ZSTD_pthread_cond_wait(a, b) pthread_cond_wait(*(a), *(b)) +#define ZSTD_pthread_cond_signal(a) pthread_cond_signal(*(a)) +#define ZSTD_pthread_cond_broadcast(a) pthread_cond_broadcast(*(a)) + +#define ZSTD_pthread_t pthread_t +#define ZSTD_pthread_create(a, b, c, d) pthread_create((a), (b), (c), (d)) +#define ZSTD_pthread_join(a) pthread_join((a),NULL) + +#endif + +#else /* ZSTD_MULTITHREAD not defined */ +/* No multithreading support */ + +typedef int ZSTD_pthread_mutex_t; +#define ZSTD_pthread_mutex_init(a, b) ((void)(a), (void)(b), 0) +#define ZSTD_pthread_mutex_destroy(a) ((void)(a)) +#define ZSTD_pthread_mutex_lock(a) ((void)(a)) +#define ZSTD_pthread_mutex_unlock(a) ((void)(a)) + +typedef int ZSTD_pthread_cond_t; +#define ZSTD_pthread_cond_init(a, b) ((void)(a), (void)(b), 0) +#define ZSTD_pthread_cond_destroy(a) ((void)(a)) +#define ZSTD_pthread_cond_wait(a, b) ((void)(a), (void)(b)) +#define ZSTD_pthread_cond_signal(a) ((void)(a)) +#define ZSTD_pthread_cond_broadcast(a) ((void)(a)) + +/* do not use ZSTD_pthread_t */ + +#endif /* ZSTD_MULTITHREAD */ + +#if defined (__cplusplus) +} +#endif + +#endif /* THREADING_H_938743 */ +/**** ended inlining threading.h ****/ + +/* create fake symbol to avoid empty translation unit warning */ +int g_ZSTD_threading_useless_symbol; + +#if defined(ZSTD_MULTITHREAD) && defined(_WIN32) + +/** + * Windows minimalist Pthread Wrapper + */ + + +/* === Dependencies === */ +#include +#include + + +/* === Implementation === */ + +typedef struct { + void* (*start_routine)(void*); + void* arg; + int initialized; + ZSTD_pthread_cond_t initialized_cond; + ZSTD_pthread_mutex_t initialized_mutex; +} ZSTD_thread_params_t; + +static unsigned __stdcall worker(void *arg) +{ + void* (*start_routine)(void*); + void* thread_arg; + + /* Initialized thread_arg and start_routine and signal main thread that we don't need it + * to wait any longer. + */ + { + ZSTD_thread_params_t* thread_param = (ZSTD_thread_params_t*)arg; + thread_arg = thread_param->arg; + start_routine = thread_param->start_routine; + + /* Signal main thread that we are running and do not depend on its memory anymore */ + ZSTD_pthread_mutex_lock(&thread_param->initialized_mutex); + thread_param->initialized = 1; + ZSTD_pthread_cond_signal(&thread_param->initialized_cond); + ZSTD_pthread_mutex_unlock(&thread_param->initialized_mutex); + } + + start_routine(thread_arg); + + return 0; +} + +int ZSTD_pthread_create(ZSTD_pthread_t* thread, const void* unused, + void* (*start_routine) (void*), void* arg) +{ + ZSTD_thread_params_t thread_param; + (void)unused; + + if (thread==NULL) return -1; + *thread = NULL; + + thread_param.start_routine = start_routine; + thread_param.arg = arg; + thread_param.initialized = 0; + + /* Setup thread initialization synchronization */ + if(ZSTD_pthread_cond_init(&thread_param.initialized_cond, NULL)) { + /* Should never happen on Windows */ + return -1; + } + if(ZSTD_pthread_mutex_init(&thread_param.initialized_mutex, NULL)) { + /* Should never happen on Windows */ + ZSTD_pthread_cond_destroy(&thread_param.initialized_cond); + return -1; + } + + /* Spawn thread */ + *thread = (HANDLE)_beginthreadex(NULL, 0, worker, &thread_param, 0, NULL); + if (*thread==NULL) { + ZSTD_pthread_mutex_destroy(&thread_param.initialized_mutex); + ZSTD_pthread_cond_destroy(&thread_param.initialized_cond); + return errno; + } + + /* Wait for thread to be initialized */ + ZSTD_pthread_mutex_lock(&thread_param.initialized_mutex); + while(!thread_param.initialized) { + ZSTD_pthread_cond_wait(&thread_param.initialized_cond, &thread_param.initialized_mutex); + } + ZSTD_pthread_mutex_unlock(&thread_param.initialized_mutex); + ZSTD_pthread_mutex_destroy(&thread_param.initialized_mutex); + ZSTD_pthread_cond_destroy(&thread_param.initialized_cond); + + return 0; +} + +int ZSTD_pthread_join(ZSTD_pthread_t thread) +{ + DWORD result; + + if (!thread) return 0; + + result = WaitForSingleObject(thread, INFINITE); + CloseHandle(thread); + + switch (result) { + case WAIT_OBJECT_0: + return 0; + case WAIT_ABANDONED: + return EINVAL; + default: + return GetLastError(); + } +} + +#endif /* ZSTD_MULTITHREAD */ + +#if defined(ZSTD_MULTITHREAD) && DEBUGLEVEL >= 1 && !defined(_WIN32) + +#define ZSTD_DEPS_NEED_MALLOC +/**** skipping file: zstd_deps.h ****/ + +int ZSTD_pthread_mutex_init(ZSTD_pthread_mutex_t* mutex, pthread_mutexattr_t const* attr) +{ + assert(mutex != NULL); + *mutex = (pthread_mutex_t*)ZSTD_malloc(sizeof(pthread_mutex_t)); + if (!*mutex) + return 1; + return pthread_mutex_init(*mutex, attr); +} + +int ZSTD_pthread_mutex_destroy(ZSTD_pthread_mutex_t* mutex) +{ + assert(mutex != NULL); + if (!*mutex) + return 0; + { + int const ret = pthread_mutex_destroy(*mutex); + ZSTD_free(*mutex); + return ret; + } +} + +int ZSTD_pthread_cond_init(ZSTD_pthread_cond_t* cond, pthread_condattr_t const* attr) +{ + assert(cond != NULL); + *cond = (pthread_cond_t*)ZSTD_malloc(sizeof(pthread_cond_t)); + if (!*cond) + return 1; + return pthread_cond_init(*cond, attr); +} + +int ZSTD_pthread_cond_destroy(ZSTD_pthread_cond_t* cond) +{ + assert(cond != NULL); + if (!*cond) + return 0; + { + int const ret = pthread_cond_destroy(*cond); + ZSTD_free(*cond); + return ret; + } +} + +#endif +/**** ended inlining common/threading.c ****/ +/**** start inlining common/pool.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + +/* ====== Dependencies ======= */ +/**** start inlining ../common/allocations.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/* This file provides custom allocation primitives + */ + +#define ZSTD_DEPS_NEED_MALLOC +/**** skipping file: zstd_deps.h ****/ + +/**** skipping file: compiler.h ****/ +#define ZSTD_STATIC_LINKING_ONLY +/**** start inlining ../zstd.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ +#if defined (__cplusplus) +extern "C" { +#endif + +#ifndef ZSTD_H_235446 +#define ZSTD_H_235446 + +/* ====== Dependencies ======*/ +#include /* INT_MAX */ +#include /* size_t */ + + +/* ===== ZSTDLIB_API : control library symbols visibility ===== */ +#ifndef ZSTDLIB_VISIBLE + /* Backwards compatibility with old macro name */ +# ifdef ZSTDLIB_VISIBILITY +# define ZSTDLIB_VISIBLE ZSTDLIB_VISIBILITY +# elif defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__) +# define ZSTDLIB_VISIBLE __attribute__ ((visibility ("default"))) +# else +# define ZSTDLIB_VISIBLE +# endif +#endif + +#ifndef ZSTDLIB_HIDDEN +# if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__) +# define ZSTDLIB_HIDDEN __attribute__ ((visibility ("hidden"))) +# else +# define ZSTDLIB_HIDDEN +# endif +#endif + +#if defined(ZSTD_DLL_EXPORT) && (ZSTD_DLL_EXPORT==1) +# define ZSTDLIB_API __declspec(dllexport) ZSTDLIB_VISIBLE +#elif defined(ZSTD_DLL_IMPORT) && (ZSTD_DLL_IMPORT==1) +# define ZSTDLIB_API __declspec(dllimport) ZSTDLIB_VISIBLE /* It isn't required but allows to generate better code, saving a function pointer load from the IAT and an indirect jump.*/ +#else +# define ZSTDLIB_API ZSTDLIB_VISIBLE +#endif + +/* Deprecation warnings : + * Should these warnings be a problem, it is generally possible to disable them, + * typically with -Wno-deprecated-declarations for gcc or _CRT_SECURE_NO_WARNINGS in Visual. + * Otherwise, it's also possible to define ZSTD_DISABLE_DEPRECATE_WARNINGS. + */ +#ifdef ZSTD_DISABLE_DEPRECATE_WARNINGS +# define ZSTD_DEPRECATED(message) /* disable deprecation warnings */ +#else +# if defined (__cplusplus) && (__cplusplus >= 201402) /* C++14 or greater */ +# define ZSTD_DEPRECATED(message) [[deprecated(message)]] +# elif (defined(GNUC) && (GNUC > 4 || (GNUC == 4 && GNUC_MINOR >= 5))) || defined(__clang__) +# define ZSTD_DEPRECATED(message) __attribute__((deprecated(message))) +# elif defined(__GNUC__) && (__GNUC__ >= 3) +# define ZSTD_DEPRECATED(message) __attribute__((deprecated)) +# elif defined(_MSC_VER) +# define ZSTD_DEPRECATED(message) __declspec(deprecated(message)) +# else +# pragma message("WARNING: You need to implement ZSTD_DEPRECATED for this compiler") +# define ZSTD_DEPRECATED(message) +# endif +#endif /* ZSTD_DISABLE_DEPRECATE_WARNINGS */ + + +/******************************************************************************* + Introduction + + zstd, short for Zstandard, is a fast lossless compression algorithm, targeting + real-time compression scenarios at zlib-level and better compression ratios. + The zstd compression library provides in-memory compression and decompression + functions. + + The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), + which is currently 22. Levels >= 20, labeled `--ultra`, should be used with + caution, as they require more memory. The library also offers negative + compression levels, which extend the range of speed vs. ratio preferences. + The lower the level, the faster the speed (at the cost of compression). + + Compression can be done in: + - a single step (described as Simple API) + - a single step, reusing a context (described as Explicit context) + - unbounded multiple steps (described as Streaming compression) + + The compression ratio achievable on small data can be highly improved using + a dictionary. Dictionary compression can be performed in: + - a single step (described as Simple dictionary API) + - a single step, reusing a dictionary (described as Bulk-processing + dictionary API) + + Advanced experimental functions can be accessed using + `#define ZSTD_STATIC_LINKING_ONLY` before including zstd.h. + + Advanced experimental APIs should never be used with a dynamically-linked + library. They are not "stable"; their definitions or signatures may change in + the future. Only static linking is allowed. +*******************************************************************************/ + +/*------ Version ------*/ +#define ZSTD_VERSION_MAJOR 1 +#define ZSTD_VERSION_MINOR 5 +#define ZSTD_VERSION_RELEASE 6 +#define ZSTD_VERSION_NUMBER (ZSTD_VERSION_MAJOR *100*100 + ZSTD_VERSION_MINOR *100 + ZSTD_VERSION_RELEASE) + +/*! ZSTD_versionNumber() : + * Return runtime library version, the value is (MAJOR*100*100 + MINOR*100 + RELEASE). */ +ZSTDLIB_API unsigned ZSTD_versionNumber(void); + +#define ZSTD_LIB_VERSION ZSTD_VERSION_MAJOR.ZSTD_VERSION_MINOR.ZSTD_VERSION_RELEASE +#define ZSTD_QUOTE(str) #str +#define ZSTD_EXPAND_AND_QUOTE(str) ZSTD_QUOTE(str) +#define ZSTD_VERSION_STRING ZSTD_EXPAND_AND_QUOTE(ZSTD_LIB_VERSION) + +/*! ZSTD_versionString() : + * Return runtime library version, like "1.4.5". Requires v1.3.0+. */ +ZSTDLIB_API const char* ZSTD_versionString(void); + +/* ************************************* + * Default constant + ***************************************/ +#ifndef ZSTD_CLEVEL_DEFAULT +# define ZSTD_CLEVEL_DEFAULT 3 +#endif + +/* ************************************* + * Constants + ***************************************/ + +/* All magic numbers are supposed read/written to/from files/memory using little-endian convention */ +#define ZSTD_MAGICNUMBER 0xFD2FB528 /* valid since v0.8.0 */ +#define ZSTD_MAGIC_DICTIONARY 0xEC30A437 /* valid since v0.7.0 */ +#define ZSTD_MAGIC_SKIPPABLE_START 0x184D2A50 /* all 16 values, from 0x184D2A50 to 0x184D2A5F, signal the beginning of a skippable frame */ +#define ZSTD_MAGIC_SKIPPABLE_MASK 0xFFFFFFF0 + +#define ZSTD_BLOCKSIZELOG_MAX 17 +#define ZSTD_BLOCKSIZE_MAX (1<= ZSTD_compressBound(srcSize)` guarantees that zstd will have + * enough space to successfully compress the data. + * @return : compressed size written into `dst` (<= `dstCapacity), + * or an error code if it fails (which can be tested using ZSTD_isError()). */ +ZSTDLIB_API size_t ZSTD_compress( void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + int compressionLevel); + +/*! ZSTD_decompress() : + * `compressedSize` : must be the _exact_ size of some number of compressed and/or skippable frames. + * `dstCapacity` is an upper bound of originalSize to regenerate. + * If user cannot imply a maximum upper bound, it's better to use streaming mode to decompress data. + * @return : the number of bytes decompressed into `dst` (<= `dstCapacity`), + * or an errorCode if it fails (which can be tested using ZSTD_isError()). */ +ZSTDLIB_API size_t ZSTD_decompress( void* dst, size_t dstCapacity, + const void* src, size_t compressedSize); + +/*! ZSTD_getFrameContentSize() : requires v1.3.0+ + * `src` should point to the start of a ZSTD encoded frame. + * `srcSize` must be at least as large as the frame header. + * hint : any size >= `ZSTD_frameHeaderSize_max` is large enough. + * @return : - decompressed size of `src` frame content, if known + * - ZSTD_CONTENTSIZE_UNKNOWN if the size cannot be determined + * - ZSTD_CONTENTSIZE_ERROR if an error occurred (e.g. invalid magic number, srcSize too small) + * note 1 : a 0 return value means the frame is valid but "empty". + * note 2 : decompressed size is an optional field, it may not be present, typically in streaming mode. + * When `return==ZSTD_CONTENTSIZE_UNKNOWN`, data to decompress could be any size. + * In which case, it's necessary to use streaming mode to decompress data. + * Optionally, application can rely on some implicit limit, + * as ZSTD_decompress() only needs an upper bound of decompressed size. + * (For example, data could be necessarily cut into blocks <= 16 KB). + * note 3 : decompressed size is always present when compression is completed using single-pass functions, + * such as ZSTD_compress(), ZSTD_compressCCtx() ZSTD_compress_usingDict() or ZSTD_compress_usingCDict(). + * note 4 : decompressed size can be very large (64-bits value), + * potentially larger than what local system can handle as a single memory segment. + * In which case, it's necessary to use streaming mode to decompress data. + * note 5 : If source is untrusted, decompressed size could be wrong or intentionally modified. + * Always ensure return value fits within application's authorized limits. + * Each application can set its own limits. + * note 6 : This function replaces ZSTD_getDecompressedSize() */ +#define ZSTD_CONTENTSIZE_UNKNOWN (0ULL - 1) +#define ZSTD_CONTENTSIZE_ERROR (0ULL - 2) +ZSTDLIB_API unsigned long long ZSTD_getFrameContentSize(const void *src, size_t srcSize); + +/*! ZSTD_getDecompressedSize() : + * NOTE: This function is now obsolete, in favor of ZSTD_getFrameContentSize(). + * Both functions work the same way, but ZSTD_getDecompressedSize() blends + * "empty", "unknown" and "error" results to the same return value (0), + * while ZSTD_getFrameContentSize() gives them separate return values. + * @return : decompressed size of `src` frame content _if known and not empty_, 0 otherwise. */ +ZSTD_DEPRECATED("Replaced by ZSTD_getFrameContentSize") +ZSTDLIB_API +unsigned long long ZSTD_getDecompressedSize(const void* src, size_t srcSize); + +/*! ZSTD_findFrameCompressedSize() : Requires v1.4.0+ + * `src` should point to the start of a ZSTD frame or skippable frame. + * `srcSize` must be >= first frame size + * @return : the compressed size of the first frame starting at `src`, + * suitable to pass as `srcSize` to `ZSTD_decompress` or similar, + * or an error code if input is invalid */ +ZSTDLIB_API size_t ZSTD_findFrameCompressedSize(const void* src, size_t srcSize); + + +/*====== Helper functions ======*/ +/* ZSTD_compressBound() : + * maximum compressed size in worst case single-pass scenario. + * When invoking `ZSTD_compress()` or any other one-pass compression function, + * it's recommended to provide @dstCapacity >= ZSTD_compressBound(srcSize) + * as it eliminates one potential failure scenario, + * aka not enough room in dst buffer to write the compressed frame. + * Note : ZSTD_compressBound() itself can fail, if @srcSize > ZSTD_MAX_INPUT_SIZE . + * In which case, ZSTD_compressBound() will return an error code + * which can be tested using ZSTD_isError(). + * + * ZSTD_COMPRESSBOUND() : + * same as ZSTD_compressBound(), but as a macro. + * It can be used to produce constants, which can be useful for static allocation, + * for example to size a static array on stack. + * Will produce constant value 0 if srcSize too large. + */ +#define ZSTD_MAX_INPUT_SIZE ((sizeof(size_t)==8) ? 0xFF00FF00FF00FF00ULL : 0xFF00FF00U) +#define ZSTD_COMPRESSBOUND(srcSize) (((size_t)(srcSize) >= ZSTD_MAX_INPUT_SIZE) ? 0 : (srcSize) + ((srcSize)>>8) + (((srcSize) < (128<<10)) ? (((128<<10) - (srcSize)) >> 11) /* margin, from 64 to 0 */ : 0)) /* this formula ensures that bound(A) + bound(B) <= bound(A+B) as long as A and B >= 128 KB */ +ZSTDLIB_API size_t ZSTD_compressBound(size_t srcSize); /*!< maximum compressed size in worst case single-pass scenario */ +/* ZSTD_isError() : + * Most ZSTD_* functions returning a size_t value can be tested for error, + * using ZSTD_isError(). + * @return 1 if error, 0 otherwise + */ +ZSTDLIB_API unsigned ZSTD_isError(size_t code); /*!< tells if a `size_t` function result is an error code */ +ZSTDLIB_API const char* ZSTD_getErrorName(size_t code); /*!< provides readable string from an error code */ +ZSTDLIB_API int ZSTD_minCLevel(void); /*!< minimum negative compression level allowed, requires v1.4.0+ */ +ZSTDLIB_API int ZSTD_maxCLevel(void); /*!< maximum compression level available */ +ZSTDLIB_API int ZSTD_defaultCLevel(void); /*!< default compression level, specified by ZSTD_CLEVEL_DEFAULT, requires v1.5.0+ */ + + +/*************************************** +* Explicit context +***************************************/ +/*= Compression context + * When compressing many times, + * it is recommended to allocate a context just once, + * and reuse it for each successive compression operation. + * This will make workload friendlier for system's memory. + * Note : re-using context is just a speed / resource optimization. + * It doesn't change the compression ratio, which remains identical. + * Note 2 : In multi-threaded environments, + * use one different context per thread for parallel execution. + */ +typedef struct ZSTD_CCtx_s ZSTD_CCtx; +ZSTDLIB_API ZSTD_CCtx* ZSTD_createCCtx(void); +ZSTDLIB_API size_t ZSTD_freeCCtx(ZSTD_CCtx* cctx); /* accept NULL pointer */ + +/*! ZSTD_compressCCtx() : + * Same as ZSTD_compress(), using an explicit ZSTD_CCtx. + * Important : in order to mirror `ZSTD_compress()` behavior, + * this function compresses at the requested compression level, + * __ignoring any other advanced parameter__ . + * If any advanced parameter was set using the advanced API, + * they will all be reset. Only `compressionLevel` remains. + */ +ZSTDLIB_API size_t ZSTD_compressCCtx(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + int compressionLevel); + +/*= Decompression context + * When decompressing many times, + * it is recommended to allocate a context only once, + * and reuse it for each successive compression operation. + * This will make workload friendlier for system's memory. + * Use one context per thread for parallel execution. */ +typedef struct ZSTD_DCtx_s ZSTD_DCtx; +ZSTDLIB_API ZSTD_DCtx* ZSTD_createDCtx(void); +ZSTDLIB_API size_t ZSTD_freeDCtx(ZSTD_DCtx* dctx); /* accept NULL pointer */ + +/*! ZSTD_decompressDCtx() : + * Same as ZSTD_decompress(), + * requires an allocated ZSTD_DCtx. + * Compatible with sticky parameters (see below). + */ +ZSTDLIB_API size_t ZSTD_decompressDCtx(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize); + + +/********************************************* +* Advanced compression API (Requires v1.4.0+) +**********************************************/ + +/* API design : + * Parameters are pushed one by one into an existing context, + * using ZSTD_CCtx_set*() functions. + * Pushed parameters are sticky : they are valid for next compressed frame, and any subsequent frame. + * "sticky" parameters are applicable to `ZSTD_compress2()` and `ZSTD_compressStream*()` ! + * __They do not apply to one-shot variants such as ZSTD_compressCCtx()__ . + * + * It's possible to reset all parameters to "default" using ZSTD_CCtx_reset(). + * + * This API supersedes all other "advanced" API entry points in the experimental section. + * In the future, we expect to remove API entry points from experimental which are redundant with this API. + */ + + +/* Compression strategies, listed from fastest to strongest */ +typedef enum { ZSTD_fast=1, + ZSTD_dfast=2, + ZSTD_greedy=3, + ZSTD_lazy=4, + ZSTD_lazy2=5, + ZSTD_btlazy2=6, + ZSTD_btopt=7, + ZSTD_btultra=8, + ZSTD_btultra2=9 + /* note : new strategies _might_ be added in the future. + Only the order (from fast to strong) is guaranteed */ +} ZSTD_strategy; + +typedef enum { + + /* compression parameters + * Note: When compressing with a ZSTD_CDict these parameters are superseded + * by the parameters used to construct the ZSTD_CDict. + * See ZSTD_CCtx_refCDict() for more info (superseded-by-cdict). */ + ZSTD_c_compressionLevel=100, /* Set compression parameters according to pre-defined cLevel table. + * Note that exact compression parameters are dynamically determined, + * depending on both compression level and srcSize (when known). + * Default level is ZSTD_CLEVEL_DEFAULT==3. + * Special: value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT. + * Note 1 : it's possible to pass a negative compression level. + * Note 2 : setting a level does not automatically set all other compression parameters + * to default. Setting this will however eventually dynamically impact the compression + * parameters which have not been manually set. The manually set + * ones will 'stick'. */ + /* Advanced compression parameters : + * It's possible to pin down compression parameters to some specific values. + * In which case, these values are no longer dynamically selected by the compressor */ + ZSTD_c_windowLog=101, /* Maximum allowed back-reference distance, expressed as power of 2. + * This will set a memory budget for streaming decompression, + * with larger values requiring more memory + * and typically compressing more. + * Must be clamped between ZSTD_WINDOWLOG_MIN and ZSTD_WINDOWLOG_MAX. + * Special: value 0 means "use default windowLog". + * Note: Using a windowLog greater than ZSTD_WINDOWLOG_LIMIT_DEFAULT + * requires explicitly allowing such size at streaming decompression stage. */ + ZSTD_c_hashLog=102, /* Size of the initial probe table, as a power of 2. + * Resulting memory usage is (1 << (hashLog+2)). + * Must be clamped between ZSTD_HASHLOG_MIN and ZSTD_HASHLOG_MAX. + * Larger tables improve compression ratio of strategies <= dFast, + * and improve speed of strategies > dFast. + * Special: value 0 means "use default hashLog". */ + ZSTD_c_chainLog=103, /* Size of the multi-probe search table, as a power of 2. + * Resulting memory usage is (1 << (chainLog+2)). + * Must be clamped between ZSTD_CHAINLOG_MIN and ZSTD_CHAINLOG_MAX. + * Larger tables result in better and slower compression. + * This parameter is useless for "fast" strategy. + * It's still useful when using "dfast" strategy, + * in which case it defines a secondary probe table. + * Special: value 0 means "use default chainLog". */ + ZSTD_c_searchLog=104, /* Number of search attempts, as a power of 2. + * More attempts result in better and slower compression. + * This parameter is useless for "fast" and "dFast" strategies. + * Special: value 0 means "use default searchLog". */ + ZSTD_c_minMatch=105, /* Minimum size of searched matches. + * Note that Zstandard can still find matches of smaller size, + * it just tweaks its search algorithm to look for this size and larger. + * Larger values increase compression and decompression speed, but decrease ratio. + * Must be clamped between ZSTD_MINMATCH_MIN and ZSTD_MINMATCH_MAX. + * Note that currently, for all strategies < btopt, effective minimum is 4. + * , for all strategies > fast, effective maximum is 6. + * Special: value 0 means "use default minMatchLength". */ + ZSTD_c_targetLength=106, /* Impact of this field depends on strategy. + * For strategies btopt, btultra & btultra2: + * Length of Match considered "good enough" to stop search. + * Larger values make compression stronger, and slower. + * For strategy fast: + * Distance between match sampling. + * Larger values make compression faster, and weaker. + * Special: value 0 means "use default targetLength". */ + ZSTD_c_strategy=107, /* See ZSTD_strategy enum definition. + * The higher the value of selected strategy, the more complex it is, + * resulting in stronger and slower compression. + * Special: value 0 means "use default strategy". */ + + ZSTD_c_targetCBlockSize=130, /* v1.5.6+ + * Attempts to fit compressed block size into approximatively targetCBlockSize. + * Bound by ZSTD_TARGETCBLOCKSIZE_MIN and ZSTD_TARGETCBLOCKSIZE_MAX. + * Note that it's not a guarantee, just a convergence target (default:0). + * No target when targetCBlockSize == 0. + * This is helpful in low bandwidth streaming environments to improve end-to-end latency, + * when a client can make use of partial documents (a prominent example being Chrome). + * Note: this parameter is stable since v1.5.6. + * It was present as an experimental parameter in earlier versions, + * but it's not recommended using it with earlier library versions + * due to massive performance regressions. + */ + /* LDM mode parameters */ + ZSTD_c_enableLongDistanceMatching=160, /* Enable long distance matching. + * This parameter is designed to improve compression ratio + * for large inputs, by finding large matches at long distance. + * It increases memory usage and window size. + * Note: enabling this parameter increases default ZSTD_c_windowLog to 128 MB + * except when expressly set to a different value. + * Note: will be enabled by default if ZSTD_c_windowLog >= 128 MB and + * compression strategy >= ZSTD_btopt (== compression level 16+) */ + ZSTD_c_ldmHashLog=161, /* Size of the table for long distance matching, as a power of 2. + * Larger values increase memory usage and compression ratio, + * but decrease compression speed. + * Must be clamped between ZSTD_HASHLOG_MIN and ZSTD_HASHLOG_MAX + * default: windowlog - 7. + * Special: value 0 means "automatically determine hashlog". */ + ZSTD_c_ldmMinMatch=162, /* Minimum match size for long distance matcher. + * Larger/too small values usually decrease compression ratio. + * Must be clamped between ZSTD_LDM_MINMATCH_MIN and ZSTD_LDM_MINMATCH_MAX. + * Special: value 0 means "use default value" (default: 64). */ + ZSTD_c_ldmBucketSizeLog=163, /* Log size of each bucket in the LDM hash table for collision resolution. + * Larger values improve collision resolution but decrease compression speed. + * The maximum value is ZSTD_LDM_BUCKETSIZELOG_MAX. + * Special: value 0 means "use default value" (default: 3). */ + ZSTD_c_ldmHashRateLog=164, /* Frequency of inserting/looking up entries into the LDM hash table. + * Must be clamped between 0 and (ZSTD_WINDOWLOG_MAX - ZSTD_HASHLOG_MIN). + * Default is MAX(0, (windowLog - ldmHashLog)), optimizing hash table usage. + * Larger values improve compression speed. + * Deviating far from default value will likely result in a compression ratio decrease. + * Special: value 0 means "automatically determine hashRateLog". */ + + /* frame parameters */ + ZSTD_c_contentSizeFlag=200, /* Content size will be written into frame header _whenever known_ (default:1) + * Content size must be known at the beginning of compression. + * This is automatically the case when using ZSTD_compress2(), + * For streaming scenarios, content size must be provided with ZSTD_CCtx_setPledgedSrcSize() */ + ZSTD_c_checksumFlag=201, /* A 32-bits checksum of content is written at end of frame (default:0) */ + ZSTD_c_dictIDFlag=202, /* When applicable, dictionary's ID is written into frame header (default:1) */ + + /* multi-threading parameters */ + /* These parameters are only active if multi-threading is enabled (compiled with build macro ZSTD_MULTITHREAD). + * Otherwise, trying to set any other value than default (0) will be a no-op and return an error. + * In a situation where it's unknown if the linked library supports multi-threading or not, + * setting ZSTD_c_nbWorkers to any value >= 1 and consulting the return value provides a quick way to check this property. + */ + ZSTD_c_nbWorkers=400, /* Select how many threads will be spawned to compress in parallel. + * When nbWorkers >= 1, triggers asynchronous mode when invoking ZSTD_compressStream*() : + * ZSTD_compressStream*() consumes input and flush output if possible, but immediately gives back control to caller, + * while compression is performed in parallel, within worker thread(s). + * (note : a strong exception to this rule is when first invocation of ZSTD_compressStream2() sets ZSTD_e_end : + * in which case, ZSTD_compressStream2() delegates to ZSTD_compress2(), which is always a blocking call). + * More workers improve speed, but also increase memory usage. + * Default value is `0`, aka "single-threaded mode" : no worker is spawned, + * compression is performed inside Caller's thread, and all invocations are blocking */ + ZSTD_c_jobSize=401, /* Size of a compression job. This value is enforced only when nbWorkers >= 1. + * Each compression job is completed in parallel, so this value can indirectly impact the nb of active threads. + * 0 means default, which is dynamically determined based on compression parameters. + * Job size must be a minimum of overlap size, or ZSTDMT_JOBSIZE_MIN (= 512 KB), whichever is largest. + * The minimum size is automatically and transparently enforced. */ + ZSTD_c_overlapLog=402, /* Control the overlap size, as a fraction of window size. + * The overlap size is an amount of data reloaded from previous job at the beginning of a new job. + * It helps preserve compression ratio, while each job is compressed in parallel. + * This value is enforced only when nbWorkers >= 1. + * Larger values increase compression ratio, but decrease speed. + * Possible values range from 0 to 9 : + * - 0 means "default" : value will be determined by the library, depending on strategy + * - 1 means "no overlap" + * - 9 means "full overlap", using a full window size. + * Each intermediate rank increases/decreases load size by a factor 2 : + * 9: full window; 8: w/2; 7: w/4; 6: w/8; 5:w/16; 4: w/32; 3:w/64; 2:w/128; 1:no overlap; 0:default + * default value varies between 6 and 9, depending on strategy */ + + /* note : additional experimental parameters are also available + * within the experimental section of the API. + * At the time of this writing, they include : + * ZSTD_c_rsyncable + * ZSTD_c_format + * ZSTD_c_forceMaxWindow + * ZSTD_c_forceAttachDict + * ZSTD_c_literalCompressionMode + * ZSTD_c_srcSizeHint + * ZSTD_c_enableDedicatedDictSearch + * ZSTD_c_stableInBuffer + * ZSTD_c_stableOutBuffer + * ZSTD_c_blockDelimiters + * ZSTD_c_validateSequences + * ZSTD_c_useBlockSplitter + * ZSTD_c_useRowMatchFinder + * ZSTD_c_prefetchCDictTables + * ZSTD_c_enableSeqProducerFallback + * ZSTD_c_maxBlockSize + * Because they are not stable, it's necessary to define ZSTD_STATIC_LINKING_ONLY to access them. + * note : never ever use experimentalParam? names directly; + * also, the enums values themselves are unstable and can still change. + */ + ZSTD_c_experimentalParam1=500, + ZSTD_c_experimentalParam2=10, + ZSTD_c_experimentalParam3=1000, + ZSTD_c_experimentalParam4=1001, + ZSTD_c_experimentalParam5=1002, + /* was ZSTD_c_experimentalParam6=1003; is now ZSTD_c_targetCBlockSize */ + ZSTD_c_experimentalParam7=1004, + ZSTD_c_experimentalParam8=1005, + ZSTD_c_experimentalParam9=1006, + ZSTD_c_experimentalParam10=1007, + ZSTD_c_experimentalParam11=1008, + ZSTD_c_experimentalParam12=1009, + ZSTD_c_experimentalParam13=1010, + ZSTD_c_experimentalParam14=1011, + ZSTD_c_experimentalParam15=1012, + ZSTD_c_experimentalParam16=1013, + ZSTD_c_experimentalParam17=1014, + ZSTD_c_experimentalParam18=1015, + ZSTD_c_experimentalParam19=1016 +} ZSTD_cParameter; + +typedef struct { + size_t error; + int lowerBound; + int upperBound; +} ZSTD_bounds; + +/*! ZSTD_cParam_getBounds() : + * All parameters must belong to an interval with lower and upper bounds, + * otherwise they will either trigger an error or be automatically clamped. + * @return : a structure, ZSTD_bounds, which contains + * - an error status field, which must be tested using ZSTD_isError() + * - lower and upper bounds, both inclusive + */ +ZSTDLIB_API ZSTD_bounds ZSTD_cParam_getBounds(ZSTD_cParameter cParam); + +/*! ZSTD_CCtx_setParameter() : + * Set one compression parameter, selected by enum ZSTD_cParameter. + * All parameters have valid bounds. Bounds can be queried using ZSTD_cParam_getBounds(). + * Providing a value beyond bound will either clamp it, or trigger an error (depending on parameter). + * Setting a parameter is generally only possible during frame initialization (before starting compression). + * Exception : when using multi-threading mode (nbWorkers >= 1), + * the following parameters can be updated _during_ compression (within same frame): + * => compressionLevel, hashLog, chainLog, searchLog, minMatch, targetLength and strategy. + * new parameters will be active for next job only (after a flush()). + * @return : an error code (which can be tested using ZSTD_isError()). + */ +ZSTDLIB_API size_t ZSTD_CCtx_setParameter(ZSTD_CCtx* cctx, ZSTD_cParameter param, int value); + +/*! ZSTD_CCtx_setPledgedSrcSize() : + * Total input data size to be compressed as a single frame. + * Value will be written in frame header, unless if explicitly forbidden using ZSTD_c_contentSizeFlag. + * This value will also be controlled at end of frame, and trigger an error if not respected. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Note 1 : pledgedSrcSize==0 actually means zero, aka an empty frame. + * In order to mean "unknown content size", pass constant ZSTD_CONTENTSIZE_UNKNOWN. + * ZSTD_CONTENTSIZE_UNKNOWN is default value for any new frame. + * Note 2 : pledgedSrcSize is only valid once, for the next frame. + * It's discarded at the end of the frame, and replaced by ZSTD_CONTENTSIZE_UNKNOWN. + * Note 3 : Whenever all input data is provided and consumed in a single round, + * for example with ZSTD_compress2(), + * or invoking immediately ZSTD_compressStream2(,,,ZSTD_e_end), + * this value is automatically overridden by srcSize instead. + */ +ZSTDLIB_API size_t ZSTD_CCtx_setPledgedSrcSize(ZSTD_CCtx* cctx, unsigned long long pledgedSrcSize); + +typedef enum { + ZSTD_reset_session_only = 1, + ZSTD_reset_parameters = 2, + ZSTD_reset_session_and_parameters = 3 +} ZSTD_ResetDirective; + +/*! ZSTD_CCtx_reset() : + * There are 2 different things that can be reset, independently or jointly : + * - The session : will stop compressing current frame, and make CCtx ready to start a new one. + * Useful after an error, or to interrupt any ongoing compression. + * Any internal data not yet flushed is cancelled. + * Compression parameters and dictionary remain unchanged. + * They will be used to compress next frame. + * Resetting session never fails. + * - The parameters : changes all parameters back to "default". + * This also removes any reference to any dictionary or external sequence producer. + * Parameters can only be changed between 2 sessions (i.e. no compression is currently ongoing) + * otherwise the reset fails, and function returns an error value (which can be tested using ZSTD_isError()) + * - Both : similar to resetting the session, followed by resetting parameters. + */ +ZSTDLIB_API size_t ZSTD_CCtx_reset(ZSTD_CCtx* cctx, ZSTD_ResetDirective reset); + +/*! ZSTD_compress2() : + * Behave the same as ZSTD_compressCCtx(), but compression parameters are set using the advanced API. + * (note that this entry point doesn't even expose a compression level parameter). + * ZSTD_compress2() always starts a new frame. + * Should cctx hold data from a previously unfinished frame, everything about it is forgotten. + * - Compression parameters are pushed into CCtx before starting compression, using ZSTD_CCtx_set*() + * - The function is always blocking, returns when compression is completed. + * NOTE: Providing `dstCapacity >= ZSTD_compressBound(srcSize)` guarantees that zstd will have + * enough space to successfully compress the data, though it is possible it fails for other reasons. + * @return : compressed size written into `dst` (<= `dstCapacity), + * or an error code if it fails (which can be tested using ZSTD_isError()). + */ +ZSTDLIB_API size_t ZSTD_compress2( ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize); + + +/*********************************************** +* Advanced decompression API (Requires v1.4.0+) +************************************************/ + +/* The advanced API pushes parameters one by one into an existing DCtx context. + * Parameters are sticky, and remain valid for all following frames + * using the same DCtx context. + * It's possible to reset parameters to default values using ZSTD_DCtx_reset(). + * Note : This API is compatible with existing ZSTD_decompressDCtx() and ZSTD_decompressStream(). + * Therefore, no new decompression function is necessary. + */ + +typedef enum { + + ZSTD_d_windowLogMax=100, /* Select a size limit (in power of 2) beyond which + * the streaming API will refuse to allocate memory buffer + * in order to protect the host from unreasonable memory requirements. + * This parameter is only useful in streaming mode, since no internal buffer is allocated in single-pass mode. + * By default, a decompression context accepts window sizes <= (1 << ZSTD_WINDOWLOG_LIMIT_DEFAULT). + * Special: value 0 means "use default maximum windowLog". */ + + /* note : additional experimental parameters are also available + * within the experimental section of the API. + * At the time of this writing, they include : + * ZSTD_d_format + * ZSTD_d_stableOutBuffer + * ZSTD_d_forceIgnoreChecksum + * ZSTD_d_refMultipleDDicts + * ZSTD_d_disableHuffmanAssembly + * ZSTD_d_maxBlockSize + * Because they are not stable, it's necessary to define ZSTD_STATIC_LINKING_ONLY to access them. + * note : never ever use experimentalParam? names directly + */ + ZSTD_d_experimentalParam1=1000, + ZSTD_d_experimentalParam2=1001, + ZSTD_d_experimentalParam3=1002, + ZSTD_d_experimentalParam4=1003, + ZSTD_d_experimentalParam5=1004, + ZSTD_d_experimentalParam6=1005 + +} ZSTD_dParameter; + +/*! ZSTD_dParam_getBounds() : + * All parameters must belong to an interval with lower and upper bounds, + * otherwise they will either trigger an error or be automatically clamped. + * @return : a structure, ZSTD_bounds, which contains + * - an error status field, which must be tested using ZSTD_isError() + * - both lower and upper bounds, inclusive + */ +ZSTDLIB_API ZSTD_bounds ZSTD_dParam_getBounds(ZSTD_dParameter dParam); + +/*! ZSTD_DCtx_setParameter() : + * Set one compression parameter, selected by enum ZSTD_dParameter. + * All parameters have valid bounds. Bounds can be queried using ZSTD_dParam_getBounds(). + * Providing a value beyond bound will either clamp it, or trigger an error (depending on parameter). + * Setting a parameter is only possible during frame initialization (before starting decompression). + * @return : 0, or an error code (which can be tested using ZSTD_isError()). + */ +ZSTDLIB_API size_t ZSTD_DCtx_setParameter(ZSTD_DCtx* dctx, ZSTD_dParameter param, int value); + +/*! ZSTD_DCtx_reset() : + * Return a DCtx to clean state. + * Session and parameters can be reset jointly or separately. + * Parameters can only be reset when no active frame is being decompressed. + * @return : 0, or an error code, which can be tested with ZSTD_isError() + */ +ZSTDLIB_API size_t ZSTD_DCtx_reset(ZSTD_DCtx* dctx, ZSTD_ResetDirective reset); + + +/**************************** +* Streaming +****************************/ + +typedef struct ZSTD_inBuffer_s { + const void* src; /**< start of input buffer */ + size_t size; /**< size of input buffer */ + size_t pos; /**< position where reading stopped. Will be updated. Necessarily 0 <= pos <= size */ +} ZSTD_inBuffer; + +typedef struct ZSTD_outBuffer_s { + void* dst; /**< start of output buffer */ + size_t size; /**< size of output buffer */ + size_t pos; /**< position where writing stopped. Will be updated. Necessarily 0 <= pos <= size */ +} ZSTD_outBuffer; + + + +/*-*********************************************************************** +* Streaming compression - HowTo +* +* A ZSTD_CStream object is required to track streaming operation. +* Use ZSTD_createCStream() and ZSTD_freeCStream() to create/release resources. +* ZSTD_CStream objects can be reused multiple times on consecutive compression operations. +* It is recommended to reuse ZSTD_CStream since it will play nicer with system's memory, by re-using already allocated memory. +* +* For parallel execution, use one separate ZSTD_CStream per thread. +* +* note : since v1.3.0, ZSTD_CStream and ZSTD_CCtx are the same thing. +* +* Parameters are sticky : when starting a new compression on the same context, +* it will reuse the same sticky parameters as previous compression session. +* When in doubt, it's recommended to fully initialize the context before usage. +* Use ZSTD_CCtx_reset() to reset the context and ZSTD_CCtx_setParameter(), +* ZSTD_CCtx_setPledgedSrcSize(), or ZSTD_CCtx_loadDictionary() and friends to +* set more specific parameters, the pledged source size, or load a dictionary. +* +* Use ZSTD_compressStream2() with ZSTD_e_continue as many times as necessary to +* consume input stream. The function will automatically update both `pos` +* fields within `input` and `output`. +* Note that the function may not consume the entire input, for example, because +* the output buffer is already full, in which case `input.pos < input.size`. +* The caller must check if input has been entirely consumed. +* If not, the caller must make some room to receive more compressed data, +* and then present again remaining input data. +* note: ZSTD_e_continue is guaranteed to make some forward progress when called, +* but doesn't guarantee maximal forward progress. This is especially relevant +* when compressing with multiple threads. The call won't block if it can +* consume some input, but if it can't it will wait for some, but not all, +* output to be flushed. +* @return : provides a minimum amount of data remaining to be flushed from internal buffers +* or an error code, which can be tested using ZSTD_isError(). +* +* At any moment, it's possible to flush whatever data might remain stuck within internal buffer, +* using ZSTD_compressStream2() with ZSTD_e_flush. `output->pos` will be updated. +* Note that, if `output->size` is too small, a single invocation with ZSTD_e_flush might not be enough (return code > 0). +* In which case, make some room to receive more compressed data, and call again ZSTD_compressStream2() with ZSTD_e_flush. +* You must continue calling ZSTD_compressStream2() with ZSTD_e_flush until it returns 0, at which point you can change the +* operation. +* note: ZSTD_e_flush will flush as much output as possible, meaning when compressing with multiple threads, it will +* block until the flush is complete or the output buffer is full. +* @return : 0 if internal buffers are entirely flushed, +* >0 if some data still present within internal buffer (the value is minimal estimation of remaining size), +* or an error code, which can be tested using ZSTD_isError(). +* +* Calling ZSTD_compressStream2() with ZSTD_e_end instructs to finish a frame. +* It will perform a flush and write frame epilogue. +* The epilogue is required for decoders to consider a frame completed. +* flush operation is the same, and follows same rules as calling ZSTD_compressStream2() with ZSTD_e_flush. +* You must continue calling ZSTD_compressStream2() with ZSTD_e_end until it returns 0, at which point you are free to +* start a new frame. +* note: ZSTD_e_end will flush as much output as possible, meaning when compressing with multiple threads, it will +* block until the flush is complete or the output buffer is full. +* @return : 0 if frame fully completed and fully flushed, +* >0 if some data still present within internal buffer (the value is minimal estimation of remaining size), +* or an error code, which can be tested using ZSTD_isError(). +* +* *******************************************************************/ + +typedef ZSTD_CCtx ZSTD_CStream; /**< CCtx and CStream are now effectively same object (>= v1.3.0) */ + /* Continue to distinguish them for compatibility with older versions <= v1.2.0 */ +/*===== ZSTD_CStream management functions =====*/ +ZSTDLIB_API ZSTD_CStream* ZSTD_createCStream(void); +ZSTDLIB_API size_t ZSTD_freeCStream(ZSTD_CStream* zcs); /* accept NULL pointer */ + +/*===== Streaming compression functions =====*/ +typedef enum { + ZSTD_e_continue=0, /* collect more data, encoder decides when to output compressed result, for optimal compression ratio */ + ZSTD_e_flush=1, /* flush any data provided so far, + * it creates (at least) one new block, that can be decoded immediately on reception; + * frame will continue: any future data can still reference previously compressed data, improving compression. + * note : multithreaded compression will block to flush as much output as possible. */ + ZSTD_e_end=2 /* flush any remaining data _and_ close current frame. + * note that frame is only closed after compressed data is fully flushed (return value == 0). + * After that point, any additional data starts a new frame. + * note : each frame is independent (does not reference any content from previous frame). + : note : multithreaded compression will block to flush as much output as possible. */ +} ZSTD_EndDirective; + +/*! ZSTD_compressStream2() : Requires v1.4.0+ + * Behaves about the same as ZSTD_compressStream, with additional control on end directive. + * - Compression parameters are pushed into CCtx before starting compression, using ZSTD_CCtx_set*() + * - Compression parameters cannot be changed once compression is started (save a list of exceptions in multi-threading mode) + * - output->pos must be <= dstCapacity, input->pos must be <= srcSize + * - output->pos and input->pos will be updated. They are guaranteed to remain below their respective limit. + * - endOp must be a valid directive + * - When nbWorkers==0 (default), function is blocking : it completes its job before returning to caller. + * - When nbWorkers>=1, function is non-blocking : it copies a portion of input, distributes jobs to internal worker threads, flush to output whatever is available, + * and then immediately returns, just indicating that there is some data remaining to be flushed. + * The function nonetheless guarantees forward progress : it will return only after it reads or write at least 1+ byte. + * - Exception : if the first call requests a ZSTD_e_end directive and provides enough dstCapacity, the function delegates to ZSTD_compress2() which is always blocking. + * - @return provides a minimum amount of data remaining to be flushed from internal buffers + * or an error code, which can be tested using ZSTD_isError(). + * if @return != 0, flush is not fully completed, there is still some data left within internal buffers. + * This is useful for ZSTD_e_flush, since in this case more flushes are necessary to empty all buffers. + * For ZSTD_e_end, @return == 0 when internal buffers are fully flushed and frame is completed. + * - after a ZSTD_e_end directive, if internal buffer is not fully flushed (@return != 0), + * only ZSTD_e_end or ZSTD_e_flush operations are allowed. + * Before starting a new compression job, or changing compression parameters, + * it is required to fully flush internal buffers. + * - note: if an operation ends with an error, it may leave @cctx in an undefined state. + * Therefore, it's UB to invoke ZSTD_compressStream2() of ZSTD_compressStream() on such a state. + * In order to be re-employed after an error, a state must be reset, + * which can be done explicitly (ZSTD_CCtx_reset()), + * or is sometimes implied by methods starting a new compression job (ZSTD_initCStream(), ZSTD_compressCCtx()) + */ +ZSTDLIB_API size_t ZSTD_compressStream2( ZSTD_CCtx* cctx, + ZSTD_outBuffer* output, + ZSTD_inBuffer* input, + ZSTD_EndDirective endOp); + + +/* These buffer sizes are softly recommended. + * They are not required : ZSTD_compressStream*() happily accepts any buffer size, for both input and output. + * Respecting the recommended size just makes it a bit easier for ZSTD_compressStream*(), + * reducing the amount of memory shuffling and buffering, resulting in minor performance savings. + * + * However, note that these recommendations are from the perspective of a C caller program. + * If the streaming interface is invoked from some other language, + * especially managed ones such as Java or Go, through a foreign function interface such as jni or cgo, + * a major performance rule is to reduce crossing such interface to an absolute minimum. + * It's not rare that performance ends being spent more into the interface, rather than compression itself. + * In which cases, prefer using large buffers, as large as practical, + * for both input and output, to reduce the nb of roundtrips. + */ +ZSTDLIB_API size_t ZSTD_CStreamInSize(void); /**< recommended size for input buffer */ +ZSTDLIB_API size_t ZSTD_CStreamOutSize(void); /**< recommended size for output buffer. Guarantee to successfully flush at least one complete compressed block. */ + + +/* ***************************************************************************** + * This following is a legacy streaming API, available since v1.0+ . + * It can be replaced by ZSTD_CCtx_reset() and ZSTD_compressStream2(). + * It is redundant, but remains fully supported. + ******************************************************************************/ + +/*! + * Equivalent to: + * + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_refCDict(zcs, NULL); // clear the dictionary (if any) + * ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel); + * + * Note that ZSTD_initCStream() clears any previously set dictionary. Use the new API + * to compress with a dictionary. + */ +ZSTDLIB_API size_t ZSTD_initCStream(ZSTD_CStream* zcs, int compressionLevel); +/*! + * Alternative for ZSTD_compressStream2(zcs, output, input, ZSTD_e_continue). + * NOTE: The return value is different. ZSTD_compressStream() returns a hint for + * the next read size (if non-zero and not an error). ZSTD_compressStream2() + * returns the minimum nb of bytes left to flush (if non-zero and not an error). + */ +ZSTDLIB_API size_t ZSTD_compressStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output, ZSTD_inBuffer* input); +/*! Equivalent to ZSTD_compressStream2(zcs, output, &emptyInput, ZSTD_e_flush). */ +ZSTDLIB_API size_t ZSTD_flushStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output); +/*! Equivalent to ZSTD_compressStream2(zcs, output, &emptyInput, ZSTD_e_end). */ +ZSTDLIB_API size_t ZSTD_endStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output); + + +/*-*************************************************************************** +* Streaming decompression - HowTo +* +* A ZSTD_DStream object is required to track streaming operations. +* Use ZSTD_createDStream() and ZSTD_freeDStream() to create/release resources. +* ZSTD_DStream objects can be reused multiple times. +* +* Use ZSTD_initDStream() to start a new decompression operation. +* @return : recommended first input size +* Alternatively, use advanced API to set specific properties. +* +* Use ZSTD_decompressStream() repetitively to consume your input. +* The function will update both `pos` fields. +* If `input.pos < input.size`, some input has not been consumed. +* It's up to the caller to present again remaining data. +* The function tries to flush all data decoded immediately, respecting output buffer size. +* If `output.pos < output.size`, decoder has flushed everything it could. +* But if `output.pos == output.size`, there might be some data left within internal buffers., +* In which case, call ZSTD_decompressStream() again to flush whatever remains in the buffer. +* Note : with no additional input provided, amount of data flushed is necessarily <= ZSTD_BLOCKSIZE_MAX. +* @return : 0 when a frame is completely decoded and fully flushed, +* or an error code, which can be tested using ZSTD_isError(), +* or any other value > 0, which means there is still some decoding or flushing to do to complete current frame : +* the return value is a suggested next input size (just a hint for better latency) +* that will never request more than the remaining frame size. +* *******************************************************************************/ + +typedef ZSTD_DCtx ZSTD_DStream; /**< DCtx and DStream are now effectively same object (>= v1.3.0) */ + /* For compatibility with versions <= v1.2.0, prefer differentiating them. */ +/*===== ZSTD_DStream management functions =====*/ +ZSTDLIB_API ZSTD_DStream* ZSTD_createDStream(void); +ZSTDLIB_API size_t ZSTD_freeDStream(ZSTD_DStream* zds); /* accept NULL pointer */ + +/*===== Streaming decompression functions =====*/ + +/*! ZSTD_initDStream() : + * Initialize/reset DStream state for new decompression operation. + * Call before new decompression operation using same DStream. + * + * Note : This function is redundant with the advanced API and equivalent to: + * ZSTD_DCtx_reset(zds, ZSTD_reset_session_only); + * ZSTD_DCtx_refDDict(zds, NULL); + */ +ZSTDLIB_API size_t ZSTD_initDStream(ZSTD_DStream* zds); + +/*! ZSTD_decompressStream() : + * Streaming decompression function. + * Call repetitively to consume full input updating it as necessary. + * Function will update both input and output `pos` fields exposing current state via these fields: + * - `input.pos < input.size`, some input remaining and caller should provide remaining input + * on the next call. + * - `output.pos < output.size`, decoder finished and flushed all remaining buffers. + * - `output.pos == output.size`, potentially uncflushed data present in the internal buffers, + * call ZSTD_decompressStream() again to flush remaining data to output. + * Note : with no additional input, amount of data flushed <= ZSTD_BLOCKSIZE_MAX. + * + * @return : 0 when a frame is completely decoded and fully flushed, + * or an error code, which can be tested using ZSTD_isError(), + * or any other value > 0, which means there is some decoding or flushing to do to complete current frame. + * + * Note: when an operation returns with an error code, the @zds state may be left in undefined state. + * It's UB to invoke `ZSTD_decompressStream()` on such a state. + * In order to re-use such a state, it must be first reset, + * which can be done explicitly (`ZSTD_DCtx_reset()`), + * or is implied for operations starting some new decompression job (`ZSTD_initDStream`, `ZSTD_decompressDCtx()`, `ZSTD_decompress_usingDict()`) + */ +ZSTDLIB_API size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inBuffer* input); + +ZSTDLIB_API size_t ZSTD_DStreamInSize(void); /*!< recommended size for input buffer */ +ZSTDLIB_API size_t ZSTD_DStreamOutSize(void); /*!< recommended size for output buffer. Guarantee to successfully flush at least one complete block in all circumstances. */ + + +/************************** +* Simple dictionary API +***************************/ +/*! ZSTD_compress_usingDict() : + * Compression at an explicit compression level using a Dictionary. + * A dictionary can be any arbitrary data segment (also called a prefix), + * or a buffer with specified information (see zdict.h). + * Note : This function loads the dictionary, resulting in significant startup delay. + * It's intended for a dictionary used only once. + * Note 2 : When `dict == NULL || dictSize < 8` no dictionary is used. */ +ZSTDLIB_API size_t ZSTD_compress_usingDict(ZSTD_CCtx* ctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize, + int compressionLevel); + +/*! ZSTD_decompress_usingDict() : + * Decompression using a known Dictionary. + * Dictionary must be identical to the one used during compression. + * Note : This function loads the dictionary, resulting in significant startup delay. + * It's intended for a dictionary used only once. + * Note : When `dict == NULL || dictSize < 8` no dictionary is used. */ +ZSTDLIB_API size_t ZSTD_decompress_usingDict(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize); + + +/*********************************** + * Bulk processing dictionary API + **********************************/ +typedef struct ZSTD_CDict_s ZSTD_CDict; + +/*! ZSTD_createCDict() : + * When compressing multiple messages or blocks using the same dictionary, + * it's recommended to digest the dictionary only once, since it's a costly operation. + * ZSTD_createCDict() will create a state from digesting a dictionary. + * The resulting state can be used for future compression operations with very limited startup cost. + * ZSTD_CDict can be created once and shared by multiple threads concurrently, since its usage is read-only. + * @dictBuffer can be released after ZSTD_CDict creation, because its content is copied within CDict. + * Note 1 : Consider experimental function `ZSTD_createCDict_byReference()` if you prefer to not duplicate @dictBuffer content. + * Note 2 : A ZSTD_CDict can be created from an empty @dictBuffer, + * in which case the only thing that it transports is the @compressionLevel. + * This can be useful in a pipeline featuring ZSTD_compress_usingCDict() exclusively, + * expecting a ZSTD_CDict parameter with any data, including those without a known dictionary. */ +ZSTDLIB_API ZSTD_CDict* ZSTD_createCDict(const void* dictBuffer, size_t dictSize, + int compressionLevel); + +/*! ZSTD_freeCDict() : + * Function frees memory allocated by ZSTD_createCDict(). + * If a NULL pointer is passed, no operation is performed. */ +ZSTDLIB_API size_t ZSTD_freeCDict(ZSTD_CDict* CDict); + +/*! ZSTD_compress_usingCDict() : + * Compression using a digested Dictionary. + * Recommended when same dictionary is used multiple times. + * Note : compression level is _decided at dictionary creation time_, + * and frame parameters are hardcoded (dictID=yes, contentSize=yes, checksum=no) */ +ZSTDLIB_API size_t ZSTD_compress_usingCDict(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_CDict* cdict); + + +typedef struct ZSTD_DDict_s ZSTD_DDict; + +/*! ZSTD_createDDict() : + * Create a digested dictionary, ready to start decompression operation without startup delay. + * dictBuffer can be released after DDict creation, as its content is copied inside DDict. */ +ZSTDLIB_API ZSTD_DDict* ZSTD_createDDict(const void* dictBuffer, size_t dictSize); + +/*! ZSTD_freeDDict() : + * Function frees memory allocated with ZSTD_createDDict() + * If a NULL pointer is passed, no operation is performed. */ +ZSTDLIB_API size_t ZSTD_freeDDict(ZSTD_DDict* ddict); + +/*! ZSTD_decompress_usingDDict() : + * Decompression using a digested Dictionary. + * Recommended when same dictionary is used multiple times. */ +ZSTDLIB_API size_t ZSTD_decompress_usingDDict(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_DDict* ddict); + + +/******************************** + * Dictionary helper functions + *******************************/ + +/*! ZSTD_getDictID_fromDict() : Requires v1.4.0+ + * Provides the dictID stored within dictionary. + * if @return == 0, the dictionary is not conformant with Zstandard specification. + * It can still be loaded, but as a content-only dictionary. */ +ZSTDLIB_API unsigned ZSTD_getDictID_fromDict(const void* dict, size_t dictSize); + +/*! ZSTD_getDictID_fromCDict() : Requires v1.5.0+ + * Provides the dictID of the dictionary loaded into `cdict`. + * If @return == 0, the dictionary is not conformant to Zstandard specification, or empty. + * Non-conformant dictionaries can still be loaded, but as content-only dictionaries. */ +ZSTDLIB_API unsigned ZSTD_getDictID_fromCDict(const ZSTD_CDict* cdict); + +/*! ZSTD_getDictID_fromDDict() : Requires v1.4.0+ + * Provides the dictID of the dictionary loaded into `ddict`. + * If @return == 0, the dictionary is not conformant to Zstandard specification, or empty. + * Non-conformant dictionaries can still be loaded, but as content-only dictionaries. */ +ZSTDLIB_API unsigned ZSTD_getDictID_fromDDict(const ZSTD_DDict* ddict); + +/*! ZSTD_getDictID_fromFrame() : Requires v1.4.0+ + * Provides the dictID required to decompressed the frame stored within `src`. + * If @return == 0, the dictID could not be decoded. + * This could for one of the following reasons : + * - The frame does not require a dictionary to be decoded (most common case). + * - The frame was built with dictID intentionally removed. Whatever dictionary is necessary is a hidden piece of information. + * Note : this use case also happens when using a non-conformant dictionary. + * - `srcSize` is too small, and as a result, the frame header could not be decoded (only possible if `srcSize < ZSTD_FRAMEHEADERSIZE_MAX`). + * - This is not a Zstandard frame. + * When identifying the exact failure cause, it's possible to use ZSTD_getFrameHeader(), which will provide a more precise error code. */ +ZSTDLIB_API unsigned ZSTD_getDictID_fromFrame(const void* src, size_t srcSize); + + +/******************************************************************************* + * Advanced dictionary and prefix API (Requires v1.4.0+) + * + * This API allows dictionaries to be used with ZSTD_compress2(), + * ZSTD_compressStream2(), and ZSTD_decompressDCtx(). + * Dictionaries are sticky, they remain valid when same context is reused, + * they only reset when the context is reset + * with ZSTD_reset_parameters or ZSTD_reset_session_and_parameters. + * In contrast, Prefixes are single-use. + ******************************************************************************/ + + +/*! ZSTD_CCtx_loadDictionary() : Requires v1.4.0+ + * Create an internal CDict from `dict` buffer. + * Decompression will have to use same dictionary. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special: Loading a NULL (or 0-size) dictionary invalidates previous dictionary, + * meaning "return to no-dictionary mode". + * Note 1 : Dictionary is sticky, it will be used for all future compressed frames, + * until parameters are reset, a new dictionary is loaded, or the dictionary + * is explicitly invalidated by loading a NULL dictionary. + * Note 2 : Loading a dictionary involves building tables. + * It's also a CPU consuming operation, with non-negligible impact on latency. + * Tables are dependent on compression parameters, and for this reason, + * compression parameters can no longer be changed after loading a dictionary. + * Note 3 :`dict` content will be copied internally. + * Use experimental ZSTD_CCtx_loadDictionary_byReference() to reference content instead. + * In such a case, dictionary buffer must outlive its users. + * Note 4 : Use ZSTD_CCtx_loadDictionary_advanced() + * to precisely select how dictionary content must be interpreted. + * Note 5 : This method does not benefit from LDM (long distance mode). + * If you want to employ LDM on some large dictionary content, + * prefer employing ZSTD_CCtx_refPrefix() described below. + */ +ZSTDLIB_API size_t ZSTD_CCtx_loadDictionary(ZSTD_CCtx* cctx, const void* dict, size_t dictSize); + +/*! ZSTD_CCtx_refCDict() : Requires v1.4.0+ + * Reference a prepared dictionary, to be used for all future compressed frames. + * Note that compression parameters are enforced from within CDict, + * and supersede any compression parameter previously set within CCtx. + * The parameters ignored are labelled as "superseded-by-cdict" in the ZSTD_cParameter enum docs. + * The ignored parameters will be used again if the CCtx is returned to no-dictionary mode. + * The dictionary will remain valid for future compressed frames using same CCtx. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special : Referencing a NULL CDict means "return to no-dictionary mode". + * Note 1 : Currently, only one dictionary can be managed. + * Referencing a new dictionary effectively "discards" any previous one. + * Note 2 : CDict is just referenced, its lifetime must outlive its usage within CCtx. */ +ZSTDLIB_API size_t ZSTD_CCtx_refCDict(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict); + +/*! ZSTD_CCtx_refPrefix() : Requires v1.4.0+ + * Reference a prefix (single-usage dictionary) for next compressed frame. + * A prefix is **only used once**. Tables are discarded at end of frame (ZSTD_e_end). + * Decompression will need same prefix to properly regenerate data. + * Compressing with a prefix is similar in outcome as performing a diff and compressing it, + * but performs much faster, especially during decompression (compression speed is tunable with compression level). + * This method is compatible with LDM (long distance mode). + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special: Adding any prefix (including NULL) invalidates any previous prefix or dictionary + * Note 1 : Prefix buffer is referenced. It **must** outlive compression. + * Its content must remain unmodified during compression. + * Note 2 : If the intention is to diff some large src data blob with some prior version of itself, + * ensure that the window size is large enough to contain the entire source. + * See ZSTD_c_windowLog. + * Note 3 : Referencing a prefix involves building tables, which are dependent on compression parameters. + * It's a CPU consuming operation, with non-negligible impact on latency. + * If there is a need to use the same prefix multiple times, consider loadDictionary instead. + * Note 4 : By default, the prefix is interpreted as raw content (ZSTD_dct_rawContent). + * Use experimental ZSTD_CCtx_refPrefix_advanced() to alter dictionary interpretation. */ +ZSTDLIB_API size_t ZSTD_CCtx_refPrefix(ZSTD_CCtx* cctx, + const void* prefix, size_t prefixSize); + +/*! ZSTD_DCtx_loadDictionary() : Requires v1.4.0+ + * Create an internal DDict from dict buffer, to be used to decompress all future frames. + * The dictionary remains valid for all future frames, until explicitly invalidated, or + * a new dictionary is loaded. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special : Adding a NULL (or 0-size) dictionary invalidates any previous dictionary, + * meaning "return to no-dictionary mode". + * Note 1 : Loading a dictionary involves building tables, + * which has a non-negligible impact on CPU usage and latency. + * It's recommended to "load once, use many times", to amortize the cost + * Note 2 :`dict` content will be copied internally, so `dict` can be released after loading. + * Use ZSTD_DCtx_loadDictionary_byReference() to reference dictionary content instead. + * Note 3 : Use ZSTD_DCtx_loadDictionary_advanced() to take control of + * how dictionary content is loaded and interpreted. + */ +ZSTDLIB_API size_t ZSTD_DCtx_loadDictionary(ZSTD_DCtx* dctx, const void* dict, size_t dictSize); + +/*! ZSTD_DCtx_refDDict() : Requires v1.4.0+ + * Reference a prepared dictionary, to be used to decompress next frames. + * The dictionary remains active for decompression of future frames using same DCtx. + * + * If called with ZSTD_d_refMultipleDDicts enabled, repeated calls of this function + * will store the DDict references in a table, and the DDict used for decompression + * will be determined at decompression time, as per the dict ID in the frame. + * The memory for the table is allocated on the first call to refDDict, and can be + * freed with ZSTD_freeDCtx(). + * + * If called with ZSTD_d_refMultipleDDicts disabled (the default), only one dictionary + * will be managed, and referencing a dictionary effectively "discards" any previous one. + * + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special: referencing a NULL DDict means "return to no-dictionary mode". + * Note 2 : DDict is just referenced, its lifetime must outlive its usage from DCtx. + */ +ZSTDLIB_API size_t ZSTD_DCtx_refDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict); + +/*! ZSTD_DCtx_refPrefix() : Requires v1.4.0+ + * Reference a prefix (single-usage dictionary) to decompress next frame. + * This is the reverse operation of ZSTD_CCtx_refPrefix(), + * and must use the same prefix as the one used during compression. + * Prefix is **only used once**. Reference is discarded at end of frame. + * End of frame is reached when ZSTD_decompressStream() returns 0. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Note 1 : Adding any prefix (including NULL) invalidates any previously set prefix or dictionary + * Note 2 : Prefix buffer is referenced. It **must** outlive decompression. + * Prefix buffer must remain unmodified up to the end of frame, + * reached when ZSTD_decompressStream() returns 0. + * Note 3 : By default, the prefix is treated as raw content (ZSTD_dct_rawContent). + * Use ZSTD_CCtx_refPrefix_advanced() to alter dictMode (Experimental section) + * Note 4 : Referencing a raw content prefix has almost no cpu nor memory cost. + * A full dictionary is more costly, as it requires building tables. + */ +ZSTDLIB_API size_t ZSTD_DCtx_refPrefix(ZSTD_DCtx* dctx, + const void* prefix, size_t prefixSize); + +/* === Memory management === */ + +/*! ZSTD_sizeof_*() : Requires v1.4.0+ + * These functions give the _current_ memory usage of selected object. + * Note that object memory usage can evolve (increase or decrease) over time. */ +ZSTDLIB_API size_t ZSTD_sizeof_CCtx(const ZSTD_CCtx* cctx); +ZSTDLIB_API size_t ZSTD_sizeof_DCtx(const ZSTD_DCtx* dctx); +ZSTDLIB_API size_t ZSTD_sizeof_CStream(const ZSTD_CStream* zcs); +ZSTDLIB_API size_t ZSTD_sizeof_DStream(const ZSTD_DStream* zds); +ZSTDLIB_API size_t ZSTD_sizeof_CDict(const ZSTD_CDict* cdict); +ZSTDLIB_API size_t ZSTD_sizeof_DDict(const ZSTD_DDict* ddict); + +#endif /* ZSTD_H_235446 */ + + +/* ************************************************************************************** + * ADVANCED AND EXPERIMENTAL FUNCTIONS + **************************************************************************************** + * The definitions in the following section are considered experimental. + * They are provided for advanced scenarios. + * They should never be used with a dynamic library, as prototypes may change in the future. + * Use them only in association with static linking. + * ***************************************************************************************/ + +#if defined(ZSTD_STATIC_LINKING_ONLY) && !defined(ZSTD_H_ZSTD_STATIC_LINKING_ONLY) +#define ZSTD_H_ZSTD_STATIC_LINKING_ONLY + +/* This can be overridden externally to hide static symbols. */ +#ifndef ZSTDLIB_STATIC_API +# if defined(ZSTD_DLL_EXPORT) && (ZSTD_DLL_EXPORT==1) +# define ZSTDLIB_STATIC_API __declspec(dllexport) ZSTDLIB_VISIBLE +# elif defined(ZSTD_DLL_IMPORT) && (ZSTD_DLL_IMPORT==1) +# define ZSTDLIB_STATIC_API __declspec(dllimport) ZSTDLIB_VISIBLE +# else +# define ZSTDLIB_STATIC_API ZSTDLIB_VISIBLE +# endif +#endif + +/**************************************************************************************** + * experimental API (static linking only) + **************************************************************************************** + * The following symbols and constants + * are not planned to join "stable API" status in the near future. + * They can still change in future versions. + * Some of them are planned to remain in the static_only section indefinitely. + * Some of them might be removed in the future (especially when redundant with existing stable functions) + * ***************************************************************************************/ + +#define ZSTD_FRAMEHEADERSIZE_PREFIX(format) ((format) == ZSTD_f_zstd1 ? 5 : 1) /* minimum input size required to query frame header size */ +#define ZSTD_FRAMEHEADERSIZE_MIN(format) ((format) == ZSTD_f_zstd1 ? 6 : 2) +#define ZSTD_FRAMEHEADERSIZE_MAX 18 /* can be useful for static allocation */ +#define ZSTD_SKIPPABLEHEADERSIZE 8 + +/* compression parameter bounds */ +#define ZSTD_WINDOWLOG_MAX_32 30 +#define ZSTD_WINDOWLOG_MAX_64 31 +#define ZSTD_WINDOWLOG_MAX ((int)(sizeof(size_t) == 4 ? ZSTD_WINDOWLOG_MAX_32 : ZSTD_WINDOWLOG_MAX_64)) +#define ZSTD_WINDOWLOG_MIN 10 +#define ZSTD_HASHLOG_MAX ((ZSTD_WINDOWLOG_MAX < 30) ? ZSTD_WINDOWLOG_MAX : 30) +#define ZSTD_HASHLOG_MIN 6 +#define ZSTD_CHAINLOG_MAX_32 29 +#define ZSTD_CHAINLOG_MAX_64 30 +#define ZSTD_CHAINLOG_MAX ((int)(sizeof(size_t) == 4 ? ZSTD_CHAINLOG_MAX_32 : ZSTD_CHAINLOG_MAX_64)) +#define ZSTD_CHAINLOG_MIN ZSTD_HASHLOG_MIN +#define ZSTD_SEARCHLOG_MAX (ZSTD_WINDOWLOG_MAX-1) +#define ZSTD_SEARCHLOG_MIN 1 +#define ZSTD_MINMATCH_MAX 7 /* only for ZSTD_fast, other strategies are limited to 6 */ +#define ZSTD_MINMATCH_MIN 3 /* only for ZSTD_btopt+, faster strategies are limited to 4 */ +#define ZSTD_TARGETLENGTH_MAX ZSTD_BLOCKSIZE_MAX +#define ZSTD_TARGETLENGTH_MIN 0 /* note : comparing this constant to an unsigned results in a tautological test */ +#define ZSTD_STRATEGY_MIN ZSTD_fast +#define ZSTD_STRATEGY_MAX ZSTD_btultra2 +#define ZSTD_BLOCKSIZE_MAX_MIN (1 << 10) /* The minimum valid max blocksize. Maximum blocksizes smaller than this make compressBound() inaccurate. */ + + +#define ZSTD_OVERLAPLOG_MIN 0 +#define ZSTD_OVERLAPLOG_MAX 9 + +#define ZSTD_WINDOWLOG_LIMIT_DEFAULT 27 /* by default, the streaming decoder will refuse any frame + * requiring larger than (1< 0: + * If litLength != 0: + * rep == 1 --> offset == repeat_offset_1 + * rep == 2 --> offset == repeat_offset_2 + * rep == 3 --> offset == repeat_offset_3 + * If litLength == 0: + * rep == 1 --> offset == repeat_offset_2 + * rep == 2 --> offset == repeat_offset_3 + * rep == 3 --> offset == repeat_offset_1 - 1 + * + * Note: This field is optional. ZSTD_generateSequences() will calculate the value of + * 'rep', but repeat offsets do not necessarily need to be calculated from an external + * sequence provider's perspective. For example, ZSTD_compressSequences() does not + * use this 'rep' field at all (as of now). + */ +} ZSTD_Sequence; + +typedef struct { + unsigned windowLog; /**< largest match distance : larger == more compression, more memory needed during decompression */ + unsigned chainLog; /**< fully searched segment : larger == more compression, slower, more memory (useless for fast) */ + unsigned hashLog; /**< dispatch table : larger == faster, more memory */ + unsigned searchLog; /**< nb of searches : larger == more compression, slower */ + unsigned minMatch; /**< match length searched : larger == faster decompression, sometimes less compression */ + unsigned targetLength; /**< acceptable match size for optimal parser (only) : larger == more compression, slower */ + ZSTD_strategy strategy; /**< see ZSTD_strategy definition above */ +} ZSTD_compressionParameters; + +typedef struct { + int contentSizeFlag; /**< 1: content size will be in frame header (when known) */ + int checksumFlag; /**< 1: generate a 32-bits checksum using XXH64 algorithm at end of frame, for error detection */ + int noDictIDFlag; /**< 1: no dictID will be saved into frame header (dictID is only useful for dictionary compression) */ +} ZSTD_frameParameters; + +typedef struct { + ZSTD_compressionParameters cParams; + ZSTD_frameParameters fParams; +} ZSTD_parameters; + +typedef enum { + ZSTD_dct_auto = 0, /* dictionary is "full" when starting with ZSTD_MAGIC_DICTIONARY, otherwise it is "rawContent" */ + ZSTD_dct_rawContent = 1, /* ensures dictionary is always loaded as rawContent, even if it starts with ZSTD_MAGIC_DICTIONARY */ + ZSTD_dct_fullDict = 2 /* refuses to load a dictionary if it does not respect Zstandard's specification, starting with ZSTD_MAGIC_DICTIONARY */ +} ZSTD_dictContentType_e; + +typedef enum { + ZSTD_dlm_byCopy = 0, /**< Copy dictionary content internally */ + ZSTD_dlm_byRef = 1 /**< Reference dictionary content -- the dictionary buffer must outlive its users. */ +} ZSTD_dictLoadMethod_e; + +typedef enum { + ZSTD_f_zstd1 = 0, /* zstd frame format, specified in zstd_compression_format.md (default) */ + ZSTD_f_zstd1_magicless = 1 /* Variant of zstd frame format, without initial 4-bytes magic number. + * Useful to save 4 bytes per generated frame. + * Decoder cannot recognise automatically this format, requiring this instruction. */ +} ZSTD_format_e; + +typedef enum { + /* Note: this enum controls ZSTD_d_forceIgnoreChecksum */ + ZSTD_d_validateChecksum = 0, + ZSTD_d_ignoreChecksum = 1 +} ZSTD_forceIgnoreChecksum_e; + +typedef enum { + /* Note: this enum controls ZSTD_d_refMultipleDDicts */ + ZSTD_rmd_refSingleDDict = 0, + ZSTD_rmd_refMultipleDDicts = 1 +} ZSTD_refMultipleDDicts_e; + +typedef enum { + /* Note: this enum and the behavior it controls are effectively internal + * implementation details of the compressor. They are expected to continue + * to evolve and should be considered only in the context of extremely + * advanced performance tuning. + * + * Zstd currently supports the use of a CDict in three ways: + * + * - The contents of the CDict can be copied into the working context. This + * means that the compression can search both the dictionary and input + * while operating on a single set of internal tables. This makes + * the compression faster per-byte of input. However, the initial copy of + * the CDict's tables incurs a fixed cost at the beginning of the + * compression. For small compressions (< 8 KB), that copy can dominate + * the cost of the compression. + * + * - The CDict's tables can be used in-place. In this model, compression is + * slower per input byte, because the compressor has to search two sets of + * tables. However, this model incurs no start-up cost (as long as the + * working context's tables can be reused). For small inputs, this can be + * faster than copying the CDict's tables. + * + * - The CDict's tables are not used at all, and instead we use the working + * context alone to reload the dictionary and use params based on the source + * size. See ZSTD_compress_insertDictionary() and ZSTD_compress_usingDict(). + * This method is effective when the dictionary sizes are very small relative + * to the input size, and the input size is fairly large to begin with. + * + * Zstd has a simple internal heuristic that selects which strategy to use + * at the beginning of a compression. However, if experimentation shows that + * Zstd is making poor choices, it is possible to override that choice with + * this enum. + */ + ZSTD_dictDefaultAttach = 0, /* Use the default heuristic. */ + ZSTD_dictForceAttach = 1, /* Never copy the dictionary. */ + ZSTD_dictForceCopy = 2, /* Always copy the dictionary. */ + ZSTD_dictForceLoad = 3 /* Always reload the dictionary */ +} ZSTD_dictAttachPref_e; + +typedef enum { + ZSTD_lcm_auto = 0, /**< Automatically determine the compression mode based on the compression level. + * Negative compression levels will be uncompressed, and positive compression + * levels will be compressed. */ + ZSTD_lcm_huffman = 1, /**< Always attempt Huffman compression. Uncompressed literals will still be + * emitted if Huffman compression is not profitable. */ + ZSTD_lcm_uncompressed = 2 /**< Always emit uncompressed literals. */ +} ZSTD_literalCompressionMode_e; + +typedef enum { + /* Note: This enum controls features which are conditionally beneficial. Zstd typically will make a final + * decision on whether or not to enable the feature (ZSTD_ps_auto), but setting the switch to ZSTD_ps_enable + * or ZSTD_ps_disable allow for a force enable/disable the feature. + */ + ZSTD_ps_auto = 0, /* Let the library automatically determine whether the feature shall be enabled */ + ZSTD_ps_enable = 1, /* Force-enable the feature */ + ZSTD_ps_disable = 2 /* Do not use the feature */ +} ZSTD_paramSwitch_e; + +/*************************************** +* Frame header and size functions +***************************************/ + +/*! ZSTD_findDecompressedSize() : + * `src` should point to the start of a series of ZSTD encoded and/or skippable frames + * `srcSize` must be the _exact_ size of this series + * (i.e. there should be a frame boundary at `src + srcSize`) + * @return : - decompressed size of all data in all successive frames + * - if the decompressed size cannot be determined: ZSTD_CONTENTSIZE_UNKNOWN + * - if an error occurred: ZSTD_CONTENTSIZE_ERROR + * + * note 1 : decompressed size is an optional field, that may not be present, especially in streaming mode. + * When `return==ZSTD_CONTENTSIZE_UNKNOWN`, data to decompress could be any size. + * In which case, it's necessary to use streaming mode to decompress data. + * note 2 : decompressed size is always present when compression is done with ZSTD_compress() + * note 3 : decompressed size can be very large (64-bits value), + * potentially larger than what local system can handle as a single memory segment. + * In which case, it's necessary to use streaming mode to decompress data. + * note 4 : If source is untrusted, decompressed size could be wrong or intentionally modified. + * Always ensure result fits within application's authorized limits. + * Each application can set its own limits. + * note 5 : ZSTD_findDecompressedSize handles multiple frames, and so it must traverse the input to + * read each contained frame header. This is fast as most of the data is skipped, + * however it does mean that all frame data must be present and valid. */ +ZSTDLIB_STATIC_API unsigned long long ZSTD_findDecompressedSize(const void* src, size_t srcSize); + +/*! ZSTD_decompressBound() : + * `src` should point to the start of a series of ZSTD encoded and/or skippable frames + * `srcSize` must be the _exact_ size of this series + * (i.e. there should be a frame boundary at `src + srcSize`) + * @return : - upper-bound for the decompressed size of all data in all successive frames + * - if an error occurred: ZSTD_CONTENTSIZE_ERROR + * + * note 1 : an error can occur if `src` contains an invalid or incorrectly formatted frame. + * note 2 : the upper-bound is exact when the decompressed size field is available in every ZSTD encoded frame of `src`. + * in this case, `ZSTD_findDecompressedSize` and `ZSTD_decompressBound` return the same value. + * note 3 : when the decompressed size field isn't available, the upper-bound for that frame is calculated by: + * upper-bound = # blocks * min(128 KB, Window_Size) + */ +ZSTDLIB_STATIC_API unsigned long long ZSTD_decompressBound(const void* src, size_t srcSize); + +/*! ZSTD_frameHeaderSize() : + * srcSize must be >= ZSTD_FRAMEHEADERSIZE_PREFIX. + * @return : size of the Frame Header, + * or an error code (if srcSize is too small) */ +ZSTDLIB_STATIC_API size_t ZSTD_frameHeaderSize(const void* src, size_t srcSize); + +typedef enum { ZSTD_frame, ZSTD_skippableFrame } ZSTD_frameType_e; +typedef struct { + unsigned long long frameContentSize; /* if == ZSTD_CONTENTSIZE_UNKNOWN, it means this field is not available. 0 means "empty" */ + unsigned long long windowSize; /* can be very large, up to <= frameContentSize */ + unsigned blockSizeMax; + ZSTD_frameType_e frameType; /* if == ZSTD_skippableFrame, frameContentSize is the size of skippable content */ + unsigned headerSize; + unsigned dictID; + unsigned checksumFlag; + unsigned _reserved1; + unsigned _reserved2; +} ZSTD_frameHeader; + +/*! ZSTD_getFrameHeader() : + * decode Frame Header, or requires larger `srcSize`. + * @return : 0, `zfhPtr` is correctly filled, + * >0, `srcSize` is too small, value is wanted `srcSize` amount, + * or an error code, which can be tested using ZSTD_isError() */ +ZSTDLIB_STATIC_API size_t ZSTD_getFrameHeader(ZSTD_frameHeader* zfhPtr, const void* src, size_t srcSize); /**< doesn't consume input */ +/*! ZSTD_getFrameHeader_advanced() : + * same as ZSTD_getFrameHeader(), + * with added capability to select a format (like ZSTD_f_zstd1_magicless) */ +ZSTDLIB_STATIC_API size_t ZSTD_getFrameHeader_advanced(ZSTD_frameHeader* zfhPtr, const void* src, size_t srcSize, ZSTD_format_e format); + +/*! ZSTD_decompressionMargin() : + * Zstd supports in-place decompression, where the input and output buffers overlap. + * In this case, the output buffer must be at least (Margin + Output_Size) bytes large, + * and the input buffer must be at the end of the output buffer. + * + * _______________________ Output Buffer ________________________ + * | | + * | ____ Input Buffer ____| + * | | | + * v v v + * |---------------------------------------|-----------|----------| + * ^ ^ ^ + * |___________________ Output_Size ___________________|_ Margin _| + * + * NOTE: See also ZSTD_DECOMPRESSION_MARGIN(). + * NOTE: This applies only to single-pass decompression through ZSTD_decompress() or + * ZSTD_decompressDCtx(). + * NOTE: This function supports multi-frame input. + * + * @param src The compressed frame(s) + * @param srcSize The size of the compressed frame(s) + * @returns The decompression margin or an error that can be checked with ZSTD_isError(). + */ +ZSTDLIB_STATIC_API size_t ZSTD_decompressionMargin(const void* src, size_t srcSize); + +/*! ZSTD_DECOMPRESS_MARGIN() : + * Similar to ZSTD_decompressionMargin(), but instead of computing the margin from + * the compressed frame, compute it from the original size and the blockSizeLog. + * See ZSTD_decompressionMargin() for details. + * + * WARNING: This macro does not support multi-frame input, the input must be a single + * zstd frame. If you need that support use the function, or implement it yourself. + * + * @param originalSize The original uncompressed size of the data. + * @param blockSize The block size == MIN(windowSize, ZSTD_BLOCKSIZE_MAX). + * Unless you explicitly set the windowLog smaller than + * ZSTD_BLOCKSIZELOG_MAX you can just use ZSTD_BLOCKSIZE_MAX. + */ +#define ZSTD_DECOMPRESSION_MARGIN(originalSize, blockSize) ((size_t)( \ + ZSTD_FRAMEHEADERSIZE_MAX /* Frame header */ + \ + 4 /* checksum */ + \ + ((originalSize) == 0 ? 0 : 3 * (((originalSize) + (blockSize) - 1) / blockSize)) /* 3 bytes per block */ + \ + (blockSize) /* One block of margin */ \ + )) + +typedef enum { + ZSTD_sf_noBlockDelimiters = 0, /* Representation of ZSTD_Sequence has no block delimiters, sequences only */ + ZSTD_sf_explicitBlockDelimiters = 1 /* Representation of ZSTD_Sequence contains explicit block delimiters */ +} ZSTD_sequenceFormat_e; + +/*! ZSTD_sequenceBound() : + * `srcSize` : size of the input buffer + * @return : upper-bound for the number of sequences that can be generated + * from a buffer of srcSize bytes + * + * note : returns number of sequences - to get bytes, multiply by sizeof(ZSTD_Sequence). + */ +ZSTDLIB_STATIC_API size_t ZSTD_sequenceBound(size_t srcSize); + +/*! ZSTD_generateSequences() : + * WARNING: This function is meant for debugging and informational purposes ONLY! + * Its implementation is flawed, and it will be deleted in a future version. + * It is not guaranteed to succeed, as there are several cases where it will give + * up and fail. You should NOT use this function in production code. + * + * This function is deprecated, and will be removed in a future version. + * + * Generate sequences using ZSTD_compress2(), given a source buffer. + * + * @param zc The compression context to be used for ZSTD_compress2(). Set any + * compression parameters you need on this context. + * @param outSeqs The output sequences buffer of size @p outSeqsSize + * @param outSeqsSize The size of the output sequences buffer. + * ZSTD_sequenceBound(srcSize) is an upper bound on the number + * of sequences that can be generated. + * @param src The source buffer to generate sequences from of size @p srcSize. + * @param srcSize The size of the source buffer. + * + * Each block will end with a dummy sequence + * with offset == 0, matchLength == 0, and litLength == length of last literals. + * litLength may be == 0, and if so, then the sequence of (of: 0 ml: 0 ll: 0) + * simply acts as a block delimiter. + * + * @returns The number of sequences generated, necessarily less than + * ZSTD_sequenceBound(srcSize), or an error code that can be checked + * with ZSTD_isError(). + */ +ZSTD_DEPRECATED("For debugging only, will be replaced by ZSTD_extractSequences()") +ZSTDLIB_STATIC_API size_t +ZSTD_generateSequences(ZSTD_CCtx* zc, + ZSTD_Sequence* outSeqs, size_t outSeqsSize, + const void* src, size_t srcSize); + +/*! ZSTD_mergeBlockDelimiters() : + * Given an array of ZSTD_Sequence, remove all sequences that represent block delimiters/last literals + * by merging them into the literals of the next sequence. + * + * As such, the final generated result has no explicit representation of block boundaries, + * and the final last literals segment is not represented in the sequences. + * + * The output of this function can be fed into ZSTD_compressSequences() with CCtx + * setting of ZSTD_c_blockDelimiters as ZSTD_sf_noBlockDelimiters + * @return : number of sequences left after merging + */ +ZSTDLIB_STATIC_API size_t ZSTD_mergeBlockDelimiters(ZSTD_Sequence* sequences, size_t seqsSize); + +/*! ZSTD_compressSequences() : + * Compress an array of ZSTD_Sequence, associated with @src buffer, into dst. + * @src contains the entire input (not just the literals). + * If @srcSize > sum(sequence.length), the remaining bytes are considered all literals + * If a dictionary is included, then the cctx should reference the dict. (see: ZSTD_CCtx_refCDict(), ZSTD_CCtx_loadDictionary(), etc.) + * The entire source is compressed into a single frame. + * + * The compression behavior changes based on cctx params. In particular: + * If ZSTD_c_blockDelimiters == ZSTD_sf_noBlockDelimiters, the array of ZSTD_Sequence is expected to contain + * no block delimiters (defined in ZSTD_Sequence). Block boundaries are roughly determined based on + * the block size derived from the cctx, and sequences may be split. This is the default setting. + * + * If ZSTD_c_blockDelimiters == ZSTD_sf_explicitBlockDelimiters, the array of ZSTD_Sequence is expected to contain + * block delimiters (defined in ZSTD_Sequence). Behavior is undefined if no block delimiters are provided. + * + * If ZSTD_c_validateSequences == 0, this function will blindly accept the sequences provided. Invalid sequences cause undefined + * behavior. If ZSTD_c_validateSequences == 1, then if sequence is invalid (see doc/zstd_compression_format.md for + * specifics regarding offset/matchlength requirements) then the function will bail out and return an error. + * + * In addition to the two adjustable experimental params, there are other important cctx params. + * - ZSTD_c_minMatch MUST be set as less than or equal to the smallest match generated by the match finder. It has a minimum value of ZSTD_MINMATCH_MIN. + * - ZSTD_c_compressionLevel accordingly adjusts the strength of the entropy coder, as it would in typical compression. + * - ZSTD_c_windowLog affects offset validation: this function will return an error at higher debug levels if a provided offset + * is larger than what the spec allows for a given window log and dictionary (if present). See: doc/zstd_compression_format.md + * + * Note: Repcodes are, as of now, always re-calculated within this function, so ZSTD_Sequence::rep is unused. + * Note 2: Once we integrate ability to ingest repcodes, the explicit block delims mode must respect those repcodes exactly, + * and cannot emit an RLE block that disagrees with the repcode history + * @return : final compressed size, or a ZSTD error code. + */ +ZSTDLIB_STATIC_API size_t +ZSTD_compressSequences( ZSTD_CCtx* cctx, void* dst, size_t dstSize, + const ZSTD_Sequence* inSeqs, size_t inSeqsSize, + const void* src, size_t srcSize); + + +/*! ZSTD_writeSkippableFrame() : + * Generates a zstd skippable frame containing data given by src, and writes it to dst buffer. + * + * Skippable frames begin with a 4-byte magic number. There are 16 possible choices of magic number, + * ranging from ZSTD_MAGIC_SKIPPABLE_START to ZSTD_MAGIC_SKIPPABLE_START+15. + * As such, the parameter magicVariant controls the exact skippable frame magic number variant used, so + * the magic number used will be ZSTD_MAGIC_SKIPPABLE_START + magicVariant. + * + * Returns an error if destination buffer is not large enough, if the source size is not representable + * with a 4-byte unsigned int, or if the parameter magicVariant is greater than 15 (and therefore invalid). + * + * @return : number of bytes written or a ZSTD error. + */ +ZSTDLIB_STATIC_API size_t ZSTD_writeSkippableFrame(void* dst, size_t dstCapacity, + const void* src, size_t srcSize, unsigned magicVariant); + +/*! ZSTD_readSkippableFrame() : + * Retrieves a zstd skippable frame containing data given by src, and writes it to dst buffer. + * + * The parameter magicVariant will receive the magicVariant that was supplied when the frame was written, + * i.e. magicNumber - ZSTD_MAGIC_SKIPPABLE_START. This can be NULL if the caller is not interested + * in the magicVariant. + * + * Returns an error if destination buffer is not large enough, or if the frame is not skippable. + * + * @return : number of bytes written or a ZSTD error. + */ +ZSTDLIB_API size_t ZSTD_readSkippableFrame(void* dst, size_t dstCapacity, unsigned* magicVariant, + const void* src, size_t srcSize); + +/*! ZSTD_isSkippableFrame() : + * Tells if the content of `buffer` starts with a valid Frame Identifier for a skippable frame. + */ +ZSTDLIB_API unsigned ZSTD_isSkippableFrame(const void* buffer, size_t size); + + + +/*************************************** +* Memory management +***************************************/ + +/*! ZSTD_estimate*() : + * These functions make it possible to estimate memory usage + * of a future {D,C}Ctx, before its creation. + * This is useful in combination with ZSTD_initStatic(), + * which makes it possible to employ a static buffer for ZSTD_CCtx* state. + * + * ZSTD_estimateCCtxSize() will provide a memory budget large enough + * to compress data of any size using one-shot compression ZSTD_compressCCtx() or ZSTD_compress2() + * associated with any compression level up to max specified one. + * The estimate will assume the input may be arbitrarily large, + * which is the worst case. + * + * Note that the size estimation is specific for one-shot compression, + * it is not valid for streaming (see ZSTD_estimateCStreamSize*()) + * nor other potential ways of using a ZSTD_CCtx* state. + * + * When srcSize can be bound by a known and rather "small" value, + * this knowledge can be used to provide a tighter budget estimation + * because the ZSTD_CCtx* state will need less memory for small inputs. + * This tighter estimation can be provided by employing more advanced functions + * ZSTD_estimateCCtxSize_usingCParams(), which can be used in tandem with ZSTD_getCParams(), + * and ZSTD_estimateCCtxSize_usingCCtxParams(), which can be used in tandem with ZSTD_CCtxParams_setParameter(). + * Both can be used to estimate memory using custom compression parameters and arbitrary srcSize limits. + * + * Note : only single-threaded compression is supported. + * ZSTD_estimateCCtxSize_usingCCtxParams() will return an error code if ZSTD_c_nbWorkers is >= 1. + */ +ZSTDLIB_STATIC_API size_t ZSTD_estimateCCtxSize(int maxCompressionLevel); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCCtxSize_usingCParams(ZSTD_compressionParameters cParams); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCCtxSize_usingCCtxParams(const ZSTD_CCtx_params* params); +ZSTDLIB_STATIC_API size_t ZSTD_estimateDCtxSize(void); + +/*! ZSTD_estimateCStreamSize() : + * ZSTD_estimateCStreamSize() will provide a memory budget large enough for streaming compression + * using any compression level up to the max specified one. + * It will also consider src size to be arbitrarily "large", which is a worst case scenario. + * If srcSize is known to always be small, ZSTD_estimateCStreamSize_usingCParams() can provide a tighter estimation. + * ZSTD_estimateCStreamSize_usingCParams() can be used in tandem with ZSTD_getCParams() to create cParams from compressionLevel. + * ZSTD_estimateCStreamSize_usingCCtxParams() can be used in tandem with ZSTD_CCtxParams_setParameter(). Only single-threaded compression is supported. This function will return an error code if ZSTD_c_nbWorkers is >= 1. + * Note : CStream size estimation is only correct for single-threaded compression. + * ZSTD_estimateCStreamSize_usingCCtxParams() will return an error code if ZSTD_c_nbWorkers is >= 1. + * Note 2 : ZSTD_estimateCStreamSize* functions are not compatible with the Block-Level Sequence Producer API at this time. + * Size estimates assume that no external sequence producer is registered. + * + * ZSTD_DStream memory budget depends on frame's window Size. + * This information can be passed manually, using ZSTD_estimateDStreamSize, + * or deducted from a valid frame Header, using ZSTD_estimateDStreamSize_fromFrame(); + * Any frame requesting a window size larger than max specified one will be rejected. + * Note : if streaming is init with function ZSTD_init?Stream_usingDict(), + * an internal ?Dict will be created, which additional size is not estimated here. + * In this case, get total size by adding ZSTD_estimate?DictSize + */ +ZSTDLIB_STATIC_API size_t ZSTD_estimateCStreamSize(int maxCompressionLevel); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCStreamSize_usingCParams(ZSTD_compressionParameters cParams); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCStreamSize_usingCCtxParams(const ZSTD_CCtx_params* params); +ZSTDLIB_STATIC_API size_t ZSTD_estimateDStreamSize(size_t maxWindowSize); +ZSTDLIB_STATIC_API size_t ZSTD_estimateDStreamSize_fromFrame(const void* src, size_t srcSize); + +/*! ZSTD_estimate?DictSize() : + * ZSTD_estimateCDictSize() will bet that src size is relatively "small", and content is copied, like ZSTD_createCDict(). + * ZSTD_estimateCDictSize_advanced() makes it possible to control compression parameters precisely, like ZSTD_createCDict_advanced(). + * Note : dictionaries created by reference (`ZSTD_dlm_byRef`) are logically smaller. + */ +ZSTDLIB_STATIC_API size_t ZSTD_estimateCDictSize(size_t dictSize, int compressionLevel); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCDictSize_advanced(size_t dictSize, ZSTD_compressionParameters cParams, ZSTD_dictLoadMethod_e dictLoadMethod); +ZSTDLIB_STATIC_API size_t ZSTD_estimateDDictSize(size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod); + +/*! ZSTD_initStatic*() : + * Initialize an object using a pre-allocated fixed-size buffer. + * workspace: The memory area to emplace the object into. + * Provided pointer *must be 8-bytes aligned*. + * Buffer must outlive object. + * workspaceSize: Use ZSTD_estimate*Size() to determine + * how large workspace must be to support target scenario. + * @return : pointer to object (same address as workspace, just different type), + * or NULL if error (size too small, incorrect alignment, etc.) + * Note : zstd will never resize nor malloc() when using a static buffer. + * If the object requires more memory than available, + * zstd will just error out (typically ZSTD_error_memory_allocation). + * Note 2 : there is no corresponding "free" function. + * Since workspace is allocated externally, it must be freed externally too. + * Note 3 : cParams : use ZSTD_getCParams() to convert a compression level + * into its associated cParams. + * Limitation 1 : currently not compatible with internal dictionary creation, triggered by + * ZSTD_CCtx_loadDictionary(), ZSTD_initCStream_usingDict() or ZSTD_initDStream_usingDict(). + * Limitation 2 : static cctx currently not compatible with multi-threading. + * Limitation 3 : static dctx is incompatible with legacy support. + */ +ZSTDLIB_STATIC_API ZSTD_CCtx* ZSTD_initStaticCCtx(void* workspace, size_t workspaceSize); +ZSTDLIB_STATIC_API ZSTD_CStream* ZSTD_initStaticCStream(void* workspace, size_t workspaceSize); /**< same as ZSTD_initStaticCCtx() */ + +ZSTDLIB_STATIC_API ZSTD_DCtx* ZSTD_initStaticDCtx(void* workspace, size_t workspaceSize); +ZSTDLIB_STATIC_API ZSTD_DStream* ZSTD_initStaticDStream(void* workspace, size_t workspaceSize); /**< same as ZSTD_initStaticDCtx() */ + +ZSTDLIB_STATIC_API const ZSTD_CDict* ZSTD_initStaticCDict( + void* workspace, size_t workspaceSize, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_compressionParameters cParams); + +ZSTDLIB_STATIC_API const ZSTD_DDict* ZSTD_initStaticDDict( + void* workspace, size_t workspaceSize, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType); + + +/*! Custom memory allocation : + * These prototypes make it possible to pass your own allocation/free functions. + * ZSTD_customMem is provided at creation time, using ZSTD_create*_advanced() variants listed below. + * All allocation/free operations will be completed using these custom variants instead of regular ones. + */ +typedef void* (*ZSTD_allocFunction) (void* opaque, size_t size); +typedef void (*ZSTD_freeFunction) (void* opaque, void* address); +typedef struct { ZSTD_allocFunction customAlloc; ZSTD_freeFunction customFree; void* opaque; } ZSTD_customMem; +static +#ifdef __GNUC__ +__attribute__((__unused__)) +#endif +ZSTD_customMem const ZSTD_defaultCMem = { NULL, NULL, NULL }; /**< this constant defers to stdlib's functions */ + +ZSTDLIB_STATIC_API ZSTD_CCtx* ZSTD_createCCtx_advanced(ZSTD_customMem customMem); +ZSTDLIB_STATIC_API ZSTD_CStream* ZSTD_createCStream_advanced(ZSTD_customMem customMem); +ZSTDLIB_STATIC_API ZSTD_DCtx* ZSTD_createDCtx_advanced(ZSTD_customMem customMem); +ZSTDLIB_STATIC_API ZSTD_DStream* ZSTD_createDStream_advanced(ZSTD_customMem customMem); + +ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_advanced(const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_compressionParameters cParams, + ZSTD_customMem customMem); + +/*! Thread pool : + * These prototypes make it possible to share a thread pool among multiple compression contexts. + * This can limit resources for applications with multiple threads where each one uses + * a threaded compression mode (via ZSTD_c_nbWorkers parameter). + * ZSTD_createThreadPool creates a new thread pool with a given number of threads. + * Note that the lifetime of such pool must exist while being used. + * ZSTD_CCtx_refThreadPool assigns a thread pool to a context (use NULL argument value + * to use an internal thread pool). + * ZSTD_freeThreadPool frees a thread pool, accepts NULL pointer. + */ +typedef struct POOL_ctx_s ZSTD_threadPool; +ZSTDLIB_STATIC_API ZSTD_threadPool* ZSTD_createThreadPool(size_t numThreads); +ZSTDLIB_STATIC_API void ZSTD_freeThreadPool (ZSTD_threadPool* pool); /* accept NULL pointer */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_refThreadPool(ZSTD_CCtx* cctx, ZSTD_threadPool* pool); + + +/* + * This API is temporary and is expected to change or disappear in the future! + */ +ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_advanced2( + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + const ZSTD_CCtx_params* cctxParams, + ZSTD_customMem customMem); + +ZSTDLIB_STATIC_API ZSTD_DDict* ZSTD_createDDict_advanced( + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_customMem customMem); + + +/*************************************** +* Advanced compression functions +***************************************/ + +/*! ZSTD_createCDict_byReference() : + * Create a digested dictionary for compression + * Dictionary content is just referenced, not duplicated. + * As a consequence, `dictBuffer` **must** outlive CDict, + * and its content must remain unmodified throughout the lifetime of CDict. + * note: equivalent to ZSTD_createCDict_advanced(), with dictLoadMethod==ZSTD_dlm_byRef */ +ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_byReference(const void* dictBuffer, size_t dictSize, int compressionLevel); + +/*! ZSTD_getCParams() : + * @return ZSTD_compressionParameters structure for a selected compression level and estimated srcSize. + * `estimatedSrcSize` value is optional, select 0 if not known */ +ZSTDLIB_STATIC_API ZSTD_compressionParameters ZSTD_getCParams(int compressionLevel, unsigned long long estimatedSrcSize, size_t dictSize); + +/*! ZSTD_getParams() : + * same as ZSTD_getCParams(), but @return a full `ZSTD_parameters` object instead of sub-component `ZSTD_compressionParameters`. + * All fields of `ZSTD_frameParameters` are set to default : contentSize=1, checksum=0, noDictID=0 */ +ZSTDLIB_STATIC_API ZSTD_parameters ZSTD_getParams(int compressionLevel, unsigned long long estimatedSrcSize, size_t dictSize); + +/*! ZSTD_checkCParams() : + * Ensure param values remain within authorized range. + * @return 0 on success, or an error code (can be checked with ZSTD_isError()) */ +ZSTDLIB_STATIC_API size_t ZSTD_checkCParams(ZSTD_compressionParameters params); + +/*! ZSTD_adjustCParams() : + * optimize params for a given `srcSize` and `dictSize`. + * `srcSize` can be unknown, in which case use ZSTD_CONTENTSIZE_UNKNOWN. + * `dictSize` must be `0` when there is no dictionary. + * cPar can be invalid : all parameters will be clamped within valid range in the @return struct. + * This function never fails (wide contract) */ +ZSTDLIB_STATIC_API ZSTD_compressionParameters ZSTD_adjustCParams(ZSTD_compressionParameters cPar, unsigned long long srcSize, size_t dictSize); + +/*! ZSTD_CCtx_setCParams() : + * Set all parameters provided within @p cparams into the working @p cctx. + * Note : if modifying parameters during compression (MT mode only), + * note that changes to the .windowLog parameter will be ignored. + * @return 0 on success, or an error code (can be checked with ZSTD_isError()). + * On failure, no parameters are updated. + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_setCParams(ZSTD_CCtx* cctx, ZSTD_compressionParameters cparams); + +/*! ZSTD_CCtx_setFParams() : + * Set all parameters provided within @p fparams into the working @p cctx. + * @return 0 on success, or an error code (can be checked with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_setFParams(ZSTD_CCtx* cctx, ZSTD_frameParameters fparams); + +/*! ZSTD_CCtx_setParams() : + * Set all parameters provided within @p params into the working @p cctx. + * @return 0 on success, or an error code (can be checked with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_setParams(ZSTD_CCtx* cctx, ZSTD_parameters params); + +/*! ZSTD_compress_advanced() : + * Note : this function is now DEPRECATED. + * It can be replaced by ZSTD_compress2(), in combination with ZSTD_CCtx_setParameter() and other parameter setters. + * This prototype will generate compilation warnings. */ +ZSTD_DEPRECATED("use ZSTD_compress2") +ZSTDLIB_STATIC_API +size_t ZSTD_compress_advanced(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize, + ZSTD_parameters params); + +/*! ZSTD_compress_usingCDict_advanced() : + * Note : this function is now DEPRECATED. + * It can be replaced by ZSTD_compress2(), in combination with ZSTD_CCtx_loadDictionary() and other parameter setters. + * This prototype will generate compilation warnings. */ +ZSTD_DEPRECATED("use ZSTD_compress2 with ZSTD_CCtx_loadDictionary") +ZSTDLIB_STATIC_API +size_t ZSTD_compress_usingCDict_advanced(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_CDict* cdict, + ZSTD_frameParameters fParams); + + +/*! ZSTD_CCtx_loadDictionary_byReference() : + * Same as ZSTD_CCtx_loadDictionary(), but dictionary content is referenced, instead of being copied into CCtx. + * It saves some memory, but also requires that `dict` outlives its usage within `cctx` */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_loadDictionary_byReference(ZSTD_CCtx* cctx, const void* dict, size_t dictSize); + +/*! ZSTD_CCtx_loadDictionary_advanced() : + * Same as ZSTD_CCtx_loadDictionary(), but gives finer control over + * how to load the dictionary (by copy ? by reference ?) + * and how to interpret it (automatic ? force raw mode ? full mode only ?) */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_loadDictionary_advanced(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod, ZSTD_dictContentType_e dictContentType); + +/*! ZSTD_CCtx_refPrefix_advanced() : + * Same as ZSTD_CCtx_refPrefix(), but gives finer control over + * how to interpret prefix content (automatic ? force raw mode (default) ? full mode only ?) */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_refPrefix_advanced(ZSTD_CCtx* cctx, const void* prefix, size_t prefixSize, ZSTD_dictContentType_e dictContentType); + +/* === experimental parameters === */ +/* these parameters can be used with ZSTD_setParameter() + * they are not guaranteed to remain supported in the future */ + + /* Enables rsyncable mode, + * which makes compressed files more rsync friendly + * by adding periodic synchronization points to the compressed data. + * The target average block size is ZSTD_c_jobSize / 2. + * It's possible to modify the job size to increase or decrease + * the granularity of the synchronization point. + * Once the jobSize is smaller than the window size, + * it will result in compression ratio degradation. + * NOTE 1: rsyncable mode only works when multithreading is enabled. + * NOTE 2: rsyncable performs poorly in combination with long range mode, + * since it will decrease the effectiveness of synchronization points, + * though mileage may vary. + * NOTE 3: Rsyncable mode limits maximum compression speed to ~400 MB/s. + * If the selected compression level is already running significantly slower, + * the overall speed won't be significantly impacted. + */ + #define ZSTD_c_rsyncable ZSTD_c_experimentalParam1 + +/* Select a compression format. + * The value must be of type ZSTD_format_e. + * See ZSTD_format_e enum definition for details */ +#define ZSTD_c_format ZSTD_c_experimentalParam2 + +/* Force back-reference distances to remain < windowSize, + * even when referencing into Dictionary content (default:0) */ +#define ZSTD_c_forceMaxWindow ZSTD_c_experimentalParam3 + +/* Controls whether the contents of a CDict + * are used in place, or copied into the working context. + * Accepts values from the ZSTD_dictAttachPref_e enum. + * See the comments on that enum for an explanation of the feature. */ +#define ZSTD_c_forceAttachDict ZSTD_c_experimentalParam4 + +/* Controlled with ZSTD_paramSwitch_e enum. + * Default is ZSTD_ps_auto. + * Set to ZSTD_ps_disable to never compress literals. + * Set to ZSTD_ps_enable to always compress literals. (Note: uncompressed literals + * may still be emitted if huffman is not beneficial to use.) + * + * By default, in ZSTD_ps_auto, the library will decide at runtime whether to use + * literals compression based on the compression parameters - specifically, + * negative compression levels do not use literal compression. + */ +#define ZSTD_c_literalCompressionMode ZSTD_c_experimentalParam5 + +/* User's best guess of source size. + * Hint is not valid when srcSizeHint == 0. + * There is no guarantee that hint is close to actual source size, + * but compression ratio may regress significantly if guess considerably underestimates */ +#define ZSTD_c_srcSizeHint ZSTD_c_experimentalParam7 + +/* Controls whether the new and experimental "dedicated dictionary search + * structure" can be used. This feature is still rough around the edges, be + * prepared for surprising behavior! + * + * How to use it: + * + * When using a CDict, whether to use this feature or not is controlled at + * CDict creation, and it must be set in a CCtxParams set passed into that + * construction (via ZSTD_createCDict_advanced2()). A compression will then + * use the feature or not based on how the CDict was constructed; the value of + * this param, set in the CCtx, will have no effect. + * + * However, when a dictionary buffer is passed into a CCtx, such as via + * ZSTD_CCtx_loadDictionary(), this param can be set on the CCtx to control + * whether the CDict that is created internally can use the feature or not. + * + * What it does: + * + * Normally, the internal data structures of the CDict are analogous to what + * would be stored in a CCtx after compressing the contents of a dictionary. + * To an approximation, a compression using a dictionary can then use those + * data structures to simply continue what is effectively a streaming + * compression where the simulated compression of the dictionary left off. + * Which is to say, the search structures in the CDict are normally the same + * format as in the CCtx. + * + * It is possible to do better, since the CDict is not like a CCtx: the search + * structures are written once during CDict creation, and then are only read + * after that, while the search structures in the CCtx are both read and + * written as the compression goes along. This means we can choose a search + * structure for the dictionary that is read-optimized. + * + * This feature enables the use of that different structure. + * + * Note that some of the members of the ZSTD_compressionParameters struct have + * different semantics and constraints in the dedicated search structure. It is + * highly recommended that you simply set a compression level in the CCtxParams + * you pass into the CDict creation call, and avoid messing with the cParams + * directly. + * + * Effects: + * + * This will only have any effect when the selected ZSTD_strategy + * implementation supports this feature. Currently, that's limited to + * ZSTD_greedy, ZSTD_lazy, and ZSTD_lazy2. + * + * Note that this means that the CDict tables can no longer be copied into the + * CCtx, so the dict attachment mode ZSTD_dictForceCopy will no longer be + * usable. The dictionary can only be attached or reloaded. + * + * In general, you should expect compression to be faster--sometimes very much + * so--and CDict creation to be slightly slower. Eventually, we will probably + * make this mode the default. + */ +#define ZSTD_c_enableDedicatedDictSearch ZSTD_c_experimentalParam8 + +/* ZSTD_c_stableInBuffer + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable. + * + * Tells the compressor that input data presented with ZSTD_inBuffer + * will ALWAYS be the same between calls. + * Technically, the @src pointer must never be changed, + * and the @pos field can only be updated by zstd. + * However, it's possible to increase the @size field, + * allowing scenarios where more data can be appended after compressions starts. + * These conditions are checked by the compressor, + * and compression will fail if they are not respected. + * Also, data in the ZSTD_inBuffer within the range [src, src + pos) + * MUST not be modified during compression or it will result in data corruption. + * + * When this flag is enabled zstd won't allocate an input window buffer, + * because the user guarantees it can reference the ZSTD_inBuffer until + * the frame is complete. But, it will still allocate an output buffer + * large enough to fit a block (see ZSTD_c_stableOutBuffer). This will also + * avoid the memcpy() from the input buffer to the input window buffer. + * + * NOTE: So long as the ZSTD_inBuffer always points to valid memory, using + * this flag is ALWAYS memory safe, and will never access out-of-bounds + * memory. However, compression WILL fail if conditions are not respected. + * + * WARNING: The data in the ZSTD_inBuffer in the range [src, src + pos) MUST + * not be modified during compression or it will result in data corruption. + * This is because zstd needs to reference data in the ZSTD_inBuffer to find + * matches. Normally zstd maintains its own window buffer for this purpose, + * but passing this flag tells zstd to rely on user provided buffer instead. + */ +#define ZSTD_c_stableInBuffer ZSTD_c_experimentalParam9 + +/* ZSTD_c_stableOutBuffer + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable. + * + * Tells he compressor that the ZSTD_outBuffer will not be resized between + * calls. Specifically: (out.size - out.pos) will never grow. This gives the + * compressor the freedom to say: If the compressed data doesn't fit in the + * output buffer then return ZSTD_error_dstSizeTooSmall. This allows us to + * always decompress directly into the output buffer, instead of decompressing + * into an internal buffer and copying to the output buffer. + * + * When this flag is enabled zstd won't allocate an output buffer, because + * it can write directly to the ZSTD_outBuffer. It will still allocate the + * input window buffer (see ZSTD_c_stableInBuffer). + * + * Zstd will check that (out.size - out.pos) never grows and return an error + * if it does. While not strictly necessary, this should prevent surprises. + */ +#define ZSTD_c_stableOutBuffer ZSTD_c_experimentalParam10 + +/* ZSTD_c_blockDelimiters + * Default is 0 == ZSTD_sf_noBlockDelimiters. + * + * For use with sequence compression API: ZSTD_compressSequences(). + * + * Designates whether or not the given array of ZSTD_Sequence contains block delimiters + * and last literals, which are defined as sequences with offset == 0 and matchLength == 0. + * See the definition of ZSTD_Sequence for more specifics. + */ +#define ZSTD_c_blockDelimiters ZSTD_c_experimentalParam11 + +/* ZSTD_c_validateSequences + * Default is 0 == disabled. Set to 1 to enable sequence validation. + * + * For use with sequence compression API: ZSTD_compressSequences(). + * Designates whether or not we validate sequences provided to ZSTD_compressSequences() + * during function execution. + * + * Without validation, providing a sequence that does not conform to the zstd spec will cause + * undefined behavior, and may produce a corrupted block. + * + * With validation enabled, if sequence is invalid (see doc/zstd_compression_format.md for + * specifics regarding offset/matchlength requirements) then the function will bail out and + * return an error. + * + */ +#define ZSTD_c_validateSequences ZSTD_c_experimentalParam12 + +/* ZSTD_c_useBlockSplitter + * Controlled with ZSTD_paramSwitch_e enum. + * Default is ZSTD_ps_auto. + * Set to ZSTD_ps_disable to never use block splitter. + * Set to ZSTD_ps_enable to always use block splitter. + * + * By default, in ZSTD_ps_auto, the library will decide at runtime whether to use + * block splitting based on the compression parameters. + */ +#define ZSTD_c_useBlockSplitter ZSTD_c_experimentalParam13 + +/* ZSTD_c_useRowMatchFinder + * Controlled with ZSTD_paramSwitch_e enum. + * Default is ZSTD_ps_auto. + * Set to ZSTD_ps_disable to never use row-based matchfinder. + * Set to ZSTD_ps_enable to force usage of row-based matchfinder. + * + * By default, in ZSTD_ps_auto, the library will decide at runtime whether to use + * the row-based matchfinder based on support for SIMD instructions and the window log. + * Note that this only pertains to compression strategies: greedy, lazy, and lazy2 + */ +#define ZSTD_c_useRowMatchFinder ZSTD_c_experimentalParam14 + +/* ZSTD_c_deterministicRefPrefix + * Default is 0 == disabled. Set to 1 to enable. + * + * Zstd produces different results for prefix compression when the prefix is + * directly adjacent to the data about to be compressed vs. when it isn't. + * This is because zstd detects that the two buffers are contiguous and it can + * use a more efficient match finding algorithm. However, this produces different + * results than when the two buffers are non-contiguous. This flag forces zstd + * to always load the prefix in non-contiguous mode, even if it happens to be + * adjacent to the data, to guarantee determinism. + * + * If you really care about determinism when using a dictionary or prefix, + * like when doing delta compression, you should select this option. It comes + * at a speed penalty of about ~2.5% if the dictionary and data happened to be + * contiguous, and is free if they weren't contiguous. We don't expect that + * intentionally making the dictionary and data contiguous will be worth the + * cost to memcpy() the data. + */ +#define ZSTD_c_deterministicRefPrefix ZSTD_c_experimentalParam15 + +/* ZSTD_c_prefetchCDictTables + * Controlled with ZSTD_paramSwitch_e enum. Default is ZSTD_ps_auto. + * + * In some situations, zstd uses CDict tables in-place rather than copying them + * into the working context. (See docs on ZSTD_dictAttachPref_e above for details). + * In such situations, compression speed is seriously impacted when CDict tables are + * "cold" (outside CPU cache). This parameter instructs zstd to prefetch CDict tables + * when they are used in-place. + * + * For sufficiently small inputs, the cost of the prefetch will outweigh the benefit. + * For sufficiently large inputs, zstd will by default memcpy() CDict tables + * into the working context, so there is no need to prefetch. This parameter is + * targeted at a middle range of input sizes, where a prefetch is cheap enough to be + * useful but memcpy() is too expensive. The exact range of input sizes where this + * makes sense is best determined by careful experimentation. + * + * Note: for this parameter, ZSTD_ps_auto is currently equivalent to ZSTD_ps_disable, + * but in the future zstd may conditionally enable this feature via an auto-detection + * heuristic for cold CDicts. + * Use ZSTD_ps_disable to opt out of prefetching under any circumstances. + */ +#define ZSTD_c_prefetchCDictTables ZSTD_c_experimentalParam16 + +/* ZSTD_c_enableSeqProducerFallback + * Allowed values are 0 (disable) and 1 (enable). The default setting is 0. + * + * Controls whether zstd will fall back to an internal sequence producer if an + * external sequence producer is registered and returns an error code. This fallback + * is block-by-block: the internal sequence producer will only be called for blocks + * where the external sequence producer returns an error code. Fallback parsing will + * follow any other cParam settings, such as compression level, the same as in a + * normal (fully-internal) compression operation. + * + * The user is strongly encouraged to read the full Block-Level Sequence Producer API + * documentation (below) before setting this parameter. */ +#define ZSTD_c_enableSeqProducerFallback ZSTD_c_experimentalParam17 + +/* ZSTD_c_maxBlockSize + * Allowed values are between 1KB and ZSTD_BLOCKSIZE_MAX (128KB). + * The default is ZSTD_BLOCKSIZE_MAX, and setting to 0 will set to the default. + * + * This parameter can be used to set an upper bound on the blocksize + * that overrides the default ZSTD_BLOCKSIZE_MAX. It cannot be used to set upper + * bounds greater than ZSTD_BLOCKSIZE_MAX or bounds lower than 1KB (will make + * compressBound() inaccurate). Only currently meant to be used for testing. + * + */ +#define ZSTD_c_maxBlockSize ZSTD_c_experimentalParam18 + +/* ZSTD_c_searchForExternalRepcodes + * This parameter affects how zstd parses external sequences, such as sequences + * provided through the compressSequences() API or from an external block-level + * sequence producer. + * + * If set to ZSTD_ps_enable, the library will check for repeated offsets in + * external sequences, even if those repcodes are not explicitly indicated in + * the "rep" field. Note that this is the only way to exploit repcode matches + * while using compressSequences() or an external sequence producer, since zstd + * currently ignores the "rep" field of external sequences. + * + * If set to ZSTD_ps_disable, the library will not exploit repeated offsets in + * external sequences, regardless of whether the "rep" field has been set. This + * reduces sequence compression overhead by about 25% while sacrificing some + * compression ratio. + * + * The default value is ZSTD_ps_auto, for which the library will enable/disable + * based on compression level. + * + * Note: for now, this param only has an effect if ZSTD_c_blockDelimiters is + * set to ZSTD_sf_explicitBlockDelimiters. That may change in the future. + */ +#define ZSTD_c_searchForExternalRepcodes ZSTD_c_experimentalParam19 + +/*! ZSTD_CCtx_getParameter() : + * Get the requested compression parameter value, selected by enum ZSTD_cParameter, + * and store it into int* value. + * @return : 0, or an error code (which can be tested with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_getParameter(const ZSTD_CCtx* cctx, ZSTD_cParameter param, int* value); + + +/*! ZSTD_CCtx_params : + * Quick howto : + * - ZSTD_createCCtxParams() : Create a ZSTD_CCtx_params structure + * - ZSTD_CCtxParams_setParameter() : Push parameters one by one into + * an existing ZSTD_CCtx_params structure. + * This is similar to + * ZSTD_CCtx_setParameter(). + * - ZSTD_CCtx_setParametersUsingCCtxParams() : Apply parameters to + * an existing CCtx. + * These parameters will be applied to + * all subsequent frames. + * - ZSTD_compressStream2() : Do compression using the CCtx. + * - ZSTD_freeCCtxParams() : Free the memory, accept NULL pointer. + * + * This can be used with ZSTD_estimateCCtxSize_advanced_usingCCtxParams() + * for static allocation of CCtx for single-threaded compression. + */ +ZSTDLIB_STATIC_API ZSTD_CCtx_params* ZSTD_createCCtxParams(void); +ZSTDLIB_STATIC_API size_t ZSTD_freeCCtxParams(ZSTD_CCtx_params* params); /* accept NULL pointer */ + +/*! ZSTD_CCtxParams_reset() : + * Reset params to default values. + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_reset(ZSTD_CCtx_params* params); + +/*! ZSTD_CCtxParams_init() : + * Initializes the compression parameters of cctxParams according to + * compression level. All other parameters are reset to their default values. + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_init(ZSTD_CCtx_params* cctxParams, int compressionLevel); + +/*! ZSTD_CCtxParams_init_advanced() : + * Initializes the compression and frame parameters of cctxParams according to + * params. All other parameters are reset to their default values. + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_init_advanced(ZSTD_CCtx_params* cctxParams, ZSTD_parameters params); + +/*! ZSTD_CCtxParams_setParameter() : Requires v1.4.0+ + * Similar to ZSTD_CCtx_setParameter. + * Set one compression parameter, selected by enum ZSTD_cParameter. + * Parameters must be applied to a ZSTD_CCtx using + * ZSTD_CCtx_setParametersUsingCCtxParams(). + * @result : a code representing success or failure (which can be tested with + * ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_setParameter(ZSTD_CCtx_params* params, ZSTD_cParameter param, int value); + +/*! ZSTD_CCtxParams_getParameter() : + * Similar to ZSTD_CCtx_getParameter. + * Get the requested value of one compression parameter, selected by enum ZSTD_cParameter. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_getParameter(const ZSTD_CCtx_params* params, ZSTD_cParameter param, int* value); + +/*! ZSTD_CCtx_setParametersUsingCCtxParams() : + * Apply a set of ZSTD_CCtx_params to the compression context. + * This can be done even after compression is started, + * if nbWorkers==0, this will have no impact until a new compression is started. + * if nbWorkers>=1, new parameters will be picked up at next job, + * with a few restrictions (windowLog, pledgedSrcSize, nbWorkers, jobSize, and overlapLog are not updated). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_setParametersUsingCCtxParams( + ZSTD_CCtx* cctx, const ZSTD_CCtx_params* params); + +/*! ZSTD_compressStream2_simpleArgs() : + * Same as ZSTD_compressStream2(), + * but using only integral types as arguments. + * This variant might be helpful for binders from dynamic languages + * which have troubles handling structures containing memory pointers. + */ +ZSTDLIB_STATIC_API size_t ZSTD_compressStream2_simpleArgs ( + ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, size_t* dstPos, + const void* src, size_t srcSize, size_t* srcPos, + ZSTD_EndDirective endOp); + + +/*************************************** +* Advanced decompression functions +***************************************/ + +/*! ZSTD_isFrame() : + * Tells if the content of `buffer` starts with a valid Frame Identifier. + * Note : Frame Identifier is 4 bytes. If `size < 4`, @return will always be 0. + * Note 2 : Legacy Frame Identifiers are considered valid only if Legacy Support is enabled. + * Note 3 : Skippable Frame Identifiers are considered valid. */ +ZSTDLIB_STATIC_API unsigned ZSTD_isFrame(const void* buffer, size_t size); + +/*! ZSTD_createDDict_byReference() : + * Create a digested dictionary, ready to start decompression operation without startup delay. + * Dictionary content is referenced, and therefore stays in dictBuffer. + * It is important that dictBuffer outlives DDict, + * it must remain read accessible throughout the lifetime of DDict */ +ZSTDLIB_STATIC_API ZSTD_DDict* ZSTD_createDDict_byReference(const void* dictBuffer, size_t dictSize); + +/*! ZSTD_DCtx_loadDictionary_byReference() : + * Same as ZSTD_DCtx_loadDictionary(), + * but references `dict` content instead of copying it into `dctx`. + * This saves memory if `dict` remains around., + * However, it's imperative that `dict` remains accessible (and unmodified) while being used, so it must outlive decompression. */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_loadDictionary_byReference(ZSTD_DCtx* dctx, const void* dict, size_t dictSize); + +/*! ZSTD_DCtx_loadDictionary_advanced() : + * Same as ZSTD_DCtx_loadDictionary(), + * but gives direct control over + * how to load the dictionary (by copy ? by reference ?) + * and how to interpret it (automatic ? force raw mode ? full mode only ?). */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_loadDictionary_advanced(ZSTD_DCtx* dctx, const void* dict, size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod, ZSTD_dictContentType_e dictContentType); + +/*! ZSTD_DCtx_refPrefix_advanced() : + * Same as ZSTD_DCtx_refPrefix(), but gives finer control over + * how to interpret prefix content (automatic ? force raw mode (default) ? full mode only ?) */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_refPrefix_advanced(ZSTD_DCtx* dctx, const void* prefix, size_t prefixSize, ZSTD_dictContentType_e dictContentType); + +/*! ZSTD_DCtx_setMaxWindowSize() : + * Refuses allocating internal buffers for frames requiring a window size larger than provided limit. + * This protects a decoder context from reserving too much memory for itself (potential attack scenario). + * This parameter is only useful in streaming mode, since no internal buffer is allocated in single-pass mode. + * By default, a decompression context accepts all window sizes <= (1 << ZSTD_WINDOWLOG_LIMIT_DEFAULT) + * @return : 0, or an error code (which can be tested using ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_setMaxWindowSize(ZSTD_DCtx* dctx, size_t maxWindowSize); + +/*! ZSTD_DCtx_getParameter() : + * Get the requested decompression parameter value, selected by enum ZSTD_dParameter, + * and store it into int* value. + * @return : 0, or an error code (which can be tested with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_getParameter(ZSTD_DCtx* dctx, ZSTD_dParameter param, int* value); + +/* ZSTD_d_format + * experimental parameter, + * allowing selection between ZSTD_format_e input compression formats + */ +#define ZSTD_d_format ZSTD_d_experimentalParam1 +/* ZSTD_d_stableOutBuffer + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable. + * + * Tells the decompressor that the ZSTD_outBuffer will ALWAYS be the same + * between calls, except for the modifications that zstd makes to pos (the + * caller must not modify pos). This is checked by the decompressor, and + * decompression will fail if it ever changes. Therefore the ZSTD_outBuffer + * MUST be large enough to fit the entire decompressed frame. This will be + * checked when the frame content size is known. The data in the ZSTD_outBuffer + * in the range [dst, dst + pos) MUST not be modified during decompression + * or you will get data corruption. + * + * When this flag is enabled zstd won't allocate an output buffer, because + * it can write directly to the ZSTD_outBuffer, but it will still allocate + * an input buffer large enough to fit any compressed block. This will also + * avoid the memcpy() from the internal output buffer to the ZSTD_outBuffer. + * If you need to avoid the input buffer allocation use the buffer-less + * streaming API. + * + * NOTE: So long as the ZSTD_outBuffer always points to valid memory, using + * this flag is ALWAYS memory safe, and will never access out-of-bounds + * memory. However, decompression WILL fail if you violate the preconditions. + * + * WARNING: The data in the ZSTD_outBuffer in the range [dst, dst + pos) MUST + * not be modified during decompression or you will get data corruption. This + * is because zstd needs to reference data in the ZSTD_outBuffer to regenerate + * matches. Normally zstd maintains its own buffer for this purpose, but passing + * this flag tells zstd to use the user provided buffer. + */ +#define ZSTD_d_stableOutBuffer ZSTD_d_experimentalParam2 + +/* ZSTD_d_forceIgnoreChecksum + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable + * + * Tells the decompressor to skip checksum validation during decompression, regardless + * of whether checksumming was specified during compression. This offers some + * slight performance benefits, and may be useful for debugging. + * Param has values of type ZSTD_forceIgnoreChecksum_e + */ +#define ZSTD_d_forceIgnoreChecksum ZSTD_d_experimentalParam3 + +/* ZSTD_d_refMultipleDDicts + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable + * + * If enabled and dctx is allocated on the heap, then additional memory will be allocated + * to store references to multiple ZSTD_DDict. That is, multiple calls of ZSTD_refDDict() + * using a given ZSTD_DCtx, rather than overwriting the previous DDict reference, will instead + * store all references. At decompression time, the appropriate dictID is selected + * from the set of DDicts based on the dictID in the frame. + * + * Usage is simply calling ZSTD_refDDict() on multiple dict buffers. + * + * Param has values of byte ZSTD_refMultipleDDicts_e + * + * WARNING: Enabling this parameter and calling ZSTD_DCtx_refDDict(), will trigger memory + * allocation for the hash table. ZSTD_freeDCtx() also frees this memory. + * Memory is allocated as per ZSTD_DCtx::customMem. + * + * Although this function allocates memory for the table, the user is still responsible for + * memory management of the underlying ZSTD_DDict* themselves. + */ +#define ZSTD_d_refMultipleDDicts ZSTD_d_experimentalParam4 + +/* ZSTD_d_disableHuffmanAssembly + * Set to 1 to disable the Huffman assembly implementation. + * The default value is 0, which allows zstd to use the Huffman assembly + * implementation if available. + * + * This parameter can be used to disable Huffman assembly at runtime. + * If you want to disable it at compile time you can define the macro + * ZSTD_DISABLE_ASM. + */ +#define ZSTD_d_disableHuffmanAssembly ZSTD_d_experimentalParam5 + +/* ZSTD_d_maxBlockSize + * Allowed values are between 1KB and ZSTD_BLOCKSIZE_MAX (128KB). + * The default is ZSTD_BLOCKSIZE_MAX, and setting to 0 will set to the default. + * + * Forces the decompressor to reject blocks whose content size is + * larger than the configured maxBlockSize. When maxBlockSize is + * larger than the windowSize, the windowSize is used instead. + * This saves memory on the decoder when you know all blocks are small. + * + * This option is typically used in conjunction with ZSTD_c_maxBlockSize. + * + * WARNING: This causes the decoder to reject otherwise valid frames + * that have block sizes larger than the configured maxBlockSize. + */ +#define ZSTD_d_maxBlockSize ZSTD_d_experimentalParam6 + + +/*! ZSTD_DCtx_setFormat() : + * This function is REDUNDANT. Prefer ZSTD_DCtx_setParameter(). + * Instruct the decoder context about what kind of data to decode next. + * This instruction is mandatory to decode data without a fully-formed header, + * such ZSTD_f_zstd1_magicless for example. + * @return : 0, or an error code (which can be tested using ZSTD_isError()). */ +ZSTD_DEPRECATED("use ZSTD_DCtx_setParameter() instead") +ZSTDLIB_STATIC_API +size_t ZSTD_DCtx_setFormat(ZSTD_DCtx* dctx, ZSTD_format_e format); + +/*! ZSTD_decompressStream_simpleArgs() : + * Same as ZSTD_decompressStream(), + * but using only integral types as arguments. + * This can be helpful for binders from dynamic languages + * which have troubles handling structures containing memory pointers. + */ +ZSTDLIB_STATIC_API size_t ZSTD_decompressStream_simpleArgs ( + ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, size_t* dstPos, + const void* src, size_t srcSize, size_t* srcPos); + + +/******************************************************************** +* Advanced streaming functions +* Warning : most of these functions are now redundant with the Advanced API. +* Once Advanced API reaches "stable" status, +* redundant functions will be deprecated, and then at some point removed. +********************************************************************/ + +/*===== Advanced Streaming compression functions =====*/ + +/*! ZSTD_initCStream_srcSize() : + * This function is DEPRECATED, and equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_refCDict(zcs, NULL); // clear the dictionary (if any) + * ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel); + * ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize); + * + * pledgedSrcSize must be correct. If it is not known at init time, use + * ZSTD_CONTENTSIZE_UNKNOWN. Note that, for compatibility with older programs, + * "0" also disables frame content size field. It may be enabled in the future. + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_srcSize(ZSTD_CStream* zcs, + int compressionLevel, + unsigned long long pledgedSrcSize); + +/*! ZSTD_initCStream_usingDict() : + * This function is DEPRECATED, and is equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel); + * ZSTD_CCtx_loadDictionary(zcs, dict, dictSize); + * + * Creates of an internal CDict (incompatible with static CCtx), except if + * dict == NULL or dictSize < 8, in which case no dict is used. + * Note: dict is loaded with ZSTD_dct_auto (treated as a full zstd dictionary if + * it begins with ZSTD_MAGIC_DICTIONARY, else as raw content) and ZSTD_dlm_byCopy. + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_usingDict(ZSTD_CStream* zcs, + const void* dict, size_t dictSize, + int compressionLevel); + +/*! ZSTD_initCStream_advanced() : + * This function is DEPRECATED, and is equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_setParams(zcs, params); + * ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize); + * ZSTD_CCtx_loadDictionary(zcs, dict, dictSize); + * + * dict is loaded with ZSTD_dct_auto and ZSTD_dlm_byCopy. + * pledgedSrcSize must be correct. + * If srcSize is not known at init time, use value ZSTD_CONTENTSIZE_UNKNOWN. + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_advanced(ZSTD_CStream* zcs, + const void* dict, size_t dictSize, + ZSTD_parameters params, + unsigned long long pledgedSrcSize); + +/*! ZSTD_initCStream_usingCDict() : + * This function is DEPRECATED, and equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_refCDict(zcs, cdict); + * + * note : cdict will just be referenced, and must outlive compression session + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset and ZSTD_CCtx_refCDict, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_usingCDict(ZSTD_CStream* zcs, const ZSTD_CDict* cdict); + +/*! ZSTD_initCStream_usingCDict_advanced() : + * This function is DEPRECATED, and is equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_setFParams(zcs, fParams); + * ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize); + * ZSTD_CCtx_refCDict(zcs, cdict); + * + * same as ZSTD_initCStream_usingCDict(), with control over frame parameters. + * pledgedSrcSize must be correct. If srcSize is not known at init time, use + * value ZSTD_CONTENTSIZE_UNKNOWN. + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset and ZSTD_CCtx_refCDict, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_usingCDict_advanced(ZSTD_CStream* zcs, + const ZSTD_CDict* cdict, + ZSTD_frameParameters fParams, + unsigned long long pledgedSrcSize); + +/*! ZSTD_resetCStream() : + * This function is DEPRECATED, and is equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize); + * Note: ZSTD_resetCStream() interprets pledgedSrcSize == 0 as ZSTD_CONTENTSIZE_UNKNOWN, but + * ZSTD_CCtx_setPledgedSrcSize() does not do the same, so ZSTD_CONTENTSIZE_UNKNOWN must be + * explicitly specified. + * + * start a new frame, using same parameters from previous frame. + * This is typically useful to skip dictionary loading stage, since it will reuse it in-place. + * Note that zcs must be init at least once before using ZSTD_resetCStream(). + * If pledgedSrcSize is not known at reset time, use macro ZSTD_CONTENTSIZE_UNKNOWN. + * If pledgedSrcSize > 0, its value must be correct, as it will be written in header, and controlled at the end. + * For the time being, pledgedSrcSize==0 is interpreted as "srcSize unknown" for compatibility with older programs, + * but it will change to mean "empty" in future version, so use macro ZSTD_CONTENTSIZE_UNKNOWN instead. + * @return : 0, or an error code (which can be tested using ZSTD_isError()) + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_resetCStream(ZSTD_CStream* zcs, unsigned long long pledgedSrcSize); + + +typedef struct { + unsigned long long ingested; /* nb input bytes read and buffered */ + unsigned long long consumed; /* nb input bytes actually compressed */ + unsigned long long produced; /* nb of compressed bytes generated and buffered */ + unsigned long long flushed; /* nb of compressed bytes flushed : not provided; can be tracked from caller side */ + unsigned currentJobID; /* MT only : latest started job nb */ + unsigned nbActiveWorkers; /* MT only : nb of workers actively compressing at probe time */ +} ZSTD_frameProgression; + +/* ZSTD_getFrameProgression() : + * tells how much data has been ingested (read from input) + * consumed (input actually compressed) and produced (output) for current frame. + * Note : (ingested - consumed) is amount of input data buffered internally, not yet compressed. + * Aggregates progression inside active worker threads. + */ +ZSTDLIB_STATIC_API ZSTD_frameProgression ZSTD_getFrameProgression(const ZSTD_CCtx* cctx); + +/*! ZSTD_toFlushNow() : + * Tell how many bytes are ready to be flushed immediately. + * Useful for multithreading scenarios (nbWorkers >= 1). + * Probe the oldest active job, defined as oldest job not yet entirely flushed, + * and check its output buffer. + * @return : amount of data stored in oldest job and ready to be flushed immediately. + * if @return == 0, it means either : + * + there is no active job (could be checked with ZSTD_frameProgression()), or + * + oldest job is still actively compressing data, + * but everything it has produced has also been flushed so far, + * therefore flush speed is limited by production speed of oldest job + * irrespective of the speed of concurrent (and newer) jobs. + */ +ZSTDLIB_STATIC_API size_t ZSTD_toFlushNow(ZSTD_CCtx* cctx); + + +/*===== Advanced Streaming decompression functions =====*/ + +/*! + * This function is deprecated, and is equivalent to: + * + * ZSTD_DCtx_reset(zds, ZSTD_reset_session_only); + * ZSTD_DCtx_loadDictionary(zds, dict, dictSize); + * + * note: no dictionary will be used if dict == NULL or dictSize < 8 + */ +ZSTD_DEPRECATED("use ZSTD_DCtx_reset + ZSTD_DCtx_loadDictionary, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API size_t ZSTD_initDStream_usingDict(ZSTD_DStream* zds, const void* dict, size_t dictSize); + +/*! + * This function is deprecated, and is equivalent to: + * + * ZSTD_DCtx_reset(zds, ZSTD_reset_session_only); + * ZSTD_DCtx_refDDict(zds, ddict); + * + * note : ddict is referenced, it must outlive decompression session + */ +ZSTD_DEPRECATED("use ZSTD_DCtx_reset + ZSTD_DCtx_refDDict, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API size_t ZSTD_initDStream_usingDDict(ZSTD_DStream* zds, const ZSTD_DDict* ddict); + +/*! + * This function is deprecated, and is equivalent to: + * + * ZSTD_DCtx_reset(zds, ZSTD_reset_session_only); + * + * reuse decompression parameters from previous init; saves dictionary loading + */ +ZSTD_DEPRECATED("use ZSTD_DCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API size_t ZSTD_resetDStream(ZSTD_DStream* zds); + + +/* ********************* BLOCK-LEVEL SEQUENCE PRODUCER API ********************* + * + * *** OVERVIEW *** + * The Block-Level Sequence Producer API allows users to provide their own custom + * sequence producer which libzstd invokes to process each block. The produced list + * of sequences (literals and matches) is then post-processed by libzstd to produce + * valid compressed blocks. + * + * This block-level offload API is a more granular complement of the existing + * frame-level offload API compressSequences() (introduced in v1.5.1). It offers + * an easier migration story for applications already integrated with libzstd: the + * user application continues to invoke the same compression functions + * ZSTD_compress2() or ZSTD_compressStream2() as usual, and transparently benefits + * from the specific advantages of the external sequence producer. For example, + * the sequence producer could be tuned to take advantage of known characteristics + * of the input, to offer better speed / ratio, or could leverage hardware + * acceleration not available within libzstd itself. + * + * See contrib/externalSequenceProducer for an example program employing the + * Block-Level Sequence Producer API. + * + * *** USAGE *** + * The user is responsible for implementing a function of type + * ZSTD_sequenceProducer_F. For each block, zstd will pass the following + * arguments to the user-provided function: + * + * - sequenceProducerState: a pointer to a user-managed state for the sequence + * producer. + * + * - outSeqs, outSeqsCapacity: an output buffer for the sequence producer. + * outSeqsCapacity is guaranteed >= ZSTD_sequenceBound(srcSize). The memory + * backing outSeqs is managed by the CCtx. + * + * - src, srcSize: an input buffer for the sequence producer to parse. + * srcSize is guaranteed to be <= ZSTD_BLOCKSIZE_MAX. + * + * - dict, dictSize: a history buffer, which may be empty, which the sequence + * producer may reference as it parses the src buffer. Currently, zstd will + * always pass dictSize == 0 into external sequence producers, but this will + * change in the future. + * + * - compressionLevel: a signed integer representing the zstd compression level + * set by the user for the current operation. The sequence producer may choose + * to use this information to change its compression strategy and speed/ratio + * tradeoff. Note: the compression level does not reflect zstd parameters set + * through the advanced API. + * + * - windowSize: a size_t representing the maximum allowed offset for external + * sequences. Note that sequence offsets are sometimes allowed to exceed the + * windowSize if a dictionary is present, see doc/zstd_compression_format.md + * for details. + * + * The user-provided function shall return a size_t representing the number of + * sequences written to outSeqs. This return value will be treated as an error + * code if it is greater than outSeqsCapacity. The return value must be non-zero + * if srcSize is non-zero. The ZSTD_SEQUENCE_PRODUCER_ERROR macro is provided + * for convenience, but any value greater than outSeqsCapacity will be treated as + * an error code. + * + * If the user-provided function does not return an error code, the sequences + * written to outSeqs must be a valid parse of the src buffer. Data corruption may + * occur if the parse is not valid. A parse is defined to be valid if the + * following conditions hold: + * - The sum of matchLengths and literalLengths must equal srcSize. + * - All sequences in the parse, except for the final sequence, must have + * matchLength >= ZSTD_MINMATCH_MIN. The final sequence must have + * matchLength >= ZSTD_MINMATCH_MIN or matchLength == 0. + * - All offsets must respect the windowSize parameter as specified in + * doc/zstd_compression_format.md. + * - If the final sequence has matchLength == 0, it must also have offset == 0. + * + * zstd will only validate these conditions (and fail compression if they do not + * hold) if the ZSTD_c_validateSequences cParam is enabled. Note that sequence + * validation has a performance cost. + * + * If the user-provided function returns an error, zstd will either fall back + * to an internal sequence producer or fail the compression operation. The user can + * choose between the two behaviors by setting the ZSTD_c_enableSeqProducerFallback + * cParam. Fallback compression will follow any other cParam settings, such as + * compression level, the same as in a normal compression operation. + * + * The user shall instruct zstd to use a particular ZSTD_sequenceProducer_F + * function by calling + * ZSTD_registerSequenceProducer(cctx, + * sequenceProducerState, + * sequenceProducer) + * This setting will persist until the next parameter reset of the CCtx. + * + * The sequenceProducerState must be initialized by the user before calling + * ZSTD_registerSequenceProducer(). The user is responsible for destroying the + * sequenceProducerState. + * + * *** LIMITATIONS *** + * This API is compatible with all zstd compression APIs which respect advanced parameters. + * However, there are three limitations: + * + * First, the ZSTD_c_enableLongDistanceMatching cParam is not currently supported. + * COMPRESSION WILL FAIL if it is enabled and the user tries to compress with a block-level + * external sequence producer. + * - Note that ZSTD_c_enableLongDistanceMatching is auto-enabled by default in some + * cases (see its documentation for details). Users must explicitly set + * ZSTD_c_enableLongDistanceMatching to ZSTD_ps_disable in such cases if an external + * sequence producer is registered. + * - As of this writing, ZSTD_c_enableLongDistanceMatching is disabled by default + * whenever ZSTD_c_windowLog < 128MB, but that's subject to change. Users should + * check the docs on ZSTD_c_enableLongDistanceMatching whenever the Block-Level Sequence + * Producer API is used in conjunction with advanced settings (like ZSTD_c_windowLog). + * + * Second, history buffers are not currently supported. Concretely, zstd will always pass + * dictSize == 0 to the external sequence producer (for now). This has two implications: + * - Dictionaries are not currently supported. Compression will *not* fail if the user + * references a dictionary, but the dictionary won't have any effect. + * - Stream history is not currently supported. All advanced compression APIs, including + * streaming APIs, work with external sequence producers, but each block is treated as + * an independent chunk without history from previous blocks. + * + * Third, multi-threading within a single compression is not currently supported. In other words, + * COMPRESSION WILL FAIL if ZSTD_c_nbWorkers > 0 and an external sequence producer is registered. + * Multi-threading across compressions is fine: simply create one CCtx per thread. + * + * Long-term, we plan to overcome all three limitations. There is no technical blocker to + * overcoming them. It is purely a question of engineering effort. + */ + +#define ZSTD_SEQUENCE_PRODUCER_ERROR ((size_t)(-1)) + +typedef size_t (*ZSTD_sequenceProducer_F) ( + void* sequenceProducerState, + ZSTD_Sequence* outSeqs, size_t outSeqsCapacity, + const void* src, size_t srcSize, + const void* dict, size_t dictSize, + int compressionLevel, + size_t windowSize +); + +/*! ZSTD_registerSequenceProducer() : + * Instruct zstd to use a block-level external sequence producer function. + * + * The sequenceProducerState must be initialized by the caller, and the caller is + * responsible for managing its lifetime. This parameter is sticky across + * compressions. It will remain set until the user explicitly resets compression + * parameters. + * + * Sequence producer registration is considered to be an "advanced parameter", + * part of the "advanced API". This means it will only have an effect on compression + * APIs which respect advanced parameters, such as compress2() and compressStream2(). + * Older compression APIs such as compressCCtx(), which predate the introduction of + * "advanced parameters", will ignore any external sequence producer setting. + * + * The sequence producer can be "cleared" by registering a NULL function pointer. This + * removes all limitations described above in the "LIMITATIONS" section of the API docs. + * + * The user is strongly encouraged to read the full API documentation (above) before + * calling this function. */ +ZSTDLIB_STATIC_API void +ZSTD_registerSequenceProducer( + ZSTD_CCtx* cctx, + void* sequenceProducerState, + ZSTD_sequenceProducer_F sequenceProducer +); + +/*! ZSTD_CCtxParams_registerSequenceProducer() : + * Same as ZSTD_registerSequenceProducer(), but operates on ZSTD_CCtx_params. + * This is used for accurate size estimation with ZSTD_estimateCCtxSize_usingCCtxParams(), + * which is needed when creating a ZSTD_CCtx with ZSTD_initStaticCCtx(). + * + * If you are using the external sequence producer API in a scenario where ZSTD_initStaticCCtx() + * is required, then this function is for you. Otherwise, you probably don't need it. + * + * See tests/zstreamtest.c for example usage. */ +ZSTDLIB_STATIC_API void +ZSTD_CCtxParams_registerSequenceProducer( + ZSTD_CCtx_params* params, + void* sequenceProducerState, + ZSTD_sequenceProducer_F sequenceProducer +); + + +/********************************************************************* +* Buffer-less and synchronous inner streaming functions (DEPRECATED) +* +* This API is deprecated, and will be removed in a future version. +* It allows streaming (de)compression with user allocated buffers. +* However, it is hard to use, and not as well tested as the rest of +* our API. +* +* Please use the normal streaming API instead: ZSTD_compressStream2, +* and ZSTD_decompressStream. +* If there is functionality that you need, but it doesn't provide, +* please open an issue on our GitHub. +********************************************************************* */ + +/** + Buffer-less streaming compression (synchronous mode) + + A ZSTD_CCtx object is required to track streaming operations. + Use ZSTD_createCCtx() / ZSTD_freeCCtx() to manage resource. + ZSTD_CCtx object can be reused multiple times within successive compression operations. + + Start by initializing a context. + Use ZSTD_compressBegin(), or ZSTD_compressBegin_usingDict() for dictionary compression. + + Then, consume your input using ZSTD_compressContinue(). + There are some important considerations to keep in mind when using this advanced function : + - ZSTD_compressContinue() has no internal buffer. It uses externally provided buffers only. + - Interface is synchronous : input is consumed entirely and produces 1+ compressed blocks. + - Caller must ensure there is enough space in `dst` to store compressed data under worst case scenario. + Worst case evaluation is provided by ZSTD_compressBound(). + ZSTD_compressContinue() doesn't guarantee recover after a failed compression. + - ZSTD_compressContinue() presumes prior input ***is still accessible and unmodified*** (up to maximum distance size, see WindowLog). + It remembers all previous contiguous blocks, plus one separated memory segment (which can itself consists of multiple contiguous blocks) + - ZSTD_compressContinue() detects that prior input has been overwritten when `src` buffer overlaps. + In which case, it will "discard" the relevant memory section from its history. + + Finish a frame with ZSTD_compressEnd(), which will write the last block(s) and optional checksum. + It's possible to use srcSize==0, in which case, it will write a final empty block to end the frame. + Without last block mark, frames are considered unfinished (hence corrupted) by compliant decoders. + + `ZSTD_CCtx` object can be reused (ZSTD_compressBegin()) to compress again. +*/ + +/*===== Buffer-less streaming compression functions =====*/ +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressBegin(ZSTD_CCtx* cctx, int compressionLevel); +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressBegin_usingDict(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, int compressionLevel); +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressBegin_usingCDict(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict); /**< note: fails if cdict==NULL */ + +ZSTD_DEPRECATED("This function will likely be removed in a future release. It is misleading and has very limited utility.") +ZSTDLIB_STATIC_API +size_t ZSTD_copyCCtx(ZSTD_CCtx* cctx, const ZSTD_CCtx* preparedCCtx, unsigned long long pledgedSrcSize); /**< note: if pledgedSrcSize is not known, use ZSTD_CONTENTSIZE_UNKNOWN */ + +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressContinue(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressEnd(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); + +/* The ZSTD_compressBegin_advanced() and ZSTD_compressBegin_usingCDict_advanced() are now DEPRECATED and will generate a compiler warning */ +ZSTD_DEPRECATED("use advanced API to access custom parameters") +ZSTDLIB_STATIC_API +size_t ZSTD_compressBegin_advanced(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, ZSTD_parameters params, unsigned long long pledgedSrcSize); /**< pledgedSrcSize : If srcSize is not known at init time, use ZSTD_CONTENTSIZE_UNKNOWN */ +ZSTD_DEPRECATED("use advanced API to access custom parameters") +ZSTDLIB_STATIC_API +size_t ZSTD_compressBegin_usingCDict_advanced(ZSTD_CCtx* const cctx, const ZSTD_CDict* const cdict, ZSTD_frameParameters const fParams, unsigned long long const pledgedSrcSize); /* compression parameters are already set within cdict. pledgedSrcSize must be correct. If srcSize is not known, use macro ZSTD_CONTENTSIZE_UNKNOWN */ +/** + Buffer-less streaming decompression (synchronous mode) + + A ZSTD_DCtx object is required to track streaming operations. + Use ZSTD_createDCtx() / ZSTD_freeDCtx() to manage it. + A ZSTD_DCtx object can be reused multiple times. + + First typical operation is to retrieve frame parameters, using ZSTD_getFrameHeader(). + Frame header is extracted from the beginning of compressed frame, so providing only the frame's beginning is enough. + Data fragment must be large enough to ensure successful decoding. + `ZSTD_frameHeaderSize_max` bytes is guaranteed to always be large enough. + result : 0 : successful decoding, the `ZSTD_frameHeader` structure is correctly filled. + >0 : `srcSize` is too small, please provide at least result bytes on next attempt. + errorCode, which can be tested using ZSTD_isError(). + + It fills a ZSTD_frameHeader structure with important information to correctly decode the frame, + such as the dictionary ID, content size, or maximum back-reference distance (`windowSize`). + Note that these values could be wrong, either because of data corruption, or because a 3rd party deliberately spoofs false information. + As a consequence, check that values remain within valid application range. + For example, do not allocate memory blindly, check that `windowSize` is within expectation. + Each application can set its own limits, depending on local restrictions. + For extended interoperability, it is recommended to support `windowSize` of at least 8 MB. + + ZSTD_decompressContinue() needs previous data blocks during decompression, up to `windowSize` bytes. + ZSTD_decompressContinue() is very sensitive to contiguity, + if 2 blocks don't follow each other, make sure that either the compressor breaks contiguity at the same place, + or that previous contiguous segment is large enough to properly handle maximum back-reference distance. + There are multiple ways to guarantee this condition. + + The most memory efficient way is to use a round buffer of sufficient size. + Sufficient size is determined by invoking ZSTD_decodingBufferSize_min(), + which can return an error code if required value is too large for current system (in 32-bits mode). + In a round buffer methodology, ZSTD_decompressContinue() decompresses each block next to previous one, + up to the moment there is not enough room left in the buffer to guarantee decoding another full block, + which maximum size is provided in `ZSTD_frameHeader` structure, field `blockSizeMax`. + At which point, decoding can resume from the beginning of the buffer. + Note that already decoded data stored in the buffer should be flushed before being overwritten. + + There are alternatives possible, for example using two or more buffers of size `windowSize` each, though they consume more memory. + + Finally, if you control the compression process, you can also ignore all buffer size rules, + as long as the encoder and decoder progress in "lock-step", + aka use exactly the same buffer sizes, break contiguity at the same place, etc. + + Once buffers are setup, start decompression, with ZSTD_decompressBegin(). + If decompression requires a dictionary, use ZSTD_decompressBegin_usingDict() or ZSTD_decompressBegin_usingDDict(). + + Then use ZSTD_nextSrcSizeToDecompress() and ZSTD_decompressContinue() alternatively. + ZSTD_nextSrcSizeToDecompress() tells how many bytes to provide as 'srcSize' to ZSTD_decompressContinue(). + ZSTD_decompressContinue() requires this _exact_ amount of bytes, or it will fail. + + result of ZSTD_decompressContinue() is the number of bytes regenerated within 'dst' (necessarily <= dstCapacity). + It can be zero : it just means ZSTD_decompressContinue() has decoded some metadata item. + It can also be an error code, which can be tested with ZSTD_isError(). + + A frame is fully decoded when ZSTD_nextSrcSizeToDecompress() returns zero. + Context can then be reset to start a new decompression. + + Note : it's possible to know if next input to present is a header or a block, using ZSTD_nextInputType(). + This information is not required to properly decode a frame. + + == Special case : skippable frames == + + Skippable frames allow integration of user-defined data into a flow of concatenated frames. + Skippable frames will be ignored (skipped) by decompressor. + The format of skippable frames is as follows : + a) Skippable frame ID - 4 Bytes, Little endian format, any value from 0x184D2A50 to 0x184D2A5F + b) Frame Size - 4 Bytes, Little endian format, unsigned 32-bits + c) Frame Content - any content (User Data) of length equal to Frame Size + For skippable frames ZSTD_getFrameHeader() returns zfhPtr->frameType==ZSTD_skippableFrame. + For skippable frames ZSTD_decompressContinue() always returns 0 : it only skips the content. +*/ + +/*===== Buffer-less streaming decompression functions =====*/ + +ZSTDLIB_STATIC_API size_t ZSTD_decodingBufferSize_min(unsigned long long windowSize, unsigned long long frameContentSize); /**< when frame content size is not known, pass in frameContentSize == ZSTD_CONTENTSIZE_UNKNOWN */ + +ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin(ZSTD_DCtx* dctx); +ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin_usingDict(ZSTD_DCtx* dctx, const void* dict, size_t dictSize); +ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin_usingDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict); + +ZSTDLIB_STATIC_API size_t ZSTD_nextSrcSizeToDecompress(ZSTD_DCtx* dctx); +ZSTDLIB_STATIC_API size_t ZSTD_decompressContinue(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); + +/* misc */ +ZSTD_DEPRECATED("This function will likely be removed in the next minor release. It is misleading and has very limited utility.") +ZSTDLIB_STATIC_API void ZSTD_copyDCtx(ZSTD_DCtx* dctx, const ZSTD_DCtx* preparedDCtx); +typedef enum { ZSTDnit_frameHeader, ZSTDnit_blockHeader, ZSTDnit_block, ZSTDnit_lastBlock, ZSTDnit_checksum, ZSTDnit_skippableFrame } ZSTD_nextInputType_e; +ZSTDLIB_STATIC_API ZSTD_nextInputType_e ZSTD_nextInputType(ZSTD_DCtx* dctx); + + + + +/* ========================================= */ +/** Block level API (DEPRECATED) */ +/* ========================================= */ + +/*! + + This API is deprecated in favor of the regular compression API. + You can get the frame header down to 2 bytes by setting: + - ZSTD_c_format = ZSTD_f_zstd1_magicless + - ZSTD_c_contentSizeFlag = 0 + - ZSTD_c_checksumFlag = 0 + - ZSTD_c_dictIDFlag = 0 + + This API is not as well tested as our normal API, so we recommend not using it. + We will be removing it in a future version. If the normal API doesn't provide + the functionality you need, please open a GitHub issue. + + Block functions produce and decode raw zstd blocks, without frame metadata. + Frame metadata cost is typically ~12 bytes, which can be non-negligible for very small blocks (< 100 bytes). + But users will have to take in charge needed metadata to regenerate data, such as compressed and content sizes. + + A few rules to respect : + - Compressing and decompressing require a context structure + + Use ZSTD_createCCtx() and ZSTD_createDCtx() + - It is necessary to init context before starting + + compression : any ZSTD_compressBegin*() variant, including with dictionary + + decompression : any ZSTD_decompressBegin*() variant, including with dictionary + - Block size is limited, it must be <= ZSTD_getBlockSize() <= ZSTD_BLOCKSIZE_MAX == 128 KB + + If input is larger than a block size, it's necessary to split input data into multiple blocks + + For inputs larger than a single block, consider using regular ZSTD_compress() instead. + Frame metadata is not that costly, and quickly becomes negligible as source size grows larger than a block. + - When a block is considered not compressible enough, ZSTD_compressBlock() result will be 0 (zero) ! + ===> In which case, nothing is produced into `dst` ! + + User __must__ test for such outcome and deal directly with uncompressed data + + A block cannot be declared incompressible if ZSTD_compressBlock() return value was != 0. + Doing so would mess up with statistics history, leading to potential data corruption. + + ZSTD_decompressBlock() _doesn't accept uncompressed data as input_ !! + + In case of multiple successive blocks, should some of them be uncompressed, + decoder must be informed of their existence in order to follow proper history. + Use ZSTD_insertBlock() for such a case. +*/ + +/*===== Raw zstd block functions =====*/ +ZSTD_DEPRECATED("The block API is deprecated in favor of the normal compression API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_getBlockSize (const ZSTD_CCtx* cctx); +ZSTD_DEPRECATED("The block API is deprecated in favor of the normal compression API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressBlock (ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); +ZSTD_DEPRECATED("The block API is deprecated in favor of the normal compression API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_decompressBlock(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); +ZSTD_DEPRECATED("The block API is deprecated in favor of the normal compression API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_insertBlock (ZSTD_DCtx* dctx, const void* blockStart, size_t blockSize); /**< insert uncompressed block into `dctx` history. Useful for multi-blocks decompression. */ + +#endif /* ZSTD_H_ZSTD_STATIC_LINKING_ONLY */ + +#if defined (__cplusplus) +} +#endif +/**** ended inlining ../zstd.h ****/ + +#ifndef ZSTD_ALLOCATIONS_H +#define ZSTD_ALLOCATIONS_H + +/* custom memory allocation functions */ + +MEM_STATIC void* ZSTD_customMalloc(size_t size, ZSTD_customMem customMem) +{ + if (customMem.customAlloc) + return customMem.customAlloc(customMem.opaque, size); + return ZSTD_malloc(size); +} + +MEM_STATIC void* ZSTD_customCalloc(size_t size, ZSTD_customMem customMem) +{ + if (customMem.customAlloc) { + /* calloc implemented as malloc+memset; + * not as efficient as calloc, but next best guess for custom malloc */ + void* const ptr = customMem.customAlloc(customMem.opaque, size); + ZSTD_memset(ptr, 0, size); + return ptr; + } + return ZSTD_calloc(1, size); +} + +MEM_STATIC void ZSTD_customFree(void* ptr, ZSTD_customMem customMem) +{ + if (ptr!=NULL) { + if (customMem.customFree) + customMem.customFree(customMem.opaque, ptr); + else + ZSTD_free(ptr); + } +} + +#endif /* ZSTD_ALLOCATIONS_H */ +/**** ended inlining ../common/allocations.h ****/ +/**** skipping file: zstd_deps.h ****/ +/**** skipping file: debug.h ****/ +/**** start inlining pool.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef POOL_H +#define POOL_H + +#if defined (__cplusplus) +extern "C" { +#endif + + +/**** skipping file: zstd_deps.h ****/ +#define ZSTD_STATIC_LINKING_ONLY /* ZSTD_customMem */ +/**** skipping file: ../zstd.h ****/ + +typedef struct POOL_ctx_s POOL_ctx; + +/*! POOL_create() : + * Create a thread pool with at most `numThreads` threads. + * `numThreads` must be at least 1. + * The maximum number of queued jobs before blocking is `queueSize`. + * @return : POOL_ctx pointer on success, else NULL. +*/ +POOL_ctx* POOL_create(size_t numThreads, size_t queueSize); + +POOL_ctx* POOL_create_advanced(size_t numThreads, size_t queueSize, + ZSTD_customMem customMem); + +/*! POOL_free() : + * Free a thread pool returned by POOL_create(). + */ +void POOL_free(POOL_ctx* ctx); + + +/*! POOL_joinJobs() : + * Waits for all queued jobs to finish executing. + */ +void POOL_joinJobs(POOL_ctx* ctx); + +/*! POOL_resize() : + * Expands or shrinks pool's number of threads. + * This is more efficient than releasing + creating a new context, + * since it tries to preserve and reuse existing threads. + * `numThreads` must be at least 1. + * @return : 0 when resize was successful, + * !0 (typically 1) if there is an error. + * note : only numThreads can be resized, queueSize remains unchanged. + */ +int POOL_resize(POOL_ctx* ctx, size_t numThreads); + +/*! POOL_sizeof() : + * @return threadpool memory usage + * note : compatible with NULL (returns 0 in this case) + */ +size_t POOL_sizeof(const POOL_ctx* ctx); + +/*! POOL_function : + * The function type that can be added to a thread pool. + */ +typedef void (*POOL_function)(void*); + +/*! POOL_add() : + * Add the job `function(opaque)` to the thread pool. `ctx` must be valid. + * Possibly blocks until there is room in the queue. + * Note : The function may be executed asynchronously, + * therefore, `opaque` must live until function has been completed. + */ +void POOL_add(POOL_ctx* ctx, POOL_function function, void* opaque); + + +/*! POOL_tryAdd() : + * Add the job `function(opaque)` to thread pool _if_ a queue slot is available. + * Returns immediately even if not (does not block). + * @return : 1 if successful, 0 if not. + */ +int POOL_tryAdd(POOL_ctx* ctx, POOL_function function, void* opaque); + + +#if defined (__cplusplus) +} +#endif + +#endif +/**** ended inlining pool.h ****/ + +/* ====== Compiler specifics ====== */ +#if defined(_MSC_VER) +# pragma warning(disable : 4204) /* disable: C4204: non-constant aggregate initializer */ +#endif + + +#ifdef ZSTD_MULTITHREAD + +/**** skipping file: threading.h ****/ + +/* A job is a function and an opaque argument */ +typedef struct POOL_job_s { + POOL_function function; + void *opaque; +} POOL_job; + +struct POOL_ctx_s { + ZSTD_customMem customMem; + /* Keep track of the threads */ + ZSTD_pthread_t* threads; + size_t threadCapacity; + size_t threadLimit; + + /* The queue is a circular buffer */ + POOL_job *queue; + size_t queueHead; + size_t queueTail; + size_t queueSize; + + /* The number of threads working on jobs */ + size_t numThreadsBusy; + /* Indicates if the queue is empty */ + int queueEmpty; + + /* The mutex protects the queue */ + ZSTD_pthread_mutex_t queueMutex; + /* Condition variable for pushers to wait on when the queue is full */ + ZSTD_pthread_cond_t queuePushCond; + /* Condition variables for poppers to wait on when the queue is empty */ + ZSTD_pthread_cond_t queuePopCond; + /* Indicates if the queue is shutting down */ + int shutdown; +}; + +/* POOL_thread() : + * Work thread for the thread pool. + * Waits for jobs and executes them. + * @returns : NULL on failure else non-null. + */ +static void* POOL_thread(void* opaque) { + POOL_ctx* const ctx = (POOL_ctx*)opaque; + if (!ctx) { return NULL; } + for (;;) { + /* Lock the mutex and wait for a non-empty queue or until shutdown */ + ZSTD_pthread_mutex_lock(&ctx->queueMutex); + + while ( ctx->queueEmpty + || (ctx->numThreadsBusy >= ctx->threadLimit) ) { + if (ctx->shutdown) { + /* even if !queueEmpty, (possible if numThreadsBusy >= threadLimit), + * a few threads will be shutdown while !queueEmpty, + * but enough threads will remain active to finish the queue */ + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); + return opaque; + } + ZSTD_pthread_cond_wait(&ctx->queuePopCond, &ctx->queueMutex); + } + /* Pop a job off the queue */ + { POOL_job const job = ctx->queue[ctx->queueHead]; + ctx->queueHead = (ctx->queueHead + 1) % ctx->queueSize; + ctx->numThreadsBusy++; + ctx->queueEmpty = (ctx->queueHead == ctx->queueTail); + /* Unlock the mutex, signal a pusher, and run the job */ + ZSTD_pthread_cond_signal(&ctx->queuePushCond); + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); + + job.function(job.opaque); + + /* If the intended queue size was 0, signal after finishing job */ + ZSTD_pthread_mutex_lock(&ctx->queueMutex); + ctx->numThreadsBusy--; + ZSTD_pthread_cond_signal(&ctx->queuePushCond); + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); + } + } /* for (;;) */ + assert(0); /* Unreachable */ +} + +/* ZSTD_createThreadPool() : public access point */ +POOL_ctx* ZSTD_createThreadPool(size_t numThreads) { + return POOL_create (numThreads, 0); +} + +POOL_ctx* POOL_create(size_t numThreads, size_t queueSize) { + return POOL_create_advanced(numThreads, queueSize, ZSTD_defaultCMem); +} + +POOL_ctx* POOL_create_advanced(size_t numThreads, size_t queueSize, + ZSTD_customMem customMem) +{ + POOL_ctx* ctx; + /* Check parameters */ + if (!numThreads) { return NULL; } + /* Allocate the context and zero initialize */ + ctx = (POOL_ctx*)ZSTD_customCalloc(sizeof(POOL_ctx), customMem); + if (!ctx) { return NULL; } + /* Initialize the job queue. + * It needs one extra space since one space is wasted to differentiate + * empty and full queues. + */ + ctx->queueSize = queueSize + 1; + ctx->queue = (POOL_job*)ZSTD_customCalloc(ctx->queueSize * sizeof(POOL_job), customMem); + ctx->queueHead = 0; + ctx->queueTail = 0; + ctx->numThreadsBusy = 0; + ctx->queueEmpty = 1; + { + int error = 0; + error |= ZSTD_pthread_mutex_init(&ctx->queueMutex, NULL); + error |= ZSTD_pthread_cond_init(&ctx->queuePushCond, NULL); + error |= ZSTD_pthread_cond_init(&ctx->queuePopCond, NULL); + if (error) { POOL_free(ctx); return NULL; } + } + ctx->shutdown = 0; + /* Allocate space for the thread handles */ + ctx->threads = (ZSTD_pthread_t*)ZSTD_customCalloc(numThreads * sizeof(ZSTD_pthread_t), customMem); + ctx->threadCapacity = 0; + ctx->customMem = customMem; + /* Check for errors */ + if (!ctx->threads || !ctx->queue) { POOL_free(ctx); return NULL; } + /* Initialize the threads */ + { size_t i; + for (i = 0; i < numThreads; ++i) { + if (ZSTD_pthread_create(&ctx->threads[i], NULL, &POOL_thread, ctx)) { + ctx->threadCapacity = i; + POOL_free(ctx); + return NULL; + } } + ctx->threadCapacity = numThreads; + ctx->threadLimit = numThreads; + } + return ctx; +} + +/*! POOL_join() : + Shutdown the queue, wake any sleeping threads, and join all of the threads. +*/ +static void POOL_join(POOL_ctx* ctx) { + /* Shut down the queue */ + ZSTD_pthread_mutex_lock(&ctx->queueMutex); + ctx->shutdown = 1; + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); + /* Wake up sleeping threads */ + ZSTD_pthread_cond_broadcast(&ctx->queuePushCond); + ZSTD_pthread_cond_broadcast(&ctx->queuePopCond); + /* Join all of the threads */ + { size_t i; + for (i = 0; i < ctx->threadCapacity; ++i) { + ZSTD_pthread_join(ctx->threads[i]); /* note : could fail */ + } } +} + +void POOL_free(POOL_ctx *ctx) { + if (!ctx) { return; } + POOL_join(ctx); + ZSTD_pthread_mutex_destroy(&ctx->queueMutex); + ZSTD_pthread_cond_destroy(&ctx->queuePushCond); + ZSTD_pthread_cond_destroy(&ctx->queuePopCond); + ZSTD_customFree(ctx->queue, ctx->customMem); + ZSTD_customFree(ctx->threads, ctx->customMem); + ZSTD_customFree(ctx, ctx->customMem); +} + +/*! POOL_joinJobs() : + * Waits for all queued jobs to finish executing. + */ +void POOL_joinJobs(POOL_ctx* ctx) { + ZSTD_pthread_mutex_lock(&ctx->queueMutex); + while(!ctx->queueEmpty || ctx->numThreadsBusy > 0) { + ZSTD_pthread_cond_wait(&ctx->queuePushCond, &ctx->queueMutex); + } + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); +} + +void ZSTD_freeThreadPool (ZSTD_threadPool* pool) { + POOL_free (pool); +} + +size_t POOL_sizeof(const POOL_ctx* ctx) { + if (ctx==NULL) return 0; /* supports sizeof NULL */ + return sizeof(*ctx) + + ctx->queueSize * sizeof(POOL_job) + + ctx->threadCapacity * sizeof(ZSTD_pthread_t); +} + + +/* @return : 0 on success, 1 on error */ +static int POOL_resize_internal(POOL_ctx* ctx, size_t numThreads) +{ + if (numThreads <= ctx->threadCapacity) { + if (!numThreads) return 1; + ctx->threadLimit = numThreads; + return 0; + } + /* numThreads > threadCapacity */ + { ZSTD_pthread_t* const threadPool = (ZSTD_pthread_t*)ZSTD_customCalloc(numThreads * sizeof(ZSTD_pthread_t), ctx->customMem); + if (!threadPool) return 1; + /* replace existing thread pool */ + ZSTD_memcpy(threadPool, ctx->threads, ctx->threadCapacity * sizeof(ZSTD_pthread_t)); + ZSTD_customFree(ctx->threads, ctx->customMem); + ctx->threads = threadPool; + /* Initialize additional threads */ + { size_t threadId; + for (threadId = ctx->threadCapacity; threadId < numThreads; ++threadId) { + if (ZSTD_pthread_create(&threadPool[threadId], NULL, &POOL_thread, ctx)) { + ctx->threadCapacity = threadId; + return 1; + } } + } } + /* successfully expanded */ + ctx->threadCapacity = numThreads; + ctx->threadLimit = numThreads; + return 0; +} + +/* @return : 0 on success, 1 on error */ +int POOL_resize(POOL_ctx* ctx, size_t numThreads) +{ + int result; + if (ctx==NULL) return 1; + ZSTD_pthread_mutex_lock(&ctx->queueMutex); + result = POOL_resize_internal(ctx, numThreads); + ZSTD_pthread_cond_broadcast(&ctx->queuePopCond); + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); + return result; +} + +/** + * Returns 1 if the queue is full and 0 otherwise. + * + * When queueSize is 1 (pool was created with an intended queueSize of 0), + * then a queue is empty if there is a thread free _and_ no job is waiting. + */ +static int isQueueFull(POOL_ctx const* ctx) { + if (ctx->queueSize > 1) { + return ctx->queueHead == ((ctx->queueTail + 1) % ctx->queueSize); + } else { + return (ctx->numThreadsBusy == ctx->threadLimit) || + !ctx->queueEmpty; + } +} + + +static void +POOL_add_internal(POOL_ctx* ctx, POOL_function function, void *opaque) +{ + POOL_job job; + job.function = function; + job.opaque = opaque; + assert(ctx != NULL); + if (ctx->shutdown) return; + + ctx->queueEmpty = 0; + ctx->queue[ctx->queueTail] = job; + ctx->queueTail = (ctx->queueTail + 1) % ctx->queueSize; + ZSTD_pthread_cond_signal(&ctx->queuePopCond); +} + +void POOL_add(POOL_ctx* ctx, POOL_function function, void* opaque) +{ + assert(ctx != NULL); + ZSTD_pthread_mutex_lock(&ctx->queueMutex); + /* Wait until there is space in the queue for the new job */ + while (isQueueFull(ctx) && (!ctx->shutdown)) { + ZSTD_pthread_cond_wait(&ctx->queuePushCond, &ctx->queueMutex); + } + POOL_add_internal(ctx, function, opaque); + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); +} + + +int POOL_tryAdd(POOL_ctx* ctx, POOL_function function, void* opaque) +{ + assert(ctx != NULL); + ZSTD_pthread_mutex_lock(&ctx->queueMutex); + if (isQueueFull(ctx)) { + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); + return 0; + } + POOL_add_internal(ctx, function, opaque); + ZSTD_pthread_mutex_unlock(&ctx->queueMutex); + return 1; +} + + +#else /* ZSTD_MULTITHREAD not defined */ + +/* ========================== */ +/* No multi-threading support */ +/* ========================== */ + + +/* We don't need any data, but if it is empty, malloc() might return NULL. */ +struct POOL_ctx_s { + int dummy; +}; +static POOL_ctx g_poolCtx; + +POOL_ctx* POOL_create(size_t numThreads, size_t queueSize) { + return POOL_create_advanced(numThreads, queueSize, ZSTD_defaultCMem); +} + +POOL_ctx* +POOL_create_advanced(size_t numThreads, size_t queueSize, ZSTD_customMem customMem) +{ + (void)numThreads; + (void)queueSize; + (void)customMem; + return &g_poolCtx; +} + +void POOL_free(POOL_ctx* ctx) { + assert(!ctx || ctx == &g_poolCtx); + (void)ctx; +} + +void POOL_joinJobs(POOL_ctx* ctx){ + assert(!ctx || ctx == &g_poolCtx); + (void)ctx; +} + +int POOL_resize(POOL_ctx* ctx, size_t numThreads) { + (void)ctx; (void)numThreads; + return 0; +} + +void POOL_add(POOL_ctx* ctx, POOL_function function, void* opaque) { + (void)ctx; + function(opaque); +} + +int POOL_tryAdd(POOL_ctx* ctx, POOL_function function, void* opaque) { + (void)ctx; + function(opaque); + return 1; +} + +size_t POOL_sizeof(const POOL_ctx* ctx) { + if (ctx==NULL) return 0; /* supports sizeof NULL */ + assert(ctx == &g_poolCtx); + return sizeof(*ctx); +} + +#endif /* ZSTD_MULTITHREAD */ +/**** ended inlining common/pool.c ****/ +/**** start inlining common/zstd_common.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + + +/*-************************************* +* Dependencies +***************************************/ +#define ZSTD_DEPS_NEED_MALLOC +/**** skipping file: error_private.h ****/ +/**** start inlining zstd_internal.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_CCOMMON_H_MODULE +#define ZSTD_CCOMMON_H_MODULE + +/* this module contains definitions which must be identical + * across compression, decompression and dictBuilder. + * It also contains a few functions useful to at least 2 of them + * and which benefit from being inlined */ + +/*-************************************* +* Dependencies +***************************************/ +/**** skipping file: compiler.h ****/ +/**** start inlining cpu.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_COMMON_CPU_H +#define ZSTD_COMMON_CPU_H + +/** + * Implementation taken from folly/CpuId.h + * https://github.com/facebook/folly/blob/master/folly/CpuId.h + */ + +/**** skipping file: mem.h ****/ + +#ifdef _MSC_VER +#include +#endif + +typedef struct { + U32 f1c; + U32 f1d; + U32 f7b; + U32 f7c; +} ZSTD_cpuid_t; + +MEM_STATIC ZSTD_cpuid_t ZSTD_cpuid(void) { + U32 f1c = 0; + U32 f1d = 0; + U32 f7b = 0; + U32 f7c = 0; +#if defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) +#if !defined(__clang__) + int reg[4]; + __cpuid((int*)reg, 0); + { + int const n = reg[0]; + if (n >= 1) { + __cpuid((int*)reg, 1); + f1c = (U32)reg[2]; + f1d = (U32)reg[3]; + } + if (n >= 7) { + __cpuidex((int*)reg, 7, 0); + f7b = (U32)reg[1]; + f7c = (U32)reg[2]; + } + } +#else + /* Clang compiler has a bug (fixed in https://reviews.llvm.org/D101338) in + * which the `__cpuid` intrinsic does not save and restore `rbx` as it needs + * to due to being a reserved register. So in that case, do the `cpuid` + * ourselves. Clang supports inline assembly anyway. + */ + U32 n; + __asm__( + "pushq %%rbx\n\t" + "cpuid\n\t" + "popq %%rbx\n\t" + : "=a"(n) + : "a"(0) + : "rcx", "rdx"); + if (n >= 1) { + U32 f1a; + __asm__( + "pushq %%rbx\n\t" + "cpuid\n\t" + "popq %%rbx\n\t" + : "=a"(f1a), "=c"(f1c), "=d"(f1d) + : "a"(1) + :); + } + if (n >= 7) { + __asm__( + "pushq %%rbx\n\t" + "cpuid\n\t" + "movq %%rbx, %%rax\n\t" + "popq %%rbx" + : "=a"(f7b), "=c"(f7c) + : "a"(7), "c"(0) + : "rdx"); + } +#endif +#elif defined(__i386__) && defined(__PIC__) && !defined(__clang__) && defined(__GNUC__) + /* The following block like the normal cpuid branch below, but gcc + * reserves ebx for use of its pic register so we must specially + * handle the save and restore to avoid clobbering the register + */ + U32 n; + __asm__( + "pushl %%ebx\n\t" + "cpuid\n\t" + "popl %%ebx\n\t" + : "=a"(n) + : "a"(0) + : "ecx", "edx"); + if (n >= 1) { + U32 f1a; + __asm__( + "pushl %%ebx\n\t" + "cpuid\n\t" + "popl %%ebx\n\t" + : "=a"(f1a), "=c"(f1c), "=d"(f1d) + : "a"(1)); + } + if (n >= 7) { + __asm__( + "pushl %%ebx\n\t" + "cpuid\n\t" + "movl %%ebx, %%eax\n\t" + "popl %%ebx" + : "=a"(f7b), "=c"(f7c) + : "a"(7), "c"(0) + : "edx"); + } +#elif defined(__x86_64__) || defined(_M_X64) || defined(__i386__) + U32 n; + __asm__("cpuid" : "=a"(n) : "a"(0) : "ebx", "ecx", "edx"); + if (n >= 1) { + U32 f1a; + __asm__("cpuid" : "=a"(f1a), "=c"(f1c), "=d"(f1d) : "a"(1) : "ebx"); + } + if (n >= 7) { + U32 f7a; + __asm__("cpuid" + : "=a"(f7a), "=b"(f7b), "=c"(f7c) + : "a"(7), "c"(0) + : "edx"); + } +#endif + { + ZSTD_cpuid_t cpuid; + cpuid.f1c = f1c; + cpuid.f1d = f1d; + cpuid.f7b = f7b; + cpuid.f7c = f7c; + return cpuid; + } +} + +#define X(name, r, bit) \ + MEM_STATIC int ZSTD_cpuid_##name(ZSTD_cpuid_t const cpuid) { \ + return ((cpuid.r) & (1U << bit)) != 0; \ + } + +/* cpuid(1): Processor Info and Feature Bits. */ +#define C(name, bit) X(name, f1c, bit) + C(sse3, 0) + C(pclmuldq, 1) + C(dtes64, 2) + C(monitor, 3) + C(dscpl, 4) + C(vmx, 5) + C(smx, 6) + C(eist, 7) + C(tm2, 8) + C(ssse3, 9) + C(cnxtid, 10) + C(fma, 12) + C(cx16, 13) + C(xtpr, 14) + C(pdcm, 15) + C(pcid, 17) + C(dca, 18) + C(sse41, 19) + C(sse42, 20) + C(x2apic, 21) + C(movbe, 22) + C(popcnt, 23) + C(tscdeadline, 24) + C(aes, 25) + C(xsave, 26) + C(osxsave, 27) + C(avx, 28) + C(f16c, 29) + C(rdrand, 30) +#undef C +#define D(name, bit) X(name, f1d, bit) + D(fpu, 0) + D(vme, 1) + D(de, 2) + D(pse, 3) + D(tsc, 4) + D(msr, 5) + D(pae, 6) + D(mce, 7) + D(cx8, 8) + D(apic, 9) + D(sep, 11) + D(mtrr, 12) + D(pge, 13) + D(mca, 14) + D(cmov, 15) + D(pat, 16) + D(pse36, 17) + D(psn, 18) + D(clfsh, 19) + D(ds, 21) + D(acpi, 22) + D(mmx, 23) + D(fxsr, 24) + D(sse, 25) + D(sse2, 26) + D(ss, 27) + D(htt, 28) + D(tm, 29) + D(pbe, 31) +#undef D + +/* cpuid(7): Extended Features. */ +#define B(name, bit) X(name, f7b, bit) + B(bmi1, 3) + B(hle, 4) + B(avx2, 5) + B(smep, 7) + B(bmi2, 8) + B(erms, 9) + B(invpcid, 10) + B(rtm, 11) + B(mpx, 14) + B(avx512f, 16) + B(avx512dq, 17) + B(rdseed, 18) + B(adx, 19) + B(smap, 20) + B(avx512ifma, 21) + B(pcommit, 22) + B(clflushopt, 23) + B(clwb, 24) + B(avx512pf, 26) + B(avx512er, 27) + B(avx512cd, 28) + B(sha, 29) + B(avx512bw, 30) + B(avx512vl, 31) +#undef B +#define C(name, bit) X(name, f7c, bit) + C(prefetchwt1, 0) + C(avx512vbmi, 1) +#undef C + +#undef X + +#endif /* ZSTD_COMMON_CPU_H */ +/**** ended inlining cpu.h ****/ +/**** skipping file: mem.h ****/ +/**** skipping file: debug.h ****/ +/**** skipping file: error_private.h ****/ +#define ZSTD_STATIC_LINKING_ONLY +/**** skipping file: ../zstd.h ****/ +#define FSE_STATIC_LINKING_ONLY +/**** skipping file: fse.h ****/ +/**** skipping file: huf.h ****/ +#ifndef XXH_STATIC_LINKING_ONLY +# define XXH_STATIC_LINKING_ONLY /* XXH64_state_t */ +#endif +/**** start inlining xxhash.h ****/ +/* + * xxHash - Extremely Fast Hash algorithm + * Header File + * Copyright (c) Yann Collet - Meta Platforms, Inc + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/* Local adaptations for Zstandard */ + +#ifndef XXH_NO_XXH3 +# define XXH_NO_XXH3 +#endif + +#ifndef XXH_NAMESPACE +# define XXH_NAMESPACE ZSTD_ +#endif + +/*! + * @mainpage xxHash + * + * xxHash is an extremely fast non-cryptographic hash algorithm, working at RAM speed + * limits. + * + * It is proposed in four flavors, in three families: + * 1. @ref XXH32_family + * - Classic 32-bit hash function. Simple, compact, and runs on almost all + * 32-bit and 64-bit systems. + * 2. @ref XXH64_family + * - Classic 64-bit adaptation of XXH32. Just as simple, and runs well on most + * 64-bit systems (but _not_ 32-bit systems). + * 3. @ref XXH3_family + * - Modern 64-bit and 128-bit hash function family which features improved + * strength and performance across the board, especially on smaller data. + * It benefits greatly from SIMD and 64-bit without requiring it. + * + * Benchmarks + * --- + * The reference system uses an Intel i7-9700K CPU, and runs Ubuntu x64 20.04. + * The open source benchmark program is compiled with clang v10.0 using -O3 flag. + * + * | Hash Name | ISA ext | Width | Large Data Speed | Small Data Velocity | + * | -------------------- | ------- | ----: | ---------------: | ------------------: | + * | XXH3_64bits() | @b AVX2 | 64 | 59.4 GB/s | 133.1 | + * | MeowHash | AES-NI | 128 | 58.2 GB/s | 52.5 | + * | XXH3_128bits() | @b AVX2 | 128 | 57.9 GB/s | 118.1 | + * | CLHash | PCLMUL | 64 | 37.1 GB/s | 58.1 | + * | XXH3_64bits() | @b SSE2 | 64 | 31.5 GB/s | 133.1 | + * | XXH3_128bits() | @b SSE2 | 128 | 29.6 GB/s | 118.1 | + * | RAM sequential read | | N/A | 28.0 GB/s | N/A | + * | ahash | AES-NI | 64 | 22.5 GB/s | 107.2 | + * | City64 | | 64 | 22.0 GB/s | 76.6 | + * | T1ha2 | | 64 | 22.0 GB/s | 99.0 | + * | City128 | | 128 | 21.7 GB/s | 57.7 | + * | FarmHash | AES-NI | 64 | 21.3 GB/s | 71.9 | + * | XXH64() | | 64 | 19.4 GB/s | 71.0 | + * | SpookyHash | | 64 | 19.3 GB/s | 53.2 | + * | Mum | | 64 | 18.0 GB/s | 67.0 | + * | CRC32C | SSE4.2 | 32 | 13.0 GB/s | 57.9 | + * | XXH32() | | 32 | 9.7 GB/s | 71.9 | + * | City32 | | 32 | 9.1 GB/s | 66.0 | + * | Blake3* | @b AVX2 | 256 | 4.4 GB/s | 8.1 | + * | Murmur3 | | 32 | 3.9 GB/s | 56.1 | + * | SipHash* | | 64 | 3.0 GB/s | 43.2 | + * | Blake3* | @b SSE2 | 256 | 2.4 GB/s | 8.1 | + * | HighwayHash | | 64 | 1.4 GB/s | 6.0 | + * | FNV64 | | 64 | 1.2 GB/s | 62.7 | + * | Blake2* | | 256 | 1.1 GB/s | 5.1 | + * | SHA1* | | 160 | 0.8 GB/s | 5.6 | + * | MD5* | | 128 | 0.6 GB/s | 7.8 | + * @note + * - Hashes which require a specific ISA extension are noted. SSE2 is also noted, + * even though it is mandatory on x64. + * - Hashes with an asterisk are cryptographic. Note that MD5 is non-cryptographic + * by modern standards. + * - Small data velocity is a rough average of algorithm's efficiency for small + * data. For more accurate information, see the wiki. + * - More benchmarks and strength tests are found on the wiki: + * https://github.com/Cyan4973/xxHash/wiki + * + * Usage + * ------ + * All xxHash variants use a similar API. Changing the algorithm is a trivial + * substitution. + * + * @pre + * For functions which take an input and length parameter, the following + * requirements are assumed: + * - The range from [`input`, `input + length`) is valid, readable memory. + * - The only exception is if the `length` is `0`, `input` may be `NULL`. + * - For C++, the objects must have the *TriviallyCopyable* property, as the + * functions access bytes directly as if it was an array of `unsigned char`. + * + * @anchor single_shot_example + * **Single Shot** + * + * These functions are stateless functions which hash a contiguous block of memory, + * immediately returning the result. They are the easiest and usually the fastest + * option. + * + * XXH32(), XXH64(), XXH3_64bits(), XXH3_128bits() + * + * @code{.c} + * #include + * #include "xxhash.h" + * + * // Example for a function which hashes a null terminated string with XXH32(). + * XXH32_hash_t hash_string(const char* string, XXH32_hash_t seed) + * { + * // NULL pointers are only valid if the length is zero + * size_t length = (string == NULL) ? 0 : strlen(string); + * return XXH32(string, length, seed); + * } + * @endcode + * + * + * @anchor streaming_example + * **Streaming** + * + * These groups of functions allow incremental hashing of unknown size, even + * more than what would fit in a size_t. + * + * XXH32_reset(), XXH64_reset(), XXH3_64bits_reset(), XXH3_128bits_reset() + * + * @code{.c} + * #include + * #include + * #include "xxhash.h" + * // Example for a function which hashes a FILE incrementally with XXH3_64bits(). + * XXH64_hash_t hashFile(FILE* f) + * { + * // Allocate a state struct. Do not just use malloc() or new. + * XXH3_state_t* state = XXH3_createState(); + * assert(state != NULL && "Out of memory!"); + * // Reset the state to start a new hashing session. + * XXH3_64bits_reset(state); + * char buffer[4096]; + * size_t count; + * // Read the file in chunks + * while ((count = fread(buffer, 1, sizeof(buffer), f)) != 0) { + * // Run update() as many times as necessary to process the data + * XXH3_64bits_update(state, buffer, count); + * } + * // Retrieve the finalized hash. This will not change the state. + * XXH64_hash_t result = XXH3_64bits_digest(state); + * // Free the state. Do not use free(). + * XXH3_freeState(state); + * return result; + * } + * @endcode + * + * Streaming functions generate the xxHash value from an incremental input. + * This method is slower than single-call functions, due to state management. + * For small inputs, prefer `XXH32()` and `XXH64()`, which are better optimized. + * + * An XXH state must first be allocated using `XXH*_createState()`. + * + * Start a new hash by initializing the state with a seed using `XXH*_reset()`. + * + * Then, feed the hash state by calling `XXH*_update()` as many times as necessary. + * + * The function returns an error code, with 0 meaning OK, and any other value + * meaning there is an error. + * + * Finally, a hash value can be produced anytime, by using `XXH*_digest()`. + * This function returns the nn-bits hash as an int or long long. + * + * It's still possible to continue inserting input into the hash state after a + * digest, and generate new hash values later on by invoking `XXH*_digest()`. + * + * When done, release the state using `XXH*_freeState()`. + * + * + * @anchor canonical_representation_example + * **Canonical Representation** + * + * The default return values from XXH functions are unsigned 32, 64 and 128 bit + * integers. + * This the simplest and fastest format for further post-processing. + * + * However, this leaves open the question of what is the order on the byte level, + * since little and big endian conventions will store the same number differently. + * + * The canonical representation settles this issue by mandating big-endian + * convention, the same convention as human-readable numbers (large digits first). + * + * When writing hash values to storage, sending them over a network, or printing + * them, it's highly recommended to use the canonical representation to ensure + * portability across a wider range of systems, present and future. + * + * The following functions allow transformation of hash values to and from + * canonical format. + * + * XXH32_canonicalFromHash(), XXH32_hashFromCanonical(), + * XXH64_canonicalFromHash(), XXH64_hashFromCanonical(), + * XXH128_canonicalFromHash(), XXH128_hashFromCanonical(), + * + * @code{.c} + * #include + * #include "xxhash.h" + * + * // Example for a function which prints XXH32_hash_t in human readable format + * void printXxh32(XXH32_hash_t hash) + * { + * XXH32_canonical_t cano; + * XXH32_canonicalFromHash(&cano, hash); + * size_t i; + * for(i = 0; i < sizeof(cano.digest); ++i) { + * printf("%02x", cano.digest[i]); + * } + * printf("\n"); + * } + * + * // Example for a function which converts XXH32_canonical_t to XXH32_hash_t + * XXH32_hash_t convertCanonicalToXxh32(XXH32_canonical_t cano) + * { + * XXH32_hash_t hash = XXH32_hashFromCanonical(&cano); + * return hash; + * } + * @endcode + * + * + * @file xxhash.h + * xxHash prototypes and implementation + */ + +#if defined (__cplusplus) +extern "C" { +#endif + +/* **************************** + * INLINE mode + ******************************/ +/*! + * @defgroup public Public API + * Contains details on the public xxHash functions. + * @{ + */ +#ifdef XXH_DOXYGEN +/*! + * @brief Gives access to internal state declaration, required for static allocation. + * + * Incompatible with dynamic linking, due to risks of ABI changes. + * + * Usage: + * @code{.c} + * #define XXH_STATIC_LINKING_ONLY + * #include "xxhash.h" + * @endcode + */ +# define XXH_STATIC_LINKING_ONLY +/* Do not undef XXH_STATIC_LINKING_ONLY for Doxygen */ + +/*! + * @brief Gives access to internal definitions. + * + * Usage: + * @code{.c} + * #define XXH_STATIC_LINKING_ONLY + * #define XXH_IMPLEMENTATION + * #include "xxhash.h" + * @endcode + */ +# define XXH_IMPLEMENTATION +/* Do not undef XXH_IMPLEMENTATION for Doxygen */ + +/*! + * @brief Exposes the implementation and marks all functions as `inline`. + * + * Use these build macros to inline xxhash into the target unit. + * Inlining improves performance on small inputs, especially when the length is + * expressed as a compile-time constant: + * + * https://fastcompression.blogspot.com/2018/03/xxhash-for-small-keys-impressive-power.html + * + * It also keeps xxHash symbols private to the unit, so they are not exported. + * + * Usage: + * @code{.c} + * #define XXH_INLINE_ALL + * #include "xxhash.h" + * @endcode + * Do not compile and link xxhash.o as a separate object, as it is not useful. + */ +# define XXH_INLINE_ALL +# undef XXH_INLINE_ALL +/*! + * @brief Exposes the implementation without marking functions as inline. + */ +# define XXH_PRIVATE_API +# undef XXH_PRIVATE_API +/*! + * @brief Emulate a namespace by transparently prefixing all symbols. + * + * If you want to include _and expose_ xxHash functions from within your own + * library, but also want to avoid symbol collisions with other libraries which + * may also include xxHash, you can use @ref XXH_NAMESPACE to automatically prefix + * any public symbol from xxhash library with the value of @ref XXH_NAMESPACE + * (therefore, avoid empty or numeric values). + * + * Note that no change is required within the calling program as long as it + * includes `xxhash.h`: Regular symbol names will be automatically translated + * by this header. + */ +# define XXH_NAMESPACE /* YOUR NAME HERE */ +# undef XXH_NAMESPACE +#endif + +#if (defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API)) \ + && !defined(XXH_INLINE_ALL_31684351384) + /* this section should be traversed only once */ +# define XXH_INLINE_ALL_31684351384 + /* give access to the advanced API, required to compile implementations */ +# undef XXH_STATIC_LINKING_ONLY /* avoid macro redef */ +# define XXH_STATIC_LINKING_ONLY + /* make all functions private */ +# undef XXH_PUBLIC_API +# if defined(__GNUC__) +# define XXH_PUBLIC_API static __inline __attribute__((unused)) +# elif defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) +# define XXH_PUBLIC_API static inline +# elif defined(_MSC_VER) +# define XXH_PUBLIC_API static __inline +# else + /* note: this version may generate warnings for unused static functions */ +# define XXH_PUBLIC_API static +# endif + + /* + * This part deals with the special case where a unit wants to inline xxHash, + * but "xxhash.h" has previously been included without XXH_INLINE_ALL, + * such as part of some previously included *.h header file. + * Without further action, the new include would just be ignored, + * and functions would effectively _not_ be inlined (silent failure). + * The following macros solve this situation by prefixing all inlined names, + * avoiding naming collision with previous inclusions. + */ + /* Before that, we unconditionally #undef all symbols, + * in case they were already defined with XXH_NAMESPACE. + * They will then be redefined for XXH_INLINE_ALL + */ +# undef XXH_versionNumber + /* XXH32 */ +# undef XXH32 +# undef XXH32_createState +# undef XXH32_freeState +# undef XXH32_reset +# undef XXH32_update +# undef XXH32_digest +# undef XXH32_copyState +# undef XXH32_canonicalFromHash +# undef XXH32_hashFromCanonical + /* XXH64 */ +# undef XXH64 +# undef XXH64_createState +# undef XXH64_freeState +# undef XXH64_reset +# undef XXH64_update +# undef XXH64_digest +# undef XXH64_copyState +# undef XXH64_canonicalFromHash +# undef XXH64_hashFromCanonical + /* XXH3_64bits */ +# undef XXH3_64bits +# undef XXH3_64bits_withSecret +# undef XXH3_64bits_withSeed +# undef XXH3_64bits_withSecretandSeed +# undef XXH3_createState +# undef XXH3_freeState +# undef XXH3_copyState +# undef XXH3_64bits_reset +# undef XXH3_64bits_reset_withSeed +# undef XXH3_64bits_reset_withSecret +# undef XXH3_64bits_update +# undef XXH3_64bits_digest +# undef XXH3_generateSecret + /* XXH3_128bits */ +# undef XXH128 +# undef XXH3_128bits +# undef XXH3_128bits_withSeed +# undef XXH3_128bits_withSecret +# undef XXH3_128bits_reset +# undef XXH3_128bits_reset_withSeed +# undef XXH3_128bits_reset_withSecret +# undef XXH3_128bits_reset_withSecretandSeed +# undef XXH3_128bits_update +# undef XXH3_128bits_digest +# undef XXH128_isEqual +# undef XXH128_cmp +# undef XXH128_canonicalFromHash +# undef XXH128_hashFromCanonical + /* Finally, free the namespace itself */ +# undef XXH_NAMESPACE + + /* employ the namespace for XXH_INLINE_ALL */ +# define XXH_NAMESPACE XXH_INLINE_ + /* + * Some identifiers (enums, type names) are not symbols, + * but they must nonetheless be renamed to avoid redeclaration. + * Alternative solution: do not redeclare them. + * However, this requires some #ifdefs, and has a more dispersed impact. + * Meanwhile, renaming can be achieved in a single place. + */ +# define XXH_IPREF(Id) XXH_NAMESPACE ## Id +# define XXH_OK XXH_IPREF(XXH_OK) +# define XXH_ERROR XXH_IPREF(XXH_ERROR) +# define XXH_errorcode XXH_IPREF(XXH_errorcode) +# define XXH32_canonical_t XXH_IPREF(XXH32_canonical_t) +# define XXH64_canonical_t XXH_IPREF(XXH64_canonical_t) +# define XXH128_canonical_t XXH_IPREF(XXH128_canonical_t) +# define XXH32_state_s XXH_IPREF(XXH32_state_s) +# define XXH32_state_t XXH_IPREF(XXH32_state_t) +# define XXH64_state_s XXH_IPREF(XXH64_state_s) +# define XXH64_state_t XXH_IPREF(XXH64_state_t) +# define XXH3_state_s XXH_IPREF(XXH3_state_s) +# define XXH3_state_t XXH_IPREF(XXH3_state_t) +# define XXH128_hash_t XXH_IPREF(XXH128_hash_t) + /* Ensure the header is parsed again, even if it was previously included */ +# undef XXHASH_H_5627135585666179 +# undef XXHASH_H_STATIC_13879238742 +#endif /* XXH_INLINE_ALL || XXH_PRIVATE_API */ + +/* **************************************************************** + * Stable API + *****************************************************************/ +#ifndef XXHASH_H_5627135585666179 +#define XXHASH_H_5627135585666179 1 + +/*! @brief Marks a global symbol. */ +#if !defined(XXH_INLINE_ALL) && !defined(XXH_PRIVATE_API) +# if defined(WIN32) && defined(_MSC_VER) && (defined(XXH_IMPORT) || defined(XXH_EXPORT)) +# ifdef XXH_EXPORT +# define XXH_PUBLIC_API __declspec(dllexport) +# elif XXH_IMPORT +# define XXH_PUBLIC_API __declspec(dllimport) +# endif +# else +# define XXH_PUBLIC_API /* do nothing */ +# endif +#endif + +#ifdef XXH_NAMESPACE +# define XXH_CAT(A,B) A##B +# define XXH_NAME2(A,B) XXH_CAT(A,B) +# define XXH_versionNumber XXH_NAME2(XXH_NAMESPACE, XXH_versionNumber) +/* XXH32 */ +# define XXH32 XXH_NAME2(XXH_NAMESPACE, XXH32) +# define XXH32_createState XXH_NAME2(XXH_NAMESPACE, XXH32_createState) +# define XXH32_freeState XXH_NAME2(XXH_NAMESPACE, XXH32_freeState) +# define XXH32_reset XXH_NAME2(XXH_NAMESPACE, XXH32_reset) +# define XXH32_update XXH_NAME2(XXH_NAMESPACE, XXH32_update) +# define XXH32_digest XXH_NAME2(XXH_NAMESPACE, XXH32_digest) +# define XXH32_copyState XXH_NAME2(XXH_NAMESPACE, XXH32_copyState) +# define XXH32_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH32_canonicalFromHash) +# define XXH32_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH32_hashFromCanonical) +/* XXH64 */ +# define XXH64 XXH_NAME2(XXH_NAMESPACE, XXH64) +# define XXH64_createState XXH_NAME2(XXH_NAMESPACE, XXH64_createState) +# define XXH64_freeState XXH_NAME2(XXH_NAMESPACE, XXH64_freeState) +# define XXH64_reset XXH_NAME2(XXH_NAMESPACE, XXH64_reset) +# define XXH64_update XXH_NAME2(XXH_NAMESPACE, XXH64_update) +# define XXH64_digest XXH_NAME2(XXH_NAMESPACE, XXH64_digest) +# define XXH64_copyState XXH_NAME2(XXH_NAMESPACE, XXH64_copyState) +# define XXH64_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH64_canonicalFromHash) +# define XXH64_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH64_hashFromCanonical) +/* XXH3_64bits */ +# define XXH3_64bits XXH_NAME2(XXH_NAMESPACE, XXH3_64bits) +# define XXH3_64bits_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_withSecret) +# define XXH3_64bits_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_withSeed) +# define XXH3_64bits_withSecretandSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_withSecretandSeed) +# define XXH3_createState XXH_NAME2(XXH_NAMESPACE, XXH3_createState) +# define XXH3_freeState XXH_NAME2(XXH_NAMESPACE, XXH3_freeState) +# define XXH3_copyState XXH_NAME2(XXH_NAMESPACE, XXH3_copyState) +# define XXH3_64bits_reset XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset) +# define XXH3_64bits_reset_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset_withSeed) +# define XXH3_64bits_reset_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset_withSecret) +# define XXH3_64bits_reset_withSecretandSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset_withSecretandSeed) +# define XXH3_64bits_update XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_update) +# define XXH3_64bits_digest XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_digest) +# define XXH3_generateSecret XXH_NAME2(XXH_NAMESPACE, XXH3_generateSecret) +# define XXH3_generateSecret_fromSeed XXH_NAME2(XXH_NAMESPACE, XXH3_generateSecret_fromSeed) +/* XXH3_128bits */ +# define XXH128 XXH_NAME2(XXH_NAMESPACE, XXH128) +# define XXH3_128bits XXH_NAME2(XXH_NAMESPACE, XXH3_128bits) +# define XXH3_128bits_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_withSeed) +# define XXH3_128bits_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_withSecret) +# define XXH3_128bits_withSecretandSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_withSecretandSeed) +# define XXH3_128bits_reset XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset) +# define XXH3_128bits_reset_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset_withSeed) +# define XXH3_128bits_reset_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset_withSecret) +# define XXH3_128bits_reset_withSecretandSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset_withSecretandSeed) +# define XXH3_128bits_update XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_update) +# define XXH3_128bits_digest XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_digest) +# define XXH128_isEqual XXH_NAME2(XXH_NAMESPACE, XXH128_isEqual) +# define XXH128_cmp XXH_NAME2(XXH_NAMESPACE, XXH128_cmp) +# define XXH128_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH128_canonicalFromHash) +# define XXH128_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH128_hashFromCanonical) +#endif + + +/* ************************************* +* Compiler specifics +***************************************/ + +/* specific declaration modes for Windows */ +#if !defined(XXH_INLINE_ALL) && !defined(XXH_PRIVATE_API) +# if defined(WIN32) && defined(_MSC_VER) && (defined(XXH_IMPORT) || defined(XXH_EXPORT)) +# ifdef XXH_EXPORT +# define XXH_PUBLIC_API __declspec(dllexport) +# elif XXH_IMPORT +# define XXH_PUBLIC_API __declspec(dllimport) +# endif +# else +# define XXH_PUBLIC_API /* do nothing */ +# endif +#endif + +#if defined (__GNUC__) +# define XXH_CONSTF __attribute__((const)) +# define XXH_PUREF __attribute__((pure)) +# define XXH_MALLOCF __attribute__((malloc)) +#else +# define XXH_CONSTF /* disable */ +# define XXH_PUREF +# define XXH_MALLOCF +#endif + +/* ************************************* +* Version +***************************************/ +#define XXH_VERSION_MAJOR 0 +#define XXH_VERSION_MINOR 8 +#define XXH_VERSION_RELEASE 2 +/*! @brief Version number, encoded as two digits each */ +#define XXH_VERSION_NUMBER (XXH_VERSION_MAJOR *100*100 + XXH_VERSION_MINOR *100 + XXH_VERSION_RELEASE) + +/*! + * @brief Obtains the xxHash version. + * + * This is mostly useful when xxHash is compiled as a shared library, + * since the returned value comes from the library, as opposed to header file. + * + * @return @ref XXH_VERSION_NUMBER of the invoked library. + */ +XXH_PUBLIC_API XXH_CONSTF unsigned XXH_versionNumber (void); + + +/* **************************** +* Common basic types +******************************/ +#include /* size_t */ +/*! + * @brief Exit code for the streaming API. + */ +typedef enum { + XXH_OK = 0, /*!< OK */ + XXH_ERROR /*!< Error */ +} XXH_errorcode; + + +/*-********************************************************************** +* 32-bit hash +************************************************************************/ +#if defined(XXH_DOXYGEN) /* Don't show include */ +/*! + * @brief An unsigned 32-bit integer. + * + * Not necessarily defined to `uint32_t` but functionally equivalent. + */ +typedef uint32_t XXH32_hash_t; + +#elif !defined (__VMS) \ + && (defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# ifdef _AIX +# include +# else +# include +# endif + typedef uint32_t XXH32_hash_t; + +#else +# include +# if UINT_MAX == 0xFFFFFFFFUL + typedef unsigned int XXH32_hash_t; +# elif ULONG_MAX == 0xFFFFFFFFUL + typedef unsigned long XXH32_hash_t; +# else +# error "unsupported platform: need a 32-bit type" +# endif +#endif + +/*! + * @} + * + * @defgroup XXH32_family XXH32 family + * @ingroup public + * Contains functions used in the classic 32-bit xxHash algorithm. + * + * @note + * XXH32 is useful for older platforms, with no or poor 64-bit performance. + * Note that the @ref XXH3_family provides competitive speed for both 32-bit + * and 64-bit systems, and offers true 64/128 bit hash results. + * + * @see @ref XXH64_family, @ref XXH3_family : Other xxHash families + * @see @ref XXH32_impl for implementation details + * @{ + */ + +/*! + * @brief Calculates the 32-bit hash of @p input using xxHash32. + * + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * @param seed The 32-bit seed to alter the hash's output predictably. + * + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return The calculated 32-bit xxHash32 value. + * + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH32_hash_t XXH32 (const void* input, size_t length, XXH32_hash_t seed); + +#ifndef XXH_NO_STREAM +/*! + * @typedef struct XXH32_state_s XXH32_state_t + * @brief The opaque state struct for the XXH32 streaming API. + * + * @see XXH32_state_s for details. + */ +typedef struct XXH32_state_s XXH32_state_t; + +/*! + * @brief Allocates an @ref XXH32_state_t. + * + * @return An allocated pointer of @ref XXH32_state_t on success. + * @return `NULL` on failure. + * + * @note Must be freed with XXH32_freeState(). + */ +XXH_PUBLIC_API XXH_MALLOCF XXH32_state_t* XXH32_createState(void); +/*! + * @brief Frees an @ref XXH32_state_t. + * + * @param statePtr A pointer to an @ref XXH32_state_t allocated with @ref XXH32_createState(). + * + * @return @ref XXH_OK. + * + * @note @p statePtr must be allocated with XXH32_createState(). + * + */ +XXH_PUBLIC_API XXH_errorcode XXH32_freeState(XXH32_state_t* statePtr); +/*! + * @brief Copies one @ref XXH32_state_t to another. + * + * @param dst_state The state to copy to. + * @param src_state The state to copy from. + * @pre + * @p dst_state and @p src_state must not be `NULL` and must not overlap. + */ +XXH_PUBLIC_API void XXH32_copyState(XXH32_state_t* dst_state, const XXH32_state_t* src_state); + +/*! + * @brief Resets an @ref XXH32_state_t to begin a new hash. + * + * @param statePtr The state struct to reset. + * @param seed The 32-bit seed to alter the hash result predictably. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note This function resets and seeds a state. Call it before @ref XXH32_update(). + */ +XXH_PUBLIC_API XXH_errorcode XXH32_reset (XXH32_state_t* statePtr, XXH32_hash_t seed); + +/*! + * @brief Consumes a block of @p input to an @ref XXH32_state_t. + * + * @param statePtr The state struct to update. + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * + * @pre + * @p statePtr must not be `NULL`. + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note Call this to incrementally consume blocks of data. + */ +XXH_PUBLIC_API XXH_errorcode XXH32_update (XXH32_state_t* statePtr, const void* input, size_t length); + +/*! + * @brief Returns the calculated hash value from an @ref XXH32_state_t. + * + * @param statePtr The state struct to calculate the hash from. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return The calculated 32-bit xxHash32 value from that state. + * + * @note + * Calling XXH32_digest() will not affect @p statePtr, so you can update, + * digest, and update again. + */ +XXH_PUBLIC_API XXH_PUREF XXH32_hash_t XXH32_digest (const XXH32_state_t* statePtr); +#endif /* !XXH_NO_STREAM */ + +/******* Canonical representation *******/ + +/*! + * @brief Canonical (big endian) representation of @ref XXH32_hash_t. + */ +typedef struct { + unsigned char digest[4]; /*!< Hash bytes, big endian */ +} XXH32_canonical_t; + +/*! + * @brief Converts an @ref XXH32_hash_t to a big endian @ref XXH32_canonical_t. + * + * @param dst The @ref XXH32_canonical_t pointer to be stored to. + * @param hash The @ref XXH32_hash_t to be converted. + * + * @pre + * @p dst must not be `NULL`. + * + * @see @ref canonical_representation_example "Canonical Representation Example" + */ +XXH_PUBLIC_API void XXH32_canonicalFromHash(XXH32_canonical_t* dst, XXH32_hash_t hash); + +/*! + * @brief Converts an @ref XXH32_canonical_t to a native @ref XXH32_hash_t. + * + * @param src The @ref XXH32_canonical_t to convert. + * + * @pre + * @p src must not be `NULL`. + * + * @return The converted hash. + * + * @see @ref canonical_representation_example "Canonical Representation Example" + */ +XXH_PUBLIC_API XXH_PUREF XXH32_hash_t XXH32_hashFromCanonical(const XXH32_canonical_t* src); + + +/*! @cond Doxygen ignores this part */ +#ifdef __has_attribute +# define XXH_HAS_ATTRIBUTE(x) __has_attribute(x) +#else +# define XXH_HAS_ATTRIBUTE(x) 0 +#endif +/*! @endcond */ + +/*! @cond Doxygen ignores this part */ +/* + * C23 __STDC_VERSION__ number hasn't been specified yet. For now + * leave as `201711L` (C17 + 1). + * TODO: Update to correct value when its been specified. + */ +#define XXH_C23_VN 201711L +/*! @endcond */ + +/*! @cond Doxygen ignores this part */ +/* C-language Attributes are added in C23. */ +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= XXH_C23_VN) && defined(__has_c_attribute) +# define XXH_HAS_C_ATTRIBUTE(x) __has_c_attribute(x) +#else +# define XXH_HAS_C_ATTRIBUTE(x) 0 +#endif +/*! @endcond */ + +/*! @cond Doxygen ignores this part */ +#if defined(__cplusplus) && defined(__has_cpp_attribute) +# define XXH_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute(x) +#else +# define XXH_HAS_CPP_ATTRIBUTE(x) 0 +#endif +/*! @endcond */ + +/*! @cond Doxygen ignores this part */ +/* + * Define XXH_FALLTHROUGH macro for annotating switch case with the 'fallthrough' attribute + * introduced in CPP17 and C23. + * CPP17 : https://en.cppreference.com/w/cpp/language/attributes/fallthrough + * C23 : https://en.cppreference.com/w/c/language/attributes/fallthrough + */ +#if XXH_HAS_C_ATTRIBUTE(fallthrough) || XXH_HAS_CPP_ATTRIBUTE(fallthrough) +# define XXH_FALLTHROUGH [[fallthrough]] +#elif XXH_HAS_ATTRIBUTE(__fallthrough__) +# define XXH_FALLTHROUGH __attribute__ ((__fallthrough__)) +#else +# define XXH_FALLTHROUGH /* fallthrough */ +#endif +/*! @endcond */ + +/*! @cond Doxygen ignores this part */ +/* + * Define XXH_NOESCAPE for annotated pointers in public API. + * https://clang.llvm.org/docs/AttributeReference.html#noescape + * As of writing this, only supported by clang. + */ +#if XXH_HAS_ATTRIBUTE(noescape) +# define XXH_NOESCAPE __attribute__((noescape)) +#else +# define XXH_NOESCAPE +#endif +/*! @endcond */ + + +/*! + * @} + * @ingroup public + * @{ + */ + +#ifndef XXH_NO_LONG_LONG +/*-********************************************************************** +* 64-bit hash +************************************************************************/ +#if defined(XXH_DOXYGEN) /* don't include */ +/*! + * @brief An unsigned 64-bit integer. + * + * Not necessarily defined to `uint64_t` but functionally equivalent. + */ +typedef uint64_t XXH64_hash_t; +#elif !defined (__VMS) \ + && (defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# ifdef _AIX +# include +# else +# include +# endif + typedef uint64_t XXH64_hash_t; +#else +# include +# if defined(__LP64__) && ULONG_MAX == 0xFFFFFFFFFFFFFFFFULL + /* LP64 ABI says uint64_t is unsigned long */ + typedef unsigned long XXH64_hash_t; +# else + /* the following type must have a width of 64-bit */ + typedef unsigned long long XXH64_hash_t; +# endif +#endif + +/*! + * @} + * + * @defgroup XXH64_family XXH64 family + * @ingroup public + * @{ + * Contains functions used in the classic 64-bit xxHash algorithm. + * + * @note + * XXH3 provides competitive speed for both 32-bit and 64-bit systems, + * and offers true 64/128 bit hash results. + * It provides better speed for systems with vector processing capabilities. + */ + +/*! + * @brief Calculates the 64-bit hash of @p input using xxHash64. + * + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * @param seed The 64-bit seed to alter the hash's output predictably. + * + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return The calculated 64-bit xxHash64 value. + * + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH64(XXH_NOESCAPE const void* input, size_t length, XXH64_hash_t seed); + +/******* Streaming *******/ +#ifndef XXH_NO_STREAM +/*! + * @brief The opaque state struct for the XXH64 streaming API. + * + * @see XXH64_state_s for details. + */ +typedef struct XXH64_state_s XXH64_state_t; /* incomplete type */ + +/*! + * @brief Allocates an @ref XXH64_state_t. + * + * @return An allocated pointer of @ref XXH64_state_t on success. + * @return `NULL` on failure. + * + * @note Must be freed with XXH64_freeState(). + */ +XXH_PUBLIC_API XXH_MALLOCF XXH64_state_t* XXH64_createState(void); + +/*! + * @brief Frees an @ref XXH64_state_t. + * + * @param statePtr A pointer to an @ref XXH64_state_t allocated with @ref XXH64_createState(). + * + * @return @ref XXH_OK. + * + * @note @p statePtr must be allocated with XXH64_createState(). + */ +XXH_PUBLIC_API XXH_errorcode XXH64_freeState(XXH64_state_t* statePtr); + +/*! + * @brief Copies one @ref XXH64_state_t to another. + * + * @param dst_state The state to copy to. + * @param src_state The state to copy from. + * @pre + * @p dst_state and @p src_state must not be `NULL` and must not overlap. + */ +XXH_PUBLIC_API void XXH64_copyState(XXH_NOESCAPE XXH64_state_t* dst_state, const XXH64_state_t* src_state); + +/*! + * @brief Resets an @ref XXH64_state_t to begin a new hash. + * + * @param statePtr The state struct to reset. + * @param seed The 64-bit seed to alter the hash result predictably. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note This function resets and seeds a state. Call it before @ref XXH64_update(). + */ +XXH_PUBLIC_API XXH_errorcode XXH64_reset (XXH_NOESCAPE XXH64_state_t* statePtr, XXH64_hash_t seed); + +/*! + * @brief Consumes a block of @p input to an @ref XXH64_state_t. + * + * @param statePtr The state struct to update. + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * + * @pre + * @p statePtr must not be `NULL`. + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note Call this to incrementally consume blocks of data. + */ +XXH_PUBLIC_API XXH_errorcode XXH64_update (XXH_NOESCAPE XXH64_state_t* statePtr, XXH_NOESCAPE const void* input, size_t length); + +/*! + * @brief Returns the calculated hash value from an @ref XXH64_state_t. + * + * @param statePtr The state struct to calculate the hash from. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return The calculated 64-bit xxHash64 value from that state. + * + * @note + * Calling XXH64_digest() will not affect @p statePtr, so you can update, + * digest, and update again. + */ +XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH64_digest (XXH_NOESCAPE const XXH64_state_t* statePtr); +#endif /* !XXH_NO_STREAM */ +/******* Canonical representation *******/ + +/*! + * @brief Canonical (big endian) representation of @ref XXH64_hash_t. + */ +typedef struct { unsigned char digest[sizeof(XXH64_hash_t)]; } XXH64_canonical_t; + +/*! + * @brief Converts an @ref XXH64_hash_t to a big endian @ref XXH64_canonical_t. + * + * @param dst The @ref XXH64_canonical_t pointer to be stored to. + * @param hash The @ref XXH64_hash_t to be converted. + * + * @pre + * @p dst must not be `NULL`. + * + * @see @ref canonical_representation_example "Canonical Representation Example" + */ +XXH_PUBLIC_API void XXH64_canonicalFromHash(XXH_NOESCAPE XXH64_canonical_t* dst, XXH64_hash_t hash); + +/*! + * @brief Converts an @ref XXH64_canonical_t to a native @ref XXH64_hash_t. + * + * @param src The @ref XXH64_canonical_t to convert. + * + * @pre + * @p src must not be `NULL`. + * + * @return The converted hash. + * + * @see @ref canonical_representation_example "Canonical Representation Example" + */ +XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH64_hashFromCanonical(XXH_NOESCAPE const XXH64_canonical_t* src); + +#ifndef XXH_NO_XXH3 + +/*! + * @} + * ************************************************************************ + * @defgroup XXH3_family XXH3 family + * @ingroup public + * @{ + * + * XXH3 is a more recent hash algorithm featuring: + * - Improved speed for both small and large inputs + * - True 64-bit and 128-bit outputs + * - SIMD acceleration + * - Improved 32-bit viability + * + * Speed analysis methodology is explained here: + * + * https://fastcompression.blogspot.com/2019/03/presenting-xxh3.html + * + * Compared to XXH64, expect XXH3 to run approximately + * ~2x faster on large inputs and >3x faster on small ones, + * exact differences vary depending on platform. + * + * XXH3's speed benefits greatly from SIMD and 64-bit arithmetic, + * but does not require it. + * Most 32-bit and 64-bit targets that can run XXH32 smoothly can run XXH3 + * at competitive speeds, even without vector support. Further details are + * explained in the implementation. + * + * XXH3 has a fast scalar implementation, but it also includes accelerated SIMD + * implementations for many common platforms: + * - AVX512 + * - AVX2 + * - SSE2 + * - ARM NEON + * - WebAssembly SIMD128 + * - POWER8 VSX + * - s390x ZVector + * This can be controlled via the @ref XXH_VECTOR macro, but it automatically + * selects the best version according to predefined macros. For the x86 family, an + * automatic runtime dispatcher is included separately in @ref xxh_x86dispatch.c. + * + * XXH3 implementation is portable: + * it has a generic C90 formulation that can be compiled on any platform, + * all implementations generate exactly the same hash value on all platforms. + * Starting from v0.8.0, it's also labelled "stable", meaning that + * any future version will also generate the same hash value. + * + * XXH3 offers 2 variants, _64bits and _128bits. + * + * When only 64 bits are needed, prefer invoking the _64bits variant, as it + * reduces the amount of mixing, resulting in faster speed on small inputs. + * It's also generally simpler to manipulate a scalar return type than a struct. + * + * The API supports one-shot hashing, streaming mode, and custom secrets. + */ +/*-********************************************************************** +* XXH3 64-bit variant +************************************************************************/ + +/*! + * @brief Calculates 64-bit unseeded variant of XXH3 hash of @p input. + * + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return The calculated 64-bit XXH3 hash value. + * + * @note + * This is equivalent to @ref XXH3_64bits_withSeed() with a seed of `0`, however + * it may have slightly better performance due to constant propagation of the + * defaults. + * + * @see + * XXH3_64bits_withSeed(), XXH3_64bits_withSecret(): other seeding variants + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH3_64bits(XXH_NOESCAPE const void* input, size_t length); + +/*! + * @brief Calculates 64-bit seeded variant of XXH3 hash of @p input. + * + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * @param seed The 64-bit seed to alter the hash result predictably. + * + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return The calculated 64-bit XXH3 hash value. + * + * @note + * seed == 0 produces the same results as @ref XXH3_64bits(). + * + * This variant generates a custom secret on the fly based on default secret + * altered using the @p seed value. + * + * While this operation is decently fast, note that it's not completely free. + * + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH3_64bits_withSeed(XXH_NOESCAPE const void* input, size_t length, XXH64_hash_t seed); + +/*! + * The bare minimum size for a custom secret. + * + * @see + * XXH3_64bits_withSecret(), XXH3_64bits_reset_withSecret(), + * XXH3_128bits_withSecret(), XXH3_128bits_reset_withSecret(). + */ +#define XXH3_SECRET_SIZE_MIN 136 + +/*! + * @brief Calculates 64-bit variant of XXH3 with a custom "secret". + * + * @param data The block of data to be hashed, at least @p len bytes in size. + * @param len The length of @p data, in bytes. + * @param secret The secret data. + * @param secretSize The length of @p secret, in bytes. + * + * @return The calculated 64-bit XXH3 hash value. + * + * @pre + * The memory between @p data and @p data + @p len must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p data may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * It's possible to provide any blob of bytes as a "secret" to generate the hash. + * This makes it more difficult for an external actor to prepare an intentional collision. + * The main condition is that @p secretSize *must* be large enough (>= @ref XXH3_SECRET_SIZE_MIN). + * However, the quality of the secret impacts the dispersion of the hash algorithm. + * Therefore, the secret _must_ look like a bunch of random bytes. + * Avoid "trivial" or structured data such as repeated sequences or a text document. + * Whenever in doubt about the "randomness" of the blob of bytes, + * consider employing @ref XXH3_generateSecret() instead (see below). + * It will generate a proper high entropy secret derived from the blob of bytes. + * Another advantage of using XXH3_generateSecret() is that + * it guarantees that all bits within the initial blob of bytes + * will impact every bit of the output. + * This is not necessarily the case when using the blob of bytes directly + * because, when hashing _small_ inputs, only a portion of the secret is employed. + * + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH3_64bits_withSecret(XXH_NOESCAPE const void* data, size_t len, XXH_NOESCAPE const void* secret, size_t secretSize); + + +/******* Streaming *******/ +#ifndef XXH_NO_STREAM +/* + * Streaming requires state maintenance. + * This operation costs memory and CPU. + * As a consequence, streaming is slower than one-shot hashing. + * For better performance, prefer one-shot functions whenever applicable. + */ + +/*! + * @brief The opaque state struct for the XXH3 streaming API. + * + * @see XXH3_state_s for details. + */ +typedef struct XXH3_state_s XXH3_state_t; +XXH_PUBLIC_API XXH_MALLOCF XXH3_state_t* XXH3_createState(void); +XXH_PUBLIC_API XXH_errorcode XXH3_freeState(XXH3_state_t* statePtr); + +/*! + * @brief Copies one @ref XXH3_state_t to another. + * + * @param dst_state The state to copy to. + * @param src_state The state to copy from. + * @pre + * @p dst_state and @p src_state must not be `NULL` and must not overlap. + */ +XXH_PUBLIC_API void XXH3_copyState(XXH_NOESCAPE XXH3_state_t* dst_state, XXH_NOESCAPE const XXH3_state_t* src_state); + +/*! + * @brief Resets an @ref XXH3_state_t to begin a new hash. + * + * @param statePtr The state struct to reset. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note + * - This function resets `statePtr` and generate a secret with default parameters. + * - Call this function before @ref XXH3_64bits_update(). + * - Digest will be equivalent to `XXH3_64bits()`. + * + */ +XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset(XXH_NOESCAPE XXH3_state_t* statePtr); + +/*! + * @brief Resets an @ref XXH3_state_t with 64-bit seed to begin a new hash. + * + * @param statePtr The state struct to reset. + * @param seed The 64-bit seed to alter the hash result predictably. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note + * - This function resets `statePtr` and generate a secret from `seed`. + * - Call this function before @ref XXH3_64bits_update(). + * - Digest will be equivalent to `XXH3_64bits_withSeed()`. + * + */ +XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset_withSeed(XXH_NOESCAPE XXH3_state_t* statePtr, XXH64_hash_t seed); + +/*! + * @brief Resets an @ref XXH3_state_t with secret data to begin a new hash. + * + * @param statePtr The state struct to reset. + * @param secret The secret data. + * @param secretSize The length of @p secret, in bytes. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note + * `secret` is referenced, it _must outlive_ the hash streaming session. + * + * Similar to one-shot API, `secretSize` must be >= @ref XXH3_SECRET_SIZE_MIN, + * and the quality of produced hash values depends on secret's entropy + * (secret's content should look like a bunch of random bytes). + * When in doubt about the randomness of a candidate `secret`, + * consider employing `XXH3_generateSecret()` instead (see below). + */ +XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset_withSecret(XXH_NOESCAPE XXH3_state_t* statePtr, XXH_NOESCAPE const void* secret, size_t secretSize); + +/*! + * @brief Consumes a block of @p input to an @ref XXH3_state_t. + * + * @param statePtr The state struct to update. + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * + * @pre + * @p statePtr must not be `NULL`. + * @pre + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note Call this to incrementally consume blocks of data. + */ +XXH_PUBLIC_API XXH_errorcode XXH3_64bits_update (XXH_NOESCAPE XXH3_state_t* statePtr, XXH_NOESCAPE const void* input, size_t length); + +/*! + * @brief Returns the calculated XXH3 64-bit hash value from an @ref XXH3_state_t. + * + * @param statePtr The state struct to calculate the hash from. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return The calculated XXH3 64-bit hash value from that state. + * + * @note + * Calling XXH3_64bits_digest() will not affect @p statePtr, so you can update, + * digest, and update again. + */ +XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH3_64bits_digest (XXH_NOESCAPE const XXH3_state_t* statePtr); +#endif /* !XXH_NO_STREAM */ + +/* note : canonical representation of XXH3 is the same as XXH64 + * since they both produce XXH64_hash_t values */ + + +/*-********************************************************************** +* XXH3 128-bit variant +************************************************************************/ + +/*! + * @brief The return value from 128-bit hashes. + * + * Stored in little endian order, although the fields themselves are in native + * endianness. + */ +typedef struct { + XXH64_hash_t low64; /*!< `value & 0xFFFFFFFFFFFFFFFF` */ + XXH64_hash_t high64; /*!< `value >> 64` */ +} XXH128_hash_t; + +/*! + * @brief Calculates 128-bit unseeded variant of XXH3 of @p data. + * + * @param data The block of data to be hashed, at least @p length bytes in size. + * @param len The length of @p data, in bytes. + * + * @return The calculated 128-bit variant of XXH3 value. + * + * The 128-bit variant of XXH3 has more strength, but it has a bit of overhead + * for shorter inputs. + * + * This is equivalent to @ref XXH3_128bits_withSeed() with a seed of `0`, however + * it may have slightly better performance due to constant propagation of the + * defaults. + * + * @see XXH3_128bits_withSeed(), XXH3_128bits_withSecret(): other seeding variants + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH3_128bits(XXH_NOESCAPE const void* data, size_t len); +/*! @brief Calculates 128-bit seeded variant of XXH3 hash of @p data. + * + * @param data The block of data to be hashed, at least @p length bytes in size. + * @param len The length of @p data, in bytes. + * @param seed The 64-bit seed to alter the hash result predictably. + * + * @return The calculated 128-bit variant of XXH3 value. + * + * @note + * seed == 0 produces the same results as @ref XXH3_64bits(). + * + * This variant generates a custom secret on the fly based on default secret + * altered using the @p seed value. + * + * While this operation is decently fast, note that it's not completely free. + * + * @see XXH3_128bits(), XXH3_128bits_withSecret(): other seeding variants + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH3_128bits_withSeed(XXH_NOESCAPE const void* data, size_t len, XXH64_hash_t seed); +/*! + * @brief Calculates 128-bit variant of XXH3 with a custom "secret". + * + * @param data The block of data to be hashed, at least @p len bytes in size. + * @param len The length of @p data, in bytes. + * @param secret The secret data. + * @param secretSize The length of @p secret, in bytes. + * + * @return The calculated 128-bit variant of XXH3 value. + * + * It's possible to provide any blob of bytes as a "secret" to generate the hash. + * This makes it more difficult for an external actor to prepare an intentional collision. + * The main condition is that @p secretSize *must* be large enough (>= @ref XXH3_SECRET_SIZE_MIN). + * However, the quality of the secret impacts the dispersion of the hash algorithm. + * Therefore, the secret _must_ look like a bunch of random bytes. + * Avoid "trivial" or structured data such as repeated sequences or a text document. + * Whenever in doubt about the "randomness" of the blob of bytes, + * consider employing @ref XXH3_generateSecret() instead (see below). + * It will generate a proper high entropy secret derived from the blob of bytes. + * Another advantage of using XXH3_generateSecret() is that + * it guarantees that all bits within the initial blob of bytes + * will impact every bit of the output. + * This is not necessarily the case when using the blob of bytes directly + * because, when hashing _small_ inputs, only a portion of the secret is employed. + * + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH3_128bits_withSecret(XXH_NOESCAPE const void* data, size_t len, XXH_NOESCAPE const void* secret, size_t secretSize); + +/******* Streaming *******/ +#ifndef XXH_NO_STREAM +/* + * Streaming requires state maintenance. + * This operation costs memory and CPU. + * As a consequence, streaming is slower than one-shot hashing. + * For better performance, prefer one-shot functions whenever applicable. + * + * XXH3_128bits uses the same XXH3_state_t as XXH3_64bits(). + * Use already declared XXH3_createState() and XXH3_freeState(). + * + * All reset and streaming functions have same meaning as their 64-bit counterpart. + */ + +/*! + * @brief Resets an @ref XXH3_state_t to begin a new hash. + * + * @param statePtr The state struct to reset. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note + * - This function resets `statePtr` and generate a secret with default parameters. + * - Call it before @ref XXH3_128bits_update(). + * - Digest will be equivalent to `XXH3_128bits()`. + */ +XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset(XXH_NOESCAPE XXH3_state_t* statePtr); + +/*! + * @brief Resets an @ref XXH3_state_t with 64-bit seed to begin a new hash. + * + * @param statePtr The state struct to reset. + * @param seed The 64-bit seed to alter the hash result predictably. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note + * - This function resets `statePtr` and generate a secret from `seed`. + * - Call it before @ref XXH3_128bits_update(). + * - Digest will be equivalent to `XXH3_128bits_withSeed()`. + */ +XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset_withSeed(XXH_NOESCAPE XXH3_state_t* statePtr, XXH64_hash_t seed); +/*! + * @brief Resets an @ref XXH3_state_t with secret data to begin a new hash. + * + * @param statePtr The state struct to reset. + * @param secret The secret data. + * @param secretSize The length of @p secret, in bytes. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * `secret` is referenced, it _must outlive_ the hash streaming session. + * Similar to one-shot API, `secretSize` must be >= @ref XXH3_SECRET_SIZE_MIN, + * and the quality of produced hash values depends on secret's entropy + * (secret's content should look like a bunch of random bytes). + * When in doubt about the randomness of a candidate `secret`, + * consider employing `XXH3_generateSecret()` instead (see below). + */ +XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset_withSecret(XXH_NOESCAPE XXH3_state_t* statePtr, XXH_NOESCAPE const void* secret, size_t secretSize); + +/*! + * @brief Consumes a block of @p input to an @ref XXH3_state_t. + * + * Call this to incrementally consume blocks of data. + * + * @param statePtr The state struct to update. + * @param input The block of data to be hashed, at least @p length bytes in size. + * @param length The length of @p input, in bytes. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @note + * The memory between @p input and @p input + @p length must be valid, + * readable, contiguous memory. However, if @p length is `0`, @p input may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + */ +XXH_PUBLIC_API XXH_errorcode XXH3_128bits_update (XXH_NOESCAPE XXH3_state_t* statePtr, XXH_NOESCAPE const void* input, size_t length); + +/*! + * @brief Returns the calculated XXH3 128-bit hash value from an @ref XXH3_state_t. + * + * @param statePtr The state struct to calculate the hash from. + * + * @pre + * @p statePtr must not be `NULL`. + * + * @return The calculated XXH3 128-bit hash value from that state. + * + * @note + * Calling XXH3_128bits_digest() will not affect @p statePtr, so you can update, + * digest, and update again. + * + */ +XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH3_128bits_digest (XXH_NOESCAPE const XXH3_state_t* statePtr); +#endif /* !XXH_NO_STREAM */ + +/* Following helper functions make it possible to compare XXH128_hast_t values. + * Since XXH128_hash_t is a structure, this capability is not offered by the language. + * Note: For better performance, these functions can be inlined using XXH_INLINE_ALL */ + +/*! + * @brief Check equality of two XXH128_hash_t values + * + * @param h1 The 128-bit hash value. + * @param h2 Another 128-bit hash value. + * + * @return `1` if `h1` and `h2` are equal. + * @return `0` if they are not. + */ +XXH_PUBLIC_API XXH_PUREF int XXH128_isEqual(XXH128_hash_t h1, XXH128_hash_t h2); + +/*! + * @brief Compares two @ref XXH128_hash_t + * + * This comparator is compatible with stdlib's `qsort()`/`bsearch()`. + * + * @param h128_1 Left-hand side value + * @param h128_2 Right-hand side value + * + * @return >0 if @p h128_1 > @p h128_2 + * @return =0 if @p h128_1 == @p h128_2 + * @return <0 if @p h128_1 < @p h128_2 + */ +XXH_PUBLIC_API XXH_PUREF int XXH128_cmp(XXH_NOESCAPE const void* h128_1, XXH_NOESCAPE const void* h128_2); + + +/******* Canonical representation *******/ +typedef struct { unsigned char digest[sizeof(XXH128_hash_t)]; } XXH128_canonical_t; + + +/*! + * @brief Converts an @ref XXH128_hash_t to a big endian @ref XXH128_canonical_t. + * + * @param dst The @ref XXH128_canonical_t pointer to be stored to. + * @param hash The @ref XXH128_hash_t to be converted. + * + * @pre + * @p dst must not be `NULL`. + * @see @ref canonical_representation_example "Canonical Representation Example" + */ +XXH_PUBLIC_API void XXH128_canonicalFromHash(XXH_NOESCAPE XXH128_canonical_t* dst, XXH128_hash_t hash); + +/*! + * @brief Converts an @ref XXH128_canonical_t to a native @ref XXH128_hash_t. + * + * @param src The @ref XXH128_canonical_t to convert. + * + * @pre + * @p src must not be `NULL`. + * + * @return The converted hash. + * @see @ref canonical_representation_example "Canonical Representation Example" + */ +XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH128_hashFromCanonical(XXH_NOESCAPE const XXH128_canonical_t* src); + + +#endif /* !XXH_NO_XXH3 */ +#endif /* XXH_NO_LONG_LONG */ + +/*! + * @} + */ +#endif /* XXHASH_H_5627135585666179 */ + + + +#if defined(XXH_STATIC_LINKING_ONLY) && !defined(XXHASH_H_STATIC_13879238742) +#define XXHASH_H_STATIC_13879238742 +/* **************************************************************************** + * This section contains declarations which are not guaranteed to remain stable. + * They may change in future versions, becoming incompatible with a different + * version of the library. + * These declarations should only be used with static linking. + * Never use them in association with dynamic linking! + ***************************************************************************** */ + +/* + * These definitions are only present to allow static allocation + * of XXH states, on stack or in a struct, for example. + * Never **ever** access their members directly. + */ + +/*! + * @internal + * @brief Structure for XXH32 streaming API. + * + * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY, + * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined. Otherwise it is + * an opaque type. This allows fields to safely be changed. + * + * Typedef'd to @ref XXH32_state_t. + * Do not access the members of this struct directly. + * @see XXH64_state_s, XXH3_state_s + */ +struct XXH32_state_s { + XXH32_hash_t total_len_32; /*!< Total length hashed, modulo 2^32 */ + XXH32_hash_t large_len; /*!< Whether the hash is >= 16 (handles @ref total_len_32 overflow) */ + XXH32_hash_t v[4]; /*!< Accumulator lanes */ + XXH32_hash_t mem32[4]; /*!< Internal buffer for partial reads. Treated as unsigned char[16]. */ + XXH32_hash_t memsize; /*!< Amount of data in @ref mem32 */ + XXH32_hash_t reserved; /*!< Reserved field. Do not read nor write to it. */ +}; /* typedef'd to XXH32_state_t */ + + +#ifndef XXH_NO_LONG_LONG /* defined when there is no 64-bit support */ + +/*! + * @internal + * @brief Structure for XXH64 streaming API. + * + * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY, + * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined. Otherwise it is + * an opaque type. This allows fields to safely be changed. + * + * Typedef'd to @ref XXH64_state_t. + * Do not access the members of this struct directly. + * @see XXH32_state_s, XXH3_state_s + */ +struct XXH64_state_s { + XXH64_hash_t total_len; /*!< Total length hashed. This is always 64-bit. */ + XXH64_hash_t v[4]; /*!< Accumulator lanes */ + XXH64_hash_t mem64[4]; /*!< Internal buffer for partial reads. Treated as unsigned char[32]. */ + XXH32_hash_t memsize; /*!< Amount of data in @ref mem64 */ + XXH32_hash_t reserved32; /*!< Reserved field, needed for padding anyways*/ + XXH64_hash_t reserved64; /*!< Reserved field. Do not read or write to it. */ +}; /* typedef'd to XXH64_state_t */ + +#ifndef XXH_NO_XXH3 + +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) /* >= C11 */ +# include +# define XXH_ALIGN(n) alignas(n) +#elif defined(__cplusplus) && (__cplusplus >= 201103L) /* >= C++11 */ +/* In C++ alignas() is a keyword */ +# define XXH_ALIGN(n) alignas(n) +#elif defined(__GNUC__) +# define XXH_ALIGN(n) __attribute__ ((aligned(n))) +#elif defined(_MSC_VER) +# define XXH_ALIGN(n) __declspec(align(n)) +#else +# define XXH_ALIGN(n) /* disabled */ +#endif + +/* Old GCC versions only accept the attribute after the type in structures. */ +#if !(defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) /* C11+ */ \ + && ! (defined(__cplusplus) && (__cplusplus >= 201103L)) /* >= C++11 */ \ + && defined(__GNUC__) +# define XXH_ALIGN_MEMBER(align, type) type XXH_ALIGN(align) +#else +# define XXH_ALIGN_MEMBER(align, type) XXH_ALIGN(align) type +#endif + +/*! + * @brief The size of the internal XXH3 buffer. + * + * This is the optimal update size for incremental hashing. + * + * @see XXH3_64b_update(), XXH3_128b_update(). + */ +#define XXH3_INTERNALBUFFER_SIZE 256 + +/*! + * @internal + * @brief Default size of the secret buffer (and @ref XXH3_kSecret). + * + * This is the size used in @ref XXH3_kSecret and the seeded functions. + * + * Not to be confused with @ref XXH3_SECRET_SIZE_MIN. + */ +#define XXH3_SECRET_DEFAULT_SIZE 192 + +/*! + * @internal + * @brief Structure for XXH3 streaming API. + * + * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY, + * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined. + * Otherwise it is an opaque type. + * Never use this definition in combination with dynamic library. + * This allows fields to safely be changed in the future. + * + * @note ** This structure has a strict alignment requirement of 64 bytes!! ** + * Do not allocate this with `malloc()` or `new`, + * it will not be sufficiently aligned. + * Use @ref XXH3_createState() and @ref XXH3_freeState(), or stack allocation. + * + * Typedef'd to @ref XXH3_state_t. + * Do never access the members of this struct directly. + * + * @see XXH3_INITSTATE() for stack initialization. + * @see XXH3_createState(), XXH3_freeState(). + * @see XXH32_state_s, XXH64_state_s + */ +struct XXH3_state_s { + XXH_ALIGN_MEMBER(64, XXH64_hash_t acc[8]); + /*!< The 8 accumulators. See @ref XXH32_state_s::v and @ref XXH64_state_s::v */ + XXH_ALIGN_MEMBER(64, unsigned char customSecret[XXH3_SECRET_DEFAULT_SIZE]); + /*!< Used to store a custom secret generated from a seed. */ + XXH_ALIGN_MEMBER(64, unsigned char buffer[XXH3_INTERNALBUFFER_SIZE]); + /*!< The internal buffer. @see XXH32_state_s::mem32 */ + XXH32_hash_t bufferedSize; + /*!< The amount of memory in @ref buffer, @see XXH32_state_s::memsize */ + XXH32_hash_t useSeed; + /*!< Reserved field. Needed for padding on 64-bit. */ + size_t nbStripesSoFar; + /*!< Number or stripes processed. */ + XXH64_hash_t totalLen; + /*!< Total length hashed. 64-bit even on 32-bit targets. */ + size_t nbStripesPerBlock; + /*!< Number of stripes per block. */ + size_t secretLimit; + /*!< Size of @ref customSecret or @ref extSecret */ + XXH64_hash_t seed; + /*!< Seed for _withSeed variants. Must be zero otherwise, @see XXH3_INITSTATE() */ + XXH64_hash_t reserved64; + /*!< Reserved field. */ + const unsigned char* extSecret; + /*!< Reference to an external secret for the _withSecret variants, NULL + * for other variants. */ + /* note: there may be some padding at the end due to alignment on 64 bytes */ +}; /* typedef'd to XXH3_state_t */ + +#undef XXH_ALIGN_MEMBER + +/*! + * @brief Initializes a stack-allocated `XXH3_state_s`. + * + * When the @ref XXH3_state_t structure is merely emplaced on stack, + * it should be initialized with XXH3_INITSTATE() or a memset() + * in case its first reset uses XXH3_NNbits_reset_withSeed(). + * This init can be omitted if the first reset uses default or _withSecret mode. + * This operation isn't necessary when the state is created with XXH3_createState(). + * Note that this doesn't prepare the state for a streaming operation, + * it's still necessary to use XXH3_NNbits_reset*() afterwards. + */ +#define XXH3_INITSTATE(XXH3_state_ptr) \ + do { \ + XXH3_state_t* tmp_xxh3_state_ptr = (XXH3_state_ptr); \ + tmp_xxh3_state_ptr->seed = 0; \ + tmp_xxh3_state_ptr->extSecret = NULL; \ + } while(0) + + +/*! + * @brief Calculates the 128-bit hash of @p data using XXH3. + * + * @param data The block of data to be hashed, at least @p len bytes in size. + * @param len The length of @p data, in bytes. + * @param seed The 64-bit seed to alter the hash's output predictably. + * + * @pre + * The memory between @p data and @p data + @p len must be valid, + * readable, contiguous memory. However, if @p len is `0`, @p data may be + * `NULL`. In C++, this also must be *TriviallyCopyable*. + * + * @return The calculated 128-bit XXH3 value. + * + * @see @ref single_shot_example "Single Shot Example" for an example. + */ +XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH128(XXH_NOESCAPE const void* data, size_t len, XXH64_hash_t seed); + + +/* === Experimental API === */ +/* Symbols defined below must be considered tied to a specific library version. */ + +/*! + * @brief Derive a high-entropy secret from any user-defined content, named customSeed. + * + * @param secretBuffer A writable buffer for derived high-entropy secret data. + * @param secretSize Size of secretBuffer, in bytes. Must be >= XXH3_SECRET_DEFAULT_SIZE. + * @param customSeed A user-defined content. + * @param customSeedSize Size of customSeed, in bytes. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * The generated secret can be used in combination with `*_withSecret()` functions. + * The `_withSecret()` variants are useful to provide a higher level of protection + * than 64-bit seed, as it becomes much more difficult for an external actor to + * guess how to impact the calculation logic. + * + * The function accepts as input a custom seed of any length and any content, + * and derives from it a high-entropy secret of length @p secretSize into an + * already allocated buffer @p secretBuffer. + * + * The generated secret can then be used with any `*_withSecret()` variant. + * The functions @ref XXH3_128bits_withSecret(), @ref XXH3_64bits_withSecret(), + * @ref XXH3_128bits_reset_withSecret() and @ref XXH3_64bits_reset_withSecret() + * are part of this list. They all accept a `secret` parameter + * which must be large enough for implementation reasons (>= @ref XXH3_SECRET_SIZE_MIN) + * _and_ feature very high entropy (consist of random-looking bytes). + * These conditions can be a high bar to meet, so @ref XXH3_generateSecret() can + * be employed to ensure proper quality. + * + * @p customSeed can be anything. It can have any size, even small ones, + * and its content can be anything, even "poor entropy" sources such as a bunch + * of zeroes. The resulting `secret` will nonetheless provide all required qualities. + * + * @pre + * - @p secretSize must be >= @ref XXH3_SECRET_SIZE_MIN + * - When @p customSeedSize > 0, supplying NULL as customSeed is undefined behavior. + * + * Example code: + * @code{.c} + * #include + * #include + * #include + * #define XXH_STATIC_LINKING_ONLY // expose unstable API + * #include "xxhash.h" + * // Hashes argv[2] using the entropy from argv[1]. + * int main(int argc, char* argv[]) + * { + * char secret[XXH3_SECRET_SIZE_MIN]; + * if (argv != 3) { return 1; } + * XXH3_generateSecret(secret, sizeof(secret), argv[1], strlen(argv[1])); + * XXH64_hash_t h = XXH3_64bits_withSecret( + * argv[2], strlen(argv[2]), + * secret, sizeof(secret) + * ); + * printf("%016llx\n", (unsigned long long) h); + * } + * @endcode + */ +XXH_PUBLIC_API XXH_errorcode XXH3_generateSecret(XXH_NOESCAPE void* secretBuffer, size_t secretSize, XXH_NOESCAPE const void* customSeed, size_t customSeedSize); + +/*! + * @brief Generate the same secret as the _withSeed() variants. + * + * @param secretBuffer A writable buffer of @ref XXH3_SECRET_SIZE_MIN bytes + * @param seed The 64-bit seed to alter the hash result predictably. + * + * The generated secret can be used in combination with + *`*_withSecret()` and `_withSecretandSeed()` variants. + * + * Example C++ `std::string` hash class: + * @code{.cpp} + * #include + * #define XXH_STATIC_LINKING_ONLY // expose unstable API + * #include "xxhash.h" + * // Slow, seeds each time + * class HashSlow { + * XXH64_hash_t seed; + * public: + * HashSlow(XXH64_hash_t s) : seed{s} {} + * size_t operator()(const std::string& x) const { + * return size_t{XXH3_64bits_withSeed(x.c_str(), x.length(), seed)}; + * } + * }; + * // Fast, caches the seeded secret for future uses. + * class HashFast { + * unsigned char secret[XXH3_SECRET_SIZE_MIN]; + * public: + * HashFast(XXH64_hash_t s) { + * XXH3_generateSecret_fromSeed(secret, seed); + * } + * size_t operator()(const std::string& x) const { + * return size_t{ + * XXH3_64bits_withSecret(x.c_str(), x.length(), secret, sizeof(secret)) + * }; + * } + * }; + * @endcode + */ +XXH_PUBLIC_API void XXH3_generateSecret_fromSeed(XXH_NOESCAPE void* secretBuffer, XXH64_hash_t seed); + +/*! + * @brief Calculates 64/128-bit seeded variant of XXH3 hash of @p data. + * + * @param data The block of data to be hashed, at least @p len bytes in size. + * @param len The length of @p data, in bytes. + * @param secret The secret data. + * @param secretSize The length of @p secret, in bytes. + * @param seed The 64-bit seed to alter the hash result predictably. + * + * These variants generate hash values using either + * @p seed for "short" keys (< @ref XXH3_MIDSIZE_MAX = 240 bytes) + * or @p secret for "large" keys (>= @ref XXH3_MIDSIZE_MAX). + * + * This generally benefits speed, compared to `_withSeed()` or `_withSecret()`. + * `_withSeed()` has to generate the secret on the fly for "large" keys. + * It's fast, but can be perceptible for "not so large" keys (< 1 KB). + * `_withSecret()` has to generate the masks on the fly for "small" keys, + * which requires more instructions than _withSeed() variants. + * Therefore, _withSecretandSeed variant combines the best of both worlds. + * + * When @p secret has been generated by XXH3_generateSecret_fromSeed(), + * this variant produces *exactly* the same results as `_withSeed()` variant, + * hence offering only a pure speed benefit on "large" input, + * by skipping the need to regenerate the secret for every large input. + * + * Another usage scenario is to hash the secret to a 64-bit hash value, + * for example with XXH3_64bits(), which then becomes the seed, + * and then employ both the seed and the secret in _withSecretandSeed(). + * On top of speed, an added benefit is that each bit in the secret + * has a 50% chance to swap each bit in the output, via its impact to the seed. + * + * This is not guaranteed when using the secret directly in "small data" scenarios, + * because only portions of the secret are employed for small data. + */ +XXH_PUBLIC_API XXH_PUREF XXH64_hash_t +XXH3_64bits_withSecretandSeed(XXH_NOESCAPE const void* data, size_t len, + XXH_NOESCAPE const void* secret, size_t secretSize, + XXH64_hash_t seed); +/*! + * @brief Calculates 128-bit seeded variant of XXH3 hash of @p data. + * + * @param input The block of data to be hashed, at least @p len bytes in size. + * @param length The length of @p data, in bytes. + * @param secret The secret data. + * @param secretSize The length of @p secret, in bytes. + * @param seed64 The 64-bit seed to alter the hash result predictably. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @see XXH3_64bits_withSecretandSeed() + */ +XXH_PUBLIC_API XXH_PUREF XXH128_hash_t +XXH3_128bits_withSecretandSeed(XXH_NOESCAPE const void* input, size_t length, + XXH_NOESCAPE const void* secret, size_t secretSize, + XXH64_hash_t seed64); +#ifndef XXH_NO_STREAM +/*! + * @brief Resets an @ref XXH3_state_t with secret data to begin a new hash. + * + * @param statePtr A pointer to an @ref XXH3_state_t allocated with @ref XXH3_createState(). + * @param secret The secret data. + * @param secretSize The length of @p secret, in bytes. + * @param seed64 The 64-bit seed to alter the hash result predictably. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @see XXH3_64bits_withSecretandSeed() + */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_reset_withSecretandSeed(XXH_NOESCAPE XXH3_state_t* statePtr, + XXH_NOESCAPE const void* secret, size_t secretSize, + XXH64_hash_t seed64); +/*! + * @brief Resets an @ref XXH3_state_t with secret data to begin a new hash. + * + * @param statePtr A pointer to an @ref XXH3_state_t allocated with @ref XXH3_createState(). + * @param secret The secret data. + * @param secretSize The length of @p secret, in bytes. + * @param seed64 The 64-bit seed to alter the hash result predictably. + * + * @return @ref XXH_OK on success. + * @return @ref XXH_ERROR on failure. + * + * @see XXH3_64bits_withSecretandSeed() + */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_reset_withSecretandSeed(XXH_NOESCAPE XXH3_state_t* statePtr, + XXH_NOESCAPE const void* secret, size_t secretSize, + XXH64_hash_t seed64); +#endif /* !XXH_NO_STREAM */ + +#endif /* !XXH_NO_XXH3 */ +#endif /* XXH_NO_LONG_LONG */ +#if defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API) +# define XXH_IMPLEMENTATION +#endif + +#endif /* defined(XXH_STATIC_LINKING_ONLY) && !defined(XXHASH_H_STATIC_13879238742) */ + + +/* ======================================================================== */ +/* ======================================================================== */ +/* ======================================================================== */ + + +/*-********************************************************************** + * xxHash implementation + *-********************************************************************** + * xxHash's implementation used to be hosted inside xxhash.c. + * + * However, inlining requires implementation to be visible to the compiler, + * hence be included alongside the header. + * Previously, implementation was hosted inside xxhash.c, + * which was then #included when inlining was activated. + * This construction created issues with a few build and install systems, + * as it required xxhash.c to be stored in /include directory. + * + * xxHash implementation is now directly integrated within xxhash.h. + * As a consequence, xxhash.c is no longer needed in /include. + * + * xxhash.c is still available and is still useful. + * In a "normal" setup, when xxhash is not inlined, + * xxhash.h only exposes the prototypes and public symbols, + * while xxhash.c can be built into an object file xxhash.o + * which can then be linked into the final binary. + ************************************************************************/ + +#if ( defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API) \ + || defined(XXH_IMPLEMENTATION) ) && !defined(XXH_IMPLEM_13a8737387) +# define XXH_IMPLEM_13a8737387 + +/* ************************************* +* Tuning parameters +***************************************/ + +/*! + * @defgroup tuning Tuning parameters + * @{ + * + * Various macros to control xxHash's behavior. + */ +#ifdef XXH_DOXYGEN +/*! + * @brief Define this to disable 64-bit code. + * + * Useful if only using the @ref XXH32_family and you have a strict C90 compiler. + */ +# define XXH_NO_LONG_LONG +# undef XXH_NO_LONG_LONG /* don't actually */ +/*! + * @brief Controls how unaligned memory is accessed. + * + * By default, access to unaligned memory is controlled by `memcpy()`, which is + * safe and portable. + * + * Unfortunately, on some target/compiler combinations, the generated assembly + * is sub-optimal. + * + * The below switch allow selection of a different access method + * in the search for improved performance. + * + * @par Possible options: + * + * - `XXH_FORCE_MEMORY_ACCESS=0` (default): `memcpy` + * @par + * Use `memcpy()`. Safe and portable. Note that most modern compilers will + * eliminate the function call and treat it as an unaligned access. + * + * - `XXH_FORCE_MEMORY_ACCESS=1`: `__attribute__((aligned(1)))` + * @par + * Depends on compiler extensions and is therefore not portable. + * This method is safe _if_ your compiler supports it, + * and *generally* as fast or faster than `memcpy`. + * + * - `XXH_FORCE_MEMORY_ACCESS=2`: Direct cast + * @par + * Casts directly and dereferences. This method doesn't depend on the + * compiler, but it violates the C standard as it directly dereferences an + * unaligned pointer. It can generate buggy code on targets which do not + * support unaligned memory accesses, but in some circumstances, it's the + * only known way to get the most performance. + * + * - `XXH_FORCE_MEMORY_ACCESS=3`: Byteshift + * @par + * Also portable. This can generate the best code on old compilers which don't + * inline small `memcpy()` calls, and it might also be faster on big-endian + * systems which lack a native byteswap instruction. However, some compilers + * will emit literal byteshifts even if the target supports unaligned access. + * + * + * @warning + * Methods 1 and 2 rely on implementation-defined behavior. Use these with + * care, as what works on one compiler/platform/optimization level may cause + * another to read garbage data or even crash. + * + * See https://fastcompression.blogspot.com/2015/08/accessing-unaligned-memory.html for details. + * + * Prefer these methods in priority order (0 > 3 > 1 > 2) + */ +# define XXH_FORCE_MEMORY_ACCESS 0 + +/*! + * @def XXH_SIZE_OPT + * @brief Controls how much xxHash optimizes for size. + * + * xxHash, when compiled, tends to result in a rather large binary size. This + * is mostly due to heavy usage to forced inlining and constant folding of the + * @ref XXH3_family to increase performance. + * + * However, some developers prefer size over speed. This option can + * significantly reduce the size of the generated code. When using the `-Os` + * or `-Oz` options on GCC or Clang, this is defined to 1 by default, + * otherwise it is defined to 0. + * + * Most of these size optimizations can be controlled manually. + * + * This is a number from 0-2. + * - `XXH_SIZE_OPT` == 0: Default. xxHash makes no size optimizations. Speed + * comes first. + * - `XXH_SIZE_OPT` == 1: Default for `-Os` and `-Oz`. xxHash is more + * conservative and disables hacks that increase code size. It implies the + * options @ref XXH_NO_INLINE_HINTS == 1, @ref XXH_FORCE_ALIGN_CHECK == 0, + * and @ref XXH3_NEON_LANES == 8 if they are not already defined. + * - `XXH_SIZE_OPT` == 2: xxHash tries to make itself as small as possible. + * Performance may cry. For example, the single shot functions just use the + * streaming API. + */ +# define XXH_SIZE_OPT 0 + +/*! + * @def XXH_FORCE_ALIGN_CHECK + * @brief If defined to non-zero, adds a special path for aligned inputs (XXH32() + * and XXH64() only). + * + * This is an important performance trick for architectures without decent + * unaligned memory access performance. + * + * It checks for input alignment, and when conditions are met, uses a "fast + * path" employing direct 32-bit/64-bit reads, resulting in _dramatically + * faster_ read speed. + * + * The check costs one initial branch per hash, which is generally negligible, + * but not zero. + * + * Moreover, it's not useful to generate an additional code path if memory + * access uses the same instruction for both aligned and unaligned + * addresses (e.g. x86 and aarch64). + * + * In these cases, the alignment check can be removed by setting this macro to 0. + * Then the code will always use unaligned memory access. + * Align check is automatically disabled on x86, x64, ARM64, and some ARM chips + * which are platforms known to offer good unaligned memory accesses performance. + * + * It is also disabled by default when @ref XXH_SIZE_OPT >= 1. + * + * This option does not affect XXH3 (only XXH32 and XXH64). + */ +# define XXH_FORCE_ALIGN_CHECK 0 + +/*! + * @def XXH_NO_INLINE_HINTS + * @brief When non-zero, sets all functions to `static`. + * + * By default, xxHash tries to force the compiler to inline almost all internal + * functions. + * + * This can usually improve performance due to reduced jumping and improved + * constant folding, but significantly increases the size of the binary which + * might not be favorable. + * + * Additionally, sometimes the forced inlining can be detrimental to performance, + * depending on the architecture. + * + * XXH_NO_INLINE_HINTS marks all internal functions as static, giving the + * compiler full control on whether to inline or not. + * + * When not optimizing (-O0), using `-fno-inline` with GCC or Clang, or if + * @ref XXH_SIZE_OPT >= 1, this will automatically be defined. + */ +# define XXH_NO_INLINE_HINTS 0 + +/*! + * @def XXH3_INLINE_SECRET + * @brief Determines whether to inline the XXH3 withSecret code. + * + * When the secret size is known, the compiler can improve the performance + * of XXH3_64bits_withSecret() and XXH3_128bits_withSecret(). + * + * However, if the secret size is not known, it doesn't have any benefit. This + * happens when xxHash is compiled into a global symbol. Therefore, if + * @ref XXH_INLINE_ALL is *not* defined, this will be defined to 0. + * + * Additionally, this defaults to 0 on GCC 12+, which has an issue with function pointers + * that are *sometimes* force inline on -Og, and it is impossible to automatically + * detect this optimization level. + */ +# define XXH3_INLINE_SECRET 0 + +/*! + * @def XXH32_ENDJMP + * @brief Whether to use a jump for `XXH32_finalize`. + * + * For performance, `XXH32_finalize` uses multiple branches in the finalizer. + * This is generally preferable for performance, + * but depending on exact architecture, a jmp may be preferable. + * + * This setting is only possibly making a difference for very small inputs. + */ +# define XXH32_ENDJMP 0 + +/*! + * @internal + * @brief Redefines old internal names. + * + * For compatibility with code that uses xxHash's internals before the names + * were changed to improve namespacing. There is no other reason to use this. + */ +# define XXH_OLD_NAMES +# undef XXH_OLD_NAMES /* don't actually use, it is ugly. */ + +/*! + * @def XXH_NO_STREAM + * @brief Disables the streaming API. + * + * When xxHash is not inlined and the streaming functions are not used, disabling + * the streaming functions can improve code size significantly, especially with + * the @ref XXH3_family which tends to make constant folded copies of itself. + */ +# define XXH_NO_STREAM +# undef XXH_NO_STREAM /* don't actually */ +#endif /* XXH_DOXYGEN */ +/*! + * @} + */ + +#ifndef XXH_FORCE_MEMORY_ACCESS /* can be defined externally, on command line for example */ + /* prefer __packed__ structures (method 1) for GCC + * < ARMv7 with unaligned access (e.g. Raspbian armhf) still uses byte shifting, so we use memcpy + * which for some reason does unaligned loads. */ +# if defined(__GNUC__) && !(defined(__ARM_ARCH) && __ARM_ARCH < 7 && defined(__ARM_FEATURE_UNALIGNED)) +# define XXH_FORCE_MEMORY_ACCESS 1 +# endif +#endif + +#ifndef XXH_SIZE_OPT + /* default to 1 for -Os or -Oz */ +# if (defined(__GNUC__) || defined(__clang__)) && defined(__OPTIMIZE_SIZE__) +# define XXH_SIZE_OPT 1 +# else +# define XXH_SIZE_OPT 0 +# endif +#endif + +#ifndef XXH_FORCE_ALIGN_CHECK /* can be defined externally */ + /* don't check on sizeopt, x86, aarch64, or arm when unaligned access is available */ +# if XXH_SIZE_OPT >= 1 || \ + defined(__i386) || defined(__x86_64__) || defined(__aarch64__) || defined(__ARM_FEATURE_UNALIGNED) \ + || defined(_M_IX86) || defined(_M_X64) || defined(_M_ARM64) || defined(_M_ARM) /* visual */ +# define XXH_FORCE_ALIGN_CHECK 0 +# else +# define XXH_FORCE_ALIGN_CHECK 1 +# endif +#endif + +#ifndef XXH_NO_INLINE_HINTS +# if XXH_SIZE_OPT >= 1 || defined(__NO_INLINE__) /* -O0, -fno-inline */ +# define XXH_NO_INLINE_HINTS 1 +# else +# define XXH_NO_INLINE_HINTS 0 +# endif +#endif + +#ifndef XXH3_INLINE_SECRET +# if (defined(__GNUC__) && !defined(__clang__) && __GNUC__ >= 12) \ + || !defined(XXH_INLINE_ALL) +# define XXH3_INLINE_SECRET 0 +# else +# define XXH3_INLINE_SECRET 1 +# endif +#endif + +#ifndef XXH32_ENDJMP +/* generally preferable for performance */ +# define XXH32_ENDJMP 0 +#endif + +/*! + * @defgroup impl Implementation + * @{ + */ + + +/* ************************************* +* Includes & Memory related functions +***************************************/ +#if defined(XXH_NO_STREAM) +/* nothing */ +#elif defined(XXH_NO_STDLIB) + +/* When requesting to disable any mention of stdlib, + * the library loses the ability to invoked malloc / free. + * In practice, it means that functions like `XXH*_createState()` + * will always fail, and return NULL. + * This flag is useful in situations where + * xxhash.h is integrated into some kernel, embedded or limited environment + * without access to dynamic allocation. + */ + +static XXH_CONSTF void* XXH_malloc(size_t s) { (void)s; return NULL; } +static void XXH_free(void* p) { (void)p; } + +#else + +/* + * Modify the local functions below should you wish to use + * different memory routines for malloc() and free() + */ +#include + +/*! + * @internal + * @brief Modify this function to use a different routine than malloc(). + */ +static XXH_MALLOCF void* XXH_malloc(size_t s) { return malloc(s); } + +/*! + * @internal + * @brief Modify this function to use a different routine than free(). + */ +static void XXH_free(void* p) { free(p); } + +#endif /* XXH_NO_STDLIB */ + +#include + +/*! + * @internal + * @brief Modify this function to use a different routine than memcpy(). + */ +static void* XXH_memcpy(void* dest, const void* src, size_t size) +{ + return memcpy(dest,src,size); +} + +#include /* ULLONG_MAX */ + + +/* ************************************* +* Compiler Specific Options +***************************************/ +#ifdef _MSC_VER /* Visual Studio warning fix */ +# pragma warning(disable : 4127) /* disable: C4127: conditional expression is constant */ +#endif + +#if XXH_NO_INLINE_HINTS /* disable inlining hints */ +# if defined(__GNUC__) || defined(__clang__) +# define XXH_FORCE_INLINE static __attribute__((unused)) +# else +# define XXH_FORCE_INLINE static +# endif +# define XXH_NO_INLINE static +/* enable inlining hints */ +#elif defined(__GNUC__) || defined(__clang__) +# define XXH_FORCE_INLINE static __inline__ __attribute__((always_inline, unused)) +# define XXH_NO_INLINE static __attribute__((noinline)) +#elif defined(_MSC_VER) /* Visual Studio */ +# define XXH_FORCE_INLINE static __forceinline +# define XXH_NO_INLINE static __declspec(noinline) +#elif defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) /* C99 */ +# define XXH_FORCE_INLINE static inline +# define XXH_NO_INLINE static +#else +# define XXH_FORCE_INLINE static +# define XXH_NO_INLINE static +#endif + +#if XXH3_INLINE_SECRET +# define XXH3_WITH_SECRET_INLINE XXH_FORCE_INLINE +#else +# define XXH3_WITH_SECRET_INLINE XXH_NO_INLINE +#endif + + +/* ************************************* +* Debug +***************************************/ +/*! + * @ingroup tuning + * @def XXH_DEBUGLEVEL + * @brief Sets the debugging level. + * + * XXH_DEBUGLEVEL is expected to be defined externally, typically via the + * compiler's command line options. The value must be a number. + */ +#ifndef XXH_DEBUGLEVEL +# ifdef DEBUGLEVEL /* backwards compat */ +# define XXH_DEBUGLEVEL DEBUGLEVEL +# else +# define XXH_DEBUGLEVEL 0 +# endif +#endif + +#if (XXH_DEBUGLEVEL>=1) +# include /* note: can still be disabled with NDEBUG */ +# define XXH_ASSERT(c) assert(c) +#else +# if defined(__INTEL_COMPILER) +# define XXH_ASSERT(c) XXH_ASSUME((unsigned char) (c)) +# else +# define XXH_ASSERT(c) XXH_ASSUME(c) +# endif +#endif + +/* note: use after variable declarations */ +#ifndef XXH_STATIC_ASSERT +# if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) /* C11 */ +# define XXH_STATIC_ASSERT_WITH_MESSAGE(c,m) do { _Static_assert((c),m); } while(0) +# elif defined(__cplusplus) && (__cplusplus >= 201103L) /* C++11 */ +# define XXH_STATIC_ASSERT_WITH_MESSAGE(c,m) do { static_assert((c),m); } while(0) +# else +# define XXH_STATIC_ASSERT_WITH_MESSAGE(c,m) do { struct xxh_sa { char x[(c) ? 1 : -1]; }; } while(0) +# endif +# define XXH_STATIC_ASSERT(c) XXH_STATIC_ASSERT_WITH_MESSAGE((c),#c) +#endif + +/*! + * @internal + * @def XXH_COMPILER_GUARD(var) + * @brief Used to prevent unwanted optimizations for @p var. + * + * It uses an empty GCC inline assembly statement with a register constraint + * which forces @p var into a general purpose register (eg eax, ebx, ecx + * on x86) and marks it as modified. + * + * This is used in a few places to avoid unwanted autovectorization (e.g. + * XXH32_round()). All vectorization we want is explicit via intrinsics, + * and _usually_ isn't wanted elsewhere. + * + * We also use it to prevent unwanted constant folding for AArch64 in + * XXH3_initCustomSecret_scalar(). + */ +#if defined(__GNUC__) || defined(__clang__) +# define XXH_COMPILER_GUARD(var) __asm__("" : "+r" (var)) +#else +# define XXH_COMPILER_GUARD(var) ((void)0) +#endif + +/* Specifically for NEON vectors which use the "w" constraint, on + * Clang. */ +#if defined(__clang__) && defined(__ARM_ARCH) && !defined(__wasm__) +# define XXH_COMPILER_GUARD_CLANG_NEON(var) __asm__("" : "+w" (var)) +#else +# define XXH_COMPILER_GUARD_CLANG_NEON(var) ((void)0) +#endif + +/* ************************************* +* Basic Types +***************************************/ +#if !defined (__VMS) \ + && (defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) +# ifdef _AIX +# include +# else +# include +# endif + typedef uint8_t xxh_u8; +#else + typedef unsigned char xxh_u8; +#endif +typedef XXH32_hash_t xxh_u32; + +#ifdef XXH_OLD_NAMES +# warning "XXH_OLD_NAMES is planned to be removed starting v0.9. If the program depends on it, consider moving away from it by employing newer type names directly" +# define BYTE xxh_u8 +# define U8 xxh_u8 +# define U32 xxh_u32 +#endif + +/* *** Memory access *** */ + +/*! + * @internal + * @fn xxh_u32 XXH_read32(const void* ptr) + * @brief Reads an unaligned 32-bit integer from @p ptr in native endianness. + * + * Affected by @ref XXH_FORCE_MEMORY_ACCESS. + * + * @param ptr The pointer to read from. + * @return The 32-bit native endian integer from the bytes at @p ptr. + */ + +/*! + * @internal + * @fn xxh_u32 XXH_readLE32(const void* ptr) + * @brief Reads an unaligned 32-bit little endian integer from @p ptr. + * + * Affected by @ref XXH_FORCE_MEMORY_ACCESS. + * + * @param ptr The pointer to read from. + * @return The 32-bit little endian integer from the bytes at @p ptr. + */ + +/*! + * @internal + * @fn xxh_u32 XXH_readBE32(const void* ptr) + * @brief Reads an unaligned 32-bit big endian integer from @p ptr. + * + * Affected by @ref XXH_FORCE_MEMORY_ACCESS. + * + * @param ptr The pointer to read from. + * @return The 32-bit big endian integer from the bytes at @p ptr. + */ + +/*! + * @internal + * @fn xxh_u32 XXH_readLE32_align(const void* ptr, XXH_alignment align) + * @brief Like @ref XXH_readLE32(), but has an option for aligned reads. + * + * Affected by @ref XXH_FORCE_MEMORY_ACCESS. + * Note that when @ref XXH_FORCE_ALIGN_CHECK == 0, the @p align parameter is + * always @ref XXH_alignment::XXH_unaligned. + * + * @param ptr The pointer to read from. + * @param align Whether @p ptr is aligned. + * @pre + * If @p align == @ref XXH_alignment::XXH_aligned, @p ptr must be 4 byte + * aligned. + * @return The 32-bit little endian integer from the bytes at @p ptr. + */ + +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3)) +/* + * Manual byteshift. Best for old compilers which don't inline memcpy. + * We actually directly use XXH_readLE32 and XXH_readBE32. + */ +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==2)) + +/* + * Force direct memory access. Only works on CPU which support unaligned memory + * access in hardware. + */ +static xxh_u32 XXH_read32(const void* memPtr) { return *(const xxh_u32*) memPtr; } + +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==1)) + +/* + * __attribute__((aligned(1))) is supported by gcc and clang. Originally the + * documentation claimed that it only increased the alignment, but actually it + * can decrease it on gcc, clang, and icc: + * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69502, + * https://gcc.godbolt.org/z/xYez1j67Y. + */ +#ifdef XXH_OLD_NAMES +typedef union { xxh_u32 u32; } __attribute__((packed)) unalign; +#endif +static xxh_u32 XXH_read32(const void* ptr) +{ + typedef __attribute__((aligned(1))) xxh_u32 xxh_unalign32; + return *((const xxh_unalign32*)ptr); +} + +#else + +/* + * Portable and safe solution. Generally efficient. + * see: https://fastcompression.blogspot.com/2015/08/accessing-unaligned-memory.html + */ +static xxh_u32 XXH_read32(const void* memPtr) +{ + xxh_u32 val; + XXH_memcpy(&val, memPtr, sizeof(val)); + return val; +} + +#endif /* XXH_FORCE_DIRECT_MEMORY_ACCESS */ + + +/* *** Endianness *** */ + +/*! + * @ingroup tuning + * @def XXH_CPU_LITTLE_ENDIAN + * @brief Whether the target is little endian. + * + * Defined to 1 if the target is little endian, or 0 if it is big endian. + * It can be defined externally, for example on the compiler command line. + * + * If it is not defined, + * a runtime check (which is usually constant folded) is used instead. + * + * @note + * This is not necessarily defined to an integer constant. + * + * @see XXH_isLittleEndian() for the runtime check. + */ +#ifndef XXH_CPU_LITTLE_ENDIAN +/* + * Try to detect endianness automatically, to avoid the nonstandard behavior + * in `XXH_isLittleEndian()` + */ +# if defined(_WIN32) /* Windows is always little endian */ \ + || defined(__LITTLE_ENDIAN__) \ + || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) +# define XXH_CPU_LITTLE_ENDIAN 1 +# elif defined(__BIG_ENDIAN__) \ + || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +# define XXH_CPU_LITTLE_ENDIAN 0 +# else +/*! + * @internal + * @brief Runtime check for @ref XXH_CPU_LITTLE_ENDIAN. + * + * Most compilers will constant fold this. + */ +static int XXH_isLittleEndian(void) +{ + /* + * Portable and well-defined behavior. + * Don't use static: it is detrimental to performance. + */ + const union { xxh_u32 u; xxh_u8 c[4]; } one = { 1 }; + return one.c[0]; +} +# define XXH_CPU_LITTLE_ENDIAN XXH_isLittleEndian() +# endif +#endif + + + + +/* **************************************** +* Compiler-specific Functions and Macros +******************************************/ +#define XXH_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__) + +#ifdef __has_builtin +# define XXH_HAS_BUILTIN(x) __has_builtin(x) +#else +# define XXH_HAS_BUILTIN(x) 0 +#endif + + + +/* + * C23 and future versions have standard "unreachable()". + * Once it has been implemented reliably we can add it as an + * additional case: + * + * ``` + * #if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= XXH_C23_VN) + * # include + * # ifdef unreachable + * # define XXH_UNREACHABLE() unreachable() + * # endif + * #endif + * ``` + * + * Note C++23 also has std::unreachable() which can be detected + * as follows: + * ``` + * #if defined(__cpp_lib_unreachable) && (__cpp_lib_unreachable >= 202202L) + * # include + * # define XXH_UNREACHABLE() std::unreachable() + * #endif + * ``` + * NB: `__cpp_lib_unreachable` is defined in the `` header. + * We don't use that as including `` in `extern "C"` blocks + * doesn't work on GCC12 + */ + +#if XXH_HAS_BUILTIN(__builtin_unreachable) +# define XXH_UNREACHABLE() __builtin_unreachable() + +#elif defined(_MSC_VER) +# define XXH_UNREACHABLE() __assume(0) + +#else +# define XXH_UNREACHABLE() +#endif + +#if XXH_HAS_BUILTIN(__builtin_assume) +# define XXH_ASSUME(c) __builtin_assume(c) +#else +# define XXH_ASSUME(c) if (!(c)) { XXH_UNREACHABLE(); } +#endif + +/*! + * @internal + * @def XXH_rotl32(x,r) + * @brief 32-bit rotate left. + * + * @param x The 32-bit integer to be rotated. + * @param r The number of bits to rotate. + * @pre + * @p r > 0 && @p r < 32 + * @note + * @p x and @p r may be evaluated multiple times. + * @return The rotated result. + */ +#if !defined(NO_CLANG_BUILTIN) && XXH_HAS_BUILTIN(__builtin_rotateleft32) \ + && XXH_HAS_BUILTIN(__builtin_rotateleft64) +# define XXH_rotl32 __builtin_rotateleft32 +# define XXH_rotl64 __builtin_rotateleft64 +/* Note: although _rotl exists for minGW (GCC under windows), performance seems poor */ +#elif defined(_MSC_VER) +# define XXH_rotl32(x,r) _rotl(x,r) +# define XXH_rotl64(x,r) _rotl64(x,r) +#else +# define XXH_rotl32(x,r) (((x) << (r)) | ((x) >> (32 - (r)))) +# define XXH_rotl64(x,r) (((x) << (r)) | ((x) >> (64 - (r)))) +#endif + +/*! + * @internal + * @fn xxh_u32 XXH_swap32(xxh_u32 x) + * @brief A 32-bit byteswap. + * + * @param x The 32-bit integer to byteswap. + * @return @p x, byteswapped. + */ +#if defined(_MSC_VER) /* Visual Studio */ +# define XXH_swap32 _byteswap_ulong +#elif XXH_GCC_VERSION >= 403 +# define XXH_swap32 __builtin_bswap32 +#else +static xxh_u32 XXH_swap32 (xxh_u32 x) +{ + return ((x << 24) & 0xff000000 ) | + ((x << 8) & 0x00ff0000 ) | + ((x >> 8) & 0x0000ff00 ) | + ((x >> 24) & 0x000000ff ); +} +#endif + + +/* *************************** +* Memory reads +*****************************/ + +/*! + * @internal + * @brief Enum to indicate whether a pointer is aligned. + */ +typedef enum { + XXH_aligned, /*!< Aligned */ + XXH_unaligned /*!< Possibly unaligned */ +} XXH_alignment; + +/* + * XXH_FORCE_MEMORY_ACCESS==3 is an endian-independent byteshift load. + * + * This is ideal for older compilers which don't inline memcpy. + */ +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3)) + +XXH_FORCE_INLINE xxh_u32 XXH_readLE32(const void* memPtr) +{ + const xxh_u8* bytePtr = (const xxh_u8 *)memPtr; + return bytePtr[0] + | ((xxh_u32)bytePtr[1] << 8) + | ((xxh_u32)bytePtr[2] << 16) + | ((xxh_u32)bytePtr[3] << 24); +} + +XXH_FORCE_INLINE xxh_u32 XXH_readBE32(const void* memPtr) +{ + const xxh_u8* bytePtr = (const xxh_u8 *)memPtr; + return bytePtr[3] + | ((xxh_u32)bytePtr[2] << 8) + | ((xxh_u32)bytePtr[1] << 16) + | ((xxh_u32)bytePtr[0] << 24); +} + +#else +XXH_FORCE_INLINE xxh_u32 XXH_readLE32(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_read32(ptr) : XXH_swap32(XXH_read32(ptr)); +} + +static xxh_u32 XXH_readBE32(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_swap32(XXH_read32(ptr)) : XXH_read32(ptr); +} +#endif + +XXH_FORCE_INLINE xxh_u32 +XXH_readLE32_align(const void* ptr, XXH_alignment align) +{ + if (align==XXH_unaligned) { + return XXH_readLE32(ptr); + } else { + return XXH_CPU_LITTLE_ENDIAN ? *(const xxh_u32*)ptr : XXH_swap32(*(const xxh_u32*)ptr); + } +} + + +/* ************************************* +* Misc +***************************************/ +/*! @ingroup public */ +XXH_PUBLIC_API unsigned XXH_versionNumber (void) { return XXH_VERSION_NUMBER; } + + +/* ******************************************************************* +* 32-bit hash functions +*********************************************************************/ +/*! + * @} + * @defgroup XXH32_impl XXH32 implementation + * @ingroup impl + * + * Details on the XXH32 implementation. + * @{ + */ + /* #define instead of static const, to be used as initializers */ +#define XXH_PRIME32_1 0x9E3779B1U /*!< 0b10011110001101110111100110110001 */ +#define XXH_PRIME32_2 0x85EBCA77U /*!< 0b10000101111010111100101001110111 */ +#define XXH_PRIME32_3 0xC2B2AE3DU /*!< 0b11000010101100101010111000111101 */ +#define XXH_PRIME32_4 0x27D4EB2FU /*!< 0b00100111110101001110101100101111 */ +#define XXH_PRIME32_5 0x165667B1U /*!< 0b00010110010101100110011110110001 */ + +#ifdef XXH_OLD_NAMES +# define PRIME32_1 XXH_PRIME32_1 +# define PRIME32_2 XXH_PRIME32_2 +# define PRIME32_3 XXH_PRIME32_3 +# define PRIME32_4 XXH_PRIME32_4 +# define PRIME32_5 XXH_PRIME32_5 +#endif + +/*! + * @internal + * @brief Normal stripe processing routine. + * + * This shuffles the bits so that any bit from @p input impacts several bits in + * @p acc. + * + * @param acc The accumulator lane. + * @param input The stripe of input to mix. + * @return The mixed accumulator lane. + */ +static xxh_u32 XXH32_round(xxh_u32 acc, xxh_u32 input) +{ + acc += input * XXH_PRIME32_2; + acc = XXH_rotl32(acc, 13); + acc *= XXH_PRIME32_1; +#if (defined(__SSE4_1__) || defined(__aarch64__) || defined(__wasm_simd128__)) && !defined(XXH_ENABLE_AUTOVECTORIZE) + /* + * UGLY HACK: + * A compiler fence is the only thing that prevents GCC and Clang from + * autovectorizing the XXH32 loop (pragmas and attributes don't work for some + * reason) without globally disabling SSE4.1. + * + * The reason we want to avoid vectorization is because despite working on + * 4 integers at a time, there are multiple factors slowing XXH32 down on + * SSE4: + * - There's a ridiculous amount of lag from pmulld (10 cycles of latency on + * newer chips!) making it slightly slower to multiply four integers at + * once compared to four integers independently. Even when pmulld was + * fastest, Sandy/Ivy Bridge, it is still not worth it to go into SSE + * just to multiply unless doing a long operation. + * + * - Four instructions are required to rotate, + * movqda tmp, v // not required with VEX encoding + * pslld tmp, 13 // tmp <<= 13 + * psrld v, 19 // x >>= 19 + * por v, tmp // x |= tmp + * compared to one for scalar: + * roll v, 13 // reliably fast across the board + * shldl v, v, 13 // Sandy Bridge and later prefer this for some reason + * + * - Instruction level parallelism is actually more beneficial here because + * the SIMD actually serializes this operation: While v1 is rotating, v2 + * can load data, while v3 can multiply. SSE forces them to operate + * together. + * + * This is also enabled on AArch64, as Clang is *very aggressive* in vectorizing + * the loop. NEON is only faster on the A53, and with the newer cores, it is less + * than half the speed. + * + * Additionally, this is used on WASM SIMD128 because it JITs to the same + * SIMD instructions and has the same issue. + */ + XXH_COMPILER_GUARD(acc); +#endif + return acc; +} + +/*! + * @internal + * @brief Mixes all bits to finalize the hash. + * + * The final mix ensures that all input bits have a chance to impact any bit in + * the output digest, resulting in an unbiased distribution. + * + * @param hash The hash to avalanche. + * @return The avalanched hash. + */ +static xxh_u32 XXH32_avalanche(xxh_u32 hash) +{ + hash ^= hash >> 15; + hash *= XXH_PRIME32_2; + hash ^= hash >> 13; + hash *= XXH_PRIME32_3; + hash ^= hash >> 16; + return hash; +} + +#define XXH_get32bits(p) XXH_readLE32_align(p, align) + +/*! + * @internal + * @brief Processes the last 0-15 bytes of @p ptr. + * + * There may be up to 15 bytes remaining to consume from the input. + * This final stage will digest them to ensure that all input bytes are present + * in the final mix. + * + * @param hash The hash to finalize. + * @param ptr The pointer to the remaining input. + * @param len The remaining length, modulo 16. + * @param align Whether @p ptr is aligned. + * @return The finalized hash. + * @see XXH64_finalize(). + */ +static XXH_PUREF xxh_u32 +XXH32_finalize(xxh_u32 hash, const xxh_u8* ptr, size_t len, XXH_alignment align) +{ +#define XXH_PROCESS1 do { \ + hash += (*ptr++) * XXH_PRIME32_5; \ + hash = XXH_rotl32(hash, 11) * XXH_PRIME32_1; \ +} while (0) + +#define XXH_PROCESS4 do { \ + hash += XXH_get32bits(ptr) * XXH_PRIME32_3; \ + ptr += 4; \ + hash = XXH_rotl32(hash, 17) * XXH_PRIME32_4; \ +} while (0) + + if (ptr==NULL) XXH_ASSERT(len == 0); + + /* Compact rerolled version; generally faster */ + if (!XXH32_ENDJMP) { + len &= 15; + while (len >= 4) { + XXH_PROCESS4; + len -= 4; + } + while (len > 0) { + XXH_PROCESS1; + --len; + } + return XXH32_avalanche(hash); + } else { + switch(len&15) /* or switch(bEnd - p) */ { + case 12: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 8: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 4: XXH_PROCESS4; + return XXH32_avalanche(hash); + + case 13: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 9: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 5: XXH_PROCESS4; + XXH_PROCESS1; + return XXH32_avalanche(hash); + + case 14: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 10: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 6: XXH_PROCESS4; + XXH_PROCESS1; + XXH_PROCESS1; + return XXH32_avalanche(hash); + + case 15: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 11: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 7: XXH_PROCESS4; + XXH_FALLTHROUGH; /* fallthrough */ + case 3: XXH_PROCESS1; + XXH_FALLTHROUGH; /* fallthrough */ + case 2: XXH_PROCESS1; + XXH_FALLTHROUGH; /* fallthrough */ + case 1: XXH_PROCESS1; + XXH_FALLTHROUGH; /* fallthrough */ + case 0: return XXH32_avalanche(hash); + } + XXH_ASSERT(0); + return hash; /* reaching this point is deemed impossible */ + } +} + +#ifdef XXH_OLD_NAMES +# define PROCESS1 XXH_PROCESS1 +# define PROCESS4 XXH_PROCESS4 +#else +# undef XXH_PROCESS1 +# undef XXH_PROCESS4 +#endif + +/*! + * @internal + * @brief The implementation for @ref XXH32(). + * + * @param input , len , seed Directly passed from @ref XXH32(). + * @param align Whether @p input is aligned. + * @return The calculated hash. + */ +XXH_FORCE_INLINE XXH_PUREF xxh_u32 +XXH32_endian_align(const xxh_u8* input, size_t len, xxh_u32 seed, XXH_alignment align) +{ + xxh_u32 h32; + + if (input==NULL) XXH_ASSERT(len == 0); + + if (len>=16) { + const xxh_u8* const bEnd = input + len; + const xxh_u8* const limit = bEnd - 15; + xxh_u32 v1 = seed + XXH_PRIME32_1 + XXH_PRIME32_2; + xxh_u32 v2 = seed + XXH_PRIME32_2; + xxh_u32 v3 = seed + 0; + xxh_u32 v4 = seed - XXH_PRIME32_1; + + do { + v1 = XXH32_round(v1, XXH_get32bits(input)); input += 4; + v2 = XXH32_round(v2, XXH_get32bits(input)); input += 4; + v3 = XXH32_round(v3, XXH_get32bits(input)); input += 4; + v4 = XXH32_round(v4, XXH_get32bits(input)); input += 4; + } while (input < limit); + + h32 = XXH_rotl32(v1, 1) + XXH_rotl32(v2, 7) + + XXH_rotl32(v3, 12) + XXH_rotl32(v4, 18); + } else { + h32 = seed + XXH_PRIME32_5; + } + + h32 += (xxh_u32)len; + + return XXH32_finalize(h32, input, len&15, align); +} + +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API XXH32_hash_t XXH32 (const void* input, size_t len, XXH32_hash_t seed) +{ +#if !defined(XXH_NO_STREAM) && XXH_SIZE_OPT >= 2 + /* Simple version, good for code maintenance, but unfortunately slow for small inputs */ + XXH32_state_t state; + XXH32_reset(&state, seed); + XXH32_update(&state, (const xxh_u8*)input, len); + return XXH32_digest(&state); +#else + if (XXH_FORCE_ALIGN_CHECK) { + if ((((size_t)input) & 3) == 0) { /* Input is 4-bytes aligned, leverage the speed benefit */ + return XXH32_endian_align((const xxh_u8*)input, len, seed, XXH_aligned); + } } + + return XXH32_endian_align((const xxh_u8*)input, len, seed, XXH_unaligned); +#endif +} + + + +/******* Hash streaming *******/ +#ifndef XXH_NO_STREAM +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API XXH32_state_t* XXH32_createState(void) +{ + return (XXH32_state_t*)XXH_malloc(sizeof(XXH32_state_t)); +} +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API XXH_errorcode XXH32_freeState(XXH32_state_t* statePtr) +{ + XXH_free(statePtr); + return XXH_OK; +} + +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API void XXH32_copyState(XXH32_state_t* dstState, const XXH32_state_t* srcState) +{ + XXH_memcpy(dstState, srcState, sizeof(*dstState)); +} + +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API XXH_errorcode XXH32_reset(XXH32_state_t* statePtr, XXH32_hash_t seed) +{ + XXH_ASSERT(statePtr != NULL); + memset(statePtr, 0, sizeof(*statePtr)); + statePtr->v[0] = seed + XXH_PRIME32_1 + XXH_PRIME32_2; + statePtr->v[1] = seed + XXH_PRIME32_2; + statePtr->v[2] = seed + 0; + statePtr->v[3] = seed - XXH_PRIME32_1; + return XXH_OK; +} + + +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API XXH_errorcode +XXH32_update(XXH32_state_t* state, const void* input, size_t len) +{ + if (input==NULL) { + XXH_ASSERT(len == 0); + return XXH_OK; + } + + { const xxh_u8* p = (const xxh_u8*)input; + const xxh_u8* const bEnd = p + len; + + state->total_len_32 += (XXH32_hash_t)len; + state->large_len |= (XXH32_hash_t)((len>=16) | (state->total_len_32>=16)); + + if (state->memsize + len < 16) { /* fill in tmp buffer */ + XXH_memcpy((xxh_u8*)(state->mem32) + state->memsize, input, len); + state->memsize += (XXH32_hash_t)len; + return XXH_OK; + } + + if (state->memsize) { /* some data left from previous update */ + XXH_memcpy((xxh_u8*)(state->mem32) + state->memsize, input, 16-state->memsize); + { const xxh_u32* p32 = state->mem32; + state->v[0] = XXH32_round(state->v[0], XXH_readLE32(p32)); p32++; + state->v[1] = XXH32_round(state->v[1], XXH_readLE32(p32)); p32++; + state->v[2] = XXH32_round(state->v[2], XXH_readLE32(p32)); p32++; + state->v[3] = XXH32_round(state->v[3], XXH_readLE32(p32)); + } + p += 16-state->memsize; + state->memsize = 0; + } + + if (p <= bEnd-16) { + const xxh_u8* const limit = bEnd - 16; + + do { + state->v[0] = XXH32_round(state->v[0], XXH_readLE32(p)); p+=4; + state->v[1] = XXH32_round(state->v[1], XXH_readLE32(p)); p+=4; + state->v[2] = XXH32_round(state->v[2], XXH_readLE32(p)); p+=4; + state->v[3] = XXH32_round(state->v[3], XXH_readLE32(p)); p+=4; + } while (p<=limit); + + } + + if (p < bEnd) { + XXH_memcpy(state->mem32, p, (size_t)(bEnd-p)); + state->memsize = (unsigned)(bEnd-p); + } + } + + return XXH_OK; +} + + +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API XXH32_hash_t XXH32_digest(const XXH32_state_t* state) +{ + xxh_u32 h32; + + if (state->large_len) { + h32 = XXH_rotl32(state->v[0], 1) + + XXH_rotl32(state->v[1], 7) + + XXH_rotl32(state->v[2], 12) + + XXH_rotl32(state->v[3], 18); + } else { + h32 = state->v[2] /* == seed */ + XXH_PRIME32_5; + } + + h32 += state->total_len_32; + + return XXH32_finalize(h32, (const xxh_u8*)state->mem32, state->memsize, XXH_aligned); +} +#endif /* !XXH_NO_STREAM */ + +/******* Canonical representation *******/ + +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API void XXH32_canonicalFromHash(XXH32_canonical_t* dst, XXH32_hash_t hash) +{ + XXH_STATIC_ASSERT(sizeof(XXH32_canonical_t) == sizeof(XXH32_hash_t)); + if (XXH_CPU_LITTLE_ENDIAN) hash = XXH_swap32(hash); + XXH_memcpy(dst, &hash, sizeof(*dst)); +} +/*! @ingroup XXH32_family */ +XXH_PUBLIC_API XXH32_hash_t XXH32_hashFromCanonical(const XXH32_canonical_t* src) +{ + return XXH_readBE32(src); +} + + +#ifndef XXH_NO_LONG_LONG + +/* ******************************************************************* +* 64-bit hash functions +*********************************************************************/ +/*! + * @} + * @ingroup impl + * @{ + */ +/******* Memory access *******/ + +typedef XXH64_hash_t xxh_u64; + +#ifdef XXH_OLD_NAMES +# define U64 xxh_u64 +#endif + +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3)) +/* + * Manual byteshift. Best for old compilers which don't inline memcpy. + * We actually directly use XXH_readLE64 and XXH_readBE64. + */ +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==2)) + +/* Force direct memory access. Only works on CPU which support unaligned memory access in hardware */ +static xxh_u64 XXH_read64(const void* memPtr) +{ + return *(const xxh_u64*) memPtr; +} + +#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==1)) + +/* + * __attribute__((aligned(1))) is supported by gcc and clang. Originally the + * documentation claimed that it only increased the alignment, but actually it + * can decrease it on gcc, clang, and icc: + * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69502, + * https://gcc.godbolt.org/z/xYez1j67Y. + */ +#ifdef XXH_OLD_NAMES +typedef union { xxh_u32 u32; xxh_u64 u64; } __attribute__((packed)) unalign64; +#endif +static xxh_u64 XXH_read64(const void* ptr) +{ + typedef __attribute__((aligned(1))) xxh_u64 xxh_unalign64; + return *((const xxh_unalign64*)ptr); +} + +#else + +/* + * Portable and safe solution. Generally efficient. + * see: https://fastcompression.blogspot.com/2015/08/accessing-unaligned-memory.html + */ +static xxh_u64 XXH_read64(const void* memPtr) +{ + xxh_u64 val; + XXH_memcpy(&val, memPtr, sizeof(val)); + return val; +} + +#endif /* XXH_FORCE_DIRECT_MEMORY_ACCESS */ + +#if defined(_MSC_VER) /* Visual Studio */ +# define XXH_swap64 _byteswap_uint64 +#elif XXH_GCC_VERSION >= 403 +# define XXH_swap64 __builtin_bswap64 +#else +static xxh_u64 XXH_swap64(xxh_u64 x) +{ + return ((x << 56) & 0xff00000000000000ULL) | + ((x << 40) & 0x00ff000000000000ULL) | + ((x << 24) & 0x0000ff0000000000ULL) | + ((x << 8) & 0x000000ff00000000ULL) | + ((x >> 8) & 0x00000000ff000000ULL) | + ((x >> 24) & 0x0000000000ff0000ULL) | + ((x >> 40) & 0x000000000000ff00ULL) | + ((x >> 56) & 0x00000000000000ffULL); +} +#endif + + +/* XXH_FORCE_MEMORY_ACCESS==3 is an endian-independent byteshift load. */ +#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3)) + +XXH_FORCE_INLINE xxh_u64 XXH_readLE64(const void* memPtr) +{ + const xxh_u8* bytePtr = (const xxh_u8 *)memPtr; + return bytePtr[0] + | ((xxh_u64)bytePtr[1] << 8) + | ((xxh_u64)bytePtr[2] << 16) + | ((xxh_u64)bytePtr[3] << 24) + | ((xxh_u64)bytePtr[4] << 32) + | ((xxh_u64)bytePtr[5] << 40) + | ((xxh_u64)bytePtr[6] << 48) + | ((xxh_u64)bytePtr[7] << 56); +} + +XXH_FORCE_INLINE xxh_u64 XXH_readBE64(const void* memPtr) +{ + const xxh_u8* bytePtr = (const xxh_u8 *)memPtr; + return bytePtr[7] + | ((xxh_u64)bytePtr[6] << 8) + | ((xxh_u64)bytePtr[5] << 16) + | ((xxh_u64)bytePtr[4] << 24) + | ((xxh_u64)bytePtr[3] << 32) + | ((xxh_u64)bytePtr[2] << 40) + | ((xxh_u64)bytePtr[1] << 48) + | ((xxh_u64)bytePtr[0] << 56); +} + +#else +XXH_FORCE_INLINE xxh_u64 XXH_readLE64(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_read64(ptr) : XXH_swap64(XXH_read64(ptr)); +} + +static xxh_u64 XXH_readBE64(const void* ptr) +{ + return XXH_CPU_LITTLE_ENDIAN ? XXH_swap64(XXH_read64(ptr)) : XXH_read64(ptr); +} +#endif + +XXH_FORCE_INLINE xxh_u64 +XXH_readLE64_align(const void* ptr, XXH_alignment align) +{ + if (align==XXH_unaligned) + return XXH_readLE64(ptr); + else + return XXH_CPU_LITTLE_ENDIAN ? *(const xxh_u64*)ptr : XXH_swap64(*(const xxh_u64*)ptr); +} + + +/******* xxh64 *******/ +/*! + * @} + * @defgroup XXH64_impl XXH64 implementation + * @ingroup impl + * + * Details on the XXH64 implementation. + * @{ + */ +/* #define rather that static const, to be used as initializers */ +#define XXH_PRIME64_1 0x9E3779B185EBCA87ULL /*!< 0b1001111000110111011110011011000110000101111010111100101010000111 */ +#define XXH_PRIME64_2 0xC2B2AE3D27D4EB4FULL /*!< 0b1100001010110010101011100011110100100111110101001110101101001111 */ +#define XXH_PRIME64_3 0x165667B19E3779F9ULL /*!< 0b0001011001010110011001111011000110011110001101110111100111111001 */ +#define XXH_PRIME64_4 0x85EBCA77C2B2AE63ULL /*!< 0b1000010111101011110010100111011111000010101100101010111001100011 */ +#define XXH_PRIME64_5 0x27D4EB2F165667C5ULL /*!< 0b0010011111010100111010110010111100010110010101100110011111000101 */ + +#ifdef XXH_OLD_NAMES +# define PRIME64_1 XXH_PRIME64_1 +# define PRIME64_2 XXH_PRIME64_2 +# define PRIME64_3 XXH_PRIME64_3 +# define PRIME64_4 XXH_PRIME64_4 +# define PRIME64_5 XXH_PRIME64_5 +#endif + +/*! @copydoc XXH32_round */ +static xxh_u64 XXH64_round(xxh_u64 acc, xxh_u64 input) +{ + acc += input * XXH_PRIME64_2; + acc = XXH_rotl64(acc, 31); + acc *= XXH_PRIME64_1; +#if (defined(__AVX512F__)) && !defined(XXH_ENABLE_AUTOVECTORIZE) + /* + * DISABLE AUTOVECTORIZATION: + * A compiler fence is used to prevent GCC and Clang from + * autovectorizing the XXH64 loop (pragmas and attributes don't work for some + * reason) without globally disabling AVX512. + * + * Autovectorization of XXH64 tends to be detrimental, + * though the exact outcome may change depending on exact cpu and compiler version. + * For information, it has been reported as detrimental for Skylake-X, + * but possibly beneficial for Zen4. + * + * The default is to disable auto-vectorization, + * but you can select to enable it instead using `XXH_ENABLE_AUTOVECTORIZE` build variable. + */ + XXH_COMPILER_GUARD(acc); +#endif + return acc; +} + +static xxh_u64 XXH64_mergeRound(xxh_u64 acc, xxh_u64 val) +{ + val = XXH64_round(0, val); + acc ^= val; + acc = acc * XXH_PRIME64_1 + XXH_PRIME64_4; + return acc; +} + +/*! @copydoc XXH32_avalanche */ +static xxh_u64 XXH64_avalanche(xxh_u64 hash) +{ + hash ^= hash >> 33; + hash *= XXH_PRIME64_2; + hash ^= hash >> 29; + hash *= XXH_PRIME64_3; + hash ^= hash >> 32; + return hash; +} + + +#define XXH_get64bits(p) XXH_readLE64_align(p, align) + +/*! + * @internal + * @brief Processes the last 0-31 bytes of @p ptr. + * + * There may be up to 31 bytes remaining to consume from the input. + * This final stage will digest them to ensure that all input bytes are present + * in the final mix. + * + * @param hash The hash to finalize. + * @param ptr The pointer to the remaining input. + * @param len The remaining length, modulo 32. + * @param align Whether @p ptr is aligned. + * @return The finalized hash + * @see XXH32_finalize(). + */ +static XXH_PUREF xxh_u64 +XXH64_finalize(xxh_u64 hash, const xxh_u8* ptr, size_t len, XXH_alignment align) +{ + if (ptr==NULL) XXH_ASSERT(len == 0); + len &= 31; + while (len >= 8) { + xxh_u64 const k1 = XXH64_round(0, XXH_get64bits(ptr)); + ptr += 8; + hash ^= k1; + hash = XXH_rotl64(hash,27) * XXH_PRIME64_1 + XXH_PRIME64_4; + len -= 8; + } + if (len >= 4) { + hash ^= (xxh_u64)(XXH_get32bits(ptr)) * XXH_PRIME64_1; + ptr += 4; + hash = XXH_rotl64(hash, 23) * XXH_PRIME64_2 + XXH_PRIME64_3; + len -= 4; + } + while (len > 0) { + hash ^= (*ptr++) * XXH_PRIME64_5; + hash = XXH_rotl64(hash, 11) * XXH_PRIME64_1; + --len; + } + return XXH64_avalanche(hash); +} + +#ifdef XXH_OLD_NAMES +# define PROCESS1_64 XXH_PROCESS1_64 +# define PROCESS4_64 XXH_PROCESS4_64 +# define PROCESS8_64 XXH_PROCESS8_64 +#else +# undef XXH_PROCESS1_64 +# undef XXH_PROCESS4_64 +# undef XXH_PROCESS8_64 +#endif + +/*! + * @internal + * @brief The implementation for @ref XXH64(). + * + * @param input , len , seed Directly passed from @ref XXH64(). + * @param align Whether @p input is aligned. + * @return The calculated hash. + */ +XXH_FORCE_INLINE XXH_PUREF xxh_u64 +XXH64_endian_align(const xxh_u8* input, size_t len, xxh_u64 seed, XXH_alignment align) +{ + xxh_u64 h64; + if (input==NULL) XXH_ASSERT(len == 0); + + if (len>=32) { + const xxh_u8* const bEnd = input + len; + const xxh_u8* const limit = bEnd - 31; + xxh_u64 v1 = seed + XXH_PRIME64_1 + XXH_PRIME64_2; + xxh_u64 v2 = seed + XXH_PRIME64_2; + xxh_u64 v3 = seed + 0; + xxh_u64 v4 = seed - XXH_PRIME64_1; + + do { + v1 = XXH64_round(v1, XXH_get64bits(input)); input+=8; + v2 = XXH64_round(v2, XXH_get64bits(input)); input+=8; + v3 = XXH64_round(v3, XXH_get64bits(input)); input+=8; + v4 = XXH64_round(v4, XXH_get64bits(input)); input+=8; + } while (input= 2 + /* Simple version, good for code maintenance, but unfortunately slow for small inputs */ + XXH64_state_t state; + XXH64_reset(&state, seed); + XXH64_update(&state, (const xxh_u8*)input, len); + return XXH64_digest(&state); +#else + if (XXH_FORCE_ALIGN_CHECK) { + if ((((size_t)input) & 7)==0) { /* Input is aligned, let's leverage the speed advantage */ + return XXH64_endian_align((const xxh_u8*)input, len, seed, XXH_aligned); + } } + + return XXH64_endian_align((const xxh_u8*)input, len, seed, XXH_unaligned); + +#endif +} + +/******* Hash Streaming *******/ +#ifndef XXH_NO_STREAM +/*! @ingroup XXH64_family*/ +XXH_PUBLIC_API XXH64_state_t* XXH64_createState(void) +{ + return (XXH64_state_t*)XXH_malloc(sizeof(XXH64_state_t)); +} +/*! @ingroup XXH64_family */ +XXH_PUBLIC_API XXH_errorcode XXH64_freeState(XXH64_state_t* statePtr) +{ + XXH_free(statePtr); + return XXH_OK; +} + +/*! @ingroup XXH64_family */ +XXH_PUBLIC_API void XXH64_copyState(XXH_NOESCAPE XXH64_state_t* dstState, const XXH64_state_t* srcState) +{ + XXH_memcpy(dstState, srcState, sizeof(*dstState)); +} + +/*! @ingroup XXH64_family */ +XXH_PUBLIC_API XXH_errorcode XXH64_reset(XXH_NOESCAPE XXH64_state_t* statePtr, XXH64_hash_t seed) +{ + XXH_ASSERT(statePtr != NULL); + memset(statePtr, 0, sizeof(*statePtr)); + statePtr->v[0] = seed + XXH_PRIME64_1 + XXH_PRIME64_2; + statePtr->v[1] = seed + XXH_PRIME64_2; + statePtr->v[2] = seed + 0; + statePtr->v[3] = seed - XXH_PRIME64_1; + return XXH_OK; +} + +/*! @ingroup XXH64_family */ +XXH_PUBLIC_API XXH_errorcode +XXH64_update (XXH_NOESCAPE XXH64_state_t* state, XXH_NOESCAPE const void* input, size_t len) +{ + if (input==NULL) { + XXH_ASSERT(len == 0); + return XXH_OK; + } + + { const xxh_u8* p = (const xxh_u8*)input; + const xxh_u8* const bEnd = p + len; + + state->total_len += len; + + if (state->memsize + len < 32) { /* fill in tmp buffer */ + XXH_memcpy(((xxh_u8*)state->mem64) + state->memsize, input, len); + state->memsize += (xxh_u32)len; + return XXH_OK; + } + + if (state->memsize) { /* tmp buffer is full */ + XXH_memcpy(((xxh_u8*)state->mem64) + state->memsize, input, 32-state->memsize); + state->v[0] = XXH64_round(state->v[0], XXH_readLE64(state->mem64+0)); + state->v[1] = XXH64_round(state->v[1], XXH_readLE64(state->mem64+1)); + state->v[2] = XXH64_round(state->v[2], XXH_readLE64(state->mem64+2)); + state->v[3] = XXH64_round(state->v[3], XXH_readLE64(state->mem64+3)); + p += 32 - state->memsize; + state->memsize = 0; + } + + if (p+32 <= bEnd) { + const xxh_u8* const limit = bEnd - 32; + + do { + state->v[0] = XXH64_round(state->v[0], XXH_readLE64(p)); p+=8; + state->v[1] = XXH64_round(state->v[1], XXH_readLE64(p)); p+=8; + state->v[2] = XXH64_round(state->v[2], XXH_readLE64(p)); p+=8; + state->v[3] = XXH64_round(state->v[3], XXH_readLE64(p)); p+=8; + } while (p<=limit); + + } + + if (p < bEnd) { + XXH_memcpy(state->mem64, p, (size_t)(bEnd-p)); + state->memsize = (unsigned)(bEnd-p); + } + } + + return XXH_OK; +} + + +/*! @ingroup XXH64_family */ +XXH_PUBLIC_API XXH64_hash_t XXH64_digest(XXH_NOESCAPE const XXH64_state_t* state) +{ + xxh_u64 h64; + + if (state->total_len >= 32) { + h64 = XXH_rotl64(state->v[0], 1) + XXH_rotl64(state->v[1], 7) + XXH_rotl64(state->v[2], 12) + XXH_rotl64(state->v[3], 18); + h64 = XXH64_mergeRound(h64, state->v[0]); + h64 = XXH64_mergeRound(h64, state->v[1]); + h64 = XXH64_mergeRound(h64, state->v[2]); + h64 = XXH64_mergeRound(h64, state->v[3]); + } else { + h64 = state->v[2] /*seed*/ + XXH_PRIME64_5; + } + + h64 += (xxh_u64) state->total_len; + + return XXH64_finalize(h64, (const xxh_u8*)state->mem64, (size_t)state->total_len, XXH_aligned); +} +#endif /* !XXH_NO_STREAM */ + +/******* Canonical representation *******/ + +/*! @ingroup XXH64_family */ +XXH_PUBLIC_API void XXH64_canonicalFromHash(XXH_NOESCAPE XXH64_canonical_t* dst, XXH64_hash_t hash) +{ + XXH_STATIC_ASSERT(sizeof(XXH64_canonical_t) == sizeof(XXH64_hash_t)); + if (XXH_CPU_LITTLE_ENDIAN) hash = XXH_swap64(hash); + XXH_memcpy(dst, &hash, sizeof(*dst)); +} + +/*! @ingroup XXH64_family */ +XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(XXH_NOESCAPE const XXH64_canonical_t* src) +{ + return XXH_readBE64(src); +} + +#ifndef XXH_NO_XXH3 + +/* ********************************************************************* +* XXH3 +* New generation hash designed for speed on small keys and vectorization +************************************************************************ */ +/*! + * @} + * @defgroup XXH3_impl XXH3 implementation + * @ingroup impl + * @{ + */ + +/* === Compiler specifics === */ + +#if ((defined(sun) || defined(__sun)) && __cplusplus) /* Solaris includes __STDC_VERSION__ with C++. Tested with GCC 5.5 */ +# define XXH_RESTRICT /* disable */ +#elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* >= C99 */ +# define XXH_RESTRICT restrict +#elif (defined (__GNUC__) && ((__GNUC__ > 3) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1))) \ + || (defined (__clang__)) \ + || (defined (_MSC_VER) && (_MSC_VER >= 1400)) \ + || (defined (__INTEL_COMPILER) && (__INTEL_COMPILER >= 1300)) +/* + * There are a LOT more compilers that recognize __restrict but this + * covers the major ones. + */ +# define XXH_RESTRICT __restrict +#else +# define XXH_RESTRICT /* disable */ +#endif + +#if (defined(__GNUC__) && (__GNUC__ >= 3)) \ + || (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 800)) \ + || defined(__clang__) +# define XXH_likely(x) __builtin_expect(x, 1) +# define XXH_unlikely(x) __builtin_expect(x, 0) +#else +# define XXH_likely(x) (x) +# define XXH_unlikely(x) (x) +#endif + +#ifndef XXH_HAS_INCLUDE +# ifdef __has_include +/* + * Not defined as XXH_HAS_INCLUDE(x) (function-like) because + * this causes segfaults in Apple Clang 4.2 (on Mac OS X 10.7 Lion) + */ +# define XXH_HAS_INCLUDE __has_include +# else +# define XXH_HAS_INCLUDE(x) 0 +# endif +#endif + +#if defined(__GNUC__) || defined(__clang__) +# if defined(__ARM_FEATURE_SVE) +# include +# endif +# if defined(__ARM_NEON__) || defined(__ARM_NEON) \ + || (defined(_M_ARM) && _M_ARM >= 7) \ + || defined(_M_ARM64) || defined(_M_ARM64EC) \ + || (defined(__wasm_simd128__) && XXH_HAS_INCLUDE()) /* WASM SIMD128 via SIMDe */ +# define inline __inline__ /* circumvent a clang bug */ +# include +# undef inline +# elif defined(__AVX2__) +# include +# elif defined(__SSE2__) +# include +# endif +#endif + +#if defined(_MSC_VER) +# include +#endif + +/* + * One goal of XXH3 is to make it fast on both 32-bit and 64-bit, while + * remaining a true 64-bit/128-bit hash function. + * + * This is done by prioritizing a subset of 64-bit operations that can be + * emulated without too many steps on the average 32-bit machine. + * + * For example, these two lines seem similar, and run equally fast on 64-bit: + * + * xxh_u64 x; + * x ^= (x >> 47); // good + * x ^= (x >> 13); // bad + * + * However, to a 32-bit machine, there is a major difference. + * + * x ^= (x >> 47) looks like this: + * + * x.lo ^= (x.hi >> (47 - 32)); + * + * while x ^= (x >> 13) looks like this: + * + * // note: funnel shifts are not usually cheap. + * x.lo ^= (x.lo >> 13) | (x.hi << (32 - 13)); + * x.hi ^= (x.hi >> 13); + * + * The first one is significantly faster than the second, simply because the + * shift is larger than 32. This means: + * - All the bits we need are in the upper 32 bits, so we can ignore the lower + * 32 bits in the shift. + * - The shift result will always fit in the lower 32 bits, and therefore, + * we can ignore the upper 32 bits in the xor. + * + * Thanks to this optimization, XXH3 only requires these features to be efficient: + * + * - Usable unaligned access + * - A 32-bit or 64-bit ALU + * - If 32-bit, a decent ADC instruction + * - A 32 or 64-bit multiply with a 64-bit result + * - For the 128-bit variant, a decent byteswap helps short inputs. + * + * The first two are already required by XXH32, and almost all 32-bit and 64-bit + * platforms which can run XXH32 can run XXH3 efficiently. + * + * Thumb-1, the classic 16-bit only subset of ARM's instruction set, is one + * notable exception. + * + * First of all, Thumb-1 lacks support for the UMULL instruction which + * performs the important long multiply. This means numerous __aeabi_lmul + * calls. + * + * Second of all, the 8 functional registers are just not enough. + * Setup for __aeabi_lmul, byteshift loads, pointers, and all arithmetic need + * Lo registers, and this shuffling results in thousands more MOVs than A32. + * + * A32 and T32 don't have this limitation. They can access all 14 registers, + * do a 32->64 multiply with UMULL, and the flexible operand allowing free + * shifts is helpful, too. + * + * Therefore, we do a quick sanity check. + * + * If compiling Thumb-1 for a target which supports ARM instructions, we will + * emit a warning, as it is not a "sane" platform to compile for. + * + * Usually, if this happens, it is because of an accident and you probably need + * to specify -march, as you likely meant to compile for a newer architecture. + * + * Credit: large sections of the vectorial and asm source code paths + * have been contributed by @easyaspi314 + */ +#if defined(__thumb__) && !defined(__thumb2__) && defined(__ARM_ARCH_ISA_ARM) +# warning "XXH3 is highly inefficient without ARM or Thumb-2." +#endif + +/* ========================================== + * Vectorization detection + * ========================================== */ + +#ifdef XXH_DOXYGEN +/*! + * @ingroup tuning + * @brief Overrides the vectorization implementation chosen for XXH3. + * + * Can be defined to 0 to disable SIMD or any of the values mentioned in + * @ref XXH_VECTOR_TYPE. + * + * If this is not defined, it uses predefined macros to determine the best + * implementation. + */ +# define XXH_VECTOR XXH_SCALAR +/*! + * @ingroup tuning + * @brief Possible values for @ref XXH_VECTOR. + * + * Note that these are actually implemented as macros. + * + * If this is not defined, it is detected automatically. + * internal macro XXH_X86DISPATCH overrides this. + */ +enum XXH_VECTOR_TYPE /* fake enum */ { + XXH_SCALAR = 0, /*!< Portable scalar version */ + XXH_SSE2 = 1, /*!< + * SSE2 for Pentium 4, Opteron, all x86_64. + * + * @note SSE2 is also guaranteed on Windows 10, macOS, and + * Android x86. + */ + XXH_AVX2 = 2, /*!< AVX2 for Haswell and Bulldozer */ + XXH_AVX512 = 3, /*!< AVX512 for Skylake and Icelake */ + XXH_NEON = 4, /*!< + * NEON for most ARMv7-A, all AArch64, and WASM SIMD128 + * via the SIMDeverywhere polyfill provided with the + * Emscripten SDK. + */ + XXH_VSX = 5, /*!< VSX and ZVector for POWER8/z13 (64-bit) */ + XXH_SVE = 6, /*!< SVE for some ARMv8-A and ARMv9-A */ +}; +/*! + * @ingroup tuning + * @brief Selects the minimum alignment for XXH3's accumulators. + * + * When using SIMD, this should match the alignment required for said vector + * type, so, for example, 32 for AVX2. + * + * Default: Auto detected. + */ +# define XXH_ACC_ALIGN 8 +#endif + +/* Actual definition */ +#ifndef XXH_DOXYGEN +# define XXH_SCALAR 0 +# define XXH_SSE2 1 +# define XXH_AVX2 2 +# define XXH_AVX512 3 +# define XXH_NEON 4 +# define XXH_VSX 5 +# define XXH_SVE 6 +#endif + +#ifndef XXH_VECTOR /* can be defined on command line */ +# if defined(__ARM_FEATURE_SVE) +# define XXH_VECTOR XXH_SVE +# elif ( \ + defined(__ARM_NEON__) || defined(__ARM_NEON) /* gcc */ \ + || defined(_M_ARM) || defined(_M_ARM64) || defined(_M_ARM64EC) /* msvc */ \ + || (defined(__wasm_simd128__) && XXH_HAS_INCLUDE()) /* wasm simd128 via SIMDe */ \ + ) && ( \ + defined(_WIN32) || defined(__LITTLE_ENDIAN__) /* little endian only */ \ + || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) \ + ) +# define XXH_VECTOR XXH_NEON +# elif defined(__AVX512F__) +# define XXH_VECTOR XXH_AVX512 +# elif defined(__AVX2__) +# define XXH_VECTOR XXH_AVX2 +# elif defined(__SSE2__) || defined(_M_AMD64) || defined(_M_X64) || (defined(_M_IX86_FP) && (_M_IX86_FP == 2)) +# define XXH_VECTOR XXH_SSE2 +# elif (defined(__PPC64__) && defined(__POWER8_VECTOR__)) \ + || (defined(__s390x__) && defined(__VEC__)) \ + && defined(__GNUC__) /* TODO: IBM XL */ +# define XXH_VECTOR XXH_VSX +# else +# define XXH_VECTOR XXH_SCALAR +# endif +#endif + +/* __ARM_FEATURE_SVE is only supported by GCC & Clang. */ +#if (XXH_VECTOR == XXH_SVE) && !defined(__ARM_FEATURE_SVE) +# ifdef _MSC_VER +# pragma warning(once : 4606) +# else +# warning "__ARM_FEATURE_SVE isn't supported. Use SCALAR instead." +# endif +# undef XXH_VECTOR +# define XXH_VECTOR XXH_SCALAR +#endif + +/* + * Controls the alignment of the accumulator, + * for compatibility with aligned vector loads, which are usually faster. + */ +#ifndef XXH_ACC_ALIGN +# if defined(XXH_X86DISPATCH) +# define XXH_ACC_ALIGN 64 /* for compatibility with avx512 */ +# elif XXH_VECTOR == XXH_SCALAR /* scalar */ +# define XXH_ACC_ALIGN 8 +# elif XXH_VECTOR == XXH_SSE2 /* sse2 */ +# define XXH_ACC_ALIGN 16 +# elif XXH_VECTOR == XXH_AVX2 /* avx2 */ +# define XXH_ACC_ALIGN 32 +# elif XXH_VECTOR == XXH_NEON /* neon */ +# define XXH_ACC_ALIGN 16 +# elif XXH_VECTOR == XXH_VSX /* vsx */ +# define XXH_ACC_ALIGN 16 +# elif XXH_VECTOR == XXH_AVX512 /* avx512 */ +# define XXH_ACC_ALIGN 64 +# elif XXH_VECTOR == XXH_SVE /* sve */ +# define XXH_ACC_ALIGN 64 +# endif +#endif + +#if defined(XXH_X86DISPATCH) || XXH_VECTOR == XXH_SSE2 \ + || XXH_VECTOR == XXH_AVX2 || XXH_VECTOR == XXH_AVX512 +# define XXH_SEC_ALIGN XXH_ACC_ALIGN +#elif XXH_VECTOR == XXH_SVE +# define XXH_SEC_ALIGN XXH_ACC_ALIGN +#else +# define XXH_SEC_ALIGN 8 +#endif + +#if defined(__GNUC__) || defined(__clang__) +# define XXH_ALIASING __attribute__((may_alias)) +#else +# define XXH_ALIASING /* nothing */ +#endif + +/* + * UGLY HACK: + * GCC usually generates the best code with -O3 for xxHash. + * + * However, when targeting AVX2, it is overzealous in its unrolling resulting + * in code roughly 3/4 the speed of Clang. + * + * There are other issues, such as GCC splitting _mm256_loadu_si256 into + * _mm_loadu_si128 + _mm256_inserti128_si256. This is an optimization which + * only applies to Sandy and Ivy Bridge... which don't even support AVX2. + * + * That is why when compiling the AVX2 version, it is recommended to use either + * -O2 -mavx2 -march=haswell + * or + * -O2 -mavx2 -mno-avx256-split-unaligned-load + * for decent performance, or to use Clang instead. + * + * Fortunately, we can control the first one with a pragma that forces GCC into + * -O2, but the other one we can't control without "failed to inline always + * inline function due to target mismatch" warnings. + */ +#if XXH_VECTOR == XXH_AVX2 /* AVX2 */ \ + && defined(__GNUC__) && !defined(__clang__) /* GCC, not Clang */ \ + && defined(__OPTIMIZE__) && XXH_SIZE_OPT <= 0 /* respect -O0 and -Os */ +# pragma GCC push_options +# pragma GCC optimize("-O2") +#endif + +#if XXH_VECTOR == XXH_NEON + +/* + * UGLY HACK: While AArch64 GCC on Linux does not seem to care, on macOS, GCC -O3 + * optimizes out the entire hashLong loop because of the aliasing violation. + * + * However, GCC is also inefficient at load-store optimization with vld1q/vst1q, + * so the only option is to mark it as aliasing. + */ +typedef uint64x2_t xxh_aliasing_uint64x2_t XXH_ALIASING; + +/*! + * @internal + * @brief `vld1q_u64` but faster and alignment-safe. + * + * On AArch64, unaligned access is always safe, but on ARMv7-a, it is only + * *conditionally* safe (`vld1` has an alignment bit like `movdq[ua]` in x86). + * + * GCC for AArch64 sees `vld1q_u8` as an intrinsic instead of a load, so it + * prohibits load-store optimizations. Therefore, a direct dereference is used. + * + * Otherwise, `vld1q_u8` is used with `vreinterpretq_u8_u64` to do a safe + * unaligned load. + */ +#if defined(__aarch64__) && defined(__GNUC__) && !defined(__clang__) +XXH_FORCE_INLINE uint64x2_t XXH_vld1q_u64(void const* ptr) /* silence -Wcast-align */ +{ + return *(xxh_aliasing_uint64x2_t const *)ptr; +} +#else +XXH_FORCE_INLINE uint64x2_t XXH_vld1q_u64(void const* ptr) +{ + return vreinterpretq_u64_u8(vld1q_u8((uint8_t const*)ptr)); +} +#endif + +/*! + * @internal + * @brief `vmlal_u32` on low and high halves of a vector. + * + * This is a workaround for AArch64 GCC < 11 which implemented arm_neon.h with + * inline assembly and were therefore incapable of merging the `vget_{low, high}_u32` + * with `vmlal_u32`. + */ +#if defined(__aarch64__) && defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 11 +XXH_FORCE_INLINE uint64x2_t +XXH_vmlal_low_u32(uint64x2_t acc, uint32x4_t lhs, uint32x4_t rhs) +{ + /* Inline assembly is the only way */ + __asm__("umlal %0.2d, %1.2s, %2.2s" : "+w" (acc) : "w" (lhs), "w" (rhs)); + return acc; +} +XXH_FORCE_INLINE uint64x2_t +XXH_vmlal_high_u32(uint64x2_t acc, uint32x4_t lhs, uint32x4_t rhs) +{ + /* This intrinsic works as expected */ + return vmlal_high_u32(acc, lhs, rhs); +} +#else +/* Portable intrinsic versions */ +XXH_FORCE_INLINE uint64x2_t +XXH_vmlal_low_u32(uint64x2_t acc, uint32x4_t lhs, uint32x4_t rhs) +{ + return vmlal_u32(acc, vget_low_u32(lhs), vget_low_u32(rhs)); +} +/*! @copydoc XXH_vmlal_low_u32 + * Assume the compiler converts this to vmlal_high_u32 on aarch64 */ +XXH_FORCE_INLINE uint64x2_t +XXH_vmlal_high_u32(uint64x2_t acc, uint32x4_t lhs, uint32x4_t rhs) +{ + return vmlal_u32(acc, vget_high_u32(lhs), vget_high_u32(rhs)); +} +#endif + +/*! + * @ingroup tuning + * @brief Controls the NEON to scalar ratio for XXH3 + * + * This can be set to 2, 4, 6, or 8. + * + * ARM Cortex CPUs are _very_ sensitive to how their pipelines are used. + * + * For example, the Cortex-A73 can dispatch 3 micro-ops per cycle, but only 2 of those + * can be NEON. If you are only using NEON instructions, you are only using 2/3 of the CPU + * bandwidth. + * + * This is even more noticeable on the more advanced cores like the Cortex-A76 which + * can dispatch 8 micro-ops per cycle, but still only 2 NEON micro-ops at once. + * + * Therefore, to make the most out of the pipeline, it is beneficial to run 6 NEON lanes + * and 2 scalar lanes, which is chosen by default. + * + * This does not apply to Apple processors or 32-bit processors, which run better with + * full NEON. These will default to 8. Additionally, size-optimized builds run 8 lanes. + * + * This change benefits CPUs with large micro-op buffers without negatively affecting + * most other CPUs: + * + * | Chipset | Dispatch type | NEON only | 6:2 hybrid | Diff. | + * |:----------------------|:--------------------|----------:|-----------:|------:| + * | Snapdragon 730 (A76) | 2 NEON/8 micro-ops | 8.8 GB/s | 10.1 GB/s | ~16% | + * | Snapdragon 835 (A73) | 2 NEON/3 micro-ops | 5.1 GB/s | 5.3 GB/s | ~5% | + * | Marvell PXA1928 (A53) | In-order dual-issue | 1.9 GB/s | 1.9 GB/s | 0% | + * | Apple M1 | 4 NEON/8 micro-ops | 37.3 GB/s | 36.1 GB/s | ~-3% | + * + * It also seems to fix some bad codegen on GCC, making it almost as fast as clang. + * + * When using WASM SIMD128, if this is 2 or 6, SIMDe will scalarize 2 of the lanes meaning + * it effectively becomes worse 4. + * + * @see XXH3_accumulate_512_neon() + */ +# ifndef XXH3_NEON_LANES +# if (defined(__aarch64__) || defined(__arm64__) || defined(_M_ARM64) || defined(_M_ARM64EC)) \ + && !defined(__APPLE__) && XXH_SIZE_OPT <= 0 +# define XXH3_NEON_LANES 6 +# else +# define XXH3_NEON_LANES XXH_ACC_NB +# endif +# endif +#endif /* XXH_VECTOR == XXH_NEON */ + +/* + * VSX and Z Vector helpers. + * + * This is very messy, and any pull requests to clean this up are welcome. + * + * There are a lot of problems with supporting VSX and s390x, due to + * inconsistent intrinsics, spotty coverage, and multiple endiannesses. + */ +#if XXH_VECTOR == XXH_VSX +/* Annoyingly, these headers _may_ define three macros: `bool`, `vector`, + * and `pixel`. This is a problem for obvious reasons. + * + * These keywords are unnecessary; the spec literally says they are + * equivalent to `__bool`, `__vector`, and `__pixel` and may be undef'd + * after including the header. + * + * We use pragma push_macro/pop_macro to keep the namespace clean. */ +# pragma push_macro("bool") +# pragma push_macro("vector") +# pragma push_macro("pixel") +/* silence potential macro redefined warnings */ +# undef bool +# undef vector +# undef pixel + +# if defined(__s390x__) +# include +# else +# include +# endif + +/* Restore the original macro values, if applicable. */ +# pragma pop_macro("pixel") +# pragma pop_macro("vector") +# pragma pop_macro("bool") + +typedef __vector unsigned long long xxh_u64x2; +typedef __vector unsigned char xxh_u8x16; +typedef __vector unsigned xxh_u32x4; + +/* + * UGLY HACK: Similar to aarch64 macOS GCC, s390x GCC has the same aliasing issue. + */ +typedef xxh_u64x2 xxh_aliasing_u64x2 XXH_ALIASING; + +# ifndef XXH_VSX_BE +# if defined(__BIG_ENDIAN__) \ + || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +# define XXH_VSX_BE 1 +# elif defined(__VEC_ELEMENT_REG_ORDER__) && __VEC_ELEMENT_REG_ORDER__ == __ORDER_BIG_ENDIAN__ +# warning "-maltivec=be is not recommended. Please use native endianness." +# define XXH_VSX_BE 1 +# else +# define XXH_VSX_BE 0 +# endif +# endif /* !defined(XXH_VSX_BE) */ + +# if XXH_VSX_BE +# if defined(__POWER9_VECTOR__) || (defined(__clang__) && defined(__s390x__)) +# define XXH_vec_revb vec_revb +# else +/*! + * A polyfill for POWER9's vec_revb(). + */ +XXH_FORCE_INLINE xxh_u64x2 XXH_vec_revb(xxh_u64x2 val) +{ + xxh_u8x16 const vByteSwap = { 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, + 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08 }; + return vec_perm(val, val, vByteSwap); +} +# endif +# endif /* XXH_VSX_BE */ + +/*! + * Performs an unaligned vector load and byte swaps it on big endian. + */ +XXH_FORCE_INLINE xxh_u64x2 XXH_vec_loadu(const void *ptr) +{ + xxh_u64x2 ret; + XXH_memcpy(&ret, ptr, sizeof(xxh_u64x2)); +# if XXH_VSX_BE + ret = XXH_vec_revb(ret); +# endif + return ret; +} + +/* + * vec_mulo and vec_mule are very problematic intrinsics on PowerPC + * + * These intrinsics weren't added until GCC 8, despite existing for a while, + * and they are endian dependent. Also, their meaning swap depending on version. + * */ +# if defined(__s390x__) + /* s390x is always big endian, no issue on this platform */ +# define XXH_vec_mulo vec_mulo +# define XXH_vec_mule vec_mule +# elif defined(__clang__) && XXH_HAS_BUILTIN(__builtin_altivec_vmuleuw) && !defined(__ibmxl__) +/* Clang has a better way to control this, we can just use the builtin which doesn't swap. */ + /* The IBM XL Compiler (which defined __clang__) only implements the vec_* operations */ +# define XXH_vec_mulo __builtin_altivec_vmulouw +# define XXH_vec_mule __builtin_altivec_vmuleuw +# else +/* gcc needs inline assembly */ +/* Adapted from https://github.com/google/highwayhash/blob/master/highwayhash/hh_vsx.h. */ +XXH_FORCE_INLINE xxh_u64x2 XXH_vec_mulo(xxh_u32x4 a, xxh_u32x4 b) +{ + xxh_u64x2 result; + __asm__("vmulouw %0, %1, %2" : "=v" (result) : "v" (a), "v" (b)); + return result; +} +XXH_FORCE_INLINE xxh_u64x2 XXH_vec_mule(xxh_u32x4 a, xxh_u32x4 b) +{ + xxh_u64x2 result; + __asm__("vmuleuw %0, %1, %2" : "=v" (result) : "v" (a), "v" (b)); + return result; +} +# endif /* XXH_vec_mulo, XXH_vec_mule */ +#endif /* XXH_VECTOR == XXH_VSX */ + +#if XXH_VECTOR == XXH_SVE +#define ACCRND(acc, offset) \ +do { \ + svuint64_t input_vec = svld1_u64(mask, xinput + offset); \ + svuint64_t secret_vec = svld1_u64(mask, xsecret + offset); \ + svuint64_t mixed = sveor_u64_x(mask, secret_vec, input_vec); \ + svuint64_t swapped = svtbl_u64(input_vec, kSwap); \ + svuint64_t mixed_lo = svextw_u64_x(mask, mixed); \ + svuint64_t mixed_hi = svlsr_n_u64_x(mask, mixed, 32); \ + svuint64_t mul = svmad_u64_x(mask, mixed_lo, mixed_hi, swapped); \ + acc = svadd_u64_x(mask, acc, mul); \ +} while (0) +#endif /* XXH_VECTOR == XXH_SVE */ + +/* prefetch + * can be disabled, by declaring XXH_NO_PREFETCH build macro */ +#if defined(XXH_NO_PREFETCH) +# define XXH_PREFETCH(ptr) (void)(ptr) /* disabled */ +#else +# if XXH_SIZE_OPT >= 1 +# define XXH_PREFETCH(ptr) (void)(ptr) +# elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) /* _mm_prefetch() not defined outside of x86/x64 */ +# include /* https://msdn.microsoft.com/fr-fr/library/84szxsww(v=vs.90).aspx */ +# define XXH_PREFETCH(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T0) +# elif defined(__GNUC__) && ( (__GNUC__ >= 4) || ( (__GNUC__ == 3) && (__GNUC_MINOR__ >= 1) ) ) +# define XXH_PREFETCH(ptr) __builtin_prefetch((ptr), 0 /* rw==read */, 3 /* locality */) +# else +# define XXH_PREFETCH(ptr) (void)(ptr) /* disabled */ +# endif +#endif /* XXH_NO_PREFETCH */ + + +/* ========================================== + * XXH3 default settings + * ========================================== */ + +#define XXH_SECRET_DEFAULT_SIZE 192 /* minimum XXH3_SECRET_SIZE_MIN */ + +#if (XXH_SECRET_DEFAULT_SIZE < XXH3_SECRET_SIZE_MIN) +# error "default keyset is not large enough" +#endif + +/*! Pseudorandom secret taken directly from FARSH. */ +XXH_ALIGN(64) static const xxh_u8 XXH3_kSecret[XXH_SECRET_DEFAULT_SIZE] = { + 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c, + 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb, 0x72, 0x40, 0xa4, 0xa4, 0xb7, 0xb3, 0x67, 0x1f, + 0xcb, 0x79, 0xe6, 0x4e, 0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0, 0x7d, 0xcc, 0xff, 0x72, 0x21, + 0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90, 0xe6, 0x81, 0x3a, 0x26, 0x4c, + 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb, 0x88, 0xd0, 0x65, 0x8b, 0x1b, 0x53, 0x2e, 0xa3, + 0x71, 0x64, 0x48, 0x97, 0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef, 0x46, 0xa9, 0xde, 0xac, 0xd8, + 0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb, 0xc7, 0xc7, 0x0b, 0x4f, 0x1d, + 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31, 0xc8, 0x9f, 0x7e, 0xc9, 0xd9, 0x78, 0x73, 0x64, + 0xea, 0xc5, 0xac, 0x83, 0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0, 0xff, 0xfa, 0x13, 0x63, 0xeb, + 0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55, 0x26, 0x29, 0xd4, 0x68, 0x9e, + 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce, + 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e, +}; + +static const xxh_u64 PRIME_MX1 = 0x165667919E3779F9ULL; /*!< 0b0001011001010110011001111001000110011110001101110111100111111001 */ +static const xxh_u64 PRIME_MX2 = 0x9FB21C651E98DF25ULL; /*!< 0b1001111110110010000111000110010100011110100110001101111100100101 */ + +#ifdef XXH_OLD_NAMES +# define kSecret XXH3_kSecret +#endif + +#ifdef XXH_DOXYGEN +/*! + * @brief Calculates a 32-bit to 64-bit long multiply. + * + * Implemented as a macro. + * + * Wraps `__emulu` on MSVC x86 because it tends to call `__allmul` when it doesn't + * need to (but it shouldn't need to anyways, it is about 7 instructions to do + * a 64x64 multiply...). Since we know that this will _always_ emit `MULL`, we + * use that instead of the normal method. + * + * If you are compiling for platforms like Thumb-1 and don't have a better option, + * you may also want to write your own long multiply routine here. + * + * @param x, y Numbers to be multiplied + * @return 64-bit product of the low 32 bits of @p x and @p y. + */ +XXH_FORCE_INLINE xxh_u64 +XXH_mult32to64(xxh_u64 x, xxh_u64 y) +{ + return (x & 0xFFFFFFFF) * (y & 0xFFFFFFFF); +} +#elif defined(_MSC_VER) && defined(_M_IX86) +# define XXH_mult32to64(x, y) __emulu((unsigned)(x), (unsigned)(y)) +#else +/* + * Downcast + upcast is usually better than masking on older compilers like + * GCC 4.2 (especially 32-bit ones), all without affecting newer compilers. + * + * The other method, (x & 0xFFFFFFFF) * (y & 0xFFFFFFFF), will AND both operands + * and perform a full 64x64 multiply -- entirely redundant on 32-bit. + */ +# define XXH_mult32to64(x, y) ((xxh_u64)(xxh_u32)(x) * (xxh_u64)(xxh_u32)(y)) +#endif + +/*! + * @brief Calculates a 64->128-bit long multiply. + * + * Uses `__uint128_t` and `_umul128` if available, otherwise uses a scalar + * version. + * + * @param lhs , rhs The 64-bit integers to be multiplied + * @return The 128-bit result represented in an @ref XXH128_hash_t. + */ +static XXH128_hash_t +XXH_mult64to128(xxh_u64 lhs, xxh_u64 rhs) +{ + /* + * GCC/Clang __uint128_t method. + * + * On most 64-bit targets, GCC and Clang define a __uint128_t type. + * This is usually the best way as it usually uses a native long 64-bit + * multiply, such as MULQ on x86_64 or MUL + UMULH on aarch64. + * + * Usually. + * + * Despite being a 32-bit platform, Clang (and emscripten) define this type + * despite not having the arithmetic for it. This results in a laggy + * compiler builtin call which calculates a full 128-bit multiply. + * In that case it is best to use the portable one. + * https://github.com/Cyan4973/xxHash/issues/211#issuecomment-515575677 + */ +#if (defined(__GNUC__) || defined(__clang__)) && !defined(__wasm__) \ + && defined(__SIZEOF_INT128__) \ + || (defined(_INTEGRAL_MAX_BITS) && _INTEGRAL_MAX_BITS >= 128) + + __uint128_t const product = (__uint128_t)lhs * (__uint128_t)rhs; + XXH128_hash_t r128; + r128.low64 = (xxh_u64)(product); + r128.high64 = (xxh_u64)(product >> 64); + return r128; + + /* + * MSVC for x64's _umul128 method. + * + * xxh_u64 _umul128(xxh_u64 Multiplier, xxh_u64 Multiplicand, xxh_u64 *HighProduct); + * + * This compiles to single operand MUL on x64. + */ +#elif (defined(_M_X64) || defined(_M_IA64)) && !defined(_M_ARM64EC) + +#ifndef _MSC_VER +# pragma intrinsic(_umul128) +#endif + xxh_u64 product_high; + xxh_u64 const product_low = _umul128(lhs, rhs, &product_high); + XXH128_hash_t r128; + r128.low64 = product_low; + r128.high64 = product_high; + return r128; + + /* + * MSVC for ARM64's __umulh method. + * + * This compiles to the same MUL + UMULH as GCC/Clang's __uint128_t method. + */ +#elif defined(_M_ARM64) || defined(_M_ARM64EC) + +#ifndef _MSC_VER +# pragma intrinsic(__umulh) +#endif + XXH128_hash_t r128; + r128.low64 = lhs * rhs; + r128.high64 = __umulh(lhs, rhs); + return r128; + +#else + /* + * Portable scalar method. Optimized for 32-bit and 64-bit ALUs. + * + * This is a fast and simple grade school multiply, which is shown below + * with base 10 arithmetic instead of base 0x100000000. + * + * 9 3 // D2 lhs = 93 + * x 7 5 // D2 rhs = 75 + * ---------- + * 1 5 // D2 lo_lo = (93 % 10) * (75 % 10) = 15 + * 4 5 | // D2 hi_lo = (93 / 10) * (75 % 10) = 45 + * 2 1 | // D2 lo_hi = (93 % 10) * (75 / 10) = 21 + * + 6 3 | | // D2 hi_hi = (93 / 10) * (75 / 10) = 63 + * --------- + * 2 7 | // D2 cross = (15 / 10) + (45 % 10) + 21 = 27 + * + 6 7 | | // D2 upper = (27 / 10) + (45 / 10) + 63 = 67 + * --------- + * 6 9 7 5 // D4 res = (27 * 10) + (15 % 10) + (67 * 100) = 6975 + * + * The reasons for adding the products like this are: + * 1. It avoids manual carry tracking. Just like how + * (9 * 9) + 9 + 9 = 99, the same applies with this for UINT64_MAX. + * This avoids a lot of complexity. + * + * 2. It hints for, and on Clang, compiles to, the powerful UMAAL + * instruction available in ARM's Digital Signal Processing extension + * in 32-bit ARMv6 and later, which is shown below: + * + * void UMAAL(xxh_u32 *RdLo, xxh_u32 *RdHi, xxh_u32 Rn, xxh_u32 Rm) + * { + * xxh_u64 product = (xxh_u64)*RdLo * (xxh_u64)*RdHi + Rn + Rm; + * *RdLo = (xxh_u32)(product & 0xFFFFFFFF); + * *RdHi = (xxh_u32)(product >> 32); + * } + * + * This instruction was designed for efficient long multiplication, and + * allows this to be calculated in only 4 instructions at speeds + * comparable to some 64-bit ALUs. + * + * 3. It isn't terrible on other platforms. Usually this will be a couple + * of 32-bit ADD/ADCs. + */ + + /* First calculate all of the cross products. */ + xxh_u64 const lo_lo = XXH_mult32to64(lhs & 0xFFFFFFFF, rhs & 0xFFFFFFFF); + xxh_u64 const hi_lo = XXH_mult32to64(lhs >> 32, rhs & 0xFFFFFFFF); + xxh_u64 const lo_hi = XXH_mult32to64(lhs & 0xFFFFFFFF, rhs >> 32); + xxh_u64 const hi_hi = XXH_mult32to64(lhs >> 32, rhs >> 32); + + /* Now add the products together. These will never overflow. */ + xxh_u64 const cross = (lo_lo >> 32) + (hi_lo & 0xFFFFFFFF) + lo_hi; + xxh_u64 const upper = (hi_lo >> 32) + (cross >> 32) + hi_hi; + xxh_u64 const lower = (cross << 32) | (lo_lo & 0xFFFFFFFF); + + XXH128_hash_t r128; + r128.low64 = lower; + r128.high64 = upper; + return r128; +#endif +} + +/*! + * @brief Calculates a 64-bit to 128-bit multiply, then XOR folds it. + * + * The reason for the separate function is to prevent passing too many structs + * around by value. This will hopefully inline the multiply, but we don't force it. + * + * @param lhs , rhs The 64-bit integers to multiply + * @return The low 64 bits of the product XOR'd by the high 64 bits. + * @see XXH_mult64to128() + */ +static xxh_u64 +XXH3_mul128_fold64(xxh_u64 lhs, xxh_u64 rhs) +{ + XXH128_hash_t product = XXH_mult64to128(lhs, rhs); + return product.low64 ^ product.high64; +} + +/*! Seems to produce slightly better code on GCC for some reason. */ +XXH_FORCE_INLINE XXH_CONSTF xxh_u64 XXH_xorshift64(xxh_u64 v64, int shift) +{ + XXH_ASSERT(0 <= shift && shift < 64); + return v64 ^ (v64 >> shift); +} + +/* + * This is a fast avalanche stage, + * suitable when input bits are already partially mixed + */ +static XXH64_hash_t XXH3_avalanche(xxh_u64 h64) +{ + h64 = XXH_xorshift64(h64, 37); + h64 *= PRIME_MX1; + h64 = XXH_xorshift64(h64, 32); + return h64; +} + +/* + * This is a stronger avalanche, + * inspired by Pelle Evensen's rrmxmx + * preferable when input has not been previously mixed + */ +static XXH64_hash_t XXH3_rrmxmx(xxh_u64 h64, xxh_u64 len) +{ + /* this mix is inspired by Pelle Evensen's rrmxmx */ + h64 ^= XXH_rotl64(h64, 49) ^ XXH_rotl64(h64, 24); + h64 *= PRIME_MX2; + h64 ^= (h64 >> 35) + len ; + h64 *= PRIME_MX2; + return XXH_xorshift64(h64, 28); +} + + +/* ========================================== + * Short keys + * ========================================== + * One of the shortcomings of XXH32 and XXH64 was that their performance was + * sub-optimal on short lengths. It used an iterative algorithm which strongly + * favored lengths that were a multiple of 4 or 8. + * + * Instead of iterating over individual inputs, we use a set of single shot + * functions which piece together a range of lengths and operate in constant time. + * + * Additionally, the number of multiplies has been significantly reduced. This + * reduces latency, especially when emulating 64-bit multiplies on 32-bit. + * + * Depending on the platform, this may or may not be faster than XXH32, but it + * is almost guaranteed to be faster than XXH64. + */ + +/* + * At very short lengths, there isn't enough input to fully hide secrets, or use + * the entire secret. + * + * There is also only a limited amount of mixing we can do before significantly + * impacting performance. + * + * Therefore, we use different sections of the secret and always mix two secret + * samples with an XOR. This should have no effect on performance on the + * seedless or withSeed variants because everything _should_ be constant folded + * by modern compilers. + * + * The XOR mixing hides individual parts of the secret and increases entropy. + * + * This adds an extra layer of strength for custom secrets. + */ +XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t +XXH3_len_1to3_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(1 <= len && len <= 3); + XXH_ASSERT(secret != NULL); + /* + * len = 1: combined = { input[0], 0x01, input[0], input[0] } + * len = 2: combined = { input[1], 0x02, input[0], input[1] } + * len = 3: combined = { input[2], 0x03, input[0], input[1] } + */ + { xxh_u8 const c1 = input[0]; + xxh_u8 const c2 = input[len >> 1]; + xxh_u8 const c3 = input[len - 1]; + xxh_u32 const combined = ((xxh_u32)c1 << 16) | ((xxh_u32)c2 << 24) + | ((xxh_u32)c3 << 0) | ((xxh_u32)len << 8); + xxh_u64 const bitflip = (XXH_readLE32(secret) ^ XXH_readLE32(secret+4)) + seed; + xxh_u64 const keyed = (xxh_u64)combined ^ bitflip; + return XXH64_avalanche(keyed); + } +} + +XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t +XXH3_len_4to8_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(secret != NULL); + XXH_ASSERT(4 <= len && len <= 8); + seed ^= (xxh_u64)XXH_swap32((xxh_u32)seed) << 32; + { xxh_u32 const input1 = XXH_readLE32(input); + xxh_u32 const input2 = XXH_readLE32(input + len - 4); + xxh_u64 const bitflip = (XXH_readLE64(secret+8) ^ XXH_readLE64(secret+16)) - seed; + xxh_u64 const input64 = input2 + (((xxh_u64)input1) << 32); + xxh_u64 const keyed = input64 ^ bitflip; + return XXH3_rrmxmx(keyed, len); + } +} + +XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t +XXH3_len_9to16_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(secret != NULL); + XXH_ASSERT(9 <= len && len <= 16); + { xxh_u64 const bitflip1 = (XXH_readLE64(secret+24) ^ XXH_readLE64(secret+32)) + seed; + xxh_u64 const bitflip2 = (XXH_readLE64(secret+40) ^ XXH_readLE64(secret+48)) - seed; + xxh_u64 const input_lo = XXH_readLE64(input) ^ bitflip1; + xxh_u64 const input_hi = XXH_readLE64(input + len - 8) ^ bitflip2; + xxh_u64 const acc = len + + XXH_swap64(input_lo) + input_hi + + XXH3_mul128_fold64(input_lo, input_hi); + return XXH3_avalanche(acc); + } +} + +XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t +XXH3_len_0to16_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(len <= 16); + { if (XXH_likely(len > 8)) return XXH3_len_9to16_64b(input, len, secret, seed); + if (XXH_likely(len >= 4)) return XXH3_len_4to8_64b(input, len, secret, seed); + if (len) return XXH3_len_1to3_64b(input, len, secret, seed); + return XXH64_avalanche(seed ^ (XXH_readLE64(secret+56) ^ XXH_readLE64(secret+64))); + } +} + +/* + * DISCLAIMER: There are known *seed-dependent* multicollisions here due to + * multiplication by zero, affecting hashes of lengths 17 to 240. + * + * However, they are very unlikely. + * + * Keep this in mind when using the unseeded XXH3_64bits() variant: As with all + * unseeded non-cryptographic hashes, it does not attempt to defend itself + * against specially crafted inputs, only random inputs. + * + * Compared to classic UMAC where a 1 in 2^31 chance of 4 consecutive bytes + * cancelling out the secret is taken an arbitrary number of times (addressed + * in XXH3_accumulate_512), this collision is very unlikely with random inputs + * and/or proper seeding: + * + * This only has a 1 in 2^63 chance of 8 consecutive bytes cancelling out, in a + * function that is only called up to 16 times per hash with up to 240 bytes of + * input. + * + * This is not too bad for a non-cryptographic hash function, especially with + * only 64 bit outputs. + * + * The 128-bit variant (which trades some speed for strength) is NOT affected + * by this, although it is always a good idea to use a proper seed if you care + * about strength. + */ +XXH_FORCE_INLINE xxh_u64 XXH3_mix16B(const xxh_u8* XXH_RESTRICT input, + const xxh_u8* XXH_RESTRICT secret, xxh_u64 seed64) +{ +#if defined(__GNUC__) && !defined(__clang__) /* GCC, not Clang */ \ + && defined(__i386__) && defined(__SSE2__) /* x86 + SSE2 */ \ + && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable like XXH32 hack */ + /* + * UGLY HACK: + * GCC for x86 tends to autovectorize the 128-bit multiply, resulting in + * slower code. + * + * By forcing seed64 into a register, we disrupt the cost model and + * cause it to scalarize. See `XXH32_round()` + * + * FIXME: Clang's output is still _much_ faster -- On an AMD Ryzen 3600, + * XXH3_64bits @ len=240 runs at 4.6 GB/s with Clang 9, but 3.3 GB/s on + * GCC 9.2, despite both emitting scalar code. + * + * GCC generates much better scalar code than Clang for the rest of XXH3, + * which is why finding a more optimal codepath is an interest. + */ + XXH_COMPILER_GUARD(seed64); +#endif + { xxh_u64 const input_lo = XXH_readLE64(input); + xxh_u64 const input_hi = XXH_readLE64(input+8); + return XXH3_mul128_fold64( + input_lo ^ (XXH_readLE64(secret) + seed64), + input_hi ^ (XXH_readLE64(secret+8) - seed64) + ); + } +} + +/* For mid range keys, XXH3 uses a Mum-hash variant. */ +XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t +XXH3_len_17to128_64b(const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH64_hash_t seed) +{ + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize; + XXH_ASSERT(16 < len && len <= 128); + + { xxh_u64 acc = len * XXH_PRIME64_1; +#if XXH_SIZE_OPT >= 1 + /* Smaller and cleaner, but slightly slower. */ + unsigned int i = (unsigned int)(len - 1) / 32; + do { + acc += XXH3_mix16B(input+16 * i, secret+32*i, seed); + acc += XXH3_mix16B(input+len-16*(i+1), secret+32*i+16, seed); + } while (i-- != 0); +#else + if (len > 32) { + if (len > 64) { + if (len > 96) { + acc += XXH3_mix16B(input+48, secret+96, seed); + acc += XXH3_mix16B(input+len-64, secret+112, seed); + } + acc += XXH3_mix16B(input+32, secret+64, seed); + acc += XXH3_mix16B(input+len-48, secret+80, seed); + } + acc += XXH3_mix16B(input+16, secret+32, seed); + acc += XXH3_mix16B(input+len-32, secret+48, seed); + } + acc += XXH3_mix16B(input+0, secret+0, seed); + acc += XXH3_mix16B(input+len-16, secret+16, seed); +#endif + return XXH3_avalanche(acc); + } +} + +/*! + * @brief Maximum size of "short" key in bytes. + */ +#define XXH3_MIDSIZE_MAX 240 + +XXH_NO_INLINE XXH_PUREF XXH64_hash_t +XXH3_len_129to240_64b(const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH64_hash_t seed) +{ + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize; + XXH_ASSERT(128 < len && len <= XXH3_MIDSIZE_MAX); + + #define XXH3_MIDSIZE_STARTOFFSET 3 + #define XXH3_MIDSIZE_LASTOFFSET 17 + + { xxh_u64 acc = len * XXH_PRIME64_1; + xxh_u64 acc_end; + unsigned int const nbRounds = (unsigned int)len / 16; + unsigned int i; + XXH_ASSERT(128 < len && len <= XXH3_MIDSIZE_MAX); + for (i=0; i<8; i++) { + acc += XXH3_mix16B(input+(16*i), secret+(16*i), seed); + } + /* last bytes */ + acc_end = XXH3_mix16B(input + len - 16, secret + XXH3_SECRET_SIZE_MIN - XXH3_MIDSIZE_LASTOFFSET, seed); + XXH_ASSERT(nbRounds >= 8); + acc = XXH3_avalanche(acc); +#if defined(__clang__) /* Clang */ \ + && (defined(__ARM_NEON) || defined(__ARM_NEON__)) /* NEON */ \ + && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable */ + /* + * UGLY HACK: + * Clang for ARMv7-A tries to vectorize this loop, similar to GCC x86. + * In everywhere else, it uses scalar code. + * + * For 64->128-bit multiplies, even if the NEON was 100% optimal, it + * would still be slower than UMAAL (see XXH_mult64to128). + * + * Unfortunately, Clang doesn't handle the long multiplies properly and + * converts them to the nonexistent "vmulq_u64" intrinsic, which is then + * scalarized into an ugly mess of VMOV.32 instructions. + * + * This mess is difficult to avoid without turning autovectorization + * off completely, but they are usually relatively minor and/or not + * worth it to fix. + * + * This loop is the easiest to fix, as unlike XXH32, this pragma + * _actually works_ because it is a loop vectorization instead of an + * SLP vectorization. + */ + #pragma clang loop vectorize(disable) +#endif + for (i=8 ; i < nbRounds; i++) { + /* + * Prevents clang for unrolling the acc loop and interleaving with this one. + */ + XXH_COMPILER_GUARD(acc); + acc_end += XXH3_mix16B(input+(16*i), secret+(16*(i-8)) + XXH3_MIDSIZE_STARTOFFSET, seed); + } + return XXH3_avalanche(acc + acc_end); + } +} + + +/* ======= Long Keys ======= */ + +#define XXH_STRIPE_LEN 64 +#define XXH_SECRET_CONSUME_RATE 8 /* nb of secret bytes consumed at each accumulation */ +#define XXH_ACC_NB (XXH_STRIPE_LEN / sizeof(xxh_u64)) + +#ifdef XXH_OLD_NAMES +# define STRIPE_LEN XXH_STRIPE_LEN +# define ACC_NB XXH_ACC_NB +#endif + +#ifndef XXH_PREFETCH_DIST +# ifdef __clang__ +# define XXH_PREFETCH_DIST 320 +# else +# if (XXH_VECTOR == XXH_AVX512) +# define XXH_PREFETCH_DIST 512 +# else +# define XXH_PREFETCH_DIST 384 +# endif +# endif /* __clang__ */ +#endif /* XXH_PREFETCH_DIST */ + +/* + * These macros are to generate an XXH3_accumulate() function. + * The two arguments select the name suffix and target attribute. + * + * The name of this symbol is XXH3_accumulate_() and it calls + * XXH3_accumulate_512_(). + * + * It may be useful to hand implement this function if the compiler fails to + * optimize the inline function. + */ +#define XXH3_ACCUMULATE_TEMPLATE(name) \ +void \ +XXH3_accumulate_##name(xxh_u64* XXH_RESTRICT acc, \ + const xxh_u8* XXH_RESTRICT input, \ + const xxh_u8* XXH_RESTRICT secret, \ + size_t nbStripes) \ +{ \ + size_t n; \ + for (n = 0; n < nbStripes; n++ ) { \ + const xxh_u8* const in = input + n*XXH_STRIPE_LEN; \ + XXH_PREFETCH(in + XXH_PREFETCH_DIST); \ + XXH3_accumulate_512_##name( \ + acc, \ + in, \ + secret + n*XXH_SECRET_CONSUME_RATE); \ + } \ +} + + +XXH_FORCE_INLINE void XXH_writeLE64(void* dst, xxh_u64 v64) +{ + if (!XXH_CPU_LITTLE_ENDIAN) v64 = XXH_swap64(v64); + XXH_memcpy(dst, &v64, sizeof(v64)); +} + +/* Several intrinsic functions below are supposed to accept __int64 as argument, + * as documented in https://software.intel.com/sites/landingpage/IntrinsicsGuide/ . + * However, several environments do not define __int64 type, + * requiring a workaround. + */ +#if !defined (__VMS) \ + && (defined (__cplusplus) \ + || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) ) + typedef int64_t xxh_i64; +#else + /* the following type must have a width of 64-bit */ + typedef long long xxh_i64; +#endif + + +/* + * XXH3_accumulate_512 is the tightest loop for long inputs, and it is the most optimized. + * + * It is a hardened version of UMAC, based off of FARSH's implementation. + * + * This was chosen because it adapts quite well to 32-bit, 64-bit, and SIMD + * implementations, and it is ridiculously fast. + * + * We harden it by mixing the original input to the accumulators as well as the product. + * + * This means that in the (relatively likely) case of a multiply by zero, the + * original input is preserved. + * + * On 128-bit inputs, we swap 64-bit pairs when we add the input to improve + * cross-pollination, as otherwise the upper and lower halves would be + * essentially independent. + * + * This doesn't matter on 64-bit hashes since they all get merged together in + * the end, so we skip the extra step. + * + * Both XXH3_64bits and XXH3_128bits use this subroutine. + */ + +#if (XXH_VECTOR == XXH_AVX512) \ + || (defined(XXH_DISPATCH_AVX512) && XXH_DISPATCH_AVX512 != 0) + +#ifndef XXH_TARGET_AVX512 +# define XXH_TARGET_AVX512 /* disable attribute target */ +#endif + +XXH_FORCE_INLINE XXH_TARGET_AVX512 void +XXH3_accumulate_512_avx512(void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + __m512i* const xacc = (__m512i *) acc; + XXH_ASSERT((((size_t)acc) & 63) == 0); + XXH_STATIC_ASSERT(XXH_STRIPE_LEN == sizeof(__m512i)); + + { + /* data_vec = input[0]; */ + __m512i const data_vec = _mm512_loadu_si512 (input); + /* key_vec = secret[0]; */ + __m512i const key_vec = _mm512_loadu_si512 (secret); + /* data_key = data_vec ^ key_vec; */ + __m512i const data_key = _mm512_xor_si512 (data_vec, key_vec); + /* data_key_lo = data_key >> 32; */ + __m512i const data_key_lo = _mm512_srli_epi64 (data_key, 32); + /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */ + __m512i const product = _mm512_mul_epu32 (data_key, data_key_lo); + /* xacc[0] += swap(data_vec); */ + __m512i const data_swap = _mm512_shuffle_epi32(data_vec, (_MM_PERM_ENUM)_MM_SHUFFLE(1, 0, 3, 2)); + __m512i const sum = _mm512_add_epi64(*xacc, data_swap); + /* xacc[0] += product; */ + *xacc = _mm512_add_epi64(product, sum); + } +} +XXH_FORCE_INLINE XXH_TARGET_AVX512 XXH3_ACCUMULATE_TEMPLATE(avx512) + +/* + * XXH3_scrambleAcc: Scrambles the accumulators to improve mixing. + * + * Multiplication isn't perfect, as explained by Google in HighwayHash: + * + * // Multiplication mixes/scrambles bytes 0-7 of the 64-bit result to + * // varying degrees. In descending order of goodness, bytes + * // 3 4 2 5 1 6 0 7 have quality 228 224 164 160 100 96 36 32. + * // As expected, the upper and lower bytes are much worse. + * + * Source: https://github.com/google/highwayhash/blob/0aaf66b/highwayhash/hh_avx2.h#L291 + * + * Since our algorithm uses a pseudorandom secret to add some variance into the + * mix, we don't need to (or want to) mix as often or as much as HighwayHash does. + * + * This isn't as tight as XXH3_accumulate, but still written in SIMD to avoid + * extraction. + * + * Both XXH3_64bits and XXH3_128bits use this subroutine. + */ + +XXH_FORCE_INLINE XXH_TARGET_AVX512 void +XXH3_scrambleAcc_avx512(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 63) == 0); + XXH_STATIC_ASSERT(XXH_STRIPE_LEN == sizeof(__m512i)); + { __m512i* const xacc = (__m512i*) acc; + const __m512i prime32 = _mm512_set1_epi32((int)XXH_PRIME32_1); + + /* xacc[0] ^= (xacc[0] >> 47) */ + __m512i const acc_vec = *xacc; + __m512i const shifted = _mm512_srli_epi64 (acc_vec, 47); + /* xacc[0] ^= secret; */ + __m512i const key_vec = _mm512_loadu_si512 (secret); + __m512i const data_key = _mm512_ternarylogic_epi32(key_vec, acc_vec, shifted, 0x96 /* key_vec ^ acc_vec ^ shifted */); + + /* xacc[0] *= XXH_PRIME32_1; */ + __m512i const data_key_hi = _mm512_srli_epi64 (data_key, 32); + __m512i const prod_lo = _mm512_mul_epu32 (data_key, prime32); + __m512i const prod_hi = _mm512_mul_epu32 (data_key_hi, prime32); + *xacc = _mm512_add_epi64(prod_lo, _mm512_slli_epi64(prod_hi, 32)); + } +} + +XXH_FORCE_INLINE XXH_TARGET_AVX512 void +XXH3_initCustomSecret_avx512(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 63) == 0); + XXH_STATIC_ASSERT(XXH_SEC_ALIGN == 64); + XXH_ASSERT(((size_t)customSecret & 63) == 0); + (void)(&XXH_writeLE64); + { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / sizeof(__m512i); + __m512i const seed_pos = _mm512_set1_epi64((xxh_i64)seed64); + __m512i const seed = _mm512_mask_sub_epi64(seed_pos, 0xAA, _mm512_set1_epi8(0), seed_pos); + + const __m512i* const src = (const __m512i*) ((const void*) XXH3_kSecret); + __m512i* const dest = ( __m512i*) customSecret; + int i; + XXH_ASSERT(((size_t)src & 63) == 0); /* control alignment */ + XXH_ASSERT(((size_t)dest & 63) == 0); + for (i=0; i < nbRounds; ++i) { + dest[i] = _mm512_add_epi64(_mm512_load_si512(src + i), seed); + } } +} + +#endif + +#if (XXH_VECTOR == XXH_AVX2) \ + || (defined(XXH_DISPATCH_AVX2) && XXH_DISPATCH_AVX2 != 0) + +#ifndef XXH_TARGET_AVX2 +# define XXH_TARGET_AVX2 /* disable attribute target */ +#endif + +XXH_FORCE_INLINE XXH_TARGET_AVX2 void +XXH3_accumulate_512_avx2( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 31) == 0); + { __m256i* const xacc = (__m256i *) acc; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */ + const __m256i* const xinput = (const __m256i *) input; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */ + const __m256i* const xsecret = (const __m256i *) secret; + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(__m256i); i++) { + /* data_vec = xinput[i]; */ + __m256i const data_vec = _mm256_loadu_si256 (xinput+i); + /* key_vec = xsecret[i]; */ + __m256i const key_vec = _mm256_loadu_si256 (xsecret+i); + /* data_key = data_vec ^ key_vec; */ + __m256i const data_key = _mm256_xor_si256 (data_vec, key_vec); + /* data_key_lo = data_key >> 32; */ + __m256i const data_key_lo = _mm256_srli_epi64 (data_key, 32); + /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */ + __m256i const product = _mm256_mul_epu32 (data_key, data_key_lo); + /* xacc[i] += swap(data_vec); */ + __m256i const data_swap = _mm256_shuffle_epi32(data_vec, _MM_SHUFFLE(1, 0, 3, 2)); + __m256i const sum = _mm256_add_epi64(xacc[i], data_swap); + /* xacc[i] += product; */ + xacc[i] = _mm256_add_epi64(product, sum); + } } +} +XXH_FORCE_INLINE XXH_TARGET_AVX2 XXH3_ACCUMULATE_TEMPLATE(avx2) + +XXH_FORCE_INLINE XXH_TARGET_AVX2 void +XXH3_scrambleAcc_avx2(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 31) == 0); + { __m256i* const xacc = (__m256i*) acc; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */ + const __m256i* const xsecret = (const __m256i *) secret; + const __m256i prime32 = _mm256_set1_epi32((int)XXH_PRIME32_1); + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(__m256i); i++) { + /* xacc[i] ^= (xacc[i] >> 47) */ + __m256i const acc_vec = xacc[i]; + __m256i const shifted = _mm256_srli_epi64 (acc_vec, 47); + __m256i const data_vec = _mm256_xor_si256 (acc_vec, shifted); + /* xacc[i] ^= xsecret; */ + __m256i const key_vec = _mm256_loadu_si256 (xsecret+i); + __m256i const data_key = _mm256_xor_si256 (data_vec, key_vec); + + /* xacc[i] *= XXH_PRIME32_1; */ + __m256i const data_key_hi = _mm256_srli_epi64 (data_key, 32); + __m256i const prod_lo = _mm256_mul_epu32 (data_key, prime32); + __m256i const prod_hi = _mm256_mul_epu32 (data_key_hi, prime32); + xacc[i] = _mm256_add_epi64(prod_lo, _mm256_slli_epi64(prod_hi, 32)); + } + } +} + +XXH_FORCE_INLINE XXH_TARGET_AVX2 void XXH3_initCustomSecret_avx2(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 31) == 0); + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE / sizeof(__m256i)) == 6); + XXH_STATIC_ASSERT(XXH_SEC_ALIGN <= 64); + (void)(&XXH_writeLE64); + XXH_PREFETCH(customSecret); + { __m256i const seed = _mm256_set_epi64x((xxh_i64)(0U - seed64), (xxh_i64)seed64, (xxh_i64)(0U - seed64), (xxh_i64)seed64); + + const __m256i* const src = (const __m256i*) ((const void*) XXH3_kSecret); + __m256i* dest = ( __m256i*) customSecret; + +# if defined(__GNUC__) || defined(__clang__) + /* + * On GCC & Clang, marking 'dest' as modified will cause the compiler: + * - do not extract the secret from sse registers in the internal loop + * - use less common registers, and avoid pushing these reg into stack + */ + XXH_COMPILER_GUARD(dest); +# endif + XXH_ASSERT(((size_t)src & 31) == 0); /* control alignment */ + XXH_ASSERT(((size_t)dest & 31) == 0); + + /* GCC -O2 need unroll loop manually */ + dest[0] = _mm256_add_epi64(_mm256_load_si256(src+0), seed); + dest[1] = _mm256_add_epi64(_mm256_load_si256(src+1), seed); + dest[2] = _mm256_add_epi64(_mm256_load_si256(src+2), seed); + dest[3] = _mm256_add_epi64(_mm256_load_si256(src+3), seed); + dest[4] = _mm256_add_epi64(_mm256_load_si256(src+4), seed); + dest[5] = _mm256_add_epi64(_mm256_load_si256(src+5), seed); + } +} + +#endif + +/* x86dispatch always generates SSE2 */ +#if (XXH_VECTOR == XXH_SSE2) || defined(XXH_X86DISPATCH) + +#ifndef XXH_TARGET_SSE2 +# define XXH_TARGET_SSE2 /* disable attribute target */ +#endif + +XXH_FORCE_INLINE XXH_TARGET_SSE2 void +XXH3_accumulate_512_sse2( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + /* SSE2 is just a half-scale version of the AVX2 version. */ + XXH_ASSERT((((size_t)acc) & 15) == 0); + { __m128i* const xacc = (__m128i *) acc; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */ + const __m128i* const xinput = (const __m128i *) input; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */ + const __m128i* const xsecret = (const __m128i *) secret; + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(__m128i); i++) { + /* data_vec = xinput[i]; */ + __m128i const data_vec = _mm_loadu_si128 (xinput+i); + /* key_vec = xsecret[i]; */ + __m128i const key_vec = _mm_loadu_si128 (xsecret+i); + /* data_key = data_vec ^ key_vec; */ + __m128i const data_key = _mm_xor_si128 (data_vec, key_vec); + /* data_key_lo = data_key >> 32; */ + __m128i const data_key_lo = _mm_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1)); + /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */ + __m128i const product = _mm_mul_epu32 (data_key, data_key_lo); + /* xacc[i] += swap(data_vec); */ + __m128i const data_swap = _mm_shuffle_epi32(data_vec, _MM_SHUFFLE(1,0,3,2)); + __m128i const sum = _mm_add_epi64(xacc[i], data_swap); + /* xacc[i] += product; */ + xacc[i] = _mm_add_epi64(product, sum); + } } +} +XXH_FORCE_INLINE XXH_TARGET_SSE2 XXH3_ACCUMULATE_TEMPLATE(sse2) + +XXH_FORCE_INLINE XXH_TARGET_SSE2 void +XXH3_scrambleAcc_sse2(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + { __m128i* const xacc = (__m128i*) acc; + /* Unaligned. This is mainly for pointer arithmetic, and because + * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */ + const __m128i* const xsecret = (const __m128i *) secret; + const __m128i prime32 = _mm_set1_epi32((int)XXH_PRIME32_1); + + size_t i; + for (i=0; i < XXH_STRIPE_LEN/sizeof(__m128i); i++) { + /* xacc[i] ^= (xacc[i] >> 47) */ + __m128i const acc_vec = xacc[i]; + __m128i const shifted = _mm_srli_epi64 (acc_vec, 47); + __m128i const data_vec = _mm_xor_si128 (acc_vec, shifted); + /* xacc[i] ^= xsecret[i]; */ + __m128i const key_vec = _mm_loadu_si128 (xsecret+i); + __m128i const data_key = _mm_xor_si128 (data_vec, key_vec); + + /* xacc[i] *= XXH_PRIME32_1; */ + __m128i const data_key_hi = _mm_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1)); + __m128i const prod_lo = _mm_mul_epu32 (data_key, prime32); + __m128i const prod_hi = _mm_mul_epu32 (data_key_hi, prime32); + xacc[i] = _mm_add_epi64(prod_lo, _mm_slli_epi64(prod_hi, 32)); + } + } +} + +XXH_FORCE_INLINE XXH_TARGET_SSE2 void XXH3_initCustomSecret_sse2(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 15) == 0); + (void)(&XXH_writeLE64); + { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / sizeof(__m128i); + +# if defined(_MSC_VER) && defined(_M_IX86) && _MSC_VER < 1900 + /* MSVC 32bit mode does not support _mm_set_epi64x before 2015 */ + XXH_ALIGN(16) const xxh_i64 seed64x2[2] = { (xxh_i64)seed64, (xxh_i64)(0U - seed64) }; + __m128i const seed = _mm_load_si128((__m128i const*)seed64x2); +# else + __m128i const seed = _mm_set_epi64x((xxh_i64)(0U - seed64), (xxh_i64)seed64); +# endif + int i; + + const void* const src16 = XXH3_kSecret; + __m128i* dst16 = (__m128i*) customSecret; +# if defined(__GNUC__) || defined(__clang__) + /* + * On GCC & Clang, marking 'dest' as modified will cause the compiler: + * - do not extract the secret from sse registers in the internal loop + * - use less common registers, and avoid pushing these reg into stack + */ + XXH_COMPILER_GUARD(dst16); +# endif + XXH_ASSERT(((size_t)src16 & 15) == 0); /* control alignment */ + XXH_ASSERT(((size_t)dst16 & 15) == 0); + + for (i=0; i < nbRounds; ++i) { + dst16[i] = _mm_add_epi64(_mm_load_si128((const __m128i *)src16+i), seed); + } } +} + +#endif + +#if (XXH_VECTOR == XXH_NEON) + +/* forward declarations for the scalar routines */ +XXH_FORCE_INLINE void +XXH3_scalarRound(void* XXH_RESTRICT acc, void const* XXH_RESTRICT input, + void const* XXH_RESTRICT secret, size_t lane); + +XXH_FORCE_INLINE void +XXH3_scalarScrambleRound(void* XXH_RESTRICT acc, + void const* XXH_RESTRICT secret, size_t lane); + +/*! + * @internal + * @brief The bulk processing loop for NEON and WASM SIMD128. + * + * The NEON code path is actually partially scalar when running on AArch64. This + * is to optimize the pipelining and can have up to 15% speedup depending on the + * CPU, and it also mitigates some GCC codegen issues. + * + * @see XXH3_NEON_LANES for configuring this and details about this optimization. + * + * NEON's 32-bit to 64-bit long multiply takes a half vector of 32-bit + * integers instead of the other platforms which mask full 64-bit vectors, + * so the setup is more complicated than just shifting right. + * + * Additionally, there is an optimization for 4 lanes at once noted below. + * + * Since, as stated, the most optimal amount of lanes for Cortexes is 6, + * there needs to be *three* versions of the accumulate operation used + * for the remaining 2 lanes. + * + * WASM's SIMD128 uses SIMDe's arm_neon.h polyfill because the intrinsics overlap + * nearly perfectly. + */ + +XXH_FORCE_INLINE void +XXH3_accumulate_512_neon( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + XXH_STATIC_ASSERT(XXH3_NEON_LANES > 0 && XXH3_NEON_LANES <= XXH_ACC_NB && XXH3_NEON_LANES % 2 == 0); + { /* GCC for darwin arm64 does not like aliasing here */ + xxh_aliasing_uint64x2_t* const xacc = (xxh_aliasing_uint64x2_t*) acc; + /* We don't use a uint32x4_t pointer because it causes bus errors on ARMv7. */ + uint8_t const* xinput = (const uint8_t *) input; + uint8_t const* xsecret = (const uint8_t *) secret; + + size_t i; +#ifdef __wasm_simd128__ + /* + * On WASM SIMD128, Clang emits direct address loads when XXH3_kSecret + * is constant propagated, which results in it converting it to this + * inside the loop: + * + * a = v128.load(XXH3_kSecret + 0 + $secret_offset, offset = 0) + * b = v128.load(XXH3_kSecret + 16 + $secret_offset, offset = 0) + * ... + * + * This requires a full 32-bit address immediate (and therefore a 6 byte + * instruction) as well as an add for each offset. + * + * Putting an asm guard prevents it from folding (at the cost of losing + * the alignment hint), and uses the free offset in `v128.load` instead + * of adding secret_offset each time which overall reduces code size by + * about a kilobyte and improves performance. + */ + XXH_COMPILER_GUARD(xsecret); +#endif + /* Scalar lanes use the normal scalarRound routine */ + for (i = XXH3_NEON_LANES; i < XXH_ACC_NB; i++) { + XXH3_scalarRound(acc, input, secret, i); + } + i = 0; + /* 4 NEON lanes at a time. */ + for (; i+1 < XXH3_NEON_LANES / 2; i+=2) { + /* data_vec = xinput[i]; */ + uint64x2_t data_vec_1 = XXH_vld1q_u64(xinput + (i * 16)); + uint64x2_t data_vec_2 = XXH_vld1q_u64(xinput + ((i+1) * 16)); + /* key_vec = xsecret[i]; */ + uint64x2_t key_vec_1 = XXH_vld1q_u64(xsecret + (i * 16)); + uint64x2_t key_vec_2 = XXH_vld1q_u64(xsecret + ((i+1) * 16)); + /* data_swap = swap(data_vec) */ + uint64x2_t data_swap_1 = vextq_u64(data_vec_1, data_vec_1, 1); + uint64x2_t data_swap_2 = vextq_u64(data_vec_2, data_vec_2, 1); + /* data_key = data_vec ^ key_vec; */ + uint64x2_t data_key_1 = veorq_u64(data_vec_1, key_vec_1); + uint64x2_t data_key_2 = veorq_u64(data_vec_2, key_vec_2); + + /* + * If we reinterpret the 64x2 vectors as 32x4 vectors, we can use a + * de-interleave operation for 4 lanes in 1 step with `vuzpq_u32` to + * get one vector with the low 32 bits of each lane, and one vector + * with the high 32 bits of each lane. + * + * The intrinsic returns a double vector because the original ARMv7-a + * instruction modified both arguments in place. AArch64 and SIMD128 emit + * two instructions from this intrinsic. + * + * [ dk11L | dk11H | dk12L | dk12H ] -> [ dk11L | dk12L | dk21L | dk22L ] + * [ dk21L | dk21H | dk22L | dk22H ] -> [ dk11H | dk12H | dk21H | dk22H ] + */ + uint32x4x2_t unzipped = vuzpq_u32( + vreinterpretq_u32_u64(data_key_1), + vreinterpretq_u32_u64(data_key_2) + ); + /* data_key_lo = data_key & 0xFFFFFFFF */ + uint32x4_t data_key_lo = unzipped.val[0]; + /* data_key_hi = data_key >> 32 */ + uint32x4_t data_key_hi = unzipped.val[1]; + /* + * Then, we can split the vectors horizontally and multiply which, as for most + * widening intrinsics, have a variant that works on both high half vectors + * for free on AArch64. A similar instruction is available on SIMD128. + * + * sum = data_swap + (u64x2) data_key_lo * (u64x2) data_key_hi + */ + uint64x2_t sum_1 = XXH_vmlal_low_u32(data_swap_1, data_key_lo, data_key_hi); + uint64x2_t sum_2 = XXH_vmlal_high_u32(data_swap_2, data_key_lo, data_key_hi); + /* + * Clang reorders + * a += b * c; // umlal swap.2d, dkl.2s, dkh.2s + * c += a; // add acc.2d, acc.2d, swap.2d + * to + * c += a; // add acc.2d, acc.2d, swap.2d + * c += b * c; // umlal acc.2d, dkl.2s, dkh.2s + * + * While it would make sense in theory since the addition is faster, + * for reasons likely related to umlal being limited to certain NEON + * pipelines, this is worse. A compiler guard fixes this. + */ + XXH_COMPILER_GUARD_CLANG_NEON(sum_1); + XXH_COMPILER_GUARD_CLANG_NEON(sum_2); + /* xacc[i] = acc_vec + sum; */ + xacc[i] = vaddq_u64(xacc[i], sum_1); + xacc[i+1] = vaddq_u64(xacc[i+1], sum_2); + } + /* Operate on the remaining NEON lanes 2 at a time. */ + for (; i < XXH3_NEON_LANES / 2; i++) { + /* data_vec = xinput[i]; */ + uint64x2_t data_vec = XXH_vld1q_u64(xinput + (i * 16)); + /* key_vec = xsecret[i]; */ + uint64x2_t key_vec = XXH_vld1q_u64(xsecret + (i * 16)); + /* acc_vec_2 = swap(data_vec) */ + uint64x2_t data_swap = vextq_u64(data_vec, data_vec, 1); + /* data_key = data_vec ^ key_vec; */ + uint64x2_t data_key = veorq_u64(data_vec, key_vec); + /* For two lanes, just use VMOVN and VSHRN. */ + /* data_key_lo = data_key & 0xFFFFFFFF; */ + uint32x2_t data_key_lo = vmovn_u64(data_key); + /* data_key_hi = data_key >> 32; */ + uint32x2_t data_key_hi = vshrn_n_u64(data_key, 32); + /* sum = data_swap + (u64x2) data_key_lo * (u64x2) data_key_hi; */ + uint64x2_t sum = vmlal_u32(data_swap, data_key_lo, data_key_hi); + /* Same Clang workaround as before */ + XXH_COMPILER_GUARD_CLANG_NEON(sum); + /* xacc[i] = acc_vec + sum; */ + xacc[i] = vaddq_u64 (xacc[i], sum); + } + } +} +XXH_FORCE_INLINE XXH3_ACCUMULATE_TEMPLATE(neon) + +XXH_FORCE_INLINE void +XXH3_scrambleAcc_neon(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + + { xxh_aliasing_uint64x2_t* xacc = (xxh_aliasing_uint64x2_t*) acc; + uint8_t const* xsecret = (uint8_t const*) secret; + + size_t i; + /* WASM uses operator overloads and doesn't need these. */ +#ifndef __wasm_simd128__ + /* { prime32_1, prime32_1 } */ + uint32x2_t const kPrimeLo = vdup_n_u32(XXH_PRIME32_1); + /* { 0, prime32_1, 0, prime32_1 } */ + uint32x4_t const kPrimeHi = vreinterpretq_u32_u64(vdupq_n_u64((xxh_u64)XXH_PRIME32_1 << 32)); +#endif + + /* AArch64 uses both scalar and neon at the same time */ + for (i = XXH3_NEON_LANES; i < XXH_ACC_NB; i++) { + XXH3_scalarScrambleRound(acc, secret, i); + } + for (i=0; i < XXH3_NEON_LANES / 2; i++) { + /* xacc[i] ^= (xacc[i] >> 47); */ + uint64x2_t acc_vec = xacc[i]; + uint64x2_t shifted = vshrq_n_u64(acc_vec, 47); + uint64x2_t data_vec = veorq_u64(acc_vec, shifted); + + /* xacc[i] ^= xsecret[i]; */ + uint64x2_t key_vec = XXH_vld1q_u64(xsecret + (i * 16)); + uint64x2_t data_key = veorq_u64(data_vec, key_vec); + /* xacc[i] *= XXH_PRIME32_1 */ +#ifdef __wasm_simd128__ + /* SIMD128 has multiply by u64x2, use it instead of expanding and scalarizing */ + xacc[i] = data_key * XXH_PRIME32_1; +#else + /* + * Expanded version with portable NEON intrinsics + * + * lo(x) * lo(y) + (hi(x) * lo(y) << 32) + * + * prod_hi = hi(data_key) * lo(prime) << 32 + * + * Since we only need 32 bits of this multiply a trick can be used, reinterpreting the vector + * as a uint32x4_t and multiplying by { 0, prime, 0, prime } to cancel out the unwanted bits + * and avoid the shift. + */ + uint32x4_t prod_hi = vmulq_u32 (vreinterpretq_u32_u64(data_key), kPrimeHi); + /* Extract low bits for vmlal_u32 */ + uint32x2_t data_key_lo = vmovn_u64(data_key); + /* xacc[i] = prod_hi + lo(data_key) * XXH_PRIME32_1; */ + xacc[i] = vmlal_u32(vreinterpretq_u64_u32(prod_hi), data_key_lo, kPrimeLo); +#endif + } + } +} +#endif + +#if (XXH_VECTOR == XXH_VSX) + +XXH_FORCE_INLINE void +XXH3_accumulate_512_vsx( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + /* presumed aligned */ + xxh_aliasing_u64x2* const xacc = (xxh_aliasing_u64x2*) acc; + xxh_u8 const* const xinput = (xxh_u8 const*) input; /* no alignment restriction */ + xxh_u8 const* const xsecret = (xxh_u8 const*) secret; /* no alignment restriction */ + xxh_u64x2 const v32 = { 32, 32 }; + size_t i; + for (i = 0; i < XXH_STRIPE_LEN / sizeof(xxh_u64x2); i++) { + /* data_vec = xinput[i]; */ + xxh_u64x2 const data_vec = XXH_vec_loadu(xinput + 16*i); + /* key_vec = xsecret[i]; */ + xxh_u64x2 const key_vec = XXH_vec_loadu(xsecret + 16*i); + xxh_u64x2 const data_key = data_vec ^ key_vec; + /* shuffled = (data_key << 32) | (data_key >> 32); */ + xxh_u32x4 const shuffled = (xxh_u32x4)vec_rl(data_key, v32); + /* product = ((xxh_u64x2)data_key & 0xFFFFFFFF) * ((xxh_u64x2)shuffled & 0xFFFFFFFF); */ + xxh_u64x2 const product = XXH_vec_mulo((xxh_u32x4)data_key, shuffled); + /* acc_vec = xacc[i]; */ + xxh_u64x2 acc_vec = xacc[i]; + acc_vec += product; + + /* swap high and low halves */ +#ifdef __s390x__ + acc_vec += vec_permi(data_vec, data_vec, 2); +#else + acc_vec += vec_xxpermdi(data_vec, data_vec, 2); +#endif + xacc[i] = acc_vec; + } +} +XXH_FORCE_INLINE XXH3_ACCUMULATE_TEMPLATE(vsx) + +XXH_FORCE_INLINE void +XXH3_scrambleAcc_vsx(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + XXH_ASSERT((((size_t)acc) & 15) == 0); + + { xxh_aliasing_u64x2* const xacc = (xxh_aliasing_u64x2*) acc; + const xxh_u8* const xsecret = (const xxh_u8*) secret; + /* constants */ + xxh_u64x2 const v32 = { 32, 32 }; + xxh_u64x2 const v47 = { 47, 47 }; + xxh_u32x4 const prime = { XXH_PRIME32_1, XXH_PRIME32_1, XXH_PRIME32_1, XXH_PRIME32_1 }; + size_t i; + for (i = 0; i < XXH_STRIPE_LEN / sizeof(xxh_u64x2); i++) { + /* xacc[i] ^= (xacc[i] >> 47); */ + xxh_u64x2 const acc_vec = xacc[i]; + xxh_u64x2 const data_vec = acc_vec ^ (acc_vec >> v47); + + /* xacc[i] ^= xsecret[i]; */ + xxh_u64x2 const key_vec = XXH_vec_loadu(xsecret + 16*i); + xxh_u64x2 const data_key = data_vec ^ key_vec; + + /* xacc[i] *= XXH_PRIME32_1 */ + /* prod_lo = ((xxh_u64x2)data_key & 0xFFFFFFFF) * ((xxh_u64x2)prime & 0xFFFFFFFF); */ + xxh_u64x2 const prod_even = XXH_vec_mule((xxh_u32x4)data_key, prime); + /* prod_hi = ((xxh_u64x2)data_key >> 32) * ((xxh_u64x2)prime >> 32); */ + xxh_u64x2 const prod_odd = XXH_vec_mulo((xxh_u32x4)data_key, prime); + xacc[i] = prod_odd + (prod_even << v32); + } } +} + +#endif + +#if (XXH_VECTOR == XXH_SVE) + +XXH_FORCE_INLINE void +XXH3_accumulate_512_sve( void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + uint64_t *xacc = (uint64_t *)acc; + const uint64_t *xinput = (const uint64_t *)(const void *)input; + const uint64_t *xsecret = (const uint64_t *)(const void *)secret; + svuint64_t kSwap = sveor_n_u64_z(svptrue_b64(), svindex_u64(0, 1), 1); + uint64_t element_count = svcntd(); + if (element_count >= 8) { + svbool_t mask = svptrue_pat_b64(SV_VL8); + svuint64_t vacc = svld1_u64(mask, xacc); + ACCRND(vacc, 0); + svst1_u64(mask, xacc, vacc); + } else if (element_count == 2) { /* sve128 */ + svbool_t mask = svptrue_pat_b64(SV_VL2); + svuint64_t acc0 = svld1_u64(mask, xacc + 0); + svuint64_t acc1 = svld1_u64(mask, xacc + 2); + svuint64_t acc2 = svld1_u64(mask, xacc + 4); + svuint64_t acc3 = svld1_u64(mask, xacc + 6); + ACCRND(acc0, 0); + ACCRND(acc1, 2); + ACCRND(acc2, 4); + ACCRND(acc3, 6); + svst1_u64(mask, xacc + 0, acc0); + svst1_u64(mask, xacc + 2, acc1); + svst1_u64(mask, xacc + 4, acc2); + svst1_u64(mask, xacc + 6, acc3); + } else { + svbool_t mask = svptrue_pat_b64(SV_VL4); + svuint64_t acc0 = svld1_u64(mask, xacc + 0); + svuint64_t acc1 = svld1_u64(mask, xacc + 4); + ACCRND(acc0, 0); + ACCRND(acc1, 4); + svst1_u64(mask, xacc + 0, acc0); + svst1_u64(mask, xacc + 4, acc1); + } +} + +XXH_FORCE_INLINE void +XXH3_accumulate_sve(xxh_u64* XXH_RESTRICT acc, + const xxh_u8* XXH_RESTRICT input, + const xxh_u8* XXH_RESTRICT secret, + size_t nbStripes) +{ + if (nbStripes != 0) { + uint64_t *xacc = (uint64_t *)acc; + const uint64_t *xinput = (const uint64_t *)(const void *)input; + const uint64_t *xsecret = (const uint64_t *)(const void *)secret; + svuint64_t kSwap = sveor_n_u64_z(svptrue_b64(), svindex_u64(0, 1), 1); + uint64_t element_count = svcntd(); + if (element_count >= 8) { + svbool_t mask = svptrue_pat_b64(SV_VL8); + svuint64_t vacc = svld1_u64(mask, xacc + 0); + do { + /* svprfd(svbool_t, void *, enum svfprop); */ + svprfd(mask, xinput + 128, SV_PLDL1STRM); + ACCRND(vacc, 0); + xinput += 8; + xsecret += 1; + nbStripes--; + } while (nbStripes != 0); + + svst1_u64(mask, xacc + 0, vacc); + } else if (element_count == 2) { /* sve128 */ + svbool_t mask = svptrue_pat_b64(SV_VL2); + svuint64_t acc0 = svld1_u64(mask, xacc + 0); + svuint64_t acc1 = svld1_u64(mask, xacc + 2); + svuint64_t acc2 = svld1_u64(mask, xacc + 4); + svuint64_t acc3 = svld1_u64(mask, xacc + 6); + do { + svprfd(mask, xinput + 128, SV_PLDL1STRM); + ACCRND(acc0, 0); + ACCRND(acc1, 2); + ACCRND(acc2, 4); + ACCRND(acc3, 6); + xinput += 8; + xsecret += 1; + nbStripes--; + } while (nbStripes != 0); + + svst1_u64(mask, xacc + 0, acc0); + svst1_u64(mask, xacc + 2, acc1); + svst1_u64(mask, xacc + 4, acc2); + svst1_u64(mask, xacc + 6, acc3); + } else { + svbool_t mask = svptrue_pat_b64(SV_VL4); + svuint64_t acc0 = svld1_u64(mask, xacc + 0); + svuint64_t acc1 = svld1_u64(mask, xacc + 4); + do { + svprfd(mask, xinput + 128, SV_PLDL1STRM); + ACCRND(acc0, 0); + ACCRND(acc1, 4); + xinput += 8; + xsecret += 1; + nbStripes--; + } while (nbStripes != 0); + + svst1_u64(mask, xacc + 0, acc0); + svst1_u64(mask, xacc + 4, acc1); + } + } +} + +#endif + +/* scalar variants - universal */ + +#if defined(__aarch64__) && (defined(__GNUC__) || defined(__clang__)) +/* + * In XXH3_scalarRound(), GCC and Clang have a similar codegen issue, where they + * emit an excess mask and a full 64-bit multiply-add (MADD X-form). + * + * While this might not seem like much, as AArch64 is a 64-bit architecture, only + * big Cortex designs have a full 64-bit multiplier. + * + * On the little cores, the smaller 32-bit multiplier is used, and full 64-bit + * multiplies expand to 2-3 multiplies in microcode. This has a major penalty + * of up to 4 latency cycles and 2 stall cycles in the multiply pipeline. + * + * Thankfully, AArch64 still provides the 32-bit long multiply-add (UMADDL) which does + * not have this penalty and does the mask automatically. + */ +XXH_FORCE_INLINE xxh_u64 +XXH_mult32to64_add64(xxh_u64 lhs, xxh_u64 rhs, xxh_u64 acc) +{ + xxh_u64 ret; + /* note: %x = 64-bit register, %w = 32-bit register */ + __asm__("umaddl %x0, %w1, %w2, %x3" : "=r" (ret) : "r" (lhs), "r" (rhs), "r" (acc)); + return ret; +} +#else +XXH_FORCE_INLINE xxh_u64 +XXH_mult32to64_add64(xxh_u64 lhs, xxh_u64 rhs, xxh_u64 acc) +{ + return XXH_mult32to64((xxh_u32)lhs, (xxh_u32)rhs) + acc; +} +#endif + +/*! + * @internal + * @brief Scalar round for @ref XXH3_accumulate_512_scalar(). + * + * This is extracted to its own function because the NEON path uses a combination + * of NEON and scalar. + */ +XXH_FORCE_INLINE void +XXH3_scalarRound(void* XXH_RESTRICT acc, + void const* XXH_RESTRICT input, + void const* XXH_RESTRICT secret, + size_t lane) +{ + xxh_u64* xacc = (xxh_u64*) acc; + xxh_u8 const* xinput = (xxh_u8 const*) input; + xxh_u8 const* xsecret = (xxh_u8 const*) secret; + XXH_ASSERT(lane < XXH_ACC_NB); + XXH_ASSERT(((size_t)acc & (XXH_ACC_ALIGN-1)) == 0); + { + xxh_u64 const data_val = XXH_readLE64(xinput + lane * 8); + xxh_u64 const data_key = data_val ^ XXH_readLE64(xsecret + lane * 8); + xacc[lane ^ 1] += data_val; /* swap adjacent lanes */ + xacc[lane] = XXH_mult32to64_add64(data_key /* & 0xFFFFFFFF */, data_key >> 32, xacc[lane]); + } +} + +/*! + * @internal + * @brief Processes a 64 byte block of data using the scalar path. + */ +XXH_FORCE_INLINE void +XXH3_accumulate_512_scalar(void* XXH_RESTRICT acc, + const void* XXH_RESTRICT input, + const void* XXH_RESTRICT secret) +{ + size_t i; + /* ARM GCC refuses to unroll this loop, resulting in a 24% slowdown on ARMv6. */ +#if defined(__GNUC__) && !defined(__clang__) \ + && (defined(__arm__) || defined(__thumb2__)) \ + && defined(__ARM_FEATURE_UNALIGNED) /* no unaligned access just wastes bytes */ \ + && XXH_SIZE_OPT <= 0 +# pragma GCC unroll 8 +#endif + for (i=0; i < XXH_ACC_NB; i++) { + XXH3_scalarRound(acc, input, secret, i); + } +} +XXH_FORCE_INLINE XXH3_ACCUMULATE_TEMPLATE(scalar) + +/*! + * @internal + * @brief Scalar scramble step for @ref XXH3_scrambleAcc_scalar(). + * + * This is extracted to its own function because the NEON path uses a combination + * of NEON and scalar. + */ +XXH_FORCE_INLINE void +XXH3_scalarScrambleRound(void* XXH_RESTRICT acc, + void const* XXH_RESTRICT secret, + size_t lane) +{ + xxh_u64* const xacc = (xxh_u64*) acc; /* presumed aligned */ + const xxh_u8* const xsecret = (const xxh_u8*) secret; /* no alignment restriction */ + XXH_ASSERT((((size_t)acc) & (XXH_ACC_ALIGN-1)) == 0); + XXH_ASSERT(lane < XXH_ACC_NB); + { + xxh_u64 const key64 = XXH_readLE64(xsecret + lane * 8); + xxh_u64 acc64 = xacc[lane]; + acc64 = XXH_xorshift64(acc64, 47); + acc64 ^= key64; + acc64 *= XXH_PRIME32_1; + xacc[lane] = acc64; + } +} + +/*! + * @internal + * @brief Scrambles the accumulators after a large chunk has been read + */ +XXH_FORCE_INLINE void +XXH3_scrambleAcc_scalar(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret) +{ + size_t i; + for (i=0; i < XXH_ACC_NB; i++) { + XXH3_scalarScrambleRound(acc, secret, i); + } +} + +XXH_FORCE_INLINE void +XXH3_initCustomSecret_scalar(void* XXH_RESTRICT customSecret, xxh_u64 seed64) +{ + /* + * We need a separate pointer for the hack below, + * which requires a non-const pointer. + * Any decent compiler will optimize this out otherwise. + */ + const xxh_u8* kSecretPtr = XXH3_kSecret; + XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 15) == 0); + +#if defined(__GNUC__) && defined(__aarch64__) + /* + * UGLY HACK: + * GCC and Clang generate a bunch of MOV/MOVK pairs for aarch64, and they are + * placed sequentially, in order, at the top of the unrolled loop. + * + * While MOVK is great for generating constants (2 cycles for a 64-bit + * constant compared to 4 cycles for LDR), it fights for bandwidth with + * the arithmetic instructions. + * + * I L S + * MOVK + * MOVK + * MOVK + * MOVK + * ADD + * SUB STR + * STR + * By forcing loads from memory (as the asm line causes the compiler to assume + * that XXH3_kSecretPtr has been changed), the pipelines are used more + * efficiently: + * I L S + * LDR + * ADD LDR + * SUB STR + * STR + * + * See XXH3_NEON_LANES for details on the pipsline. + * + * XXH3_64bits_withSeed, len == 256, Snapdragon 835 + * without hack: 2654.4 MB/s + * with hack: 3202.9 MB/s + */ + XXH_COMPILER_GUARD(kSecretPtr); +#endif + { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / 16; + int i; + for (i=0; i < nbRounds; i++) { + /* + * The asm hack causes the compiler to assume that kSecretPtr aliases with + * customSecret, and on aarch64, this prevented LDP from merging two + * loads together for free. Putting the loads together before the stores + * properly generates LDP. + */ + xxh_u64 lo = XXH_readLE64(kSecretPtr + 16*i) + seed64; + xxh_u64 hi = XXH_readLE64(kSecretPtr + 16*i + 8) - seed64; + XXH_writeLE64((xxh_u8*)customSecret + 16*i, lo); + XXH_writeLE64((xxh_u8*)customSecret + 16*i + 8, hi); + } } +} + + +typedef void (*XXH3_f_accumulate)(xxh_u64* XXH_RESTRICT, const xxh_u8* XXH_RESTRICT, const xxh_u8* XXH_RESTRICT, size_t); +typedef void (*XXH3_f_scrambleAcc)(void* XXH_RESTRICT, const void*); +typedef void (*XXH3_f_initCustomSecret)(void* XXH_RESTRICT, xxh_u64); + + +#if (XXH_VECTOR == XXH_AVX512) + +#define XXH3_accumulate_512 XXH3_accumulate_512_avx512 +#define XXH3_accumulate XXH3_accumulate_avx512 +#define XXH3_scrambleAcc XXH3_scrambleAcc_avx512 +#define XXH3_initCustomSecret XXH3_initCustomSecret_avx512 + +#elif (XXH_VECTOR == XXH_AVX2) + +#define XXH3_accumulate_512 XXH3_accumulate_512_avx2 +#define XXH3_accumulate XXH3_accumulate_avx2 +#define XXH3_scrambleAcc XXH3_scrambleAcc_avx2 +#define XXH3_initCustomSecret XXH3_initCustomSecret_avx2 + +#elif (XXH_VECTOR == XXH_SSE2) + +#define XXH3_accumulate_512 XXH3_accumulate_512_sse2 +#define XXH3_accumulate XXH3_accumulate_sse2 +#define XXH3_scrambleAcc XXH3_scrambleAcc_sse2 +#define XXH3_initCustomSecret XXH3_initCustomSecret_sse2 + +#elif (XXH_VECTOR == XXH_NEON) + +#define XXH3_accumulate_512 XXH3_accumulate_512_neon +#define XXH3_accumulate XXH3_accumulate_neon +#define XXH3_scrambleAcc XXH3_scrambleAcc_neon +#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar + +#elif (XXH_VECTOR == XXH_VSX) + +#define XXH3_accumulate_512 XXH3_accumulate_512_vsx +#define XXH3_accumulate XXH3_accumulate_vsx +#define XXH3_scrambleAcc XXH3_scrambleAcc_vsx +#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar + +#elif (XXH_VECTOR == XXH_SVE) +#define XXH3_accumulate_512 XXH3_accumulate_512_sve +#define XXH3_accumulate XXH3_accumulate_sve +#define XXH3_scrambleAcc XXH3_scrambleAcc_scalar +#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar + +#else /* scalar */ + +#define XXH3_accumulate_512 XXH3_accumulate_512_scalar +#define XXH3_accumulate XXH3_accumulate_scalar +#define XXH3_scrambleAcc XXH3_scrambleAcc_scalar +#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar + +#endif + +#if XXH_SIZE_OPT >= 1 /* don't do SIMD for initialization */ +# undef XXH3_initCustomSecret +# define XXH3_initCustomSecret XXH3_initCustomSecret_scalar +#endif + +XXH_FORCE_INLINE void +XXH3_hashLong_internal_loop(xxh_u64* XXH_RESTRICT acc, + const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH3_f_accumulate f_acc, + XXH3_f_scrambleAcc f_scramble) +{ + size_t const nbStripesPerBlock = (secretSize - XXH_STRIPE_LEN) / XXH_SECRET_CONSUME_RATE; + size_t const block_len = XXH_STRIPE_LEN * nbStripesPerBlock; + size_t const nb_blocks = (len - 1) / block_len; + + size_t n; + + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); + + for (n = 0; n < nb_blocks; n++) { + f_acc(acc, input + n*block_len, secret, nbStripesPerBlock); + f_scramble(acc, secret + secretSize - XXH_STRIPE_LEN); + } + + /* last partial block */ + XXH_ASSERT(len > XXH_STRIPE_LEN); + { size_t const nbStripes = ((len - 1) - (block_len * nb_blocks)) / XXH_STRIPE_LEN; + XXH_ASSERT(nbStripes <= (secretSize / XXH_SECRET_CONSUME_RATE)); + f_acc(acc, input + nb_blocks*block_len, secret, nbStripes); + + /* last stripe */ + { const xxh_u8* const p = input + len - XXH_STRIPE_LEN; +#define XXH_SECRET_LASTACC_START 7 /* not aligned on 8, last secret is different from acc & scrambler */ + XXH3_accumulate_512(acc, p, secret + secretSize - XXH_STRIPE_LEN - XXH_SECRET_LASTACC_START); + } } +} + +XXH_FORCE_INLINE xxh_u64 +XXH3_mix2Accs(const xxh_u64* XXH_RESTRICT acc, const xxh_u8* XXH_RESTRICT secret) +{ + return XXH3_mul128_fold64( + acc[0] ^ XXH_readLE64(secret), + acc[1] ^ XXH_readLE64(secret+8) ); +} + +static XXH64_hash_t +XXH3_mergeAccs(const xxh_u64* XXH_RESTRICT acc, const xxh_u8* XXH_RESTRICT secret, xxh_u64 start) +{ + xxh_u64 result64 = start; + size_t i = 0; + + for (i = 0; i < 4; i++) { + result64 += XXH3_mix2Accs(acc+2*i, secret + 16*i); +#if defined(__clang__) /* Clang */ \ + && (defined(__arm__) || defined(__thumb__)) /* ARMv7 */ \ + && (defined(__ARM_NEON) || defined(__ARM_NEON__)) /* NEON */ \ + && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable */ + /* + * UGLY HACK: + * Prevent autovectorization on Clang ARMv7-a. Exact same problem as + * the one in XXH3_len_129to240_64b. Speeds up shorter keys > 240b. + * XXH3_64bits, len == 256, Snapdragon 835: + * without hack: 2063.7 MB/s + * with hack: 2560.7 MB/s + */ + XXH_COMPILER_GUARD(result64); +#endif + } + + return XXH3_avalanche(result64); +} + +#define XXH3_INIT_ACC { XXH_PRIME32_3, XXH_PRIME64_1, XXH_PRIME64_2, XXH_PRIME64_3, \ + XXH_PRIME64_4, XXH_PRIME32_2, XXH_PRIME64_5, XXH_PRIME32_1 } + +XXH_FORCE_INLINE XXH64_hash_t +XXH3_hashLong_64b_internal(const void* XXH_RESTRICT input, size_t len, + const void* XXH_RESTRICT secret, size_t secretSize, + XXH3_f_accumulate f_acc, + XXH3_f_scrambleAcc f_scramble) +{ + XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[XXH_ACC_NB] = XXH3_INIT_ACC; + + XXH3_hashLong_internal_loop(acc, (const xxh_u8*)input, len, (const xxh_u8*)secret, secretSize, f_acc, f_scramble); + + /* converge into final hash */ + XXH_STATIC_ASSERT(sizeof(acc) == 64); + /* do not align on 8, so that the secret is different from the accumulator */ +#define XXH_SECRET_MERGEACCS_START 11 + XXH_ASSERT(secretSize >= sizeof(acc) + XXH_SECRET_MERGEACCS_START); + return XXH3_mergeAccs(acc, (const xxh_u8*)secret + XXH_SECRET_MERGEACCS_START, (xxh_u64)len * XXH_PRIME64_1); +} + +/* + * It's important for performance to transmit secret's size (when it's static) + * so that the compiler can properly optimize the vectorized loop. + * This makes a big performance difference for "medium" keys (<1 KB) when using AVX instruction set. + * When the secret size is unknown, or on GCC 12 where the mix of NO_INLINE and FORCE_INLINE + * breaks -Og, this is XXH_NO_INLINE. + */ +XXH3_WITH_SECRET_INLINE XXH64_hash_t +XXH3_hashLong_64b_withSecret(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, const xxh_u8* XXH_RESTRICT secret, size_t secretLen) +{ + (void)seed64; + return XXH3_hashLong_64b_internal(input, len, secret, secretLen, XXH3_accumulate, XXH3_scrambleAcc); +} + +/* + * It's preferable for performance that XXH3_hashLong is not inlined, + * as it results in a smaller function for small data, easier to the instruction cache. + * Note that inside this no_inline function, we do inline the internal loop, + * and provide a statically defined secret size to allow optimization of vector loop. + */ +XXH_NO_INLINE XXH_PUREF XXH64_hash_t +XXH3_hashLong_64b_default(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, const xxh_u8* XXH_RESTRICT secret, size_t secretLen) +{ + (void)seed64; (void)secret; (void)secretLen; + return XXH3_hashLong_64b_internal(input, len, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_accumulate, XXH3_scrambleAcc); +} + +/* + * XXH3_hashLong_64b_withSeed(): + * Generate a custom key based on alteration of default XXH3_kSecret with the seed, + * and then use this key for long mode hashing. + * + * This operation is decently fast but nonetheless costs a little bit of time. + * Try to avoid it whenever possible (typically when seed==0). + * + * It's important for performance that XXH3_hashLong is not inlined. Not sure + * why (uop cache maybe?), but the difference is large and easily measurable. + */ +XXH_FORCE_INLINE XXH64_hash_t +XXH3_hashLong_64b_withSeed_internal(const void* input, size_t len, + XXH64_hash_t seed, + XXH3_f_accumulate f_acc, + XXH3_f_scrambleAcc f_scramble, + XXH3_f_initCustomSecret f_initSec) +{ +#if XXH_SIZE_OPT <= 0 + if (seed == 0) + return XXH3_hashLong_64b_internal(input, len, + XXH3_kSecret, sizeof(XXH3_kSecret), + f_acc, f_scramble); +#endif + { XXH_ALIGN(XXH_SEC_ALIGN) xxh_u8 secret[XXH_SECRET_DEFAULT_SIZE]; + f_initSec(secret, seed); + return XXH3_hashLong_64b_internal(input, len, secret, sizeof(secret), + f_acc, f_scramble); + } +} + +/* + * It's important for performance that XXH3_hashLong is not inlined. + */ +XXH_NO_INLINE XXH64_hash_t +XXH3_hashLong_64b_withSeed(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed, const xxh_u8* XXH_RESTRICT secret, size_t secretLen) +{ + (void)secret; (void)secretLen; + return XXH3_hashLong_64b_withSeed_internal(input, len, seed, + XXH3_accumulate, XXH3_scrambleAcc, XXH3_initCustomSecret); +} + + +typedef XXH64_hash_t (*XXH3_hashLong64_f)(const void* XXH_RESTRICT, size_t, + XXH64_hash_t, const xxh_u8* XXH_RESTRICT, size_t); + +XXH_FORCE_INLINE XXH64_hash_t +XXH3_64bits_internal(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen, + XXH3_hashLong64_f f_hashLong) +{ + XXH_ASSERT(secretLen >= XXH3_SECRET_SIZE_MIN); + /* + * If an action is to be taken if `secretLen` condition is not respected, + * it should be done here. + * For now, it's a contract pre-condition. + * Adding a check and a branch here would cost performance at every hash. + * Also, note that function signature doesn't offer room to return an error. + */ + if (len <= 16) + return XXH3_len_0to16_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, seed64); + if (len <= 128) + return XXH3_len_17to128_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); + if (len <= XXH3_MIDSIZE_MAX) + return XXH3_len_129to240_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); + return f_hashLong(input, len, seed64, (const xxh_u8*)secret, secretLen); +} + + +/* === Public entry point === */ + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH64_hash_t XXH3_64bits(XXH_NOESCAPE const void* input, size_t length) +{ + return XXH3_64bits_internal(input, length, 0, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_default); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH64_hash_t +XXH3_64bits_withSecret(XXH_NOESCAPE const void* input, size_t length, XXH_NOESCAPE const void* secret, size_t secretSize) +{ + return XXH3_64bits_internal(input, length, 0, secret, secretSize, XXH3_hashLong_64b_withSecret); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH64_hash_t +XXH3_64bits_withSeed(XXH_NOESCAPE const void* input, size_t length, XXH64_hash_t seed) +{ + return XXH3_64bits_internal(input, length, seed, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_withSeed); +} + +XXH_PUBLIC_API XXH64_hash_t +XXH3_64bits_withSecretandSeed(XXH_NOESCAPE const void* input, size_t length, XXH_NOESCAPE const void* secret, size_t secretSize, XXH64_hash_t seed) +{ + if (length <= XXH3_MIDSIZE_MAX) + return XXH3_64bits_internal(input, length, seed, XXH3_kSecret, sizeof(XXH3_kSecret), NULL); + return XXH3_hashLong_64b_withSecret(input, length, seed, (const xxh_u8*)secret, secretSize); +} + + +/* === XXH3 streaming === */ +#ifndef XXH_NO_STREAM +/* + * Malloc's a pointer that is always aligned to align. + * + * This must be freed with `XXH_alignedFree()`. + * + * malloc typically guarantees 16 byte alignment on 64-bit systems and 8 byte + * alignment on 32-bit. This isn't enough for the 32 byte aligned loads in AVX2 + * or on 32-bit, the 16 byte aligned loads in SSE2 and NEON. + * + * This underalignment previously caused a rather obvious crash which went + * completely unnoticed due to XXH3_createState() not actually being tested. + * Credit to RedSpah for noticing this bug. + * + * The alignment is done manually: Functions like posix_memalign or _mm_malloc + * are avoided: To maintain portability, we would have to write a fallback + * like this anyways, and besides, testing for the existence of library + * functions without relying on external build tools is impossible. + * + * The method is simple: Overallocate, manually align, and store the offset + * to the original behind the returned pointer. + * + * Align must be a power of 2 and 8 <= align <= 128. + */ +static XXH_MALLOCF void* XXH_alignedMalloc(size_t s, size_t align) +{ + XXH_ASSERT(align <= 128 && align >= 8); /* range check */ + XXH_ASSERT((align & (align-1)) == 0); /* power of 2 */ + XXH_ASSERT(s != 0 && s < (s + align)); /* empty/overflow */ + { /* Overallocate to make room for manual realignment and an offset byte */ + xxh_u8* base = (xxh_u8*)XXH_malloc(s + align); + if (base != NULL) { + /* + * Get the offset needed to align this pointer. + * + * Even if the returned pointer is aligned, there will always be + * at least one byte to store the offset to the original pointer. + */ + size_t offset = align - ((size_t)base & (align - 1)); /* base % align */ + /* Add the offset for the now-aligned pointer */ + xxh_u8* ptr = base + offset; + + XXH_ASSERT((size_t)ptr % align == 0); + + /* Store the offset immediately before the returned pointer. */ + ptr[-1] = (xxh_u8)offset; + return ptr; + } + return NULL; + } +} +/* + * Frees an aligned pointer allocated by XXH_alignedMalloc(). Don't pass + * normal malloc'd pointers, XXH_alignedMalloc has a specific data layout. + */ +static void XXH_alignedFree(void* p) +{ + if (p != NULL) { + xxh_u8* ptr = (xxh_u8*)p; + /* Get the offset byte we added in XXH_malloc. */ + xxh_u8 offset = ptr[-1]; + /* Free the original malloc'd pointer */ + xxh_u8* base = ptr - offset; + XXH_free(base); + } +} +/*! @ingroup XXH3_family */ +/*! + * @brief Allocate an @ref XXH3_state_t. + * + * @return An allocated pointer of @ref XXH3_state_t on success. + * @return `NULL` on failure. + * + * @note Must be freed with XXH3_freeState(). + */ +XXH_PUBLIC_API XXH3_state_t* XXH3_createState(void) +{ + XXH3_state_t* const state = (XXH3_state_t*)XXH_alignedMalloc(sizeof(XXH3_state_t), 64); + if (state==NULL) return NULL; + XXH3_INITSTATE(state); + return state; +} + +/*! @ingroup XXH3_family */ +/*! + * @brief Frees an @ref XXH3_state_t. + * + * @param statePtr A pointer to an @ref XXH3_state_t allocated with @ref XXH3_createState(). + * + * @return @ref XXH_OK. + * + * @note Must be allocated with XXH3_createState(). + */ +XXH_PUBLIC_API XXH_errorcode XXH3_freeState(XXH3_state_t* statePtr) +{ + XXH_alignedFree(statePtr); + return XXH_OK; +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API void +XXH3_copyState(XXH_NOESCAPE XXH3_state_t* dst_state, XXH_NOESCAPE const XXH3_state_t* src_state) +{ + XXH_memcpy(dst_state, src_state, sizeof(*dst_state)); +} + +static void +XXH3_reset_internal(XXH3_state_t* statePtr, + XXH64_hash_t seed, + const void* secret, size_t secretSize) +{ + size_t const initStart = offsetof(XXH3_state_t, bufferedSize); + size_t const initLength = offsetof(XXH3_state_t, nbStripesPerBlock) - initStart; + XXH_ASSERT(offsetof(XXH3_state_t, nbStripesPerBlock) > initStart); + XXH_ASSERT(statePtr != NULL); + /* set members from bufferedSize to nbStripesPerBlock (excluded) to 0 */ + memset((char*)statePtr + initStart, 0, initLength); + statePtr->acc[0] = XXH_PRIME32_3; + statePtr->acc[1] = XXH_PRIME64_1; + statePtr->acc[2] = XXH_PRIME64_2; + statePtr->acc[3] = XXH_PRIME64_3; + statePtr->acc[4] = XXH_PRIME64_4; + statePtr->acc[5] = XXH_PRIME32_2; + statePtr->acc[6] = XXH_PRIME64_5; + statePtr->acc[7] = XXH_PRIME32_1; + statePtr->seed = seed; + statePtr->useSeed = (seed != 0); + statePtr->extSecret = (const unsigned char*)secret; + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); + statePtr->secretLimit = secretSize - XXH_STRIPE_LEN; + statePtr->nbStripesPerBlock = statePtr->secretLimit / XXH_SECRET_CONSUME_RATE; +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_reset(XXH_NOESCAPE XXH3_state_t* statePtr) +{ + if (statePtr == NULL) return XXH_ERROR; + XXH3_reset_internal(statePtr, 0, XXH3_kSecret, XXH_SECRET_DEFAULT_SIZE); + return XXH_OK; +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_reset_withSecret(XXH_NOESCAPE XXH3_state_t* statePtr, XXH_NOESCAPE const void* secret, size_t secretSize) +{ + if (statePtr == NULL) return XXH_ERROR; + XXH3_reset_internal(statePtr, 0, secret, secretSize); + if (secret == NULL) return XXH_ERROR; + if (secretSize < XXH3_SECRET_SIZE_MIN) return XXH_ERROR; + return XXH_OK; +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_reset_withSeed(XXH_NOESCAPE XXH3_state_t* statePtr, XXH64_hash_t seed) +{ + if (statePtr == NULL) return XXH_ERROR; + if (seed==0) return XXH3_64bits_reset(statePtr); + if ((seed != statePtr->seed) || (statePtr->extSecret != NULL)) + XXH3_initCustomSecret(statePtr->customSecret, seed); + XXH3_reset_internal(statePtr, seed, NULL, XXH_SECRET_DEFAULT_SIZE); + return XXH_OK; +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_reset_withSecretandSeed(XXH_NOESCAPE XXH3_state_t* statePtr, XXH_NOESCAPE const void* secret, size_t secretSize, XXH64_hash_t seed64) +{ + if (statePtr == NULL) return XXH_ERROR; + if (secret == NULL) return XXH_ERROR; + if (secretSize < XXH3_SECRET_SIZE_MIN) return XXH_ERROR; + XXH3_reset_internal(statePtr, seed64, secret, secretSize); + statePtr->useSeed = 1; /* always, even if seed64==0 */ + return XXH_OK; +} + +/*! + * @internal + * @brief Processes a large input for XXH3_update() and XXH3_digest_long(). + * + * Unlike XXH3_hashLong_internal_loop(), this can process data that overlaps a block. + * + * @param acc Pointer to the 8 accumulator lanes + * @param nbStripesSoFarPtr In/out pointer to the number of leftover stripes in the block* + * @param nbStripesPerBlock Number of stripes in a block + * @param input Input pointer + * @param nbStripes Number of stripes to process + * @param secret Secret pointer + * @param secretLimit Offset of the last block in @p secret + * @param f_acc Pointer to an XXH3_accumulate implementation + * @param f_scramble Pointer to an XXH3_scrambleAcc implementation + * @return Pointer past the end of @p input after processing + */ +XXH_FORCE_INLINE const xxh_u8 * +XXH3_consumeStripes(xxh_u64* XXH_RESTRICT acc, + size_t* XXH_RESTRICT nbStripesSoFarPtr, size_t nbStripesPerBlock, + const xxh_u8* XXH_RESTRICT input, size_t nbStripes, + const xxh_u8* XXH_RESTRICT secret, size_t secretLimit, + XXH3_f_accumulate f_acc, + XXH3_f_scrambleAcc f_scramble) +{ + const xxh_u8* initialSecret = secret + *nbStripesSoFarPtr * XXH_SECRET_CONSUME_RATE; + /* Process full blocks */ + if (nbStripes >= (nbStripesPerBlock - *nbStripesSoFarPtr)) { + /* Process the initial partial block... */ + size_t nbStripesThisIter = nbStripesPerBlock - *nbStripesSoFarPtr; + + do { + /* Accumulate and scramble */ + f_acc(acc, input, initialSecret, nbStripesThisIter); + f_scramble(acc, secret + secretLimit); + input += nbStripesThisIter * XXH_STRIPE_LEN; + nbStripes -= nbStripesThisIter; + /* Then continue the loop with the full block size */ + nbStripesThisIter = nbStripesPerBlock; + initialSecret = secret; + } while (nbStripes >= nbStripesPerBlock); + *nbStripesSoFarPtr = 0; + } + /* Process a partial block */ + if (nbStripes > 0) { + f_acc(acc, input, initialSecret, nbStripes); + input += nbStripes * XXH_STRIPE_LEN; + *nbStripesSoFarPtr += nbStripes; + } + /* Return end pointer */ + return input; +} + +#ifndef XXH3_STREAM_USE_STACK +# if XXH_SIZE_OPT <= 0 && !defined(__clang__) /* clang doesn't need additional stack space */ +# define XXH3_STREAM_USE_STACK 1 +# endif +#endif +/* + * Both XXH3_64bits_update and XXH3_128bits_update use this routine. + */ +XXH_FORCE_INLINE XXH_errorcode +XXH3_update(XXH3_state_t* XXH_RESTRICT const state, + const xxh_u8* XXH_RESTRICT input, size_t len, + XXH3_f_accumulate f_acc, + XXH3_f_scrambleAcc f_scramble) +{ + if (input==NULL) { + XXH_ASSERT(len == 0); + return XXH_OK; + } + + XXH_ASSERT(state != NULL); + { const xxh_u8* const bEnd = input + len; + const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret; +#if defined(XXH3_STREAM_USE_STACK) && XXH3_STREAM_USE_STACK >= 1 + /* For some reason, gcc and MSVC seem to suffer greatly + * when operating accumulators directly into state. + * Operating into stack space seems to enable proper optimization. + * clang, on the other hand, doesn't seem to need this trick */ + XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[8]; + XXH_memcpy(acc, state->acc, sizeof(acc)); +#else + xxh_u64* XXH_RESTRICT const acc = state->acc; +#endif + state->totalLen += len; + XXH_ASSERT(state->bufferedSize <= XXH3_INTERNALBUFFER_SIZE); + + /* small input : just fill in tmp buffer */ + if (len <= XXH3_INTERNALBUFFER_SIZE - state->bufferedSize) { + XXH_memcpy(state->buffer + state->bufferedSize, input, len); + state->bufferedSize += (XXH32_hash_t)len; + return XXH_OK; + } + + /* total input is now > XXH3_INTERNALBUFFER_SIZE */ + #define XXH3_INTERNALBUFFER_STRIPES (XXH3_INTERNALBUFFER_SIZE / XXH_STRIPE_LEN) + XXH_STATIC_ASSERT(XXH3_INTERNALBUFFER_SIZE % XXH_STRIPE_LEN == 0); /* clean multiple */ + + /* + * Internal buffer is partially filled (always, except at beginning) + * Complete it, then consume it. + */ + if (state->bufferedSize) { + size_t const loadSize = XXH3_INTERNALBUFFER_SIZE - state->bufferedSize; + XXH_memcpy(state->buffer + state->bufferedSize, input, loadSize); + input += loadSize; + XXH3_consumeStripes(acc, + &state->nbStripesSoFar, state->nbStripesPerBlock, + state->buffer, XXH3_INTERNALBUFFER_STRIPES, + secret, state->secretLimit, + f_acc, f_scramble); + state->bufferedSize = 0; + } + XXH_ASSERT(input < bEnd); + if (bEnd - input > XXH3_INTERNALBUFFER_SIZE) { + size_t nbStripes = (size_t)(bEnd - 1 - input) / XXH_STRIPE_LEN; + input = XXH3_consumeStripes(acc, + &state->nbStripesSoFar, state->nbStripesPerBlock, + input, nbStripes, + secret, state->secretLimit, + f_acc, f_scramble); + XXH_memcpy(state->buffer + sizeof(state->buffer) - XXH_STRIPE_LEN, input - XXH_STRIPE_LEN, XXH_STRIPE_LEN); + + } + /* Some remaining input (always) : buffer it */ + XXH_ASSERT(input < bEnd); + XXH_ASSERT(bEnd - input <= XXH3_INTERNALBUFFER_SIZE); + XXH_ASSERT(state->bufferedSize == 0); + XXH_memcpy(state->buffer, input, (size_t)(bEnd-input)); + state->bufferedSize = (XXH32_hash_t)(bEnd-input); +#if defined(XXH3_STREAM_USE_STACK) && XXH3_STREAM_USE_STACK >= 1 + /* save stack accumulators into state */ + XXH_memcpy(state->acc, acc, sizeof(acc)); +#endif + } + + return XXH_OK; +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_64bits_update(XXH_NOESCAPE XXH3_state_t* state, XXH_NOESCAPE const void* input, size_t len) +{ + return XXH3_update(state, (const xxh_u8*)input, len, + XXH3_accumulate, XXH3_scrambleAcc); +} + + +XXH_FORCE_INLINE void +XXH3_digest_long (XXH64_hash_t* acc, + const XXH3_state_t* state, + const unsigned char* secret) +{ + xxh_u8 lastStripe[XXH_STRIPE_LEN]; + const xxh_u8* lastStripePtr; + + /* + * Digest on a local copy. This way, the state remains unaltered, and it can + * continue ingesting more input afterwards. + */ + XXH_memcpy(acc, state->acc, sizeof(state->acc)); + if (state->bufferedSize >= XXH_STRIPE_LEN) { + /* Consume remaining stripes then point to remaining data in buffer */ + size_t const nbStripes = (state->bufferedSize - 1) / XXH_STRIPE_LEN; + size_t nbStripesSoFar = state->nbStripesSoFar; + XXH3_consumeStripes(acc, + &nbStripesSoFar, state->nbStripesPerBlock, + state->buffer, nbStripes, + secret, state->secretLimit, + XXH3_accumulate, XXH3_scrambleAcc); + lastStripePtr = state->buffer + state->bufferedSize - XXH_STRIPE_LEN; + } else { /* bufferedSize < XXH_STRIPE_LEN */ + /* Copy to temp buffer */ + size_t const catchupSize = XXH_STRIPE_LEN - state->bufferedSize; + XXH_ASSERT(state->bufferedSize > 0); /* there is always some input buffered */ + XXH_memcpy(lastStripe, state->buffer + sizeof(state->buffer) - catchupSize, catchupSize); + XXH_memcpy(lastStripe + catchupSize, state->buffer, state->bufferedSize); + lastStripePtr = lastStripe; + } + /* Last stripe */ + XXH3_accumulate_512(acc, + lastStripePtr, + secret + state->secretLimit - XXH_SECRET_LASTACC_START); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_digest (XXH_NOESCAPE const XXH3_state_t* state) +{ + const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret; + if (state->totalLen > XXH3_MIDSIZE_MAX) { + XXH_ALIGN(XXH_ACC_ALIGN) XXH64_hash_t acc[XXH_ACC_NB]; + XXH3_digest_long(acc, state, secret); + return XXH3_mergeAccs(acc, + secret + XXH_SECRET_MERGEACCS_START, + (xxh_u64)state->totalLen * XXH_PRIME64_1); + } + /* totalLen <= XXH3_MIDSIZE_MAX: digesting a short input */ + if (state->useSeed) + return XXH3_64bits_withSeed(state->buffer, (size_t)state->totalLen, state->seed); + return XXH3_64bits_withSecret(state->buffer, (size_t)(state->totalLen), + secret, state->secretLimit + XXH_STRIPE_LEN); +} +#endif /* !XXH_NO_STREAM */ + + +/* ========================================== + * XXH3 128 bits (a.k.a XXH128) + * ========================================== + * XXH3's 128-bit variant has better mixing and strength than the 64-bit variant, + * even without counting the significantly larger output size. + * + * For example, extra steps are taken to avoid the seed-dependent collisions + * in 17-240 byte inputs (See XXH3_mix16B and XXH128_mix32B). + * + * This strength naturally comes at the cost of some speed, especially on short + * lengths. Note that longer hashes are about as fast as the 64-bit version + * due to it using only a slight modification of the 64-bit loop. + * + * XXH128 is also more oriented towards 64-bit machines. It is still extremely + * fast for a _128-bit_ hash on 32-bit (it usually clears XXH64). + */ + +XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t +XXH3_len_1to3_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + /* A doubled version of 1to3_64b with different constants. */ + XXH_ASSERT(input != NULL); + XXH_ASSERT(1 <= len && len <= 3); + XXH_ASSERT(secret != NULL); + /* + * len = 1: combinedl = { input[0], 0x01, input[0], input[0] } + * len = 2: combinedl = { input[1], 0x02, input[0], input[1] } + * len = 3: combinedl = { input[2], 0x03, input[0], input[1] } + */ + { xxh_u8 const c1 = input[0]; + xxh_u8 const c2 = input[len >> 1]; + xxh_u8 const c3 = input[len - 1]; + xxh_u32 const combinedl = ((xxh_u32)c1 <<16) | ((xxh_u32)c2 << 24) + | ((xxh_u32)c3 << 0) | ((xxh_u32)len << 8); + xxh_u32 const combinedh = XXH_rotl32(XXH_swap32(combinedl), 13); + xxh_u64 const bitflipl = (XXH_readLE32(secret) ^ XXH_readLE32(secret+4)) + seed; + xxh_u64 const bitfliph = (XXH_readLE32(secret+8) ^ XXH_readLE32(secret+12)) - seed; + xxh_u64 const keyed_lo = (xxh_u64)combinedl ^ bitflipl; + xxh_u64 const keyed_hi = (xxh_u64)combinedh ^ bitfliph; + XXH128_hash_t h128; + h128.low64 = XXH64_avalanche(keyed_lo); + h128.high64 = XXH64_avalanche(keyed_hi); + return h128; + } +} + +XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t +XXH3_len_4to8_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(secret != NULL); + XXH_ASSERT(4 <= len && len <= 8); + seed ^= (xxh_u64)XXH_swap32((xxh_u32)seed) << 32; + { xxh_u32 const input_lo = XXH_readLE32(input); + xxh_u32 const input_hi = XXH_readLE32(input + len - 4); + xxh_u64 const input_64 = input_lo + ((xxh_u64)input_hi << 32); + xxh_u64 const bitflip = (XXH_readLE64(secret+16) ^ XXH_readLE64(secret+24)) + seed; + xxh_u64 const keyed = input_64 ^ bitflip; + + /* Shift len to the left to ensure it is even, this avoids even multiplies. */ + XXH128_hash_t m128 = XXH_mult64to128(keyed, XXH_PRIME64_1 + (len << 2)); + + m128.high64 += (m128.low64 << 1); + m128.low64 ^= (m128.high64 >> 3); + + m128.low64 = XXH_xorshift64(m128.low64, 35); + m128.low64 *= PRIME_MX2; + m128.low64 = XXH_xorshift64(m128.low64, 28); + m128.high64 = XXH3_avalanche(m128.high64); + return m128; + } +} + +XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t +XXH3_len_9to16_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(input != NULL); + XXH_ASSERT(secret != NULL); + XXH_ASSERT(9 <= len && len <= 16); + { xxh_u64 const bitflipl = (XXH_readLE64(secret+32) ^ XXH_readLE64(secret+40)) - seed; + xxh_u64 const bitfliph = (XXH_readLE64(secret+48) ^ XXH_readLE64(secret+56)) + seed; + xxh_u64 const input_lo = XXH_readLE64(input); + xxh_u64 input_hi = XXH_readLE64(input + len - 8); + XXH128_hash_t m128 = XXH_mult64to128(input_lo ^ input_hi ^ bitflipl, XXH_PRIME64_1); + /* + * Put len in the middle of m128 to ensure that the length gets mixed to + * both the low and high bits in the 128x64 multiply below. + */ + m128.low64 += (xxh_u64)(len - 1) << 54; + input_hi ^= bitfliph; + /* + * Add the high 32 bits of input_hi to the high 32 bits of m128, then + * add the long product of the low 32 bits of input_hi and XXH_PRIME32_2 to + * the high 64 bits of m128. + * + * The best approach to this operation is different on 32-bit and 64-bit. + */ + if (sizeof(void *) < sizeof(xxh_u64)) { /* 32-bit */ + /* + * 32-bit optimized version, which is more readable. + * + * On 32-bit, it removes an ADC and delays a dependency between the two + * halves of m128.high64, but it generates an extra mask on 64-bit. + */ + m128.high64 += (input_hi & 0xFFFFFFFF00000000ULL) + XXH_mult32to64((xxh_u32)input_hi, XXH_PRIME32_2); + } else { + /* + * 64-bit optimized (albeit more confusing) version. + * + * Uses some properties of addition and multiplication to remove the mask: + * + * Let: + * a = input_hi.lo = (input_hi & 0x00000000FFFFFFFF) + * b = input_hi.hi = (input_hi & 0xFFFFFFFF00000000) + * c = XXH_PRIME32_2 + * + * a + (b * c) + * Inverse Property: x + y - x == y + * a + (b * (1 + c - 1)) + * Distributive Property: x * (y + z) == (x * y) + (x * z) + * a + (b * 1) + (b * (c - 1)) + * Identity Property: x * 1 == x + * a + b + (b * (c - 1)) + * + * Substitute a, b, and c: + * input_hi.hi + input_hi.lo + ((xxh_u64)input_hi.lo * (XXH_PRIME32_2 - 1)) + * + * Since input_hi.hi + input_hi.lo == input_hi, we get this: + * input_hi + ((xxh_u64)input_hi.lo * (XXH_PRIME32_2 - 1)) + */ + m128.high64 += input_hi + XXH_mult32to64((xxh_u32)input_hi, XXH_PRIME32_2 - 1); + } + /* m128 ^= XXH_swap64(m128 >> 64); */ + m128.low64 ^= XXH_swap64(m128.high64); + + { /* 128x64 multiply: h128 = m128 * XXH_PRIME64_2; */ + XXH128_hash_t h128 = XXH_mult64to128(m128.low64, XXH_PRIME64_2); + h128.high64 += m128.high64 * XXH_PRIME64_2; + + h128.low64 = XXH3_avalanche(h128.low64); + h128.high64 = XXH3_avalanche(h128.high64); + return h128; + } } +} + +/* + * Assumption: `secret` size is >= XXH3_SECRET_SIZE_MIN + */ +XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t +XXH3_len_0to16_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed) +{ + XXH_ASSERT(len <= 16); + { if (len > 8) return XXH3_len_9to16_128b(input, len, secret, seed); + if (len >= 4) return XXH3_len_4to8_128b(input, len, secret, seed); + if (len) return XXH3_len_1to3_128b(input, len, secret, seed); + { XXH128_hash_t h128; + xxh_u64 const bitflipl = XXH_readLE64(secret+64) ^ XXH_readLE64(secret+72); + xxh_u64 const bitfliph = XXH_readLE64(secret+80) ^ XXH_readLE64(secret+88); + h128.low64 = XXH64_avalanche(seed ^ bitflipl); + h128.high64 = XXH64_avalanche( seed ^ bitfliph); + return h128; + } } +} + +/* + * A bit slower than XXH3_mix16B, but handles multiply by zero better. + */ +XXH_FORCE_INLINE XXH128_hash_t +XXH128_mix32B(XXH128_hash_t acc, const xxh_u8* input_1, const xxh_u8* input_2, + const xxh_u8* secret, XXH64_hash_t seed) +{ + acc.low64 += XXH3_mix16B (input_1, secret+0, seed); + acc.low64 ^= XXH_readLE64(input_2) + XXH_readLE64(input_2 + 8); + acc.high64 += XXH3_mix16B (input_2, secret+16, seed); + acc.high64 ^= XXH_readLE64(input_1) + XXH_readLE64(input_1 + 8); + return acc; +} + + +XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t +XXH3_len_17to128_128b(const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH64_hash_t seed) +{ + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize; + XXH_ASSERT(16 < len && len <= 128); + + { XXH128_hash_t acc; + acc.low64 = len * XXH_PRIME64_1; + acc.high64 = 0; + +#if XXH_SIZE_OPT >= 1 + { + /* Smaller, but slightly slower. */ + unsigned int i = (unsigned int)(len - 1) / 32; + do { + acc = XXH128_mix32B(acc, input+16*i, input+len-16*(i+1), secret+32*i, seed); + } while (i-- != 0); + } +#else + if (len > 32) { + if (len > 64) { + if (len > 96) { + acc = XXH128_mix32B(acc, input+48, input+len-64, secret+96, seed); + } + acc = XXH128_mix32B(acc, input+32, input+len-48, secret+64, seed); + } + acc = XXH128_mix32B(acc, input+16, input+len-32, secret+32, seed); + } + acc = XXH128_mix32B(acc, input, input+len-16, secret, seed); +#endif + { XXH128_hash_t h128; + h128.low64 = acc.low64 + acc.high64; + h128.high64 = (acc.low64 * XXH_PRIME64_1) + + (acc.high64 * XXH_PRIME64_4) + + ((len - seed) * XXH_PRIME64_2); + h128.low64 = XXH3_avalanche(h128.low64); + h128.high64 = (XXH64_hash_t)0 - XXH3_avalanche(h128.high64); + return h128; + } + } +} + +XXH_NO_INLINE XXH_PUREF XXH128_hash_t +XXH3_len_129to240_128b(const xxh_u8* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH64_hash_t seed) +{ + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize; + XXH_ASSERT(128 < len && len <= XXH3_MIDSIZE_MAX); + + { XXH128_hash_t acc; + unsigned i; + acc.low64 = len * XXH_PRIME64_1; + acc.high64 = 0; + /* + * We set as `i` as offset + 32. We do this so that unchanged + * `len` can be used as upper bound. This reaches a sweet spot + * where both x86 and aarch64 get simple agen and good codegen + * for the loop. + */ + for (i = 32; i < 160; i += 32) { + acc = XXH128_mix32B(acc, + input + i - 32, + input + i - 16, + secret + i - 32, + seed); + } + acc.low64 = XXH3_avalanche(acc.low64); + acc.high64 = XXH3_avalanche(acc.high64); + /* + * NB: `i <= len` will duplicate the last 32-bytes if + * len % 32 was zero. This is an unfortunate necessity to keep + * the hash result stable. + */ + for (i=160; i <= len; i += 32) { + acc = XXH128_mix32B(acc, + input + i - 32, + input + i - 16, + secret + XXH3_MIDSIZE_STARTOFFSET + i - 160, + seed); + } + /* last bytes */ + acc = XXH128_mix32B(acc, + input + len - 16, + input + len - 32, + secret + XXH3_SECRET_SIZE_MIN - XXH3_MIDSIZE_LASTOFFSET - 16, + (XXH64_hash_t)0 - seed); + + { XXH128_hash_t h128; + h128.low64 = acc.low64 + acc.high64; + h128.high64 = (acc.low64 * XXH_PRIME64_1) + + (acc.high64 * XXH_PRIME64_4) + + ((len - seed) * XXH_PRIME64_2); + h128.low64 = XXH3_avalanche(h128.low64); + h128.high64 = (XXH64_hash_t)0 - XXH3_avalanche(h128.high64); + return h128; + } + } +} + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_hashLong_128b_internal(const void* XXH_RESTRICT input, size_t len, + const xxh_u8* XXH_RESTRICT secret, size_t secretSize, + XXH3_f_accumulate f_acc, + XXH3_f_scrambleAcc f_scramble) +{ + XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[XXH_ACC_NB] = XXH3_INIT_ACC; + + XXH3_hashLong_internal_loop(acc, (const xxh_u8*)input, len, secret, secretSize, f_acc, f_scramble); + + /* converge into final hash */ + XXH_STATIC_ASSERT(sizeof(acc) == 64); + XXH_ASSERT(secretSize >= sizeof(acc) + XXH_SECRET_MERGEACCS_START); + { XXH128_hash_t h128; + h128.low64 = XXH3_mergeAccs(acc, + secret + XXH_SECRET_MERGEACCS_START, + (xxh_u64)len * XXH_PRIME64_1); + h128.high64 = XXH3_mergeAccs(acc, + secret + secretSize + - sizeof(acc) - XXH_SECRET_MERGEACCS_START, + ~((xxh_u64)len * XXH_PRIME64_2)); + return h128; + } +} + +/* + * It's important for performance that XXH3_hashLong() is not inlined. + */ +XXH_NO_INLINE XXH_PUREF XXH128_hash_t +XXH3_hashLong_128b_default(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, + const void* XXH_RESTRICT secret, size_t secretLen) +{ + (void)seed64; (void)secret; (void)secretLen; + return XXH3_hashLong_128b_internal(input, len, XXH3_kSecret, sizeof(XXH3_kSecret), + XXH3_accumulate, XXH3_scrambleAcc); +} + +/* + * It's important for performance to pass @p secretLen (when it's static) + * to the compiler, so that it can properly optimize the vectorized loop. + * + * When the secret size is unknown, or on GCC 12 where the mix of NO_INLINE and FORCE_INLINE + * breaks -Og, this is XXH_NO_INLINE. + */ +XXH3_WITH_SECRET_INLINE XXH128_hash_t +XXH3_hashLong_128b_withSecret(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, + const void* XXH_RESTRICT secret, size_t secretLen) +{ + (void)seed64; + return XXH3_hashLong_128b_internal(input, len, (const xxh_u8*)secret, secretLen, + XXH3_accumulate, XXH3_scrambleAcc); +} + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_hashLong_128b_withSeed_internal(const void* XXH_RESTRICT input, size_t len, + XXH64_hash_t seed64, + XXH3_f_accumulate f_acc, + XXH3_f_scrambleAcc f_scramble, + XXH3_f_initCustomSecret f_initSec) +{ + if (seed64 == 0) + return XXH3_hashLong_128b_internal(input, len, + XXH3_kSecret, sizeof(XXH3_kSecret), + f_acc, f_scramble); + { XXH_ALIGN(XXH_SEC_ALIGN) xxh_u8 secret[XXH_SECRET_DEFAULT_SIZE]; + f_initSec(secret, seed64); + return XXH3_hashLong_128b_internal(input, len, (const xxh_u8*)secret, sizeof(secret), + f_acc, f_scramble); + } +} + +/* + * It's important for performance that XXH3_hashLong is not inlined. + */ +XXH_NO_INLINE XXH128_hash_t +XXH3_hashLong_128b_withSeed(const void* input, size_t len, + XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen) +{ + (void)secret; (void)secretLen; + return XXH3_hashLong_128b_withSeed_internal(input, len, seed64, + XXH3_accumulate, XXH3_scrambleAcc, XXH3_initCustomSecret); +} + +typedef XXH128_hash_t (*XXH3_hashLong128_f)(const void* XXH_RESTRICT, size_t, + XXH64_hash_t, const void* XXH_RESTRICT, size_t); + +XXH_FORCE_INLINE XXH128_hash_t +XXH3_128bits_internal(const void* input, size_t len, + XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen, + XXH3_hashLong128_f f_hl128) +{ + XXH_ASSERT(secretLen >= XXH3_SECRET_SIZE_MIN); + /* + * If an action is to be taken if `secret` conditions are not respected, + * it should be done here. + * For now, it's a contract pre-condition. + * Adding a check and a branch here would cost performance at every hash. + */ + if (len <= 16) + return XXH3_len_0to16_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, seed64); + if (len <= 128) + return XXH3_len_17to128_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); + if (len <= XXH3_MIDSIZE_MAX) + return XXH3_len_129to240_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64); + return f_hl128(input, len, seed64, secret, secretLen); +} + + +/* === Public XXH128 API === */ + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH128_hash_t XXH3_128bits(XXH_NOESCAPE const void* input, size_t len) +{ + return XXH3_128bits_internal(input, len, 0, + XXH3_kSecret, sizeof(XXH3_kSecret), + XXH3_hashLong_128b_default); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH3_128bits_withSecret(XXH_NOESCAPE const void* input, size_t len, XXH_NOESCAPE const void* secret, size_t secretSize) +{ + return XXH3_128bits_internal(input, len, 0, + (const xxh_u8*)secret, secretSize, + XXH3_hashLong_128b_withSecret); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH3_128bits_withSeed(XXH_NOESCAPE const void* input, size_t len, XXH64_hash_t seed) +{ + return XXH3_128bits_internal(input, len, seed, + XXH3_kSecret, sizeof(XXH3_kSecret), + XXH3_hashLong_128b_withSeed); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH3_128bits_withSecretandSeed(XXH_NOESCAPE const void* input, size_t len, XXH_NOESCAPE const void* secret, size_t secretSize, XXH64_hash_t seed) +{ + if (len <= XXH3_MIDSIZE_MAX) + return XXH3_128bits_internal(input, len, seed, XXH3_kSecret, sizeof(XXH3_kSecret), NULL); + return XXH3_hashLong_128b_withSecret(input, len, seed, secret, secretSize); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH128(XXH_NOESCAPE const void* input, size_t len, XXH64_hash_t seed) +{ + return XXH3_128bits_withSeed(input, len, seed); +} + + +/* === XXH3 128-bit streaming === */ +#ifndef XXH_NO_STREAM +/* + * All initialization and update functions are identical to 64-bit streaming variant. + * The only difference is the finalization routine. + */ + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_reset(XXH_NOESCAPE XXH3_state_t* statePtr) +{ + return XXH3_64bits_reset(statePtr); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_reset_withSecret(XXH_NOESCAPE XXH3_state_t* statePtr, XXH_NOESCAPE const void* secret, size_t secretSize) +{ + return XXH3_64bits_reset_withSecret(statePtr, secret, secretSize); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_reset_withSeed(XXH_NOESCAPE XXH3_state_t* statePtr, XXH64_hash_t seed) +{ + return XXH3_64bits_reset_withSeed(statePtr, seed); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_reset_withSecretandSeed(XXH_NOESCAPE XXH3_state_t* statePtr, XXH_NOESCAPE const void* secret, size_t secretSize, XXH64_hash_t seed) +{ + return XXH3_64bits_reset_withSecretandSeed(statePtr, secret, secretSize, seed); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_128bits_update(XXH_NOESCAPE XXH3_state_t* state, XXH_NOESCAPE const void* input, size_t len) +{ + return XXH3_64bits_update(state, input, len); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_digest (XXH_NOESCAPE const XXH3_state_t* state) +{ + const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret; + if (state->totalLen > XXH3_MIDSIZE_MAX) { + XXH_ALIGN(XXH_ACC_ALIGN) XXH64_hash_t acc[XXH_ACC_NB]; + XXH3_digest_long(acc, state, secret); + XXH_ASSERT(state->secretLimit + XXH_STRIPE_LEN >= sizeof(acc) + XXH_SECRET_MERGEACCS_START); + { XXH128_hash_t h128; + h128.low64 = XXH3_mergeAccs(acc, + secret + XXH_SECRET_MERGEACCS_START, + (xxh_u64)state->totalLen * XXH_PRIME64_1); + h128.high64 = XXH3_mergeAccs(acc, + secret + state->secretLimit + XXH_STRIPE_LEN + - sizeof(acc) - XXH_SECRET_MERGEACCS_START, + ~((xxh_u64)state->totalLen * XXH_PRIME64_2)); + return h128; + } + } + /* len <= XXH3_MIDSIZE_MAX : short code */ + if (state->seed) + return XXH3_128bits_withSeed(state->buffer, (size_t)state->totalLen, state->seed); + return XXH3_128bits_withSecret(state->buffer, (size_t)(state->totalLen), + secret, state->secretLimit + XXH_STRIPE_LEN); +} +#endif /* !XXH_NO_STREAM */ +/* 128-bit utility functions */ + +#include /* memcmp, memcpy */ + +/* return : 1 is equal, 0 if different */ +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API int XXH128_isEqual(XXH128_hash_t h1, XXH128_hash_t h2) +{ + /* note : XXH128_hash_t is compact, it has no padding byte */ + return !(memcmp(&h1, &h2, sizeof(h1))); +} + +/* This prototype is compatible with stdlib's qsort(). + * @return : >0 if *h128_1 > *h128_2 + * <0 if *h128_1 < *h128_2 + * =0 if *h128_1 == *h128_2 */ +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API int XXH128_cmp(XXH_NOESCAPE const void* h128_1, XXH_NOESCAPE const void* h128_2) +{ + XXH128_hash_t const h1 = *(const XXH128_hash_t*)h128_1; + XXH128_hash_t const h2 = *(const XXH128_hash_t*)h128_2; + int const hcmp = (h1.high64 > h2.high64) - (h2.high64 > h1.high64); + /* note : bets that, in most cases, hash values are different */ + if (hcmp) return hcmp; + return (h1.low64 > h2.low64) - (h2.low64 > h1.low64); +} + + +/*====== Canonical representation ======*/ +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API void +XXH128_canonicalFromHash(XXH_NOESCAPE XXH128_canonical_t* dst, XXH128_hash_t hash) +{ + XXH_STATIC_ASSERT(sizeof(XXH128_canonical_t) == sizeof(XXH128_hash_t)); + if (XXH_CPU_LITTLE_ENDIAN) { + hash.high64 = XXH_swap64(hash.high64); + hash.low64 = XXH_swap64(hash.low64); + } + XXH_memcpy(dst, &hash.high64, sizeof(hash.high64)); + XXH_memcpy((char*)dst + sizeof(hash.high64), &hash.low64, sizeof(hash.low64)); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH128_hash_t +XXH128_hashFromCanonical(XXH_NOESCAPE const XXH128_canonical_t* src) +{ + XXH128_hash_t h; + h.high64 = XXH_readBE64(src); + h.low64 = XXH_readBE64(src->digest + 8); + return h; +} + + + +/* ========================================== + * Secret generators + * ========================================== + */ +#define XXH_MIN(x, y) (((x) > (y)) ? (y) : (x)) + +XXH_FORCE_INLINE void XXH3_combine16(void* dst, XXH128_hash_t h128) +{ + XXH_writeLE64( dst, XXH_readLE64(dst) ^ h128.low64 ); + XXH_writeLE64( (char*)dst+8, XXH_readLE64((char*)dst+8) ^ h128.high64 ); +} + +/*! @ingroup XXH3_family */ +XXH_PUBLIC_API XXH_errorcode +XXH3_generateSecret(XXH_NOESCAPE void* secretBuffer, size_t secretSize, XXH_NOESCAPE const void* customSeed, size_t customSeedSize) +{ +#if (XXH_DEBUGLEVEL >= 1) + XXH_ASSERT(secretBuffer != NULL); + XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); +#else + /* production mode, assert() are disabled */ + if (secretBuffer == NULL) return XXH_ERROR; + if (secretSize < XXH3_SECRET_SIZE_MIN) return XXH_ERROR; +#endif + + if (customSeedSize == 0) { + customSeed = XXH3_kSecret; + customSeedSize = XXH_SECRET_DEFAULT_SIZE; + } +#if (XXH_DEBUGLEVEL >= 1) + XXH_ASSERT(customSeed != NULL); +#else + if (customSeed == NULL) return XXH_ERROR; +#endif + + /* Fill secretBuffer with a copy of customSeed - repeat as needed */ + { size_t pos = 0; + while (pos < secretSize) { + size_t const toCopy = XXH_MIN((secretSize - pos), customSeedSize); + memcpy((char*)secretBuffer + pos, customSeed, toCopy); + pos += toCopy; + } } + + { size_t const nbSeg16 = secretSize / 16; + size_t n; + XXH128_canonical_t scrambler; + XXH128_canonicalFromHash(&scrambler, XXH128(customSeed, customSeedSize, 0)); + for (n=0; n + +/* weak symbol support + * For now, enable conservatively: + * - Only GNUC + * - Only ELF + * - Only x86-64, i386 and aarch64 + * Also, explicitly disable on platforms known not to work so they aren't + * forgotten in the future. + */ +#if !defined(ZSTD_HAVE_WEAK_SYMBOLS) && \ + defined(__GNUC__) && defined(__ELF__) && \ + (defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86) || defined(__aarch64__)) && \ + !defined(__APPLE__) && !defined(_WIN32) && !defined(__MINGW32__) && \ + !defined(__CYGWIN__) && !defined(_AIX) +# define ZSTD_HAVE_WEAK_SYMBOLS 1 +#else +# define ZSTD_HAVE_WEAK_SYMBOLS 0 +#endif +#if ZSTD_HAVE_WEAK_SYMBOLS +# define ZSTD_WEAK_ATTR __attribute__((__weak__)) +#else +# define ZSTD_WEAK_ATTR +#endif + +/* Only enable tracing when weak symbols are available. */ +#ifndef ZSTD_TRACE +# define ZSTD_TRACE ZSTD_HAVE_WEAK_SYMBOLS +#endif + +#if ZSTD_TRACE + +struct ZSTD_CCtx_s; +struct ZSTD_DCtx_s; +struct ZSTD_CCtx_params_s; + +typedef struct { + /** + * ZSTD_VERSION_NUMBER + * + * This is guaranteed to be the first member of ZSTD_trace. + * Otherwise, this struct is not stable between versions. If + * the version number does not match your expectation, you + * should not interpret the rest of the struct. + */ + unsigned version; + /** + * Non-zero if streaming (de)compression is used. + */ + unsigned streaming; + /** + * The dictionary ID. + */ + unsigned dictionaryID; + /** + * Is the dictionary cold? + * Only set on decompression. + */ + unsigned dictionaryIsCold; + /** + * The dictionary size or zero if no dictionary. + */ + size_t dictionarySize; + /** + * The uncompressed size of the data. + */ + size_t uncompressedSize; + /** + * The compressed size of the data. + */ + size_t compressedSize; + /** + * The fully resolved CCtx parameters (NULL on decompression). + */ + struct ZSTD_CCtx_params_s const* params; + /** + * The ZSTD_CCtx pointer (NULL on decompression). + */ + struct ZSTD_CCtx_s const* cctx; + /** + * The ZSTD_DCtx pointer (NULL on compression). + */ + struct ZSTD_DCtx_s const* dctx; +} ZSTD_Trace; + +/** + * A tracing context. It must be 0 when tracing is disabled. + * Otherwise, any non-zero value returned by a tracing begin() + * function is presented to any subsequent calls to end(). + * + * Any non-zero value is treated as tracing is enabled and not + * interpreted by the library. + * + * Two possible uses are: + * * A timestamp for when the begin() function was called. + * * A unique key identifying the (de)compression, like the + * address of the [dc]ctx pointer if you need to track + * more information than just a timestamp. + */ +typedef unsigned long long ZSTD_TraceCtx; + +/** + * Trace the beginning of a compression call. + * @param cctx The dctx pointer for the compression. + * It can be used as a key to map begin() to end(). + * @returns Non-zero if tracing is enabled. The return value is + * passed to ZSTD_trace_compress_end(). + */ +ZSTD_WEAK_ATTR ZSTD_TraceCtx ZSTD_trace_compress_begin( + struct ZSTD_CCtx_s const* cctx); + +/** + * Trace the end of a compression call. + * @param ctx The return value of ZSTD_trace_compress_begin(). + * @param trace The zstd tracing info. + */ +ZSTD_WEAK_ATTR void ZSTD_trace_compress_end( + ZSTD_TraceCtx ctx, + ZSTD_Trace const* trace); + +/** + * Trace the beginning of a decompression call. + * @param dctx The dctx pointer for the decompression. + * It can be used as a key to map begin() to end(). + * @returns Non-zero if tracing is enabled. The return value is + * passed to ZSTD_trace_compress_end(). + */ +ZSTD_WEAK_ATTR ZSTD_TraceCtx ZSTD_trace_decompress_begin( + struct ZSTD_DCtx_s const* dctx); + +/** + * Trace the end of a decompression call. + * @param ctx The return value of ZSTD_trace_decompress_begin(). + * @param trace The zstd tracing info. + */ +ZSTD_WEAK_ATTR void ZSTD_trace_decompress_end( + ZSTD_TraceCtx ctx, + ZSTD_Trace const* trace); + +#endif /* ZSTD_TRACE */ + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_TRACE_H */ +/**** ended inlining zstd_trace.h ****/ +#else +# define ZSTD_TRACE 0 +#endif + +#if defined (__cplusplus) +extern "C" { +#endif + +/* ---- static assert (debug) --- */ +#define ZSTD_STATIC_ASSERT(c) DEBUG_STATIC_ASSERT(c) +#define ZSTD_isError ERR_isError /* for inlining */ +#define FSE_isError ERR_isError +#define HUF_isError ERR_isError + + +/*-************************************* +* shared macros +***************************************/ +#undef MIN +#undef MAX +#define MIN(a,b) ((a)<(b) ? (a) : (b)) +#define MAX(a,b) ((a)>(b) ? (a) : (b)) +#define BOUNDED(min,val,max) (MAX(min,MIN(val,max))) + + +/*-************************************* +* Common constants +***************************************/ +#define ZSTD_OPT_NUM (1<<12) + +#define ZSTD_REP_NUM 3 /* number of repcodes */ +static UNUSED_ATTR const U32 repStartValue[ZSTD_REP_NUM] = { 1, 4, 8 }; + +#define KB *(1 <<10) +#define MB *(1 <<20) +#define GB *(1U<<30) + +#define BIT7 128 +#define BIT6 64 +#define BIT5 32 +#define BIT4 16 +#define BIT1 2 +#define BIT0 1 + +#define ZSTD_WINDOWLOG_ABSOLUTEMIN 10 +static UNUSED_ATTR const size_t ZSTD_fcs_fieldSize[4] = { 0, 2, 4, 8 }; +static UNUSED_ATTR const size_t ZSTD_did_fieldSize[4] = { 0, 1, 2, 4 }; + +#define ZSTD_FRAMEIDSIZE 4 /* magic number size */ + +#define ZSTD_BLOCKHEADERSIZE 3 /* C standard doesn't allow `static const` variable to be init using another `static const` variable */ +static UNUSED_ATTR const size_t ZSTD_blockHeaderSize = ZSTD_BLOCKHEADERSIZE; +typedef enum { bt_raw, bt_rle, bt_compressed, bt_reserved } blockType_e; + +#define ZSTD_FRAMECHECKSUMSIZE 4 + +#define MIN_SEQUENCES_SIZE 1 /* nbSeq==0 */ +#define MIN_CBLOCK_SIZE (1 /*litCSize*/ + 1 /* RLE or RAW */) /* for a non-null block */ +#define MIN_LITERALS_FOR_4_STREAMS 6 + +typedef enum { set_basic, set_rle, set_compressed, set_repeat } symbolEncodingType_e; + +#define LONGNBSEQ 0x7F00 + +#define MINMATCH 3 + +#define Litbits 8 +#define LitHufLog 11 +#define MaxLit ((1<= WILDCOPY_VECLEN || diff <= -WILDCOPY_VECLEN); + /* Separate out the first COPY16() call because the copy length is + * almost certain to be short, so the branches have different + * probabilities. Since it is almost certain to be short, only do + * one COPY16() in the first call. Then, do two calls per loop since + * at that point it is more likely to have a high trip count. + */ + ZSTD_copy16(op, ip); + if (16 >= length) return; + op += 16; + ip += 16; + do { + COPY16(op, ip); + COPY16(op, ip); + } + while (op < oend); + } +} + +MEM_STATIC size_t ZSTD_limitCopy(void* dst, size_t dstCapacity, const void* src, size_t srcSize) +{ + size_t const length = MIN(dstCapacity, srcSize); + if (length > 0) { + ZSTD_memcpy(dst, src, length); + } + return length; +} + +/* define "workspace is too large" as this number of times larger than needed */ +#define ZSTD_WORKSPACETOOLARGE_FACTOR 3 + +/* when workspace is continuously too large + * during at least this number of times, + * context's memory usage is considered wasteful, + * because it's sized to handle a worst case scenario which rarely happens. + * In which case, resize it down to free some memory */ +#define ZSTD_WORKSPACETOOLARGE_MAXDURATION 128 + +/* Controls whether the input/output buffer is buffered or stable. */ +typedef enum { + ZSTD_bm_buffered = 0, /* Buffer the input/output */ + ZSTD_bm_stable = 1 /* ZSTD_inBuffer/ZSTD_outBuffer is stable */ +} ZSTD_bufferMode_e; + + +/*-******************************************* +* Private declarations +*********************************************/ +typedef struct seqDef_s { + U32 offBase; /* offBase == Offset + ZSTD_REP_NUM, or repcode 1,2,3 */ + U16 litLength; + U16 mlBase; /* mlBase == matchLength - MINMATCH */ +} seqDef; + +/* Controls whether seqStore has a single "long" litLength or matchLength. See seqStore_t. */ +typedef enum { + ZSTD_llt_none = 0, /* no longLengthType */ + ZSTD_llt_literalLength = 1, /* represents a long literal */ + ZSTD_llt_matchLength = 2 /* represents a long match */ +} ZSTD_longLengthType_e; + +typedef struct { + seqDef* sequencesStart; + seqDef* sequences; /* ptr to end of sequences */ + BYTE* litStart; + BYTE* lit; /* ptr to end of literals */ + BYTE* llCode; + BYTE* mlCode; + BYTE* ofCode; + size_t maxNbSeq; + size_t maxNbLit; + + /* longLengthPos and longLengthType to allow us to represent either a single litLength or matchLength + * in the seqStore that has a value larger than U16 (if it exists). To do so, we increment + * the existing value of the litLength or matchLength by 0x10000. + */ + ZSTD_longLengthType_e longLengthType; + U32 longLengthPos; /* Index of the sequence to apply long length modification to */ +} seqStore_t; + +typedef struct { + U32 litLength; + U32 matchLength; +} ZSTD_sequenceLength; + +/** + * Returns the ZSTD_sequenceLength for the given sequences. It handles the decoding of long sequences + * indicated by longLengthPos and longLengthType, and adds MINMATCH back to matchLength. + */ +MEM_STATIC ZSTD_sequenceLength ZSTD_getSequenceLength(seqStore_t const* seqStore, seqDef const* seq) +{ + ZSTD_sequenceLength seqLen; + seqLen.litLength = seq->litLength; + seqLen.matchLength = seq->mlBase + MINMATCH; + if (seqStore->longLengthPos == (U32)(seq - seqStore->sequencesStart)) { + if (seqStore->longLengthType == ZSTD_llt_literalLength) { + seqLen.litLength += 0x10000; + } + if (seqStore->longLengthType == ZSTD_llt_matchLength) { + seqLen.matchLength += 0x10000; + } + } + return seqLen; +} + +/** + * Contains the compressed frame size and an upper-bound for the decompressed frame size. + * Note: before using `compressedSize`, check for errors using ZSTD_isError(). + * similarly, before using `decompressedBound`, check for errors using: + * `decompressedBound != ZSTD_CONTENTSIZE_ERROR` + */ +typedef struct { + size_t nbBlocks; + size_t compressedSize; + unsigned long long decompressedBound; +} ZSTD_frameSizeInfo; /* decompress & legacy */ + +const seqStore_t* ZSTD_getSeqStore(const ZSTD_CCtx* ctx); /* compress & dictBuilder */ +int ZSTD_seqToCodes(const seqStore_t* seqStorePtr); /* compress, dictBuilder, decodeCorpus (shouldn't get its definition from here) */ + + +/* ZSTD_invalidateRepCodes() : + * ensures next compression will not use repcodes from previous block. + * Note : only works with regular variant; + * do not use with extDict variant ! */ +void ZSTD_invalidateRepCodes(ZSTD_CCtx* cctx); /* zstdmt, adaptive_compression (shouldn't get this definition from here) */ + + +typedef struct { + blockType_e blockType; + U32 lastBlock; + U32 origSize; +} blockProperties_t; /* declared here for decompress and fullbench */ + +/*! ZSTD_getcBlockSize() : + * Provides the size of compressed block from block header `src` */ +/* Used by: decompress, fullbench */ +size_t ZSTD_getcBlockSize(const void* src, size_t srcSize, + blockProperties_t* bpPtr); + +/*! ZSTD_decodeSeqHeaders() : + * decode sequence header from src */ +/* Used by: zstd_decompress_block, fullbench */ +size_t ZSTD_decodeSeqHeaders(ZSTD_DCtx* dctx, int* nbSeqPtr, + const void* src, size_t srcSize); + +/** + * @returns true iff the CPU supports dynamic BMI2 dispatch. + */ +MEM_STATIC int ZSTD_cpuSupportsBmi2(void) +{ + ZSTD_cpuid_t cpuid = ZSTD_cpuid(); + return ZSTD_cpuid_bmi1(cpuid) && ZSTD_cpuid_bmi2(cpuid); +} + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_CCOMMON_H_MODULE */ +/**** ended inlining zstd_internal.h ****/ + + +/*-**************************************** +* Version +******************************************/ +unsigned ZSTD_versionNumber(void) { return ZSTD_VERSION_NUMBER; } + +const char* ZSTD_versionString(void) { return ZSTD_VERSION_STRING; } + + +/*-**************************************** +* ZSTD Error Management +******************************************/ +#undef ZSTD_isError /* defined within zstd_internal.h */ +/*! ZSTD_isError() : + * tells if a return value is an error code + * symbol is required for external callers */ +unsigned ZSTD_isError(size_t code) { return ERR_isError(code); } + +/*! ZSTD_getErrorName() : + * provides error code string from function result (useful for debugging) */ +const char* ZSTD_getErrorName(size_t code) { return ERR_getErrorName(code); } + +/*! ZSTD_getError() : + * convert a `size_t` function result into a proper ZSTD_errorCode enum */ +ZSTD_ErrorCode ZSTD_getErrorCode(size_t code) { return ERR_getErrorCode(code); } + +/*! ZSTD_getErrorString() : + * provides error code string from enum */ +const char* ZSTD_getErrorString(ZSTD_ErrorCode code) { return ERR_getErrorString(code); } +/**** ended inlining common/zstd_common.c ****/ + +/**** start inlining compress/fse_compress.c ****/ +/* ****************************************************************** + * FSE : Finite State Entropy encoder + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - FSE source repository : https://github.com/Cyan4973/FiniteStateEntropy + * - Public forum : https://groups.google.com/forum/#!forum/lz4c + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + +/* ************************************************************** +* Includes +****************************************************************/ +/**** skipping file: ../common/compiler.h ****/ +/**** skipping file: ../common/mem.h ****/ +/**** skipping file: ../common/debug.h ****/ +/**** start inlining hist.h ****/ +/* ****************************************************************** + * hist : Histogram functions + * part of Finite State Entropy project + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - FSE source repository : https://github.com/Cyan4973/FiniteStateEntropy + * - Public forum : https://groups.google.com/forum/#!forum/lz4c + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + +/* --- dependencies --- */ +/**** skipping file: ../common/zstd_deps.h ****/ + + +/* --- simple histogram functions --- */ + +/*! HIST_count(): + * Provides the precise count of each byte within a table 'count'. + * 'count' is a table of unsigned int, of minimum size (*maxSymbolValuePtr+1). + * Updates *maxSymbolValuePtr with actual largest symbol value detected. + * @return : count of the most frequent symbol (which isn't identified). + * or an error code, which can be tested using HIST_isError(). + * note : if return == srcSize, there is only one symbol. + */ +size_t HIST_count(unsigned* count, unsigned* maxSymbolValuePtr, + const void* src, size_t srcSize); + +unsigned HIST_isError(size_t code); /**< tells if a return value is an error code */ + + +/* --- advanced histogram functions --- */ + +#define HIST_WKSP_SIZE_U32 1024 +#define HIST_WKSP_SIZE (HIST_WKSP_SIZE_U32 * sizeof(unsigned)) +/** HIST_count_wksp() : + * Same as HIST_count(), but using an externally provided scratch buffer. + * Benefit is this function will use very little stack space. + * `workSpace` is a writable buffer which must be 4-bytes aligned, + * `workSpaceSize` must be >= HIST_WKSP_SIZE + */ +size_t HIST_count_wksp(unsigned* count, unsigned* maxSymbolValuePtr, + const void* src, size_t srcSize, + void* workSpace, size_t workSpaceSize); + +/** HIST_countFast() : + * same as HIST_count(), but blindly trusts that all byte values within src are <= *maxSymbolValuePtr. + * This function is unsafe, and will segfault if any value within `src` is `> *maxSymbolValuePtr` + */ +size_t HIST_countFast(unsigned* count, unsigned* maxSymbolValuePtr, + const void* src, size_t srcSize); + +/** HIST_countFast_wksp() : + * Same as HIST_countFast(), but using an externally provided scratch buffer. + * `workSpace` is a writable buffer which must be 4-bytes aligned, + * `workSpaceSize` must be >= HIST_WKSP_SIZE + */ +size_t HIST_countFast_wksp(unsigned* count, unsigned* maxSymbolValuePtr, + const void* src, size_t srcSize, + void* workSpace, size_t workSpaceSize); + +/*! HIST_count_simple() : + * Same as HIST_countFast(), this function is unsafe, + * and will segfault if any value within `src` is `> *maxSymbolValuePtr`. + * It is also a bit slower for large inputs. + * However, it does not need any additional memory (not even on stack). + * @return : count of the most frequent symbol. + * Note this function doesn't produce any error (i.e. it must succeed). + */ +unsigned HIST_count_simple(unsigned* count, unsigned* maxSymbolValuePtr, + const void* src, size_t srcSize); +/**** ended inlining hist.h ****/ +/**** skipping file: ../common/bitstream.h ****/ +#define FSE_STATIC_LINKING_ONLY +/**** skipping file: ../common/fse.h ****/ +/**** skipping file: ../common/error_private.h ****/ +#define ZSTD_DEPS_NEED_MALLOC +#define ZSTD_DEPS_NEED_MATH64 +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../common/bits.h ****/ + + +/* ************************************************************** +* Error Management +****************************************************************/ +#define FSE_isError ERR_isError + + +/* ************************************************************** +* Templates +****************************************************************/ +/* + designed to be included + for type-specific functions (template emulation in C) + Objective is to write these functions only once, for improved maintenance +*/ + +/* safety checks */ +#ifndef FSE_FUNCTION_EXTENSION +# error "FSE_FUNCTION_EXTENSION must be defined" +#endif +#ifndef FSE_FUNCTION_TYPE +# error "FSE_FUNCTION_TYPE must be defined" +#endif + +/* Function names */ +#define FSE_CAT(X,Y) X##Y +#define FSE_FUNCTION_NAME(X,Y) FSE_CAT(X,Y) +#define FSE_TYPE_NAME(X,Y) FSE_CAT(X,Y) + + +/* Function templates */ + +/* FSE_buildCTable_wksp() : + * Same as FSE_buildCTable(), but using an externally allocated scratch buffer (`workSpace`). + * wkspSize should be sized to handle worst case situation, which is `1<>1 : 1) ; + FSE_symbolCompressionTransform* const symbolTT = (FSE_symbolCompressionTransform*) (FSCT); + U32 const step = FSE_TABLESTEP(tableSize); + U32 const maxSV1 = maxSymbolValue+1; + + U16* cumul = (U16*)workSpace; /* size = maxSV1 */ + FSE_FUNCTION_TYPE* const tableSymbol = (FSE_FUNCTION_TYPE*)(cumul + (maxSV1+1)); /* size = tableSize */ + + U32 highThreshold = tableSize-1; + + assert(((size_t)workSpace & 1) == 0); /* Must be 2 bytes-aligned */ + if (FSE_BUILD_CTABLE_WORKSPACE_SIZE(maxSymbolValue, tableLog) > wkspSize) return ERROR(tableLog_tooLarge); + /* CTable header */ + tableU16[-2] = (U16) tableLog; + tableU16[-1] = (U16) maxSymbolValue; + assert(tableLog < 16); /* required for threshold strategy to work */ + + /* For explanations on how to distribute symbol values over the table : + * https://fastcompression.blogspot.fr/2014/02/fse-distributing-symbol-values.html */ + + #ifdef __clang_analyzer__ + ZSTD_memset(tableSymbol, 0, sizeof(*tableSymbol) * tableSize); /* useless initialization, just to keep scan-build happy */ + #endif + + /* symbol start positions */ + { U32 u; + cumul[0] = 0; + for (u=1; u <= maxSV1; u++) { + if (normalizedCounter[u-1]==-1) { /* Low proba symbol */ + cumul[u] = cumul[u-1] + 1; + tableSymbol[highThreshold--] = (FSE_FUNCTION_TYPE)(u-1); + } else { + assert(normalizedCounter[u-1] >= 0); + cumul[u] = cumul[u-1] + (U16)normalizedCounter[u-1]; + assert(cumul[u] >= cumul[u-1]); /* no overflow */ + } } + cumul[maxSV1] = (U16)(tableSize+1); + } + + /* Spread symbols */ + if (highThreshold == tableSize - 1) { + /* Case for no low prob count symbols. Lay down 8 bytes at a time + * to reduce branch misses since we are operating on a small block + */ + BYTE* const spread = tableSymbol + tableSize; /* size = tableSize + 8 (may write beyond tableSize) */ + { U64 const add = 0x0101010101010101ull; + size_t pos = 0; + U64 sv = 0; + U32 s; + for (s=0; s=0); + pos += (size_t)n; + } + } + /* Spread symbols across the table. Lack of lowprob symbols means that + * we don't need variable sized inner loop, so we can unroll the loop and + * reduce branch misses. + */ + { size_t position = 0; + size_t s; + size_t const unroll = 2; /* Experimentally determined optimal unroll */ + assert(tableSize % unroll == 0); /* FSE_MIN_TABLELOG is 5 */ + for (s = 0; s < (size_t)tableSize; s += unroll) { + size_t u; + for (u = 0; u < unroll; ++u) { + size_t const uPosition = (position + (u * step)) & tableMask; + tableSymbol[uPosition] = spread[s + u]; + } + position = (position + (unroll * step)) & tableMask; + } + assert(position == 0); /* Must have initialized all positions */ + } + } else { + U32 position = 0; + U32 symbol; + for (symbol=0; symbol highThreshold) + position = (position + step) & tableMask; /* Low proba area */ + } } + assert(position==0); /* Must have initialized all positions */ + } + + /* Build table */ + { U32 u; for (u=0; u 1); + { U32 const maxBitsOut = tableLog - ZSTD_highbit32 ((U32)normalizedCounter[s]-1); + U32 const minStatePlus = (U32)normalizedCounter[s] << maxBitsOut; + symbolTT[s].deltaNbBits = (maxBitsOut << 16) - minStatePlus; + symbolTT[s].deltaFindState = (int)(total - (unsigned)normalizedCounter[s]); + total += (unsigned)normalizedCounter[s]; + } } } } + +#if 0 /* debug : symbol costs */ + DEBUGLOG(5, "\n --- table statistics : "); + { U32 symbol; + for (symbol=0; symbol<=maxSymbolValue; symbol++) { + DEBUGLOG(5, "%3u: w=%3i, maxBits=%u, fracBits=%.2f", + symbol, normalizedCounter[symbol], + FSE_getMaxNbBits(symbolTT, symbol), + (double)FSE_bitCost(symbolTT, tableLog, symbol, 8) / 256); + } } +#endif + + return 0; +} + + + +#ifndef FSE_COMMONDEFS_ONLY + +/*-************************************************************** +* FSE NCount encoding +****************************************************************/ +size_t FSE_NCountWriteBound(unsigned maxSymbolValue, unsigned tableLog) +{ + size_t const maxHeaderSize = (((maxSymbolValue+1) * tableLog + + 4 /* bitCount initialized at 4 */ + + 2 /* first two symbols may use one additional bit each */) / 8) + + 1 /* round up to whole nb bytes */ + + 2 /* additional two bytes for bitstream flush */; + return maxSymbolValue ? maxHeaderSize : FSE_NCOUNTBOUND; /* maxSymbolValue==0 ? use default */ +} + +static size_t +FSE_writeNCount_generic (void* header, size_t headerBufferSize, + const short* normalizedCounter, unsigned maxSymbolValue, unsigned tableLog, + unsigned writeIsSafe) +{ + BYTE* const ostart = (BYTE*) header; + BYTE* out = ostart; + BYTE* const oend = ostart + headerBufferSize; + int nbBits; + const int tableSize = 1 << tableLog; + int remaining; + int threshold; + U32 bitStream = 0; + int bitCount = 0; + unsigned symbol = 0; + unsigned const alphabetSize = maxSymbolValue + 1; + int previousIs0 = 0; + + /* Table Size */ + bitStream += (tableLog-FSE_MIN_TABLELOG) << bitCount; + bitCount += 4; + + /* Init */ + remaining = tableSize+1; /* +1 for extra accuracy */ + threshold = tableSize; + nbBits = (int)tableLog+1; + + while ((symbol < alphabetSize) && (remaining>1)) { /* stops at 1 */ + if (previousIs0) { + unsigned start = symbol; + while ((symbol < alphabetSize) && !normalizedCounter[symbol]) symbol++; + if (symbol == alphabetSize) break; /* incorrect distribution */ + while (symbol >= start+24) { + start+=24; + bitStream += 0xFFFFU << bitCount; + if ((!writeIsSafe) && (out > oend-2)) + return ERROR(dstSize_tooSmall); /* Buffer overflow */ + out[0] = (BYTE) bitStream; + out[1] = (BYTE)(bitStream>>8); + out+=2; + bitStream>>=16; + } + while (symbol >= start+3) { + start+=3; + bitStream += 3U << bitCount; + bitCount += 2; + } + bitStream += (symbol-start) << bitCount; + bitCount += 2; + if (bitCount>16) { + if ((!writeIsSafe) && (out > oend - 2)) + return ERROR(dstSize_tooSmall); /* Buffer overflow */ + out[0] = (BYTE)bitStream; + out[1] = (BYTE)(bitStream>>8); + out += 2; + bitStream >>= 16; + bitCount -= 16; + } } + { int count = normalizedCounter[symbol++]; + int const max = (2*threshold-1) - remaining; + remaining -= count < 0 ? -count : count; + count++; /* +1 for extra accuracy */ + if (count>=threshold) + count += max; /* [0..max[ [max..threshold[ (...) [threshold+max 2*threshold[ */ + bitStream += (U32)count << bitCount; + bitCount += nbBits; + bitCount -= (count>=1; } + } + if (bitCount>16) { + if ((!writeIsSafe) && (out > oend - 2)) + return ERROR(dstSize_tooSmall); /* Buffer overflow */ + out[0] = (BYTE)bitStream; + out[1] = (BYTE)(bitStream>>8); + out += 2; + bitStream >>= 16; + bitCount -= 16; + } } + + if (remaining != 1) + return ERROR(GENERIC); /* incorrect normalized distribution */ + assert(symbol <= alphabetSize); + + /* flush remaining bitStream */ + if ((!writeIsSafe) && (out > oend - 2)) + return ERROR(dstSize_tooSmall); /* Buffer overflow */ + out[0] = (BYTE)bitStream; + out[1] = (BYTE)(bitStream>>8); + out+= (bitCount+7) /8; + + assert(out >= ostart); + return (size_t)(out-ostart); +} + + +size_t FSE_writeNCount (void* buffer, size_t bufferSize, + const short* normalizedCounter, unsigned maxSymbolValue, unsigned tableLog) +{ + if (tableLog > FSE_MAX_TABLELOG) return ERROR(tableLog_tooLarge); /* Unsupported */ + if (tableLog < FSE_MIN_TABLELOG) return ERROR(GENERIC); /* Unsupported */ + + if (bufferSize < FSE_NCountWriteBound(maxSymbolValue, tableLog)) + return FSE_writeNCount_generic(buffer, bufferSize, normalizedCounter, maxSymbolValue, tableLog, 0); + + return FSE_writeNCount_generic(buffer, bufferSize, normalizedCounter, maxSymbolValue, tableLog, 1 /* write in buffer is safe */); +} + + +/*-************************************************************** +* FSE Compression Code +****************************************************************/ + +/* provides the minimum logSize to safely represent a distribution */ +static unsigned FSE_minTableLog(size_t srcSize, unsigned maxSymbolValue) +{ + U32 minBitsSrc = ZSTD_highbit32((U32)(srcSize)) + 1; + U32 minBitsSymbols = ZSTD_highbit32(maxSymbolValue) + 2; + U32 minBits = minBitsSrc < minBitsSymbols ? minBitsSrc : minBitsSymbols; + assert(srcSize > 1); /* Not supported, RLE should be used instead */ + return minBits; +} + +unsigned FSE_optimalTableLog_internal(unsigned maxTableLog, size_t srcSize, unsigned maxSymbolValue, unsigned minus) +{ + U32 maxBitsSrc = ZSTD_highbit32((U32)(srcSize - 1)) - minus; + U32 tableLog = maxTableLog; + U32 minBits = FSE_minTableLog(srcSize, maxSymbolValue); + assert(srcSize > 1); /* Not supported, RLE should be used instead */ + if (tableLog==0) tableLog = FSE_DEFAULT_TABLELOG; + if (maxBitsSrc < tableLog) tableLog = maxBitsSrc; /* Accuracy can be reduced */ + if (minBits > tableLog) tableLog = minBits; /* Need a minimum to safely represent all symbol values */ + if (tableLog < FSE_MIN_TABLELOG) tableLog = FSE_MIN_TABLELOG; + if (tableLog > FSE_MAX_TABLELOG) tableLog = FSE_MAX_TABLELOG; + return tableLog; +} + +unsigned FSE_optimalTableLog(unsigned maxTableLog, size_t srcSize, unsigned maxSymbolValue) +{ + return FSE_optimalTableLog_internal(maxTableLog, srcSize, maxSymbolValue, 2); +} + +/* Secondary normalization method. + To be used when primary method fails. */ + +static size_t FSE_normalizeM2(short* norm, U32 tableLog, const unsigned* count, size_t total, U32 maxSymbolValue, short lowProbCount) +{ + short const NOT_YET_ASSIGNED = -2; + U32 s; + U32 distributed = 0; + U32 ToDistribute; + + /* Init */ + U32 const lowThreshold = (U32)(total >> tableLog); + U32 lowOne = (U32)((total * 3) >> (tableLog + 1)); + + for (s=0; s<=maxSymbolValue; s++) { + if (count[s] == 0) { + norm[s]=0; + continue; + } + if (count[s] <= lowThreshold) { + norm[s] = lowProbCount; + distributed++; + total -= count[s]; + continue; + } + if (count[s] <= lowOne) { + norm[s] = 1; + distributed++; + total -= count[s]; + continue; + } + + norm[s]=NOT_YET_ASSIGNED; + } + ToDistribute = (1 << tableLog) - distributed; + + if (ToDistribute == 0) + return 0; + + if ((total / ToDistribute) > lowOne) { + /* risk of rounding to zero */ + lowOne = (U32)((total * 3) / (ToDistribute * 2)); + for (s=0; s<=maxSymbolValue; s++) { + if ((norm[s] == NOT_YET_ASSIGNED) && (count[s] <= lowOne)) { + norm[s] = 1; + distributed++; + total -= count[s]; + continue; + } } + ToDistribute = (1 << tableLog) - distributed; + } + + if (distributed == maxSymbolValue+1) { + /* all values are pretty poor; + probably incompressible data (should have already been detected); + find max, then give all remaining points to max */ + U32 maxV = 0, maxC = 0; + for (s=0; s<=maxSymbolValue; s++) + if (count[s] > maxC) { maxV=s; maxC=count[s]; } + norm[maxV] += (short)ToDistribute; + return 0; + } + + if (total == 0) { + /* all of the symbols were low enough for the lowOne or lowThreshold */ + for (s=0; ToDistribute > 0; s = (s+1)%(maxSymbolValue+1)) + if (norm[s] > 0) { ToDistribute--; norm[s]++; } + return 0; + } + + { U64 const vStepLog = 62 - tableLog; + U64 const mid = (1ULL << (vStepLog-1)) - 1; + U64 const rStep = ZSTD_div64((((U64)1<> vStepLog); + U32 const sEnd = (U32)(end >> vStepLog); + U32 const weight = sEnd - sStart; + if (weight < 1) + return ERROR(GENERIC); + norm[s] = (short)weight; + tmpTotal = end; + } } } + + return 0; +} + +size_t FSE_normalizeCount (short* normalizedCounter, unsigned tableLog, + const unsigned* count, size_t total, + unsigned maxSymbolValue, unsigned useLowProbCount) +{ + /* Sanity checks */ + if (tableLog==0) tableLog = FSE_DEFAULT_TABLELOG; + if (tableLog < FSE_MIN_TABLELOG) return ERROR(GENERIC); /* Unsupported size */ + if (tableLog > FSE_MAX_TABLELOG) return ERROR(tableLog_tooLarge); /* Unsupported size */ + if (tableLog < FSE_minTableLog(total, maxSymbolValue)) return ERROR(GENERIC); /* Too small tableLog, compression potentially impossible */ + + { static U32 const rtbTable[] = { 0, 473195, 504333, 520860, 550000, 700000, 750000, 830000 }; + short const lowProbCount = useLowProbCount ? -1 : 1; + U64 const scale = 62 - tableLog; + U64 const step = ZSTD_div64((U64)1<<62, (U32)total); /* <== here, one division ! */ + U64 const vStep = 1ULL<<(scale-20); + int stillToDistribute = 1<> tableLog); + + for (s=0; s<=maxSymbolValue; s++) { + if (count[s] == total) return 0; /* rle special case */ + if (count[s] == 0) { normalizedCounter[s]=0; continue; } + if (count[s] <= lowThreshold) { + normalizedCounter[s] = lowProbCount; + stillToDistribute--; + } else { + short proba = (short)((count[s]*step) >> scale); + if (proba<8) { + U64 restToBeat = vStep * rtbTable[proba]; + proba += (count[s]*step) - ((U64)proba< restToBeat; + } + if (proba > largestP) { largestP=proba; largest=s; } + normalizedCounter[s] = proba; + stillToDistribute -= proba; + } } + if (-stillToDistribute >= (normalizedCounter[largest] >> 1)) { + /* corner case, need another normalization method */ + size_t const errorCode = FSE_normalizeM2(normalizedCounter, tableLog, count, total, maxSymbolValue, lowProbCount); + if (FSE_isError(errorCode)) return errorCode; + } + else normalizedCounter[largest] += (short)stillToDistribute; + } + +#if 0 + { /* Print Table (debug) */ + U32 s; + U32 nTotal = 0; + for (s=0; s<=maxSymbolValue; s++) + RAWLOG(2, "%3i: %4i \n", s, normalizedCounter[s]); + for (s=0; s<=maxSymbolValue; s++) + nTotal += abs(normalizedCounter[s]); + if (nTotal != (1U< FSE_MAX_TABLELOG*4+7 ) && (srcSize & 2)) { /* test bit 2 */ + FSE_encodeSymbol(&bitC, &CState2, *--ip); + FSE_encodeSymbol(&bitC, &CState1, *--ip); + FSE_FLUSHBITS(&bitC); + } + + /* 2 or 4 encoding per loop */ + while ( ip>istart ) { + + FSE_encodeSymbol(&bitC, &CState2, *--ip); + + if (sizeof(bitC.bitContainer)*8 < FSE_MAX_TABLELOG*2+7 ) /* this test must be static */ + FSE_FLUSHBITS(&bitC); + + FSE_encodeSymbol(&bitC, &CState1, *--ip); + + if (sizeof(bitC.bitContainer)*8 > FSE_MAX_TABLELOG*4+7 ) { /* this test must be static */ + FSE_encodeSymbol(&bitC, &CState2, *--ip); + FSE_encodeSymbol(&bitC, &CState1, *--ip); + } + + FSE_FLUSHBITS(&bitC); + } + + FSE_flushCState(&bitC, &CState2); + FSE_flushCState(&bitC, &CState1); + return BIT_closeCStream(&bitC); +} + +size_t FSE_compress_usingCTable (void* dst, size_t dstSize, + const void* src, size_t srcSize, + const FSE_CTable* ct) +{ + unsigned const fast = (dstSize >= FSE_BLOCKBOUND(srcSize)); + + if (fast) + return FSE_compress_usingCTable_generic(dst, dstSize, src, srcSize, ct, 1); + else + return FSE_compress_usingCTable_generic(dst, dstSize, src, srcSize, ct, 0); +} + + +size_t FSE_compressBound(size_t size) { return FSE_COMPRESSBOUND(size); } + +#endif /* FSE_COMMONDEFS_ONLY */ +/**** ended inlining compress/fse_compress.c ****/ +/**** start inlining compress/hist.c ****/ +/* ****************************************************************** + * hist : Histogram functions + * part of Finite State Entropy project + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - FSE source repository : https://github.com/Cyan4973/FiniteStateEntropy + * - Public forum : https://groups.google.com/forum/#!forum/lz4c + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + +/* --- dependencies --- */ +/**** skipping file: ../common/mem.h ****/ +/**** skipping file: ../common/debug.h ****/ +/**** skipping file: ../common/error_private.h ****/ +/**** skipping file: hist.h ****/ + + +/* --- Error management --- */ +unsigned HIST_isError(size_t code) { return ERR_isError(code); } + +/*-************************************************************** + * Histogram functions + ****************************************************************/ +unsigned HIST_count_simple(unsigned* count, unsigned* maxSymbolValuePtr, + const void* src, size_t srcSize) +{ + const BYTE* ip = (const BYTE*)src; + const BYTE* const end = ip + srcSize; + unsigned maxSymbolValue = *maxSymbolValuePtr; + unsigned largestCount=0; + + ZSTD_memset(count, 0, (maxSymbolValue+1) * sizeof(*count)); + if (srcSize==0) { *maxSymbolValuePtr = 0; return 0; } + + while (ip largestCount) largestCount = count[s]; + } + + return largestCount; +} + +typedef enum { trustInput, checkMaxSymbolValue } HIST_checkInput_e; + +/* HIST_count_parallel_wksp() : + * store histogram into 4 intermediate tables, recombined at the end. + * this design makes better use of OoO cpus, + * and is noticeably faster when some values are heavily repeated. + * But it needs some additional workspace for intermediate tables. + * `workSpace` must be a U32 table of size >= HIST_WKSP_SIZE_U32. + * @return : largest histogram frequency, + * or an error code (notably when histogram's alphabet is larger than *maxSymbolValuePtr) */ +static size_t HIST_count_parallel_wksp( + unsigned* count, unsigned* maxSymbolValuePtr, + const void* source, size_t sourceSize, + HIST_checkInput_e check, + U32* const workSpace) +{ + const BYTE* ip = (const BYTE*)source; + const BYTE* const iend = ip+sourceSize; + size_t const countSize = (*maxSymbolValuePtr + 1) * sizeof(*count); + unsigned max=0; + U32* const Counting1 = workSpace; + U32* const Counting2 = Counting1 + 256; + U32* const Counting3 = Counting2 + 256; + U32* const Counting4 = Counting3 + 256; + + /* safety checks */ + assert(*maxSymbolValuePtr <= 255); + if (!sourceSize) { + ZSTD_memset(count, 0, countSize); + *maxSymbolValuePtr = 0; + return 0; + } + ZSTD_memset(workSpace, 0, 4*256*sizeof(unsigned)); + + /* by stripes of 16 bytes */ + { U32 cached = MEM_read32(ip); ip += 4; + while (ip < iend-15) { + U32 c = cached; cached = MEM_read32(ip); ip += 4; + Counting1[(BYTE) c ]++; + Counting2[(BYTE)(c>>8) ]++; + Counting3[(BYTE)(c>>16)]++; + Counting4[ c>>24 ]++; + c = cached; cached = MEM_read32(ip); ip += 4; + Counting1[(BYTE) c ]++; + Counting2[(BYTE)(c>>8) ]++; + Counting3[(BYTE)(c>>16)]++; + Counting4[ c>>24 ]++; + c = cached; cached = MEM_read32(ip); ip += 4; + Counting1[(BYTE) c ]++; + Counting2[(BYTE)(c>>8) ]++; + Counting3[(BYTE)(c>>16)]++; + Counting4[ c>>24 ]++; + c = cached; cached = MEM_read32(ip); ip += 4; + Counting1[(BYTE) c ]++; + Counting2[(BYTE)(c>>8) ]++; + Counting3[(BYTE)(c>>16)]++; + Counting4[ c>>24 ]++; + } + ip-=4; + } + + /* finish last symbols */ + while (ip max) max = Counting1[s]; + } } + + { unsigned maxSymbolValue = 255; + while (!Counting1[maxSymbolValue]) maxSymbolValue--; + if (check && maxSymbolValue > *maxSymbolValuePtr) return ERROR(maxSymbolValue_tooSmall); + *maxSymbolValuePtr = maxSymbolValue; + ZSTD_memmove(count, Counting1, countSize); /* in case count & Counting1 are overlapping */ + } + return (size_t)max; +} + +/* HIST_countFast_wksp() : + * Same as HIST_countFast(), but using an externally provided scratch buffer. + * `workSpace` is a writable buffer which must be 4-bytes aligned, + * `workSpaceSize` must be >= HIST_WKSP_SIZE + */ +size_t HIST_countFast_wksp(unsigned* count, unsigned* maxSymbolValuePtr, + const void* source, size_t sourceSize, + void* workSpace, size_t workSpaceSize) +{ + if (sourceSize < 1500) /* heuristic threshold */ + return HIST_count_simple(count, maxSymbolValuePtr, source, sourceSize); + if ((size_t)workSpace & 3) return ERROR(GENERIC); /* must be aligned on 4-bytes boundaries */ + if (workSpaceSize < HIST_WKSP_SIZE) return ERROR(workSpace_tooSmall); + return HIST_count_parallel_wksp(count, maxSymbolValuePtr, source, sourceSize, trustInput, (U32*)workSpace); +} + +/* HIST_count_wksp() : + * Same as HIST_count(), but using an externally provided scratch buffer. + * `workSpace` size must be table of >= HIST_WKSP_SIZE_U32 unsigned */ +size_t HIST_count_wksp(unsigned* count, unsigned* maxSymbolValuePtr, + const void* source, size_t sourceSize, + void* workSpace, size_t workSpaceSize) +{ + if ((size_t)workSpace & 3) return ERROR(GENERIC); /* must be aligned on 4-bytes boundaries */ + if (workSpaceSize < HIST_WKSP_SIZE) return ERROR(workSpace_tooSmall); + if (*maxSymbolValuePtr < 255) + return HIST_count_parallel_wksp(count, maxSymbolValuePtr, source, sourceSize, checkMaxSymbolValue, (U32*)workSpace); + *maxSymbolValuePtr = 255; + return HIST_countFast_wksp(count, maxSymbolValuePtr, source, sourceSize, workSpace, workSpaceSize); +} + +#ifndef ZSTD_NO_UNUSED_FUNCTIONS +/* fast variant (unsafe : won't check if src contains values beyond count[] limit) */ +size_t HIST_countFast(unsigned* count, unsigned* maxSymbolValuePtr, + const void* source, size_t sourceSize) +{ + unsigned tmpCounters[HIST_WKSP_SIZE_U32]; + return HIST_countFast_wksp(count, maxSymbolValuePtr, source, sourceSize, tmpCounters, sizeof(tmpCounters)); +} + +size_t HIST_count(unsigned* count, unsigned* maxSymbolValuePtr, + const void* src, size_t srcSize) +{ + unsigned tmpCounters[HIST_WKSP_SIZE_U32]; + return HIST_count_wksp(count, maxSymbolValuePtr, src, srcSize, tmpCounters, sizeof(tmpCounters)); +} +#endif +/**** ended inlining compress/hist.c ****/ +/**** start inlining compress/huf_compress.c ****/ +/* ****************************************************************** + * Huffman encoder, part of New Generation Entropy library + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - FSE+HUF source repository : https://github.com/Cyan4973/FiniteStateEntropy + * - Public forum : https://groups.google.com/forum/#!forum/lz4c + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + +/* ************************************************************** +* Compiler specifics +****************************************************************/ +#ifdef _MSC_VER /* Visual Studio */ +# pragma warning(disable : 4127) /* disable: C4127: conditional expression is constant */ +#endif + + +/* ************************************************************** +* Includes +****************************************************************/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../common/compiler.h ****/ +/**** skipping file: ../common/bitstream.h ****/ +/**** skipping file: hist.h ****/ +#define FSE_STATIC_LINKING_ONLY /* FSE_optimalTableLog_internal */ +/**** skipping file: ../common/fse.h ****/ +/**** skipping file: ../common/huf.h ****/ +/**** skipping file: ../common/error_private.h ****/ +/**** skipping file: ../common/bits.h ****/ + + +/* ************************************************************** +* Error Management +****************************************************************/ +#define HUF_isError ERR_isError +#define HUF_STATIC_ASSERT(c) DEBUG_STATIC_ASSERT(c) /* use only *after* variable declarations */ + + +/* ************************************************************** +* Required declarations +****************************************************************/ +typedef struct nodeElt_s { + U32 count; + U16 parent; + BYTE byte; + BYTE nbBits; +} nodeElt; + + +/* ************************************************************** +* Debug Traces +****************************************************************/ + +#if DEBUGLEVEL >= 2 + +static size_t showU32(const U32* arr, size_t size) +{ + size_t u; + for (u=0; u= add) { + assert(add < align); + assert(((size_t)aligned & mask) == 0); + *workspaceSizePtr -= add; + return aligned; + } else { + *workspaceSizePtr = 0; + return NULL; + } +} + + +/* HUF_compressWeights() : + * Same as FSE_compress(), but dedicated to huff0's weights compression. + * The use case needs much less stack memory. + * Note : all elements within weightTable are supposed to be <= HUF_TABLELOG_MAX. + */ +#define MAX_FSE_TABLELOG_FOR_HUFF_HEADER 6 + +typedef struct { + FSE_CTable CTable[FSE_CTABLE_SIZE_U32(MAX_FSE_TABLELOG_FOR_HUFF_HEADER, HUF_TABLELOG_MAX)]; + U32 scratchBuffer[FSE_BUILD_CTABLE_WORKSPACE_SIZE_U32(HUF_TABLELOG_MAX, MAX_FSE_TABLELOG_FOR_HUFF_HEADER)]; + unsigned count[HUF_TABLELOG_MAX+1]; + S16 norm[HUF_TABLELOG_MAX+1]; +} HUF_CompressWeightsWksp; + +static size_t +HUF_compressWeights(void* dst, size_t dstSize, + const void* weightTable, size_t wtSize, + void* workspace, size_t workspaceSize) +{ + BYTE* const ostart = (BYTE*) dst; + BYTE* op = ostart; + BYTE* const oend = ostart + dstSize; + + unsigned maxSymbolValue = HUF_TABLELOG_MAX; + U32 tableLog = MAX_FSE_TABLELOG_FOR_HUFF_HEADER; + HUF_CompressWeightsWksp* wksp = (HUF_CompressWeightsWksp*)HUF_alignUpWorkspace(workspace, &workspaceSize, ZSTD_ALIGNOF(U32)); + + if (workspaceSize < sizeof(HUF_CompressWeightsWksp)) return ERROR(GENERIC); + + /* init conditions */ + if (wtSize <= 1) return 0; /* Not compressible */ + + /* Scan input and build symbol stats */ + { unsigned const maxCount = HIST_count_simple(wksp->count, &maxSymbolValue, weightTable, wtSize); /* never fails */ + if (maxCount == wtSize) return 1; /* only a single symbol in src : rle */ + if (maxCount == 1) return 0; /* each symbol present maximum once => not compressible */ + } + + tableLog = FSE_optimalTableLog(tableLog, wtSize, maxSymbolValue); + CHECK_F( FSE_normalizeCount(wksp->norm, tableLog, wksp->count, wtSize, maxSymbolValue, /* useLowProbCount */ 0) ); + + /* Write table description header */ + { CHECK_V_F(hSize, FSE_writeNCount(op, (size_t)(oend-op), wksp->norm, maxSymbolValue, tableLog) ); + op += hSize; + } + + /* Compress */ + CHECK_F( FSE_buildCTable_wksp(wksp->CTable, wksp->norm, maxSymbolValue, tableLog, wksp->scratchBuffer, sizeof(wksp->scratchBuffer)) ); + { CHECK_V_F(cSize, FSE_compress_usingCTable(op, (size_t)(oend - op), weightTable, wtSize, wksp->CTable) ); + if (cSize == 0) return 0; /* not enough space for compressed data */ + op += cSize; + } + + return (size_t)(op-ostart); +} + +static size_t HUF_getNbBits(HUF_CElt elt) +{ + return elt & 0xFF; +} + +static size_t HUF_getNbBitsFast(HUF_CElt elt) +{ + return elt; +} + +static size_t HUF_getValue(HUF_CElt elt) +{ + return elt & ~(size_t)0xFF; +} + +static size_t HUF_getValueFast(HUF_CElt elt) +{ + return elt; +} + +static void HUF_setNbBits(HUF_CElt* elt, size_t nbBits) +{ + assert(nbBits <= HUF_TABLELOG_ABSOLUTEMAX); + *elt = nbBits; +} + +static void HUF_setValue(HUF_CElt* elt, size_t value) +{ + size_t const nbBits = HUF_getNbBits(*elt); + if (nbBits > 0) { + assert((value >> nbBits) == 0); + *elt |= value << (sizeof(HUF_CElt) * 8 - nbBits); + } +} + +HUF_CTableHeader HUF_readCTableHeader(HUF_CElt const* ctable) +{ + HUF_CTableHeader header; + ZSTD_memcpy(&header, ctable, sizeof(header)); + return header; +} + +static void HUF_writeCTableHeader(HUF_CElt* ctable, U32 tableLog, U32 maxSymbolValue) +{ + HUF_CTableHeader header; + HUF_STATIC_ASSERT(sizeof(ctable[0]) == sizeof(header)); + ZSTD_memset(&header, 0, sizeof(header)); + assert(tableLog < 256); + header.tableLog = (BYTE)tableLog; + assert(maxSymbolValue < 256); + header.maxSymbolValue = (BYTE)maxSymbolValue; + ZSTD_memcpy(ctable, &header, sizeof(header)); +} + +typedef struct { + HUF_CompressWeightsWksp wksp; + BYTE bitsToWeight[HUF_TABLELOG_MAX + 1]; /* precomputed conversion table */ + BYTE huffWeight[HUF_SYMBOLVALUE_MAX]; +} HUF_WriteCTableWksp; + +size_t HUF_writeCTable_wksp(void* dst, size_t maxDstSize, + const HUF_CElt* CTable, unsigned maxSymbolValue, unsigned huffLog, + void* workspace, size_t workspaceSize) +{ + HUF_CElt const* const ct = CTable + 1; + BYTE* op = (BYTE*)dst; + U32 n; + HUF_WriteCTableWksp* wksp = (HUF_WriteCTableWksp*)HUF_alignUpWorkspace(workspace, &workspaceSize, ZSTD_ALIGNOF(U32)); + + HUF_STATIC_ASSERT(HUF_CTABLE_WORKSPACE_SIZE >= sizeof(HUF_WriteCTableWksp)); + + assert(HUF_readCTableHeader(CTable).maxSymbolValue == maxSymbolValue); + assert(HUF_readCTableHeader(CTable).tableLog == huffLog); + + /* check conditions */ + if (workspaceSize < sizeof(HUF_WriteCTableWksp)) return ERROR(GENERIC); + if (maxSymbolValue > HUF_SYMBOLVALUE_MAX) return ERROR(maxSymbolValue_tooLarge); + + /* convert to weight */ + wksp->bitsToWeight[0] = 0; + for (n=1; nbitsToWeight[n] = (BYTE)(huffLog + 1 - n); + for (n=0; nhuffWeight[n] = wksp->bitsToWeight[HUF_getNbBits(ct[n])]; + + /* attempt weights compression by FSE */ + if (maxDstSize < 1) return ERROR(dstSize_tooSmall); + { CHECK_V_F(hSize, HUF_compressWeights(op+1, maxDstSize-1, wksp->huffWeight, maxSymbolValue, &wksp->wksp, sizeof(wksp->wksp)) ); + if ((hSize>1) & (hSize < maxSymbolValue/2)) { /* FSE compressed */ + op[0] = (BYTE)hSize; + return hSize+1; + } } + + /* write raw values as 4-bits (max : 15) */ + if (maxSymbolValue > (256-128)) return ERROR(GENERIC); /* should not happen : likely means source cannot be compressed */ + if (((maxSymbolValue+1)/2) + 1 > maxDstSize) return ERROR(dstSize_tooSmall); /* not enough space within dst buffer */ + op[0] = (BYTE)(128 /*special case*/ + (maxSymbolValue-1)); + wksp->huffWeight[maxSymbolValue] = 0; /* to be sure it doesn't cause msan issue in final combination */ + for (n=0; nhuffWeight[n] << 4) + wksp->huffWeight[n+1]); + return ((maxSymbolValue+1)/2) + 1; +} + + +size_t HUF_readCTable (HUF_CElt* CTable, unsigned* maxSymbolValuePtr, const void* src, size_t srcSize, unsigned* hasZeroWeights) +{ + BYTE huffWeight[HUF_SYMBOLVALUE_MAX + 1]; /* init not required, even though some static analyzer may complain */ + U32 rankVal[HUF_TABLELOG_ABSOLUTEMAX + 1]; /* large enough for values from 0 to 16 */ + U32 tableLog = 0; + U32 nbSymbols = 0; + HUF_CElt* const ct = CTable + 1; + + /* get symbol weights */ + CHECK_V_F(readSize, HUF_readStats(huffWeight, HUF_SYMBOLVALUE_MAX+1, rankVal, &nbSymbols, &tableLog, src, srcSize)); + *hasZeroWeights = (rankVal[0] > 0); + + /* check result */ + if (tableLog > HUF_TABLELOG_MAX) return ERROR(tableLog_tooLarge); + if (nbSymbols > *maxSymbolValuePtr+1) return ERROR(maxSymbolValue_tooSmall); + + *maxSymbolValuePtr = nbSymbols - 1; + + HUF_writeCTableHeader(CTable, tableLog, *maxSymbolValuePtr); + + /* Prepare base value per rank */ + { U32 n, nextRankStart = 0; + for (n=1; n<=tableLog; n++) { + U32 curr = nextRankStart; + nextRankStart += (rankVal[n] << (n-1)); + rankVal[n] = curr; + } } + + /* fill nbBits */ + { U32 n; for (n=0; nn=tableLog+1 */ + U16 valPerRank[HUF_TABLELOG_MAX+2] = {0}; + { U32 n; for (n=0; n0; n--) { /* start at n=tablelog <-> w=1 */ + valPerRank[n] = min; /* get starting value within each rank */ + min += nbPerRank[n]; + min >>= 1; + } } + /* assign value within rank, symbol order */ + { U32 n; for (n=0; n HUF_readCTableHeader(CTable).maxSymbolValue) + return 0; + return (U32)HUF_getNbBits(ct[symbolValue]); +} + + +/** + * HUF_setMaxHeight(): + * Try to enforce @targetNbBits on the Huffman tree described in @huffNode. + * + * It attempts to convert all nodes with nbBits > @targetNbBits + * to employ @targetNbBits instead. Then it adjusts the tree + * so that it remains a valid canonical Huffman tree. + * + * @pre The sum of the ranks of each symbol == 2^largestBits, + * where largestBits == huffNode[lastNonNull].nbBits. + * @post The sum of the ranks of each symbol == 2^largestBits, + * where largestBits is the return value (expected <= targetNbBits). + * + * @param huffNode The Huffman tree modified in place to enforce targetNbBits. + * It's presumed sorted, from most frequent to rarest symbol. + * @param lastNonNull The symbol with the lowest count in the Huffman tree. + * @param targetNbBits The allowed number of bits, which the Huffman tree + * may not respect. After this function the Huffman tree will + * respect targetNbBits. + * @return The maximum number of bits of the Huffman tree after adjustment. + */ +static U32 HUF_setMaxHeight(nodeElt* huffNode, U32 lastNonNull, U32 targetNbBits) +{ + const U32 largestBits = huffNode[lastNonNull].nbBits; + /* early exit : no elt > targetNbBits, so the tree is already valid. */ + if (largestBits <= targetNbBits) return largestBits; + + DEBUGLOG(5, "HUF_setMaxHeight (targetNbBits = %u)", targetNbBits); + + /* there are several too large elements (at least >= 2) */ + { int totalCost = 0; + const U32 baseCost = 1 << (largestBits - targetNbBits); + int n = (int)lastNonNull; + + /* Adjust any ranks > targetNbBits to targetNbBits. + * Compute totalCost, which is how far the sum of the ranks is + * we are over 2^largestBits after adjust the offending ranks. + */ + while (huffNode[n].nbBits > targetNbBits) { + totalCost += baseCost - (1 << (largestBits - huffNode[n].nbBits)); + huffNode[n].nbBits = (BYTE)targetNbBits; + n--; + } + /* n stops at huffNode[n].nbBits <= targetNbBits */ + assert(huffNode[n].nbBits <= targetNbBits); + /* n end at index of smallest symbol using < targetNbBits */ + while (huffNode[n].nbBits == targetNbBits) --n; + + /* renorm totalCost from 2^largestBits to 2^targetNbBits + * note : totalCost is necessarily a multiple of baseCost */ + assert(((U32)totalCost & (baseCost - 1)) == 0); + totalCost >>= (largestBits - targetNbBits); + assert(totalCost > 0); + + /* repay normalized cost */ + { U32 const noSymbol = 0xF0F0F0F0; + U32 rankLast[HUF_TABLELOG_MAX+2]; + + /* Get pos of last (smallest = lowest cum. count) symbol per rank */ + ZSTD_memset(rankLast, 0xF0, sizeof(rankLast)); + { U32 currentNbBits = targetNbBits; + int pos; + for (pos=n ; pos >= 0; pos--) { + if (huffNode[pos].nbBits >= currentNbBits) continue; + currentNbBits = huffNode[pos].nbBits; /* < targetNbBits */ + rankLast[targetNbBits-currentNbBits] = (U32)pos; + } } + + while (totalCost > 0) { + /* Try to reduce the next power of 2 above totalCost because we + * gain back half the rank. + */ + U32 nBitsToDecrease = ZSTD_highbit32((U32)totalCost) + 1; + for ( ; nBitsToDecrease > 1; nBitsToDecrease--) { + U32 const highPos = rankLast[nBitsToDecrease]; + U32 const lowPos = rankLast[nBitsToDecrease-1]; + if (highPos == noSymbol) continue; + /* Decrease highPos if no symbols of lowPos or if it is + * not cheaper to remove 2 lowPos than highPos. + */ + if (lowPos == noSymbol) break; + { U32 const highTotal = huffNode[highPos].count; + U32 const lowTotal = 2 * huffNode[lowPos].count; + if (highTotal <= lowTotal) break; + } } + /* only triggered when no more rank 1 symbol left => find closest one (note : there is necessarily at least one !) */ + assert(rankLast[nBitsToDecrease] != noSymbol || nBitsToDecrease == 1); + /* HUF_MAX_TABLELOG test just to please gcc 5+; but it should not be necessary */ + while ((nBitsToDecrease<=HUF_TABLELOG_MAX) && (rankLast[nBitsToDecrease] == noSymbol)) + nBitsToDecrease++; + assert(rankLast[nBitsToDecrease] != noSymbol); + /* Increase the number of bits to gain back half the rank cost. */ + totalCost -= 1 << (nBitsToDecrease-1); + huffNode[rankLast[nBitsToDecrease]].nbBits++; + + /* Fix up the new rank. + * If the new rank was empty, this symbol is now its smallest. + * Otherwise, this symbol will be the largest in the new rank so no adjustment. + */ + if (rankLast[nBitsToDecrease-1] == noSymbol) + rankLast[nBitsToDecrease-1] = rankLast[nBitsToDecrease]; + /* Fix up the old rank. + * If the symbol was at position 0, meaning it was the highest weight symbol in the tree, + * it must be the only symbol in its rank, so the old rank now has no symbols. + * Otherwise, since the Huffman nodes are sorted by count, the previous position is now + * the smallest node in the rank. If the previous position belongs to a different rank, + * then the rank is now empty. + */ + if (rankLast[nBitsToDecrease] == 0) /* special case, reached largest symbol */ + rankLast[nBitsToDecrease] = noSymbol; + else { + rankLast[nBitsToDecrease]--; + if (huffNode[rankLast[nBitsToDecrease]].nbBits != targetNbBits-nBitsToDecrease) + rankLast[nBitsToDecrease] = noSymbol; /* this rank is now empty */ + } + } /* while (totalCost > 0) */ + + /* If we've removed too much weight, then we have to add it back. + * To avoid overshooting again, we only adjust the smallest rank. + * We take the largest nodes from the lowest rank 0 and move them + * to rank 1. There's guaranteed to be enough rank 0 symbols because + * TODO. + */ + while (totalCost < 0) { /* Sometimes, cost correction overshoot */ + /* special case : no rank 1 symbol (using targetNbBits-1); + * let's create one from largest rank 0 (using targetNbBits). + */ + if (rankLast[1] == noSymbol) { + while (huffNode[n].nbBits == targetNbBits) n--; + huffNode[n+1].nbBits--; + assert(n >= 0); + rankLast[1] = (U32)(n+1); + totalCost++; + continue; + } + huffNode[ rankLast[1] + 1 ].nbBits--; + rankLast[1]++; + totalCost ++; + } + } /* repay normalized cost */ + } /* there are several too large elements (at least >= 2) */ + + return targetNbBits; +} + +typedef struct { + U16 base; + U16 curr; +} rankPos; + +typedef nodeElt huffNodeTable[2 * (HUF_SYMBOLVALUE_MAX + 1)]; + +/* Number of buckets available for HUF_sort() */ +#define RANK_POSITION_TABLE_SIZE 192 + +typedef struct { + huffNodeTable huffNodeTbl; + rankPos rankPosition[RANK_POSITION_TABLE_SIZE]; +} HUF_buildCTable_wksp_tables; + +/* RANK_POSITION_DISTINCT_COUNT_CUTOFF == Cutoff point in HUF_sort() buckets for which we use log2 bucketing. + * Strategy is to use as many buckets as possible for representing distinct + * counts while using the remainder to represent all "large" counts. + * + * To satisfy this requirement for 192 buckets, we can do the following: + * Let buckets 0-166 represent distinct counts of [0, 166] + * Let buckets 166 to 192 represent all remaining counts up to RANK_POSITION_MAX_COUNT_LOG using log2 bucketing. + */ +#define RANK_POSITION_MAX_COUNT_LOG 32 +#define RANK_POSITION_LOG_BUCKETS_BEGIN ((RANK_POSITION_TABLE_SIZE - 1) - RANK_POSITION_MAX_COUNT_LOG - 1 /* == 158 */) +#define RANK_POSITION_DISTINCT_COUNT_CUTOFF (RANK_POSITION_LOG_BUCKETS_BEGIN + ZSTD_highbit32(RANK_POSITION_LOG_BUCKETS_BEGIN) /* == 166 */) + +/* Return the appropriate bucket index for a given count. See definition of + * RANK_POSITION_DISTINCT_COUNT_CUTOFF for explanation of bucketing strategy. + */ +static U32 HUF_getIndex(U32 const count) { + return (count < RANK_POSITION_DISTINCT_COUNT_CUTOFF) + ? count + : ZSTD_highbit32(count) + RANK_POSITION_LOG_BUCKETS_BEGIN; +} + +/* Helper swap function for HUF_quickSortPartition() */ +static void HUF_swapNodes(nodeElt* a, nodeElt* b) { + nodeElt tmp = *a; + *a = *b; + *b = tmp; +} + +/* Returns 0 if the huffNode array is not sorted by descending count */ +MEM_STATIC int HUF_isSorted(nodeElt huffNode[], U32 const maxSymbolValue1) { + U32 i; + for (i = 1; i < maxSymbolValue1; ++i) { + if (huffNode[i].count > huffNode[i-1].count) { + return 0; + } + } + return 1; +} + +/* Insertion sort by descending order */ +HINT_INLINE void HUF_insertionSort(nodeElt huffNode[], int const low, int const high) { + int i; + int const size = high-low+1; + huffNode += low; + for (i = 1; i < size; ++i) { + nodeElt const key = huffNode[i]; + int j = i - 1; + while (j >= 0 && huffNode[j].count < key.count) { + huffNode[j + 1] = huffNode[j]; + j--; + } + huffNode[j + 1] = key; + } +} + +/* Pivot helper function for quicksort. */ +static int HUF_quickSortPartition(nodeElt arr[], int const low, int const high) { + /* Simply select rightmost element as pivot. "Better" selectors like + * median-of-three don't experimentally appear to have any benefit. + */ + U32 const pivot = arr[high].count; + int i = low - 1; + int j = low; + for ( ; j < high; j++) { + if (arr[j].count > pivot) { + i++; + HUF_swapNodes(&arr[i], &arr[j]); + } + } + HUF_swapNodes(&arr[i + 1], &arr[high]); + return i + 1; +} + +/* Classic quicksort by descending with partially iterative calls + * to reduce worst case callstack size. + */ +static void HUF_simpleQuickSort(nodeElt arr[], int low, int high) { + int const kInsertionSortThreshold = 8; + if (high - low < kInsertionSortThreshold) { + HUF_insertionSort(arr, low, high); + return; + } + while (low < high) { + int const idx = HUF_quickSortPartition(arr, low, high); + if (idx - low < high - idx) { + HUF_simpleQuickSort(arr, low, idx - 1); + low = idx + 1; + } else { + HUF_simpleQuickSort(arr, idx + 1, high); + high = idx - 1; + } + } +} + +/** + * HUF_sort(): + * Sorts the symbols [0, maxSymbolValue] by count[symbol] in decreasing order. + * This is a typical bucket sorting strategy that uses either quicksort or insertion sort to sort each bucket. + * + * @param[out] huffNode Sorted symbols by decreasing count. Only members `.count` and `.byte` are filled. + * Must have (maxSymbolValue + 1) entries. + * @param[in] count Histogram of the symbols. + * @param[in] maxSymbolValue Maximum symbol value. + * @param rankPosition This is a scratch workspace. Must have RANK_POSITION_TABLE_SIZE entries. + */ +static void HUF_sort(nodeElt huffNode[], const unsigned count[], U32 const maxSymbolValue, rankPos rankPosition[]) { + U32 n; + U32 const maxSymbolValue1 = maxSymbolValue+1; + + /* Compute base and set curr to base. + * For symbol s let lowerRank = HUF_getIndex(count[n]) and rank = lowerRank + 1. + * See HUF_getIndex to see bucketing strategy. + * We attribute each symbol to lowerRank's base value, because we want to know where + * each rank begins in the output, so for rank R we want to count ranks R+1 and above. + */ + ZSTD_memset(rankPosition, 0, sizeof(*rankPosition) * RANK_POSITION_TABLE_SIZE); + for (n = 0; n < maxSymbolValue1; ++n) { + U32 lowerRank = HUF_getIndex(count[n]); + assert(lowerRank < RANK_POSITION_TABLE_SIZE - 1); + rankPosition[lowerRank].base++; + } + + assert(rankPosition[RANK_POSITION_TABLE_SIZE - 1].base == 0); + /* Set up the rankPosition table */ + for (n = RANK_POSITION_TABLE_SIZE - 1; n > 0; --n) { + rankPosition[n-1].base += rankPosition[n].base; + rankPosition[n-1].curr = rankPosition[n-1].base; + } + + /* Insert each symbol into their appropriate bucket, setting up rankPosition table. */ + for (n = 0; n < maxSymbolValue1; ++n) { + U32 const c = count[n]; + U32 const r = HUF_getIndex(c) + 1; + U32 const pos = rankPosition[r].curr++; + assert(pos < maxSymbolValue1); + huffNode[pos].count = c; + huffNode[pos].byte = (BYTE)n; + } + + /* Sort each bucket. */ + for (n = RANK_POSITION_DISTINCT_COUNT_CUTOFF; n < RANK_POSITION_TABLE_SIZE - 1; ++n) { + int const bucketSize = rankPosition[n].curr - rankPosition[n].base; + U32 const bucketStartIdx = rankPosition[n].base; + if (bucketSize > 1) { + assert(bucketStartIdx < maxSymbolValue1); + HUF_simpleQuickSort(huffNode + bucketStartIdx, 0, bucketSize-1); + } + } + + assert(HUF_isSorted(huffNode, maxSymbolValue1)); +} + + +/** HUF_buildCTable_wksp() : + * Same as HUF_buildCTable(), but using externally allocated scratch buffer. + * `workSpace` must be aligned on 4-bytes boundaries, and be at least as large as sizeof(HUF_buildCTable_wksp_tables). + */ +#define STARTNODE (HUF_SYMBOLVALUE_MAX+1) + +/* HUF_buildTree(): + * Takes the huffNode array sorted by HUF_sort() and builds an unlimited-depth Huffman tree. + * + * @param huffNode The array sorted by HUF_sort(). Builds the Huffman tree in this array. + * @param maxSymbolValue The maximum symbol value. + * @return The smallest node in the Huffman tree (by count). + */ +static int HUF_buildTree(nodeElt* huffNode, U32 maxSymbolValue) +{ + nodeElt* const huffNode0 = huffNode - 1; + int nonNullRank; + int lowS, lowN; + int nodeNb = STARTNODE; + int n, nodeRoot; + DEBUGLOG(5, "HUF_buildTree (alphabet size = %u)", maxSymbolValue + 1); + /* init for parents */ + nonNullRank = (int)maxSymbolValue; + while(huffNode[nonNullRank].count == 0) nonNullRank--; + lowS = nonNullRank; nodeRoot = nodeNb + lowS - 1; lowN = nodeNb; + huffNode[nodeNb].count = huffNode[lowS].count + huffNode[lowS-1].count; + huffNode[lowS].parent = huffNode[lowS-1].parent = (U16)nodeNb; + nodeNb++; lowS-=2; + for (n=nodeNb; n<=nodeRoot; n++) huffNode[n].count = (U32)(1U<<30); + huffNode0[0].count = (U32)(1U<<31); /* fake entry, strong barrier */ + + /* create parents */ + while (nodeNb <= nodeRoot) { + int const n1 = (huffNode[lowS].count < huffNode[lowN].count) ? lowS-- : lowN++; + int const n2 = (huffNode[lowS].count < huffNode[lowN].count) ? lowS-- : lowN++; + huffNode[nodeNb].count = huffNode[n1].count + huffNode[n2].count; + huffNode[n1].parent = huffNode[n2].parent = (U16)nodeNb; + nodeNb++; + } + + /* distribute weights (unlimited tree height) */ + huffNode[nodeRoot].nbBits = 0; + for (n=nodeRoot-1; n>=STARTNODE; n--) + huffNode[n].nbBits = huffNode[ huffNode[n].parent ].nbBits + 1; + for (n=0; n<=nonNullRank; n++) + huffNode[n].nbBits = huffNode[ huffNode[n].parent ].nbBits + 1; + + DEBUGLOG(6, "Initial distribution of bits completed (%zu sorted symbols)", showHNodeBits(huffNode, maxSymbolValue+1)); + + return nonNullRank; +} + +/** + * HUF_buildCTableFromTree(): + * Build the CTable given the Huffman tree in huffNode. + * + * @param[out] CTable The output Huffman CTable. + * @param huffNode The Huffman tree. + * @param nonNullRank The last and smallest node in the Huffman tree. + * @param maxSymbolValue The maximum symbol value. + * @param maxNbBits The exact maximum number of bits used in the Huffman tree. + */ +static void HUF_buildCTableFromTree(HUF_CElt* CTable, nodeElt const* huffNode, int nonNullRank, U32 maxSymbolValue, U32 maxNbBits) +{ + HUF_CElt* const ct = CTable + 1; + /* fill result into ctable (val, nbBits) */ + int n; + U16 nbPerRank[HUF_TABLELOG_MAX+1] = {0}; + U16 valPerRank[HUF_TABLELOG_MAX+1] = {0}; + int const alphabetSize = (int)(maxSymbolValue + 1); + for (n=0; n<=nonNullRank; n++) + nbPerRank[huffNode[n].nbBits]++; + /* determine starting value per rank */ + { U16 min = 0; + for (n=(int)maxNbBits; n>0; n--) { + valPerRank[n] = min; /* get starting value within each rank */ + min += nbPerRank[n]; + min >>= 1; + } } + for (n=0; nhuffNodeTbl; + nodeElt* const huffNode = huffNode0+1; + int nonNullRank; + + HUF_STATIC_ASSERT(HUF_CTABLE_WORKSPACE_SIZE == sizeof(HUF_buildCTable_wksp_tables)); + + DEBUGLOG(5, "HUF_buildCTable_wksp (alphabet size = %u)", maxSymbolValue+1); + + /* safety checks */ + if (wkspSize < sizeof(HUF_buildCTable_wksp_tables)) + return ERROR(workSpace_tooSmall); + if (maxNbBits == 0) maxNbBits = HUF_TABLELOG_DEFAULT; + if (maxSymbolValue > HUF_SYMBOLVALUE_MAX) + return ERROR(maxSymbolValue_tooLarge); + ZSTD_memset(huffNode0, 0, sizeof(huffNodeTable)); + + /* sort, decreasing order */ + HUF_sort(huffNode, count, maxSymbolValue, wksp_tables->rankPosition); + DEBUGLOG(6, "sorted symbols completed (%zu symbols)", showHNodeSymbols(huffNode, maxSymbolValue+1)); + + /* build tree */ + nonNullRank = HUF_buildTree(huffNode, maxSymbolValue); + + /* determine and enforce maxTableLog */ + maxNbBits = HUF_setMaxHeight(huffNode, (U32)nonNullRank, maxNbBits); + if (maxNbBits > HUF_TABLELOG_MAX) return ERROR(GENERIC); /* check fit into table */ + + HUF_buildCTableFromTree(CTable, huffNode, nonNullRank, maxSymbolValue, maxNbBits); + + return maxNbBits; +} + +size_t HUF_estimateCompressedSize(const HUF_CElt* CTable, const unsigned* count, unsigned maxSymbolValue) +{ + HUF_CElt const* ct = CTable + 1; + size_t nbBits = 0; + int s; + for (s = 0; s <= (int)maxSymbolValue; ++s) { + nbBits += HUF_getNbBits(ct[s]) * count[s]; + } + return nbBits >> 3; +} + +int HUF_validateCTable(const HUF_CElt* CTable, const unsigned* count, unsigned maxSymbolValue) { + HUF_CTableHeader header = HUF_readCTableHeader(CTable); + HUF_CElt const* ct = CTable + 1; + int bad = 0; + int s; + + assert(header.tableLog <= HUF_TABLELOG_ABSOLUTEMAX); + + if (header.maxSymbolValue < maxSymbolValue) + return 0; + + for (s = 0; s <= (int)maxSymbolValue; ++s) { + bad |= (count[s] != 0) & (HUF_getNbBits(ct[s]) == 0); + } + return !bad; +} + +size_t HUF_compressBound(size_t size) { return HUF_COMPRESSBOUND(size); } + +/** HUF_CStream_t: + * Huffman uses its own BIT_CStream_t implementation. + * There are three major differences from BIT_CStream_t: + * 1. HUF_addBits() takes a HUF_CElt (size_t) which is + * the pair (nbBits, value) in the format: + * format: + * - Bits [0, 4) = nbBits + * - Bits [4, 64 - nbBits) = 0 + * - Bits [64 - nbBits, 64) = value + * 2. The bitContainer is built from the upper bits and + * right shifted. E.g. to add a new value of N bits + * you right shift the bitContainer by N, then or in + * the new value into the N upper bits. + * 3. The bitstream has two bit containers. You can add + * bits to the second container and merge them into + * the first container. + */ + +#define HUF_BITS_IN_CONTAINER (sizeof(size_t) * 8) + +typedef struct { + size_t bitContainer[2]; + size_t bitPos[2]; + + BYTE* startPtr; + BYTE* ptr; + BYTE* endPtr; +} HUF_CStream_t; + +/**! HUF_initCStream(): + * Initializes the bitstream. + * @returns 0 or an error code. + */ +static size_t HUF_initCStream(HUF_CStream_t* bitC, + void* startPtr, size_t dstCapacity) +{ + ZSTD_memset(bitC, 0, sizeof(*bitC)); + bitC->startPtr = (BYTE*)startPtr; + bitC->ptr = bitC->startPtr; + bitC->endPtr = bitC->startPtr + dstCapacity - sizeof(bitC->bitContainer[0]); + if (dstCapacity <= sizeof(bitC->bitContainer[0])) return ERROR(dstSize_tooSmall); + return 0; +} + +/*! HUF_addBits(): + * Adds the symbol stored in HUF_CElt elt to the bitstream. + * + * @param elt The element we're adding. This is a (nbBits, value) pair. + * See the HUF_CStream_t docs for the format. + * @param idx Insert into the bitstream at this idx. + * @param kFast This is a template parameter. If the bitstream is guaranteed + * to have at least 4 unused bits after this call it may be 1, + * otherwise it must be 0. HUF_addBits() is faster when fast is set. + */ +FORCE_INLINE_TEMPLATE void HUF_addBits(HUF_CStream_t* bitC, HUF_CElt elt, int idx, int kFast) +{ + assert(idx <= 1); + assert(HUF_getNbBits(elt) <= HUF_TABLELOG_ABSOLUTEMAX); + /* This is efficient on x86-64 with BMI2 because shrx + * only reads the low 6 bits of the register. The compiler + * knows this and elides the mask. When fast is set, + * every operation can use the same value loaded from elt. + */ + bitC->bitContainer[idx] >>= HUF_getNbBits(elt); + bitC->bitContainer[idx] |= kFast ? HUF_getValueFast(elt) : HUF_getValue(elt); + /* We only read the low 8 bits of bitC->bitPos[idx] so it + * doesn't matter that the high bits have noise from the value. + */ + bitC->bitPos[idx] += HUF_getNbBitsFast(elt); + assert((bitC->bitPos[idx] & 0xFF) <= HUF_BITS_IN_CONTAINER); + /* The last 4-bits of elt are dirty if fast is set, + * so we must not be overwriting bits that have already been + * inserted into the bit container. + */ +#if DEBUGLEVEL >= 1 + { + size_t const nbBits = HUF_getNbBits(elt); + size_t const dirtyBits = nbBits == 0 ? 0 : ZSTD_highbit32((U32)nbBits) + 1; + (void)dirtyBits; + /* Middle bits are 0. */ + assert(((elt >> dirtyBits) << (dirtyBits + nbBits)) == 0); + /* We didn't overwrite any bits in the bit container. */ + assert(!kFast || (bitC->bitPos[idx] & 0xFF) <= HUF_BITS_IN_CONTAINER); + (void)dirtyBits; + } +#endif +} + +FORCE_INLINE_TEMPLATE void HUF_zeroIndex1(HUF_CStream_t* bitC) +{ + bitC->bitContainer[1] = 0; + bitC->bitPos[1] = 0; +} + +/*! HUF_mergeIndex1() : + * Merges the bit container @ index 1 into the bit container @ index 0 + * and zeros the bit container @ index 1. + */ +FORCE_INLINE_TEMPLATE void HUF_mergeIndex1(HUF_CStream_t* bitC) +{ + assert((bitC->bitPos[1] & 0xFF) < HUF_BITS_IN_CONTAINER); + bitC->bitContainer[0] >>= (bitC->bitPos[1] & 0xFF); + bitC->bitContainer[0] |= bitC->bitContainer[1]; + bitC->bitPos[0] += bitC->bitPos[1]; + assert((bitC->bitPos[0] & 0xFF) <= HUF_BITS_IN_CONTAINER); +} + +/*! HUF_flushBits() : +* Flushes the bits in the bit container @ index 0. +* +* @post bitPos will be < 8. +* @param kFast If kFast is set then we must know a-priori that +* the bit container will not overflow. +*/ +FORCE_INLINE_TEMPLATE void HUF_flushBits(HUF_CStream_t* bitC, int kFast) +{ + /* The upper bits of bitPos are noisy, so we must mask by 0xFF. */ + size_t const nbBits = bitC->bitPos[0] & 0xFF; + size_t const nbBytes = nbBits >> 3; + /* The top nbBits bits of bitContainer are the ones we need. */ + size_t const bitContainer = bitC->bitContainer[0] >> (HUF_BITS_IN_CONTAINER - nbBits); + /* Mask bitPos to account for the bytes we consumed. */ + bitC->bitPos[0] &= 7; + assert(nbBits > 0); + assert(nbBits <= sizeof(bitC->bitContainer[0]) * 8); + assert(bitC->ptr <= bitC->endPtr); + MEM_writeLEST(bitC->ptr, bitContainer); + bitC->ptr += nbBytes; + assert(!kFast || bitC->ptr <= bitC->endPtr); + if (!kFast && bitC->ptr > bitC->endPtr) bitC->ptr = bitC->endPtr; + /* bitContainer doesn't need to be modified because the leftover + * bits are already the top bitPos bits. And we don't care about + * noise in the lower values. + */ +} + +/*! HUF_endMark() + * @returns The Huffman stream end mark: A 1-bit value = 1. + */ +static HUF_CElt HUF_endMark(void) +{ + HUF_CElt endMark; + HUF_setNbBits(&endMark, 1); + HUF_setValue(&endMark, 1); + return endMark; +} + +/*! HUF_closeCStream() : + * @return Size of CStream, in bytes, + * or 0 if it could not fit into dstBuffer */ +static size_t HUF_closeCStream(HUF_CStream_t* bitC) +{ + HUF_addBits(bitC, HUF_endMark(), /* idx */ 0, /* kFast */ 0); + HUF_flushBits(bitC, /* kFast */ 0); + { + size_t const nbBits = bitC->bitPos[0] & 0xFF; + if (bitC->ptr >= bitC->endPtr) return 0; /* overflow detected */ + return (size_t)(bitC->ptr - bitC->startPtr) + (nbBits > 0); + } +} + +FORCE_INLINE_TEMPLATE void +HUF_encodeSymbol(HUF_CStream_t* bitCPtr, U32 symbol, const HUF_CElt* CTable, int idx, int fast) +{ + HUF_addBits(bitCPtr, CTable[symbol], idx, fast); +} + +FORCE_INLINE_TEMPLATE void +HUF_compress1X_usingCTable_internal_body_loop(HUF_CStream_t* bitC, + const BYTE* ip, size_t srcSize, + const HUF_CElt* ct, + int kUnroll, int kFastFlush, int kLastFast) +{ + /* Join to kUnroll */ + int n = (int)srcSize; + int rem = n % kUnroll; + if (rem > 0) { + for (; rem > 0; --rem) { + HUF_encodeSymbol(bitC, ip[--n], ct, 0, /* fast */ 0); + } + HUF_flushBits(bitC, kFastFlush); + } + assert(n % kUnroll == 0); + + /* Join to 2 * kUnroll */ + if (n % (2 * kUnroll)) { + int u; + for (u = 1; u < kUnroll; ++u) { + HUF_encodeSymbol(bitC, ip[n - u], ct, 0, 1); + } + HUF_encodeSymbol(bitC, ip[n - kUnroll], ct, 0, kLastFast); + HUF_flushBits(bitC, kFastFlush); + n -= kUnroll; + } + assert(n % (2 * kUnroll) == 0); + + for (; n>0; n-= 2 * kUnroll) { + /* Encode kUnroll symbols into the bitstream @ index 0. */ + int u; + for (u = 1; u < kUnroll; ++u) { + HUF_encodeSymbol(bitC, ip[n - u], ct, /* idx */ 0, /* fast */ 1); + } + HUF_encodeSymbol(bitC, ip[n - kUnroll], ct, /* idx */ 0, /* fast */ kLastFast); + HUF_flushBits(bitC, kFastFlush); + /* Encode kUnroll symbols into the bitstream @ index 1. + * This allows us to start filling the bit container + * without any data dependencies. + */ + HUF_zeroIndex1(bitC); + for (u = 1; u < kUnroll; ++u) { + HUF_encodeSymbol(bitC, ip[n - kUnroll - u], ct, /* idx */ 1, /* fast */ 1); + } + HUF_encodeSymbol(bitC, ip[n - kUnroll - kUnroll], ct, /* idx */ 1, /* fast */ kLastFast); + /* Merge bitstream @ index 1 into the bitstream @ index 0 */ + HUF_mergeIndex1(bitC); + HUF_flushBits(bitC, kFastFlush); + } + assert(n == 0); + +} + +/** + * Returns a tight upper bound on the output space needed by Huffman + * with 8 bytes buffer to handle over-writes. If the output is at least + * this large we don't need to do bounds checks during Huffman encoding. + */ +static size_t HUF_tightCompressBound(size_t srcSize, size_t tableLog) +{ + return ((srcSize * tableLog) >> 3) + 8; +} + + +FORCE_INLINE_TEMPLATE size_t +HUF_compress1X_usingCTable_internal_body(void* dst, size_t dstSize, + const void* src, size_t srcSize, + const HUF_CElt* CTable) +{ + U32 const tableLog = HUF_readCTableHeader(CTable).tableLog; + HUF_CElt const* ct = CTable + 1; + const BYTE* ip = (const BYTE*) src; + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = ostart + dstSize; + HUF_CStream_t bitC; + + /* init */ + if (dstSize < 8) return 0; /* not enough space to compress */ + { BYTE* op = ostart; + size_t const initErr = HUF_initCStream(&bitC, op, (size_t)(oend-op)); + if (HUF_isError(initErr)) return 0; } + + if (dstSize < HUF_tightCompressBound(srcSize, (size_t)tableLog) || tableLog > 11) + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ MEM_32bits() ? 2 : 4, /* kFast */ 0, /* kLastFast */ 0); + else { + if (MEM_32bits()) { + switch (tableLog) { + case 11: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 2, /* kFastFlush */ 1, /* kLastFast */ 0); + break; + case 10: ZSTD_FALLTHROUGH; + case 9: ZSTD_FALLTHROUGH; + case 8: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 2, /* kFastFlush */ 1, /* kLastFast */ 1); + break; + case 7: ZSTD_FALLTHROUGH; + default: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 3, /* kFastFlush */ 1, /* kLastFast */ 1); + break; + } + } else { + switch (tableLog) { + case 11: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 5, /* kFastFlush */ 1, /* kLastFast */ 0); + break; + case 10: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 5, /* kFastFlush */ 1, /* kLastFast */ 1); + break; + case 9: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 6, /* kFastFlush */ 1, /* kLastFast */ 0); + break; + case 8: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 7, /* kFastFlush */ 1, /* kLastFast */ 0); + break; + case 7: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 8, /* kFastFlush */ 1, /* kLastFast */ 0); + break; + case 6: ZSTD_FALLTHROUGH; + default: + HUF_compress1X_usingCTable_internal_body_loop(&bitC, ip, srcSize, ct, /* kUnroll */ 9, /* kFastFlush */ 1, /* kLastFast */ 1); + break; + } + } + } + assert(bitC.ptr <= bitC.endPtr); + + return HUF_closeCStream(&bitC); +} + +#if DYNAMIC_BMI2 + +static BMI2_TARGET_ATTRIBUTE size_t +HUF_compress1X_usingCTable_internal_bmi2(void* dst, size_t dstSize, + const void* src, size_t srcSize, + const HUF_CElt* CTable) +{ + return HUF_compress1X_usingCTable_internal_body(dst, dstSize, src, srcSize, CTable); +} + +static size_t +HUF_compress1X_usingCTable_internal_default(void* dst, size_t dstSize, + const void* src, size_t srcSize, + const HUF_CElt* CTable) +{ + return HUF_compress1X_usingCTable_internal_body(dst, dstSize, src, srcSize, CTable); +} + +static size_t +HUF_compress1X_usingCTable_internal(void* dst, size_t dstSize, + const void* src, size_t srcSize, + const HUF_CElt* CTable, const int flags) +{ + if (flags & HUF_flags_bmi2) { + return HUF_compress1X_usingCTable_internal_bmi2(dst, dstSize, src, srcSize, CTable); + } + return HUF_compress1X_usingCTable_internal_default(dst, dstSize, src, srcSize, CTable); +} + +#else + +static size_t +HUF_compress1X_usingCTable_internal(void* dst, size_t dstSize, + const void* src, size_t srcSize, + const HUF_CElt* CTable, const int flags) +{ + (void)flags; + return HUF_compress1X_usingCTable_internal_body(dst, dstSize, src, srcSize, CTable); +} + +#endif + +size_t HUF_compress1X_usingCTable(void* dst, size_t dstSize, const void* src, size_t srcSize, const HUF_CElt* CTable, int flags) +{ + return HUF_compress1X_usingCTable_internal(dst, dstSize, src, srcSize, CTable, flags); +} + +static size_t +HUF_compress4X_usingCTable_internal(void* dst, size_t dstSize, + const void* src, size_t srcSize, + const HUF_CElt* CTable, int flags) +{ + size_t const segmentSize = (srcSize+3)/4; /* first 3 segments */ + const BYTE* ip = (const BYTE*) src; + const BYTE* const iend = ip + srcSize; + BYTE* const ostart = (BYTE*) dst; + BYTE* const oend = ostart + dstSize; + BYTE* op = ostart; + + if (dstSize < 6 + 1 + 1 + 1 + 8) return 0; /* minimum space to compress successfully */ + if (srcSize < 12) return 0; /* no saving possible : too small input */ + op += 6; /* jumpTable */ + + assert(op <= oend); + { CHECK_V_F(cSize, HUF_compress1X_usingCTable_internal(op, (size_t)(oend-op), ip, segmentSize, CTable, flags) ); + if (cSize == 0 || cSize > 65535) return 0; + MEM_writeLE16(ostart, (U16)cSize); + op += cSize; + } + + ip += segmentSize; + assert(op <= oend); + { CHECK_V_F(cSize, HUF_compress1X_usingCTable_internal(op, (size_t)(oend-op), ip, segmentSize, CTable, flags) ); + if (cSize == 0 || cSize > 65535) return 0; + MEM_writeLE16(ostart+2, (U16)cSize); + op += cSize; + } + + ip += segmentSize; + assert(op <= oend); + { CHECK_V_F(cSize, HUF_compress1X_usingCTable_internal(op, (size_t)(oend-op), ip, segmentSize, CTable, flags) ); + if (cSize == 0 || cSize > 65535) return 0; + MEM_writeLE16(ostart+4, (U16)cSize); + op += cSize; + } + + ip += segmentSize; + assert(op <= oend); + assert(ip <= iend); + { CHECK_V_F(cSize, HUF_compress1X_usingCTable_internal(op, (size_t)(oend-op), ip, (size_t)(iend-ip), CTable, flags) ); + if (cSize == 0 || cSize > 65535) return 0; + op += cSize; + } + + return (size_t)(op-ostart); +} + +size_t HUF_compress4X_usingCTable(void* dst, size_t dstSize, const void* src, size_t srcSize, const HUF_CElt* CTable, int flags) +{ + return HUF_compress4X_usingCTable_internal(dst, dstSize, src, srcSize, CTable, flags); +} + +typedef enum { HUF_singleStream, HUF_fourStreams } HUF_nbStreams_e; + +static size_t HUF_compressCTable_internal( + BYTE* const ostart, BYTE* op, BYTE* const oend, + const void* src, size_t srcSize, + HUF_nbStreams_e nbStreams, const HUF_CElt* CTable, const int flags) +{ + size_t const cSize = (nbStreams==HUF_singleStream) ? + HUF_compress1X_usingCTable_internal(op, (size_t)(oend - op), src, srcSize, CTable, flags) : + HUF_compress4X_usingCTable_internal(op, (size_t)(oend - op), src, srcSize, CTable, flags); + if (HUF_isError(cSize)) { return cSize; } + if (cSize==0) { return 0; } /* uncompressible */ + op += cSize; + /* check compressibility */ + assert(op >= ostart); + if ((size_t)(op-ostart) >= srcSize-1) { return 0; } + return (size_t)(op-ostart); +} + +typedef struct { + unsigned count[HUF_SYMBOLVALUE_MAX + 1]; + HUF_CElt CTable[HUF_CTABLE_SIZE_ST(HUF_SYMBOLVALUE_MAX)]; + union { + HUF_buildCTable_wksp_tables buildCTable_wksp; + HUF_WriteCTableWksp writeCTable_wksp; + U32 hist_wksp[HIST_WKSP_SIZE_U32]; + } wksps; +} HUF_compress_tables_t; + +#define SUSPECT_INCOMPRESSIBLE_SAMPLE_SIZE 4096 +#define SUSPECT_INCOMPRESSIBLE_SAMPLE_RATIO 10 /* Must be >= 2 */ + +unsigned HUF_cardinality(const unsigned* count, unsigned maxSymbolValue) +{ + unsigned cardinality = 0; + unsigned i; + + for (i = 0; i < maxSymbolValue + 1; i++) { + if (count[i] != 0) cardinality += 1; + } + + return cardinality; +} + +unsigned HUF_minTableLog(unsigned symbolCardinality) +{ + U32 minBitsSymbols = ZSTD_highbit32(symbolCardinality) + 1; + return minBitsSymbols; +} + +unsigned HUF_optimalTableLog( + unsigned maxTableLog, + size_t srcSize, + unsigned maxSymbolValue, + void* workSpace, size_t wkspSize, + HUF_CElt* table, + const unsigned* count, + int flags) +{ + assert(srcSize > 1); /* Not supported, RLE should be used instead */ + assert(wkspSize >= sizeof(HUF_buildCTable_wksp_tables)); + + if (!(flags & HUF_flags_optimalDepth)) { + /* cheap evaluation, based on FSE */ + return FSE_optimalTableLog_internal(maxTableLog, srcSize, maxSymbolValue, 1); + } + + { BYTE* dst = (BYTE*)workSpace + sizeof(HUF_WriteCTableWksp); + size_t dstSize = wkspSize - sizeof(HUF_WriteCTableWksp); + size_t hSize, newSize; + const unsigned symbolCardinality = HUF_cardinality(count, maxSymbolValue); + const unsigned minTableLog = HUF_minTableLog(symbolCardinality); + size_t optSize = ((size_t) ~0) - 1; + unsigned optLog = maxTableLog, optLogGuess; + + DEBUGLOG(6, "HUF_optimalTableLog: probing huf depth (srcSize=%zu)", srcSize); + + /* Search until size increases */ + for (optLogGuess = minTableLog; optLogGuess <= maxTableLog; optLogGuess++) { + DEBUGLOG(7, "checking for huffLog=%u", optLogGuess); + + { size_t maxBits = HUF_buildCTable_wksp(table, count, maxSymbolValue, optLogGuess, workSpace, wkspSize); + if (ERR_isError(maxBits)) continue; + + if (maxBits < optLogGuess && optLogGuess > minTableLog) break; + + hSize = HUF_writeCTable_wksp(dst, dstSize, table, maxSymbolValue, (U32)maxBits, workSpace, wkspSize); + } + + if (ERR_isError(hSize)) continue; + + newSize = HUF_estimateCompressedSize(table, count, maxSymbolValue) + hSize; + + if (newSize > optSize + 1) { + break; + } + + if (newSize < optSize) { + optSize = newSize; + optLog = optLogGuess; + } + } + assert(optLog <= HUF_TABLELOG_MAX); + return optLog; + } +} + +/* HUF_compress_internal() : + * `workSpace_align4` must be aligned on 4-bytes boundaries, + * and occupies the same space as a table of HUF_WORKSPACE_SIZE_U64 unsigned */ +static size_t +HUF_compress_internal (void* dst, size_t dstSize, + const void* src, size_t srcSize, + unsigned maxSymbolValue, unsigned huffLog, + HUF_nbStreams_e nbStreams, + void* workSpace, size_t wkspSize, + HUF_CElt* oldHufTable, HUF_repeat* repeat, int flags) +{ + HUF_compress_tables_t* const table = (HUF_compress_tables_t*)HUF_alignUpWorkspace(workSpace, &wkspSize, ZSTD_ALIGNOF(size_t)); + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = ostart + dstSize; + BYTE* op = ostart; + + DEBUGLOG(5, "HUF_compress_internal (srcSize=%zu)", srcSize); + HUF_STATIC_ASSERT(sizeof(*table) + HUF_WORKSPACE_MAX_ALIGNMENT <= HUF_WORKSPACE_SIZE); + + /* checks & inits */ + if (wkspSize < sizeof(*table)) return ERROR(workSpace_tooSmall); + if (!srcSize) return 0; /* Uncompressed */ + if (!dstSize) return 0; /* cannot fit anything within dst budget */ + if (srcSize > HUF_BLOCKSIZE_MAX) return ERROR(srcSize_wrong); /* current block size limit */ + if (huffLog > HUF_TABLELOG_MAX) return ERROR(tableLog_tooLarge); + if (maxSymbolValue > HUF_SYMBOLVALUE_MAX) return ERROR(maxSymbolValue_tooLarge); + if (!maxSymbolValue) maxSymbolValue = HUF_SYMBOLVALUE_MAX; + if (!huffLog) huffLog = HUF_TABLELOG_DEFAULT; + + /* Heuristic : If old table is valid, use it for small inputs */ + if ((flags & HUF_flags_preferRepeat) && repeat && *repeat == HUF_repeat_valid) { + return HUF_compressCTable_internal(ostart, op, oend, + src, srcSize, + nbStreams, oldHufTable, flags); + } + + /* If uncompressible data is suspected, do a smaller sampling first */ + DEBUG_STATIC_ASSERT(SUSPECT_INCOMPRESSIBLE_SAMPLE_RATIO >= 2); + if ((flags & HUF_flags_suspectUncompressible) && srcSize >= (SUSPECT_INCOMPRESSIBLE_SAMPLE_SIZE * SUSPECT_INCOMPRESSIBLE_SAMPLE_RATIO)) { + size_t largestTotal = 0; + DEBUGLOG(5, "input suspected incompressible : sampling to check"); + { unsigned maxSymbolValueBegin = maxSymbolValue; + CHECK_V_F(largestBegin, HIST_count_simple (table->count, &maxSymbolValueBegin, (const BYTE*)src, SUSPECT_INCOMPRESSIBLE_SAMPLE_SIZE) ); + largestTotal += largestBegin; + } + { unsigned maxSymbolValueEnd = maxSymbolValue; + CHECK_V_F(largestEnd, HIST_count_simple (table->count, &maxSymbolValueEnd, (const BYTE*)src + srcSize - SUSPECT_INCOMPRESSIBLE_SAMPLE_SIZE, SUSPECT_INCOMPRESSIBLE_SAMPLE_SIZE) ); + largestTotal += largestEnd; + } + if (largestTotal <= ((2 * SUSPECT_INCOMPRESSIBLE_SAMPLE_SIZE) >> 7)+4) return 0; /* heuristic : probably not compressible enough */ + } + + /* Scan input and build symbol stats */ + { CHECK_V_F(largest, HIST_count_wksp (table->count, &maxSymbolValue, (const BYTE*)src, srcSize, table->wksps.hist_wksp, sizeof(table->wksps.hist_wksp)) ); + if (largest == srcSize) { *ostart = ((const BYTE*)src)[0]; return 1; } /* single symbol, rle */ + if (largest <= (srcSize >> 7)+4) return 0; /* heuristic : probably not compressible enough */ + } + DEBUGLOG(6, "histogram detail completed (%zu symbols)", showU32(table->count, maxSymbolValue+1)); + + /* Check validity of previous table */ + if ( repeat + && *repeat == HUF_repeat_check + && !HUF_validateCTable(oldHufTable, table->count, maxSymbolValue)) { + *repeat = HUF_repeat_none; + } + /* Heuristic : use existing table for small inputs */ + if ((flags & HUF_flags_preferRepeat) && repeat && *repeat != HUF_repeat_none) { + return HUF_compressCTable_internal(ostart, op, oend, + src, srcSize, + nbStreams, oldHufTable, flags); + } + + /* Build Huffman Tree */ + huffLog = HUF_optimalTableLog(huffLog, srcSize, maxSymbolValue, &table->wksps, sizeof(table->wksps), table->CTable, table->count, flags); + { size_t const maxBits = HUF_buildCTable_wksp(table->CTable, table->count, + maxSymbolValue, huffLog, + &table->wksps.buildCTable_wksp, sizeof(table->wksps.buildCTable_wksp)); + CHECK_F(maxBits); + huffLog = (U32)maxBits; + DEBUGLOG(6, "bit distribution completed (%zu symbols)", showCTableBits(table->CTable + 1, maxSymbolValue+1)); + } + + /* Write table description header */ + { CHECK_V_F(hSize, HUF_writeCTable_wksp(op, dstSize, table->CTable, maxSymbolValue, huffLog, + &table->wksps.writeCTable_wksp, sizeof(table->wksps.writeCTable_wksp)) ); + /* Check if using previous huffman table is beneficial */ + if (repeat && *repeat != HUF_repeat_none) { + size_t const oldSize = HUF_estimateCompressedSize(oldHufTable, table->count, maxSymbolValue); + size_t const newSize = HUF_estimateCompressedSize(table->CTable, table->count, maxSymbolValue); + if (oldSize <= hSize + newSize || hSize + 12 >= srcSize) { + return HUF_compressCTable_internal(ostart, op, oend, + src, srcSize, + nbStreams, oldHufTable, flags); + } } + + /* Use the new huffman table */ + if (hSize + 12ul >= srcSize) { return 0; } + op += hSize; + if (repeat) { *repeat = HUF_repeat_none; } + if (oldHufTable) + ZSTD_memcpy(oldHufTable, table->CTable, sizeof(table->CTable)); /* Save new table */ + } + return HUF_compressCTable_internal(ostart, op, oend, + src, srcSize, + nbStreams, table->CTable, flags); +} + +size_t HUF_compress1X_repeat (void* dst, size_t dstSize, + const void* src, size_t srcSize, + unsigned maxSymbolValue, unsigned huffLog, + void* workSpace, size_t wkspSize, + HUF_CElt* hufTable, HUF_repeat* repeat, int flags) +{ + DEBUGLOG(5, "HUF_compress1X_repeat (srcSize = %zu)", srcSize); + return HUF_compress_internal(dst, dstSize, src, srcSize, + maxSymbolValue, huffLog, HUF_singleStream, + workSpace, wkspSize, hufTable, + repeat, flags); +} + +/* HUF_compress4X_repeat(): + * compress input using 4 streams. + * consider skipping quickly + * reuse an existing huffman compression table */ +size_t HUF_compress4X_repeat (void* dst, size_t dstSize, + const void* src, size_t srcSize, + unsigned maxSymbolValue, unsigned huffLog, + void* workSpace, size_t wkspSize, + HUF_CElt* hufTable, HUF_repeat* repeat, int flags) +{ + DEBUGLOG(5, "HUF_compress4X_repeat (srcSize = %zu)", srcSize); + return HUF_compress_internal(dst, dstSize, src, srcSize, + maxSymbolValue, huffLog, HUF_fourStreams, + workSpace, wkspSize, + hufTable, repeat, flags); +} +/**** ended inlining compress/huf_compress.c ****/ +/**** start inlining compress/zstd_compress_literals.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + /*-************************************* + * Dependencies + ***************************************/ +/**** start inlining zstd_compress_literals.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_COMPRESS_LITERALS_H +#define ZSTD_COMPRESS_LITERALS_H + +/**** start inlining zstd_compress_internal.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/* This header contains definitions + * that shall **only** be used by modules within lib/compress. + */ + +#ifndef ZSTD_COMPRESS_H +#define ZSTD_COMPRESS_H + +/*-************************************* +* Dependencies +***************************************/ +/**** skipping file: ../common/zstd_internal.h ****/ +/**** start inlining zstd_cwksp.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_CWKSP_H +#define ZSTD_CWKSP_H + +/*-************************************* +* Dependencies +***************************************/ +/**** skipping file: ../common/allocations.h ****/ +/**** skipping file: ../common/zstd_internal.h ****/ +/**** skipping file: ../common/portability_macros.h ****/ + +#if defined (__cplusplus) +extern "C" { +#endif + +/*-************************************* +* Constants +***************************************/ + +/* Since the workspace is effectively its own little malloc implementation / + * arena, when we run under ASAN, we should similarly insert redzones between + * each internal element of the workspace, so ASAN will catch overruns that + * reach outside an object but that stay inside the workspace. + * + * This defines the size of that redzone. + */ +#ifndef ZSTD_CWKSP_ASAN_REDZONE_SIZE +#define ZSTD_CWKSP_ASAN_REDZONE_SIZE 128 +#endif + + +/* Set our tables and aligneds to align by 64 bytes */ +#define ZSTD_CWKSP_ALIGNMENT_BYTES 64 + +/*-************************************* +* Structures +***************************************/ +typedef enum { + ZSTD_cwksp_alloc_objects, + ZSTD_cwksp_alloc_aligned_init_once, + ZSTD_cwksp_alloc_aligned, + ZSTD_cwksp_alloc_buffers +} ZSTD_cwksp_alloc_phase_e; + +/** + * Used to describe whether the workspace is statically allocated (and will not + * necessarily ever be freed), or if it's dynamically allocated and we can + * expect a well-formed caller to free this. + */ +typedef enum { + ZSTD_cwksp_dynamic_alloc, + ZSTD_cwksp_static_alloc +} ZSTD_cwksp_static_alloc_e; + +/** + * Zstd fits all its internal datastructures into a single continuous buffer, + * so that it only needs to perform a single OS allocation (or so that a buffer + * can be provided to it and it can perform no allocations at all). This buffer + * is called the workspace. + * + * Several optimizations complicate that process of allocating memory ranges + * from this workspace for each internal datastructure: + * + * - These different internal datastructures have different setup requirements: + * + * - The static objects need to be cleared once and can then be trivially + * reused for each compression. + * + * - Various buffers don't need to be initialized at all--they are always + * written into before they're read. + * + * - The matchstate tables have a unique requirement that they don't need + * their memory to be totally cleared, but they do need the memory to have + * some bound, i.e., a guarantee that all values in the memory they've been + * allocated is less than some maximum value (which is the starting value + * for the indices that they will then use for compression). When this + * guarantee is provided to them, they can use the memory without any setup + * work. When it can't, they have to clear the area. + * + * - These buffers also have different alignment requirements. + * + * - We would like to reuse the objects in the workspace for multiple + * compressions without having to perform any expensive reallocation or + * reinitialization work. + * + * - We would like to be able to efficiently reuse the workspace across + * multiple compressions **even when the compression parameters change** and + * we need to resize some of the objects (where possible). + * + * To attempt to manage this buffer, given these constraints, the ZSTD_cwksp + * abstraction was created. It works as follows: + * + * Workspace Layout: + * + * [ ... workspace ... ] + * [objects][tables ->] free space [<- buffers][<- aligned][<- init once] + * + * The various objects that live in the workspace are divided into the + * following categories, and are allocated separately: + * + * - Static objects: this is optionally the enclosing ZSTD_CCtx or ZSTD_CDict, + * so that literally everything fits in a single buffer. Note: if present, + * this must be the first object in the workspace, since ZSTD_customFree{CCtx, + * CDict}() rely on a pointer comparison to see whether one or two frees are + * required. + * + * - Fixed size objects: these are fixed-size, fixed-count objects that are + * nonetheless "dynamically" allocated in the workspace so that we can + * control how they're initialized separately from the broader ZSTD_CCtx. + * Examples: + * - Entropy Workspace + * - 2 x ZSTD_compressedBlockState_t + * - CDict dictionary contents + * + * - Tables: these are any of several different datastructures (hash tables, + * chain tables, binary trees) that all respect a common format: they are + * uint32_t arrays, all of whose values are between 0 and (nextSrc - base). + * Their sizes depend on the cparams. These tables are 64-byte aligned. + * + * - Init once: these buffers require to be initialized at least once before + * use. They should be used when we want to skip memory initialization + * while not triggering memory checkers (like Valgrind) when reading from + * from this memory without writing to it first. + * These buffers should be used carefully as they might contain data + * from previous compressions. + * Buffers are aligned to 64 bytes. + * + * - Aligned: these buffers don't require any initialization before they're + * used. The user of the buffer should make sure they write into a buffer + * location before reading from it. + * Buffers are aligned to 64 bytes. + * + * - Buffers: these buffers are used for various purposes that don't require + * any alignment or initialization before they're used. This means they can + * be moved around at no cost for a new compression. + * + * Allocating Memory: + * + * The various types of objects must be allocated in order, so they can be + * correctly packed into the workspace buffer. That order is: + * + * 1. Objects + * 2. Init once / Tables + * 3. Aligned / Tables + * 4. Buffers / Tables + * + * Attempts to reserve objects of different types out of order will fail. + */ +typedef struct { + void* workspace; + void* workspaceEnd; + + void* objectEnd; + void* tableEnd; + void* tableValidEnd; + void* allocStart; + void* initOnceStart; + + BYTE allocFailed; + int workspaceOversizedDuration; + ZSTD_cwksp_alloc_phase_e phase; + ZSTD_cwksp_static_alloc_e isStatic; +} ZSTD_cwksp; + +/*-************************************* +* Functions +***************************************/ + +MEM_STATIC size_t ZSTD_cwksp_available_space(ZSTD_cwksp* ws); +MEM_STATIC void* ZSTD_cwksp_initialAllocStart(ZSTD_cwksp* ws); + +MEM_STATIC void ZSTD_cwksp_assert_internal_consistency(ZSTD_cwksp* ws) { + (void)ws; + assert(ws->workspace <= ws->objectEnd); + assert(ws->objectEnd <= ws->tableEnd); + assert(ws->objectEnd <= ws->tableValidEnd); + assert(ws->tableEnd <= ws->allocStart); + assert(ws->tableValidEnd <= ws->allocStart); + assert(ws->allocStart <= ws->workspaceEnd); + assert(ws->initOnceStart <= ZSTD_cwksp_initialAllocStart(ws)); + assert(ws->workspace <= ws->initOnceStart); +#if ZSTD_MEMORY_SANITIZER + { + intptr_t const offset = __msan_test_shadow(ws->initOnceStart, + (U8*)ZSTD_cwksp_initialAllocStart(ws) - (U8*)ws->initOnceStart); + (void)offset; +#if defined(ZSTD_MSAN_PRINT) + if(offset!=-1) { + __msan_print_shadow((U8*)ws->initOnceStart + offset - 8, 32); + } +#endif + assert(offset==-1); + }; +#endif +} + +/** + * Align must be a power of 2. + */ +MEM_STATIC size_t ZSTD_cwksp_align(size_t size, size_t const align) { + size_t const mask = align - 1; + assert((align & mask) == 0); + return (size + mask) & ~mask; +} + +/** + * Use this to determine how much space in the workspace we will consume to + * allocate this object. (Normally it should be exactly the size of the object, + * but under special conditions, like ASAN, where we pad each object, it might + * be larger.) + * + * Since tables aren't currently redzoned, you don't need to call through this + * to figure out how much space you need for the matchState tables. Everything + * else is though. + * + * Do not use for sizing aligned buffers. Instead, use ZSTD_cwksp_aligned_alloc_size(). + */ +MEM_STATIC size_t ZSTD_cwksp_alloc_size(size_t size) { + if (size == 0) + return 0; +#if ZSTD_ADDRESS_SANITIZER && !defined (ZSTD_ASAN_DONT_POISON_WORKSPACE) + return size + 2 * ZSTD_CWKSP_ASAN_REDZONE_SIZE; +#else + return size; +#endif +} + +/** + * Returns an adjusted alloc size that is the nearest larger multiple of 64 bytes. + * Used to determine the number of bytes required for a given "aligned". + */ +MEM_STATIC size_t ZSTD_cwksp_aligned_alloc_size(size_t size) { + return ZSTD_cwksp_alloc_size(ZSTD_cwksp_align(size, ZSTD_CWKSP_ALIGNMENT_BYTES)); +} + +/** + * Returns the amount of additional space the cwksp must allocate + * for internal purposes (currently only alignment). + */ +MEM_STATIC size_t ZSTD_cwksp_slack_space_required(void) { + /* For alignment, the wksp will always allocate an additional 2*ZSTD_CWKSP_ALIGNMENT_BYTES + * bytes to align the beginning of tables section and end of buffers; + */ + size_t const slackSpace = ZSTD_CWKSP_ALIGNMENT_BYTES * 2; + return slackSpace; +} + + +/** + * Return the number of additional bytes required to align a pointer to the given number of bytes. + * alignBytes must be a power of two. + */ +MEM_STATIC size_t ZSTD_cwksp_bytes_to_align_ptr(void* ptr, const size_t alignBytes) { + size_t const alignBytesMask = alignBytes - 1; + size_t const bytes = (alignBytes - ((size_t)ptr & (alignBytesMask))) & alignBytesMask; + assert((alignBytes & alignBytesMask) == 0); + assert(bytes < alignBytes); + return bytes; +} + +/** + * Returns the initial value for allocStart which is used to determine the position from + * which we can allocate from the end of the workspace. + */ +MEM_STATIC void* ZSTD_cwksp_initialAllocStart(ZSTD_cwksp* ws) { + return (void*)((size_t)ws->workspaceEnd & ~(ZSTD_CWKSP_ALIGNMENT_BYTES-1)); +} + +/** + * Internal function. Do not use directly. + * Reserves the given number of bytes within the aligned/buffer segment of the wksp, + * which counts from the end of the wksp (as opposed to the object/table segment). + * + * Returns a pointer to the beginning of that space. + */ +MEM_STATIC void* +ZSTD_cwksp_reserve_internal_buffer_space(ZSTD_cwksp* ws, size_t const bytes) +{ + void* const alloc = (BYTE*)ws->allocStart - bytes; + void* const bottom = ws->tableEnd; + DEBUGLOG(5, "cwksp: reserving %p %zd bytes, %zd bytes remaining", + alloc, bytes, ZSTD_cwksp_available_space(ws) - bytes); + ZSTD_cwksp_assert_internal_consistency(ws); + assert(alloc >= bottom); + if (alloc < bottom) { + DEBUGLOG(4, "cwksp: alloc failed!"); + ws->allocFailed = 1; + return NULL; + } + /* the area is reserved from the end of wksp. + * If it overlaps with tableValidEnd, it voids guarantees on values' range */ + if (alloc < ws->tableValidEnd) { + ws->tableValidEnd = alloc; + } + ws->allocStart = alloc; + return alloc; +} + +/** + * Moves the cwksp to the next phase, and does any necessary allocations. + * cwksp initialization must necessarily go through each phase in order. + * Returns a 0 on success, or zstd error + */ +MEM_STATIC size_t +ZSTD_cwksp_internal_advance_phase(ZSTD_cwksp* ws, ZSTD_cwksp_alloc_phase_e phase) +{ + assert(phase >= ws->phase); + if (phase > ws->phase) { + /* Going from allocating objects to allocating initOnce / tables */ + if (ws->phase < ZSTD_cwksp_alloc_aligned_init_once && + phase >= ZSTD_cwksp_alloc_aligned_init_once) { + ws->tableValidEnd = ws->objectEnd; + ws->initOnceStart = ZSTD_cwksp_initialAllocStart(ws); + + { /* Align the start of the tables to 64 bytes. Use [0, 63] bytes */ + void *const alloc = ws->objectEnd; + size_t const bytesToAlign = ZSTD_cwksp_bytes_to_align_ptr(alloc, ZSTD_CWKSP_ALIGNMENT_BYTES); + void *const objectEnd = (BYTE *) alloc + bytesToAlign; + DEBUGLOG(5, "reserving table alignment addtl space: %zu", bytesToAlign); + RETURN_ERROR_IF(objectEnd > ws->workspaceEnd, memory_allocation, + "table phase - alignment initial allocation failed!"); + ws->objectEnd = objectEnd; + ws->tableEnd = objectEnd; /* table area starts being empty */ + if (ws->tableValidEnd < ws->tableEnd) { + ws->tableValidEnd = ws->tableEnd; + } + } + } + ws->phase = phase; + ZSTD_cwksp_assert_internal_consistency(ws); + } + return 0; +} + +/** + * Returns whether this object/buffer/etc was allocated in this workspace. + */ +MEM_STATIC int ZSTD_cwksp_owns_buffer(const ZSTD_cwksp* ws, const void* ptr) +{ + return (ptr != NULL) && (ws->workspace <= ptr) && (ptr < ws->workspaceEnd); +} + +/** + * Internal function. Do not use directly. + */ +MEM_STATIC void* +ZSTD_cwksp_reserve_internal(ZSTD_cwksp* ws, size_t bytes, ZSTD_cwksp_alloc_phase_e phase) +{ + void* alloc; + if (ZSTD_isError(ZSTD_cwksp_internal_advance_phase(ws, phase)) || bytes == 0) { + return NULL; + } + +#if ZSTD_ADDRESS_SANITIZER && !defined (ZSTD_ASAN_DONT_POISON_WORKSPACE) + /* over-reserve space */ + bytes += 2 * ZSTD_CWKSP_ASAN_REDZONE_SIZE; +#endif + + alloc = ZSTD_cwksp_reserve_internal_buffer_space(ws, bytes); + +#if ZSTD_ADDRESS_SANITIZER && !defined (ZSTD_ASAN_DONT_POISON_WORKSPACE) + /* Move alloc so there's ZSTD_CWKSP_ASAN_REDZONE_SIZE unused space on + * either size. */ + if (alloc) { + alloc = (BYTE *)alloc + ZSTD_CWKSP_ASAN_REDZONE_SIZE; + if (ws->isStatic == ZSTD_cwksp_dynamic_alloc) { + /* We need to keep the redzone poisoned while unpoisoning the bytes that + * are actually allocated. */ + __asan_unpoison_memory_region(alloc, bytes - 2 * ZSTD_CWKSP_ASAN_REDZONE_SIZE); + } + } +#endif + + return alloc; +} + +/** + * Reserves and returns unaligned memory. + */ +MEM_STATIC BYTE* ZSTD_cwksp_reserve_buffer(ZSTD_cwksp* ws, size_t bytes) +{ + return (BYTE*)ZSTD_cwksp_reserve_internal(ws, bytes, ZSTD_cwksp_alloc_buffers); +} + +/** + * Reserves and returns memory sized on and aligned on ZSTD_CWKSP_ALIGNMENT_BYTES (64 bytes). + * This memory has been initialized at least once in the past. + * This doesn't mean it has been initialized this time, and it might contain data from previous + * operations. + * The main usage is for algorithms that might need read access into uninitialized memory. + * The algorithm must maintain safety under these conditions and must make sure it doesn't + * leak any of the past data (directly or in side channels). + */ +MEM_STATIC void* ZSTD_cwksp_reserve_aligned_init_once(ZSTD_cwksp* ws, size_t bytes) +{ + size_t const alignedBytes = ZSTD_cwksp_align(bytes, ZSTD_CWKSP_ALIGNMENT_BYTES); + void* ptr = ZSTD_cwksp_reserve_internal(ws, alignedBytes, ZSTD_cwksp_alloc_aligned_init_once); + assert(((size_t)ptr & (ZSTD_CWKSP_ALIGNMENT_BYTES-1))== 0); + if(ptr && ptr < ws->initOnceStart) { + /* We assume the memory following the current allocation is either: + * 1. Not usable as initOnce memory (end of workspace) + * 2. Another initOnce buffer that has been allocated before (and so was previously memset) + * 3. An ASAN redzone, in which case we don't want to write on it + * For these reasons it should be fine to not explicitly zero every byte up to ws->initOnceStart. + * Note that we assume here that MSAN and ASAN cannot run in the same time. */ + ZSTD_memset(ptr, 0, MIN((size_t)((U8*)ws->initOnceStart - (U8*)ptr), alignedBytes)); + ws->initOnceStart = ptr; + } +#if ZSTD_MEMORY_SANITIZER + assert(__msan_test_shadow(ptr, bytes) == -1); +#endif + return ptr; +} + +/** + * Reserves and returns memory sized on and aligned on ZSTD_CWKSP_ALIGNMENT_BYTES (64 bytes). + */ +MEM_STATIC void* ZSTD_cwksp_reserve_aligned(ZSTD_cwksp* ws, size_t bytes) +{ + void* ptr = ZSTD_cwksp_reserve_internal(ws, ZSTD_cwksp_align(bytes, ZSTD_CWKSP_ALIGNMENT_BYTES), + ZSTD_cwksp_alloc_aligned); + assert(((size_t)ptr & (ZSTD_CWKSP_ALIGNMENT_BYTES-1))== 0); + return ptr; +} + +/** + * Aligned on 64 bytes. These buffers have the special property that + * their values remain constrained, allowing us to reuse them without + * memset()-ing them. + */ +MEM_STATIC void* ZSTD_cwksp_reserve_table(ZSTD_cwksp* ws, size_t bytes) +{ + const ZSTD_cwksp_alloc_phase_e phase = ZSTD_cwksp_alloc_aligned_init_once; + void* alloc; + void* end; + void* top; + + /* We can only start allocating tables after we are done reserving space for objects at the + * start of the workspace */ + if(ws->phase < phase) { + if (ZSTD_isError(ZSTD_cwksp_internal_advance_phase(ws, phase))) { + return NULL; + } + } + alloc = ws->tableEnd; + end = (BYTE *)alloc + bytes; + top = ws->allocStart; + + DEBUGLOG(5, "cwksp: reserving %p table %zd bytes, %zd bytes remaining", + alloc, bytes, ZSTD_cwksp_available_space(ws) - bytes); + assert((bytes & (sizeof(U32)-1)) == 0); + ZSTD_cwksp_assert_internal_consistency(ws); + assert(end <= top); + if (end > top) { + DEBUGLOG(4, "cwksp: table alloc failed!"); + ws->allocFailed = 1; + return NULL; + } + ws->tableEnd = end; + +#if ZSTD_ADDRESS_SANITIZER && !defined (ZSTD_ASAN_DONT_POISON_WORKSPACE) + if (ws->isStatic == ZSTD_cwksp_dynamic_alloc) { + __asan_unpoison_memory_region(alloc, bytes); + } +#endif + + assert((bytes & (ZSTD_CWKSP_ALIGNMENT_BYTES-1)) == 0); + assert(((size_t)alloc & (ZSTD_CWKSP_ALIGNMENT_BYTES-1))== 0); + return alloc; +} + +/** + * Aligned on sizeof(void*). + * Note : should happen only once, at workspace first initialization + */ +MEM_STATIC void* ZSTD_cwksp_reserve_object(ZSTD_cwksp* ws, size_t bytes) +{ + size_t const roundedBytes = ZSTD_cwksp_align(bytes, sizeof(void*)); + void* alloc = ws->objectEnd; + void* end = (BYTE*)alloc + roundedBytes; + +#if ZSTD_ADDRESS_SANITIZER && !defined (ZSTD_ASAN_DONT_POISON_WORKSPACE) + /* over-reserve space */ + end = (BYTE *)end + 2 * ZSTD_CWKSP_ASAN_REDZONE_SIZE; +#endif + + DEBUGLOG(4, + "cwksp: reserving %p object %zd bytes (rounded to %zd), %zd bytes remaining", + alloc, bytes, roundedBytes, ZSTD_cwksp_available_space(ws) - roundedBytes); + assert((size_t)alloc % ZSTD_ALIGNOF(void*) == 0); + assert(bytes % ZSTD_ALIGNOF(void*) == 0); + ZSTD_cwksp_assert_internal_consistency(ws); + /* we must be in the first phase, no advance is possible */ + if (ws->phase != ZSTD_cwksp_alloc_objects || end > ws->workspaceEnd) { + DEBUGLOG(3, "cwksp: object alloc failed!"); + ws->allocFailed = 1; + return NULL; + } + ws->objectEnd = end; + ws->tableEnd = end; + ws->tableValidEnd = end; + +#if ZSTD_ADDRESS_SANITIZER && !defined (ZSTD_ASAN_DONT_POISON_WORKSPACE) + /* Move alloc so there's ZSTD_CWKSP_ASAN_REDZONE_SIZE unused space on + * either size. */ + alloc = (BYTE*)alloc + ZSTD_CWKSP_ASAN_REDZONE_SIZE; + if (ws->isStatic == ZSTD_cwksp_dynamic_alloc) { + __asan_unpoison_memory_region(alloc, bytes); + } +#endif + + return alloc; +} + +MEM_STATIC void ZSTD_cwksp_mark_tables_dirty(ZSTD_cwksp* ws) +{ + DEBUGLOG(4, "cwksp: ZSTD_cwksp_mark_tables_dirty"); + +#if ZSTD_MEMORY_SANITIZER && !defined (ZSTD_MSAN_DONT_POISON_WORKSPACE) + /* To validate that the table reuse logic is sound, and that we don't + * access table space that we haven't cleaned, we re-"poison" the table + * space every time we mark it dirty. + * Since tableValidEnd space and initOnce space may overlap we don't poison + * the initOnce portion as it break its promise. This means that this poisoning + * check isn't always applied fully. */ + { + size_t size = (BYTE*)ws->tableValidEnd - (BYTE*)ws->objectEnd; + assert(__msan_test_shadow(ws->objectEnd, size) == -1); + if((BYTE*)ws->tableValidEnd < (BYTE*)ws->initOnceStart) { + __msan_poison(ws->objectEnd, size); + } else { + assert(ws->initOnceStart >= ws->objectEnd); + __msan_poison(ws->objectEnd, (BYTE*)ws->initOnceStart - (BYTE*)ws->objectEnd); + } + } +#endif + + assert(ws->tableValidEnd >= ws->objectEnd); + assert(ws->tableValidEnd <= ws->allocStart); + ws->tableValidEnd = ws->objectEnd; + ZSTD_cwksp_assert_internal_consistency(ws); +} + +MEM_STATIC void ZSTD_cwksp_mark_tables_clean(ZSTD_cwksp* ws) { + DEBUGLOG(4, "cwksp: ZSTD_cwksp_mark_tables_clean"); + assert(ws->tableValidEnd >= ws->objectEnd); + assert(ws->tableValidEnd <= ws->allocStart); + if (ws->tableValidEnd < ws->tableEnd) { + ws->tableValidEnd = ws->tableEnd; + } + ZSTD_cwksp_assert_internal_consistency(ws); +} + +/** + * Zero the part of the allocated tables not already marked clean. + */ +MEM_STATIC void ZSTD_cwksp_clean_tables(ZSTD_cwksp* ws) { + DEBUGLOG(4, "cwksp: ZSTD_cwksp_clean_tables"); + assert(ws->tableValidEnd >= ws->objectEnd); + assert(ws->tableValidEnd <= ws->allocStart); + if (ws->tableValidEnd < ws->tableEnd) { + ZSTD_memset(ws->tableValidEnd, 0, (size_t)((BYTE*)ws->tableEnd - (BYTE*)ws->tableValidEnd)); + } + ZSTD_cwksp_mark_tables_clean(ws); +} + +/** + * Invalidates table allocations. + * All other allocations remain valid. + */ +MEM_STATIC void ZSTD_cwksp_clear_tables(ZSTD_cwksp* ws) { + DEBUGLOG(4, "cwksp: clearing tables!"); + +#if ZSTD_ADDRESS_SANITIZER && !defined (ZSTD_ASAN_DONT_POISON_WORKSPACE) + /* We don't do this when the workspace is statically allocated, because + * when that is the case, we have no capability to hook into the end of the + * workspace's lifecycle to unpoison the memory. + */ + if (ws->isStatic == ZSTD_cwksp_dynamic_alloc) { + size_t size = (BYTE*)ws->tableValidEnd - (BYTE*)ws->objectEnd; + __asan_poison_memory_region(ws->objectEnd, size); + } +#endif + + ws->tableEnd = ws->objectEnd; + ZSTD_cwksp_assert_internal_consistency(ws); +} + +/** + * Invalidates all buffer, aligned, and table allocations. + * Object allocations remain valid. + */ +MEM_STATIC void ZSTD_cwksp_clear(ZSTD_cwksp* ws) { + DEBUGLOG(4, "cwksp: clearing!"); + +#if ZSTD_MEMORY_SANITIZER && !defined (ZSTD_MSAN_DONT_POISON_WORKSPACE) + /* To validate that the context reuse logic is sound, and that we don't + * access stuff that this compression hasn't initialized, we re-"poison" + * the workspace except for the areas in which we expect memory reuse + * without initialization (objects, valid tables area and init once + * memory). */ + { + if((BYTE*)ws->tableValidEnd < (BYTE*)ws->initOnceStart) { + size_t size = (BYTE*)ws->initOnceStart - (BYTE*)ws->tableValidEnd; + __msan_poison(ws->tableValidEnd, size); + } + } +#endif + +#if ZSTD_ADDRESS_SANITIZER && !defined (ZSTD_ASAN_DONT_POISON_WORKSPACE) + /* We don't do this when the workspace is statically allocated, because + * when that is the case, we have no capability to hook into the end of the + * workspace's lifecycle to unpoison the memory. + */ + if (ws->isStatic == ZSTD_cwksp_dynamic_alloc) { + size_t size = (BYTE*)ws->workspaceEnd - (BYTE*)ws->objectEnd; + __asan_poison_memory_region(ws->objectEnd, size); + } +#endif + + ws->tableEnd = ws->objectEnd; + ws->allocStart = ZSTD_cwksp_initialAllocStart(ws); + ws->allocFailed = 0; + if (ws->phase > ZSTD_cwksp_alloc_aligned_init_once) { + ws->phase = ZSTD_cwksp_alloc_aligned_init_once; + } + ZSTD_cwksp_assert_internal_consistency(ws); +} + +MEM_STATIC size_t ZSTD_cwksp_sizeof(const ZSTD_cwksp* ws) { + return (size_t)((BYTE*)ws->workspaceEnd - (BYTE*)ws->workspace); +} + +MEM_STATIC size_t ZSTD_cwksp_used(const ZSTD_cwksp* ws) { + return (size_t)((BYTE*)ws->tableEnd - (BYTE*)ws->workspace) + + (size_t)((BYTE*)ws->workspaceEnd - (BYTE*)ws->allocStart); +} + +/** + * The provided workspace takes ownership of the buffer [start, start+size). + * Any existing values in the workspace are ignored (the previously managed + * buffer, if present, must be separately freed). + */ +MEM_STATIC void ZSTD_cwksp_init(ZSTD_cwksp* ws, void* start, size_t size, ZSTD_cwksp_static_alloc_e isStatic) { + DEBUGLOG(4, "cwksp: init'ing workspace with %zd bytes", size); + assert(((size_t)start & (sizeof(void*)-1)) == 0); /* ensure correct alignment */ + ws->workspace = start; + ws->workspaceEnd = (BYTE*)start + size; + ws->objectEnd = ws->workspace; + ws->tableValidEnd = ws->objectEnd; + ws->initOnceStart = ZSTD_cwksp_initialAllocStart(ws); + ws->phase = ZSTD_cwksp_alloc_objects; + ws->isStatic = isStatic; + ZSTD_cwksp_clear(ws); + ws->workspaceOversizedDuration = 0; + ZSTD_cwksp_assert_internal_consistency(ws); +} + +MEM_STATIC size_t ZSTD_cwksp_create(ZSTD_cwksp* ws, size_t size, ZSTD_customMem customMem) { + void* workspace = ZSTD_customMalloc(size, customMem); + DEBUGLOG(4, "cwksp: creating new workspace with %zd bytes", size); + RETURN_ERROR_IF(workspace == NULL, memory_allocation, "NULL pointer!"); + ZSTD_cwksp_init(ws, workspace, size, ZSTD_cwksp_dynamic_alloc); + return 0; +} + +MEM_STATIC void ZSTD_cwksp_free(ZSTD_cwksp* ws, ZSTD_customMem customMem) { + void *ptr = ws->workspace; + DEBUGLOG(4, "cwksp: freeing workspace"); +#if ZSTD_MEMORY_SANITIZER && !defined(ZSTD_MSAN_DONT_POISON_WORKSPACE) + if (ptr != NULL && customMem.customFree != NULL) { + __msan_unpoison(ptr, ZSTD_cwksp_sizeof(ws)); + } +#endif + ZSTD_memset(ws, 0, sizeof(ZSTD_cwksp)); + ZSTD_customFree(ptr, customMem); +} + +/** + * Moves the management of a workspace from one cwksp to another. The src cwksp + * is left in an invalid state (src must be re-init()'ed before it's used again). + */ +MEM_STATIC void ZSTD_cwksp_move(ZSTD_cwksp* dst, ZSTD_cwksp* src) { + *dst = *src; + ZSTD_memset(src, 0, sizeof(ZSTD_cwksp)); +} + +MEM_STATIC int ZSTD_cwksp_reserve_failed(const ZSTD_cwksp* ws) { + return ws->allocFailed; +} + +/*-************************************* +* Functions Checking Free Space +***************************************/ + +/* ZSTD_alignmentSpaceWithinBounds() : + * Returns if the estimated space needed for a wksp is within an acceptable limit of the + * actual amount of space used. + */ +MEM_STATIC int ZSTD_cwksp_estimated_space_within_bounds(const ZSTD_cwksp *const ws, size_t const estimatedSpace) { + /* We have an alignment space between objects and tables between tables and buffers, so we can have up to twice + * the alignment bytes difference between estimation and actual usage */ + return (estimatedSpace - ZSTD_cwksp_slack_space_required()) <= ZSTD_cwksp_used(ws) && + ZSTD_cwksp_used(ws) <= estimatedSpace; +} + + +MEM_STATIC size_t ZSTD_cwksp_available_space(ZSTD_cwksp* ws) { + return (size_t)((BYTE*)ws->allocStart - (BYTE*)ws->tableEnd); +} + +MEM_STATIC int ZSTD_cwksp_check_available(ZSTD_cwksp* ws, size_t additionalNeededSpace) { + return ZSTD_cwksp_available_space(ws) >= additionalNeededSpace; +} + +MEM_STATIC int ZSTD_cwksp_check_too_large(ZSTD_cwksp* ws, size_t additionalNeededSpace) { + return ZSTD_cwksp_check_available( + ws, additionalNeededSpace * ZSTD_WORKSPACETOOLARGE_FACTOR); +} + +MEM_STATIC int ZSTD_cwksp_check_wasteful(ZSTD_cwksp* ws, size_t additionalNeededSpace) { + return ZSTD_cwksp_check_too_large(ws, additionalNeededSpace) + && ws->workspaceOversizedDuration > ZSTD_WORKSPACETOOLARGE_MAXDURATION; +} + +MEM_STATIC void ZSTD_cwksp_bump_oversized_duration( + ZSTD_cwksp* ws, size_t additionalNeededSpace) { + if (ZSTD_cwksp_check_too_large(ws, additionalNeededSpace)) { + ws->workspaceOversizedDuration++; + } else { + ws->workspaceOversizedDuration = 0; + } +} + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_CWKSP_H */ +/**** ended inlining zstd_cwksp.h ****/ +#ifdef ZSTD_MULTITHREAD +/**** start inlining zstdmt_compress.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + #ifndef ZSTDMT_COMPRESS_H + #define ZSTDMT_COMPRESS_H + + #if defined (__cplusplus) + extern "C" { + #endif + + +/* Note : This is an internal API. + * These APIs used to be exposed with ZSTDLIB_API, + * because it used to be the only way to invoke MT compression. + * Now, you must use ZSTD_compress2 and ZSTD_compressStream2() instead. + * + * This API requires ZSTD_MULTITHREAD to be defined during compilation, + * otherwise ZSTDMT_createCCtx*() will fail. + */ + +/* === Dependencies === */ +/**** skipping file: ../common/zstd_deps.h ****/ +#define ZSTD_STATIC_LINKING_ONLY /* ZSTD_parameters */ +/**** skipping file: ../zstd.h ****/ + + +/* === Constants === */ +#ifndef ZSTDMT_NBWORKERS_MAX /* a different value can be selected at compile time */ +# define ZSTDMT_NBWORKERS_MAX ((sizeof(void*)==4) /*32-bit*/ ? 64 : 256) +#endif +#ifndef ZSTDMT_JOBSIZE_MIN /* a different value can be selected at compile time */ +# define ZSTDMT_JOBSIZE_MIN (512 KB) +#endif +#define ZSTDMT_JOBLOG_MAX (MEM_32bits() ? 29 : 30) +#define ZSTDMT_JOBSIZE_MAX (MEM_32bits() ? (512 MB) : (1024 MB)) + + +/* ======================================================== + * === Private interface, for use by ZSTD_compress.c === + * === Not exposed in libzstd. Never invoke directly === + * ======================================================== */ + +/* === Memory management === */ +typedef struct ZSTDMT_CCtx_s ZSTDMT_CCtx; +/* Requires ZSTD_MULTITHREAD to be defined during compilation, otherwise it will return NULL. */ +ZSTDMT_CCtx* ZSTDMT_createCCtx_advanced(unsigned nbWorkers, + ZSTD_customMem cMem, + ZSTD_threadPool *pool); +size_t ZSTDMT_freeCCtx(ZSTDMT_CCtx* mtctx); + +size_t ZSTDMT_sizeof_CCtx(ZSTDMT_CCtx* mtctx); + +/* === Streaming functions === */ + +size_t ZSTDMT_nextInputSizeHint(const ZSTDMT_CCtx* mtctx); + +/*! ZSTDMT_initCStream_internal() : + * Private use only. Init streaming operation. + * expects params to be valid. + * must receive dict, or cdict, or none, but not both. + * mtctx can be freshly constructed or reused from a prior compression. + * If mtctx is reused, memory allocations from the prior compression may not be freed, + * even if they are not needed for the current compression. + * @return : 0, or an error code */ +size_t ZSTDMT_initCStream_internal(ZSTDMT_CCtx* mtctx, + const void* dict, size_t dictSize, ZSTD_dictContentType_e dictContentType, + const ZSTD_CDict* cdict, + ZSTD_CCtx_params params, unsigned long long pledgedSrcSize); + +/*! ZSTDMT_compressStream_generic() : + * Combines ZSTDMT_compressStream() with optional ZSTDMT_flushStream() or ZSTDMT_endStream() + * depending on flush directive. + * @return : minimum amount of data still to be flushed + * 0 if fully flushed + * or an error code + * note : needs to be init using any ZSTD_initCStream*() variant */ +size_t ZSTDMT_compressStream_generic(ZSTDMT_CCtx* mtctx, + ZSTD_outBuffer* output, + ZSTD_inBuffer* input, + ZSTD_EndDirective endOp); + + /*! ZSTDMT_toFlushNow() + * Tell how many bytes are ready to be flushed immediately. + * Probe the oldest active job (not yet entirely flushed) and check its output buffer. + * If return 0, it means there is no active job, + * or, it means oldest job is still active, but everything produced has been flushed so far, + * therefore flushing is limited by speed of oldest job. */ +size_t ZSTDMT_toFlushNow(ZSTDMT_CCtx* mtctx); + +/*! ZSTDMT_updateCParams_whileCompressing() : + * Updates only a selected set of compression parameters, to remain compatible with current frame. + * New parameters will be applied to next compression job. */ +void ZSTDMT_updateCParams_whileCompressing(ZSTDMT_CCtx* mtctx, const ZSTD_CCtx_params* cctxParams); + +/*! ZSTDMT_getFrameProgression(): + * tells how much data has been consumed (input) and produced (output) for current frame. + * able to count progression inside worker threads. + */ +ZSTD_frameProgression ZSTDMT_getFrameProgression(ZSTDMT_CCtx* mtctx); + + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTDMT_COMPRESS_H */ +/**** ended inlining zstdmt_compress.h ****/ +#endif +/**** skipping file: ../common/bits.h ****/ + +#if defined (__cplusplus) +extern "C" { +#endif + +/*-************************************* +* Constants +***************************************/ +#define kSearchStrength 8 +#define HASH_READ_SIZE 8 +#define ZSTD_DUBT_UNSORTED_MARK 1 /* For btlazy2 strategy, index ZSTD_DUBT_UNSORTED_MARK==1 means "unsorted". + It could be confused for a real successor at index "1", if sorted as larger than its predecessor. + It's not a big deal though : candidate will just be sorted again. + Additionally, candidate position 1 will be lost. + But candidate 1 cannot hide a large tree of candidates, so it's a minimal loss. + The benefit is that ZSTD_DUBT_UNSORTED_MARK cannot be mishandled after table reuse with a different strategy. + This constant is required by ZSTD_compressBlock_btlazy2() and ZSTD_reduceTable_internal() */ + + +/*-************************************* +* Context memory management +***************************************/ +typedef enum { ZSTDcs_created=0, ZSTDcs_init, ZSTDcs_ongoing, ZSTDcs_ending } ZSTD_compressionStage_e; +typedef enum { zcss_init=0, zcss_load, zcss_flush } ZSTD_cStreamStage; + +typedef struct ZSTD_prefixDict_s { + const void* dict; + size_t dictSize; + ZSTD_dictContentType_e dictContentType; +} ZSTD_prefixDict; + +typedef struct { + void* dictBuffer; + void const* dict; + size_t dictSize; + ZSTD_dictContentType_e dictContentType; + ZSTD_CDict* cdict; +} ZSTD_localDict; + +typedef struct { + HUF_CElt CTable[HUF_CTABLE_SIZE_ST(255)]; + HUF_repeat repeatMode; +} ZSTD_hufCTables_t; + +typedef struct { + FSE_CTable offcodeCTable[FSE_CTABLE_SIZE_U32(OffFSELog, MaxOff)]; + FSE_CTable matchlengthCTable[FSE_CTABLE_SIZE_U32(MLFSELog, MaxML)]; + FSE_CTable litlengthCTable[FSE_CTABLE_SIZE_U32(LLFSELog, MaxLL)]; + FSE_repeat offcode_repeatMode; + FSE_repeat matchlength_repeatMode; + FSE_repeat litlength_repeatMode; +} ZSTD_fseCTables_t; + +typedef struct { + ZSTD_hufCTables_t huf; + ZSTD_fseCTables_t fse; +} ZSTD_entropyCTables_t; + +/*********************************************** +* Entropy buffer statistics structs and funcs * +***********************************************/ +/** ZSTD_hufCTablesMetadata_t : + * Stores Literals Block Type for a super-block in hType, and + * huffman tree description in hufDesBuffer. + * hufDesSize refers to the size of huffman tree description in bytes. + * This metadata is populated in ZSTD_buildBlockEntropyStats_literals() */ +typedef struct { + symbolEncodingType_e hType; + BYTE hufDesBuffer[ZSTD_MAX_HUF_HEADER_SIZE]; + size_t hufDesSize; +} ZSTD_hufCTablesMetadata_t; + +/** ZSTD_fseCTablesMetadata_t : + * Stores symbol compression modes for a super-block in {ll, ol, ml}Type, and + * fse tables in fseTablesBuffer. + * fseTablesSize refers to the size of fse tables in bytes. + * This metadata is populated in ZSTD_buildBlockEntropyStats_sequences() */ +typedef struct { + symbolEncodingType_e llType; + symbolEncodingType_e ofType; + symbolEncodingType_e mlType; + BYTE fseTablesBuffer[ZSTD_MAX_FSE_HEADERS_SIZE]; + size_t fseTablesSize; + size_t lastCountSize; /* This is to account for bug in 1.3.4. More detail in ZSTD_entropyCompressSeqStore_internal() */ +} ZSTD_fseCTablesMetadata_t; + +typedef struct { + ZSTD_hufCTablesMetadata_t hufMetadata; + ZSTD_fseCTablesMetadata_t fseMetadata; +} ZSTD_entropyCTablesMetadata_t; + +/** ZSTD_buildBlockEntropyStats() : + * Builds entropy for the block. + * @return : 0 on success or error code */ +size_t ZSTD_buildBlockEntropyStats( + const seqStore_t* seqStorePtr, + const ZSTD_entropyCTables_t* prevEntropy, + ZSTD_entropyCTables_t* nextEntropy, + const ZSTD_CCtx_params* cctxParams, + ZSTD_entropyCTablesMetadata_t* entropyMetadata, + void* workspace, size_t wkspSize); + +/********************************* +* Compression internals structs * +*********************************/ + +typedef struct { + U32 off; /* Offset sumtype code for the match, using ZSTD_storeSeq() format */ + U32 len; /* Raw length of match */ +} ZSTD_match_t; + +typedef struct { + U32 offset; /* Offset of sequence */ + U32 litLength; /* Length of literals prior to match */ + U32 matchLength; /* Raw length of match */ +} rawSeq; + +typedef struct { + rawSeq* seq; /* The start of the sequences */ + size_t pos; /* The index in seq where reading stopped. pos <= size. */ + size_t posInSequence; /* The position within the sequence at seq[pos] where reading + stopped. posInSequence <= seq[pos].litLength + seq[pos].matchLength */ + size_t size; /* The number of sequences. <= capacity. */ + size_t capacity; /* The capacity starting from `seq` pointer */ +} rawSeqStore_t; + +typedef struct { + U32 idx; /* Index in array of ZSTD_Sequence */ + U32 posInSequence; /* Position within sequence at idx */ + size_t posInSrc; /* Number of bytes given by sequences provided so far */ +} ZSTD_sequencePosition; + +UNUSED_ATTR static const rawSeqStore_t kNullRawSeqStore = {NULL, 0, 0, 0, 0}; + +typedef struct { + int price; /* price from beginning of segment to this position */ + U32 off; /* offset of previous match */ + U32 mlen; /* length of previous match */ + U32 litlen; /* nb of literals since previous match */ + U32 rep[ZSTD_REP_NUM]; /* offset history after previous match */ +} ZSTD_optimal_t; + +typedef enum { zop_dynamic=0, zop_predef } ZSTD_OptPrice_e; + +#define ZSTD_OPT_SIZE (ZSTD_OPT_NUM+3) +typedef struct { + /* All tables are allocated inside cctx->workspace by ZSTD_resetCCtx_internal() */ + unsigned* litFreq; /* table of literals statistics, of size 256 */ + unsigned* litLengthFreq; /* table of litLength statistics, of size (MaxLL+1) */ + unsigned* matchLengthFreq; /* table of matchLength statistics, of size (MaxML+1) */ + unsigned* offCodeFreq; /* table of offCode statistics, of size (MaxOff+1) */ + ZSTD_match_t* matchTable; /* list of found matches, of size ZSTD_OPT_SIZE */ + ZSTD_optimal_t* priceTable; /* All positions tracked by optimal parser, of size ZSTD_OPT_SIZE */ + + U32 litSum; /* nb of literals */ + U32 litLengthSum; /* nb of litLength codes */ + U32 matchLengthSum; /* nb of matchLength codes */ + U32 offCodeSum; /* nb of offset codes */ + U32 litSumBasePrice; /* to compare to log2(litfreq) */ + U32 litLengthSumBasePrice; /* to compare to log2(llfreq) */ + U32 matchLengthSumBasePrice;/* to compare to log2(mlfreq) */ + U32 offCodeSumBasePrice; /* to compare to log2(offreq) */ + ZSTD_OptPrice_e priceType; /* prices can be determined dynamically, or follow a pre-defined cost structure */ + const ZSTD_entropyCTables_t* symbolCosts; /* pre-calculated dictionary statistics */ + ZSTD_paramSwitch_e literalCompressionMode; +} optState_t; + +typedef struct { + ZSTD_entropyCTables_t entropy; + U32 rep[ZSTD_REP_NUM]; +} ZSTD_compressedBlockState_t; + +typedef struct { + BYTE const* nextSrc; /* next block here to continue on current prefix */ + BYTE const* base; /* All regular indexes relative to this position */ + BYTE const* dictBase; /* extDict indexes relative to this position */ + U32 dictLimit; /* below that point, need extDict */ + U32 lowLimit; /* below that point, no more valid data */ + U32 nbOverflowCorrections; /* Number of times overflow correction has run since + * ZSTD_window_init(). Useful for debugging coredumps + * and for ZSTD_WINDOW_OVERFLOW_CORRECT_FREQUENTLY. + */ +} ZSTD_window_t; + +#define ZSTD_WINDOW_START_INDEX 2 + +typedef struct ZSTD_matchState_t ZSTD_matchState_t; + +#define ZSTD_ROW_HASH_CACHE_SIZE 8 /* Size of prefetching hash cache for row-based matchfinder */ + +struct ZSTD_matchState_t { + ZSTD_window_t window; /* State for window round buffer management */ + U32 loadedDictEnd; /* index of end of dictionary, within context's referential. + * When loadedDictEnd != 0, a dictionary is in use, and still valid. + * This relies on a mechanism to set loadedDictEnd=0 when dictionary is no longer within distance. + * Such mechanism is provided within ZSTD_window_enforceMaxDist() and ZSTD_checkDictValidity(). + * When dict referential is copied into active context (i.e. not attached), + * loadedDictEnd == dictSize, since referential starts from zero. + */ + U32 nextToUpdate; /* index from which to continue table update */ + U32 hashLog3; /* dispatch table for matches of len==3 : larger == faster, more memory */ + + U32 rowHashLog; /* For row-based matchfinder: Hashlog based on nb of rows in the hashTable.*/ + BYTE* tagTable; /* For row-based matchFinder: A row-based table containing the hashes and head index. */ + U32 hashCache[ZSTD_ROW_HASH_CACHE_SIZE]; /* For row-based matchFinder: a cache of hashes to improve speed */ + U64 hashSalt; /* For row-based matchFinder: salts the hash for reuse of tag table */ + U32 hashSaltEntropy; /* For row-based matchFinder: collects entropy for salt generation */ + + U32* hashTable; + U32* hashTable3; + U32* chainTable; + + U32 forceNonContiguous; /* Non-zero if we should force non-contiguous load for the next window update. */ + + int dedicatedDictSearch; /* Indicates whether this matchState is using the + * dedicated dictionary search structure. + */ + optState_t opt; /* optimal parser state */ + const ZSTD_matchState_t* dictMatchState; + ZSTD_compressionParameters cParams; + const rawSeqStore_t* ldmSeqStore; + + /* Controls prefetching in some dictMatchState matchfinders. + * This behavior is controlled from the cctx ms. + * This parameter has no effect in the cdict ms. */ + int prefetchCDictTables; + + /* When == 0, lazy match finders insert every position. + * When != 0, lazy match finders only insert positions they search. + * This allows them to skip much faster over incompressible data, + * at a small cost to compression ratio. + */ + int lazySkipping; +}; + +typedef struct { + ZSTD_compressedBlockState_t* prevCBlock; + ZSTD_compressedBlockState_t* nextCBlock; + ZSTD_matchState_t matchState; +} ZSTD_blockState_t; + +typedef struct { + U32 offset; + U32 checksum; +} ldmEntry_t; + +typedef struct { + BYTE const* split; + U32 hash; + U32 checksum; + ldmEntry_t* bucket; +} ldmMatchCandidate_t; + +#define LDM_BATCH_SIZE 64 + +typedef struct { + ZSTD_window_t window; /* State for the window round buffer management */ + ldmEntry_t* hashTable; + U32 loadedDictEnd; + BYTE* bucketOffsets; /* Next position in bucket to insert entry */ + size_t splitIndices[LDM_BATCH_SIZE]; + ldmMatchCandidate_t matchCandidates[LDM_BATCH_SIZE]; +} ldmState_t; + +typedef struct { + ZSTD_paramSwitch_e enableLdm; /* ZSTD_ps_enable to enable LDM. ZSTD_ps_auto by default */ + U32 hashLog; /* Log size of hashTable */ + U32 bucketSizeLog; /* Log bucket size for collision resolution, at most 8 */ + U32 minMatchLength; /* Minimum match length */ + U32 hashRateLog; /* Log number of entries to skip */ + U32 windowLog; /* Window log for the LDM */ +} ldmParams_t; + +typedef struct { + int collectSequences; + ZSTD_Sequence* seqStart; + size_t seqIndex; + size_t maxSequences; +} SeqCollector; + +struct ZSTD_CCtx_params_s { + ZSTD_format_e format; + ZSTD_compressionParameters cParams; + ZSTD_frameParameters fParams; + + int compressionLevel; + int forceWindow; /* force back-references to respect limit of + * 1< 63) ? ZSTD_highbit32(litLength) + LL_deltaCode : LL_Code[litLength]; +} + +/* ZSTD_MLcode() : + * note : mlBase = matchLength - MINMATCH; + * because it's the format it's stored in seqStore->sequences */ +MEM_STATIC U32 ZSTD_MLcode(U32 mlBase) +{ + static const BYTE ML_Code[128] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + 32, 32, 33, 33, 34, 34, 35, 35, 36, 36, 36, 36, 37, 37, 37, 37, + 38, 38, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 39, 39, + 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40, + 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, 41, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42 }; + static const U32 ML_deltaCode = 36; + return (mlBase > 127) ? ZSTD_highbit32(mlBase) + ML_deltaCode : ML_Code[mlBase]; +} + +/* ZSTD_cParam_withinBounds: + * @return 1 if value is within cParam bounds, + * 0 otherwise */ +MEM_STATIC int ZSTD_cParam_withinBounds(ZSTD_cParameter cParam, int value) +{ + ZSTD_bounds const bounds = ZSTD_cParam_getBounds(cParam); + if (ZSTD_isError(bounds.error)) return 0; + if (value < bounds.lowerBound) return 0; + if (value > bounds.upperBound) return 0; + return 1; +} + +/* ZSTD_noCompressBlock() : + * Writes uncompressed block to dst buffer from given src. + * Returns the size of the block */ +MEM_STATIC size_t +ZSTD_noCompressBlock(void* dst, size_t dstCapacity, const void* src, size_t srcSize, U32 lastBlock) +{ + U32 const cBlockHeader24 = lastBlock + (((U32)bt_raw)<<1) + (U32)(srcSize << 3); + DEBUGLOG(5, "ZSTD_noCompressBlock (srcSize=%zu, dstCapacity=%zu)", srcSize, dstCapacity); + RETURN_ERROR_IF(srcSize + ZSTD_blockHeaderSize > dstCapacity, + dstSize_tooSmall, "dst buf too small for uncompressed block"); + MEM_writeLE24(dst, cBlockHeader24); + ZSTD_memcpy((BYTE*)dst + ZSTD_blockHeaderSize, src, srcSize); + return ZSTD_blockHeaderSize + srcSize; +} + +MEM_STATIC size_t +ZSTD_rleCompressBlock(void* dst, size_t dstCapacity, BYTE src, size_t srcSize, U32 lastBlock) +{ + BYTE* const op = (BYTE*)dst; + U32 const cBlockHeader = lastBlock + (((U32)bt_rle)<<1) + (U32)(srcSize << 3); + RETURN_ERROR_IF(dstCapacity < 4, dstSize_tooSmall, ""); + MEM_writeLE24(op, cBlockHeader); + op[3] = src; + return 4; +} + + +/* ZSTD_minGain() : + * minimum compression required + * to generate a compress block or a compressed literals section. + * note : use same formula for both situations */ +MEM_STATIC size_t ZSTD_minGain(size_t srcSize, ZSTD_strategy strat) +{ + U32 const minlog = (strat>=ZSTD_btultra) ? (U32)(strat) - 1 : 6; + ZSTD_STATIC_ASSERT(ZSTD_btultra == 8); + assert(ZSTD_cParam_withinBounds(ZSTD_c_strategy, (int)strat)); + return (srcSize >> minlog) + 2; +} + +MEM_STATIC int ZSTD_literalsCompressionIsDisabled(const ZSTD_CCtx_params* cctxParams) +{ + switch (cctxParams->literalCompressionMode) { + case ZSTD_ps_enable: + return 0; + case ZSTD_ps_disable: + return 1; + default: + assert(0 /* impossible: pre-validated */); + ZSTD_FALLTHROUGH; + case ZSTD_ps_auto: + return (cctxParams->cParams.strategy == ZSTD_fast) && (cctxParams->cParams.targetLength > 0); + } +} + +/*! ZSTD_safecopyLiterals() : + * memcpy() function that won't read beyond more than WILDCOPY_OVERLENGTH bytes past ilimit_w. + * Only called when the sequence ends past ilimit_w, so it only needs to be optimized for single + * large copies. + */ +static void +ZSTD_safecopyLiterals(BYTE* op, BYTE const* ip, BYTE const* const iend, BYTE const* ilimit_w) +{ + assert(iend > ilimit_w); + if (ip <= ilimit_w) { + ZSTD_wildcopy(op, ip, ilimit_w - ip, ZSTD_no_overlap); + op += ilimit_w - ip; + ip = ilimit_w; + } + while (ip < iend) *op++ = *ip++; +} + + +#define REPCODE1_TO_OFFBASE REPCODE_TO_OFFBASE(1) +#define REPCODE2_TO_OFFBASE REPCODE_TO_OFFBASE(2) +#define REPCODE3_TO_OFFBASE REPCODE_TO_OFFBASE(3) +#define REPCODE_TO_OFFBASE(r) (assert((r)>=1), assert((r)<=ZSTD_REP_NUM), (r)) /* accepts IDs 1,2,3 */ +#define OFFSET_TO_OFFBASE(o) (assert((o)>0), o + ZSTD_REP_NUM) +#define OFFBASE_IS_OFFSET(o) ((o) > ZSTD_REP_NUM) +#define OFFBASE_IS_REPCODE(o) ( 1 <= (o) && (o) <= ZSTD_REP_NUM) +#define OFFBASE_TO_OFFSET(o) (assert(OFFBASE_IS_OFFSET(o)), (o) - ZSTD_REP_NUM) +#define OFFBASE_TO_REPCODE(o) (assert(OFFBASE_IS_REPCODE(o)), (o)) /* returns ID 1,2,3 */ + +/*! ZSTD_storeSeq() : + * Store a sequence (litlen, litPtr, offBase and matchLength) into seqStore_t. + * @offBase : Users should employ macros REPCODE_TO_OFFBASE() and OFFSET_TO_OFFBASE(). + * @matchLength : must be >= MINMATCH + * Allowed to over-read literals up to litLimit. +*/ +HINT_INLINE UNUSED_ATTR void +ZSTD_storeSeq(seqStore_t* seqStorePtr, + size_t litLength, const BYTE* literals, const BYTE* litLimit, + U32 offBase, + size_t matchLength) +{ + BYTE const* const litLimit_w = litLimit - WILDCOPY_OVERLENGTH; + BYTE const* const litEnd = literals + litLength; +#if defined(DEBUGLEVEL) && (DEBUGLEVEL >= 6) + static const BYTE* g_start = NULL; + if (g_start==NULL) g_start = (const BYTE*)literals; /* note : index only works for compression within a single segment */ + { U32 const pos = (U32)((const BYTE*)literals - g_start); + DEBUGLOG(6, "Cpos%7u :%3u literals, match%4u bytes at offBase%7u", + pos, (U32)litLength, (U32)matchLength, (U32)offBase); + } +#endif + assert((size_t)(seqStorePtr->sequences - seqStorePtr->sequencesStart) < seqStorePtr->maxNbSeq); + /* copy Literals */ + assert(seqStorePtr->maxNbLit <= 128 KB); + assert(seqStorePtr->lit + litLength <= seqStorePtr->litStart + seqStorePtr->maxNbLit); + assert(literals + litLength <= litLimit); + if (litEnd <= litLimit_w) { + /* Common case we can use wildcopy. + * First copy 16 bytes, because literals are likely short. + */ + ZSTD_STATIC_ASSERT(WILDCOPY_OVERLENGTH >= 16); + ZSTD_copy16(seqStorePtr->lit, literals); + if (litLength > 16) { + ZSTD_wildcopy(seqStorePtr->lit+16, literals+16, (ptrdiff_t)litLength-16, ZSTD_no_overlap); + } + } else { + ZSTD_safecopyLiterals(seqStorePtr->lit, literals, litEnd, litLimit_w); + } + seqStorePtr->lit += litLength; + + /* literal Length */ + if (litLength>0xFFFF) { + assert(seqStorePtr->longLengthType == ZSTD_llt_none); /* there can only be a single long length */ + seqStorePtr->longLengthType = ZSTD_llt_literalLength; + seqStorePtr->longLengthPos = (U32)(seqStorePtr->sequences - seqStorePtr->sequencesStart); + } + seqStorePtr->sequences[0].litLength = (U16)litLength; + + /* match offset */ + seqStorePtr->sequences[0].offBase = offBase; + + /* match Length */ + assert(matchLength >= MINMATCH); + { size_t const mlBase = matchLength - MINMATCH; + if (mlBase>0xFFFF) { + assert(seqStorePtr->longLengthType == ZSTD_llt_none); /* there can only be a single long length */ + seqStorePtr->longLengthType = ZSTD_llt_matchLength; + seqStorePtr->longLengthPos = (U32)(seqStorePtr->sequences - seqStorePtr->sequencesStart); + } + seqStorePtr->sequences[0].mlBase = (U16)mlBase; + } + + seqStorePtr->sequences++; +} + +/* ZSTD_updateRep() : + * updates in-place @rep (array of repeat offsets) + * @offBase : sum-type, using numeric representation of ZSTD_storeSeq() + */ +MEM_STATIC void +ZSTD_updateRep(U32 rep[ZSTD_REP_NUM], U32 const offBase, U32 const ll0) +{ + if (OFFBASE_IS_OFFSET(offBase)) { /* full offset */ + rep[2] = rep[1]; + rep[1] = rep[0]; + rep[0] = OFFBASE_TO_OFFSET(offBase); + } else { /* repcode */ + U32 const repCode = OFFBASE_TO_REPCODE(offBase) - 1 + ll0; + if (repCode > 0) { /* note : if repCode==0, no change */ + U32 const currentOffset = (repCode==ZSTD_REP_NUM) ? (rep[0] - 1) : rep[repCode]; + rep[2] = (repCode >= 2) ? rep[1] : rep[2]; + rep[1] = rep[0]; + rep[0] = currentOffset; + } else { /* repCode == 0 */ + /* nothing to do */ + } + } +} + +typedef struct repcodes_s { + U32 rep[3]; +} repcodes_t; + +MEM_STATIC repcodes_t +ZSTD_newRep(U32 const rep[ZSTD_REP_NUM], U32 const offBase, U32 const ll0) +{ + repcodes_t newReps; + ZSTD_memcpy(&newReps, rep, sizeof(newReps)); + ZSTD_updateRep(newReps.rep, offBase, ll0); + return newReps; +} + + +/*-************************************* +* Match length counter +***************************************/ +MEM_STATIC size_t ZSTD_count(const BYTE* pIn, const BYTE* pMatch, const BYTE* const pInLimit) +{ + const BYTE* const pStart = pIn; + const BYTE* const pInLoopLimit = pInLimit - (sizeof(size_t)-1); + + if (pIn < pInLoopLimit) { + { size_t const diff = MEM_readST(pMatch) ^ MEM_readST(pIn); + if (diff) return ZSTD_NbCommonBytes(diff); } + pIn+=sizeof(size_t); pMatch+=sizeof(size_t); + while (pIn < pInLoopLimit) { + size_t const diff = MEM_readST(pMatch) ^ MEM_readST(pIn); + if (!diff) { pIn+=sizeof(size_t); pMatch+=sizeof(size_t); continue; } + pIn += ZSTD_NbCommonBytes(diff); + return (size_t)(pIn - pStart); + } } + if (MEM_64bits() && (pIn<(pInLimit-3)) && (MEM_read32(pMatch) == MEM_read32(pIn))) { pIn+=4; pMatch+=4; } + if ((pIn<(pInLimit-1)) && (MEM_read16(pMatch) == MEM_read16(pIn))) { pIn+=2; pMatch+=2; } + if ((pIn> (32-h) ; } +MEM_STATIC size_t ZSTD_hash3Ptr(const void* ptr, U32 h) { return ZSTD_hash3(MEM_readLE32(ptr), h, 0); } /* only in zstd_opt.h */ +MEM_STATIC size_t ZSTD_hash3PtrS(const void* ptr, U32 h, U32 s) { return ZSTD_hash3(MEM_readLE32(ptr), h, s); } + +static const U32 prime4bytes = 2654435761U; +static U32 ZSTD_hash4(U32 u, U32 h, U32 s) { assert(h <= 32); return ((u * prime4bytes) ^ s) >> (32-h) ; } +static size_t ZSTD_hash4Ptr(const void* ptr, U32 h) { return ZSTD_hash4(MEM_readLE32(ptr), h, 0); } +static size_t ZSTD_hash4PtrS(const void* ptr, U32 h, U32 s) { return ZSTD_hash4(MEM_readLE32(ptr), h, s); } + +static const U64 prime5bytes = 889523592379ULL; +static size_t ZSTD_hash5(U64 u, U32 h, U64 s) { assert(h <= 64); return (size_t)((((u << (64-40)) * prime5bytes) ^ s) >> (64-h)) ; } +static size_t ZSTD_hash5Ptr(const void* p, U32 h) { return ZSTD_hash5(MEM_readLE64(p), h, 0); } +static size_t ZSTD_hash5PtrS(const void* p, U32 h, U64 s) { return ZSTD_hash5(MEM_readLE64(p), h, s); } + +static const U64 prime6bytes = 227718039650203ULL; +static size_t ZSTD_hash6(U64 u, U32 h, U64 s) { assert(h <= 64); return (size_t)((((u << (64-48)) * prime6bytes) ^ s) >> (64-h)) ; } +static size_t ZSTD_hash6Ptr(const void* p, U32 h) { return ZSTD_hash6(MEM_readLE64(p), h, 0); } +static size_t ZSTD_hash6PtrS(const void* p, U32 h, U64 s) { return ZSTD_hash6(MEM_readLE64(p), h, s); } + +static const U64 prime7bytes = 58295818150454627ULL; +static size_t ZSTD_hash7(U64 u, U32 h, U64 s) { assert(h <= 64); return (size_t)((((u << (64-56)) * prime7bytes) ^ s) >> (64-h)) ; } +static size_t ZSTD_hash7Ptr(const void* p, U32 h) { return ZSTD_hash7(MEM_readLE64(p), h, 0); } +static size_t ZSTD_hash7PtrS(const void* p, U32 h, U64 s) { return ZSTD_hash7(MEM_readLE64(p), h, s); } + +static const U64 prime8bytes = 0xCF1BBCDCB7A56463ULL; +static size_t ZSTD_hash8(U64 u, U32 h, U64 s) { assert(h <= 64); return (size_t)((((u) * prime8bytes) ^ s) >> (64-h)) ; } +static size_t ZSTD_hash8Ptr(const void* p, U32 h) { return ZSTD_hash8(MEM_readLE64(p), h, 0); } +static size_t ZSTD_hash8PtrS(const void* p, U32 h, U64 s) { return ZSTD_hash8(MEM_readLE64(p), h, s); } + + +MEM_STATIC FORCE_INLINE_ATTR +size_t ZSTD_hashPtr(const void* p, U32 hBits, U32 mls) +{ + /* Although some of these hashes do support hBits up to 64, some do not. + * To be on the safe side, always avoid hBits > 32. */ + assert(hBits <= 32); + + switch(mls) + { + default: + case 4: return ZSTD_hash4Ptr(p, hBits); + case 5: return ZSTD_hash5Ptr(p, hBits); + case 6: return ZSTD_hash6Ptr(p, hBits); + case 7: return ZSTD_hash7Ptr(p, hBits); + case 8: return ZSTD_hash8Ptr(p, hBits); + } +} + +MEM_STATIC FORCE_INLINE_ATTR +size_t ZSTD_hashPtrSalted(const void* p, U32 hBits, U32 mls, const U64 hashSalt) { + /* Although some of these hashes do support hBits up to 64, some do not. + * To be on the safe side, always avoid hBits > 32. */ + assert(hBits <= 32); + + switch(mls) + { + default: + case 4: return ZSTD_hash4PtrS(p, hBits, (U32)hashSalt); + case 5: return ZSTD_hash5PtrS(p, hBits, hashSalt); + case 6: return ZSTD_hash6PtrS(p, hBits, hashSalt); + case 7: return ZSTD_hash7PtrS(p, hBits, hashSalt); + case 8: return ZSTD_hash8PtrS(p, hBits, hashSalt); + } +} + + +/** ZSTD_ipow() : + * Return base^exponent. + */ +static U64 ZSTD_ipow(U64 base, U64 exponent) +{ + U64 power = 1; + while (exponent) { + if (exponent & 1) power *= base; + exponent >>= 1; + base *= base; + } + return power; +} + +#define ZSTD_ROLL_HASH_CHAR_OFFSET 10 + +/** ZSTD_rollingHash_append() : + * Add the buffer to the hash value. + */ +static U64 ZSTD_rollingHash_append(U64 hash, void const* buf, size_t size) +{ + BYTE const* istart = (BYTE const*)buf; + size_t pos; + for (pos = 0; pos < size; ++pos) { + hash *= prime8bytes; + hash += istart[pos] + ZSTD_ROLL_HASH_CHAR_OFFSET; + } + return hash; +} + +/** ZSTD_rollingHash_compute() : + * Compute the rolling hash value of the buffer. + */ +MEM_STATIC U64 ZSTD_rollingHash_compute(void const* buf, size_t size) +{ + return ZSTD_rollingHash_append(0, buf, size); +} + +/** ZSTD_rollingHash_primePower() : + * Compute the primePower to be passed to ZSTD_rollingHash_rotate() for a hash + * over a window of length bytes. + */ +MEM_STATIC U64 ZSTD_rollingHash_primePower(U32 length) +{ + return ZSTD_ipow(prime8bytes, length - 1); +} + +/** ZSTD_rollingHash_rotate() : + * Rotate the rolling hash by one byte. + */ +MEM_STATIC U64 ZSTD_rollingHash_rotate(U64 hash, BYTE toRemove, BYTE toAdd, U64 primePower) +{ + hash -= (toRemove + ZSTD_ROLL_HASH_CHAR_OFFSET) * primePower; + hash *= prime8bytes; + hash += toAdd + ZSTD_ROLL_HASH_CHAR_OFFSET; + return hash; +} + +/*-************************************* +* Round buffer management +***************************************/ +#if (ZSTD_WINDOWLOG_MAX_64 > 31) +# error "ZSTD_WINDOWLOG_MAX is too large : would overflow ZSTD_CURRENT_MAX" +#endif +/* Max current allowed */ +#define ZSTD_CURRENT_MAX ((3U << 29) + (1U << ZSTD_WINDOWLOG_MAX)) +/* Maximum chunk size before overflow correction needs to be called again */ +#define ZSTD_CHUNKSIZE_MAX \ + ( ((U32)-1) /* Maximum ending current index */ \ + - ZSTD_CURRENT_MAX) /* Maximum beginning lowLimit */ + +/** + * ZSTD_window_clear(): + * Clears the window containing the history by simply setting it to empty. + */ +MEM_STATIC void ZSTD_window_clear(ZSTD_window_t* window) +{ + size_t const endT = (size_t)(window->nextSrc - window->base); + U32 const end = (U32)endT; + + window->lowLimit = end; + window->dictLimit = end; +} + +MEM_STATIC U32 ZSTD_window_isEmpty(ZSTD_window_t const window) +{ + return window.dictLimit == ZSTD_WINDOW_START_INDEX && + window.lowLimit == ZSTD_WINDOW_START_INDEX && + (window.nextSrc - window.base) == ZSTD_WINDOW_START_INDEX; +} + +/** + * ZSTD_window_hasExtDict(): + * Returns non-zero if the window has a non-empty extDict. + */ +MEM_STATIC U32 ZSTD_window_hasExtDict(ZSTD_window_t const window) +{ + return window.lowLimit < window.dictLimit; +} + +/** + * ZSTD_matchState_dictMode(): + * Inspects the provided matchState and figures out what dictMode should be + * passed to the compressor. + */ +MEM_STATIC ZSTD_dictMode_e ZSTD_matchState_dictMode(const ZSTD_matchState_t *ms) +{ + return ZSTD_window_hasExtDict(ms->window) ? + ZSTD_extDict : + ms->dictMatchState != NULL ? + (ms->dictMatchState->dedicatedDictSearch ? ZSTD_dedicatedDictSearch : ZSTD_dictMatchState) : + ZSTD_noDict; +} + +/* Defining this macro to non-zero tells zstd to run the overflow correction + * code much more frequently. This is very inefficient, and should only be + * used for tests and fuzzers. + */ +#ifndef ZSTD_WINDOW_OVERFLOW_CORRECT_FREQUENTLY +# ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION +# define ZSTD_WINDOW_OVERFLOW_CORRECT_FREQUENTLY 1 +# else +# define ZSTD_WINDOW_OVERFLOW_CORRECT_FREQUENTLY 0 +# endif +#endif + +/** + * ZSTD_window_canOverflowCorrect(): + * Returns non-zero if the indices are large enough for overflow correction + * to work correctly without impacting compression ratio. + */ +MEM_STATIC U32 ZSTD_window_canOverflowCorrect(ZSTD_window_t const window, + U32 cycleLog, + U32 maxDist, + U32 loadedDictEnd, + void const* src) +{ + U32 const cycleSize = 1u << cycleLog; + U32 const curr = (U32)((BYTE const*)src - window.base); + U32 const minIndexToOverflowCorrect = cycleSize + + MAX(maxDist, cycleSize) + + ZSTD_WINDOW_START_INDEX; + + /* Adjust the min index to backoff the overflow correction frequency, + * so we don't waste too much CPU in overflow correction. If this + * computation overflows we don't really care, we just need to make + * sure it is at least minIndexToOverflowCorrect. + */ + U32 const adjustment = window.nbOverflowCorrections + 1; + U32 const adjustedIndex = MAX(minIndexToOverflowCorrect * adjustment, + minIndexToOverflowCorrect); + U32 const indexLargeEnough = curr > adjustedIndex; + + /* Only overflow correct early if the dictionary is invalidated already, + * so we don't hurt compression ratio. + */ + U32 const dictionaryInvalidated = curr > maxDist + loadedDictEnd; + + return indexLargeEnough && dictionaryInvalidated; +} + +/** + * ZSTD_window_needOverflowCorrection(): + * Returns non-zero if the indices are getting too large and need overflow + * protection. + */ +MEM_STATIC U32 ZSTD_window_needOverflowCorrection(ZSTD_window_t const window, + U32 cycleLog, + U32 maxDist, + U32 loadedDictEnd, + void const* src, + void const* srcEnd) +{ + U32 const curr = (U32)((BYTE const*)srcEnd - window.base); + if (ZSTD_WINDOW_OVERFLOW_CORRECT_FREQUENTLY) { + if (ZSTD_window_canOverflowCorrect(window, cycleLog, maxDist, loadedDictEnd, src)) { + return 1; + } + } + return curr > ZSTD_CURRENT_MAX; +} + +/** + * ZSTD_window_correctOverflow(): + * Reduces the indices to protect from index overflow. + * Returns the correction made to the indices, which must be applied to every + * stored index. + * + * The least significant cycleLog bits of the indices must remain the same, + * which may be 0. Every index up to maxDist in the past must be valid. + */ +MEM_STATIC +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +U32 ZSTD_window_correctOverflow(ZSTD_window_t* window, U32 cycleLog, + U32 maxDist, void const* src) +{ + /* preemptive overflow correction: + * 1. correction is large enough: + * lowLimit > (3<<29) ==> current > 3<<29 + 1< (3<<29 + 1< (3<<29) - (1< (3<<29) - (1<<30) (NOTE: chainLog <= 30) + * > 1<<29 + * + * 2. (ip+ZSTD_CHUNKSIZE_MAX - cctx->base) doesn't overflow: + * After correction, current is less than (1<base < 1<<32. + * 3. (cctx->lowLimit + 1< 3<<29 + 1<base); + U32 const currentCycle = curr & cycleMask; + /* Ensure newCurrent - maxDist >= ZSTD_WINDOW_START_INDEX. */ + U32 const currentCycleCorrection = currentCycle < ZSTD_WINDOW_START_INDEX + ? MAX(cycleSize, ZSTD_WINDOW_START_INDEX) + : 0; + U32 const newCurrent = currentCycle + + currentCycleCorrection + + MAX(maxDist, cycleSize); + U32 const correction = curr - newCurrent; + /* maxDist must be a power of two so that: + * (newCurrent & cycleMask) == (curr & cycleMask) + * This is required to not corrupt the chains / binary tree. + */ + assert((maxDist & (maxDist - 1)) == 0); + assert((curr & cycleMask) == (newCurrent & cycleMask)); + assert(curr > newCurrent); + if (!ZSTD_WINDOW_OVERFLOW_CORRECT_FREQUENTLY) { + /* Loose bound, should be around 1<<29 (see above) */ + assert(correction > 1<<28); + } + + window->base += correction; + window->dictBase += correction; + if (window->lowLimit < correction + ZSTD_WINDOW_START_INDEX) { + window->lowLimit = ZSTD_WINDOW_START_INDEX; + } else { + window->lowLimit -= correction; + } + if (window->dictLimit < correction + ZSTD_WINDOW_START_INDEX) { + window->dictLimit = ZSTD_WINDOW_START_INDEX; + } else { + window->dictLimit -= correction; + } + + /* Ensure we can still reference the full window. */ + assert(newCurrent >= maxDist); + assert(newCurrent - maxDist >= ZSTD_WINDOW_START_INDEX); + /* Ensure that lowLimit and dictLimit didn't underflow. */ + assert(window->lowLimit <= newCurrent); + assert(window->dictLimit <= newCurrent); + + ++window->nbOverflowCorrections; + + DEBUGLOG(4, "Correction of 0x%x bytes to lowLimit=0x%x", correction, + window->lowLimit); + return correction; +} + +/** + * ZSTD_window_enforceMaxDist(): + * Updates lowLimit so that: + * (srcEnd - base) - lowLimit == maxDist + loadedDictEnd + * + * It ensures index is valid as long as index >= lowLimit. + * This must be called before a block compression call. + * + * loadedDictEnd is only defined if a dictionary is in use for current compression. + * As the name implies, loadedDictEnd represents the index at end of dictionary. + * The value lies within context's referential, it can be directly compared to blockEndIdx. + * + * If loadedDictEndPtr is NULL, no dictionary is in use, and we use loadedDictEnd == 0. + * If loadedDictEndPtr is not NULL, we set it to zero after updating lowLimit. + * This is because dictionaries are allowed to be referenced fully + * as long as the last byte of the dictionary is in the window. + * Once input has progressed beyond window size, dictionary cannot be referenced anymore. + * + * In normal dict mode, the dictionary lies between lowLimit and dictLimit. + * In dictMatchState mode, lowLimit and dictLimit are the same, + * and the dictionary is below them. + * forceWindow and dictMatchState are therefore incompatible. + */ +MEM_STATIC void +ZSTD_window_enforceMaxDist(ZSTD_window_t* window, + const void* blockEnd, + U32 maxDist, + U32* loadedDictEndPtr, + const ZSTD_matchState_t** dictMatchStatePtr) +{ + U32 const blockEndIdx = (U32)((BYTE const*)blockEnd - window->base); + U32 const loadedDictEnd = (loadedDictEndPtr != NULL) ? *loadedDictEndPtr : 0; + DEBUGLOG(5, "ZSTD_window_enforceMaxDist: blockEndIdx=%u, maxDist=%u, loadedDictEnd=%u", + (unsigned)blockEndIdx, (unsigned)maxDist, (unsigned)loadedDictEnd); + + /* - When there is no dictionary : loadedDictEnd == 0. + In which case, the test (blockEndIdx > maxDist) is merely to avoid + overflowing next operation `newLowLimit = blockEndIdx - maxDist`. + - When there is a standard dictionary : + Index referential is copied from the dictionary, + which means it starts from 0. + In which case, loadedDictEnd == dictSize, + and it makes sense to compare `blockEndIdx > maxDist + dictSize` + since `blockEndIdx` also starts from zero. + - When there is an attached dictionary : + loadedDictEnd is expressed within the referential of the context, + so it can be directly compared against blockEndIdx. + */ + if (blockEndIdx > maxDist + loadedDictEnd) { + U32 const newLowLimit = blockEndIdx - maxDist; + if (window->lowLimit < newLowLimit) window->lowLimit = newLowLimit; + if (window->dictLimit < window->lowLimit) { + DEBUGLOG(5, "Update dictLimit to match lowLimit, from %u to %u", + (unsigned)window->dictLimit, (unsigned)window->lowLimit); + window->dictLimit = window->lowLimit; + } + /* On reaching window size, dictionaries are invalidated */ + if (loadedDictEndPtr) *loadedDictEndPtr = 0; + if (dictMatchStatePtr) *dictMatchStatePtr = NULL; + } +} + +/* Similar to ZSTD_window_enforceMaxDist(), + * but only invalidates dictionary + * when input progresses beyond window size. + * assumption : loadedDictEndPtr and dictMatchStatePtr are valid (non NULL) + * loadedDictEnd uses same referential as window->base + * maxDist is the window size */ +MEM_STATIC void +ZSTD_checkDictValidity(const ZSTD_window_t* window, + const void* blockEnd, + U32 maxDist, + U32* loadedDictEndPtr, + const ZSTD_matchState_t** dictMatchStatePtr) +{ + assert(loadedDictEndPtr != NULL); + assert(dictMatchStatePtr != NULL); + { U32 const blockEndIdx = (U32)((BYTE const*)blockEnd - window->base); + U32 const loadedDictEnd = *loadedDictEndPtr; + DEBUGLOG(5, "ZSTD_checkDictValidity: blockEndIdx=%u, maxDist=%u, loadedDictEnd=%u", + (unsigned)blockEndIdx, (unsigned)maxDist, (unsigned)loadedDictEnd); + assert(blockEndIdx >= loadedDictEnd); + + if (blockEndIdx > loadedDictEnd + maxDist || loadedDictEnd != window->dictLimit) { + /* On reaching window size, dictionaries are invalidated. + * For simplification, if window size is reached anywhere within next block, + * the dictionary is invalidated for the full block. + * + * We also have to invalidate the dictionary if ZSTD_window_update() has detected + * non-contiguous segments, which means that loadedDictEnd != window->dictLimit. + * loadedDictEnd may be 0, if forceWindow is true, but in that case we never use + * dictMatchState, so setting it to NULL is not a problem. + */ + DEBUGLOG(6, "invalidating dictionary for current block (distance > windowSize)"); + *loadedDictEndPtr = 0; + *dictMatchStatePtr = NULL; + } else { + if (*loadedDictEndPtr != 0) { + DEBUGLOG(6, "dictionary considered valid for current block"); + } } } +} + +MEM_STATIC void ZSTD_window_init(ZSTD_window_t* window) { + ZSTD_memset(window, 0, sizeof(*window)); + window->base = (BYTE const*)" "; + window->dictBase = (BYTE const*)" "; + ZSTD_STATIC_ASSERT(ZSTD_DUBT_UNSORTED_MARK < ZSTD_WINDOW_START_INDEX); /* Start above ZSTD_DUBT_UNSORTED_MARK */ + window->dictLimit = ZSTD_WINDOW_START_INDEX; /* start from >0, so that 1st position is valid */ + window->lowLimit = ZSTD_WINDOW_START_INDEX; /* it ensures first and later CCtx usages compress the same */ + window->nextSrc = window->base + ZSTD_WINDOW_START_INDEX; /* see issue #1241 */ + window->nbOverflowCorrections = 0; +} + +/** + * ZSTD_window_update(): + * Updates the window by appending [src, src + srcSize) to the window. + * If it is not contiguous, the current prefix becomes the extDict, and we + * forget about the extDict. Handles overlap of the prefix and extDict. + * Returns non-zero if the segment is contiguous. + */ +MEM_STATIC +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +U32 ZSTD_window_update(ZSTD_window_t* window, + void const* src, size_t srcSize, + int forceNonContiguous) +{ + BYTE const* const ip = (BYTE const*)src; + U32 contiguous = 1; + DEBUGLOG(5, "ZSTD_window_update"); + if (srcSize == 0) + return contiguous; + assert(window->base != NULL); + assert(window->dictBase != NULL); + /* Check if blocks follow each other */ + if (src != window->nextSrc || forceNonContiguous) { + /* not contiguous */ + size_t const distanceFromBase = (size_t)(window->nextSrc - window->base); + DEBUGLOG(5, "Non contiguous blocks, new segment starts at %u", window->dictLimit); + window->lowLimit = window->dictLimit; + assert(distanceFromBase == (size_t)(U32)distanceFromBase); /* should never overflow */ + window->dictLimit = (U32)distanceFromBase; + window->dictBase = window->base; + window->base = ip - distanceFromBase; + /* ms->nextToUpdate = window->dictLimit; */ + if (window->dictLimit - window->lowLimit < HASH_READ_SIZE) window->lowLimit = window->dictLimit; /* too small extDict */ + contiguous = 0; + } + window->nextSrc = ip + srcSize; + /* if input and dictionary overlap : reduce dictionary (area presumed modified by input) */ + if ( (ip+srcSize > window->dictBase + window->lowLimit) + & (ip < window->dictBase + window->dictLimit)) { + ptrdiff_t const highInputIdx = (ip + srcSize) - window->dictBase; + U32 const lowLimitMax = (highInputIdx > (ptrdiff_t)window->dictLimit) ? window->dictLimit : (U32)highInputIdx; + window->lowLimit = lowLimitMax; + DEBUGLOG(5, "Overlapping extDict and input : new lowLimit = %u", window->lowLimit); + } + return contiguous; +} + +/** + * Returns the lowest allowed match index. It may either be in the ext-dict or the prefix. + */ +MEM_STATIC U32 ZSTD_getLowestMatchIndex(const ZSTD_matchState_t* ms, U32 curr, unsigned windowLog) +{ + U32 const maxDistance = 1U << windowLog; + U32 const lowestValid = ms->window.lowLimit; + U32 const withinWindow = (curr - lowestValid > maxDistance) ? curr - maxDistance : lowestValid; + U32 const isDictionary = (ms->loadedDictEnd != 0); + /* When using a dictionary the entire dictionary is valid if a single byte of the dictionary + * is within the window. We invalidate the dictionary (and set loadedDictEnd to 0) when it isn't + * valid for the entire block. So this check is sufficient to find the lowest valid match index. + */ + U32 const matchLowest = isDictionary ? lowestValid : withinWindow; + return matchLowest; +} + +/** + * Returns the lowest allowed match index in the prefix. + */ +MEM_STATIC U32 ZSTD_getLowestPrefixIndex(const ZSTD_matchState_t* ms, U32 curr, unsigned windowLog) +{ + U32 const maxDistance = 1U << windowLog; + U32 const lowestValid = ms->window.dictLimit; + U32 const withinWindow = (curr - lowestValid > maxDistance) ? curr - maxDistance : lowestValid; + U32 const isDictionary = (ms->loadedDictEnd != 0); + /* When computing the lowest prefix index we need to take the dictionary into account to handle + * the edge case where the dictionary and the source are contiguous in memory. + */ + U32 const matchLowest = isDictionary ? lowestValid : withinWindow; + return matchLowest; +} + + + +/* debug functions */ +#if (DEBUGLEVEL>=2) + +MEM_STATIC double ZSTD_fWeight(U32 rawStat) +{ + U32 const fp_accuracy = 8; + U32 const fp_multiplier = (1 << fp_accuracy); + U32 const newStat = rawStat + 1; + U32 const hb = ZSTD_highbit32(newStat); + U32 const BWeight = hb * fp_multiplier; + U32 const FWeight = (newStat << fp_accuracy) >> hb; + U32 const weight = BWeight + FWeight; + assert(hb + fp_accuracy < 31); + return (double)weight / fp_multiplier; +} + +/* display a table content, + * listing each element, its frequency, and its predicted bit cost */ +MEM_STATIC void ZSTD_debugTable(const U32* table, U32 max) +{ + unsigned u, sum; + for (u=0, sum=0; u<=max; u++) sum += table[u]; + DEBUGLOG(2, "total nb elts: %u", sum); + for (u=0; u<=max; u++) { + DEBUGLOG(2, "%2u: %5u (%.2f)", + u, table[u], ZSTD_fWeight(sum) - ZSTD_fWeight(table[u]) ); + } +} + +#endif + +/* Short Cache */ + +/* Normally, zstd matchfinders follow this flow: + * 1. Compute hash at ip + * 2. Load index from hashTable[hash] + * 3. Check if *ip == *(base + index) + * In dictionary compression, loading *(base + index) is often an L2 or even L3 miss. + * + * Short cache is an optimization which allows us to avoid step 3 most of the time + * when the data doesn't actually match. With short cache, the flow becomes: + * 1. Compute (hash, currentTag) at ip. currentTag is an 8-bit independent hash at ip. + * 2. Load (index, matchTag) from hashTable[hash]. See ZSTD_writeTaggedIndex to understand how this works. + * 3. Only if currentTag == matchTag, check *ip == *(base + index). Otherwise, continue. + * + * Currently, short cache is only implemented in CDict hashtables. Thus, its use is limited to + * dictMatchState matchfinders. + */ +#define ZSTD_SHORT_CACHE_TAG_BITS 8 +#define ZSTD_SHORT_CACHE_TAG_MASK ((1u << ZSTD_SHORT_CACHE_TAG_BITS) - 1) + +/* Helper function for ZSTD_fillHashTable and ZSTD_fillDoubleHashTable. + * Unpacks hashAndTag into (hash, tag), then packs (index, tag) into hashTable[hash]. */ +MEM_STATIC void ZSTD_writeTaggedIndex(U32* const hashTable, size_t hashAndTag, U32 index) { + size_t const hash = hashAndTag >> ZSTD_SHORT_CACHE_TAG_BITS; + U32 const tag = (U32)(hashAndTag & ZSTD_SHORT_CACHE_TAG_MASK); + assert(index >> (32 - ZSTD_SHORT_CACHE_TAG_BITS) == 0); + hashTable[hash] = (index << ZSTD_SHORT_CACHE_TAG_BITS) | tag; +} + +/* Helper function for short cache matchfinders. + * Unpacks tag1 and tag2 from lower bits of packedTag1 and packedTag2, then checks if the tags match. */ +MEM_STATIC int ZSTD_comparePackedTags(size_t packedTag1, size_t packedTag2) { + U32 const tag1 = packedTag1 & ZSTD_SHORT_CACHE_TAG_MASK; + U32 const tag2 = packedTag2 & ZSTD_SHORT_CACHE_TAG_MASK; + return tag1 == tag2; +} + +#if defined (__cplusplus) +} +#endif + +/* =============================================================== + * Shared internal declarations + * These prototypes may be called from sources not in lib/compress + * =============================================================== */ + +/* ZSTD_loadCEntropy() : + * dict : must point at beginning of a valid zstd dictionary. + * return : size of dictionary header (size of magic number + dict ID + entropy tables) + * assumptions : magic number supposed already checked + * and dictSize >= 8 */ +size_t ZSTD_loadCEntropy(ZSTD_compressedBlockState_t* bs, void* workspace, + const void* const dict, size_t dictSize); + +void ZSTD_reset_compressedBlockState(ZSTD_compressedBlockState_t* bs); + +/* ============================================================== + * Private declarations + * These prototypes shall only be called from within lib/compress + * ============================================================== */ + +/* ZSTD_getCParamsFromCCtxParams() : + * cParams are built depending on compressionLevel, src size hints, + * LDM and manually set compression parameters. + * Note: srcSizeHint == 0 means 0! + */ +ZSTD_compressionParameters ZSTD_getCParamsFromCCtxParams( + const ZSTD_CCtx_params* CCtxParams, U64 srcSizeHint, size_t dictSize, ZSTD_cParamMode_e mode); + +/*! ZSTD_initCStream_internal() : + * Private use only. Init streaming operation. + * expects params to be valid. + * must receive dict, or cdict, or none, but not both. + * @return : 0, or an error code */ +size_t ZSTD_initCStream_internal(ZSTD_CStream* zcs, + const void* dict, size_t dictSize, + const ZSTD_CDict* cdict, + const ZSTD_CCtx_params* params, unsigned long long pledgedSrcSize); + +void ZSTD_resetSeqStore(seqStore_t* ssPtr); + +/*! ZSTD_getCParamsFromCDict() : + * as the name implies */ +ZSTD_compressionParameters ZSTD_getCParamsFromCDict(const ZSTD_CDict* cdict); + +/* ZSTD_compressBegin_advanced_internal() : + * Private use only. To be called from zstdmt_compress.c. */ +size_t ZSTD_compressBegin_advanced_internal(ZSTD_CCtx* cctx, + const void* dict, size_t dictSize, + ZSTD_dictContentType_e dictContentType, + ZSTD_dictTableLoadMethod_e dtlm, + const ZSTD_CDict* cdict, + const ZSTD_CCtx_params* params, + unsigned long long pledgedSrcSize); + +/* ZSTD_compress_advanced_internal() : + * Private use only. To be called from zstdmt_compress.c. */ +size_t ZSTD_compress_advanced_internal(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize, + const ZSTD_CCtx_params* params); + + +/* ZSTD_writeLastEmptyBlock() : + * output an empty Block with end-of-frame mark to complete a frame + * @return : size of data written into `dst` (== ZSTD_blockHeaderSize (defined in zstd_internal.h)) + * or an error code if `dstCapacity` is too small ( 1 */ +U32 ZSTD_cycleLog(U32 hashLog, ZSTD_strategy strat); + +/** ZSTD_CCtx_trace() : + * Trace the end of a compression call. + */ +void ZSTD_CCtx_trace(ZSTD_CCtx* cctx, size_t extraCSize); + +/* Returns 0 on success, and a ZSTD_error otherwise. This function scans through an array of + * ZSTD_Sequence, storing the sequences it finds, until it reaches a block delimiter. + * Note that the block delimiter must include the last literals of the block. + */ +size_t +ZSTD_copySequencesToSeqStoreExplicitBlockDelim(ZSTD_CCtx* cctx, + ZSTD_sequencePosition* seqPos, + const ZSTD_Sequence* const inSeqs, size_t inSeqsSize, + const void* src, size_t blockSize, ZSTD_paramSwitch_e externalRepSearch); + +/* Returns the number of bytes to move the current read position back by. + * Only non-zero if we ended up splitting a sequence. + * Otherwise, it may return a ZSTD error if something went wrong. + * + * This function will attempt to scan through blockSize bytes + * represented by the sequences in @inSeqs, + * storing any (partial) sequences. + * + * Occasionally, we may want to change the actual number of bytes we consumed from inSeqs to + * avoid splitting a match, or to avoid splitting a match such that it would produce a match + * smaller than MINMATCH. In this case, we return the number of bytes that we didn't read from this block. + */ +size_t +ZSTD_copySequencesToSeqStoreNoBlockDelim(ZSTD_CCtx* cctx, ZSTD_sequencePosition* seqPos, + const ZSTD_Sequence* const inSeqs, size_t inSeqsSize, + const void* src, size_t blockSize, ZSTD_paramSwitch_e externalRepSearch); + +/* Returns 1 if an external sequence producer is registered, otherwise returns 0. */ +MEM_STATIC int ZSTD_hasExtSeqProd(const ZSTD_CCtx_params* params) { + return params->extSeqProdFunc != NULL; +} + +/* =============================================================== + * Deprecated definitions that are still used internally to avoid + * deprecation warnings. These functions are exactly equivalent to + * their public variants, but avoid the deprecation warnings. + * =============================================================== */ + +size_t ZSTD_compressBegin_usingCDict_deprecated(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict); + +size_t ZSTD_compressContinue_public(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize); + +size_t ZSTD_compressEnd_public(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize); + +size_t ZSTD_compressBlock_deprecated(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); + + +#endif /* ZSTD_COMPRESS_H */ +/**** ended inlining zstd_compress_internal.h ****/ + + +size_t ZSTD_noCompressLiterals (void* dst, size_t dstCapacity, const void* src, size_t srcSize); + +/* ZSTD_compressRleLiteralsBlock() : + * Conditions : + * - All bytes in @src are identical + * - dstCapacity >= 4 */ +size_t ZSTD_compressRleLiteralsBlock (void* dst, size_t dstCapacity, const void* src, size_t srcSize); + +/* ZSTD_compressLiterals(): + * @entropyWorkspace: must be aligned on 4-bytes boundaries + * @entropyWorkspaceSize : must be >= HUF_WORKSPACE_SIZE + * @suspectUncompressible: sampling checks, to potentially skip huffman coding + */ +size_t ZSTD_compressLiterals (void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + void* entropyWorkspace, size_t entropyWorkspaceSize, + const ZSTD_hufCTables_t* prevHuf, + ZSTD_hufCTables_t* nextHuf, + ZSTD_strategy strategy, int disableLiteralCompression, + int suspectUncompressible, + int bmi2); + +#endif /* ZSTD_COMPRESS_LITERALS_H */ +/**** ended inlining zstd_compress_literals.h ****/ + + +/* ************************************************************** +* Debug Traces +****************************************************************/ +#if DEBUGLEVEL >= 2 + +static size_t showHexa(const void* src, size_t srcSize) +{ + const BYTE* const ip = (const BYTE*)src; + size_t u; + for (u=0; u31) + (srcSize>4095); + + DEBUGLOG(5, "ZSTD_noCompressLiterals: srcSize=%zu, dstCapacity=%zu", srcSize, dstCapacity); + + RETURN_ERROR_IF(srcSize + flSize > dstCapacity, dstSize_tooSmall, ""); + + switch(flSize) + { + case 1: /* 2 - 1 - 5 */ + ostart[0] = (BYTE)((U32)set_basic + (srcSize<<3)); + break; + case 2: /* 2 - 2 - 12 */ + MEM_writeLE16(ostart, (U16)((U32)set_basic + (1<<2) + (srcSize<<4))); + break; + case 3: /* 2 - 2 - 20 */ + MEM_writeLE32(ostart, (U32)((U32)set_basic + (3<<2) + (srcSize<<4))); + break; + default: /* not necessary : flSize is {1,2,3} */ + assert(0); + } + + ZSTD_memcpy(ostart + flSize, src, srcSize); + DEBUGLOG(5, "Raw (uncompressed) literals: %u -> %u", (U32)srcSize, (U32)(srcSize + flSize)); + return srcSize + flSize; +} + +static int allBytesIdentical(const void* src, size_t srcSize) +{ + assert(srcSize >= 1); + assert(src != NULL); + { const BYTE b = ((const BYTE*)src)[0]; + size_t p; + for (p=1; p31) + (srcSize>4095); + + assert(dstCapacity >= 4); (void)dstCapacity; + assert(allBytesIdentical(src, srcSize)); + + switch(flSize) + { + case 1: /* 2 - 1 - 5 */ + ostart[0] = (BYTE)((U32)set_rle + (srcSize<<3)); + break; + case 2: /* 2 - 2 - 12 */ + MEM_writeLE16(ostart, (U16)((U32)set_rle + (1<<2) + (srcSize<<4))); + break; + case 3: /* 2 - 2 - 20 */ + MEM_writeLE32(ostart, (U32)((U32)set_rle + (3<<2) + (srcSize<<4))); + break; + default: /* not necessary : flSize is {1,2,3} */ + assert(0); + } + + ostart[flSize] = *(const BYTE*)src; + DEBUGLOG(5, "RLE : Repeated Literal (%02X: %u times) -> %u bytes encoded", ((const BYTE*)src)[0], (U32)srcSize, (U32)flSize + 1); + return flSize+1; +} + +/* ZSTD_minLiteralsToCompress() : + * returns minimal amount of literals + * for literal compression to even be attempted. + * Minimum is made tighter as compression strategy increases. + */ +static size_t +ZSTD_minLiteralsToCompress(ZSTD_strategy strategy, HUF_repeat huf_repeat) +{ + assert((int)strategy >= 0); + assert((int)strategy <= 9); + /* btultra2 : min 8 bytes; + * then 2x larger for each successive compression strategy + * max threshold 64 bytes */ + { int const shift = MIN(9-(int)strategy, 3); + size_t const mintc = (huf_repeat == HUF_repeat_valid) ? 6 : (size_t)8 << shift; + DEBUGLOG(7, "minLiteralsToCompress = %zu", mintc); + return mintc; + } +} + +size_t ZSTD_compressLiterals ( + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + void* entropyWorkspace, size_t entropyWorkspaceSize, + const ZSTD_hufCTables_t* prevHuf, + ZSTD_hufCTables_t* nextHuf, + ZSTD_strategy strategy, + int disableLiteralCompression, + int suspectUncompressible, + int bmi2) +{ + size_t const lhSize = 3 + (srcSize >= 1 KB) + (srcSize >= 16 KB); + BYTE* const ostart = (BYTE*)dst; + U32 singleStream = srcSize < 256; + symbolEncodingType_e hType = set_compressed; + size_t cLitSize; + + DEBUGLOG(5,"ZSTD_compressLiterals (disableLiteralCompression=%i, srcSize=%u, dstCapacity=%zu)", + disableLiteralCompression, (U32)srcSize, dstCapacity); + + DEBUGLOG(6, "Completed literals listing (%zu bytes)", showHexa(src, srcSize)); + + /* Prepare nextEntropy assuming reusing the existing table */ + ZSTD_memcpy(nextHuf, prevHuf, sizeof(*prevHuf)); + + if (disableLiteralCompression) + return ZSTD_noCompressLiterals(dst, dstCapacity, src, srcSize); + + /* if too small, don't even attempt compression (speed opt) */ + if (srcSize < ZSTD_minLiteralsToCompress(strategy, prevHuf->repeatMode)) + return ZSTD_noCompressLiterals(dst, dstCapacity, src, srcSize); + + RETURN_ERROR_IF(dstCapacity < lhSize+1, dstSize_tooSmall, "not enough space for compression"); + { HUF_repeat repeat = prevHuf->repeatMode; + int const flags = 0 + | (bmi2 ? HUF_flags_bmi2 : 0) + | (strategy < ZSTD_lazy && srcSize <= 1024 ? HUF_flags_preferRepeat : 0) + | (strategy >= HUF_OPTIMAL_DEPTH_THRESHOLD ? HUF_flags_optimalDepth : 0) + | (suspectUncompressible ? HUF_flags_suspectUncompressible : 0); + + typedef size_t (*huf_compress_f)(void*, size_t, const void*, size_t, unsigned, unsigned, void*, size_t, HUF_CElt*, HUF_repeat*, int); + huf_compress_f huf_compress; + if (repeat == HUF_repeat_valid && lhSize == 3) singleStream = 1; + huf_compress = singleStream ? HUF_compress1X_repeat : HUF_compress4X_repeat; + cLitSize = huf_compress(ostart+lhSize, dstCapacity-lhSize, + src, srcSize, + HUF_SYMBOLVALUE_MAX, LitHufLog, + entropyWorkspace, entropyWorkspaceSize, + (HUF_CElt*)nextHuf->CTable, + &repeat, flags); + DEBUGLOG(5, "%zu literals compressed into %zu bytes (before header)", srcSize, cLitSize); + if (repeat != HUF_repeat_none) { + /* reused the existing table */ + DEBUGLOG(5, "reusing statistics from previous huffman block"); + hType = set_repeat; + } + } + + { size_t const minGain = ZSTD_minGain(srcSize, strategy); + if ((cLitSize==0) || (cLitSize >= srcSize - minGain) || ERR_isError(cLitSize)) { + ZSTD_memcpy(nextHuf, prevHuf, sizeof(*prevHuf)); + return ZSTD_noCompressLiterals(dst, dstCapacity, src, srcSize); + } } + if (cLitSize==1) { + /* A return value of 1 signals that the alphabet consists of a single symbol. + * However, in some rare circumstances, it could be the compressed size (a single byte). + * For that outcome to have a chance to happen, it's necessary that `srcSize < 8`. + * (it's also necessary to not generate statistics). + * Therefore, in such a case, actively check that all bytes are identical. */ + if ((srcSize >= 8) || allBytesIdentical(src, srcSize)) { + ZSTD_memcpy(nextHuf, prevHuf, sizeof(*prevHuf)); + return ZSTD_compressRleLiteralsBlock(dst, dstCapacity, src, srcSize); + } } + + if (hType == set_compressed) { + /* using a newly constructed table */ + nextHuf->repeatMode = HUF_repeat_check; + } + + /* Build header */ + switch(lhSize) + { + case 3: /* 2 - 2 - 10 - 10 */ + if (!singleStream) assert(srcSize >= MIN_LITERALS_FOR_4_STREAMS); + { U32 const lhc = hType + ((U32)(!singleStream) << 2) + ((U32)srcSize<<4) + ((U32)cLitSize<<14); + MEM_writeLE24(ostart, lhc); + break; + } + case 4: /* 2 - 2 - 14 - 14 */ + assert(srcSize >= MIN_LITERALS_FOR_4_STREAMS); + { U32 const lhc = hType + (2 << 2) + ((U32)srcSize<<4) + ((U32)cLitSize<<18); + MEM_writeLE32(ostart, lhc); + break; + } + case 5: /* 2 - 2 - 18 - 18 */ + assert(srcSize >= MIN_LITERALS_FOR_4_STREAMS); + { U32 const lhc = hType + (3 << 2) + ((U32)srcSize<<4) + ((U32)cLitSize<<22); + MEM_writeLE32(ostart, lhc); + ostart[4] = (BYTE)(cLitSize >> 10); + break; + } + default: /* not possible : lhSize is {3,4,5} */ + assert(0); + } + DEBUGLOG(5, "Compressed literals: %u -> %u", (U32)srcSize, (U32)(lhSize+cLitSize)); + return lhSize+cLitSize; +} +/**** ended inlining compress/zstd_compress_literals.c ****/ +/**** start inlining compress/zstd_compress_sequences.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + /*-************************************* + * Dependencies + ***************************************/ +/**** start inlining zstd_compress_sequences.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_COMPRESS_SEQUENCES_H +#define ZSTD_COMPRESS_SEQUENCES_H + +/**** skipping file: ../common/fse.h ****/ +/**** skipping file: ../common/zstd_internal.h ****/ + +typedef enum { + ZSTD_defaultDisallowed = 0, + ZSTD_defaultAllowed = 1 +} ZSTD_defaultPolicy_e; + +symbolEncodingType_e +ZSTD_selectEncodingType( + FSE_repeat* repeatMode, unsigned const* count, unsigned const max, + size_t const mostFrequent, size_t nbSeq, unsigned const FSELog, + FSE_CTable const* prevCTable, + short const* defaultNorm, U32 defaultNormLog, + ZSTD_defaultPolicy_e const isDefaultAllowed, + ZSTD_strategy const strategy); + +size_t +ZSTD_buildCTable(void* dst, size_t dstCapacity, + FSE_CTable* nextCTable, U32 FSELog, symbolEncodingType_e type, + unsigned* count, U32 max, + const BYTE* codeTable, size_t nbSeq, + const S16* defaultNorm, U32 defaultNormLog, U32 defaultMax, + const FSE_CTable* prevCTable, size_t prevCTableSize, + void* entropyWorkspace, size_t entropyWorkspaceSize); + +size_t ZSTD_encodeSequences( + void* dst, size_t dstCapacity, + FSE_CTable const* CTable_MatchLength, BYTE const* mlCodeTable, + FSE_CTable const* CTable_OffsetBits, BYTE const* ofCodeTable, + FSE_CTable const* CTable_LitLength, BYTE const* llCodeTable, + seqDef const* sequences, size_t nbSeq, int longOffsets, int bmi2); + +size_t ZSTD_fseBitCost( + FSE_CTable const* ctable, + unsigned const* count, + unsigned const max); + +size_t ZSTD_crossEntropyCost(short const* norm, unsigned accuracyLog, + unsigned const* count, unsigned const max); +#endif /* ZSTD_COMPRESS_SEQUENCES_H */ +/**** ended inlining zstd_compress_sequences.h ****/ + +/** + * -log2(x / 256) lookup table for x in [0, 256). + * If x == 0: Return 0 + * Else: Return floor(-log2(x / 256) * 256) + */ +static unsigned const kInverseProbabilityLog256[256] = { + 0, 2048, 1792, 1642, 1536, 1453, 1386, 1329, 1280, 1236, 1197, 1162, + 1130, 1100, 1073, 1047, 1024, 1001, 980, 960, 941, 923, 906, 889, + 874, 859, 844, 830, 817, 804, 791, 779, 768, 756, 745, 734, + 724, 714, 704, 694, 685, 676, 667, 658, 650, 642, 633, 626, + 618, 610, 603, 595, 588, 581, 574, 567, 561, 554, 548, 542, + 535, 529, 523, 517, 512, 506, 500, 495, 489, 484, 478, 473, + 468, 463, 458, 453, 448, 443, 438, 434, 429, 424, 420, 415, + 411, 407, 402, 398, 394, 390, 386, 382, 377, 373, 370, 366, + 362, 358, 354, 350, 347, 343, 339, 336, 332, 329, 325, 322, + 318, 315, 311, 308, 305, 302, 298, 295, 292, 289, 286, 282, + 279, 276, 273, 270, 267, 264, 261, 258, 256, 253, 250, 247, + 244, 241, 239, 236, 233, 230, 228, 225, 222, 220, 217, 215, + 212, 209, 207, 204, 202, 199, 197, 194, 192, 190, 187, 185, + 182, 180, 178, 175, 173, 171, 168, 166, 164, 162, 159, 157, + 155, 153, 151, 149, 146, 144, 142, 140, 138, 136, 134, 132, + 130, 128, 126, 123, 121, 119, 117, 115, 114, 112, 110, 108, + 106, 104, 102, 100, 98, 96, 94, 93, 91, 89, 87, 85, + 83, 82, 80, 78, 76, 74, 73, 71, 69, 67, 66, 64, + 62, 61, 59, 57, 55, 54, 52, 50, 49, 47, 46, 44, + 42, 41, 39, 37, 36, 34, 33, 31, 30, 28, 26, 25, + 23, 22, 20, 19, 17, 16, 14, 13, 11, 10, 8, 7, + 5, 4, 2, 1, +}; + +static unsigned ZSTD_getFSEMaxSymbolValue(FSE_CTable const* ctable) { + void const* ptr = ctable; + U16 const* u16ptr = (U16 const*)ptr; + U32 const maxSymbolValue = MEM_read16(u16ptr + 1); + return maxSymbolValue; +} + +/** + * Returns true if we should use ncount=-1 else we should + * use ncount=1 for low probability symbols instead. + */ +static unsigned ZSTD_useLowProbCount(size_t const nbSeq) +{ + /* Heuristic: This should cover most blocks <= 16K and + * start to fade out after 16K to about 32K depending on + * compressibility. + */ + return nbSeq >= 2048; +} + +/** + * Returns the cost in bytes of encoding the normalized count header. + * Returns an error if any of the helper functions return an error. + */ +static size_t ZSTD_NCountCost(unsigned const* count, unsigned const max, + size_t const nbSeq, unsigned const FSELog) +{ + BYTE wksp[FSE_NCOUNTBOUND]; + S16 norm[MaxSeq + 1]; + const U32 tableLog = FSE_optimalTableLog(FSELog, nbSeq, max); + FORWARD_IF_ERROR(FSE_normalizeCount(norm, tableLog, count, nbSeq, max, ZSTD_useLowProbCount(nbSeq)), ""); + return FSE_writeNCount(wksp, sizeof(wksp), norm, max, tableLog); +} + +/** + * Returns the cost in bits of encoding the distribution described by count + * using the entropy bound. + */ +static size_t ZSTD_entropyCost(unsigned const* count, unsigned const max, size_t const total) +{ + unsigned cost = 0; + unsigned s; + + assert(total > 0); + for (s = 0; s <= max; ++s) { + unsigned norm = (unsigned)((256 * count[s]) / total); + if (count[s] != 0 && norm == 0) + norm = 1; + assert(count[s] < total); + cost += count[s] * kInverseProbabilityLog256[norm]; + } + return cost >> 8; +} + +/** + * Returns the cost in bits of encoding the distribution in count using ctable. + * Returns an error if ctable cannot represent all the symbols in count. + */ +size_t ZSTD_fseBitCost( + FSE_CTable const* ctable, + unsigned const* count, + unsigned const max) +{ + unsigned const kAccuracyLog = 8; + size_t cost = 0; + unsigned s; + FSE_CState_t cstate; + FSE_initCState(&cstate, ctable); + if (ZSTD_getFSEMaxSymbolValue(ctable) < max) { + DEBUGLOG(5, "Repeat FSE_CTable has maxSymbolValue %u < %u", + ZSTD_getFSEMaxSymbolValue(ctable), max); + return ERROR(GENERIC); + } + for (s = 0; s <= max; ++s) { + unsigned const tableLog = cstate.stateLog; + unsigned const badCost = (tableLog + 1) << kAccuracyLog; + unsigned const bitCost = FSE_bitCost(cstate.symbolTT, tableLog, s, kAccuracyLog); + if (count[s] == 0) + continue; + if (bitCost >= badCost) { + DEBUGLOG(5, "Repeat FSE_CTable has Prob[%u] == 0", s); + return ERROR(GENERIC); + } + cost += (size_t)count[s] * bitCost; + } + return cost >> kAccuracyLog; +} + +/** + * Returns the cost in bits of encoding the distribution in count using the + * table described by norm. The max symbol support by norm is assumed >= max. + * norm must be valid for every symbol with non-zero probability in count. + */ +size_t ZSTD_crossEntropyCost(short const* norm, unsigned accuracyLog, + unsigned const* count, unsigned const max) +{ + unsigned const shift = 8 - accuracyLog; + size_t cost = 0; + unsigned s; + assert(accuracyLog <= 8); + for (s = 0; s <= max; ++s) { + unsigned const normAcc = (norm[s] != -1) ? (unsigned)norm[s] : 1; + unsigned const norm256 = normAcc << shift; + assert(norm256 > 0); + assert(norm256 < 256); + cost += count[s] * kInverseProbabilityLog256[norm256]; + } + return cost >> 8; +} + +symbolEncodingType_e +ZSTD_selectEncodingType( + FSE_repeat* repeatMode, unsigned const* count, unsigned const max, + size_t const mostFrequent, size_t nbSeq, unsigned const FSELog, + FSE_CTable const* prevCTable, + short const* defaultNorm, U32 defaultNormLog, + ZSTD_defaultPolicy_e const isDefaultAllowed, + ZSTD_strategy const strategy) +{ + ZSTD_STATIC_ASSERT(ZSTD_defaultDisallowed == 0 && ZSTD_defaultAllowed != 0); + if (mostFrequent == nbSeq) { + *repeatMode = FSE_repeat_none; + if (isDefaultAllowed && nbSeq <= 2) { + /* Prefer set_basic over set_rle when there are 2 or fewer symbols, + * since RLE uses 1 byte, but set_basic uses 5-6 bits per symbol. + * If basic encoding isn't possible, always choose RLE. + */ + DEBUGLOG(5, "Selected set_basic"); + return set_basic; + } + DEBUGLOG(5, "Selected set_rle"); + return set_rle; + } + if (strategy < ZSTD_lazy) { + if (isDefaultAllowed) { + size_t const staticFse_nbSeq_max = 1000; + size_t const mult = 10 - strategy; + size_t const baseLog = 3; + size_t const dynamicFse_nbSeq_min = (((size_t)1 << defaultNormLog) * mult) >> baseLog; /* 28-36 for offset, 56-72 for lengths */ + assert(defaultNormLog >= 5 && defaultNormLog <= 6); /* xx_DEFAULTNORMLOG */ + assert(mult <= 9 && mult >= 7); + if ( (*repeatMode == FSE_repeat_valid) + && (nbSeq < staticFse_nbSeq_max) ) { + DEBUGLOG(5, "Selected set_repeat"); + return set_repeat; + } + if ( (nbSeq < dynamicFse_nbSeq_min) + || (mostFrequent < (nbSeq >> (defaultNormLog-1))) ) { + DEBUGLOG(5, "Selected set_basic"); + /* The format allows default tables to be repeated, but it isn't useful. + * When using simple heuristics to select encoding type, we don't want + * to confuse these tables with dictionaries. When running more careful + * analysis, we don't need to waste time checking both repeating tables + * and default tables. + */ + *repeatMode = FSE_repeat_none; + return set_basic; + } + } + } else { + size_t const basicCost = isDefaultAllowed ? ZSTD_crossEntropyCost(defaultNorm, defaultNormLog, count, max) : ERROR(GENERIC); + size_t const repeatCost = *repeatMode != FSE_repeat_none ? ZSTD_fseBitCost(prevCTable, count, max) : ERROR(GENERIC); + size_t const NCountCost = ZSTD_NCountCost(count, max, nbSeq, FSELog); + size_t const compressedCost = (NCountCost << 3) + ZSTD_entropyCost(count, max, nbSeq); + + if (isDefaultAllowed) { + assert(!ZSTD_isError(basicCost)); + assert(!(*repeatMode == FSE_repeat_valid && ZSTD_isError(repeatCost))); + } + assert(!ZSTD_isError(NCountCost)); + assert(compressedCost < ERROR(maxCode)); + DEBUGLOG(5, "Estimated bit costs: basic=%u\trepeat=%u\tcompressed=%u", + (unsigned)basicCost, (unsigned)repeatCost, (unsigned)compressedCost); + if (basicCost <= repeatCost && basicCost <= compressedCost) { + DEBUGLOG(5, "Selected set_basic"); + assert(isDefaultAllowed); + *repeatMode = FSE_repeat_none; + return set_basic; + } + if (repeatCost <= compressedCost) { + DEBUGLOG(5, "Selected set_repeat"); + assert(!ZSTD_isError(repeatCost)); + return set_repeat; + } + assert(compressedCost < basicCost && compressedCost < repeatCost); + } + DEBUGLOG(5, "Selected set_compressed"); + *repeatMode = FSE_repeat_check; + return set_compressed; +} + +typedef struct { + S16 norm[MaxSeq + 1]; + U32 wksp[FSE_BUILD_CTABLE_WORKSPACE_SIZE_U32(MaxSeq, MaxFSELog)]; +} ZSTD_BuildCTableWksp; + +size_t +ZSTD_buildCTable(void* dst, size_t dstCapacity, + FSE_CTable* nextCTable, U32 FSELog, symbolEncodingType_e type, + unsigned* count, U32 max, + const BYTE* codeTable, size_t nbSeq, + const S16* defaultNorm, U32 defaultNormLog, U32 defaultMax, + const FSE_CTable* prevCTable, size_t prevCTableSize, + void* entropyWorkspace, size_t entropyWorkspaceSize) +{ + BYTE* op = (BYTE*)dst; + const BYTE* const oend = op + dstCapacity; + DEBUGLOG(6, "ZSTD_buildCTable (dstCapacity=%u)", (unsigned)dstCapacity); + + switch (type) { + case set_rle: + FORWARD_IF_ERROR(FSE_buildCTable_rle(nextCTable, (BYTE)max), ""); + RETURN_ERROR_IF(dstCapacity==0, dstSize_tooSmall, "not enough space"); + *op = codeTable[0]; + return 1; + case set_repeat: + ZSTD_memcpy(nextCTable, prevCTable, prevCTableSize); + return 0; + case set_basic: + FORWARD_IF_ERROR(FSE_buildCTable_wksp(nextCTable, defaultNorm, defaultMax, defaultNormLog, entropyWorkspace, entropyWorkspaceSize), ""); /* note : could be pre-calculated */ + return 0; + case set_compressed: { + ZSTD_BuildCTableWksp* wksp = (ZSTD_BuildCTableWksp*)entropyWorkspace; + size_t nbSeq_1 = nbSeq; + const U32 tableLog = FSE_optimalTableLog(FSELog, nbSeq, max); + if (count[codeTable[nbSeq-1]] > 1) { + count[codeTable[nbSeq-1]]--; + nbSeq_1--; + } + assert(nbSeq_1 > 1); + assert(entropyWorkspaceSize >= sizeof(ZSTD_BuildCTableWksp)); + (void)entropyWorkspaceSize; + FORWARD_IF_ERROR(FSE_normalizeCount(wksp->norm, tableLog, count, nbSeq_1, max, ZSTD_useLowProbCount(nbSeq_1)), "FSE_normalizeCount failed"); + assert(oend >= op); + { size_t const NCountSize = FSE_writeNCount(op, (size_t)(oend - op), wksp->norm, max, tableLog); /* overflow protected */ + FORWARD_IF_ERROR(NCountSize, "FSE_writeNCount failed"); + FORWARD_IF_ERROR(FSE_buildCTable_wksp(nextCTable, wksp->norm, max, tableLog, wksp->wksp, sizeof(wksp->wksp)), "FSE_buildCTable_wksp failed"); + return NCountSize; + } + } + default: assert(0); RETURN_ERROR(GENERIC, "impossible to reach"); + } +} + +FORCE_INLINE_TEMPLATE size_t +ZSTD_encodeSequences_body( + void* dst, size_t dstCapacity, + FSE_CTable const* CTable_MatchLength, BYTE const* mlCodeTable, + FSE_CTable const* CTable_OffsetBits, BYTE const* ofCodeTable, + FSE_CTable const* CTable_LitLength, BYTE const* llCodeTable, + seqDef const* sequences, size_t nbSeq, int longOffsets) +{ + BIT_CStream_t blockStream; + FSE_CState_t stateMatchLength; + FSE_CState_t stateOffsetBits; + FSE_CState_t stateLitLength; + + RETURN_ERROR_IF( + ERR_isError(BIT_initCStream(&blockStream, dst, dstCapacity)), + dstSize_tooSmall, "not enough space remaining"); + DEBUGLOG(6, "available space for bitstream : %i (dstCapacity=%u)", + (int)(blockStream.endPtr - blockStream.startPtr), + (unsigned)dstCapacity); + + /* first symbols */ + FSE_initCState2(&stateMatchLength, CTable_MatchLength, mlCodeTable[nbSeq-1]); + FSE_initCState2(&stateOffsetBits, CTable_OffsetBits, ofCodeTable[nbSeq-1]); + FSE_initCState2(&stateLitLength, CTable_LitLength, llCodeTable[nbSeq-1]); + BIT_addBits(&blockStream, sequences[nbSeq-1].litLength, LL_bits[llCodeTable[nbSeq-1]]); + if (MEM_32bits()) BIT_flushBits(&blockStream); + BIT_addBits(&blockStream, sequences[nbSeq-1].mlBase, ML_bits[mlCodeTable[nbSeq-1]]); + if (MEM_32bits()) BIT_flushBits(&blockStream); + if (longOffsets) { + U32 const ofBits = ofCodeTable[nbSeq-1]; + unsigned const extraBits = ofBits - MIN(ofBits, STREAM_ACCUMULATOR_MIN-1); + if (extraBits) { + BIT_addBits(&blockStream, sequences[nbSeq-1].offBase, extraBits); + BIT_flushBits(&blockStream); + } + BIT_addBits(&blockStream, sequences[nbSeq-1].offBase >> extraBits, + ofBits - extraBits); + } else { + BIT_addBits(&blockStream, sequences[nbSeq-1].offBase, ofCodeTable[nbSeq-1]); + } + BIT_flushBits(&blockStream); + + { size_t n; + for (n=nbSeq-2 ; n= 64-7-(LLFSELog+MLFSELog+OffFSELog))) + BIT_flushBits(&blockStream); /* (7)*/ + BIT_addBits(&blockStream, sequences[n].litLength, llBits); + if (MEM_32bits() && ((llBits+mlBits)>24)) BIT_flushBits(&blockStream); + BIT_addBits(&blockStream, sequences[n].mlBase, mlBits); + if (MEM_32bits() || (ofBits+mlBits+llBits > 56)) BIT_flushBits(&blockStream); + if (longOffsets) { + unsigned const extraBits = ofBits - MIN(ofBits, STREAM_ACCUMULATOR_MIN-1); + if (extraBits) { + BIT_addBits(&blockStream, sequences[n].offBase, extraBits); + BIT_flushBits(&blockStream); /* (7)*/ + } + BIT_addBits(&blockStream, sequences[n].offBase >> extraBits, + ofBits - extraBits); /* 31 */ + } else { + BIT_addBits(&blockStream, sequences[n].offBase, ofBits); /* 31 */ + } + BIT_flushBits(&blockStream); /* (7)*/ + DEBUGLOG(7, "remaining space : %i", (int)(blockStream.endPtr - blockStream.ptr)); + } } + + DEBUGLOG(6, "ZSTD_encodeSequences: flushing ML state with %u bits", stateMatchLength.stateLog); + FSE_flushCState(&blockStream, &stateMatchLength); + DEBUGLOG(6, "ZSTD_encodeSequences: flushing Off state with %u bits", stateOffsetBits.stateLog); + FSE_flushCState(&blockStream, &stateOffsetBits); + DEBUGLOG(6, "ZSTD_encodeSequences: flushing LL state with %u bits", stateLitLength.stateLog); + FSE_flushCState(&blockStream, &stateLitLength); + + { size_t const streamSize = BIT_closeCStream(&blockStream); + RETURN_ERROR_IF(streamSize==0, dstSize_tooSmall, "not enough space"); + return streamSize; + } +} + +static size_t +ZSTD_encodeSequences_default( + void* dst, size_t dstCapacity, + FSE_CTable const* CTable_MatchLength, BYTE const* mlCodeTable, + FSE_CTable const* CTable_OffsetBits, BYTE const* ofCodeTable, + FSE_CTable const* CTable_LitLength, BYTE const* llCodeTable, + seqDef const* sequences, size_t nbSeq, int longOffsets) +{ + return ZSTD_encodeSequences_body(dst, dstCapacity, + CTable_MatchLength, mlCodeTable, + CTable_OffsetBits, ofCodeTable, + CTable_LitLength, llCodeTable, + sequences, nbSeq, longOffsets); +} + + +#if DYNAMIC_BMI2 + +static BMI2_TARGET_ATTRIBUTE size_t +ZSTD_encodeSequences_bmi2( + void* dst, size_t dstCapacity, + FSE_CTable const* CTable_MatchLength, BYTE const* mlCodeTable, + FSE_CTable const* CTable_OffsetBits, BYTE const* ofCodeTable, + FSE_CTable const* CTable_LitLength, BYTE const* llCodeTable, + seqDef const* sequences, size_t nbSeq, int longOffsets) +{ + return ZSTD_encodeSequences_body(dst, dstCapacity, + CTable_MatchLength, mlCodeTable, + CTable_OffsetBits, ofCodeTable, + CTable_LitLength, llCodeTable, + sequences, nbSeq, longOffsets); +} + +#endif + +size_t ZSTD_encodeSequences( + void* dst, size_t dstCapacity, + FSE_CTable const* CTable_MatchLength, BYTE const* mlCodeTable, + FSE_CTable const* CTable_OffsetBits, BYTE const* ofCodeTable, + FSE_CTable const* CTable_LitLength, BYTE const* llCodeTable, + seqDef const* sequences, size_t nbSeq, int longOffsets, int bmi2) +{ + DEBUGLOG(5, "ZSTD_encodeSequences: dstCapacity = %u", (unsigned)dstCapacity); +#if DYNAMIC_BMI2 + if (bmi2) { + return ZSTD_encodeSequences_bmi2(dst, dstCapacity, + CTable_MatchLength, mlCodeTable, + CTable_OffsetBits, ofCodeTable, + CTable_LitLength, llCodeTable, + sequences, nbSeq, longOffsets); + } +#endif + (void)bmi2; + return ZSTD_encodeSequences_default(dst, dstCapacity, + CTable_MatchLength, mlCodeTable, + CTable_OffsetBits, ofCodeTable, + CTable_LitLength, llCodeTable, + sequences, nbSeq, longOffsets); +} +/**** ended inlining compress/zstd_compress_sequences.c ****/ +/**** start inlining compress/zstd_compress_superblock.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + /*-************************************* + * Dependencies + ***************************************/ +/**** start inlining zstd_compress_superblock.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_COMPRESS_ADVANCED_H +#define ZSTD_COMPRESS_ADVANCED_H + +/*-************************************* +* Dependencies +***************************************/ + +/**** skipping file: ../zstd.h ****/ + +/*-************************************* +* Target Compressed Block Size +***************************************/ + +/* ZSTD_compressSuperBlock() : + * Used to compress a super block when targetCBlockSize is being used. + * The given block will be compressed into multiple sub blocks that are around targetCBlockSize. */ +size_t ZSTD_compressSuperBlock(ZSTD_CCtx* zc, + void* dst, size_t dstCapacity, + void const* src, size_t srcSize, + unsigned lastBlock); + +#endif /* ZSTD_COMPRESS_ADVANCED_H */ +/**** ended inlining zstd_compress_superblock.h ****/ + +/**** skipping file: ../common/zstd_internal.h ****/ +/**** skipping file: hist.h ****/ +/**** skipping file: zstd_compress_internal.h ****/ +/**** skipping file: zstd_compress_sequences.h ****/ +/**** skipping file: zstd_compress_literals.h ****/ + +/** ZSTD_compressSubBlock_literal() : + * Compresses literals section for a sub-block. + * When we have to write the Huffman table we will sometimes choose a header + * size larger than necessary. This is because we have to pick the header size + * before we know the table size + compressed size, so we have a bound on the + * table size. If we guessed incorrectly, we fall back to uncompressed literals. + * + * We write the header when writeEntropy=1 and set entropyWritten=1 when we succeeded + * in writing the header, otherwise it is set to 0. + * + * hufMetadata->hType has literals block type info. + * If it is set_basic, all sub-blocks literals section will be Raw_Literals_Block. + * If it is set_rle, all sub-blocks literals section will be RLE_Literals_Block. + * If it is set_compressed, first sub-block's literals section will be Compressed_Literals_Block + * If it is set_compressed, first sub-block's literals section will be Treeless_Literals_Block + * and the following sub-blocks' literals sections will be Treeless_Literals_Block. + * @return : compressed size of literals section of a sub-block + * Or 0 if unable to compress. + * Or error code */ +static size_t +ZSTD_compressSubBlock_literal(const HUF_CElt* hufTable, + const ZSTD_hufCTablesMetadata_t* hufMetadata, + const BYTE* literals, size_t litSize, + void* dst, size_t dstSize, + const int bmi2, int writeEntropy, int* entropyWritten) +{ + size_t const header = writeEntropy ? 200 : 0; + size_t const lhSize = 3 + (litSize >= (1 KB - header)) + (litSize >= (16 KB - header)); + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = ostart + dstSize; + BYTE* op = ostart + lhSize; + U32 const singleStream = lhSize == 3; + symbolEncodingType_e hType = writeEntropy ? hufMetadata->hType : set_repeat; + size_t cLitSize = 0; + + DEBUGLOG(5, "ZSTD_compressSubBlock_literal (litSize=%zu, lhSize=%zu, writeEntropy=%d)", litSize, lhSize, writeEntropy); + + *entropyWritten = 0; + if (litSize == 0 || hufMetadata->hType == set_basic) { + DEBUGLOG(5, "ZSTD_compressSubBlock_literal using raw literal"); + return ZSTD_noCompressLiterals(dst, dstSize, literals, litSize); + } else if (hufMetadata->hType == set_rle) { + DEBUGLOG(5, "ZSTD_compressSubBlock_literal using rle literal"); + return ZSTD_compressRleLiteralsBlock(dst, dstSize, literals, litSize); + } + + assert(litSize > 0); + assert(hufMetadata->hType == set_compressed || hufMetadata->hType == set_repeat); + + if (writeEntropy && hufMetadata->hType == set_compressed) { + ZSTD_memcpy(op, hufMetadata->hufDesBuffer, hufMetadata->hufDesSize); + op += hufMetadata->hufDesSize; + cLitSize += hufMetadata->hufDesSize; + DEBUGLOG(5, "ZSTD_compressSubBlock_literal (hSize=%zu)", hufMetadata->hufDesSize); + } + + { int const flags = bmi2 ? HUF_flags_bmi2 : 0; + const size_t cSize = singleStream ? HUF_compress1X_usingCTable(op, (size_t)(oend-op), literals, litSize, hufTable, flags) + : HUF_compress4X_usingCTable(op, (size_t)(oend-op), literals, litSize, hufTable, flags); + op += cSize; + cLitSize += cSize; + if (cSize == 0 || ERR_isError(cSize)) { + DEBUGLOG(5, "Failed to write entropy tables %s", ZSTD_getErrorName(cSize)); + return 0; + } + /* If we expand and we aren't writing a header then emit uncompressed */ + if (!writeEntropy && cLitSize >= litSize) { + DEBUGLOG(5, "ZSTD_compressSubBlock_literal using raw literal because uncompressible"); + return ZSTD_noCompressLiterals(dst, dstSize, literals, litSize); + } + /* If we are writing headers then allow expansion that doesn't change our header size. */ + if (lhSize < (size_t)(3 + (cLitSize >= 1 KB) + (cLitSize >= 16 KB))) { + assert(cLitSize > litSize); + DEBUGLOG(5, "Literals expanded beyond allowed header size"); + return ZSTD_noCompressLiterals(dst, dstSize, literals, litSize); + } + DEBUGLOG(5, "ZSTD_compressSubBlock_literal (cSize=%zu)", cSize); + } + + /* Build header */ + switch(lhSize) + { + case 3: /* 2 - 2 - 10 - 10 */ + { U32 const lhc = hType + ((U32)(!singleStream) << 2) + ((U32)litSize<<4) + ((U32)cLitSize<<14); + MEM_writeLE24(ostart, lhc); + break; + } + case 4: /* 2 - 2 - 14 - 14 */ + { U32 const lhc = hType + (2 << 2) + ((U32)litSize<<4) + ((U32)cLitSize<<18); + MEM_writeLE32(ostart, lhc); + break; + } + case 5: /* 2 - 2 - 18 - 18 */ + { U32 const lhc = hType + (3 << 2) + ((U32)litSize<<4) + ((U32)cLitSize<<22); + MEM_writeLE32(ostart, lhc); + ostart[4] = (BYTE)(cLitSize >> 10); + break; + } + default: /* not possible : lhSize is {3,4,5} */ + assert(0); + } + *entropyWritten = 1; + DEBUGLOG(5, "Compressed literals: %u -> %u", (U32)litSize, (U32)(op-ostart)); + return (size_t)(op-ostart); +} + +static size_t +ZSTD_seqDecompressedSize(seqStore_t const* seqStore, + const seqDef* sequences, size_t nbSeqs, + size_t litSize, int lastSubBlock) +{ + size_t matchLengthSum = 0; + size_t litLengthSum = 0; + size_t n; + for (n=0; nllType, fseMetadata->ofType, and fseMetadata->mlType have + * symbol compression modes for the super-block. + * The first successfully compressed block will have these in its header. + * We set entropyWritten=1 when we succeed in compressing the sequences. + * The following sub-blocks will always have repeat mode. + * @return : compressed size of sequences section of a sub-block + * Or 0 if it is unable to compress + * Or error code. */ +static size_t +ZSTD_compressSubBlock_sequences(const ZSTD_fseCTables_t* fseTables, + const ZSTD_fseCTablesMetadata_t* fseMetadata, + const seqDef* sequences, size_t nbSeq, + const BYTE* llCode, const BYTE* mlCode, const BYTE* ofCode, + const ZSTD_CCtx_params* cctxParams, + void* dst, size_t dstCapacity, + const int bmi2, int writeEntropy, int* entropyWritten) +{ + const int longOffsets = cctxParams->cParams.windowLog > STREAM_ACCUMULATOR_MIN; + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = ostart + dstCapacity; + BYTE* op = ostart; + BYTE* seqHead; + + DEBUGLOG(5, "ZSTD_compressSubBlock_sequences (nbSeq=%zu, writeEntropy=%d, longOffsets=%d)", nbSeq, writeEntropy, longOffsets); + + *entropyWritten = 0; + /* Sequences Header */ + RETURN_ERROR_IF((oend-op) < 3 /*max nbSeq Size*/ + 1 /*seqHead*/, + dstSize_tooSmall, ""); + if (nbSeq < 128) + *op++ = (BYTE)nbSeq; + else if (nbSeq < LONGNBSEQ) + op[0] = (BYTE)((nbSeq>>8) + 0x80), op[1] = (BYTE)nbSeq, op+=2; + else + op[0]=0xFF, MEM_writeLE16(op+1, (U16)(nbSeq - LONGNBSEQ)), op+=3; + if (nbSeq==0) { + return (size_t)(op - ostart); + } + + /* seqHead : flags for FSE encoding type */ + seqHead = op++; + + DEBUGLOG(5, "ZSTD_compressSubBlock_sequences (seqHeadSize=%u)", (unsigned)(op-ostart)); + + if (writeEntropy) { + const U32 LLtype = fseMetadata->llType; + const U32 Offtype = fseMetadata->ofType; + const U32 MLtype = fseMetadata->mlType; + DEBUGLOG(5, "ZSTD_compressSubBlock_sequences (fseTablesSize=%zu)", fseMetadata->fseTablesSize); + *seqHead = (BYTE)((LLtype<<6) + (Offtype<<4) + (MLtype<<2)); + ZSTD_memcpy(op, fseMetadata->fseTablesBuffer, fseMetadata->fseTablesSize); + op += fseMetadata->fseTablesSize; + } else { + const U32 repeat = set_repeat; + *seqHead = (BYTE)((repeat<<6) + (repeat<<4) + (repeat<<2)); + } + + { size_t const bitstreamSize = ZSTD_encodeSequences( + op, (size_t)(oend - op), + fseTables->matchlengthCTable, mlCode, + fseTables->offcodeCTable, ofCode, + fseTables->litlengthCTable, llCode, + sequences, nbSeq, + longOffsets, bmi2); + FORWARD_IF_ERROR(bitstreamSize, "ZSTD_encodeSequences failed"); + op += bitstreamSize; + /* zstd versions <= 1.3.4 mistakenly report corruption when + * FSE_readNCount() receives a buffer < 4 bytes. + * Fixed by https://github.com/facebook/zstd/pull/1146. + * This can happen when the last set_compressed table present is 2 + * bytes and the bitstream is only one byte. + * In this exceedingly rare case, we will simply emit an uncompressed + * block, since it isn't worth optimizing. + */ +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + if (writeEntropy && fseMetadata->lastCountSize && fseMetadata->lastCountSize + bitstreamSize < 4) { + /* NCountSize >= 2 && bitstreamSize > 0 ==> lastCountSize == 3 */ + assert(fseMetadata->lastCountSize + bitstreamSize == 3); + DEBUGLOG(5, "Avoiding bug in zstd decoder in versions <= 1.3.4 by " + "emitting an uncompressed block."); + return 0; + } +#endif + DEBUGLOG(5, "ZSTD_compressSubBlock_sequences (bitstreamSize=%zu)", bitstreamSize); + } + + /* zstd versions <= 1.4.0 mistakenly report error when + * sequences section body size is less than 3 bytes. + * Fixed by https://github.com/facebook/zstd/pull/1664. + * This can happen when the previous sequences section block is compressed + * with rle mode and the current block's sequences section is compressed + * with repeat mode where sequences section body size can be 1 byte. + */ +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + if (op-seqHead < 4) { + DEBUGLOG(5, "Avoiding bug in zstd decoder in versions <= 1.4.0 by emitting " + "an uncompressed block when sequences are < 4 bytes"); + return 0; + } +#endif + + *entropyWritten = 1; + return (size_t)(op - ostart); +} + +/** ZSTD_compressSubBlock() : + * Compresses a single sub-block. + * @return : compressed size of the sub-block + * Or 0 if it failed to compress. */ +static size_t ZSTD_compressSubBlock(const ZSTD_entropyCTables_t* entropy, + const ZSTD_entropyCTablesMetadata_t* entropyMetadata, + const seqDef* sequences, size_t nbSeq, + const BYTE* literals, size_t litSize, + const BYTE* llCode, const BYTE* mlCode, const BYTE* ofCode, + const ZSTD_CCtx_params* cctxParams, + void* dst, size_t dstCapacity, + const int bmi2, + int writeLitEntropy, int writeSeqEntropy, + int* litEntropyWritten, int* seqEntropyWritten, + U32 lastBlock) +{ + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = ostart + dstCapacity; + BYTE* op = ostart + ZSTD_blockHeaderSize; + DEBUGLOG(5, "ZSTD_compressSubBlock (litSize=%zu, nbSeq=%zu, writeLitEntropy=%d, writeSeqEntropy=%d, lastBlock=%d)", + litSize, nbSeq, writeLitEntropy, writeSeqEntropy, lastBlock); + { size_t cLitSize = ZSTD_compressSubBlock_literal((const HUF_CElt*)entropy->huf.CTable, + &entropyMetadata->hufMetadata, literals, litSize, + op, (size_t)(oend-op), + bmi2, writeLitEntropy, litEntropyWritten); + FORWARD_IF_ERROR(cLitSize, "ZSTD_compressSubBlock_literal failed"); + if (cLitSize == 0) return 0; + op += cLitSize; + } + { size_t cSeqSize = ZSTD_compressSubBlock_sequences(&entropy->fse, + &entropyMetadata->fseMetadata, + sequences, nbSeq, + llCode, mlCode, ofCode, + cctxParams, + op, (size_t)(oend-op), + bmi2, writeSeqEntropy, seqEntropyWritten); + FORWARD_IF_ERROR(cSeqSize, "ZSTD_compressSubBlock_sequences failed"); + if (cSeqSize == 0) return 0; + op += cSeqSize; + } + /* Write block header */ + { size_t cSize = (size_t)(op-ostart) - ZSTD_blockHeaderSize; + U32 const cBlockHeader24 = lastBlock + (((U32)bt_compressed)<<1) + (U32)(cSize << 3); + MEM_writeLE24(ostart, cBlockHeader24); + } + return (size_t)(op-ostart); +} + +static size_t ZSTD_estimateSubBlockSize_literal(const BYTE* literals, size_t litSize, + const ZSTD_hufCTables_t* huf, + const ZSTD_hufCTablesMetadata_t* hufMetadata, + void* workspace, size_t wkspSize, + int writeEntropy) +{ + unsigned* const countWksp = (unsigned*)workspace; + unsigned maxSymbolValue = 255; + size_t literalSectionHeaderSize = 3; /* Use hard coded size of 3 bytes */ + + if (hufMetadata->hType == set_basic) return litSize; + else if (hufMetadata->hType == set_rle) return 1; + else if (hufMetadata->hType == set_compressed || hufMetadata->hType == set_repeat) { + size_t const largest = HIST_count_wksp (countWksp, &maxSymbolValue, (const BYTE*)literals, litSize, workspace, wkspSize); + if (ZSTD_isError(largest)) return litSize; + { size_t cLitSizeEstimate = HUF_estimateCompressedSize((const HUF_CElt*)huf->CTable, countWksp, maxSymbolValue); + if (writeEntropy) cLitSizeEstimate += hufMetadata->hufDesSize; + return cLitSizeEstimate + literalSectionHeaderSize; + } } + assert(0); /* impossible */ + return 0; +} + +static size_t ZSTD_estimateSubBlockSize_symbolType(symbolEncodingType_e type, + const BYTE* codeTable, unsigned maxCode, + size_t nbSeq, const FSE_CTable* fseCTable, + const U8* additionalBits, + short const* defaultNorm, U32 defaultNormLog, U32 defaultMax, + void* workspace, size_t wkspSize) +{ + unsigned* const countWksp = (unsigned*)workspace; + const BYTE* ctp = codeTable; + const BYTE* const ctStart = ctp; + const BYTE* const ctEnd = ctStart + nbSeq; + size_t cSymbolTypeSizeEstimateInBits = 0; + unsigned max = maxCode; + + HIST_countFast_wksp(countWksp, &max, codeTable, nbSeq, workspace, wkspSize); /* can't fail */ + if (type == set_basic) { + /* We selected this encoding type, so it must be valid. */ + assert(max <= defaultMax); + cSymbolTypeSizeEstimateInBits = max <= defaultMax + ? ZSTD_crossEntropyCost(defaultNorm, defaultNormLog, countWksp, max) + : ERROR(GENERIC); + } else if (type == set_rle) { + cSymbolTypeSizeEstimateInBits = 0; + } else if (type == set_compressed || type == set_repeat) { + cSymbolTypeSizeEstimateInBits = ZSTD_fseBitCost(fseCTable, countWksp, max); + } + if (ZSTD_isError(cSymbolTypeSizeEstimateInBits)) return nbSeq * 10; + while (ctp < ctEnd) { + if (additionalBits) cSymbolTypeSizeEstimateInBits += additionalBits[*ctp]; + else cSymbolTypeSizeEstimateInBits += *ctp; /* for offset, offset code is also the number of additional bits */ + ctp++; + } + return cSymbolTypeSizeEstimateInBits / 8; +} + +static size_t ZSTD_estimateSubBlockSize_sequences(const BYTE* ofCodeTable, + const BYTE* llCodeTable, + const BYTE* mlCodeTable, + size_t nbSeq, + const ZSTD_fseCTables_t* fseTables, + const ZSTD_fseCTablesMetadata_t* fseMetadata, + void* workspace, size_t wkspSize, + int writeEntropy) +{ + size_t const sequencesSectionHeaderSize = 3; /* Use hard coded size of 3 bytes */ + size_t cSeqSizeEstimate = 0; + if (nbSeq == 0) return sequencesSectionHeaderSize; + cSeqSizeEstimate += ZSTD_estimateSubBlockSize_symbolType(fseMetadata->ofType, ofCodeTable, MaxOff, + nbSeq, fseTables->offcodeCTable, NULL, + OF_defaultNorm, OF_defaultNormLog, DefaultMaxOff, + workspace, wkspSize); + cSeqSizeEstimate += ZSTD_estimateSubBlockSize_symbolType(fseMetadata->llType, llCodeTable, MaxLL, + nbSeq, fseTables->litlengthCTable, LL_bits, + LL_defaultNorm, LL_defaultNormLog, MaxLL, + workspace, wkspSize); + cSeqSizeEstimate += ZSTD_estimateSubBlockSize_symbolType(fseMetadata->mlType, mlCodeTable, MaxML, + nbSeq, fseTables->matchlengthCTable, ML_bits, + ML_defaultNorm, ML_defaultNormLog, MaxML, + workspace, wkspSize); + if (writeEntropy) cSeqSizeEstimate += fseMetadata->fseTablesSize; + return cSeqSizeEstimate + sequencesSectionHeaderSize; +} + +typedef struct { + size_t estLitSize; + size_t estBlockSize; +} EstimatedBlockSize; +static EstimatedBlockSize ZSTD_estimateSubBlockSize(const BYTE* literals, size_t litSize, + const BYTE* ofCodeTable, + const BYTE* llCodeTable, + const BYTE* mlCodeTable, + size_t nbSeq, + const ZSTD_entropyCTables_t* entropy, + const ZSTD_entropyCTablesMetadata_t* entropyMetadata, + void* workspace, size_t wkspSize, + int writeLitEntropy, int writeSeqEntropy) +{ + EstimatedBlockSize ebs; + ebs.estLitSize = ZSTD_estimateSubBlockSize_literal(literals, litSize, + &entropy->huf, &entropyMetadata->hufMetadata, + workspace, wkspSize, writeLitEntropy); + ebs.estBlockSize = ZSTD_estimateSubBlockSize_sequences(ofCodeTable, llCodeTable, mlCodeTable, + nbSeq, &entropy->fse, &entropyMetadata->fseMetadata, + workspace, wkspSize, writeSeqEntropy); + ebs.estBlockSize += ebs.estLitSize + ZSTD_blockHeaderSize; + return ebs; +} + +static int ZSTD_needSequenceEntropyTables(ZSTD_fseCTablesMetadata_t const* fseMetadata) +{ + if (fseMetadata->llType == set_compressed || fseMetadata->llType == set_rle) + return 1; + if (fseMetadata->mlType == set_compressed || fseMetadata->mlType == set_rle) + return 1; + if (fseMetadata->ofType == set_compressed || fseMetadata->ofType == set_rle) + return 1; + return 0; +} + +static size_t countLiterals(seqStore_t const* seqStore, const seqDef* sp, size_t seqCount) +{ + size_t n, total = 0; + assert(sp != NULL); + for (n=0; n %zu bytes", seqCount, (const void*)sp, total); + return total; +} + +#define BYTESCALE 256 + +static size_t sizeBlockSequences(const seqDef* sp, size_t nbSeqs, + size_t targetBudget, size_t avgLitCost, size_t avgSeqCost, + int firstSubBlock) +{ + size_t n, budget = 0, inSize=0; + /* entropy headers */ + size_t const headerSize = (size_t)firstSubBlock * 120 * BYTESCALE; /* generous estimate */ + assert(firstSubBlock==0 || firstSubBlock==1); + budget += headerSize; + + /* first sequence => at least one sequence*/ + budget += sp[0].litLength * avgLitCost + avgSeqCost; + if (budget > targetBudget) return 1; + inSize = sp[0].litLength + (sp[0].mlBase+MINMATCH); + + /* loop over sequences */ + for (n=1; n targetBudget) + /* though continue to expand until the sub-block is deemed compressible */ + && (budget < inSize * BYTESCALE) ) + break; + } + + return n; +} + +/** ZSTD_compressSubBlock_multi() : + * Breaks super-block into multiple sub-blocks and compresses them. + * Entropy will be written into the first block. + * The following blocks use repeat_mode to compress. + * Sub-blocks are all compressed, except the last one when beneficial. + * @return : compressed size of the super block (which features multiple ZSTD blocks) + * or 0 if it failed to compress. */ +static size_t ZSTD_compressSubBlock_multi(const seqStore_t* seqStorePtr, + const ZSTD_compressedBlockState_t* prevCBlock, + ZSTD_compressedBlockState_t* nextCBlock, + const ZSTD_entropyCTablesMetadata_t* entropyMetadata, + const ZSTD_CCtx_params* cctxParams, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const int bmi2, U32 lastBlock, + void* workspace, size_t wkspSize) +{ + const seqDef* const sstart = seqStorePtr->sequencesStart; + const seqDef* const send = seqStorePtr->sequences; + const seqDef* sp = sstart; /* tracks progresses within seqStorePtr->sequences */ + size_t const nbSeqs = (size_t)(send - sstart); + const BYTE* const lstart = seqStorePtr->litStart; + const BYTE* const lend = seqStorePtr->lit; + const BYTE* lp = lstart; + size_t const nbLiterals = (size_t)(lend - lstart); + BYTE const* ip = (BYTE const*)src; + BYTE const* const iend = ip + srcSize; + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = ostart + dstCapacity; + BYTE* op = ostart; + const BYTE* llCodePtr = seqStorePtr->llCode; + const BYTE* mlCodePtr = seqStorePtr->mlCode; + const BYTE* ofCodePtr = seqStorePtr->ofCode; + size_t const minTarget = ZSTD_TARGETCBLOCKSIZE_MIN; /* enforce minimum size, to reduce undesirable side effects */ + size_t const targetCBlockSize = MAX(minTarget, cctxParams->targetCBlockSize); + int writeLitEntropy = (entropyMetadata->hufMetadata.hType == set_compressed); + int writeSeqEntropy = 1; + + DEBUGLOG(5, "ZSTD_compressSubBlock_multi (srcSize=%u, litSize=%u, nbSeq=%u)", + (unsigned)srcSize, (unsigned)(lend-lstart), (unsigned)(send-sstart)); + + /* let's start by a general estimation for the full block */ + if (nbSeqs > 0) { + EstimatedBlockSize const ebs = + ZSTD_estimateSubBlockSize(lp, nbLiterals, + ofCodePtr, llCodePtr, mlCodePtr, nbSeqs, + &nextCBlock->entropy, entropyMetadata, + workspace, wkspSize, + writeLitEntropy, writeSeqEntropy); + /* quick estimation */ + size_t const avgLitCost = nbLiterals ? (ebs.estLitSize * BYTESCALE) / nbLiterals : BYTESCALE; + size_t const avgSeqCost = ((ebs.estBlockSize - ebs.estLitSize) * BYTESCALE) / nbSeqs; + const size_t nbSubBlocks = MAX((ebs.estBlockSize + (targetCBlockSize/2)) / targetCBlockSize, 1); + size_t n, avgBlockBudget, blockBudgetSupp=0; + avgBlockBudget = (ebs.estBlockSize * BYTESCALE) / nbSubBlocks; + DEBUGLOG(5, "estimated fullblock size=%u bytes ; avgLitCost=%.2f ; avgSeqCost=%.2f ; targetCBlockSize=%u, nbSubBlocks=%u ; avgBlockBudget=%.0f bytes", + (unsigned)ebs.estBlockSize, (double)avgLitCost/BYTESCALE, (double)avgSeqCost/BYTESCALE, + (unsigned)targetCBlockSize, (unsigned)nbSubBlocks, (double)avgBlockBudget/BYTESCALE); + /* simplification: if estimates states that the full superblock doesn't compress, just bail out immediately + * this will result in the production of a single uncompressed block covering @srcSize.*/ + if (ebs.estBlockSize > srcSize) return 0; + + /* compress and write sub-blocks */ + assert(nbSubBlocks>0); + for (n=0; n < nbSubBlocks-1; n++) { + /* determine nb of sequences for current sub-block + nbLiterals from next sequence */ + size_t const seqCount = sizeBlockSequences(sp, (size_t)(send-sp), + avgBlockBudget + blockBudgetSupp, avgLitCost, avgSeqCost, n==0); + /* if reached last sequence : break to last sub-block (simplification) */ + assert(seqCount <= (size_t)(send-sp)); + if (sp + seqCount == send) break; + assert(seqCount > 0); + /* compress sub-block */ + { int litEntropyWritten = 0; + int seqEntropyWritten = 0; + size_t litSize = countLiterals(seqStorePtr, sp, seqCount); + const size_t decompressedSize = + ZSTD_seqDecompressedSize(seqStorePtr, sp, seqCount, litSize, 0); + size_t const cSize = ZSTD_compressSubBlock(&nextCBlock->entropy, entropyMetadata, + sp, seqCount, + lp, litSize, + llCodePtr, mlCodePtr, ofCodePtr, + cctxParams, + op, (size_t)(oend-op), + bmi2, writeLitEntropy, writeSeqEntropy, + &litEntropyWritten, &seqEntropyWritten, + 0); + FORWARD_IF_ERROR(cSize, "ZSTD_compressSubBlock failed"); + + /* check compressibility, update state components */ + if (cSize > 0 && cSize < decompressedSize) { + DEBUGLOG(5, "Committed sub-block compressing %u bytes => %u bytes", + (unsigned)decompressedSize, (unsigned)cSize); + assert(ip + decompressedSize <= iend); + ip += decompressedSize; + lp += litSize; + op += cSize; + llCodePtr += seqCount; + mlCodePtr += seqCount; + ofCodePtr += seqCount; + /* Entropy only needs to be written once */ + if (litEntropyWritten) { + writeLitEntropy = 0; + } + if (seqEntropyWritten) { + writeSeqEntropy = 0; + } + sp += seqCount; + blockBudgetSupp = 0; + } } + /* otherwise : do not compress yet, coalesce current sub-block with following one */ + } + } /* if (nbSeqs > 0) */ + + /* write last block */ + DEBUGLOG(5, "Generate last sub-block: %u sequences remaining", (unsigned)(send - sp)); + { int litEntropyWritten = 0; + int seqEntropyWritten = 0; + size_t litSize = (size_t)(lend - lp); + size_t seqCount = (size_t)(send - sp); + const size_t decompressedSize = + ZSTD_seqDecompressedSize(seqStorePtr, sp, seqCount, litSize, 1); + size_t const cSize = ZSTD_compressSubBlock(&nextCBlock->entropy, entropyMetadata, + sp, seqCount, + lp, litSize, + llCodePtr, mlCodePtr, ofCodePtr, + cctxParams, + op, (size_t)(oend-op), + bmi2, writeLitEntropy, writeSeqEntropy, + &litEntropyWritten, &seqEntropyWritten, + lastBlock); + FORWARD_IF_ERROR(cSize, "ZSTD_compressSubBlock failed"); + + /* update pointers, the nb of literals borrowed from next sequence must be preserved */ + if (cSize > 0 && cSize < decompressedSize) { + DEBUGLOG(5, "Last sub-block compressed %u bytes => %u bytes", + (unsigned)decompressedSize, (unsigned)cSize); + assert(ip + decompressedSize <= iend); + ip += decompressedSize; + lp += litSize; + op += cSize; + llCodePtr += seqCount; + mlCodePtr += seqCount; + ofCodePtr += seqCount; + /* Entropy only needs to be written once */ + if (litEntropyWritten) { + writeLitEntropy = 0; + } + if (seqEntropyWritten) { + writeSeqEntropy = 0; + } + sp += seqCount; + } + } + + + if (writeLitEntropy) { + DEBUGLOG(5, "Literal entropy tables were never written"); + ZSTD_memcpy(&nextCBlock->entropy.huf, &prevCBlock->entropy.huf, sizeof(prevCBlock->entropy.huf)); + } + if (writeSeqEntropy && ZSTD_needSequenceEntropyTables(&entropyMetadata->fseMetadata)) { + /* If we haven't written our entropy tables, then we've violated our contract and + * must emit an uncompressed block. + */ + DEBUGLOG(5, "Sequence entropy tables were never written => cancel, emit an uncompressed block"); + return 0; + } + + if (ip < iend) { + /* some data left : last part of the block sent uncompressed */ + size_t const rSize = (size_t)((iend - ip)); + size_t const cSize = ZSTD_noCompressBlock(op, (size_t)(oend - op), ip, rSize, lastBlock); + DEBUGLOG(5, "Generate last uncompressed sub-block of %u bytes", (unsigned)(rSize)); + FORWARD_IF_ERROR(cSize, "ZSTD_noCompressBlock failed"); + assert(cSize != 0); + op += cSize; + /* We have to regenerate the repcodes because we've skipped some sequences */ + if (sp < send) { + const seqDef* seq; + repcodes_t rep; + ZSTD_memcpy(&rep, prevCBlock->rep, sizeof(rep)); + for (seq = sstart; seq < sp; ++seq) { + ZSTD_updateRep(rep.rep, seq->offBase, ZSTD_getSequenceLength(seqStorePtr, seq).litLength == 0); + } + ZSTD_memcpy(nextCBlock->rep, &rep, sizeof(rep)); + } + } + + DEBUGLOG(5, "ZSTD_compressSubBlock_multi compressed all subBlocks: total compressed size = %u", + (unsigned)(op-ostart)); + return (size_t)(op-ostart); +} + +size_t ZSTD_compressSuperBlock(ZSTD_CCtx* zc, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + unsigned lastBlock) +{ + ZSTD_entropyCTablesMetadata_t entropyMetadata; + + FORWARD_IF_ERROR(ZSTD_buildBlockEntropyStats(&zc->seqStore, + &zc->blockState.prevCBlock->entropy, + &zc->blockState.nextCBlock->entropy, + &zc->appliedParams, + &entropyMetadata, + zc->entropyWorkspace, ENTROPY_WORKSPACE_SIZE /* statically allocated in resetCCtx */), ""); + + return ZSTD_compressSubBlock_multi(&zc->seqStore, + zc->blockState.prevCBlock, + zc->blockState.nextCBlock, + &entropyMetadata, + &zc->appliedParams, + dst, dstCapacity, + src, srcSize, + zc->bmi2, lastBlock, + zc->entropyWorkspace, ENTROPY_WORKSPACE_SIZE /* statically allocated in resetCCtx */); +} +/**** ended inlining compress/zstd_compress_superblock.c ****/ +/**** start inlining compress/zstd_compress.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/*-************************************* +* Dependencies +***************************************/ +/**** skipping file: ../common/allocations.h ****/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../common/mem.h ****/ +/**** skipping file: hist.h ****/ +#define FSE_STATIC_LINKING_ONLY /* FSE_encodeSymbol */ +/**** skipping file: ../common/fse.h ****/ +/**** skipping file: ../common/huf.h ****/ +/**** skipping file: zstd_compress_internal.h ****/ +/**** skipping file: zstd_compress_sequences.h ****/ +/**** skipping file: zstd_compress_literals.h ****/ +/**** start inlining zstd_fast.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_FAST_H +#define ZSTD_FAST_H + +#if defined (__cplusplus) +extern "C" { +#endif + +/**** skipping file: ../common/mem.h ****/ +/**** skipping file: zstd_compress_internal.h ****/ + +void ZSTD_fillHashTable(ZSTD_matchState_t* ms, + void const* end, ZSTD_dictTableLoadMethod_e dtlm, + ZSTD_tableFillPurpose_e tfp); +size_t ZSTD_compressBlock_fast( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_fast_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_fast_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_FAST_H */ +/**** ended inlining zstd_fast.h ****/ +/**** start inlining zstd_double_fast.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_DOUBLE_FAST_H +#define ZSTD_DOUBLE_FAST_H + +#if defined (__cplusplus) +extern "C" { +#endif + +/**** skipping file: ../common/mem.h ****/ +/**** skipping file: zstd_compress_internal.h ****/ + +#ifndef ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR + +void ZSTD_fillDoubleHashTable(ZSTD_matchState_t* ms, + void const* end, ZSTD_dictTableLoadMethod_e dtlm, + ZSTD_tableFillPurpose_e tfp); + +size_t ZSTD_compressBlock_doubleFast( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_doubleFast_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_doubleFast_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + +#define ZSTD_COMPRESSBLOCK_DOUBLEFAST ZSTD_compressBlock_doubleFast +#define ZSTD_COMPRESSBLOCK_DOUBLEFAST_DICTMATCHSTATE ZSTD_compressBlock_doubleFast_dictMatchState +#define ZSTD_COMPRESSBLOCK_DOUBLEFAST_EXTDICT ZSTD_compressBlock_doubleFast_extDict +#else +#define ZSTD_COMPRESSBLOCK_DOUBLEFAST NULL +#define ZSTD_COMPRESSBLOCK_DOUBLEFAST_DICTMATCHSTATE NULL +#define ZSTD_COMPRESSBLOCK_DOUBLEFAST_EXTDICT NULL +#endif /* ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR */ + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_DOUBLE_FAST_H */ +/**** ended inlining zstd_double_fast.h ****/ +/**** start inlining zstd_lazy.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_LAZY_H +#define ZSTD_LAZY_H + +#if defined (__cplusplus) +extern "C" { +#endif + +/**** skipping file: zstd_compress_internal.h ****/ + +/** + * Dedicated Dictionary Search Structure bucket log. In the + * ZSTD_dedicatedDictSearch mode, the hashTable has + * 2 ** ZSTD_LAZY_DDSS_BUCKET_LOG entries in each bucket, rather than just + * one. + */ +#define ZSTD_LAZY_DDSS_BUCKET_LOG 2 + +#define ZSTD_ROW_HASH_TAG_BITS 8 /* nb bits to use for the tag */ + +#if !defined(ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_LAZY_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_LAZY2_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR) +U32 ZSTD_insertAndFindFirstIndex(ZSTD_matchState_t* ms, const BYTE* ip); +void ZSTD_row_update(ZSTD_matchState_t* const ms, const BYTE* ip); + +void ZSTD_dedicatedDictSearch_lazy_loadDictionary(ZSTD_matchState_t* ms, const BYTE* const ip); + +void ZSTD_preserveUnsortedMark (U32* const table, U32 const size, U32 const reducerValue); /*! used in ZSTD_reduceIndex(). preemptively increase value of ZSTD_DUBT_UNSORTED_MARK */ +#endif + +#ifndef ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_greedy( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_greedy_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_greedy_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_greedy_dictMatchState_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_greedy_dedicatedDictSearch( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_greedy_dedicatedDictSearch_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_greedy_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_greedy_extDict_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + +#define ZSTD_COMPRESSBLOCK_GREEDY ZSTD_compressBlock_greedy +#define ZSTD_COMPRESSBLOCK_GREEDY_ROW ZSTD_compressBlock_greedy_row +#define ZSTD_COMPRESSBLOCK_GREEDY_DICTMATCHSTATE ZSTD_compressBlock_greedy_dictMatchState +#define ZSTD_COMPRESSBLOCK_GREEDY_DICTMATCHSTATE_ROW ZSTD_compressBlock_greedy_dictMatchState_row +#define ZSTD_COMPRESSBLOCK_GREEDY_DEDICATEDDICTSEARCH ZSTD_compressBlock_greedy_dedicatedDictSearch +#define ZSTD_COMPRESSBLOCK_GREEDY_DEDICATEDDICTSEARCH_ROW ZSTD_compressBlock_greedy_dedicatedDictSearch_row +#define ZSTD_COMPRESSBLOCK_GREEDY_EXTDICT ZSTD_compressBlock_greedy_extDict +#define ZSTD_COMPRESSBLOCK_GREEDY_EXTDICT_ROW ZSTD_compressBlock_greedy_extDict_row +#else +#define ZSTD_COMPRESSBLOCK_GREEDY NULL +#define ZSTD_COMPRESSBLOCK_GREEDY_ROW NULL +#define ZSTD_COMPRESSBLOCK_GREEDY_DICTMATCHSTATE NULL +#define ZSTD_COMPRESSBLOCK_GREEDY_DICTMATCHSTATE_ROW NULL +#define ZSTD_COMPRESSBLOCK_GREEDY_DEDICATEDDICTSEARCH NULL +#define ZSTD_COMPRESSBLOCK_GREEDY_DEDICATEDDICTSEARCH_ROW NULL +#define ZSTD_COMPRESSBLOCK_GREEDY_EXTDICT NULL +#define ZSTD_COMPRESSBLOCK_GREEDY_EXTDICT_ROW NULL +#endif + +#ifndef ZSTD_EXCLUDE_LAZY_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_lazy( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy_dictMatchState_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy_dedicatedDictSearch( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy_dedicatedDictSearch_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy_extDict_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + +#define ZSTD_COMPRESSBLOCK_LAZY ZSTD_compressBlock_lazy +#define ZSTD_COMPRESSBLOCK_LAZY_ROW ZSTD_compressBlock_lazy_row +#define ZSTD_COMPRESSBLOCK_LAZY_DICTMATCHSTATE ZSTD_compressBlock_lazy_dictMatchState +#define ZSTD_COMPRESSBLOCK_LAZY_DICTMATCHSTATE_ROW ZSTD_compressBlock_lazy_dictMatchState_row +#define ZSTD_COMPRESSBLOCK_LAZY_DEDICATEDDICTSEARCH ZSTD_compressBlock_lazy_dedicatedDictSearch +#define ZSTD_COMPRESSBLOCK_LAZY_DEDICATEDDICTSEARCH_ROW ZSTD_compressBlock_lazy_dedicatedDictSearch_row +#define ZSTD_COMPRESSBLOCK_LAZY_EXTDICT ZSTD_compressBlock_lazy_extDict +#define ZSTD_COMPRESSBLOCK_LAZY_EXTDICT_ROW ZSTD_compressBlock_lazy_extDict_row +#else +#define ZSTD_COMPRESSBLOCK_LAZY NULL +#define ZSTD_COMPRESSBLOCK_LAZY_ROW NULL +#define ZSTD_COMPRESSBLOCK_LAZY_DICTMATCHSTATE NULL +#define ZSTD_COMPRESSBLOCK_LAZY_DICTMATCHSTATE_ROW NULL +#define ZSTD_COMPRESSBLOCK_LAZY_DEDICATEDDICTSEARCH NULL +#define ZSTD_COMPRESSBLOCK_LAZY_DEDICATEDDICTSEARCH_ROW NULL +#define ZSTD_COMPRESSBLOCK_LAZY_EXTDICT NULL +#define ZSTD_COMPRESSBLOCK_LAZY_EXTDICT_ROW NULL +#endif + +#ifndef ZSTD_EXCLUDE_LAZY2_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_lazy2( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy2_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy2_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy2_dictMatchState_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy2_dedicatedDictSearch( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy2_dedicatedDictSearch_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy2_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_lazy2_extDict_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + +#define ZSTD_COMPRESSBLOCK_LAZY2 ZSTD_compressBlock_lazy2 +#define ZSTD_COMPRESSBLOCK_LAZY2_ROW ZSTD_compressBlock_lazy2_row +#define ZSTD_COMPRESSBLOCK_LAZY2_DICTMATCHSTATE ZSTD_compressBlock_lazy2_dictMatchState +#define ZSTD_COMPRESSBLOCK_LAZY2_DICTMATCHSTATE_ROW ZSTD_compressBlock_lazy2_dictMatchState_row +#define ZSTD_COMPRESSBLOCK_LAZY2_DEDICATEDDICTSEARCH ZSTD_compressBlock_lazy2_dedicatedDictSearch +#define ZSTD_COMPRESSBLOCK_LAZY2_DEDICATEDDICTSEARCH_ROW ZSTD_compressBlock_lazy2_dedicatedDictSearch_row +#define ZSTD_COMPRESSBLOCK_LAZY2_EXTDICT ZSTD_compressBlock_lazy2_extDict +#define ZSTD_COMPRESSBLOCK_LAZY2_EXTDICT_ROW ZSTD_compressBlock_lazy2_extDict_row +#else +#define ZSTD_COMPRESSBLOCK_LAZY2 NULL +#define ZSTD_COMPRESSBLOCK_LAZY2_ROW NULL +#define ZSTD_COMPRESSBLOCK_LAZY2_DICTMATCHSTATE NULL +#define ZSTD_COMPRESSBLOCK_LAZY2_DICTMATCHSTATE_ROW NULL +#define ZSTD_COMPRESSBLOCK_LAZY2_DEDICATEDDICTSEARCH NULL +#define ZSTD_COMPRESSBLOCK_LAZY2_DEDICATEDDICTSEARCH_ROW NULL +#define ZSTD_COMPRESSBLOCK_LAZY2_EXTDICT NULL +#define ZSTD_COMPRESSBLOCK_LAZY2_EXTDICT_ROW NULL +#endif + +#ifndef ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_btlazy2( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_btlazy2_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_btlazy2_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + +#define ZSTD_COMPRESSBLOCK_BTLAZY2 ZSTD_compressBlock_btlazy2 +#define ZSTD_COMPRESSBLOCK_BTLAZY2_DICTMATCHSTATE ZSTD_compressBlock_btlazy2_dictMatchState +#define ZSTD_COMPRESSBLOCK_BTLAZY2_EXTDICT ZSTD_compressBlock_btlazy2_extDict +#else +#define ZSTD_COMPRESSBLOCK_BTLAZY2 NULL +#define ZSTD_COMPRESSBLOCK_BTLAZY2_DICTMATCHSTATE NULL +#define ZSTD_COMPRESSBLOCK_BTLAZY2_EXTDICT NULL +#endif + + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_LAZY_H */ +/**** ended inlining zstd_lazy.h ****/ +/**** start inlining zstd_opt.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_OPT_H +#define ZSTD_OPT_H + +#if defined (__cplusplus) +extern "C" { +#endif + +/**** skipping file: zstd_compress_internal.h ****/ + +#if !defined(ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTOPT_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTULTRA_BLOCK_COMPRESSOR) +/* used in ZSTD_loadDictionaryContent() */ +void ZSTD_updateTree(ZSTD_matchState_t* ms, const BYTE* ip, const BYTE* iend); +#endif + +#ifndef ZSTD_EXCLUDE_BTOPT_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_btopt( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_btopt_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_btopt_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + +#define ZSTD_COMPRESSBLOCK_BTOPT ZSTD_compressBlock_btopt +#define ZSTD_COMPRESSBLOCK_BTOPT_DICTMATCHSTATE ZSTD_compressBlock_btopt_dictMatchState +#define ZSTD_COMPRESSBLOCK_BTOPT_EXTDICT ZSTD_compressBlock_btopt_extDict +#else +#define ZSTD_COMPRESSBLOCK_BTOPT NULL +#define ZSTD_COMPRESSBLOCK_BTOPT_DICTMATCHSTATE NULL +#define ZSTD_COMPRESSBLOCK_BTOPT_EXTDICT NULL +#endif + +#ifndef ZSTD_EXCLUDE_BTULTRA_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_btultra( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_btultra_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); +size_t ZSTD_compressBlock_btultra_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + + /* note : no btultra2 variant for extDict nor dictMatchState, + * because btultra2 is not meant to work with dictionaries + * and is only specific for the first block (no prefix) */ +size_t ZSTD_compressBlock_btultra2( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize); + +#define ZSTD_COMPRESSBLOCK_BTULTRA ZSTD_compressBlock_btultra +#define ZSTD_COMPRESSBLOCK_BTULTRA_DICTMATCHSTATE ZSTD_compressBlock_btultra_dictMatchState +#define ZSTD_COMPRESSBLOCK_BTULTRA_EXTDICT ZSTD_compressBlock_btultra_extDict +#define ZSTD_COMPRESSBLOCK_BTULTRA2 ZSTD_compressBlock_btultra2 +#else +#define ZSTD_COMPRESSBLOCK_BTULTRA NULL +#define ZSTD_COMPRESSBLOCK_BTULTRA_DICTMATCHSTATE NULL +#define ZSTD_COMPRESSBLOCK_BTULTRA_EXTDICT NULL +#define ZSTD_COMPRESSBLOCK_BTULTRA2 NULL +#endif + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_OPT_H */ +/**** ended inlining zstd_opt.h ****/ +/**** start inlining zstd_ldm.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_LDM_H +#define ZSTD_LDM_H + +#if defined (__cplusplus) +extern "C" { +#endif + +/**** skipping file: zstd_compress_internal.h ****/ +/**** skipping file: ../zstd.h ****/ + +/*-************************************* +* Long distance matching +***************************************/ + +#define ZSTD_LDM_DEFAULT_WINDOW_LOG ZSTD_WINDOWLOG_LIMIT_DEFAULT + +void ZSTD_ldm_fillHashTable( + ldmState_t* state, const BYTE* ip, + const BYTE* iend, ldmParams_t const* params); + +/** + * ZSTD_ldm_generateSequences(): + * + * Generates the sequences using the long distance match finder. + * Generates long range matching sequences in `sequences`, which parse a prefix + * of the source. `sequences` must be large enough to store every sequence, + * which can be checked with `ZSTD_ldm_getMaxNbSeq()`. + * @returns 0 or an error code. + * + * NOTE: The user must have called ZSTD_window_update() for all of the input + * they have, even if they pass it to ZSTD_ldm_generateSequences() in chunks. + * NOTE: This function returns an error if it runs out of space to store + * sequences. + */ +size_t ZSTD_ldm_generateSequences( + ldmState_t* ldms, rawSeqStore_t* sequences, + ldmParams_t const* params, void const* src, size_t srcSize); + +/** + * ZSTD_ldm_blockCompress(): + * + * Compresses a block using the predefined sequences, along with a secondary + * block compressor. The literals section of every sequence is passed to the + * secondary block compressor, and those sequences are interspersed with the + * predefined sequences. Returns the length of the last literals. + * Updates `rawSeqStore.pos` to indicate how many sequences have been consumed. + * `rawSeqStore.seq` may also be updated to split the last sequence between two + * blocks. + * @return The length of the last literals. + * + * NOTE: The source must be at most the maximum block size, but the predefined + * sequences can be any size, and may be longer than the block. In the case that + * they are longer than the block, the last sequences may need to be split into + * two. We handle that case correctly, and update `rawSeqStore` appropriately. + * NOTE: This function does not return any errors. + */ +size_t ZSTD_ldm_blockCompress(rawSeqStore_t* rawSeqStore, + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + ZSTD_paramSwitch_e useRowMatchFinder, + void const* src, size_t srcSize); + +/** + * ZSTD_ldm_skipSequences(): + * + * Skip past `srcSize` bytes worth of sequences in `rawSeqStore`. + * Avoids emitting matches less than `minMatch` bytes. + * Must be called for data that is not passed to ZSTD_ldm_blockCompress(). + */ +void ZSTD_ldm_skipSequences(rawSeqStore_t* rawSeqStore, size_t srcSize, + U32 const minMatch); + +/* ZSTD_ldm_skipRawSeqStoreBytes(): + * Moves forward in rawSeqStore by nbBytes, updating fields 'pos' and 'posInSequence'. + * Not to be used in conjunction with ZSTD_ldm_skipSequences(). + * Must be called for data with is not passed to ZSTD_ldm_blockCompress(). + */ +void ZSTD_ldm_skipRawSeqStoreBytes(rawSeqStore_t* rawSeqStore, size_t nbBytes); + +/** ZSTD_ldm_getTableSize() : + * Estimate the space needed for long distance matching tables or 0 if LDM is + * disabled. + */ +size_t ZSTD_ldm_getTableSize(ldmParams_t params); + +/** ZSTD_ldm_getSeqSpace() : + * Return an upper bound on the number of sequences that can be produced by + * the long distance matcher, or 0 if LDM is disabled. + */ +size_t ZSTD_ldm_getMaxNbSeq(ldmParams_t params, size_t maxChunkSize); + +/** ZSTD_ldm_adjustParameters() : + * If the params->hashRateLog is not set, set it to its default value based on + * windowLog and params->hashLog. + * + * Ensures that params->bucketSizeLog is <= params->hashLog (setting it to + * params->hashLog if it is not). + * + * Ensures that the minMatchLength >= targetLength during optimal parsing. + */ +void ZSTD_ldm_adjustParameters(ldmParams_t* params, + ZSTD_compressionParameters const* cParams); + +#if defined (__cplusplus) +} +#endif + +#endif /* ZSTD_FAST_H */ +/**** ended inlining zstd_ldm.h ****/ +/**** skipping file: zstd_compress_superblock.h ****/ +/**** skipping file: ../common/bits.h ****/ + +/* *************************************************************** +* Tuning parameters +*****************************************************************/ +/*! + * COMPRESS_HEAPMODE : + * Select how default decompression function ZSTD_compress() allocates its context, + * on stack (0, default), or into heap (1). + * Note that functions with explicit context such as ZSTD_compressCCtx() are unaffected. + */ +#ifndef ZSTD_COMPRESS_HEAPMODE +# define ZSTD_COMPRESS_HEAPMODE 0 +#endif + +/*! + * ZSTD_HASHLOG3_MAX : + * Maximum size of the hash table dedicated to find 3-bytes matches, + * in log format, aka 17 => 1 << 17 == 128Ki positions. + * This structure is only used in zstd_opt. + * Since allocation is centralized for all strategies, it has to be known here. + * The actual (selected) size of the hash table is then stored in ZSTD_matchState_t.hashLog3, + * so that zstd_opt.c doesn't need to know about this constant. + */ +#ifndef ZSTD_HASHLOG3_MAX +# define ZSTD_HASHLOG3_MAX 17 +#endif + +/*-************************************* +* Helper functions +***************************************/ +/* ZSTD_compressBound() + * Note that the result from this function is only valid for + * the one-pass compression functions. + * When employing the streaming mode, + * if flushes are frequently altering the size of blocks, + * the overhead from block headers can make the compressed data larger + * than the return value of ZSTD_compressBound(). + */ +size_t ZSTD_compressBound(size_t srcSize) { + size_t const r = ZSTD_COMPRESSBOUND(srcSize); + if (r==0) return ERROR(srcSize_wrong); + return r; +} + + +/*-************************************* +* Context memory management +***************************************/ +struct ZSTD_CDict_s { + const void* dictContent; + size_t dictContentSize; + ZSTD_dictContentType_e dictContentType; /* The dictContentType the CDict was created with */ + U32* entropyWorkspace; /* entropy workspace of HUF_WORKSPACE_SIZE bytes */ + ZSTD_cwksp workspace; + ZSTD_matchState_t matchState; + ZSTD_compressedBlockState_t cBlockState; + ZSTD_customMem customMem; + U32 dictID; + int compressionLevel; /* 0 indicates that advanced API was used to select CDict params */ + ZSTD_paramSwitch_e useRowMatchFinder; /* Indicates whether the CDict was created with params that would use + * row-based matchfinder. Unless the cdict is reloaded, we will use + * the same greedy/lazy matchfinder at compression time. + */ +}; /* typedef'd to ZSTD_CDict within "zstd.h" */ + +ZSTD_CCtx* ZSTD_createCCtx(void) +{ + return ZSTD_createCCtx_advanced(ZSTD_defaultCMem); +} + +static void ZSTD_initCCtx(ZSTD_CCtx* cctx, ZSTD_customMem memManager) +{ + assert(cctx != NULL); + ZSTD_memset(cctx, 0, sizeof(*cctx)); + cctx->customMem = memManager; + cctx->bmi2 = ZSTD_cpuSupportsBmi2(); + { size_t const err = ZSTD_CCtx_reset(cctx, ZSTD_reset_parameters); + assert(!ZSTD_isError(err)); + (void)err; + } +} + +ZSTD_CCtx* ZSTD_createCCtx_advanced(ZSTD_customMem customMem) +{ + ZSTD_STATIC_ASSERT(zcss_init==0); + ZSTD_STATIC_ASSERT(ZSTD_CONTENTSIZE_UNKNOWN==(0ULL - 1)); + if ((!customMem.customAlloc) ^ (!customMem.customFree)) return NULL; + { ZSTD_CCtx* const cctx = (ZSTD_CCtx*)ZSTD_customMalloc(sizeof(ZSTD_CCtx), customMem); + if (!cctx) return NULL; + ZSTD_initCCtx(cctx, customMem); + return cctx; + } +} + +ZSTD_CCtx* ZSTD_initStaticCCtx(void* workspace, size_t workspaceSize) +{ + ZSTD_cwksp ws; + ZSTD_CCtx* cctx; + if (workspaceSize <= sizeof(ZSTD_CCtx)) return NULL; /* minimum size */ + if ((size_t)workspace & 7) return NULL; /* must be 8-aligned */ + ZSTD_cwksp_init(&ws, workspace, workspaceSize, ZSTD_cwksp_static_alloc); + + cctx = (ZSTD_CCtx*)ZSTD_cwksp_reserve_object(&ws, sizeof(ZSTD_CCtx)); + if (cctx == NULL) return NULL; + + ZSTD_memset(cctx, 0, sizeof(ZSTD_CCtx)); + ZSTD_cwksp_move(&cctx->workspace, &ws); + cctx->staticSize = workspaceSize; + + /* statically sized space. entropyWorkspace never moves (but prev/next block swap places) */ + if (!ZSTD_cwksp_check_available(&cctx->workspace, ENTROPY_WORKSPACE_SIZE + 2 * sizeof(ZSTD_compressedBlockState_t))) return NULL; + cctx->blockState.prevCBlock = (ZSTD_compressedBlockState_t*)ZSTD_cwksp_reserve_object(&cctx->workspace, sizeof(ZSTD_compressedBlockState_t)); + cctx->blockState.nextCBlock = (ZSTD_compressedBlockState_t*)ZSTD_cwksp_reserve_object(&cctx->workspace, sizeof(ZSTD_compressedBlockState_t)); + cctx->entropyWorkspace = (U32*)ZSTD_cwksp_reserve_object(&cctx->workspace, ENTROPY_WORKSPACE_SIZE); + cctx->bmi2 = ZSTD_cpuid_bmi2(ZSTD_cpuid()); + return cctx; +} + +/** + * Clears and frees all of the dictionaries in the CCtx. + */ +static void ZSTD_clearAllDicts(ZSTD_CCtx* cctx) +{ + ZSTD_customFree(cctx->localDict.dictBuffer, cctx->customMem); + ZSTD_freeCDict(cctx->localDict.cdict); + ZSTD_memset(&cctx->localDict, 0, sizeof(cctx->localDict)); + ZSTD_memset(&cctx->prefixDict, 0, sizeof(cctx->prefixDict)); + cctx->cdict = NULL; +} + +static size_t ZSTD_sizeof_localDict(ZSTD_localDict dict) +{ + size_t const bufferSize = dict.dictBuffer != NULL ? dict.dictSize : 0; + size_t const cdictSize = ZSTD_sizeof_CDict(dict.cdict); + return bufferSize + cdictSize; +} + +static void ZSTD_freeCCtxContent(ZSTD_CCtx* cctx) +{ + assert(cctx != NULL); + assert(cctx->staticSize == 0); + ZSTD_clearAllDicts(cctx); +#ifdef ZSTD_MULTITHREAD + ZSTDMT_freeCCtx(cctx->mtctx); cctx->mtctx = NULL; +#endif + ZSTD_cwksp_free(&cctx->workspace, cctx->customMem); +} + +size_t ZSTD_freeCCtx(ZSTD_CCtx* cctx) +{ + DEBUGLOG(3, "ZSTD_freeCCtx (address: %p)", (void*)cctx); + if (cctx==NULL) return 0; /* support free on NULL */ + RETURN_ERROR_IF(cctx->staticSize, memory_allocation, + "not compatible with static CCtx"); + { int cctxInWorkspace = ZSTD_cwksp_owns_buffer(&cctx->workspace, cctx); + ZSTD_freeCCtxContent(cctx); + if (!cctxInWorkspace) ZSTD_customFree(cctx, cctx->customMem); + } + return 0; +} + + +static size_t ZSTD_sizeof_mtctx(const ZSTD_CCtx* cctx) +{ +#ifdef ZSTD_MULTITHREAD + return ZSTDMT_sizeof_CCtx(cctx->mtctx); +#else + (void)cctx; + return 0; +#endif +} + + +size_t ZSTD_sizeof_CCtx(const ZSTD_CCtx* cctx) +{ + if (cctx==NULL) return 0; /* support sizeof on NULL */ + /* cctx may be in the workspace */ + return (cctx->workspace.workspace == cctx ? 0 : sizeof(*cctx)) + + ZSTD_cwksp_sizeof(&cctx->workspace) + + ZSTD_sizeof_localDict(cctx->localDict) + + ZSTD_sizeof_mtctx(cctx); +} + +size_t ZSTD_sizeof_CStream(const ZSTD_CStream* zcs) +{ + return ZSTD_sizeof_CCtx(zcs); /* same object */ +} + +/* private API call, for dictBuilder only */ +const seqStore_t* ZSTD_getSeqStore(const ZSTD_CCtx* ctx) { return &(ctx->seqStore); } + +/* Returns true if the strategy supports using a row based matchfinder */ +static int ZSTD_rowMatchFinderSupported(const ZSTD_strategy strategy) { + return (strategy >= ZSTD_greedy && strategy <= ZSTD_lazy2); +} + +/* Returns true if the strategy and useRowMatchFinder mode indicate that we will use the row based matchfinder + * for this compression. + */ +static int ZSTD_rowMatchFinderUsed(const ZSTD_strategy strategy, const ZSTD_paramSwitch_e mode) { + assert(mode != ZSTD_ps_auto); + return ZSTD_rowMatchFinderSupported(strategy) && (mode == ZSTD_ps_enable); +} + +/* Returns row matchfinder usage given an initial mode and cParams */ +static ZSTD_paramSwitch_e ZSTD_resolveRowMatchFinderMode(ZSTD_paramSwitch_e mode, + const ZSTD_compressionParameters* const cParams) { +#if defined(ZSTD_ARCH_X86_SSE2) || defined(ZSTD_ARCH_ARM_NEON) + int const kHasSIMD128 = 1; +#else + int const kHasSIMD128 = 0; +#endif + if (mode != ZSTD_ps_auto) return mode; /* if requested enabled, but no SIMD, we still will use row matchfinder */ + mode = ZSTD_ps_disable; + if (!ZSTD_rowMatchFinderSupported(cParams->strategy)) return mode; + if (kHasSIMD128) { + if (cParams->windowLog > 14) mode = ZSTD_ps_enable; + } else { + if (cParams->windowLog > 17) mode = ZSTD_ps_enable; + } + return mode; +} + +/* Returns block splitter usage (generally speaking, when using slower/stronger compression modes) */ +static ZSTD_paramSwitch_e ZSTD_resolveBlockSplitterMode(ZSTD_paramSwitch_e mode, + const ZSTD_compressionParameters* const cParams) { + if (mode != ZSTD_ps_auto) return mode; + return (cParams->strategy >= ZSTD_btopt && cParams->windowLog >= 17) ? ZSTD_ps_enable : ZSTD_ps_disable; +} + +/* Returns 1 if the arguments indicate that we should allocate a chainTable, 0 otherwise */ +static int ZSTD_allocateChainTable(const ZSTD_strategy strategy, + const ZSTD_paramSwitch_e useRowMatchFinder, + const U32 forDDSDict) { + assert(useRowMatchFinder != ZSTD_ps_auto); + /* We always should allocate a chaintable if we are allocating a matchstate for a DDS dictionary matchstate. + * We do not allocate a chaintable if we are using ZSTD_fast, or are using the row-based matchfinder. + */ + return forDDSDict || ((strategy != ZSTD_fast) && !ZSTD_rowMatchFinderUsed(strategy, useRowMatchFinder)); +} + +/* Returns ZSTD_ps_enable if compression parameters are such that we should + * enable long distance matching (wlog >= 27, strategy >= btopt). + * Returns ZSTD_ps_disable otherwise. + */ +static ZSTD_paramSwitch_e ZSTD_resolveEnableLdm(ZSTD_paramSwitch_e mode, + const ZSTD_compressionParameters* const cParams) { + if (mode != ZSTD_ps_auto) return mode; + return (cParams->strategy >= ZSTD_btopt && cParams->windowLog >= 27) ? ZSTD_ps_enable : ZSTD_ps_disable; +} + +static int ZSTD_resolveExternalSequenceValidation(int mode) { + return mode; +} + +/* Resolves maxBlockSize to the default if no value is present. */ +static size_t ZSTD_resolveMaxBlockSize(size_t maxBlockSize) { + if (maxBlockSize == 0) { + return ZSTD_BLOCKSIZE_MAX; + } else { + return maxBlockSize; + } +} + +static ZSTD_paramSwitch_e ZSTD_resolveExternalRepcodeSearch(ZSTD_paramSwitch_e value, int cLevel) { + if (value != ZSTD_ps_auto) return value; + if (cLevel < 10) { + return ZSTD_ps_disable; + } else { + return ZSTD_ps_enable; + } +} + +/* Returns 1 if compression parameters are such that CDict hashtable and chaintable indices are tagged. + * If so, the tags need to be removed in ZSTD_resetCCtx_byCopyingCDict. */ +static int ZSTD_CDictIndicesAreTagged(const ZSTD_compressionParameters* const cParams) { + return cParams->strategy == ZSTD_fast || cParams->strategy == ZSTD_dfast; +} + +static ZSTD_CCtx_params ZSTD_makeCCtxParamsFromCParams( + ZSTD_compressionParameters cParams) +{ + ZSTD_CCtx_params cctxParams; + /* should not matter, as all cParams are presumed properly defined */ + ZSTD_CCtxParams_init(&cctxParams, ZSTD_CLEVEL_DEFAULT); + cctxParams.cParams = cParams; + + /* Adjust advanced params according to cParams */ + cctxParams.ldmParams.enableLdm = ZSTD_resolveEnableLdm(cctxParams.ldmParams.enableLdm, &cParams); + if (cctxParams.ldmParams.enableLdm == ZSTD_ps_enable) { + ZSTD_ldm_adjustParameters(&cctxParams.ldmParams, &cParams); + assert(cctxParams.ldmParams.hashLog >= cctxParams.ldmParams.bucketSizeLog); + assert(cctxParams.ldmParams.hashRateLog < 32); + } + cctxParams.useBlockSplitter = ZSTD_resolveBlockSplitterMode(cctxParams.useBlockSplitter, &cParams); + cctxParams.useRowMatchFinder = ZSTD_resolveRowMatchFinderMode(cctxParams.useRowMatchFinder, &cParams); + cctxParams.validateSequences = ZSTD_resolveExternalSequenceValidation(cctxParams.validateSequences); + cctxParams.maxBlockSize = ZSTD_resolveMaxBlockSize(cctxParams.maxBlockSize); + cctxParams.searchForExternalRepcodes = ZSTD_resolveExternalRepcodeSearch(cctxParams.searchForExternalRepcodes, + cctxParams.compressionLevel); + assert(!ZSTD_checkCParams(cParams)); + return cctxParams; +} + +static ZSTD_CCtx_params* ZSTD_createCCtxParams_advanced( + ZSTD_customMem customMem) +{ + ZSTD_CCtx_params* params; + if ((!customMem.customAlloc) ^ (!customMem.customFree)) return NULL; + params = (ZSTD_CCtx_params*)ZSTD_customCalloc( + sizeof(ZSTD_CCtx_params), customMem); + if (!params) { return NULL; } + ZSTD_CCtxParams_init(params, ZSTD_CLEVEL_DEFAULT); + params->customMem = customMem; + return params; +} + +ZSTD_CCtx_params* ZSTD_createCCtxParams(void) +{ + return ZSTD_createCCtxParams_advanced(ZSTD_defaultCMem); +} + +size_t ZSTD_freeCCtxParams(ZSTD_CCtx_params* params) +{ + if (params == NULL) { return 0; } + ZSTD_customFree(params, params->customMem); + return 0; +} + +size_t ZSTD_CCtxParams_reset(ZSTD_CCtx_params* params) +{ + return ZSTD_CCtxParams_init(params, ZSTD_CLEVEL_DEFAULT); +} + +size_t ZSTD_CCtxParams_init(ZSTD_CCtx_params* cctxParams, int compressionLevel) { + RETURN_ERROR_IF(!cctxParams, GENERIC, "NULL pointer!"); + ZSTD_memset(cctxParams, 0, sizeof(*cctxParams)); + cctxParams->compressionLevel = compressionLevel; + cctxParams->fParams.contentSizeFlag = 1; + return 0; +} + +#define ZSTD_NO_CLEVEL 0 + +/** + * Initializes `cctxParams` from `params` and `compressionLevel`. + * @param compressionLevel If params are derived from a compression level then that compression level, otherwise ZSTD_NO_CLEVEL. + */ +static void +ZSTD_CCtxParams_init_internal(ZSTD_CCtx_params* cctxParams, + const ZSTD_parameters* params, + int compressionLevel) +{ + assert(!ZSTD_checkCParams(params->cParams)); + ZSTD_memset(cctxParams, 0, sizeof(*cctxParams)); + cctxParams->cParams = params->cParams; + cctxParams->fParams = params->fParams; + /* Should not matter, as all cParams are presumed properly defined. + * But, set it for tracing anyway. + */ + cctxParams->compressionLevel = compressionLevel; + cctxParams->useRowMatchFinder = ZSTD_resolveRowMatchFinderMode(cctxParams->useRowMatchFinder, ¶ms->cParams); + cctxParams->useBlockSplitter = ZSTD_resolveBlockSplitterMode(cctxParams->useBlockSplitter, ¶ms->cParams); + cctxParams->ldmParams.enableLdm = ZSTD_resolveEnableLdm(cctxParams->ldmParams.enableLdm, ¶ms->cParams); + cctxParams->validateSequences = ZSTD_resolveExternalSequenceValidation(cctxParams->validateSequences); + cctxParams->maxBlockSize = ZSTD_resolveMaxBlockSize(cctxParams->maxBlockSize); + cctxParams->searchForExternalRepcodes = ZSTD_resolveExternalRepcodeSearch(cctxParams->searchForExternalRepcodes, compressionLevel); + DEBUGLOG(4, "ZSTD_CCtxParams_init_internal: useRowMatchFinder=%d, useBlockSplitter=%d ldm=%d", + cctxParams->useRowMatchFinder, cctxParams->useBlockSplitter, cctxParams->ldmParams.enableLdm); +} + +size_t ZSTD_CCtxParams_init_advanced(ZSTD_CCtx_params* cctxParams, ZSTD_parameters params) +{ + RETURN_ERROR_IF(!cctxParams, GENERIC, "NULL pointer!"); + FORWARD_IF_ERROR( ZSTD_checkCParams(params.cParams) , ""); + ZSTD_CCtxParams_init_internal(cctxParams, ¶ms, ZSTD_NO_CLEVEL); + return 0; +} + +/** + * Sets cctxParams' cParams and fParams from params, but otherwise leaves them alone. + * @param params Validated zstd parameters. + */ +static void ZSTD_CCtxParams_setZstdParams( + ZSTD_CCtx_params* cctxParams, const ZSTD_parameters* params) +{ + assert(!ZSTD_checkCParams(params->cParams)); + cctxParams->cParams = params->cParams; + cctxParams->fParams = params->fParams; + /* Should not matter, as all cParams are presumed properly defined. + * But, set it for tracing anyway. + */ + cctxParams->compressionLevel = ZSTD_NO_CLEVEL; +} + +ZSTD_bounds ZSTD_cParam_getBounds(ZSTD_cParameter param) +{ + ZSTD_bounds bounds = { 0, 0, 0 }; + + switch(param) + { + case ZSTD_c_compressionLevel: + bounds.lowerBound = ZSTD_minCLevel(); + bounds.upperBound = ZSTD_maxCLevel(); + return bounds; + + case ZSTD_c_windowLog: + bounds.lowerBound = ZSTD_WINDOWLOG_MIN; + bounds.upperBound = ZSTD_WINDOWLOG_MAX; + return bounds; + + case ZSTD_c_hashLog: + bounds.lowerBound = ZSTD_HASHLOG_MIN; + bounds.upperBound = ZSTD_HASHLOG_MAX; + return bounds; + + case ZSTD_c_chainLog: + bounds.lowerBound = ZSTD_CHAINLOG_MIN; + bounds.upperBound = ZSTD_CHAINLOG_MAX; + return bounds; + + case ZSTD_c_searchLog: + bounds.lowerBound = ZSTD_SEARCHLOG_MIN; + bounds.upperBound = ZSTD_SEARCHLOG_MAX; + return bounds; + + case ZSTD_c_minMatch: + bounds.lowerBound = ZSTD_MINMATCH_MIN; + bounds.upperBound = ZSTD_MINMATCH_MAX; + return bounds; + + case ZSTD_c_targetLength: + bounds.lowerBound = ZSTD_TARGETLENGTH_MIN; + bounds.upperBound = ZSTD_TARGETLENGTH_MAX; + return bounds; + + case ZSTD_c_strategy: + bounds.lowerBound = ZSTD_STRATEGY_MIN; + bounds.upperBound = ZSTD_STRATEGY_MAX; + return bounds; + + case ZSTD_c_contentSizeFlag: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_checksumFlag: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_dictIDFlag: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_nbWorkers: + bounds.lowerBound = 0; +#ifdef ZSTD_MULTITHREAD + bounds.upperBound = ZSTDMT_NBWORKERS_MAX; +#else + bounds.upperBound = 0; +#endif + return bounds; + + case ZSTD_c_jobSize: + bounds.lowerBound = 0; +#ifdef ZSTD_MULTITHREAD + bounds.upperBound = ZSTDMT_JOBSIZE_MAX; +#else + bounds.upperBound = 0; +#endif + return bounds; + + case ZSTD_c_overlapLog: +#ifdef ZSTD_MULTITHREAD + bounds.lowerBound = ZSTD_OVERLAPLOG_MIN; + bounds.upperBound = ZSTD_OVERLAPLOG_MAX; +#else + bounds.lowerBound = 0; + bounds.upperBound = 0; +#endif + return bounds; + + case ZSTD_c_enableDedicatedDictSearch: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_enableLongDistanceMatching: + bounds.lowerBound = (int)ZSTD_ps_auto; + bounds.upperBound = (int)ZSTD_ps_disable; + return bounds; + + case ZSTD_c_ldmHashLog: + bounds.lowerBound = ZSTD_LDM_HASHLOG_MIN; + bounds.upperBound = ZSTD_LDM_HASHLOG_MAX; + return bounds; + + case ZSTD_c_ldmMinMatch: + bounds.lowerBound = ZSTD_LDM_MINMATCH_MIN; + bounds.upperBound = ZSTD_LDM_MINMATCH_MAX; + return bounds; + + case ZSTD_c_ldmBucketSizeLog: + bounds.lowerBound = ZSTD_LDM_BUCKETSIZELOG_MIN; + bounds.upperBound = ZSTD_LDM_BUCKETSIZELOG_MAX; + return bounds; + + case ZSTD_c_ldmHashRateLog: + bounds.lowerBound = ZSTD_LDM_HASHRATELOG_MIN; + bounds.upperBound = ZSTD_LDM_HASHRATELOG_MAX; + return bounds; + + /* experimental parameters */ + case ZSTD_c_rsyncable: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_forceMaxWindow : + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_format: + ZSTD_STATIC_ASSERT(ZSTD_f_zstd1 < ZSTD_f_zstd1_magicless); + bounds.lowerBound = ZSTD_f_zstd1; + bounds.upperBound = ZSTD_f_zstd1_magicless; /* note : how to ensure at compile time that this is the highest value enum ? */ + return bounds; + + case ZSTD_c_forceAttachDict: + ZSTD_STATIC_ASSERT(ZSTD_dictDefaultAttach < ZSTD_dictForceLoad); + bounds.lowerBound = ZSTD_dictDefaultAttach; + bounds.upperBound = ZSTD_dictForceLoad; /* note : how to ensure at compile time that this is the highest value enum ? */ + return bounds; + + case ZSTD_c_literalCompressionMode: + ZSTD_STATIC_ASSERT(ZSTD_ps_auto < ZSTD_ps_enable && ZSTD_ps_enable < ZSTD_ps_disable); + bounds.lowerBound = (int)ZSTD_ps_auto; + bounds.upperBound = (int)ZSTD_ps_disable; + return bounds; + + case ZSTD_c_targetCBlockSize: + bounds.lowerBound = ZSTD_TARGETCBLOCKSIZE_MIN; + bounds.upperBound = ZSTD_TARGETCBLOCKSIZE_MAX; + return bounds; + + case ZSTD_c_srcSizeHint: + bounds.lowerBound = ZSTD_SRCSIZEHINT_MIN; + bounds.upperBound = ZSTD_SRCSIZEHINT_MAX; + return bounds; + + case ZSTD_c_stableInBuffer: + case ZSTD_c_stableOutBuffer: + bounds.lowerBound = (int)ZSTD_bm_buffered; + bounds.upperBound = (int)ZSTD_bm_stable; + return bounds; + + case ZSTD_c_blockDelimiters: + bounds.lowerBound = (int)ZSTD_sf_noBlockDelimiters; + bounds.upperBound = (int)ZSTD_sf_explicitBlockDelimiters; + return bounds; + + case ZSTD_c_validateSequences: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_useBlockSplitter: + bounds.lowerBound = (int)ZSTD_ps_auto; + bounds.upperBound = (int)ZSTD_ps_disable; + return bounds; + + case ZSTD_c_useRowMatchFinder: + bounds.lowerBound = (int)ZSTD_ps_auto; + bounds.upperBound = (int)ZSTD_ps_disable; + return bounds; + + case ZSTD_c_deterministicRefPrefix: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_prefetchCDictTables: + bounds.lowerBound = (int)ZSTD_ps_auto; + bounds.upperBound = (int)ZSTD_ps_disable; + return bounds; + + case ZSTD_c_enableSeqProducerFallback: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + + case ZSTD_c_maxBlockSize: + bounds.lowerBound = ZSTD_BLOCKSIZE_MAX_MIN; + bounds.upperBound = ZSTD_BLOCKSIZE_MAX; + return bounds; + + case ZSTD_c_searchForExternalRepcodes: + bounds.lowerBound = (int)ZSTD_ps_auto; + bounds.upperBound = (int)ZSTD_ps_disable; + return bounds; + + default: + bounds.error = ERROR(parameter_unsupported); + return bounds; + } +} + +/* ZSTD_cParam_clampBounds: + * Clamps the value into the bounded range. + */ +static size_t ZSTD_cParam_clampBounds(ZSTD_cParameter cParam, int* value) +{ + ZSTD_bounds const bounds = ZSTD_cParam_getBounds(cParam); + if (ZSTD_isError(bounds.error)) return bounds.error; + if (*value < bounds.lowerBound) *value = bounds.lowerBound; + if (*value > bounds.upperBound) *value = bounds.upperBound; + return 0; +} + +#define BOUNDCHECK(cParam, val) \ + do { \ + RETURN_ERROR_IF(!ZSTD_cParam_withinBounds(cParam,val), \ + parameter_outOfBound, "Param out of bounds"); \ + } while (0) + + +static int ZSTD_isUpdateAuthorized(ZSTD_cParameter param) +{ + switch(param) + { + case ZSTD_c_compressionLevel: + case ZSTD_c_hashLog: + case ZSTD_c_chainLog: + case ZSTD_c_searchLog: + case ZSTD_c_minMatch: + case ZSTD_c_targetLength: + case ZSTD_c_strategy: + return 1; + + case ZSTD_c_format: + case ZSTD_c_windowLog: + case ZSTD_c_contentSizeFlag: + case ZSTD_c_checksumFlag: + case ZSTD_c_dictIDFlag: + case ZSTD_c_forceMaxWindow : + case ZSTD_c_nbWorkers: + case ZSTD_c_jobSize: + case ZSTD_c_overlapLog: + case ZSTD_c_rsyncable: + case ZSTD_c_enableDedicatedDictSearch: + case ZSTD_c_enableLongDistanceMatching: + case ZSTD_c_ldmHashLog: + case ZSTD_c_ldmMinMatch: + case ZSTD_c_ldmBucketSizeLog: + case ZSTD_c_ldmHashRateLog: + case ZSTD_c_forceAttachDict: + case ZSTD_c_literalCompressionMode: + case ZSTD_c_targetCBlockSize: + case ZSTD_c_srcSizeHint: + case ZSTD_c_stableInBuffer: + case ZSTD_c_stableOutBuffer: + case ZSTD_c_blockDelimiters: + case ZSTD_c_validateSequences: + case ZSTD_c_useBlockSplitter: + case ZSTD_c_useRowMatchFinder: + case ZSTD_c_deterministicRefPrefix: + case ZSTD_c_prefetchCDictTables: + case ZSTD_c_enableSeqProducerFallback: + case ZSTD_c_maxBlockSize: + case ZSTD_c_searchForExternalRepcodes: + default: + return 0; + } +} + +size_t ZSTD_CCtx_setParameter(ZSTD_CCtx* cctx, ZSTD_cParameter param, int value) +{ + DEBUGLOG(4, "ZSTD_CCtx_setParameter (%i, %i)", (int)param, value); + if (cctx->streamStage != zcss_init) { + if (ZSTD_isUpdateAuthorized(param)) { + cctx->cParamsChanged = 1; + } else { + RETURN_ERROR(stage_wrong, "can only set params in cctx init stage"); + } } + + switch(param) + { + case ZSTD_c_nbWorkers: + RETURN_ERROR_IF((value!=0) && cctx->staticSize, parameter_unsupported, + "MT not compatible with static alloc"); + break; + + case ZSTD_c_compressionLevel: + case ZSTD_c_windowLog: + case ZSTD_c_hashLog: + case ZSTD_c_chainLog: + case ZSTD_c_searchLog: + case ZSTD_c_minMatch: + case ZSTD_c_targetLength: + case ZSTD_c_strategy: + case ZSTD_c_ldmHashRateLog: + case ZSTD_c_format: + case ZSTD_c_contentSizeFlag: + case ZSTD_c_checksumFlag: + case ZSTD_c_dictIDFlag: + case ZSTD_c_forceMaxWindow: + case ZSTD_c_forceAttachDict: + case ZSTD_c_literalCompressionMode: + case ZSTD_c_jobSize: + case ZSTD_c_overlapLog: + case ZSTD_c_rsyncable: + case ZSTD_c_enableDedicatedDictSearch: + case ZSTD_c_enableLongDistanceMatching: + case ZSTD_c_ldmHashLog: + case ZSTD_c_ldmMinMatch: + case ZSTD_c_ldmBucketSizeLog: + case ZSTD_c_targetCBlockSize: + case ZSTD_c_srcSizeHint: + case ZSTD_c_stableInBuffer: + case ZSTD_c_stableOutBuffer: + case ZSTD_c_blockDelimiters: + case ZSTD_c_validateSequences: + case ZSTD_c_useBlockSplitter: + case ZSTD_c_useRowMatchFinder: + case ZSTD_c_deterministicRefPrefix: + case ZSTD_c_prefetchCDictTables: + case ZSTD_c_enableSeqProducerFallback: + case ZSTD_c_maxBlockSize: + case ZSTD_c_searchForExternalRepcodes: + break; + + default: RETURN_ERROR(parameter_unsupported, "unknown parameter"); + } + return ZSTD_CCtxParams_setParameter(&cctx->requestedParams, param, value); +} + +size_t ZSTD_CCtxParams_setParameter(ZSTD_CCtx_params* CCtxParams, + ZSTD_cParameter param, int value) +{ + DEBUGLOG(4, "ZSTD_CCtxParams_setParameter (%i, %i)", (int)param, value); + switch(param) + { + case ZSTD_c_format : + BOUNDCHECK(ZSTD_c_format, value); + CCtxParams->format = (ZSTD_format_e)value; + return (size_t)CCtxParams->format; + + case ZSTD_c_compressionLevel : { + FORWARD_IF_ERROR(ZSTD_cParam_clampBounds(param, &value), ""); + if (value == 0) + CCtxParams->compressionLevel = ZSTD_CLEVEL_DEFAULT; /* 0 == default */ + else + CCtxParams->compressionLevel = value; + if (CCtxParams->compressionLevel >= 0) return (size_t)CCtxParams->compressionLevel; + return 0; /* return type (size_t) cannot represent negative values */ + } + + case ZSTD_c_windowLog : + if (value!=0) /* 0 => use default */ + BOUNDCHECK(ZSTD_c_windowLog, value); + CCtxParams->cParams.windowLog = (U32)value; + return CCtxParams->cParams.windowLog; + + case ZSTD_c_hashLog : + if (value!=0) /* 0 => use default */ + BOUNDCHECK(ZSTD_c_hashLog, value); + CCtxParams->cParams.hashLog = (U32)value; + return CCtxParams->cParams.hashLog; + + case ZSTD_c_chainLog : + if (value!=0) /* 0 => use default */ + BOUNDCHECK(ZSTD_c_chainLog, value); + CCtxParams->cParams.chainLog = (U32)value; + return CCtxParams->cParams.chainLog; + + case ZSTD_c_searchLog : + if (value!=0) /* 0 => use default */ + BOUNDCHECK(ZSTD_c_searchLog, value); + CCtxParams->cParams.searchLog = (U32)value; + return (size_t)value; + + case ZSTD_c_minMatch : + if (value!=0) /* 0 => use default */ + BOUNDCHECK(ZSTD_c_minMatch, value); + CCtxParams->cParams.minMatch = (U32)value; + return CCtxParams->cParams.minMatch; + + case ZSTD_c_targetLength : + BOUNDCHECK(ZSTD_c_targetLength, value); + CCtxParams->cParams.targetLength = (U32)value; + return CCtxParams->cParams.targetLength; + + case ZSTD_c_strategy : + if (value!=0) /* 0 => use default */ + BOUNDCHECK(ZSTD_c_strategy, value); + CCtxParams->cParams.strategy = (ZSTD_strategy)value; + return (size_t)CCtxParams->cParams.strategy; + + case ZSTD_c_contentSizeFlag : + /* Content size written in frame header _when known_ (default:1) */ + DEBUGLOG(4, "set content size flag = %u", (value!=0)); + CCtxParams->fParams.contentSizeFlag = value != 0; + return (size_t)CCtxParams->fParams.contentSizeFlag; + + case ZSTD_c_checksumFlag : + /* A 32-bits content checksum will be calculated and written at end of frame (default:0) */ + CCtxParams->fParams.checksumFlag = value != 0; + return (size_t)CCtxParams->fParams.checksumFlag; + + case ZSTD_c_dictIDFlag : /* When applicable, dictionary's dictID is provided in frame header (default:1) */ + DEBUGLOG(4, "set dictIDFlag = %u", (value!=0)); + CCtxParams->fParams.noDictIDFlag = !value; + return !CCtxParams->fParams.noDictIDFlag; + + case ZSTD_c_forceMaxWindow : + CCtxParams->forceWindow = (value != 0); + return (size_t)CCtxParams->forceWindow; + + case ZSTD_c_forceAttachDict : { + const ZSTD_dictAttachPref_e pref = (ZSTD_dictAttachPref_e)value; + BOUNDCHECK(ZSTD_c_forceAttachDict, (int)pref); + CCtxParams->attachDictPref = pref; + return CCtxParams->attachDictPref; + } + + case ZSTD_c_literalCompressionMode : { + const ZSTD_paramSwitch_e lcm = (ZSTD_paramSwitch_e)value; + BOUNDCHECK(ZSTD_c_literalCompressionMode, (int)lcm); + CCtxParams->literalCompressionMode = lcm; + return CCtxParams->literalCompressionMode; + } + + case ZSTD_c_nbWorkers : +#ifndef ZSTD_MULTITHREAD + RETURN_ERROR_IF(value!=0, parameter_unsupported, "not compiled with multithreading"); + return 0; +#else + FORWARD_IF_ERROR(ZSTD_cParam_clampBounds(param, &value), ""); + CCtxParams->nbWorkers = value; + return (size_t)(CCtxParams->nbWorkers); +#endif + + case ZSTD_c_jobSize : +#ifndef ZSTD_MULTITHREAD + RETURN_ERROR_IF(value!=0, parameter_unsupported, "not compiled with multithreading"); + return 0; +#else + /* Adjust to the minimum non-default value. */ + if (value != 0 && value < ZSTDMT_JOBSIZE_MIN) + value = ZSTDMT_JOBSIZE_MIN; + FORWARD_IF_ERROR(ZSTD_cParam_clampBounds(param, &value), ""); + assert(value >= 0); + CCtxParams->jobSize = value; + return CCtxParams->jobSize; +#endif + + case ZSTD_c_overlapLog : +#ifndef ZSTD_MULTITHREAD + RETURN_ERROR_IF(value!=0, parameter_unsupported, "not compiled with multithreading"); + return 0; +#else + FORWARD_IF_ERROR(ZSTD_cParam_clampBounds(ZSTD_c_overlapLog, &value), ""); + CCtxParams->overlapLog = value; + return (size_t)CCtxParams->overlapLog; +#endif + + case ZSTD_c_rsyncable : +#ifndef ZSTD_MULTITHREAD + RETURN_ERROR_IF(value!=0, parameter_unsupported, "not compiled with multithreading"); + return 0; +#else + FORWARD_IF_ERROR(ZSTD_cParam_clampBounds(ZSTD_c_overlapLog, &value), ""); + CCtxParams->rsyncable = value; + return (size_t)CCtxParams->rsyncable; +#endif + + case ZSTD_c_enableDedicatedDictSearch : + CCtxParams->enableDedicatedDictSearch = (value!=0); + return (size_t)CCtxParams->enableDedicatedDictSearch; + + case ZSTD_c_enableLongDistanceMatching : + BOUNDCHECK(ZSTD_c_enableLongDistanceMatching, value); + CCtxParams->ldmParams.enableLdm = (ZSTD_paramSwitch_e)value; + return CCtxParams->ldmParams.enableLdm; + + case ZSTD_c_ldmHashLog : + if (value!=0) /* 0 ==> auto */ + BOUNDCHECK(ZSTD_c_ldmHashLog, value); + CCtxParams->ldmParams.hashLog = (U32)value; + return CCtxParams->ldmParams.hashLog; + + case ZSTD_c_ldmMinMatch : + if (value!=0) /* 0 ==> default */ + BOUNDCHECK(ZSTD_c_ldmMinMatch, value); + CCtxParams->ldmParams.minMatchLength = (U32)value; + return CCtxParams->ldmParams.minMatchLength; + + case ZSTD_c_ldmBucketSizeLog : + if (value!=0) /* 0 ==> default */ + BOUNDCHECK(ZSTD_c_ldmBucketSizeLog, value); + CCtxParams->ldmParams.bucketSizeLog = (U32)value; + return CCtxParams->ldmParams.bucketSizeLog; + + case ZSTD_c_ldmHashRateLog : + if (value!=0) /* 0 ==> default */ + BOUNDCHECK(ZSTD_c_ldmHashRateLog, value); + CCtxParams->ldmParams.hashRateLog = (U32)value; + return CCtxParams->ldmParams.hashRateLog; + + case ZSTD_c_targetCBlockSize : + if (value!=0) { /* 0 ==> default */ + value = MAX(value, ZSTD_TARGETCBLOCKSIZE_MIN); + BOUNDCHECK(ZSTD_c_targetCBlockSize, value); + } + CCtxParams->targetCBlockSize = (U32)value; + return CCtxParams->targetCBlockSize; + + case ZSTD_c_srcSizeHint : + if (value!=0) /* 0 ==> default */ + BOUNDCHECK(ZSTD_c_srcSizeHint, value); + CCtxParams->srcSizeHint = value; + return (size_t)CCtxParams->srcSizeHint; + + case ZSTD_c_stableInBuffer: + BOUNDCHECK(ZSTD_c_stableInBuffer, value); + CCtxParams->inBufferMode = (ZSTD_bufferMode_e)value; + return CCtxParams->inBufferMode; + + case ZSTD_c_stableOutBuffer: + BOUNDCHECK(ZSTD_c_stableOutBuffer, value); + CCtxParams->outBufferMode = (ZSTD_bufferMode_e)value; + return CCtxParams->outBufferMode; + + case ZSTD_c_blockDelimiters: + BOUNDCHECK(ZSTD_c_blockDelimiters, value); + CCtxParams->blockDelimiters = (ZSTD_sequenceFormat_e)value; + return CCtxParams->blockDelimiters; + + case ZSTD_c_validateSequences: + BOUNDCHECK(ZSTD_c_validateSequences, value); + CCtxParams->validateSequences = value; + return (size_t)CCtxParams->validateSequences; + + case ZSTD_c_useBlockSplitter: + BOUNDCHECK(ZSTD_c_useBlockSplitter, value); + CCtxParams->useBlockSplitter = (ZSTD_paramSwitch_e)value; + return CCtxParams->useBlockSplitter; + + case ZSTD_c_useRowMatchFinder: + BOUNDCHECK(ZSTD_c_useRowMatchFinder, value); + CCtxParams->useRowMatchFinder = (ZSTD_paramSwitch_e)value; + return CCtxParams->useRowMatchFinder; + + case ZSTD_c_deterministicRefPrefix: + BOUNDCHECK(ZSTD_c_deterministicRefPrefix, value); + CCtxParams->deterministicRefPrefix = !!value; + return (size_t)CCtxParams->deterministicRefPrefix; + + case ZSTD_c_prefetchCDictTables: + BOUNDCHECK(ZSTD_c_prefetchCDictTables, value); + CCtxParams->prefetchCDictTables = (ZSTD_paramSwitch_e)value; + return CCtxParams->prefetchCDictTables; + + case ZSTD_c_enableSeqProducerFallback: + BOUNDCHECK(ZSTD_c_enableSeqProducerFallback, value); + CCtxParams->enableMatchFinderFallback = value; + return (size_t)CCtxParams->enableMatchFinderFallback; + + case ZSTD_c_maxBlockSize: + if (value!=0) /* 0 ==> default */ + BOUNDCHECK(ZSTD_c_maxBlockSize, value); + CCtxParams->maxBlockSize = value; + return CCtxParams->maxBlockSize; + + case ZSTD_c_searchForExternalRepcodes: + BOUNDCHECK(ZSTD_c_searchForExternalRepcodes, value); + CCtxParams->searchForExternalRepcodes = (ZSTD_paramSwitch_e)value; + return CCtxParams->searchForExternalRepcodes; + + default: RETURN_ERROR(parameter_unsupported, "unknown parameter"); + } +} + +size_t ZSTD_CCtx_getParameter(ZSTD_CCtx const* cctx, ZSTD_cParameter param, int* value) +{ + return ZSTD_CCtxParams_getParameter(&cctx->requestedParams, param, value); +} + +size_t ZSTD_CCtxParams_getParameter( + ZSTD_CCtx_params const* CCtxParams, ZSTD_cParameter param, int* value) +{ + switch(param) + { + case ZSTD_c_format : + *value = CCtxParams->format; + break; + case ZSTD_c_compressionLevel : + *value = CCtxParams->compressionLevel; + break; + case ZSTD_c_windowLog : + *value = (int)CCtxParams->cParams.windowLog; + break; + case ZSTD_c_hashLog : + *value = (int)CCtxParams->cParams.hashLog; + break; + case ZSTD_c_chainLog : + *value = (int)CCtxParams->cParams.chainLog; + break; + case ZSTD_c_searchLog : + *value = CCtxParams->cParams.searchLog; + break; + case ZSTD_c_minMatch : + *value = CCtxParams->cParams.minMatch; + break; + case ZSTD_c_targetLength : + *value = CCtxParams->cParams.targetLength; + break; + case ZSTD_c_strategy : + *value = (unsigned)CCtxParams->cParams.strategy; + break; + case ZSTD_c_contentSizeFlag : + *value = CCtxParams->fParams.contentSizeFlag; + break; + case ZSTD_c_checksumFlag : + *value = CCtxParams->fParams.checksumFlag; + break; + case ZSTD_c_dictIDFlag : + *value = !CCtxParams->fParams.noDictIDFlag; + break; + case ZSTD_c_forceMaxWindow : + *value = CCtxParams->forceWindow; + break; + case ZSTD_c_forceAttachDict : + *value = CCtxParams->attachDictPref; + break; + case ZSTD_c_literalCompressionMode : + *value = CCtxParams->literalCompressionMode; + break; + case ZSTD_c_nbWorkers : +#ifndef ZSTD_MULTITHREAD + assert(CCtxParams->nbWorkers == 0); +#endif + *value = CCtxParams->nbWorkers; + break; + case ZSTD_c_jobSize : +#ifndef ZSTD_MULTITHREAD + RETURN_ERROR(parameter_unsupported, "not compiled with multithreading"); +#else + assert(CCtxParams->jobSize <= INT_MAX); + *value = (int)CCtxParams->jobSize; + break; +#endif + case ZSTD_c_overlapLog : +#ifndef ZSTD_MULTITHREAD + RETURN_ERROR(parameter_unsupported, "not compiled with multithreading"); +#else + *value = CCtxParams->overlapLog; + break; +#endif + case ZSTD_c_rsyncable : +#ifndef ZSTD_MULTITHREAD + RETURN_ERROR(parameter_unsupported, "not compiled with multithreading"); +#else + *value = CCtxParams->rsyncable; + break; +#endif + case ZSTD_c_enableDedicatedDictSearch : + *value = CCtxParams->enableDedicatedDictSearch; + break; + case ZSTD_c_enableLongDistanceMatching : + *value = CCtxParams->ldmParams.enableLdm; + break; + case ZSTD_c_ldmHashLog : + *value = CCtxParams->ldmParams.hashLog; + break; + case ZSTD_c_ldmMinMatch : + *value = CCtxParams->ldmParams.minMatchLength; + break; + case ZSTD_c_ldmBucketSizeLog : + *value = CCtxParams->ldmParams.bucketSizeLog; + break; + case ZSTD_c_ldmHashRateLog : + *value = CCtxParams->ldmParams.hashRateLog; + break; + case ZSTD_c_targetCBlockSize : + *value = (int)CCtxParams->targetCBlockSize; + break; + case ZSTD_c_srcSizeHint : + *value = (int)CCtxParams->srcSizeHint; + break; + case ZSTD_c_stableInBuffer : + *value = (int)CCtxParams->inBufferMode; + break; + case ZSTD_c_stableOutBuffer : + *value = (int)CCtxParams->outBufferMode; + break; + case ZSTD_c_blockDelimiters : + *value = (int)CCtxParams->blockDelimiters; + break; + case ZSTD_c_validateSequences : + *value = (int)CCtxParams->validateSequences; + break; + case ZSTD_c_useBlockSplitter : + *value = (int)CCtxParams->useBlockSplitter; + break; + case ZSTD_c_useRowMatchFinder : + *value = (int)CCtxParams->useRowMatchFinder; + break; + case ZSTD_c_deterministicRefPrefix: + *value = (int)CCtxParams->deterministicRefPrefix; + break; + case ZSTD_c_prefetchCDictTables: + *value = (int)CCtxParams->prefetchCDictTables; + break; + case ZSTD_c_enableSeqProducerFallback: + *value = CCtxParams->enableMatchFinderFallback; + break; + case ZSTD_c_maxBlockSize: + *value = (int)CCtxParams->maxBlockSize; + break; + case ZSTD_c_searchForExternalRepcodes: + *value = (int)CCtxParams->searchForExternalRepcodes; + break; + default: RETURN_ERROR(parameter_unsupported, "unknown parameter"); + } + return 0; +} + +/** ZSTD_CCtx_setParametersUsingCCtxParams() : + * just applies `params` into `cctx` + * no action is performed, parameters are merely stored. + * If ZSTDMT is enabled, parameters are pushed to cctx->mtctx. + * This is possible even if a compression is ongoing. + * In which case, new parameters will be applied on the fly, starting with next compression job. + */ +size_t ZSTD_CCtx_setParametersUsingCCtxParams( + ZSTD_CCtx* cctx, const ZSTD_CCtx_params* params) +{ + DEBUGLOG(4, "ZSTD_CCtx_setParametersUsingCCtxParams"); + RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong, + "The context is in the wrong stage!"); + RETURN_ERROR_IF(cctx->cdict, stage_wrong, + "Can't override parameters with cdict attached (some must " + "be inherited from the cdict)."); + + cctx->requestedParams = *params; + return 0; +} + +size_t ZSTD_CCtx_setCParams(ZSTD_CCtx* cctx, ZSTD_compressionParameters cparams) +{ + ZSTD_STATIC_ASSERT(sizeof(cparams) == 7 * 4 /* all params are listed below */); + DEBUGLOG(4, "ZSTD_CCtx_setCParams"); + /* only update if all parameters are valid */ + FORWARD_IF_ERROR(ZSTD_checkCParams(cparams), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_windowLog, cparams.windowLog), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_chainLog, cparams.chainLog), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_hashLog, cparams.hashLog), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_searchLog, cparams.searchLog), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_minMatch, cparams.minMatch), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_targetLength, cparams.targetLength), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_strategy, cparams.strategy), ""); + return 0; +} + +size_t ZSTD_CCtx_setFParams(ZSTD_CCtx* cctx, ZSTD_frameParameters fparams) +{ + ZSTD_STATIC_ASSERT(sizeof(fparams) == 3 * 4 /* all params are listed below */); + DEBUGLOG(4, "ZSTD_CCtx_setFParams"); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_contentSizeFlag, fparams.contentSizeFlag != 0), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_checksumFlag, fparams.checksumFlag != 0), ""); + FORWARD_IF_ERROR(ZSTD_CCtx_setParameter(cctx, ZSTD_c_dictIDFlag, fparams.noDictIDFlag == 0), ""); + return 0; +} + +size_t ZSTD_CCtx_setParams(ZSTD_CCtx* cctx, ZSTD_parameters params) +{ + DEBUGLOG(4, "ZSTD_CCtx_setParams"); + /* First check cParams, because we want to update all or none. */ + FORWARD_IF_ERROR(ZSTD_checkCParams(params.cParams), ""); + /* Next set fParams, because this could fail if the cctx isn't in init stage. */ + FORWARD_IF_ERROR(ZSTD_CCtx_setFParams(cctx, params.fParams), ""); + /* Finally set cParams, which should succeed. */ + FORWARD_IF_ERROR(ZSTD_CCtx_setCParams(cctx, params.cParams), ""); + return 0; +} + +size_t ZSTD_CCtx_setPledgedSrcSize(ZSTD_CCtx* cctx, unsigned long long pledgedSrcSize) +{ + DEBUGLOG(4, "ZSTD_CCtx_setPledgedSrcSize to %llu bytes", pledgedSrcSize); + RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong, + "Can't set pledgedSrcSize when not in init stage."); + cctx->pledgedSrcSizePlusOne = pledgedSrcSize+1; + return 0; +} + +static ZSTD_compressionParameters ZSTD_dedicatedDictSearch_getCParams( + int const compressionLevel, + size_t const dictSize); +static int ZSTD_dedicatedDictSearch_isSupported( + const ZSTD_compressionParameters* cParams); +static void ZSTD_dedicatedDictSearch_revertCParams( + ZSTD_compressionParameters* cParams); + +/** + * Initializes the local dictionary using requested parameters. + * NOTE: Initialization does not employ the pledged src size, + * because the dictionary may be used for multiple compressions. + */ +static size_t ZSTD_initLocalDict(ZSTD_CCtx* cctx) +{ + ZSTD_localDict* const dl = &cctx->localDict; + if (dl->dict == NULL) { + /* No local dictionary. */ + assert(dl->dictBuffer == NULL); + assert(dl->cdict == NULL); + assert(dl->dictSize == 0); + return 0; + } + if (dl->cdict != NULL) { + /* Local dictionary already initialized. */ + assert(cctx->cdict == dl->cdict); + return 0; + } + assert(dl->dictSize > 0); + assert(cctx->cdict == NULL); + assert(cctx->prefixDict.dict == NULL); + + dl->cdict = ZSTD_createCDict_advanced2( + dl->dict, + dl->dictSize, + ZSTD_dlm_byRef, + dl->dictContentType, + &cctx->requestedParams, + cctx->customMem); + RETURN_ERROR_IF(!dl->cdict, memory_allocation, "ZSTD_createCDict_advanced failed"); + cctx->cdict = dl->cdict; + return 0; +} + +size_t ZSTD_CCtx_loadDictionary_advanced( + ZSTD_CCtx* cctx, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType) +{ + DEBUGLOG(4, "ZSTD_CCtx_loadDictionary_advanced (size: %u)", (U32)dictSize); + RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong, + "Can't load a dictionary when cctx is not in init stage."); + ZSTD_clearAllDicts(cctx); /* erase any previously set dictionary */ + if (dict == NULL || dictSize == 0) /* no dictionary */ + return 0; + if (dictLoadMethod == ZSTD_dlm_byRef) { + cctx->localDict.dict = dict; + } else { + /* copy dictionary content inside CCtx to own its lifetime */ + void* dictBuffer; + RETURN_ERROR_IF(cctx->staticSize, memory_allocation, + "static CCtx can't allocate for an internal copy of dictionary"); + dictBuffer = ZSTD_customMalloc(dictSize, cctx->customMem); + RETURN_ERROR_IF(dictBuffer==NULL, memory_allocation, + "allocation failed for dictionary content"); + ZSTD_memcpy(dictBuffer, dict, dictSize); + cctx->localDict.dictBuffer = dictBuffer; /* owned ptr to free */ + cctx->localDict.dict = dictBuffer; /* read-only reference */ + } + cctx->localDict.dictSize = dictSize; + cctx->localDict.dictContentType = dictContentType; + return 0; +} + +size_t ZSTD_CCtx_loadDictionary_byReference( + ZSTD_CCtx* cctx, const void* dict, size_t dictSize) +{ + return ZSTD_CCtx_loadDictionary_advanced( + cctx, dict, dictSize, ZSTD_dlm_byRef, ZSTD_dct_auto); +} + +size_t ZSTD_CCtx_loadDictionary(ZSTD_CCtx* cctx, const void* dict, size_t dictSize) +{ + return ZSTD_CCtx_loadDictionary_advanced( + cctx, dict, dictSize, ZSTD_dlm_byCopy, ZSTD_dct_auto); +} + + +size_t ZSTD_CCtx_refCDict(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict) +{ + RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong, + "Can't ref a dict when ctx not in init stage."); + /* Free the existing local cdict (if any) to save memory. */ + ZSTD_clearAllDicts(cctx); + cctx->cdict = cdict; + return 0; +} + +size_t ZSTD_CCtx_refThreadPool(ZSTD_CCtx* cctx, ZSTD_threadPool* pool) +{ + RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong, + "Can't ref a pool when ctx not in init stage."); + cctx->pool = pool; + return 0; +} + +size_t ZSTD_CCtx_refPrefix(ZSTD_CCtx* cctx, const void* prefix, size_t prefixSize) +{ + return ZSTD_CCtx_refPrefix_advanced(cctx, prefix, prefixSize, ZSTD_dct_rawContent); +} + +size_t ZSTD_CCtx_refPrefix_advanced( + ZSTD_CCtx* cctx, const void* prefix, size_t prefixSize, ZSTD_dictContentType_e dictContentType) +{ + RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong, + "Can't ref a prefix when ctx not in init stage."); + ZSTD_clearAllDicts(cctx); + if (prefix != NULL && prefixSize > 0) { + cctx->prefixDict.dict = prefix; + cctx->prefixDict.dictSize = prefixSize; + cctx->prefixDict.dictContentType = dictContentType; + } + return 0; +} + +/*! ZSTD_CCtx_reset() : + * Also dumps dictionary */ +size_t ZSTD_CCtx_reset(ZSTD_CCtx* cctx, ZSTD_ResetDirective reset) +{ + if ( (reset == ZSTD_reset_session_only) + || (reset == ZSTD_reset_session_and_parameters) ) { + cctx->streamStage = zcss_init; + cctx->pledgedSrcSizePlusOne = 0; + } + if ( (reset == ZSTD_reset_parameters) + || (reset == ZSTD_reset_session_and_parameters) ) { + RETURN_ERROR_IF(cctx->streamStage != zcss_init, stage_wrong, + "Reset parameters is only possible during init stage."); + ZSTD_clearAllDicts(cctx); + return ZSTD_CCtxParams_reset(&cctx->requestedParams); + } + return 0; +} + + +/** ZSTD_checkCParams() : + control CParam values remain within authorized range. + @return : 0, or an error code if one value is beyond authorized range */ +size_t ZSTD_checkCParams(ZSTD_compressionParameters cParams) +{ + BOUNDCHECK(ZSTD_c_windowLog, (int)cParams.windowLog); + BOUNDCHECK(ZSTD_c_chainLog, (int)cParams.chainLog); + BOUNDCHECK(ZSTD_c_hashLog, (int)cParams.hashLog); + BOUNDCHECK(ZSTD_c_searchLog, (int)cParams.searchLog); + BOUNDCHECK(ZSTD_c_minMatch, (int)cParams.minMatch); + BOUNDCHECK(ZSTD_c_targetLength,(int)cParams.targetLength); + BOUNDCHECK(ZSTD_c_strategy, cParams.strategy); + return 0; +} + +/** ZSTD_clampCParams() : + * make CParam values within valid range. + * @return : valid CParams */ +static ZSTD_compressionParameters +ZSTD_clampCParams(ZSTD_compressionParameters cParams) +{ +# define CLAMP_TYPE(cParam, val, type) \ + do { \ + ZSTD_bounds const bounds = ZSTD_cParam_getBounds(cParam); \ + if ((int)valbounds.upperBound) val=(type)bounds.upperBound; \ + } while (0) +# define CLAMP(cParam, val) CLAMP_TYPE(cParam, val, unsigned) + CLAMP(ZSTD_c_windowLog, cParams.windowLog); + CLAMP(ZSTD_c_chainLog, cParams.chainLog); + CLAMP(ZSTD_c_hashLog, cParams.hashLog); + CLAMP(ZSTD_c_searchLog, cParams.searchLog); + CLAMP(ZSTD_c_minMatch, cParams.minMatch); + CLAMP(ZSTD_c_targetLength,cParams.targetLength); + CLAMP_TYPE(ZSTD_c_strategy,cParams.strategy, ZSTD_strategy); + return cParams; +} + +/** ZSTD_cycleLog() : + * condition for correct operation : hashLog > 1 */ +U32 ZSTD_cycleLog(U32 hashLog, ZSTD_strategy strat) +{ + U32 const btScale = ((U32)strat >= (U32)ZSTD_btlazy2); + return hashLog - btScale; +} + +/** ZSTD_dictAndWindowLog() : + * Returns an adjusted window log that is large enough to fit the source and the dictionary. + * The zstd format says that the entire dictionary is valid if one byte of the dictionary + * is within the window. So the hashLog and chainLog should be large enough to reference both + * the dictionary and the window. So we must use this adjusted dictAndWindowLog when downsizing + * the hashLog and windowLog. + * NOTE: srcSize must not be ZSTD_CONTENTSIZE_UNKNOWN. + */ +static U32 ZSTD_dictAndWindowLog(U32 windowLog, U64 srcSize, U64 dictSize) +{ + const U64 maxWindowSize = 1ULL << ZSTD_WINDOWLOG_MAX; + /* No dictionary ==> No change */ + if (dictSize == 0) { + return windowLog; + } + assert(windowLog <= ZSTD_WINDOWLOG_MAX); + assert(srcSize != ZSTD_CONTENTSIZE_UNKNOWN); /* Handled in ZSTD_adjustCParams_internal() */ + { + U64 const windowSize = 1ULL << windowLog; + U64 const dictAndWindowSize = dictSize + windowSize; + /* If the window size is already large enough to fit both the source and the dictionary + * then just use the window size. Otherwise adjust so that it fits the dictionary and + * the window. + */ + if (windowSize >= dictSize + srcSize) { + return windowLog; /* Window size large enough already */ + } else if (dictAndWindowSize >= maxWindowSize) { + return ZSTD_WINDOWLOG_MAX; /* Larger than max window log */ + } else { + return ZSTD_highbit32((U32)dictAndWindowSize - 1) + 1; + } + } +} + +/** ZSTD_adjustCParams_internal() : + * optimize `cPar` for a specified input (`srcSize` and `dictSize`). + * mostly downsize to reduce memory consumption and initialization latency. + * `srcSize` can be ZSTD_CONTENTSIZE_UNKNOWN when not known. + * `mode` is the mode for parameter adjustment. See docs for `ZSTD_cParamMode_e`. + * note : `srcSize==0` means 0! + * condition : cPar is presumed validated (can be checked using ZSTD_checkCParams()). */ +static ZSTD_compressionParameters +ZSTD_adjustCParams_internal(ZSTD_compressionParameters cPar, + unsigned long long srcSize, + size_t dictSize, + ZSTD_cParamMode_e mode, + ZSTD_paramSwitch_e useRowMatchFinder) +{ + const U64 minSrcSize = 513; /* (1<<9) + 1 */ + const U64 maxWindowResize = 1ULL << (ZSTD_WINDOWLOG_MAX-1); + assert(ZSTD_checkCParams(cPar)==0); + + /* Cascade the selected strategy down to the next-highest one built into + * this binary. */ +#ifdef ZSTD_EXCLUDE_BTULTRA_BLOCK_COMPRESSOR + if (cPar.strategy == ZSTD_btultra2) { + cPar.strategy = ZSTD_btultra; + } + if (cPar.strategy == ZSTD_btultra) { + cPar.strategy = ZSTD_btopt; + } +#endif +#ifdef ZSTD_EXCLUDE_BTOPT_BLOCK_COMPRESSOR + if (cPar.strategy == ZSTD_btopt) { + cPar.strategy = ZSTD_btlazy2; + } +#endif +#ifdef ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR + if (cPar.strategy == ZSTD_btlazy2) { + cPar.strategy = ZSTD_lazy2; + } +#endif +#ifdef ZSTD_EXCLUDE_LAZY2_BLOCK_COMPRESSOR + if (cPar.strategy == ZSTD_lazy2) { + cPar.strategy = ZSTD_lazy; + } +#endif +#ifdef ZSTD_EXCLUDE_LAZY_BLOCK_COMPRESSOR + if (cPar.strategy == ZSTD_lazy) { + cPar.strategy = ZSTD_greedy; + } +#endif +#ifdef ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR + if (cPar.strategy == ZSTD_greedy) { + cPar.strategy = ZSTD_dfast; + } +#endif +#ifdef ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR + if (cPar.strategy == ZSTD_dfast) { + cPar.strategy = ZSTD_fast; + cPar.targetLength = 0; + } +#endif + + switch (mode) { + case ZSTD_cpm_unknown: + case ZSTD_cpm_noAttachDict: + /* If we don't know the source size, don't make any + * assumptions about it. We will already have selected + * smaller parameters if a dictionary is in use. + */ + break; + case ZSTD_cpm_createCDict: + /* Assume a small source size when creating a dictionary + * with an unknown source size. + */ + if (dictSize && srcSize == ZSTD_CONTENTSIZE_UNKNOWN) + srcSize = minSrcSize; + break; + case ZSTD_cpm_attachDict: + /* Dictionary has its own dedicated parameters which have + * already been selected. We are selecting parameters + * for only the source. + */ + dictSize = 0; + break; + default: + assert(0); + break; + } + + /* resize windowLog if input is small enough, to use less memory */ + if ( (srcSize <= maxWindowResize) + && (dictSize <= maxWindowResize) ) { + U32 const tSize = (U32)(srcSize + dictSize); + static U32 const hashSizeMin = 1 << ZSTD_HASHLOG_MIN; + U32 const srcLog = (tSize < hashSizeMin) ? ZSTD_HASHLOG_MIN : + ZSTD_highbit32(tSize-1) + 1; + if (cPar.windowLog > srcLog) cPar.windowLog = srcLog; + } + if (srcSize != ZSTD_CONTENTSIZE_UNKNOWN) { + U32 const dictAndWindowLog = ZSTD_dictAndWindowLog(cPar.windowLog, (U64)srcSize, (U64)dictSize); + U32 const cycleLog = ZSTD_cycleLog(cPar.chainLog, cPar.strategy); + if (cPar.hashLog > dictAndWindowLog+1) cPar.hashLog = dictAndWindowLog+1; + if (cycleLog > dictAndWindowLog) + cPar.chainLog -= (cycleLog - dictAndWindowLog); + } + + if (cPar.windowLog < ZSTD_WINDOWLOG_ABSOLUTEMIN) + cPar.windowLog = ZSTD_WINDOWLOG_ABSOLUTEMIN; /* minimum wlog required for valid frame header */ + + /* We can't use more than 32 bits of hash in total, so that means that we require: + * (hashLog + 8) <= 32 && (chainLog + 8) <= 32 + */ + if (mode == ZSTD_cpm_createCDict && ZSTD_CDictIndicesAreTagged(&cPar)) { + U32 const maxShortCacheHashLog = 32 - ZSTD_SHORT_CACHE_TAG_BITS; + if (cPar.hashLog > maxShortCacheHashLog) { + cPar.hashLog = maxShortCacheHashLog; + } + if (cPar.chainLog > maxShortCacheHashLog) { + cPar.chainLog = maxShortCacheHashLog; + } + } + + + /* At this point, we aren't 100% sure if we are using the row match finder. + * Unless it is explicitly disabled, conservatively assume that it is enabled. + * In this case it will only be disabled for small sources, so shrinking the + * hash log a little bit shouldn't result in any ratio loss. + */ + if (useRowMatchFinder == ZSTD_ps_auto) + useRowMatchFinder = ZSTD_ps_enable; + + /* We can't hash more than 32-bits in total. So that means that we require: + * (hashLog - rowLog + 8) <= 32 + */ + if (ZSTD_rowMatchFinderUsed(cPar.strategy, useRowMatchFinder)) { + /* Switch to 32-entry rows if searchLog is 5 (or more) */ + U32 const rowLog = BOUNDED(4, cPar.searchLog, 6); + U32 const maxRowHashLog = 32 - ZSTD_ROW_HASH_TAG_BITS; + U32 const maxHashLog = maxRowHashLog + rowLog; + assert(cPar.hashLog >= rowLog); + if (cPar.hashLog > maxHashLog) { + cPar.hashLog = maxHashLog; + } + } + + return cPar; +} + +ZSTD_compressionParameters +ZSTD_adjustCParams(ZSTD_compressionParameters cPar, + unsigned long long srcSize, + size_t dictSize) +{ + cPar = ZSTD_clampCParams(cPar); /* resulting cPar is necessarily valid (all parameters within range) */ + if (srcSize == 0) srcSize = ZSTD_CONTENTSIZE_UNKNOWN; + return ZSTD_adjustCParams_internal(cPar, srcSize, dictSize, ZSTD_cpm_unknown, ZSTD_ps_auto); +} + +static ZSTD_compressionParameters ZSTD_getCParams_internal(int compressionLevel, unsigned long long srcSizeHint, size_t dictSize, ZSTD_cParamMode_e mode); +static ZSTD_parameters ZSTD_getParams_internal(int compressionLevel, unsigned long long srcSizeHint, size_t dictSize, ZSTD_cParamMode_e mode); + +static void ZSTD_overrideCParams( + ZSTD_compressionParameters* cParams, + const ZSTD_compressionParameters* overrides) +{ + if (overrides->windowLog) cParams->windowLog = overrides->windowLog; + if (overrides->hashLog) cParams->hashLog = overrides->hashLog; + if (overrides->chainLog) cParams->chainLog = overrides->chainLog; + if (overrides->searchLog) cParams->searchLog = overrides->searchLog; + if (overrides->minMatch) cParams->minMatch = overrides->minMatch; + if (overrides->targetLength) cParams->targetLength = overrides->targetLength; + if (overrides->strategy) cParams->strategy = overrides->strategy; +} + +ZSTD_compressionParameters ZSTD_getCParamsFromCCtxParams( + const ZSTD_CCtx_params* CCtxParams, U64 srcSizeHint, size_t dictSize, ZSTD_cParamMode_e mode) +{ + ZSTD_compressionParameters cParams; + if (srcSizeHint == ZSTD_CONTENTSIZE_UNKNOWN && CCtxParams->srcSizeHint > 0) { + srcSizeHint = CCtxParams->srcSizeHint; + } + cParams = ZSTD_getCParams_internal(CCtxParams->compressionLevel, srcSizeHint, dictSize, mode); + if (CCtxParams->ldmParams.enableLdm == ZSTD_ps_enable) cParams.windowLog = ZSTD_LDM_DEFAULT_WINDOW_LOG; + ZSTD_overrideCParams(&cParams, &CCtxParams->cParams); + assert(!ZSTD_checkCParams(cParams)); + /* srcSizeHint == 0 means 0 */ + return ZSTD_adjustCParams_internal(cParams, srcSizeHint, dictSize, mode, CCtxParams->useRowMatchFinder); +} + +static size_t +ZSTD_sizeof_matchState(const ZSTD_compressionParameters* const cParams, + const ZSTD_paramSwitch_e useRowMatchFinder, + const U32 enableDedicatedDictSearch, + const U32 forCCtx) +{ + /* chain table size should be 0 for fast or row-hash strategies */ + size_t const chainSize = ZSTD_allocateChainTable(cParams->strategy, useRowMatchFinder, enableDedicatedDictSearch && !forCCtx) + ? ((size_t)1 << cParams->chainLog) + : 0; + size_t const hSize = ((size_t)1) << cParams->hashLog; + U32 const hashLog3 = (forCCtx && cParams->minMatch==3) ? MIN(ZSTD_HASHLOG3_MAX, cParams->windowLog) : 0; + size_t const h3Size = hashLog3 ? ((size_t)1) << hashLog3 : 0; + /* We don't use ZSTD_cwksp_alloc_size() here because the tables aren't + * surrounded by redzones in ASAN. */ + size_t const tableSpace = chainSize * sizeof(U32) + + hSize * sizeof(U32) + + h3Size * sizeof(U32); + size_t const optPotentialSpace = + ZSTD_cwksp_aligned_alloc_size((MaxML+1) * sizeof(U32)) + + ZSTD_cwksp_aligned_alloc_size((MaxLL+1) * sizeof(U32)) + + ZSTD_cwksp_aligned_alloc_size((MaxOff+1) * sizeof(U32)) + + ZSTD_cwksp_aligned_alloc_size((1<strategy, useRowMatchFinder) + ? ZSTD_cwksp_aligned_alloc_size(hSize) + : 0; + size_t const optSpace = (forCCtx && (cParams->strategy >= ZSTD_btopt)) + ? optPotentialSpace + : 0; + size_t const slackSpace = ZSTD_cwksp_slack_space_required(); + + /* tables are guaranteed to be sized in multiples of 64 bytes (or 16 uint32_t) */ + ZSTD_STATIC_ASSERT(ZSTD_HASHLOG_MIN >= 4 && ZSTD_WINDOWLOG_MIN >= 4 && ZSTD_CHAINLOG_MIN >= 4); + assert(useRowMatchFinder != ZSTD_ps_auto); + + DEBUGLOG(4, "chainSize: %u - hSize: %u - h3Size: %u", + (U32)chainSize, (U32)hSize, (U32)h3Size); + return tableSpace + optSpace + slackSpace + lazyAdditionalSpace; +} + +/* Helper function for calculating memory requirements. + * Gives a tighter bound than ZSTD_sequenceBound() by taking minMatch into account. */ +static size_t ZSTD_maxNbSeq(size_t blockSize, unsigned minMatch, int useSequenceProducer) { + U32 const divider = (minMatch==3 || useSequenceProducer) ? 3 : 4; + return blockSize / divider; +} + +static size_t ZSTD_estimateCCtxSize_usingCCtxParams_internal( + const ZSTD_compressionParameters* cParams, + const ldmParams_t* ldmParams, + const int isStatic, + const ZSTD_paramSwitch_e useRowMatchFinder, + const size_t buffInSize, + const size_t buffOutSize, + const U64 pledgedSrcSize, + int useSequenceProducer, + size_t maxBlockSize) +{ + size_t const windowSize = (size_t) BOUNDED(1ULL, 1ULL << cParams->windowLog, pledgedSrcSize); + size_t const blockSize = MIN(ZSTD_resolveMaxBlockSize(maxBlockSize), windowSize); + size_t const maxNbSeq = ZSTD_maxNbSeq(blockSize, cParams->minMatch, useSequenceProducer); + size_t const tokenSpace = ZSTD_cwksp_alloc_size(WILDCOPY_OVERLENGTH + blockSize) + + ZSTD_cwksp_aligned_alloc_size(maxNbSeq * sizeof(seqDef)) + + 3 * ZSTD_cwksp_alloc_size(maxNbSeq * sizeof(BYTE)); + size_t const entropySpace = ZSTD_cwksp_alloc_size(ENTROPY_WORKSPACE_SIZE); + size_t const blockStateSpace = 2 * ZSTD_cwksp_alloc_size(sizeof(ZSTD_compressedBlockState_t)); + size_t const matchStateSize = ZSTD_sizeof_matchState(cParams, useRowMatchFinder, /* enableDedicatedDictSearch */ 0, /* forCCtx */ 1); + + size_t const ldmSpace = ZSTD_ldm_getTableSize(*ldmParams); + size_t const maxNbLdmSeq = ZSTD_ldm_getMaxNbSeq(*ldmParams, blockSize); + size_t const ldmSeqSpace = ldmParams->enableLdm == ZSTD_ps_enable ? + ZSTD_cwksp_aligned_alloc_size(maxNbLdmSeq * sizeof(rawSeq)) : 0; + + + size_t const bufferSpace = ZSTD_cwksp_alloc_size(buffInSize) + + ZSTD_cwksp_alloc_size(buffOutSize); + + size_t const cctxSpace = isStatic ? ZSTD_cwksp_alloc_size(sizeof(ZSTD_CCtx)) : 0; + + size_t const maxNbExternalSeq = ZSTD_sequenceBound(blockSize); + size_t const externalSeqSpace = useSequenceProducer + ? ZSTD_cwksp_aligned_alloc_size(maxNbExternalSeq * sizeof(ZSTD_Sequence)) + : 0; + + size_t const neededSpace = + cctxSpace + + entropySpace + + blockStateSpace + + ldmSpace + + ldmSeqSpace + + matchStateSize + + tokenSpace + + bufferSpace + + externalSeqSpace; + + DEBUGLOG(5, "estimate workspace : %u", (U32)neededSpace); + return neededSpace; +} + +size_t ZSTD_estimateCCtxSize_usingCCtxParams(const ZSTD_CCtx_params* params) +{ + ZSTD_compressionParameters const cParams = + ZSTD_getCParamsFromCCtxParams(params, ZSTD_CONTENTSIZE_UNKNOWN, 0, ZSTD_cpm_noAttachDict); + ZSTD_paramSwitch_e const useRowMatchFinder = ZSTD_resolveRowMatchFinderMode(params->useRowMatchFinder, + &cParams); + + RETURN_ERROR_IF(params->nbWorkers > 0, GENERIC, "Estimate CCtx size is supported for single-threaded compression only."); + /* estimateCCtxSize is for one-shot compression. So no buffers should + * be needed. However, we still allocate two 0-sized buffers, which can + * take space under ASAN. */ + return ZSTD_estimateCCtxSize_usingCCtxParams_internal( + &cParams, ¶ms->ldmParams, 1, useRowMatchFinder, 0, 0, ZSTD_CONTENTSIZE_UNKNOWN, ZSTD_hasExtSeqProd(params), params->maxBlockSize); +} + +size_t ZSTD_estimateCCtxSize_usingCParams(ZSTD_compressionParameters cParams) +{ + ZSTD_CCtx_params initialParams = ZSTD_makeCCtxParamsFromCParams(cParams); + if (ZSTD_rowMatchFinderSupported(cParams.strategy)) { + /* Pick bigger of not using and using row-based matchfinder for greedy and lazy strategies */ + size_t noRowCCtxSize; + size_t rowCCtxSize; + initialParams.useRowMatchFinder = ZSTD_ps_disable; + noRowCCtxSize = ZSTD_estimateCCtxSize_usingCCtxParams(&initialParams); + initialParams.useRowMatchFinder = ZSTD_ps_enable; + rowCCtxSize = ZSTD_estimateCCtxSize_usingCCtxParams(&initialParams); + return MAX(noRowCCtxSize, rowCCtxSize); + } else { + return ZSTD_estimateCCtxSize_usingCCtxParams(&initialParams); + } +} + +static size_t ZSTD_estimateCCtxSize_internal(int compressionLevel) +{ + int tier = 0; + size_t largestSize = 0; + static const unsigned long long srcSizeTiers[4] = {16 KB, 128 KB, 256 KB, ZSTD_CONTENTSIZE_UNKNOWN}; + for (; tier < 4; ++tier) { + /* Choose the set of cParams for a given level across all srcSizes that give the largest cctxSize */ + ZSTD_compressionParameters const cParams = ZSTD_getCParams_internal(compressionLevel, srcSizeTiers[tier], 0, ZSTD_cpm_noAttachDict); + largestSize = MAX(ZSTD_estimateCCtxSize_usingCParams(cParams), largestSize); + } + return largestSize; +} + +size_t ZSTD_estimateCCtxSize(int compressionLevel) +{ + int level; + size_t memBudget = 0; + for (level=MIN(compressionLevel, 1); level<=compressionLevel; level++) { + /* Ensure monotonically increasing memory usage as compression level increases */ + size_t const newMB = ZSTD_estimateCCtxSize_internal(level); + if (newMB > memBudget) memBudget = newMB; + } + return memBudget; +} + +size_t ZSTD_estimateCStreamSize_usingCCtxParams(const ZSTD_CCtx_params* params) +{ + RETURN_ERROR_IF(params->nbWorkers > 0, GENERIC, "Estimate CCtx size is supported for single-threaded compression only."); + { ZSTD_compressionParameters const cParams = + ZSTD_getCParamsFromCCtxParams(params, ZSTD_CONTENTSIZE_UNKNOWN, 0, ZSTD_cpm_noAttachDict); + size_t const blockSize = MIN(ZSTD_resolveMaxBlockSize(params->maxBlockSize), (size_t)1 << cParams.windowLog); + size_t const inBuffSize = (params->inBufferMode == ZSTD_bm_buffered) + ? ((size_t)1 << cParams.windowLog) + blockSize + : 0; + size_t const outBuffSize = (params->outBufferMode == ZSTD_bm_buffered) + ? ZSTD_compressBound(blockSize) + 1 + : 0; + ZSTD_paramSwitch_e const useRowMatchFinder = ZSTD_resolveRowMatchFinderMode(params->useRowMatchFinder, ¶ms->cParams); + + return ZSTD_estimateCCtxSize_usingCCtxParams_internal( + &cParams, ¶ms->ldmParams, 1, useRowMatchFinder, inBuffSize, outBuffSize, + ZSTD_CONTENTSIZE_UNKNOWN, ZSTD_hasExtSeqProd(params), params->maxBlockSize); + } +} + +size_t ZSTD_estimateCStreamSize_usingCParams(ZSTD_compressionParameters cParams) +{ + ZSTD_CCtx_params initialParams = ZSTD_makeCCtxParamsFromCParams(cParams); + if (ZSTD_rowMatchFinderSupported(cParams.strategy)) { + /* Pick bigger of not using and using row-based matchfinder for greedy and lazy strategies */ + size_t noRowCCtxSize; + size_t rowCCtxSize; + initialParams.useRowMatchFinder = ZSTD_ps_disable; + noRowCCtxSize = ZSTD_estimateCStreamSize_usingCCtxParams(&initialParams); + initialParams.useRowMatchFinder = ZSTD_ps_enable; + rowCCtxSize = ZSTD_estimateCStreamSize_usingCCtxParams(&initialParams); + return MAX(noRowCCtxSize, rowCCtxSize); + } else { + return ZSTD_estimateCStreamSize_usingCCtxParams(&initialParams); + } +} + +static size_t ZSTD_estimateCStreamSize_internal(int compressionLevel) +{ + ZSTD_compressionParameters const cParams = ZSTD_getCParams_internal(compressionLevel, ZSTD_CONTENTSIZE_UNKNOWN, 0, ZSTD_cpm_noAttachDict); + return ZSTD_estimateCStreamSize_usingCParams(cParams); +} + +size_t ZSTD_estimateCStreamSize(int compressionLevel) +{ + int level; + size_t memBudget = 0; + for (level=MIN(compressionLevel, 1); level<=compressionLevel; level++) { + size_t const newMB = ZSTD_estimateCStreamSize_internal(level); + if (newMB > memBudget) memBudget = newMB; + } + return memBudget; +} + +/* ZSTD_getFrameProgression(): + * tells how much data has been consumed (input) and produced (output) for current frame. + * able to count progression inside worker threads (non-blocking mode). + */ +ZSTD_frameProgression ZSTD_getFrameProgression(const ZSTD_CCtx* cctx) +{ +#ifdef ZSTD_MULTITHREAD + if (cctx->appliedParams.nbWorkers > 0) { + return ZSTDMT_getFrameProgression(cctx->mtctx); + } +#endif + { ZSTD_frameProgression fp; + size_t const buffered = (cctx->inBuff == NULL) ? 0 : + cctx->inBuffPos - cctx->inToCompress; + if (buffered) assert(cctx->inBuffPos >= cctx->inToCompress); + assert(buffered <= ZSTD_BLOCKSIZE_MAX); + fp.ingested = cctx->consumedSrcSize + buffered; + fp.consumed = cctx->consumedSrcSize; + fp.produced = cctx->producedCSize; + fp.flushed = cctx->producedCSize; /* simplified; some data might still be left within streaming output buffer */ + fp.currentJobID = 0; + fp.nbActiveWorkers = 0; + return fp; +} } + +/*! ZSTD_toFlushNow() + * Only useful for multithreading scenarios currently (nbWorkers >= 1). + */ +size_t ZSTD_toFlushNow(ZSTD_CCtx* cctx) +{ +#ifdef ZSTD_MULTITHREAD + if (cctx->appliedParams.nbWorkers > 0) { + return ZSTDMT_toFlushNow(cctx->mtctx); + } +#endif + (void)cctx; + return 0; /* over-simplification; could also check if context is currently running in streaming mode, and in which case, report how many bytes are left to be flushed within output buffer */ +} + +static void ZSTD_assertEqualCParams(ZSTD_compressionParameters cParams1, + ZSTD_compressionParameters cParams2) +{ + (void)cParams1; + (void)cParams2; + assert(cParams1.windowLog == cParams2.windowLog); + assert(cParams1.chainLog == cParams2.chainLog); + assert(cParams1.hashLog == cParams2.hashLog); + assert(cParams1.searchLog == cParams2.searchLog); + assert(cParams1.minMatch == cParams2.minMatch); + assert(cParams1.targetLength == cParams2.targetLength); + assert(cParams1.strategy == cParams2.strategy); +} + +void ZSTD_reset_compressedBlockState(ZSTD_compressedBlockState_t* bs) +{ + int i; + for (i = 0; i < ZSTD_REP_NUM; ++i) + bs->rep[i] = repStartValue[i]; + bs->entropy.huf.repeatMode = HUF_repeat_none; + bs->entropy.fse.offcode_repeatMode = FSE_repeat_none; + bs->entropy.fse.matchlength_repeatMode = FSE_repeat_none; + bs->entropy.fse.litlength_repeatMode = FSE_repeat_none; +} + +/*! ZSTD_invalidateMatchState() + * Invalidate all the matches in the match finder tables. + * Requires nextSrc and base to be set (can be NULL). + */ +static void ZSTD_invalidateMatchState(ZSTD_matchState_t* ms) +{ + ZSTD_window_clear(&ms->window); + + ms->nextToUpdate = ms->window.dictLimit; + ms->loadedDictEnd = 0; + ms->opt.litLengthSum = 0; /* force reset of btopt stats */ + ms->dictMatchState = NULL; +} + +/** + * Controls, for this matchState reset, whether the tables need to be cleared / + * prepared for the coming compression (ZSTDcrp_makeClean), or whether the + * tables can be left unclean (ZSTDcrp_leaveDirty), because we know that a + * subsequent operation will overwrite the table space anyways (e.g., copying + * the matchState contents in from a CDict). + */ +typedef enum { + ZSTDcrp_makeClean, + ZSTDcrp_leaveDirty +} ZSTD_compResetPolicy_e; + +/** + * Controls, for this matchState reset, whether indexing can continue where it + * left off (ZSTDirp_continue), or whether it needs to be restarted from zero + * (ZSTDirp_reset). + */ +typedef enum { + ZSTDirp_continue, + ZSTDirp_reset +} ZSTD_indexResetPolicy_e; + +typedef enum { + ZSTD_resetTarget_CDict, + ZSTD_resetTarget_CCtx +} ZSTD_resetTarget_e; + +/* Mixes bits in a 64 bits in a value, based on XXH3_rrmxmx */ +static U64 ZSTD_bitmix(U64 val, U64 len) { + val ^= ZSTD_rotateRight_U64(val, 49) ^ ZSTD_rotateRight_U64(val, 24); + val *= 0x9FB21C651E98DF25ULL; + val ^= (val >> 35) + len ; + val *= 0x9FB21C651E98DF25ULL; + return val ^ (val >> 28); +} + +/* Mixes in the hashSalt and hashSaltEntropy to create a new hashSalt */ +static void ZSTD_advanceHashSalt(ZSTD_matchState_t* ms) { + ms->hashSalt = ZSTD_bitmix(ms->hashSalt, 8) ^ ZSTD_bitmix((U64) ms->hashSaltEntropy, 4); +} + +static size_t +ZSTD_reset_matchState(ZSTD_matchState_t* ms, + ZSTD_cwksp* ws, + const ZSTD_compressionParameters* cParams, + const ZSTD_paramSwitch_e useRowMatchFinder, + const ZSTD_compResetPolicy_e crp, + const ZSTD_indexResetPolicy_e forceResetIndex, + const ZSTD_resetTarget_e forWho) +{ + /* disable chain table allocation for fast or row-based strategies */ + size_t const chainSize = ZSTD_allocateChainTable(cParams->strategy, useRowMatchFinder, + ms->dedicatedDictSearch && (forWho == ZSTD_resetTarget_CDict)) + ? ((size_t)1 << cParams->chainLog) + : 0; + size_t const hSize = ((size_t)1) << cParams->hashLog; + U32 const hashLog3 = ((forWho == ZSTD_resetTarget_CCtx) && cParams->minMatch==3) ? MIN(ZSTD_HASHLOG3_MAX, cParams->windowLog) : 0; + size_t const h3Size = hashLog3 ? ((size_t)1) << hashLog3 : 0; + + DEBUGLOG(4, "reset indices : %u", forceResetIndex == ZSTDirp_reset); + assert(useRowMatchFinder != ZSTD_ps_auto); + if (forceResetIndex == ZSTDirp_reset) { + ZSTD_window_init(&ms->window); + ZSTD_cwksp_mark_tables_dirty(ws); + } + + ms->hashLog3 = hashLog3; + ms->lazySkipping = 0; + + ZSTD_invalidateMatchState(ms); + + assert(!ZSTD_cwksp_reserve_failed(ws)); /* check that allocation hasn't already failed */ + + ZSTD_cwksp_clear_tables(ws); + + DEBUGLOG(5, "reserving table space"); + /* table Space */ + ms->hashTable = (U32*)ZSTD_cwksp_reserve_table(ws, hSize * sizeof(U32)); + ms->chainTable = (U32*)ZSTD_cwksp_reserve_table(ws, chainSize * sizeof(U32)); + ms->hashTable3 = (U32*)ZSTD_cwksp_reserve_table(ws, h3Size * sizeof(U32)); + RETURN_ERROR_IF(ZSTD_cwksp_reserve_failed(ws), memory_allocation, + "failed a workspace allocation in ZSTD_reset_matchState"); + + DEBUGLOG(4, "reset table : %u", crp!=ZSTDcrp_leaveDirty); + if (crp!=ZSTDcrp_leaveDirty) { + /* reset tables only */ + ZSTD_cwksp_clean_tables(ws); + } + + if (ZSTD_rowMatchFinderUsed(cParams->strategy, useRowMatchFinder)) { + /* Row match finder needs an additional table of hashes ("tags") */ + size_t const tagTableSize = hSize; + /* We want to generate a new salt in case we reset a Cctx, but we always want to use + * 0 when we reset a Cdict */ + if(forWho == ZSTD_resetTarget_CCtx) { + ms->tagTable = (BYTE*) ZSTD_cwksp_reserve_aligned_init_once(ws, tagTableSize); + ZSTD_advanceHashSalt(ms); + } else { + /* When we are not salting we want to always memset the memory */ + ms->tagTable = (BYTE*) ZSTD_cwksp_reserve_aligned(ws, tagTableSize); + ZSTD_memset(ms->tagTable, 0, tagTableSize); + ms->hashSalt = 0; + } + { /* Switch to 32-entry rows if searchLog is 5 (or more) */ + U32 const rowLog = BOUNDED(4, cParams->searchLog, 6); + assert(cParams->hashLog >= rowLog); + ms->rowHashLog = cParams->hashLog - rowLog; + } + } + + /* opt parser space */ + if ((forWho == ZSTD_resetTarget_CCtx) && (cParams->strategy >= ZSTD_btopt)) { + DEBUGLOG(4, "reserving optimal parser space"); + ms->opt.litFreq = (unsigned*)ZSTD_cwksp_reserve_aligned(ws, (1<opt.litLengthFreq = (unsigned*)ZSTD_cwksp_reserve_aligned(ws, (MaxLL+1) * sizeof(unsigned)); + ms->opt.matchLengthFreq = (unsigned*)ZSTD_cwksp_reserve_aligned(ws, (MaxML+1) * sizeof(unsigned)); + ms->opt.offCodeFreq = (unsigned*)ZSTD_cwksp_reserve_aligned(ws, (MaxOff+1) * sizeof(unsigned)); + ms->opt.matchTable = (ZSTD_match_t*)ZSTD_cwksp_reserve_aligned(ws, ZSTD_OPT_SIZE * sizeof(ZSTD_match_t)); + ms->opt.priceTable = (ZSTD_optimal_t*)ZSTD_cwksp_reserve_aligned(ws, ZSTD_OPT_SIZE * sizeof(ZSTD_optimal_t)); + } + + ms->cParams = *cParams; + + RETURN_ERROR_IF(ZSTD_cwksp_reserve_failed(ws), memory_allocation, + "failed a workspace allocation in ZSTD_reset_matchState"); + return 0; +} + +/* ZSTD_indexTooCloseToMax() : + * minor optimization : prefer memset() rather than reduceIndex() + * which is measurably slow in some circumstances (reported for Visual Studio). + * Works when re-using a context for a lot of smallish inputs : + * if all inputs are smaller than ZSTD_INDEXOVERFLOW_MARGIN, + * memset() will be triggered before reduceIndex(). + */ +#define ZSTD_INDEXOVERFLOW_MARGIN (16 MB) +static int ZSTD_indexTooCloseToMax(ZSTD_window_t w) +{ + return (size_t)(w.nextSrc - w.base) > (ZSTD_CURRENT_MAX - ZSTD_INDEXOVERFLOW_MARGIN); +} + +/** ZSTD_dictTooBig(): + * When dictionaries are larger than ZSTD_CHUNKSIZE_MAX they can't be loaded in + * one go generically. So we ensure that in that case we reset the tables to zero, + * so that we can load as much of the dictionary as possible. + */ +static int ZSTD_dictTooBig(size_t const loadedDictSize) +{ + return loadedDictSize > ZSTD_CHUNKSIZE_MAX; +} + +/*! ZSTD_resetCCtx_internal() : + * @param loadedDictSize The size of the dictionary to be loaded + * into the context, if any. If no dictionary is used, or the + * dictionary is being attached / copied, then pass 0. + * note : `params` are assumed fully validated at this stage. + */ +static size_t ZSTD_resetCCtx_internal(ZSTD_CCtx* zc, + ZSTD_CCtx_params const* params, + U64 const pledgedSrcSize, + size_t const loadedDictSize, + ZSTD_compResetPolicy_e const crp, + ZSTD_buffered_policy_e const zbuff) +{ + ZSTD_cwksp* const ws = &zc->workspace; + DEBUGLOG(4, "ZSTD_resetCCtx_internal: pledgedSrcSize=%u, wlog=%u, useRowMatchFinder=%d useBlockSplitter=%d", + (U32)pledgedSrcSize, params->cParams.windowLog, (int)params->useRowMatchFinder, (int)params->useBlockSplitter); + assert(!ZSTD_isError(ZSTD_checkCParams(params->cParams))); + + zc->isFirstBlock = 1; + + /* Set applied params early so we can modify them for LDM, + * and point params at the applied params. + */ + zc->appliedParams = *params; + params = &zc->appliedParams; + + assert(params->useRowMatchFinder != ZSTD_ps_auto); + assert(params->useBlockSplitter != ZSTD_ps_auto); + assert(params->ldmParams.enableLdm != ZSTD_ps_auto); + assert(params->maxBlockSize != 0); + if (params->ldmParams.enableLdm == ZSTD_ps_enable) { + /* Adjust long distance matching parameters */ + ZSTD_ldm_adjustParameters(&zc->appliedParams.ldmParams, ¶ms->cParams); + assert(params->ldmParams.hashLog >= params->ldmParams.bucketSizeLog); + assert(params->ldmParams.hashRateLog < 32); + } + + { size_t const windowSize = MAX(1, (size_t)MIN(((U64)1 << params->cParams.windowLog), pledgedSrcSize)); + size_t const blockSize = MIN(params->maxBlockSize, windowSize); + size_t const maxNbSeq = ZSTD_maxNbSeq(blockSize, params->cParams.minMatch, ZSTD_hasExtSeqProd(params)); + size_t const buffOutSize = (zbuff == ZSTDb_buffered && params->outBufferMode == ZSTD_bm_buffered) + ? ZSTD_compressBound(blockSize) + 1 + : 0; + size_t const buffInSize = (zbuff == ZSTDb_buffered && params->inBufferMode == ZSTD_bm_buffered) + ? windowSize + blockSize + : 0; + size_t const maxNbLdmSeq = ZSTD_ldm_getMaxNbSeq(params->ldmParams, blockSize); + + int const indexTooClose = ZSTD_indexTooCloseToMax(zc->blockState.matchState.window); + int const dictTooBig = ZSTD_dictTooBig(loadedDictSize); + ZSTD_indexResetPolicy_e needsIndexReset = + (indexTooClose || dictTooBig || !zc->initialized) ? ZSTDirp_reset : ZSTDirp_continue; + + size_t const neededSpace = + ZSTD_estimateCCtxSize_usingCCtxParams_internal( + ¶ms->cParams, ¶ms->ldmParams, zc->staticSize != 0, params->useRowMatchFinder, + buffInSize, buffOutSize, pledgedSrcSize, ZSTD_hasExtSeqProd(params), params->maxBlockSize); + + FORWARD_IF_ERROR(neededSpace, "cctx size estimate failed!"); + + if (!zc->staticSize) ZSTD_cwksp_bump_oversized_duration(ws, 0); + + { /* Check if workspace is large enough, alloc a new one if needed */ + int const workspaceTooSmall = ZSTD_cwksp_sizeof(ws) < neededSpace; + int const workspaceWasteful = ZSTD_cwksp_check_wasteful(ws, neededSpace); + int resizeWorkspace = workspaceTooSmall || workspaceWasteful; + DEBUGLOG(4, "Need %zu B workspace", neededSpace); + DEBUGLOG(4, "windowSize: %zu - blockSize: %zu", windowSize, blockSize); + + if (resizeWorkspace) { + DEBUGLOG(4, "Resize workspaceSize from %zuKB to %zuKB", + ZSTD_cwksp_sizeof(ws) >> 10, + neededSpace >> 10); + + RETURN_ERROR_IF(zc->staticSize, memory_allocation, "static cctx : no resize"); + + needsIndexReset = ZSTDirp_reset; + + ZSTD_cwksp_free(ws, zc->customMem); + FORWARD_IF_ERROR(ZSTD_cwksp_create(ws, neededSpace, zc->customMem), ""); + + DEBUGLOG(5, "reserving object space"); + /* Statically sized space. + * entropyWorkspace never moves, + * though prev/next block swap places */ + assert(ZSTD_cwksp_check_available(ws, 2 * sizeof(ZSTD_compressedBlockState_t))); + zc->blockState.prevCBlock = (ZSTD_compressedBlockState_t*) ZSTD_cwksp_reserve_object(ws, sizeof(ZSTD_compressedBlockState_t)); + RETURN_ERROR_IF(zc->blockState.prevCBlock == NULL, memory_allocation, "couldn't allocate prevCBlock"); + zc->blockState.nextCBlock = (ZSTD_compressedBlockState_t*) ZSTD_cwksp_reserve_object(ws, sizeof(ZSTD_compressedBlockState_t)); + RETURN_ERROR_IF(zc->blockState.nextCBlock == NULL, memory_allocation, "couldn't allocate nextCBlock"); + zc->entropyWorkspace = (U32*) ZSTD_cwksp_reserve_object(ws, ENTROPY_WORKSPACE_SIZE); + RETURN_ERROR_IF(zc->entropyWorkspace == NULL, memory_allocation, "couldn't allocate entropyWorkspace"); + } } + + ZSTD_cwksp_clear(ws); + + /* init params */ + zc->blockState.matchState.cParams = params->cParams; + zc->blockState.matchState.prefetchCDictTables = params->prefetchCDictTables == ZSTD_ps_enable; + zc->pledgedSrcSizePlusOne = pledgedSrcSize+1; + zc->consumedSrcSize = 0; + zc->producedCSize = 0; + if (pledgedSrcSize == ZSTD_CONTENTSIZE_UNKNOWN) + zc->appliedParams.fParams.contentSizeFlag = 0; + DEBUGLOG(4, "pledged content size : %u ; flag : %u", + (unsigned)pledgedSrcSize, zc->appliedParams.fParams.contentSizeFlag); + zc->blockSize = blockSize; + + XXH64_reset(&zc->xxhState, 0); + zc->stage = ZSTDcs_init; + zc->dictID = 0; + zc->dictContentSize = 0; + + ZSTD_reset_compressedBlockState(zc->blockState.prevCBlock); + + FORWARD_IF_ERROR(ZSTD_reset_matchState( + &zc->blockState.matchState, + ws, + ¶ms->cParams, + params->useRowMatchFinder, + crp, + needsIndexReset, + ZSTD_resetTarget_CCtx), ""); + + zc->seqStore.sequencesStart = (seqDef*)ZSTD_cwksp_reserve_aligned(ws, maxNbSeq * sizeof(seqDef)); + + /* ldm hash table */ + if (params->ldmParams.enableLdm == ZSTD_ps_enable) { + /* TODO: avoid memset? */ + size_t const ldmHSize = ((size_t)1) << params->ldmParams.hashLog; + zc->ldmState.hashTable = (ldmEntry_t*)ZSTD_cwksp_reserve_aligned(ws, ldmHSize * sizeof(ldmEntry_t)); + ZSTD_memset(zc->ldmState.hashTable, 0, ldmHSize * sizeof(ldmEntry_t)); + zc->ldmSequences = (rawSeq*)ZSTD_cwksp_reserve_aligned(ws, maxNbLdmSeq * sizeof(rawSeq)); + zc->maxNbLdmSequences = maxNbLdmSeq; + + ZSTD_window_init(&zc->ldmState.window); + zc->ldmState.loadedDictEnd = 0; + } + + /* reserve space for block-level external sequences */ + if (ZSTD_hasExtSeqProd(params)) { + size_t const maxNbExternalSeq = ZSTD_sequenceBound(blockSize); + zc->extSeqBufCapacity = maxNbExternalSeq; + zc->extSeqBuf = + (ZSTD_Sequence*)ZSTD_cwksp_reserve_aligned(ws, maxNbExternalSeq * sizeof(ZSTD_Sequence)); + } + + /* buffers */ + + /* ZSTD_wildcopy() is used to copy into the literals buffer, + * so we have to oversize the buffer by WILDCOPY_OVERLENGTH bytes. + */ + zc->seqStore.litStart = ZSTD_cwksp_reserve_buffer(ws, blockSize + WILDCOPY_OVERLENGTH); + zc->seqStore.maxNbLit = blockSize; + + zc->bufferedPolicy = zbuff; + zc->inBuffSize = buffInSize; + zc->inBuff = (char*)ZSTD_cwksp_reserve_buffer(ws, buffInSize); + zc->outBuffSize = buffOutSize; + zc->outBuff = (char*)ZSTD_cwksp_reserve_buffer(ws, buffOutSize); + + /* ldm bucketOffsets table */ + if (params->ldmParams.enableLdm == ZSTD_ps_enable) { + /* TODO: avoid memset? */ + size_t const numBuckets = + ((size_t)1) << (params->ldmParams.hashLog - + params->ldmParams.bucketSizeLog); + zc->ldmState.bucketOffsets = ZSTD_cwksp_reserve_buffer(ws, numBuckets); + ZSTD_memset(zc->ldmState.bucketOffsets, 0, numBuckets); + } + + /* sequences storage */ + ZSTD_referenceExternalSequences(zc, NULL, 0); + zc->seqStore.maxNbSeq = maxNbSeq; + zc->seqStore.llCode = ZSTD_cwksp_reserve_buffer(ws, maxNbSeq * sizeof(BYTE)); + zc->seqStore.mlCode = ZSTD_cwksp_reserve_buffer(ws, maxNbSeq * sizeof(BYTE)); + zc->seqStore.ofCode = ZSTD_cwksp_reserve_buffer(ws, maxNbSeq * sizeof(BYTE)); + + DEBUGLOG(3, "wksp: finished allocating, %zd bytes remain available", ZSTD_cwksp_available_space(ws)); + assert(ZSTD_cwksp_estimated_space_within_bounds(ws, neededSpace)); + + zc->initialized = 1; + + return 0; + } +} + +/* ZSTD_invalidateRepCodes() : + * ensures next compression will not use repcodes from previous block. + * Note : only works with regular variant; + * do not use with extDict variant ! */ +void ZSTD_invalidateRepCodes(ZSTD_CCtx* cctx) { + int i; + for (i=0; iblockState.prevCBlock->rep[i] = 0; + assert(!ZSTD_window_hasExtDict(cctx->blockState.matchState.window)); +} + +/* These are the approximate sizes for each strategy past which copying the + * dictionary tables into the working context is faster than using them + * in-place. + */ +static const size_t attachDictSizeCutoffs[ZSTD_STRATEGY_MAX+1] = { + 8 KB, /* unused */ + 8 KB, /* ZSTD_fast */ + 16 KB, /* ZSTD_dfast */ + 32 KB, /* ZSTD_greedy */ + 32 KB, /* ZSTD_lazy */ + 32 KB, /* ZSTD_lazy2 */ + 32 KB, /* ZSTD_btlazy2 */ + 32 KB, /* ZSTD_btopt */ + 8 KB, /* ZSTD_btultra */ + 8 KB /* ZSTD_btultra2 */ +}; + +static int ZSTD_shouldAttachDict(const ZSTD_CDict* cdict, + const ZSTD_CCtx_params* params, + U64 pledgedSrcSize) +{ + size_t cutoff = attachDictSizeCutoffs[cdict->matchState.cParams.strategy]; + int const dedicatedDictSearch = cdict->matchState.dedicatedDictSearch; + return dedicatedDictSearch + || ( ( pledgedSrcSize <= cutoff + || pledgedSrcSize == ZSTD_CONTENTSIZE_UNKNOWN + || params->attachDictPref == ZSTD_dictForceAttach ) + && params->attachDictPref != ZSTD_dictForceCopy + && !params->forceWindow ); /* dictMatchState isn't correctly + * handled in _enforceMaxDist */ +} + +static size_t +ZSTD_resetCCtx_byAttachingCDict(ZSTD_CCtx* cctx, + const ZSTD_CDict* cdict, + ZSTD_CCtx_params params, + U64 pledgedSrcSize, + ZSTD_buffered_policy_e zbuff) +{ + DEBUGLOG(4, "ZSTD_resetCCtx_byAttachingCDict() pledgedSrcSize=%llu", + (unsigned long long)pledgedSrcSize); + { + ZSTD_compressionParameters adjusted_cdict_cParams = cdict->matchState.cParams; + unsigned const windowLog = params.cParams.windowLog; + assert(windowLog != 0); + /* Resize working context table params for input only, since the dict + * has its own tables. */ + /* pledgedSrcSize == 0 means 0! */ + + if (cdict->matchState.dedicatedDictSearch) { + ZSTD_dedicatedDictSearch_revertCParams(&adjusted_cdict_cParams); + } + + params.cParams = ZSTD_adjustCParams_internal(adjusted_cdict_cParams, pledgedSrcSize, + cdict->dictContentSize, ZSTD_cpm_attachDict, + params.useRowMatchFinder); + params.cParams.windowLog = windowLog; + params.useRowMatchFinder = cdict->useRowMatchFinder; /* cdict overrides */ + FORWARD_IF_ERROR(ZSTD_resetCCtx_internal(cctx, ¶ms, pledgedSrcSize, + /* loadedDictSize */ 0, + ZSTDcrp_makeClean, zbuff), ""); + assert(cctx->appliedParams.cParams.strategy == adjusted_cdict_cParams.strategy); + } + + { const U32 cdictEnd = (U32)( cdict->matchState.window.nextSrc + - cdict->matchState.window.base); + const U32 cdictLen = cdictEnd - cdict->matchState.window.dictLimit; + if (cdictLen == 0) { + /* don't even attach dictionaries with no contents */ + DEBUGLOG(4, "skipping attaching empty dictionary"); + } else { + DEBUGLOG(4, "attaching dictionary into context"); + cctx->blockState.matchState.dictMatchState = &cdict->matchState; + + /* prep working match state so dict matches never have negative indices + * when they are translated to the working context's index space. */ + if (cctx->blockState.matchState.window.dictLimit < cdictEnd) { + cctx->blockState.matchState.window.nextSrc = + cctx->blockState.matchState.window.base + cdictEnd; + ZSTD_window_clear(&cctx->blockState.matchState.window); + } + /* loadedDictEnd is expressed within the referential of the active context */ + cctx->blockState.matchState.loadedDictEnd = cctx->blockState.matchState.window.dictLimit; + } } + + cctx->dictID = cdict->dictID; + cctx->dictContentSize = cdict->dictContentSize; + + /* copy block state */ + ZSTD_memcpy(cctx->blockState.prevCBlock, &cdict->cBlockState, sizeof(cdict->cBlockState)); + + return 0; +} + +static void ZSTD_copyCDictTableIntoCCtx(U32* dst, U32 const* src, size_t tableSize, + ZSTD_compressionParameters const* cParams) { + if (ZSTD_CDictIndicesAreTagged(cParams)){ + /* Remove tags from the CDict table if they are present. + * See docs on "short cache" in zstd_compress_internal.h for context. */ + size_t i; + for (i = 0; i < tableSize; i++) { + U32 const taggedIndex = src[i]; + U32 const index = taggedIndex >> ZSTD_SHORT_CACHE_TAG_BITS; + dst[i] = index; + } + } else { + ZSTD_memcpy(dst, src, tableSize * sizeof(U32)); + } +} + +static size_t ZSTD_resetCCtx_byCopyingCDict(ZSTD_CCtx* cctx, + const ZSTD_CDict* cdict, + ZSTD_CCtx_params params, + U64 pledgedSrcSize, + ZSTD_buffered_policy_e zbuff) +{ + const ZSTD_compressionParameters *cdict_cParams = &cdict->matchState.cParams; + + assert(!cdict->matchState.dedicatedDictSearch); + DEBUGLOG(4, "ZSTD_resetCCtx_byCopyingCDict() pledgedSrcSize=%llu", + (unsigned long long)pledgedSrcSize); + + { unsigned const windowLog = params.cParams.windowLog; + assert(windowLog != 0); + /* Copy only compression parameters related to tables. */ + params.cParams = *cdict_cParams; + params.cParams.windowLog = windowLog; + params.useRowMatchFinder = cdict->useRowMatchFinder; + FORWARD_IF_ERROR(ZSTD_resetCCtx_internal(cctx, ¶ms, pledgedSrcSize, + /* loadedDictSize */ 0, + ZSTDcrp_leaveDirty, zbuff), ""); + assert(cctx->appliedParams.cParams.strategy == cdict_cParams->strategy); + assert(cctx->appliedParams.cParams.hashLog == cdict_cParams->hashLog); + assert(cctx->appliedParams.cParams.chainLog == cdict_cParams->chainLog); + } + + ZSTD_cwksp_mark_tables_dirty(&cctx->workspace); + assert(params.useRowMatchFinder != ZSTD_ps_auto); + + /* copy tables */ + { size_t const chainSize = ZSTD_allocateChainTable(cdict_cParams->strategy, cdict->useRowMatchFinder, 0 /* DDS guaranteed disabled */) + ? ((size_t)1 << cdict_cParams->chainLog) + : 0; + size_t const hSize = (size_t)1 << cdict_cParams->hashLog; + + ZSTD_copyCDictTableIntoCCtx(cctx->blockState.matchState.hashTable, + cdict->matchState.hashTable, + hSize, cdict_cParams); + + /* Do not copy cdict's chainTable if cctx has parameters such that it would not use chainTable */ + if (ZSTD_allocateChainTable(cctx->appliedParams.cParams.strategy, cctx->appliedParams.useRowMatchFinder, 0 /* forDDSDict */)) { + ZSTD_copyCDictTableIntoCCtx(cctx->blockState.matchState.chainTable, + cdict->matchState.chainTable, + chainSize, cdict_cParams); + } + /* copy tag table */ + if (ZSTD_rowMatchFinderUsed(cdict_cParams->strategy, cdict->useRowMatchFinder)) { + size_t const tagTableSize = hSize; + ZSTD_memcpy(cctx->blockState.matchState.tagTable, + cdict->matchState.tagTable, + tagTableSize); + cctx->blockState.matchState.hashSalt = cdict->matchState.hashSalt; + } + } + + /* Zero the hashTable3, since the cdict never fills it */ + { int const h3log = cctx->blockState.matchState.hashLog3; + size_t const h3Size = h3log ? ((size_t)1 << h3log) : 0; + assert(cdict->matchState.hashLog3 == 0); + ZSTD_memset(cctx->blockState.matchState.hashTable3, 0, h3Size * sizeof(U32)); + } + + ZSTD_cwksp_mark_tables_clean(&cctx->workspace); + + /* copy dictionary offsets */ + { ZSTD_matchState_t const* srcMatchState = &cdict->matchState; + ZSTD_matchState_t* dstMatchState = &cctx->blockState.matchState; + dstMatchState->window = srcMatchState->window; + dstMatchState->nextToUpdate = srcMatchState->nextToUpdate; + dstMatchState->loadedDictEnd= srcMatchState->loadedDictEnd; + } + + cctx->dictID = cdict->dictID; + cctx->dictContentSize = cdict->dictContentSize; + + /* copy block state */ + ZSTD_memcpy(cctx->blockState.prevCBlock, &cdict->cBlockState, sizeof(cdict->cBlockState)); + + return 0; +} + +/* We have a choice between copying the dictionary context into the working + * context, or referencing the dictionary context from the working context + * in-place. We decide here which strategy to use. */ +static size_t ZSTD_resetCCtx_usingCDict(ZSTD_CCtx* cctx, + const ZSTD_CDict* cdict, + const ZSTD_CCtx_params* params, + U64 pledgedSrcSize, + ZSTD_buffered_policy_e zbuff) +{ + + DEBUGLOG(4, "ZSTD_resetCCtx_usingCDict (pledgedSrcSize=%u)", + (unsigned)pledgedSrcSize); + + if (ZSTD_shouldAttachDict(cdict, params, pledgedSrcSize)) { + return ZSTD_resetCCtx_byAttachingCDict( + cctx, cdict, *params, pledgedSrcSize, zbuff); + } else { + return ZSTD_resetCCtx_byCopyingCDict( + cctx, cdict, *params, pledgedSrcSize, zbuff); + } +} + +/*! ZSTD_copyCCtx_internal() : + * Duplicate an existing context `srcCCtx` into another one `dstCCtx`. + * Only works during stage ZSTDcs_init (i.e. after creation, but before first call to ZSTD_compressContinue()). + * The "context", in this case, refers to the hash and chain tables, + * entropy tables, and dictionary references. + * `windowLog` value is enforced if != 0, otherwise value is copied from srcCCtx. + * @return : 0, or an error code */ +static size_t ZSTD_copyCCtx_internal(ZSTD_CCtx* dstCCtx, + const ZSTD_CCtx* srcCCtx, + ZSTD_frameParameters fParams, + U64 pledgedSrcSize, + ZSTD_buffered_policy_e zbuff) +{ + RETURN_ERROR_IF(srcCCtx->stage!=ZSTDcs_init, stage_wrong, + "Can't copy a ctx that's not in init stage."); + DEBUGLOG(5, "ZSTD_copyCCtx_internal"); + ZSTD_memcpy(&dstCCtx->customMem, &srcCCtx->customMem, sizeof(ZSTD_customMem)); + { ZSTD_CCtx_params params = dstCCtx->requestedParams; + /* Copy only compression parameters related to tables. */ + params.cParams = srcCCtx->appliedParams.cParams; + assert(srcCCtx->appliedParams.useRowMatchFinder != ZSTD_ps_auto); + assert(srcCCtx->appliedParams.useBlockSplitter != ZSTD_ps_auto); + assert(srcCCtx->appliedParams.ldmParams.enableLdm != ZSTD_ps_auto); + params.useRowMatchFinder = srcCCtx->appliedParams.useRowMatchFinder; + params.useBlockSplitter = srcCCtx->appliedParams.useBlockSplitter; + params.ldmParams = srcCCtx->appliedParams.ldmParams; + params.fParams = fParams; + params.maxBlockSize = srcCCtx->appliedParams.maxBlockSize; + ZSTD_resetCCtx_internal(dstCCtx, ¶ms, pledgedSrcSize, + /* loadedDictSize */ 0, + ZSTDcrp_leaveDirty, zbuff); + assert(dstCCtx->appliedParams.cParams.windowLog == srcCCtx->appliedParams.cParams.windowLog); + assert(dstCCtx->appliedParams.cParams.strategy == srcCCtx->appliedParams.cParams.strategy); + assert(dstCCtx->appliedParams.cParams.hashLog == srcCCtx->appliedParams.cParams.hashLog); + assert(dstCCtx->appliedParams.cParams.chainLog == srcCCtx->appliedParams.cParams.chainLog); + assert(dstCCtx->blockState.matchState.hashLog3 == srcCCtx->blockState.matchState.hashLog3); + } + + ZSTD_cwksp_mark_tables_dirty(&dstCCtx->workspace); + + /* copy tables */ + { size_t const chainSize = ZSTD_allocateChainTable(srcCCtx->appliedParams.cParams.strategy, + srcCCtx->appliedParams.useRowMatchFinder, + 0 /* forDDSDict */) + ? ((size_t)1 << srcCCtx->appliedParams.cParams.chainLog) + : 0; + size_t const hSize = (size_t)1 << srcCCtx->appliedParams.cParams.hashLog; + int const h3log = srcCCtx->blockState.matchState.hashLog3; + size_t const h3Size = h3log ? ((size_t)1 << h3log) : 0; + + ZSTD_memcpy(dstCCtx->blockState.matchState.hashTable, + srcCCtx->blockState.matchState.hashTable, + hSize * sizeof(U32)); + ZSTD_memcpy(dstCCtx->blockState.matchState.chainTable, + srcCCtx->blockState.matchState.chainTable, + chainSize * sizeof(U32)); + ZSTD_memcpy(dstCCtx->blockState.matchState.hashTable3, + srcCCtx->blockState.matchState.hashTable3, + h3Size * sizeof(U32)); + } + + ZSTD_cwksp_mark_tables_clean(&dstCCtx->workspace); + + /* copy dictionary offsets */ + { + const ZSTD_matchState_t* srcMatchState = &srcCCtx->blockState.matchState; + ZSTD_matchState_t* dstMatchState = &dstCCtx->blockState.matchState; + dstMatchState->window = srcMatchState->window; + dstMatchState->nextToUpdate = srcMatchState->nextToUpdate; + dstMatchState->loadedDictEnd= srcMatchState->loadedDictEnd; + } + dstCCtx->dictID = srcCCtx->dictID; + dstCCtx->dictContentSize = srcCCtx->dictContentSize; + + /* copy block state */ + ZSTD_memcpy(dstCCtx->blockState.prevCBlock, srcCCtx->blockState.prevCBlock, sizeof(*srcCCtx->blockState.prevCBlock)); + + return 0; +} + +/*! ZSTD_copyCCtx() : + * Duplicate an existing context `srcCCtx` into another one `dstCCtx`. + * Only works during stage ZSTDcs_init (i.e. after creation, but before first call to ZSTD_compressContinue()). + * pledgedSrcSize==0 means "unknown". +* @return : 0, or an error code */ +size_t ZSTD_copyCCtx(ZSTD_CCtx* dstCCtx, const ZSTD_CCtx* srcCCtx, unsigned long long pledgedSrcSize) +{ + ZSTD_frameParameters fParams = { 1 /*content*/, 0 /*checksum*/, 0 /*noDictID*/ }; + ZSTD_buffered_policy_e const zbuff = srcCCtx->bufferedPolicy; + ZSTD_STATIC_ASSERT((U32)ZSTDb_buffered==1); + if (pledgedSrcSize==0) pledgedSrcSize = ZSTD_CONTENTSIZE_UNKNOWN; + fParams.contentSizeFlag = (pledgedSrcSize != ZSTD_CONTENTSIZE_UNKNOWN); + + return ZSTD_copyCCtx_internal(dstCCtx, srcCCtx, + fParams, pledgedSrcSize, + zbuff); +} + + +#define ZSTD_ROWSIZE 16 +/*! ZSTD_reduceTable() : + * reduce table indexes by `reducerValue`, or squash to zero. + * PreserveMark preserves "unsorted mark" for btlazy2 strategy. + * It must be set to a clear 0/1 value, to remove branch during inlining. + * Presume table size is a multiple of ZSTD_ROWSIZE + * to help auto-vectorization */ +FORCE_INLINE_TEMPLATE void +ZSTD_reduceTable_internal (U32* const table, U32 const size, U32 const reducerValue, int const preserveMark) +{ + int const nbRows = (int)size / ZSTD_ROWSIZE; + int cellNb = 0; + int rowNb; + /* Protect special index values < ZSTD_WINDOW_START_INDEX. */ + U32 const reducerThreshold = reducerValue + ZSTD_WINDOW_START_INDEX; + assert((size & (ZSTD_ROWSIZE-1)) == 0); /* multiple of ZSTD_ROWSIZE */ + assert(size < (1U<<31)); /* can be casted to int */ + +#if ZSTD_MEMORY_SANITIZER && !defined (ZSTD_MSAN_DONT_POISON_WORKSPACE) + /* To validate that the table reuse logic is sound, and that we don't + * access table space that we haven't cleaned, we re-"poison" the table + * space every time we mark it dirty. + * + * This function however is intended to operate on those dirty tables and + * re-clean them. So when this function is used correctly, we can unpoison + * the memory it operated on. This introduces a blind spot though, since + * if we now try to operate on __actually__ poisoned memory, we will not + * detect that. */ + __msan_unpoison(table, size * sizeof(U32)); +#endif + + for (rowNb=0 ; rowNb < nbRows ; rowNb++) { + int column; + for (column=0; columncParams.hashLog; + ZSTD_reduceTable(ms->hashTable, hSize, reducerValue); + } + + if (ZSTD_allocateChainTable(params->cParams.strategy, params->useRowMatchFinder, (U32)ms->dedicatedDictSearch)) { + U32 const chainSize = (U32)1 << params->cParams.chainLog; + if (params->cParams.strategy == ZSTD_btlazy2) + ZSTD_reduceTable_btlazy2(ms->chainTable, chainSize, reducerValue); + else + ZSTD_reduceTable(ms->chainTable, chainSize, reducerValue); + } + + if (ms->hashLog3) { + U32 const h3Size = (U32)1 << ms->hashLog3; + ZSTD_reduceTable(ms->hashTable3, h3Size, reducerValue); + } +} + + +/*-******************************************************* +* Block entropic compression +*********************************************************/ + +/* See doc/zstd_compression_format.md for detailed format description */ + +int ZSTD_seqToCodes(const seqStore_t* seqStorePtr) +{ + const seqDef* const sequences = seqStorePtr->sequencesStart; + BYTE* const llCodeTable = seqStorePtr->llCode; + BYTE* const ofCodeTable = seqStorePtr->ofCode; + BYTE* const mlCodeTable = seqStorePtr->mlCode; + U32 const nbSeq = (U32)(seqStorePtr->sequences - seqStorePtr->sequencesStart); + U32 u; + int longOffsets = 0; + assert(nbSeq <= seqStorePtr->maxNbSeq); + for (u=0; u= STREAM_ACCUMULATOR_MIN)); + if (MEM_32bits() && ofCode >= STREAM_ACCUMULATOR_MIN) + longOffsets = 1; + } + if (seqStorePtr->longLengthType==ZSTD_llt_literalLength) + llCodeTable[seqStorePtr->longLengthPos] = MaxLL; + if (seqStorePtr->longLengthType==ZSTD_llt_matchLength) + mlCodeTable[seqStorePtr->longLengthPos] = MaxML; + return longOffsets; +} + +/* ZSTD_useTargetCBlockSize(): + * Returns if target compressed block size param is being used. + * If used, compression will do best effort to make a compressed block size to be around targetCBlockSize. + * Returns 1 if true, 0 otherwise. */ +static int ZSTD_useTargetCBlockSize(const ZSTD_CCtx_params* cctxParams) +{ + DEBUGLOG(5, "ZSTD_useTargetCBlockSize (targetCBlockSize=%zu)", cctxParams->targetCBlockSize); + return (cctxParams->targetCBlockSize != 0); +} + +/* ZSTD_blockSplitterEnabled(): + * Returns if block splitting param is being used + * If used, compression will do best effort to split a block in order to improve compression ratio. + * At the time this function is called, the parameter must be finalized. + * Returns 1 if true, 0 otherwise. */ +static int ZSTD_blockSplitterEnabled(ZSTD_CCtx_params* cctxParams) +{ + DEBUGLOG(5, "ZSTD_blockSplitterEnabled (useBlockSplitter=%d)", cctxParams->useBlockSplitter); + assert(cctxParams->useBlockSplitter != ZSTD_ps_auto); + return (cctxParams->useBlockSplitter == ZSTD_ps_enable); +} + +/* Type returned by ZSTD_buildSequencesStatistics containing finalized symbol encoding types + * and size of the sequences statistics + */ +typedef struct { + U32 LLtype; + U32 Offtype; + U32 MLtype; + size_t size; + size_t lastCountSize; /* Accounts for bug in 1.3.4. More detail in ZSTD_entropyCompressSeqStore_internal() */ + int longOffsets; +} ZSTD_symbolEncodingTypeStats_t; + +/* ZSTD_buildSequencesStatistics(): + * Returns a ZSTD_symbolEncodingTypeStats_t, or a zstd error code in the `size` field. + * Modifies `nextEntropy` to have the appropriate values as a side effect. + * nbSeq must be greater than 0. + * + * entropyWkspSize must be of size at least ENTROPY_WORKSPACE_SIZE - (MaxSeq + 1)*sizeof(U32) + */ +static ZSTD_symbolEncodingTypeStats_t +ZSTD_buildSequencesStatistics( + const seqStore_t* seqStorePtr, size_t nbSeq, + const ZSTD_fseCTables_t* prevEntropy, ZSTD_fseCTables_t* nextEntropy, + BYTE* dst, const BYTE* const dstEnd, + ZSTD_strategy strategy, unsigned* countWorkspace, + void* entropyWorkspace, size_t entropyWkspSize) +{ + BYTE* const ostart = dst; + const BYTE* const oend = dstEnd; + BYTE* op = ostart; + FSE_CTable* CTable_LitLength = nextEntropy->litlengthCTable; + FSE_CTable* CTable_OffsetBits = nextEntropy->offcodeCTable; + FSE_CTable* CTable_MatchLength = nextEntropy->matchlengthCTable; + const BYTE* const ofCodeTable = seqStorePtr->ofCode; + const BYTE* const llCodeTable = seqStorePtr->llCode; + const BYTE* const mlCodeTable = seqStorePtr->mlCode; + ZSTD_symbolEncodingTypeStats_t stats; + + stats.lastCountSize = 0; + /* convert length/distances into codes */ + stats.longOffsets = ZSTD_seqToCodes(seqStorePtr); + assert(op <= oend); + assert(nbSeq != 0); /* ZSTD_selectEncodingType() divides by nbSeq */ + /* build CTable for Literal Lengths */ + { unsigned max = MaxLL; + size_t const mostFrequent = HIST_countFast_wksp(countWorkspace, &max, llCodeTable, nbSeq, entropyWorkspace, entropyWkspSize); /* can't fail */ + DEBUGLOG(5, "Building LL table"); + nextEntropy->litlength_repeatMode = prevEntropy->litlength_repeatMode; + stats.LLtype = ZSTD_selectEncodingType(&nextEntropy->litlength_repeatMode, + countWorkspace, max, mostFrequent, nbSeq, + LLFSELog, prevEntropy->litlengthCTable, + LL_defaultNorm, LL_defaultNormLog, + ZSTD_defaultAllowed, strategy); + assert(set_basic < set_compressed && set_rle < set_compressed); + assert(!(stats.LLtype < set_compressed && nextEntropy->litlength_repeatMode != FSE_repeat_none)); /* We don't copy tables */ + { size_t const countSize = ZSTD_buildCTable( + op, (size_t)(oend - op), + CTable_LitLength, LLFSELog, (symbolEncodingType_e)stats.LLtype, + countWorkspace, max, llCodeTable, nbSeq, + LL_defaultNorm, LL_defaultNormLog, MaxLL, + prevEntropy->litlengthCTable, + sizeof(prevEntropy->litlengthCTable), + entropyWorkspace, entropyWkspSize); + if (ZSTD_isError(countSize)) { + DEBUGLOG(3, "ZSTD_buildCTable for LitLens failed"); + stats.size = countSize; + return stats; + } + if (stats.LLtype == set_compressed) + stats.lastCountSize = countSize; + op += countSize; + assert(op <= oend); + } } + /* build CTable for Offsets */ + { unsigned max = MaxOff; + size_t const mostFrequent = HIST_countFast_wksp( + countWorkspace, &max, ofCodeTable, nbSeq, entropyWorkspace, entropyWkspSize); /* can't fail */ + /* We can only use the basic table if max <= DefaultMaxOff, otherwise the offsets are too large */ + ZSTD_defaultPolicy_e const defaultPolicy = (max <= DefaultMaxOff) ? ZSTD_defaultAllowed : ZSTD_defaultDisallowed; + DEBUGLOG(5, "Building OF table"); + nextEntropy->offcode_repeatMode = prevEntropy->offcode_repeatMode; + stats.Offtype = ZSTD_selectEncodingType(&nextEntropy->offcode_repeatMode, + countWorkspace, max, mostFrequent, nbSeq, + OffFSELog, prevEntropy->offcodeCTable, + OF_defaultNorm, OF_defaultNormLog, + defaultPolicy, strategy); + assert(!(stats.Offtype < set_compressed && nextEntropy->offcode_repeatMode != FSE_repeat_none)); /* We don't copy tables */ + { size_t const countSize = ZSTD_buildCTable( + op, (size_t)(oend - op), + CTable_OffsetBits, OffFSELog, (symbolEncodingType_e)stats.Offtype, + countWorkspace, max, ofCodeTable, nbSeq, + OF_defaultNorm, OF_defaultNormLog, DefaultMaxOff, + prevEntropy->offcodeCTable, + sizeof(prevEntropy->offcodeCTable), + entropyWorkspace, entropyWkspSize); + if (ZSTD_isError(countSize)) { + DEBUGLOG(3, "ZSTD_buildCTable for Offsets failed"); + stats.size = countSize; + return stats; + } + if (stats.Offtype == set_compressed) + stats.lastCountSize = countSize; + op += countSize; + assert(op <= oend); + } } + /* build CTable for MatchLengths */ + { unsigned max = MaxML; + size_t const mostFrequent = HIST_countFast_wksp( + countWorkspace, &max, mlCodeTable, nbSeq, entropyWorkspace, entropyWkspSize); /* can't fail */ + DEBUGLOG(5, "Building ML table (remaining space : %i)", (int)(oend-op)); + nextEntropy->matchlength_repeatMode = prevEntropy->matchlength_repeatMode; + stats.MLtype = ZSTD_selectEncodingType(&nextEntropy->matchlength_repeatMode, + countWorkspace, max, mostFrequent, nbSeq, + MLFSELog, prevEntropy->matchlengthCTable, + ML_defaultNorm, ML_defaultNormLog, + ZSTD_defaultAllowed, strategy); + assert(!(stats.MLtype < set_compressed && nextEntropy->matchlength_repeatMode != FSE_repeat_none)); /* We don't copy tables */ + { size_t const countSize = ZSTD_buildCTable( + op, (size_t)(oend - op), + CTable_MatchLength, MLFSELog, (symbolEncodingType_e)stats.MLtype, + countWorkspace, max, mlCodeTable, nbSeq, + ML_defaultNorm, ML_defaultNormLog, MaxML, + prevEntropy->matchlengthCTable, + sizeof(prevEntropy->matchlengthCTable), + entropyWorkspace, entropyWkspSize); + if (ZSTD_isError(countSize)) { + DEBUGLOG(3, "ZSTD_buildCTable for MatchLengths failed"); + stats.size = countSize; + return stats; + } + if (stats.MLtype == set_compressed) + stats.lastCountSize = countSize; + op += countSize; + assert(op <= oend); + } } + stats.size = (size_t)(op-ostart); + return stats; +} + +/* ZSTD_entropyCompressSeqStore_internal(): + * compresses both literals and sequences + * Returns compressed size of block, or a zstd error. + */ +#define SUSPECT_UNCOMPRESSIBLE_LITERAL_RATIO 20 +MEM_STATIC size_t +ZSTD_entropyCompressSeqStore_internal( + const seqStore_t* seqStorePtr, + const ZSTD_entropyCTables_t* prevEntropy, + ZSTD_entropyCTables_t* nextEntropy, + const ZSTD_CCtx_params* cctxParams, + void* dst, size_t dstCapacity, + void* entropyWorkspace, size_t entropyWkspSize, + const int bmi2) +{ + ZSTD_strategy const strategy = cctxParams->cParams.strategy; + unsigned* count = (unsigned*)entropyWorkspace; + FSE_CTable* CTable_LitLength = nextEntropy->fse.litlengthCTable; + FSE_CTable* CTable_OffsetBits = nextEntropy->fse.offcodeCTable; + FSE_CTable* CTable_MatchLength = nextEntropy->fse.matchlengthCTable; + const seqDef* const sequences = seqStorePtr->sequencesStart; + const size_t nbSeq = (size_t)(seqStorePtr->sequences - seqStorePtr->sequencesStart); + const BYTE* const ofCodeTable = seqStorePtr->ofCode; + const BYTE* const llCodeTable = seqStorePtr->llCode; + const BYTE* const mlCodeTable = seqStorePtr->mlCode; + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = ostart + dstCapacity; + BYTE* op = ostart; + size_t lastCountSize; + int longOffsets = 0; + + entropyWorkspace = count + (MaxSeq + 1); + entropyWkspSize -= (MaxSeq + 1) * sizeof(*count); + + DEBUGLOG(5, "ZSTD_entropyCompressSeqStore_internal (nbSeq=%zu, dstCapacity=%zu)", nbSeq, dstCapacity); + ZSTD_STATIC_ASSERT(HUF_WORKSPACE_SIZE >= (1<= HUF_WORKSPACE_SIZE); + + /* Compress literals */ + { const BYTE* const literals = seqStorePtr->litStart; + size_t const numSequences = (size_t)(seqStorePtr->sequences - seqStorePtr->sequencesStart); + size_t const numLiterals = (size_t)(seqStorePtr->lit - seqStorePtr->litStart); + /* Base suspicion of uncompressibility on ratio of literals to sequences */ + unsigned const suspectUncompressible = (numSequences == 0) || (numLiterals / numSequences >= SUSPECT_UNCOMPRESSIBLE_LITERAL_RATIO); + size_t const litSize = (size_t)(seqStorePtr->lit - literals); + + size_t const cSize = ZSTD_compressLiterals( + op, dstCapacity, + literals, litSize, + entropyWorkspace, entropyWkspSize, + &prevEntropy->huf, &nextEntropy->huf, + cctxParams->cParams.strategy, + ZSTD_literalsCompressionIsDisabled(cctxParams), + suspectUncompressible, bmi2); + FORWARD_IF_ERROR(cSize, "ZSTD_compressLiterals failed"); + assert(cSize <= dstCapacity); + op += cSize; + } + + /* Sequences Header */ + RETURN_ERROR_IF((oend-op) < 3 /*max nbSeq Size*/ + 1 /*seqHead*/, + dstSize_tooSmall, "Can't fit seq hdr in output buf!"); + if (nbSeq < 128) { + *op++ = (BYTE)nbSeq; + } else if (nbSeq < LONGNBSEQ) { + op[0] = (BYTE)((nbSeq>>8) + 0x80); + op[1] = (BYTE)nbSeq; + op+=2; + } else { + op[0]=0xFF; + MEM_writeLE16(op+1, (U16)(nbSeq - LONGNBSEQ)); + op+=3; + } + assert(op <= oend); + if (nbSeq==0) { + /* Copy the old tables over as if we repeated them */ + ZSTD_memcpy(&nextEntropy->fse, &prevEntropy->fse, sizeof(prevEntropy->fse)); + return (size_t)(op - ostart); + } + { BYTE* const seqHead = op++; + /* build stats for sequences */ + const ZSTD_symbolEncodingTypeStats_t stats = + ZSTD_buildSequencesStatistics(seqStorePtr, nbSeq, + &prevEntropy->fse, &nextEntropy->fse, + op, oend, + strategy, count, + entropyWorkspace, entropyWkspSize); + FORWARD_IF_ERROR(stats.size, "ZSTD_buildSequencesStatistics failed!"); + *seqHead = (BYTE)((stats.LLtype<<6) + (stats.Offtype<<4) + (stats.MLtype<<2)); + lastCountSize = stats.lastCountSize; + op += stats.size; + longOffsets = stats.longOffsets; + } + + { size_t const bitstreamSize = ZSTD_encodeSequences( + op, (size_t)(oend - op), + CTable_MatchLength, mlCodeTable, + CTable_OffsetBits, ofCodeTable, + CTable_LitLength, llCodeTable, + sequences, nbSeq, + longOffsets, bmi2); + FORWARD_IF_ERROR(bitstreamSize, "ZSTD_encodeSequences failed"); + op += bitstreamSize; + assert(op <= oend); + /* zstd versions <= 1.3.4 mistakenly report corruption when + * FSE_readNCount() receives a buffer < 4 bytes. + * Fixed by https://github.com/facebook/zstd/pull/1146. + * This can happen when the last set_compressed table present is 2 + * bytes and the bitstream is only one byte. + * In this exceedingly rare case, we will simply emit an uncompressed + * block, since it isn't worth optimizing. + */ + if (lastCountSize && (lastCountSize + bitstreamSize) < 4) { + /* lastCountSize >= 2 && bitstreamSize > 0 ==> lastCountSize == 3 */ + assert(lastCountSize + bitstreamSize == 3); + DEBUGLOG(5, "Avoiding bug in zstd decoder in versions <= 1.3.4 by " + "emitting an uncompressed block."); + return 0; + } + } + + DEBUGLOG(5, "compressed block size : %u", (unsigned)(op - ostart)); + return (size_t)(op - ostart); +} + +MEM_STATIC size_t +ZSTD_entropyCompressSeqStore( + const seqStore_t* seqStorePtr, + const ZSTD_entropyCTables_t* prevEntropy, + ZSTD_entropyCTables_t* nextEntropy, + const ZSTD_CCtx_params* cctxParams, + void* dst, size_t dstCapacity, + size_t srcSize, + void* entropyWorkspace, size_t entropyWkspSize, + int bmi2) +{ + size_t const cSize = ZSTD_entropyCompressSeqStore_internal( + seqStorePtr, prevEntropy, nextEntropy, cctxParams, + dst, dstCapacity, + entropyWorkspace, entropyWkspSize, bmi2); + if (cSize == 0) return 0; + /* When srcSize <= dstCapacity, there is enough space to write a raw uncompressed block. + * Since we ran out of space, block must be not compressible, so fall back to raw uncompressed block. + */ + if ((cSize == ERROR(dstSize_tooSmall)) & (srcSize <= dstCapacity)) { + DEBUGLOG(4, "not enough dstCapacity (%zu) for ZSTD_entropyCompressSeqStore_internal()=> do not compress block", dstCapacity); + return 0; /* block not compressed */ + } + FORWARD_IF_ERROR(cSize, "ZSTD_entropyCompressSeqStore_internal failed"); + + /* Check compressibility */ + { size_t const maxCSize = srcSize - ZSTD_minGain(srcSize, cctxParams->cParams.strategy); + if (cSize >= maxCSize) return 0; /* block not compressed */ + } + DEBUGLOG(5, "ZSTD_entropyCompressSeqStore() cSize: %zu", cSize); + /* libzstd decoder before > v1.5.4 is not compatible with compressed blocks of size ZSTD_BLOCKSIZE_MAX exactly. + * This restriction is indirectly already fulfilled by respecting ZSTD_minGain() condition above. + */ + assert(cSize < ZSTD_BLOCKSIZE_MAX); + return cSize; +} + +/* ZSTD_selectBlockCompressor() : + * Not static, but internal use only (used by long distance matcher) + * assumption : strat is a valid strategy */ +ZSTD_blockCompressor ZSTD_selectBlockCompressor(ZSTD_strategy strat, ZSTD_paramSwitch_e useRowMatchFinder, ZSTD_dictMode_e dictMode) +{ + static const ZSTD_blockCompressor blockCompressor[4][ZSTD_STRATEGY_MAX+1] = { + { ZSTD_compressBlock_fast /* default for 0 */, + ZSTD_compressBlock_fast, + ZSTD_COMPRESSBLOCK_DOUBLEFAST, + ZSTD_COMPRESSBLOCK_GREEDY, + ZSTD_COMPRESSBLOCK_LAZY, + ZSTD_COMPRESSBLOCK_LAZY2, + ZSTD_COMPRESSBLOCK_BTLAZY2, + ZSTD_COMPRESSBLOCK_BTOPT, + ZSTD_COMPRESSBLOCK_BTULTRA, + ZSTD_COMPRESSBLOCK_BTULTRA2 + }, + { ZSTD_compressBlock_fast_extDict /* default for 0 */, + ZSTD_compressBlock_fast_extDict, + ZSTD_COMPRESSBLOCK_DOUBLEFAST_EXTDICT, + ZSTD_COMPRESSBLOCK_GREEDY_EXTDICT, + ZSTD_COMPRESSBLOCK_LAZY_EXTDICT, + ZSTD_COMPRESSBLOCK_LAZY2_EXTDICT, + ZSTD_COMPRESSBLOCK_BTLAZY2_EXTDICT, + ZSTD_COMPRESSBLOCK_BTOPT_EXTDICT, + ZSTD_COMPRESSBLOCK_BTULTRA_EXTDICT, + ZSTD_COMPRESSBLOCK_BTULTRA_EXTDICT + }, + { ZSTD_compressBlock_fast_dictMatchState /* default for 0 */, + ZSTD_compressBlock_fast_dictMatchState, + ZSTD_COMPRESSBLOCK_DOUBLEFAST_DICTMATCHSTATE, + ZSTD_COMPRESSBLOCK_GREEDY_DICTMATCHSTATE, + ZSTD_COMPRESSBLOCK_LAZY_DICTMATCHSTATE, + ZSTD_COMPRESSBLOCK_LAZY2_DICTMATCHSTATE, + ZSTD_COMPRESSBLOCK_BTLAZY2_DICTMATCHSTATE, + ZSTD_COMPRESSBLOCK_BTOPT_DICTMATCHSTATE, + ZSTD_COMPRESSBLOCK_BTULTRA_DICTMATCHSTATE, + ZSTD_COMPRESSBLOCK_BTULTRA_DICTMATCHSTATE + }, + { NULL /* default for 0 */, + NULL, + NULL, + ZSTD_COMPRESSBLOCK_GREEDY_DEDICATEDDICTSEARCH, + ZSTD_COMPRESSBLOCK_LAZY_DEDICATEDDICTSEARCH, + ZSTD_COMPRESSBLOCK_LAZY2_DEDICATEDDICTSEARCH, + NULL, + NULL, + NULL, + NULL } + }; + ZSTD_blockCompressor selectedCompressor; + ZSTD_STATIC_ASSERT((unsigned)ZSTD_fast == 1); + + assert(ZSTD_cParam_withinBounds(ZSTD_c_strategy, strat)); + DEBUGLOG(4, "Selected block compressor: dictMode=%d strat=%d rowMatchfinder=%d", (int)dictMode, (int)strat, (int)useRowMatchFinder); + if (ZSTD_rowMatchFinderUsed(strat, useRowMatchFinder)) { + static const ZSTD_blockCompressor rowBasedBlockCompressors[4][3] = { + { + ZSTD_COMPRESSBLOCK_GREEDY_ROW, + ZSTD_COMPRESSBLOCK_LAZY_ROW, + ZSTD_COMPRESSBLOCK_LAZY2_ROW + }, + { + ZSTD_COMPRESSBLOCK_GREEDY_EXTDICT_ROW, + ZSTD_COMPRESSBLOCK_LAZY_EXTDICT_ROW, + ZSTD_COMPRESSBLOCK_LAZY2_EXTDICT_ROW + }, + { + ZSTD_COMPRESSBLOCK_GREEDY_DICTMATCHSTATE_ROW, + ZSTD_COMPRESSBLOCK_LAZY_DICTMATCHSTATE_ROW, + ZSTD_COMPRESSBLOCK_LAZY2_DICTMATCHSTATE_ROW + }, + { + ZSTD_COMPRESSBLOCK_GREEDY_DEDICATEDDICTSEARCH_ROW, + ZSTD_COMPRESSBLOCK_LAZY_DEDICATEDDICTSEARCH_ROW, + ZSTD_COMPRESSBLOCK_LAZY2_DEDICATEDDICTSEARCH_ROW + } + }; + DEBUGLOG(4, "Selecting a row-based matchfinder"); + assert(useRowMatchFinder != ZSTD_ps_auto); + selectedCompressor = rowBasedBlockCompressors[(int)dictMode][(int)strat - (int)ZSTD_greedy]; + } else { + selectedCompressor = blockCompressor[(int)dictMode][(int)strat]; + } + assert(selectedCompressor != NULL); + return selectedCompressor; +} + +static void ZSTD_storeLastLiterals(seqStore_t* seqStorePtr, + const BYTE* anchor, size_t lastLLSize) +{ + ZSTD_memcpy(seqStorePtr->lit, anchor, lastLLSize); + seqStorePtr->lit += lastLLSize; +} + +void ZSTD_resetSeqStore(seqStore_t* ssPtr) +{ + ssPtr->lit = ssPtr->litStart; + ssPtr->sequences = ssPtr->sequencesStart; + ssPtr->longLengthType = ZSTD_llt_none; +} + +/* ZSTD_postProcessSequenceProducerResult() : + * Validates and post-processes sequences obtained through the external matchfinder API: + * - Checks whether nbExternalSeqs represents an error condition. + * - Appends a block delimiter to outSeqs if one is not already present. + * See zstd.h for context regarding block delimiters. + * Returns the number of sequences after post-processing, or an error code. */ +static size_t ZSTD_postProcessSequenceProducerResult( + ZSTD_Sequence* outSeqs, size_t nbExternalSeqs, size_t outSeqsCapacity, size_t srcSize +) { + RETURN_ERROR_IF( + nbExternalSeqs > outSeqsCapacity, + sequenceProducer_failed, + "External sequence producer returned error code %lu", + (unsigned long)nbExternalSeqs + ); + + RETURN_ERROR_IF( + nbExternalSeqs == 0 && srcSize > 0, + sequenceProducer_failed, + "Got zero sequences from external sequence producer for a non-empty src buffer!" + ); + + if (srcSize == 0) { + ZSTD_memset(&outSeqs[0], 0, sizeof(ZSTD_Sequence)); + return 1; + } + + { + ZSTD_Sequence const lastSeq = outSeqs[nbExternalSeqs - 1]; + + /* We can return early if lastSeq is already a block delimiter. */ + if (lastSeq.offset == 0 && lastSeq.matchLength == 0) { + return nbExternalSeqs; + } + + /* This error condition is only possible if the external matchfinder + * produced an invalid parse, by definition of ZSTD_sequenceBound(). */ + RETURN_ERROR_IF( + nbExternalSeqs == outSeqsCapacity, + sequenceProducer_failed, + "nbExternalSeqs == outSeqsCapacity but lastSeq is not a block delimiter!" + ); + + /* lastSeq is not a block delimiter, so we need to append one. */ + ZSTD_memset(&outSeqs[nbExternalSeqs], 0, sizeof(ZSTD_Sequence)); + return nbExternalSeqs + 1; + } +} + +/* ZSTD_fastSequenceLengthSum() : + * Returns sum(litLen) + sum(matchLen) + lastLits for *seqBuf*. + * Similar to another function in zstd_compress.c (determine_blockSize), + * except it doesn't check for a block delimiter to end summation. + * Removing the early exit allows the compiler to auto-vectorize (https://godbolt.org/z/cY1cajz9P). + * This function can be deleted and replaced by determine_blockSize after we resolve issue #3456. */ +static size_t ZSTD_fastSequenceLengthSum(ZSTD_Sequence const* seqBuf, size_t seqBufSize) { + size_t matchLenSum, litLenSum, i; + matchLenSum = 0; + litLenSum = 0; + for (i = 0; i < seqBufSize; i++) { + litLenSum += seqBuf[i].litLength; + matchLenSum += seqBuf[i].matchLength; + } + return litLenSum + matchLenSum; +} + +typedef enum { ZSTDbss_compress, ZSTDbss_noCompress } ZSTD_buildSeqStore_e; + +static size_t ZSTD_buildSeqStore(ZSTD_CCtx* zc, const void* src, size_t srcSize) +{ + ZSTD_matchState_t* const ms = &zc->blockState.matchState; + DEBUGLOG(5, "ZSTD_buildSeqStore (srcSize=%zu)", srcSize); + assert(srcSize <= ZSTD_BLOCKSIZE_MAX); + /* Assert that we have correctly flushed the ctx params into the ms's copy */ + ZSTD_assertEqualCParams(zc->appliedParams.cParams, ms->cParams); + /* TODO: See 3090. We reduced MIN_CBLOCK_SIZE from 3 to 2 so to compensate we are adding + * additional 1. We need to revisit and change this logic to be more consistent */ + if (srcSize < MIN_CBLOCK_SIZE+ZSTD_blockHeaderSize+1+1) { + if (zc->appliedParams.cParams.strategy >= ZSTD_btopt) { + ZSTD_ldm_skipRawSeqStoreBytes(&zc->externSeqStore, srcSize); + } else { + ZSTD_ldm_skipSequences(&zc->externSeqStore, srcSize, zc->appliedParams.cParams.minMatch); + } + return ZSTDbss_noCompress; /* don't even attempt compression below a certain srcSize */ + } + ZSTD_resetSeqStore(&(zc->seqStore)); + /* required for optimal parser to read stats from dictionary */ + ms->opt.symbolCosts = &zc->blockState.prevCBlock->entropy; + /* tell the optimal parser how we expect to compress literals */ + ms->opt.literalCompressionMode = zc->appliedParams.literalCompressionMode; + /* a gap between an attached dict and the current window is not safe, + * they must remain adjacent, + * and when that stops being the case, the dict must be unset */ + assert(ms->dictMatchState == NULL || ms->loadedDictEnd == ms->window.dictLimit); + + /* limited update after a very long match */ + { const BYTE* const base = ms->window.base; + const BYTE* const istart = (const BYTE*)src; + const U32 curr = (U32)(istart-base); + if (sizeof(ptrdiff_t)==8) assert(istart - base < (ptrdiff_t)(U32)(-1)); /* ensure no overflow */ + if (curr > ms->nextToUpdate + 384) + ms->nextToUpdate = curr - MIN(192, (U32)(curr - ms->nextToUpdate - 384)); + } + + /* select and store sequences */ + { ZSTD_dictMode_e const dictMode = ZSTD_matchState_dictMode(ms); + size_t lastLLSize; + { int i; + for (i = 0; i < ZSTD_REP_NUM; ++i) + zc->blockState.nextCBlock->rep[i] = zc->blockState.prevCBlock->rep[i]; + } + if (zc->externSeqStore.pos < zc->externSeqStore.size) { + assert(zc->appliedParams.ldmParams.enableLdm == ZSTD_ps_disable); + + /* External matchfinder + LDM is technically possible, just not implemented yet. + * We need to revisit soon and implement it. */ + RETURN_ERROR_IF( + ZSTD_hasExtSeqProd(&zc->appliedParams), + parameter_combination_unsupported, + "Long-distance matching with external sequence producer enabled is not currently supported." + ); + + /* Updates ldmSeqStore.pos */ + lastLLSize = + ZSTD_ldm_blockCompress(&zc->externSeqStore, + ms, &zc->seqStore, + zc->blockState.nextCBlock->rep, + zc->appliedParams.useRowMatchFinder, + src, srcSize); + assert(zc->externSeqStore.pos <= zc->externSeqStore.size); + } else if (zc->appliedParams.ldmParams.enableLdm == ZSTD_ps_enable) { + rawSeqStore_t ldmSeqStore = kNullRawSeqStore; + + /* External matchfinder + LDM is technically possible, just not implemented yet. + * We need to revisit soon and implement it. */ + RETURN_ERROR_IF( + ZSTD_hasExtSeqProd(&zc->appliedParams), + parameter_combination_unsupported, + "Long-distance matching with external sequence producer enabled is not currently supported." + ); + + ldmSeqStore.seq = zc->ldmSequences; + ldmSeqStore.capacity = zc->maxNbLdmSequences; + /* Updates ldmSeqStore.size */ + FORWARD_IF_ERROR(ZSTD_ldm_generateSequences(&zc->ldmState, &ldmSeqStore, + &zc->appliedParams.ldmParams, + src, srcSize), ""); + /* Updates ldmSeqStore.pos */ + lastLLSize = + ZSTD_ldm_blockCompress(&ldmSeqStore, + ms, &zc->seqStore, + zc->blockState.nextCBlock->rep, + zc->appliedParams.useRowMatchFinder, + src, srcSize); + assert(ldmSeqStore.pos == ldmSeqStore.size); + } else if (ZSTD_hasExtSeqProd(&zc->appliedParams)) { + assert( + zc->extSeqBufCapacity >= ZSTD_sequenceBound(srcSize) + ); + assert(zc->appliedParams.extSeqProdFunc != NULL); + + { U32 const windowSize = (U32)1 << zc->appliedParams.cParams.windowLog; + + size_t const nbExternalSeqs = (zc->appliedParams.extSeqProdFunc)( + zc->appliedParams.extSeqProdState, + zc->extSeqBuf, + zc->extSeqBufCapacity, + src, srcSize, + NULL, 0, /* dict and dictSize, currently not supported */ + zc->appliedParams.compressionLevel, + windowSize + ); + + size_t const nbPostProcessedSeqs = ZSTD_postProcessSequenceProducerResult( + zc->extSeqBuf, + nbExternalSeqs, + zc->extSeqBufCapacity, + srcSize + ); + + /* Return early if there is no error, since we don't need to worry about last literals */ + if (!ZSTD_isError(nbPostProcessedSeqs)) { + ZSTD_sequencePosition seqPos = {0,0,0}; + size_t const seqLenSum = ZSTD_fastSequenceLengthSum(zc->extSeqBuf, nbPostProcessedSeqs); + RETURN_ERROR_IF(seqLenSum > srcSize, externalSequences_invalid, "External sequences imply too large a block!"); + FORWARD_IF_ERROR( + ZSTD_copySequencesToSeqStoreExplicitBlockDelim( + zc, &seqPos, + zc->extSeqBuf, nbPostProcessedSeqs, + src, srcSize, + zc->appliedParams.searchForExternalRepcodes + ), + "Failed to copy external sequences to seqStore!" + ); + ms->ldmSeqStore = NULL; + DEBUGLOG(5, "Copied %lu sequences from external sequence producer to internal seqStore.", (unsigned long)nbExternalSeqs); + return ZSTDbss_compress; + } + + /* Propagate the error if fallback is disabled */ + if (!zc->appliedParams.enableMatchFinderFallback) { + return nbPostProcessedSeqs; + } + + /* Fallback to software matchfinder */ + { ZSTD_blockCompressor const blockCompressor = + ZSTD_selectBlockCompressor( + zc->appliedParams.cParams.strategy, + zc->appliedParams.useRowMatchFinder, + dictMode); + ms->ldmSeqStore = NULL; + DEBUGLOG( + 5, + "External sequence producer returned error code %lu. Falling back to internal parser.", + (unsigned long)nbExternalSeqs + ); + lastLLSize = blockCompressor(ms, &zc->seqStore, zc->blockState.nextCBlock->rep, src, srcSize); + } } + } else { /* not long range mode and no external matchfinder */ + ZSTD_blockCompressor const blockCompressor = ZSTD_selectBlockCompressor( + zc->appliedParams.cParams.strategy, + zc->appliedParams.useRowMatchFinder, + dictMode); + ms->ldmSeqStore = NULL; + lastLLSize = blockCompressor(ms, &zc->seqStore, zc->blockState.nextCBlock->rep, src, srcSize); + } + { const BYTE* const lastLiterals = (const BYTE*)src + srcSize - lastLLSize; + ZSTD_storeLastLiterals(&zc->seqStore, lastLiterals, lastLLSize); + } } + return ZSTDbss_compress; +} + +static size_t ZSTD_copyBlockSequences(SeqCollector* seqCollector, const seqStore_t* seqStore, const U32 prevRepcodes[ZSTD_REP_NUM]) +{ + const seqDef* inSeqs = seqStore->sequencesStart; + const size_t nbInSequences = seqStore->sequences - inSeqs; + const size_t nbInLiterals = (size_t)(seqStore->lit - seqStore->litStart); + + ZSTD_Sequence* outSeqs = seqCollector->seqIndex == 0 ? seqCollector->seqStart : seqCollector->seqStart + seqCollector->seqIndex; + const size_t nbOutSequences = nbInSequences + 1; + size_t nbOutLiterals = 0; + repcodes_t repcodes; + size_t i; + + /* Bounds check that we have enough space for every input sequence + * and the block delimiter + */ + assert(seqCollector->seqIndex <= seqCollector->maxSequences); + RETURN_ERROR_IF( + nbOutSequences > (size_t)(seqCollector->maxSequences - seqCollector->seqIndex), + dstSize_tooSmall, + "Not enough space to copy sequences"); + + ZSTD_memcpy(&repcodes, prevRepcodes, sizeof(repcodes)); + for (i = 0; i < nbInSequences; ++i) { + U32 rawOffset; + outSeqs[i].litLength = inSeqs[i].litLength; + outSeqs[i].matchLength = inSeqs[i].mlBase + MINMATCH; + outSeqs[i].rep = 0; + + /* Handle the possible single length >= 64K + * There can only be one because we add MINMATCH to every match length, + * and blocks are at most 128K. + */ + if (i == seqStore->longLengthPos) { + if (seqStore->longLengthType == ZSTD_llt_literalLength) { + outSeqs[i].litLength += 0x10000; + } else if (seqStore->longLengthType == ZSTD_llt_matchLength) { + outSeqs[i].matchLength += 0x10000; + } + } + + /* Determine the raw offset given the offBase, which may be a repcode. */ + if (OFFBASE_IS_REPCODE(inSeqs[i].offBase)) { + const U32 repcode = OFFBASE_TO_REPCODE(inSeqs[i].offBase); + assert(repcode > 0); + outSeqs[i].rep = repcode; + if (outSeqs[i].litLength != 0) { + rawOffset = repcodes.rep[repcode - 1]; + } else { + if (repcode == 3) { + assert(repcodes.rep[0] > 1); + rawOffset = repcodes.rep[0] - 1; + } else { + rawOffset = repcodes.rep[repcode]; + } + } + } else { + rawOffset = OFFBASE_TO_OFFSET(inSeqs[i].offBase); + } + outSeqs[i].offset = rawOffset; + + /* Update repcode history for the sequence */ + ZSTD_updateRep(repcodes.rep, + inSeqs[i].offBase, + inSeqs[i].litLength == 0); + + nbOutLiterals += outSeqs[i].litLength; + } + /* Insert last literals (if any exist) in the block as a sequence with ml == off == 0. + * If there are no last literals, then we'll emit (of: 0, ml: 0, ll: 0), which is a marker + * for the block boundary, according to the API. + */ + assert(nbInLiterals >= nbOutLiterals); + { + const size_t lastLLSize = nbInLiterals - nbOutLiterals; + outSeqs[nbInSequences].litLength = (U32)lastLLSize; + outSeqs[nbInSequences].matchLength = 0; + outSeqs[nbInSequences].offset = 0; + assert(nbOutSequences == nbInSequences + 1); + } + seqCollector->seqIndex += nbOutSequences; + assert(seqCollector->seqIndex <= seqCollector->maxSequences); + + return 0; +} + +size_t ZSTD_sequenceBound(size_t srcSize) { + const size_t maxNbSeq = (srcSize / ZSTD_MINMATCH_MIN) + 1; + const size_t maxNbDelims = (srcSize / ZSTD_BLOCKSIZE_MAX_MIN) + 1; + return maxNbSeq + maxNbDelims; +} + +size_t ZSTD_generateSequences(ZSTD_CCtx* zc, ZSTD_Sequence* outSeqs, + size_t outSeqsSize, const void* src, size_t srcSize) +{ + const size_t dstCapacity = ZSTD_compressBound(srcSize); + void* dst = ZSTD_customMalloc(dstCapacity, ZSTD_defaultCMem); + SeqCollector seqCollector; + { + int targetCBlockSize; + FORWARD_IF_ERROR(ZSTD_CCtx_getParameter(zc, ZSTD_c_targetCBlockSize, &targetCBlockSize), ""); + RETURN_ERROR_IF(targetCBlockSize != 0, parameter_unsupported, "targetCBlockSize != 0"); + } + { + int nbWorkers; + FORWARD_IF_ERROR(ZSTD_CCtx_getParameter(zc, ZSTD_c_nbWorkers, &nbWorkers), ""); + RETURN_ERROR_IF(nbWorkers != 0, parameter_unsupported, "nbWorkers != 0"); + } + + RETURN_ERROR_IF(dst == NULL, memory_allocation, "NULL pointer!"); + + seqCollector.collectSequences = 1; + seqCollector.seqStart = outSeqs; + seqCollector.seqIndex = 0; + seqCollector.maxSequences = outSeqsSize; + zc->seqCollector = seqCollector; + + { + const size_t ret = ZSTD_compress2(zc, dst, dstCapacity, src, srcSize); + ZSTD_customFree(dst, ZSTD_defaultCMem); + FORWARD_IF_ERROR(ret, "ZSTD_compress2 failed"); + } + assert(zc->seqCollector.seqIndex <= ZSTD_sequenceBound(srcSize)); + return zc->seqCollector.seqIndex; +} + +size_t ZSTD_mergeBlockDelimiters(ZSTD_Sequence* sequences, size_t seqsSize) { + size_t in = 0; + size_t out = 0; + for (; in < seqsSize; ++in) { + if (sequences[in].offset == 0 && sequences[in].matchLength == 0) { + if (in != seqsSize - 1) { + sequences[in+1].litLength += sequences[in].litLength; + } + } else { + sequences[out] = sequences[in]; + ++out; + } + } + return out; +} + +/* Unrolled loop to read four size_ts of input at a time. Returns 1 if is RLE, 0 if not. */ +static int ZSTD_isRLE(const BYTE* src, size_t length) { + const BYTE* ip = src; + const BYTE value = ip[0]; + const size_t valueST = (size_t)((U64)value * 0x0101010101010101ULL); + const size_t unrollSize = sizeof(size_t) * 4; + const size_t unrollMask = unrollSize - 1; + const size_t prefixLength = length & unrollMask; + size_t i; + if (length == 1) return 1; + /* Check if prefix is RLE first before using unrolled loop */ + if (prefixLength && ZSTD_count(ip+1, ip, ip+prefixLength) != prefixLength-1) { + return 0; + } + for (i = prefixLength; i != length; i += unrollSize) { + size_t u; + for (u = 0; u < unrollSize; u += sizeof(size_t)) { + if (MEM_readST(ip + i + u) != valueST) { + return 0; + } } } + return 1; +} + +/* Returns true if the given block may be RLE. + * This is just a heuristic based on the compressibility. + * It may return both false positives and false negatives. + */ +static int ZSTD_maybeRLE(seqStore_t const* seqStore) +{ + size_t const nbSeqs = (size_t)(seqStore->sequences - seqStore->sequencesStart); + size_t const nbLits = (size_t)(seqStore->lit - seqStore->litStart); + + return nbSeqs < 4 && nbLits < 10; +} + +static void +ZSTD_blockState_confirmRepcodesAndEntropyTables(ZSTD_blockState_t* const bs) +{ + ZSTD_compressedBlockState_t* const tmp = bs->prevCBlock; + bs->prevCBlock = bs->nextCBlock; + bs->nextCBlock = tmp; +} + +/* Writes the block header */ +static void +writeBlockHeader(void* op, size_t cSize, size_t blockSize, U32 lastBlock) +{ + U32 const cBlockHeader = cSize == 1 ? + lastBlock + (((U32)bt_rle)<<1) + (U32)(blockSize << 3) : + lastBlock + (((U32)bt_compressed)<<1) + (U32)(cSize << 3); + MEM_writeLE24(op, cBlockHeader); + DEBUGLOG(3, "writeBlockHeader: cSize: %zu blockSize: %zu lastBlock: %u", cSize, blockSize, lastBlock); +} + +/** ZSTD_buildBlockEntropyStats_literals() : + * Builds entropy for the literals. + * Stores literals block type (raw, rle, compressed, repeat) and + * huffman description table to hufMetadata. + * Requires ENTROPY_WORKSPACE_SIZE workspace + * @return : size of huffman description table, or an error code + */ +static size_t +ZSTD_buildBlockEntropyStats_literals(void* const src, size_t srcSize, + const ZSTD_hufCTables_t* prevHuf, + ZSTD_hufCTables_t* nextHuf, + ZSTD_hufCTablesMetadata_t* hufMetadata, + const int literalsCompressionIsDisabled, + void* workspace, size_t wkspSize, + int hufFlags) +{ + BYTE* const wkspStart = (BYTE*)workspace; + BYTE* const wkspEnd = wkspStart + wkspSize; + BYTE* const countWkspStart = wkspStart; + unsigned* const countWksp = (unsigned*)workspace; + const size_t countWkspSize = (HUF_SYMBOLVALUE_MAX + 1) * sizeof(unsigned); + BYTE* const nodeWksp = countWkspStart + countWkspSize; + const size_t nodeWkspSize = (size_t)(wkspEnd - nodeWksp); + unsigned maxSymbolValue = HUF_SYMBOLVALUE_MAX; + unsigned huffLog = LitHufLog; + HUF_repeat repeat = prevHuf->repeatMode; + DEBUGLOG(5, "ZSTD_buildBlockEntropyStats_literals (srcSize=%zu)", srcSize); + + /* Prepare nextEntropy assuming reusing the existing table */ + ZSTD_memcpy(nextHuf, prevHuf, sizeof(*prevHuf)); + + if (literalsCompressionIsDisabled) { + DEBUGLOG(5, "set_basic - disabled"); + hufMetadata->hType = set_basic; + return 0; + } + + /* small ? don't even attempt compression (speed opt) */ +#ifndef COMPRESS_LITERALS_SIZE_MIN +# define COMPRESS_LITERALS_SIZE_MIN 63 /* heuristic */ +#endif + { size_t const minLitSize = (prevHuf->repeatMode == HUF_repeat_valid) ? 6 : COMPRESS_LITERALS_SIZE_MIN; + if (srcSize <= minLitSize) { + DEBUGLOG(5, "set_basic - too small"); + hufMetadata->hType = set_basic; + return 0; + } } + + /* Scan input and build symbol stats */ + { size_t const largest = + HIST_count_wksp (countWksp, &maxSymbolValue, + (const BYTE*)src, srcSize, + workspace, wkspSize); + FORWARD_IF_ERROR(largest, "HIST_count_wksp failed"); + if (largest == srcSize) { + /* only one literal symbol */ + DEBUGLOG(5, "set_rle"); + hufMetadata->hType = set_rle; + return 0; + } + if (largest <= (srcSize >> 7)+4) { + /* heuristic: likely not compressible */ + DEBUGLOG(5, "set_basic - no gain"); + hufMetadata->hType = set_basic; + return 0; + } } + + /* Validate the previous Huffman table */ + if (repeat == HUF_repeat_check + && !HUF_validateCTable((HUF_CElt const*)prevHuf->CTable, countWksp, maxSymbolValue)) { + repeat = HUF_repeat_none; + } + + /* Build Huffman Tree */ + ZSTD_memset(nextHuf->CTable, 0, sizeof(nextHuf->CTable)); + huffLog = HUF_optimalTableLog(huffLog, srcSize, maxSymbolValue, nodeWksp, nodeWkspSize, nextHuf->CTable, countWksp, hufFlags); + assert(huffLog <= LitHufLog); + { size_t const maxBits = HUF_buildCTable_wksp((HUF_CElt*)nextHuf->CTable, countWksp, + maxSymbolValue, huffLog, + nodeWksp, nodeWkspSize); + FORWARD_IF_ERROR(maxBits, "HUF_buildCTable_wksp"); + huffLog = (U32)maxBits; + } + { /* Build and write the CTable */ + size_t const newCSize = HUF_estimateCompressedSize( + (HUF_CElt*)nextHuf->CTable, countWksp, maxSymbolValue); + size_t const hSize = HUF_writeCTable_wksp( + hufMetadata->hufDesBuffer, sizeof(hufMetadata->hufDesBuffer), + (HUF_CElt*)nextHuf->CTable, maxSymbolValue, huffLog, + nodeWksp, nodeWkspSize); + /* Check against repeating the previous CTable */ + if (repeat != HUF_repeat_none) { + size_t const oldCSize = HUF_estimateCompressedSize( + (HUF_CElt const*)prevHuf->CTable, countWksp, maxSymbolValue); + if (oldCSize < srcSize && (oldCSize <= hSize + newCSize || hSize + 12 >= srcSize)) { + DEBUGLOG(5, "set_repeat - smaller"); + ZSTD_memcpy(nextHuf, prevHuf, sizeof(*prevHuf)); + hufMetadata->hType = set_repeat; + return 0; + } } + if (newCSize + hSize >= srcSize) { + DEBUGLOG(5, "set_basic - no gains"); + ZSTD_memcpy(nextHuf, prevHuf, sizeof(*prevHuf)); + hufMetadata->hType = set_basic; + return 0; + } + DEBUGLOG(5, "set_compressed (hSize=%u)", (U32)hSize); + hufMetadata->hType = set_compressed; + nextHuf->repeatMode = HUF_repeat_check; + return hSize; + } +} + + +/* ZSTD_buildDummySequencesStatistics(): + * Returns a ZSTD_symbolEncodingTypeStats_t with all encoding types as set_basic, + * and updates nextEntropy to the appropriate repeatMode. + */ +static ZSTD_symbolEncodingTypeStats_t +ZSTD_buildDummySequencesStatistics(ZSTD_fseCTables_t* nextEntropy) +{ + ZSTD_symbolEncodingTypeStats_t stats = {set_basic, set_basic, set_basic, 0, 0, 0}; + nextEntropy->litlength_repeatMode = FSE_repeat_none; + nextEntropy->offcode_repeatMode = FSE_repeat_none; + nextEntropy->matchlength_repeatMode = FSE_repeat_none; + return stats; +} + +/** ZSTD_buildBlockEntropyStats_sequences() : + * Builds entropy for the sequences. + * Stores symbol compression modes and fse table to fseMetadata. + * Requires ENTROPY_WORKSPACE_SIZE wksp. + * @return : size of fse tables or error code */ +static size_t +ZSTD_buildBlockEntropyStats_sequences( + const seqStore_t* seqStorePtr, + const ZSTD_fseCTables_t* prevEntropy, + ZSTD_fseCTables_t* nextEntropy, + const ZSTD_CCtx_params* cctxParams, + ZSTD_fseCTablesMetadata_t* fseMetadata, + void* workspace, size_t wkspSize) +{ + ZSTD_strategy const strategy = cctxParams->cParams.strategy; + size_t const nbSeq = (size_t)(seqStorePtr->sequences - seqStorePtr->sequencesStart); + BYTE* const ostart = fseMetadata->fseTablesBuffer; + BYTE* const oend = ostart + sizeof(fseMetadata->fseTablesBuffer); + BYTE* op = ostart; + unsigned* countWorkspace = (unsigned*)workspace; + unsigned* entropyWorkspace = countWorkspace + (MaxSeq + 1); + size_t entropyWorkspaceSize = wkspSize - (MaxSeq + 1) * sizeof(*countWorkspace); + ZSTD_symbolEncodingTypeStats_t stats; + + DEBUGLOG(5, "ZSTD_buildBlockEntropyStats_sequences (nbSeq=%zu)", nbSeq); + stats = nbSeq != 0 ? ZSTD_buildSequencesStatistics(seqStorePtr, nbSeq, + prevEntropy, nextEntropy, op, oend, + strategy, countWorkspace, + entropyWorkspace, entropyWorkspaceSize) + : ZSTD_buildDummySequencesStatistics(nextEntropy); + FORWARD_IF_ERROR(stats.size, "ZSTD_buildSequencesStatistics failed!"); + fseMetadata->llType = (symbolEncodingType_e) stats.LLtype; + fseMetadata->ofType = (symbolEncodingType_e) stats.Offtype; + fseMetadata->mlType = (symbolEncodingType_e) stats.MLtype; + fseMetadata->lastCountSize = stats.lastCountSize; + return stats.size; +} + + +/** ZSTD_buildBlockEntropyStats() : + * Builds entropy for the block. + * Requires workspace size ENTROPY_WORKSPACE_SIZE + * @return : 0 on success, or an error code + * Note : also employed in superblock + */ +size_t ZSTD_buildBlockEntropyStats( + const seqStore_t* seqStorePtr, + const ZSTD_entropyCTables_t* prevEntropy, + ZSTD_entropyCTables_t* nextEntropy, + const ZSTD_CCtx_params* cctxParams, + ZSTD_entropyCTablesMetadata_t* entropyMetadata, + void* workspace, size_t wkspSize) +{ + size_t const litSize = (size_t)(seqStorePtr->lit - seqStorePtr->litStart); + int const huf_useOptDepth = (cctxParams->cParams.strategy >= HUF_OPTIMAL_DEPTH_THRESHOLD); + int const hufFlags = huf_useOptDepth ? HUF_flags_optimalDepth : 0; + + entropyMetadata->hufMetadata.hufDesSize = + ZSTD_buildBlockEntropyStats_literals(seqStorePtr->litStart, litSize, + &prevEntropy->huf, &nextEntropy->huf, + &entropyMetadata->hufMetadata, + ZSTD_literalsCompressionIsDisabled(cctxParams), + workspace, wkspSize, hufFlags); + + FORWARD_IF_ERROR(entropyMetadata->hufMetadata.hufDesSize, "ZSTD_buildBlockEntropyStats_literals failed"); + entropyMetadata->fseMetadata.fseTablesSize = + ZSTD_buildBlockEntropyStats_sequences(seqStorePtr, + &prevEntropy->fse, &nextEntropy->fse, + cctxParams, + &entropyMetadata->fseMetadata, + workspace, wkspSize); + FORWARD_IF_ERROR(entropyMetadata->fseMetadata.fseTablesSize, "ZSTD_buildBlockEntropyStats_sequences failed"); + return 0; +} + +/* Returns the size estimate for the literals section (header + content) of a block */ +static size_t +ZSTD_estimateBlockSize_literal(const BYTE* literals, size_t litSize, + const ZSTD_hufCTables_t* huf, + const ZSTD_hufCTablesMetadata_t* hufMetadata, + void* workspace, size_t wkspSize, + int writeEntropy) +{ + unsigned* const countWksp = (unsigned*)workspace; + unsigned maxSymbolValue = HUF_SYMBOLVALUE_MAX; + size_t literalSectionHeaderSize = 3 + (litSize >= 1 KB) + (litSize >= 16 KB); + U32 singleStream = litSize < 256; + + if (hufMetadata->hType == set_basic) return litSize; + else if (hufMetadata->hType == set_rle) return 1; + else if (hufMetadata->hType == set_compressed || hufMetadata->hType == set_repeat) { + size_t const largest = HIST_count_wksp (countWksp, &maxSymbolValue, (const BYTE*)literals, litSize, workspace, wkspSize); + if (ZSTD_isError(largest)) return litSize; + { size_t cLitSizeEstimate = HUF_estimateCompressedSize((const HUF_CElt*)huf->CTable, countWksp, maxSymbolValue); + if (writeEntropy) cLitSizeEstimate += hufMetadata->hufDesSize; + if (!singleStream) cLitSizeEstimate += 6; /* multi-stream huffman uses 6-byte jump table */ + return cLitSizeEstimate + literalSectionHeaderSize; + } } + assert(0); /* impossible */ + return 0; +} + +/* Returns the size estimate for the FSE-compressed symbols (of, ml, ll) of a block */ +static size_t +ZSTD_estimateBlockSize_symbolType(symbolEncodingType_e type, + const BYTE* codeTable, size_t nbSeq, unsigned maxCode, + const FSE_CTable* fseCTable, + const U8* additionalBits, + short const* defaultNorm, U32 defaultNormLog, U32 defaultMax, + void* workspace, size_t wkspSize) +{ + unsigned* const countWksp = (unsigned*)workspace; + const BYTE* ctp = codeTable; + const BYTE* const ctStart = ctp; + const BYTE* const ctEnd = ctStart + nbSeq; + size_t cSymbolTypeSizeEstimateInBits = 0; + unsigned max = maxCode; + + HIST_countFast_wksp(countWksp, &max, codeTable, nbSeq, workspace, wkspSize); /* can't fail */ + if (type == set_basic) { + /* We selected this encoding type, so it must be valid. */ + assert(max <= defaultMax); + (void)defaultMax; + cSymbolTypeSizeEstimateInBits = ZSTD_crossEntropyCost(defaultNorm, defaultNormLog, countWksp, max); + } else if (type == set_rle) { + cSymbolTypeSizeEstimateInBits = 0; + } else if (type == set_compressed || type == set_repeat) { + cSymbolTypeSizeEstimateInBits = ZSTD_fseBitCost(fseCTable, countWksp, max); + } + if (ZSTD_isError(cSymbolTypeSizeEstimateInBits)) { + return nbSeq * 10; + } + while (ctp < ctEnd) { + if (additionalBits) cSymbolTypeSizeEstimateInBits += additionalBits[*ctp]; + else cSymbolTypeSizeEstimateInBits += *ctp; /* for offset, offset code is also the number of additional bits */ + ctp++; + } + return cSymbolTypeSizeEstimateInBits >> 3; +} + +/* Returns the size estimate for the sequences section (header + content) of a block */ +static size_t +ZSTD_estimateBlockSize_sequences(const BYTE* ofCodeTable, + const BYTE* llCodeTable, + const BYTE* mlCodeTable, + size_t nbSeq, + const ZSTD_fseCTables_t* fseTables, + const ZSTD_fseCTablesMetadata_t* fseMetadata, + void* workspace, size_t wkspSize, + int writeEntropy) +{ + size_t sequencesSectionHeaderSize = 1 /* seqHead */ + 1 /* min seqSize size */ + (nbSeq >= 128) + (nbSeq >= LONGNBSEQ); + size_t cSeqSizeEstimate = 0; + cSeqSizeEstimate += ZSTD_estimateBlockSize_symbolType(fseMetadata->ofType, ofCodeTable, nbSeq, MaxOff, + fseTables->offcodeCTable, NULL, + OF_defaultNorm, OF_defaultNormLog, DefaultMaxOff, + workspace, wkspSize); + cSeqSizeEstimate += ZSTD_estimateBlockSize_symbolType(fseMetadata->llType, llCodeTable, nbSeq, MaxLL, + fseTables->litlengthCTable, LL_bits, + LL_defaultNorm, LL_defaultNormLog, MaxLL, + workspace, wkspSize); + cSeqSizeEstimate += ZSTD_estimateBlockSize_symbolType(fseMetadata->mlType, mlCodeTable, nbSeq, MaxML, + fseTables->matchlengthCTable, ML_bits, + ML_defaultNorm, ML_defaultNormLog, MaxML, + workspace, wkspSize); + if (writeEntropy) cSeqSizeEstimate += fseMetadata->fseTablesSize; + return cSeqSizeEstimate + sequencesSectionHeaderSize; +} + +/* Returns the size estimate for a given stream of literals, of, ll, ml */ +static size_t +ZSTD_estimateBlockSize(const BYTE* literals, size_t litSize, + const BYTE* ofCodeTable, + const BYTE* llCodeTable, + const BYTE* mlCodeTable, + size_t nbSeq, + const ZSTD_entropyCTables_t* entropy, + const ZSTD_entropyCTablesMetadata_t* entropyMetadata, + void* workspace, size_t wkspSize, + int writeLitEntropy, int writeSeqEntropy) +{ + size_t const literalsSize = ZSTD_estimateBlockSize_literal(literals, litSize, + &entropy->huf, &entropyMetadata->hufMetadata, + workspace, wkspSize, writeLitEntropy); + size_t const seqSize = ZSTD_estimateBlockSize_sequences(ofCodeTable, llCodeTable, mlCodeTable, + nbSeq, &entropy->fse, &entropyMetadata->fseMetadata, + workspace, wkspSize, writeSeqEntropy); + return seqSize + literalsSize + ZSTD_blockHeaderSize; +} + +/* Builds entropy statistics and uses them for blocksize estimation. + * + * @return: estimated compressed size of the seqStore, or a zstd error. + */ +static size_t +ZSTD_buildEntropyStatisticsAndEstimateSubBlockSize(seqStore_t* seqStore, ZSTD_CCtx* zc) +{ + ZSTD_entropyCTablesMetadata_t* const entropyMetadata = &zc->blockSplitCtx.entropyMetadata; + DEBUGLOG(6, "ZSTD_buildEntropyStatisticsAndEstimateSubBlockSize()"); + FORWARD_IF_ERROR(ZSTD_buildBlockEntropyStats(seqStore, + &zc->blockState.prevCBlock->entropy, + &zc->blockState.nextCBlock->entropy, + &zc->appliedParams, + entropyMetadata, + zc->entropyWorkspace, ENTROPY_WORKSPACE_SIZE), ""); + return ZSTD_estimateBlockSize( + seqStore->litStart, (size_t)(seqStore->lit - seqStore->litStart), + seqStore->ofCode, seqStore->llCode, seqStore->mlCode, + (size_t)(seqStore->sequences - seqStore->sequencesStart), + &zc->blockState.nextCBlock->entropy, + entropyMetadata, + zc->entropyWorkspace, ENTROPY_WORKSPACE_SIZE, + (int)(entropyMetadata->hufMetadata.hType == set_compressed), 1); +} + +/* Returns literals bytes represented in a seqStore */ +static size_t ZSTD_countSeqStoreLiteralsBytes(const seqStore_t* const seqStore) +{ + size_t literalsBytes = 0; + size_t const nbSeqs = (size_t)(seqStore->sequences - seqStore->sequencesStart); + size_t i; + for (i = 0; i < nbSeqs; ++i) { + seqDef const seq = seqStore->sequencesStart[i]; + literalsBytes += seq.litLength; + if (i == seqStore->longLengthPos && seqStore->longLengthType == ZSTD_llt_literalLength) { + literalsBytes += 0x10000; + } } + return literalsBytes; +} + +/* Returns match bytes represented in a seqStore */ +static size_t ZSTD_countSeqStoreMatchBytes(const seqStore_t* const seqStore) +{ + size_t matchBytes = 0; + size_t const nbSeqs = (size_t)(seqStore->sequences - seqStore->sequencesStart); + size_t i; + for (i = 0; i < nbSeqs; ++i) { + seqDef seq = seqStore->sequencesStart[i]; + matchBytes += seq.mlBase + MINMATCH; + if (i == seqStore->longLengthPos && seqStore->longLengthType == ZSTD_llt_matchLength) { + matchBytes += 0x10000; + } } + return matchBytes; +} + +/* Derives the seqStore that is a chunk of the originalSeqStore from [startIdx, endIdx). + * Stores the result in resultSeqStore. + */ +static void ZSTD_deriveSeqStoreChunk(seqStore_t* resultSeqStore, + const seqStore_t* originalSeqStore, + size_t startIdx, size_t endIdx) +{ + *resultSeqStore = *originalSeqStore; + if (startIdx > 0) { + resultSeqStore->sequences = originalSeqStore->sequencesStart + startIdx; + resultSeqStore->litStart += ZSTD_countSeqStoreLiteralsBytes(resultSeqStore); + } + + /* Move longLengthPos into the correct position if necessary */ + if (originalSeqStore->longLengthType != ZSTD_llt_none) { + if (originalSeqStore->longLengthPos < startIdx || originalSeqStore->longLengthPos > endIdx) { + resultSeqStore->longLengthType = ZSTD_llt_none; + } else { + resultSeqStore->longLengthPos -= (U32)startIdx; + } + } + resultSeqStore->sequencesStart = originalSeqStore->sequencesStart + startIdx; + resultSeqStore->sequences = originalSeqStore->sequencesStart + endIdx; + if (endIdx == (size_t)(originalSeqStore->sequences - originalSeqStore->sequencesStart)) { + /* This accounts for possible last literals if the derived chunk reaches the end of the block */ + assert(resultSeqStore->lit == originalSeqStore->lit); + } else { + size_t const literalsBytes = ZSTD_countSeqStoreLiteralsBytes(resultSeqStore); + resultSeqStore->lit = resultSeqStore->litStart + literalsBytes; + } + resultSeqStore->llCode += startIdx; + resultSeqStore->mlCode += startIdx; + resultSeqStore->ofCode += startIdx; +} + +/** + * Returns the raw offset represented by the combination of offBase, ll0, and repcode history. + * offBase must represent a repcode in the numeric representation of ZSTD_storeSeq(). + */ +static U32 +ZSTD_resolveRepcodeToRawOffset(const U32 rep[ZSTD_REP_NUM], const U32 offBase, const U32 ll0) +{ + U32 const adjustedRepCode = OFFBASE_TO_REPCODE(offBase) - 1 + ll0; /* [ 0 - 3 ] */ + assert(OFFBASE_IS_REPCODE(offBase)); + if (adjustedRepCode == ZSTD_REP_NUM) { + assert(ll0); + /* litlength == 0 and offCode == 2 implies selection of first repcode - 1 + * This is only valid if it results in a valid offset value, aka > 0. + * Note : it may happen that `rep[0]==1` in exceptional circumstances. + * In which case this function will return 0, which is an invalid offset. + * It's not an issue though, since this value will be + * compared and discarded within ZSTD_seqStore_resolveOffCodes(). + */ + return rep[0] - 1; + } + return rep[adjustedRepCode]; +} + +/** + * ZSTD_seqStore_resolveOffCodes() reconciles any possible divergences in offset history that may arise + * due to emission of RLE/raw blocks that disturb the offset history, + * and replaces any repcodes within the seqStore that may be invalid. + * + * dRepcodes are updated as would be on the decompression side. + * cRepcodes are updated exactly in accordance with the seqStore. + * + * Note : this function assumes seq->offBase respects the following numbering scheme : + * 0 : invalid + * 1-3 : repcode 1-3 + * 4+ : real_offset+3 + */ +static void +ZSTD_seqStore_resolveOffCodes(repcodes_t* const dRepcodes, repcodes_t* const cRepcodes, + const seqStore_t* const seqStore, U32 const nbSeq) +{ + U32 idx = 0; + U32 const longLitLenIdx = seqStore->longLengthType == ZSTD_llt_literalLength ? seqStore->longLengthPos : nbSeq; + for (; idx < nbSeq; ++idx) { + seqDef* const seq = seqStore->sequencesStart + idx; + U32 const ll0 = (seq->litLength == 0) && (idx != longLitLenIdx); + U32 const offBase = seq->offBase; + assert(offBase > 0); + if (OFFBASE_IS_REPCODE(offBase)) { + U32 const dRawOffset = ZSTD_resolveRepcodeToRawOffset(dRepcodes->rep, offBase, ll0); + U32 const cRawOffset = ZSTD_resolveRepcodeToRawOffset(cRepcodes->rep, offBase, ll0); + /* Adjust simulated decompression repcode history if we come across a mismatch. Replace + * the repcode with the offset it actually references, determined by the compression + * repcode history. + */ + if (dRawOffset != cRawOffset) { + seq->offBase = OFFSET_TO_OFFBASE(cRawOffset); + } + } + /* Compression repcode history is always updated with values directly from the unmodified seqStore. + * Decompression repcode history may use modified seq->offset value taken from compression repcode history. + */ + ZSTD_updateRep(dRepcodes->rep, seq->offBase, ll0); + ZSTD_updateRep(cRepcodes->rep, offBase, ll0); + } +} + +/* ZSTD_compressSeqStore_singleBlock(): + * Compresses a seqStore into a block with a block header, into the buffer dst. + * + * Returns the total size of that block (including header) or a ZSTD error code. + */ +static size_t +ZSTD_compressSeqStore_singleBlock(ZSTD_CCtx* zc, + const seqStore_t* const seqStore, + repcodes_t* const dRep, repcodes_t* const cRep, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + U32 lastBlock, U32 isPartition) +{ + const U32 rleMaxLength = 25; + BYTE* op = (BYTE*)dst; + const BYTE* ip = (const BYTE*)src; + size_t cSize; + size_t cSeqsSize; + + /* In case of an RLE or raw block, the simulated decompression repcode history must be reset */ + repcodes_t const dRepOriginal = *dRep; + DEBUGLOG(5, "ZSTD_compressSeqStore_singleBlock"); + if (isPartition) + ZSTD_seqStore_resolveOffCodes(dRep, cRep, seqStore, (U32)(seqStore->sequences - seqStore->sequencesStart)); + + RETURN_ERROR_IF(dstCapacity < ZSTD_blockHeaderSize, dstSize_tooSmall, "Block header doesn't fit"); + cSeqsSize = ZSTD_entropyCompressSeqStore(seqStore, + &zc->blockState.prevCBlock->entropy, &zc->blockState.nextCBlock->entropy, + &zc->appliedParams, + op + ZSTD_blockHeaderSize, dstCapacity - ZSTD_blockHeaderSize, + srcSize, + zc->entropyWorkspace, ENTROPY_WORKSPACE_SIZE /* statically allocated in resetCCtx */, + zc->bmi2); + FORWARD_IF_ERROR(cSeqsSize, "ZSTD_entropyCompressSeqStore failed!"); + + if (!zc->isFirstBlock && + cSeqsSize < rleMaxLength && + ZSTD_isRLE((BYTE const*)src, srcSize)) { + /* We don't want to emit our first block as a RLE even if it qualifies because + * doing so will cause the decoder (cli only) to throw a "should consume all input error." + * This is only an issue for zstd <= v1.4.3 + */ + cSeqsSize = 1; + } + + /* Sequence collection not supported when block splitting */ + if (zc->seqCollector.collectSequences) { + FORWARD_IF_ERROR(ZSTD_copyBlockSequences(&zc->seqCollector, seqStore, dRepOriginal.rep), "copyBlockSequences failed"); + ZSTD_blockState_confirmRepcodesAndEntropyTables(&zc->blockState); + return 0; + } + + if (cSeqsSize == 0) { + cSize = ZSTD_noCompressBlock(op, dstCapacity, ip, srcSize, lastBlock); + FORWARD_IF_ERROR(cSize, "Nocompress block failed"); + DEBUGLOG(4, "Writing out nocompress block, size: %zu", cSize); + *dRep = dRepOriginal; /* reset simulated decompression repcode history */ + } else if (cSeqsSize == 1) { + cSize = ZSTD_rleCompressBlock(op, dstCapacity, *ip, srcSize, lastBlock); + FORWARD_IF_ERROR(cSize, "RLE compress block failed"); + DEBUGLOG(4, "Writing out RLE block, size: %zu", cSize); + *dRep = dRepOriginal; /* reset simulated decompression repcode history */ + } else { + ZSTD_blockState_confirmRepcodesAndEntropyTables(&zc->blockState); + writeBlockHeader(op, cSeqsSize, srcSize, lastBlock); + cSize = ZSTD_blockHeaderSize + cSeqsSize; + DEBUGLOG(4, "Writing out compressed block, size: %zu", cSize); + } + + if (zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode == FSE_repeat_valid) + zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode = FSE_repeat_check; + + return cSize; +} + +/* Struct to keep track of where we are in our recursive calls. */ +typedef struct { + U32* splitLocations; /* Array of split indices */ + size_t idx; /* The current index within splitLocations being worked on */ +} seqStoreSplits; + +#define MIN_SEQUENCES_BLOCK_SPLITTING 300 + +/* Helper function to perform the recursive search for block splits. + * Estimates the cost of seqStore prior to split, and estimates the cost of splitting the sequences in half. + * If advantageous to split, then we recurse down the two sub-blocks. + * If not, or if an error occurred in estimation, then we do not recurse. + * + * Note: The recursion depth is capped by a heuristic minimum number of sequences, + * defined by MIN_SEQUENCES_BLOCK_SPLITTING. + * In theory, this means the absolute largest recursion depth is 10 == log2(maxNbSeqInBlock/MIN_SEQUENCES_BLOCK_SPLITTING). + * In practice, recursion depth usually doesn't go beyond 4. + * + * Furthermore, the number of splits is capped by ZSTD_MAX_NB_BLOCK_SPLITS. + * At ZSTD_MAX_NB_BLOCK_SPLITS == 196 with the current existing blockSize + * maximum of 128 KB, this value is actually impossible to reach. + */ +static void +ZSTD_deriveBlockSplitsHelper(seqStoreSplits* splits, size_t startIdx, size_t endIdx, + ZSTD_CCtx* zc, const seqStore_t* origSeqStore) +{ + seqStore_t* const fullSeqStoreChunk = &zc->blockSplitCtx.fullSeqStoreChunk; + seqStore_t* const firstHalfSeqStore = &zc->blockSplitCtx.firstHalfSeqStore; + seqStore_t* const secondHalfSeqStore = &zc->blockSplitCtx.secondHalfSeqStore; + size_t estimatedOriginalSize; + size_t estimatedFirstHalfSize; + size_t estimatedSecondHalfSize; + size_t midIdx = (startIdx + endIdx)/2; + + DEBUGLOG(5, "ZSTD_deriveBlockSplitsHelper: startIdx=%zu endIdx=%zu", startIdx, endIdx); + assert(endIdx >= startIdx); + if (endIdx - startIdx < MIN_SEQUENCES_BLOCK_SPLITTING || splits->idx >= ZSTD_MAX_NB_BLOCK_SPLITS) { + DEBUGLOG(6, "ZSTD_deriveBlockSplitsHelper: Too few sequences (%zu)", endIdx - startIdx); + return; + } + ZSTD_deriveSeqStoreChunk(fullSeqStoreChunk, origSeqStore, startIdx, endIdx); + ZSTD_deriveSeqStoreChunk(firstHalfSeqStore, origSeqStore, startIdx, midIdx); + ZSTD_deriveSeqStoreChunk(secondHalfSeqStore, origSeqStore, midIdx, endIdx); + estimatedOriginalSize = ZSTD_buildEntropyStatisticsAndEstimateSubBlockSize(fullSeqStoreChunk, zc); + estimatedFirstHalfSize = ZSTD_buildEntropyStatisticsAndEstimateSubBlockSize(firstHalfSeqStore, zc); + estimatedSecondHalfSize = ZSTD_buildEntropyStatisticsAndEstimateSubBlockSize(secondHalfSeqStore, zc); + DEBUGLOG(5, "Estimated original block size: %zu -- First half split: %zu -- Second half split: %zu", + estimatedOriginalSize, estimatedFirstHalfSize, estimatedSecondHalfSize); + if (ZSTD_isError(estimatedOriginalSize) || ZSTD_isError(estimatedFirstHalfSize) || ZSTD_isError(estimatedSecondHalfSize)) { + return; + } + if (estimatedFirstHalfSize + estimatedSecondHalfSize < estimatedOriginalSize) { + DEBUGLOG(5, "split decided at seqNb:%zu", midIdx); + ZSTD_deriveBlockSplitsHelper(splits, startIdx, midIdx, zc, origSeqStore); + splits->splitLocations[splits->idx] = (U32)midIdx; + splits->idx++; + ZSTD_deriveBlockSplitsHelper(splits, midIdx, endIdx, zc, origSeqStore); + } +} + +/* Base recursive function. + * Populates a table with intra-block partition indices that can improve compression ratio. + * + * @return: number of splits made (which equals the size of the partition table - 1). + */ +static size_t ZSTD_deriveBlockSplits(ZSTD_CCtx* zc, U32 partitions[], U32 nbSeq) +{ + seqStoreSplits splits; + splits.splitLocations = partitions; + splits.idx = 0; + if (nbSeq <= 4) { + DEBUGLOG(5, "ZSTD_deriveBlockSplits: Too few sequences to split (%u <= 4)", nbSeq); + /* Refuse to try and split anything with less than 4 sequences */ + return 0; + } + ZSTD_deriveBlockSplitsHelper(&splits, 0, nbSeq, zc, &zc->seqStore); + splits.splitLocations[splits.idx] = nbSeq; + DEBUGLOG(5, "ZSTD_deriveBlockSplits: final nb partitions: %zu", splits.idx+1); + return splits.idx; +} + +/* ZSTD_compressBlock_splitBlock(): + * Attempts to split a given block into multiple blocks to improve compression ratio. + * + * Returns combined size of all blocks (which includes headers), or a ZSTD error code. + */ +static size_t +ZSTD_compressBlock_splitBlock_internal(ZSTD_CCtx* zc, + void* dst, size_t dstCapacity, + const void* src, size_t blockSize, + U32 lastBlock, U32 nbSeq) +{ + size_t cSize = 0; + const BYTE* ip = (const BYTE*)src; + BYTE* op = (BYTE*)dst; + size_t i = 0; + size_t srcBytesTotal = 0; + U32* const partitions = zc->blockSplitCtx.partitions; /* size == ZSTD_MAX_NB_BLOCK_SPLITS */ + seqStore_t* const nextSeqStore = &zc->blockSplitCtx.nextSeqStore; + seqStore_t* const currSeqStore = &zc->blockSplitCtx.currSeqStore; + size_t const numSplits = ZSTD_deriveBlockSplits(zc, partitions, nbSeq); + + /* If a block is split and some partitions are emitted as RLE/uncompressed, then repcode history + * may become invalid. In order to reconcile potentially invalid repcodes, we keep track of two + * separate repcode histories that simulate repcode history on compression and decompression side, + * and use the histories to determine whether we must replace a particular repcode with its raw offset. + * + * 1) cRep gets updated for each partition, regardless of whether the block was emitted as uncompressed + * or RLE. This allows us to retrieve the offset value that an invalid repcode references within + * a nocompress/RLE block. + * 2) dRep gets updated only for compressed partitions, and when a repcode gets replaced, will use + * the replacement offset value rather than the original repcode to update the repcode history. + * dRep also will be the final repcode history sent to the next block. + * + * See ZSTD_seqStore_resolveOffCodes() for more details. + */ + repcodes_t dRep; + repcodes_t cRep; + ZSTD_memcpy(dRep.rep, zc->blockState.prevCBlock->rep, sizeof(repcodes_t)); + ZSTD_memcpy(cRep.rep, zc->blockState.prevCBlock->rep, sizeof(repcodes_t)); + ZSTD_memset(nextSeqStore, 0, sizeof(seqStore_t)); + + DEBUGLOG(5, "ZSTD_compressBlock_splitBlock_internal (dstCapacity=%u, dictLimit=%u, nextToUpdate=%u)", + (unsigned)dstCapacity, (unsigned)zc->blockState.matchState.window.dictLimit, + (unsigned)zc->blockState.matchState.nextToUpdate); + + if (numSplits == 0) { + size_t cSizeSingleBlock = + ZSTD_compressSeqStore_singleBlock(zc, &zc->seqStore, + &dRep, &cRep, + op, dstCapacity, + ip, blockSize, + lastBlock, 0 /* isPartition */); + FORWARD_IF_ERROR(cSizeSingleBlock, "Compressing single block from splitBlock_internal() failed!"); + DEBUGLOG(5, "ZSTD_compressBlock_splitBlock_internal: No splits"); + assert(zc->blockSize <= ZSTD_BLOCKSIZE_MAX); + assert(cSizeSingleBlock <= zc->blockSize + ZSTD_blockHeaderSize); + return cSizeSingleBlock; + } + + ZSTD_deriveSeqStoreChunk(currSeqStore, &zc->seqStore, 0, partitions[0]); + for (i = 0; i <= numSplits; ++i) { + size_t cSizeChunk; + U32 const lastPartition = (i == numSplits); + U32 lastBlockEntireSrc = 0; + + size_t srcBytes = ZSTD_countSeqStoreLiteralsBytes(currSeqStore) + ZSTD_countSeqStoreMatchBytes(currSeqStore); + srcBytesTotal += srcBytes; + if (lastPartition) { + /* This is the final partition, need to account for possible last literals */ + srcBytes += blockSize - srcBytesTotal; + lastBlockEntireSrc = lastBlock; + } else { + ZSTD_deriveSeqStoreChunk(nextSeqStore, &zc->seqStore, partitions[i], partitions[i+1]); + } + + cSizeChunk = ZSTD_compressSeqStore_singleBlock(zc, currSeqStore, + &dRep, &cRep, + op, dstCapacity, + ip, srcBytes, + lastBlockEntireSrc, 1 /* isPartition */); + DEBUGLOG(5, "Estimated size: %zu vs %zu : actual size", + ZSTD_buildEntropyStatisticsAndEstimateSubBlockSize(currSeqStore, zc), cSizeChunk); + FORWARD_IF_ERROR(cSizeChunk, "Compressing chunk failed!"); + + ip += srcBytes; + op += cSizeChunk; + dstCapacity -= cSizeChunk; + cSize += cSizeChunk; + *currSeqStore = *nextSeqStore; + assert(cSizeChunk <= zc->blockSize + ZSTD_blockHeaderSize); + } + /* cRep and dRep may have diverged during the compression. + * If so, we use the dRep repcodes for the next block. + */ + ZSTD_memcpy(zc->blockState.prevCBlock->rep, dRep.rep, sizeof(repcodes_t)); + return cSize; +} + +static size_t +ZSTD_compressBlock_splitBlock(ZSTD_CCtx* zc, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, U32 lastBlock) +{ + U32 nbSeq; + size_t cSize; + DEBUGLOG(4, "ZSTD_compressBlock_splitBlock"); + assert(zc->appliedParams.useBlockSplitter == ZSTD_ps_enable); + + { const size_t bss = ZSTD_buildSeqStore(zc, src, srcSize); + FORWARD_IF_ERROR(bss, "ZSTD_buildSeqStore failed"); + if (bss == ZSTDbss_noCompress) { + if (zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode == FSE_repeat_valid) + zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode = FSE_repeat_check; + RETURN_ERROR_IF(zc->seqCollector.collectSequences, sequenceProducer_failed, "Uncompressible block"); + cSize = ZSTD_noCompressBlock(dst, dstCapacity, src, srcSize, lastBlock); + FORWARD_IF_ERROR(cSize, "ZSTD_noCompressBlock failed"); + DEBUGLOG(4, "ZSTD_compressBlock_splitBlock: Nocompress block"); + return cSize; + } + nbSeq = (U32)(zc->seqStore.sequences - zc->seqStore.sequencesStart); + } + + cSize = ZSTD_compressBlock_splitBlock_internal(zc, dst, dstCapacity, src, srcSize, lastBlock, nbSeq); + FORWARD_IF_ERROR(cSize, "Splitting blocks failed!"); + return cSize; +} + +static size_t +ZSTD_compressBlock_internal(ZSTD_CCtx* zc, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, U32 frame) +{ + /* This is an estimated upper bound for the length of an rle block. + * This isn't the actual upper bound. + * Finding the real threshold needs further investigation. + */ + const U32 rleMaxLength = 25; + size_t cSize; + const BYTE* ip = (const BYTE*)src; + BYTE* op = (BYTE*)dst; + DEBUGLOG(5, "ZSTD_compressBlock_internal (dstCapacity=%u, dictLimit=%u, nextToUpdate=%u)", + (unsigned)dstCapacity, (unsigned)zc->blockState.matchState.window.dictLimit, + (unsigned)zc->blockState.matchState.nextToUpdate); + + { const size_t bss = ZSTD_buildSeqStore(zc, src, srcSize); + FORWARD_IF_ERROR(bss, "ZSTD_buildSeqStore failed"); + if (bss == ZSTDbss_noCompress) { + RETURN_ERROR_IF(zc->seqCollector.collectSequences, sequenceProducer_failed, "Uncompressible block"); + cSize = 0; + goto out; + } + } + + if (zc->seqCollector.collectSequences) { + FORWARD_IF_ERROR(ZSTD_copyBlockSequences(&zc->seqCollector, ZSTD_getSeqStore(zc), zc->blockState.prevCBlock->rep), "copyBlockSequences failed"); + ZSTD_blockState_confirmRepcodesAndEntropyTables(&zc->blockState); + return 0; + } + + /* encode sequences and literals */ + cSize = ZSTD_entropyCompressSeqStore(&zc->seqStore, + &zc->blockState.prevCBlock->entropy, &zc->blockState.nextCBlock->entropy, + &zc->appliedParams, + dst, dstCapacity, + srcSize, + zc->entropyWorkspace, ENTROPY_WORKSPACE_SIZE /* statically allocated in resetCCtx */, + zc->bmi2); + + if (frame && + /* We don't want to emit our first block as a RLE even if it qualifies because + * doing so will cause the decoder (cli only) to throw a "should consume all input error." + * This is only an issue for zstd <= v1.4.3 + */ + !zc->isFirstBlock && + cSize < rleMaxLength && + ZSTD_isRLE(ip, srcSize)) + { + cSize = 1; + op[0] = ip[0]; + } + +out: + if (!ZSTD_isError(cSize) && cSize > 1) { + ZSTD_blockState_confirmRepcodesAndEntropyTables(&zc->blockState); + } + /* We check that dictionaries have offset codes available for the first + * block. After the first block, the offcode table might not have large + * enough codes to represent the offsets in the data. + */ + if (zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode == FSE_repeat_valid) + zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode = FSE_repeat_check; + + return cSize; +} + +static size_t ZSTD_compressBlock_targetCBlockSize_body(ZSTD_CCtx* zc, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const size_t bss, U32 lastBlock) +{ + DEBUGLOG(6, "Attempting ZSTD_compressSuperBlock()"); + if (bss == ZSTDbss_compress) { + if (/* We don't want to emit our first block as a RLE even if it qualifies because + * doing so will cause the decoder (cli only) to throw a "should consume all input error." + * This is only an issue for zstd <= v1.4.3 + */ + !zc->isFirstBlock && + ZSTD_maybeRLE(&zc->seqStore) && + ZSTD_isRLE((BYTE const*)src, srcSize)) + { + return ZSTD_rleCompressBlock(dst, dstCapacity, *(BYTE const*)src, srcSize, lastBlock); + } + /* Attempt superblock compression. + * + * Note that compressed size of ZSTD_compressSuperBlock() is not bound by the + * standard ZSTD_compressBound(). This is a problem, because even if we have + * space now, taking an extra byte now could cause us to run out of space later + * and violate ZSTD_compressBound(). + * + * Define blockBound(blockSize) = blockSize + ZSTD_blockHeaderSize. + * + * In order to respect ZSTD_compressBound() we must attempt to emit a raw + * uncompressed block in these cases: + * * cSize == 0: Return code for an uncompressed block. + * * cSize == dstSize_tooSmall: We may have expanded beyond blockBound(srcSize). + * ZSTD_noCompressBlock() will return dstSize_tooSmall if we are really out of + * output space. + * * cSize >= blockBound(srcSize): We have expanded the block too much so + * emit an uncompressed block. + */ + { size_t const cSize = + ZSTD_compressSuperBlock(zc, dst, dstCapacity, src, srcSize, lastBlock); + if (cSize != ERROR(dstSize_tooSmall)) { + size_t const maxCSize = + srcSize - ZSTD_minGain(srcSize, zc->appliedParams.cParams.strategy); + FORWARD_IF_ERROR(cSize, "ZSTD_compressSuperBlock failed"); + if (cSize != 0 && cSize < maxCSize + ZSTD_blockHeaderSize) { + ZSTD_blockState_confirmRepcodesAndEntropyTables(&zc->blockState); + return cSize; + } + } + } + } /* if (bss == ZSTDbss_compress)*/ + + DEBUGLOG(6, "Resorting to ZSTD_noCompressBlock()"); + /* Superblock compression failed, attempt to emit a single no compress block. + * The decoder will be able to stream this block since it is uncompressed. + */ + return ZSTD_noCompressBlock(dst, dstCapacity, src, srcSize, lastBlock); +} + +static size_t ZSTD_compressBlock_targetCBlockSize(ZSTD_CCtx* zc, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + U32 lastBlock) +{ + size_t cSize = 0; + const size_t bss = ZSTD_buildSeqStore(zc, src, srcSize); + DEBUGLOG(5, "ZSTD_compressBlock_targetCBlockSize (dstCapacity=%u, dictLimit=%u, nextToUpdate=%u, srcSize=%zu)", + (unsigned)dstCapacity, (unsigned)zc->blockState.matchState.window.dictLimit, (unsigned)zc->blockState.matchState.nextToUpdate, srcSize); + FORWARD_IF_ERROR(bss, "ZSTD_buildSeqStore failed"); + + cSize = ZSTD_compressBlock_targetCBlockSize_body(zc, dst, dstCapacity, src, srcSize, bss, lastBlock); + FORWARD_IF_ERROR(cSize, "ZSTD_compressBlock_targetCBlockSize_body failed"); + + if (zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode == FSE_repeat_valid) + zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode = FSE_repeat_check; + + return cSize; +} + +static void ZSTD_overflowCorrectIfNeeded(ZSTD_matchState_t* ms, + ZSTD_cwksp* ws, + ZSTD_CCtx_params const* params, + void const* ip, + void const* iend) +{ + U32 const cycleLog = ZSTD_cycleLog(params->cParams.chainLog, params->cParams.strategy); + U32 const maxDist = (U32)1 << params->cParams.windowLog; + if (ZSTD_window_needOverflowCorrection(ms->window, cycleLog, maxDist, ms->loadedDictEnd, ip, iend)) { + U32 const correction = ZSTD_window_correctOverflow(&ms->window, cycleLog, maxDist, ip); + ZSTD_STATIC_ASSERT(ZSTD_CHAINLOG_MAX <= 30); + ZSTD_STATIC_ASSERT(ZSTD_WINDOWLOG_MAX_32 <= 30); + ZSTD_STATIC_ASSERT(ZSTD_WINDOWLOG_MAX <= 31); + ZSTD_cwksp_mark_tables_dirty(ws); + ZSTD_reduceIndex(ms, params, correction); + ZSTD_cwksp_mark_tables_clean(ws); + if (ms->nextToUpdate < correction) ms->nextToUpdate = 0; + else ms->nextToUpdate -= correction; + /* invalidate dictionaries on overflow correction */ + ms->loadedDictEnd = 0; + ms->dictMatchState = NULL; + } +} + +/*! ZSTD_compress_frameChunk() : +* Compress a chunk of data into one or multiple blocks. +* All blocks will be terminated, all input will be consumed. +* Function will issue an error if there is not enough `dstCapacity` to hold the compressed content. +* Frame is supposed already started (header already produced) +* @return : compressed size, or an error code +*/ +static size_t ZSTD_compress_frameChunk(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + U32 lastFrameChunk) +{ + size_t blockSize = cctx->blockSize; + size_t remaining = srcSize; + const BYTE* ip = (const BYTE*)src; + BYTE* const ostart = (BYTE*)dst; + BYTE* op = ostart; + U32 const maxDist = (U32)1 << cctx->appliedParams.cParams.windowLog; + + assert(cctx->appliedParams.cParams.windowLog <= ZSTD_WINDOWLOG_MAX); + + DEBUGLOG(4, "ZSTD_compress_frameChunk (blockSize=%u)", (unsigned)blockSize); + if (cctx->appliedParams.fParams.checksumFlag && srcSize) + XXH64_update(&cctx->xxhState, src, srcSize); + + while (remaining) { + ZSTD_matchState_t* const ms = &cctx->blockState.matchState; + U32 const lastBlock = lastFrameChunk & (blockSize >= remaining); + + /* TODO: See 3090. We reduced MIN_CBLOCK_SIZE from 3 to 2 so to compensate we are adding + * additional 1. We need to revisit and change this logic to be more consistent */ + RETURN_ERROR_IF(dstCapacity < ZSTD_blockHeaderSize + MIN_CBLOCK_SIZE + 1, + dstSize_tooSmall, + "not enough space to store compressed block"); + if (remaining < blockSize) blockSize = remaining; + + ZSTD_overflowCorrectIfNeeded( + ms, &cctx->workspace, &cctx->appliedParams, ip, ip + blockSize); + ZSTD_checkDictValidity(&ms->window, ip + blockSize, maxDist, &ms->loadedDictEnd, &ms->dictMatchState); + ZSTD_window_enforceMaxDist(&ms->window, ip, maxDist, &ms->loadedDictEnd, &ms->dictMatchState); + + /* Ensure hash/chain table insertion resumes no sooner than lowlimit */ + if (ms->nextToUpdate < ms->window.lowLimit) ms->nextToUpdate = ms->window.lowLimit; + + { size_t cSize; + if (ZSTD_useTargetCBlockSize(&cctx->appliedParams)) { + cSize = ZSTD_compressBlock_targetCBlockSize(cctx, op, dstCapacity, ip, blockSize, lastBlock); + FORWARD_IF_ERROR(cSize, "ZSTD_compressBlock_targetCBlockSize failed"); + assert(cSize > 0); + assert(cSize <= blockSize + ZSTD_blockHeaderSize); + } else if (ZSTD_blockSplitterEnabled(&cctx->appliedParams)) { + cSize = ZSTD_compressBlock_splitBlock(cctx, op, dstCapacity, ip, blockSize, lastBlock); + FORWARD_IF_ERROR(cSize, "ZSTD_compressBlock_splitBlock failed"); + assert(cSize > 0 || cctx->seqCollector.collectSequences == 1); + } else { + cSize = ZSTD_compressBlock_internal(cctx, + op+ZSTD_blockHeaderSize, dstCapacity-ZSTD_blockHeaderSize, + ip, blockSize, 1 /* frame */); + FORWARD_IF_ERROR(cSize, "ZSTD_compressBlock_internal failed"); + + if (cSize == 0) { /* block is not compressible */ + cSize = ZSTD_noCompressBlock(op, dstCapacity, ip, blockSize, lastBlock); + FORWARD_IF_ERROR(cSize, "ZSTD_noCompressBlock failed"); + } else { + U32 const cBlockHeader = cSize == 1 ? + lastBlock + (((U32)bt_rle)<<1) + (U32)(blockSize << 3) : + lastBlock + (((U32)bt_compressed)<<1) + (U32)(cSize << 3); + MEM_writeLE24(op, cBlockHeader); + cSize += ZSTD_blockHeaderSize; + } + } /* if (ZSTD_useTargetCBlockSize(&cctx->appliedParams))*/ + + + ip += blockSize; + assert(remaining >= blockSize); + remaining -= blockSize; + op += cSize; + assert(dstCapacity >= cSize); + dstCapacity -= cSize; + cctx->isFirstBlock = 0; + DEBUGLOG(5, "ZSTD_compress_frameChunk: adding a block of size %u", + (unsigned)cSize); + } } + + if (lastFrameChunk && (op>ostart)) cctx->stage = ZSTDcs_ending; + return (size_t)(op-ostart); +} + + +static size_t ZSTD_writeFrameHeader(void* dst, size_t dstCapacity, + const ZSTD_CCtx_params* params, U64 pledgedSrcSize, U32 dictID) +{ BYTE* const op = (BYTE*)dst; + U32 const dictIDSizeCodeLength = (dictID>0) + (dictID>=256) + (dictID>=65536); /* 0-3 */ + U32 const dictIDSizeCode = params->fParams.noDictIDFlag ? 0 : dictIDSizeCodeLength; /* 0-3 */ + U32 const checksumFlag = params->fParams.checksumFlag>0; + U32 const windowSize = (U32)1 << params->cParams.windowLog; + U32 const singleSegment = params->fParams.contentSizeFlag && (windowSize >= pledgedSrcSize); + BYTE const windowLogByte = (BYTE)((params->cParams.windowLog - ZSTD_WINDOWLOG_ABSOLUTEMIN) << 3); + U32 const fcsCode = params->fParams.contentSizeFlag ? + (pledgedSrcSize>=256) + (pledgedSrcSize>=65536+256) + (pledgedSrcSize>=0xFFFFFFFFU) : 0; /* 0-3 */ + BYTE const frameHeaderDescriptionByte = (BYTE)(dictIDSizeCode + (checksumFlag<<2) + (singleSegment<<5) + (fcsCode<<6) ); + size_t pos=0; + + assert(!(params->fParams.contentSizeFlag && pledgedSrcSize == ZSTD_CONTENTSIZE_UNKNOWN)); + RETURN_ERROR_IF(dstCapacity < ZSTD_FRAMEHEADERSIZE_MAX, dstSize_tooSmall, + "dst buf is too small to fit worst-case frame header size."); + DEBUGLOG(4, "ZSTD_writeFrameHeader : dictIDFlag : %u ; dictID : %u ; dictIDSizeCode : %u", + !params->fParams.noDictIDFlag, (unsigned)dictID, (unsigned)dictIDSizeCode); + if (params->format == ZSTD_f_zstd1) { + MEM_writeLE32(dst, ZSTD_MAGICNUMBER); + pos = 4; + } + op[pos++] = frameHeaderDescriptionByte; + if (!singleSegment) op[pos++] = windowLogByte; + switch(dictIDSizeCode) + { + default: + assert(0); /* impossible */ + ZSTD_FALLTHROUGH; + case 0 : break; + case 1 : op[pos] = (BYTE)(dictID); pos++; break; + case 2 : MEM_writeLE16(op+pos, (U16)dictID); pos+=2; break; + case 3 : MEM_writeLE32(op+pos, dictID); pos+=4; break; + } + switch(fcsCode) + { + default: + assert(0); /* impossible */ + ZSTD_FALLTHROUGH; + case 0 : if (singleSegment) op[pos++] = (BYTE)(pledgedSrcSize); break; + case 1 : MEM_writeLE16(op+pos, (U16)(pledgedSrcSize-256)); pos+=2; break; + case 2 : MEM_writeLE32(op+pos, (U32)(pledgedSrcSize)); pos+=4; break; + case 3 : MEM_writeLE64(op+pos, (U64)(pledgedSrcSize)); pos+=8; break; + } + return pos; +} + +/* ZSTD_writeSkippableFrame_advanced() : + * Writes out a skippable frame with the specified magic number variant (16 are supported), + * from ZSTD_MAGIC_SKIPPABLE_START to ZSTD_MAGIC_SKIPPABLE_START+15, and the desired source data. + * + * Returns the total number of bytes written, or a ZSTD error code. + */ +size_t ZSTD_writeSkippableFrame(void* dst, size_t dstCapacity, + const void* src, size_t srcSize, unsigned magicVariant) { + BYTE* op = (BYTE*)dst; + RETURN_ERROR_IF(dstCapacity < srcSize + ZSTD_SKIPPABLEHEADERSIZE /* Skippable frame overhead */, + dstSize_tooSmall, "Not enough room for skippable frame"); + RETURN_ERROR_IF(srcSize > (unsigned)0xFFFFFFFF, srcSize_wrong, "Src size too large for skippable frame"); + RETURN_ERROR_IF(magicVariant > 15, parameter_outOfBound, "Skippable frame magic number variant not supported"); + + MEM_writeLE32(op, (U32)(ZSTD_MAGIC_SKIPPABLE_START + magicVariant)); + MEM_writeLE32(op+4, (U32)srcSize); + ZSTD_memcpy(op+8, src, srcSize); + return srcSize + ZSTD_SKIPPABLEHEADERSIZE; +} + +/* ZSTD_writeLastEmptyBlock() : + * output an empty Block with end-of-frame mark to complete a frame + * @return : size of data written into `dst` (== ZSTD_blockHeaderSize (defined in zstd_internal.h)) + * or an error code if `dstCapacity` is too small (stage == ZSTDcs_init); + assert(nbSeq == 0 || cctx->appliedParams.ldmParams.enableLdm != ZSTD_ps_enable); + cctx->externSeqStore.seq = seq; + cctx->externSeqStore.size = nbSeq; + cctx->externSeqStore.capacity = nbSeq; + cctx->externSeqStore.pos = 0; + cctx->externSeqStore.posInSequence = 0; +} + + +static size_t ZSTD_compressContinue_internal (ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + U32 frame, U32 lastFrameChunk) +{ + ZSTD_matchState_t* const ms = &cctx->blockState.matchState; + size_t fhSize = 0; + + DEBUGLOG(5, "ZSTD_compressContinue_internal, stage: %u, srcSize: %u", + cctx->stage, (unsigned)srcSize); + RETURN_ERROR_IF(cctx->stage==ZSTDcs_created, stage_wrong, + "missing init (ZSTD_compressBegin)"); + + if (frame && (cctx->stage==ZSTDcs_init)) { + fhSize = ZSTD_writeFrameHeader(dst, dstCapacity, &cctx->appliedParams, + cctx->pledgedSrcSizePlusOne-1, cctx->dictID); + FORWARD_IF_ERROR(fhSize, "ZSTD_writeFrameHeader failed"); + assert(fhSize <= dstCapacity); + dstCapacity -= fhSize; + dst = (char*)dst + fhSize; + cctx->stage = ZSTDcs_ongoing; + } + + if (!srcSize) return fhSize; /* do not generate an empty block if no input */ + + if (!ZSTD_window_update(&ms->window, src, srcSize, ms->forceNonContiguous)) { + ms->forceNonContiguous = 0; + ms->nextToUpdate = ms->window.dictLimit; + } + if (cctx->appliedParams.ldmParams.enableLdm == ZSTD_ps_enable) { + ZSTD_window_update(&cctx->ldmState.window, src, srcSize, /* forceNonContiguous */ 0); + } + + if (!frame) { + /* overflow check and correction for block mode */ + ZSTD_overflowCorrectIfNeeded( + ms, &cctx->workspace, &cctx->appliedParams, + src, (BYTE const*)src + srcSize); + } + + DEBUGLOG(5, "ZSTD_compressContinue_internal (blockSize=%u)", (unsigned)cctx->blockSize); + { size_t const cSize = frame ? + ZSTD_compress_frameChunk (cctx, dst, dstCapacity, src, srcSize, lastFrameChunk) : + ZSTD_compressBlock_internal (cctx, dst, dstCapacity, src, srcSize, 0 /* frame */); + FORWARD_IF_ERROR(cSize, "%s", frame ? "ZSTD_compress_frameChunk failed" : "ZSTD_compressBlock_internal failed"); + cctx->consumedSrcSize += srcSize; + cctx->producedCSize += (cSize + fhSize); + assert(!(cctx->appliedParams.fParams.contentSizeFlag && cctx->pledgedSrcSizePlusOne == 0)); + if (cctx->pledgedSrcSizePlusOne != 0) { /* control src size */ + ZSTD_STATIC_ASSERT(ZSTD_CONTENTSIZE_UNKNOWN == (unsigned long long)-1); + RETURN_ERROR_IF( + cctx->consumedSrcSize+1 > cctx->pledgedSrcSizePlusOne, + srcSize_wrong, + "error : pledgedSrcSize = %u, while realSrcSize >= %u", + (unsigned)cctx->pledgedSrcSizePlusOne-1, + (unsigned)cctx->consumedSrcSize); + } + return cSize + fhSize; + } +} + +size_t ZSTD_compressContinue_public(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize) +{ + DEBUGLOG(5, "ZSTD_compressContinue (srcSize=%u)", (unsigned)srcSize); + return ZSTD_compressContinue_internal(cctx, dst, dstCapacity, src, srcSize, 1 /* frame mode */, 0 /* last chunk */); +} + +/* NOTE: Must just wrap ZSTD_compressContinue_public() */ +size_t ZSTD_compressContinue(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize) +{ + return ZSTD_compressContinue_public(cctx, dst, dstCapacity, src, srcSize); +} + +static size_t ZSTD_getBlockSize_deprecated(const ZSTD_CCtx* cctx) +{ + ZSTD_compressionParameters const cParams = cctx->appliedParams.cParams; + assert(!ZSTD_checkCParams(cParams)); + return MIN(cctx->appliedParams.maxBlockSize, (size_t)1 << cParams.windowLog); +} + +/* NOTE: Must just wrap ZSTD_getBlockSize_deprecated() */ +size_t ZSTD_getBlockSize(const ZSTD_CCtx* cctx) +{ + return ZSTD_getBlockSize_deprecated(cctx); +} + +/* NOTE: Must just wrap ZSTD_compressBlock_deprecated() */ +size_t ZSTD_compressBlock_deprecated(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize) +{ + DEBUGLOG(5, "ZSTD_compressBlock: srcSize = %u", (unsigned)srcSize); + { size_t const blockSizeMax = ZSTD_getBlockSize_deprecated(cctx); + RETURN_ERROR_IF(srcSize > blockSizeMax, srcSize_wrong, "input is larger than a block"); } + + return ZSTD_compressContinue_internal(cctx, dst, dstCapacity, src, srcSize, 0 /* frame mode */, 0 /* last chunk */); +} + +/* NOTE: Must just wrap ZSTD_compressBlock_deprecated() */ +size_t ZSTD_compressBlock(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize) +{ + return ZSTD_compressBlock_deprecated(cctx, dst, dstCapacity, src, srcSize); +} + +/*! ZSTD_loadDictionaryContent() : + * @return : 0, or an error code + */ +static size_t ZSTD_loadDictionaryContent(ZSTD_matchState_t* ms, + ldmState_t* ls, + ZSTD_cwksp* ws, + ZSTD_CCtx_params const* params, + const void* src, size_t srcSize, + ZSTD_dictTableLoadMethod_e dtlm, + ZSTD_tableFillPurpose_e tfp) +{ + const BYTE* ip = (const BYTE*) src; + const BYTE* const iend = ip + srcSize; + int const loadLdmDict = params->ldmParams.enableLdm == ZSTD_ps_enable && ls != NULL; + + /* Assert that the ms params match the params we're being given */ + ZSTD_assertEqualCParams(params->cParams, ms->cParams); + + { /* Ensure large dictionaries can't cause index overflow */ + + /* Allow the dictionary to set indices up to exactly ZSTD_CURRENT_MAX. + * Dictionaries right at the edge will immediately trigger overflow + * correction, but I don't want to insert extra constraints here. + */ + U32 maxDictSize = ZSTD_CURRENT_MAX - ZSTD_WINDOW_START_INDEX; + + int const CDictTaggedIndices = ZSTD_CDictIndicesAreTagged(¶ms->cParams); + if (CDictTaggedIndices && tfp == ZSTD_tfp_forCDict) { + /* Some dictionary matchfinders in zstd use "short cache", + * which treats the lower ZSTD_SHORT_CACHE_TAG_BITS of each + * CDict hashtable entry as a tag rather than as part of an index. + * When short cache is used, we need to truncate the dictionary + * so that its indices don't overlap with the tag. */ + U32 const shortCacheMaxDictSize = (1u << (32 - ZSTD_SHORT_CACHE_TAG_BITS)) - ZSTD_WINDOW_START_INDEX; + maxDictSize = MIN(maxDictSize, shortCacheMaxDictSize); + assert(!loadLdmDict); + } + + /* If the dictionary is too large, only load the suffix of the dictionary. */ + if (srcSize > maxDictSize) { + ip = iend - maxDictSize; + src = ip; + srcSize = maxDictSize; + } + } + + if (srcSize > ZSTD_CHUNKSIZE_MAX) { + /* We must have cleared our windows when our source is this large. */ + assert(ZSTD_window_isEmpty(ms->window)); + if (loadLdmDict) assert(ZSTD_window_isEmpty(ls->window)); + } + ZSTD_window_update(&ms->window, src, srcSize, /* forceNonContiguous */ 0); + + DEBUGLOG(4, "ZSTD_loadDictionaryContent(): useRowMatchFinder=%d", (int)params->useRowMatchFinder); + + if (loadLdmDict) { /* Load the entire dict into LDM matchfinders. */ + ZSTD_window_update(&ls->window, src, srcSize, /* forceNonContiguous */ 0); + ls->loadedDictEnd = params->forceWindow ? 0 : (U32)(iend - ls->window.base); + ZSTD_ldm_fillHashTable(ls, ip, iend, ¶ms->ldmParams); + } + + /* If the dict is larger than we can reasonably index in our tables, only load the suffix. */ + if (params->cParams.strategy < ZSTD_btultra) { + U32 maxDictSize = 8U << MIN(MAX(params->cParams.hashLog, params->cParams.chainLog), 28); + if (srcSize > maxDictSize) { + ip = iend - maxDictSize; + src = ip; + srcSize = maxDictSize; + } + } + + ms->nextToUpdate = (U32)(ip - ms->window.base); + ms->loadedDictEnd = params->forceWindow ? 0 : (U32)(iend - ms->window.base); + ms->forceNonContiguous = params->deterministicRefPrefix; + + if (srcSize <= HASH_READ_SIZE) return 0; + + ZSTD_overflowCorrectIfNeeded(ms, ws, params, ip, iend); + + switch(params->cParams.strategy) + { + case ZSTD_fast: + ZSTD_fillHashTable(ms, iend, dtlm, tfp); + break; + case ZSTD_dfast: +#ifndef ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR + ZSTD_fillDoubleHashTable(ms, iend, dtlm, tfp); +#else + assert(0); /* shouldn't be called: cparams should've been adjusted. */ +#endif + break; + + case ZSTD_greedy: + case ZSTD_lazy: + case ZSTD_lazy2: +#if !defined(ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_LAZY_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_LAZY2_BLOCK_COMPRESSOR) + assert(srcSize >= HASH_READ_SIZE); + if (ms->dedicatedDictSearch) { + assert(ms->chainTable != NULL); + ZSTD_dedicatedDictSearch_lazy_loadDictionary(ms, iend-HASH_READ_SIZE); + } else { + assert(params->useRowMatchFinder != ZSTD_ps_auto); + if (params->useRowMatchFinder == ZSTD_ps_enable) { + size_t const tagTableSize = ((size_t)1 << params->cParams.hashLog); + ZSTD_memset(ms->tagTable, 0, tagTableSize); + ZSTD_row_update(ms, iend-HASH_READ_SIZE); + DEBUGLOG(4, "Using row-based hash table for lazy dict"); + } else { + ZSTD_insertAndFindFirstIndex(ms, iend-HASH_READ_SIZE); + DEBUGLOG(4, "Using chain-based hash table for lazy dict"); + } + } +#else + assert(0); /* shouldn't be called: cparams should've been adjusted. */ +#endif + break; + + case ZSTD_btlazy2: /* we want the dictionary table fully sorted */ + case ZSTD_btopt: + case ZSTD_btultra: + case ZSTD_btultra2: +#if !defined(ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTOPT_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTULTRA_BLOCK_COMPRESSOR) + assert(srcSize >= HASH_READ_SIZE); + ZSTD_updateTree(ms, iend-HASH_READ_SIZE, iend); +#else + assert(0); /* shouldn't be called: cparams should've been adjusted. */ +#endif + break; + + default: + assert(0); /* not possible : not a valid strategy id */ + } + + ms->nextToUpdate = (U32)(iend - ms->window.base); + return 0; +} + + +/* Dictionaries that assign zero probability to symbols that show up causes problems + * when FSE encoding. Mark dictionaries with zero probability symbols as FSE_repeat_check + * and only dictionaries with 100% valid symbols can be assumed valid. + */ +static FSE_repeat ZSTD_dictNCountRepeat(short* normalizedCounter, unsigned dictMaxSymbolValue, unsigned maxSymbolValue) +{ + U32 s; + if (dictMaxSymbolValue < maxSymbolValue) { + return FSE_repeat_check; + } + for (s = 0; s <= maxSymbolValue; ++s) { + if (normalizedCounter[s] == 0) { + return FSE_repeat_check; + } + } + return FSE_repeat_valid; +} + +size_t ZSTD_loadCEntropy(ZSTD_compressedBlockState_t* bs, void* workspace, + const void* const dict, size_t dictSize) +{ + short offcodeNCount[MaxOff+1]; + unsigned offcodeMaxValue = MaxOff; + const BYTE* dictPtr = (const BYTE*)dict; /* skip magic num and dict ID */ + const BYTE* const dictEnd = dictPtr + dictSize; + dictPtr += 8; + bs->entropy.huf.repeatMode = HUF_repeat_check; + + { unsigned maxSymbolValue = 255; + unsigned hasZeroWeights = 1; + size_t const hufHeaderSize = HUF_readCTable((HUF_CElt*)bs->entropy.huf.CTable, &maxSymbolValue, dictPtr, + dictEnd-dictPtr, &hasZeroWeights); + + /* We only set the loaded table as valid if it contains all non-zero + * weights. Otherwise, we set it to check */ + if (!hasZeroWeights && maxSymbolValue == 255) + bs->entropy.huf.repeatMode = HUF_repeat_valid; + + RETURN_ERROR_IF(HUF_isError(hufHeaderSize), dictionary_corrupted, ""); + dictPtr += hufHeaderSize; + } + + { unsigned offcodeLog; + size_t const offcodeHeaderSize = FSE_readNCount(offcodeNCount, &offcodeMaxValue, &offcodeLog, dictPtr, dictEnd-dictPtr); + RETURN_ERROR_IF(FSE_isError(offcodeHeaderSize), dictionary_corrupted, ""); + RETURN_ERROR_IF(offcodeLog > OffFSELog, dictionary_corrupted, ""); + /* fill all offset symbols to avoid garbage at end of table */ + RETURN_ERROR_IF(FSE_isError(FSE_buildCTable_wksp( + bs->entropy.fse.offcodeCTable, + offcodeNCount, MaxOff, offcodeLog, + workspace, HUF_WORKSPACE_SIZE)), + dictionary_corrupted, ""); + /* Defer checking offcodeMaxValue because we need to know the size of the dictionary content */ + dictPtr += offcodeHeaderSize; + } + + { short matchlengthNCount[MaxML+1]; + unsigned matchlengthMaxValue = MaxML, matchlengthLog; + size_t const matchlengthHeaderSize = FSE_readNCount(matchlengthNCount, &matchlengthMaxValue, &matchlengthLog, dictPtr, dictEnd-dictPtr); + RETURN_ERROR_IF(FSE_isError(matchlengthHeaderSize), dictionary_corrupted, ""); + RETURN_ERROR_IF(matchlengthLog > MLFSELog, dictionary_corrupted, ""); + RETURN_ERROR_IF(FSE_isError(FSE_buildCTable_wksp( + bs->entropy.fse.matchlengthCTable, + matchlengthNCount, matchlengthMaxValue, matchlengthLog, + workspace, HUF_WORKSPACE_SIZE)), + dictionary_corrupted, ""); + bs->entropy.fse.matchlength_repeatMode = ZSTD_dictNCountRepeat(matchlengthNCount, matchlengthMaxValue, MaxML); + dictPtr += matchlengthHeaderSize; + } + + { short litlengthNCount[MaxLL+1]; + unsigned litlengthMaxValue = MaxLL, litlengthLog; + size_t const litlengthHeaderSize = FSE_readNCount(litlengthNCount, &litlengthMaxValue, &litlengthLog, dictPtr, dictEnd-dictPtr); + RETURN_ERROR_IF(FSE_isError(litlengthHeaderSize), dictionary_corrupted, ""); + RETURN_ERROR_IF(litlengthLog > LLFSELog, dictionary_corrupted, ""); + RETURN_ERROR_IF(FSE_isError(FSE_buildCTable_wksp( + bs->entropy.fse.litlengthCTable, + litlengthNCount, litlengthMaxValue, litlengthLog, + workspace, HUF_WORKSPACE_SIZE)), + dictionary_corrupted, ""); + bs->entropy.fse.litlength_repeatMode = ZSTD_dictNCountRepeat(litlengthNCount, litlengthMaxValue, MaxLL); + dictPtr += litlengthHeaderSize; + } + + RETURN_ERROR_IF(dictPtr+12 > dictEnd, dictionary_corrupted, ""); + bs->rep[0] = MEM_readLE32(dictPtr+0); + bs->rep[1] = MEM_readLE32(dictPtr+4); + bs->rep[2] = MEM_readLE32(dictPtr+8); + dictPtr += 12; + + { size_t const dictContentSize = (size_t)(dictEnd - dictPtr); + U32 offcodeMax = MaxOff; + if (dictContentSize <= ((U32)-1) - 128 KB) { + U32 const maxOffset = (U32)dictContentSize + 128 KB; /* The maximum offset that must be supported */ + offcodeMax = ZSTD_highbit32(maxOffset); /* Calculate minimum offset code required to represent maxOffset */ + } + /* All offset values <= dictContentSize + 128 KB must be representable for a valid table */ + bs->entropy.fse.offcode_repeatMode = ZSTD_dictNCountRepeat(offcodeNCount, offcodeMaxValue, MIN(offcodeMax, MaxOff)); + + /* All repCodes must be <= dictContentSize and != 0 */ + { U32 u; + for (u=0; u<3; u++) { + RETURN_ERROR_IF(bs->rep[u] == 0, dictionary_corrupted, ""); + RETURN_ERROR_IF(bs->rep[u] > dictContentSize, dictionary_corrupted, ""); + } } } + + return dictPtr - (const BYTE*)dict; +} + +/* Dictionary format : + * See : + * https://github.com/facebook/zstd/blob/release/doc/zstd_compression_format.md#dictionary-format + */ +/*! ZSTD_loadZstdDictionary() : + * @return : dictID, or an error code + * assumptions : magic number supposed already checked + * dictSize supposed >= 8 + */ +static size_t ZSTD_loadZstdDictionary(ZSTD_compressedBlockState_t* bs, + ZSTD_matchState_t* ms, + ZSTD_cwksp* ws, + ZSTD_CCtx_params const* params, + const void* dict, size_t dictSize, + ZSTD_dictTableLoadMethod_e dtlm, + ZSTD_tableFillPurpose_e tfp, + void* workspace) +{ + const BYTE* dictPtr = (const BYTE*)dict; + const BYTE* const dictEnd = dictPtr + dictSize; + size_t dictID; + size_t eSize; + ZSTD_STATIC_ASSERT(HUF_WORKSPACE_SIZE >= (1<= 8); + assert(MEM_readLE32(dictPtr) == ZSTD_MAGIC_DICTIONARY); + + dictID = params->fParams.noDictIDFlag ? 0 : MEM_readLE32(dictPtr + 4 /* skip magic number */ ); + eSize = ZSTD_loadCEntropy(bs, workspace, dict, dictSize); + FORWARD_IF_ERROR(eSize, "ZSTD_loadCEntropy failed"); + dictPtr += eSize; + + { + size_t const dictContentSize = (size_t)(dictEnd - dictPtr); + FORWARD_IF_ERROR(ZSTD_loadDictionaryContent( + ms, NULL, ws, params, dictPtr, dictContentSize, dtlm, tfp), ""); + } + return dictID; +} + +/** ZSTD_compress_insertDictionary() : +* @return : dictID, or an error code */ +static size_t +ZSTD_compress_insertDictionary(ZSTD_compressedBlockState_t* bs, + ZSTD_matchState_t* ms, + ldmState_t* ls, + ZSTD_cwksp* ws, + const ZSTD_CCtx_params* params, + const void* dict, size_t dictSize, + ZSTD_dictContentType_e dictContentType, + ZSTD_dictTableLoadMethod_e dtlm, + ZSTD_tableFillPurpose_e tfp, + void* workspace) +{ + DEBUGLOG(4, "ZSTD_compress_insertDictionary (dictSize=%u)", (U32)dictSize); + if ((dict==NULL) || (dictSize<8)) { + RETURN_ERROR_IF(dictContentType == ZSTD_dct_fullDict, dictionary_wrong, ""); + return 0; + } + + ZSTD_reset_compressedBlockState(bs); + + /* dict restricted modes */ + if (dictContentType == ZSTD_dct_rawContent) + return ZSTD_loadDictionaryContent(ms, ls, ws, params, dict, dictSize, dtlm, tfp); + + if (MEM_readLE32(dict) != ZSTD_MAGIC_DICTIONARY) { + if (dictContentType == ZSTD_dct_auto) { + DEBUGLOG(4, "raw content dictionary detected"); + return ZSTD_loadDictionaryContent( + ms, ls, ws, params, dict, dictSize, dtlm, tfp); + } + RETURN_ERROR_IF(dictContentType == ZSTD_dct_fullDict, dictionary_wrong, ""); + assert(0); /* impossible */ + } + + /* dict as full zstd dictionary */ + return ZSTD_loadZstdDictionary( + bs, ms, ws, params, dict, dictSize, dtlm, tfp, workspace); +} + +#define ZSTD_USE_CDICT_PARAMS_SRCSIZE_CUTOFF (128 KB) +#define ZSTD_USE_CDICT_PARAMS_DICTSIZE_MULTIPLIER (6ULL) + +/*! ZSTD_compressBegin_internal() : + * Assumption : either @dict OR @cdict (or none) is non-NULL, never both + * @return : 0, or an error code */ +static size_t ZSTD_compressBegin_internal(ZSTD_CCtx* cctx, + const void* dict, size_t dictSize, + ZSTD_dictContentType_e dictContentType, + ZSTD_dictTableLoadMethod_e dtlm, + const ZSTD_CDict* cdict, + const ZSTD_CCtx_params* params, U64 pledgedSrcSize, + ZSTD_buffered_policy_e zbuff) +{ + size_t const dictContentSize = cdict ? cdict->dictContentSize : dictSize; +#if ZSTD_TRACE + cctx->traceCtx = (ZSTD_trace_compress_begin != NULL) ? ZSTD_trace_compress_begin(cctx) : 0; +#endif + DEBUGLOG(4, "ZSTD_compressBegin_internal: wlog=%u", params->cParams.windowLog); + /* params are supposed to be fully validated at this point */ + assert(!ZSTD_isError(ZSTD_checkCParams(params->cParams))); + assert(!((dict) && (cdict))); /* either dict or cdict, not both */ + if ( (cdict) + && (cdict->dictContentSize > 0) + && ( pledgedSrcSize < ZSTD_USE_CDICT_PARAMS_SRCSIZE_CUTOFF + || pledgedSrcSize < cdict->dictContentSize * ZSTD_USE_CDICT_PARAMS_DICTSIZE_MULTIPLIER + || pledgedSrcSize == ZSTD_CONTENTSIZE_UNKNOWN + || cdict->compressionLevel == 0) + && (params->attachDictPref != ZSTD_dictForceLoad) ) { + return ZSTD_resetCCtx_usingCDict(cctx, cdict, params, pledgedSrcSize, zbuff); + } + + FORWARD_IF_ERROR( ZSTD_resetCCtx_internal(cctx, params, pledgedSrcSize, + dictContentSize, + ZSTDcrp_makeClean, zbuff) , ""); + { size_t const dictID = cdict ? + ZSTD_compress_insertDictionary( + cctx->blockState.prevCBlock, &cctx->blockState.matchState, + &cctx->ldmState, &cctx->workspace, &cctx->appliedParams, cdict->dictContent, + cdict->dictContentSize, cdict->dictContentType, dtlm, + ZSTD_tfp_forCCtx, cctx->entropyWorkspace) + : ZSTD_compress_insertDictionary( + cctx->blockState.prevCBlock, &cctx->blockState.matchState, + &cctx->ldmState, &cctx->workspace, &cctx->appliedParams, dict, dictSize, + dictContentType, dtlm, ZSTD_tfp_forCCtx, cctx->entropyWorkspace); + FORWARD_IF_ERROR(dictID, "ZSTD_compress_insertDictionary failed"); + assert(dictID <= UINT_MAX); + cctx->dictID = (U32)dictID; + cctx->dictContentSize = dictContentSize; + } + return 0; +} + +size_t ZSTD_compressBegin_advanced_internal(ZSTD_CCtx* cctx, + const void* dict, size_t dictSize, + ZSTD_dictContentType_e dictContentType, + ZSTD_dictTableLoadMethod_e dtlm, + const ZSTD_CDict* cdict, + const ZSTD_CCtx_params* params, + unsigned long long pledgedSrcSize) +{ + DEBUGLOG(4, "ZSTD_compressBegin_advanced_internal: wlog=%u", params->cParams.windowLog); + /* compression parameters verification and optimization */ + FORWARD_IF_ERROR( ZSTD_checkCParams(params->cParams) , ""); + return ZSTD_compressBegin_internal(cctx, + dict, dictSize, dictContentType, dtlm, + cdict, + params, pledgedSrcSize, + ZSTDb_not_buffered); +} + +/*! ZSTD_compressBegin_advanced() : +* @return : 0, or an error code */ +size_t ZSTD_compressBegin_advanced(ZSTD_CCtx* cctx, + const void* dict, size_t dictSize, + ZSTD_parameters params, unsigned long long pledgedSrcSize) +{ + ZSTD_CCtx_params cctxParams; + ZSTD_CCtxParams_init_internal(&cctxParams, ¶ms, ZSTD_NO_CLEVEL); + return ZSTD_compressBegin_advanced_internal(cctx, + dict, dictSize, ZSTD_dct_auto, ZSTD_dtlm_fast, + NULL /*cdict*/, + &cctxParams, pledgedSrcSize); +} + +static size_t +ZSTD_compressBegin_usingDict_deprecated(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, int compressionLevel) +{ + ZSTD_CCtx_params cctxParams; + { ZSTD_parameters const params = ZSTD_getParams_internal(compressionLevel, ZSTD_CONTENTSIZE_UNKNOWN, dictSize, ZSTD_cpm_noAttachDict); + ZSTD_CCtxParams_init_internal(&cctxParams, ¶ms, (compressionLevel == 0) ? ZSTD_CLEVEL_DEFAULT : compressionLevel); + } + DEBUGLOG(4, "ZSTD_compressBegin_usingDict (dictSize=%u)", (unsigned)dictSize); + return ZSTD_compressBegin_internal(cctx, dict, dictSize, ZSTD_dct_auto, ZSTD_dtlm_fast, NULL, + &cctxParams, ZSTD_CONTENTSIZE_UNKNOWN, ZSTDb_not_buffered); +} + +size_t +ZSTD_compressBegin_usingDict(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, int compressionLevel) +{ + return ZSTD_compressBegin_usingDict_deprecated(cctx, dict, dictSize, compressionLevel); +} + +size_t ZSTD_compressBegin(ZSTD_CCtx* cctx, int compressionLevel) +{ + return ZSTD_compressBegin_usingDict_deprecated(cctx, NULL, 0, compressionLevel); +} + + +/*! ZSTD_writeEpilogue() : +* Ends a frame. +* @return : nb of bytes written into dst (or an error code) */ +static size_t ZSTD_writeEpilogue(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity) +{ + BYTE* const ostart = (BYTE*)dst; + BYTE* op = ostart; + + DEBUGLOG(4, "ZSTD_writeEpilogue"); + RETURN_ERROR_IF(cctx->stage == ZSTDcs_created, stage_wrong, "init missing"); + + /* special case : empty frame */ + if (cctx->stage == ZSTDcs_init) { + size_t fhSize = ZSTD_writeFrameHeader(dst, dstCapacity, &cctx->appliedParams, 0, 0); + FORWARD_IF_ERROR(fhSize, "ZSTD_writeFrameHeader failed"); + dstCapacity -= fhSize; + op += fhSize; + cctx->stage = ZSTDcs_ongoing; + } + + if (cctx->stage != ZSTDcs_ending) { + /* write one last empty block, make it the "last" block */ + U32 const cBlockHeader24 = 1 /* last block */ + (((U32)bt_raw)<<1) + 0; + ZSTD_STATIC_ASSERT(ZSTD_BLOCKHEADERSIZE == 3); + RETURN_ERROR_IF(dstCapacity<3, dstSize_tooSmall, "no room for epilogue"); + MEM_writeLE24(op, cBlockHeader24); + op += ZSTD_blockHeaderSize; + dstCapacity -= ZSTD_blockHeaderSize; + } + + if (cctx->appliedParams.fParams.checksumFlag) { + U32 const checksum = (U32) XXH64_digest(&cctx->xxhState); + RETURN_ERROR_IF(dstCapacity<4, dstSize_tooSmall, "no room for checksum"); + DEBUGLOG(4, "ZSTD_writeEpilogue: write checksum : %08X", (unsigned)checksum); + MEM_writeLE32(op, checksum); + op += 4; + } + + cctx->stage = ZSTDcs_created; /* return to "created but no init" status */ + return op-ostart; +} + +void ZSTD_CCtx_trace(ZSTD_CCtx* cctx, size_t extraCSize) +{ +#if ZSTD_TRACE + if (cctx->traceCtx && ZSTD_trace_compress_end != NULL) { + int const streaming = cctx->inBuffSize > 0 || cctx->outBuffSize > 0 || cctx->appliedParams.nbWorkers > 0; + ZSTD_Trace trace; + ZSTD_memset(&trace, 0, sizeof(trace)); + trace.version = ZSTD_VERSION_NUMBER; + trace.streaming = streaming; + trace.dictionaryID = cctx->dictID; + trace.dictionarySize = cctx->dictContentSize; + trace.uncompressedSize = cctx->consumedSrcSize; + trace.compressedSize = cctx->producedCSize + extraCSize; + trace.params = &cctx->appliedParams; + trace.cctx = cctx; + ZSTD_trace_compress_end(cctx->traceCtx, &trace); + } + cctx->traceCtx = 0; +#else + (void)cctx; + (void)extraCSize; +#endif +} + +size_t ZSTD_compressEnd_public(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize) +{ + size_t endResult; + size_t const cSize = ZSTD_compressContinue_internal(cctx, + dst, dstCapacity, src, srcSize, + 1 /* frame mode */, 1 /* last chunk */); + FORWARD_IF_ERROR(cSize, "ZSTD_compressContinue_internal failed"); + endResult = ZSTD_writeEpilogue(cctx, (char*)dst + cSize, dstCapacity-cSize); + FORWARD_IF_ERROR(endResult, "ZSTD_writeEpilogue failed"); + assert(!(cctx->appliedParams.fParams.contentSizeFlag && cctx->pledgedSrcSizePlusOne == 0)); + if (cctx->pledgedSrcSizePlusOne != 0) { /* control src size */ + ZSTD_STATIC_ASSERT(ZSTD_CONTENTSIZE_UNKNOWN == (unsigned long long)-1); + DEBUGLOG(4, "end of frame : controlling src size"); + RETURN_ERROR_IF( + cctx->pledgedSrcSizePlusOne != cctx->consumedSrcSize+1, + srcSize_wrong, + "error : pledgedSrcSize = %u, while realSrcSize = %u", + (unsigned)cctx->pledgedSrcSizePlusOne-1, + (unsigned)cctx->consumedSrcSize); + } + ZSTD_CCtx_trace(cctx, endResult); + return cSize + endResult; +} + +/* NOTE: Must just wrap ZSTD_compressEnd_public() */ +size_t ZSTD_compressEnd(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize) +{ + return ZSTD_compressEnd_public(cctx, dst, dstCapacity, src, srcSize); +} + +size_t ZSTD_compress_advanced (ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize, + ZSTD_parameters params) +{ + DEBUGLOG(4, "ZSTD_compress_advanced"); + FORWARD_IF_ERROR(ZSTD_checkCParams(params.cParams), ""); + ZSTD_CCtxParams_init_internal(&cctx->simpleApiParams, ¶ms, ZSTD_NO_CLEVEL); + return ZSTD_compress_advanced_internal(cctx, + dst, dstCapacity, + src, srcSize, + dict, dictSize, + &cctx->simpleApiParams); +} + +/* Internal */ +size_t ZSTD_compress_advanced_internal( + ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize, + const ZSTD_CCtx_params* params) +{ + DEBUGLOG(4, "ZSTD_compress_advanced_internal (srcSize:%u)", (unsigned)srcSize); + FORWARD_IF_ERROR( ZSTD_compressBegin_internal(cctx, + dict, dictSize, ZSTD_dct_auto, ZSTD_dtlm_fast, NULL, + params, srcSize, ZSTDb_not_buffered) , ""); + return ZSTD_compressEnd_public(cctx, dst, dstCapacity, src, srcSize); +} + +size_t ZSTD_compress_usingDict(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict, size_t dictSize, + int compressionLevel) +{ + { + ZSTD_parameters const params = ZSTD_getParams_internal(compressionLevel, srcSize, dict ? dictSize : 0, ZSTD_cpm_noAttachDict); + assert(params.fParams.contentSizeFlag == 1); + ZSTD_CCtxParams_init_internal(&cctx->simpleApiParams, ¶ms, (compressionLevel == 0) ? ZSTD_CLEVEL_DEFAULT: compressionLevel); + } + DEBUGLOG(4, "ZSTD_compress_usingDict (srcSize=%u)", (unsigned)srcSize); + return ZSTD_compress_advanced_internal(cctx, dst, dstCapacity, src, srcSize, dict, dictSize, &cctx->simpleApiParams); +} + +size_t ZSTD_compressCCtx(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + int compressionLevel) +{ + DEBUGLOG(4, "ZSTD_compressCCtx (srcSize=%u)", (unsigned)srcSize); + assert(cctx != NULL); + return ZSTD_compress_usingDict(cctx, dst, dstCapacity, src, srcSize, NULL, 0, compressionLevel); +} + +size_t ZSTD_compress(void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + int compressionLevel) +{ + size_t result; +#if ZSTD_COMPRESS_HEAPMODE + ZSTD_CCtx* cctx = ZSTD_createCCtx(); + RETURN_ERROR_IF(!cctx, memory_allocation, "ZSTD_createCCtx failed"); + result = ZSTD_compressCCtx(cctx, dst, dstCapacity, src, srcSize, compressionLevel); + ZSTD_freeCCtx(cctx); +#else + ZSTD_CCtx ctxBody; + ZSTD_initCCtx(&ctxBody, ZSTD_defaultCMem); + result = ZSTD_compressCCtx(&ctxBody, dst, dstCapacity, src, srcSize, compressionLevel); + ZSTD_freeCCtxContent(&ctxBody); /* can't free ctxBody itself, as it's on stack; free only heap content */ +#endif + return result; +} + + +/* ===== Dictionary API ===== */ + +/*! ZSTD_estimateCDictSize_advanced() : + * Estimate amount of memory that will be needed to create a dictionary with following arguments */ +size_t ZSTD_estimateCDictSize_advanced( + size_t dictSize, ZSTD_compressionParameters cParams, + ZSTD_dictLoadMethod_e dictLoadMethod) +{ + DEBUGLOG(5, "sizeof(ZSTD_CDict) : %u", (unsigned)sizeof(ZSTD_CDict)); + return ZSTD_cwksp_alloc_size(sizeof(ZSTD_CDict)) + + ZSTD_cwksp_alloc_size(HUF_WORKSPACE_SIZE) + /* enableDedicatedDictSearch == 1 ensures that CDict estimation will not be too small + * in case we are using DDS with row-hash. */ + + ZSTD_sizeof_matchState(&cParams, ZSTD_resolveRowMatchFinderMode(ZSTD_ps_auto, &cParams), + /* enableDedicatedDictSearch */ 1, /* forCCtx */ 0) + + (dictLoadMethod == ZSTD_dlm_byRef ? 0 + : ZSTD_cwksp_alloc_size(ZSTD_cwksp_align(dictSize, sizeof(void *)))); +} + +size_t ZSTD_estimateCDictSize(size_t dictSize, int compressionLevel) +{ + ZSTD_compressionParameters const cParams = ZSTD_getCParams_internal(compressionLevel, ZSTD_CONTENTSIZE_UNKNOWN, dictSize, ZSTD_cpm_createCDict); + return ZSTD_estimateCDictSize_advanced(dictSize, cParams, ZSTD_dlm_byCopy); +} + +size_t ZSTD_sizeof_CDict(const ZSTD_CDict* cdict) +{ + if (cdict==NULL) return 0; /* support sizeof on NULL */ + DEBUGLOG(5, "sizeof(*cdict) : %u", (unsigned)sizeof(*cdict)); + /* cdict may be in the workspace */ + return (cdict->workspace.workspace == cdict ? 0 : sizeof(*cdict)) + + ZSTD_cwksp_sizeof(&cdict->workspace); +} + +static size_t ZSTD_initCDict_internal( + ZSTD_CDict* cdict, + const void* dictBuffer, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_CCtx_params params) +{ + DEBUGLOG(3, "ZSTD_initCDict_internal (dictContentType:%u)", (unsigned)dictContentType); + assert(!ZSTD_checkCParams(params.cParams)); + cdict->matchState.cParams = params.cParams; + cdict->matchState.dedicatedDictSearch = params.enableDedicatedDictSearch; + if ((dictLoadMethod == ZSTD_dlm_byRef) || (!dictBuffer) || (!dictSize)) { + cdict->dictContent = dictBuffer; + } else { + void *internalBuffer = ZSTD_cwksp_reserve_object(&cdict->workspace, ZSTD_cwksp_align(dictSize, sizeof(void*))); + RETURN_ERROR_IF(!internalBuffer, memory_allocation, "NULL pointer!"); + cdict->dictContent = internalBuffer; + ZSTD_memcpy(internalBuffer, dictBuffer, dictSize); + } + cdict->dictContentSize = dictSize; + cdict->dictContentType = dictContentType; + + cdict->entropyWorkspace = (U32*)ZSTD_cwksp_reserve_object(&cdict->workspace, HUF_WORKSPACE_SIZE); + + + /* Reset the state to no dictionary */ + ZSTD_reset_compressedBlockState(&cdict->cBlockState); + FORWARD_IF_ERROR(ZSTD_reset_matchState( + &cdict->matchState, + &cdict->workspace, + ¶ms.cParams, + params.useRowMatchFinder, + ZSTDcrp_makeClean, + ZSTDirp_reset, + ZSTD_resetTarget_CDict), ""); + /* (Maybe) load the dictionary + * Skips loading the dictionary if it is < 8 bytes. + */ + { params.compressionLevel = ZSTD_CLEVEL_DEFAULT; + params.fParams.contentSizeFlag = 1; + { size_t const dictID = ZSTD_compress_insertDictionary( + &cdict->cBlockState, &cdict->matchState, NULL, &cdict->workspace, + ¶ms, cdict->dictContent, cdict->dictContentSize, + dictContentType, ZSTD_dtlm_full, ZSTD_tfp_forCDict, cdict->entropyWorkspace); + FORWARD_IF_ERROR(dictID, "ZSTD_compress_insertDictionary failed"); + assert(dictID <= (size_t)(U32)-1); + cdict->dictID = (U32)dictID; + } + } + + return 0; +} + +static ZSTD_CDict* ZSTD_createCDict_advanced_internal(size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_compressionParameters cParams, + ZSTD_paramSwitch_e useRowMatchFinder, + U32 enableDedicatedDictSearch, + ZSTD_customMem customMem) +{ + if ((!customMem.customAlloc) ^ (!customMem.customFree)) return NULL; + + { size_t const workspaceSize = + ZSTD_cwksp_alloc_size(sizeof(ZSTD_CDict)) + + ZSTD_cwksp_alloc_size(HUF_WORKSPACE_SIZE) + + ZSTD_sizeof_matchState(&cParams, useRowMatchFinder, enableDedicatedDictSearch, /* forCCtx */ 0) + + (dictLoadMethod == ZSTD_dlm_byRef ? 0 + : ZSTD_cwksp_alloc_size(ZSTD_cwksp_align(dictSize, sizeof(void*)))); + void* const workspace = ZSTD_customMalloc(workspaceSize, customMem); + ZSTD_cwksp ws; + ZSTD_CDict* cdict; + + if (!workspace) { + ZSTD_customFree(workspace, customMem); + return NULL; + } + + ZSTD_cwksp_init(&ws, workspace, workspaceSize, ZSTD_cwksp_dynamic_alloc); + + cdict = (ZSTD_CDict*)ZSTD_cwksp_reserve_object(&ws, sizeof(ZSTD_CDict)); + assert(cdict != NULL); + ZSTD_cwksp_move(&cdict->workspace, &ws); + cdict->customMem = customMem; + cdict->compressionLevel = ZSTD_NO_CLEVEL; /* signals advanced API usage */ + cdict->useRowMatchFinder = useRowMatchFinder; + return cdict; + } +} + +ZSTD_CDict* ZSTD_createCDict_advanced(const void* dictBuffer, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_compressionParameters cParams, + ZSTD_customMem customMem) +{ + ZSTD_CCtx_params cctxParams; + ZSTD_memset(&cctxParams, 0, sizeof(cctxParams)); + ZSTD_CCtxParams_init(&cctxParams, 0); + cctxParams.cParams = cParams; + cctxParams.customMem = customMem; + return ZSTD_createCDict_advanced2( + dictBuffer, dictSize, + dictLoadMethod, dictContentType, + &cctxParams, customMem); +} + +ZSTD_CDict* ZSTD_createCDict_advanced2( + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + const ZSTD_CCtx_params* originalCctxParams, + ZSTD_customMem customMem) +{ + ZSTD_CCtx_params cctxParams = *originalCctxParams; + ZSTD_compressionParameters cParams; + ZSTD_CDict* cdict; + + DEBUGLOG(3, "ZSTD_createCDict_advanced2, mode %u", (unsigned)dictContentType); + if (!customMem.customAlloc ^ !customMem.customFree) return NULL; + + if (cctxParams.enableDedicatedDictSearch) { + cParams = ZSTD_dedicatedDictSearch_getCParams( + cctxParams.compressionLevel, dictSize); + ZSTD_overrideCParams(&cParams, &cctxParams.cParams); + } else { + cParams = ZSTD_getCParamsFromCCtxParams( + &cctxParams, ZSTD_CONTENTSIZE_UNKNOWN, dictSize, ZSTD_cpm_createCDict); + } + + if (!ZSTD_dedicatedDictSearch_isSupported(&cParams)) { + /* Fall back to non-DDSS params */ + cctxParams.enableDedicatedDictSearch = 0; + cParams = ZSTD_getCParamsFromCCtxParams( + &cctxParams, ZSTD_CONTENTSIZE_UNKNOWN, dictSize, ZSTD_cpm_createCDict); + } + + DEBUGLOG(3, "ZSTD_createCDict_advanced2: DDS: %u", cctxParams.enableDedicatedDictSearch); + cctxParams.cParams = cParams; + cctxParams.useRowMatchFinder = ZSTD_resolveRowMatchFinderMode(cctxParams.useRowMatchFinder, &cParams); + + cdict = ZSTD_createCDict_advanced_internal(dictSize, + dictLoadMethod, cctxParams.cParams, + cctxParams.useRowMatchFinder, cctxParams.enableDedicatedDictSearch, + customMem); + + if (!cdict || ZSTD_isError( ZSTD_initCDict_internal(cdict, + dict, dictSize, + dictLoadMethod, dictContentType, + cctxParams) )) { + ZSTD_freeCDict(cdict); + return NULL; + } + + return cdict; +} + +ZSTD_CDict* ZSTD_createCDict(const void* dict, size_t dictSize, int compressionLevel) +{ + ZSTD_compressionParameters cParams = ZSTD_getCParams_internal(compressionLevel, ZSTD_CONTENTSIZE_UNKNOWN, dictSize, ZSTD_cpm_createCDict); + ZSTD_CDict* const cdict = ZSTD_createCDict_advanced(dict, dictSize, + ZSTD_dlm_byCopy, ZSTD_dct_auto, + cParams, ZSTD_defaultCMem); + if (cdict) + cdict->compressionLevel = (compressionLevel == 0) ? ZSTD_CLEVEL_DEFAULT : compressionLevel; + return cdict; +} + +ZSTD_CDict* ZSTD_createCDict_byReference(const void* dict, size_t dictSize, int compressionLevel) +{ + ZSTD_compressionParameters cParams = ZSTD_getCParams_internal(compressionLevel, ZSTD_CONTENTSIZE_UNKNOWN, dictSize, ZSTD_cpm_createCDict); + ZSTD_CDict* const cdict = ZSTD_createCDict_advanced(dict, dictSize, + ZSTD_dlm_byRef, ZSTD_dct_auto, + cParams, ZSTD_defaultCMem); + if (cdict) + cdict->compressionLevel = (compressionLevel == 0) ? ZSTD_CLEVEL_DEFAULT : compressionLevel; + return cdict; +} + +size_t ZSTD_freeCDict(ZSTD_CDict* cdict) +{ + if (cdict==NULL) return 0; /* support free on NULL */ + { ZSTD_customMem const cMem = cdict->customMem; + int cdictInWorkspace = ZSTD_cwksp_owns_buffer(&cdict->workspace, cdict); + ZSTD_cwksp_free(&cdict->workspace, cMem); + if (!cdictInWorkspace) { + ZSTD_customFree(cdict, cMem); + } + return 0; + } +} + +/*! ZSTD_initStaticCDict_advanced() : + * Generate a digested dictionary in provided memory area. + * workspace: The memory area to emplace the dictionary into. + * Provided pointer must 8-bytes aligned. + * It must outlive dictionary usage. + * workspaceSize: Use ZSTD_estimateCDictSize() + * to determine how large workspace must be. + * cParams : use ZSTD_getCParams() to transform a compression level + * into its relevants cParams. + * @return : pointer to ZSTD_CDict*, or NULL if error (size too small) + * Note : there is no corresponding "free" function. + * Since workspace was allocated externally, it must be freed externally. + */ +const ZSTD_CDict* ZSTD_initStaticCDict( + void* workspace, size_t workspaceSize, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_compressionParameters cParams) +{ + ZSTD_paramSwitch_e const useRowMatchFinder = ZSTD_resolveRowMatchFinderMode(ZSTD_ps_auto, &cParams); + /* enableDedicatedDictSearch == 1 ensures matchstate is not too small in case this CDict will be used for DDS + row hash */ + size_t const matchStateSize = ZSTD_sizeof_matchState(&cParams, useRowMatchFinder, /* enableDedicatedDictSearch */ 1, /* forCCtx */ 0); + size_t const neededSize = ZSTD_cwksp_alloc_size(sizeof(ZSTD_CDict)) + + (dictLoadMethod == ZSTD_dlm_byRef ? 0 + : ZSTD_cwksp_alloc_size(ZSTD_cwksp_align(dictSize, sizeof(void*)))) + + ZSTD_cwksp_alloc_size(HUF_WORKSPACE_SIZE) + + matchStateSize; + ZSTD_CDict* cdict; + ZSTD_CCtx_params params; + + if ((size_t)workspace & 7) return NULL; /* 8-aligned */ + + { + ZSTD_cwksp ws; + ZSTD_cwksp_init(&ws, workspace, workspaceSize, ZSTD_cwksp_static_alloc); + cdict = (ZSTD_CDict*)ZSTD_cwksp_reserve_object(&ws, sizeof(ZSTD_CDict)); + if (cdict == NULL) return NULL; + ZSTD_cwksp_move(&cdict->workspace, &ws); + } + + DEBUGLOG(4, "(workspaceSize < neededSize) : (%u < %u) => %u", + (unsigned)workspaceSize, (unsigned)neededSize, (unsigned)(workspaceSize < neededSize)); + if (workspaceSize < neededSize) return NULL; + + ZSTD_CCtxParams_init(¶ms, 0); + params.cParams = cParams; + params.useRowMatchFinder = useRowMatchFinder; + cdict->useRowMatchFinder = useRowMatchFinder; + cdict->compressionLevel = ZSTD_NO_CLEVEL; + + if (ZSTD_isError( ZSTD_initCDict_internal(cdict, + dict, dictSize, + dictLoadMethod, dictContentType, + params) )) + return NULL; + + return cdict; +} + +ZSTD_compressionParameters ZSTD_getCParamsFromCDict(const ZSTD_CDict* cdict) +{ + assert(cdict != NULL); + return cdict->matchState.cParams; +} + +/*! ZSTD_getDictID_fromCDict() : + * Provides the dictID of the dictionary loaded into `cdict`. + * If @return == 0, the dictionary is not conformant to Zstandard specification, or empty. + * Non-conformant dictionaries can still be loaded, but as content-only dictionaries. */ +unsigned ZSTD_getDictID_fromCDict(const ZSTD_CDict* cdict) +{ + if (cdict==NULL) return 0; + return cdict->dictID; +} + +/* ZSTD_compressBegin_usingCDict_internal() : + * Implementation of various ZSTD_compressBegin_usingCDict* functions. + */ +static size_t ZSTD_compressBegin_usingCDict_internal( + ZSTD_CCtx* const cctx, const ZSTD_CDict* const cdict, + ZSTD_frameParameters const fParams, unsigned long long const pledgedSrcSize) +{ + ZSTD_CCtx_params cctxParams; + DEBUGLOG(4, "ZSTD_compressBegin_usingCDict_internal"); + RETURN_ERROR_IF(cdict==NULL, dictionary_wrong, "NULL pointer!"); + /* Initialize the cctxParams from the cdict */ + { + ZSTD_parameters params; + params.fParams = fParams; + params.cParams = ( pledgedSrcSize < ZSTD_USE_CDICT_PARAMS_SRCSIZE_CUTOFF + || pledgedSrcSize < cdict->dictContentSize * ZSTD_USE_CDICT_PARAMS_DICTSIZE_MULTIPLIER + || pledgedSrcSize == ZSTD_CONTENTSIZE_UNKNOWN + || cdict->compressionLevel == 0 ) ? + ZSTD_getCParamsFromCDict(cdict) + : ZSTD_getCParams(cdict->compressionLevel, + pledgedSrcSize, + cdict->dictContentSize); + ZSTD_CCtxParams_init_internal(&cctxParams, ¶ms, cdict->compressionLevel); + } + /* Increase window log to fit the entire dictionary and source if the + * source size is known. Limit the increase to 19, which is the + * window log for compression level 1 with the largest source size. + */ + if (pledgedSrcSize != ZSTD_CONTENTSIZE_UNKNOWN) { + U32 const limitedSrcSize = (U32)MIN(pledgedSrcSize, 1U << 19); + U32 const limitedSrcLog = limitedSrcSize > 1 ? ZSTD_highbit32(limitedSrcSize - 1) + 1 : 1; + cctxParams.cParams.windowLog = MAX(cctxParams.cParams.windowLog, limitedSrcLog); + } + return ZSTD_compressBegin_internal(cctx, + NULL, 0, ZSTD_dct_auto, ZSTD_dtlm_fast, + cdict, + &cctxParams, pledgedSrcSize, + ZSTDb_not_buffered); +} + + +/* ZSTD_compressBegin_usingCDict_advanced() : + * This function is DEPRECATED. + * cdict must be != NULL */ +size_t ZSTD_compressBegin_usingCDict_advanced( + ZSTD_CCtx* const cctx, const ZSTD_CDict* const cdict, + ZSTD_frameParameters const fParams, unsigned long long const pledgedSrcSize) +{ + return ZSTD_compressBegin_usingCDict_internal(cctx, cdict, fParams, pledgedSrcSize); +} + +/* ZSTD_compressBegin_usingCDict() : + * cdict must be != NULL */ +size_t ZSTD_compressBegin_usingCDict_deprecated(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict) +{ + ZSTD_frameParameters const fParams = { 0 /*content*/, 0 /*checksum*/, 0 /*noDictID*/ }; + return ZSTD_compressBegin_usingCDict_internal(cctx, cdict, fParams, ZSTD_CONTENTSIZE_UNKNOWN); +} + +size_t ZSTD_compressBegin_usingCDict(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict) +{ + return ZSTD_compressBegin_usingCDict_deprecated(cctx, cdict); +} + +/*! ZSTD_compress_usingCDict_internal(): + * Implementation of various ZSTD_compress_usingCDict* functions. + */ +static size_t ZSTD_compress_usingCDict_internal(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_CDict* cdict, ZSTD_frameParameters fParams) +{ + FORWARD_IF_ERROR(ZSTD_compressBegin_usingCDict_internal(cctx, cdict, fParams, srcSize), ""); /* will check if cdict != NULL */ + return ZSTD_compressEnd_public(cctx, dst, dstCapacity, src, srcSize); +} + +/*! ZSTD_compress_usingCDict_advanced(): + * This function is DEPRECATED. + */ +size_t ZSTD_compress_usingCDict_advanced(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_CDict* cdict, ZSTD_frameParameters fParams) +{ + return ZSTD_compress_usingCDict_internal(cctx, dst, dstCapacity, src, srcSize, cdict, fParams); +} + +/*! ZSTD_compress_usingCDict() : + * Compression using a digested Dictionary. + * Faster startup than ZSTD_compress_usingDict(), recommended when same dictionary is used multiple times. + * Note that compression parameters are decided at CDict creation time + * while frame parameters are hardcoded */ +size_t ZSTD_compress_usingCDict(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_CDict* cdict) +{ + ZSTD_frameParameters const fParams = { 1 /*content*/, 0 /*checksum*/, 0 /*noDictID*/ }; + return ZSTD_compress_usingCDict_internal(cctx, dst, dstCapacity, src, srcSize, cdict, fParams); +} + + + +/* ****************************************************************** +* Streaming +********************************************************************/ + +ZSTD_CStream* ZSTD_createCStream(void) +{ + DEBUGLOG(3, "ZSTD_createCStream"); + return ZSTD_createCStream_advanced(ZSTD_defaultCMem); +} + +ZSTD_CStream* ZSTD_initStaticCStream(void *workspace, size_t workspaceSize) +{ + return ZSTD_initStaticCCtx(workspace, workspaceSize); +} + +ZSTD_CStream* ZSTD_createCStream_advanced(ZSTD_customMem customMem) +{ /* CStream and CCtx are now same object */ + return ZSTD_createCCtx_advanced(customMem); +} + +size_t ZSTD_freeCStream(ZSTD_CStream* zcs) +{ + return ZSTD_freeCCtx(zcs); /* same object */ +} + + + +/*====== Initialization ======*/ + +size_t ZSTD_CStreamInSize(void) { return ZSTD_BLOCKSIZE_MAX; } + +size_t ZSTD_CStreamOutSize(void) +{ + return ZSTD_compressBound(ZSTD_BLOCKSIZE_MAX) + ZSTD_blockHeaderSize + 4 /* 32-bits hash */ ; +} + +static ZSTD_cParamMode_e ZSTD_getCParamMode(ZSTD_CDict const* cdict, ZSTD_CCtx_params const* params, U64 pledgedSrcSize) +{ + if (cdict != NULL && ZSTD_shouldAttachDict(cdict, params, pledgedSrcSize)) + return ZSTD_cpm_attachDict; + else + return ZSTD_cpm_noAttachDict; +} + +/* ZSTD_resetCStream(): + * pledgedSrcSize == 0 means "unknown" */ +size_t ZSTD_resetCStream(ZSTD_CStream* zcs, unsigned long long pss) +{ + /* temporary : 0 interpreted as "unknown" during transition period. + * Users willing to specify "unknown" **must** use ZSTD_CONTENTSIZE_UNKNOWN. + * 0 will be interpreted as "empty" in the future. + */ + U64 const pledgedSrcSize = (pss==0) ? ZSTD_CONTENTSIZE_UNKNOWN : pss; + DEBUGLOG(4, "ZSTD_resetCStream: pledgedSrcSize = %u", (unsigned)pledgedSrcSize); + FORWARD_IF_ERROR( ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize) , ""); + return 0; +} + +/*! ZSTD_initCStream_internal() : + * Note : for lib/compress only. Used by zstdmt_compress.c. + * Assumption 1 : params are valid + * Assumption 2 : either dict, or cdict, is defined, not both */ +size_t ZSTD_initCStream_internal(ZSTD_CStream* zcs, + const void* dict, size_t dictSize, const ZSTD_CDict* cdict, + const ZSTD_CCtx_params* params, + unsigned long long pledgedSrcSize) +{ + DEBUGLOG(4, "ZSTD_initCStream_internal"); + FORWARD_IF_ERROR( ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize) , ""); + assert(!ZSTD_isError(ZSTD_checkCParams(params->cParams))); + zcs->requestedParams = *params; + assert(!((dict) && (cdict))); /* either dict or cdict, not both */ + if (dict) { + FORWARD_IF_ERROR( ZSTD_CCtx_loadDictionary(zcs, dict, dictSize) , ""); + } else { + /* Dictionary is cleared if !cdict */ + FORWARD_IF_ERROR( ZSTD_CCtx_refCDict(zcs, cdict) , ""); + } + return 0; +} + +/* ZSTD_initCStream_usingCDict_advanced() : + * same as ZSTD_initCStream_usingCDict(), with control over frame parameters */ +size_t ZSTD_initCStream_usingCDict_advanced(ZSTD_CStream* zcs, + const ZSTD_CDict* cdict, + ZSTD_frameParameters fParams, + unsigned long long pledgedSrcSize) +{ + DEBUGLOG(4, "ZSTD_initCStream_usingCDict_advanced"); + FORWARD_IF_ERROR( ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize) , ""); + zcs->requestedParams.fParams = fParams; + FORWARD_IF_ERROR( ZSTD_CCtx_refCDict(zcs, cdict) , ""); + return 0; +} + +/* note : cdict must outlive compression session */ +size_t ZSTD_initCStream_usingCDict(ZSTD_CStream* zcs, const ZSTD_CDict* cdict) +{ + DEBUGLOG(4, "ZSTD_initCStream_usingCDict"); + FORWARD_IF_ERROR( ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_refCDict(zcs, cdict) , ""); + return 0; +} + + +/* ZSTD_initCStream_advanced() : + * pledgedSrcSize must be exact. + * if srcSize is not known at init time, use value ZSTD_CONTENTSIZE_UNKNOWN. + * dict is loaded with default parameters ZSTD_dct_auto and ZSTD_dlm_byCopy. */ +size_t ZSTD_initCStream_advanced(ZSTD_CStream* zcs, + const void* dict, size_t dictSize, + ZSTD_parameters params, unsigned long long pss) +{ + /* for compatibility with older programs relying on this behavior. + * Users should now specify ZSTD_CONTENTSIZE_UNKNOWN. + * This line will be removed in the future. + */ + U64 const pledgedSrcSize = (pss==0 && params.fParams.contentSizeFlag==0) ? ZSTD_CONTENTSIZE_UNKNOWN : pss; + DEBUGLOG(4, "ZSTD_initCStream_advanced"); + FORWARD_IF_ERROR( ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize) , ""); + FORWARD_IF_ERROR( ZSTD_checkCParams(params.cParams) , ""); + ZSTD_CCtxParams_setZstdParams(&zcs->requestedParams, ¶ms); + FORWARD_IF_ERROR( ZSTD_CCtx_loadDictionary(zcs, dict, dictSize) , ""); + return 0; +} + +size_t ZSTD_initCStream_usingDict(ZSTD_CStream* zcs, const void* dict, size_t dictSize, int compressionLevel) +{ + DEBUGLOG(4, "ZSTD_initCStream_usingDict"); + FORWARD_IF_ERROR( ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_loadDictionary(zcs, dict, dictSize) , ""); + return 0; +} + +size_t ZSTD_initCStream_srcSize(ZSTD_CStream* zcs, int compressionLevel, unsigned long long pss) +{ + /* temporary : 0 interpreted as "unknown" during transition period. + * Users willing to specify "unknown" **must** use ZSTD_CONTENTSIZE_UNKNOWN. + * 0 will be interpreted as "empty" in the future. + */ + U64 const pledgedSrcSize = (pss==0) ? ZSTD_CONTENTSIZE_UNKNOWN : pss; + DEBUGLOG(4, "ZSTD_initCStream_srcSize"); + FORWARD_IF_ERROR( ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_refCDict(zcs, NULL) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize) , ""); + return 0; +} + +size_t ZSTD_initCStream(ZSTD_CStream* zcs, int compressionLevel) +{ + DEBUGLOG(4, "ZSTD_initCStream"); + FORWARD_IF_ERROR( ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_refCDict(zcs, NULL) , ""); + FORWARD_IF_ERROR( ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel) , ""); + return 0; +} + +/*====== Compression ======*/ + +static size_t ZSTD_nextInputSizeHint(const ZSTD_CCtx* cctx) +{ + if (cctx->appliedParams.inBufferMode == ZSTD_bm_stable) { + return cctx->blockSize - cctx->stableIn_notConsumed; + } + assert(cctx->appliedParams.inBufferMode == ZSTD_bm_buffered); + { size_t hintInSize = cctx->inBuffTarget - cctx->inBuffPos; + if (hintInSize==0) hintInSize = cctx->blockSize; + return hintInSize; + } +} + +/** ZSTD_compressStream_generic(): + * internal function for all *compressStream*() variants + * @return : hint size for next input to complete ongoing block */ +static size_t ZSTD_compressStream_generic(ZSTD_CStream* zcs, + ZSTD_outBuffer* output, + ZSTD_inBuffer* input, + ZSTD_EndDirective const flushMode) +{ + const char* const istart = (assert(input != NULL), (const char*)input->src); + const char* const iend = (istart != NULL) ? istart + input->size : istart; + const char* ip = (istart != NULL) ? istart + input->pos : istart; + char* const ostart = (assert(output != NULL), (char*)output->dst); + char* const oend = (ostart != NULL) ? ostart + output->size : ostart; + char* op = (ostart != NULL) ? ostart + output->pos : ostart; + U32 someMoreWork = 1; + + /* check expectations */ + DEBUGLOG(5, "ZSTD_compressStream_generic, flush=%i, srcSize = %zu", (int)flushMode, input->size - input->pos); + assert(zcs != NULL); + if (zcs->appliedParams.inBufferMode == ZSTD_bm_stable) { + assert(input->pos >= zcs->stableIn_notConsumed); + input->pos -= zcs->stableIn_notConsumed; + if (ip) ip -= zcs->stableIn_notConsumed; + zcs->stableIn_notConsumed = 0; + } + if (zcs->appliedParams.inBufferMode == ZSTD_bm_buffered) { + assert(zcs->inBuff != NULL); + assert(zcs->inBuffSize > 0); + } + if (zcs->appliedParams.outBufferMode == ZSTD_bm_buffered) { + assert(zcs->outBuff != NULL); + assert(zcs->outBuffSize > 0); + } + if (input->src == NULL) assert(input->size == 0); + assert(input->pos <= input->size); + if (output->dst == NULL) assert(output->size == 0); + assert(output->pos <= output->size); + assert((U32)flushMode <= (U32)ZSTD_e_end); + + while (someMoreWork) { + switch(zcs->streamStage) + { + case zcss_init: + RETURN_ERROR(init_missing, "call ZSTD_initCStream() first!"); + + case zcss_load: + if ( (flushMode == ZSTD_e_end) + && ( (size_t)(oend-op) >= ZSTD_compressBound(iend-ip) /* Enough output space */ + || zcs->appliedParams.outBufferMode == ZSTD_bm_stable) /* OR we are allowed to return dstSizeTooSmall */ + && (zcs->inBuffPos == 0) ) { + /* shortcut to compression pass directly into output buffer */ + size_t const cSize = ZSTD_compressEnd_public(zcs, + op, oend-op, ip, iend-ip); + DEBUGLOG(4, "ZSTD_compressEnd : cSize=%u", (unsigned)cSize); + FORWARD_IF_ERROR(cSize, "ZSTD_compressEnd failed"); + ip = iend; + op += cSize; + zcs->frameEnded = 1; + ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + someMoreWork = 0; break; + } + /* complete loading into inBuffer in buffered mode */ + if (zcs->appliedParams.inBufferMode == ZSTD_bm_buffered) { + size_t const toLoad = zcs->inBuffTarget - zcs->inBuffPos; + size_t const loaded = ZSTD_limitCopy( + zcs->inBuff + zcs->inBuffPos, toLoad, + ip, iend-ip); + zcs->inBuffPos += loaded; + if (ip) ip += loaded; + if ( (flushMode == ZSTD_e_continue) + && (zcs->inBuffPos < zcs->inBuffTarget) ) { + /* not enough input to fill full block : stop here */ + someMoreWork = 0; break; + } + if ( (flushMode == ZSTD_e_flush) + && (zcs->inBuffPos == zcs->inToCompress) ) { + /* empty */ + someMoreWork = 0; break; + } + } else { + assert(zcs->appliedParams.inBufferMode == ZSTD_bm_stable); + if ( (flushMode == ZSTD_e_continue) + && ( (size_t)(iend - ip) < zcs->blockSize) ) { + /* can't compress a full block : stop here */ + zcs->stableIn_notConsumed = (size_t)(iend - ip); + ip = iend; /* pretend to have consumed input */ + someMoreWork = 0; break; + } + if ( (flushMode == ZSTD_e_flush) + && (ip == iend) ) { + /* empty */ + someMoreWork = 0; break; + } + } + /* compress current block (note : this stage cannot be stopped in the middle) */ + DEBUGLOG(5, "stream compression stage (flushMode==%u)", flushMode); + { int const inputBuffered = (zcs->appliedParams.inBufferMode == ZSTD_bm_buffered); + void* cDst; + size_t cSize; + size_t oSize = oend-op; + size_t const iSize = inputBuffered ? zcs->inBuffPos - zcs->inToCompress + : MIN((size_t)(iend - ip), zcs->blockSize); + if (oSize >= ZSTD_compressBound(iSize) || zcs->appliedParams.outBufferMode == ZSTD_bm_stable) + cDst = op; /* compress into output buffer, to skip flush stage */ + else + cDst = zcs->outBuff, oSize = zcs->outBuffSize; + if (inputBuffered) { + unsigned const lastBlock = (flushMode == ZSTD_e_end) && (ip==iend); + cSize = lastBlock ? + ZSTD_compressEnd_public(zcs, cDst, oSize, + zcs->inBuff + zcs->inToCompress, iSize) : + ZSTD_compressContinue_public(zcs, cDst, oSize, + zcs->inBuff + zcs->inToCompress, iSize); + FORWARD_IF_ERROR(cSize, "%s", lastBlock ? "ZSTD_compressEnd failed" : "ZSTD_compressContinue failed"); + zcs->frameEnded = lastBlock; + /* prepare next block */ + zcs->inBuffTarget = zcs->inBuffPos + zcs->blockSize; + if (zcs->inBuffTarget > zcs->inBuffSize) + zcs->inBuffPos = 0, zcs->inBuffTarget = zcs->blockSize; + DEBUGLOG(5, "inBuffTarget:%u / inBuffSize:%u", + (unsigned)zcs->inBuffTarget, (unsigned)zcs->inBuffSize); + if (!lastBlock) + assert(zcs->inBuffTarget <= zcs->inBuffSize); + zcs->inToCompress = zcs->inBuffPos; + } else { /* !inputBuffered, hence ZSTD_bm_stable */ + unsigned const lastBlock = (flushMode == ZSTD_e_end) && (ip + iSize == iend); + cSize = lastBlock ? + ZSTD_compressEnd_public(zcs, cDst, oSize, ip, iSize) : + ZSTD_compressContinue_public(zcs, cDst, oSize, ip, iSize); + /* Consume the input prior to error checking to mirror buffered mode. */ + if (ip) ip += iSize; + FORWARD_IF_ERROR(cSize, "%s", lastBlock ? "ZSTD_compressEnd failed" : "ZSTD_compressContinue failed"); + zcs->frameEnded = lastBlock; + if (lastBlock) assert(ip == iend); + } + if (cDst == op) { /* no need to flush */ + op += cSize; + if (zcs->frameEnded) { + DEBUGLOG(5, "Frame completed directly in outBuffer"); + someMoreWork = 0; + ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + } + break; + } + zcs->outBuffContentSize = cSize; + zcs->outBuffFlushedSize = 0; + zcs->streamStage = zcss_flush; /* pass-through to flush stage */ + } + ZSTD_FALLTHROUGH; + case zcss_flush: + DEBUGLOG(5, "flush stage"); + assert(zcs->appliedParams.outBufferMode == ZSTD_bm_buffered); + { size_t const toFlush = zcs->outBuffContentSize - zcs->outBuffFlushedSize; + size_t const flushed = ZSTD_limitCopy(op, (size_t)(oend-op), + zcs->outBuff + zcs->outBuffFlushedSize, toFlush); + DEBUGLOG(5, "toFlush: %u into %u ==> flushed: %u", + (unsigned)toFlush, (unsigned)(oend-op), (unsigned)flushed); + if (flushed) + op += flushed; + zcs->outBuffFlushedSize += flushed; + if (toFlush!=flushed) { + /* flush not fully completed, presumably because dst is too small */ + assert(op==oend); + someMoreWork = 0; + break; + } + zcs->outBuffContentSize = zcs->outBuffFlushedSize = 0; + if (zcs->frameEnded) { + DEBUGLOG(5, "Frame completed on flush"); + someMoreWork = 0; + ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + break; + } + zcs->streamStage = zcss_load; + break; + } + + default: /* impossible */ + assert(0); + } + } + + input->pos = ip - istart; + output->pos = op - ostart; + if (zcs->frameEnded) return 0; + return ZSTD_nextInputSizeHint(zcs); +} + +static size_t ZSTD_nextInputSizeHint_MTorST(const ZSTD_CCtx* cctx) +{ +#ifdef ZSTD_MULTITHREAD + if (cctx->appliedParams.nbWorkers >= 1) { + assert(cctx->mtctx != NULL); + return ZSTDMT_nextInputSizeHint(cctx->mtctx); + } +#endif + return ZSTD_nextInputSizeHint(cctx); + +} + +size_t ZSTD_compressStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output, ZSTD_inBuffer* input) +{ + FORWARD_IF_ERROR( ZSTD_compressStream2(zcs, output, input, ZSTD_e_continue) , ""); + return ZSTD_nextInputSizeHint_MTorST(zcs); +} + +/* After a compression call set the expected input/output buffer. + * This is validated at the start of the next compression call. + */ +static void +ZSTD_setBufferExpectations(ZSTD_CCtx* cctx, const ZSTD_outBuffer* output, const ZSTD_inBuffer* input) +{ + DEBUGLOG(5, "ZSTD_setBufferExpectations (for advanced stable in/out modes)"); + if (cctx->appliedParams.inBufferMode == ZSTD_bm_stable) { + cctx->expectedInBuffer = *input; + } + if (cctx->appliedParams.outBufferMode == ZSTD_bm_stable) { + cctx->expectedOutBufferSize = output->size - output->pos; + } +} + +/* Validate that the input/output buffers match the expectations set by + * ZSTD_setBufferExpectations. + */ +static size_t ZSTD_checkBufferStability(ZSTD_CCtx const* cctx, + ZSTD_outBuffer const* output, + ZSTD_inBuffer const* input, + ZSTD_EndDirective endOp) +{ + if (cctx->appliedParams.inBufferMode == ZSTD_bm_stable) { + ZSTD_inBuffer const expect = cctx->expectedInBuffer; + if (expect.src != input->src || expect.pos != input->pos) + RETURN_ERROR(stabilityCondition_notRespected, "ZSTD_c_stableInBuffer enabled but input differs!"); + } + (void)endOp; + if (cctx->appliedParams.outBufferMode == ZSTD_bm_stable) { + size_t const outBufferSize = output->size - output->pos; + if (cctx->expectedOutBufferSize != outBufferSize) + RETURN_ERROR(stabilityCondition_notRespected, "ZSTD_c_stableOutBuffer enabled but output size differs!"); + } + return 0; +} + +static size_t ZSTD_CCtx_init_compressStream2(ZSTD_CCtx* cctx, + ZSTD_EndDirective endOp, + size_t inSize) +{ + ZSTD_CCtx_params params = cctx->requestedParams; + ZSTD_prefixDict const prefixDict = cctx->prefixDict; + FORWARD_IF_ERROR( ZSTD_initLocalDict(cctx) , ""); /* Init the local dict if present. */ + ZSTD_memset(&cctx->prefixDict, 0, sizeof(cctx->prefixDict)); /* single usage */ + assert(prefixDict.dict==NULL || cctx->cdict==NULL); /* only one can be set */ + if (cctx->cdict && !cctx->localDict.cdict) { + /* Let the cdict's compression level take priority over the requested params. + * But do not take the cdict's compression level if the "cdict" is actually a localDict + * generated from ZSTD_initLocalDict(). + */ + params.compressionLevel = cctx->cdict->compressionLevel; + } + DEBUGLOG(4, "ZSTD_compressStream2 : transparent init stage"); + if (endOp == ZSTD_e_end) cctx->pledgedSrcSizePlusOne = inSize + 1; /* auto-determine pledgedSrcSize */ + + { size_t const dictSize = prefixDict.dict + ? prefixDict.dictSize + : (cctx->cdict ? cctx->cdict->dictContentSize : 0); + ZSTD_cParamMode_e const mode = ZSTD_getCParamMode(cctx->cdict, ¶ms, cctx->pledgedSrcSizePlusOne - 1); + params.cParams = ZSTD_getCParamsFromCCtxParams( + ¶ms, cctx->pledgedSrcSizePlusOne-1, + dictSize, mode); + } + + params.useBlockSplitter = ZSTD_resolveBlockSplitterMode(params.useBlockSplitter, ¶ms.cParams); + params.ldmParams.enableLdm = ZSTD_resolveEnableLdm(params.ldmParams.enableLdm, ¶ms.cParams); + params.useRowMatchFinder = ZSTD_resolveRowMatchFinderMode(params.useRowMatchFinder, ¶ms.cParams); + params.validateSequences = ZSTD_resolveExternalSequenceValidation(params.validateSequences); + params.maxBlockSize = ZSTD_resolveMaxBlockSize(params.maxBlockSize); + params.searchForExternalRepcodes = ZSTD_resolveExternalRepcodeSearch(params.searchForExternalRepcodes, params.compressionLevel); + +#ifdef ZSTD_MULTITHREAD + /* If external matchfinder is enabled, make sure to fail before checking job size (for consistency) */ + RETURN_ERROR_IF( + ZSTD_hasExtSeqProd(¶ms) && params.nbWorkers >= 1, + parameter_combination_unsupported, + "External sequence producer isn't supported with nbWorkers >= 1" + ); + + if ((cctx->pledgedSrcSizePlusOne-1) <= ZSTDMT_JOBSIZE_MIN) { + params.nbWorkers = 0; /* do not invoke multi-threading when src size is too small */ + } + if (params.nbWorkers > 0) { +#if ZSTD_TRACE + cctx->traceCtx = (ZSTD_trace_compress_begin != NULL) ? ZSTD_trace_compress_begin(cctx) : 0; +#endif + /* mt context creation */ + if (cctx->mtctx == NULL) { + DEBUGLOG(4, "ZSTD_compressStream2: creating new mtctx for nbWorkers=%u", + params.nbWorkers); + cctx->mtctx = ZSTDMT_createCCtx_advanced((U32)params.nbWorkers, cctx->customMem, cctx->pool); + RETURN_ERROR_IF(cctx->mtctx == NULL, memory_allocation, "NULL pointer!"); + } + /* mt compression */ + DEBUGLOG(4, "call ZSTDMT_initCStream_internal as nbWorkers=%u", params.nbWorkers); + FORWARD_IF_ERROR( ZSTDMT_initCStream_internal( + cctx->mtctx, + prefixDict.dict, prefixDict.dictSize, prefixDict.dictContentType, + cctx->cdict, params, cctx->pledgedSrcSizePlusOne-1) , ""); + cctx->dictID = cctx->cdict ? cctx->cdict->dictID : 0; + cctx->dictContentSize = cctx->cdict ? cctx->cdict->dictContentSize : prefixDict.dictSize; + cctx->consumedSrcSize = 0; + cctx->producedCSize = 0; + cctx->streamStage = zcss_load; + cctx->appliedParams = params; + } else +#endif /* ZSTD_MULTITHREAD */ + { U64 const pledgedSrcSize = cctx->pledgedSrcSizePlusOne - 1; + assert(!ZSTD_isError(ZSTD_checkCParams(params.cParams))); + FORWARD_IF_ERROR( ZSTD_compressBegin_internal(cctx, + prefixDict.dict, prefixDict.dictSize, prefixDict.dictContentType, ZSTD_dtlm_fast, + cctx->cdict, + ¶ms, pledgedSrcSize, + ZSTDb_buffered) , ""); + assert(cctx->appliedParams.nbWorkers == 0); + cctx->inToCompress = 0; + cctx->inBuffPos = 0; + if (cctx->appliedParams.inBufferMode == ZSTD_bm_buffered) { + /* for small input: avoid automatic flush on reaching end of block, since + * it would require to add a 3-bytes null block to end frame + */ + cctx->inBuffTarget = cctx->blockSize + (cctx->blockSize == pledgedSrcSize); + } else { + cctx->inBuffTarget = 0; + } + cctx->outBuffContentSize = cctx->outBuffFlushedSize = 0; + cctx->streamStage = zcss_load; + cctx->frameEnded = 0; + } + return 0; +} + +/* @return provides a minimum amount of data remaining to be flushed from internal buffers + */ +size_t ZSTD_compressStream2( ZSTD_CCtx* cctx, + ZSTD_outBuffer* output, + ZSTD_inBuffer* input, + ZSTD_EndDirective endOp) +{ + DEBUGLOG(5, "ZSTD_compressStream2, endOp=%u ", (unsigned)endOp); + /* check conditions */ + RETURN_ERROR_IF(output->pos > output->size, dstSize_tooSmall, "invalid output buffer"); + RETURN_ERROR_IF(input->pos > input->size, srcSize_wrong, "invalid input buffer"); + RETURN_ERROR_IF((U32)endOp > (U32)ZSTD_e_end, parameter_outOfBound, "invalid endDirective"); + assert(cctx != NULL); + + /* transparent initialization stage */ + if (cctx->streamStage == zcss_init) { + size_t const inputSize = input->size - input->pos; /* no obligation to start from pos==0 */ + size_t const totalInputSize = inputSize + cctx->stableIn_notConsumed; + if ( (cctx->requestedParams.inBufferMode == ZSTD_bm_stable) /* input is presumed stable, across invocations */ + && (endOp == ZSTD_e_continue) /* no flush requested, more input to come */ + && (totalInputSize < ZSTD_BLOCKSIZE_MAX) ) { /* not even reached one block yet */ + if (cctx->stableIn_notConsumed) { /* not the first time */ + /* check stable source guarantees */ + RETURN_ERROR_IF(input->src != cctx->expectedInBuffer.src, stabilityCondition_notRespected, "stableInBuffer condition not respected: wrong src pointer"); + RETURN_ERROR_IF(input->pos != cctx->expectedInBuffer.size, stabilityCondition_notRespected, "stableInBuffer condition not respected: externally modified pos"); + } + /* pretend input was consumed, to give a sense forward progress */ + input->pos = input->size; + /* save stable inBuffer, for later control, and flush/end */ + cctx->expectedInBuffer = *input; + /* but actually input wasn't consumed, so keep track of position from where compression shall resume */ + cctx->stableIn_notConsumed += inputSize; + /* don't initialize yet, wait for the first block of flush() order, for better parameters adaptation */ + return ZSTD_FRAMEHEADERSIZE_MIN(cctx->requestedParams.format); /* at least some header to produce */ + } + FORWARD_IF_ERROR(ZSTD_CCtx_init_compressStream2(cctx, endOp, totalInputSize), "compressStream2 initialization failed"); + ZSTD_setBufferExpectations(cctx, output, input); /* Set initial buffer expectations now that we've initialized */ + } + /* end of transparent initialization stage */ + + FORWARD_IF_ERROR(ZSTD_checkBufferStability(cctx, output, input, endOp), "invalid buffers"); + /* compression stage */ +#ifdef ZSTD_MULTITHREAD + if (cctx->appliedParams.nbWorkers > 0) { + size_t flushMin; + if (cctx->cParamsChanged) { + ZSTDMT_updateCParams_whileCompressing(cctx->mtctx, &cctx->requestedParams); + cctx->cParamsChanged = 0; + } + if (cctx->stableIn_notConsumed) { + assert(cctx->appliedParams.inBufferMode == ZSTD_bm_stable); + /* some early data was skipped - make it available for consumption */ + assert(input->pos >= cctx->stableIn_notConsumed); + input->pos -= cctx->stableIn_notConsumed; + cctx->stableIn_notConsumed = 0; + } + for (;;) { + size_t const ipos = input->pos; + size_t const opos = output->pos; + flushMin = ZSTDMT_compressStream_generic(cctx->mtctx, output, input, endOp); + cctx->consumedSrcSize += (U64)(input->pos - ipos); + cctx->producedCSize += (U64)(output->pos - opos); + if ( ZSTD_isError(flushMin) + || (endOp == ZSTD_e_end && flushMin == 0) ) { /* compression completed */ + if (flushMin == 0) + ZSTD_CCtx_trace(cctx, 0); + ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only); + } + FORWARD_IF_ERROR(flushMin, "ZSTDMT_compressStream_generic failed"); + + if (endOp == ZSTD_e_continue) { + /* We only require some progress with ZSTD_e_continue, not maximal progress. + * We're done if we've consumed or produced any bytes, or either buffer is + * full. + */ + if (input->pos != ipos || output->pos != opos || input->pos == input->size || output->pos == output->size) + break; + } else { + assert(endOp == ZSTD_e_flush || endOp == ZSTD_e_end); + /* We require maximal progress. We're done when the flush is complete or the + * output buffer is full. + */ + if (flushMin == 0 || output->pos == output->size) + break; + } + } + DEBUGLOG(5, "completed ZSTD_compressStream2 delegating to ZSTDMT_compressStream_generic"); + /* Either we don't require maximum forward progress, we've finished the + * flush, or we are out of output space. + */ + assert(endOp == ZSTD_e_continue || flushMin == 0 || output->pos == output->size); + ZSTD_setBufferExpectations(cctx, output, input); + return flushMin; + } +#endif /* ZSTD_MULTITHREAD */ + FORWARD_IF_ERROR( ZSTD_compressStream_generic(cctx, output, input, endOp) , ""); + DEBUGLOG(5, "completed ZSTD_compressStream2"); + ZSTD_setBufferExpectations(cctx, output, input); + return cctx->outBuffContentSize - cctx->outBuffFlushedSize; /* remaining to flush */ +} + +size_t ZSTD_compressStream2_simpleArgs ( + ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, size_t* dstPos, + const void* src, size_t srcSize, size_t* srcPos, + ZSTD_EndDirective endOp) +{ + ZSTD_outBuffer output; + ZSTD_inBuffer input; + output.dst = dst; + output.size = dstCapacity; + output.pos = *dstPos; + input.src = src; + input.size = srcSize; + input.pos = *srcPos; + /* ZSTD_compressStream2() will check validity of dstPos and srcPos */ + { size_t const cErr = ZSTD_compressStream2(cctx, &output, &input, endOp); + *dstPos = output.pos; + *srcPos = input.pos; + return cErr; + } +} + +size_t ZSTD_compress2(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize) +{ + ZSTD_bufferMode_e const originalInBufferMode = cctx->requestedParams.inBufferMode; + ZSTD_bufferMode_e const originalOutBufferMode = cctx->requestedParams.outBufferMode; + DEBUGLOG(4, "ZSTD_compress2 (srcSize=%u)", (unsigned)srcSize); + ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only); + /* Enable stable input/output buffers. */ + cctx->requestedParams.inBufferMode = ZSTD_bm_stable; + cctx->requestedParams.outBufferMode = ZSTD_bm_stable; + { size_t oPos = 0; + size_t iPos = 0; + size_t const result = ZSTD_compressStream2_simpleArgs(cctx, + dst, dstCapacity, &oPos, + src, srcSize, &iPos, + ZSTD_e_end); + /* Reset to the original values. */ + cctx->requestedParams.inBufferMode = originalInBufferMode; + cctx->requestedParams.outBufferMode = originalOutBufferMode; + + FORWARD_IF_ERROR(result, "ZSTD_compressStream2_simpleArgs failed"); + if (result != 0) { /* compression not completed, due to lack of output space */ + assert(oPos == dstCapacity); + RETURN_ERROR(dstSize_tooSmall, ""); + } + assert(iPos == srcSize); /* all input is expected consumed */ + return oPos; + } +} + +/* ZSTD_validateSequence() : + * @offCode : is presumed to follow format required by ZSTD_storeSeq() + * @returns a ZSTD error code if sequence is not valid + */ +static size_t +ZSTD_validateSequence(U32 offCode, U32 matchLength, U32 minMatch, + size_t posInSrc, U32 windowLog, size_t dictSize, int useSequenceProducer) +{ + U32 const windowSize = 1u << windowLog; + /* posInSrc represents the amount of data the decoder would decode up to this point. + * As long as the amount of data decoded is less than or equal to window size, offsets may be + * larger than the total length of output decoded in order to reference the dict, even larger than + * window size. After output surpasses windowSize, we're limited to windowSize offsets again. + */ + size_t const offsetBound = posInSrc > windowSize ? (size_t)windowSize : posInSrc + (size_t)dictSize; + size_t const matchLenLowerBound = (minMatch == 3 || useSequenceProducer) ? 3 : 4; + RETURN_ERROR_IF(offCode > OFFSET_TO_OFFBASE(offsetBound), externalSequences_invalid, "Offset too large!"); + /* Validate maxNbSeq is large enough for the given matchLength and minMatch */ + RETURN_ERROR_IF(matchLength < matchLenLowerBound, externalSequences_invalid, "Matchlength too small for the minMatch"); + return 0; +} + +/* Returns an offset code, given a sequence's raw offset, the ongoing repcode array, and whether litLength == 0 */ +static U32 ZSTD_finalizeOffBase(U32 rawOffset, const U32 rep[ZSTD_REP_NUM], U32 ll0) +{ + U32 offBase = OFFSET_TO_OFFBASE(rawOffset); + + if (!ll0 && rawOffset == rep[0]) { + offBase = REPCODE1_TO_OFFBASE; + } else if (rawOffset == rep[1]) { + offBase = REPCODE_TO_OFFBASE(2 - ll0); + } else if (rawOffset == rep[2]) { + offBase = REPCODE_TO_OFFBASE(3 - ll0); + } else if (ll0 && rawOffset == rep[0] - 1) { + offBase = REPCODE3_TO_OFFBASE; + } + return offBase; +} + +size_t +ZSTD_copySequencesToSeqStoreExplicitBlockDelim(ZSTD_CCtx* cctx, + ZSTD_sequencePosition* seqPos, + const ZSTD_Sequence* const inSeqs, size_t inSeqsSize, + const void* src, size_t blockSize, + ZSTD_paramSwitch_e externalRepSearch) +{ + U32 idx = seqPos->idx; + U32 const startIdx = idx; + BYTE const* ip = (BYTE const*)(src); + const BYTE* const iend = ip + blockSize; + repcodes_t updatedRepcodes; + U32 dictSize; + + DEBUGLOG(5, "ZSTD_copySequencesToSeqStoreExplicitBlockDelim (blockSize = %zu)", blockSize); + + if (cctx->cdict) { + dictSize = (U32)cctx->cdict->dictContentSize; + } else if (cctx->prefixDict.dict) { + dictSize = (U32)cctx->prefixDict.dictSize; + } else { + dictSize = 0; + } + ZSTD_memcpy(updatedRepcodes.rep, cctx->blockState.prevCBlock->rep, sizeof(repcodes_t)); + for (; idx < inSeqsSize && (inSeqs[idx].matchLength != 0 || inSeqs[idx].offset != 0); ++idx) { + U32 const litLength = inSeqs[idx].litLength; + U32 const matchLength = inSeqs[idx].matchLength; + U32 offBase; + + if (externalRepSearch == ZSTD_ps_disable) { + offBase = OFFSET_TO_OFFBASE(inSeqs[idx].offset); + } else { + U32 const ll0 = (litLength == 0); + offBase = ZSTD_finalizeOffBase(inSeqs[idx].offset, updatedRepcodes.rep, ll0); + ZSTD_updateRep(updatedRepcodes.rep, offBase, ll0); + } + + DEBUGLOG(6, "Storing sequence: (of: %u, ml: %u, ll: %u)", offBase, matchLength, litLength); + if (cctx->appliedParams.validateSequences) { + seqPos->posInSrc += litLength + matchLength; + FORWARD_IF_ERROR(ZSTD_validateSequence(offBase, matchLength, cctx->appliedParams.cParams.minMatch, seqPos->posInSrc, + cctx->appliedParams.cParams.windowLog, dictSize, ZSTD_hasExtSeqProd(&cctx->appliedParams)), + "Sequence validation failed"); + } + RETURN_ERROR_IF(idx - seqPos->idx >= cctx->seqStore.maxNbSeq, externalSequences_invalid, + "Not enough memory allocated. Try adjusting ZSTD_c_minMatch."); + ZSTD_storeSeq(&cctx->seqStore, litLength, ip, iend, offBase, matchLength); + ip += matchLength + litLength; + } + + /* If we skipped repcode search while parsing, we need to update repcodes now */ + assert(externalRepSearch != ZSTD_ps_auto); + assert(idx >= startIdx); + if (externalRepSearch == ZSTD_ps_disable && idx != startIdx) { + U32* const rep = updatedRepcodes.rep; + U32 lastSeqIdx = idx - 1; /* index of last non-block-delimiter sequence */ + + if (lastSeqIdx >= startIdx + 2) { + rep[2] = inSeqs[lastSeqIdx - 2].offset; + rep[1] = inSeqs[lastSeqIdx - 1].offset; + rep[0] = inSeqs[lastSeqIdx].offset; + } else if (lastSeqIdx == startIdx + 1) { + rep[2] = rep[0]; + rep[1] = inSeqs[lastSeqIdx - 1].offset; + rep[0] = inSeqs[lastSeqIdx].offset; + } else { + assert(lastSeqIdx == startIdx); + rep[2] = rep[1]; + rep[1] = rep[0]; + rep[0] = inSeqs[lastSeqIdx].offset; + } + } + + ZSTD_memcpy(cctx->blockState.nextCBlock->rep, updatedRepcodes.rep, sizeof(repcodes_t)); + + if (inSeqs[idx].litLength) { + DEBUGLOG(6, "Storing last literals of size: %u", inSeqs[idx].litLength); + ZSTD_storeLastLiterals(&cctx->seqStore, ip, inSeqs[idx].litLength); + ip += inSeqs[idx].litLength; + seqPos->posInSrc += inSeqs[idx].litLength; + } + RETURN_ERROR_IF(ip != iend, externalSequences_invalid, "Blocksize doesn't agree with block delimiter!"); + seqPos->idx = idx+1; + return 0; +} + +size_t +ZSTD_copySequencesToSeqStoreNoBlockDelim(ZSTD_CCtx* cctx, ZSTD_sequencePosition* seqPos, + const ZSTD_Sequence* const inSeqs, size_t inSeqsSize, + const void* src, size_t blockSize, ZSTD_paramSwitch_e externalRepSearch) +{ + U32 idx = seqPos->idx; + U32 startPosInSequence = seqPos->posInSequence; + U32 endPosInSequence = seqPos->posInSequence + (U32)blockSize; + size_t dictSize; + BYTE const* ip = (BYTE const*)(src); + BYTE const* iend = ip + blockSize; /* May be adjusted if we decide to process fewer than blockSize bytes */ + repcodes_t updatedRepcodes; + U32 bytesAdjustment = 0; + U32 finalMatchSplit = 0; + + /* TODO(embg) support fast parsing mode in noBlockDelim mode */ + (void)externalRepSearch; + + if (cctx->cdict) { + dictSize = cctx->cdict->dictContentSize; + } else if (cctx->prefixDict.dict) { + dictSize = cctx->prefixDict.dictSize; + } else { + dictSize = 0; + } + DEBUGLOG(5, "ZSTD_copySequencesToSeqStoreNoBlockDelim: idx: %u PIS: %u blockSize: %zu", idx, startPosInSequence, blockSize); + DEBUGLOG(5, "Start seq: idx: %u (of: %u ml: %u ll: %u)", idx, inSeqs[idx].offset, inSeqs[idx].matchLength, inSeqs[idx].litLength); + ZSTD_memcpy(updatedRepcodes.rep, cctx->blockState.prevCBlock->rep, sizeof(repcodes_t)); + while (endPosInSequence && idx < inSeqsSize && !finalMatchSplit) { + const ZSTD_Sequence currSeq = inSeqs[idx]; + U32 litLength = currSeq.litLength; + U32 matchLength = currSeq.matchLength; + U32 const rawOffset = currSeq.offset; + U32 offBase; + + /* Modify the sequence depending on where endPosInSequence lies */ + if (endPosInSequence >= currSeq.litLength + currSeq.matchLength) { + if (startPosInSequence >= litLength) { + startPosInSequence -= litLength; + litLength = 0; + matchLength -= startPosInSequence; + } else { + litLength -= startPosInSequence; + } + /* Move to the next sequence */ + endPosInSequence -= currSeq.litLength + currSeq.matchLength; + startPosInSequence = 0; + } else { + /* This is the final (partial) sequence we're adding from inSeqs, and endPosInSequence + does not reach the end of the match. So, we have to split the sequence */ + DEBUGLOG(6, "Require a split: diff: %u, idx: %u PIS: %u", + currSeq.litLength + currSeq.matchLength - endPosInSequence, idx, endPosInSequence); + if (endPosInSequence > litLength) { + U32 firstHalfMatchLength; + litLength = startPosInSequence >= litLength ? 0 : litLength - startPosInSequence; + firstHalfMatchLength = endPosInSequence - startPosInSequence - litLength; + if (matchLength > blockSize && firstHalfMatchLength >= cctx->appliedParams.cParams.minMatch) { + /* Only ever split the match if it is larger than the block size */ + U32 secondHalfMatchLength = currSeq.matchLength + currSeq.litLength - endPosInSequence; + if (secondHalfMatchLength < cctx->appliedParams.cParams.minMatch) { + /* Move the endPosInSequence backward so that it creates match of minMatch length */ + endPosInSequence -= cctx->appliedParams.cParams.minMatch - secondHalfMatchLength; + bytesAdjustment = cctx->appliedParams.cParams.minMatch - secondHalfMatchLength; + firstHalfMatchLength -= bytesAdjustment; + } + matchLength = firstHalfMatchLength; + /* Flag that we split the last match - after storing the sequence, exit the loop, + but keep the value of endPosInSequence */ + finalMatchSplit = 1; + } else { + /* Move the position in sequence backwards so that we don't split match, and break to store + * the last literals. We use the original currSeq.litLength as a marker for where endPosInSequence + * should go. We prefer to do this whenever it is not necessary to split the match, or if doing so + * would cause the first half of the match to be too small + */ + bytesAdjustment = endPosInSequence - currSeq.litLength; + endPosInSequence = currSeq.litLength; + break; + } + } else { + /* This sequence ends inside the literals, break to store the last literals */ + break; + } + } + /* Check if this offset can be represented with a repcode */ + { U32 const ll0 = (litLength == 0); + offBase = ZSTD_finalizeOffBase(rawOffset, updatedRepcodes.rep, ll0); + ZSTD_updateRep(updatedRepcodes.rep, offBase, ll0); + } + + if (cctx->appliedParams.validateSequences) { + seqPos->posInSrc += litLength + matchLength; + FORWARD_IF_ERROR(ZSTD_validateSequence(offBase, matchLength, cctx->appliedParams.cParams.minMatch, seqPos->posInSrc, + cctx->appliedParams.cParams.windowLog, dictSize, ZSTD_hasExtSeqProd(&cctx->appliedParams)), + "Sequence validation failed"); + } + DEBUGLOG(6, "Storing sequence: (of: %u, ml: %u, ll: %u)", offBase, matchLength, litLength); + RETURN_ERROR_IF(idx - seqPos->idx >= cctx->seqStore.maxNbSeq, externalSequences_invalid, + "Not enough memory allocated. Try adjusting ZSTD_c_minMatch."); + ZSTD_storeSeq(&cctx->seqStore, litLength, ip, iend, offBase, matchLength); + ip += matchLength + litLength; + if (!finalMatchSplit) + idx++; /* Next Sequence */ + } + DEBUGLOG(5, "Ending seq: idx: %u (of: %u ml: %u ll: %u)", idx, inSeqs[idx].offset, inSeqs[idx].matchLength, inSeqs[idx].litLength); + assert(idx == inSeqsSize || endPosInSequence <= inSeqs[idx].litLength + inSeqs[idx].matchLength); + seqPos->idx = idx; + seqPos->posInSequence = endPosInSequence; + ZSTD_memcpy(cctx->blockState.nextCBlock->rep, updatedRepcodes.rep, sizeof(repcodes_t)); + + iend -= bytesAdjustment; + if (ip != iend) { + /* Store any last literals */ + U32 lastLLSize = (U32)(iend - ip); + assert(ip <= iend); + DEBUGLOG(6, "Storing last literals of size: %u", lastLLSize); + ZSTD_storeLastLiterals(&cctx->seqStore, ip, lastLLSize); + seqPos->posInSrc += lastLLSize; + } + + return bytesAdjustment; +} + +typedef size_t (*ZSTD_sequenceCopier) (ZSTD_CCtx* cctx, ZSTD_sequencePosition* seqPos, + const ZSTD_Sequence* const inSeqs, size_t inSeqsSize, + const void* src, size_t blockSize, ZSTD_paramSwitch_e externalRepSearch); +static ZSTD_sequenceCopier ZSTD_selectSequenceCopier(ZSTD_sequenceFormat_e mode) +{ + ZSTD_sequenceCopier sequenceCopier = NULL; + assert(ZSTD_cParam_withinBounds(ZSTD_c_blockDelimiters, mode)); + if (mode == ZSTD_sf_explicitBlockDelimiters) { + return ZSTD_copySequencesToSeqStoreExplicitBlockDelim; + } else if (mode == ZSTD_sf_noBlockDelimiters) { + return ZSTD_copySequencesToSeqStoreNoBlockDelim; + } + assert(sequenceCopier != NULL); + return sequenceCopier; +} + +/* Discover the size of next block by searching for the delimiter. + * Note that a block delimiter **must** exist in this mode, + * otherwise it's an input error. + * The block size retrieved will be later compared to ensure it remains within bounds */ +static size_t +blockSize_explicitDelimiter(const ZSTD_Sequence* inSeqs, size_t inSeqsSize, ZSTD_sequencePosition seqPos) +{ + int end = 0; + size_t blockSize = 0; + size_t spos = seqPos.idx; + DEBUGLOG(6, "blockSize_explicitDelimiter : seq %zu / %zu", spos, inSeqsSize); + assert(spos <= inSeqsSize); + while (spos < inSeqsSize) { + end = (inSeqs[spos].offset == 0); + blockSize += inSeqs[spos].litLength + inSeqs[spos].matchLength; + if (end) { + if (inSeqs[spos].matchLength != 0) + RETURN_ERROR(externalSequences_invalid, "delimiter format error : both matchlength and offset must be == 0"); + break; + } + spos++; + } + if (!end) + RETURN_ERROR(externalSequences_invalid, "Reached end of sequences without finding a block delimiter"); + return blockSize; +} + +/* More a "target" block size */ +static size_t blockSize_noDelimiter(size_t blockSize, size_t remaining) +{ + int const lastBlock = (remaining <= blockSize); + return lastBlock ? remaining : blockSize; +} + +static size_t determine_blockSize(ZSTD_sequenceFormat_e mode, + size_t blockSize, size_t remaining, + const ZSTD_Sequence* inSeqs, size_t inSeqsSize, ZSTD_sequencePosition seqPos) +{ + DEBUGLOG(6, "determine_blockSize : remainingSize = %zu", remaining); + if (mode == ZSTD_sf_noBlockDelimiters) + return blockSize_noDelimiter(blockSize, remaining); + { size_t const explicitBlockSize = blockSize_explicitDelimiter(inSeqs, inSeqsSize, seqPos); + FORWARD_IF_ERROR(explicitBlockSize, "Error while determining block size with explicit delimiters"); + if (explicitBlockSize > blockSize) + RETURN_ERROR(externalSequences_invalid, "sequences incorrectly define a too large block"); + if (explicitBlockSize > remaining) + RETURN_ERROR(externalSequences_invalid, "sequences define a frame longer than source"); + return explicitBlockSize; + } +} + +/* Compress, block-by-block, all of the sequences given. + * + * Returns the cumulative size of all compressed blocks (including their headers), + * otherwise a ZSTD error. + */ +static size_t +ZSTD_compressSequences_internal(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const ZSTD_Sequence* inSeqs, size_t inSeqsSize, + const void* src, size_t srcSize) +{ + size_t cSize = 0; + size_t remaining = srcSize; + ZSTD_sequencePosition seqPos = {0, 0, 0}; + + BYTE const* ip = (BYTE const*)src; + BYTE* op = (BYTE*)dst; + ZSTD_sequenceCopier const sequenceCopier = ZSTD_selectSequenceCopier(cctx->appliedParams.blockDelimiters); + + DEBUGLOG(4, "ZSTD_compressSequences_internal srcSize: %zu, inSeqsSize: %zu", srcSize, inSeqsSize); + /* Special case: empty frame */ + if (remaining == 0) { + U32 const cBlockHeader24 = 1 /* last block */ + (((U32)bt_raw)<<1); + RETURN_ERROR_IF(dstCapacity<4, dstSize_tooSmall, "No room for empty frame block header"); + MEM_writeLE32(op, cBlockHeader24); + op += ZSTD_blockHeaderSize; + dstCapacity -= ZSTD_blockHeaderSize; + cSize += ZSTD_blockHeaderSize; + } + + while (remaining) { + size_t compressedSeqsSize; + size_t cBlockSize; + size_t additionalByteAdjustment; + size_t blockSize = determine_blockSize(cctx->appliedParams.blockDelimiters, + cctx->blockSize, remaining, + inSeqs, inSeqsSize, seqPos); + U32 const lastBlock = (blockSize == remaining); + FORWARD_IF_ERROR(blockSize, "Error while trying to determine block size"); + assert(blockSize <= remaining); + ZSTD_resetSeqStore(&cctx->seqStore); + DEBUGLOG(5, "Working on new block. Blocksize: %zu (total:%zu)", blockSize, (ip - (const BYTE*)src) + blockSize); + + additionalByteAdjustment = sequenceCopier(cctx, &seqPos, inSeqs, inSeqsSize, ip, blockSize, cctx->appliedParams.searchForExternalRepcodes); + FORWARD_IF_ERROR(additionalByteAdjustment, "Bad sequence copy"); + blockSize -= additionalByteAdjustment; + + /* If blocks are too small, emit as a nocompress block */ + /* TODO: See 3090. We reduced MIN_CBLOCK_SIZE from 3 to 2 so to compensate we are adding + * additional 1. We need to revisit and change this logic to be more consistent */ + if (blockSize < MIN_CBLOCK_SIZE+ZSTD_blockHeaderSize+1+1) { + cBlockSize = ZSTD_noCompressBlock(op, dstCapacity, ip, blockSize, lastBlock); + FORWARD_IF_ERROR(cBlockSize, "Nocompress block failed"); + DEBUGLOG(5, "Block too small, writing out nocompress block: cSize: %zu", cBlockSize); + cSize += cBlockSize; + ip += blockSize; + op += cBlockSize; + remaining -= blockSize; + dstCapacity -= cBlockSize; + continue; + } + + RETURN_ERROR_IF(dstCapacity < ZSTD_blockHeaderSize, dstSize_tooSmall, "not enough dstCapacity to write a new compressed block"); + compressedSeqsSize = ZSTD_entropyCompressSeqStore(&cctx->seqStore, + &cctx->blockState.prevCBlock->entropy, &cctx->blockState.nextCBlock->entropy, + &cctx->appliedParams, + op + ZSTD_blockHeaderSize /* Leave space for block header */, dstCapacity - ZSTD_blockHeaderSize, + blockSize, + cctx->entropyWorkspace, ENTROPY_WORKSPACE_SIZE /* statically allocated in resetCCtx */, + cctx->bmi2); + FORWARD_IF_ERROR(compressedSeqsSize, "Compressing sequences of block failed"); + DEBUGLOG(5, "Compressed sequences size: %zu", compressedSeqsSize); + + if (!cctx->isFirstBlock && + ZSTD_maybeRLE(&cctx->seqStore) && + ZSTD_isRLE(ip, blockSize)) { + /* We don't want to emit our first block as a RLE even if it qualifies because + * doing so will cause the decoder (cli only) to throw a "should consume all input error." + * This is only an issue for zstd <= v1.4.3 + */ + compressedSeqsSize = 1; + } + + if (compressedSeqsSize == 0) { + /* ZSTD_noCompressBlock writes the block header as well */ + cBlockSize = ZSTD_noCompressBlock(op, dstCapacity, ip, blockSize, lastBlock); + FORWARD_IF_ERROR(cBlockSize, "ZSTD_noCompressBlock failed"); + DEBUGLOG(5, "Writing out nocompress block, size: %zu", cBlockSize); + } else if (compressedSeqsSize == 1) { + cBlockSize = ZSTD_rleCompressBlock(op, dstCapacity, *ip, blockSize, lastBlock); + FORWARD_IF_ERROR(cBlockSize, "ZSTD_rleCompressBlock failed"); + DEBUGLOG(5, "Writing out RLE block, size: %zu", cBlockSize); + } else { + U32 cBlockHeader; + /* Error checking and repcodes update */ + ZSTD_blockState_confirmRepcodesAndEntropyTables(&cctx->blockState); + if (cctx->blockState.prevCBlock->entropy.fse.offcode_repeatMode == FSE_repeat_valid) + cctx->blockState.prevCBlock->entropy.fse.offcode_repeatMode = FSE_repeat_check; + + /* Write block header into beginning of block*/ + cBlockHeader = lastBlock + (((U32)bt_compressed)<<1) + (U32)(compressedSeqsSize << 3); + MEM_writeLE24(op, cBlockHeader); + cBlockSize = ZSTD_blockHeaderSize + compressedSeqsSize; + DEBUGLOG(5, "Writing out compressed block, size: %zu", cBlockSize); + } + + cSize += cBlockSize; + + if (lastBlock) { + break; + } else { + ip += blockSize; + op += cBlockSize; + remaining -= blockSize; + dstCapacity -= cBlockSize; + cctx->isFirstBlock = 0; + } + DEBUGLOG(5, "cSize running total: %zu (remaining dstCapacity=%zu)", cSize, dstCapacity); + } + + DEBUGLOG(4, "cSize final total: %zu", cSize); + return cSize; +} + +size_t ZSTD_compressSequences(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const ZSTD_Sequence* inSeqs, size_t inSeqsSize, + const void* src, size_t srcSize) +{ + BYTE* op = (BYTE*)dst; + size_t cSize = 0; + size_t compressedBlocksSize = 0; + size_t frameHeaderSize = 0; + + /* Transparent initialization stage, same as compressStream2() */ + DEBUGLOG(4, "ZSTD_compressSequences (dstCapacity=%zu)", dstCapacity); + assert(cctx != NULL); + FORWARD_IF_ERROR(ZSTD_CCtx_init_compressStream2(cctx, ZSTD_e_end, srcSize), "CCtx initialization failed"); + /* Begin writing output, starting with frame header */ + frameHeaderSize = ZSTD_writeFrameHeader(op, dstCapacity, &cctx->appliedParams, srcSize, cctx->dictID); + op += frameHeaderSize; + dstCapacity -= frameHeaderSize; + cSize += frameHeaderSize; + if (cctx->appliedParams.fParams.checksumFlag && srcSize) { + XXH64_update(&cctx->xxhState, src, srcSize); + } + /* cSize includes block header size and compressed sequences size */ + compressedBlocksSize = ZSTD_compressSequences_internal(cctx, + op, dstCapacity, + inSeqs, inSeqsSize, + src, srcSize); + FORWARD_IF_ERROR(compressedBlocksSize, "Compressing blocks failed!"); + cSize += compressedBlocksSize; + dstCapacity -= compressedBlocksSize; + + if (cctx->appliedParams.fParams.checksumFlag) { + U32 const checksum = (U32) XXH64_digest(&cctx->xxhState); + RETURN_ERROR_IF(dstCapacity<4, dstSize_tooSmall, "no room for checksum"); + DEBUGLOG(4, "Write checksum : %08X", (unsigned)checksum); + MEM_writeLE32((char*)dst + cSize, checksum); + cSize += 4; + } + + DEBUGLOG(4, "Final compressed size: %zu", cSize); + return cSize; +} + +/*====== Finalize ======*/ + +static ZSTD_inBuffer inBuffer_forEndFlush(const ZSTD_CStream* zcs) +{ + const ZSTD_inBuffer nullInput = { NULL, 0, 0 }; + const int stableInput = (zcs->appliedParams.inBufferMode == ZSTD_bm_stable); + return stableInput ? zcs->expectedInBuffer : nullInput; +} + +/*! ZSTD_flushStream() : + * @return : amount of data remaining to flush */ +size_t ZSTD_flushStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output) +{ + ZSTD_inBuffer input = inBuffer_forEndFlush(zcs); + input.size = input.pos; /* do not ingest more input during flush */ + return ZSTD_compressStream2(zcs, output, &input, ZSTD_e_flush); +} + + +size_t ZSTD_endStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output) +{ + ZSTD_inBuffer input = inBuffer_forEndFlush(zcs); + size_t const remainingToFlush = ZSTD_compressStream2(zcs, output, &input, ZSTD_e_end); + FORWARD_IF_ERROR(remainingToFlush , "ZSTD_compressStream2(,,ZSTD_e_end) failed"); + if (zcs->appliedParams.nbWorkers > 0) return remainingToFlush; /* minimal estimation */ + /* single thread mode : attempt to calculate remaining to flush more precisely */ + { size_t const lastBlockSize = zcs->frameEnded ? 0 : ZSTD_BLOCKHEADERSIZE; + size_t const checksumSize = (size_t)(zcs->frameEnded ? 0 : zcs->appliedParams.fParams.checksumFlag * 4); + size_t const toFlush = remainingToFlush + lastBlockSize + checksumSize; + DEBUGLOG(4, "ZSTD_endStream : remaining to flush : %u", (unsigned)toFlush); + return toFlush; + } +} + + +/*-===== Pre-defined compression levels =====-*/ +/**** start inlining clevels.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_CLEVELS_H +#define ZSTD_CLEVELS_H + +#define ZSTD_STATIC_LINKING_ONLY /* ZSTD_compressionParameters */ +/**** skipping file: ../zstd.h ****/ + +/*-===== Pre-defined compression levels =====-*/ + +#define ZSTD_MAX_CLEVEL 22 + +#ifdef __GNUC__ +__attribute__((__unused__)) +#endif + +static const ZSTD_compressionParameters ZSTD_defaultCParameters[4][ZSTD_MAX_CLEVEL+1] = { +{ /* "default" - for any srcSize > 256 KB */ + /* W, C, H, S, L, TL, strat */ + { 19, 12, 13, 1, 6, 1, ZSTD_fast }, /* base for negative levels */ + { 19, 13, 14, 1, 7, 0, ZSTD_fast }, /* level 1 */ + { 20, 15, 16, 1, 6, 0, ZSTD_fast }, /* level 2 */ + { 21, 16, 17, 1, 5, 0, ZSTD_dfast }, /* level 3 */ + { 21, 18, 18, 1, 5, 0, ZSTD_dfast }, /* level 4 */ + { 21, 18, 19, 3, 5, 2, ZSTD_greedy }, /* level 5 */ + { 21, 18, 19, 3, 5, 4, ZSTD_lazy }, /* level 6 */ + { 21, 19, 20, 4, 5, 8, ZSTD_lazy }, /* level 7 */ + { 21, 19, 20, 4, 5, 16, ZSTD_lazy2 }, /* level 8 */ + { 22, 20, 21, 4, 5, 16, ZSTD_lazy2 }, /* level 9 */ + { 22, 21, 22, 5, 5, 16, ZSTD_lazy2 }, /* level 10 */ + { 22, 21, 22, 6, 5, 16, ZSTD_lazy2 }, /* level 11 */ + { 22, 22, 23, 6, 5, 32, ZSTD_lazy2 }, /* level 12 */ + { 22, 22, 22, 4, 5, 32, ZSTD_btlazy2 }, /* level 13 */ + { 22, 22, 23, 5, 5, 32, ZSTD_btlazy2 }, /* level 14 */ + { 22, 23, 23, 6, 5, 32, ZSTD_btlazy2 }, /* level 15 */ + { 22, 22, 22, 5, 5, 48, ZSTD_btopt }, /* level 16 */ + { 23, 23, 22, 5, 4, 64, ZSTD_btopt }, /* level 17 */ + { 23, 23, 22, 6, 3, 64, ZSTD_btultra }, /* level 18 */ + { 23, 24, 22, 7, 3,256, ZSTD_btultra2}, /* level 19 */ + { 25, 25, 23, 7, 3,256, ZSTD_btultra2}, /* level 20 */ + { 26, 26, 24, 7, 3,512, ZSTD_btultra2}, /* level 21 */ + { 27, 27, 25, 9, 3,999, ZSTD_btultra2}, /* level 22 */ +}, +{ /* for srcSize <= 256 KB */ + /* W, C, H, S, L, T, strat */ + { 18, 12, 13, 1, 5, 1, ZSTD_fast }, /* base for negative levels */ + { 18, 13, 14, 1, 6, 0, ZSTD_fast }, /* level 1 */ + { 18, 14, 14, 1, 5, 0, ZSTD_dfast }, /* level 2 */ + { 18, 16, 16, 1, 4, 0, ZSTD_dfast }, /* level 3 */ + { 18, 16, 17, 3, 5, 2, ZSTD_greedy }, /* level 4.*/ + { 18, 17, 18, 5, 5, 2, ZSTD_greedy }, /* level 5.*/ + { 18, 18, 19, 3, 5, 4, ZSTD_lazy }, /* level 6.*/ + { 18, 18, 19, 4, 4, 4, ZSTD_lazy }, /* level 7 */ + { 18, 18, 19, 4, 4, 8, ZSTD_lazy2 }, /* level 8 */ + { 18, 18, 19, 5, 4, 8, ZSTD_lazy2 }, /* level 9 */ + { 18, 18, 19, 6, 4, 8, ZSTD_lazy2 }, /* level 10 */ + { 18, 18, 19, 5, 4, 12, ZSTD_btlazy2 }, /* level 11.*/ + { 18, 19, 19, 7, 4, 12, ZSTD_btlazy2 }, /* level 12.*/ + { 18, 18, 19, 4, 4, 16, ZSTD_btopt }, /* level 13 */ + { 18, 18, 19, 4, 3, 32, ZSTD_btopt }, /* level 14.*/ + { 18, 18, 19, 6, 3,128, ZSTD_btopt }, /* level 15.*/ + { 18, 19, 19, 6, 3,128, ZSTD_btultra }, /* level 16.*/ + { 18, 19, 19, 8, 3,256, ZSTD_btultra }, /* level 17.*/ + { 18, 19, 19, 6, 3,128, ZSTD_btultra2}, /* level 18.*/ + { 18, 19, 19, 8, 3,256, ZSTD_btultra2}, /* level 19.*/ + { 18, 19, 19, 10, 3,512, ZSTD_btultra2}, /* level 20.*/ + { 18, 19, 19, 12, 3,512, ZSTD_btultra2}, /* level 21.*/ + { 18, 19, 19, 13, 3,999, ZSTD_btultra2}, /* level 22.*/ +}, +{ /* for srcSize <= 128 KB */ + /* W, C, H, S, L, T, strat */ + { 17, 12, 12, 1, 5, 1, ZSTD_fast }, /* base for negative levels */ + { 17, 12, 13, 1, 6, 0, ZSTD_fast }, /* level 1 */ + { 17, 13, 15, 1, 5, 0, ZSTD_fast }, /* level 2 */ + { 17, 15, 16, 2, 5, 0, ZSTD_dfast }, /* level 3 */ + { 17, 17, 17, 2, 4, 0, ZSTD_dfast }, /* level 4 */ + { 17, 16, 17, 3, 4, 2, ZSTD_greedy }, /* level 5 */ + { 17, 16, 17, 3, 4, 4, ZSTD_lazy }, /* level 6 */ + { 17, 16, 17, 3, 4, 8, ZSTD_lazy2 }, /* level 7 */ + { 17, 16, 17, 4, 4, 8, ZSTD_lazy2 }, /* level 8 */ + { 17, 16, 17, 5, 4, 8, ZSTD_lazy2 }, /* level 9 */ + { 17, 16, 17, 6, 4, 8, ZSTD_lazy2 }, /* level 10 */ + { 17, 17, 17, 5, 4, 8, ZSTD_btlazy2 }, /* level 11 */ + { 17, 18, 17, 7, 4, 12, ZSTD_btlazy2 }, /* level 12 */ + { 17, 18, 17, 3, 4, 12, ZSTD_btopt }, /* level 13.*/ + { 17, 18, 17, 4, 3, 32, ZSTD_btopt }, /* level 14.*/ + { 17, 18, 17, 6, 3,256, ZSTD_btopt }, /* level 15.*/ + { 17, 18, 17, 6, 3,128, ZSTD_btultra }, /* level 16.*/ + { 17, 18, 17, 8, 3,256, ZSTD_btultra }, /* level 17.*/ + { 17, 18, 17, 10, 3,512, ZSTD_btultra }, /* level 18.*/ + { 17, 18, 17, 5, 3,256, ZSTD_btultra2}, /* level 19.*/ + { 17, 18, 17, 7, 3,512, ZSTD_btultra2}, /* level 20.*/ + { 17, 18, 17, 9, 3,512, ZSTD_btultra2}, /* level 21.*/ + { 17, 18, 17, 11, 3,999, ZSTD_btultra2}, /* level 22.*/ +}, +{ /* for srcSize <= 16 KB */ + /* W, C, H, S, L, T, strat */ + { 14, 12, 13, 1, 5, 1, ZSTD_fast }, /* base for negative levels */ + { 14, 14, 15, 1, 5, 0, ZSTD_fast }, /* level 1 */ + { 14, 14, 15, 1, 4, 0, ZSTD_fast }, /* level 2 */ + { 14, 14, 15, 2, 4, 0, ZSTD_dfast }, /* level 3 */ + { 14, 14, 14, 4, 4, 2, ZSTD_greedy }, /* level 4 */ + { 14, 14, 14, 3, 4, 4, ZSTD_lazy }, /* level 5.*/ + { 14, 14, 14, 4, 4, 8, ZSTD_lazy2 }, /* level 6 */ + { 14, 14, 14, 6, 4, 8, ZSTD_lazy2 }, /* level 7 */ + { 14, 14, 14, 8, 4, 8, ZSTD_lazy2 }, /* level 8.*/ + { 14, 15, 14, 5, 4, 8, ZSTD_btlazy2 }, /* level 9.*/ + { 14, 15, 14, 9, 4, 8, ZSTD_btlazy2 }, /* level 10.*/ + { 14, 15, 14, 3, 4, 12, ZSTD_btopt }, /* level 11.*/ + { 14, 15, 14, 4, 3, 24, ZSTD_btopt }, /* level 12.*/ + { 14, 15, 14, 5, 3, 32, ZSTD_btultra }, /* level 13.*/ + { 14, 15, 15, 6, 3, 64, ZSTD_btultra }, /* level 14.*/ + { 14, 15, 15, 7, 3,256, ZSTD_btultra }, /* level 15.*/ + { 14, 15, 15, 5, 3, 48, ZSTD_btultra2}, /* level 16.*/ + { 14, 15, 15, 6, 3,128, ZSTD_btultra2}, /* level 17.*/ + { 14, 15, 15, 7, 3,256, ZSTD_btultra2}, /* level 18.*/ + { 14, 15, 15, 8, 3,256, ZSTD_btultra2}, /* level 19.*/ + { 14, 15, 15, 8, 3,512, ZSTD_btultra2}, /* level 20.*/ + { 14, 15, 15, 9, 3,512, ZSTD_btultra2}, /* level 21.*/ + { 14, 15, 15, 10, 3,999, ZSTD_btultra2}, /* level 22.*/ +}, +}; + + + +#endif /* ZSTD_CLEVELS_H */ +/**** ended inlining clevels.h ****/ + +int ZSTD_maxCLevel(void) { return ZSTD_MAX_CLEVEL; } +int ZSTD_minCLevel(void) { return (int)-ZSTD_TARGETLENGTH_MAX; } +int ZSTD_defaultCLevel(void) { return ZSTD_CLEVEL_DEFAULT; } + +static ZSTD_compressionParameters ZSTD_dedicatedDictSearch_getCParams(int const compressionLevel, size_t const dictSize) +{ + ZSTD_compressionParameters cParams = ZSTD_getCParams_internal(compressionLevel, 0, dictSize, ZSTD_cpm_createCDict); + switch (cParams.strategy) { + case ZSTD_fast: + case ZSTD_dfast: + break; + case ZSTD_greedy: + case ZSTD_lazy: + case ZSTD_lazy2: + cParams.hashLog += ZSTD_LAZY_DDSS_BUCKET_LOG; + break; + case ZSTD_btlazy2: + case ZSTD_btopt: + case ZSTD_btultra: + case ZSTD_btultra2: + break; + } + return cParams; +} + +static int ZSTD_dedicatedDictSearch_isSupported( + ZSTD_compressionParameters const* cParams) +{ + return (cParams->strategy >= ZSTD_greedy) + && (cParams->strategy <= ZSTD_lazy2) + && (cParams->hashLog > cParams->chainLog) + && (cParams->chainLog <= 24); +} + +/** + * Reverses the adjustment applied to cparams when enabling dedicated dict + * search. This is used to recover the params set to be used in the working + * context. (Otherwise, those tables would also grow.) + */ +static void ZSTD_dedicatedDictSearch_revertCParams( + ZSTD_compressionParameters* cParams) { + switch (cParams->strategy) { + case ZSTD_fast: + case ZSTD_dfast: + break; + case ZSTD_greedy: + case ZSTD_lazy: + case ZSTD_lazy2: + cParams->hashLog -= ZSTD_LAZY_DDSS_BUCKET_LOG; + if (cParams->hashLog < ZSTD_HASHLOG_MIN) { + cParams->hashLog = ZSTD_HASHLOG_MIN; + } + break; + case ZSTD_btlazy2: + case ZSTD_btopt: + case ZSTD_btultra: + case ZSTD_btultra2: + break; + } +} + +static U64 ZSTD_getCParamRowSize(U64 srcSizeHint, size_t dictSize, ZSTD_cParamMode_e mode) +{ + switch (mode) { + case ZSTD_cpm_unknown: + case ZSTD_cpm_noAttachDict: + case ZSTD_cpm_createCDict: + break; + case ZSTD_cpm_attachDict: + dictSize = 0; + break; + default: + assert(0); + break; + } + { int const unknown = srcSizeHint == ZSTD_CONTENTSIZE_UNKNOWN; + size_t const addedSize = unknown && dictSize > 0 ? 500 : 0; + return unknown && dictSize == 0 ? ZSTD_CONTENTSIZE_UNKNOWN : srcSizeHint+dictSize+addedSize; + } +} + +/*! ZSTD_getCParams_internal() : + * @return ZSTD_compressionParameters structure for a selected compression level, srcSize and dictSize. + * Note: srcSizeHint 0 means 0, use ZSTD_CONTENTSIZE_UNKNOWN for unknown. + * Use dictSize == 0 for unknown or unused. + * Note: `mode` controls how we treat the `dictSize`. See docs for `ZSTD_cParamMode_e`. */ +static ZSTD_compressionParameters ZSTD_getCParams_internal(int compressionLevel, unsigned long long srcSizeHint, size_t dictSize, ZSTD_cParamMode_e mode) +{ + U64 const rSize = ZSTD_getCParamRowSize(srcSizeHint, dictSize, mode); + U32 const tableID = (rSize <= 256 KB) + (rSize <= 128 KB) + (rSize <= 16 KB); + int row; + DEBUGLOG(5, "ZSTD_getCParams_internal (cLevel=%i)", compressionLevel); + + /* row */ + if (compressionLevel == 0) row = ZSTD_CLEVEL_DEFAULT; /* 0 == default */ + else if (compressionLevel < 0) row = 0; /* entry 0 is baseline for fast mode */ + else if (compressionLevel > ZSTD_MAX_CLEVEL) row = ZSTD_MAX_CLEVEL; + else row = compressionLevel; + + { ZSTD_compressionParameters cp = ZSTD_defaultCParameters[tableID][row]; + DEBUGLOG(5, "ZSTD_getCParams_internal selected tableID: %u row: %u strat: %u", tableID, row, (U32)cp.strategy); + /* acceleration factor */ + if (compressionLevel < 0) { + int const clampedCompressionLevel = MAX(ZSTD_minCLevel(), compressionLevel); + cp.targetLength = (unsigned)(-clampedCompressionLevel); + } + /* refine parameters based on srcSize & dictSize */ + return ZSTD_adjustCParams_internal(cp, srcSizeHint, dictSize, mode, ZSTD_ps_auto); + } +} + +/*! ZSTD_getCParams() : + * @return ZSTD_compressionParameters structure for a selected compression level, srcSize and dictSize. + * Size values are optional, provide 0 if not known or unused */ +ZSTD_compressionParameters ZSTD_getCParams(int compressionLevel, unsigned long long srcSizeHint, size_t dictSize) +{ + if (srcSizeHint == 0) srcSizeHint = ZSTD_CONTENTSIZE_UNKNOWN; + return ZSTD_getCParams_internal(compressionLevel, srcSizeHint, dictSize, ZSTD_cpm_unknown); +} + +/*! ZSTD_getParams() : + * same idea as ZSTD_getCParams() + * @return a `ZSTD_parameters` structure (instead of `ZSTD_compressionParameters`). + * Fields of `ZSTD_frameParameters` are set to default values */ +static ZSTD_parameters ZSTD_getParams_internal(int compressionLevel, unsigned long long srcSizeHint, size_t dictSize, ZSTD_cParamMode_e mode) { + ZSTD_parameters params; + ZSTD_compressionParameters const cParams = ZSTD_getCParams_internal(compressionLevel, srcSizeHint, dictSize, mode); + DEBUGLOG(5, "ZSTD_getParams (cLevel=%i)", compressionLevel); + ZSTD_memset(¶ms, 0, sizeof(params)); + params.cParams = cParams; + params.fParams.contentSizeFlag = 1; + return params; +} + +/*! ZSTD_getParams() : + * same idea as ZSTD_getCParams() + * @return a `ZSTD_parameters` structure (instead of `ZSTD_compressionParameters`). + * Fields of `ZSTD_frameParameters` are set to default values */ +ZSTD_parameters ZSTD_getParams(int compressionLevel, unsigned long long srcSizeHint, size_t dictSize) { + if (srcSizeHint == 0) srcSizeHint = ZSTD_CONTENTSIZE_UNKNOWN; + return ZSTD_getParams_internal(compressionLevel, srcSizeHint, dictSize, ZSTD_cpm_unknown); +} + +void ZSTD_registerSequenceProducer( + ZSTD_CCtx* zc, + void* extSeqProdState, + ZSTD_sequenceProducer_F extSeqProdFunc +) { + assert(zc != NULL); + ZSTD_CCtxParams_registerSequenceProducer( + &zc->requestedParams, extSeqProdState, extSeqProdFunc + ); +} + +void ZSTD_CCtxParams_registerSequenceProducer( + ZSTD_CCtx_params* params, + void* extSeqProdState, + ZSTD_sequenceProducer_F extSeqProdFunc +) { + assert(params != NULL); + if (extSeqProdFunc != NULL) { + params->extSeqProdFunc = extSeqProdFunc; + params->extSeqProdState = extSeqProdState; + } else { + params->extSeqProdFunc = NULL; + params->extSeqProdState = NULL; + } +} +/**** ended inlining compress/zstd_compress.c ****/ +/**** start inlining compress/zstd_double_fast.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/**** skipping file: zstd_compress_internal.h ****/ +/**** skipping file: zstd_double_fast.h ****/ + +#ifndef ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_fillDoubleHashTableForCDict(ZSTD_matchState_t* ms, + void const* end, ZSTD_dictTableLoadMethod_e dtlm) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashLarge = ms->hashTable; + U32 const hBitsL = cParams->hashLog + ZSTD_SHORT_CACHE_TAG_BITS; + U32 const mls = cParams->minMatch; + U32* const hashSmall = ms->chainTable; + U32 const hBitsS = cParams->chainLog + ZSTD_SHORT_CACHE_TAG_BITS; + const BYTE* const base = ms->window.base; + const BYTE* ip = base + ms->nextToUpdate; + const BYTE* const iend = ((const BYTE*)end) - HASH_READ_SIZE; + const U32 fastHashFillStep = 3; + + /* Always insert every fastHashFillStep position into the hash tables. + * Insert the other positions into the large hash table if their entry + * is empty. + */ + for (; ip + fastHashFillStep - 1 <= iend; ip += fastHashFillStep) { + U32 const curr = (U32)(ip - base); + U32 i; + for (i = 0; i < fastHashFillStep; ++i) { + size_t const smHashAndTag = ZSTD_hashPtr(ip + i, hBitsS, mls); + size_t const lgHashAndTag = ZSTD_hashPtr(ip + i, hBitsL, 8); + if (i == 0) { + ZSTD_writeTaggedIndex(hashSmall, smHashAndTag, curr + i); + } + if (i == 0 || hashLarge[lgHashAndTag >> ZSTD_SHORT_CACHE_TAG_BITS] == 0) { + ZSTD_writeTaggedIndex(hashLarge, lgHashAndTag, curr + i); + } + /* Only load extra positions for ZSTD_dtlm_full */ + if (dtlm == ZSTD_dtlm_fast) + break; + } } +} + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_fillDoubleHashTableForCCtx(ZSTD_matchState_t* ms, + void const* end, ZSTD_dictTableLoadMethod_e dtlm) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashLarge = ms->hashTable; + U32 const hBitsL = cParams->hashLog; + U32 const mls = cParams->minMatch; + U32* const hashSmall = ms->chainTable; + U32 const hBitsS = cParams->chainLog; + const BYTE* const base = ms->window.base; + const BYTE* ip = base + ms->nextToUpdate; + const BYTE* const iend = ((const BYTE*)end) - HASH_READ_SIZE; + const U32 fastHashFillStep = 3; + + /* Always insert every fastHashFillStep position into the hash tables. + * Insert the other positions into the large hash table if their entry + * is empty. + */ + for (; ip + fastHashFillStep - 1 <= iend; ip += fastHashFillStep) { + U32 const curr = (U32)(ip - base); + U32 i; + for (i = 0; i < fastHashFillStep; ++i) { + size_t const smHash = ZSTD_hashPtr(ip + i, hBitsS, mls); + size_t const lgHash = ZSTD_hashPtr(ip + i, hBitsL, 8); + if (i == 0) + hashSmall[smHash] = curr + i; + if (i == 0 || hashLarge[lgHash] == 0) + hashLarge[lgHash] = curr + i; + /* Only load extra positions for ZSTD_dtlm_full */ + if (dtlm == ZSTD_dtlm_fast) + break; + } } +} + +void ZSTD_fillDoubleHashTable(ZSTD_matchState_t* ms, + const void* const end, + ZSTD_dictTableLoadMethod_e dtlm, + ZSTD_tableFillPurpose_e tfp) +{ + if (tfp == ZSTD_tfp_forCDict) { + ZSTD_fillDoubleHashTableForCDict(ms, end, dtlm); + } else { + ZSTD_fillDoubleHashTableForCCtx(ms, end, dtlm); + } +} + + +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_compressBlock_doubleFast_noDict_generic( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize, U32 const mls /* template */) +{ + ZSTD_compressionParameters const* cParams = &ms->cParams; + U32* const hashLong = ms->hashTable; + const U32 hBitsL = cParams->hashLog; + U32* const hashSmall = ms->chainTable; + const U32 hBitsS = cParams->chainLog; + const BYTE* const base = ms->window.base; + const BYTE* const istart = (const BYTE*)src; + const BYTE* anchor = istart; + const U32 endIndex = (U32)((size_t)(istart - base) + srcSize); + /* presumes that, if there is a dictionary, it must be using Attach mode */ + const U32 prefixLowestIndex = ZSTD_getLowestPrefixIndex(ms, endIndex, cParams->windowLog); + const BYTE* const prefixLowest = base + prefixLowestIndex; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = iend - HASH_READ_SIZE; + U32 offset_1=rep[0], offset_2=rep[1]; + U32 offsetSaved1 = 0, offsetSaved2 = 0; + + size_t mLength; + U32 offset; + U32 curr; + + /* how many positions to search before increasing step size */ + const size_t kStepIncr = 1 << kSearchStrength; + /* the position at which to increment the step size if no match is found */ + const BYTE* nextStep; + size_t step; /* the current step size */ + + size_t hl0; /* the long hash at ip */ + size_t hl1; /* the long hash at ip1 */ + + U32 idxl0; /* the long match index for ip */ + U32 idxl1; /* the long match index for ip1 */ + + const BYTE* matchl0; /* the long match for ip */ + const BYTE* matchs0; /* the short match for ip */ + const BYTE* matchl1; /* the long match for ip1 */ + + const BYTE* ip = istart; /* the current position */ + const BYTE* ip1; /* the next position */ + + DEBUGLOG(5, "ZSTD_compressBlock_doubleFast_noDict_generic"); + + /* init */ + ip += ((ip - prefixLowest) == 0); + { + U32 const current = (U32)(ip - base); + U32 const windowLow = ZSTD_getLowestPrefixIndex(ms, current, cParams->windowLog); + U32 const maxRep = current - windowLow; + if (offset_2 > maxRep) offsetSaved2 = offset_2, offset_2 = 0; + if (offset_1 > maxRep) offsetSaved1 = offset_1, offset_1 = 0; + } + + /* Outer Loop: one iteration per match found and stored */ + while (1) { + step = 1; + nextStep = ip + kStepIncr; + ip1 = ip + step; + + if (ip1 > ilimit) { + goto _cleanup; + } + + hl0 = ZSTD_hashPtr(ip, hBitsL, 8); + idxl0 = hashLong[hl0]; + matchl0 = base + idxl0; + + /* Inner Loop: one iteration per search / position */ + do { + const size_t hs0 = ZSTD_hashPtr(ip, hBitsS, mls); + const U32 idxs0 = hashSmall[hs0]; + curr = (U32)(ip-base); + matchs0 = base + idxs0; + + hashLong[hl0] = hashSmall[hs0] = curr; /* update hash tables */ + + /* check noDict repcode */ + if ((offset_1 > 0) & (MEM_read32(ip+1-offset_1) == MEM_read32(ip+1))) { + mLength = ZSTD_count(ip+1+4, ip+1+4-offset_1, iend) + 4; + ip++; + ZSTD_storeSeq(seqStore, (size_t)(ip-anchor), anchor, iend, REPCODE1_TO_OFFBASE, mLength); + goto _match_stored; + } + + hl1 = ZSTD_hashPtr(ip1, hBitsL, 8); + + if (idxl0 > prefixLowestIndex) { + /* check prefix long match */ + if (MEM_read64(matchl0) == MEM_read64(ip)) { + mLength = ZSTD_count(ip+8, matchl0+8, iend) + 8; + offset = (U32)(ip-matchl0); + while (((ip>anchor) & (matchl0>prefixLowest)) && (ip[-1] == matchl0[-1])) { ip--; matchl0--; mLength++; } /* catch up */ + goto _match_found; + } + } + + idxl1 = hashLong[hl1]; + matchl1 = base + idxl1; + + if (idxs0 > prefixLowestIndex) { + /* check prefix short match */ + if (MEM_read32(matchs0) == MEM_read32(ip)) { + goto _search_next_long; + } + } + + if (ip1 >= nextStep) { + PREFETCH_L1(ip1 + 64); + PREFETCH_L1(ip1 + 128); + step++; + nextStep += kStepIncr; + } + ip = ip1; + ip1 += step; + + hl0 = hl1; + idxl0 = idxl1; + matchl0 = matchl1; + #if defined(__aarch64__) + PREFETCH_L1(ip+256); + #endif + } while (ip1 <= ilimit); + +_cleanup: + /* If offset_1 started invalid (offsetSaved1 != 0) and became valid (offset_1 != 0), + * rotate saved offsets. See comment in ZSTD_compressBlock_fast_noDict for more context. */ + offsetSaved2 = ((offsetSaved1 != 0) && (offset_1 != 0)) ? offsetSaved1 : offsetSaved2; + + /* save reps for next block */ + rep[0] = offset_1 ? offset_1 : offsetSaved1; + rep[1] = offset_2 ? offset_2 : offsetSaved2; + + /* Return the last literals size */ + return (size_t)(iend - anchor); + +_search_next_long: + + /* check prefix long +1 match */ + if (idxl1 > prefixLowestIndex) { + if (MEM_read64(matchl1) == MEM_read64(ip1)) { + ip = ip1; + mLength = ZSTD_count(ip+8, matchl1+8, iend) + 8; + offset = (U32)(ip-matchl1); + while (((ip>anchor) & (matchl1>prefixLowest)) && (ip[-1] == matchl1[-1])) { ip--; matchl1--; mLength++; } /* catch up */ + goto _match_found; + } + } + + /* if no long +1 match, explore the short match we found */ + mLength = ZSTD_count(ip+4, matchs0+4, iend) + 4; + offset = (U32)(ip - matchs0); + while (((ip>anchor) & (matchs0>prefixLowest)) && (ip[-1] == matchs0[-1])) { ip--; matchs0--; mLength++; } /* catch up */ + + /* fall-through */ + +_match_found: /* requires ip, offset, mLength */ + offset_2 = offset_1; + offset_1 = offset; + + if (step < 4) { + /* It is unsafe to write this value back to the hashtable when ip1 is + * greater than or equal to the new ip we will have after we're done + * processing this match. Rather than perform that test directly + * (ip1 >= ip + mLength), which costs speed in practice, we do a simpler + * more predictable test. The minmatch even if we take a short match is + * 4 bytes, so as long as step, the distance between ip and ip1 + * (initially) is less than 4, we know ip1 < new ip. */ + hashLong[hl1] = (U32)(ip1 - base); + } + + ZSTD_storeSeq(seqStore, (size_t)(ip-anchor), anchor, iend, OFFSET_TO_OFFBASE(offset), mLength); + +_match_stored: + /* match found */ + ip += mLength; + anchor = ip; + + if (ip <= ilimit) { + /* Complementary insertion */ + /* done after iLimit test, as candidates could be > iend-8 */ + { U32 const indexToInsert = curr+2; + hashLong[ZSTD_hashPtr(base+indexToInsert, hBitsL, 8)] = indexToInsert; + hashLong[ZSTD_hashPtr(ip-2, hBitsL, 8)] = (U32)(ip-2-base); + hashSmall[ZSTD_hashPtr(base+indexToInsert, hBitsS, mls)] = indexToInsert; + hashSmall[ZSTD_hashPtr(ip-1, hBitsS, mls)] = (U32)(ip-1-base); + } + + /* check immediate repcode */ + while ( (ip <= ilimit) + && ( (offset_2>0) + & (MEM_read32(ip) == MEM_read32(ip - offset_2)) )) { + /* store sequence */ + size_t const rLength = ZSTD_count(ip+4, ip+4-offset_2, iend) + 4; + U32 const tmpOff = offset_2; offset_2 = offset_1; offset_1 = tmpOff; /* swap offset_2 <=> offset_1 */ + hashSmall[ZSTD_hashPtr(ip, hBitsS, mls)] = (U32)(ip-base); + hashLong[ZSTD_hashPtr(ip, hBitsL, 8)] = (U32)(ip-base); + ZSTD_storeSeq(seqStore, 0, anchor, iend, REPCODE1_TO_OFFBASE, rLength); + ip += rLength; + anchor = ip; + continue; /* faster when present ... (?) */ + } + } + } +} + + +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_compressBlock_doubleFast_dictMatchState_generic( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize, + U32 const mls /* template */) +{ + ZSTD_compressionParameters const* cParams = &ms->cParams; + U32* const hashLong = ms->hashTable; + const U32 hBitsL = cParams->hashLog; + U32* const hashSmall = ms->chainTable; + const U32 hBitsS = cParams->chainLog; + const BYTE* const base = ms->window.base; + const BYTE* const istart = (const BYTE*)src; + const BYTE* ip = istart; + const BYTE* anchor = istart; + const U32 endIndex = (U32)((size_t)(istart - base) + srcSize); + /* presumes that, if there is a dictionary, it must be using Attach mode */ + const U32 prefixLowestIndex = ZSTD_getLowestPrefixIndex(ms, endIndex, cParams->windowLog); + const BYTE* const prefixLowest = base + prefixLowestIndex; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = iend - HASH_READ_SIZE; + U32 offset_1=rep[0], offset_2=rep[1]; + + const ZSTD_matchState_t* const dms = ms->dictMatchState; + const ZSTD_compressionParameters* const dictCParams = &dms->cParams; + const U32* const dictHashLong = dms->hashTable; + const U32* const dictHashSmall = dms->chainTable; + const U32 dictStartIndex = dms->window.dictLimit; + const BYTE* const dictBase = dms->window.base; + const BYTE* const dictStart = dictBase + dictStartIndex; + const BYTE* const dictEnd = dms->window.nextSrc; + const U32 dictIndexDelta = prefixLowestIndex - (U32)(dictEnd - dictBase); + const U32 dictHBitsL = dictCParams->hashLog + ZSTD_SHORT_CACHE_TAG_BITS; + const U32 dictHBitsS = dictCParams->chainLog + ZSTD_SHORT_CACHE_TAG_BITS; + const U32 dictAndPrefixLength = (U32)((ip - prefixLowest) + (dictEnd - dictStart)); + + DEBUGLOG(5, "ZSTD_compressBlock_doubleFast_dictMatchState_generic"); + + /* if a dictionary is attached, it must be within window range */ + assert(ms->window.dictLimit + (1U << cParams->windowLog) >= endIndex); + + if (ms->prefetchCDictTables) { + size_t const hashTableBytes = (((size_t)1) << dictCParams->hashLog) * sizeof(U32); + size_t const chainTableBytes = (((size_t)1) << dictCParams->chainLog) * sizeof(U32); + PREFETCH_AREA(dictHashLong, hashTableBytes); + PREFETCH_AREA(dictHashSmall, chainTableBytes); + } + + /* init */ + ip += (dictAndPrefixLength == 0); + + /* dictMatchState repCode checks don't currently handle repCode == 0 + * disabling. */ + assert(offset_1 <= dictAndPrefixLength); + assert(offset_2 <= dictAndPrefixLength); + + /* Main Search Loop */ + while (ip < ilimit) { /* < instead of <=, because repcode check at (ip+1) */ + size_t mLength; + U32 offset; + size_t const h2 = ZSTD_hashPtr(ip, hBitsL, 8); + size_t const h = ZSTD_hashPtr(ip, hBitsS, mls); + size_t const dictHashAndTagL = ZSTD_hashPtr(ip, dictHBitsL, 8); + size_t const dictHashAndTagS = ZSTD_hashPtr(ip, dictHBitsS, mls); + U32 const dictMatchIndexAndTagL = dictHashLong[dictHashAndTagL >> ZSTD_SHORT_CACHE_TAG_BITS]; + U32 const dictMatchIndexAndTagS = dictHashSmall[dictHashAndTagS >> ZSTD_SHORT_CACHE_TAG_BITS]; + int const dictTagsMatchL = ZSTD_comparePackedTags(dictMatchIndexAndTagL, dictHashAndTagL); + int const dictTagsMatchS = ZSTD_comparePackedTags(dictMatchIndexAndTagS, dictHashAndTagS); + U32 const curr = (U32)(ip-base); + U32 const matchIndexL = hashLong[h2]; + U32 matchIndexS = hashSmall[h]; + const BYTE* matchLong = base + matchIndexL; + const BYTE* match = base + matchIndexS; + const U32 repIndex = curr + 1 - offset_1; + const BYTE* repMatch = (repIndex < prefixLowestIndex) ? + dictBase + (repIndex - dictIndexDelta) : + base + repIndex; + hashLong[h2] = hashSmall[h] = curr; /* update hash tables */ + + /* check repcode */ + if (((U32)((prefixLowestIndex-1) - repIndex) >= 3 /* intentional underflow */) + && (MEM_read32(repMatch) == MEM_read32(ip+1)) ) { + const BYTE* repMatchEnd = repIndex < prefixLowestIndex ? dictEnd : iend; + mLength = ZSTD_count_2segments(ip+1+4, repMatch+4, iend, repMatchEnd, prefixLowest) + 4; + ip++; + ZSTD_storeSeq(seqStore, (size_t)(ip-anchor), anchor, iend, REPCODE1_TO_OFFBASE, mLength); + goto _match_stored; + } + + if (matchIndexL > prefixLowestIndex) { + /* check prefix long match */ + if (MEM_read64(matchLong) == MEM_read64(ip)) { + mLength = ZSTD_count(ip+8, matchLong+8, iend) + 8; + offset = (U32)(ip-matchLong); + while (((ip>anchor) & (matchLong>prefixLowest)) && (ip[-1] == matchLong[-1])) { ip--; matchLong--; mLength++; } /* catch up */ + goto _match_found; + } + } else if (dictTagsMatchL) { + /* check dictMatchState long match */ + U32 const dictMatchIndexL = dictMatchIndexAndTagL >> ZSTD_SHORT_CACHE_TAG_BITS; + const BYTE* dictMatchL = dictBase + dictMatchIndexL; + assert(dictMatchL < dictEnd); + + if (dictMatchL > dictStart && MEM_read64(dictMatchL) == MEM_read64(ip)) { + mLength = ZSTD_count_2segments(ip+8, dictMatchL+8, iend, dictEnd, prefixLowest) + 8; + offset = (U32)(curr - dictMatchIndexL - dictIndexDelta); + while (((ip>anchor) & (dictMatchL>dictStart)) && (ip[-1] == dictMatchL[-1])) { ip--; dictMatchL--; mLength++; } /* catch up */ + goto _match_found; + } } + + if (matchIndexS > prefixLowestIndex) { + /* check prefix short match */ + if (MEM_read32(match) == MEM_read32(ip)) { + goto _search_next_long; + } + } else if (dictTagsMatchS) { + /* check dictMatchState short match */ + U32 const dictMatchIndexS = dictMatchIndexAndTagS >> ZSTD_SHORT_CACHE_TAG_BITS; + match = dictBase + dictMatchIndexS; + matchIndexS = dictMatchIndexS + dictIndexDelta; + + if (match > dictStart && MEM_read32(match) == MEM_read32(ip)) { + goto _search_next_long; + } } + + ip += ((ip-anchor) >> kSearchStrength) + 1; +#if defined(__aarch64__) + PREFETCH_L1(ip+256); +#endif + continue; + +_search_next_long: + { size_t const hl3 = ZSTD_hashPtr(ip+1, hBitsL, 8); + size_t const dictHashAndTagL3 = ZSTD_hashPtr(ip+1, dictHBitsL, 8); + U32 const matchIndexL3 = hashLong[hl3]; + U32 const dictMatchIndexAndTagL3 = dictHashLong[dictHashAndTagL3 >> ZSTD_SHORT_CACHE_TAG_BITS]; + int const dictTagsMatchL3 = ZSTD_comparePackedTags(dictMatchIndexAndTagL3, dictHashAndTagL3); + const BYTE* matchL3 = base + matchIndexL3; + hashLong[hl3] = curr + 1; + + /* check prefix long +1 match */ + if (matchIndexL3 > prefixLowestIndex) { + if (MEM_read64(matchL3) == MEM_read64(ip+1)) { + mLength = ZSTD_count(ip+9, matchL3+8, iend) + 8; + ip++; + offset = (U32)(ip-matchL3); + while (((ip>anchor) & (matchL3>prefixLowest)) && (ip[-1] == matchL3[-1])) { ip--; matchL3--; mLength++; } /* catch up */ + goto _match_found; + } + } else if (dictTagsMatchL3) { + /* check dict long +1 match */ + U32 const dictMatchIndexL3 = dictMatchIndexAndTagL3 >> ZSTD_SHORT_CACHE_TAG_BITS; + const BYTE* dictMatchL3 = dictBase + dictMatchIndexL3; + assert(dictMatchL3 < dictEnd); + if (dictMatchL3 > dictStart && MEM_read64(dictMatchL3) == MEM_read64(ip+1)) { + mLength = ZSTD_count_2segments(ip+1+8, dictMatchL3+8, iend, dictEnd, prefixLowest) + 8; + ip++; + offset = (U32)(curr + 1 - dictMatchIndexL3 - dictIndexDelta); + while (((ip>anchor) & (dictMatchL3>dictStart)) && (ip[-1] == dictMatchL3[-1])) { ip--; dictMatchL3--; mLength++; } /* catch up */ + goto _match_found; + } } } + + /* if no long +1 match, explore the short match we found */ + if (matchIndexS < prefixLowestIndex) { + mLength = ZSTD_count_2segments(ip+4, match+4, iend, dictEnd, prefixLowest) + 4; + offset = (U32)(curr - matchIndexS); + while (((ip>anchor) & (match>dictStart)) && (ip[-1] == match[-1])) { ip--; match--; mLength++; } /* catch up */ + } else { + mLength = ZSTD_count(ip+4, match+4, iend) + 4; + offset = (U32)(ip - match); + while (((ip>anchor) & (match>prefixLowest)) && (ip[-1] == match[-1])) { ip--; match--; mLength++; } /* catch up */ + } + +_match_found: + offset_2 = offset_1; + offset_1 = offset; + + ZSTD_storeSeq(seqStore, (size_t)(ip-anchor), anchor, iend, OFFSET_TO_OFFBASE(offset), mLength); + +_match_stored: + /* match found */ + ip += mLength; + anchor = ip; + + if (ip <= ilimit) { + /* Complementary insertion */ + /* done after iLimit test, as candidates could be > iend-8 */ + { U32 const indexToInsert = curr+2; + hashLong[ZSTD_hashPtr(base+indexToInsert, hBitsL, 8)] = indexToInsert; + hashLong[ZSTD_hashPtr(ip-2, hBitsL, 8)] = (U32)(ip-2-base); + hashSmall[ZSTD_hashPtr(base+indexToInsert, hBitsS, mls)] = indexToInsert; + hashSmall[ZSTD_hashPtr(ip-1, hBitsS, mls)] = (U32)(ip-1-base); + } + + /* check immediate repcode */ + while (ip <= ilimit) { + U32 const current2 = (U32)(ip-base); + U32 const repIndex2 = current2 - offset_2; + const BYTE* repMatch2 = repIndex2 < prefixLowestIndex ? + dictBase + repIndex2 - dictIndexDelta : + base + repIndex2; + if ( ((U32)((prefixLowestIndex-1) - (U32)repIndex2) >= 3 /* intentional overflow */) + && (MEM_read32(repMatch2) == MEM_read32(ip)) ) { + const BYTE* const repEnd2 = repIndex2 < prefixLowestIndex ? dictEnd : iend; + size_t const repLength2 = ZSTD_count_2segments(ip+4, repMatch2+4, iend, repEnd2, prefixLowest) + 4; + U32 tmpOffset = offset_2; offset_2 = offset_1; offset_1 = tmpOffset; /* swap offset_2 <=> offset_1 */ + ZSTD_storeSeq(seqStore, 0, anchor, iend, REPCODE1_TO_OFFBASE, repLength2); + hashSmall[ZSTD_hashPtr(ip, hBitsS, mls)] = current2; + hashLong[ZSTD_hashPtr(ip, hBitsL, 8)] = current2; + ip += repLength2; + anchor = ip; + continue; + } + break; + } + } + } /* while (ip < ilimit) */ + + /* save reps for next block */ + rep[0] = offset_1; + rep[1] = offset_2; + + /* Return the last literals size */ + return (size_t)(iend - anchor); +} + +#define ZSTD_GEN_DFAST_FN(dictMode, mls) \ + static size_t ZSTD_compressBlock_doubleFast_##dictMode##_##mls( \ + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], \ + void const* src, size_t srcSize) \ + { \ + return ZSTD_compressBlock_doubleFast_##dictMode##_generic(ms, seqStore, rep, src, srcSize, mls); \ + } + +ZSTD_GEN_DFAST_FN(noDict, 4) +ZSTD_GEN_DFAST_FN(noDict, 5) +ZSTD_GEN_DFAST_FN(noDict, 6) +ZSTD_GEN_DFAST_FN(noDict, 7) + +ZSTD_GEN_DFAST_FN(dictMatchState, 4) +ZSTD_GEN_DFAST_FN(dictMatchState, 5) +ZSTD_GEN_DFAST_FN(dictMatchState, 6) +ZSTD_GEN_DFAST_FN(dictMatchState, 7) + + +size_t ZSTD_compressBlock_doubleFast( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + const U32 mls = ms->cParams.minMatch; + switch(mls) + { + default: /* includes case 3 */ + case 4 : + return ZSTD_compressBlock_doubleFast_noDict_4(ms, seqStore, rep, src, srcSize); + case 5 : + return ZSTD_compressBlock_doubleFast_noDict_5(ms, seqStore, rep, src, srcSize); + case 6 : + return ZSTD_compressBlock_doubleFast_noDict_6(ms, seqStore, rep, src, srcSize); + case 7 : + return ZSTD_compressBlock_doubleFast_noDict_7(ms, seqStore, rep, src, srcSize); + } +} + + +size_t ZSTD_compressBlock_doubleFast_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + const U32 mls = ms->cParams.minMatch; + switch(mls) + { + default: /* includes case 3 */ + case 4 : + return ZSTD_compressBlock_doubleFast_dictMatchState_4(ms, seqStore, rep, src, srcSize); + case 5 : + return ZSTD_compressBlock_doubleFast_dictMatchState_5(ms, seqStore, rep, src, srcSize); + case 6 : + return ZSTD_compressBlock_doubleFast_dictMatchState_6(ms, seqStore, rep, src, srcSize); + case 7 : + return ZSTD_compressBlock_doubleFast_dictMatchState_7(ms, seqStore, rep, src, srcSize); + } +} + + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_compressBlock_doubleFast_extDict_generic( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize, + U32 const mls /* template */) +{ + ZSTD_compressionParameters const* cParams = &ms->cParams; + U32* const hashLong = ms->hashTable; + U32 const hBitsL = cParams->hashLog; + U32* const hashSmall = ms->chainTable; + U32 const hBitsS = cParams->chainLog; + const BYTE* const istart = (const BYTE*)src; + const BYTE* ip = istart; + const BYTE* anchor = istart; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = iend - 8; + const BYTE* const base = ms->window.base; + const U32 endIndex = (U32)((size_t)(istart - base) + srcSize); + const U32 lowLimit = ZSTD_getLowestMatchIndex(ms, endIndex, cParams->windowLog); + const U32 dictStartIndex = lowLimit; + const U32 dictLimit = ms->window.dictLimit; + const U32 prefixStartIndex = (dictLimit > lowLimit) ? dictLimit : lowLimit; + const BYTE* const prefixStart = base + prefixStartIndex; + const BYTE* const dictBase = ms->window.dictBase; + const BYTE* const dictStart = dictBase + dictStartIndex; + const BYTE* const dictEnd = dictBase + prefixStartIndex; + U32 offset_1=rep[0], offset_2=rep[1]; + + DEBUGLOG(5, "ZSTD_compressBlock_doubleFast_extDict_generic (srcSize=%zu)", srcSize); + + /* if extDict is invalidated due to maxDistance, switch to "regular" variant */ + if (prefixStartIndex == dictStartIndex) + return ZSTD_compressBlock_doubleFast(ms, seqStore, rep, src, srcSize); + + /* Search Loop */ + while (ip < ilimit) { /* < instead of <=, because (ip+1) */ + const size_t hSmall = ZSTD_hashPtr(ip, hBitsS, mls); + const U32 matchIndex = hashSmall[hSmall]; + const BYTE* const matchBase = matchIndex < prefixStartIndex ? dictBase : base; + const BYTE* match = matchBase + matchIndex; + + const size_t hLong = ZSTD_hashPtr(ip, hBitsL, 8); + const U32 matchLongIndex = hashLong[hLong]; + const BYTE* const matchLongBase = matchLongIndex < prefixStartIndex ? dictBase : base; + const BYTE* matchLong = matchLongBase + matchLongIndex; + + const U32 curr = (U32)(ip-base); + const U32 repIndex = curr + 1 - offset_1; /* offset_1 expected <= curr +1 */ + const BYTE* const repBase = repIndex < prefixStartIndex ? dictBase : base; + const BYTE* const repMatch = repBase + repIndex; + size_t mLength; + hashSmall[hSmall] = hashLong[hLong] = curr; /* update hash table */ + + if ((((U32)((prefixStartIndex-1) - repIndex) >= 3) /* intentional underflow : ensure repIndex doesn't overlap dict + prefix */ + & (offset_1 <= curr+1 - dictStartIndex)) /* note: we are searching at curr+1 */ + && (MEM_read32(repMatch) == MEM_read32(ip+1)) ) { + const BYTE* repMatchEnd = repIndex < prefixStartIndex ? dictEnd : iend; + mLength = ZSTD_count_2segments(ip+1+4, repMatch+4, iend, repMatchEnd, prefixStart) + 4; + ip++; + ZSTD_storeSeq(seqStore, (size_t)(ip-anchor), anchor, iend, REPCODE1_TO_OFFBASE, mLength); + } else { + if ((matchLongIndex > dictStartIndex) && (MEM_read64(matchLong) == MEM_read64(ip))) { + const BYTE* const matchEnd = matchLongIndex < prefixStartIndex ? dictEnd : iend; + const BYTE* const lowMatchPtr = matchLongIndex < prefixStartIndex ? dictStart : prefixStart; + U32 offset; + mLength = ZSTD_count_2segments(ip+8, matchLong+8, iend, matchEnd, prefixStart) + 8; + offset = curr - matchLongIndex; + while (((ip>anchor) & (matchLong>lowMatchPtr)) && (ip[-1] == matchLong[-1])) { ip--; matchLong--; mLength++; } /* catch up */ + offset_2 = offset_1; + offset_1 = offset; + ZSTD_storeSeq(seqStore, (size_t)(ip-anchor), anchor, iend, OFFSET_TO_OFFBASE(offset), mLength); + + } else if ((matchIndex > dictStartIndex) && (MEM_read32(match) == MEM_read32(ip))) { + size_t const h3 = ZSTD_hashPtr(ip+1, hBitsL, 8); + U32 const matchIndex3 = hashLong[h3]; + const BYTE* const match3Base = matchIndex3 < prefixStartIndex ? dictBase : base; + const BYTE* match3 = match3Base + matchIndex3; + U32 offset; + hashLong[h3] = curr + 1; + if ( (matchIndex3 > dictStartIndex) && (MEM_read64(match3) == MEM_read64(ip+1)) ) { + const BYTE* const matchEnd = matchIndex3 < prefixStartIndex ? dictEnd : iend; + const BYTE* const lowMatchPtr = matchIndex3 < prefixStartIndex ? dictStart : prefixStart; + mLength = ZSTD_count_2segments(ip+9, match3+8, iend, matchEnd, prefixStart) + 8; + ip++; + offset = curr+1 - matchIndex3; + while (((ip>anchor) & (match3>lowMatchPtr)) && (ip[-1] == match3[-1])) { ip--; match3--; mLength++; } /* catch up */ + } else { + const BYTE* const matchEnd = matchIndex < prefixStartIndex ? dictEnd : iend; + const BYTE* const lowMatchPtr = matchIndex < prefixStartIndex ? dictStart : prefixStart; + mLength = ZSTD_count_2segments(ip+4, match+4, iend, matchEnd, prefixStart) + 4; + offset = curr - matchIndex; + while (((ip>anchor) & (match>lowMatchPtr)) && (ip[-1] == match[-1])) { ip--; match--; mLength++; } /* catch up */ + } + offset_2 = offset_1; + offset_1 = offset; + ZSTD_storeSeq(seqStore, (size_t)(ip-anchor), anchor, iend, OFFSET_TO_OFFBASE(offset), mLength); + + } else { + ip += ((ip-anchor) >> kSearchStrength) + 1; + continue; + } } + + /* move to next sequence start */ + ip += mLength; + anchor = ip; + + if (ip <= ilimit) { + /* Complementary insertion */ + /* done after iLimit test, as candidates could be > iend-8 */ + { U32 const indexToInsert = curr+2; + hashLong[ZSTD_hashPtr(base+indexToInsert, hBitsL, 8)] = indexToInsert; + hashLong[ZSTD_hashPtr(ip-2, hBitsL, 8)] = (U32)(ip-2-base); + hashSmall[ZSTD_hashPtr(base+indexToInsert, hBitsS, mls)] = indexToInsert; + hashSmall[ZSTD_hashPtr(ip-1, hBitsS, mls)] = (U32)(ip-1-base); + } + + /* check immediate repcode */ + while (ip <= ilimit) { + U32 const current2 = (U32)(ip-base); + U32 const repIndex2 = current2 - offset_2; + const BYTE* repMatch2 = repIndex2 < prefixStartIndex ? dictBase + repIndex2 : base + repIndex2; + if ( (((U32)((prefixStartIndex-1) - repIndex2) >= 3) /* intentional overflow : ensure repIndex2 doesn't overlap dict + prefix */ + & (offset_2 <= current2 - dictStartIndex)) + && (MEM_read32(repMatch2) == MEM_read32(ip)) ) { + const BYTE* const repEnd2 = repIndex2 < prefixStartIndex ? dictEnd : iend; + size_t const repLength2 = ZSTD_count_2segments(ip+4, repMatch2+4, iend, repEnd2, prefixStart) + 4; + U32 const tmpOffset = offset_2; offset_2 = offset_1; offset_1 = tmpOffset; /* swap offset_2 <=> offset_1 */ + ZSTD_storeSeq(seqStore, 0, anchor, iend, REPCODE1_TO_OFFBASE, repLength2); + hashSmall[ZSTD_hashPtr(ip, hBitsS, mls)] = current2; + hashLong[ZSTD_hashPtr(ip, hBitsL, 8)] = current2; + ip += repLength2; + anchor = ip; + continue; + } + break; + } } } + + /* save reps for next block */ + rep[0] = offset_1; + rep[1] = offset_2; + + /* Return the last literals size */ + return (size_t)(iend - anchor); +} + +ZSTD_GEN_DFAST_FN(extDict, 4) +ZSTD_GEN_DFAST_FN(extDict, 5) +ZSTD_GEN_DFAST_FN(extDict, 6) +ZSTD_GEN_DFAST_FN(extDict, 7) + +size_t ZSTD_compressBlock_doubleFast_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + U32 const mls = ms->cParams.minMatch; + switch(mls) + { + default: /* includes case 3 */ + case 4 : + return ZSTD_compressBlock_doubleFast_extDict_4(ms, seqStore, rep, src, srcSize); + case 5 : + return ZSTD_compressBlock_doubleFast_extDict_5(ms, seqStore, rep, src, srcSize); + case 6 : + return ZSTD_compressBlock_doubleFast_extDict_6(ms, seqStore, rep, src, srcSize); + case 7 : + return ZSTD_compressBlock_doubleFast_extDict_7(ms, seqStore, rep, src, srcSize); + } +} + +#endif /* ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR */ +/**** ended inlining compress/zstd_double_fast.c ****/ +/**** start inlining compress/zstd_fast.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/**** skipping file: zstd_compress_internal.h ****/ +/**** skipping file: zstd_fast.h ****/ + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_fillHashTableForCDict(ZSTD_matchState_t* ms, + const void* const end, + ZSTD_dictTableLoadMethod_e dtlm) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashTable = ms->hashTable; + U32 const hBits = cParams->hashLog + ZSTD_SHORT_CACHE_TAG_BITS; + U32 const mls = cParams->minMatch; + const BYTE* const base = ms->window.base; + const BYTE* ip = base + ms->nextToUpdate; + const BYTE* const iend = ((const BYTE*)end) - HASH_READ_SIZE; + const U32 fastHashFillStep = 3; + + /* Currently, we always use ZSTD_dtlm_full for filling CDict tables. + * Feel free to remove this assert if there's a good reason! */ + assert(dtlm == ZSTD_dtlm_full); + + /* Always insert every fastHashFillStep position into the hash table. + * Insert the other positions if their hash entry is empty. + */ + for ( ; ip + fastHashFillStep < iend + 2; ip += fastHashFillStep) { + U32 const curr = (U32)(ip - base); + { size_t const hashAndTag = ZSTD_hashPtr(ip, hBits, mls); + ZSTD_writeTaggedIndex(hashTable, hashAndTag, curr); } + + if (dtlm == ZSTD_dtlm_fast) continue; + /* Only load extra positions for ZSTD_dtlm_full */ + { U32 p; + for (p = 1; p < fastHashFillStep; ++p) { + size_t const hashAndTag = ZSTD_hashPtr(ip + p, hBits, mls); + if (hashTable[hashAndTag >> ZSTD_SHORT_CACHE_TAG_BITS] == 0) { /* not yet filled */ + ZSTD_writeTaggedIndex(hashTable, hashAndTag, curr + p); + } } } } +} + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_fillHashTableForCCtx(ZSTD_matchState_t* ms, + const void* const end, + ZSTD_dictTableLoadMethod_e dtlm) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashTable = ms->hashTable; + U32 const hBits = cParams->hashLog; + U32 const mls = cParams->minMatch; + const BYTE* const base = ms->window.base; + const BYTE* ip = base + ms->nextToUpdate; + const BYTE* const iend = ((const BYTE*)end) - HASH_READ_SIZE; + const U32 fastHashFillStep = 3; + + /* Currently, we always use ZSTD_dtlm_fast for filling CCtx tables. + * Feel free to remove this assert if there's a good reason! */ + assert(dtlm == ZSTD_dtlm_fast); + + /* Always insert every fastHashFillStep position into the hash table. + * Insert the other positions if their hash entry is empty. + */ + for ( ; ip + fastHashFillStep < iend + 2; ip += fastHashFillStep) { + U32 const curr = (U32)(ip - base); + size_t const hash0 = ZSTD_hashPtr(ip, hBits, mls); + hashTable[hash0] = curr; + if (dtlm == ZSTD_dtlm_fast) continue; + /* Only load extra positions for ZSTD_dtlm_full */ + { U32 p; + for (p = 1; p < fastHashFillStep; ++p) { + size_t const hash = ZSTD_hashPtr(ip + p, hBits, mls); + if (hashTable[hash] == 0) { /* not yet filled */ + hashTable[hash] = curr + p; + } } } } +} + +void ZSTD_fillHashTable(ZSTD_matchState_t* ms, + const void* const end, + ZSTD_dictTableLoadMethod_e dtlm, + ZSTD_tableFillPurpose_e tfp) +{ + if (tfp == ZSTD_tfp_forCDict) { + ZSTD_fillHashTableForCDict(ms, end, dtlm); + } else { + ZSTD_fillHashTableForCCtx(ms, end, dtlm); + } +} + + +/** + * If you squint hard enough (and ignore repcodes), the search operation at any + * given position is broken into 4 stages: + * + * 1. Hash (map position to hash value via input read) + * 2. Lookup (map hash val to index via hashtable read) + * 3. Load (map index to value at that position via input read) + * 4. Compare + * + * Each of these steps involves a memory read at an address which is computed + * from the previous step. This means these steps must be sequenced and their + * latencies are cumulative. + * + * Rather than do 1->2->3->4 sequentially for a single position before moving + * onto the next, this implementation interleaves these operations across the + * next few positions: + * + * R = Repcode Read & Compare + * H = Hash + * T = Table Lookup + * M = Match Read & Compare + * + * Pos | Time --> + * ----+------------------- + * N | ... M + * N+1 | ... TM + * N+2 | R H T M + * N+3 | H TM + * N+4 | R H T M + * N+5 | H ... + * N+6 | R ... + * + * This is very much analogous to the pipelining of execution in a CPU. And just + * like a CPU, we have to dump the pipeline when we find a match (i.e., take a + * branch). + * + * When this happens, we throw away our current state, and do the following prep + * to re-enter the loop: + * + * Pos | Time --> + * ----+------------------- + * N | H T + * N+1 | H + * + * This is also the work we do at the beginning to enter the loop initially. + */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_compressBlock_fast_noDict_generic( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize, + U32 const mls, U32 const hasStep) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashTable = ms->hashTable; + U32 const hlog = cParams->hashLog; + /* support stepSize of 0 */ + size_t const stepSize = hasStep ? (cParams->targetLength + !(cParams->targetLength) + 1) : 2; + const BYTE* const base = ms->window.base; + const BYTE* const istart = (const BYTE*)src; + const U32 endIndex = (U32)((size_t)(istart - base) + srcSize); + const U32 prefixStartIndex = ZSTD_getLowestPrefixIndex(ms, endIndex, cParams->windowLog); + const BYTE* const prefixStart = base + prefixStartIndex; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = iend - HASH_READ_SIZE; + + const BYTE* anchor = istart; + const BYTE* ip0 = istart; + const BYTE* ip1; + const BYTE* ip2; + const BYTE* ip3; + U32 current0; + + U32 rep_offset1 = rep[0]; + U32 rep_offset2 = rep[1]; + U32 offsetSaved1 = 0, offsetSaved2 = 0; + + size_t hash0; /* hash for ip0 */ + size_t hash1; /* hash for ip1 */ + U32 idx; /* match idx for ip0 */ + U32 mval; /* src value at match idx */ + + U32 offcode; + const BYTE* match0; + size_t mLength; + + /* ip0 and ip1 are always adjacent. The targetLength skipping and + * uncompressibility acceleration is applied to every other position, + * matching the behavior of #1562. step therefore represents the gap + * between pairs of positions, from ip0 to ip2 or ip1 to ip3. */ + size_t step; + const BYTE* nextStep; + const size_t kStepIncr = (1 << (kSearchStrength - 1)); + + DEBUGLOG(5, "ZSTD_compressBlock_fast_generic"); + ip0 += (ip0 == prefixStart); + { U32 const curr = (U32)(ip0 - base); + U32 const windowLow = ZSTD_getLowestPrefixIndex(ms, curr, cParams->windowLog); + U32 const maxRep = curr - windowLow; + if (rep_offset2 > maxRep) offsetSaved2 = rep_offset2, rep_offset2 = 0; + if (rep_offset1 > maxRep) offsetSaved1 = rep_offset1, rep_offset1 = 0; + } + + /* start each op */ +_start: /* Requires: ip0 */ + + step = stepSize; + nextStep = ip0 + kStepIncr; + + /* calculate positions, ip0 - anchor == 0, so we skip step calc */ + ip1 = ip0 + 1; + ip2 = ip0 + step; + ip3 = ip2 + 1; + + if (ip3 >= ilimit) { + goto _cleanup; + } + + hash0 = ZSTD_hashPtr(ip0, hlog, mls); + hash1 = ZSTD_hashPtr(ip1, hlog, mls); + + idx = hashTable[hash0]; + + do { + /* load repcode match for ip[2]*/ + const U32 rval = MEM_read32(ip2 - rep_offset1); + + /* write back hash table entry */ + current0 = (U32)(ip0 - base); + hashTable[hash0] = current0; + + /* check repcode at ip[2] */ + if ((MEM_read32(ip2) == rval) & (rep_offset1 > 0)) { + ip0 = ip2; + match0 = ip0 - rep_offset1; + mLength = ip0[-1] == match0[-1]; + ip0 -= mLength; + match0 -= mLength; + offcode = REPCODE1_TO_OFFBASE; + mLength += 4; + + /* First write next hash table entry; we've already calculated it. + * This write is known to be safe because the ip1 is before the + * repcode (ip2). */ + hashTable[hash1] = (U32)(ip1 - base); + + goto _match; + } + + /* load match for ip[0] */ + if (idx >= prefixStartIndex) { + mval = MEM_read32(base + idx); + } else { + mval = MEM_read32(ip0) ^ 1; /* guaranteed to not match. */ + } + + /* check match at ip[0] */ + if (MEM_read32(ip0) == mval) { + /* found a match! */ + + /* First write next hash table entry; we've already calculated it. + * This write is known to be safe because the ip1 == ip0 + 1, so + * we know we will resume searching after ip1 */ + hashTable[hash1] = (U32)(ip1 - base); + + goto _offset; + } + + /* lookup ip[1] */ + idx = hashTable[hash1]; + + /* hash ip[2] */ + hash0 = hash1; + hash1 = ZSTD_hashPtr(ip2, hlog, mls); + + /* advance to next positions */ + ip0 = ip1; + ip1 = ip2; + ip2 = ip3; + + /* write back hash table entry */ + current0 = (U32)(ip0 - base); + hashTable[hash0] = current0; + + /* load match for ip[0] */ + if (idx >= prefixStartIndex) { + mval = MEM_read32(base + idx); + } else { + mval = MEM_read32(ip0) ^ 1; /* guaranteed to not match. */ + } + + /* check match at ip[0] */ + if (MEM_read32(ip0) == mval) { + /* found a match! */ + + /* first write next hash table entry; we've already calculated it */ + if (step <= 4) { + /* We need to avoid writing an index into the hash table >= the + * position at which we will pick up our searching after we've + * taken this match. + * + * The minimum possible match has length 4, so the earliest ip0 + * can be after we take this match will be the current ip0 + 4. + * ip1 is ip0 + step - 1. If ip1 is >= ip0 + 4, we can't safely + * write this position. + */ + hashTable[hash1] = (U32)(ip1 - base); + } + + goto _offset; + } + + /* lookup ip[1] */ + idx = hashTable[hash1]; + + /* hash ip[2] */ + hash0 = hash1; + hash1 = ZSTD_hashPtr(ip2, hlog, mls); + + /* advance to next positions */ + ip0 = ip1; + ip1 = ip2; + ip2 = ip0 + step; + ip3 = ip1 + step; + + /* calculate step */ + if (ip2 >= nextStep) { + step++; + PREFETCH_L1(ip1 + 64); + PREFETCH_L1(ip1 + 128); + nextStep += kStepIncr; + } + } while (ip3 < ilimit); + +_cleanup: + /* Note that there are probably still a couple positions we could search. + * However, it seems to be a meaningful performance hit to try to search + * them. So let's not. */ + + /* When the repcodes are outside of the prefix, we set them to zero before the loop. + * When the offsets are still zero, we need to restore them after the block to have a correct + * repcode history. If only one offset was invalid, it is easy. The tricky case is when both + * offsets were invalid. We need to figure out which offset to refill with. + * - If both offsets are zero they are in the same order. + * - If both offsets are non-zero, we won't restore the offsets from `offsetSaved[12]`. + * - If only one is zero, we need to decide which offset to restore. + * - If rep_offset1 is non-zero, then rep_offset2 must be offsetSaved1. + * - It is impossible for rep_offset2 to be non-zero. + * + * So if rep_offset1 started invalid (offsetSaved1 != 0) and became valid (rep_offset1 != 0), then + * set rep[0] = rep_offset1 and rep[1] = offsetSaved1. + */ + offsetSaved2 = ((offsetSaved1 != 0) && (rep_offset1 != 0)) ? offsetSaved1 : offsetSaved2; + + /* save reps for next block */ + rep[0] = rep_offset1 ? rep_offset1 : offsetSaved1; + rep[1] = rep_offset2 ? rep_offset2 : offsetSaved2; + + /* Return the last literals size */ + return (size_t)(iend - anchor); + +_offset: /* Requires: ip0, idx */ + + /* Compute the offset code. */ + match0 = base + idx; + rep_offset2 = rep_offset1; + rep_offset1 = (U32)(ip0-match0); + offcode = OFFSET_TO_OFFBASE(rep_offset1); + mLength = 4; + + /* Count the backwards match length. */ + while (((ip0>anchor) & (match0>prefixStart)) && (ip0[-1] == match0[-1])) { + ip0--; + match0--; + mLength++; + } + +_match: /* Requires: ip0, match0, offcode */ + + /* Count the forward length. */ + mLength += ZSTD_count(ip0 + mLength, match0 + mLength, iend); + + ZSTD_storeSeq(seqStore, (size_t)(ip0 - anchor), anchor, iend, offcode, mLength); + + ip0 += mLength; + anchor = ip0; + + /* Fill table and check for immediate repcode. */ + if (ip0 <= ilimit) { + /* Fill Table */ + assert(base+current0+2 > istart); /* check base overflow */ + hashTable[ZSTD_hashPtr(base+current0+2, hlog, mls)] = current0+2; /* here because current+2 could be > iend-8 */ + hashTable[ZSTD_hashPtr(ip0-2, hlog, mls)] = (U32)(ip0-2-base); + + if (rep_offset2 > 0) { /* rep_offset2==0 means rep_offset2 is invalidated */ + while ( (ip0 <= ilimit) && (MEM_read32(ip0) == MEM_read32(ip0 - rep_offset2)) ) { + /* store sequence */ + size_t const rLength = ZSTD_count(ip0+4, ip0+4-rep_offset2, iend) + 4; + { U32 const tmpOff = rep_offset2; rep_offset2 = rep_offset1; rep_offset1 = tmpOff; } /* swap rep_offset2 <=> rep_offset1 */ + hashTable[ZSTD_hashPtr(ip0, hlog, mls)] = (U32)(ip0-base); + ip0 += rLength; + ZSTD_storeSeq(seqStore, 0 /*litLen*/, anchor, iend, REPCODE1_TO_OFFBASE, rLength); + anchor = ip0; + continue; /* faster when present (confirmed on gcc-8) ... (?) */ + } } } + + goto _start; +} + +#define ZSTD_GEN_FAST_FN(dictMode, mls, step) \ + static size_t ZSTD_compressBlock_fast_##dictMode##_##mls##_##step( \ + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], \ + void const* src, size_t srcSize) \ + { \ + return ZSTD_compressBlock_fast_##dictMode##_generic(ms, seqStore, rep, src, srcSize, mls, step); \ + } + +ZSTD_GEN_FAST_FN(noDict, 4, 1) +ZSTD_GEN_FAST_FN(noDict, 5, 1) +ZSTD_GEN_FAST_FN(noDict, 6, 1) +ZSTD_GEN_FAST_FN(noDict, 7, 1) + +ZSTD_GEN_FAST_FN(noDict, 4, 0) +ZSTD_GEN_FAST_FN(noDict, 5, 0) +ZSTD_GEN_FAST_FN(noDict, 6, 0) +ZSTD_GEN_FAST_FN(noDict, 7, 0) + +size_t ZSTD_compressBlock_fast( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + U32 const mls = ms->cParams.minMatch; + assert(ms->dictMatchState == NULL); + if (ms->cParams.targetLength > 1) { + switch(mls) + { + default: /* includes case 3 */ + case 4 : + return ZSTD_compressBlock_fast_noDict_4_1(ms, seqStore, rep, src, srcSize); + case 5 : + return ZSTD_compressBlock_fast_noDict_5_1(ms, seqStore, rep, src, srcSize); + case 6 : + return ZSTD_compressBlock_fast_noDict_6_1(ms, seqStore, rep, src, srcSize); + case 7 : + return ZSTD_compressBlock_fast_noDict_7_1(ms, seqStore, rep, src, srcSize); + } + } else { + switch(mls) + { + default: /* includes case 3 */ + case 4 : + return ZSTD_compressBlock_fast_noDict_4_0(ms, seqStore, rep, src, srcSize); + case 5 : + return ZSTD_compressBlock_fast_noDict_5_0(ms, seqStore, rep, src, srcSize); + case 6 : + return ZSTD_compressBlock_fast_noDict_6_0(ms, seqStore, rep, src, srcSize); + case 7 : + return ZSTD_compressBlock_fast_noDict_7_0(ms, seqStore, rep, src, srcSize); + } + + } +} + +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_compressBlock_fast_dictMatchState_generic( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize, U32 const mls, U32 const hasStep) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashTable = ms->hashTable; + U32 const hlog = cParams->hashLog; + /* support stepSize of 0 */ + U32 const stepSize = cParams->targetLength + !(cParams->targetLength); + const BYTE* const base = ms->window.base; + const BYTE* const istart = (const BYTE*)src; + const BYTE* ip0 = istart; + const BYTE* ip1 = ip0 + stepSize; /* we assert below that stepSize >= 1 */ + const BYTE* anchor = istart; + const U32 prefixStartIndex = ms->window.dictLimit; + const BYTE* const prefixStart = base + prefixStartIndex; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = iend - HASH_READ_SIZE; + U32 offset_1=rep[0], offset_2=rep[1]; + + const ZSTD_matchState_t* const dms = ms->dictMatchState; + const ZSTD_compressionParameters* const dictCParams = &dms->cParams ; + const U32* const dictHashTable = dms->hashTable; + const U32 dictStartIndex = dms->window.dictLimit; + const BYTE* const dictBase = dms->window.base; + const BYTE* const dictStart = dictBase + dictStartIndex; + const BYTE* const dictEnd = dms->window.nextSrc; + const U32 dictIndexDelta = prefixStartIndex - (U32)(dictEnd - dictBase); + const U32 dictAndPrefixLength = (U32)(istart - prefixStart + dictEnd - dictStart); + const U32 dictHBits = dictCParams->hashLog + ZSTD_SHORT_CACHE_TAG_BITS; + + /* if a dictionary is still attached, it necessarily means that + * it is within window size. So we just check it. */ + const U32 maxDistance = 1U << cParams->windowLog; + const U32 endIndex = (U32)((size_t)(istart - base) + srcSize); + assert(endIndex - prefixStartIndex <= maxDistance); + (void)maxDistance; (void)endIndex; /* these variables are not used when assert() is disabled */ + + (void)hasStep; /* not currently specialized on whether it's accelerated */ + + /* ensure there will be no underflow + * when translating a dict index into a local index */ + assert(prefixStartIndex >= (U32)(dictEnd - dictBase)); + + if (ms->prefetchCDictTables) { + size_t const hashTableBytes = (((size_t)1) << dictCParams->hashLog) * sizeof(U32); + PREFETCH_AREA(dictHashTable, hashTableBytes); + } + + /* init */ + DEBUGLOG(5, "ZSTD_compressBlock_fast_dictMatchState_generic"); + ip0 += (dictAndPrefixLength == 0); + /* dictMatchState repCode checks don't currently handle repCode == 0 + * disabling. */ + assert(offset_1 <= dictAndPrefixLength); + assert(offset_2 <= dictAndPrefixLength); + + /* Outer search loop */ + assert(stepSize >= 1); + while (ip1 <= ilimit) { /* repcode check at (ip0 + 1) is safe because ip0 < ip1 */ + size_t mLength; + size_t hash0 = ZSTD_hashPtr(ip0, hlog, mls); + + size_t const dictHashAndTag0 = ZSTD_hashPtr(ip0, dictHBits, mls); + U32 dictMatchIndexAndTag = dictHashTable[dictHashAndTag0 >> ZSTD_SHORT_CACHE_TAG_BITS]; + int dictTagsMatch = ZSTD_comparePackedTags(dictMatchIndexAndTag, dictHashAndTag0); + + U32 matchIndex = hashTable[hash0]; + U32 curr = (U32)(ip0 - base); + size_t step = stepSize; + const size_t kStepIncr = 1 << kSearchStrength; + const BYTE* nextStep = ip0 + kStepIncr; + + /* Inner search loop */ + while (1) { + const BYTE* match = base + matchIndex; + const U32 repIndex = curr + 1 - offset_1; + const BYTE* repMatch = (repIndex < prefixStartIndex) ? + dictBase + (repIndex - dictIndexDelta) : + base + repIndex; + const size_t hash1 = ZSTD_hashPtr(ip1, hlog, mls); + size_t const dictHashAndTag1 = ZSTD_hashPtr(ip1, dictHBits, mls); + hashTable[hash0] = curr; /* update hash table */ + + if (((U32) ((prefixStartIndex - 1) - repIndex) >= + 3) /* intentional underflow : ensure repIndex isn't overlapping dict + prefix */ + && (MEM_read32(repMatch) == MEM_read32(ip0 + 1))) { + const BYTE* const repMatchEnd = repIndex < prefixStartIndex ? dictEnd : iend; + mLength = ZSTD_count_2segments(ip0 + 1 + 4, repMatch + 4, iend, repMatchEnd, prefixStart) + 4; + ip0++; + ZSTD_storeSeq(seqStore, (size_t) (ip0 - anchor), anchor, iend, REPCODE1_TO_OFFBASE, mLength); + break; + } + + if (dictTagsMatch) { + /* Found a possible dict match */ + const U32 dictMatchIndex = dictMatchIndexAndTag >> ZSTD_SHORT_CACHE_TAG_BITS; + const BYTE* dictMatch = dictBase + dictMatchIndex; + if (dictMatchIndex > dictStartIndex && + MEM_read32(dictMatch) == MEM_read32(ip0)) { + /* To replicate extDict parse behavior, we only use dict matches when the normal matchIndex is invalid */ + if (matchIndex <= prefixStartIndex) { + U32 const offset = (U32) (curr - dictMatchIndex - dictIndexDelta); + mLength = ZSTD_count_2segments(ip0 + 4, dictMatch + 4, iend, dictEnd, prefixStart) + 4; + while (((ip0 > anchor) & (dictMatch > dictStart)) + && (ip0[-1] == dictMatch[-1])) { + ip0--; + dictMatch--; + mLength++; + } /* catch up */ + offset_2 = offset_1; + offset_1 = offset; + ZSTD_storeSeq(seqStore, (size_t) (ip0 - anchor), anchor, iend, OFFSET_TO_OFFBASE(offset), mLength); + break; + } + } + } + + if (matchIndex > prefixStartIndex && MEM_read32(match) == MEM_read32(ip0)) { + /* found a regular match */ + U32 const offset = (U32) (ip0 - match); + mLength = ZSTD_count(ip0 + 4, match + 4, iend) + 4; + while (((ip0 > anchor) & (match > prefixStart)) + && (ip0[-1] == match[-1])) { + ip0--; + match--; + mLength++; + } /* catch up */ + offset_2 = offset_1; + offset_1 = offset; + ZSTD_storeSeq(seqStore, (size_t) (ip0 - anchor), anchor, iend, OFFSET_TO_OFFBASE(offset), mLength); + break; + } + + /* Prepare for next iteration */ + dictMatchIndexAndTag = dictHashTable[dictHashAndTag1 >> ZSTD_SHORT_CACHE_TAG_BITS]; + dictTagsMatch = ZSTD_comparePackedTags(dictMatchIndexAndTag, dictHashAndTag1); + matchIndex = hashTable[hash1]; + + if (ip1 >= nextStep) { + step++; + nextStep += kStepIncr; + } + ip0 = ip1; + ip1 = ip1 + step; + if (ip1 > ilimit) goto _cleanup; + + curr = (U32)(ip0 - base); + hash0 = hash1; + } /* end inner search loop */ + + /* match found */ + assert(mLength); + ip0 += mLength; + anchor = ip0; + + if (ip0 <= ilimit) { + /* Fill Table */ + assert(base+curr+2 > istart); /* check base overflow */ + hashTable[ZSTD_hashPtr(base+curr+2, hlog, mls)] = curr+2; /* here because curr+2 could be > iend-8 */ + hashTable[ZSTD_hashPtr(ip0-2, hlog, mls)] = (U32)(ip0-2-base); + + /* check immediate repcode */ + while (ip0 <= ilimit) { + U32 const current2 = (U32)(ip0-base); + U32 const repIndex2 = current2 - offset_2; + const BYTE* repMatch2 = repIndex2 < prefixStartIndex ? + dictBase - dictIndexDelta + repIndex2 : + base + repIndex2; + if ( ((U32)((prefixStartIndex-1) - (U32)repIndex2) >= 3 /* intentional overflow */) + && (MEM_read32(repMatch2) == MEM_read32(ip0))) { + const BYTE* const repEnd2 = repIndex2 < prefixStartIndex ? dictEnd : iend; + size_t const repLength2 = ZSTD_count_2segments(ip0+4, repMatch2+4, iend, repEnd2, prefixStart) + 4; + U32 tmpOffset = offset_2; offset_2 = offset_1; offset_1 = tmpOffset; /* swap offset_2 <=> offset_1 */ + ZSTD_storeSeq(seqStore, 0, anchor, iend, REPCODE1_TO_OFFBASE, repLength2); + hashTable[ZSTD_hashPtr(ip0, hlog, mls)] = current2; + ip0 += repLength2; + anchor = ip0; + continue; + } + break; + } + } + + /* Prepare for next iteration */ + assert(ip0 == anchor); + ip1 = ip0 + stepSize; + } + +_cleanup: + /* save reps for next block */ + rep[0] = offset_1; + rep[1] = offset_2; + + /* Return the last literals size */ + return (size_t)(iend - anchor); +} + + +ZSTD_GEN_FAST_FN(dictMatchState, 4, 0) +ZSTD_GEN_FAST_FN(dictMatchState, 5, 0) +ZSTD_GEN_FAST_FN(dictMatchState, 6, 0) +ZSTD_GEN_FAST_FN(dictMatchState, 7, 0) + +size_t ZSTD_compressBlock_fast_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + U32 const mls = ms->cParams.minMatch; + assert(ms->dictMatchState != NULL); + switch(mls) + { + default: /* includes case 3 */ + case 4 : + return ZSTD_compressBlock_fast_dictMatchState_4_0(ms, seqStore, rep, src, srcSize); + case 5 : + return ZSTD_compressBlock_fast_dictMatchState_5_0(ms, seqStore, rep, src, srcSize); + case 6 : + return ZSTD_compressBlock_fast_dictMatchState_6_0(ms, seqStore, rep, src, srcSize); + case 7 : + return ZSTD_compressBlock_fast_dictMatchState_7_0(ms, seqStore, rep, src, srcSize); + } +} + + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_compressBlock_fast_extDict_generic( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize, U32 const mls, U32 const hasStep) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashTable = ms->hashTable; + U32 const hlog = cParams->hashLog; + /* support stepSize of 0 */ + size_t const stepSize = cParams->targetLength + !(cParams->targetLength) + 1; + const BYTE* const base = ms->window.base; + const BYTE* const dictBase = ms->window.dictBase; + const BYTE* const istart = (const BYTE*)src; + const BYTE* anchor = istart; + const U32 endIndex = (U32)((size_t)(istart - base) + srcSize); + const U32 lowLimit = ZSTD_getLowestMatchIndex(ms, endIndex, cParams->windowLog); + const U32 dictStartIndex = lowLimit; + const BYTE* const dictStart = dictBase + dictStartIndex; + const U32 dictLimit = ms->window.dictLimit; + const U32 prefixStartIndex = dictLimit < lowLimit ? lowLimit : dictLimit; + const BYTE* const prefixStart = base + prefixStartIndex; + const BYTE* const dictEnd = dictBase + prefixStartIndex; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = iend - 8; + U32 offset_1=rep[0], offset_2=rep[1]; + U32 offsetSaved1 = 0, offsetSaved2 = 0; + + const BYTE* ip0 = istart; + const BYTE* ip1; + const BYTE* ip2; + const BYTE* ip3; + U32 current0; + + + size_t hash0; /* hash for ip0 */ + size_t hash1; /* hash for ip1 */ + U32 idx; /* match idx for ip0 */ + const BYTE* idxBase; /* base pointer for idx */ + + U32 offcode; + const BYTE* match0; + size_t mLength; + const BYTE* matchEnd = 0; /* initialize to avoid warning, assert != 0 later */ + + size_t step; + const BYTE* nextStep; + const size_t kStepIncr = (1 << (kSearchStrength - 1)); + + (void)hasStep; /* not currently specialized on whether it's accelerated */ + + DEBUGLOG(5, "ZSTD_compressBlock_fast_extDict_generic (offset_1=%u)", offset_1); + + /* switch to "regular" variant if extDict is invalidated due to maxDistance */ + if (prefixStartIndex == dictStartIndex) + return ZSTD_compressBlock_fast(ms, seqStore, rep, src, srcSize); + + { U32 const curr = (U32)(ip0 - base); + U32 const maxRep = curr - dictStartIndex; + if (offset_2 >= maxRep) offsetSaved2 = offset_2, offset_2 = 0; + if (offset_1 >= maxRep) offsetSaved1 = offset_1, offset_1 = 0; + } + + /* start each op */ +_start: /* Requires: ip0 */ + + step = stepSize; + nextStep = ip0 + kStepIncr; + + /* calculate positions, ip0 - anchor == 0, so we skip step calc */ + ip1 = ip0 + 1; + ip2 = ip0 + step; + ip3 = ip2 + 1; + + if (ip3 >= ilimit) { + goto _cleanup; + } + + hash0 = ZSTD_hashPtr(ip0, hlog, mls); + hash1 = ZSTD_hashPtr(ip1, hlog, mls); + + idx = hashTable[hash0]; + idxBase = idx < prefixStartIndex ? dictBase : base; + + do { + { /* load repcode match for ip[2] */ + U32 const current2 = (U32)(ip2 - base); + U32 const repIndex = current2 - offset_1; + const BYTE* const repBase = repIndex < prefixStartIndex ? dictBase : base; + U32 rval; + if ( ((U32)(prefixStartIndex - repIndex) >= 4) /* intentional underflow */ + & (offset_1 > 0) ) { + rval = MEM_read32(repBase + repIndex); + } else { + rval = MEM_read32(ip2) ^ 1; /* guaranteed to not match. */ + } + + /* write back hash table entry */ + current0 = (U32)(ip0 - base); + hashTable[hash0] = current0; + + /* check repcode at ip[2] */ + if (MEM_read32(ip2) == rval) { + ip0 = ip2; + match0 = repBase + repIndex; + matchEnd = repIndex < prefixStartIndex ? dictEnd : iend; + assert((match0 != prefixStart) & (match0 != dictStart)); + mLength = ip0[-1] == match0[-1]; + ip0 -= mLength; + match0 -= mLength; + offcode = REPCODE1_TO_OFFBASE; + mLength += 4; + goto _match; + } } + + { /* load match for ip[0] */ + U32 const mval = idx >= dictStartIndex ? + MEM_read32(idxBase + idx) : + MEM_read32(ip0) ^ 1; /* guaranteed not to match */ + + /* check match at ip[0] */ + if (MEM_read32(ip0) == mval) { + /* found a match! */ + goto _offset; + } } + + /* lookup ip[1] */ + idx = hashTable[hash1]; + idxBase = idx < prefixStartIndex ? dictBase : base; + + /* hash ip[2] */ + hash0 = hash1; + hash1 = ZSTD_hashPtr(ip2, hlog, mls); + + /* advance to next positions */ + ip0 = ip1; + ip1 = ip2; + ip2 = ip3; + + /* write back hash table entry */ + current0 = (U32)(ip0 - base); + hashTable[hash0] = current0; + + { /* load match for ip[0] */ + U32 const mval = idx >= dictStartIndex ? + MEM_read32(idxBase + idx) : + MEM_read32(ip0) ^ 1; /* guaranteed not to match */ + + /* check match at ip[0] */ + if (MEM_read32(ip0) == mval) { + /* found a match! */ + goto _offset; + } } + + /* lookup ip[1] */ + idx = hashTable[hash1]; + idxBase = idx < prefixStartIndex ? dictBase : base; + + /* hash ip[2] */ + hash0 = hash1; + hash1 = ZSTD_hashPtr(ip2, hlog, mls); + + /* advance to next positions */ + ip0 = ip1; + ip1 = ip2; + ip2 = ip0 + step; + ip3 = ip1 + step; + + /* calculate step */ + if (ip2 >= nextStep) { + step++; + PREFETCH_L1(ip1 + 64); + PREFETCH_L1(ip1 + 128); + nextStep += kStepIncr; + } + } while (ip3 < ilimit); + +_cleanup: + /* Note that there are probably still a couple positions we could search. + * However, it seems to be a meaningful performance hit to try to search + * them. So let's not. */ + + /* If offset_1 started invalid (offsetSaved1 != 0) and became valid (offset_1 != 0), + * rotate saved offsets. See comment in ZSTD_compressBlock_fast_noDict for more context. */ + offsetSaved2 = ((offsetSaved1 != 0) && (offset_1 != 0)) ? offsetSaved1 : offsetSaved2; + + /* save reps for next block */ + rep[0] = offset_1 ? offset_1 : offsetSaved1; + rep[1] = offset_2 ? offset_2 : offsetSaved2; + + /* Return the last literals size */ + return (size_t)(iend - anchor); + +_offset: /* Requires: ip0, idx, idxBase */ + + /* Compute the offset code. */ + { U32 const offset = current0 - idx; + const BYTE* const lowMatchPtr = idx < prefixStartIndex ? dictStart : prefixStart; + matchEnd = idx < prefixStartIndex ? dictEnd : iend; + match0 = idxBase + idx; + offset_2 = offset_1; + offset_1 = offset; + offcode = OFFSET_TO_OFFBASE(offset); + mLength = 4; + + /* Count the backwards match length. */ + while (((ip0>anchor) & (match0>lowMatchPtr)) && (ip0[-1] == match0[-1])) { + ip0--; + match0--; + mLength++; + } } + +_match: /* Requires: ip0, match0, offcode, matchEnd */ + + /* Count the forward length. */ + assert(matchEnd != 0); + mLength += ZSTD_count_2segments(ip0 + mLength, match0 + mLength, iend, matchEnd, prefixStart); + + ZSTD_storeSeq(seqStore, (size_t)(ip0 - anchor), anchor, iend, offcode, mLength); + + ip0 += mLength; + anchor = ip0; + + /* write next hash table entry */ + if (ip1 < ip0) { + hashTable[hash1] = (U32)(ip1 - base); + } + + /* Fill table and check for immediate repcode. */ + if (ip0 <= ilimit) { + /* Fill Table */ + assert(base+current0+2 > istart); /* check base overflow */ + hashTable[ZSTD_hashPtr(base+current0+2, hlog, mls)] = current0+2; /* here because current+2 could be > iend-8 */ + hashTable[ZSTD_hashPtr(ip0-2, hlog, mls)] = (U32)(ip0-2-base); + + while (ip0 <= ilimit) { + U32 const repIndex2 = (U32)(ip0-base) - offset_2; + const BYTE* const repMatch2 = repIndex2 < prefixStartIndex ? dictBase + repIndex2 : base + repIndex2; + if ( (((U32)((prefixStartIndex-1) - repIndex2) >= 3) & (offset_2 > 0)) /* intentional underflow */ + && (MEM_read32(repMatch2) == MEM_read32(ip0)) ) { + const BYTE* const repEnd2 = repIndex2 < prefixStartIndex ? dictEnd : iend; + size_t const repLength2 = ZSTD_count_2segments(ip0+4, repMatch2+4, iend, repEnd2, prefixStart) + 4; + { U32 const tmpOffset = offset_2; offset_2 = offset_1; offset_1 = tmpOffset; } /* swap offset_2 <=> offset_1 */ + ZSTD_storeSeq(seqStore, 0 /*litlen*/, anchor, iend, REPCODE1_TO_OFFBASE, repLength2); + hashTable[ZSTD_hashPtr(ip0, hlog, mls)] = (U32)(ip0-base); + ip0 += repLength2; + anchor = ip0; + continue; + } + break; + } } + + goto _start; +} + +ZSTD_GEN_FAST_FN(extDict, 4, 0) +ZSTD_GEN_FAST_FN(extDict, 5, 0) +ZSTD_GEN_FAST_FN(extDict, 6, 0) +ZSTD_GEN_FAST_FN(extDict, 7, 0) + +size_t ZSTD_compressBlock_fast_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + U32 const mls = ms->cParams.minMatch; + assert(ms->dictMatchState == NULL); + switch(mls) + { + default: /* includes case 3 */ + case 4 : + return ZSTD_compressBlock_fast_extDict_4_0(ms, seqStore, rep, src, srcSize); + case 5 : + return ZSTD_compressBlock_fast_extDict_5_0(ms, seqStore, rep, src, srcSize); + case 6 : + return ZSTD_compressBlock_fast_extDict_6_0(ms, seqStore, rep, src, srcSize); + case 7 : + return ZSTD_compressBlock_fast_extDict_7_0(ms, seqStore, rep, src, srcSize); + } +} +/**** ended inlining compress/zstd_fast.c ****/ +/**** start inlining compress/zstd_lazy.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/**** skipping file: zstd_compress_internal.h ****/ +/**** skipping file: zstd_lazy.h ****/ +/**** skipping file: ../common/bits.h ****/ + +#if !defined(ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_LAZY_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_LAZY2_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR) + +#define kLazySkippingStep 8 + + +/*-************************************* +* Binary Tree search +***************************************/ + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_updateDUBT(ZSTD_matchState_t* ms, + const BYTE* ip, const BYTE* iend, + U32 mls) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashTable = ms->hashTable; + U32 const hashLog = cParams->hashLog; + + U32* const bt = ms->chainTable; + U32 const btLog = cParams->chainLog - 1; + U32 const btMask = (1 << btLog) - 1; + + const BYTE* const base = ms->window.base; + U32 const target = (U32)(ip - base); + U32 idx = ms->nextToUpdate; + + if (idx != target) + DEBUGLOG(7, "ZSTD_updateDUBT, from %u to %u (dictLimit:%u)", + idx, target, ms->window.dictLimit); + assert(ip + 8 <= iend); /* condition for ZSTD_hashPtr */ + (void)iend; + + assert(idx >= ms->window.dictLimit); /* condition for valid base+idx */ + for ( ; idx < target ; idx++) { + size_t const h = ZSTD_hashPtr(base + idx, hashLog, mls); /* assumption : ip + 8 <= iend */ + U32 const matchIndex = hashTable[h]; + + U32* const nextCandidatePtr = bt + 2*(idx&btMask); + U32* const sortMarkPtr = nextCandidatePtr + 1; + + DEBUGLOG(8, "ZSTD_updateDUBT: insert %u", idx); + hashTable[h] = idx; /* Update Hash Table */ + *nextCandidatePtr = matchIndex; /* update BT like a chain */ + *sortMarkPtr = ZSTD_DUBT_UNSORTED_MARK; + } + ms->nextToUpdate = target; +} + + +/** ZSTD_insertDUBT1() : + * sort one already inserted but unsorted position + * assumption : curr >= btlow == (curr - btmask) + * doesn't fail */ +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_insertDUBT1(const ZSTD_matchState_t* ms, + U32 curr, const BYTE* inputEnd, + U32 nbCompares, U32 btLow, + const ZSTD_dictMode_e dictMode) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const bt = ms->chainTable; + U32 const btLog = cParams->chainLog - 1; + U32 const btMask = (1 << btLog) - 1; + size_t commonLengthSmaller=0, commonLengthLarger=0; + const BYTE* const base = ms->window.base; + const BYTE* const dictBase = ms->window.dictBase; + const U32 dictLimit = ms->window.dictLimit; + const BYTE* const ip = (curr>=dictLimit) ? base + curr : dictBase + curr; + const BYTE* const iend = (curr>=dictLimit) ? inputEnd : dictBase + dictLimit; + const BYTE* const dictEnd = dictBase + dictLimit; + const BYTE* const prefixStart = base + dictLimit; + const BYTE* match; + U32* smallerPtr = bt + 2*(curr&btMask); + U32* largerPtr = smallerPtr + 1; + U32 matchIndex = *smallerPtr; /* this candidate is unsorted : next sorted candidate is reached through *smallerPtr, while *largerPtr contains previous unsorted candidate (which is already saved and can be overwritten) */ + U32 dummy32; /* to be nullified at the end */ + U32 const windowValid = ms->window.lowLimit; + U32 const maxDistance = 1U << cParams->windowLog; + U32 const windowLow = (curr - windowValid > maxDistance) ? curr - maxDistance : windowValid; + + + DEBUGLOG(8, "ZSTD_insertDUBT1(%u) (dictLimit=%u, lowLimit=%u)", + curr, dictLimit, windowLow); + assert(curr >= btLow); + assert(ip < iend); /* condition for ZSTD_count */ + + for (; nbCompares && (matchIndex > windowLow); --nbCompares) { + U32* const nextPtr = bt + 2*(matchIndex & btMask); + size_t matchLength = MIN(commonLengthSmaller, commonLengthLarger); /* guaranteed minimum nb of common bytes */ + assert(matchIndex < curr); + /* note : all candidates are now supposed sorted, + * but it's still possible to have nextPtr[1] == ZSTD_DUBT_UNSORTED_MARK + * when a real index has the same value as ZSTD_DUBT_UNSORTED_MARK */ + + if ( (dictMode != ZSTD_extDict) + || (matchIndex+matchLength >= dictLimit) /* both in current segment*/ + || (curr < dictLimit) /* both in extDict */) { + const BYTE* const mBase = ( (dictMode != ZSTD_extDict) + || (matchIndex+matchLength >= dictLimit)) ? + base : dictBase; + assert( (matchIndex+matchLength >= dictLimit) /* might be wrong if extDict is incorrectly set to 0 */ + || (curr < dictLimit) ); + match = mBase + matchIndex; + matchLength += ZSTD_count(ip+matchLength, match+matchLength, iend); + } else { + match = dictBase + matchIndex; + matchLength += ZSTD_count_2segments(ip+matchLength, match+matchLength, iend, dictEnd, prefixStart); + if (matchIndex+matchLength >= dictLimit) + match = base + matchIndex; /* preparation for next read of match[matchLength] */ + } + + DEBUGLOG(8, "ZSTD_insertDUBT1: comparing %u with %u : found %u common bytes ", + curr, matchIndex, (U32)matchLength); + + if (ip+matchLength == iend) { /* equal : no way to know if inf or sup */ + break; /* drop , to guarantee consistency ; miss a bit of compression, but other solutions can corrupt tree */ + } + + if (match[matchLength] < ip[matchLength]) { /* necessarily within buffer */ + /* match is smaller than current */ + *smallerPtr = matchIndex; /* update smaller idx */ + commonLengthSmaller = matchLength; /* all smaller will now have at least this guaranteed common length */ + if (matchIndex <= btLow) { smallerPtr=&dummy32; break; } /* beyond tree size, stop searching */ + DEBUGLOG(8, "ZSTD_insertDUBT1: %u (>btLow=%u) is smaller : next => %u", + matchIndex, btLow, nextPtr[1]); + smallerPtr = nextPtr+1; /* new "candidate" => larger than match, which was smaller than target */ + matchIndex = nextPtr[1]; /* new matchIndex, larger than previous and closer to current */ + } else { + /* match is larger than current */ + *largerPtr = matchIndex; + commonLengthLarger = matchLength; + if (matchIndex <= btLow) { largerPtr=&dummy32; break; } /* beyond tree size, stop searching */ + DEBUGLOG(8, "ZSTD_insertDUBT1: %u (>btLow=%u) is larger => %u", + matchIndex, btLow, nextPtr[0]); + largerPtr = nextPtr; + matchIndex = nextPtr[0]; + } } + + *smallerPtr = *largerPtr = 0; +} + + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_DUBT_findBetterDictMatch ( + const ZSTD_matchState_t* ms, + const BYTE* const ip, const BYTE* const iend, + size_t* offsetPtr, + size_t bestLength, + U32 nbCompares, + U32 const mls, + const ZSTD_dictMode_e dictMode) +{ + const ZSTD_matchState_t * const dms = ms->dictMatchState; + const ZSTD_compressionParameters* const dmsCParams = &dms->cParams; + const U32 * const dictHashTable = dms->hashTable; + U32 const hashLog = dmsCParams->hashLog; + size_t const h = ZSTD_hashPtr(ip, hashLog, mls); + U32 dictMatchIndex = dictHashTable[h]; + + const BYTE* const base = ms->window.base; + const BYTE* const prefixStart = base + ms->window.dictLimit; + U32 const curr = (U32)(ip-base); + const BYTE* const dictBase = dms->window.base; + const BYTE* const dictEnd = dms->window.nextSrc; + U32 const dictHighLimit = (U32)(dms->window.nextSrc - dms->window.base); + U32 const dictLowLimit = dms->window.lowLimit; + U32 const dictIndexDelta = ms->window.lowLimit - dictHighLimit; + + U32* const dictBt = dms->chainTable; + U32 const btLog = dmsCParams->chainLog - 1; + U32 const btMask = (1 << btLog) - 1; + U32 const btLow = (btMask >= dictHighLimit - dictLowLimit) ? dictLowLimit : dictHighLimit - btMask; + + size_t commonLengthSmaller=0, commonLengthLarger=0; + + (void)dictMode; + assert(dictMode == ZSTD_dictMatchState); + + for (; nbCompares && (dictMatchIndex > dictLowLimit); --nbCompares) { + U32* const nextPtr = dictBt + 2*(dictMatchIndex & btMask); + size_t matchLength = MIN(commonLengthSmaller, commonLengthLarger); /* guaranteed minimum nb of common bytes */ + const BYTE* match = dictBase + dictMatchIndex; + matchLength += ZSTD_count_2segments(ip+matchLength, match+matchLength, iend, dictEnd, prefixStart); + if (dictMatchIndex+matchLength >= dictHighLimit) + match = base + dictMatchIndex + dictIndexDelta; /* to prepare for next usage of match[matchLength] */ + + if (matchLength > bestLength) { + U32 matchIndex = dictMatchIndex + dictIndexDelta; + if ( (4*(int)(matchLength-bestLength)) > (int)(ZSTD_highbit32(curr-matchIndex+1) - ZSTD_highbit32((U32)offsetPtr[0]+1)) ) { + DEBUGLOG(9, "ZSTD_DUBT_findBetterDictMatch(%u) : found better match length %u -> %u and offsetCode %u -> %u (dictMatchIndex %u, matchIndex %u)", + curr, (U32)bestLength, (U32)matchLength, (U32)*offsetPtr, OFFSET_TO_OFFBASE(curr - matchIndex), dictMatchIndex, matchIndex); + bestLength = matchLength, *offsetPtr = OFFSET_TO_OFFBASE(curr - matchIndex); + } + if (ip+matchLength == iend) { /* reached end of input : ip[matchLength] is not valid, no way to know if it's larger or smaller than match */ + break; /* drop, to guarantee consistency (miss a little bit of compression) */ + } + } + + if (match[matchLength] < ip[matchLength]) { + if (dictMatchIndex <= btLow) { break; } /* beyond tree size, stop the search */ + commonLengthSmaller = matchLength; /* all smaller will now have at least this guaranteed common length */ + dictMatchIndex = nextPtr[1]; /* new matchIndex larger than previous (closer to current) */ + } else { + /* match is larger than current */ + if (dictMatchIndex <= btLow) { break; } /* beyond tree size, stop the search */ + commonLengthLarger = matchLength; + dictMatchIndex = nextPtr[0]; + } + } + + if (bestLength >= MINMATCH) { + U32 const mIndex = curr - (U32)OFFBASE_TO_OFFSET(*offsetPtr); (void)mIndex; + DEBUGLOG(8, "ZSTD_DUBT_findBetterDictMatch(%u) : found match of length %u and offsetCode %u (pos %u)", + curr, (U32)bestLength, (U32)*offsetPtr, mIndex); + } + return bestLength; + +} + + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_DUBT_findBestMatch(ZSTD_matchState_t* ms, + const BYTE* const ip, const BYTE* const iend, + size_t* offBasePtr, + U32 const mls, + const ZSTD_dictMode_e dictMode) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashTable = ms->hashTable; + U32 const hashLog = cParams->hashLog; + size_t const h = ZSTD_hashPtr(ip, hashLog, mls); + U32 matchIndex = hashTable[h]; + + const BYTE* const base = ms->window.base; + U32 const curr = (U32)(ip-base); + U32 const windowLow = ZSTD_getLowestMatchIndex(ms, curr, cParams->windowLog); + + U32* const bt = ms->chainTable; + U32 const btLog = cParams->chainLog - 1; + U32 const btMask = (1 << btLog) - 1; + U32 const btLow = (btMask >= curr) ? 0 : curr - btMask; + U32 const unsortLimit = MAX(btLow, windowLow); + + U32* nextCandidate = bt + 2*(matchIndex&btMask); + U32* unsortedMark = bt + 2*(matchIndex&btMask) + 1; + U32 nbCompares = 1U << cParams->searchLog; + U32 nbCandidates = nbCompares; + U32 previousCandidate = 0; + + DEBUGLOG(7, "ZSTD_DUBT_findBestMatch (%u) ", curr); + assert(ip <= iend-8); /* required for h calculation */ + assert(dictMode != ZSTD_dedicatedDictSearch); + + /* reach end of unsorted candidates list */ + while ( (matchIndex > unsortLimit) + && (*unsortedMark == ZSTD_DUBT_UNSORTED_MARK) + && (nbCandidates > 1) ) { + DEBUGLOG(8, "ZSTD_DUBT_findBestMatch: candidate %u is unsorted", + matchIndex); + *unsortedMark = previousCandidate; /* the unsortedMark becomes a reversed chain, to move up back to original position */ + previousCandidate = matchIndex; + matchIndex = *nextCandidate; + nextCandidate = bt + 2*(matchIndex&btMask); + unsortedMark = bt + 2*(matchIndex&btMask) + 1; + nbCandidates --; + } + + /* nullify last candidate if it's still unsorted + * simplification, detrimental to compression ratio, beneficial for speed */ + if ( (matchIndex > unsortLimit) + && (*unsortedMark==ZSTD_DUBT_UNSORTED_MARK) ) { + DEBUGLOG(7, "ZSTD_DUBT_findBestMatch: nullify last unsorted candidate %u", + matchIndex); + *nextCandidate = *unsortedMark = 0; + } + + /* batch sort stacked candidates */ + matchIndex = previousCandidate; + while (matchIndex) { /* will end on matchIndex == 0 */ + U32* const nextCandidateIdxPtr = bt + 2*(matchIndex&btMask) + 1; + U32 const nextCandidateIdx = *nextCandidateIdxPtr; + ZSTD_insertDUBT1(ms, matchIndex, iend, + nbCandidates, unsortLimit, dictMode); + matchIndex = nextCandidateIdx; + nbCandidates++; + } + + /* find longest match */ + { size_t commonLengthSmaller = 0, commonLengthLarger = 0; + const BYTE* const dictBase = ms->window.dictBase; + const U32 dictLimit = ms->window.dictLimit; + const BYTE* const dictEnd = dictBase + dictLimit; + const BYTE* const prefixStart = base + dictLimit; + U32* smallerPtr = bt + 2*(curr&btMask); + U32* largerPtr = bt + 2*(curr&btMask) + 1; + U32 matchEndIdx = curr + 8 + 1; + U32 dummy32; /* to be nullified at the end */ + size_t bestLength = 0; + + matchIndex = hashTable[h]; + hashTable[h] = curr; /* Update Hash Table */ + + for (; nbCompares && (matchIndex > windowLow); --nbCompares) { + U32* const nextPtr = bt + 2*(matchIndex & btMask); + size_t matchLength = MIN(commonLengthSmaller, commonLengthLarger); /* guaranteed minimum nb of common bytes */ + const BYTE* match; + + if ((dictMode != ZSTD_extDict) || (matchIndex+matchLength >= dictLimit)) { + match = base + matchIndex; + matchLength += ZSTD_count(ip+matchLength, match+matchLength, iend); + } else { + match = dictBase + matchIndex; + matchLength += ZSTD_count_2segments(ip+matchLength, match+matchLength, iend, dictEnd, prefixStart); + if (matchIndex+matchLength >= dictLimit) + match = base + matchIndex; /* to prepare for next usage of match[matchLength] */ + } + + if (matchLength > bestLength) { + if (matchLength > matchEndIdx - matchIndex) + matchEndIdx = matchIndex + (U32)matchLength; + if ( (4*(int)(matchLength-bestLength)) > (int)(ZSTD_highbit32(curr - matchIndex + 1) - ZSTD_highbit32((U32)*offBasePtr)) ) + bestLength = matchLength, *offBasePtr = OFFSET_TO_OFFBASE(curr - matchIndex); + if (ip+matchLength == iend) { /* equal : no way to know if inf or sup */ + if (dictMode == ZSTD_dictMatchState) { + nbCompares = 0; /* in addition to avoiding checking any + * further in this loop, make sure we + * skip checking in the dictionary. */ + } + break; /* drop, to guarantee consistency (miss a little bit of compression) */ + } + } + + if (match[matchLength] < ip[matchLength]) { + /* match is smaller than current */ + *smallerPtr = matchIndex; /* update smaller idx */ + commonLengthSmaller = matchLength; /* all smaller will now have at least this guaranteed common length */ + if (matchIndex <= btLow) { smallerPtr=&dummy32; break; } /* beyond tree size, stop the search */ + smallerPtr = nextPtr+1; /* new "smaller" => larger of match */ + matchIndex = nextPtr[1]; /* new matchIndex larger than previous (closer to current) */ + } else { + /* match is larger than current */ + *largerPtr = matchIndex; + commonLengthLarger = matchLength; + if (matchIndex <= btLow) { largerPtr=&dummy32; break; } /* beyond tree size, stop the search */ + largerPtr = nextPtr; + matchIndex = nextPtr[0]; + } } + + *smallerPtr = *largerPtr = 0; + + assert(nbCompares <= (1U << ZSTD_SEARCHLOG_MAX)); /* Check we haven't underflowed. */ + if (dictMode == ZSTD_dictMatchState && nbCompares) { + bestLength = ZSTD_DUBT_findBetterDictMatch( + ms, ip, iend, + offBasePtr, bestLength, nbCompares, + mls, dictMode); + } + + assert(matchEndIdx > curr+8); /* ensure nextToUpdate is increased */ + ms->nextToUpdate = matchEndIdx - 8; /* skip repetitive patterns */ + if (bestLength >= MINMATCH) { + U32 const mIndex = curr - (U32)OFFBASE_TO_OFFSET(*offBasePtr); (void)mIndex; + DEBUGLOG(8, "ZSTD_DUBT_findBestMatch(%u) : found match of length %u and offsetCode %u (pos %u)", + curr, (U32)bestLength, (U32)*offBasePtr, mIndex); + } + return bestLength; + } +} + + +/** ZSTD_BtFindBestMatch() : Tree updater, providing best match */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_BtFindBestMatch( ZSTD_matchState_t* ms, + const BYTE* const ip, const BYTE* const iLimit, + size_t* offBasePtr, + const U32 mls /* template */, + const ZSTD_dictMode_e dictMode) +{ + DEBUGLOG(7, "ZSTD_BtFindBestMatch"); + if (ip < ms->window.base + ms->nextToUpdate) return 0; /* skipped area */ + ZSTD_updateDUBT(ms, ip, iLimit, mls); + return ZSTD_DUBT_findBestMatch(ms, ip, iLimit, offBasePtr, mls, dictMode); +} + +/*********************************** +* Dedicated dict search +***********************************/ + +void ZSTD_dedicatedDictSearch_lazy_loadDictionary(ZSTD_matchState_t* ms, const BYTE* const ip) +{ + const BYTE* const base = ms->window.base; + U32 const target = (U32)(ip - base); + U32* const hashTable = ms->hashTable; + U32* const chainTable = ms->chainTable; + U32 const chainSize = 1 << ms->cParams.chainLog; + U32 idx = ms->nextToUpdate; + U32 const minChain = chainSize < target - idx ? target - chainSize : idx; + U32 const bucketSize = 1 << ZSTD_LAZY_DDSS_BUCKET_LOG; + U32 const cacheSize = bucketSize - 1; + U32 const chainAttempts = (1 << ms->cParams.searchLog) - cacheSize; + U32 const chainLimit = chainAttempts > 255 ? 255 : chainAttempts; + + /* We know the hashtable is oversized by a factor of `bucketSize`. + * We are going to temporarily pretend `bucketSize == 1`, keeping only a + * single entry. We will use the rest of the space to construct a temporary + * chaintable. + */ + U32 const hashLog = ms->cParams.hashLog - ZSTD_LAZY_DDSS_BUCKET_LOG; + U32* const tmpHashTable = hashTable; + U32* const tmpChainTable = hashTable + ((size_t)1 << hashLog); + U32 const tmpChainSize = (U32)((1 << ZSTD_LAZY_DDSS_BUCKET_LOG) - 1) << hashLog; + U32 const tmpMinChain = tmpChainSize < target ? target - tmpChainSize : idx; + U32 hashIdx; + + assert(ms->cParams.chainLog <= 24); + assert(ms->cParams.hashLog > ms->cParams.chainLog); + assert(idx != 0); + assert(tmpMinChain <= minChain); + + /* fill conventional hash table and conventional chain table */ + for ( ; idx < target; idx++) { + U32 const h = (U32)ZSTD_hashPtr(base + idx, hashLog, ms->cParams.minMatch); + if (idx >= tmpMinChain) { + tmpChainTable[idx - tmpMinChain] = hashTable[h]; + } + tmpHashTable[h] = idx; + } + + /* sort chains into ddss chain table */ + { + U32 chainPos = 0; + for (hashIdx = 0; hashIdx < (1U << hashLog); hashIdx++) { + U32 count; + U32 countBeyondMinChain = 0; + U32 i = tmpHashTable[hashIdx]; + for (count = 0; i >= tmpMinChain && count < cacheSize; count++) { + /* skip through the chain to the first position that won't be + * in the hash cache bucket */ + if (i < minChain) { + countBeyondMinChain++; + } + i = tmpChainTable[i - tmpMinChain]; + } + if (count == cacheSize) { + for (count = 0; count < chainLimit;) { + if (i < minChain) { + if (!i || ++countBeyondMinChain > cacheSize) { + /* only allow pulling `cacheSize` number of entries + * into the cache or chainTable beyond `minChain`, + * to replace the entries pulled out of the + * chainTable into the cache. This lets us reach + * back further without increasing the total number + * of entries in the chainTable, guaranteeing the + * DDSS chain table will fit into the space + * allocated for the regular one. */ + break; + } + } + chainTable[chainPos++] = i; + count++; + if (i < tmpMinChain) { + break; + } + i = tmpChainTable[i - tmpMinChain]; + } + } else { + count = 0; + } + if (count) { + tmpHashTable[hashIdx] = ((chainPos - count) << 8) + count; + } else { + tmpHashTable[hashIdx] = 0; + } + } + assert(chainPos <= chainSize); /* I believe this is guaranteed... */ + } + + /* move chain pointers into the last entry of each hash bucket */ + for (hashIdx = (1 << hashLog); hashIdx; ) { + U32 const bucketIdx = --hashIdx << ZSTD_LAZY_DDSS_BUCKET_LOG; + U32 const chainPackedPointer = tmpHashTable[hashIdx]; + U32 i; + for (i = 0; i < cacheSize; i++) { + hashTable[bucketIdx + i] = 0; + } + hashTable[bucketIdx + bucketSize - 1] = chainPackedPointer; + } + + /* fill the buckets of the hash table */ + for (idx = ms->nextToUpdate; idx < target; idx++) { + U32 const h = (U32)ZSTD_hashPtr(base + idx, hashLog, ms->cParams.minMatch) + << ZSTD_LAZY_DDSS_BUCKET_LOG; + U32 i; + /* Shift hash cache down 1. */ + for (i = cacheSize - 1; i; i--) + hashTable[h + i] = hashTable[h + i - 1]; + hashTable[h] = idx; + } + + ms->nextToUpdate = target; +} + +/* Returns the longest match length found in the dedicated dict search structure. + * If none are longer than the argument ml, then ml will be returned. + */ +FORCE_INLINE_TEMPLATE +size_t ZSTD_dedicatedDictSearch_lazy_search(size_t* offsetPtr, size_t ml, U32 nbAttempts, + const ZSTD_matchState_t* const dms, + const BYTE* const ip, const BYTE* const iLimit, + const BYTE* const prefixStart, const U32 curr, + const U32 dictLimit, const size_t ddsIdx) { + const U32 ddsLowestIndex = dms->window.dictLimit; + const BYTE* const ddsBase = dms->window.base; + const BYTE* const ddsEnd = dms->window.nextSrc; + const U32 ddsSize = (U32)(ddsEnd - ddsBase); + const U32 ddsIndexDelta = dictLimit - ddsSize; + const U32 bucketSize = (1 << ZSTD_LAZY_DDSS_BUCKET_LOG); + const U32 bucketLimit = nbAttempts < bucketSize - 1 ? nbAttempts : bucketSize - 1; + U32 ddsAttempt; + U32 matchIndex; + + for (ddsAttempt = 0; ddsAttempt < bucketSize - 1; ddsAttempt++) { + PREFETCH_L1(ddsBase + dms->hashTable[ddsIdx + ddsAttempt]); + } + + { + U32 const chainPackedPointer = dms->hashTable[ddsIdx + bucketSize - 1]; + U32 const chainIndex = chainPackedPointer >> 8; + + PREFETCH_L1(&dms->chainTable[chainIndex]); + } + + for (ddsAttempt = 0; ddsAttempt < bucketLimit; ddsAttempt++) { + size_t currentMl=0; + const BYTE* match; + matchIndex = dms->hashTable[ddsIdx + ddsAttempt]; + match = ddsBase + matchIndex; + + if (!matchIndex) { + return ml; + } + + /* guaranteed by table construction */ + (void)ddsLowestIndex; + assert(matchIndex >= ddsLowestIndex); + assert(match+4 <= ddsEnd); + if (MEM_read32(match) == MEM_read32(ip)) { + /* assumption : matchIndex <= dictLimit-4 (by table construction) */ + currentMl = ZSTD_count_2segments(ip+4, match+4, iLimit, ddsEnd, prefixStart) + 4; + } + + /* save best solution */ + if (currentMl > ml) { + ml = currentMl; + *offsetPtr = OFFSET_TO_OFFBASE(curr - (matchIndex + ddsIndexDelta)); + if (ip+currentMl == iLimit) { + /* best possible, avoids read overflow on next attempt */ + return ml; + } + } + } + + { + U32 const chainPackedPointer = dms->hashTable[ddsIdx + bucketSize - 1]; + U32 chainIndex = chainPackedPointer >> 8; + U32 const chainLength = chainPackedPointer & 0xFF; + U32 const chainAttempts = nbAttempts - ddsAttempt; + U32 const chainLimit = chainAttempts > chainLength ? chainLength : chainAttempts; + U32 chainAttempt; + + for (chainAttempt = 0 ; chainAttempt < chainLimit; chainAttempt++) { + PREFETCH_L1(ddsBase + dms->chainTable[chainIndex + chainAttempt]); + } + + for (chainAttempt = 0 ; chainAttempt < chainLimit; chainAttempt++, chainIndex++) { + size_t currentMl=0; + const BYTE* match; + matchIndex = dms->chainTable[chainIndex]; + match = ddsBase + matchIndex; + + /* guaranteed by table construction */ + assert(matchIndex >= ddsLowestIndex); + assert(match+4 <= ddsEnd); + if (MEM_read32(match) == MEM_read32(ip)) { + /* assumption : matchIndex <= dictLimit-4 (by table construction) */ + currentMl = ZSTD_count_2segments(ip+4, match+4, iLimit, ddsEnd, prefixStart) + 4; + } + + /* save best solution */ + if (currentMl > ml) { + ml = currentMl; + *offsetPtr = OFFSET_TO_OFFBASE(curr - (matchIndex + ddsIndexDelta)); + if (ip+currentMl == iLimit) break; /* best possible, avoids read overflow on next attempt */ + } + } + } + return ml; +} + + +/* ********************************* +* Hash Chain +***********************************/ +#define NEXT_IN_CHAIN(d, mask) chainTable[(d) & (mask)] + +/* Update chains up to ip (excluded) + Assumption : always within prefix (i.e. not within extDict) */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +U32 ZSTD_insertAndFindFirstIndex_internal( + ZSTD_matchState_t* ms, + const ZSTD_compressionParameters* const cParams, + const BYTE* ip, U32 const mls, U32 const lazySkipping) +{ + U32* const hashTable = ms->hashTable; + const U32 hashLog = cParams->hashLog; + U32* const chainTable = ms->chainTable; + const U32 chainMask = (1 << cParams->chainLog) - 1; + const BYTE* const base = ms->window.base; + const U32 target = (U32)(ip - base); + U32 idx = ms->nextToUpdate; + + while(idx < target) { /* catch up */ + size_t const h = ZSTD_hashPtr(base+idx, hashLog, mls); + NEXT_IN_CHAIN(idx, chainMask) = hashTable[h]; + hashTable[h] = idx; + idx++; + /* Stop inserting every position when in the lazy skipping mode. */ + if (lazySkipping) + break; + } + + ms->nextToUpdate = target; + return hashTable[ZSTD_hashPtr(ip, hashLog, mls)]; +} + +U32 ZSTD_insertAndFindFirstIndex(ZSTD_matchState_t* ms, const BYTE* ip) { + const ZSTD_compressionParameters* const cParams = &ms->cParams; + return ZSTD_insertAndFindFirstIndex_internal(ms, cParams, ip, ms->cParams.minMatch, /* lazySkipping*/ 0); +} + +/* inlining is important to hardwire a hot branch (template emulation) */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_HcFindBestMatch( + ZSTD_matchState_t* ms, + const BYTE* const ip, const BYTE* const iLimit, + size_t* offsetPtr, + const U32 mls, const ZSTD_dictMode_e dictMode) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const chainTable = ms->chainTable; + const U32 chainSize = (1 << cParams->chainLog); + const U32 chainMask = chainSize-1; + const BYTE* const base = ms->window.base; + const BYTE* const dictBase = ms->window.dictBase; + const U32 dictLimit = ms->window.dictLimit; + const BYTE* const prefixStart = base + dictLimit; + const BYTE* const dictEnd = dictBase + dictLimit; + const U32 curr = (U32)(ip-base); + const U32 maxDistance = 1U << cParams->windowLog; + const U32 lowestValid = ms->window.lowLimit; + const U32 withinMaxDistance = (curr - lowestValid > maxDistance) ? curr - maxDistance : lowestValid; + const U32 isDictionary = (ms->loadedDictEnd != 0); + const U32 lowLimit = isDictionary ? lowestValid : withinMaxDistance; + const U32 minChain = curr > chainSize ? curr - chainSize : 0; + U32 nbAttempts = 1U << cParams->searchLog; + size_t ml=4-1; + + const ZSTD_matchState_t* const dms = ms->dictMatchState; + const U32 ddsHashLog = dictMode == ZSTD_dedicatedDictSearch + ? dms->cParams.hashLog - ZSTD_LAZY_DDSS_BUCKET_LOG : 0; + const size_t ddsIdx = dictMode == ZSTD_dedicatedDictSearch + ? ZSTD_hashPtr(ip, ddsHashLog, mls) << ZSTD_LAZY_DDSS_BUCKET_LOG : 0; + + U32 matchIndex; + + if (dictMode == ZSTD_dedicatedDictSearch) { + const U32* entry = &dms->hashTable[ddsIdx]; + PREFETCH_L1(entry); + } + + /* HC4 match finder */ + matchIndex = ZSTD_insertAndFindFirstIndex_internal(ms, cParams, ip, mls, ms->lazySkipping); + + for ( ; (matchIndex>=lowLimit) & (nbAttempts>0) ; nbAttempts--) { + size_t currentMl=0; + if ((dictMode != ZSTD_extDict) || matchIndex >= dictLimit) { + const BYTE* const match = base + matchIndex; + assert(matchIndex >= dictLimit); /* ensures this is true if dictMode != ZSTD_extDict */ + /* read 4B starting from (match + ml + 1 - sizeof(U32)) */ + if (MEM_read32(match + ml - 3) == MEM_read32(ip + ml - 3)) /* potentially better */ + currentMl = ZSTD_count(ip, match, iLimit); + } else { + const BYTE* const match = dictBase + matchIndex; + assert(match+4 <= dictEnd); + if (MEM_read32(match) == MEM_read32(ip)) /* assumption : matchIndex <= dictLimit-4 (by table construction) */ + currentMl = ZSTD_count_2segments(ip+4, match+4, iLimit, dictEnd, prefixStart) + 4; + } + + /* save best solution */ + if (currentMl > ml) { + ml = currentMl; + *offsetPtr = OFFSET_TO_OFFBASE(curr - matchIndex); + if (ip+currentMl == iLimit) break; /* best possible, avoids read overflow on next attempt */ + } + + if (matchIndex <= minChain) break; + matchIndex = NEXT_IN_CHAIN(matchIndex, chainMask); + } + + assert(nbAttempts <= (1U << ZSTD_SEARCHLOG_MAX)); /* Check we haven't underflowed. */ + if (dictMode == ZSTD_dedicatedDictSearch) { + ml = ZSTD_dedicatedDictSearch_lazy_search(offsetPtr, ml, nbAttempts, dms, + ip, iLimit, prefixStart, curr, dictLimit, ddsIdx); + } else if (dictMode == ZSTD_dictMatchState) { + const U32* const dmsChainTable = dms->chainTable; + const U32 dmsChainSize = (1 << dms->cParams.chainLog); + const U32 dmsChainMask = dmsChainSize - 1; + const U32 dmsLowestIndex = dms->window.dictLimit; + const BYTE* const dmsBase = dms->window.base; + const BYTE* const dmsEnd = dms->window.nextSrc; + const U32 dmsSize = (U32)(dmsEnd - dmsBase); + const U32 dmsIndexDelta = dictLimit - dmsSize; + const U32 dmsMinChain = dmsSize > dmsChainSize ? dmsSize - dmsChainSize : 0; + + matchIndex = dms->hashTable[ZSTD_hashPtr(ip, dms->cParams.hashLog, mls)]; + + for ( ; (matchIndex>=dmsLowestIndex) & (nbAttempts>0) ; nbAttempts--) { + size_t currentMl=0; + const BYTE* const match = dmsBase + matchIndex; + assert(match+4 <= dmsEnd); + if (MEM_read32(match) == MEM_read32(ip)) /* assumption : matchIndex <= dictLimit-4 (by table construction) */ + currentMl = ZSTD_count_2segments(ip+4, match+4, iLimit, dmsEnd, prefixStart) + 4; + + /* save best solution */ + if (currentMl > ml) { + ml = currentMl; + assert(curr > matchIndex + dmsIndexDelta); + *offsetPtr = OFFSET_TO_OFFBASE(curr - (matchIndex + dmsIndexDelta)); + if (ip+currentMl == iLimit) break; /* best possible, avoids read overflow on next attempt */ + } + + if (matchIndex <= dmsMinChain) break; + + matchIndex = dmsChainTable[matchIndex & dmsChainMask]; + } + } + + return ml; +} + +/* ********************************* +* (SIMD) Row-based matchfinder +***********************************/ +/* Constants for row-based hash */ +#define ZSTD_ROW_HASH_TAG_MASK ((1u << ZSTD_ROW_HASH_TAG_BITS) - 1) +#define ZSTD_ROW_HASH_MAX_ENTRIES 64 /* absolute maximum number of entries per row, for all configurations */ + +#define ZSTD_ROW_HASH_CACHE_MASK (ZSTD_ROW_HASH_CACHE_SIZE - 1) + +typedef U64 ZSTD_VecMask; /* Clarifies when we are interacting with a U64 representing a mask of matches */ + +/* ZSTD_VecMask_next(): + * Starting from the LSB, returns the idx of the next non-zero bit. + * Basically counting the nb of trailing zeroes. + */ +MEM_STATIC U32 ZSTD_VecMask_next(ZSTD_VecMask val) { + return ZSTD_countTrailingZeros64(val); +} + +/* ZSTD_row_nextIndex(): + * Returns the next index to insert at within a tagTable row, and updates the "head" + * value to reflect the update. Essentially cycles backwards from [1, {entries per row}) + */ +FORCE_INLINE_TEMPLATE U32 ZSTD_row_nextIndex(BYTE* const tagRow, U32 const rowMask) { + U32 next = (*tagRow-1) & rowMask; + next += (next == 0) ? rowMask : 0; /* skip first position */ + *tagRow = (BYTE)next; + return next; +} + +/* ZSTD_isAligned(): + * Checks that a pointer is aligned to "align" bytes which must be a power of 2. + */ +MEM_STATIC int ZSTD_isAligned(void const* ptr, size_t align) { + assert((align & (align - 1)) == 0); + return (((size_t)ptr) & (align - 1)) == 0; +} + +/* ZSTD_row_prefetch(): + * Performs prefetching for the hashTable and tagTable at a given row. + */ +FORCE_INLINE_TEMPLATE void ZSTD_row_prefetch(U32 const* hashTable, BYTE const* tagTable, U32 const relRow, U32 const rowLog) { + PREFETCH_L1(hashTable + relRow); + if (rowLog >= 5) { + PREFETCH_L1(hashTable + relRow + 16); + /* Note: prefetching more of the hash table does not appear to be beneficial for 128-entry rows */ + } + PREFETCH_L1(tagTable + relRow); + if (rowLog == 6) { + PREFETCH_L1(tagTable + relRow + 32); + } + assert(rowLog == 4 || rowLog == 5 || rowLog == 6); + assert(ZSTD_isAligned(hashTable + relRow, 64)); /* prefetched hash row always 64-byte aligned */ + assert(ZSTD_isAligned(tagTable + relRow, (size_t)1 << rowLog)); /* prefetched tagRow sits on correct multiple of bytes (32,64,128) */ +} + +/* ZSTD_row_fillHashCache(): + * Fill up the hash cache starting at idx, prefetching up to ZSTD_ROW_HASH_CACHE_SIZE entries, + * but not beyond iLimit. + */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_row_fillHashCache(ZSTD_matchState_t* ms, const BYTE* base, + U32 const rowLog, U32 const mls, + U32 idx, const BYTE* const iLimit) +{ + U32 const* const hashTable = ms->hashTable; + BYTE const* const tagTable = ms->tagTable; + U32 const hashLog = ms->rowHashLog; + U32 const maxElemsToPrefetch = (base + idx) > iLimit ? 0 : (U32)(iLimit - (base + idx) + 1); + U32 const lim = idx + MIN(ZSTD_ROW_HASH_CACHE_SIZE, maxElemsToPrefetch); + + for (; idx < lim; ++idx) { + U32 const hash = (U32)ZSTD_hashPtrSalted(base + idx, hashLog + ZSTD_ROW_HASH_TAG_BITS, mls, ms->hashSalt); + U32 const row = (hash >> ZSTD_ROW_HASH_TAG_BITS) << rowLog; + ZSTD_row_prefetch(hashTable, tagTable, row, rowLog); + ms->hashCache[idx & ZSTD_ROW_HASH_CACHE_MASK] = hash; + } + + DEBUGLOG(6, "ZSTD_row_fillHashCache(): [%u %u %u %u %u %u %u %u]", ms->hashCache[0], ms->hashCache[1], + ms->hashCache[2], ms->hashCache[3], ms->hashCache[4], + ms->hashCache[5], ms->hashCache[6], ms->hashCache[7]); +} + +/* ZSTD_row_nextCachedHash(): + * Returns the hash of base + idx, and replaces the hash in the hash cache with the byte at + * base + idx + ZSTD_ROW_HASH_CACHE_SIZE. Also prefetches the appropriate rows from hashTable and tagTable. + */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +U32 ZSTD_row_nextCachedHash(U32* cache, U32 const* hashTable, + BYTE const* tagTable, BYTE const* base, + U32 idx, U32 const hashLog, + U32 const rowLog, U32 const mls, + U64 const hashSalt) +{ + U32 const newHash = (U32)ZSTD_hashPtrSalted(base+idx+ZSTD_ROW_HASH_CACHE_SIZE, hashLog + ZSTD_ROW_HASH_TAG_BITS, mls, hashSalt); + U32 const row = (newHash >> ZSTD_ROW_HASH_TAG_BITS) << rowLog; + ZSTD_row_prefetch(hashTable, tagTable, row, rowLog); + { U32 const hash = cache[idx & ZSTD_ROW_HASH_CACHE_MASK]; + cache[idx & ZSTD_ROW_HASH_CACHE_MASK] = newHash; + return hash; + } +} + +/* ZSTD_row_update_internalImpl(): + * Updates the hash table with positions starting from updateStartIdx until updateEndIdx. + */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_row_update_internalImpl(ZSTD_matchState_t* ms, + U32 updateStartIdx, U32 const updateEndIdx, + U32 const mls, U32 const rowLog, + U32 const rowMask, U32 const useCache) +{ + U32* const hashTable = ms->hashTable; + BYTE* const tagTable = ms->tagTable; + U32 const hashLog = ms->rowHashLog; + const BYTE* const base = ms->window.base; + + DEBUGLOG(6, "ZSTD_row_update_internalImpl(): updateStartIdx=%u, updateEndIdx=%u", updateStartIdx, updateEndIdx); + for (; updateStartIdx < updateEndIdx; ++updateStartIdx) { + U32 const hash = useCache ? ZSTD_row_nextCachedHash(ms->hashCache, hashTable, tagTable, base, updateStartIdx, hashLog, rowLog, mls, ms->hashSalt) + : (U32)ZSTD_hashPtrSalted(base + updateStartIdx, hashLog + ZSTD_ROW_HASH_TAG_BITS, mls, ms->hashSalt); + U32 const relRow = (hash >> ZSTD_ROW_HASH_TAG_BITS) << rowLog; + U32* const row = hashTable + relRow; + BYTE* tagRow = tagTable + relRow; + U32 const pos = ZSTD_row_nextIndex(tagRow, rowMask); + + assert(hash == ZSTD_hashPtrSalted(base + updateStartIdx, hashLog + ZSTD_ROW_HASH_TAG_BITS, mls, ms->hashSalt)); + tagRow[pos] = hash & ZSTD_ROW_HASH_TAG_MASK; + row[pos] = updateStartIdx; + } +} + +/* ZSTD_row_update_internal(): + * Inserts the byte at ip into the appropriate position in the hash table, and updates ms->nextToUpdate. + * Skips sections of long matches as is necessary. + */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_row_update_internal(ZSTD_matchState_t* ms, const BYTE* ip, + U32 const mls, U32 const rowLog, + U32 const rowMask, U32 const useCache) +{ + U32 idx = ms->nextToUpdate; + const BYTE* const base = ms->window.base; + const U32 target = (U32)(ip - base); + const U32 kSkipThreshold = 384; + const U32 kMaxMatchStartPositionsToUpdate = 96; + const U32 kMaxMatchEndPositionsToUpdate = 32; + + if (useCache) { + /* Only skip positions when using hash cache, i.e. + * if we are loading a dict, don't skip anything. + * If we decide to skip, then we only update a set number + * of positions at the beginning and end of the match. + */ + if (UNLIKELY(target - idx > kSkipThreshold)) { + U32 const bound = idx + kMaxMatchStartPositionsToUpdate; + ZSTD_row_update_internalImpl(ms, idx, bound, mls, rowLog, rowMask, useCache); + idx = target - kMaxMatchEndPositionsToUpdate; + ZSTD_row_fillHashCache(ms, base, rowLog, mls, idx, ip+1); + } + } + assert(target >= idx); + ZSTD_row_update_internalImpl(ms, idx, target, mls, rowLog, rowMask, useCache); + ms->nextToUpdate = target; +} + +/* ZSTD_row_update(): + * External wrapper for ZSTD_row_update_internal(). Used for filling the hashtable during dictionary + * processing. + */ +void ZSTD_row_update(ZSTD_matchState_t* const ms, const BYTE* ip) { + const U32 rowLog = BOUNDED(4, ms->cParams.searchLog, 6); + const U32 rowMask = (1u << rowLog) - 1; + const U32 mls = MIN(ms->cParams.minMatch, 6 /* mls caps out at 6 */); + + DEBUGLOG(5, "ZSTD_row_update(), rowLog=%u", rowLog); + ZSTD_row_update_internal(ms, ip, mls, rowLog, rowMask, 0 /* don't use cache */); +} + +/* Returns the mask width of bits group of which will be set to 1. Given not all + * architectures have easy movemask instruction, this helps to iterate over + * groups of bits easier and faster. + */ +FORCE_INLINE_TEMPLATE U32 +ZSTD_row_matchMaskGroupWidth(const U32 rowEntries) +{ + assert((rowEntries == 16) || (rowEntries == 32) || rowEntries == 64); + assert(rowEntries <= ZSTD_ROW_HASH_MAX_ENTRIES); + (void)rowEntries; +#if defined(ZSTD_ARCH_ARM_NEON) + /* NEON path only works for little endian */ + if (!MEM_isLittleEndian()) { + return 1; + } + if (rowEntries == 16) { + return 4; + } + if (rowEntries == 32) { + return 2; + } + if (rowEntries == 64) { + return 1; + } +#endif + return 1; +} + +#if defined(ZSTD_ARCH_X86_SSE2) +FORCE_INLINE_TEMPLATE ZSTD_VecMask +ZSTD_row_getSSEMask(int nbChunks, const BYTE* const src, const BYTE tag, const U32 head) +{ + const __m128i comparisonMask = _mm_set1_epi8((char)tag); + int matches[4] = {0}; + int i; + assert(nbChunks == 1 || nbChunks == 2 || nbChunks == 4); + for (i=0; i> chunkSize; + do { + size_t chunk = MEM_readST(&src[i]); + chunk ^= splatChar; + chunk = (((chunk | x80) - x01) | chunk) & x80; + matches <<= chunkSize; + matches |= (chunk * extractMagic) >> shiftAmount; + i -= chunkSize; + } while (i >= 0); + } else { /* big endian: reverse bits during extraction */ + const size_t msb = xFF ^ (xFF >> 1); + const size_t extractMagic = (msb / 0x1FF) | msb; + do { + size_t chunk = MEM_readST(&src[i]); + chunk ^= splatChar; + chunk = (((chunk | x80) - x01) | chunk) & x80; + matches <<= chunkSize; + matches |= ((chunk >> 7) * extractMagic) >> shiftAmount; + i -= chunkSize; + } while (i >= 0); + } + matches = ~matches; + if (rowEntries == 16) { + return ZSTD_rotateRight_U16((U16)matches, headGrouped); + } else if (rowEntries == 32) { + return ZSTD_rotateRight_U32((U32)matches, headGrouped); + } else { + return ZSTD_rotateRight_U64((U64)matches, headGrouped); + } + } +#endif +} + +/* The high-level approach of the SIMD row based match finder is as follows: + * - Figure out where to insert the new entry: + * - Generate a hash for current input posistion and split it into a one byte of tag and `rowHashLog` bits of index. + * - The hash is salted by a value that changes on every contex reset, so when the same table is used + * we will avoid collisions that would otherwise slow us down by intorducing phantom matches. + * - The hashTable is effectively split into groups or "rows" of 15 or 31 entries of U32, and the index determines + * which row to insert into. + * - Determine the correct position within the row to insert the entry into. Each row of 15 or 31 can + * be considered as a circular buffer with a "head" index that resides in the tagTable (overall 16 or 32 bytes + * per row). + * - Use SIMD to efficiently compare the tags in the tagTable to the 1-byte tag calculated for the position and + * generate a bitfield that we can cycle through to check the collisions in the hash table. + * - Pick the longest match. + * - Insert the tag into the equivalent row and position in the tagTable. + */ +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_RowFindBestMatch( + ZSTD_matchState_t* ms, + const BYTE* const ip, const BYTE* const iLimit, + size_t* offsetPtr, + const U32 mls, const ZSTD_dictMode_e dictMode, + const U32 rowLog) +{ + U32* const hashTable = ms->hashTable; + BYTE* const tagTable = ms->tagTable; + U32* const hashCache = ms->hashCache; + const U32 hashLog = ms->rowHashLog; + const ZSTD_compressionParameters* const cParams = &ms->cParams; + const BYTE* const base = ms->window.base; + const BYTE* const dictBase = ms->window.dictBase; + const U32 dictLimit = ms->window.dictLimit; + const BYTE* const prefixStart = base + dictLimit; + const BYTE* const dictEnd = dictBase + dictLimit; + const U32 curr = (U32)(ip-base); + const U32 maxDistance = 1U << cParams->windowLog; + const U32 lowestValid = ms->window.lowLimit; + const U32 withinMaxDistance = (curr - lowestValid > maxDistance) ? curr - maxDistance : lowestValid; + const U32 isDictionary = (ms->loadedDictEnd != 0); + const U32 lowLimit = isDictionary ? lowestValid : withinMaxDistance; + const U32 rowEntries = (1U << rowLog); + const U32 rowMask = rowEntries - 1; + const U32 cappedSearchLog = MIN(cParams->searchLog, rowLog); /* nb of searches is capped at nb entries per row */ + const U32 groupWidth = ZSTD_row_matchMaskGroupWidth(rowEntries); + const U64 hashSalt = ms->hashSalt; + U32 nbAttempts = 1U << cappedSearchLog; + size_t ml=4-1; + U32 hash; + + /* DMS/DDS variables that may be referenced laster */ + const ZSTD_matchState_t* const dms = ms->dictMatchState; + + /* Initialize the following variables to satisfy static analyzer */ + size_t ddsIdx = 0; + U32 ddsExtraAttempts = 0; /* cctx hash tables are limited in searches, but allow extra searches into DDS */ + U32 dmsTag = 0; + U32* dmsRow = NULL; + BYTE* dmsTagRow = NULL; + + if (dictMode == ZSTD_dedicatedDictSearch) { + const U32 ddsHashLog = dms->cParams.hashLog - ZSTD_LAZY_DDSS_BUCKET_LOG; + { /* Prefetch DDS hashtable entry */ + ddsIdx = ZSTD_hashPtr(ip, ddsHashLog, mls) << ZSTD_LAZY_DDSS_BUCKET_LOG; + PREFETCH_L1(&dms->hashTable[ddsIdx]); + } + ddsExtraAttempts = cParams->searchLog > rowLog ? 1U << (cParams->searchLog - rowLog) : 0; + } + + if (dictMode == ZSTD_dictMatchState) { + /* Prefetch DMS rows */ + U32* const dmsHashTable = dms->hashTable; + BYTE* const dmsTagTable = dms->tagTable; + U32 const dmsHash = (U32)ZSTD_hashPtr(ip, dms->rowHashLog + ZSTD_ROW_HASH_TAG_BITS, mls); + U32 const dmsRelRow = (dmsHash >> ZSTD_ROW_HASH_TAG_BITS) << rowLog; + dmsTag = dmsHash & ZSTD_ROW_HASH_TAG_MASK; + dmsTagRow = (BYTE*)(dmsTagTable + dmsRelRow); + dmsRow = dmsHashTable + dmsRelRow; + ZSTD_row_prefetch(dmsHashTable, dmsTagTable, dmsRelRow, rowLog); + } + + /* Update the hashTable and tagTable up to (but not including) ip */ + if (!ms->lazySkipping) { + ZSTD_row_update_internal(ms, ip, mls, rowLog, rowMask, 1 /* useCache */); + hash = ZSTD_row_nextCachedHash(hashCache, hashTable, tagTable, base, curr, hashLog, rowLog, mls, hashSalt); + } else { + /* Stop inserting every position when in the lazy skipping mode. + * The hash cache is also not kept up to date in this mode. + */ + hash = (U32)ZSTD_hashPtrSalted(ip, hashLog + ZSTD_ROW_HASH_TAG_BITS, mls, hashSalt); + ms->nextToUpdate = curr; + } + ms->hashSaltEntropy += hash; /* collect salt entropy */ + + { /* Get the hash for ip, compute the appropriate row */ + U32 const relRow = (hash >> ZSTD_ROW_HASH_TAG_BITS) << rowLog; + U32 const tag = hash & ZSTD_ROW_HASH_TAG_MASK; + U32* const row = hashTable + relRow; + BYTE* tagRow = (BYTE*)(tagTable + relRow); + U32 const headGrouped = (*tagRow & rowMask) * groupWidth; + U32 matchBuffer[ZSTD_ROW_HASH_MAX_ENTRIES]; + size_t numMatches = 0; + size_t currMatch = 0; + ZSTD_VecMask matches = ZSTD_row_getMatchMask(tagRow, (BYTE)tag, headGrouped, rowEntries); + + /* Cycle through the matches and prefetch */ + for (; (matches > 0) && (nbAttempts > 0); matches &= (matches - 1)) { + U32 const matchPos = ((headGrouped + ZSTD_VecMask_next(matches)) / groupWidth) & rowMask; + U32 const matchIndex = row[matchPos]; + if(matchPos == 0) continue; + assert(numMatches < rowEntries); + if (matchIndex < lowLimit) + break; + if ((dictMode != ZSTD_extDict) || matchIndex >= dictLimit) { + PREFETCH_L1(base + matchIndex); + } else { + PREFETCH_L1(dictBase + matchIndex); + } + matchBuffer[numMatches++] = matchIndex; + --nbAttempts; + } + + /* Speed opt: insert current byte into hashtable too. This allows us to avoid one iteration of the loop + in ZSTD_row_update_internal() at the next search. */ + { + U32 const pos = ZSTD_row_nextIndex(tagRow, rowMask); + tagRow[pos] = (BYTE)tag; + row[pos] = ms->nextToUpdate++; + } + + /* Return the longest match */ + for (; currMatch < numMatches; ++currMatch) { + U32 const matchIndex = matchBuffer[currMatch]; + size_t currentMl=0; + assert(matchIndex < curr); + assert(matchIndex >= lowLimit); + + if ((dictMode != ZSTD_extDict) || matchIndex >= dictLimit) { + const BYTE* const match = base + matchIndex; + assert(matchIndex >= dictLimit); /* ensures this is true if dictMode != ZSTD_extDict */ + /* read 4B starting from (match + ml + 1 - sizeof(U32)) */ + if (MEM_read32(match + ml - 3) == MEM_read32(ip + ml - 3)) /* potentially better */ + currentMl = ZSTD_count(ip, match, iLimit); + } else { + const BYTE* const match = dictBase + matchIndex; + assert(match+4 <= dictEnd); + if (MEM_read32(match) == MEM_read32(ip)) /* assumption : matchIndex <= dictLimit-4 (by table construction) */ + currentMl = ZSTD_count_2segments(ip+4, match+4, iLimit, dictEnd, prefixStart) + 4; + } + + /* Save best solution */ + if (currentMl > ml) { + ml = currentMl; + *offsetPtr = OFFSET_TO_OFFBASE(curr - matchIndex); + if (ip+currentMl == iLimit) break; /* best possible, avoids read overflow on next attempt */ + } + } + } + + assert(nbAttempts <= (1U << ZSTD_SEARCHLOG_MAX)); /* Check we haven't underflowed. */ + if (dictMode == ZSTD_dedicatedDictSearch) { + ml = ZSTD_dedicatedDictSearch_lazy_search(offsetPtr, ml, nbAttempts + ddsExtraAttempts, dms, + ip, iLimit, prefixStart, curr, dictLimit, ddsIdx); + } else if (dictMode == ZSTD_dictMatchState) { + /* TODO: Measure and potentially add prefetching to DMS */ + const U32 dmsLowestIndex = dms->window.dictLimit; + const BYTE* const dmsBase = dms->window.base; + const BYTE* const dmsEnd = dms->window.nextSrc; + const U32 dmsSize = (U32)(dmsEnd - dmsBase); + const U32 dmsIndexDelta = dictLimit - dmsSize; + + { U32 const headGrouped = (*dmsTagRow & rowMask) * groupWidth; + U32 matchBuffer[ZSTD_ROW_HASH_MAX_ENTRIES]; + size_t numMatches = 0; + size_t currMatch = 0; + ZSTD_VecMask matches = ZSTD_row_getMatchMask(dmsTagRow, (BYTE)dmsTag, headGrouped, rowEntries); + + for (; (matches > 0) && (nbAttempts > 0); matches &= (matches - 1)) { + U32 const matchPos = ((headGrouped + ZSTD_VecMask_next(matches)) / groupWidth) & rowMask; + U32 const matchIndex = dmsRow[matchPos]; + if(matchPos == 0) continue; + if (matchIndex < dmsLowestIndex) + break; + PREFETCH_L1(dmsBase + matchIndex); + matchBuffer[numMatches++] = matchIndex; + --nbAttempts; + } + + /* Return the longest match */ + for (; currMatch < numMatches; ++currMatch) { + U32 const matchIndex = matchBuffer[currMatch]; + size_t currentMl=0; + assert(matchIndex >= dmsLowestIndex); + assert(matchIndex < curr); + + { const BYTE* const match = dmsBase + matchIndex; + assert(match+4 <= dmsEnd); + if (MEM_read32(match) == MEM_read32(ip)) + currentMl = ZSTD_count_2segments(ip+4, match+4, iLimit, dmsEnd, prefixStart) + 4; + } + + if (currentMl > ml) { + ml = currentMl; + assert(curr > matchIndex + dmsIndexDelta); + *offsetPtr = OFFSET_TO_OFFBASE(curr - (matchIndex + dmsIndexDelta)); + if (ip+currentMl == iLimit) break; + } + } + } + } + return ml; +} + + +/** + * Generate search functions templated on (dictMode, mls, rowLog). + * These functions are outlined for code size & compilation time. + * ZSTD_searchMax() dispatches to the correct implementation function. + * + * TODO: The start of the search function involves loading and calculating a + * bunch of constants from the ZSTD_matchState_t. These computations could be + * done in an initialization function, and saved somewhere in the match state. + * Then we could pass a pointer to the saved state instead of the match state, + * and avoid duplicate computations. + * + * TODO: Move the match re-winding into searchMax. This improves compression + * ratio, and unlocks further simplifications with the next TODO. + * + * TODO: Try moving the repcode search into searchMax. After the re-winding + * and repcode search are in searchMax, there is no more logic in the match + * finder loop that requires knowledge about the dictMode. So we should be + * able to avoid force inlining it, and we can join the extDict loop with + * the single segment loop. It should go in searchMax instead of its own + * function to avoid having multiple virtual function calls per search. + */ + +#define ZSTD_BT_SEARCH_FN(dictMode, mls) ZSTD_BtFindBestMatch_##dictMode##_##mls +#define ZSTD_HC_SEARCH_FN(dictMode, mls) ZSTD_HcFindBestMatch_##dictMode##_##mls +#define ZSTD_ROW_SEARCH_FN(dictMode, mls, rowLog) ZSTD_RowFindBestMatch_##dictMode##_##mls##_##rowLog + +#define ZSTD_SEARCH_FN_ATTRS FORCE_NOINLINE + +#define GEN_ZSTD_BT_SEARCH_FN(dictMode, mls) \ + ZSTD_SEARCH_FN_ATTRS size_t ZSTD_BT_SEARCH_FN(dictMode, mls)( \ + ZSTD_matchState_t* ms, \ + const BYTE* ip, const BYTE* const iLimit, \ + size_t* offBasePtr) \ + { \ + assert(MAX(4, MIN(6, ms->cParams.minMatch)) == mls); \ + return ZSTD_BtFindBestMatch(ms, ip, iLimit, offBasePtr, mls, ZSTD_##dictMode); \ + } \ + +#define GEN_ZSTD_HC_SEARCH_FN(dictMode, mls) \ + ZSTD_SEARCH_FN_ATTRS size_t ZSTD_HC_SEARCH_FN(dictMode, mls)( \ + ZSTD_matchState_t* ms, \ + const BYTE* ip, const BYTE* const iLimit, \ + size_t* offsetPtr) \ + { \ + assert(MAX(4, MIN(6, ms->cParams.minMatch)) == mls); \ + return ZSTD_HcFindBestMatch(ms, ip, iLimit, offsetPtr, mls, ZSTD_##dictMode); \ + } \ + +#define GEN_ZSTD_ROW_SEARCH_FN(dictMode, mls, rowLog) \ + ZSTD_SEARCH_FN_ATTRS size_t ZSTD_ROW_SEARCH_FN(dictMode, mls, rowLog)( \ + ZSTD_matchState_t* ms, \ + const BYTE* ip, const BYTE* const iLimit, \ + size_t* offsetPtr) \ + { \ + assert(MAX(4, MIN(6, ms->cParams.minMatch)) == mls); \ + assert(MAX(4, MIN(6, ms->cParams.searchLog)) == rowLog); \ + return ZSTD_RowFindBestMatch(ms, ip, iLimit, offsetPtr, mls, ZSTD_##dictMode, rowLog); \ + } \ + +#define ZSTD_FOR_EACH_ROWLOG(X, dictMode, mls) \ + X(dictMode, mls, 4) \ + X(dictMode, mls, 5) \ + X(dictMode, mls, 6) + +#define ZSTD_FOR_EACH_MLS_ROWLOG(X, dictMode) \ + ZSTD_FOR_EACH_ROWLOG(X, dictMode, 4) \ + ZSTD_FOR_EACH_ROWLOG(X, dictMode, 5) \ + ZSTD_FOR_EACH_ROWLOG(X, dictMode, 6) + +#define ZSTD_FOR_EACH_MLS(X, dictMode) \ + X(dictMode, 4) \ + X(dictMode, 5) \ + X(dictMode, 6) + +#define ZSTD_FOR_EACH_DICT_MODE(X, ...) \ + X(__VA_ARGS__, noDict) \ + X(__VA_ARGS__, extDict) \ + X(__VA_ARGS__, dictMatchState) \ + X(__VA_ARGS__, dedicatedDictSearch) + +/* Generate row search fns for each combination of (dictMode, mls, rowLog) */ +ZSTD_FOR_EACH_DICT_MODE(ZSTD_FOR_EACH_MLS_ROWLOG, GEN_ZSTD_ROW_SEARCH_FN) +/* Generate binary Tree search fns for each combination of (dictMode, mls) */ +ZSTD_FOR_EACH_DICT_MODE(ZSTD_FOR_EACH_MLS, GEN_ZSTD_BT_SEARCH_FN) +/* Generate hash chain search fns for each combination of (dictMode, mls) */ +ZSTD_FOR_EACH_DICT_MODE(ZSTD_FOR_EACH_MLS, GEN_ZSTD_HC_SEARCH_FN) + +typedef enum { search_hashChain=0, search_binaryTree=1, search_rowHash=2 } searchMethod_e; + +#define GEN_ZSTD_CALL_BT_SEARCH_FN(dictMode, mls) \ + case mls: \ + return ZSTD_BT_SEARCH_FN(dictMode, mls)(ms, ip, iend, offsetPtr); +#define GEN_ZSTD_CALL_HC_SEARCH_FN(dictMode, mls) \ + case mls: \ + return ZSTD_HC_SEARCH_FN(dictMode, mls)(ms, ip, iend, offsetPtr); +#define GEN_ZSTD_CALL_ROW_SEARCH_FN(dictMode, mls, rowLog) \ + case rowLog: \ + return ZSTD_ROW_SEARCH_FN(dictMode, mls, rowLog)(ms, ip, iend, offsetPtr); + +#define ZSTD_SWITCH_MLS(X, dictMode) \ + switch (mls) { \ + ZSTD_FOR_EACH_MLS(X, dictMode) \ + } + +#define ZSTD_SWITCH_ROWLOG(dictMode, mls) \ + case mls: \ + switch (rowLog) { \ + ZSTD_FOR_EACH_ROWLOG(GEN_ZSTD_CALL_ROW_SEARCH_FN, dictMode, mls) \ + } \ + ZSTD_UNREACHABLE; \ + break; + +#define ZSTD_SWITCH_SEARCH_METHOD(dictMode) \ + switch (searchMethod) { \ + case search_hashChain: \ + ZSTD_SWITCH_MLS(GEN_ZSTD_CALL_HC_SEARCH_FN, dictMode) \ + break; \ + case search_binaryTree: \ + ZSTD_SWITCH_MLS(GEN_ZSTD_CALL_BT_SEARCH_FN, dictMode) \ + break; \ + case search_rowHash: \ + ZSTD_SWITCH_MLS(ZSTD_SWITCH_ROWLOG, dictMode) \ + break; \ + } \ + ZSTD_UNREACHABLE; + +/** + * Searches for the longest match at @p ip. + * Dispatches to the correct implementation function based on the + * (searchMethod, dictMode, mls, rowLog). We use switch statements + * here instead of using an indirect function call through a function + * pointer because after Spectre and Meltdown mitigations, indirect + * function calls can be very costly, especially in the kernel. + * + * NOTE: dictMode and searchMethod should be templated, so those switch + * statements should be optimized out. Only the mls & rowLog switches + * should be left. + * + * @param ms The match state. + * @param ip The position to search at. + * @param iend The end of the input data. + * @param[out] offsetPtr Stores the match offset into this pointer. + * @param mls The minimum search length, in the range [4, 6]. + * @param rowLog The row log (if applicable), in the range [4, 6]. + * @param searchMethod The search method to use (templated). + * @param dictMode The dictMode (templated). + * + * @returns The length of the longest match found, or < mls if no match is found. + * If a match is found its offset is stored in @p offsetPtr. + */ +FORCE_INLINE_TEMPLATE size_t ZSTD_searchMax( + ZSTD_matchState_t* ms, + const BYTE* ip, + const BYTE* iend, + size_t* offsetPtr, + U32 const mls, + U32 const rowLog, + searchMethod_e const searchMethod, + ZSTD_dictMode_e const dictMode) +{ + if (dictMode == ZSTD_noDict) { + ZSTD_SWITCH_SEARCH_METHOD(noDict) + } else if (dictMode == ZSTD_extDict) { + ZSTD_SWITCH_SEARCH_METHOD(extDict) + } else if (dictMode == ZSTD_dictMatchState) { + ZSTD_SWITCH_SEARCH_METHOD(dictMatchState) + } else if (dictMode == ZSTD_dedicatedDictSearch) { + ZSTD_SWITCH_SEARCH_METHOD(dedicatedDictSearch) + } + ZSTD_UNREACHABLE; + return 0; +} + +/* ******************************* +* Common parser - lazy strategy +*********************************/ + +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_compressBlock_lazy_generic( + ZSTD_matchState_t* ms, seqStore_t* seqStore, + U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize, + const searchMethod_e searchMethod, const U32 depth, + ZSTD_dictMode_e const dictMode) +{ + const BYTE* const istart = (const BYTE*)src; + const BYTE* ip = istart; + const BYTE* anchor = istart; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = (searchMethod == search_rowHash) ? iend - 8 - ZSTD_ROW_HASH_CACHE_SIZE : iend - 8; + const BYTE* const base = ms->window.base; + const U32 prefixLowestIndex = ms->window.dictLimit; + const BYTE* const prefixLowest = base + prefixLowestIndex; + const U32 mls = BOUNDED(4, ms->cParams.minMatch, 6); + const U32 rowLog = BOUNDED(4, ms->cParams.searchLog, 6); + + U32 offset_1 = rep[0], offset_2 = rep[1]; + U32 offsetSaved1 = 0, offsetSaved2 = 0; + + const int isDMS = dictMode == ZSTD_dictMatchState; + const int isDDS = dictMode == ZSTD_dedicatedDictSearch; + const int isDxS = isDMS || isDDS; + const ZSTD_matchState_t* const dms = ms->dictMatchState; + const U32 dictLowestIndex = isDxS ? dms->window.dictLimit : 0; + const BYTE* const dictBase = isDxS ? dms->window.base : NULL; + const BYTE* const dictLowest = isDxS ? dictBase + dictLowestIndex : NULL; + const BYTE* const dictEnd = isDxS ? dms->window.nextSrc : NULL; + const U32 dictIndexDelta = isDxS ? + prefixLowestIndex - (U32)(dictEnd - dictBase) : + 0; + const U32 dictAndPrefixLength = (U32)((ip - prefixLowest) + (dictEnd - dictLowest)); + + DEBUGLOG(5, "ZSTD_compressBlock_lazy_generic (dictMode=%u) (searchFunc=%u)", (U32)dictMode, (U32)searchMethod); + ip += (dictAndPrefixLength == 0); + if (dictMode == ZSTD_noDict) { + U32 const curr = (U32)(ip - base); + U32 const windowLow = ZSTD_getLowestPrefixIndex(ms, curr, ms->cParams.windowLog); + U32 const maxRep = curr - windowLow; + if (offset_2 > maxRep) offsetSaved2 = offset_2, offset_2 = 0; + if (offset_1 > maxRep) offsetSaved1 = offset_1, offset_1 = 0; + } + if (isDxS) { + /* dictMatchState repCode checks don't currently handle repCode == 0 + * disabling. */ + assert(offset_1 <= dictAndPrefixLength); + assert(offset_2 <= dictAndPrefixLength); + } + + /* Reset the lazy skipping state */ + ms->lazySkipping = 0; + + if (searchMethod == search_rowHash) { + ZSTD_row_fillHashCache(ms, base, rowLog, mls, ms->nextToUpdate, ilimit); + } + + /* Match Loop */ +#if defined(__GNUC__) && defined(__x86_64__) + /* I've measured random a 5% speed loss on levels 5 & 6 (greedy) when the + * code alignment is perturbed. To fix the instability align the loop on 32-bytes. + */ + __asm__(".p2align 5"); +#endif + while (ip < ilimit) { + size_t matchLength=0; + size_t offBase = REPCODE1_TO_OFFBASE; + const BYTE* start=ip+1; + DEBUGLOG(7, "search baseline (depth 0)"); + + /* check repCode */ + if (isDxS) { + const U32 repIndex = (U32)(ip - base) + 1 - offset_1; + const BYTE* repMatch = ((dictMode == ZSTD_dictMatchState || dictMode == ZSTD_dedicatedDictSearch) + && repIndex < prefixLowestIndex) ? + dictBase + (repIndex - dictIndexDelta) : + base + repIndex; + if (((U32)((prefixLowestIndex-1) - repIndex) >= 3 /* intentional underflow */) + && (MEM_read32(repMatch) == MEM_read32(ip+1)) ) { + const BYTE* repMatchEnd = repIndex < prefixLowestIndex ? dictEnd : iend; + matchLength = ZSTD_count_2segments(ip+1+4, repMatch+4, iend, repMatchEnd, prefixLowest) + 4; + if (depth==0) goto _storeSequence; + } + } + if ( dictMode == ZSTD_noDict + && ((offset_1 > 0) & (MEM_read32(ip+1-offset_1) == MEM_read32(ip+1)))) { + matchLength = ZSTD_count(ip+1+4, ip+1+4-offset_1, iend) + 4; + if (depth==0) goto _storeSequence; + } + + /* first search (depth 0) */ + { size_t offbaseFound = 999999999; + size_t const ml2 = ZSTD_searchMax(ms, ip, iend, &offbaseFound, mls, rowLog, searchMethod, dictMode); + if (ml2 > matchLength) + matchLength = ml2, start = ip, offBase = offbaseFound; + } + + if (matchLength < 4) { + size_t const step = ((size_t)(ip-anchor) >> kSearchStrength) + 1; /* jump faster over incompressible sections */; + ip += step; + /* Enter the lazy skipping mode once we are skipping more than 8 bytes at a time. + * In this mode we stop inserting every position into our tables, and only insert + * positions that we search, which is one in step positions. + * The exact cutoff is flexible, I've just chosen a number that is reasonably high, + * so we minimize the compression ratio loss in "normal" scenarios. This mode gets + * triggered once we've gone 2KB without finding any matches. + */ + ms->lazySkipping = step > kLazySkippingStep; + continue; + } + + /* let's try to find a better solution */ + if (depth>=1) + while (ip0) & (MEM_read32(ip) == MEM_read32(ip - offset_1)))) { + size_t const mlRep = ZSTD_count(ip+4, ip+4-offset_1, iend) + 4; + int const gain2 = (int)(mlRep * 3); + int const gain1 = (int)(matchLength*3 - ZSTD_highbit32((U32)offBase) + 1); + if ((mlRep >= 4) && (gain2 > gain1)) + matchLength = mlRep, offBase = REPCODE1_TO_OFFBASE, start = ip; + } + if (isDxS) { + const U32 repIndex = (U32)(ip - base) - offset_1; + const BYTE* repMatch = repIndex < prefixLowestIndex ? + dictBase + (repIndex - dictIndexDelta) : + base + repIndex; + if (((U32)((prefixLowestIndex-1) - repIndex) >= 3 /* intentional underflow */) + && (MEM_read32(repMatch) == MEM_read32(ip)) ) { + const BYTE* repMatchEnd = repIndex < prefixLowestIndex ? dictEnd : iend; + size_t const mlRep = ZSTD_count_2segments(ip+4, repMatch+4, iend, repMatchEnd, prefixLowest) + 4; + int const gain2 = (int)(mlRep * 3); + int const gain1 = (int)(matchLength*3 - ZSTD_highbit32((U32)offBase) + 1); + if ((mlRep >= 4) && (gain2 > gain1)) + matchLength = mlRep, offBase = REPCODE1_TO_OFFBASE, start = ip; + } + } + { size_t ofbCandidate=999999999; + size_t const ml2 = ZSTD_searchMax(ms, ip, iend, &ofbCandidate, mls, rowLog, searchMethod, dictMode); + int const gain2 = (int)(ml2*4 - ZSTD_highbit32((U32)ofbCandidate)); /* raw approx */ + int const gain1 = (int)(matchLength*4 - ZSTD_highbit32((U32)offBase) + 4); + if ((ml2 >= 4) && (gain2 > gain1)) { + matchLength = ml2, offBase = ofbCandidate, start = ip; + continue; /* search a better one */ + } } + + /* let's find an even better one */ + if ((depth==2) && (ip0) & (MEM_read32(ip) == MEM_read32(ip - offset_1)))) { + size_t const mlRep = ZSTD_count(ip+4, ip+4-offset_1, iend) + 4; + int const gain2 = (int)(mlRep * 4); + int const gain1 = (int)(matchLength*4 - ZSTD_highbit32((U32)offBase) + 1); + if ((mlRep >= 4) && (gain2 > gain1)) + matchLength = mlRep, offBase = REPCODE1_TO_OFFBASE, start = ip; + } + if (isDxS) { + const U32 repIndex = (U32)(ip - base) - offset_1; + const BYTE* repMatch = repIndex < prefixLowestIndex ? + dictBase + (repIndex - dictIndexDelta) : + base + repIndex; + if (((U32)((prefixLowestIndex-1) - repIndex) >= 3 /* intentional underflow */) + && (MEM_read32(repMatch) == MEM_read32(ip)) ) { + const BYTE* repMatchEnd = repIndex < prefixLowestIndex ? dictEnd : iend; + size_t const mlRep = ZSTD_count_2segments(ip+4, repMatch+4, iend, repMatchEnd, prefixLowest) + 4; + int const gain2 = (int)(mlRep * 4); + int const gain1 = (int)(matchLength*4 - ZSTD_highbit32((U32)offBase) + 1); + if ((mlRep >= 4) && (gain2 > gain1)) + matchLength = mlRep, offBase = REPCODE1_TO_OFFBASE, start = ip; + } + } + { size_t ofbCandidate=999999999; + size_t const ml2 = ZSTD_searchMax(ms, ip, iend, &ofbCandidate, mls, rowLog, searchMethod, dictMode); + int const gain2 = (int)(ml2*4 - ZSTD_highbit32((U32)ofbCandidate)); /* raw approx */ + int const gain1 = (int)(matchLength*4 - ZSTD_highbit32((U32)offBase) + 7); + if ((ml2 >= 4) && (gain2 > gain1)) { + matchLength = ml2, offBase = ofbCandidate, start = ip; + continue; + } } } + break; /* nothing found : store previous solution */ + } + + /* NOTE: + * Pay attention that `start[-value]` can lead to strange undefined behavior + * notably if `value` is unsigned, resulting in a large positive `-value`. + */ + /* catch up */ + if (OFFBASE_IS_OFFSET(offBase)) { + if (dictMode == ZSTD_noDict) { + while ( ((start > anchor) & (start - OFFBASE_TO_OFFSET(offBase) > prefixLowest)) + && (start[-1] == (start-OFFBASE_TO_OFFSET(offBase))[-1]) ) /* only search for offset within prefix */ + { start--; matchLength++; } + } + if (isDxS) { + U32 const matchIndex = (U32)((size_t)(start-base) - OFFBASE_TO_OFFSET(offBase)); + const BYTE* match = (matchIndex < prefixLowestIndex) ? dictBase + matchIndex - dictIndexDelta : base + matchIndex; + const BYTE* const mStart = (matchIndex < prefixLowestIndex) ? dictLowest : prefixLowest; + while ((start>anchor) && (match>mStart) && (start[-1] == match[-1])) { start--; match--; matchLength++; } /* catch up */ + } + offset_2 = offset_1; offset_1 = (U32)OFFBASE_TO_OFFSET(offBase); + } + /* store sequence */ +_storeSequence: + { size_t const litLength = (size_t)(start - anchor); + ZSTD_storeSeq(seqStore, litLength, anchor, iend, (U32)offBase, matchLength); + anchor = ip = start + matchLength; + } + if (ms->lazySkipping) { + /* We've found a match, disable lazy skipping mode, and refill the hash cache. */ + if (searchMethod == search_rowHash) { + ZSTD_row_fillHashCache(ms, base, rowLog, mls, ms->nextToUpdate, ilimit); + } + ms->lazySkipping = 0; + } + + /* check immediate repcode */ + if (isDxS) { + while (ip <= ilimit) { + U32 const current2 = (U32)(ip-base); + U32 const repIndex = current2 - offset_2; + const BYTE* repMatch = repIndex < prefixLowestIndex ? + dictBase - dictIndexDelta + repIndex : + base + repIndex; + if ( ((U32)((prefixLowestIndex-1) - (U32)repIndex) >= 3 /* intentional overflow */) + && (MEM_read32(repMatch) == MEM_read32(ip)) ) { + const BYTE* const repEnd2 = repIndex < prefixLowestIndex ? dictEnd : iend; + matchLength = ZSTD_count_2segments(ip+4, repMatch+4, iend, repEnd2, prefixLowest) + 4; + offBase = offset_2; offset_2 = offset_1; offset_1 = (U32)offBase; /* swap offset_2 <=> offset_1 */ + ZSTD_storeSeq(seqStore, 0, anchor, iend, REPCODE1_TO_OFFBASE, matchLength); + ip += matchLength; + anchor = ip; + continue; + } + break; + } + } + + if (dictMode == ZSTD_noDict) { + while ( ((ip <= ilimit) & (offset_2>0)) + && (MEM_read32(ip) == MEM_read32(ip - offset_2)) ) { + /* store sequence */ + matchLength = ZSTD_count(ip+4, ip+4-offset_2, iend) + 4; + offBase = offset_2; offset_2 = offset_1; offset_1 = (U32)offBase; /* swap repcodes */ + ZSTD_storeSeq(seqStore, 0, anchor, iend, REPCODE1_TO_OFFBASE, matchLength); + ip += matchLength; + anchor = ip; + continue; /* faster when present ... (?) */ + } } } + + /* If offset_1 started invalid (offsetSaved1 != 0) and became valid (offset_1 != 0), + * rotate saved offsets. See comment in ZSTD_compressBlock_fast_noDict for more context. */ + offsetSaved2 = ((offsetSaved1 != 0) && (offset_1 != 0)) ? offsetSaved1 : offsetSaved2; + + /* save reps for next block */ + rep[0] = offset_1 ? offset_1 : offsetSaved1; + rep[1] = offset_2 ? offset_2 : offsetSaved2; + + /* Return the last literals size */ + return (size_t)(iend - anchor); +} +#endif /* build exclusions */ + + +#ifndef ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_greedy( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 0, ZSTD_noDict); +} + +size_t ZSTD_compressBlock_greedy_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 0, ZSTD_dictMatchState); +} + +size_t ZSTD_compressBlock_greedy_dedicatedDictSearch( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 0, ZSTD_dedicatedDictSearch); +} + +size_t ZSTD_compressBlock_greedy_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 0, ZSTD_noDict); +} + +size_t ZSTD_compressBlock_greedy_dictMatchState_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 0, ZSTD_dictMatchState); +} + +size_t ZSTD_compressBlock_greedy_dedicatedDictSearch_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 0, ZSTD_dedicatedDictSearch); +} +#endif + +#ifndef ZSTD_EXCLUDE_LAZY_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_lazy( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 1, ZSTD_noDict); +} + +size_t ZSTD_compressBlock_lazy_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 1, ZSTD_dictMatchState); +} + +size_t ZSTD_compressBlock_lazy_dedicatedDictSearch( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 1, ZSTD_dedicatedDictSearch); +} + +size_t ZSTD_compressBlock_lazy_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 1, ZSTD_noDict); +} + +size_t ZSTD_compressBlock_lazy_dictMatchState_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 1, ZSTD_dictMatchState); +} + +size_t ZSTD_compressBlock_lazy_dedicatedDictSearch_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 1, ZSTD_dedicatedDictSearch); +} +#endif + +#ifndef ZSTD_EXCLUDE_LAZY2_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_lazy2( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 2, ZSTD_noDict); +} + +size_t ZSTD_compressBlock_lazy2_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 2, ZSTD_dictMatchState); +} + +size_t ZSTD_compressBlock_lazy2_dedicatedDictSearch( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 2, ZSTD_dedicatedDictSearch); +} + +size_t ZSTD_compressBlock_lazy2_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 2, ZSTD_noDict); +} + +size_t ZSTD_compressBlock_lazy2_dictMatchState_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 2, ZSTD_dictMatchState); +} + +size_t ZSTD_compressBlock_lazy2_dedicatedDictSearch_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 2, ZSTD_dedicatedDictSearch); +} +#endif + +#ifndef ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_btlazy2( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_binaryTree, 2, ZSTD_noDict); +} + +size_t ZSTD_compressBlock_btlazy2_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_generic(ms, seqStore, rep, src, srcSize, search_binaryTree, 2, ZSTD_dictMatchState); +} +#endif + +#if !defined(ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_LAZY_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_LAZY2_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR) +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_compressBlock_lazy_extDict_generic( + ZSTD_matchState_t* ms, seqStore_t* seqStore, + U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize, + const searchMethod_e searchMethod, const U32 depth) +{ + const BYTE* const istart = (const BYTE*)src; + const BYTE* ip = istart; + const BYTE* anchor = istart; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = searchMethod == search_rowHash ? iend - 8 - ZSTD_ROW_HASH_CACHE_SIZE : iend - 8; + const BYTE* const base = ms->window.base; + const U32 dictLimit = ms->window.dictLimit; + const BYTE* const prefixStart = base + dictLimit; + const BYTE* const dictBase = ms->window.dictBase; + const BYTE* const dictEnd = dictBase + dictLimit; + const BYTE* const dictStart = dictBase + ms->window.lowLimit; + const U32 windowLog = ms->cParams.windowLog; + const U32 mls = BOUNDED(4, ms->cParams.minMatch, 6); + const U32 rowLog = BOUNDED(4, ms->cParams.searchLog, 6); + + U32 offset_1 = rep[0], offset_2 = rep[1]; + + DEBUGLOG(5, "ZSTD_compressBlock_lazy_extDict_generic (searchFunc=%u)", (U32)searchMethod); + + /* Reset the lazy skipping state */ + ms->lazySkipping = 0; + + /* init */ + ip += (ip == prefixStart); + if (searchMethod == search_rowHash) { + ZSTD_row_fillHashCache(ms, base, rowLog, mls, ms->nextToUpdate, ilimit); + } + + /* Match Loop */ +#if defined(__GNUC__) && defined(__x86_64__) + /* I've measured random a 5% speed loss on levels 5 & 6 (greedy) when the + * code alignment is perturbed. To fix the instability align the loop on 32-bytes. + */ + __asm__(".p2align 5"); +#endif + while (ip < ilimit) { + size_t matchLength=0; + size_t offBase = REPCODE1_TO_OFFBASE; + const BYTE* start=ip+1; + U32 curr = (U32)(ip-base); + + /* check repCode */ + { const U32 windowLow = ZSTD_getLowestMatchIndex(ms, curr+1, windowLog); + const U32 repIndex = (U32)(curr+1 - offset_1); + const BYTE* const repBase = repIndex < dictLimit ? dictBase : base; + const BYTE* const repMatch = repBase + repIndex; + if ( ((U32)((dictLimit-1) - repIndex) >= 3) /* intentional overflow */ + & (offset_1 <= curr+1 - windowLow) ) /* note: we are searching at curr+1 */ + if (MEM_read32(ip+1) == MEM_read32(repMatch)) { + /* repcode detected we should take it */ + const BYTE* const repEnd = repIndex < dictLimit ? dictEnd : iend; + matchLength = ZSTD_count_2segments(ip+1+4, repMatch+4, iend, repEnd, prefixStart) + 4; + if (depth==0) goto _storeSequence; + } } + + /* first search (depth 0) */ + { size_t ofbCandidate = 999999999; + size_t const ml2 = ZSTD_searchMax(ms, ip, iend, &ofbCandidate, mls, rowLog, searchMethod, ZSTD_extDict); + if (ml2 > matchLength) + matchLength = ml2, start = ip, offBase = ofbCandidate; + } + + if (matchLength < 4) { + size_t const step = ((size_t)(ip-anchor) >> kSearchStrength); + ip += step + 1; /* jump faster over incompressible sections */ + /* Enter the lazy skipping mode once we are skipping more than 8 bytes at a time. + * In this mode we stop inserting every position into our tables, and only insert + * positions that we search, which is one in step positions. + * The exact cutoff is flexible, I've just chosen a number that is reasonably high, + * so we minimize the compression ratio loss in "normal" scenarios. This mode gets + * triggered once we've gone 2KB without finding any matches. + */ + ms->lazySkipping = step > kLazySkippingStep; + continue; + } + + /* let's try to find a better solution */ + if (depth>=1) + while (ip= 3) /* intentional overflow : do not test positions overlapping 2 memory segments */ + & (offset_1 <= curr - windowLow) ) /* equivalent to `curr > repIndex >= windowLow` */ + if (MEM_read32(ip) == MEM_read32(repMatch)) { + /* repcode detected */ + const BYTE* const repEnd = repIndex < dictLimit ? dictEnd : iend; + size_t const repLength = ZSTD_count_2segments(ip+4, repMatch+4, iend, repEnd, prefixStart) + 4; + int const gain2 = (int)(repLength * 3); + int const gain1 = (int)(matchLength*3 - ZSTD_highbit32((U32)offBase) + 1); + if ((repLength >= 4) && (gain2 > gain1)) + matchLength = repLength, offBase = REPCODE1_TO_OFFBASE, start = ip; + } } + + /* search match, depth 1 */ + { size_t ofbCandidate = 999999999; + size_t const ml2 = ZSTD_searchMax(ms, ip, iend, &ofbCandidate, mls, rowLog, searchMethod, ZSTD_extDict); + int const gain2 = (int)(ml2*4 - ZSTD_highbit32((U32)ofbCandidate)); /* raw approx */ + int const gain1 = (int)(matchLength*4 - ZSTD_highbit32((U32)offBase) + 4); + if ((ml2 >= 4) && (gain2 > gain1)) { + matchLength = ml2, offBase = ofbCandidate, start = ip; + continue; /* search a better one */ + } } + + /* let's find an even better one */ + if ((depth==2) && (ip= 3) /* intentional overflow : do not test positions overlapping 2 memory segments */ + & (offset_1 <= curr - windowLow) ) /* equivalent to `curr > repIndex >= windowLow` */ + if (MEM_read32(ip) == MEM_read32(repMatch)) { + /* repcode detected */ + const BYTE* const repEnd = repIndex < dictLimit ? dictEnd : iend; + size_t const repLength = ZSTD_count_2segments(ip+4, repMatch+4, iend, repEnd, prefixStart) + 4; + int const gain2 = (int)(repLength * 4); + int const gain1 = (int)(matchLength*4 - ZSTD_highbit32((U32)offBase) + 1); + if ((repLength >= 4) && (gain2 > gain1)) + matchLength = repLength, offBase = REPCODE1_TO_OFFBASE, start = ip; + } } + + /* search match, depth 2 */ + { size_t ofbCandidate = 999999999; + size_t const ml2 = ZSTD_searchMax(ms, ip, iend, &ofbCandidate, mls, rowLog, searchMethod, ZSTD_extDict); + int const gain2 = (int)(ml2*4 - ZSTD_highbit32((U32)ofbCandidate)); /* raw approx */ + int const gain1 = (int)(matchLength*4 - ZSTD_highbit32((U32)offBase) + 7); + if ((ml2 >= 4) && (gain2 > gain1)) { + matchLength = ml2, offBase = ofbCandidate, start = ip; + continue; + } } } + break; /* nothing found : store previous solution */ + } + + /* catch up */ + if (OFFBASE_IS_OFFSET(offBase)) { + U32 const matchIndex = (U32)((size_t)(start-base) - OFFBASE_TO_OFFSET(offBase)); + const BYTE* match = (matchIndex < dictLimit) ? dictBase + matchIndex : base + matchIndex; + const BYTE* const mStart = (matchIndex < dictLimit) ? dictStart : prefixStart; + while ((start>anchor) && (match>mStart) && (start[-1] == match[-1])) { start--; match--; matchLength++; } /* catch up */ + offset_2 = offset_1; offset_1 = (U32)OFFBASE_TO_OFFSET(offBase); + } + + /* store sequence */ +_storeSequence: + { size_t const litLength = (size_t)(start - anchor); + ZSTD_storeSeq(seqStore, litLength, anchor, iend, (U32)offBase, matchLength); + anchor = ip = start + matchLength; + } + if (ms->lazySkipping) { + /* We've found a match, disable lazy skipping mode, and refill the hash cache. */ + if (searchMethod == search_rowHash) { + ZSTD_row_fillHashCache(ms, base, rowLog, mls, ms->nextToUpdate, ilimit); + } + ms->lazySkipping = 0; + } + + /* check immediate repcode */ + while (ip <= ilimit) { + const U32 repCurrent = (U32)(ip-base); + const U32 windowLow = ZSTD_getLowestMatchIndex(ms, repCurrent, windowLog); + const U32 repIndex = repCurrent - offset_2; + const BYTE* const repBase = repIndex < dictLimit ? dictBase : base; + const BYTE* const repMatch = repBase + repIndex; + if ( ((U32)((dictLimit-1) - repIndex) >= 3) /* intentional overflow : do not test positions overlapping 2 memory segments */ + & (offset_2 <= repCurrent - windowLow) ) /* equivalent to `curr > repIndex >= windowLow` */ + if (MEM_read32(ip) == MEM_read32(repMatch)) { + /* repcode detected we should take it */ + const BYTE* const repEnd = repIndex < dictLimit ? dictEnd : iend; + matchLength = ZSTD_count_2segments(ip+4, repMatch+4, iend, repEnd, prefixStart) + 4; + offBase = offset_2; offset_2 = offset_1; offset_1 = (U32)offBase; /* swap offset history */ + ZSTD_storeSeq(seqStore, 0, anchor, iend, REPCODE1_TO_OFFBASE, matchLength); + ip += matchLength; + anchor = ip; + continue; /* faster when present ... (?) */ + } + break; + } } + + /* Save reps for next block */ + rep[0] = offset_1; + rep[1] = offset_2; + + /* Return the last literals size */ + return (size_t)(iend - anchor); +} +#endif /* build exclusions */ + +#ifndef ZSTD_EXCLUDE_GREEDY_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_greedy_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_extDict_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 0); +} + +size_t ZSTD_compressBlock_greedy_extDict_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_extDict_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 0); +} +#endif + +#ifndef ZSTD_EXCLUDE_LAZY_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_lazy_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) + +{ + return ZSTD_compressBlock_lazy_extDict_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 1); +} + +size_t ZSTD_compressBlock_lazy_extDict_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) + +{ + return ZSTD_compressBlock_lazy_extDict_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 1); +} +#endif + +#ifndef ZSTD_EXCLUDE_LAZY2_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_lazy2_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) + +{ + return ZSTD_compressBlock_lazy_extDict_generic(ms, seqStore, rep, src, srcSize, search_hashChain, 2); +} + +size_t ZSTD_compressBlock_lazy2_extDict_row( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) +{ + return ZSTD_compressBlock_lazy_extDict_generic(ms, seqStore, rep, src, srcSize, search_rowHash, 2); +} +#endif + +#ifndef ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_btlazy2_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + void const* src, size_t srcSize) + +{ + return ZSTD_compressBlock_lazy_extDict_generic(ms, seqStore, rep, src, srcSize, search_binaryTree, 2); +} +#endif +/**** ended inlining compress/zstd_lazy.c ****/ +/**** start inlining compress/zstd_ldm.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/**** skipping file: zstd_ldm.h ****/ + +/**** skipping file: ../common/debug.h ****/ +/**** skipping file: ../common/xxhash.h ****/ +/**** skipping file: zstd_fast.h ****/ +/**** skipping file: zstd_double_fast.h ****/ +/**** start inlining zstd_ldm_geartab.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +#ifndef ZSTD_LDM_GEARTAB_H +#define ZSTD_LDM_GEARTAB_H + +/**** skipping file: ../common/compiler.h ****/ +/**** skipping file: ../common/mem.h ****/ + +static UNUSED_ATTR const U64 ZSTD_ldm_gearTab[256] = { + 0xf5b8f72c5f77775c, 0x84935f266b7ac412, 0xb647ada9ca730ccc, + 0xb065bb4b114fb1de, 0x34584e7e8c3a9fd0, 0x4e97e17c6ae26b05, + 0x3a03d743bc99a604, 0xcecd042422c4044f, 0x76de76c58524259e, + 0x9c8528f65badeaca, 0x86563706e2097529, 0x2902475fa375d889, + 0xafb32a9739a5ebe6, 0xce2714da3883e639, 0x21eaf821722e69e, + 0x37b628620b628, 0x49a8d455d88caf5, 0x8556d711e6958140, + 0x4f7ae74fc605c1f, 0x829f0c3468bd3a20, 0x4ffdc885c625179e, + 0x8473de048a3daf1b, 0x51008822b05646b2, 0x69d75d12b2d1cc5f, + 0x8c9d4a19159154bc, 0xc3cc10f4abbd4003, 0xd06ddc1cecb97391, + 0xbe48e6e7ed80302e, 0x3481db31cee03547, 0xacc3f67cdaa1d210, + 0x65cb771d8c7f96cc, 0x8eb27177055723dd, 0xc789950d44cd94be, + 0x934feadc3700b12b, 0x5e485f11edbdf182, 0x1e2e2a46fd64767a, + 0x2969ca71d82efa7c, 0x9d46e9935ebbba2e, 0xe056b67e05e6822b, + 0x94d73f55739d03a0, 0xcd7010bdb69b5a03, 0x455ef9fcd79b82f4, + 0x869cb54a8749c161, 0x38d1a4fa6185d225, 0xb475166f94bbe9bb, + 0xa4143548720959f1, 0x7aed4780ba6b26ba, 0xd0ce264439e02312, + 0x84366d746078d508, 0xa8ce973c72ed17be, 0x21c323a29a430b01, + 0x9962d617e3af80ee, 0xab0ce91d9c8cf75b, 0x530e8ee6d19a4dbc, + 0x2ef68c0cf53f5d72, 0xc03a681640a85506, 0x496e4e9f9c310967, + 0x78580472b59b14a0, 0x273824c23b388577, 0x66bf923ad45cb553, + 0x47ae1a5a2492ba86, 0x35e304569e229659, 0x4765182a46870b6f, + 0x6cbab625e9099412, 0xddac9a2e598522c1, 0x7172086e666624f2, + 0xdf5003ca503b7837, 0x88c0c1db78563d09, 0x58d51865acfc289d, + 0x177671aec65224f1, 0xfb79d8a241e967d7, 0x2be1e101cad9a49a, + 0x6625682f6e29186b, 0x399553457ac06e50, 0x35dffb4c23abb74, + 0x429db2591f54aade, 0xc52802a8037d1009, 0x6acb27381f0b25f3, + 0xf45e2551ee4f823b, 0x8b0ea2d99580c2f7, 0x3bed519cbcb4e1e1, + 0xff452823dbb010a, 0x9d42ed614f3dd267, 0x5b9313c06257c57b, + 0xa114b8008b5e1442, 0xc1fe311c11c13d4b, 0x66e8763ea34c5568, + 0x8b982af1c262f05d, 0xee8876faaa75fbb7, 0x8a62a4d0d172bb2a, + 0xc13d94a3b7449a97, 0x6dbbba9dc15d037c, 0xc786101f1d92e0f1, + 0xd78681a907a0b79b, 0xf61aaf2962c9abb9, 0x2cfd16fcd3cb7ad9, + 0x868c5b6744624d21, 0x25e650899c74ddd7, 0xba042af4a7c37463, + 0x4eb1a539465a3eca, 0xbe09dbf03b05d5ca, 0x774e5a362b5472ba, + 0x47a1221229d183cd, 0x504b0ca18ef5a2df, 0xdffbdfbde2456eb9, + 0x46cd2b2fbee34634, 0xf2aef8fe819d98c3, 0x357f5276d4599d61, + 0x24a5483879c453e3, 0x88026889192b4b9, 0x28da96671782dbec, + 0x4ef37c40588e9aaa, 0x8837b90651bc9fb3, 0xc164f741d3f0e5d6, + 0xbc135a0a704b70ba, 0x69cd868f7622ada, 0xbc37ba89e0b9c0ab, + 0x47c14a01323552f6, 0x4f00794bacee98bb, 0x7107de7d637a69d5, + 0x88af793bb6f2255e, 0xf3c6466b8799b598, 0xc288c616aa7f3b59, + 0x81ca63cf42fca3fd, 0x88d85ace36a2674b, 0xd056bd3792389e7, + 0xe55c396c4e9dd32d, 0xbefb504571e6c0a6, 0x96ab32115e91e8cc, + 0xbf8acb18de8f38d1, 0x66dae58801672606, 0x833b6017872317fb, + 0xb87c16f2d1c92864, 0xdb766a74e58b669c, 0x89659f85c61417be, + 0xc8daad856011ea0c, 0x76a4b565b6fe7eae, 0xa469d085f6237312, + 0xaaf0365683a3e96c, 0x4dbb746f8424f7b8, 0x638755af4e4acc1, + 0x3d7807f5bde64486, 0x17be6d8f5bbb7639, 0x903f0cd44dc35dc, + 0x67b672eafdf1196c, 0xa676ff93ed4c82f1, 0x521d1004c5053d9d, + 0x37ba9ad09ccc9202, 0x84e54d297aacfb51, 0xa0b4b776a143445, + 0x820d471e20b348e, 0x1874383cb83d46dc, 0x97edeec7a1efe11c, + 0xb330e50b1bdc42aa, 0x1dd91955ce70e032, 0xa514cdb88f2939d5, + 0x2791233fd90db9d3, 0x7b670a4cc50f7a9b, 0x77c07d2a05c6dfa5, + 0xe3778b6646d0a6fa, 0xb39c8eda47b56749, 0x933ed448addbef28, + 0xaf846af6ab7d0bf4, 0xe5af208eb666e49, 0x5e6622f73534cd6a, + 0x297daeca42ef5b6e, 0x862daef3d35539a6, 0xe68722498f8e1ea9, + 0x981c53093dc0d572, 0xfa09b0bfbf86fbf5, 0x30b1e96166219f15, + 0x70e7d466bdc4fb83, 0x5a66736e35f2a8e9, 0xcddb59d2b7c1baef, + 0xd6c7d247d26d8996, 0xea4e39eac8de1ba3, 0x539c8bb19fa3aff2, + 0x9f90e4c5fd508d8, 0xa34e5956fbaf3385, 0x2e2f8e151d3ef375, + 0x173691e9b83faec1, 0xb85a8d56bf016379, 0x8382381267408ae3, + 0xb90f901bbdc0096d, 0x7c6ad32933bcec65, 0x76bb5e2f2c8ad595, + 0x390f851a6cf46d28, 0xc3e6064da1c2da72, 0xc52a0c101cfa5389, + 0xd78eaf84a3fbc530, 0x3781b9e2288b997e, 0x73c2f6dea83d05c4, + 0x4228e364c5b5ed7, 0x9d7a3edf0da43911, 0x8edcfeda24686756, + 0x5e7667a7b7a9b3a1, 0x4c4f389fa143791d, 0xb08bc1023da7cddc, + 0x7ab4be3ae529b1cc, 0x754e6132dbe74ff9, 0x71635442a839df45, + 0x2f6fb1643fbe52de, 0x961e0a42cf7a8177, 0xf3b45d83d89ef2ea, + 0xee3de4cf4a6e3e9b, 0xcd6848542c3295e7, 0xe4cee1664c78662f, + 0x9947548b474c68c4, 0x25d73777a5ed8b0b, 0xc915b1d636b7fc, + 0x21c2ba75d9b0d2da, 0x5f6b5dcf608a64a1, 0xdcf333255ff9570c, + 0x633b922418ced4ee, 0xc136dde0b004b34a, 0x58cc83b05d4b2f5a, + 0x5eb424dda28e42d2, 0x62df47369739cd98, 0xb4e0b42485e4ce17, + 0x16e1f0c1f9a8d1e7, 0x8ec3916707560ebf, 0x62ba6e2df2cc9db3, + 0xcbf9f4ff77d83a16, 0x78d9d7d07d2bbcc4, 0xef554ce1e02c41f4, + 0x8d7581127eccf94d, 0xa9b53336cb3c8a05, 0x38c42c0bf45c4f91, + 0x640893cdf4488863, 0x80ec34bc575ea568, 0x39f324f5b48eaa40, + 0xe9d9ed1f8eff527f, 0x9224fc058cc5a214, 0xbaba00b04cfe7741, + 0x309a9f120fcf52af, 0xa558f3ec65626212, 0x424bec8b7adabe2f, + 0x41622513a6aea433, 0xb88da2d5324ca798, 0xd287733b245528a4, + 0x9a44697e6d68aec3, 0x7b1093be2f49bb28, 0x50bbec632e3d8aad, + 0x6cd90723e1ea8283, 0x897b9e7431b02bf3, 0x219efdcb338a7047, + 0x3b0311f0a27c0656, 0xdb17bf91c0db96e7, 0x8cd4fd6b4e85a5b2, + 0xfab071054ba6409d, 0x40d6fe831fa9dfd9, 0xaf358debad7d791e, + 0xeb8d0e25a65e3e58, 0xbbcbd3df14e08580, 0xcf751f27ecdab2b, + 0x2b4da14f2613d8f4 +}; + +#endif /* ZSTD_LDM_GEARTAB_H */ +/**** ended inlining zstd_ldm_geartab.h ****/ + +#define LDM_BUCKET_SIZE_LOG 3 +#define LDM_MIN_MATCH_LENGTH 64 +#define LDM_HASH_RLOG 7 + +typedef struct { + U64 rolling; + U64 stopMask; +} ldmRollingHashState_t; + +/** ZSTD_ldm_gear_init(): + * + * Initializes the rolling hash state such that it will honor the + * settings in params. */ +static void ZSTD_ldm_gear_init(ldmRollingHashState_t* state, ldmParams_t const* params) +{ + unsigned maxBitsInMask = MIN(params->minMatchLength, 64); + unsigned hashRateLog = params->hashRateLog; + + state->rolling = ~(U32)0; + + /* The choice of the splitting criterion is subject to two conditions: + * 1. it has to trigger on average every 2^(hashRateLog) bytes; + * 2. ideally, it has to depend on a window of minMatchLength bytes. + * + * In the gear hash algorithm, bit n depends on the last n bytes; + * so in order to obtain a good quality splitting criterion it is + * preferable to use bits with high weight. + * + * To match condition 1 we use a mask with hashRateLog bits set + * and, because of the previous remark, we make sure these bits + * have the highest possible weight while still respecting + * condition 2. + */ + if (hashRateLog > 0 && hashRateLog <= maxBitsInMask) { + state->stopMask = (((U64)1 << hashRateLog) - 1) << (maxBitsInMask - hashRateLog); + } else { + /* In this degenerate case we simply honor the hash rate. */ + state->stopMask = ((U64)1 << hashRateLog) - 1; + } +} + +/** ZSTD_ldm_gear_reset() + * Feeds [data, data + minMatchLength) into the hash without registering any + * splits. This effectively resets the hash state. This is used when skipping + * over data, either at the beginning of a block, or skipping sections. + */ +static void ZSTD_ldm_gear_reset(ldmRollingHashState_t* state, + BYTE const* data, size_t minMatchLength) +{ + U64 hash = state->rolling; + size_t n = 0; + +#define GEAR_ITER_ONCE() do { \ + hash = (hash << 1) + ZSTD_ldm_gearTab[data[n] & 0xff]; \ + n += 1; \ + } while (0) + while (n + 3 < minMatchLength) { + GEAR_ITER_ONCE(); + GEAR_ITER_ONCE(); + GEAR_ITER_ONCE(); + GEAR_ITER_ONCE(); + } + while (n < minMatchLength) { + GEAR_ITER_ONCE(); + } +#undef GEAR_ITER_ONCE +} + +/** ZSTD_ldm_gear_feed(): + * + * Registers in the splits array all the split points found in the first + * size bytes following the data pointer. This function terminates when + * either all the data has been processed or LDM_BATCH_SIZE splits are + * present in the splits array. + * + * Precondition: The splits array must not be full. + * Returns: The number of bytes processed. */ +static size_t ZSTD_ldm_gear_feed(ldmRollingHashState_t* state, + BYTE const* data, size_t size, + size_t* splits, unsigned* numSplits) +{ + size_t n; + U64 hash, mask; + + hash = state->rolling; + mask = state->stopMask; + n = 0; + +#define GEAR_ITER_ONCE() do { \ + hash = (hash << 1) + ZSTD_ldm_gearTab[data[n] & 0xff]; \ + n += 1; \ + if (UNLIKELY((hash & mask) == 0)) { \ + splits[*numSplits] = n; \ + *numSplits += 1; \ + if (*numSplits == LDM_BATCH_SIZE) \ + goto done; \ + } \ + } while (0) + + while (n + 3 < size) { + GEAR_ITER_ONCE(); + GEAR_ITER_ONCE(); + GEAR_ITER_ONCE(); + GEAR_ITER_ONCE(); + } + while (n < size) { + GEAR_ITER_ONCE(); + } + +#undef GEAR_ITER_ONCE + +done: + state->rolling = hash; + return n; +} + +void ZSTD_ldm_adjustParameters(ldmParams_t* params, + ZSTD_compressionParameters const* cParams) +{ + params->windowLog = cParams->windowLog; + ZSTD_STATIC_ASSERT(LDM_BUCKET_SIZE_LOG <= ZSTD_LDM_BUCKETSIZELOG_MAX); + DEBUGLOG(4, "ZSTD_ldm_adjustParameters"); + if (!params->bucketSizeLog) params->bucketSizeLog = LDM_BUCKET_SIZE_LOG; + if (!params->minMatchLength) params->minMatchLength = LDM_MIN_MATCH_LENGTH; + if (params->hashLog == 0) { + params->hashLog = MAX(ZSTD_HASHLOG_MIN, params->windowLog - LDM_HASH_RLOG); + assert(params->hashLog <= ZSTD_HASHLOG_MAX); + } + if (params->hashRateLog == 0) { + params->hashRateLog = params->windowLog < params->hashLog + ? 0 + : params->windowLog - params->hashLog; + } + params->bucketSizeLog = MIN(params->bucketSizeLog, params->hashLog); +} + +size_t ZSTD_ldm_getTableSize(ldmParams_t params) +{ + size_t const ldmHSize = ((size_t)1) << params.hashLog; + size_t const ldmBucketSizeLog = MIN(params.bucketSizeLog, params.hashLog); + size_t const ldmBucketSize = ((size_t)1) << (params.hashLog - ldmBucketSizeLog); + size_t const totalSize = ZSTD_cwksp_alloc_size(ldmBucketSize) + + ZSTD_cwksp_alloc_size(ldmHSize * sizeof(ldmEntry_t)); + return params.enableLdm == ZSTD_ps_enable ? totalSize : 0; +} + +size_t ZSTD_ldm_getMaxNbSeq(ldmParams_t params, size_t maxChunkSize) +{ + return params.enableLdm == ZSTD_ps_enable ? (maxChunkSize / params.minMatchLength) : 0; +} + +/** ZSTD_ldm_getBucket() : + * Returns a pointer to the start of the bucket associated with hash. */ +static ldmEntry_t* ZSTD_ldm_getBucket( + ldmState_t* ldmState, size_t hash, ldmParams_t const ldmParams) +{ + return ldmState->hashTable + (hash << ldmParams.bucketSizeLog); +} + +/** ZSTD_ldm_insertEntry() : + * Insert the entry with corresponding hash into the hash table */ +static void ZSTD_ldm_insertEntry(ldmState_t* ldmState, + size_t const hash, const ldmEntry_t entry, + ldmParams_t const ldmParams) +{ + BYTE* const pOffset = ldmState->bucketOffsets + hash; + unsigned const offset = *pOffset; + + *(ZSTD_ldm_getBucket(ldmState, hash, ldmParams) + offset) = entry; + *pOffset = (BYTE)((offset + 1) & ((1u << ldmParams.bucketSizeLog) - 1)); + +} + +/** ZSTD_ldm_countBackwardsMatch() : + * Returns the number of bytes that match backwards before pIn and pMatch. + * + * We count only bytes where pMatch >= pBase and pIn >= pAnchor. */ +static size_t ZSTD_ldm_countBackwardsMatch( + const BYTE* pIn, const BYTE* pAnchor, + const BYTE* pMatch, const BYTE* pMatchBase) +{ + size_t matchLength = 0; + while (pIn > pAnchor && pMatch > pMatchBase && pIn[-1] == pMatch[-1]) { + pIn--; + pMatch--; + matchLength++; + } + return matchLength; +} + +/** ZSTD_ldm_countBackwardsMatch_2segments() : + * Returns the number of bytes that match backwards from pMatch, + * even with the backwards match spanning 2 different segments. + * + * On reaching `pMatchBase`, start counting from mEnd */ +static size_t ZSTD_ldm_countBackwardsMatch_2segments( + const BYTE* pIn, const BYTE* pAnchor, + const BYTE* pMatch, const BYTE* pMatchBase, + const BYTE* pExtDictStart, const BYTE* pExtDictEnd) +{ + size_t matchLength = ZSTD_ldm_countBackwardsMatch(pIn, pAnchor, pMatch, pMatchBase); + if (pMatch - matchLength != pMatchBase || pMatchBase == pExtDictStart) { + /* If backwards match is entirely in the extDict or prefix, immediately return */ + return matchLength; + } + DEBUGLOG(7, "ZSTD_ldm_countBackwardsMatch_2segments: found 2-parts backwards match (length in prefix==%zu)", matchLength); + matchLength += ZSTD_ldm_countBackwardsMatch(pIn - matchLength, pAnchor, pExtDictEnd, pExtDictStart); + DEBUGLOG(7, "final backwards match length = %zu", matchLength); + return matchLength; +} + +/** ZSTD_ldm_fillFastTables() : + * + * Fills the relevant tables for the ZSTD_fast and ZSTD_dfast strategies. + * This is similar to ZSTD_loadDictionaryContent. + * + * The tables for the other strategies are filled within their + * block compressors. */ +static size_t ZSTD_ldm_fillFastTables(ZSTD_matchState_t* ms, + void const* end) +{ + const BYTE* const iend = (const BYTE*)end; + + switch(ms->cParams.strategy) + { + case ZSTD_fast: + ZSTD_fillHashTable(ms, iend, ZSTD_dtlm_fast, ZSTD_tfp_forCCtx); + break; + + case ZSTD_dfast: +#ifndef ZSTD_EXCLUDE_DFAST_BLOCK_COMPRESSOR + ZSTD_fillDoubleHashTable(ms, iend, ZSTD_dtlm_fast, ZSTD_tfp_forCCtx); +#else + assert(0); /* shouldn't be called: cparams should've been adjusted. */ +#endif + break; + + case ZSTD_greedy: + case ZSTD_lazy: + case ZSTD_lazy2: + case ZSTD_btlazy2: + case ZSTD_btopt: + case ZSTD_btultra: + case ZSTD_btultra2: + break; + default: + assert(0); /* not possible : not a valid strategy id */ + } + + return 0; +} + +void ZSTD_ldm_fillHashTable( + ldmState_t* ldmState, const BYTE* ip, + const BYTE* iend, ldmParams_t const* params) +{ + U32 const minMatchLength = params->minMatchLength; + U32 const hBits = params->hashLog - params->bucketSizeLog; + BYTE const* const base = ldmState->window.base; + BYTE const* const istart = ip; + ldmRollingHashState_t hashState; + size_t* const splits = ldmState->splitIndices; + unsigned numSplits; + + DEBUGLOG(5, "ZSTD_ldm_fillHashTable"); + + ZSTD_ldm_gear_init(&hashState, params); + while (ip < iend) { + size_t hashed; + unsigned n; + + numSplits = 0; + hashed = ZSTD_ldm_gear_feed(&hashState, ip, iend - ip, splits, &numSplits); + + for (n = 0; n < numSplits; n++) { + if (ip + splits[n] >= istart + minMatchLength) { + BYTE const* const split = ip + splits[n] - minMatchLength; + U64 const xxhash = XXH64(split, minMatchLength, 0); + U32 const hash = (U32)(xxhash & (((U32)1 << hBits) - 1)); + ldmEntry_t entry; + + entry.offset = (U32)(split - base); + entry.checksum = (U32)(xxhash >> 32); + ZSTD_ldm_insertEntry(ldmState, hash, entry, *params); + } + } + + ip += hashed; + } +} + + +/** ZSTD_ldm_limitTableUpdate() : + * + * Sets cctx->nextToUpdate to a position corresponding closer to anchor + * if it is far way + * (after a long match, only update tables a limited amount). */ +static void ZSTD_ldm_limitTableUpdate(ZSTD_matchState_t* ms, const BYTE* anchor) +{ + U32 const curr = (U32)(anchor - ms->window.base); + if (curr > ms->nextToUpdate + 1024) { + ms->nextToUpdate = + curr - MIN(512, curr - ms->nextToUpdate - 1024); + } +} + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_ldm_generateSequences_internal( + ldmState_t* ldmState, rawSeqStore_t* rawSeqStore, + ldmParams_t const* params, void const* src, size_t srcSize) +{ + /* LDM parameters */ + int const extDict = ZSTD_window_hasExtDict(ldmState->window); + U32 const minMatchLength = params->minMatchLength; + U32 const entsPerBucket = 1U << params->bucketSizeLog; + U32 const hBits = params->hashLog - params->bucketSizeLog; + /* Prefix and extDict parameters */ + U32 const dictLimit = ldmState->window.dictLimit; + U32 const lowestIndex = extDict ? ldmState->window.lowLimit : dictLimit; + BYTE const* const base = ldmState->window.base; + BYTE const* const dictBase = extDict ? ldmState->window.dictBase : NULL; + BYTE const* const dictStart = extDict ? dictBase + lowestIndex : NULL; + BYTE const* const dictEnd = extDict ? dictBase + dictLimit : NULL; + BYTE const* const lowPrefixPtr = base + dictLimit; + /* Input bounds */ + BYTE const* const istart = (BYTE const*)src; + BYTE const* const iend = istart + srcSize; + BYTE const* const ilimit = iend - HASH_READ_SIZE; + /* Input positions */ + BYTE const* anchor = istart; + BYTE const* ip = istart; + /* Rolling hash state */ + ldmRollingHashState_t hashState; + /* Arrays for staged-processing */ + size_t* const splits = ldmState->splitIndices; + ldmMatchCandidate_t* const candidates = ldmState->matchCandidates; + unsigned numSplits; + + if (srcSize < minMatchLength) + return iend - anchor; + + /* Initialize the rolling hash state with the first minMatchLength bytes */ + ZSTD_ldm_gear_init(&hashState, params); + ZSTD_ldm_gear_reset(&hashState, ip, minMatchLength); + ip += minMatchLength; + + while (ip < ilimit) { + size_t hashed; + unsigned n; + + numSplits = 0; + hashed = ZSTD_ldm_gear_feed(&hashState, ip, ilimit - ip, + splits, &numSplits); + + for (n = 0; n < numSplits; n++) { + BYTE const* const split = ip + splits[n] - minMatchLength; + U64 const xxhash = XXH64(split, minMatchLength, 0); + U32 const hash = (U32)(xxhash & (((U32)1 << hBits) - 1)); + + candidates[n].split = split; + candidates[n].hash = hash; + candidates[n].checksum = (U32)(xxhash >> 32); + candidates[n].bucket = ZSTD_ldm_getBucket(ldmState, hash, *params); + PREFETCH_L1(candidates[n].bucket); + } + + for (n = 0; n < numSplits; n++) { + size_t forwardMatchLength = 0, backwardMatchLength = 0, + bestMatchLength = 0, mLength; + U32 offset; + BYTE const* const split = candidates[n].split; + U32 const checksum = candidates[n].checksum; + U32 const hash = candidates[n].hash; + ldmEntry_t* const bucket = candidates[n].bucket; + ldmEntry_t const* cur; + ldmEntry_t const* bestEntry = NULL; + ldmEntry_t newEntry; + + newEntry.offset = (U32)(split - base); + newEntry.checksum = checksum; + + /* If a split point would generate a sequence overlapping with + * the previous one, we merely register it in the hash table and + * move on */ + if (split < anchor) { + ZSTD_ldm_insertEntry(ldmState, hash, newEntry, *params); + continue; + } + + for (cur = bucket; cur < bucket + entsPerBucket; cur++) { + size_t curForwardMatchLength, curBackwardMatchLength, + curTotalMatchLength; + if (cur->checksum != checksum || cur->offset <= lowestIndex) { + continue; + } + if (extDict) { + BYTE const* const curMatchBase = + cur->offset < dictLimit ? dictBase : base; + BYTE const* const pMatch = curMatchBase + cur->offset; + BYTE const* const matchEnd = + cur->offset < dictLimit ? dictEnd : iend; + BYTE const* const lowMatchPtr = + cur->offset < dictLimit ? dictStart : lowPrefixPtr; + curForwardMatchLength = + ZSTD_count_2segments(split, pMatch, iend, matchEnd, lowPrefixPtr); + if (curForwardMatchLength < minMatchLength) { + continue; + } + curBackwardMatchLength = ZSTD_ldm_countBackwardsMatch_2segments( + split, anchor, pMatch, lowMatchPtr, dictStart, dictEnd); + } else { /* !extDict */ + BYTE const* const pMatch = base + cur->offset; + curForwardMatchLength = ZSTD_count(split, pMatch, iend); + if (curForwardMatchLength < minMatchLength) { + continue; + } + curBackwardMatchLength = + ZSTD_ldm_countBackwardsMatch(split, anchor, pMatch, lowPrefixPtr); + } + curTotalMatchLength = curForwardMatchLength + curBackwardMatchLength; + + if (curTotalMatchLength > bestMatchLength) { + bestMatchLength = curTotalMatchLength; + forwardMatchLength = curForwardMatchLength; + backwardMatchLength = curBackwardMatchLength; + bestEntry = cur; + } + } + + /* No match found -- insert an entry into the hash table + * and process the next candidate match */ + if (bestEntry == NULL) { + ZSTD_ldm_insertEntry(ldmState, hash, newEntry, *params); + continue; + } + + /* Match found */ + offset = (U32)(split - base) - bestEntry->offset; + mLength = forwardMatchLength + backwardMatchLength; + { + rawSeq* const seq = rawSeqStore->seq + rawSeqStore->size; + + /* Out of sequence storage */ + if (rawSeqStore->size == rawSeqStore->capacity) + return ERROR(dstSize_tooSmall); + seq->litLength = (U32)(split - backwardMatchLength - anchor); + seq->matchLength = (U32)mLength; + seq->offset = offset; + rawSeqStore->size++; + } + + /* Insert the current entry into the hash table --- it must be + * done after the previous block to avoid clobbering bestEntry */ + ZSTD_ldm_insertEntry(ldmState, hash, newEntry, *params); + + anchor = split + forwardMatchLength; + + /* If we find a match that ends after the data that we've hashed + * then we have a repeating, overlapping, pattern. E.g. all zeros. + * If one repetition of the pattern matches our `stopMask` then all + * repetitions will. We don't need to insert them all into out table, + * only the first one. So skip over overlapping matches. + * This is a major speed boost (20x) for compressing a single byte + * repeated, when that byte ends up in the table. + */ + if (anchor > ip + hashed) { + ZSTD_ldm_gear_reset(&hashState, anchor - minMatchLength, minMatchLength); + /* Continue the outer loop at anchor (ip + hashed == anchor). */ + ip = anchor - hashed; + break; + } + } + + ip += hashed; + } + + return iend - anchor; +} + +/*! ZSTD_ldm_reduceTable() : + * reduce table indexes by `reducerValue` */ +static void ZSTD_ldm_reduceTable(ldmEntry_t* const table, U32 const size, + U32 const reducerValue) +{ + U32 u; + for (u = 0; u < size; u++) { + if (table[u].offset < reducerValue) table[u].offset = 0; + else table[u].offset -= reducerValue; + } +} + +size_t ZSTD_ldm_generateSequences( + ldmState_t* ldmState, rawSeqStore_t* sequences, + ldmParams_t const* params, void const* src, size_t srcSize) +{ + U32 const maxDist = 1U << params->windowLog; + BYTE const* const istart = (BYTE const*)src; + BYTE const* const iend = istart + srcSize; + size_t const kMaxChunkSize = 1 << 20; + size_t const nbChunks = (srcSize / kMaxChunkSize) + ((srcSize % kMaxChunkSize) != 0); + size_t chunk; + size_t leftoverSize = 0; + + assert(ZSTD_CHUNKSIZE_MAX >= kMaxChunkSize); + /* Check that ZSTD_window_update() has been called for this chunk prior + * to passing it to this function. + */ + assert(ldmState->window.nextSrc >= (BYTE const*)src + srcSize); + /* The input could be very large (in zstdmt), so it must be broken up into + * chunks to enforce the maximum distance and handle overflow correction. + */ + assert(sequences->pos <= sequences->size); + assert(sequences->size <= sequences->capacity); + for (chunk = 0; chunk < nbChunks && sequences->size < sequences->capacity; ++chunk) { + BYTE const* const chunkStart = istart + chunk * kMaxChunkSize; + size_t const remaining = (size_t)(iend - chunkStart); + BYTE const *const chunkEnd = + (remaining < kMaxChunkSize) ? iend : chunkStart + kMaxChunkSize; + size_t const chunkSize = chunkEnd - chunkStart; + size_t newLeftoverSize; + size_t const prevSize = sequences->size; + + assert(chunkStart < iend); + /* 1. Perform overflow correction if necessary. */ + if (ZSTD_window_needOverflowCorrection(ldmState->window, 0, maxDist, ldmState->loadedDictEnd, chunkStart, chunkEnd)) { + U32 const ldmHSize = 1U << params->hashLog; + U32 const correction = ZSTD_window_correctOverflow( + &ldmState->window, /* cycleLog */ 0, maxDist, chunkStart); + ZSTD_ldm_reduceTable(ldmState->hashTable, ldmHSize, correction); + /* invalidate dictionaries on overflow correction */ + ldmState->loadedDictEnd = 0; + } + /* 2. We enforce the maximum offset allowed. + * + * kMaxChunkSize should be small enough that we don't lose too much of + * the window through early invalidation. + * TODO: * Test the chunk size. + * * Try invalidation after the sequence generation and test the + * offset against maxDist directly. + * + * NOTE: Because of dictionaries + sequence splitting we MUST make sure + * that any offset used is valid at the END of the sequence, since it may + * be split into two sequences. This condition holds when using + * ZSTD_window_enforceMaxDist(), but if we move to checking offsets + * against maxDist directly, we'll have to carefully handle that case. + */ + ZSTD_window_enforceMaxDist(&ldmState->window, chunkEnd, maxDist, &ldmState->loadedDictEnd, NULL); + /* 3. Generate the sequences for the chunk, and get newLeftoverSize. */ + newLeftoverSize = ZSTD_ldm_generateSequences_internal( + ldmState, sequences, params, chunkStart, chunkSize); + if (ZSTD_isError(newLeftoverSize)) + return newLeftoverSize; + /* 4. We add the leftover literals from previous iterations to the first + * newly generated sequence, or add the `newLeftoverSize` if none are + * generated. + */ + /* Prepend the leftover literals from the last call */ + if (prevSize < sequences->size) { + sequences->seq[prevSize].litLength += (U32)leftoverSize; + leftoverSize = newLeftoverSize; + } else { + assert(newLeftoverSize == chunkSize); + leftoverSize += chunkSize; + } + } + return 0; +} + +void +ZSTD_ldm_skipSequences(rawSeqStore_t* rawSeqStore, size_t srcSize, U32 const minMatch) +{ + while (srcSize > 0 && rawSeqStore->pos < rawSeqStore->size) { + rawSeq* seq = rawSeqStore->seq + rawSeqStore->pos; + if (srcSize <= seq->litLength) { + /* Skip past srcSize literals */ + seq->litLength -= (U32)srcSize; + return; + } + srcSize -= seq->litLength; + seq->litLength = 0; + if (srcSize < seq->matchLength) { + /* Skip past the first srcSize of the match */ + seq->matchLength -= (U32)srcSize; + if (seq->matchLength < minMatch) { + /* The match is too short, omit it */ + if (rawSeqStore->pos + 1 < rawSeqStore->size) { + seq[1].litLength += seq[0].matchLength; + } + rawSeqStore->pos++; + } + return; + } + srcSize -= seq->matchLength; + seq->matchLength = 0; + rawSeqStore->pos++; + } +} + +/** + * If the sequence length is longer than remaining then the sequence is split + * between this block and the next. + * + * Returns the current sequence to handle, or if the rest of the block should + * be literals, it returns a sequence with offset == 0. + */ +static rawSeq maybeSplitSequence(rawSeqStore_t* rawSeqStore, + U32 const remaining, U32 const minMatch) +{ + rawSeq sequence = rawSeqStore->seq[rawSeqStore->pos]; + assert(sequence.offset > 0); + /* Likely: No partial sequence */ + if (remaining >= sequence.litLength + sequence.matchLength) { + rawSeqStore->pos++; + return sequence; + } + /* Cut the sequence short (offset == 0 ==> rest is literals). */ + if (remaining <= sequence.litLength) { + sequence.offset = 0; + } else if (remaining < sequence.litLength + sequence.matchLength) { + sequence.matchLength = remaining - sequence.litLength; + if (sequence.matchLength < minMatch) { + sequence.offset = 0; + } + } + /* Skip past `remaining` bytes for the future sequences. */ + ZSTD_ldm_skipSequences(rawSeqStore, remaining, minMatch); + return sequence; +} + +void ZSTD_ldm_skipRawSeqStoreBytes(rawSeqStore_t* rawSeqStore, size_t nbBytes) { + U32 currPos = (U32)(rawSeqStore->posInSequence + nbBytes); + while (currPos && rawSeqStore->pos < rawSeqStore->size) { + rawSeq currSeq = rawSeqStore->seq[rawSeqStore->pos]; + if (currPos >= currSeq.litLength + currSeq.matchLength) { + currPos -= currSeq.litLength + currSeq.matchLength; + rawSeqStore->pos++; + } else { + rawSeqStore->posInSequence = currPos; + break; + } + } + if (currPos == 0 || rawSeqStore->pos == rawSeqStore->size) { + rawSeqStore->posInSequence = 0; + } +} + +size_t ZSTD_ldm_blockCompress(rawSeqStore_t* rawSeqStore, + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + ZSTD_paramSwitch_e useRowMatchFinder, + void const* src, size_t srcSize) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + unsigned const minMatch = cParams->minMatch; + ZSTD_blockCompressor const blockCompressor = + ZSTD_selectBlockCompressor(cParams->strategy, useRowMatchFinder, ZSTD_matchState_dictMode(ms)); + /* Input bounds */ + BYTE const* const istart = (BYTE const*)src; + BYTE const* const iend = istart + srcSize; + /* Input positions */ + BYTE const* ip = istart; + + DEBUGLOG(5, "ZSTD_ldm_blockCompress: srcSize=%zu", srcSize); + /* If using opt parser, use LDMs only as candidates rather than always accepting them */ + if (cParams->strategy >= ZSTD_btopt) { + size_t lastLLSize; + ms->ldmSeqStore = rawSeqStore; + lastLLSize = blockCompressor(ms, seqStore, rep, src, srcSize); + ZSTD_ldm_skipRawSeqStoreBytes(rawSeqStore, srcSize); + return lastLLSize; + } + + assert(rawSeqStore->pos <= rawSeqStore->size); + assert(rawSeqStore->size <= rawSeqStore->capacity); + /* Loop through each sequence and apply the block compressor to the literals */ + while (rawSeqStore->pos < rawSeqStore->size && ip < iend) { + /* maybeSplitSequence updates rawSeqStore->pos */ + rawSeq const sequence = maybeSplitSequence(rawSeqStore, + (U32)(iend - ip), minMatch); + /* End signal */ + if (sequence.offset == 0) + break; + + assert(ip + sequence.litLength + sequence.matchLength <= iend); + + /* Fill tables for block compressor */ + ZSTD_ldm_limitTableUpdate(ms, ip); + ZSTD_ldm_fillFastTables(ms, ip); + /* Run the block compressor */ + DEBUGLOG(5, "pos %u : calling block compressor on segment of size %u", (unsigned)(ip-istart), sequence.litLength); + { + int i; + size_t const newLitLength = + blockCompressor(ms, seqStore, rep, ip, sequence.litLength); + ip += sequence.litLength; + /* Update the repcodes */ + for (i = ZSTD_REP_NUM - 1; i > 0; i--) + rep[i] = rep[i-1]; + rep[0] = sequence.offset; + /* Store the sequence */ + ZSTD_storeSeq(seqStore, newLitLength, ip - newLitLength, iend, + OFFSET_TO_OFFBASE(sequence.offset), + sequence.matchLength); + ip += sequence.matchLength; + } + } + /* Fill the tables for the block compressor */ + ZSTD_ldm_limitTableUpdate(ms, ip); + ZSTD_ldm_fillFastTables(ms, ip); + /* Compress the last literals */ + return blockCompressor(ms, seqStore, rep, ip, iend - ip); +} +/**** ended inlining compress/zstd_ldm.c ****/ +/**** start inlining compress/zstd_opt.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/**** skipping file: zstd_compress_internal.h ****/ +/**** skipping file: hist.h ****/ +/**** skipping file: zstd_opt.h ****/ + +#if !defined(ZSTD_EXCLUDE_BTLAZY2_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTOPT_BLOCK_COMPRESSOR) \ + || !defined(ZSTD_EXCLUDE_BTULTRA_BLOCK_COMPRESSOR) + +#define ZSTD_LITFREQ_ADD 2 /* scaling factor for litFreq, so that frequencies adapt faster to new stats */ +#define ZSTD_MAX_PRICE (1<<30) + +#define ZSTD_PREDEF_THRESHOLD 8 /* if srcSize < ZSTD_PREDEF_THRESHOLD, symbols' cost is assumed static, directly determined by pre-defined distributions */ + + +/*-************************************* +* Price functions for optimal parser +***************************************/ + +#if 0 /* approximation at bit level (for tests) */ +# define BITCOST_ACCURACY 0 +# define BITCOST_MULTIPLIER (1 << BITCOST_ACCURACY) +# define WEIGHT(stat, opt) ((void)(opt), ZSTD_bitWeight(stat)) +#elif 0 /* fractional bit accuracy (for tests) */ +# define BITCOST_ACCURACY 8 +# define BITCOST_MULTIPLIER (1 << BITCOST_ACCURACY) +# define WEIGHT(stat,opt) ((void)(opt), ZSTD_fracWeight(stat)) +#else /* opt==approx, ultra==accurate */ +# define BITCOST_ACCURACY 8 +# define BITCOST_MULTIPLIER (1 << BITCOST_ACCURACY) +# define WEIGHT(stat,opt) ((opt) ? ZSTD_fracWeight(stat) : ZSTD_bitWeight(stat)) +#endif + +/* ZSTD_bitWeight() : + * provide estimated "cost" of a stat in full bits only */ +MEM_STATIC U32 ZSTD_bitWeight(U32 stat) +{ + return (ZSTD_highbit32(stat+1) * BITCOST_MULTIPLIER); +} + +/* ZSTD_fracWeight() : + * provide fractional-bit "cost" of a stat, + * using linear interpolation approximation */ +MEM_STATIC U32 ZSTD_fracWeight(U32 rawStat) +{ + U32 const stat = rawStat + 1; + U32 const hb = ZSTD_highbit32(stat); + U32 const BWeight = hb * BITCOST_MULTIPLIER; + /* Fweight was meant for "Fractional weight" + * but it's effectively a value between 1 and 2 + * using fixed point arithmetic */ + U32 const FWeight = (stat << BITCOST_ACCURACY) >> hb; + U32 const weight = BWeight + FWeight; + assert(hb + BITCOST_ACCURACY < 31); + return weight; +} + +#if (DEBUGLEVEL>=2) +/* debugging function, + * @return price in bytes as fractional value + * for debug messages only */ +MEM_STATIC double ZSTD_fCost(int price) +{ + return (double)price / (BITCOST_MULTIPLIER*8); +} +#endif + +static int ZSTD_compressedLiterals(optState_t const* const optPtr) +{ + return optPtr->literalCompressionMode != ZSTD_ps_disable; +} + +static void ZSTD_setBasePrices(optState_t* optPtr, int optLevel) +{ + if (ZSTD_compressedLiterals(optPtr)) + optPtr->litSumBasePrice = WEIGHT(optPtr->litSum, optLevel); + optPtr->litLengthSumBasePrice = WEIGHT(optPtr->litLengthSum, optLevel); + optPtr->matchLengthSumBasePrice = WEIGHT(optPtr->matchLengthSum, optLevel); + optPtr->offCodeSumBasePrice = WEIGHT(optPtr->offCodeSum, optLevel); +} + + +static U32 sum_u32(const unsigned table[], size_t nbElts) +{ + size_t n; + U32 total = 0; + for (n=0; n0); + unsigned const newStat = base + (table[s] >> shift); + sum += newStat; + table[s] = newStat; + } + return sum; +} + +/* ZSTD_scaleStats() : + * reduce all elt frequencies in table if sum too large + * return the resulting sum of elements */ +static U32 ZSTD_scaleStats(unsigned* table, U32 lastEltIndex, U32 logTarget) +{ + U32 const prevsum = sum_u32(table, lastEltIndex+1); + U32 const factor = prevsum >> logTarget; + DEBUGLOG(5, "ZSTD_scaleStats (nbElts=%u, target=%u)", (unsigned)lastEltIndex+1, (unsigned)logTarget); + assert(logTarget < 30); + if (factor <= 1) return prevsum; + return ZSTD_downscaleStats(table, lastEltIndex, ZSTD_highbit32(factor), base_1guaranteed); +} + +/* ZSTD_rescaleFreqs() : + * if first block (detected by optPtr->litLengthSum == 0) : init statistics + * take hints from dictionary if there is one + * and init from zero if there is none, + * using src for literals stats, and baseline stats for sequence symbols + * otherwise downscale existing stats, to be used as seed for next block. + */ +static void +ZSTD_rescaleFreqs(optState_t* const optPtr, + const BYTE* const src, size_t const srcSize, + int const optLevel) +{ + int const compressedLiterals = ZSTD_compressedLiterals(optPtr); + DEBUGLOG(5, "ZSTD_rescaleFreqs (srcSize=%u)", (unsigned)srcSize); + optPtr->priceType = zop_dynamic; + + if (optPtr->litLengthSum == 0) { /* no literals stats collected -> first block assumed -> init */ + + /* heuristic: use pre-defined stats for too small inputs */ + if (srcSize <= ZSTD_PREDEF_THRESHOLD) { + DEBUGLOG(5, "srcSize <= %i : use predefined stats", ZSTD_PREDEF_THRESHOLD); + optPtr->priceType = zop_predef; + } + + assert(optPtr->symbolCosts != NULL); + if (optPtr->symbolCosts->huf.repeatMode == HUF_repeat_valid) { + + /* huffman stats covering the full value set : table presumed generated by dictionary */ + optPtr->priceType = zop_dynamic; + + if (compressedLiterals) { + /* generate literals statistics from huffman table */ + unsigned lit; + assert(optPtr->litFreq != NULL); + optPtr->litSum = 0; + for (lit=0; lit<=MaxLit; lit++) { + U32 const scaleLog = 11; /* scale to 2K */ + U32 const bitCost = HUF_getNbBitsFromCTable(optPtr->symbolCosts->huf.CTable, lit); + assert(bitCost <= scaleLog); + optPtr->litFreq[lit] = bitCost ? 1 << (scaleLog-bitCost) : 1 /*minimum to calculate cost*/; + optPtr->litSum += optPtr->litFreq[lit]; + } } + + { unsigned ll; + FSE_CState_t llstate; + FSE_initCState(&llstate, optPtr->symbolCosts->fse.litlengthCTable); + optPtr->litLengthSum = 0; + for (ll=0; ll<=MaxLL; ll++) { + U32 const scaleLog = 10; /* scale to 1K */ + U32 const bitCost = FSE_getMaxNbBits(llstate.symbolTT, ll); + assert(bitCost < scaleLog); + optPtr->litLengthFreq[ll] = bitCost ? 1 << (scaleLog-bitCost) : 1 /*minimum to calculate cost*/; + optPtr->litLengthSum += optPtr->litLengthFreq[ll]; + } } + + { unsigned ml; + FSE_CState_t mlstate; + FSE_initCState(&mlstate, optPtr->symbolCosts->fse.matchlengthCTable); + optPtr->matchLengthSum = 0; + for (ml=0; ml<=MaxML; ml++) { + U32 const scaleLog = 10; + U32 const bitCost = FSE_getMaxNbBits(mlstate.symbolTT, ml); + assert(bitCost < scaleLog); + optPtr->matchLengthFreq[ml] = bitCost ? 1 << (scaleLog-bitCost) : 1 /*minimum to calculate cost*/; + optPtr->matchLengthSum += optPtr->matchLengthFreq[ml]; + } } + + { unsigned of; + FSE_CState_t ofstate; + FSE_initCState(&ofstate, optPtr->symbolCosts->fse.offcodeCTable); + optPtr->offCodeSum = 0; + for (of=0; of<=MaxOff; of++) { + U32 const scaleLog = 10; + U32 const bitCost = FSE_getMaxNbBits(ofstate.symbolTT, of); + assert(bitCost < scaleLog); + optPtr->offCodeFreq[of] = bitCost ? 1 << (scaleLog-bitCost) : 1 /*minimum to calculate cost*/; + optPtr->offCodeSum += optPtr->offCodeFreq[of]; + } } + + } else { /* first block, no dictionary */ + + assert(optPtr->litFreq != NULL); + if (compressedLiterals) { + /* base initial cost of literals on direct frequency within src */ + unsigned lit = MaxLit; + HIST_count_simple(optPtr->litFreq, &lit, src, srcSize); /* use raw first block to init statistics */ + optPtr->litSum = ZSTD_downscaleStats(optPtr->litFreq, MaxLit, 8, base_0possible); + } + + { unsigned const baseLLfreqs[MaxLL+1] = { + 4, 2, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1 + }; + ZSTD_memcpy(optPtr->litLengthFreq, baseLLfreqs, sizeof(baseLLfreqs)); + optPtr->litLengthSum = sum_u32(baseLLfreqs, MaxLL+1); + } + + { unsigned ml; + for (ml=0; ml<=MaxML; ml++) + optPtr->matchLengthFreq[ml] = 1; + } + optPtr->matchLengthSum = MaxML+1; + + { unsigned const baseOFCfreqs[MaxOff+1] = { + 6, 2, 1, 1, 2, 3, 4, 4, + 4, 3, 2, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1 + }; + ZSTD_memcpy(optPtr->offCodeFreq, baseOFCfreqs, sizeof(baseOFCfreqs)); + optPtr->offCodeSum = sum_u32(baseOFCfreqs, MaxOff+1); + } + + } + + } else { /* new block : scale down accumulated statistics */ + + if (compressedLiterals) + optPtr->litSum = ZSTD_scaleStats(optPtr->litFreq, MaxLit, 12); + optPtr->litLengthSum = ZSTD_scaleStats(optPtr->litLengthFreq, MaxLL, 11); + optPtr->matchLengthSum = ZSTD_scaleStats(optPtr->matchLengthFreq, MaxML, 11); + optPtr->offCodeSum = ZSTD_scaleStats(optPtr->offCodeFreq, MaxOff, 11); + } + + ZSTD_setBasePrices(optPtr, optLevel); +} + +/* ZSTD_rawLiteralsCost() : + * price of literals (only) in specified segment (which length can be 0). + * does not include price of literalLength symbol */ +static U32 ZSTD_rawLiteralsCost(const BYTE* const literals, U32 const litLength, + const optState_t* const optPtr, + int optLevel) +{ + DEBUGLOG(8, "ZSTD_rawLiteralsCost (%u literals)", litLength); + if (litLength == 0) return 0; + + if (!ZSTD_compressedLiterals(optPtr)) + return (litLength << 3) * BITCOST_MULTIPLIER; /* Uncompressed - 8 bytes per literal. */ + + if (optPtr->priceType == zop_predef) + return (litLength*6) * BITCOST_MULTIPLIER; /* 6 bit per literal - no statistic used */ + + /* dynamic statistics */ + { U32 price = optPtr->litSumBasePrice * litLength; + U32 const litPriceMax = optPtr->litSumBasePrice - BITCOST_MULTIPLIER; + U32 u; + assert(optPtr->litSumBasePrice >= BITCOST_MULTIPLIER); + for (u=0; u < litLength; u++) { + U32 litPrice = WEIGHT(optPtr->litFreq[literals[u]], optLevel); + if (UNLIKELY(litPrice > litPriceMax)) litPrice = litPriceMax; + price -= litPrice; + } + return price; + } +} + +/* ZSTD_litLengthPrice() : + * cost of literalLength symbol */ +static U32 ZSTD_litLengthPrice(U32 const litLength, const optState_t* const optPtr, int optLevel) +{ + assert(litLength <= ZSTD_BLOCKSIZE_MAX); + if (optPtr->priceType == zop_predef) + return WEIGHT(litLength, optLevel); + + /* ZSTD_LLcode() can't compute litLength price for sizes >= ZSTD_BLOCKSIZE_MAX + * because it isn't representable in the zstd format. + * So instead just pretend it would cost 1 bit more than ZSTD_BLOCKSIZE_MAX - 1. + * In such a case, the block would be all literals. + */ + if (litLength == ZSTD_BLOCKSIZE_MAX) + return BITCOST_MULTIPLIER + ZSTD_litLengthPrice(ZSTD_BLOCKSIZE_MAX - 1, optPtr, optLevel); + + /* dynamic statistics */ + { U32 const llCode = ZSTD_LLcode(litLength); + return (LL_bits[llCode] * BITCOST_MULTIPLIER) + + optPtr->litLengthSumBasePrice + - WEIGHT(optPtr->litLengthFreq[llCode], optLevel); + } +} + +/* ZSTD_getMatchPrice() : + * Provides the cost of the match part (offset + matchLength) of a sequence. + * Must be combined with ZSTD_fullLiteralsCost() to get the full cost of a sequence. + * @offBase : sumtype, representing an offset or a repcode, and using numeric representation of ZSTD_storeSeq() + * @optLevel: when <2, favors small offset for decompression speed (improved cache efficiency) + */ +FORCE_INLINE_TEMPLATE U32 +ZSTD_getMatchPrice(U32 const offBase, + U32 const matchLength, + const optState_t* const optPtr, + int const optLevel) +{ + U32 price; + U32 const offCode = ZSTD_highbit32(offBase); + U32 const mlBase = matchLength - MINMATCH; + assert(matchLength >= MINMATCH); + + if (optPtr->priceType == zop_predef) /* fixed scheme, does not use statistics */ + return WEIGHT(mlBase, optLevel) + + ((16 + offCode) * BITCOST_MULTIPLIER); /* emulated offset cost */ + + /* dynamic statistics */ + price = (offCode * BITCOST_MULTIPLIER) + (optPtr->offCodeSumBasePrice - WEIGHT(optPtr->offCodeFreq[offCode], optLevel)); + if ((optLevel<2) /*static*/ && offCode >= 20) + price += (offCode-19)*2 * BITCOST_MULTIPLIER; /* handicap for long distance offsets, favor decompression speed */ + + /* match Length */ + { U32 const mlCode = ZSTD_MLcode(mlBase); + price += (ML_bits[mlCode] * BITCOST_MULTIPLIER) + (optPtr->matchLengthSumBasePrice - WEIGHT(optPtr->matchLengthFreq[mlCode], optLevel)); + } + + price += BITCOST_MULTIPLIER / 5; /* heuristic : make matches a bit more costly to favor less sequences -> faster decompression speed */ + + DEBUGLOG(8, "ZSTD_getMatchPrice(ml:%u) = %u", matchLength, price); + return price; +} + +/* ZSTD_updateStats() : + * assumption : literals + litLength <= iend */ +static void ZSTD_updateStats(optState_t* const optPtr, + U32 litLength, const BYTE* literals, + U32 offBase, U32 matchLength) +{ + /* literals */ + if (ZSTD_compressedLiterals(optPtr)) { + U32 u; + for (u=0; u < litLength; u++) + optPtr->litFreq[literals[u]] += ZSTD_LITFREQ_ADD; + optPtr->litSum += litLength*ZSTD_LITFREQ_ADD; + } + + /* literal Length */ + { U32 const llCode = ZSTD_LLcode(litLength); + optPtr->litLengthFreq[llCode]++; + optPtr->litLengthSum++; + } + + /* offset code : follows storeSeq() numeric representation */ + { U32 const offCode = ZSTD_highbit32(offBase); + assert(offCode <= MaxOff); + optPtr->offCodeFreq[offCode]++; + optPtr->offCodeSum++; + } + + /* match Length */ + { U32 const mlBase = matchLength - MINMATCH; + U32 const mlCode = ZSTD_MLcode(mlBase); + optPtr->matchLengthFreq[mlCode]++; + optPtr->matchLengthSum++; + } +} + + +/* ZSTD_readMINMATCH() : + * function safe only for comparisons + * assumption : memPtr must be at least 4 bytes before end of buffer */ +MEM_STATIC U32 ZSTD_readMINMATCH(const void* memPtr, U32 length) +{ + switch (length) + { + default : + case 4 : return MEM_read32(memPtr); + case 3 : if (MEM_isLittleEndian()) + return MEM_read32(memPtr)<<8; + else + return MEM_read32(memPtr)>>8; + } +} + + +/* Update hashTable3 up to ip (excluded) + Assumption : always within prefix (i.e. not within extDict) */ +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +U32 ZSTD_insertAndFindFirstIndexHash3 (const ZSTD_matchState_t* ms, + U32* nextToUpdate3, + const BYTE* const ip) +{ + U32* const hashTable3 = ms->hashTable3; + U32 const hashLog3 = ms->hashLog3; + const BYTE* const base = ms->window.base; + U32 idx = *nextToUpdate3; + U32 const target = (U32)(ip - base); + size_t const hash3 = ZSTD_hash3Ptr(ip, hashLog3); + assert(hashLog3 > 0); + + while(idx < target) { + hashTable3[ZSTD_hash3Ptr(base+idx, hashLog3)] = idx; + idx++; + } + + *nextToUpdate3 = target; + return hashTable3[hash3]; +} + + +/*-************************************* +* Binary Tree search +***************************************/ +/** ZSTD_insertBt1() : add one or multiple positions to tree. + * @param ip assumed <= iend-8 . + * @param target The target of ZSTD_updateTree_internal() - we are filling to this position + * @return : nb of positions added */ +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +U32 ZSTD_insertBt1( + const ZSTD_matchState_t* ms, + const BYTE* const ip, const BYTE* const iend, + U32 const target, + U32 const mls, const int extDict) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32* const hashTable = ms->hashTable; + U32 const hashLog = cParams->hashLog; + size_t const h = ZSTD_hashPtr(ip, hashLog, mls); + U32* const bt = ms->chainTable; + U32 const btLog = cParams->chainLog - 1; + U32 const btMask = (1 << btLog) - 1; + U32 matchIndex = hashTable[h]; + size_t commonLengthSmaller=0, commonLengthLarger=0; + const BYTE* const base = ms->window.base; + const BYTE* const dictBase = ms->window.dictBase; + const U32 dictLimit = ms->window.dictLimit; + const BYTE* const dictEnd = dictBase + dictLimit; + const BYTE* const prefixStart = base + dictLimit; + const BYTE* match; + const U32 curr = (U32)(ip-base); + const U32 btLow = btMask >= curr ? 0 : curr - btMask; + U32* smallerPtr = bt + 2*(curr&btMask); + U32* largerPtr = smallerPtr + 1; + U32 dummy32; /* to be nullified at the end */ + /* windowLow is based on target because + * we only need positions that will be in the window at the end of the tree update. + */ + U32 const windowLow = ZSTD_getLowestMatchIndex(ms, target, cParams->windowLog); + U32 matchEndIdx = curr+8+1; + size_t bestLength = 8; + U32 nbCompares = 1U << cParams->searchLog; +#ifdef ZSTD_C_PREDICT + U32 predictedSmall = *(bt + 2*((curr-1)&btMask) + 0); + U32 predictedLarge = *(bt + 2*((curr-1)&btMask) + 1); + predictedSmall += (predictedSmall>0); + predictedLarge += (predictedLarge>0); +#endif /* ZSTD_C_PREDICT */ + + DEBUGLOG(8, "ZSTD_insertBt1 (%u)", curr); + + assert(curr <= target); + assert(ip <= iend-8); /* required for h calculation */ + hashTable[h] = curr; /* Update Hash Table */ + + assert(windowLow > 0); + for (; nbCompares && (matchIndex >= windowLow); --nbCompares) { + U32* const nextPtr = bt + 2*(matchIndex & btMask); + size_t matchLength = MIN(commonLengthSmaller, commonLengthLarger); /* guaranteed minimum nb of common bytes */ + assert(matchIndex < curr); + +#ifdef ZSTD_C_PREDICT /* note : can create issues when hlog small <= 11 */ + const U32* predictPtr = bt + 2*((matchIndex-1) & btMask); /* written this way, as bt is a roll buffer */ + if (matchIndex == predictedSmall) { + /* no need to check length, result known */ + *smallerPtr = matchIndex; + if (matchIndex <= btLow) { smallerPtr=&dummy32; break; } /* beyond tree size, stop the search */ + smallerPtr = nextPtr+1; /* new "smaller" => larger of match */ + matchIndex = nextPtr[1]; /* new matchIndex larger than previous (closer to current) */ + predictedSmall = predictPtr[1] + (predictPtr[1]>0); + continue; + } + if (matchIndex == predictedLarge) { + *largerPtr = matchIndex; + if (matchIndex <= btLow) { largerPtr=&dummy32; break; } /* beyond tree size, stop the search */ + largerPtr = nextPtr; + matchIndex = nextPtr[0]; + predictedLarge = predictPtr[0] + (predictPtr[0]>0); + continue; + } +#endif + + if (!extDict || (matchIndex+matchLength >= dictLimit)) { + assert(matchIndex+matchLength >= dictLimit); /* might be wrong if actually extDict */ + match = base + matchIndex; + matchLength += ZSTD_count(ip+matchLength, match+matchLength, iend); + } else { + match = dictBase + matchIndex; + matchLength += ZSTD_count_2segments(ip+matchLength, match+matchLength, iend, dictEnd, prefixStart); + if (matchIndex+matchLength >= dictLimit) + match = base + matchIndex; /* to prepare for next usage of match[matchLength] */ + } + + if (matchLength > bestLength) { + bestLength = matchLength; + if (matchLength > matchEndIdx - matchIndex) + matchEndIdx = matchIndex + (U32)matchLength; + } + + if (ip+matchLength == iend) { /* equal : no way to know if inf or sup */ + break; /* drop , to guarantee consistency ; miss a bit of compression, but other solutions can corrupt tree */ + } + + if (match[matchLength] < ip[matchLength]) { /* necessarily within buffer */ + /* match is smaller than current */ + *smallerPtr = matchIndex; /* update smaller idx */ + commonLengthSmaller = matchLength; /* all smaller will now have at least this guaranteed common length */ + if (matchIndex <= btLow) { smallerPtr=&dummy32; break; } /* beyond tree size, stop searching */ + smallerPtr = nextPtr+1; /* new "candidate" => larger than match, which was smaller than target */ + matchIndex = nextPtr[1]; /* new matchIndex, larger than previous and closer to current */ + } else { + /* match is larger than current */ + *largerPtr = matchIndex; + commonLengthLarger = matchLength; + if (matchIndex <= btLow) { largerPtr=&dummy32; break; } /* beyond tree size, stop searching */ + largerPtr = nextPtr; + matchIndex = nextPtr[0]; + } } + + *smallerPtr = *largerPtr = 0; + { U32 positions = 0; + if (bestLength > 384) positions = MIN(192, (U32)(bestLength - 384)); /* speed optimization */ + assert(matchEndIdx > curr + 8); + return MAX(positions, matchEndIdx - (curr + 8)); + } +} + +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_updateTree_internal( + ZSTD_matchState_t* ms, + const BYTE* const ip, const BYTE* const iend, + const U32 mls, const ZSTD_dictMode_e dictMode) +{ + const BYTE* const base = ms->window.base; + U32 const target = (U32)(ip - base); + U32 idx = ms->nextToUpdate; + DEBUGLOG(7, "ZSTD_updateTree_internal, from %u to %u (dictMode:%u)", + idx, target, dictMode); + + while(idx < target) { + U32 const forward = ZSTD_insertBt1(ms, base+idx, iend, target, mls, dictMode == ZSTD_extDict); + assert(idx < (U32)(idx + forward)); + idx += forward; + } + assert((size_t)(ip - base) <= (size_t)(U32)(-1)); + assert((size_t)(iend - base) <= (size_t)(U32)(-1)); + ms->nextToUpdate = target; +} + +void ZSTD_updateTree(ZSTD_matchState_t* ms, const BYTE* ip, const BYTE* iend) { + ZSTD_updateTree_internal(ms, ip, iend, ms->cParams.minMatch, ZSTD_noDict); +} + +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +U32 +ZSTD_insertBtAndGetAllMatches ( + ZSTD_match_t* matches, /* store result (found matches) in this table (presumed large enough) */ + ZSTD_matchState_t* ms, + U32* nextToUpdate3, + const BYTE* const ip, const BYTE* const iLimit, + const ZSTD_dictMode_e dictMode, + const U32 rep[ZSTD_REP_NUM], + const U32 ll0, /* tells if associated literal length is 0 or not. This value must be 0 or 1 */ + const U32 lengthToBeat, + const U32 mls /* template */) +{ + const ZSTD_compressionParameters* const cParams = &ms->cParams; + U32 const sufficient_len = MIN(cParams->targetLength, ZSTD_OPT_NUM -1); + const BYTE* const base = ms->window.base; + U32 const curr = (U32)(ip-base); + U32 const hashLog = cParams->hashLog; + U32 const minMatch = (mls==3) ? 3 : 4; + U32* const hashTable = ms->hashTable; + size_t const h = ZSTD_hashPtr(ip, hashLog, mls); + U32 matchIndex = hashTable[h]; + U32* const bt = ms->chainTable; + U32 const btLog = cParams->chainLog - 1; + U32 const btMask= (1U << btLog) - 1; + size_t commonLengthSmaller=0, commonLengthLarger=0; + const BYTE* const dictBase = ms->window.dictBase; + U32 const dictLimit = ms->window.dictLimit; + const BYTE* const dictEnd = dictBase + dictLimit; + const BYTE* const prefixStart = base + dictLimit; + U32 const btLow = (btMask >= curr) ? 0 : curr - btMask; + U32 const windowLow = ZSTD_getLowestMatchIndex(ms, curr, cParams->windowLog); + U32 const matchLow = windowLow ? windowLow : 1; + U32* smallerPtr = bt + 2*(curr&btMask); + U32* largerPtr = bt + 2*(curr&btMask) + 1; + U32 matchEndIdx = curr+8+1; /* farthest referenced position of any match => detects repetitive patterns */ + U32 dummy32; /* to be nullified at the end */ + U32 mnum = 0; + U32 nbCompares = 1U << cParams->searchLog; + + const ZSTD_matchState_t* dms = dictMode == ZSTD_dictMatchState ? ms->dictMatchState : NULL; + const ZSTD_compressionParameters* const dmsCParams = + dictMode == ZSTD_dictMatchState ? &dms->cParams : NULL; + const BYTE* const dmsBase = dictMode == ZSTD_dictMatchState ? dms->window.base : NULL; + const BYTE* const dmsEnd = dictMode == ZSTD_dictMatchState ? dms->window.nextSrc : NULL; + U32 const dmsHighLimit = dictMode == ZSTD_dictMatchState ? (U32)(dmsEnd - dmsBase) : 0; + U32 const dmsLowLimit = dictMode == ZSTD_dictMatchState ? dms->window.lowLimit : 0; + U32 const dmsIndexDelta = dictMode == ZSTD_dictMatchState ? windowLow - dmsHighLimit : 0; + U32 const dmsHashLog = dictMode == ZSTD_dictMatchState ? dmsCParams->hashLog : hashLog; + U32 const dmsBtLog = dictMode == ZSTD_dictMatchState ? dmsCParams->chainLog - 1 : btLog; + U32 const dmsBtMask = dictMode == ZSTD_dictMatchState ? (1U << dmsBtLog) - 1 : 0; + U32 const dmsBtLow = dictMode == ZSTD_dictMatchState && dmsBtMask < dmsHighLimit - dmsLowLimit ? dmsHighLimit - dmsBtMask : dmsLowLimit; + + size_t bestLength = lengthToBeat-1; + DEBUGLOG(8, "ZSTD_insertBtAndGetAllMatches: current=%u", curr); + + /* check repCode */ + assert(ll0 <= 1); /* necessarily 1 or 0 */ + { U32 const lastR = ZSTD_REP_NUM + ll0; + U32 repCode; + for (repCode = ll0; repCode < lastR; repCode++) { + U32 const repOffset = (repCode==ZSTD_REP_NUM) ? (rep[0] - 1) : rep[repCode]; + U32 const repIndex = curr - repOffset; + U32 repLen = 0; + assert(curr >= dictLimit); + if (repOffset-1 /* intentional overflow, discards 0 and -1 */ < curr-dictLimit) { /* equivalent to `curr > repIndex >= dictLimit` */ + /* We must validate the repcode offset because when we're using a dictionary the + * valid offset range shrinks when the dictionary goes out of bounds. + */ + if ((repIndex >= windowLow) & (ZSTD_readMINMATCH(ip, minMatch) == ZSTD_readMINMATCH(ip - repOffset, minMatch))) { + repLen = (U32)ZSTD_count(ip+minMatch, ip+minMatch-repOffset, iLimit) + minMatch; + } + } else { /* repIndex < dictLimit || repIndex >= curr */ + const BYTE* const repMatch = dictMode == ZSTD_dictMatchState ? + dmsBase + repIndex - dmsIndexDelta : + dictBase + repIndex; + assert(curr >= windowLow); + if ( dictMode == ZSTD_extDict + && ( ((repOffset-1) /*intentional overflow*/ < curr - windowLow) /* equivalent to `curr > repIndex >= windowLow` */ + & (((U32)((dictLimit-1) - repIndex) >= 3) ) /* intentional overflow : do not test positions overlapping 2 memory segments */) + && (ZSTD_readMINMATCH(ip, minMatch) == ZSTD_readMINMATCH(repMatch, minMatch)) ) { + repLen = (U32)ZSTD_count_2segments(ip+minMatch, repMatch+minMatch, iLimit, dictEnd, prefixStart) + minMatch; + } + if (dictMode == ZSTD_dictMatchState + && ( ((repOffset-1) /*intentional overflow*/ < curr - (dmsLowLimit + dmsIndexDelta)) /* equivalent to `curr > repIndex >= dmsLowLimit` */ + & ((U32)((dictLimit-1) - repIndex) >= 3) ) /* intentional overflow : do not test positions overlapping 2 memory segments */ + && (ZSTD_readMINMATCH(ip, minMatch) == ZSTD_readMINMATCH(repMatch, minMatch)) ) { + repLen = (U32)ZSTD_count_2segments(ip+minMatch, repMatch+minMatch, iLimit, dmsEnd, prefixStart) + minMatch; + } } + /* save longer solution */ + if (repLen > bestLength) { + DEBUGLOG(8, "found repCode %u (ll0:%u, offset:%u) of length %u", + repCode, ll0, repOffset, repLen); + bestLength = repLen; + matches[mnum].off = REPCODE_TO_OFFBASE(repCode - ll0 + 1); /* expect value between 1 and 3 */ + matches[mnum].len = (U32)repLen; + mnum++; + if ( (repLen > sufficient_len) + | (ip+repLen == iLimit) ) { /* best possible */ + return mnum; + } } } } + + /* HC3 match finder */ + if ((mls == 3) /*static*/ && (bestLength < mls)) { + U32 const matchIndex3 = ZSTD_insertAndFindFirstIndexHash3(ms, nextToUpdate3, ip); + if ((matchIndex3 >= matchLow) + & (curr - matchIndex3 < (1<<18)) /*heuristic : longer distance likely too expensive*/ ) { + size_t mlen; + if ((dictMode == ZSTD_noDict) /*static*/ || (dictMode == ZSTD_dictMatchState) /*static*/ || (matchIndex3 >= dictLimit)) { + const BYTE* const match = base + matchIndex3; + mlen = ZSTD_count(ip, match, iLimit); + } else { + const BYTE* const match = dictBase + matchIndex3; + mlen = ZSTD_count_2segments(ip, match, iLimit, dictEnd, prefixStart); + } + + /* save best solution */ + if (mlen >= mls /* == 3 > bestLength */) { + DEBUGLOG(8, "found small match with hlog3, of length %u", + (U32)mlen); + bestLength = mlen; + assert(curr > matchIndex3); + assert(mnum==0); /* no prior solution */ + matches[0].off = OFFSET_TO_OFFBASE(curr - matchIndex3); + matches[0].len = (U32)mlen; + mnum = 1; + if ( (mlen > sufficient_len) | + (ip+mlen == iLimit) ) { /* best possible length */ + ms->nextToUpdate = curr+1; /* skip insertion */ + return 1; + } } } + /* no dictMatchState lookup: dicts don't have a populated HC3 table */ + } /* if (mls == 3) */ + + hashTable[h] = curr; /* Update Hash Table */ + + for (; nbCompares && (matchIndex >= matchLow); --nbCompares) { + U32* const nextPtr = bt + 2*(matchIndex & btMask); + const BYTE* match; + size_t matchLength = MIN(commonLengthSmaller, commonLengthLarger); /* guaranteed minimum nb of common bytes */ + assert(curr > matchIndex); + + if ((dictMode == ZSTD_noDict) || (dictMode == ZSTD_dictMatchState) || (matchIndex+matchLength >= dictLimit)) { + assert(matchIndex+matchLength >= dictLimit); /* ensure the condition is correct when !extDict */ + match = base + matchIndex; + if (matchIndex >= dictLimit) assert(memcmp(match, ip, matchLength) == 0); /* ensure early section of match is equal as expected */ + matchLength += ZSTD_count(ip+matchLength, match+matchLength, iLimit); + } else { + match = dictBase + matchIndex; + assert(memcmp(match, ip, matchLength) == 0); /* ensure early section of match is equal as expected */ + matchLength += ZSTD_count_2segments(ip+matchLength, match+matchLength, iLimit, dictEnd, prefixStart); + if (matchIndex+matchLength >= dictLimit) + match = base + matchIndex; /* prepare for match[matchLength] read */ + } + + if (matchLength > bestLength) { + DEBUGLOG(8, "found match of length %u at distance %u (offBase=%u)", + (U32)matchLength, curr - matchIndex, OFFSET_TO_OFFBASE(curr - matchIndex)); + assert(matchEndIdx > matchIndex); + if (matchLength > matchEndIdx - matchIndex) + matchEndIdx = matchIndex + (U32)matchLength; + bestLength = matchLength; + matches[mnum].off = OFFSET_TO_OFFBASE(curr - matchIndex); + matches[mnum].len = (U32)matchLength; + mnum++; + if ( (matchLength > ZSTD_OPT_NUM) + | (ip+matchLength == iLimit) /* equal : no way to know if inf or sup */) { + if (dictMode == ZSTD_dictMatchState) nbCompares = 0; /* break should also skip searching dms */ + break; /* drop, to preserve bt consistency (miss a little bit of compression) */ + } } + + if (match[matchLength] < ip[matchLength]) { + /* match smaller than current */ + *smallerPtr = matchIndex; /* update smaller idx */ + commonLengthSmaller = matchLength; /* all smaller will now have at least this guaranteed common length */ + if (matchIndex <= btLow) { smallerPtr=&dummy32; break; } /* beyond tree size, stop the search */ + smallerPtr = nextPtr+1; /* new candidate => larger than match, which was smaller than current */ + matchIndex = nextPtr[1]; /* new matchIndex, larger than previous, closer to current */ + } else { + *largerPtr = matchIndex; + commonLengthLarger = matchLength; + if (matchIndex <= btLow) { largerPtr=&dummy32; break; } /* beyond tree size, stop the search */ + largerPtr = nextPtr; + matchIndex = nextPtr[0]; + } } + + *smallerPtr = *largerPtr = 0; + + assert(nbCompares <= (1U << ZSTD_SEARCHLOG_MAX)); /* Check we haven't underflowed. */ + if (dictMode == ZSTD_dictMatchState && nbCompares) { + size_t const dmsH = ZSTD_hashPtr(ip, dmsHashLog, mls); + U32 dictMatchIndex = dms->hashTable[dmsH]; + const U32* const dmsBt = dms->chainTable; + commonLengthSmaller = commonLengthLarger = 0; + for (; nbCompares && (dictMatchIndex > dmsLowLimit); --nbCompares) { + const U32* const nextPtr = dmsBt + 2*(dictMatchIndex & dmsBtMask); + size_t matchLength = MIN(commonLengthSmaller, commonLengthLarger); /* guaranteed minimum nb of common bytes */ + const BYTE* match = dmsBase + dictMatchIndex; + matchLength += ZSTD_count_2segments(ip+matchLength, match+matchLength, iLimit, dmsEnd, prefixStart); + if (dictMatchIndex+matchLength >= dmsHighLimit) + match = base + dictMatchIndex + dmsIndexDelta; /* to prepare for next usage of match[matchLength] */ + + if (matchLength > bestLength) { + matchIndex = dictMatchIndex + dmsIndexDelta; + DEBUGLOG(8, "found dms match of length %u at distance %u (offBase=%u)", + (U32)matchLength, curr - matchIndex, OFFSET_TO_OFFBASE(curr - matchIndex)); + if (matchLength > matchEndIdx - matchIndex) + matchEndIdx = matchIndex + (U32)matchLength; + bestLength = matchLength; + matches[mnum].off = OFFSET_TO_OFFBASE(curr - matchIndex); + matches[mnum].len = (U32)matchLength; + mnum++; + if ( (matchLength > ZSTD_OPT_NUM) + | (ip+matchLength == iLimit) /* equal : no way to know if inf or sup */) { + break; /* drop, to guarantee consistency (miss a little bit of compression) */ + } } + + if (dictMatchIndex <= dmsBtLow) { break; } /* beyond tree size, stop the search */ + if (match[matchLength] < ip[matchLength]) { + commonLengthSmaller = matchLength; /* all smaller will now have at least this guaranteed common length */ + dictMatchIndex = nextPtr[1]; /* new matchIndex larger than previous (closer to current) */ + } else { + /* match is larger than current */ + commonLengthLarger = matchLength; + dictMatchIndex = nextPtr[0]; + } } } /* if (dictMode == ZSTD_dictMatchState) */ + + assert(matchEndIdx > curr+8); + ms->nextToUpdate = matchEndIdx - 8; /* skip repetitive patterns */ + return mnum; +} + +typedef U32 (*ZSTD_getAllMatchesFn)( + ZSTD_match_t*, + ZSTD_matchState_t*, + U32*, + const BYTE*, + const BYTE*, + const U32 rep[ZSTD_REP_NUM], + U32 const ll0, + U32 const lengthToBeat); + +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +U32 ZSTD_btGetAllMatches_internal( + ZSTD_match_t* matches, + ZSTD_matchState_t* ms, + U32* nextToUpdate3, + const BYTE* ip, + const BYTE* const iHighLimit, + const U32 rep[ZSTD_REP_NUM], + U32 const ll0, + U32 const lengthToBeat, + const ZSTD_dictMode_e dictMode, + const U32 mls) +{ + assert(BOUNDED(3, ms->cParams.minMatch, 6) == mls); + DEBUGLOG(8, "ZSTD_BtGetAllMatches(dictMode=%d, mls=%u)", (int)dictMode, mls); + if (ip < ms->window.base + ms->nextToUpdate) + return 0; /* skipped area */ + ZSTD_updateTree_internal(ms, ip, iHighLimit, mls, dictMode); + return ZSTD_insertBtAndGetAllMatches(matches, ms, nextToUpdate3, ip, iHighLimit, dictMode, rep, ll0, lengthToBeat, mls); +} + +#define ZSTD_BT_GET_ALL_MATCHES_FN(dictMode, mls) ZSTD_btGetAllMatches_##dictMode##_##mls + +#define GEN_ZSTD_BT_GET_ALL_MATCHES_(dictMode, mls) \ + static U32 ZSTD_BT_GET_ALL_MATCHES_FN(dictMode, mls)( \ + ZSTD_match_t* matches, \ + ZSTD_matchState_t* ms, \ + U32* nextToUpdate3, \ + const BYTE* ip, \ + const BYTE* const iHighLimit, \ + const U32 rep[ZSTD_REP_NUM], \ + U32 const ll0, \ + U32 const lengthToBeat) \ + { \ + return ZSTD_btGetAllMatches_internal( \ + matches, ms, nextToUpdate3, ip, iHighLimit, \ + rep, ll0, lengthToBeat, ZSTD_##dictMode, mls); \ + } + +#define GEN_ZSTD_BT_GET_ALL_MATCHES(dictMode) \ + GEN_ZSTD_BT_GET_ALL_MATCHES_(dictMode, 3) \ + GEN_ZSTD_BT_GET_ALL_MATCHES_(dictMode, 4) \ + GEN_ZSTD_BT_GET_ALL_MATCHES_(dictMode, 5) \ + GEN_ZSTD_BT_GET_ALL_MATCHES_(dictMode, 6) + +GEN_ZSTD_BT_GET_ALL_MATCHES(noDict) +GEN_ZSTD_BT_GET_ALL_MATCHES(extDict) +GEN_ZSTD_BT_GET_ALL_MATCHES(dictMatchState) + +#define ZSTD_BT_GET_ALL_MATCHES_ARRAY(dictMode) \ + { \ + ZSTD_BT_GET_ALL_MATCHES_FN(dictMode, 3), \ + ZSTD_BT_GET_ALL_MATCHES_FN(dictMode, 4), \ + ZSTD_BT_GET_ALL_MATCHES_FN(dictMode, 5), \ + ZSTD_BT_GET_ALL_MATCHES_FN(dictMode, 6) \ + } + +static ZSTD_getAllMatchesFn +ZSTD_selectBtGetAllMatches(ZSTD_matchState_t const* ms, ZSTD_dictMode_e const dictMode) +{ + ZSTD_getAllMatchesFn const getAllMatchesFns[3][4] = { + ZSTD_BT_GET_ALL_MATCHES_ARRAY(noDict), + ZSTD_BT_GET_ALL_MATCHES_ARRAY(extDict), + ZSTD_BT_GET_ALL_MATCHES_ARRAY(dictMatchState) + }; + U32 const mls = BOUNDED(3, ms->cParams.minMatch, 6); + assert((U32)dictMode < 3); + assert(mls - 3 < 4); + return getAllMatchesFns[(int)dictMode][mls - 3]; +} + +/************************* +* LDM helper functions * +*************************/ + +/* Struct containing info needed to make decision about ldm inclusion */ +typedef struct { + rawSeqStore_t seqStore; /* External match candidates store for this block */ + U32 startPosInBlock; /* Start position of the current match candidate */ + U32 endPosInBlock; /* End position of the current match candidate */ + U32 offset; /* Offset of the match candidate */ +} ZSTD_optLdm_t; + +/* ZSTD_optLdm_skipRawSeqStoreBytes(): + * Moves forward in @rawSeqStore by @nbBytes, + * which will update the fields 'pos' and 'posInSequence'. + */ +static void ZSTD_optLdm_skipRawSeqStoreBytes(rawSeqStore_t* rawSeqStore, size_t nbBytes) +{ + U32 currPos = (U32)(rawSeqStore->posInSequence + nbBytes); + while (currPos && rawSeqStore->pos < rawSeqStore->size) { + rawSeq currSeq = rawSeqStore->seq[rawSeqStore->pos]; + if (currPos >= currSeq.litLength + currSeq.matchLength) { + currPos -= currSeq.litLength + currSeq.matchLength; + rawSeqStore->pos++; + } else { + rawSeqStore->posInSequence = currPos; + break; + } + } + if (currPos == 0 || rawSeqStore->pos == rawSeqStore->size) { + rawSeqStore->posInSequence = 0; + } +} + +/* ZSTD_opt_getNextMatchAndUpdateSeqStore(): + * Calculates the beginning and end of the next match in the current block. + * Updates 'pos' and 'posInSequence' of the ldmSeqStore. + */ +static void +ZSTD_opt_getNextMatchAndUpdateSeqStore(ZSTD_optLdm_t* optLdm, U32 currPosInBlock, + U32 blockBytesRemaining) +{ + rawSeq currSeq; + U32 currBlockEndPos; + U32 literalsBytesRemaining; + U32 matchBytesRemaining; + + /* Setting match end position to MAX to ensure we never use an LDM during this block */ + if (optLdm->seqStore.size == 0 || optLdm->seqStore.pos >= optLdm->seqStore.size) { + optLdm->startPosInBlock = UINT_MAX; + optLdm->endPosInBlock = UINT_MAX; + return; + } + /* Calculate appropriate bytes left in matchLength and litLength + * after adjusting based on ldmSeqStore->posInSequence */ + currSeq = optLdm->seqStore.seq[optLdm->seqStore.pos]; + assert(optLdm->seqStore.posInSequence <= currSeq.litLength + currSeq.matchLength); + currBlockEndPos = currPosInBlock + blockBytesRemaining; + literalsBytesRemaining = (optLdm->seqStore.posInSequence < currSeq.litLength) ? + currSeq.litLength - (U32)optLdm->seqStore.posInSequence : + 0; + matchBytesRemaining = (literalsBytesRemaining == 0) ? + currSeq.matchLength - ((U32)optLdm->seqStore.posInSequence - currSeq.litLength) : + currSeq.matchLength; + + /* If there are more literal bytes than bytes remaining in block, no ldm is possible */ + if (literalsBytesRemaining >= blockBytesRemaining) { + optLdm->startPosInBlock = UINT_MAX; + optLdm->endPosInBlock = UINT_MAX; + ZSTD_optLdm_skipRawSeqStoreBytes(&optLdm->seqStore, blockBytesRemaining); + return; + } + + /* Matches may be < MINMATCH by this process. In that case, we will reject them + when we are deciding whether or not to add the ldm */ + optLdm->startPosInBlock = currPosInBlock + literalsBytesRemaining; + optLdm->endPosInBlock = optLdm->startPosInBlock + matchBytesRemaining; + optLdm->offset = currSeq.offset; + + if (optLdm->endPosInBlock > currBlockEndPos) { + /* Match ends after the block ends, we can't use the whole match */ + optLdm->endPosInBlock = currBlockEndPos; + ZSTD_optLdm_skipRawSeqStoreBytes(&optLdm->seqStore, currBlockEndPos - currPosInBlock); + } else { + /* Consume nb of bytes equal to size of sequence left */ + ZSTD_optLdm_skipRawSeqStoreBytes(&optLdm->seqStore, literalsBytesRemaining + matchBytesRemaining); + } +} + +/* ZSTD_optLdm_maybeAddMatch(): + * Adds a match if it's long enough, + * based on it's 'matchStartPosInBlock' and 'matchEndPosInBlock', + * into 'matches'. Maintains the correct ordering of 'matches'. + */ +static void ZSTD_optLdm_maybeAddMatch(ZSTD_match_t* matches, U32* nbMatches, + const ZSTD_optLdm_t* optLdm, U32 currPosInBlock) +{ + U32 const posDiff = currPosInBlock - optLdm->startPosInBlock; + /* Note: ZSTD_match_t actually contains offBase and matchLength (before subtracting MINMATCH) */ + U32 const candidateMatchLength = optLdm->endPosInBlock - optLdm->startPosInBlock - posDiff; + + /* Ensure that current block position is not outside of the match */ + if (currPosInBlock < optLdm->startPosInBlock + || currPosInBlock >= optLdm->endPosInBlock + || candidateMatchLength < MINMATCH) { + return; + } + + if (*nbMatches == 0 || ((candidateMatchLength > matches[*nbMatches-1].len) && *nbMatches < ZSTD_OPT_NUM)) { + U32 const candidateOffBase = OFFSET_TO_OFFBASE(optLdm->offset); + DEBUGLOG(6, "ZSTD_optLdm_maybeAddMatch(): Adding ldm candidate match (offBase: %u matchLength %u) at block position=%u", + candidateOffBase, candidateMatchLength, currPosInBlock); + matches[*nbMatches].len = candidateMatchLength; + matches[*nbMatches].off = candidateOffBase; + (*nbMatches)++; + } +} + +/* ZSTD_optLdm_processMatchCandidate(): + * Wrapper function to update ldm seq store and call ldm functions as necessary. + */ +static void +ZSTD_optLdm_processMatchCandidate(ZSTD_optLdm_t* optLdm, + ZSTD_match_t* matches, U32* nbMatches, + U32 currPosInBlock, U32 remainingBytes) +{ + if (optLdm->seqStore.size == 0 || optLdm->seqStore.pos >= optLdm->seqStore.size) { + return; + } + + if (currPosInBlock >= optLdm->endPosInBlock) { + if (currPosInBlock > optLdm->endPosInBlock) { + /* The position at which ZSTD_optLdm_processMatchCandidate() is called is not necessarily + * at the end of a match from the ldm seq store, and will often be some bytes + * over beyond matchEndPosInBlock. As such, we need to correct for these "overshoots" + */ + U32 const posOvershoot = currPosInBlock - optLdm->endPosInBlock; + ZSTD_optLdm_skipRawSeqStoreBytes(&optLdm->seqStore, posOvershoot); + } + ZSTD_opt_getNextMatchAndUpdateSeqStore(optLdm, currPosInBlock, remainingBytes); + } + ZSTD_optLdm_maybeAddMatch(matches, nbMatches, optLdm, currPosInBlock); +} + + +/*-******************************* +* Optimal parser +*********************************/ + +#if 0 /* debug */ + +static void +listStats(const U32* table, int lastEltID) +{ + int const nbElts = lastEltID + 1; + int enb; + for (enb=0; enb < nbElts; enb++) { + (void)table; + /* RAWLOG(2, "%3i:%3i, ", enb, table[enb]); */ + RAWLOG(2, "%4i,", table[enb]); + } + RAWLOG(2, " \n"); +} + +#endif + +#define LIT_PRICE(_p) (int)ZSTD_rawLiteralsCost(_p, 1, optStatePtr, optLevel) +#define LL_PRICE(_l) (int)ZSTD_litLengthPrice(_l, optStatePtr, optLevel) +#define LL_INCPRICE(_l) (LL_PRICE(_l) - LL_PRICE(_l-1)) + +FORCE_INLINE_TEMPLATE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t +ZSTD_compressBlock_opt_generic(ZSTD_matchState_t* ms, + seqStore_t* seqStore, + U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize, + const int optLevel, + const ZSTD_dictMode_e dictMode) +{ + optState_t* const optStatePtr = &ms->opt; + const BYTE* const istart = (const BYTE*)src; + const BYTE* ip = istart; + const BYTE* anchor = istart; + const BYTE* const iend = istart + srcSize; + const BYTE* const ilimit = iend - 8; + const BYTE* const base = ms->window.base; + const BYTE* const prefixStart = base + ms->window.dictLimit; + const ZSTD_compressionParameters* const cParams = &ms->cParams; + + ZSTD_getAllMatchesFn getAllMatches = ZSTD_selectBtGetAllMatches(ms, dictMode); + + U32 const sufficient_len = MIN(cParams->targetLength, ZSTD_OPT_NUM -1); + U32 const minMatch = (cParams->minMatch == 3) ? 3 : 4; + U32 nextToUpdate3 = ms->nextToUpdate; + + ZSTD_optimal_t* const opt = optStatePtr->priceTable; + ZSTD_match_t* const matches = optStatePtr->matchTable; + ZSTD_optimal_t lastStretch; + ZSTD_optLdm_t optLdm; + + ZSTD_memset(&lastStretch, 0, sizeof(ZSTD_optimal_t)); + + optLdm.seqStore = ms->ldmSeqStore ? *ms->ldmSeqStore : kNullRawSeqStore; + optLdm.endPosInBlock = optLdm.startPosInBlock = optLdm.offset = 0; + ZSTD_opt_getNextMatchAndUpdateSeqStore(&optLdm, (U32)(ip-istart), (U32)(iend-ip)); + + /* init */ + DEBUGLOG(5, "ZSTD_compressBlock_opt_generic: current=%u, prefix=%u, nextToUpdate=%u", + (U32)(ip - base), ms->window.dictLimit, ms->nextToUpdate); + assert(optLevel <= 2); + ZSTD_rescaleFreqs(optStatePtr, (const BYTE*)src, srcSize, optLevel); + ip += (ip==prefixStart); + + /* Match Loop */ + while (ip < ilimit) { + U32 cur, last_pos = 0; + + /* find first match */ + { U32 const litlen = (U32)(ip - anchor); + U32 const ll0 = !litlen; + U32 nbMatches = getAllMatches(matches, ms, &nextToUpdate3, ip, iend, rep, ll0, minMatch); + ZSTD_optLdm_processMatchCandidate(&optLdm, matches, &nbMatches, + (U32)(ip-istart), (U32)(iend-ip)); + if (!nbMatches) { + DEBUGLOG(8, "no match found at cPos %u", (unsigned)(ip-istart)); + ip++; + continue; + } + + /* Match found: let's store this solution, and eventually find more candidates. + * During this forward pass, @opt is used to store stretches, + * defined as "a match followed by N literals". + * Note how this is different from a Sequence, which is "N literals followed by a match". + * Storing stretches allows us to store different match predecessors + * for each literal position part of a literals run. */ + + /* initialize opt[0] */ + opt[0].mlen = 0; /* there are only literals so far */ + opt[0].litlen = litlen; + /* No need to include the actual price of the literals before the first match + * because it is static for the duration of the forward pass, and is included + * in every subsequent price. But, we include the literal length because + * the cost variation of litlen depends on the value of litlen. + */ + opt[0].price = LL_PRICE(litlen); + ZSTD_STATIC_ASSERT(sizeof(opt[0].rep[0]) == sizeof(rep[0])); + ZSTD_memcpy(&opt[0].rep, rep, sizeof(opt[0].rep)); + + /* large match -> immediate encoding */ + { U32 const maxML = matches[nbMatches-1].len; + U32 const maxOffBase = matches[nbMatches-1].off; + DEBUGLOG(6, "found %u matches of maxLength=%u and maxOffBase=%u at cPos=%u => start new series", + nbMatches, maxML, maxOffBase, (U32)(ip-prefixStart)); + + if (maxML > sufficient_len) { + lastStretch.litlen = 0; + lastStretch.mlen = maxML; + lastStretch.off = maxOffBase; + DEBUGLOG(6, "large match (%u>%u) => immediate encoding", + maxML, sufficient_len); + cur = 0; + last_pos = maxML; + goto _shortestPath; + } } + + /* set prices for first matches starting position == 0 */ + assert(opt[0].price >= 0); + { U32 pos; + U32 matchNb; + for (pos = 1; pos < minMatch; pos++) { + opt[pos].price = ZSTD_MAX_PRICE; + opt[pos].mlen = 0; + opt[pos].litlen = litlen + pos; + } + for (matchNb = 0; matchNb < nbMatches; matchNb++) { + U32 const offBase = matches[matchNb].off; + U32 const end = matches[matchNb].len; + for ( ; pos <= end ; pos++ ) { + int const matchPrice = (int)ZSTD_getMatchPrice(offBase, pos, optStatePtr, optLevel); + int const sequencePrice = opt[0].price + matchPrice; + DEBUGLOG(7, "rPos:%u => set initial price : %.2f", + pos, ZSTD_fCost(sequencePrice)); + opt[pos].mlen = pos; + opt[pos].off = offBase; + opt[pos].litlen = 0; /* end of match */ + opt[pos].price = sequencePrice + LL_PRICE(0); + } + } + last_pos = pos-1; + opt[pos].price = ZSTD_MAX_PRICE; + } + } + + /* check further positions */ + for (cur = 1; cur <= last_pos; cur++) { + const BYTE* const inr = ip + cur; + assert(cur <= ZSTD_OPT_NUM); + DEBUGLOG(7, "cPos:%zi==rPos:%u", inr-istart, cur); + + /* Fix current position with one literal if cheaper */ + { U32 const litlen = opt[cur-1].litlen + 1; + int const price = opt[cur-1].price + + LIT_PRICE(ip+cur-1) + + LL_INCPRICE(litlen); + assert(price < 1000000000); /* overflow check */ + if (price <= opt[cur].price) { + ZSTD_optimal_t const prevMatch = opt[cur]; + DEBUGLOG(7, "cPos:%zi==rPos:%u : better price (%.2f<=%.2f) using literal (ll==%u) (hist:%u,%u,%u)", + inr-istart, cur, ZSTD_fCost(price), ZSTD_fCost(opt[cur].price), litlen, + opt[cur-1].rep[0], opt[cur-1].rep[1], opt[cur-1].rep[2]); + opt[cur] = opt[cur-1]; + opt[cur].litlen = litlen; + opt[cur].price = price; + if ( (optLevel >= 1) /* additional check only for higher modes */ + && (prevMatch.litlen == 0) /* replace a match */ + && (LL_INCPRICE(1) < 0) /* ll1 is cheaper than ll0 */ + && LIKELY(ip + cur < iend) + ) { + /* check next position, in case it would be cheaper */ + int with1literal = prevMatch.price + LIT_PRICE(ip+cur) + LL_INCPRICE(1); + int withMoreLiterals = price + LIT_PRICE(ip+cur) + LL_INCPRICE(litlen+1); + DEBUGLOG(7, "then at next rPos %u : match+1lit %.2f vs %ulits %.2f", + cur+1, ZSTD_fCost(with1literal), litlen+1, ZSTD_fCost(withMoreLiterals)); + if ( (with1literal < withMoreLiterals) + && (with1literal < opt[cur+1].price) ) { + /* update offset history - before it disappears */ + U32 const prev = cur - prevMatch.mlen; + repcodes_t const newReps = ZSTD_newRep(opt[prev].rep, prevMatch.off, opt[prev].litlen==0); + assert(cur >= prevMatch.mlen); + DEBUGLOG(7, "==> match+1lit is cheaper (%.2f < %.2f) (hist:%u,%u,%u) !", + ZSTD_fCost(with1literal), ZSTD_fCost(withMoreLiterals), + newReps.rep[0], newReps.rep[1], newReps.rep[2] ); + opt[cur+1] = prevMatch; /* mlen & offbase */ + ZSTD_memcpy(opt[cur+1].rep, &newReps, sizeof(repcodes_t)); + opt[cur+1].litlen = 1; + opt[cur+1].price = with1literal; + if (last_pos < cur+1) last_pos = cur+1; + } + } + } else { + DEBUGLOG(7, "cPos:%zi==rPos:%u : literal would cost more (%.2f>%.2f)", + inr-istart, cur, ZSTD_fCost(price), ZSTD_fCost(opt[cur].price)); + } + } + + /* Offset history is not updated during match comparison. + * Do it here, now that the match is selected and confirmed. + */ + ZSTD_STATIC_ASSERT(sizeof(opt[cur].rep) == sizeof(repcodes_t)); + assert(cur >= opt[cur].mlen); + if (opt[cur].litlen == 0) { + /* just finished a match => alter offset history */ + U32 const prev = cur - opt[cur].mlen; + repcodes_t const newReps = ZSTD_newRep(opt[prev].rep, opt[cur].off, opt[prev].litlen==0); + ZSTD_memcpy(opt[cur].rep, &newReps, sizeof(repcodes_t)); + } + + /* last match must start at a minimum distance of 8 from oend */ + if (inr > ilimit) continue; + + if (cur == last_pos) break; + + if ( (optLevel==0) /*static_test*/ + && (opt[cur+1].price <= opt[cur].price + (BITCOST_MULTIPLIER/2)) ) { + DEBUGLOG(7, "skip current position : next rPos(%u) price is cheaper", cur+1); + continue; /* skip unpromising positions; about ~+6% speed, -0.01 ratio */ + } + + assert(opt[cur].price >= 0); + { U32 const ll0 = (opt[cur].litlen == 0); + int const previousPrice = opt[cur].price; + int const basePrice = previousPrice + LL_PRICE(0); + U32 nbMatches = getAllMatches(matches, ms, &nextToUpdate3, inr, iend, opt[cur].rep, ll0, minMatch); + U32 matchNb; + + ZSTD_optLdm_processMatchCandidate(&optLdm, matches, &nbMatches, + (U32)(inr-istart), (U32)(iend-inr)); + + if (!nbMatches) { + DEBUGLOG(7, "rPos:%u : no match found", cur); + continue; + } + + { U32 const longestML = matches[nbMatches-1].len; + DEBUGLOG(7, "cPos:%zi==rPos:%u, found %u matches, of longest ML=%u", + inr-istart, cur, nbMatches, longestML); + + if ( (longestML > sufficient_len) + || (cur + longestML >= ZSTD_OPT_NUM) + || (ip + cur + longestML >= iend) ) { + lastStretch.mlen = longestML; + lastStretch.off = matches[nbMatches-1].off; + lastStretch.litlen = 0; + last_pos = cur + longestML; + goto _shortestPath; + } } + + /* set prices using matches found at position == cur */ + for (matchNb = 0; matchNb < nbMatches; matchNb++) { + U32 const offset = matches[matchNb].off; + U32 const lastML = matches[matchNb].len; + U32 const startML = (matchNb>0) ? matches[matchNb-1].len+1 : minMatch; + U32 mlen; + + DEBUGLOG(7, "testing match %u => offBase=%4u, mlen=%2u, llen=%2u", + matchNb, matches[matchNb].off, lastML, opt[cur].litlen); + + for (mlen = lastML; mlen >= startML; mlen--) { /* scan downward */ + U32 const pos = cur + mlen; + int const price = basePrice + (int)ZSTD_getMatchPrice(offset, mlen, optStatePtr, optLevel); + + if ((pos > last_pos) || (price < opt[pos].price)) { + DEBUGLOG(7, "rPos:%u (ml=%2u) => new better price (%.2f<%.2f)", + pos, mlen, ZSTD_fCost(price), ZSTD_fCost(opt[pos].price)); + while (last_pos < pos) { + /* fill empty positions, for future comparisons */ + last_pos++; + opt[last_pos].price = ZSTD_MAX_PRICE; + opt[last_pos].litlen = !0; /* just needs to be != 0, to mean "not an end of match" */ + } + opt[pos].mlen = mlen; + opt[pos].off = offset; + opt[pos].litlen = 0; + opt[pos].price = price; + } else { + DEBUGLOG(7, "rPos:%u (ml=%2u) => new price is worse (%.2f>=%.2f)", + pos, mlen, ZSTD_fCost(price), ZSTD_fCost(opt[pos].price)); + if (optLevel==0) break; /* early update abort; gets ~+10% speed for about -0.01 ratio loss */ + } + } } } + opt[last_pos+1].price = ZSTD_MAX_PRICE; + } /* for (cur = 1; cur <= last_pos; cur++) */ + + lastStretch = opt[last_pos]; + assert(cur >= lastStretch.mlen); + cur = last_pos - lastStretch.mlen; + +_shortestPath: /* cur, last_pos, best_mlen, best_off have to be set */ + assert(opt[0].mlen == 0); + assert(last_pos >= lastStretch.mlen); + assert(cur == last_pos - lastStretch.mlen); + + if (lastStretch.mlen==0) { + /* no solution : all matches have been converted into literals */ + assert(lastStretch.litlen == (ip - anchor) + last_pos); + ip += last_pos; + continue; + } + assert(lastStretch.off > 0); + + /* Update offset history */ + if (lastStretch.litlen == 0) { + /* finishing on a match : update offset history */ + repcodes_t const reps = ZSTD_newRep(opt[cur].rep, lastStretch.off, opt[cur].litlen==0); + ZSTD_memcpy(rep, &reps, sizeof(repcodes_t)); + } else { + ZSTD_memcpy(rep, lastStretch.rep, sizeof(repcodes_t)); + assert(cur >= lastStretch.litlen); + cur -= lastStretch.litlen; + } + + /* Let's write the shortest path solution. + * It is stored in @opt in reverse order, + * starting from @storeEnd (==cur+2), + * effectively partially @opt overwriting. + * Content is changed too: + * - So far, @opt stored stretches, aka a match followed by literals + * - Now, it will store sequences, aka literals followed by a match + */ + { U32 const storeEnd = cur + 2; + U32 storeStart = storeEnd; + U32 stretchPos = cur; + + DEBUGLOG(6, "start reverse traversal (last_pos:%u, cur:%u)", + last_pos, cur); (void)last_pos; + assert(storeEnd < ZSTD_OPT_SIZE); + DEBUGLOG(6, "last stretch copied into pos=%u (llen=%u,mlen=%u,ofc=%u)", + storeEnd, lastStretch.litlen, lastStretch.mlen, lastStretch.off); + if (lastStretch.litlen > 0) { + /* last "sequence" is unfinished: just a bunch of literals */ + opt[storeEnd].litlen = lastStretch.litlen; + opt[storeEnd].mlen = 0; + storeStart = storeEnd-1; + opt[storeStart] = lastStretch; + } { + opt[storeEnd] = lastStretch; /* note: litlen will be fixed */ + storeStart = storeEnd; + } + while (1) { + ZSTD_optimal_t nextStretch = opt[stretchPos]; + opt[storeStart].litlen = nextStretch.litlen; + DEBUGLOG(6, "selected sequence (llen=%u,mlen=%u,ofc=%u)", + opt[storeStart].litlen, opt[storeStart].mlen, opt[storeStart].off); + if (nextStretch.mlen == 0) { + /* reaching beginning of segment */ + break; + } + storeStart--; + opt[storeStart] = nextStretch; /* note: litlen will be fixed */ + assert(nextStretch.litlen + nextStretch.mlen <= stretchPos); + stretchPos -= nextStretch.litlen + nextStretch.mlen; + } + + /* save sequences */ + DEBUGLOG(6, "sending selected sequences into seqStore"); + { U32 storePos; + for (storePos=storeStart; storePos <= storeEnd; storePos++) { + U32 const llen = opt[storePos].litlen; + U32 const mlen = opt[storePos].mlen; + U32 const offBase = opt[storePos].off; + U32 const advance = llen + mlen; + DEBUGLOG(6, "considering seq starting at %zi, llen=%u, mlen=%u", + anchor - istart, (unsigned)llen, (unsigned)mlen); + + if (mlen==0) { /* only literals => must be last "sequence", actually starting a new stream of sequences */ + assert(storePos == storeEnd); /* must be last sequence */ + ip = anchor + llen; /* last "sequence" is a bunch of literals => don't progress anchor */ + continue; /* will finish */ + } + + assert(anchor + llen <= iend); + ZSTD_updateStats(optStatePtr, llen, anchor, offBase, mlen); + ZSTD_storeSeq(seqStore, llen, anchor, iend, offBase, mlen); + anchor += advance; + ip = anchor; + } } + DEBUGLOG(7, "new offset history : %u, %u, %u", rep[0], rep[1], rep[2]); + + /* update all costs */ + ZSTD_setBasePrices(optStatePtr, optLevel); + } + } /* while (ip < ilimit) */ + + /* Return the last literals size */ + return (size_t)(iend - anchor); +} +#endif /* build exclusions */ + +#ifndef ZSTD_EXCLUDE_BTOPT_BLOCK_COMPRESSOR +static size_t ZSTD_compressBlock_opt0( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize, const ZSTD_dictMode_e dictMode) +{ + return ZSTD_compressBlock_opt_generic(ms, seqStore, rep, src, srcSize, 0 /* optLevel */, dictMode); +} +#endif + +#ifndef ZSTD_EXCLUDE_BTULTRA_BLOCK_COMPRESSOR +static size_t ZSTD_compressBlock_opt2( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize, const ZSTD_dictMode_e dictMode) +{ + return ZSTD_compressBlock_opt_generic(ms, seqStore, rep, src, srcSize, 2 /* optLevel */, dictMode); +} +#endif + +#ifndef ZSTD_EXCLUDE_BTOPT_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_btopt( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize) +{ + DEBUGLOG(5, "ZSTD_compressBlock_btopt"); + return ZSTD_compressBlock_opt0(ms, seqStore, rep, src, srcSize, ZSTD_noDict); +} +#endif + + + + +#ifndef ZSTD_EXCLUDE_BTULTRA_BLOCK_COMPRESSOR +/* ZSTD_initStats_ultra(): + * make a first compression pass, just to seed stats with more accurate starting values. + * only works on first block, with no dictionary and no ldm. + * this function cannot error out, its narrow contract must be respected. + */ +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_initStats_ultra(ZSTD_matchState_t* ms, + seqStore_t* seqStore, + U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize) +{ + U32 tmpRep[ZSTD_REP_NUM]; /* updated rep codes will sink here */ + ZSTD_memcpy(tmpRep, rep, sizeof(tmpRep)); + + DEBUGLOG(4, "ZSTD_initStats_ultra (srcSize=%zu)", srcSize); + assert(ms->opt.litLengthSum == 0); /* first block */ + assert(seqStore->sequences == seqStore->sequencesStart); /* no ldm */ + assert(ms->window.dictLimit == ms->window.lowLimit); /* no dictionary */ + assert(ms->window.dictLimit - ms->nextToUpdate <= 1); /* no prefix (note: intentional overflow, defined as 2-complement) */ + + ZSTD_compressBlock_opt2(ms, seqStore, tmpRep, src, srcSize, ZSTD_noDict); /* generate stats into ms->opt*/ + + /* invalidate first scan from history, only keep entropy stats */ + ZSTD_resetSeqStore(seqStore); + ms->window.base -= srcSize; + ms->window.dictLimit += (U32)srcSize; + ms->window.lowLimit = ms->window.dictLimit; + ms->nextToUpdate = ms->window.dictLimit; + +} + +size_t ZSTD_compressBlock_btultra( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize) +{ + DEBUGLOG(5, "ZSTD_compressBlock_btultra (srcSize=%zu)", srcSize); + return ZSTD_compressBlock_opt2(ms, seqStore, rep, src, srcSize, ZSTD_noDict); +} + +size_t ZSTD_compressBlock_btultra2( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize) +{ + U32 const curr = (U32)((const BYTE*)src - ms->window.base); + DEBUGLOG(5, "ZSTD_compressBlock_btultra2 (srcSize=%zu)", srcSize); + + /* 2-passes strategy: + * this strategy makes a first pass over first block to collect statistics + * in order to seed next round's statistics with it. + * After 1st pass, function forgets history, and starts a new block. + * Consequently, this can only work if no data has been previously loaded in tables, + * aka, no dictionary, no prefix, no ldm preprocessing. + * The compression ratio gain is generally small (~0.5% on first block), + * the cost is 2x cpu time on first block. */ + assert(srcSize <= ZSTD_BLOCKSIZE_MAX); + if ( (ms->opt.litLengthSum==0) /* first block */ + && (seqStore->sequences == seqStore->sequencesStart) /* no ldm */ + && (ms->window.dictLimit == ms->window.lowLimit) /* no dictionary */ + && (curr == ms->window.dictLimit) /* start of frame, nothing already loaded nor skipped */ + && (srcSize > ZSTD_PREDEF_THRESHOLD) /* input large enough to not employ default stats */ + ) { + ZSTD_initStats_ultra(ms, seqStore, rep, src, srcSize); + } + + return ZSTD_compressBlock_opt2(ms, seqStore, rep, src, srcSize, ZSTD_noDict); +} +#endif + +#ifndef ZSTD_EXCLUDE_BTOPT_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_btopt_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize) +{ + return ZSTD_compressBlock_opt0(ms, seqStore, rep, src, srcSize, ZSTD_dictMatchState); +} + +size_t ZSTD_compressBlock_btopt_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize) +{ + return ZSTD_compressBlock_opt0(ms, seqStore, rep, src, srcSize, ZSTD_extDict); +} +#endif + +#ifndef ZSTD_EXCLUDE_BTULTRA_BLOCK_COMPRESSOR +size_t ZSTD_compressBlock_btultra_dictMatchState( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize) +{ + return ZSTD_compressBlock_opt2(ms, seqStore, rep, src, srcSize, ZSTD_dictMatchState); +} + +size_t ZSTD_compressBlock_btultra_extDict( + ZSTD_matchState_t* ms, seqStore_t* seqStore, U32 rep[ZSTD_REP_NUM], + const void* src, size_t srcSize) +{ + return ZSTD_compressBlock_opt2(ms, seqStore, rep, src, srcSize, ZSTD_extDict); +} +#endif + +/* note : no btultra2 variant for extDict nor dictMatchState, + * because btultra2 is not meant to work with dictionaries + * and is only specific for the first block (no prefix) */ +/**** ended inlining compress/zstd_opt.c ****/ +#ifdef ZSTD_MULTITHREAD +/**** start inlining compress/zstdmt_compress.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + +/* ====== Compiler specifics ====== */ +#if defined(_MSC_VER) +# pragma warning(disable : 4204) /* disable: C4204: non-constant aggregate initializer */ +#endif + + +/* ====== Dependencies ====== */ +/**** skipping file: ../common/allocations.h ****/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../common/mem.h ****/ +/**** skipping file: ../common/pool.h ****/ +/**** skipping file: ../common/threading.h ****/ +/**** skipping file: zstd_compress_internal.h ****/ +/**** skipping file: zstd_ldm.h ****/ +/**** skipping file: zstdmt_compress.h ****/ + +/* Guards code to support resizing the SeqPool. + * We will want to resize the SeqPool to save memory in the future. + * Until then, comment the code out since it is unused. + */ +#define ZSTD_RESIZE_SEQPOOL 0 + +/* ====== Debug ====== */ +#if defined(DEBUGLEVEL) && (DEBUGLEVEL>=2) \ + && !defined(_MSC_VER) \ + && !defined(__MINGW32__) + +# include +# include +# include + +# define DEBUG_PRINTHEX(l,p,n) \ + do { \ + unsigned debug_u; \ + for (debug_u=0; debug_u<(n); debug_u++) \ + RAWLOG(l, "%02X ", ((const unsigned char*)(p))[debug_u]); \ + RAWLOG(l, " \n"); \ + } while (0) + +static unsigned long long GetCurrentClockTimeMicroseconds(void) +{ + static clock_t _ticksPerSecond = 0; + if (_ticksPerSecond <= 0) _ticksPerSecond = sysconf(_SC_CLK_TCK); + + { struct tms junk; clock_t newTicks = (clock_t) times(&junk); + return ((((unsigned long long)newTicks)*(1000000))/_ticksPerSecond); +} } + +#define MUTEX_WAIT_TIME_DLEVEL 6 +#define ZSTD_PTHREAD_MUTEX_LOCK(mutex) \ + do { \ + if (DEBUGLEVEL >= MUTEX_WAIT_TIME_DLEVEL) { \ + unsigned long long const beforeTime = GetCurrentClockTimeMicroseconds(); \ + ZSTD_pthread_mutex_lock(mutex); \ + { unsigned long long const afterTime = GetCurrentClockTimeMicroseconds(); \ + unsigned long long const elapsedTime = (afterTime-beforeTime); \ + if (elapsedTime > 1000) { \ + /* or whatever threshold you like; I'm using 1 millisecond here */ \ + DEBUGLOG(MUTEX_WAIT_TIME_DLEVEL, \ + "Thread took %llu microseconds to acquire mutex %s \n", \ + elapsedTime, #mutex); \ + } } \ + } else { \ + ZSTD_pthread_mutex_lock(mutex); \ + } \ + } while (0) + +#else + +# define ZSTD_PTHREAD_MUTEX_LOCK(m) ZSTD_pthread_mutex_lock(m) +# define DEBUG_PRINTHEX(l,p,n) do { } while (0) + +#endif + + +/* ===== Buffer Pool ===== */ +/* a single Buffer Pool can be invoked from multiple threads in parallel */ + +typedef struct buffer_s { + void* start; + size_t capacity; +} buffer_t; + +static const buffer_t g_nullBuffer = { NULL, 0 }; + +typedef struct ZSTDMT_bufferPool_s { + ZSTD_pthread_mutex_t poolMutex; + size_t bufferSize; + unsigned totalBuffers; + unsigned nbBuffers; + ZSTD_customMem cMem; + buffer_t* buffers; +} ZSTDMT_bufferPool; + +static void ZSTDMT_freeBufferPool(ZSTDMT_bufferPool* bufPool) +{ + DEBUGLOG(3, "ZSTDMT_freeBufferPool (address:%08X)", (U32)(size_t)bufPool); + if (!bufPool) return; /* compatibility with free on NULL */ + if (bufPool->buffers) { + unsigned u; + for (u=0; utotalBuffers; u++) { + DEBUGLOG(4, "free buffer %2u (address:%08X)", u, (U32)(size_t)bufPool->buffers[u].start); + ZSTD_customFree(bufPool->buffers[u].start, bufPool->cMem); + } + ZSTD_customFree(bufPool->buffers, bufPool->cMem); + } + ZSTD_pthread_mutex_destroy(&bufPool->poolMutex); + ZSTD_customFree(bufPool, bufPool->cMem); +} + +static ZSTDMT_bufferPool* ZSTDMT_createBufferPool(unsigned maxNbBuffers, ZSTD_customMem cMem) +{ + ZSTDMT_bufferPool* const bufPool = + (ZSTDMT_bufferPool*)ZSTD_customCalloc(sizeof(ZSTDMT_bufferPool), cMem); + if (bufPool==NULL) return NULL; + if (ZSTD_pthread_mutex_init(&bufPool->poolMutex, NULL)) { + ZSTD_customFree(bufPool, cMem); + return NULL; + } + bufPool->buffers = (buffer_t*)ZSTD_customCalloc(maxNbBuffers * sizeof(buffer_t), cMem); + if (bufPool->buffers==NULL) { + ZSTDMT_freeBufferPool(bufPool); + return NULL; + } + bufPool->bufferSize = 64 KB; + bufPool->totalBuffers = maxNbBuffers; + bufPool->nbBuffers = 0; + bufPool->cMem = cMem; + return bufPool; +} + +/* only works at initialization, not during compression */ +static size_t ZSTDMT_sizeof_bufferPool(ZSTDMT_bufferPool* bufPool) +{ + size_t const poolSize = sizeof(*bufPool); + size_t const arraySize = bufPool->totalBuffers * sizeof(buffer_t); + unsigned u; + size_t totalBufferSize = 0; + ZSTD_pthread_mutex_lock(&bufPool->poolMutex); + for (u=0; utotalBuffers; u++) + totalBufferSize += bufPool->buffers[u].capacity; + ZSTD_pthread_mutex_unlock(&bufPool->poolMutex); + + return poolSize + arraySize + totalBufferSize; +} + +/* ZSTDMT_setBufferSize() : + * all future buffers provided by this buffer pool will have _at least_ this size + * note : it's better for all buffers to have same size, + * as they become freely interchangeable, reducing malloc/free usages and memory fragmentation */ +static void ZSTDMT_setBufferSize(ZSTDMT_bufferPool* const bufPool, size_t const bSize) +{ + ZSTD_pthread_mutex_lock(&bufPool->poolMutex); + DEBUGLOG(4, "ZSTDMT_setBufferSize: bSize = %u", (U32)bSize); + bufPool->bufferSize = bSize; + ZSTD_pthread_mutex_unlock(&bufPool->poolMutex); +} + + +static ZSTDMT_bufferPool* ZSTDMT_expandBufferPool(ZSTDMT_bufferPool* srcBufPool, unsigned maxNbBuffers) +{ + if (srcBufPool==NULL) return NULL; + if (srcBufPool->totalBuffers >= maxNbBuffers) /* good enough */ + return srcBufPool; + /* need a larger buffer pool */ + { ZSTD_customMem const cMem = srcBufPool->cMem; + size_t const bSize = srcBufPool->bufferSize; /* forward parameters */ + ZSTDMT_bufferPool* newBufPool; + ZSTDMT_freeBufferPool(srcBufPool); + newBufPool = ZSTDMT_createBufferPool(maxNbBuffers, cMem); + if (newBufPool==NULL) return newBufPool; + ZSTDMT_setBufferSize(newBufPool, bSize); + return newBufPool; + } +} + +/** ZSTDMT_getBuffer() : + * assumption : bufPool must be valid + * @return : a buffer, with start pointer and size + * note: allocation may fail, in this case, start==NULL and size==0 */ +static buffer_t ZSTDMT_getBuffer(ZSTDMT_bufferPool* bufPool) +{ + size_t const bSize = bufPool->bufferSize; + DEBUGLOG(5, "ZSTDMT_getBuffer: bSize = %u", (U32)bufPool->bufferSize); + ZSTD_pthread_mutex_lock(&bufPool->poolMutex); + if (bufPool->nbBuffers) { /* try to use an existing buffer */ + buffer_t const buf = bufPool->buffers[--(bufPool->nbBuffers)]; + size_t const availBufferSize = buf.capacity; + bufPool->buffers[bufPool->nbBuffers] = g_nullBuffer; + if ((availBufferSize >= bSize) & ((availBufferSize>>3) <= bSize)) { + /* large enough, but not too much */ + DEBUGLOG(5, "ZSTDMT_getBuffer: provide buffer %u of size %u", + bufPool->nbBuffers, (U32)buf.capacity); + ZSTD_pthread_mutex_unlock(&bufPool->poolMutex); + return buf; + } + /* size conditions not respected : scratch this buffer, create new one */ + DEBUGLOG(5, "ZSTDMT_getBuffer: existing buffer does not meet size conditions => freeing"); + ZSTD_customFree(buf.start, bufPool->cMem); + } + ZSTD_pthread_mutex_unlock(&bufPool->poolMutex); + /* create new buffer */ + DEBUGLOG(5, "ZSTDMT_getBuffer: create a new buffer"); + { buffer_t buffer; + void* const start = ZSTD_customMalloc(bSize, bufPool->cMem); + buffer.start = start; /* note : start can be NULL if malloc fails ! */ + buffer.capacity = (start==NULL) ? 0 : bSize; + if (start==NULL) { + DEBUGLOG(5, "ZSTDMT_getBuffer: buffer allocation failure !!"); + } else { + DEBUGLOG(5, "ZSTDMT_getBuffer: created buffer of size %u", (U32)bSize); + } + return buffer; + } +} + +#if ZSTD_RESIZE_SEQPOOL +/** ZSTDMT_resizeBuffer() : + * assumption : bufPool must be valid + * @return : a buffer that is at least the buffer pool buffer size. + * If a reallocation happens, the data in the input buffer is copied. + */ +static buffer_t ZSTDMT_resizeBuffer(ZSTDMT_bufferPool* bufPool, buffer_t buffer) +{ + size_t const bSize = bufPool->bufferSize; + if (buffer.capacity < bSize) { + void* const start = ZSTD_customMalloc(bSize, bufPool->cMem); + buffer_t newBuffer; + newBuffer.start = start; + newBuffer.capacity = start == NULL ? 0 : bSize; + if (start != NULL) { + assert(newBuffer.capacity >= buffer.capacity); + ZSTD_memcpy(newBuffer.start, buffer.start, buffer.capacity); + DEBUGLOG(5, "ZSTDMT_resizeBuffer: created buffer of size %u", (U32)bSize); + return newBuffer; + } + DEBUGLOG(5, "ZSTDMT_resizeBuffer: buffer allocation failure !!"); + } + return buffer; +} +#endif + +/* store buffer for later re-use, up to pool capacity */ +static void ZSTDMT_releaseBuffer(ZSTDMT_bufferPool* bufPool, buffer_t buf) +{ + DEBUGLOG(5, "ZSTDMT_releaseBuffer"); + if (buf.start == NULL) return; /* compatible with release on NULL */ + ZSTD_pthread_mutex_lock(&bufPool->poolMutex); + if (bufPool->nbBuffers < bufPool->totalBuffers) { + bufPool->buffers[bufPool->nbBuffers++] = buf; /* stored for later use */ + DEBUGLOG(5, "ZSTDMT_releaseBuffer: stored buffer of size %u in slot %u", + (U32)buf.capacity, (U32)(bufPool->nbBuffers-1)); + ZSTD_pthread_mutex_unlock(&bufPool->poolMutex); + return; + } + ZSTD_pthread_mutex_unlock(&bufPool->poolMutex); + /* Reached bufferPool capacity (note: should not happen) */ + DEBUGLOG(5, "ZSTDMT_releaseBuffer: pool capacity reached => freeing "); + ZSTD_customFree(buf.start, bufPool->cMem); +} + +/* We need 2 output buffers per worker since each dstBuff must be flushed after it is released. + * The 3 additional buffers are as follows: + * 1 buffer for input loading + * 1 buffer for "next input" when submitting current one + * 1 buffer stuck in queue */ +#define BUF_POOL_MAX_NB_BUFFERS(nbWorkers) (2*(nbWorkers) + 3) + +/* After a worker releases its rawSeqStore, it is immediately ready for reuse. + * So we only need one seq buffer per worker. */ +#define SEQ_POOL_MAX_NB_BUFFERS(nbWorkers) (nbWorkers) + +/* ===== Seq Pool Wrapper ====== */ + +typedef ZSTDMT_bufferPool ZSTDMT_seqPool; + +static size_t ZSTDMT_sizeof_seqPool(ZSTDMT_seqPool* seqPool) +{ + return ZSTDMT_sizeof_bufferPool(seqPool); +} + +static rawSeqStore_t bufferToSeq(buffer_t buffer) +{ + rawSeqStore_t seq = kNullRawSeqStore; + seq.seq = (rawSeq*)buffer.start; + seq.capacity = buffer.capacity / sizeof(rawSeq); + return seq; +} + +static buffer_t seqToBuffer(rawSeqStore_t seq) +{ + buffer_t buffer; + buffer.start = seq.seq; + buffer.capacity = seq.capacity * sizeof(rawSeq); + return buffer; +} + +static rawSeqStore_t ZSTDMT_getSeq(ZSTDMT_seqPool* seqPool) +{ + if (seqPool->bufferSize == 0) { + return kNullRawSeqStore; + } + return bufferToSeq(ZSTDMT_getBuffer(seqPool)); +} + +#if ZSTD_RESIZE_SEQPOOL +static rawSeqStore_t ZSTDMT_resizeSeq(ZSTDMT_seqPool* seqPool, rawSeqStore_t seq) +{ + return bufferToSeq(ZSTDMT_resizeBuffer(seqPool, seqToBuffer(seq))); +} +#endif + +static void ZSTDMT_releaseSeq(ZSTDMT_seqPool* seqPool, rawSeqStore_t seq) +{ + ZSTDMT_releaseBuffer(seqPool, seqToBuffer(seq)); +} + +static void ZSTDMT_setNbSeq(ZSTDMT_seqPool* const seqPool, size_t const nbSeq) +{ + ZSTDMT_setBufferSize(seqPool, nbSeq * sizeof(rawSeq)); +} + +static ZSTDMT_seqPool* ZSTDMT_createSeqPool(unsigned nbWorkers, ZSTD_customMem cMem) +{ + ZSTDMT_seqPool* const seqPool = ZSTDMT_createBufferPool(SEQ_POOL_MAX_NB_BUFFERS(nbWorkers), cMem); + if (seqPool == NULL) return NULL; + ZSTDMT_setNbSeq(seqPool, 0); + return seqPool; +} + +static void ZSTDMT_freeSeqPool(ZSTDMT_seqPool* seqPool) +{ + ZSTDMT_freeBufferPool(seqPool); +} + +static ZSTDMT_seqPool* ZSTDMT_expandSeqPool(ZSTDMT_seqPool* pool, U32 nbWorkers) +{ + return ZSTDMT_expandBufferPool(pool, SEQ_POOL_MAX_NB_BUFFERS(nbWorkers)); +} + + +/* ===== CCtx Pool ===== */ +/* a single CCtx Pool can be invoked from multiple threads in parallel */ + +typedef struct { + ZSTD_pthread_mutex_t poolMutex; + int totalCCtx; + int availCCtx; + ZSTD_customMem cMem; + ZSTD_CCtx** cctxs; +} ZSTDMT_CCtxPool; + +/* note : all CCtx borrowed from the pool must be reverted back to the pool _before_ freeing the pool */ +static void ZSTDMT_freeCCtxPool(ZSTDMT_CCtxPool* pool) +{ + if (!pool) return; + ZSTD_pthread_mutex_destroy(&pool->poolMutex); + if (pool->cctxs) { + int cid; + for (cid=0; cidtotalCCtx; cid++) + ZSTD_freeCCtx(pool->cctxs[cid]); /* free compatible with NULL */ + ZSTD_customFree(pool->cctxs, pool->cMem); + } + ZSTD_customFree(pool, pool->cMem); +} + +/* ZSTDMT_createCCtxPool() : + * implies nbWorkers >= 1 , checked by caller ZSTDMT_createCCtx() */ +static ZSTDMT_CCtxPool* ZSTDMT_createCCtxPool(int nbWorkers, + ZSTD_customMem cMem) +{ + ZSTDMT_CCtxPool* const cctxPool = + (ZSTDMT_CCtxPool*) ZSTD_customCalloc(sizeof(ZSTDMT_CCtxPool), cMem); + assert(nbWorkers > 0); + if (!cctxPool) return NULL; + if (ZSTD_pthread_mutex_init(&cctxPool->poolMutex, NULL)) { + ZSTD_customFree(cctxPool, cMem); + return NULL; + } + cctxPool->totalCCtx = nbWorkers; + cctxPool->cctxs = (ZSTD_CCtx**)ZSTD_customCalloc(nbWorkers * sizeof(ZSTD_CCtx*), cMem); + if (!cctxPool->cctxs) { + ZSTDMT_freeCCtxPool(cctxPool); + return NULL; + } + cctxPool->cMem = cMem; + cctxPool->cctxs[0] = ZSTD_createCCtx_advanced(cMem); + if (!cctxPool->cctxs[0]) { ZSTDMT_freeCCtxPool(cctxPool); return NULL; } + cctxPool->availCCtx = 1; /* at least one cctx for single-thread mode */ + DEBUGLOG(3, "cctxPool created, with %u workers", nbWorkers); + return cctxPool; +} + +static ZSTDMT_CCtxPool* ZSTDMT_expandCCtxPool(ZSTDMT_CCtxPool* srcPool, + int nbWorkers) +{ + if (srcPool==NULL) return NULL; + if (nbWorkers <= srcPool->totalCCtx) return srcPool; /* good enough */ + /* need a larger cctx pool */ + { ZSTD_customMem const cMem = srcPool->cMem; + ZSTDMT_freeCCtxPool(srcPool); + return ZSTDMT_createCCtxPool(nbWorkers, cMem); + } +} + +/* only works during initialization phase, not during compression */ +static size_t ZSTDMT_sizeof_CCtxPool(ZSTDMT_CCtxPool* cctxPool) +{ + ZSTD_pthread_mutex_lock(&cctxPool->poolMutex); + { unsigned const nbWorkers = cctxPool->totalCCtx; + size_t const poolSize = sizeof(*cctxPool); + size_t const arraySize = cctxPool->totalCCtx * sizeof(ZSTD_CCtx*); + size_t totalCCtxSize = 0; + unsigned u; + for (u=0; ucctxs[u]); + } + ZSTD_pthread_mutex_unlock(&cctxPool->poolMutex); + assert(nbWorkers > 0); + return poolSize + arraySize + totalCCtxSize; + } +} + +static ZSTD_CCtx* ZSTDMT_getCCtx(ZSTDMT_CCtxPool* cctxPool) +{ + DEBUGLOG(5, "ZSTDMT_getCCtx"); + ZSTD_pthread_mutex_lock(&cctxPool->poolMutex); + if (cctxPool->availCCtx) { + cctxPool->availCCtx--; + { ZSTD_CCtx* const cctx = cctxPool->cctxs[cctxPool->availCCtx]; + ZSTD_pthread_mutex_unlock(&cctxPool->poolMutex); + return cctx; + } } + ZSTD_pthread_mutex_unlock(&cctxPool->poolMutex); + DEBUGLOG(5, "create one more CCtx"); + return ZSTD_createCCtx_advanced(cctxPool->cMem); /* note : can be NULL, when creation fails ! */ +} + +static void ZSTDMT_releaseCCtx(ZSTDMT_CCtxPool* pool, ZSTD_CCtx* cctx) +{ + if (cctx==NULL) return; /* compatibility with release on NULL */ + ZSTD_pthread_mutex_lock(&pool->poolMutex); + if (pool->availCCtx < pool->totalCCtx) + pool->cctxs[pool->availCCtx++] = cctx; + else { + /* pool overflow : should not happen, since totalCCtx==nbWorkers */ + DEBUGLOG(4, "CCtx pool overflow : free cctx"); + ZSTD_freeCCtx(cctx); + } + ZSTD_pthread_mutex_unlock(&pool->poolMutex); +} + +/* ==== Serial State ==== */ + +typedef struct { + void const* start; + size_t size; +} range_t; + +typedef struct { + /* All variables in the struct are protected by mutex. */ + ZSTD_pthread_mutex_t mutex; + ZSTD_pthread_cond_t cond; + ZSTD_CCtx_params params; + ldmState_t ldmState; + XXH64_state_t xxhState; + unsigned nextJobID; + /* Protects ldmWindow. + * Must be acquired after the main mutex when acquiring both. + */ + ZSTD_pthread_mutex_t ldmWindowMutex; + ZSTD_pthread_cond_t ldmWindowCond; /* Signaled when ldmWindow is updated */ + ZSTD_window_t ldmWindow; /* A thread-safe copy of ldmState.window */ +} serialState_t; + +static int +ZSTDMT_serialState_reset(serialState_t* serialState, + ZSTDMT_seqPool* seqPool, + ZSTD_CCtx_params params, + size_t jobSize, + const void* dict, size_t const dictSize, + ZSTD_dictContentType_e dictContentType) +{ + /* Adjust parameters */ + if (params.ldmParams.enableLdm == ZSTD_ps_enable) { + DEBUGLOG(4, "LDM window size = %u KB", (1U << params.cParams.windowLog) >> 10); + ZSTD_ldm_adjustParameters(¶ms.ldmParams, ¶ms.cParams); + assert(params.ldmParams.hashLog >= params.ldmParams.bucketSizeLog); + assert(params.ldmParams.hashRateLog < 32); + } else { + ZSTD_memset(¶ms.ldmParams, 0, sizeof(params.ldmParams)); + } + serialState->nextJobID = 0; + if (params.fParams.checksumFlag) + XXH64_reset(&serialState->xxhState, 0); + if (params.ldmParams.enableLdm == ZSTD_ps_enable) { + ZSTD_customMem cMem = params.customMem; + unsigned const hashLog = params.ldmParams.hashLog; + size_t const hashSize = ((size_t)1 << hashLog) * sizeof(ldmEntry_t); + unsigned const bucketLog = + params.ldmParams.hashLog - params.ldmParams.bucketSizeLog; + unsigned const prevBucketLog = + serialState->params.ldmParams.hashLog - + serialState->params.ldmParams.bucketSizeLog; + size_t const numBuckets = (size_t)1 << bucketLog; + /* Size the seq pool tables */ + ZSTDMT_setNbSeq(seqPool, ZSTD_ldm_getMaxNbSeq(params.ldmParams, jobSize)); + /* Reset the window */ + ZSTD_window_init(&serialState->ldmState.window); + /* Resize tables and output space if necessary. */ + if (serialState->ldmState.hashTable == NULL || serialState->params.ldmParams.hashLog < hashLog) { + ZSTD_customFree(serialState->ldmState.hashTable, cMem); + serialState->ldmState.hashTable = (ldmEntry_t*)ZSTD_customMalloc(hashSize, cMem); + } + if (serialState->ldmState.bucketOffsets == NULL || prevBucketLog < bucketLog) { + ZSTD_customFree(serialState->ldmState.bucketOffsets, cMem); + serialState->ldmState.bucketOffsets = (BYTE*)ZSTD_customMalloc(numBuckets, cMem); + } + if (!serialState->ldmState.hashTable || !serialState->ldmState.bucketOffsets) + return 1; + /* Zero the tables */ + ZSTD_memset(serialState->ldmState.hashTable, 0, hashSize); + ZSTD_memset(serialState->ldmState.bucketOffsets, 0, numBuckets); + + /* Update window state and fill hash table with dict */ + serialState->ldmState.loadedDictEnd = 0; + if (dictSize > 0) { + if (dictContentType == ZSTD_dct_rawContent) { + BYTE const* const dictEnd = (const BYTE*)dict + dictSize; + ZSTD_window_update(&serialState->ldmState.window, dict, dictSize, /* forceNonContiguous */ 0); + ZSTD_ldm_fillHashTable(&serialState->ldmState, (const BYTE*)dict, dictEnd, ¶ms.ldmParams); + serialState->ldmState.loadedDictEnd = params.forceWindow ? 0 : (U32)(dictEnd - serialState->ldmState.window.base); + } else { + /* don't even load anything */ + } + } + + /* Initialize serialState's copy of ldmWindow. */ + serialState->ldmWindow = serialState->ldmState.window; + } + + serialState->params = params; + serialState->params.jobSize = (U32)jobSize; + return 0; +} + +static int ZSTDMT_serialState_init(serialState_t* serialState) +{ + int initError = 0; + ZSTD_memset(serialState, 0, sizeof(*serialState)); + initError |= ZSTD_pthread_mutex_init(&serialState->mutex, NULL); + initError |= ZSTD_pthread_cond_init(&serialState->cond, NULL); + initError |= ZSTD_pthread_mutex_init(&serialState->ldmWindowMutex, NULL); + initError |= ZSTD_pthread_cond_init(&serialState->ldmWindowCond, NULL); + return initError; +} + +static void ZSTDMT_serialState_free(serialState_t* serialState) +{ + ZSTD_customMem cMem = serialState->params.customMem; + ZSTD_pthread_mutex_destroy(&serialState->mutex); + ZSTD_pthread_cond_destroy(&serialState->cond); + ZSTD_pthread_mutex_destroy(&serialState->ldmWindowMutex); + ZSTD_pthread_cond_destroy(&serialState->ldmWindowCond); + ZSTD_customFree(serialState->ldmState.hashTable, cMem); + ZSTD_customFree(serialState->ldmState.bucketOffsets, cMem); +} + +static void ZSTDMT_serialState_update(serialState_t* serialState, + ZSTD_CCtx* jobCCtx, rawSeqStore_t seqStore, + range_t src, unsigned jobID) +{ + /* Wait for our turn */ + ZSTD_PTHREAD_MUTEX_LOCK(&serialState->mutex); + while (serialState->nextJobID < jobID) { + DEBUGLOG(5, "wait for serialState->cond"); + ZSTD_pthread_cond_wait(&serialState->cond, &serialState->mutex); + } + /* A future job may error and skip our job */ + if (serialState->nextJobID == jobID) { + /* It is now our turn, do any processing necessary */ + if (serialState->params.ldmParams.enableLdm == ZSTD_ps_enable) { + size_t error; + assert(seqStore.seq != NULL && seqStore.pos == 0 && + seqStore.size == 0 && seqStore.capacity > 0); + assert(src.size <= serialState->params.jobSize); + ZSTD_window_update(&serialState->ldmState.window, src.start, src.size, /* forceNonContiguous */ 0); + error = ZSTD_ldm_generateSequences( + &serialState->ldmState, &seqStore, + &serialState->params.ldmParams, src.start, src.size); + /* We provide a large enough buffer to never fail. */ + assert(!ZSTD_isError(error)); (void)error; + /* Update ldmWindow to match the ldmState.window and signal the main + * thread if it is waiting for a buffer. + */ + ZSTD_PTHREAD_MUTEX_LOCK(&serialState->ldmWindowMutex); + serialState->ldmWindow = serialState->ldmState.window; + ZSTD_pthread_cond_signal(&serialState->ldmWindowCond); + ZSTD_pthread_mutex_unlock(&serialState->ldmWindowMutex); + } + if (serialState->params.fParams.checksumFlag && src.size > 0) + XXH64_update(&serialState->xxhState, src.start, src.size); + } + /* Now it is the next jobs turn */ + serialState->nextJobID++; + ZSTD_pthread_cond_broadcast(&serialState->cond); + ZSTD_pthread_mutex_unlock(&serialState->mutex); + + if (seqStore.size > 0) { + ZSTD_referenceExternalSequences(jobCCtx, seqStore.seq, seqStore.size); + assert(serialState->params.ldmParams.enableLdm == ZSTD_ps_enable); + } +} + +static void ZSTDMT_serialState_ensureFinished(serialState_t* serialState, + unsigned jobID, size_t cSize) +{ + ZSTD_PTHREAD_MUTEX_LOCK(&serialState->mutex); + if (serialState->nextJobID <= jobID) { + assert(ZSTD_isError(cSize)); (void)cSize; + DEBUGLOG(5, "Skipping past job %u because of error", jobID); + serialState->nextJobID = jobID + 1; + ZSTD_pthread_cond_broadcast(&serialState->cond); + + ZSTD_PTHREAD_MUTEX_LOCK(&serialState->ldmWindowMutex); + ZSTD_window_clear(&serialState->ldmWindow); + ZSTD_pthread_cond_signal(&serialState->ldmWindowCond); + ZSTD_pthread_mutex_unlock(&serialState->ldmWindowMutex); + } + ZSTD_pthread_mutex_unlock(&serialState->mutex); + +} + + +/* ------------------------------------------ */ +/* ===== Worker thread ===== */ +/* ------------------------------------------ */ + +static const range_t kNullRange = { NULL, 0 }; + +typedef struct { + size_t consumed; /* SHARED - set0 by mtctx, then modified by worker AND read by mtctx */ + size_t cSize; /* SHARED - set0 by mtctx, then modified by worker AND read by mtctx, then set0 by mtctx */ + ZSTD_pthread_mutex_t job_mutex; /* Thread-safe - used by mtctx and worker */ + ZSTD_pthread_cond_t job_cond; /* Thread-safe - used by mtctx and worker */ + ZSTDMT_CCtxPool* cctxPool; /* Thread-safe - used by mtctx and (all) workers */ + ZSTDMT_bufferPool* bufPool; /* Thread-safe - used by mtctx and (all) workers */ + ZSTDMT_seqPool* seqPool; /* Thread-safe - used by mtctx and (all) workers */ + serialState_t* serial; /* Thread-safe - used by mtctx and (all) workers */ + buffer_t dstBuff; /* set by worker (or mtctx), then read by worker & mtctx, then modified by mtctx => no barrier */ + range_t prefix; /* set by mtctx, then read by worker & mtctx => no barrier */ + range_t src; /* set by mtctx, then read by worker & mtctx => no barrier */ + unsigned jobID; /* set by mtctx, then read by worker => no barrier */ + unsigned firstJob; /* set by mtctx, then read by worker => no barrier */ + unsigned lastJob; /* set by mtctx, then read by worker => no barrier */ + ZSTD_CCtx_params params; /* set by mtctx, then read by worker => no barrier */ + const ZSTD_CDict* cdict; /* set by mtctx, then read by worker => no barrier */ + unsigned long long fullFrameSize; /* set by mtctx, then read by worker => no barrier */ + size_t dstFlushed; /* used only by mtctx */ + unsigned frameChecksumNeeded; /* used only by mtctx */ +} ZSTDMT_jobDescription; + +#define JOB_ERROR(e) \ + do { \ + ZSTD_PTHREAD_MUTEX_LOCK(&job->job_mutex); \ + job->cSize = e; \ + ZSTD_pthread_mutex_unlock(&job->job_mutex); \ + goto _endJob; \ + } while (0) + +/* ZSTDMT_compressionJob() is a POOL_function type */ +static void ZSTDMT_compressionJob(void* jobDescription) +{ + ZSTDMT_jobDescription* const job = (ZSTDMT_jobDescription*)jobDescription; + ZSTD_CCtx_params jobParams = job->params; /* do not modify job->params ! copy it, modify the copy */ + ZSTD_CCtx* const cctx = ZSTDMT_getCCtx(job->cctxPool); + rawSeqStore_t rawSeqStore = ZSTDMT_getSeq(job->seqPool); + buffer_t dstBuff = job->dstBuff; + size_t lastCBlockSize = 0; + + /* resources */ + if (cctx==NULL) JOB_ERROR(ERROR(memory_allocation)); + if (dstBuff.start == NULL) { /* streaming job : doesn't provide a dstBuffer */ + dstBuff = ZSTDMT_getBuffer(job->bufPool); + if (dstBuff.start==NULL) JOB_ERROR(ERROR(memory_allocation)); + job->dstBuff = dstBuff; /* this value can be read in ZSTDMT_flush, when it copies the whole job */ + } + if (jobParams.ldmParams.enableLdm == ZSTD_ps_enable && rawSeqStore.seq == NULL) + JOB_ERROR(ERROR(memory_allocation)); + + /* Don't compute the checksum for chunks, since we compute it externally, + * but write it in the header. + */ + if (job->jobID != 0) jobParams.fParams.checksumFlag = 0; + /* Don't run LDM for the chunks, since we handle it externally */ + jobParams.ldmParams.enableLdm = ZSTD_ps_disable; + /* Correct nbWorkers to 0. */ + jobParams.nbWorkers = 0; + + + /* init */ + if (job->cdict) { + size_t const initError = ZSTD_compressBegin_advanced_internal(cctx, NULL, 0, ZSTD_dct_auto, ZSTD_dtlm_fast, job->cdict, &jobParams, job->fullFrameSize); + assert(job->firstJob); /* only allowed for first job */ + if (ZSTD_isError(initError)) JOB_ERROR(initError); + } else { /* srcStart points at reloaded section */ + U64 const pledgedSrcSize = job->firstJob ? job->fullFrameSize : job->src.size; + { size_t const forceWindowError = ZSTD_CCtxParams_setParameter(&jobParams, ZSTD_c_forceMaxWindow, !job->firstJob); + if (ZSTD_isError(forceWindowError)) JOB_ERROR(forceWindowError); + } + if (!job->firstJob) { + size_t const err = ZSTD_CCtxParams_setParameter(&jobParams, ZSTD_c_deterministicRefPrefix, 0); + if (ZSTD_isError(err)) JOB_ERROR(err); + } + { size_t const initError = ZSTD_compressBegin_advanced_internal(cctx, + job->prefix.start, job->prefix.size, ZSTD_dct_rawContent, /* load dictionary in "content-only" mode (no header analysis) */ + ZSTD_dtlm_fast, + NULL, /*cdict*/ + &jobParams, pledgedSrcSize); + if (ZSTD_isError(initError)) JOB_ERROR(initError); + } } + + /* Perform serial step as early as possible, but after CCtx initialization */ + ZSTDMT_serialState_update(job->serial, cctx, rawSeqStore, job->src, job->jobID); + + if (!job->firstJob) { /* flush and overwrite frame header when it's not first job */ + size_t const hSize = ZSTD_compressContinue_public(cctx, dstBuff.start, dstBuff.capacity, job->src.start, 0); + if (ZSTD_isError(hSize)) JOB_ERROR(hSize); + DEBUGLOG(5, "ZSTDMT_compressionJob: flush and overwrite %u bytes of frame header (not first job)", (U32)hSize); + ZSTD_invalidateRepCodes(cctx); + } + + /* compress */ + { size_t const chunkSize = 4*ZSTD_BLOCKSIZE_MAX; + int const nbChunks = (int)((job->src.size + (chunkSize-1)) / chunkSize); + const BYTE* ip = (const BYTE*) job->src.start; + BYTE* const ostart = (BYTE*)dstBuff.start; + BYTE* op = ostart; + BYTE* oend = op + dstBuff.capacity; + int chunkNb; + if (sizeof(size_t) > sizeof(int)) assert(job->src.size < ((size_t)INT_MAX) * chunkSize); /* check overflow */ + DEBUGLOG(5, "ZSTDMT_compressionJob: compress %u bytes in %i blocks", (U32)job->src.size, nbChunks); + assert(job->cSize == 0); + for (chunkNb = 1; chunkNb < nbChunks; chunkNb++) { + size_t const cSize = ZSTD_compressContinue_public(cctx, op, oend-op, ip, chunkSize); + if (ZSTD_isError(cSize)) JOB_ERROR(cSize); + ip += chunkSize; + op += cSize; assert(op < oend); + /* stats */ + ZSTD_PTHREAD_MUTEX_LOCK(&job->job_mutex); + job->cSize += cSize; + job->consumed = chunkSize * chunkNb; + DEBUGLOG(5, "ZSTDMT_compressionJob: compress new block : cSize==%u bytes (total: %u)", + (U32)cSize, (U32)job->cSize); + ZSTD_pthread_cond_signal(&job->job_cond); /* warns some more data is ready to be flushed */ + ZSTD_pthread_mutex_unlock(&job->job_mutex); + } + /* last block */ + assert(chunkSize > 0); + assert((chunkSize & (chunkSize - 1)) == 0); /* chunkSize must be power of 2 for mask==(chunkSize-1) to work */ + if ((nbChunks > 0) | job->lastJob /*must output a "last block" flag*/ ) { + size_t const lastBlockSize1 = job->src.size & (chunkSize-1); + size_t const lastBlockSize = ((lastBlockSize1==0) & (job->src.size>=chunkSize)) ? chunkSize : lastBlockSize1; + size_t const cSize = (job->lastJob) ? + ZSTD_compressEnd_public(cctx, op, oend-op, ip, lastBlockSize) : + ZSTD_compressContinue_public(cctx, op, oend-op, ip, lastBlockSize); + if (ZSTD_isError(cSize)) JOB_ERROR(cSize); + lastCBlockSize = cSize; + } } + if (!job->firstJob) { + /* Double check that we don't have an ext-dict, because then our + * repcode invalidation doesn't work. + */ + assert(!ZSTD_window_hasExtDict(cctx->blockState.matchState.window)); + } + ZSTD_CCtx_trace(cctx, 0); + +_endJob: + ZSTDMT_serialState_ensureFinished(job->serial, job->jobID, job->cSize); + if (job->prefix.size > 0) + DEBUGLOG(5, "Finished with prefix: %zx", (size_t)job->prefix.start); + DEBUGLOG(5, "Finished with source: %zx", (size_t)job->src.start); + /* release resources */ + ZSTDMT_releaseSeq(job->seqPool, rawSeqStore); + ZSTDMT_releaseCCtx(job->cctxPool, cctx); + /* report */ + ZSTD_PTHREAD_MUTEX_LOCK(&job->job_mutex); + if (ZSTD_isError(job->cSize)) assert(lastCBlockSize == 0); + job->cSize += lastCBlockSize; + job->consumed = job->src.size; /* when job->consumed == job->src.size , compression job is presumed completed */ + ZSTD_pthread_cond_signal(&job->job_cond); + ZSTD_pthread_mutex_unlock(&job->job_mutex); +} + + +/* ------------------------------------------ */ +/* ===== Multi-threaded compression ===== */ +/* ------------------------------------------ */ + +typedef struct { + range_t prefix; /* read-only non-owned prefix buffer */ + buffer_t buffer; + size_t filled; +} inBuff_t; + +typedef struct { + BYTE* buffer; /* The round input buffer. All jobs get references + * to pieces of the buffer. ZSTDMT_tryGetInputRange() + * handles handing out job input buffers, and makes + * sure it doesn't overlap with any pieces still in use. + */ + size_t capacity; /* The capacity of buffer. */ + size_t pos; /* The position of the current inBuff in the round + * buffer. Updated past the end if the inBuff once + * the inBuff is sent to the worker thread. + * pos <= capacity. + */ +} roundBuff_t; + +static const roundBuff_t kNullRoundBuff = {NULL, 0, 0}; + +#define RSYNC_LENGTH 32 +/* Don't create chunks smaller than the zstd block size. + * This stops us from regressing compression ratio too much, + * and ensures our output fits in ZSTD_compressBound(). + * + * If this is shrunk < ZSTD_BLOCKSIZELOG_MIN then + * ZSTD_COMPRESSBOUND() will need to be updated. + */ +#define RSYNC_MIN_BLOCK_LOG ZSTD_BLOCKSIZELOG_MAX +#define RSYNC_MIN_BLOCK_SIZE (1< one job is already prepared, but pool has shortage of workers. Don't create a new job. */ + inBuff_t inBuff; + roundBuff_t roundBuff; + serialState_t serial; + rsyncState_t rsync; + unsigned jobIDMask; + unsigned doneJobID; + unsigned nextJobID; + unsigned frameEnded; + unsigned allJobsCompleted; + unsigned long long frameContentSize; + unsigned long long consumed; + unsigned long long produced; + ZSTD_customMem cMem; + ZSTD_CDict* cdictLocal; + const ZSTD_CDict* cdict; + unsigned providedFactory: 1; +}; + +static void ZSTDMT_freeJobsTable(ZSTDMT_jobDescription* jobTable, U32 nbJobs, ZSTD_customMem cMem) +{ + U32 jobNb; + if (jobTable == NULL) return; + for (jobNb=0; jobNb mtctx->jobIDMask+1) { /* need more job capacity */ + ZSTDMT_freeJobsTable(mtctx->jobs, mtctx->jobIDMask+1, mtctx->cMem); + mtctx->jobIDMask = 0; + mtctx->jobs = ZSTDMT_createJobsTable(&nbJobs, mtctx->cMem); + if (mtctx->jobs==NULL) return ERROR(memory_allocation); + assert((nbJobs != 0) && ((nbJobs & (nbJobs - 1)) == 0)); /* ensure nbJobs is a power of 2 */ + mtctx->jobIDMask = nbJobs - 1; + } + return 0; +} + + +/* ZSTDMT_CCtxParam_setNbWorkers(): + * Internal use only */ +static size_t ZSTDMT_CCtxParam_setNbWorkers(ZSTD_CCtx_params* params, unsigned nbWorkers) +{ + return ZSTD_CCtxParams_setParameter(params, ZSTD_c_nbWorkers, (int)nbWorkers); +} + +MEM_STATIC ZSTDMT_CCtx* ZSTDMT_createCCtx_advanced_internal(unsigned nbWorkers, ZSTD_customMem cMem, ZSTD_threadPool* pool) +{ + ZSTDMT_CCtx* mtctx; + U32 nbJobs = nbWorkers + 2; + int initError; + DEBUGLOG(3, "ZSTDMT_createCCtx_advanced (nbWorkers = %u)", nbWorkers); + + if (nbWorkers < 1) return NULL; + nbWorkers = MIN(nbWorkers , ZSTDMT_NBWORKERS_MAX); + if ((cMem.customAlloc!=NULL) ^ (cMem.customFree!=NULL)) + /* invalid custom allocator */ + return NULL; + + mtctx = (ZSTDMT_CCtx*) ZSTD_customCalloc(sizeof(ZSTDMT_CCtx), cMem); + if (!mtctx) return NULL; + ZSTDMT_CCtxParam_setNbWorkers(&mtctx->params, nbWorkers); + mtctx->cMem = cMem; + mtctx->allJobsCompleted = 1; + if (pool != NULL) { + mtctx->factory = pool; + mtctx->providedFactory = 1; + } + else { + mtctx->factory = POOL_create_advanced(nbWorkers, 0, cMem); + mtctx->providedFactory = 0; + } + mtctx->jobs = ZSTDMT_createJobsTable(&nbJobs, cMem); + assert(nbJobs > 0); assert((nbJobs & (nbJobs - 1)) == 0); /* ensure nbJobs is a power of 2 */ + mtctx->jobIDMask = nbJobs - 1; + mtctx->bufPool = ZSTDMT_createBufferPool(BUF_POOL_MAX_NB_BUFFERS(nbWorkers), cMem); + mtctx->cctxPool = ZSTDMT_createCCtxPool(nbWorkers, cMem); + mtctx->seqPool = ZSTDMT_createSeqPool(nbWorkers, cMem); + initError = ZSTDMT_serialState_init(&mtctx->serial); + mtctx->roundBuff = kNullRoundBuff; + if (!mtctx->factory | !mtctx->jobs | !mtctx->bufPool | !mtctx->cctxPool | !mtctx->seqPool | initError) { + ZSTDMT_freeCCtx(mtctx); + return NULL; + } + DEBUGLOG(3, "mt_cctx created, for %u threads", nbWorkers); + return mtctx; +} + +ZSTDMT_CCtx* ZSTDMT_createCCtx_advanced(unsigned nbWorkers, ZSTD_customMem cMem, ZSTD_threadPool* pool) +{ +#ifdef ZSTD_MULTITHREAD + return ZSTDMT_createCCtx_advanced_internal(nbWorkers, cMem, pool); +#else + (void)nbWorkers; + (void)cMem; + (void)pool; + return NULL; +#endif +} + + +/* ZSTDMT_releaseAllJobResources() : + * note : ensure all workers are killed first ! */ +static void ZSTDMT_releaseAllJobResources(ZSTDMT_CCtx* mtctx) +{ + unsigned jobID; + DEBUGLOG(3, "ZSTDMT_releaseAllJobResources"); + for (jobID=0; jobID <= mtctx->jobIDMask; jobID++) { + /* Copy the mutex/cond out */ + ZSTD_pthread_mutex_t const mutex = mtctx->jobs[jobID].job_mutex; + ZSTD_pthread_cond_t const cond = mtctx->jobs[jobID].job_cond; + + DEBUGLOG(4, "job%02u: release dst address %08X", jobID, (U32)(size_t)mtctx->jobs[jobID].dstBuff.start); + ZSTDMT_releaseBuffer(mtctx->bufPool, mtctx->jobs[jobID].dstBuff); + + /* Clear the job description, but keep the mutex/cond */ + ZSTD_memset(&mtctx->jobs[jobID], 0, sizeof(mtctx->jobs[jobID])); + mtctx->jobs[jobID].job_mutex = mutex; + mtctx->jobs[jobID].job_cond = cond; + } + mtctx->inBuff.buffer = g_nullBuffer; + mtctx->inBuff.filled = 0; + mtctx->allJobsCompleted = 1; +} + +static void ZSTDMT_waitForAllJobsCompleted(ZSTDMT_CCtx* mtctx) +{ + DEBUGLOG(4, "ZSTDMT_waitForAllJobsCompleted"); + while (mtctx->doneJobID < mtctx->nextJobID) { + unsigned const jobID = mtctx->doneJobID & mtctx->jobIDMask; + ZSTD_PTHREAD_MUTEX_LOCK(&mtctx->jobs[jobID].job_mutex); + while (mtctx->jobs[jobID].consumed < mtctx->jobs[jobID].src.size) { + DEBUGLOG(4, "waiting for jobCompleted signal from job %u", mtctx->doneJobID); /* we want to block when waiting for data to flush */ + ZSTD_pthread_cond_wait(&mtctx->jobs[jobID].job_cond, &mtctx->jobs[jobID].job_mutex); + } + ZSTD_pthread_mutex_unlock(&mtctx->jobs[jobID].job_mutex); + mtctx->doneJobID++; + } +} + +size_t ZSTDMT_freeCCtx(ZSTDMT_CCtx* mtctx) +{ + if (mtctx==NULL) return 0; /* compatible with free on NULL */ + if (!mtctx->providedFactory) + POOL_free(mtctx->factory); /* stop and free worker threads */ + ZSTDMT_releaseAllJobResources(mtctx); /* release job resources into pools first */ + ZSTDMT_freeJobsTable(mtctx->jobs, mtctx->jobIDMask+1, mtctx->cMem); + ZSTDMT_freeBufferPool(mtctx->bufPool); + ZSTDMT_freeCCtxPool(mtctx->cctxPool); + ZSTDMT_freeSeqPool(mtctx->seqPool); + ZSTDMT_serialState_free(&mtctx->serial); + ZSTD_freeCDict(mtctx->cdictLocal); + if (mtctx->roundBuff.buffer) + ZSTD_customFree(mtctx->roundBuff.buffer, mtctx->cMem); + ZSTD_customFree(mtctx, mtctx->cMem); + return 0; +} + +size_t ZSTDMT_sizeof_CCtx(ZSTDMT_CCtx* mtctx) +{ + if (mtctx == NULL) return 0; /* supports sizeof NULL */ + return sizeof(*mtctx) + + POOL_sizeof(mtctx->factory) + + ZSTDMT_sizeof_bufferPool(mtctx->bufPool) + + (mtctx->jobIDMask+1) * sizeof(ZSTDMT_jobDescription) + + ZSTDMT_sizeof_CCtxPool(mtctx->cctxPool) + + ZSTDMT_sizeof_seqPool(mtctx->seqPool) + + ZSTD_sizeof_CDict(mtctx->cdictLocal) + + mtctx->roundBuff.capacity; +} + + +/* ZSTDMT_resize() : + * @return : error code if fails, 0 on success */ +static size_t ZSTDMT_resize(ZSTDMT_CCtx* mtctx, unsigned nbWorkers) +{ + if (POOL_resize(mtctx->factory, nbWorkers)) return ERROR(memory_allocation); + FORWARD_IF_ERROR( ZSTDMT_expandJobsTable(mtctx, nbWorkers) , ""); + mtctx->bufPool = ZSTDMT_expandBufferPool(mtctx->bufPool, BUF_POOL_MAX_NB_BUFFERS(nbWorkers)); + if (mtctx->bufPool == NULL) return ERROR(memory_allocation); + mtctx->cctxPool = ZSTDMT_expandCCtxPool(mtctx->cctxPool, nbWorkers); + if (mtctx->cctxPool == NULL) return ERROR(memory_allocation); + mtctx->seqPool = ZSTDMT_expandSeqPool(mtctx->seqPool, nbWorkers); + if (mtctx->seqPool == NULL) return ERROR(memory_allocation); + ZSTDMT_CCtxParam_setNbWorkers(&mtctx->params, nbWorkers); + return 0; +} + + +/*! ZSTDMT_updateCParams_whileCompressing() : + * Updates a selected set of compression parameters, remaining compatible with currently active frame. + * New parameters will be applied to next compression job. */ +void ZSTDMT_updateCParams_whileCompressing(ZSTDMT_CCtx* mtctx, const ZSTD_CCtx_params* cctxParams) +{ + U32 const saved_wlog = mtctx->params.cParams.windowLog; /* Do not modify windowLog while compressing */ + int const compressionLevel = cctxParams->compressionLevel; + DEBUGLOG(5, "ZSTDMT_updateCParams_whileCompressing (level:%i)", + compressionLevel); + mtctx->params.compressionLevel = compressionLevel; + { ZSTD_compressionParameters cParams = ZSTD_getCParamsFromCCtxParams(cctxParams, ZSTD_CONTENTSIZE_UNKNOWN, 0, ZSTD_cpm_noAttachDict); + cParams.windowLog = saved_wlog; + mtctx->params.cParams = cParams; + } +} + +/* ZSTDMT_getFrameProgression(): + * tells how much data has been consumed (input) and produced (output) for current frame. + * able to count progression inside worker threads. + * Note : mutex will be acquired during statistics collection inside workers. */ +ZSTD_frameProgression ZSTDMT_getFrameProgression(ZSTDMT_CCtx* mtctx) +{ + ZSTD_frameProgression fps; + DEBUGLOG(5, "ZSTDMT_getFrameProgression"); + fps.ingested = mtctx->consumed + mtctx->inBuff.filled; + fps.consumed = mtctx->consumed; + fps.produced = fps.flushed = mtctx->produced; + fps.currentJobID = mtctx->nextJobID; + fps.nbActiveWorkers = 0; + { unsigned jobNb; + unsigned lastJobNb = mtctx->nextJobID + mtctx->jobReady; assert(mtctx->jobReady <= 1); + DEBUGLOG(6, "ZSTDMT_getFrameProgression: jobs: from %u to <%u (jobReady:%u)", + mtctx->doneJobID, lastJobNb, mtctx->jobReady); + for (jobNb = mtctx->doneJobID ; jobNb < lastJobNb ; jobNb++) { + unsigned const wJobID = jobNb & mtctx->jobIDMask; + ZSTDMT_jobDescription* jobPtr = &mtctx->jobs[wJobID]; + ZSTD_pthread_mutex_lock(&jobPtr->job_mutex); + { size_t const cResult = jobPtr->cSize; + size_t const produced = ZSTD_isError(cResult) ? 0 : cResult; + size_t const flushed = ZSTD_isError(cResult) ? 0 : jobPtr->dstFlushed; + assert(flushed <= produced); + fps.ingested += jobPtr->src.size; + fps.consumed += jobPtr->consumed; + fps.produced += produced; + fps.flushed += flushed; + fps.nbActiveWorkers += (jobPtr->consumed < jobPtr->src.size); + } + ZSTD_pthread_mutex_unlock(&mtctx->jobs[wJobID].job_mutex); + } + } + return fps; +} + + +size_t ZSTDMT_toFlushNow(ZSTDMT_CCtx* mtctx) +{ + size_t toFlush; + unsigned const jobID = mtctx->doneJobID; + assert(jobID <= mtctx->nextJobID); + if (jobID == mtctx->nextJobID) return 0; /* no active job => nothing to flush */ + + /* look into oldest non-fully-flushed job */ + { unsigned const wJobID = jobID & mtctx->jobIDMask; + ZSTDMT_jobDescription* const jobPtr = &mtctx->jobs[wJobID]; + ZSTD_pthread_mutex_lock(&jobPtr->job_mutex); + { size_t const cResult = jobPtr->cSize; + size_t const produced = ZSTD_isError(cResult) ? 0 : cResult; + size_t const flushed = ZSTD_isError(cResult) ? 0 : jobPtr->dstFlushed; + assert(flushed <= produced); + assert(jobPtr->consumed <= jobPtr->src.size); + toFlush = produced - flushed; + /* if toFlush==0, nothing is available to flush. + * However, jobID is expected to still be active: + * if jobID was already completed and fully flushed, + * ZSTDMT_flushProduced() should have already moved onto next job. + * Therefore, some input has not yet been consumed. */ + if (toFlush==0) { + assert(jobPtr->consumed < jobPtr->src.size); + } + } + ZSTD_pthread_mutex_unlock(&mtctx->jobs[wJobID].job_mutex); + } + + return toFlush; +} + + +/* ------------------------------------------ */ +/* ===== Multi-threaded compression ===== */ +/* ------------------------------------------ */ + +static unsigned ZSTDMT_computeTargetJobLog(const ZSTD_CCtx_params* params) +{ + unsigned jobLog; + if (params->ldmParams.enableLdm == ZSTD_ps_enable) { + /* In Long Range Mode, the windowLog is typically oversized. + * In which case, it's preferable to determine the jobSize + * based on cycleLog instead. */ + jobLog = MAX(21, ZSTD_cycleLog(params->cParams.chainLog, params->cParams.strategy) + 3); + } else { + jobLog = MAX(20, params->cParams.windowLog + 2); + } + return MIN(jobLog, (unsigned)ZSTDMT_JOBLOG_MAX); +} + +static int ZSTDMT_overlapLog_default(ZSTD_strategy strat) +{ + switch(strat) + { + case ZSTD_btultra2: + return 9; + case ZSTD_btultra: + case ZSTD_btopt: + return 8; + case ZSTD_btlazy2: + case ZSTD_lazy2: + return 7; + case ZSTD_lazy: + case ZSTD_greedy: + case ZSTD_dfast: + case ZSTD_fast: + default:; + } + return 6; +} + +static int ZSTDMT_overlapLog(int ovlog, ZSTD_strategy strat) +{ + assert(0 <= ovlog && ovlog <= 9); + if (ovlog == 0) return ZSTDMT_overlapLog_default(strat); + return ovlog; +} + +static size_t ZSTDMT_computeOverlapSize(const ZSTD_CCtx_params* params) +{ + int const overlapRLog = 9 - ZSTDMT_overlapLog(params->overlapLog, params->cParams.strategy); + int ovLog = (overlapRLog >= 8) ? 0 : (params->cParams.windowLog - overlapRLog); + assert(0 <= overlapRLog && overlapRLog <= 8); + if (params->ldmParams.enableLdm == ZSTD_ps_enable) { + /* In Long Range Mode, the windowLog is typically oversized. + * In which case, it's preferable to determine the jobSize + * based on chainLog instead. + * Then, ovLog becomes a fraction of the jobSize, rather than windowSize */ + ovLog = MIN(params->cParams.windowLog, ZSTDMT_computeTargetJobLog(params) - 2) + - overlapRLog; + } + assert(0 <= ovLog && ovLog <= ZSTD_WINDOWLOG_MAX); + DEBUGLOG(4, "overlapLog : %i", params->overlapLog); + DEBUGLOG(4, "overlap size : %i", 1 << ovLog); + return (ovLog==0) ? 0 : (size_t)1 << ovLog; +} + +/* ====================================== */ +/* ======= Streaming API ======= */ +/* ====================================== */ + +size_t ZSTDMT_initCStream_internal( + ZSTDMT_CCtx* mtctx, + const void* dict, size_t dictSize, ZSTD_dictContentType_e dictContentType, + const ZSTD_CDict* cdict, ZSTD_CCtx_params params, + unsigned long long pledgedSrcSize) +{ + DEBUGLOG(4, "ZSTDMT_initCStream_internal (pledgedSrcSize=%u, nbWorkers=%u, cctxPool=%u)", + (U32)pledgedSrcSize, params.nbWorkers, mtctx->cctxPool->totalCCtx); + + /* params supposed partially fully validated at this point */ + assert(!ZSTD_isError(ZSTD_checkCParams(params.cParams))); + assert(!((dict) && (cdict))); /* either dict or cdict, not both */ + + /* init */ + if (params.nbWorkers != mtctx->params.nbWorkers) + FORWARD_IF_ERROR( ZSTDMT_resize(mtctx, params.nbWorkers) , ""); + + if (params.jobSize != 0 && params.jobSize < ZSTDMT_JOBSIZE_MIN) params.jobSize = ZSTDMT_JOBSIZE_MIN; + if (params.jobSize > (size_t)ZSTDMT_JOBSIZE_MAX) params.jobSize = (size_t)ZSTDMT_JOBSIZE_MAX; + + DEBUGLOG(4, "ZSTDMT_initCStream_internal: %u workers", params.nbWorkers); + + if (mtctx->allJobsCompleted == 0) { /* previous compression not correctly finished */ + ZSTDMT_waitForAllJobsCompleted(mtctx); + ZSTDMT_releaseAllJobResources(mtctx); + mtctx->allJobsCompleted = 1; + } + + mtctx->params = params; + mtctx->frameContentSize = pledgedSrcSize; + if (dict) { + ZSTD_freeCDict(mtctx->cdictLocal); + mtctx->cdictLocal = ZSTD_createCDict_advanced(dict, dictSize, + ZSTD_dlm_byCopy, dictContentType, /* note : a loadPrefix becomes an internal CDict */ + params.cParams, mtctx->cMem); + mtctx->cdict = mtctx->cdictLocal; + if (mtctx->cdictLocal == NULL) return ERROR(memory_allocation); + } else { + ZSTD_freeCDict(mtctx->cdictLocal); + mtctx->cdictLocal = NULL; + mtctx->cdict = cdict; + } + + mtctx->targetPrefixSize = ZSTDMT_computeOverlapSize(¶ms); + DEBUGLOG(4, "overlapLog=%i => %u KB", params.overlapLog, (U32)(mtctx->targetPrefixSize>>10)); + mtctx->targetSectionSize = params.jobSize; + if (mtctx->targetSectionSize == 0) { + mtctx->targetSectionSize = 1ULL << ZSTDMT_computeTargetJobLog(¶ms); + } + assert(mtctx->targetSectionSize <= (size_t)ZSTDMT_JOBSIZE_MAX); + + if (params.rsyncable) { + /* Aim for the targetsectionSize as the average job size. */ + U32 const jobSizeKB = (U32)(mtctx->targetSectionSize >> 10); + U32 const rsyncBits = (assert(jobSizeKB >= 1), ZSTD_highbit32(jobSizeKB) + 10); + /* We refuse to create jobs < RSYNC_MIN_BLOCK_SIZE bytes, so make sure our + * expected job size is at least 4x larger. */ + assert(rsyncBits >= RSYNC_MIN_BLOCK_LOG + 2); + DEBUGLOG(4, "rsyncLog = %u", rsyncBits); + mtctx->rsync.hash = 0; + mtctx->rsync.hitMask = (1ULL << rsyncBits) - 1; + mtctx->rsync.primePower = ZSTD_rollingHash_primePower(RSYNC_LENGTH); + } + if (mtctx->targetSectionSize < mtctx->targetPrefixSize) mtctx->targetSectionSize = mtctx->targetPrefixSize; /* job size must be >= overlap size */ + DEBUGLOG(4, "Job Size : %u KB (note : set to %u)", (U32)(mtctx->targetSectionSize>>10), (U32)params.jobSize); + DEBUGLOG(4, "inBuff Size : %u KB", (U32)(mtctx->targetSectionSize>>10)); + ZSTDMT_setBufferSize(mtctx->bufPool, ZSTD_compressBound(mtctx->targetSectionSize)); + { + /* If ldm is enabled we need windowSize space. */ + size_t const windowSize = mtctx->params.ldmParams.enableLdm == ZSTD_ps_enable ? (1U << mtctx->params.cParams.windowLog) : 0; + /* Two buffers of slack, plus extra space for the overlap + * This is the minimum slack that LDM works with. One extra because + * flush might waste up to targetSectionSize-1 bytes. Another extra + * for the overlap (if > 0), then one to fill which doesn't overlap + * with the LDM window. + */ + size_t const nbSlackBuffers = 2 + (mtctx->targetPrefixSize > 0); + size_t const slackSize = mtctx->targetSectionSize * nbSlackBuffers; + /* Compute the total size, and always have enough slack */ + size_t const nbWorkers = MAX(mtctx->params.nbWorkers, 1); + size_t const sectionsSize = mtctx->targetSectionSize * nbWorkers; + size_t const capacity = MAX(windowSize, sectionsSize) + slackSize; + if (mtctx->roundBuff.capacity < capacity) { + if (mtctx->roundBuff.buffer) + ZSTD_customFree(mtctx->roundBuff.buffer, mtctx->cMem); + mtctx->roundBuff.buffer = (BYTE*)ZSTD_customMalloc(capacity, mtctx->cMem); + if (mtctx->roundBuff.buffer == NULL) { + mtctx->roundBuff.capacity = 0; + return ERROR(memory_allocation); + } + mtctx->roundBuff.capacity = capacity; + } + } + DEBUGLOG(4, "roundBuff capacity : %u KB", (U32)(mtctx->roundBuff.capacity>>10)); + mtctx->roundBuff.pos = 0; + mtctx->inBuff.buffer = g_nullBuffer; + mtctx->inBuff.filled = 0; + mtctx->inBuff.prefix = kNullRange; + mtctx->doneJobID = 0; + mtctx->nextJobID = 0; + mtctx->frameEnded = 0; + mtctx->allJobsCompleted = 0; + mtctx->consumed = 0; + mtctx->produced = 0; + if (ZSTDMT_serialState_reset(&mtctx->serial, mtctx->seqPool, params, mtctx->targetSectionSize, + dict, dictSize, dictContentType)) + return ERROR(memory_allocation); + return 0; +} + + +/* ZSTDMT_writeLastEmptyBlock() + * Write a single empty block with an end-of-frame to finish a frame. + * Job must be created from streaming variant. + * This function is always successful if expected conditions are fulfilled. + */ +static void ZSTDMT_writeLastEmptyBlock(ZSTDMT_jobDescription* job) +{ + assert(job->lastJob == 1); + assert(job->src.size == 0); /* last job is empty -> will be simplified into a last empty block */ + assert(job->firstJob == 0); /* cannot be first job, as it also needs to create frame header */ + assert(job->dstBuff.start == NULL); /* invoked from streaming variant only (otherwise, dstBuff might be user's output) */ + job->dstBuff = ZSTDMT_getBuffer(job->bufPool); + if (job->dstBuff.start == NULL) { + job->cSize = ERROR(memory_allocation); + return; + } + assert(job->dstBuff.capacity >= ZSTD_blockHeaderSize); /* no buffer should ever be that small */ + job->src = kNullRange; + job->cSize = ZSTD_writeLastEmptyBlock(job->dstBuff.start, job->dstBuff.capacity); + assert(!ZSTD_isError(job->cSize)); + assert(job->consumed == 0); +} + +static size_t ZSTDMT_createCompressionJob(ZSTDMT_CCtx* mtctx, size_t srcSize, ZSTD_EndDirective endOp) +{ + unsigned const jobID = mtctx->nextJobID & mtctx->jobIDMask; + int const endFrame = (endOp == ZSTD_e_end); + + if (mtctx->nextJobID > mtctx->doneJobID + mtctx->jobIDMask) { + DEBUGLOG(5, "ZSTDMT_createCompressionJob: will not create new job : table is full"); + assert((mtctx->nextJobID & mtctx->jobIDMask) == (mtctx->doneJobID & mtctx->jobIDMask)); + return 0; + } + + if (!mtctx->jobReady) { + BYTE const* src = (BYTE const*)mtctx->inBuff.buffer.start; + DEBUGLOG(5, "ZSTDMT_createCompressionJob: preparing job %u to compress %u bytes with %u preload ", + mtctx->nextJobID, (U32)srcSize, (U32)mtctx->inBuff.prefix.size); + mtctx->jobs[jobID].src.start = src; + mtctx->jobs[jobID].src.size = srcSize; + assert(mtctx->inBuff.filled >= srcSize); + mtctx->jobs[jobID].prefix = mtctx->inBuff.prefix; + mtctx->jobs[jobID].consumed = 0; + mtctx->jobs[jobID].cSize = 0; + mtctx->jobs[jobID].params = mtctx->params; + mtctx->jobs[jobID].cdict = mtctx->nextJobID==0 ? mtctx->cdict : NULL; + mtctx->jobs[jobID].fullFrameSize = mtctx->frameContentSize; + mtctx->jobs[jobID].dstBuff = g_nullBuffer; + mtctx->jobs[jobID].cctxPool = mtctx->cctxPool; + mtctx->jobs[jobID].bufPool = mtctx->bufPool; + mtctx->jobs[jobID].seqPool = mtctx->seqPool; + mtctx->jobs[jobID].serial = &mtctx->serial; + mtctx->jobs[jobID].jobID = mtctx->nextJobID; + mtctx->jobs[jobID].firstJob = (mtctx->nextJobID==0); + mtctx->jobs[jobID].lastJob = endFrame; + mtctx->jobs[jobID].frameChecksumNeeded = mtctx->params.fParams.checksumFlag && endFrame && (mtctx->nextJobID>0); + mtctx->jobs[jobID].dstFlushed = 0; + + /* Update the round buffer pos and clear the input buffer to be reset */ + mtctx->roundBuff.pos += srcSize; + mtctx->inBuff.buffer = g_nullBuffer; + mtctx->inBuff.filled = 0; + /* Set the prefix */ + if (!endFrame) { + size_t const newPrefixSize = MIN(srcSize, mtctx->targetPrefixSize); + mtctx->inBuff.prefix.start = src + srcSize - newPrefixSize; + mtctx->inBuff.prefix.size = newPrefixSize; + } else { /* endFrame==1 => no need for another input buffer */ + mtctx->inBuff.prefix = kNullRange; + mtctx->frameEnded = endFrame; + if (mtctx->nextJobID == 0) { + /* single job exception : checksum is already calculated directly within worker thread */ + mtctx->params.fParams.checksumFlag = 0; + } } + + if ( (srcSize == 0) + && (mtctx->nextJobID>0)/*single job must also write frame header*/ ) { + DEBUGLOG(5, "ZSTDMT_createCompressionJob: creating a last empty block to end frame"); + assert(endOp == ZSTD_e_end); /* only possible case : need to end the frame with an empty last block */ + ZSTDMT_writeLastEmptyBlock(mtctx->jobs + jobID); + mtctx->nextJobID++; + return 0; + } + } + + DEBUGLOG(5, "ZSTDMT_createCompressionJob: posting job %u : %u bytes (end:%u, jobNb == %u (mod:%u))", + mtctx->nextJobID, + (U32)mtctx->jobs[jobID].src.size, + mtctx->jobs[jobID].lastJob, + mtctx->nextJobID, + jobID); + if (POOL_tryAdd(mtctx->factory, ZSTDMT_compressionJob, &mtctx->jobs[jobID])) { + mtctx->nextJobID++; + mtctx->jobReady = 0; + } else { + DEBUGLOG(5, "ZSTDMT_createCompressionJob: no worker available for job %u", mtctx->nextJobID); + mtctx->jobReady = 1; + } + return 0; +} + + +/*! ZSTDMT_flushProduced() : + * flush whatever data has been produced but not yet flushed in current job. + * move to next job if current one is fully flushed. + * `output` : `pos` will be updated with amount of data flushed . + * `blockToFlush` : if >0, the function will block and wait if there is no data available to flush . + * @return : amount of data remaining within internal buffer, 0 if no more, 1 if unknown but > 0, or an error code */ +static size_t ZSTDMT_flushProduced(ZSTDMT_CCtx* mtctx, ZSTD_outBuffer* output, unsigned blockToFlush, ZSTD_EndDirective end) +{ + unsigned const wJobID = mtctx->doneJobID & mtctx->jobIDMask; + DEBUGLOG(5, "ZSTDMT_flushProduced (blocking:%u , job %u <= %u)", + blockToFlush, mtctx->doneJobID, mtctx->nextJobID); + assert(output->size >= output->pos); + + ZSTD_PTHREAD_MUTEX_LOCK(&mtctx->jobs[wJobID].job_mutex); + if ( blockToFlush + && (mtctx->doneJobID < mtctx->nextJobID) ) { + assert(mtctx->jobs[wJobID].dstFlushed <= mtctx->jobs[wJobID].cSize); + while (mtctx->jobs[wJobID].dstFlushed == mtctx->jobs[wJobID].cSize) { /* nothing to flush */ + if (mtctx->jobs[wJobID].consumed == mtctx->jobs[wJobID].src.size) { + DEBUGLOG(5, "job %u is completely consumed (%u == %u) => don't wait for cond, there will be none", + mtctx->doneJobID, (U32)mtctx->jobs[wJobID].consumed, (U32)mtctx->jobs[wJobID].src.size); + break; + } + DEBUGLOG(5, "waiting for something to flush from job %u (currently flushed: %u bytes)", + mtctx->doneJobID, (U32)mtctx->jobs[wJobID].dstFlushed); + ZSTD_pthread_cond_wait(&mtctx->jobs[wJobID].job_cond, &mtctx->jobs[wJobID].job_mutex); /* block when nothing to flush but some to come */ + } } + + /* try to flush something */ + { size_t cSize = mtctx->jobs[wJobID].cSize; /* shared */ + size_t const srcConsumed = mtctx->jobs[wJobID].consumed; /* shared */ + size_t const srcSize = mtctx->jobs[wJobID].src.size; /* read-only, could be done after mutex lock, but no-declaration-after-statement */ + ZSTD_pthread_mutex_unlock(&mtctx->jobs[wJobID].job_mutex); + if (ZSTD_isError(cSize)) { + DEBUGLOG(5, "ZSTDMT_flushProduced: job %u : compression error detected : %s", + mtctx->doneJobID, ZSTD_getErrorName(cSize)); + ZSTDMT_waitForAllJobsCompleted(mtctx); + ZSTDMT_releaseAllJobResources(mtctx); + return cSize; + } + /* add frame checksum if necessary (can only happen once) */ + assert(srcConsumed <= srcSize); + if ( (srcConsumed == srcSize) /* job completed -> worker no longer active */ + && mtctx->jobs[wJobID].frameChecksumNeeded ) { + U32 const checksum = (U32)XXH64_digest(&mtctx->serial.xxhState); + DEBUGLOG(4, "ZSTDMT_flushProduced: writing checksum : %08X \n", checksum); + MEM_writeLE32((char*)mtctx->jobs[wJobID].dstBuff.start + mtctx->jobs[wJobID].cSize, checksum); + cSize += 4; + mtctx->jobs[wJobID].cSize += 4; /* can write this shared value, as worker is no longer active */ + mtctx->jobs[wJobID].frameChecksumNeeded = 0; + } + + if (cSize > 0) { /* compression is ongoing or completed */ + size_t const toFlush = MIN(cSize - mtctx->jobs[wJobID].dstFlushed, output->size - output->pos); + DEBUGLOG(5, "ZSTDMT_flushProduced: Flushing %u bytes from job %u (completion:%u/%u, generated:%u)", + (U32)toFlush, mtctx->doneJobID, (U32)srcConsumed, (U32)srcSize, (U32)cSize); + assert(mtctx->doneJobID < mtctx->nextJobID); + assert(cSize >= mtctx->jobs[wJobID].dstFlushed); + assert(mtctx->jobs[wJobID].dstBuff.start != NULL); + if (toFlush > 0) { + ZSTD_memcpy((char*)output->dst + output->pos, + (const char*)mtctx->jobs[wJobID].dstBuff.start + mtctx->jobs[wJobID].dstFlushed, + toFlush); + } + output->pos += toFlush; + mtctx->jobs[wJobID].dstFlushed += toFlush; /* can write : this value is only used by mtctx */ + + if ( (srcConsumed == srcSize) /* job is completed */ + && (mtctx->jobs[wJobID].dstFlushed == cSize) ) { /* output buffer fully flushed => free this job position */ + DEBUGLOG(5, "Job %u completed (%u bytes), moving to next one", + mtctx->doneJobID, (U32)mtctx->jobs[wJobID].dstFlushed); + ZSTDMT_releaseBuffer(mtctx->bufPool, mtctx->jobs[wJobID].dstBuff); + DEBUGLOG(5, "dstBuffer released"); + mtctx->jobs[wJobID].dstBuff = g_nullBuffer; + mtctx->jobs[wJobID].cSize = 0; /* ensure this job slot is considered "not started" in future check */ + mtctx->consumed += srcSize; + mtctx->produced += cSize; + mtctx->doneJobID++; + } } + + /* return value : how many bytes left in buffer ; fake it to 1 when unknown but >0 */ + if (cSize > mtctx->jobs[wJobID].dstFlushed) return (cSize - mtctx->jobs[wJobID].dstFlushed); + if (srcSize > srcConsumed) return 1; /* current job not completely compressed */ + } + if (mtctx->doneJobID < mtctx->nextJobID) return 1; /* some more jobs ongoing */ + if (mtctx->jobReady) return 1; /* one job is ready to push, just not yet in the list */ + if (mtctx->inBuff.filled > 0) return 1; /* input is not empty, and still needs to be converted into a job */ + mtctx->allJobsCompleted = mtctx->frameEnded; /* all jobs are entirely flushed => if this one is last one, frame is completed */ + if (end == ZSTD_e_end) return !mtctx->frameEnded; /* for ZSTD_e_end, question becomes : is frame completed ? instead of : are internal buffers fully flushed ? */ + return 0; /* internal buffers fully flushed */ +} + +/** + * Returns the range of data used by the earliest job that is not yet complete. + * If the data of the first job is broken up into two segments, we cover both + * sections. + */ +static range_t ZSTDMT_getInputDataInUse(ZSTDMT_CCtx* mtctx) +{ + unsigned const firstJobID = mtctx->doneJobID; + unsigned const lastJobID = mtctx->nextJobID; + unsigned jobID; + + for (jobID = firstJobID; jobID < lastJobID; ++jobID) { + unsigned const wJobID = jobID & mtctx->jobIDMask; + size_t consumed; + + ZSTD_PTHREAD_MUTEX_LOCK(&mtctx->jobs[wJobID].job_mutex); + consumed = mtctx->jobs[wJobID].consumed; + ZSTD_pthread_mutex_unlock(&mtctx->jobs[wJobID].job_mutex); + + if (consumed < mtctx->jobs[wJobID].src.size) { + range_t range = mtctx->jobs[wJobID].prefix; + if (range.size == 0) { + /* Empty prefix */ + range = mtctx->jobs[wJobID].src; + } + /* Job source in multiple segments not supported yet */ + assert(range.start <= mtctx->jobs[wJobID].src.start); + return range; + } + } + return kNullRange; +} + +/** + * Returns non-zero iff buffer and range overlap. + */ +static int ZSTDMT_isOverlapped(buffer_t buffer, range_t range) +{ + BYTE const* const bufferStart = (BYTE const*)buffer.start; + BYTE const* const rangeStart = (BYTE const*)range.start; + + if (rangeStart == NULL || bufferStart == NULL) + return 0; + + { + BYTE const* const bufferEnd = bufferStart + buffer.capacity; + BYTE const* const rangeEnd = rangeStart + range.size; + + /* Empty ranges cannot overlap */ + if (bufferStart == bufferEnd || rangeStart == rangeEnd) + return 0; + + return bufferStart < rangeEnd && rangeStart < bufferEnd; + } +} + +static int ZSTDMT_doesOverlapWindow(buffer_t buffer, ZSTD_window_t window) +{ + range_t extDict; + range_t prefix; + + DEBUGLOG(5, "ZSTDMT_doesOverlapWindow"); + extDict.start = window.dictBase + window.lowLimit; + extDict.size = window.dictLimit - window.lowLimit; + + prefix.start = window.base + window.dictLimit; + prefix.size = window.nextSrc - (window.base + window.dictLimit); + DEBUGLOG(5, "extDict [0x%zx, 0x%zx)", + (size_t)extDict.start, + (size_t)extDict.start + extDict.size); + DEBUGLOG(5, "prefix [0x%zx, 0x%zx)", + (size_t)prefix.start, + (size_t)prefix.start + prefix.size); + + return ZSTDMT_isOverlapped(buffer, extDict) + || ZSTDMT_isOverlapped(buffer, prefix); +} + +static void ZSTDMT_waitForLdmComplete(ZSTDMT_CCtx* mtctx, buffer_t buffer) +{ + if (mtctx->params.ldmParams.enableLdm == ZSTD_ps_enable) { + ZSTD_pthread_mutex_t* mutex = &mtctx->serial.ldmWindowMutex; + DEBUGLOG(5, "ZSTDMT_waitForLdmComplete"); + DEBUGLOG(5, "source [0x%zx, 0x%zx)", + (size_t)buffer.start, + (size_t)buffer.start + buffer.capacity); + ZSTD_PTHREAD_MUTEX_LOCK(mutex); + while (ZSTDMT_doesOverlapWindow(buffer, mtctx->serial.ldmWindow)) { + DEBUGLOG(5, "Waiting for LDM to finish..."); + ZSTD_pthread_cond_wait(&mtctx->serial.ldmWindowCond, mutex); + } + DEBUGLOG(6, "Done waiting for LDM to finish"); + ZSTD_pthread_mutex_unlock(mutex); + } +} + +/** + * Attempts to set the inBuff to the next section to fill. + * If any part of the new section is still in use we give up. + * Returns non-zero if the buffer is filled. + */ +static int ZSTDMT_tryGetInputRange(ZSTDMT_CCtx* mtctx) +{ + range_t const inUse = ZSTDMT_getInputDataInUse(mtctx); + size_t const spaceLeft = mtctx->roundBuff.capacity - mtctx->roundBuff.pos; + size_t const target = mtctx->targetSectionSize; + buffer_t buffer; + + DEBUGLOG(5, "ZSTDMT_tryGetInputRange"); + assert(mtctx->inBuff.buffer.start == NULL); + assert(mtctx->roundBuff.capacity >= target); + + if (spaceLeft < target) { + /* ZSTD_invalidateRepCodes() doesn't work for extDict variants. + * Simply copy the prefix to the beginning in that case. + */ + BYTE* const start = (BYTE*)mtctx->roundBuff.buffer; + size_t const prefixSize = mtctx->inBuff.prefix.size; + + buffer.start = start; + buffer.capacity = prefixSize; + if (ZSTDMT_isOverlapped(buffer, inUse)) { + DEBUGLOG(5, "Waiting for buffer..."); + return 0; + } + ZSTDMT_waitForLdmComplete(mtctx, buffer); + ZSTD_memmove(start, mtctx->inBuff.prefix.start, prefixSize); + mtctx->inBuff.prefix.start = start; + mtctx->roundBuff.pos = prefixSize; + } + buffer.start = mtctx->roundBuff.buffer + mtctx->roundBuff.pos; + buffer.capacity = target; + + if (ZSTDMT_isOverlapped(buffer, inUse)) { + DEBUGLOG(5, "Waiting for buffer..."); + return 0; + } + assert(!ZSTDMT_isOverlapped(buffer, mtctx->inBuff.prefix)); + + ZSTDMT_waitForLdmComplete(mtctx, buffer); + + DEBUGLOG(5, "Using prefix range [%zx, %zx)", + (size_t)mtctx->inBuff.prefix.start, + (size_t)mtctx->inBuff.prefix.start + mtctx->inBuff.prefix.size); + DEBUGLOG(5, "Using source range [%zx, %zx)", + (size_t)buffer.start, + (size_t)buffer.start + buffer.capacity); + + + mtctx->inBuff.buffer = buffer; + mtctx->inBuff.filled = 0; + assert(mtctx->roundBuff.pos + buffer.capacity <= mtctx->roundBuff.capacity); + return 1; +} + +typedef struct { + size_t toLoad; /* The number of bytes to load from the input. */ + int flush; /* Boolean declaring if we must flush because we found a synchronization point. */ +} syncPoint_t; + +/** + * Searches through the input for a synchronization point. If one is found, we + * will instruct the caller to flush, and return the number of bytes to load. + * Otherwise, we will load as many bytes as possible and instruct the caller + * to continue as normal. + */ +static syncPoint_t +findSynchronizationPoint(ZSTDMT_CCtx const* mtctx, ZSTD_inBuffer const input) +{ + BYTE const* const istart = (BYTE const*)input.src + input.pos; + U64 const primePower = mtctx->rsync.primePower; + U64 const hitMask = mtctx->rsync.hitMask; + + syncPoint_t syncPoint; + U64 hash; + BYTE const* prev; + size_t pos; + + syncPoint.toLoad = MIN(input.size - input.pos, mtctx->targetSectionSize - mtctx->inBuff.filled); + syncPoint.flush = 0; + if (!mtctx->params.rsyncable) + /* Rsync is disabled. */ + return syncPoint; + if (mtctx->inBuff.filled + input.size - input.pos < RSYNC_MIN_BLOCK_SIZE) + /* We don't emit synchronization points if it would produce too small blocks. + * We don't have enough input to find a synchronization point, so don't look. + */ + return syncPoint; + if (mtctx->inBuff.filled + syncPoint.toLoad < RSYNC_LENGTH) + /* Not enough to compute the hash. + * We will miss any synchronization points in this RSYNC_LENGTH byte + * window. However, since it depends only in the internal buffers, if the + * state is already synchronized, we will remain synchronized. + * Additionally, the probability that we miss a synchronization point is + * low: RSYNC_LENGTH / targetSectionSize. + */ + return syncPoint; + /* Initialize the loop variables. */ + if (mtctx->inBuff.filled < RSYNC_MIN_BLOCK_SIZE) { + /* We don't need to scan the first RSYNC_MIN_BLOCK_SIZE positions + * because they can't possibly be a sync point. So we can start + * part way through the input buffer. + */ + pos = RSYNC_MIN_BLOCK_SIZE - mtctx->inBuff.filled; + if (pos >= RSYNC_LENGTH) { + prev = istart + pos - RSYNC_LENGTH; + hash = ZSTD_rollingHash_compute(prev, RSYNC_LENGTH); + } else { + assert(mtctx->inBuff.filled >= RSYNC_LENGTH); + prev = (BYTE const*)mtctx->inBuff.buffer.start + mtctx->inBuff.filled - RSYNC_LENGTH; + hash = ZSTD_rollingHash_compute(prev + pos, (RSYNC_LENGTH - pos)); + hash = ZSTD_rollingHash_append(hash, istart, pos); + } + } else { + /* We have enough bytes buffered to initialize the hash, + * and have processed enough bytes to find a sync point. + * Start scanning at the beginning of the input. + */ + assert(mtctx->inBuff.filled >= RSYNC_MIN_BLOCK_SIZE); + assert(RSYNC_MIN_BLOCK_SIZE >= RSYNC_LENGTH); + pos = 0; + prev = (BYTE const*)mtctx->inBuff.buffer.start + mtctx->inBuff.filled - RSYNC_LENGTH; + hash = ZSTD_rollingHash_compute(prev, RSYNC_LENGTH); + if ((hash & hitMask) == hitMask) { + /* We're already at a sync point so don't load any more until + * we're able to flush this sync point. + * This likely happened because the job table was full so we + * couldn't add our job. + */ + syncPoint.toLoad = 0; + syncPoint.flush = 1; + return syncPoint; + } + } + /* Starting with the hash of the previous RSYNC_LENGTH bytes, roll + * through the input. If we hit a synchronization point, then cut the + * job off, and tell the compressor to flush the job. Otherwise, load + * all the bytes and continue as normal. + * If we go too long without a synchronization point (targetSectionSize) + * then a block will be emitted anyways, but this is okay, since if we + * are already synchronized we will remain synchronized. + */ + assert(pos < RSYNC_LENGTH || ZSTD_rollingHash_compute(istart + pos - RSYNC_LENGTH, RSYNC_LENGTH) == hash); + for (; pos < syncPoint.toLoad; ++pos) { + BYTE const toRemove = pos < RSYNC_LENGTH ? prev[pos] : istart[pos - RSYNC_LENGTH]; + /* This assert is very expensive, and Debian compiles with asserts enabled. + * So disable it for now. We can get similar coverage by checking it at the + * beginning & end of the loop. + * assert(pos < RSYNC_LENGTH || ZSTD_rollingHash_compute(istart + pos - RSYNC_LENGTH, RSYNC_LENGTH) == hash); + */ + hash = ZSTD_rollingHash_rotate(hash, toRemove, istart[pos], primePower); + assert(mtctx->inBuff.filled + pos >= RSYNC_MIN_BLOCK_SIZE); + if ((hash & hitMask) == hitMask) { + syncPoint.toLoad = pos + 1; + syncPoint.flush = 1; + ++pos; /* for assert */ + break; + } + } + assert(pos < RSYNC_LENGTH || ZSTD_rollingHash_compute(istart + pos - RSYNC_LENGTH, RSYNC_LENGTH) == hash); + return syncPoint; +} + +size_t ZSTDMT_nextInputSizeHint(const ZSTDMT_CCtx* mtctx) +{ + size_t hintInSize = mtctx->targetSectionSize - mtctx->inBuff.filled; + if (hintInSize==0) hintInSize = mtctx->targetSectionSize; + return hintInSize; +} + +/** ZSTDMT_compressStream_generic() : + * internal use only - exposed to be invoked from zstd_compress.c + * assumption : output and input are valid (pos <= size) + * @return : minimum amount of data remaining to flush, 0 if none */ +size_t ZSTDMT_compressStream_generic(ZSTDMT_CCtx* mtctx, + ZSTD_outBuffer* output, + ZSTD_inBuffer* input, + ZSTD_EndDirective endOp) +{ + unsigned forwardInputProgress = 0; + DEBUGLOG(5, "ZSTDMT_compressStream_generic (endOp=%u, srcSize=%u)", + (U32)endOp, (U32)(input->size - input->pos)); + assert(output->pos <= output->size); + assert(input->pos <= input->size); + + if ((mtctx->frameEnded) && (endOp==ZSTD_e_continue)) { + /* current frame being ended. Only flush/end are allowed */ + return ERROR(stage_wrong); + } + + /* fill input buffer */ + if ( (!mtctx->jobReady) + && (input->size > input->pos) ) { /* support NULL input */ + if (mtctx->inBuff.buffer.start == NULL) { + assert(mtctx->inBuff.filled == 0); /* Can't fill an empty buffer */ + if (!ZSTDMT_tryGetInputRange(mtctx)) { + /* It is only possible for this operation to fail if there are + * still compression jobs ongoing. + */ + DEBUGLOG(5, "ZSTDMT_tryGetInputRange failed"); + assert(mtctx->doneJobID != mtctx->nextJobID); + } else + DEBUGLOG(5, "ZSTDMT_tryGetInputRange completed successfully : mtctx->inBuff.buffer.start = %p", mtctx->inBuff.buffer.start); + } + if (mtctx->inBuff.buffer.start != NULL) { + syncPoint_t const syncPoint = findSynchronizationPoint(mtctx, *input); + if (syncPoint.flush && endOp == ZSTD_e_continue) { + endOp = ZSTD_e_flush; + } + assert(mtctx->inBuff.buffer.capacity >= mtctx->targetSectionSize); + DEBUGLOG(5, "ZSTDMT_compressStream_generic: adding %u bytes on top of %u to buffer of size %u", + (U32)syncPoint.toLoad, (U32)mtctx->inBuff.filled, (U32)mtctx->targetSectionSize); + ZSTD_memcpy((char*)mtctx->inBuff.buffer.start + mtctx->inBuff.filled, (const char*)input->src + input->pos, syncPoint.toLoad); + input->pos += syncPoint.toLoad; + mtctx->inBuff.filled += syncPoint.toLoad; + forwardInputProgress = syncPoint.toLoad>0; + } + } + if ((input->pos < input->size) && (endOp == ZSTD_e_end)) { + /* Can't end yet because the input is not fully consumed. + * We are in one of these cases: + * - mtctx->inBuff is NULL & empty: we couldn't get an input buffer so don't create a new job. + * - We filled the input buffer: flush this job but don't end the frame. + * - We hit a synchronization point: flush this job but don't end the frame. + */ + assert(mtctx->inBuff.filled == 0 || mtctx->inBuff.filled == mtctx->targetSectionSize || mtctx->params.rsyncable); + endOp = ZSTD_e_flush; + } + + if ( (mtctx->jobReady) + || (mtctx->inBuff.filled >= mtctx->targetSectionSize) /* filled enough : let's compress */ + || ((endOp != ZSTD_e_continue) && (mtctx->inBuff.filled > 0)) /* something to flush : let's go */ + || ((endOp == ZSTD_e_end) && (!mtctx->frameEnded)) ) { /* must finish the frame with a zero-size block */ + size_t const jobSize = mtctx->inBuff.filled; + assert(mtctx->inBuff.filled <= mtctx->targetSectionSize); + FORWARD_IF_ERROR( ZSTDMT_createCompressionJob(mtctx, jobSize, endOp) , ""); + } + + /* check for potential compressed data ready to be flushed */ + { size_t const remainingToFlush = ZSTDMT_flushProduced(mtctx, output, !forwardInputProgress, endOp); /* block if there was no forward input progress */ + if (input->pos < input->size) return MAX(remainingToFlush, 1); /* input not consumed : do not end flush yet */ + DEBUGLOG(5, "end of ZSTDMT_compressStream_generic: remainingToFlush = %u", (U32)remainingToFlush); + return remainingToFlush; + } +} +/**** ended inlining compress/zstdmt_compress.c ****/ +#endif + +/**** start inlining decompress/huf_decompress.c ****/ +/* ****************************************************************** + * huff0 huffman decoder, + * part of Finite State Entropy library + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * You can contact the author at : + * - FSE+HUF source repository : https://github.com/Cyan4973/FiniteStateEntropy + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. +****************************************************************** */ + +/* ************************************************************** +* Dependencies +****************************************************************/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../common/compiler.h ****/ +/**** skipping file: ../common/bitstream.h ****/ +/**** skipping file: ../common/fse.h ****/ +/**** skipping file: ../common/huf.h ****/ +/**** skipping file: ../common/error_private.h ****/ +/**** skipping file: ../common/zstd_internal.h ****/ +/**** skipping file: ../common/bits.h ****/ + +/* ************************************************************** +* Constants +****************************************************************/ + +#define HUF_DECODER_FAST_TABLELOG 11 + +/* ************************************************************** +* Macros +****************************************************************/ + +#ifdef HUF_DISABLE_FAST_DECODE +# define HUF_ENABLE_FAST_DECODE 0 +#else +# define HUF_ENABLE_FAST_DECODE 1 +#endif + +/* These two optional macros force the use one way or another of the two + * Huffman decompression implementations. You can't force in both directions + * at the same time. + */ +#if defined(HUF_FORCE_DECOMPRESS_X1) && \ + defined(HUF_FORCE_DECOMPRESS_X2) +#error "Cannot force the use of the X1 and X2 decoders at the same time!" +#endif + +/* When DYNAMIC_BMI2 is enabled, fast decoders are only called when bmi2 is + * supported at runtime, so we can add the BMI2 target attribute. + * When it is disabled, we will still get BMI2 if it is enabled statically. + */ +#if DYNAMIC_BMI2 +# define HUF_FAST_BMI2_ATTRS BMI2_TARGET_ATTRIBUTE +#else +# define HUF_FAST_BMI2_ATTRS +#endif + +#ifdef __cplusplus +# define HUF_EXTERN_C extern "C" +#else +# define HUF_EXTERN_C +#endif +#define HUF_ASM_DECL HUF_EXTERN_C + +#if DYNAMIC_BMI2 +# define HUF_NEED_BMI2_FUNCTION 1 +#else +# define HUF_NEED_BMI2_FUNCTION 0 +#endif + +/* ************************************************************** +* Error Management +****************************************************************/ +#define HUF_isError ERR_isError + + +/* ************************************************************** +* Byte alignment for workSpace management +****************************************************************/ +#define HUF_ALIGN(x, a) HUF_ALIGN_MASK((x), (a) - 1) +#define HUF_ALIGN_MASK(x, mask) (((x) + (mask)) & ~(mask)) + + +/* ************************************************************** +* BMI2 Variant Wrappers +****************************************************************/ +typedef size_t (*HUF_DecompressUsingDTableFn)(void *dst, size_t dstSize, + const void *cSrc, + size_t cSrcSize, + const HUF_DTable *DTable); + +#if DYNAMIC_BMI2 + +#define HUF_DGEN(fn) \ + \ + static size_t fn##_default( \ + void* dst, size_t dstSize, \ + const void* cSrc, size_t cSrcSize, \ + const HUF_DTable* DTable) \ + { \ + return fn##_body(dst, dstSize, cSrc, cSrcSize, DTable); \ + } \ + \ + static BMI2_TARGET_ATTRIBUTE size_t fn##_bmi2( \ + void* dst, size_t dstSize, \ + const void* cSrc, size_t cSrcSize, \ + const HUF_DTable* DTable) \ + { \ + return fn##_body(dst, dstSize, cSrc, cSrcSize, DTable); \ + } \ + \ + static size_t fn(void* dst, size_t dstSize, void const* cSrc, \ + size_t cSrcSize, HUF_DTable const* DTable, int flags) \ + { \ + if (flags & HUF_flags_bmi2) { \ + return fn##_bmi2(dst, dstSize, cSrc, cSrcSize, DTable); \ + } \ + return fn##_default(dst, dstSize, cSrc, cSrcSize, DTable); \ + } + +#else + +#define HUF_DGEN(fn) \ + static size_t fn(void* dst, size_t dstSize, void const* cSrc, \ + size_t cSrcSize, HUF_DTable const* DTable, int flags) \ + { \ + (void)flags; \ + return fn##_body(dst, dstSize, cSrc, cSrcSize, DTable); \ + } + +#endif + + +/*-***************************/ +/* generic DTableDesc */ +/*-***************************/ +typedef struct { BYTE maxTableLog; BYTE tableType; BYTE tableLog; BYTE reserved; } DTableDesc; + +static DTableDesc HUF_getDTableDesc(const HUF_DTable* table) +{ + DTableDesc dtd; + ZSTD_memcpy(&dtd, table, sizeof(dtd)); + return dtd; +} + +static size_t HUF_initFastDStream(BYTE const* ip) { + BYTE const lastByte = ip[7]; + size_t const bitsConsumed = lastByte ? 8 - ZSTD_highbit32(lastByte) : 0; + size_t const value = MEM_readLEST(ip) | 1; + assert(bitsConsumed <= 8); + assert(sizeof(size_t) == 8); + return value << bitsConsumed; +} + + +/** + * The input/output arguments to the Huffman fast decoding loop: + * + * ip [in/out] - The input pointers, must be updated to reflect what is consumed. + * op [in/out] - The output pointers, must be updated to reflect what is written. + * bits [in/out] - The bitstream containers, must be updated to reflect the current state. + * dt [in] - The decoding table. + * ilowest [in] - The beginning of the valid range of the input. Decoders may read + * down to this pointer. It may be below iend[0]. + * oend [in] - The end of the output stream. op[3] must not cross oend. + * iend [in] - The end of each input stream. ip[i] may cross iend[i], + * as long as it is above ilowest, but that indicates corruption. + */ +typedef struct { + BYTE const* ip[4]; + BYTE* op[4]; + U64 bits[4]; + void const* dt; + BYTE const* ilowest; + BYTE* oend; + BYTE const* iend[4]; +} HUF_DecompressFastArgs; + +typedef void (*HUF_DecompressFastLoopFn)(HUF_DecompressFastArgs*); + +/** + * Initializes args for the fast decoding loop. + * @returns 1 on success + * 0 if the fallback implementation should be used. + * Or an error code on failure. + */ +static size_t HUF_DecompressFastArgs_init(HUF_DecompressFastArgs* args, void* dst, size_t dstSize, void const* src, size_t srcSize, const HUF_DTable* DTable) +{ + void const* dt = DTable + 1; + U32 const dtLog = HUF_getDTableDesc(DTable).tableLog; + + const BYTE* const istart = (const BYTE*)src; + + BYTE* const oend = ZSTD_maybeNullPtrAdd((BYTE*)dst, dstSize); + + /* The fast decoding loop assumes 64-bit little-endian. + * This condition is false on x32. + */ + if (!MEM_isLittleEndian() || MEM_32bits()) + return 0; + + /* Avoid nullptr addition */ + if (dstSize == 0) + return 0; + assert(dst != NULL); + + /* strict minimum : jump table + 1 byte per stream */ + if (srcSize < 10) + return ERROR(corruption_detected); + + /* Must have at least 8 bytes per stream because we don't handle initializing smaller bit containers. + * If table log is not correct at this point, fallback to the old decoder. + * On small inputs we don't have enough data to trigger the fast loop, so use the old decoder. + */ + if (dtLog != HUF_DECODER_FAST_TABLELOG) + return 0; + + /* Read the jump table. */ + { + size_t const length1 = MEM_readLE16(istart); + size_t const length2 = MEM_readLE16(istart+2); + size_t const length3 = MEM_readLE16(istart+4); + size_t const length4 = srcSize - (length1 + length2 + length3 + 6); + args->iend[0] = istart + 6; /* jumpTable */ + args->iend[1] = args->iend[0] + length1; + args->iend[2] = args->iend[1] + length2; + args->iend[3] = args->iend[2] + length3; + + /* HUF_initFastDStream() requires this, and this small of an input + * won't benefit from the ASM loop anyways. + */ + if (length1 < 8 || length2 < 8 || length3 < 8 || length4 < 8) + return 0; + if (length4 > srcSize) return ERROR(corruption_detected); /* overflow */ + } + /* ip[] contains the position that is currently loaded into bits[]. */ + args->ip[0] = args->iend[1] - sizeof(U64); + args->ip[1] = args->iend[2] - sizeof(U64); + args->ip[2] = args->iend[3] - sizeof(U64); + args->ip[3] = (BYTE const*)src + srcSize - sizeof(U64); + + /* op[] contains the output pointers. */ + args->op[0] = (BYTE*)dst; + args->op[1] = args->op[0] + (dstSize+3)/4; + args->op[2] = args->op[1] + (dstSize+3)/4; + args->op[3] = args->op[2] + (dstSize+3)/4; + + /* No point to call the ASM loop for tiny outputs. */ + if (args->op[3] >= oend) + return 0; + + /* bits[] is the bit container. + * It is read from the MSB down to the LSB. + * It is shifted left as it is read, and zeros are + * shifted in. After the lowest valid bit a 1 is + * set, so that CountTrailingZeros(bits[]) can be used + * to count how many bits we've consumed. + */ + args->bits[0] = HUF_initFastDStream(args->ip[0]); + args->bits[1] = HUF_initFastDStream(args->ip[1]); + args->bits[2] = HUF_initFastDStream(args->ip[2]); + args->bits[3] = HUF_initFastDStream(args->ip[3]); + + /* The decoders must be sure to never read beyond ilowest. + * This is lower than iend[0], but allowing decoders to read + * down to ilowest can allow an extra iteration or two in the + * fast loop. + */ + args->ilowest = istart; + + args->oend = oend; + args->dt = dt; + + return 1; +} + +static size_t HUF_initRemainingDStream(BIT_DStream_t* bit, HUF_DecompressFastArgs const* args, int stream, BYTE* segmentEnd) +{ + /* Validate that we haven't overwritten. */ + if (args->op[stream] > segmentEnd) + return ERROR(corruption_detected); + /* Validate that we haven't read beyond iend[]. + * Note that ip[] may be < iend[] because the MSB is + * the next bit to read, and we may have consumed 100% + * of the stream, so down to iend[i] - 8 is valid. + */ + if (args->ip[stream] < args->iend[stream] - 8) + return ERROR(corruption_detected); + + /* Construct the BIT_DStream_t. */ + assert(sizeof(size_t) == 8); + bit->bitContainer = MEM_readLEST(args->ip[stream]); + bit->bitsConsumed = ZSTD_countTrailingZeros64(args->bits[stream]); + bit->start = (const char*)args->ilowest; + bit->limitPtr = bit->start + sizeof(size_t); + bit->ptr = (const char*)args->ip[stream]; + + return 0; +} + +/* Calls X(N) for each stream 0, 1, 2, 3. */ +#define HUF_4X_FOR_EACH_STREAM(X) \ + do { \ + X(0); \ + X(1); \ + X(2); \ + X(3); \ + } while (0) + +/* Calls X(N, var) for each stream 0, 1, 2, 3. */ +#define HUF_4X_FOR_EACH_STREAM_WITH_VAR(X, var) \ + do { \ + X(0, (var)); \ + X(1, (var)); \ + X(2, (var)); \ + X(3, (var)); \ + } while (0) + + +#ifndef HUF_FORCE_DECOMPRESS_X2 + +/*-***************************/ +/* single-symbol decoding */ +/*-***************************/ +typedef struct { BYTE nbBits; BYTE byte; } HUF_DEltX1; /* single-symbol decoding */ + +/** + * Packs 4 HUF_DEltX1 structs into a U64. This is used to lay down 4 entries at + * a time. + */ +static U64 HUF_DEltX1_set4(BYTE symbol, BYTE nbBits) { + U64 D4; + if (MEM_isLittleEndian()) { + D4 = (U64)((symbol << 8) + nbBits); + } else { + D4 = (U64)(symbol + (nbBits << 8)); + } + assert(D4 < (1U << 16)); + D4 *= 0x0001000100010001ULL; + return D4; +} + +/** + * Increase the tableLog to targetTableLog and rescales the stats. + * If tableLog > targetTableLog this is a no-op. + * @returns New tableLog + */ +static U32 HUF_rescaleStats(BYTE* huffWeight, U32* rankVal, U32 nbSymbols, U32 tableLog, U32 targetTableLog) +{ + if (tableLog > targetTableLog) + return tableLog; + if (tableLog < targetTableLog) { + U32 const scale = targetTableLog - tableLog; + U32 s; + /* Increase the weight for all non-zero probability symbols by scale. */ + for (s = 0; s < nbSymbols; ++s) { + huffWeight[s] += (BYTE)((huffWeight[s] == 0) ? 0 : scale); + } + /* Update rankVal to reflect the new weights. + * All weights except 0 get moved to weight + scale. + * Weights [1, scale] are empty. + */ + for (s = targetTableLog; s > scale; --s) { + rankVal[s] = rankVal[s - scale]; + } + for (s = scale; s > 0; --s) { + rankVal[s] = 0; + } + } + return targetTableLog; +} + +typedef struct { + U32 rankVal[HUF_TABLELOG_ABSOLUTEMAX + 1]; + U32 rankStart[HUF_TABLELOG_ABSOLUTEMAX + 1]; + U32 statsWksp[HUF_READ_STATS_WORKSPACE_SIZE_U32]; + BYTE symbols[HUF_SYMBOLVALUE_MAX + 1]; + BYTE huffWeight[HUF_SYMBOLVALUE_MAX + 1]; +} HUF_ReadDTableX1_Workspace; + +size_t HUF_readDTableX1_wksp(HUF_DTable* DTable, const void* src, size_t srcSize, void* workSpace, size_t wkspSize, int flags) +{ + U32 tableLog = 0; + U32 nbSymbols = 0; + size_t iSize; + void* const dtPtr = DTable + 1; + HUF_DEltX1* const dt = (HUF_DEltX1*)dtPtr; + HUF_ReadDTableX1_Workspace* wksp = (HUF_ReadDTableX1_Workspace*)workSpace; + + DEBUG_STATIC_ASSERT(HUF_DECOMPRESS_WORKSPACE_SIZE >= sizeof(*wksp)); + if (sizeof(*wksp) > wkspSize) return ERROR(tableLog_tooLarge); + + DEBUG_STATIC_ASSERT(sizeof(DTableDesc) == sizeof(HUF_DTable)); + /* ZSTD_memset(huffWeight, 0, sizeof(huffWeight)); */ /* is not necessary, even though some analyzer complain ... */ + + iSize = HUF_readStats_wksp(wksp->huffWeight, HUF_SYMBOLVALUE_MAX + 1, wksp->rankVal, &nbSymbols, &tableLog, src, srcSize, wksp->statsWksp, sizeof(wksp->statsWksp), flags); + if (HUF_isError(iSize)) return iSize; + + + /* Table header */ + { DTableDesc dtd = HUF_getDTableDesc(DTable); + U32 const maxTableLog = dtd.maxTableLog + 1; + U32 const targetTableLog = MIN(maxTableLog, HUF_DECODER_FAST_TABLELOG); + tableLog = HUF_rescaleStats(wksp->huffWeight, wksp->rankVal, nbSymbols, tableLog, targetTableLog); + if (tableLog > (U32)(dtd.maxTableLog+1)) return ERROR(tableLog_tooLarge); /* DTable too small, Huffman tree cannot fit in */ + dtd.tableType = 0; + dtd.tableLog = (BYTE)tableLog; + ZSTD_memcpy(DTable, &dtd, sizeof(dtd)); + } + + /* Compute symbols and rankStart given rankVal: + * + * rankVal already contains the number of values of each weight. + * + * symbols contains the symbols ordered by weight. First are the rankVal[0] + * weight 0 symbols, followed by the rankVal[1] weight 1 symbols, and so on. + * symbols[0] is filled (but unused) to avoid a branch. + * + * rankStart contains the offset where each rank belongs in the DTable. + * rankStart[0] is not filled because there are no entries in the table for + * weight 0. + */ + { int n; + U32 nextRankStart = 0; + int const unroll = 4; + int const nLimit = (int)nbSymbols - unroll + 1; + for (n=0; n<(int)tableLog+1; n++) { + U32 const curr = nextRankStart; + nextRankStart += wksp->rankVal[n]; + wksp->rankStart[n] = curr; + } + for (n=0; n < nLimit; n += unroll) { + int u; + for (u=0; u < unroll; ++u) { + size_t const w = wksp->huffWeight[n+u]; + wksp->symbols[wksp->rankStart[w]++] = (BYTE)(n+u); + } + } + for (; n < (int)nbSymbols; ++n) { + size_t const w = wksp->huffWeight[n]; + wksp->symbols[wksp->rankStart[w]++] = (BYTE)n; + } + } + + /* fill DTable + * We fill all entries of each weight in order. + * That way length is a constant for each iteration of the outer loop. + * We can switch based on the length to a different inner loop which is + * optimized for that particular case. + */ + { U32 w; + int symbol = wksp->rankVal[0]; + int rankStart = 0; + for (w=1; wrankVal[w]; + int const length = (1 << w) >> 1; + int uStart = rankStart; + BYTE const nbBits = (BYTE)(tableLog + 1 - w); + int s; + int u; + switch (length) { + case 1: + for (s=0; ssymbols[symbol + s]; + D.nbBits = nbBits; + dt[uStart] = D; + uStart += 1; + } + break; + case 2: + for (s=0; ssymbols[symbol + s]; + D.nbBits = nbBits; + dt[uStart+0] = D; + dt[uStart+1] = D; + uStart += 2; + } + break; + case 4: + for (s=0; ssymbols[symbol + s], nbBits); + MEM_write64(dt + uStart, D4); + uStart += 4; + } + break; + case 8: + for (s=0; ssymbols[symbol + s], nbBits); + MEM_write64(dt + uStart, D4); + MEM_write64(dt + uStart + 4, D4); + uStart += 8; + } + break; + default: + for (s=0; ssymbols[symbol + s], nbBits); + for (u=0; u < length; u += 16) { + MEM_write64(dt + uStart + u + 0, D4); + MEM_write64(dt + uStart + u + 4, D4); + MEM_write64(dt + uStart + u + 8, D4); + MEM_write64(dt + uStart + u + 12, D4); + } + assert(u == length); + uStart += length; + } + break; + } + symbol += symbolCount; + rankStart += symbolCount * length; + } + } + return iSize; +} + +FORCE_INLINE_TEMPLATE BYTE +HUF_decodeSymbolX1(BIT_DStream_t* Dstream, const HUF_DEltX1* dt, const U32 dtLog) +{ + size_t const val = BIT_lookBitsFast(Dstream, dtLog); /* note : dtLog >= 1 */ + BYTE const c = dt[val].byte; + BIT_skipBits(Dstream, dt[val].nbBits); + return c; +} + +#define HUF_DECODE_SYMBOLX1_0(ptr, DStreamPtr) \ + do { *ptr++ = HUF_decodeSymbolX1(DStreamPtr, dt, dtLog); } while (0) + +#define HUF_DECODE_SYMBOLX1_1(ptr, DStreamPtr) \ + do { \ + if (MEM_64bits() || (HUF_TABLELOG_MAX<=12)) \ + HUF_DECODE_SYMBOLX1_0(ptr, DStreamPtr); \ + } while (0) + +#define HUF_DECODE_SYMBOLX1_2(ptr, DStreamPtr) \ + do { \ + if (MEM_64bits()) \ + HUF_DECODE_SYMBOLX1_0(ptr, DStreamPtr); \ + } while (0) + +HINT_INLINE size_t +HUF_decodeStreamX1(BYTE* p, BIT_DStream_t* const bitDPtr, BYTE* const pEnd, const HUF_DEltX1* const dt, const U32 dtLog) +{ + BYTE* const pStart = p; + + /* up to 4 symbols at a time */ + if ((pEnd - p) > 3) { + while ((BIT_reloadDStream(bitDPtr) == BIT_DStream_unfinished) & (p < pEnd-3)) { + HUF_DECODE_SYMBOLX1_2(p, bitDPtr); + HUF_DECODE_SYMBOLX1_1(p, bitDPtr); + HUF_DECODE_SYMBOLX1_2(p, bitDPtr); + HUF_DECODE_SYMBOLX1_0(p, bitDPtr); + } + } else { + BIT_reloadDStream(bitDPtr); + } + + /* [0-3] symbols remaining */ + if (MEM_32bits()) + while ((BIT_reloadDStream(bitDPtr) == BIT_DStream_unfinished) & (p < pEnd)) + HUF_DECODE_SYMBOLX1_0(p, bitDPtr); + + /* no more data to retrieve from bitstream, no need to reload */ + while (p < pEnd) + HUF_DECODE_SYMBOLX1_0(p, bitDPtr); + + return (size_t)(pEnd-pStart); +} + +FORCE_INLINE_TEMPLATE size_t +HUF_decompress1X1_usingDTable_internal_body( + void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + const HUF_DTable* DTable) +{ + BYTE* op = (BYTE*)dst; + BYTE* const oend = ZSTD_maybeNullPtrAdd(op, dstSize); + const void* dtPtr = DTable + 1; + const HUF_DEltX1* const dt = (const HUF_DEltX1*)dtPtr; + BIT_DStream_t bitD; + DTableDesc const dtd = HUF_getDTableDesc(DTable); + U32 const dtLog = dtd.tableLog; + + CHECK_F( BIT_initDStream(&bitD, cSrc, cSrcSize) ); + + HUF_decodeStreamX1(op, &bitD, oend, dt, dtLog); + + if (!BIT_endOfDStream(&bitD)) return ERROR(corruption_detected); + + return dstSize; +} + +/* HUF_decompress4X1_usingDTable_internal_body(): + * Conditions : + * @dstSize >= 6 + */ +FORCE_INLINE_TEMPLATE size_t +HUF_decompress4X1_usingDTable_internal_body( + void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + const HUF_DTable* DTable) +{ + /* Check */ + if (cSrcSize < 10) return ERROR(corruption_detected); /* strict minimum : jump table + 1 byte per stream */ + if (dstSize < 6) return ERROR(corruption_detected); /* stream 4-split doesn't work */ + + { const BYTE* const istart = (const BYTE*) cSrc; + BYTE* const ostart = (BYTE*) dst; + BYTE* const oend = ostart + dstSize; + BYTE* const olimit = oend - 3; + const void* const dtPtr = DTable + 1; + const HUF_DEltX1* const dt = (const HUF_DEltX1*)dtPtr; + + /* Init */ + BIT_DStream_t bitD1; + BIT_DStream_t bitD2; + BIT_DStream_t bitD3; + BIT_DStream_t bitD4; + size_t const length1 = MEM_readLE16(istart); + size_t const length2 = MEM_readLE16(istart+2); + size_t const length3 = MEM_readLE16(istart+4); + size_t const length4 = cSrcSize - (length1 + length2 + length3 + 6); + const BYTE* const istart1 = istart + 6; /* jumpTable */ + const BYTE* const istart2 = istart1 + length1; + const BYTE* const istart3 = istart2 + length2; + const BYTE* const istart4 = istart3 + length3; + const size_t segmentSize = (dstSize+3) / 4; + BYTE* const opStart2 = ostart + segmentSize; + BYTE* const opStart3 = opStart2 + segmentSize; + BYTE* const opStart4 = opStart3 + segmentSize; + BYTE* op1 = ostart; + BYTE* op2 = opStart2; + BYTE* op3 = opStart3; + BYTE* op4 = opStart4; + DTableDesc const dtd = HUF_getDTableDesc(DTable); + U32 const dtLog = dtd.tableLog; + U32 endSignal = 1; + + if (length4 > cSrcSize) return ERROR(corruption_detected); /* overflow */ + if (opStart4 > oend) return ERROR(corruption_detected); /* overflow */ + assert(dstSize >= 6); /* validated above */ + CHECK_F( BIT_initDStream(&bitD1, istart1, length1) ); + CHECK_F( BIT_initDStream(&bitD2, istart2, length2) ); + CHECK_F( BIT_initDStream(&bitD3, istart3, length3) ); + CHECK_F( BIT_initDStream(&bitD4, istart4, length4) ); + + /* up to 16 symbols per loop (4 symbols per stream) in 64-bit mode */ + if ((size_t)(oend - op4) >= sizeof(size_t)) { + for ( ; (endSignal) & (op4 < olimit) ; ) { + HUF_DECODE_SYMBOLX1_2(op1, &bitD1); + HUF_DECODE_SYMBOLX1_2(op2, &bitD2); + HUF_DECODE_SYMBOLX1_2(op3, &bitD3); + HUF_DECODE_SYMBOLX1_2(op4, &bitD4); + HUF_DECODE_SYMBOLX1_1(op1, &bitD1); + HUF_DECODE_SYMBOLX1_1(op2, &bitD2); + HUF_DECODE_SYMBOLX1_1(op3, &bitD3); + HUF_DECODE_SYMBOLX1_1(op4, &bitD4); + HUF_DECODE_SYMBOLX1_2(op1, &bitD1); + HUF_DECODE_SYMBOLX1_2(op2, &bitD2); + HUF_DECODE_SYMBOLX1_2(op3, &bitD3); + HUF_DECODE_SYMBOLX1_2(op4, &bitD4); + HUF_DECODE_SYMBOLX1_0(op1, &bitD1); + HUF_DECODE_SYMBOLX1_0(op2, &bitD2); + HUF_DECODE_SYMBOLX1_0(op3, &bitD3); + HUF_DECODE_SYMBOLX1_0(op4, &bitD4); + endSignal &= BIT_reloadDStreamFast(&bitD1) == BIT_DStream_unfinished; + endSignal &= BIT_reloadDStreamFast(&bitD2) == BIT_DStream_unfinished; + endSignal &= BIT_reloadDStreamFast(&bitD3) == BIT_DStream_unfinished; + endSignal &= BIT_reloadDStreamFast(&bitD4) == BIT_DStream_unfinished; + } + } + + /* check corruption */ + /* note : should not be necessary : op# advance in lock step, and we control op4. + * but curiously, binary generated by gcc 7.2 & 7.3 with -mbmi2 runs faster when >=1 test is present */ + if (op1 > opStart2) return ERROR(corruption_detected); + if (op2 > opStart3) return ERROR(corruption_detected); + if (op3 > opStart4) return ERROR(corruption_detected); + /* note : op4 supposed already verified within main loop */ + + /* finish bitStreams one by one */ + HUF_decodeStreamX1(op1, &bitD1, opStart2, dt, dtLog); + HUF_decodeStreamX1(op2, &bitD2, opStart3, dt, dtLog); + HUF_decodeStreamX1(op3, &bitD3, opStart4, dt, dtLog); + HUF_decodeStreamX1(op4, &bitD4, oend, dt, dtLog); + + /* check */ + { U32 const endCheck = BIT_endOfDStream(&bitD1) & BIT_endOfDStream(&bitD2) & BIT_endOfDStream(&bitD3) & BIT_endOfDStream(&bitD4); + if (!endCheck) return ERROR(corruption_detected); } + + /* decoded size */ + return dstSize; + } +} + +#if HUF_NEED_BMI2_FUNCTION +static BMI2_TARGET_ATTRIBUTE +size_t HUF_decompress4X1_usingDTable_internal_bmi2(void* dst, size_t dstSize, void const* cSrc, + size_t cSrcSize, HUF_DTable const* DTable) { + return HUF_decompress4X1_usingDTable_internal_body(dst, dstSize, cSrc, cSrcSize, DTable); +} +#endif + +static +size_t HUF_decompress4X1_usingDTable_internal_default(void* dst, size_t dstSize, void const* cSrc, + size_t cSrcSize, HUF_DTable const* DTable) { + return HUF_decompress4X1_usingDTable_internal_body(dst, dstSize, cSrc, cSrcSize, DTable); +} + +#if ZSTD_ENABLE_ASM_X86_64_BMI2 + +HUF_ASM_DECL void HUF_decompress4X1_usingDTable_internal_fast_asm_loop(HUF_DecompressFastArgs* args) ZSTDLIB_HIDDEN; + +#endif + +static HUF_FAST_BMI2_ATTRS +void HUF_decompress4X1_usingDTable_internal_fast_c_loop(HUF_DecompressFastArgs* args) +{ + U64 bits[4]; + BYTE const* ip[4]; + BYTE* op[4]; + U16 const* const dtable = (U16 const*)args->dt; + BYTE* const oend = args->oend; + BYTE const* const ilowest = args->ilowest; + + /* Copy the arguments to local variables */ + ZSTD_memcpy(&bits, &args->bits, sizeof(bits)); + ZSTD_memcpy((void*)(&ip), &args->ip, sizeof(ip)); + ZSTD_memcpy(&op, &args->op, sizeof(op)); + + assert(MEM_isLittleEndian()); + assert(!MEM_32bits()); + + for (;;) { + BYTE* olimit; + int stream; + + /* Assert loop preconditions */ +#ifndef NDEBUG + for (stream = 0; stream < 4; ++stream) { + assert(op[stream] <= (stream == 3 ? oend : op[stream + 1])); + assert(ip[stream] >= ilowest); + } +#endif + /* Compute olimit */ + { + /* Each iteration produces 5 output symbols per stream */ + size_t const oiters = (size_t)(oend - op[3]) / 5; + /* Each iteration consumes up to 11 bits * 5 = 55 bits < 7 bytes + * per stream. + */ + size_t const iiters = (size_t)(ip[0] - ilowest) / 7; + /* We can safely run iters iterations before running bounds checks */ + size_t const iters = MIN(oiters, iiters); + size_t const symbols = iters * 5; + + /* We can simply check that op[3] < olimit, instead of checking all + * of our bounds, since we can't hit the other bounds until we've run + * iters iterations, which only happens when op[3] == olimit. + */ + olimit = op[3] + symbols; + + /* Exit fast decoding loop once we reach the end. */ + if (op[3] == olimit) + break; + + /* Exit the decoding loop if any input pointer has crossed the + * previous one. This indicates corruption, and a precondition + * to our loop is that ip[i] >= ip[0]. + */ + for (stream = 1; stream < 4; ++stream) { + if (ip[stream] < ip[stream - 1]) + goto _out; + } + } + +#ifndef NDEBUG + for (stream = 1; stream < 4; ++stream) { + assert(ip[stream] >= ip[stream - 1]); + } +#endif + +#define HUF_4X1_DECODE_SYMBOL(_stream, _symbol) \ + do { \ + int const index = (int)(bits[(_stream)] >> 53); \ + int const entry = (int)dtable[index]; \ + bits[(_stream)] <<= (entry & 0x3F); \ + op[(_stream)][(_symbol)] = (BYTE)((entry >> 8) & 0xFF); \ + } while (0) + +#define HUF_4X1_RELOAD_STREAM(_stream) \ + do { \ + int const ctz = ZSTD_countTrailingZeros64(bits[(_stream)]); \ + int const nbBits = ctz & 7; \ + int const nbBytes = ctz >> 3; \ + op[(_stream)] += 5; \ + ip[(_stream)] -= nbBytes; \ + bits[(_stream)] = MEM_read64(ip[(_stream)]) | 1; \ + bits[(_stream)] <<= nbBits; \ + } while (0) + + /* Manually unroll the loop because compilers don't consistently + * unroll the inner loops, which destroys performance. + */ + do { + /* Decode 5 symbols in each of the 4 streams */ + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X1_DECODE_SYMBOL, 0); + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X1_DECODE_SYMBOL, 1); + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X1_DECODE_SYMBOL, 2); + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X1_DECODE_SYMBOL, 3); + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X1_DECODE_SYMBOL, 4); + + /* Reload each of the 4 the bitstreams */ + HUF_4X_FOR_EACH_STREAM(HUF_4X1_RELOAD_STREAM); + } while (op[3] < olimit); + +#undef HUF_4X1_DECODE_SYMBOL +#undef HUF_4X1_RELOAD_STREAM + } + +_out: + + /* Save the final values of each of the state variables back to args. */ + ZSTD_memcpy(&args->bits, &bits, sizeof(bits)); + ZSTD_memcpy((void*)(&args->ip), &ip, sizeof(ip)); + ZSTD_memcpy(&args->op, &op, sizeof(op)); +} + +/** + * @returns @p dstSize on success (>= 6) + * 0 if the fallback implementation should be used + * An error if an error occurred + */ +static HUF_FAST_BMI2_ATTRS +size_t +HUF_decompress4X1_usingDTable_internal_fast( + void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + const HUF_DTable* DTable, + HUF_DecompressFastLoopFn loopFn) +{ + void const* dt = DTable + 1; + BYTE const* const ilowest = (BYTE const*)cSrc; + BYTE* const oend = ZSTD_maybeNullPtrAdd((BYTE*)dst, dstSize); + HUF_DecompressFastArgs args; + { size_t const ret = HUF_DecompressFastArgs_init(&args, dst, dstSize, cSrc, cSrcSize, DTable); + FORWARD_IF_ERROR(ret, "Failed to init fast loop args"); + if (ret == 0) + return 0; + } + + assert(args.ip[0] >= args.ilowest); + loopFn(&args); + + /* Our loop guarantees that ip[] >= ilowest and that we haven't + * overwritten any op[]. + */ + assert(args.ip[0] >= ilowest); + assert(args.ip[0] >= ilowest); + assert(args.ip[1] >= ilowest); + assert(args.ip[2] >= ilowest); + assert(args.ip[3] >= ilowest); + assert(args.op[3] <= oend); + + assert(ilowest == args.ilowest); + assert(ilowest + 6 == args.iend[0]); + (void)ilowest; + + /* finish bit streams one by one. */ + { size_t const segmentSize = (dstSize+3) / 4; + BYTE* segmentEnd = (BYTE*)dst; + int i; + for (i = 0; i < 4; ++i) { + BIT_DStream_t bit; + if (segmentSize <= (size_t)(oend - segmentEnd)) + segmentEnd += segmentSize; + else + segmentEnd = oend; + FORWARD_IF_ERROR(HUF_initRemainingDStream(&bit, &args, i, segmentEnd), "corruption"); + /* Decompress and validate that we've produced exactly the expected length. */ + args.op[i] += HUF_decodeStreamX1(args.op[i], &bit, segmentEnd, (HUF_DEltX1 const*)dt, HUF_DECODER_FAST_TABLELOG); + if (args.op[i] != segmentEnd) return ERROR(corruption_detected); + } + } + + /* decoded size */ + assert(dstSize != 0); + return dstSize; +} + +HUF_DGEN(HUF_decompress1X1_usingDTable_internal) + +static size_t HUF_decompress4X1_usingDTable_internal(void* dst, size_t dstSize, void const* cSrc, + size_t cSrcSize, HUF_DTable const* DTable, int flags) +{ + HUF_DecompressUsingDTableFn fallbackFn = HUF_decompress4X1_usingDTable_internal_default; + HUF_DecompressFastLoopFn loopFn = HUF_decompress4X1_usingDTable_internal_fast_c_loop; + +#if DYNAMIC_BMI2 + if (flags & HUF_flags_bmi2) { + fallbackFn = HUF_decompress4X1_usingDTable_internal_bmi2; +# if ZSTD_ENABLE_ASM_X86_64_BMI2 + if (!(flags & HUF_flags_disableAsm)) { + loopFn = HUF_decompress4X1_usingDTable_internal_fast_asm_loop; + } +# endif + } else { + return fallbackFn(dst, dstSize, cSrc, cSrcSize, DTable); + } +#endif + +#if ZSTD_ENABLE_ASM_X86_64_BMI2 && defined(__BMI2__) + if (!(flags & HUF_flags_disableAsm)) { + loopFn = HUF_decompress4X1_usingDTable_internal_fast_asm_loop; + } +#endif + + if (HUF_ENABLE_FAST_DECODE && !(flags & HUF_flags_disableFast)) { + size_t const ret = HUF_decompress4X1_usingDTable_internal_fast(dst, dstSize, cSrc, cSrcSize, DTable, loopFn); + if (ret != 0) + return ret; + } + return fallbackFn(dst, dstSize, cSrc, cSrcSize, DTable); +} + +static size_t HUF_decompress4X1_DCtx_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + void* workSpace, size_t wkspSize, int flags) +{ + const BYTE* ip = (const BYTE*) cSrc; + + size_t const hSize = HUF_readDTableX1_wksp(dctx, cSrc, cSrcSize, workSpace, wkspSize, flags); + if (HUF_isError(hSize)) return hSize; + if (hSize >= cSrcSize) return ERROR(srcSize_wrong); + ip += hSize; cSrcSize -= hSize; + + return HUF_decompress4X1_usingDTable_internal(dst, dstSize, ip, cSrcSize, dctx, flags); +} + +#endif /* HUF_FORCE_DECOMPRESS_X2 */ + + +#ifndef HUF_FORCE_DECOMPRESS_X1 + +/* *************************/ +/* double-symbols decoding */ +/* *************************/ + +typedef struct { U16 sequence; BYTE nbBits; BYTE length; } HUF_DEltX2; /* double-symbols decoding */ +typedef struct { BYTE symbol; } sortedSymbol_t; +typedef U32 rankValCol_t[HUF_TABLELOG_MAX + 1]; +typedef rankValCol_t rankVal_t[HUF_TABLELOG_MAX]; + +/** + * Constructs a HUF_DEltX2 in a U32. + */ +static U32 HUF_buildDEltX2U32(U32 symbol, U32 nbBits, U32 baseSeq, int level) +{ + U32 seq; + DEBUG_STATIC_ASSERT(offsetof(HUF_DEltX2, sequence) == 0); + DEBUG_STATIC_ASSERT(offsetof(HUF_DEltX2, nbBits) == 2); + DEBUG_STATIC_ASSERT(offsetof(HUF_DEltX2, length) == 3); + DEBUG_STATIC_ASSERT(sizeof(HUF_DEltX2) == sizeof(U32)); + if (MEM_isLittleEndian()) { + seq = level == 1 ? symbol : (baseSeq + (symbol << 8)); + return seq + (nbBits << 16) + ((U32)level << 24); + } else { + seq = level == 1 ? (symbol << 8) : ((baseSeq << 8) + symbol); + return (seq << 16) + (nbBits << 8) + (U32)level; + } +} + +/** + * Constructs a HUF_DEltX2. + */ +static HUF_DEltX2 HUF_buildDEltX2(U32 symbol, U32 nbBits, U32 baseSeq, int level) +{ + HUF_DEltX2 DElt; + U32 const val = HUF_buildDEltX2U32(symbol, nbBits, baseSeq, level); + DEBUG_STATIC_ASSERT(sizeof(DElt) == sizeof(val)); + ZSTD_memcpy(&DElt, &val, sizeof(val)); + return DElt; +} + +/** + * Constructs 2 HUF_DEltX2s and packs them into a U64. + */ +static U64 HUF_buildDEltX2U64(U32 symbol, U32 nbBits, U16 baseSeq, int level) +{ + U32 DElt = HUF_buildDEltX2U32(symbol, nbBits, baseSeq, level); + return (U64)DElt + ((U64)DElt << 32); +} + +/** + * Fills the DTable rank with all the symbols from [begin, end) that are each + * nbBits long. + * + * @param DTableRank The start of the rank in the DTable. + * @param begin The first symbol to fill (inclusive). + * @param end The last symbol to fill (exclusive). + * @param nbBits Each symbol is nbBits long. + * @param tableLog The table log. + * @param baseSeq If level == 1 { 0 } else { the first level symbol } + * @param level The level in the table. Must be 1 or 2. + */ +static void HUF_fillDTableX2ForWeight( + HUF_DEltX2* DTableRank, + sortedSymbol_t const* begin, sortedSymbol_t const* end, + U32 nbBits, U32 tableLog, + U16 baseSeq, int const level) +{ + U32 const length = 1U << ((tableLog - nbBits) & 0x1F /* quiet static-analyzer */); + const sortedSymbol_t* ptr; + assert(level >= 1 && level <= 2); + switch (length) { + case 1: + for (ptr = begin; ptr != end; ++ptr) { + HUF_DEltX2 const DElt = HUF_buildDEltX2(ptr->symbol, nbBits, baseSeq, level); + *DTableRank++ = DElt; + } + break; + case 2: + for (ptr = begin; ptr != end; ++ptr) { + HUF_DEltX2 const DElt = HUF_buildDEltX2(ptr->symbol, nbBits, baseSeq, level); + DTableRank[0] = DElt; + DTableRank[1] = DElt; + DTableRank += 2; + } + break; + case 4: + for (ptr = begin; ptr != end; ++ptr) { + U64 const DEltX2 = HUF_buildDEltX2U64(ptr->symbol, nbBits, baseSeq, level); + ZSTD_memcpy(DTableRank + 0, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTableRank + 2, &DEltX2, sizeof(DEltX2)); + DTableRank += 4; + } + break; + case 8: + for (ptr = begin; ptr != end; ++ptr) { + U64 const DEltX2 = HUF_buildDEltX2U64(ptr->symbol, nbBits, baseSeq, level); + ZSTD_memcpy(DTableRank + 0, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTableRank + 2, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTableRank + 4, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTableRank + 6, &DEltX2, sizeof(DEltX2)); + DTableRank += 8; + } + break; + default: + for (ptr = begin; ptr != end; ++ptr) { + U64 const DEltX2 = HUF_buildDEltX2U64(ptr->symbol, nbBits, baseSeq, level); + HUF_DEltX2* const DTableRankEnd = DTableRank + length; + for (; DTableRank != DTableRankEnd; DTableRank += 8) { + ZSTD_memcpy(DTableRank + 0, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTableRank + 2, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTableRank + 4, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTableRank + 6, &DEltX2, sizeof(DEltX2)); + } + } + break; + } +} + +/* HUF_fillDTableX2Level2() : + * `rankValOrigin` must be a table of at least (HUF_TABLELOG_MAX + 1) U32 */ +static void HUF_fillDTableX2Level2(HUF_DEltX2* DTable, U32 targetLog, const U32 consumedBits, + const U32* rankVal, const int minWeight, const int maxWeight1, + const sortedSymbol_t* sortedSymbols, U32 const* rankStart, + U32 nbBitsBaseline, U16 baseSeq) +{ + /* Fill skipped values (all positions up to rankVal[minWeight]). + * These are positions only get a single symbol because the combined weight + * is too large. + */ + if (minWeight>1) { + U32 const length = 1U << ((targetLog - consumedBits) & 0x1F /* quiet static-analyzer */); + U64 const DEltX2 = HUF_buildDEltX2U64(baseSeq, consumedBits, /* baseSeq */ 0, /* level */ 1); + int const skipSize = rankVal[minWeight]; + assert(length > 1); + assert((U32)skipSize < length); + switch (length) { + case 2: + assert(skipSize == 1); + ZSTD_memcpy(DTable, &DEltX2, sizeof(DEltX2)); + break; + case 4: + assert(skipSize <= 4); + ZSTD_memcpy(DTable + 0, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTable + 2, &DEltX2, sizeof(DEltX2)); + break; + default: + { + int i; + for (i = 0; i < skipSize; i += 8) { + ZSTD_memcpy(DTable + i + 0, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTable + i + 2, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTable + i + 4, &DEltX2, sizeof(DEltX2)); + ZSTD_memcpy(DTable + i + 6, &DEltX2, sizeof(DEltX2)); + } + } + } + } + + /* Fill each of the second level symbols by weight. */ + { + int w; + for (w = minWeight; w < maxWeight1; ++w) { + int const begin = rankStart[w]; + int const end = rankStart[w+1]; + U32 const nbBits = nbBitsBaseline - w; + U32 const totalBits = nbBits + consumedBits; + HUF_fillDTableX2ForWeight( + DTable + rankVal[w], + sortedSymbols + begin, sortedSymbols + end, + totalBits, targetLog, + baseSeq, /* level */ 2); + } + } +} + +static void HUF_fillDTableX2(HUF_DEltX2* DTable, const U32 targetLog, + const sortedSymbol_t* sortedList, + const U32* rankStart, rankValCol_t* rankValOrigin, const U32 maxWeight, + const U32 nbBitsBaseline) +{ + U32* const rankVal = rankValOrigin[0]; + const int scaleLog = nbBitsBaseline - targetLog; /* note : targetLog >= srcLog, hence scaleLog <= 1 */ + const U32 minBits = nbBitsBaseline - maxWeight; + int w; + int const wEnd = (int)maxWeight + 1; + + /* Fill DTable in order of weight. */ + for (w = 1; w < wEnd; ++w) { + int const begin = (int)rankStart[w]; + int const end = (int)rankStart[w+1]; + U32 const nbBits = nbBitsBaseline - w; + + if (targetLog-nbBits >= minBits) { + /* Enough room for a second symbol. */ + int start = rankVal[w]; + U32 const length = 1U << ((targetLog - nbBits) & 0x1F /* quiet static-analyzer */); + int minWeight = nbBits + scaleLog; + int s; + if (minWeight < 1) minWeight = 1; + /* Fill the DTable for every symbol of weight w. + * These symbols get at least 1 second symbol. + */ + for (s = begin; s != end; ++s) { + HUF_fillDTableX2Level2( + DTable + start, targetLog, nbBits, + rankValOrigin[nbBits], minWeight, wEnd, + sortedList, rankStart, + nbBitsBaseline, sortedList[s].symbol); + start += length; + } + } else { + /* Only a single symbol. */ + HUF_fillDTableX2ForWeight( + DTable + rankVal[w], + sortedList + begin, sortedList + end, + nbBits, targetLog, + /* baseSeq */ 0, /* level */ 1); + } + } +} + +typedef struct { + rankValCol_t rankVal[HUF_TABLELOG_MAX]; + U32 rankStats[HUF_TABLELOG_MAX + 1]; + U32 rankStart0[HUF_TABLELOG_MAX + 3]; + sortedSymbol_t sortedSymbol[HUF_SYMBOLVALUE_MAX + 1]; + BYTE weightList[HUF_SYMBOLVALUE_MAX + 1]; + U32 calleeWksp[HUF_READ_STATS_WORKSPACE_SIZE_U32]; +} HUF_ReadDTableX2_Workspace; + +size_t HUF_readDTableX2_wksp(HUF_DTable* DTable, + const void* src, size_t srcSize, + void* workSpace, size_t wkspSize, int flags) +{ + U32 tableLog, maxW, nbSymbols; + DTableDesc dtd = HUF_getDTableDesc(DTable); + U32 maxTableLog = dtd.maxTableLog; + size_t iSize; + void* dtPtr = DTable+1; /* force compiler to avoid strict-aliasing */ + HUF_DEltX2* const dt = (HUF_DEltX2*)dtPtr; + U32 *rankStart; + + HUF_ReadDTableX2_Workspace* const wksp = (HUF_ReadDTableX2_Workspace*)workSpace; + + if (sizeof(*wksp) > wkspSize) return ERROR(GENERIC); + + rankStart = wksp->rankStart0 + 1; + ZSTD_memset(wksp->rankStats, 0, sizeof(wksp->rankStats)); + ZSTD_memset(wksp->rankStart0, 0, sizeof(wksp->rankStart0)); + + DEBUG_STATIC_ASSERT(sizeof(HUF_DEltX2) == sizeof(HUF_DTable)); /* if compiler fails here, assertion is wrong */ + if (maxTableLog > HUF_TABLELOG_MAX) return ERROR(tableLog_tooLarge); + /* ZSTD_memset(weightList, 0, sizeof(weightList)); */ /* is not necessary, even though some analyzer complain ... */ + + iSize = HUF_readStats_wksp(wksp->weightList, HUF_SYMBOLVALUE_MAX + 1, wksp->rankStats, &nbSymbols, &tableLog, src, srcSize, wksp->calleeWksp, sizeof(wksp->calleeWksp), flags); + if (HUF_isError(iSize)) return iSize; + + /* check result */ + if (tableLog > maxTableLog) return ERROR(tableLog_tooLarge); /* DTable can't fit code depth */ + if (tableLog <= HUF_DECODER_FAST_TABLELOG && maxTableLog > HUF_DECODER_FAST_TABLELOG) maxTableLog = HUF_DECODER_FAST_TABLELOG; + + /* find maxWeight */ + for (maxW = tableLog; wksp->rankStats[maxW]==0; maxW--) {} /* necessarily finds a solution before 0 */ + + /* Get start index of each weight */ + { U32 w, nextRankStart = 0; + for (w=1; wrankStats[w]; + rankStart[w] = curr; + } + rankStart[0] = nextRankStart; /* put all 0w symbols at the end of sorted list*/ + rankStart[maxW+1] = nextRankStart; + } + + /* sort symbols by weight */ + { U32 s; + for (s=0; sweightList[s]; + U32 const r = rankStart[w]++; + wksp->sortedSymbol[r].symbol = (BYTE)s; + } + rankStart[0] = 0; /* forget 0w symbols; this is beginning of weight(1) */ + } + + /* Build rankVal */ + { U32* const rankVal0 = wksp->rankVal[0]; + { int const rescale = (maxTableLog-tableLog) - 1; /* tableLog <= maxTableLog */ + U32 nextRankVal = 0; + U32 w; + for (w=1; wrankStats[w] << (w+rescale); + rankVal0[w] = curr; + } } + { U32 const minBits = tableLog+1 - maxW; + U32 consumed; + for (consumed = minBits; consumed < maxTableLog - minBits + 1; consumed++) { + U32* const rankValPtr = wksp->rankVal[consumed]; + U32 w; + for (w = 1; w < maxW+1; w++) { + rankValPtr[w] = rankVal0[w] >> consumed; + } } } } + + HUF_fillDTableX2(dt, maxTableLog, + wksp->sortedSymbol, + wksp->rankStart0, wksp->rankVal, maxW, + tableLog+1); + + dtd.tableLog = (BYTE)maxTableLog; + dtd.tableType = 1; + ZSTD_memcpy(DTable, &dtd, sizeof(dtd)); + return iSize; +} + + +FORCE_INLINE_TEMPLATE U32 +HUF_decodeSymbolX2(void* op, BIT_DStream_t* DStream, const HUF_DEltX2* dt, const U32 dtLog) +{ + size_t const val = BIT_lookBitsFast(DStream, dtLog); /* note : dtLog >= 1 */ + ZSTD_memcpy(op, &dt[val].sequence, 2); + BIT_skipBits(DStream, dt[val].nbBits); + return dt[val].length; +} + +FORCE_INLINE_TEMPLATE U32 +HUF_decodeLastSymbolX2(void* op, BIT_DStream_t* DStream, const HUF_DEltX2* dt, const U32 dtLog) +{ + size_t const val = BIT_lookBitsFast(DStream, dtLog); /* note : dtLog >= 1 */ + ZSTD_memcpy(op, &dt[val].sequence, 1); + if (dt[val].length==1) { + BIT_skipBits(DStream, dt[val].nbBits); + } else { + if (DStream->bitsConsumed < (sizeof(DStream->bitContainer)*8)) { + BIT_skipBits(DStream, dt[val].nbBits); + if (DStream->bitsConsumed > (sizeof(DStream->bitContainer)*8)) + /* ugly hack; works only because it's the last symbol. Note : can't easily extract nbBits from just this symbol */ + DStream->bitsConsumed = (sizeof(DStream->bitContainer)*8); + } + } + return 1; +} + +#define HUF_DECODE_SYMBOLX2_0(ptr, DStreamPtr) \ + do { ptr += HUF_decodeSymbolX2(ptr, DStreamPtr, dt, dtLog); } while (0) + +#define HUF_DECODE_SYMBOLX2_1(ptr, DStreamPtr) \ + do { \ + if (MEM_64bits() || (HUF_TABLELOG_MAX<=12)) \ + ptr += HUF_decodeSymbolX2(ptr, DStreamPtr, dt, dtLog); \ + } while (0) + +#define HUF_DECODE_SYMBOLX2_2(ptr, DStreamPtr) \ + do { \ + if (MEM_64bits()) \ + ptr += HUF_decodeSymbolX2(ptr, DStreamPtr, dt, dtLog); \ + } while (0) + +HINT_INLINE size_t +HUF_decodeStreamX2(BYTE* p, BIT_DStream_t* bitDPtr, BYTE* const pEnd, + const HUF_DEltX2* const dt, const U32 dtLog) +{ + BYTE* const pStart = p; + + /* up to 8 symbols at a time */ + if ((size_t)(pEnd - p) >= sizeof(bitDPtr->bitContainer)) { + if (dtLog <= 11 && MEM_64bits()) { + /* up to 10 symbols at a time */ + while ((BIT_reloadDStream(bitDPtr) == BIT_DStream_unfinished) & (p < pEnd-9)) { + HUF_DECODE_SYMBOLX2_0(p, bitDPtr); + HUF_DECODE_SYMBOLX2_0(p, bitDPtr); + HUF_DECODE_SYMBOLX2_0(p, bitDPtr); + HUF_DECODE_SYMBOLX2_0(p, bitDPtr); + HUF_DECODE_SYMBOLX2_0(p, bitDPtr); + } + } else { + /* up to 8 symbols at a time */ + while ((BIT_reloadDStream(bitDPtr) == BIT_DStream_unfinished) & (p < pEnd-(sizeof(bitDPtr->bitContainer)-1))) { + HUF_DECODE_SYMBOLX2_2(p, bitDPtr); + HUF_DECODE_SYMBOLX2_1(p, bitDPtr); + HUF_DECODE_SYMBOLX2_2(p, bitDPtr); + HUF_DECODE_SYMBOLX2_0(p, bitDPtr); + } + } + } else { + BIT_reloadDStream(bitDPtr); + } + + /* closer to end : up to 2 symbols at a time */ + if ((size_t)(pEnd - p) >= 2) { + while ((BIT_reloadDStream(bitDPtr) == BIT_DStream_unfinished) & (p <= pEnd-2)) + HUF_DECODE_SYMBOLX2_0(p, bitDPtr); + + while (p <= pEnd-2) + HUF_DECODE_SYMBOLX2_0(p, bitDPtr); /* no need to reload : reached the end of DStream */ + } + + if (p < pEnd) + p += HUF_decodeLastSymbolX2(p, bitDPtr, dt, dtLog); + + return p-pStart; +} + +FORCE_INLINE_TEMPLATE size_t +HUF_decompress1X2_usingDTable_internal_body( + void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + const HUF_DTable* DTable) +{ + BIT_DStream_t bitD; + + /* Init */ + CHECK_F( BIT_initDStream(&bitD, cSrc, cSrcSize) ); + + /* decode */ + { BYTE* const ostart = (BYTE*) dst; + BYTE* const oend = ZSTD_maybeNullPtrAdd(ostart, dstSize); + const void* const dtPtr = DTable+1; /* force compiler to not use strict-aliasing */ + const HUF_DEltX2* const dt = (const HUF_DEltX2*)dtPtr; + DTableDesc const dtd = HUF_getDTableDesc(DTable); + HUF_decodeStreamX2(ostart, &bitD, oend, dt, dtd.tableLog); + } + + /* check */ + if (!BIT_endOfDStream(&bitD)) return ERROR(corruption_detected); + + /* decoded size */ + return dstSize; +} + +/* HUF_decompress4X2_usingDTable_internal_body(): + * Conditions: + * @dstSize >= 6 + */ +FORCE_INLINE_TEMPLATE size_t +HUF_decompress4X2_usingDTable_internal_body( + void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + const HUF_DTable* DTable) +{ + if (cSrcSize < 10) return ERROR(corruption_detected); /* strict minimum : jump table + 1 byte per stream */ + if (dstSize < 6) return ERROR(corruption_detected); /* stream 4-split doesn't work */ + + { const BYTE* const istart = (const BYTE*) cSrc; + BYTE* const ostart = (BYTE*) dst; + BYTE* const oend = ostart + dstSize; + BYTE* const olimit = oend - (sizeof(size_t)-1); + const void* const dtPtr = DTable+1; + const HUF_DEltX2* const dt = (const HUF_DEltX2*)dtPtr; + + /* Init */ + BIT_DStream_t bitD1; + BIT_DStream_t bitD2; + BIT_DStream_t bitD3; + BIT_DStream_t bitD4; + size_t const length1 = MEM_readLE16(istart); + size_t const length2 = MEM_readLE16(istart+2); + size_t const length3 = MEM_readLE16(istart+4); + size_t const length4 = cSrcSize - (length1 + length2 + length3 + 6); + const BYTE* const istart1 = istart + 6; /* jumpTable */ + const BYTE* const istart2 = istart1 + length1; + const BYTE* const istart3 = istart2 + length2; + const BYTE* const istart4 = istart3 + length3; + size_t const segmentSize = (dstSize+3) / 4; + BYTE* const opStart2 = ostart + segmentSize; + BYTE* const opStart3 = opStart2 + segmentSize; + BYTE* const opStart4 = opStart3 + segmentSize; + BYTE* op1 = ostart; + BYTE* op2 = opStart2; + BYTE* op3 = opStart3; + BYTE* op4 = opStart4; + U32 endSignal = 1; + DTableDesc const dtd = HUF_getDTableDesc(DTable); + U32 const dtLog = dtd.tableLog; + + if (length4 > cSrcSize) return ERROR(corruption_detected); /* overflow */ + if (opStart4 > oend) return ERROR(corruption_detected); /* overflow */ + assert(dstSize >= 6 /* validated above */); + CHECK_F( BIT_initDStream(&bitD1, istart1, length1) ); + CHECK_F( BIT_initDStream(&bitD2, istart2, length2) ); + CHECK_F( BIT_initDStream(&bitD3, istart3, length3) ); + CHECK_F( BIT_initDStream(&bitD4, istart4, length4) ); + + /* 16-32 symbols per loop (4-8 symbols per stream) */ + if ((size_t)(oend - op4) >= sizeof(size_t)) { + for ( ; (endSignal) & (op4 < olimit); ) { +#if defined(__clang__) && (defined(__x86_64__) || defined(__i386__)) + HUF_DECODE_SYMBOLX2_2(op1, &bitD1); + HUF_DECODE_SYMBOLX2_1(op1, &bitD1); + HUF_DECODE_SYMBOLX2_2(op1, &bitD1); + HUF_DECODE_SYMBOLX2_0(op1, &bitD1); + HUF_DECODE_SYMBOLX2_2(op2, &bitD2); + HUF_DECODE_SYMBOLX2_1(op2, &bitD2); + HUF_DECODE_SYMBOLX2_2(op2, &bitD2); + HUF_DECODE_SYMBOLX2_0(op2, &bitD2); + endSignal &= BIT_reloadDStreamFast(&bitD1) == BIT_DStream_unfinished; + endSignal &= BIT_reloadDStreamFast(&bitD2) == BIT_DStream_unfinished; + HUF_DECODE_SYMBOLX2_2(op3, &bitD3); + HUF_DECODE_SYMBOLX2_1(op3, &bitD3); + HUF_DECODE_SYMBOLX2_2(op3, &bitD3); + HUF_DECODE_SYMBOLX2_0(op3, &bitD3); + HUF_DECODE_SYMBOLX2_2(op4, &bitD4); + HUF_DECODE_SYMBOLX2_1(op4, &bitD4); + HUF_DECODE_SYMBOLX2_2(op4, &bitD4); + HUF_DECODE_SYMBOLX2_0(op4, &bitD4); + endSignal &= BIT_reloadDStreamFast(&bitD3) == BIT_DStream_unfinished; + endSignal &= BIT_reloadDStreamFast(&bitD4) == BIT_DStream_unfinished; +#else + HUF_DECODE_SYMBOLX2_2(op1, &bitD1); + HUF_DECODE_SYMBOLX2_2(op2, &bitD2); + HUF_DECODE_SYMBOLX2_2(op3, &bitD3); + HUF_DECODE_SYMBOLX2_2(op4, &bitD4); + HUF_DECODE_SYMBOLX2_1(op1, &bitD1); + HUF_DECODE_SYMBOLX2_1(op2, &bitD2); + HUF_DECODE_SYMBOLX2_1(op3, &bitD3); + HUF_DECODE_SYMBOLX2_1(op4, &bitD4); + HUF_DECODE_SYMBOLX2_2(op1, &bitD1); + HUF_DECODE_SYMBOLX2_2(op2, &bitD2); + HUF_DECODE_SYMBOLX2_2(op3, &bitD3); + HUF_DECODE_SYMBOLX2_2(op4, &bitD4); + HUF_DECODE_SYMBOLX2_0(op1, &bitD1); + HUF_DECODE_SYMBOLX2_0(op2, &bitD2); + HUF_DECODE_SYMBOLX2_0(op3, &bitD3); + HUF_DECODE_SYMBOLX2_0(op4, &bitD4); + endSignal = (U32)LIKELY((U32) + (BIT_reloadDStreamFast(&bitD1) == BIT_DStream_unfinished) + & (BIT_reloadDStreamFast(&bitD2) == BIT_DStream_unfinished) + & (BIT_reloadDStreamFast(&bitD3) == BIT_DStream_unfinished) + & (BIT_reloadDStreamFast(&bitD4) == BIT_DStream_unfinished)); +#endif + } + } + + /* check corruption */ + if (op1 > opStart2) return ERROR(corruption_detected); + if (op2 > opStart3) return ERROR(corruption_detected); + if (op3 > opStart4) return ERROR(corruption_detected); + /* note : op4 already verified within main loop */ + + /* finish bitStreams one by one */ + HUF_decodeStreamX2(op1, &bitD1, opStart2, dt, dtLog); + HUF_decodeStreamX2(op2, &bitD2, opStart3, dt, dtLog); + HUF_decodeStreamX2(op3, &bitD3, opStart4, dt, dtLog); + HUF_decodeStreamX2(op4, &bitD4, oend, dt, dtLog); + + /* check */ + { U32 const endCheck = BIT_endOfDStream(&bitD1) & BIT_endOfDStream(&bitD2) & BIT_endOfDStream(&bitD3) & BIT_endOfDStream(&bitD4); + if (!endCheck) return ERROR(corruption_detected); } + + /* decoded size */ + return dstSize; + } +} + +#if HUF_NEED_BMI2_FUNCTION +static BMI2_TARGET_ATTRIBUTE +size_t HUF_decompress4X2_usingDTable_internal_bmi2(void* dst, size_t dstSize, void const* cSrc, + size_t cSrcSize, HUF_DTable const* DTable) { + return HUF_decompress4X2_usingDTable_internal_body(dst, dstSize, cSrc, cSrcSize, DTable); +} +#endif + +static +size_t HUF_decompress4X2_usingDTable_internal_default(void* dst, size_t dstSize, void const* cSrc, + size_t cSrcSize, HUF_DTable const* DTable) { + return HUF_decompress4X2_usingDTable_internal_body(dst, dstSize, cSrc, cSrcSize, DTable); +} + +#if ZSTD_ENABLE_ASM_X86_64_BMI2 + +HUF_ASM_DECL void HUF_decompress4X2_usingDTable_internal_fast_asm_loop(HUF_DecompressFastArgs* args) ZSTDLIB_HIDDEN; + +#endif + +static HUF_FAST_BMI2_ATTRS +void HUF_decompress4X2_usingDTable_internal_fast_c_loop(HUF_DecompressFastArgs* args) +{ + U64 bits[4]; + BYTE const* ip[4]; + BYTE* op[4]; + BYTE* oend[4]; + HUF_DEltX2 const* const dtable = (HUF_DEltX2 const*)args->dt; + BYTE const* const ilowest = args->ilowest; + + /* Copy the arguments to local registers. */ + ZSTD_memcpy(&bits, &args->bits, sizeof(bits)); + ZSTD_memcpy((void*)(&ip), &args->ip, sizeof(ip)); + ZSTD_memcpy(&op, &args->op, sizeof(op)); + + oend[0] = op[1]; + oend[1] = op[2]; + oend[2] = op[3]; + oend[3] = args->oend; + + assert(MEM_isLittleEndian()); + assert(!MEM_32bits()); + + for (;;) { + BYTE* olimit; + int stream; + + /* Assert loop preconditions */ +#ifndef NDEBUG + for (stream = 0; stream < 4; ++stream) { + assert(op[stream] <= oend[stream]); + assert(ip[stream] >= ilowest); + } +#endif + /* Compute olimit */ + { + /* Each loop does 5 table lookups for each of the 4 streams. + * Each table lookup consumes up to 11 bits of input, and produces + * up to 2 bytes of output. + */ + /* We can consume up to 7 bytes of input per iteration per stream. + * We also know that each input pointer is >= ip[0]. So we can run + * iters loops before running out of input. + */ + size_t iters = (size_t)(ip[0] - ilowest) / 7; + /* Each iteration can produce up to 10 bytes of output per stream. + * Each output stream my advance at different rates. So take the + * minimum number of safe iterations among all the output streams. + */ + for (stream = 0; stream < 4; ++stream) { + size_t const oiters = (size_t)(oend[stream] - op[stream]) / 10; + iters = MIN(iters, oiters); + } + + /* Each iteration produces at least 5 output symbols. So until + * op[3] crosses olimit, we know we haven't executed iters + * iterations yet. This saves us maintaining an iters counter, + * at the expense of computing the remaining # of iterations + * more frequently. + */ + olimit = op[3] + (iters * 5); + + /* Exit the fast decoding loop once we reach the end. */ + if (op[3] == olimit) + break; + + /* Exit the decoding loop if any input pointer has crossed the + * previous one. This indicates corruption, and a precondition + * to our loop is that ip[i] >= ip[0]. + */ + for (stream = 1; stream < 4; ++stream) { + if (ip[stream] < ip[stream - 1]) + goto _out; + } + } + +#ifndef NDEBUG + for (stream = 1; stream < 4; ++stream) { + assert(ip[stream] >= ip[stream - 1]); + } +#endif + +#define HUF_4X2_DECODE_SYMBOL(_stream, _decode3) \ + do { \ + if ((_decode3) || (_stream) != 3) { \ + int const index = (int)(bits[(_stream)] >> 53); \ + HUF_DEltX2 const entry = dtable[index]; \ + MEM_write16(op[(_stream)], entry.sequence); \ + bits[(_stream)] <<= (entry.nbBits) & 0x3F; \ + op[(_stream)] += (entry.length); \ + } \ + } while (0) + +#define HUF_4X2_RELOAD_STREAM(_stream) \ + do { \ + HUF_4X2_DECODE_SYMBOL(3, 1); \ + { \ + int const ctz = ZSTD_countTrailingZeros64(bits[(_stream)]); \ + int const nbBits = ctz & 7; \ + int const nbBytes = ctz >> 3; \ + ip[(_stream)] -= nbBytes; \ + bits[(_stream)] = MEM_read64(ip[(_stream)]) | 1; \ + bits[(_stream)] <<= nbBits; \ + } \ + } while (0) + + /* Manually unroll the loop because compilers don't consistently + * unroll the inner loops, which destroys performance. + */ + do { + /* Decode 5 symbols from each of the first 3 streams. + * The final stream will be decoded during the reload phase + * to reduce register pressure. + */ + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X2_DECODE_SYMBOL, 0); + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X2_DECODE_SYMBOL, 0); + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X2_DECODE_SYMBOL, 0); + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X2_DECODE_SYMBOL, 0); + HUF_4X_FOR_EACH_STREAM_WITH_VAR(HUF_4X2_DECODE_SYMBOL, 0); + + /* Decode one symbol from the final stream */ + HUF_4X2_DECODE_SYMBOL(3, 1); + + /* Decode 4 symbols from the final stream & reload bitstreams. + * The final stream is reloaded last, meaning that all 5 symbols + * are decoded from the final stream before it is reloaded. + */ + HUF_4X_FOR_EACH_STREAM(HUF_4X2_RELOAD_STREAM); + } while (op[3] < olimit); + } + +#undef HUF_4X2_DECODE_SYMBOL +#undef HUF_4X2_RELOAD_STREAM + +_out: + + /* Save the final values of each of the state variables back to args. */ + ZSTD_memcpy(&args->bits, &bits, sizeof(bits)); + ZSTD_memcpy((void*)(&args->ip), &ip, sizeof(ip)); + ZSTD_memcpy(&args->op, &op, sizeof(op)); +} + + +static HUF_FAST_BMI2_ATTRS size_t +HUF_decompress4X2_usingDTable_internal_fast( + void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + const HUF_DTable* DTable, + HUF_DecompressFastLoopFn loopFn) { + void const* dt = DTable + 1; + const BYTE* const ilowest = (const BYTE*)cSrc; + BYTE* const oend = ZSTD_maybeNullPtrAdd((BYTE*)dst, dstSize); + HUF_DecompressFastArgs args; + { + size_t const ret = HUF_DecompressFastArgs_init(&args, dst, dstSize, cSrc, cSrcSize, DTable); + FORWARD_IF_ERROR(ret, "Failed to init asm args"); + if (ret == 0) + return 0; + } + + assert(args.ip[0] >= args.ilowest); + loopFn(&args); + + /* note : op4 already verified within main loop */ + assert(args.ip[0] >= ilowest); + assert(args.ip[1] >= ilowest); + assert(args.ip[2] >= ilowest); + assert(args.ip[3] >= ilowest); + assert(args.op[3] <= oend); + + assert(ilowest == args.ilowest); + assert(ilowest + 6 == args.iend[0]); + (void)ilowest; + + /* finish bitStreams one by one */ + { + size_t const segmentSize = (dstSize+3) / 4; + BYTE* segmentEnd = (BYTE*)dst; + int i; + for (i = 0; i < 4; ++i) { + BIT_DStream_t bit; + if (segmentSize <= (size_t)(oend - segmentEnd)) + segmentEnd += segmentSize; + else + segmentEnd = oend; + FORWARD_IF_ERROR(HUF_initRemainingDStream(&bit, &args, i, segmentEnd), "corruption"); + args.op[i] += HUF_decodeStreamX2(args.op[i], &bit, segmentEnd, (HUF_DEltX2 const*)dt, HUF_DECODER_FAST_TABLELOG); + if (args.op[i] != segmentEnd) + return ERROR(corruption_detected); + } + } + + /* decoded size */ + return dstSize; +} + +static size_t HUF_decompress4X2_usingDTable_internal(void* dst, size_t dstSize, void const* cSrc, + size_t cSrcSize, HUF_DTable const* DTable, int flags) +{ + HUF_DecompressUsingDTableFn fallbackFn = HUF_decompress4X2_usingDTable_internal_default; + HUF_DecompressFastLoopFn loopFn = HUF_decompress4X2_usingDTable_internal_fast_c_loop; + +#if DYNAMIC_BMI2 + if (flags & HUF_flags_bmi2) { + fallbackFn = HUF_decompress4X2_usingDTable_internal_bmi2; +# if ZSTD_ENABLE_ASM_X86_64_BMI2 + if (!(flags & HUF_flags_disableAsm)) { + loopFn = HUF_decompress4X2_usingDTable_internal_fast_asm_loop; + } +# endif + } else { + return fallbackFn(dst, dstSize, cSrc, cSrcSize, DTable); + } +#endif + +#if ZSTD_ENABLE_ASM_X86_64_BMI2 && defined(__BMI2__) + if (!(flags & HUF_flags_disableAsm)) { + loopFn = HUF_decompress4X2_usingDTable_internal_fast_asm_loop; + } +#endif + + if (HUF_ENABLE_FAST_DECODE && !(flags & HUF_flags_disableFast)) { + size_t const ret = HUF_decompress4X2_usingDTable_internal_fast(dst, dstSize, cSrc, cSrcSize, DTable, loopFn); + if (ret != 0) + return ret; + } + return fallbackFn(dst, dstSize, cSrc, cSrcSize, DTable); +} + +HUF_DGEN(HUF_decompress1X2_usingDTable_internal) + +size_t HUF_decompress1X2_DCtx_wksp(HUF_DTable* DCtx, void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + void* workSpace, size_t wkspSize, int flags) +{ + const BYTE* ip = (const BYTE*) cSrc; + + size_t const hSize = HUF_readDTableX2_wksp(DCtx, cSrc, cSrcSize, + workSpace, wkspSize, flags); + if (HUF_isError(hSize)) return hSize; + if (hSize >= cSrcSize) return ERROR(srcSize_wrong); + ip += hSize; cSrcSize -= hSize; + + return HUF_decompress1X2_usingDTable_internal(dst, dstSize, ip, cSrcSize, DCtx, flags); +} + +static size_t HUF_decompress4X2_DCtx_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + void* workSpace, size_t wkspSize, int flags) +{ + const BYTE* ip = (const BYTE*) cSrc; + + size_t hSize = HUF_readDTableX2_wksp(dctx, cSrc, cSrcSize, + workSpace, wkspSize, flags); + if (HUF_isError(hSize)) return hSize; + if (hSize >= cSrcSize) return ERROR(srcSize_wrong); + ip += hSize; cSrcSize -= hSize; + + return HUF_decompress4X2_usingDTable_internal(dst, dstSize, ip, cSrcSize, dctx, flags); +} + +#endif /* HUF_FORCE_DECOMPRESS_X1 */ + + +/* ***********************************/ +/* Universal decompression selectors */ +/* ***********************************/ + + +#if !defined(HUF_FORCE_DECOMPRESS_X1) && !defined(HUF_FORCE_DECOMPRESS_X2) +typedef struct { U32 tableTime; U32 decode256Time; } algo_time_t; +static const algo_time_t algoTime[16 /* Quantization */][2 /* single, double */] = +{ + /* single, double, quad */ + {{0,0}, {1,1}}, /* Q==0 : impossible */ + {{0,0}, {1,1}}, /* Q==1 : impossible */ + {{ 150,216}, { 381,119}}, /* Q == 2 : 12-18% */ + {{ 170,205}, { 514,112}}, /* Q == 3 : 18-25% */ + {{ 177,199}, { 539,110}}, /* Q == 4 : 25-32% */ + {{ 197,194}, { 644,107}}, /* Q == 5 : 32-38% */ + {{ 221,192}, { 735,107}}, /* Q == 6 : 38-44% */ + {{ 256,189}, { 881,106}}, /* Q == 7 : 44-50% */ + {{ 359,188}, {1167,109}}, /* Q == 8 : 50-56% */ + {{ 582,187}, {1570,114}}, /* Q == 9 : 56-62% */ + {{ 688,187}, {1712,122}}, /* Q ==10 : 62-69% */ + {{ 825,186}, {1965,136}}, /* Q ==11 : 69-75% */ + {{ 976,185}, {2131,150}}, /* Q ==12 : 75-81% */ + {{1180,186}, {2070,175}}, /* Q ==13 : 81-87% */ + {{1377,185}, {1731,202}}, /* Q ==14 : 87-93% */ + {{1412,185}, {1695,202}}, /* Q ==15 : 93-99% */ +}; +#endif + +/** HUF_selectDecoder() : + * Tells which decoder is likely to decode faster, + * based on a set of pre-computed metrics. + * @return : 0==HUF_decompress4X1, 1==HUF_decompress4X2 . + * Assumption : 0 < dstSize <= 128 KB */ +U32 HUF_selectDecoder (size_t dstSize, size_t cSrcSize) +{ + assert(dstSize > 0); + assert(dstSize <= 128*1024); +#if defined(HUF_FORCE_DECOMPRESS_X1) + (void)dstSize; + (void)cSrcSize; + return 0; +#elif defined(HUF_FORCE_DECOMPRESS_X2) + (void)dstSize; + (void)cSrcSize; + return 1; +#else + /* decoder timing evaluation */ + { U32 const Q = (cSrcSize >= dstSize) ? 15 : (U32)(cSrcSize * 16 / dstSize); /* Q < 16 */ + U32 const D256 = (U32)(dstSize >> 8); + U32 const DTime0 = algoTime[Q][0].tableTime + (algoTime[Q][0].decode256Time * D256); + U32 DTime1 = algoTime[Q][1].tableTime + (algoTime[Q][1].decode256Time * D256); + DTime1 += DTime1 >> 5; /* small advantage to algorithm using less memory, to reduce cache eviction */ + return DTime1 < DTime0; + } +#endif +} + +size_t HUF_decompress1X_DCtx_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, + const void* cSrc, size_t cSrcSize, + void* workSpace, size_t wkspSize, int flags) +{ + /* validation checks */ + if (dstSize == 0) return ERROR(dstSize_tooSmall); + if (cSrcSize > dstSize) return ERROR(corruption_detected); /* invalid */ + if (cSrcSize == dstSize) { ZSTD_memcpy(dst, cSrc, dstSize); return dstSize; } /* not compressed */ + if (cSrcSize == 1) { ZSTD_memset(dst, *(const BYTE*)cSrc, dstSize); return dstSize; } /* RLE */ + + { U32 const algoNb = HUF_selectDecoder(dstSize, cSrcSize); +#if defined(HUF_FORCE_DECOMPRESS_X1) + (void)algoNb; + assert(algoNb == 0); + return HUF_decompress1X1_DCtx_wksp(dctx, dst, dstSize, cSrc, + cSrcSize, workSpace, wkspSize, flags); +#elif defined(HUF_FORCE_DECOMPRESS_X2) + (void)algoNb; + assert(algoNb == 1); + return HUF_decompress1X2_DCtx_wksp(dctx, dst, dstSize, cSrc, + cSrcSize, workSpace, wkspSize, flags); +#else + return algoNb ? HUF_decompress1X2_DCtx_wksp(dctx, dst, dstSize, cSrc, + cSrcSize, workSpace, wkspSize, flags): + HUF_decompress1X1_DCtx_wksp(dctx, dst, dstSize, cSrc, + cSrcSize, workSpace, wkspSize, flags); +#endif + } +} + + +size_t HUF_decompress1X_usingDTable(void* dst, size_t maxDstSize, const void* cSrc, size_t cSrcSize, const HUF_DTable* DTable, int flags) +{ + DTableDesc const dtd = HUF_getDTableDesc(DTable); +#if defined(HUF_FORCE_DECOMPRESS_X1) + (void)dtd; + assert(dtd.tableType == 0); + return HUF_decompress1X1_usingDTable_internal(dst, maxDstSize, cSrc, cSrcSize, DTable, flags); +#elif defined(HUF_FORCE_DECOMPRESS_X2) + (void)dtd; + assert(dtd.tableType == 1); + return HUF_decompress1X2_usingDTable_internal(dst, maxDstSize, cSrc, cSrcSize, DTable, flags); +#else + return dtd.tableType ? HUF_decompress1X2_usingDTable_internal(dst, maxDstSize, cSrc, cSrcSize, DTable, flags) : + HUF_decompress1X1_usingDTable_internal(dst, maxDstSize, cSrc, cSrcSize, DTable, flags); +#endif +} + +#ifndef HUF_FORCE_DECOMPRESS_X2 +size_t HUF_decompress1X1_DCtx_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, const void* cSrc, size_t cSrcSize, void* workSpace, size_t wkspSize, int flags) +{ + const BYTE* ip = (const BYTE*) cSrc; + + size_t const hSize = HUF_readDTableX1_wksp(dctx, cSrc, cSrcSize, workSpace, wkspSize, flags); + if (HUF_isError(hSize)) return hSize; + if (hSize >= cSrcSize) return ERROR(srcSize_wrong); + ip += hSize; cSrcSize -= hSize; + + return HUF_decompress1X1_usingDTable_internal(dst, dstSize, ip, cSrcSize, dctx, flags); +} +#endif + +size_t HUF_decompress4X_usingDTable(void* dst, size_t maxDstSize, const void* cSrc, size_t cSrcSize, const HUF_DTable* DTable, int flags) +{ + DTableDesc const dtd = HUF_getDTableDesc(DTable); +#if defined(HUF_FORCE_DECOMPRESS_X1) + (void)dtd; + assert(dtd.tableType == 0); + return HUF_decompress4X1_usingDTable_internal(dst, maxDstSize, cSrc, cSrcSize, DTable, flags); +#elif defined(HUF_FORCE_DECOMPRESS_X2) + (void)dtd; + assert(dtd.tableType == 1); + return HUF_decompress4X2_usingDTable_internal(dst, maxDstSize, cSrc, cSrcSize, DTable, flags); +#else + return dtd.tableType ? HUF_decompress4X2_usingDTable_internal(dst, maxDstSize, cSrc, cSrcSize, DTable, flags) : + HUF_decompress4X1_usingDTable_internal(dst, maxDstSize, cSrc, cSrcSize, DTable, flags); +#endif +} + +size_t HUF_decompress4X_hufOnly_wksp(HUF_DTable* dctx, void* dst, size_t dstSize, const void* cSrc, size_t cSrcSize, void* workSpace, size_t wkspSize, int flags) +{ + /* validation checks */ + if (dstSize == 0) return ERROR(dstSize_tooSmall); + if (cSrcSize == 0) return ERROR(corruption_detected); + + { U32 const algoNb = HUF_selectDecoder(dstSize, cSrcSize); +#if defined(HUF_FORCE_DECOMPRESS_X1) + (void)algoNb; + assert(algoNb == 0); + return HUF_decompress4X1_DCtx_wksp(dctx, dst, dstSize, cSrc, cSrcSize, workSpace, wkspSize, flags); +#elif defined(HUF_FORCE_DECOMPRESS_X2) + (void)algoNb; + assert(algoNb == 1); + return HUF_decompress4X2_DCtx_wksp(dctx, dst, dstSize, cSrc, cSrcSize, workSpace, wkspSize, flags); +#else + return algoNb ? HUF_decompress4X2_DCtx_wksp(dctx, dst, dstSize, cSrc, cSrcSize, workSpace, wkspSize, flags) : + HUF_decompress4X1_DCtx_wksp(dctx, dst, dstSize, cSrc, cSrcSize, workSpace, wkspSize, flags); +#endif + } +} +/**** ended inlining decompress/huf_decompress.c ****/ +/**** start inlining decompress/zstd_ddict.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/* zstd_ddict.c : + * concentrates all logic that needs to know the internals of ZSTD_DDict object */ + +/*-******************************************************* +* Dependencies +*********************************************************/ +/**** skipping file: ../common/allocations.h ****/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../common/cpu.h ****/ +/**** skipping file: ../common/mem.h ****/ +#define FSE_STATIC_LINKING_ONLY +/**** skipping file: ../common/fse.h ****/ +/**** skipping file: ../common/huf.h ****/ +/**** start inlining zstd_decompress_internal.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + +/* zstd_decompress_internal: + * objects and definitions shared within lib/decompress modules */ + + #ifndef ZSTD_DECOMPRESS_INTERNAL_H + #define ZSTD_DECOMPRESS_INTERNAL_H + + +/*-******************************************************* + * Dependencies + *********************************************************/ +/**** skipping file: ../common/mem.h ****/ +/**** skipping file: ../common/zstd_internal.h ****/ + + + +/*-******************************************************* + * Constants + *********************************************************/ +static UNUSED_ATTR const U32 LL_base[MaxLL+1] = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 18, 20, 22, 24, 28, 32, 40, + 48, 64, 0x80, 0x100, 0x200, 0x400, 0x800, 0x1000, + 0x2000, 0x4000, 0x8000, 0x10000 }; + +static UNUSED_ATTR const U32 OF_base[MaxOff+1] = { + 0, 1, 1, 5, 0xD, 0x1D, 0x3D, 0x7D, + 0xFD, 0x1FD, 0x3FD, 0x7FD, 0xFFD, 0x1FFD, 0x3FFD, 0x7FFD, + 0xFFFD, 0x1FFFD, 0x3FFFD, 0x7FFFD, 0xFFFFD, 0x1FFFFD, 0x3FFFFD, 0x7FFFFD, + 0xFFFFFD, 0x1FFFFFD, 0x3FFFFFD, 0x7FFFFFD, 0xFFFFFFD, 0x1FFFFFFD, 0x3FFFFFFD, 0x7FFFFFFD }; + +static UNUSED_ATTR const U8 OF_bits[MaxOff+1] = { + 0, 1, 2, 3, 4, 5, 6, 7, + 8, 9, 10, 11, 12, 13, 14, 15, + 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31 }; + +static UNUSED_ATTR const U32 ML_base[MaxML+1] = { + 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, + 35, 37, 39, 41, 43, 47, 51, 59, + 67, 83, 99, 0x83, 0x103, 0x203, 0x403, 0x803, + 0x1003, 0x2003, 0x4003, 0x8003, 0x10003 }; + + +/*-******************************************************* + * Decompression types + *********************************************************/ + typedef struct { + U32 fastMode; + U32 tableLog; + } ZSTD_seqSymbol_header; + + typedef struct { + U16 nextState; + BYTE nbAdditionalBits; + BYTE nbBits; + U32 baseValue; + } ZSTD_seqSymbol; + + #define SEQSYMBOL_TABLE_SIZE(log) (1 + (1 << (log))) + +#define ZSTD_BUILD_FSE_TABLE_WKSP_SIZE (sizeof(S16) * (MaxSeq + 1) + (1u << MaxFSELog) + sizeof(U64)) +#define ZSTD_BUILD_FSE_TABLE_WKSP_SIZE_U32 ((ZSTD_BUILD_FSE_TABLE_WKSP_SIZE + sizeof(U32) - 1) / sizeof(U32)) +#define ZSTD_HUFFDTABLE_CAPACITY_LOG 12 + +typedef struct { + ZSTD_seqSymbol LLTable[SEQSYMBOL_TABLE_SIZE(LLFSELog)]; /* Note : Space reserved for FSE Tables */ + ZSTD_seqSymbol OFTable[SEQSYMBOL_TABLE_SIZE(OffFSELog)]; /* is also used as temporary workspace while building hufTable during DDict creation */ + ZSTD_seqSymbol MLTable[SEQSYMBOL_TABLE_SIZE(MLFSELog)]; /* and therefore must be at least HUF_DECOMPRESS_WORKSPACE_SIZE large */ + HUF_DTable hufTable[HUF_DTABLE_SIZE(ZSTD_HUFFDTABLE_CAPACITY_LOG)]; /* can accommodate HUF_decompress4X */ + U32 rep[ZSTD_REP_NUM]; + U32 workspace[ZSTD_BUILD_FSE_TABLE_WKSP_SIZE_U32]; +} ZSTD_entropyDTables_t; + +typedef enum { ZSTDds_getFrameHeaderSize, ZSTDds_decodeFrameHeader, + ZSTDds_decodeBlockHeader, ZSTDds_decompressBlock, + ZSTDds_decompressLastBlock, ZSTDds_checkChecksum, + ZSTDds_decodeSkippableHeader, ZSTDds_skipFrame } ZSTD_dStage; + +typedef enum { zdss_init=0, zdss_loadHeader, + zdss_read, zdss_load, zdss_flush } ZSTD_dStreamStage; + +typedef enum { + ZSTD_use_indefinitely = -1, /* Use the dictionary indefinitely */ + ZSTD_dont_use = 0, /* Do not use the dictionary (if one exists free it) */ + ZSTD_use_once = 1 /* Use the dictionary once and set to ZSTD_dont_use */ +} ZSTD_dictUses_e; + +/* Hashset for storing references to multiple ZSTD_DDict within ZSTD_DCtx */ +typedef struct { + const ZSTD_DDict** ddictPtrTable; + size_t ddictPtrTableSize; + size_t ddictPtrCount; +} ZSTD_DDictHashSet; + +#ifndef ZSTD_DECODER_INTERNAL_BUFFER +# define ZSTD_DECODER_INTERNAL_BUFFER (1 << 16) +#endif + +#define ZSTD_LBMIN 64 +#define ZSTD_LBMAX (128 << 10) + +/* extra buffer, compensates when dst is not large enough to store litBuffer */ +#define ZSTD_LITBUFFEREXTRASIZE BOUNDED(ZSTD_LBMIN, ZSTD_DECODER_INTERNAL_BUFFER, ZSTD_LBMAX) + +typedef enum { + ZSTD_not_in_dst = 0, /* Stored entirely within litExtraBuffer */ + ZSTD_in_dst = 1, /* Stored entirely within dst (in memory after current output write) */ + ZSTD_split = 2 /* Split between litExtraBuffer and dst */ +} ZSTD_litLocation_e; + +struct ZSTD_DCtx_s +{ + const ZSTD_seqSymbol* LLTptr; + const ZSTD_seqSymbol* MLTptr; + const ZSTD_seqSymbol* OFTptr; + const HUF_DTable* HUFptr; + ZSTD_entropyDTables_t entropy; + U32 workspace[HUF_DECOMPRESS_WORKSPACE_SIZE_U32]; /* space needed when building huffman tables */ + const void* previousDstEnd; /* detect continuity */ + const void* prefixStart; /* start of current segment */ + const void* virtualStart; /* virtual start of previous segment if it was just before current one */ + const void* dictEnd; /* end of previous segment */ + size_t expected; + ZSTD_frameHeader fParams; + U64 processedCSize; + U64 decodedSize; + blockType_e bType; /* used in ZSTD_decompressContinue(), store blockType between block header decoding and block decompression stages */ + ZSTD_dStage stage; + U32 litEntropy; + U32 fseEntropy; + XXH64_state_t xxhState; + size_t headerSize; + ZSTD_format_e format; + ZSTD_forceIgnoreChecksum_e forceIgnoreChecksum; /* User specified: if == 1, will ignore checksums in compressed frame. Default == 0 */ + U32 validateChecksum; /* if == 1, will validate checksum. Is == 1 if (fParams.checksumFlag == 1) and (forceIgnoreChecksum == 0). */ + const BYTE* litPtr; + ZSTD_customMem customMem; + size_t litSize; + size_t rleSize; + size_t staticSize; + int isFrameDecompression; +#if DYNAMIC_BMI2 != 0 + int bmi2; /* == 1 if the CPU supports BMI2 and 0 otherwise. CPU support is determined dynamically once per context lifetime. */ +#endif + + /* dictionary */ + ZSTD_DDict* ddictLocal; + const ZSTD_DDict* ddict; /* set by ZSTD_initDStream_usingDDict(), or ZSTD_DCtx_refDDict() */ + U32 dictID; + int ddictIsCold; /* if == 1 : dictionary is "new" for working context, and presumed "cold" (not in cpu cache) */ + ZSTD_dictUses_e dictUses; + ZSTD_DDictHashSet* ddictSet; /* Hash set for multiple ddicts */ + ZSTD_refMultipleDDicts_e refMultipleDDicts; /* User specified: if == 1, will allow references to multiple DDicts. Default == 0 (disabled) */ + int disableHufAsm; + int maxBlockSizeParam; + + /* streaming */ + ZSTD_dStreamStage streamStage; + char* inBuff; + size_t inBuffSize; + size_t inPos; + size_t maxWindowSize; + char* outBuff; + size_t outBuffSize; + size_t outStart; + size_t outEnd; + size_t lhSize; +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) + void* legacyContext; + U32 previousLegacyVersion; + U32 legacyVersion; +#endif + U32 hostageByte; + int noForwardProgress; + ZSTD_bufferMode_e outBufferMode; + ZSTD_outBuffer expectedOutBuffer; + + /* workspace */ + BYTE* litBuffer; + const BYTE* litBufferEnd; + ZSTD_litLocation_e litBufferLocation; + BYTE litExtraBuffer[ZSTD_LITBUFFEREXTRASIZE + WILDCOPY_OVERLENGTH]; /* literal buffer can be split between storage within dst and within this scratch buffer */ + BYTE headerBuffer[ZSTD_FRAMEHEADERSIZE_MAX]; + + size_t oversizedDuration; + +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + void const* dictContentBeginForFuzzing; + void const* dictContentEndForFuzzing; +#endif + + /* Tracing */ +#if ZSTD_TRACE + ZSTD_TraceCtx traceCtx; +#endif +}; /* typedef'd to ZSTD_DCtx within "zstd.h" */ + +MEM_STATIC int ZSTD_DCtx_get_bmi2(const struct ZSTD_DCtx_s *dctx) { +#if DYNAMIC_BMI2 != 0 + return dctx->bmi2; +#else + (void)dctx; + return 0; +#endif +} + +/*-******************************************************* + * Shared internal functions + *********************************************************/ + +/*! ZSTD_loadDEntropy() : + * dict : must point at beginning of a valid zstd dictionary. + * @return : size of dictionary header (size of magic number + dict ID + entropy tables) */ +size_t ZSTD_loadDEntropy(ZSTD_entropyDTables_t* entropy, + const void* const dict, size_t const dictSize); + +/*! ZSTD_checkContinuity() : + * check if next `dst` follows previous position, where decompression ended. + * If yes, do nothing (continue on current segment). + * If not, classify previous segment as "external dictionary", and start a new segment. + * This function cannot fail. */ +void ZSTD_checkContinuity(ZSTD_DCtx* dctx, const void* dst, size_t dstSize); + + +#endif /* ZSTD_DECOMPRESS_INTERNAL_H */ +/**** ended inlining zstd_decompress_internal.h ****/ +/**** start inlining zstd_ddict.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + +#ifndef ZSTD_DDICT_H +#define ZSTD_DDICT_H + +/*-******************************************************* + * Dependencies + *********************************************************/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../zstd.h ****/ + + +/*-******************************************************* + * Interface + *********************************************************/ + +/* note: several prototypes are already published in `zstd.h` : + * ZSTD_createDDict() + * ZSTD_createDDict_byReference() + * ZSTD_createDDict_advanced() + * ZSTD_freeDDict() + * ZSTD_initStaticDDict() + * ZSTD_sizeof_DDict() + * ZSTD_estimateDDictSize() + * ZSTD_getDictID_fromDict() + */ + +const void* ZSTD_DDict_dictContent(const ZSTD_DDict* ddict); +size_t ZSTD_DDict_dictSize(const ZSTD_DDict* ddict); + +void ZSTD_copyDDictParameters(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict); + + + +#endif /* ZSTD_DDICT_H */ +/**** ended inlining zstd_ddict.h ****/ + +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) +#error Using excluded file: ../legacy/zstd_legacy.h (re-amalgamate source to fix) +#endif + + + +/*-******************************************************* +* Types +*********************************************************/ +struct ZSTD_DDict_s { + void* dictBuffer; + const void* dictContent; + size_t dictSize; + ZSTD_entropyDTables_t entropy; + U32 dictID; + U32 entropyPresent; + ZSTD_customMem cMem; +}; /* typedef'd to ZSTD_DDict within "zstd.h" */ + +const void* ZSTD_DDict_dictContent(const ZSTD_DDict* ddict) +{ + assert(ddict != NULL); + return ddict->dictContent; +} + +size_t ZSTD_DDict_dictSize(const ZSTD_DDict* ddict) +{ + assert(ddict != NULL); + return ddict->dictSize; +} + +void ZSTD_copyDDictParameters(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict) +{ + DEBUGLOG(4, "ZSTD_copyDDictParameters"); + assert(dctx != NULL); + assert(ddict != NULL); + dctx->dictID = ddict->dictID; + dctx->prefixStart = ddict->dictContent; + dctx->virtualStart = ddict->dictContent; + dctx->dictEnd = (const BYTE*)ddict->dictContent + ddict->dictSize; + dctx->previousDstEnd = dctx->dictEnd; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + dctx->dictContentBeginForFuzzing = dctx->prefixStart; + dctx->dictContentEndForFuzzing = dctx->previousDstEnd; +#endif + if (ddict->entropyPresent) { + dctx->litEntropy = 1; + dctx->fseEntropy = 1; + dctx->LLTptr = ddict->entropy.LLTable; + dctx->MLTptr = ddict->entropy.MLTable; + dctx->OFTptr = ddict->entropy.OFTable; + dctx->HUFptr = ddict->entropy.hufTable; + dctx->entropy.rep[0] = ddict->entropy.rep[0]; + dctx->entropy.rep[1] = ddict->entropy.rep[1]; + dctx->entropy.rep[2] = ddict->entropy.rep[2]; + } else { + dctx->litEntropy = 0; + dctx->fseEntropy = 0; + } +} + + +static size_t +ZSTD_loadEntropy_intoDDict(ZSTD_DDict* ddict, + ZSTD_dictContentType_e dictContentType) +{ + ddict->dictID = 0; + ddict->entropyPresent = 0; + if (dictContentType == ZSTD_dct_rawContent) return 0; + + if (ddict->dictSize < 8) { + if (dictContentType == ZSTD_dct_fullDict) + return ERROR(dictionary_corrupted); /* only accept specified dictionaries */ + return 0; /* pure content mode */ + } + { U32 const magic = MEM_readLE32(ddict->dictContent); + if (magic != ZSTD_MAGIC_DICTIONARY) { + if (dictContentType == ZSTD_dct_fullDict) + return ERROR(dictionary_corrupted); /* only accept specified dictionaries */ + return 0; /* pure content mode */ + } + } + ddict->dictID = MEM_readLE32((const char*)ddict->dictContent + ZSTD_FRAMEIDSIZE); + + /* load entropy tables */ + RETURN_ERROR_IF(ZSTD_isError(ZSTD_loadDEntropy( + &ddict->entropy, ddict->dictContent, ddict->dictSize)), + dictionary_corrupted, ""); + ddict->entropyPresent = 1; + return 0; +} + + +static size_t ZSTD_initDDict_internal(ZSTD_DDict* ddict, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType) +{ + if ((dictLoadMethod == ZSTD_dlm_byRef) || (!dict) || (!dictSize)) { + ddict->dictBuffer = NULL; + ddict->dictContent = dict; + if (!dict) dictSize = 0; + } else { + void* const internalBuffer = ZSTD_customMalloc(dictSize, ddict->cMem); + ddict->dictBuffer = internalBuffer; + ddict->dictContent = internalBuffer; + if (!internalBuffer) return ERROR(memory_allocation); + ZSTD_memcpy(internalBuffer, dict, dictSize); + } + ddict->dictSize = dictSize; + ddict->entropy.hufTable[0] = (HUF_DTable)((ZSTD_HUFFDTABLE_CAPACITY_LOG)*0x1000001); /* cover both little and big endian */ + + /* parse dictionary content */ + FORWARD_IF_ERROR( ZSTD_loadEntropy_intoDDict(ddict, dictContentType) , ""); + + return 0; +} + +ZSTD_DDict* ZSTD_createDDict_advanced(const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_customMem customMem) +{ + if ((!customMem.customAlloc) ^ (!customMem.customFree)) return NULL; + + { ZSTD_DDict* const ddict = (ZSTD_DDict*) ZSTD_customMalloc(sizeof(ZSTD_DDict), customMem); + if (ddict == NULL) return NULL; + ddict->cMem = customMem; + { size_t const initResult = ZSTD_initDDict_internal(ddict, + dict, dictSize, + dictLoadMethod, dictContentType); + if (ZSTD_isError(initResult)) { + ZSTD_freeDDict(ddict); + return NULL; + } } + return ddict; + } +} + +/*! ZSTD_createDDict() : +* Create a digested dictionary, to start decompression without startup delay. +* `dict` content is copied inside DDict. +* Consequently, `dict` can be released after `ZSTD_DDict` creation */ +ZSTD_DDict* ZSTD_createDDict(const void* dict, size_t dictSize) +{ + ZSTD_customMem const allocator = { NULL, NULL, NULL }; + return ZSTD_createDDict_advanced(dict, dictSize, ZSTD_dlm_byCopy, ZSTD_dct_auto, allocator); +} + +/*! ZSTD_createDDict_byReference() : + * Create a digested dictionary, to start decompression without startup delay. + * Dictionary content is simply referenced, it will be accessed during decompression. + * Warning : dictBuffer must outlive DDict (DDict must be freed before dictBuffer) */ +ZSTD_DDict* ZSTD_createDDict_byReference(const void* dictBuffer, size_t dictSize) +{ + ZSTD_customMem const allocator = { NULL, NULL, NULL }; + return ZSTD_createDDict_advanced(dictBuffer, dictSize, ZSTD_dlm_byRef, ZSTD_dct_auto, allocator); +} + + +const ZSTD_DDict* ZSTD_initStaticDDict( + void* sBuffer, size_t sBufferSize, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType) +{ + size_t const neededSpace = sizeof(ZSTD_DDict) + + (dictLoadMethod == ZSTD_dlm_byRef ? 0 : dictSize); + ZSTD_DDict* const ddict = (ZSTD_DDict*)sBuffer; + assert(sBuffer != NULL); + assert(dict != NULL); + if ((size_t)sBuffer & 7) return NULL; /* 8-aligned */ + if (sBufferSize < neededSpace) return NULL; + if (dictLoadMethod == ZSTD_dlm_byCopy) { + ZSTD_memcpy(ddict+1, dict, dictSize); /* local copy */ + dict = ddict+1; + } + if (ZSTD_isError( ZSTD_initDDict_internal(ddict, + dict, dictSize, + ZSTD_dlm_byRef, dictContentType) )) + return NULL; + return ddict; +} + + +size_t ZSTD_freeDDict(ZSTD_DDict* ddict) +{ + if (ddict==NULL) return 0; /* support free on NULL */ + { ZSTD_customMem const cMem = ddict->cMem; + ZSTD_customFree(ddict->dictBuffer, cMem); + ZSTD_customFree(ddict, cMem); + return 0; + } +} + +/*! ZSTD_estimateDDictSize() : + * Estimate amount of memory that will be needed to create a dictionary for decompression. + * Note : dictionary created by reference using ZSTD_dlm_byRef are smaller */ +size_t ZSTD_estimateDDictSize(size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod) +{ + return sizeof(ZSTD_DDict) + (dictLoadMethod == ZSTD_dlm_byRef ? 0 : dictSize); +} + +size_t ZSTD_sizeof_DDict(const ZSTD_DDict* ddict) +{ + if (ddict==NULL) return 0; /* support sizeof on NULL */ + return sizeof(*ddict) + (ddict->dictBuffer ? ddict->dictSize : 0) ; +} + +/*! ZSTD_getDictID_fromDDict() : + * Provides the dictID of the dictionary loaded into `ddict`. + * If @return == 0, the dictionary is not conformant to Zstandard specification, or empty. + * Non-conformant dictionaries can still be loaded, but as content-only dictionaries. */ +unsigned ZSTD_getDictID_fromDDict(const ZSTD_DDict* ddict) +{ + if (ddict==NULL) return 0; + return ddict->dictID; +} +/**** ended inlining decompress/zstd_ddict.c ****/ +/**** start inlining decompress/zstd_decompress.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + +/* *************************************************************** +* Tuning parameters +*****************************************************************/ +/*! + * HEAPMODE : + * Select how default decompression function ZSTD_decompress() allocates its context, + * on stack (0), or into heap (1, default; requires malloc()). + * Note that functions with explicit context such as ZSTD_decompressDCtx() are unaffected. + */ +#ifndef ZSTD_HEAPMODE +# define ZSTD_HEAPMODE 1 +#endif + +/*! +* LEGACY_SUPPORT : +* if set to 1+, ZSTD_decompress() can decode older formats (v0.1+) +*/ +#ifndef ZSTD_LEGACY_SUPPORT +# define ZSTD_LEGACY_SUPPORT 0 +#endif + +/*! + * MAXWINDOWSIZE_DEFAULT : + * maximum window size accepted by DStream __by default__. + * Frames requiring more memory will be rejected. + * It's possible to set a different limit using ZSTD_DCtx_setMaxWindowSize(). + */ +#ifndef ZSTD_MAXWINDOWSIZE_DEFAULT +# define ZSTD_MAXWINDOWSIZE_DEFAULT (((U32)1 << ZSTD_WINDOWLOG_LIMIT_DEFAULT) + 1) +#endif + +/*! + * NO_FORWARD_PROGRESS_MAX : + * maximum allowed nb of calls to ZSTD_decompressStream() + * without any forward progress + * (defined as: no byte read from input, and no byte flushed to output) + * before triggering an error. + */ +#ifndef ZSTD_NO_FORWARD_PROGRESS_MAX +# define ZSTD_NO_FORWARD_PROGRESS_MAX 16 +#endif + + +/*-******************************************************* +* Dependencies +*********************************************************/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../common/allocations.h ****/ +/**** skipping file: ../common/error_private.h ****/ +/**** skipping file: ../common/zstd_internal.h ****/ +/**** skipping file: ../common/mem.h ****/ +/**** skipping file: ../common/bits.h ****/ +#define FSE_STATIC_LINKING_ONLY +/**** skipping file: ../common/fse.h ****/ +/**** skipping file: ../common/huf.h ****/ +/**** skipping file: ../common/xxhash.h ****/ +/**** skipping file: zstd_decompress_internal.h ****/ +/**** skipping file: zstd_ddict.h ****/ +/**** start inlining zstd_decompress_block.h ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + + +#ifndef ZSTD_DEC_BLOCK_H +#define ZSTD_DEC_BLOCK_H + +/*-******************************************************* + * Dependencies + *********************************************************/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../zstd.h ****/ +/**** skipping file: ../common/zstd_internal.h ****/ +/**** skipping file: zstd_decompress_internal.h ****/ + + +/* === Prototypes === */ + +/* note: prototypes already published within `zstd.h` : + * ZSTD_decompressBlock() + */ + +/* note: prototypes already published within `zstd_internal.h` : + * ZSTD_getcBlockSize() + * ZSTD_decodeSeqHeaders() + */ + + + /* Streaming state is used to inform allocation of the literal buffer */ +typedef enum { + not_streaming = 0, + is_streaming = 1 +} streaming_operation; + +/* ZSTD_decompressBlock_internal() : + * decompress block, starting at `src`, + * into destination buffer `dst`. + * @return : decompressed block size, + * or an error code (which can be tested using ZSTD_isError()) + */ +size_t ZSTD_decompressBlock_internal(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, const streaming_operation streaming); + +/* ZSTD_buildFSETable() : + * generate FSE decoding table for one symbol (ll, ml or off) + * this function must be called with valid parameters only + * (dt is large enough, normalizedCounter distribution total is a power of 2, max is within range, etc.) + * in which case it cannot fail. + * The workspace must be 4-byte aligned and at least ZSTD_BUILD_FSE_TABLE_WKSP_SIZE bytes, which is + * defined in zstd_decompress_internal.h. + * Internal use only. + */ +void ZSTD_buildFSETable(ZSTD_seqSymbol* dt, + const short* normalizedCounter, unsigned maxSymbolValue, + const U32* baseValue, const U8* nbAdditionalBits, + unsigned tableLog, void* wksp, size_t wkspSize, + int bmi2); + +/* Internal definition of ZSTD_decompressBlock() to avoid deprecation warnings. */ +size_t ZSTD_decompressBlock_deprecated(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize); + + +#endif /* ZSTD_DEC_BLOCK_H */ +/**** ended inlining zstd_decompress_block.h ****/ + +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) +#error Using excluded file: ../legacy/zstd_legacy.h (re-amalgamate source to fix) +#endif + + + +/************************************* + * Multiple DDicts Hashset internals * + *************************************/ + +#define DDICT_HASHSET_MAX_LOAD_FACTOR_COUNT_MULT 4 +#define DDICT_HASHSET_MAX_LOAD_FACTOR_SIZE_MULT 3 /* These two constants represent SIZE_MULT/COUNT_MULT load factor without using a float. + * Currently, that means a 0.75 load factor. + * So, if count * COUNT_MULT / size * SIZE_MULT != 0, then we've exceeded + * the load factor of the ddict hash set. + */ + +#define DDICT_HASHSET_TABLE_BASE_SIZE 64 +#define DDICT_HASHSET_RESIZE_FACTOR 2 + +/* Hash function to determine starting position of dict insertion within the table + * Returns an index between [0, hashSet->ddictPtrTableSize] + */ +static size_t ZSTD_DDictHashSet_getIndex(const ZSTD_DDictHashSet* hashSet, U32 dictID) { + const U64 hash = XXH64(&dictID, sizeof(U32), 0); + /* DDict ptr table size is a multiple of 2, use size - 1 as mask to get index within [0, hashSet->ddictPtrTableSize) */ + return hash & (hashSet->ddictPtrTableSize - 1); +} + +/* Adds DDict to a hashset without resizing it. + * If inserting a DDict with a dictID that already exists in the set, replaces the one in the set. + * Returns 0 if successful, or a zstd error code if something went wrong. + */ +static size_t ZSTD_DDictHashSet_emplaceDDict(ZSTD_DDictHashSet* hashSet, const ZSTD_DDict* ddict) { + const U32 dictID = ZSTD_getDictID_fromDDict(ddict); + size_t idx = ZSTD_DDictHashSet_getIndex(hashSet, dictID); + const size_t idxRangeMask = hashSet->ddictPtrTableSize - 1; + RETURN_ERROR_IF(hashSet->ddictPtrCount == hashSet->ddictPtrTableSize, GENERIC, "Hash set is full!"); + DEBUGLOG(4, "Hashed index: for dictID: %u is %zu", dictID, idx); + while (hashSet->ddictPtrTable[idx] != NULL) { + /* Replace existing ddict if inserting ddict with same dictID */ + if (ZSTD_getDictID_fromDDict(hashSet->ddictPtrTable[idx]) == dictID) { + DEBUGLOG(4, "DictID already exists, replacing rather than adding"); + hashSet->ddictPtrTable[idx] = ddict; + return 0; + } + idx &= idxRangeMask; + idx++; + } + DEBUGLOG(4, "Final idx after probing for dictID %u is: %zu", dictID, idx); + hashSet->ddictPtrTable[idx] = ddict; + hashSet->ddictPtrCount++; + return 0; +} + +/* Expands hash table by factor of DDICT_HASHSET_RESIZE_FACTOR and + * rehashes all values, allocates new table, frees old table. + * Returns 0 on success, otherwise a zstd error code. + */ +static size_t ZSTD_DDictHashSet_expand(ZSTD_DDictHashSet* hashSet, ZSTD_customMem customMem) { + size_t newTableSize = hashSet->ddictPtrTableSize * DDICT_HASHSET_RESIZE_FACTOR; + const ZSTD_DDict** newTable = (const ZSTD_DDict**)ZSTD_customCalloc(sizeof(ZSTD_DDict*) * newTableSize, customMem); + const ZSTD_DDict** oldTable = hashSet->ddictPtrTable; + size_t oldTableSize = hashSet->ddictPtrTableSize; + size_t i; + + DEBUGLOG(4, "Expanding DDict hash table! Old size: %zu new size: %zu", oldTableSize, newTableSize); + RETURN_ERROR_IF(!newTable, memory_allocation, "Expanded hashset allocation failed!"); + hashSet->ddictPtrTable = newTable; + hashSet->ddictPtrTableSize = newTableSize; + hashSet->ddictPtrCount = 0; + for (i = 0; i < oldTableSize; ++i) { + if (oldTable[i] != NULL) { + FORWARD_IF_ERROR(ZSTD_DDictHashSet_emplaceDDict(hashSet, oldTable[i]), ""); + } + } + ZSTD_customFree((void*)oldTable, customMem); + DEBUGLOG(4, "Finished re-hash"); + return 0; +} + +/* Fetches a DDict with the given dictID + * Returns the ZSTD_DDict* with the requested dictID. If it doesn't exist, then returns NULL. + */ +static const ZSTD_DDict* ZSTD_DDictHashSet_getDDict(ZSTD_DDictHashSet* hashSet, U32 dictID) { + size_t idx = ZSTD_DDictHashSet_getIndex(hashSet, dictID); + const size_t idxRangeMask = hashSet->ddictPtrTableSize - 1; + DEBUGLOG(4, "Hashed index: for dictID: %u is %zu", dictID, idx); + for (;;) { + size_t currDictID = ZSTD_getDictID_fromDDict(hashSet->ddictPtrTable[idx]); + if (currDictID == dictID || currDictID == 0) { + /* currDictID == 0 implies a NULL ddict entry */ + break; + } else { + idx &= idxRangeMask; /* Goes to start of table when we reach the end */ + idx++; + } + } + DEBUGLOG(4, "Final idx after probing for dictID %u is: %zu", dictID, idx); + return hashSet->ddictPtrTable[idx]; +} + +/* Allocates space for and returns a ddict hash set + * The hash set's ZSTD_DDict* table has all values automatically set to NULL to begin with. + * Returns NULL if allocation failed. + */ +static ZSTD_DDictHashSet* ZSTD_createDDictHashSet(ZSTD_customMem customMem) { + ZSTD_DDictHashSet* ret = (ZSTD_DDictHashSet*)ZSTD_customMalloc(sizeof(ZSTD_DDictHashSet), customMem); + DEBUGLOG(4, "Allocating new hash set"); + if (!ret) + return NULL; + ret->ddictPtrTable = (const ZSTD_DDict**)ZSTD_customCalloc(DDICT_HASHSET_TABLE_BASE_SIZE * sizeof(ZSTD_DDict*), customMem); + if (!ret->ddictPtrTable) { + ZSTD_customFree(ret, customMem); + return NULL; + } + ret->ddictPtrTableSize = DDICT_HASHSET_TABLE_BASE_SIZE; + ret->ddictPtrCount = 0; + return ret; +} + +/* Frees the table of ZSTD_DDict* within a hashset, then frees the hashset itself. + * Note: The ZSTD_DDict* within the table are NOT freed. + */ +static void ZSTD_freeDDictHashSet(ZSTD_DDictHashSet* hashSet, ZSTD_customMem customMem) { + DEBUGLOG(4, "Freeing ddict hash set"); + if (hashSet && hashSet->ddictPtrTable) { + ZSTD_customFree((void*)hashSet->ddictPtrTable, customMem); + } + if (hashSet) { + ZSTD_customFree(hashSet, customMem); + } +} + +/* Public function: Adds a DDict into the ZSTD_DDictHashSet, possibly triggering a resize of the hash set. + * Returns 0 on success, or a ZSTD error. + */ +static size_t ZSTD_DDictHashSet_addDDict(ZSTD_DDictHashSet* hashSet, const ZSTD_DDict* ddict, ZSTD_customMem customMem) { + DEBUGLOG(4, "Adding dict ID: %u to hashset with - Count: %zu Tablesize: %zu", ZSTD_getDictID_fromDDict(ddict), hashSet->ddictPtrCount, hashSet->ddictPtrTableSize); + if (hashSet->ddictPtrCount * DDICT_HASHSET_MAX_LOAD_FACTOR_COUNT_MULT / hashSet->ddictPtrTableSize * DDICT_HASHSET_MAX_LOAD_FACTOR_SIZE_MULT != 0) { + FORWARD_IF_ERROR(ZSTD_DDictHashSet_expand(hashSet, customMem), ""); + } + FORWARD_IF_ERROR(ZSTD_DDictHashSet_emplaceDDict(hashSet, ddict), ""); + return 0; +} + +/*-************************************************************* +* Context management +***************************************************************/ +size_t ZSTD_sizeof_DCtx (const ZSTD_DCtx* dctx) +{ + if (dctx==NULL) return 0; /* support sizeof NULL */ + return sizeof(*dctx) + + ZSTD_sizeof_DDict(dctx->ddictLocal) + + dctx->inBuffSize + dctx->outBuffSize; +} + +size_t ZSTD_estimateDCtxSize(void) { return sizeof(ZSTD_DCtx); } + + +static size_t ZSTD_startingInputLength(ZSTD_format_e format) +{ + size_t const startingInputLength = ZSTD_FRAMEHEADERSIZE_PREFIX(format); + /* only supports formats ZSTD_f_zstd1 and ZSTD_f_zstd1_magicless */ + assert( (format == ZSTD_f_zstd1) || (format == ZSTD_f_zstd1_magicless) ); + return startingInputLength; +} + +static void ZSTD_DCtx_resetParameters(ZSTD_DCtx* dctx) +{ + assert(dctx->streamStage == zdss_init); + dctx->format = ZSTD_f_zstd1; + dctx->maxWindowSize = ZSTD_MAXWINDOWSIZE_DEFAULT; + dctx->outBufferMode = ZSTD_bm_buffered; + dctx->forceIgnoreChecksum = ZSTD_d_validateChecksum; + dctx->refMultipleDDicts = ZSTD_rmd_refSingleDDict; + dctx->disableHufAsm = 0; + dctx->maxBlockSizeParam = 0; +} + +static void ZSTD_initDCtx_internal(ZSTD_DCtx* dctx) +{ + dctx->staticSize = 0; + dctx->ddict = NULL; + dctx->ddictLocal = NULL; + dctx->dictEnd = NULL; + dctx->ddictIsCold = 0; + dctx->dictUses = ZSTD_dont_use; + dctx->inBuff = NULL; + dctx->inBuffSize = 0; + dctx->outBuffSize = 0; + dctx->streamStage = zdss_init; +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) + dctx->legacyContext = NULL; + dctx->previousLegacyVersion = 0; +#endif + dctx->noForwardProgress = 0; + dctx->oversizedDuration = 0; + dctx->isFrameDecompression = 1; +#if DYNAMIC_BMI2 + dctx->bmi2 = ZSTD_cpuSupportsBmi2(); +#endif + dctx->ddictSet = NULL; + ZSTD_DCtx_resetParameters(dctx); +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + dctx->dictContentEndForFuzzing = NULL; +#endif +} + +ZSTD_DCtx* ZSTD_initStaticDCtx(void *workspace, size_t workspaceSize) +{ + ZSTD_DCtx* const dctx = (ZSTD_DCtx*) workspace; + + if ((size_t)workspace & 7) return NULL; /* 8-aligned */ + if (workspaceSize < sizeof(ZSTD_DCtx)) return NULL; /* minimum size */ + + ZSTD_initDCtx_internal(dctx); + dctx->staticSize = workspaceSize; + dctx->inBuff = (char*)(dctx+1); + return dctx; +} + +static ZSTD_DCtx* ZSTD_createDCtx_internal(ZSTD_customMem customMem) { + if ((!customMem.customAlloc) ^ (!customMem.customFree)) return NULL; + + { ZSTD_DCtx* const dctx = (ZSTD_DCtx*)ZSTD_customMalloc(sizeof(*dctx), customMem); + if (!dctx) return NULL; + dctx->customMem = customMem; + ZSTD_initDCtx_internal(dctx); + return dctx; + } +} + +ZSTD_DCtx* ZSTD_createDCtx_advanced(ZSTD_customMem customMem) +{ + return ZSTD_createDCtx_internal(customMem); +} + +ZSTD_DCtx* ZSTD_createDCtx(void) +{ + DEBUGLOG(3, "ZSTD_createDCtx"); + return ZSTD_createDCtx_internal(ZSTD_defaultCMem); +} + +static void ZSTD_clearDict(ZSTD_DCtx* dctx) +{ + ZSTD_freeDDict(dctx->ddictLocal); + dctx->ddictLocal = NULL; + dctx->ddict = NULL; + dctx->dictUses = ZSTD_dont_use; +} + +size_t ZSTD_freeDCtx(ZSTD_DCtx* dctx) +{ + if (dctx==NULL) return 0; /* support free on NULL */ + RETURN_ERROR_IF(dctx->staticSize, memory_allocation, "not compatible with static DCtx"); + { ZSTD_customMem const cMem = dctx->customMem; + ZSTD_clearDict(dctx); + ZSTD_customFree(dctx->inBuff, cMem); + dctx->inBuff = NULL; +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) + if (dctx->legacyContext) + ZSTD_freeLegacyStreamContext(dctx->legacyContext, dctx->previousLegacyVersion); +#endif + if (dctx->ddictSet) { + ZSTD_freeDDictHashSet(dctx->ddictSet, cMem); + dctx->ddictSet = NULL; + } + ZSTD_customFree(dctx, cMem); + return 0; + } +} + +/* no longer useful */ +void ZSTD_copyDCtx(ZSTD_DCtx* dstDCtx, const ZSTD_DCtx* srcDCtx) +{ + size_t const toCopy = (size_t)((char*)(&dstDCtx->inBuff) - (char*)dstDCtx); + ZSTD_memcpy(dstDCtx, srcDCtx, toCopy); /* no need to copy workspace */ +} + +/* Given a dctx with a digested frame params, re-selects the correct ZSTD_DDict based on + * the requested dict ID from the frame. If there exists a reference to the correct ZSTD_DDict, then + * accordingly sets the ddict to be used to decompress the frame. + * + * If no DDict is found, then no action is taken, and the ZSTD_DCtx::ddict remains as-is. + * + * ZSTD_d_refMultipleDDicts must be enabled for this function to be called. + */ +static void ZSTD_DCtx_selectFrameDDict(ZSTD_DCtx* dctx) { + assert(dctx->refMultipleDDicts && dctx->ddictSet); + DEBUGLOG(4, "Adjusting DDict based on requested dict ID from frame"); + if (dctx->ddict) { + const ZSTD_DDict* frameDDict = ZSTD_DDictHashSet_getDDict(dctx->ddictSet, dctx->fParams.dictID); + if (frameDDict) { + DEBUGLOG(4, "DDict found!"); + ZSTD_clearDict(dctx); + dctx->dictID = dctx->fParams.dictID; + dctx->ddict = frameDDict; + dctx->dictUses = ZSTD_use_indefinitely; + } + } +} + + +/*-************************************************************* + * Frame header decoding + ***************************************************************/ + +/*! ZSTD_isFrame() : + * Tells if the content of `buffer` starts with a valid Frame Identifier. + * Note : Frame Identifier is 4 bytes. If `size < 4`, @return will always be 0. + * Note 2 : Legacy Frame Identifiers are considered valid only if Legacy Support is enabled. + * Note 3 : Skippable Frame Identifiers are considered valid. */ +unsigned ZSTD_isFrame(const void* buffer, size_t size) +{ + if (size < ZSTD_FRAMEIDSIZE) return 0; + { U32 const magic = MEM_readLE32(buffer); + if (magic == ZSTD_MAGICNUMBER) return 1; + if ((magic & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) return 1; + } +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) + if (ZSTD_isLegacy(buffer, size)) return 1; +#endif + return 0; +} + +/*! ZSTD_isSkippableFrame() : + * Tells if the content of `buffer` starts with a valid Frame Identifier for a skippable frame. + * Note : Frame Identifier is 4 bytes. If `size < 4`, @return will always be 0. + */ +unsigned ZSTD_isSkippableFrame(const void* buffer, size_t size) +{ + if (size < ZSTD_FRAMEIDSIZE) return 0; + { U32 const magic = MEM_readLE32(buffer); + if ((magic & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) return 1; + } + return 0; +} + +/** ZSTD_frameHeaderSize_internal() : + * srcSize must be large enough to reach header size fields. + * note : only works for formats ZSTD_f_zstd1 and ZSTD_f_zstd1_magicless. + * @return : size of the Frame Header + * or an error code, which can be tested with ZSTD_isError() */ +static size_t ZSTD_frameHeaderSize_internal(const void* src, size_t srcSize, ZSTD_format_e format) +{ + size_t const minInputSize = ZSTD_startingInputLength(format); + RETURN_ERROR_IF(srcSize < minInputSize, srcSize_wrong, ""); + + { BYTE const fhd = ((const BYTE*)src)[minInputSize-1]; + U32 const dictID= fhd & 3; + U32 const singleSegment = (fhd >> 5) & 1; + U32 const fcsId = fhd >> 6; + return minInputSize + !singleSegment + + ZSTD_did_fieldSize[dictID] + ZSTD_fcs_fieldSize[fcsId] + + (singleSegment && !fcsId); + } +} + +/** ZSTD_frameHeaderSize() : + * srcSize must be >= ZSTD_frameHeaderSize_prefix. + * @return : size of the Frame Header, + * or an error code (if srcSize is too small) */ +size_t ZSTD_frameHeaderSize(const void* src, size_t srcSize) +{ + return ZSTD_frameHeaderSize_internal(src, srcSize, ZSTD_f_zstd1); +} + + +/** ZSTD_getFrameHeader_advanced() : + * decode Frame Header, or require larger `srcSize`. + * note : only works for formats ZSTD_f_zstd1 and ZSTD_f_zstd1_magicless + * @return : 0, `zfhPtr` is correctly filled, + * >0, `srcSize` is too small, value is wanted `srcSize` amount, +** or an error code, which can be tested using ZSTD_isError() */ +size_t ZSTD_getFrameHeader_advanced(ZSTD_frameHeader* zfhPtr, const void* src, size_t srcSize, ZSTD_format_e format) +{ + const BYTE* ip = (const BYTE*)src; + size_t const minInputSize = ZSTD_startingInputLength(format); + + DEBUGLOG(5, "ZSTD_getFrameHeader_advanced: minInputSize = %zu, srcSize = %zu", minInputSize, srcSize); + + if (srcSize > 0) { + /* note : technically could be considered an assert(), since it's an invalid entry */ + RETURN_ERROR_IF(src==NULL, GENERIC, "invalid parameter : src==NULL, but srcSize>0"); + } + if (srcSize < minInputSize) { + if (srcSize > 0 && format != ZSTD_f_zstd1_magicless) { + /* when receiving less than @minInputSize bytes, + * control these bytes at least correspond to a supported magic number + * in order to error out early if they don't. + **/ + size_t const toCopy = MIN(4, srcSize); + unsigned char hbuf[4]; MEM_writeLE32(hbuf, ZSTD_MAGICNUMBER); + assert(src != NULL); + ZSTD_memcpy(hbuf, src, toCopy); + if ( MEM_readLE32(hbuf) != ZSTD_MAGICNUMBER ) { + /* not a zstd frame : let's check if it's a skippable frame */ + MEM_writeLE32(hbuf, ZSTD_MAGIC_SKIPPABLE_START); + ZSTD_memcpy(hbuf, src, toCopy); + if ((MEM_readLE32(hbuf) & ZSTD_MAGIC_SKIPPABLE_MASK) != ZSTD_MAGIC_SKIPPABLE_START) { + RETURN_ERROR(prefix_unknown, + "first bytes don't correspond to any supported magic number"); + } } } + return minInputSize; + } + + ZSTD_memset(zfhPtr, 0, sizeof(*zfhPtr)); /* not strictly necessary, but static analyzers may not understand that zfhPtr will be read only if return value is zero, since they are 2 different signals */ + if ( (format != ZSTD_f_zstd1_magicless) + && (MEM_readLE32(src) != ZSTD_MAGICNUMBER) ) { + if ((MEM_readLE32(src) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) { + /* skippable frame */ + if (srcSize < ZSTD_SKIPPABLEHEADERSIZE) + return ZSTD_SKIPPABLEHEADERSIZE; /* magic number + frame length */ + ZSTD_memset(zfhPtr, 0, sizeof(*zfhPtr)); + zfhPtr->frameContentSize = MEM_readLE32((const char *)src + ZSTD_FRAMEIDSIZE); + zfhPtr->frameType = ZSTD_skippableFrame; + return 0; + } + RETURN_ERROR(prefix_unknown, ""); + } + + /* ensure there is enough `srcSize` to fully read/decode frame header */ + { size_t const fhsize = ZSTD_frameHeaderSize_internal(src, srcSize, format); + if (srcSize < fhsize) return fhsize; + zfhPtr->headerSize = (U32)fhsize; + } + + { BYTE const fhdByte = ip[minInputSize-1]; + size_t pos = minInputSize; + U32 const dictIDSizeCode = fhdByte&3; + U32 const checksumFlag = (fhdByte>>2)&1; + U32 const singleSegment = (fhdByte>>5)&1; + U32 const fcsID = fhdByte>>6; + U64 windowSize = 0; + U32 dictID = 0; + U64 frameContentSize = ZSTD_CONTENTSIZE_UNKNOWN; + RETURN_ERROR_IF((fhdByte & 0x08) != 0, frameParameter_unsupported, + "reserved bits, must be zero"); + + if (!singleSegment) { + BYTE const wlByte = ip[pos++]; + U32 const windowLog = (wlByte >> 3) + ZSTD_WINDOWLOG_ABSOLUTEMIN; + RETURN_ERROR_IF(windowLog > ZSTD_WINDOWLOG_MAX, frameParameter_windowTooLarge, ""); + windowSize = (1ULL << windowLog); + windowSize += (windowSize >> 3) * (wlByte&7); + } + switch(dictIDSizeCode) + { + default: + assert(0); /* impossible */ + ZSTD_FALLTHROUGH; + case 0 : break; + case 1 : dictID = ip[pos]; pos++; break; + case 2 : dictID = MEM_readLE16(ip+pos); pos+=2; break; + case 3 : dictID = MEM_readLE32(ip+pos); pos+=4; break; + } + switch(fcsID) + { + default: + assert(0); /* impossible */ + ZSTD_FALLTHROUGH; + case 0 : if (singleSegment) frameContentSize = ip[pos]; break; + case 1 : frameContentSize = MEM_readLE16(ip+pos)+256; break; + case 2 : frameContentSize = MEM_readLE32(ip+pos); break; + case 3 : frameContentSize = MEM_readLE64(ip+pos); break; + } + if (singleSegment) windowSize = frameContentSize; + + zfhPtr->frameType = ZSTD_frame; + zfhPtr->frameContentSize = frameContentSize; + zfhPtr->windowSize = windowSize; + zfhPtr->blockSizeMax = (unsigned) MIN(windowSize, ZSTD_BLOCKSIZE_MAX); + zfhPtr->dictID = dictID; + zfhPtr->checksumFlag = checksumFlag; + } + return 0; +} + +/** ZSTD_getFrameHeader() : + * decode Frame Header, or require larger `srcSize`. + * note : this function does not consume input, it only reads it. + * @return : 0, `zfhPtr` is correctly filled, + * >0, `srcSize` is too small, value is wanted `srcSize` amount, + * or an error code, which can be tested using ZSTD_isError() */ +size_t ZSTD_getFrameHeader(ZSTD_frameHeader* zfhPtr, const void* src, size_t srcSize) +{ + return ZSTD_getFrameHeader_advanced(zfhPtr, src, srcSize, ZSTD_f_zstd1); +} + +/** ZSTD_getFrameContentSize() : + * compatible with legacy mode + * @return : decompressed size of the single frame pointed to be `src` if known, otherwise + * - ZSTD_CONTENTSIZE_UNKNOWN if the size cannot be determined + * - ZSTD_CONTENTSIZE_ERROR if an error occurred (e.g. invalid magic number, srcSize too small) */ +unsigned long long ZSTD_getFrameContentSize(const void *src, size_t srcSize) +{ +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) + if (ZSTD_isLegacy(src, srcSize)) { + unsigned long long const ret = ZSTD_getDecompressedSize_legacy(src, srcSize); + return ret == 0 ? ZSTD_CONTENTSIZE_UNKNOWN : ret; + } +#endif + { ZSTD_frameHeader zfh; + if (ZSTD_getFrameHeader(&zfh, src, srcSize) != 0) + return ZSTD_CONTENTSIZE_ERROR; + if (zfh.frameType == ZSTD_skippableFrame) { + return 0; + } else { + return zfh.frameContentSize; + } } +} + +static size_t readSkippableFrameSize(void const* src, size_t srcSize) +{ + size_t const skippableHeaderSize = ZSTD_SKIPPABLEHEADERSIZE; + U32 sizeU32; + + RETURN_ERROR_IF(srcSize < ZSTD_SKIPPABLEHEADERSIZE, srcSize_wrong, ""); + + sizeU32 = MEM_readLE32((BYTE const*)src + ZSTD_FRAMEIDSIZE); + RETURN_ERROR_IF((U32)(sizeU32 + ZSTD_SKIPPABLEHEADERSIZE) < sizeU32, + frameParameter_unsupported, ""); + { size_t const skippableSize = skippableHeaderSize + sizeU32; + RETURN_ERROR_IF(skippableSize > srcSize, srcSize_wrong, ""); + return skippableSize; + } +} + +/*! ZSTD_readSkippableFrame() : + * Retrieves content of a skippable frame, and writes it to dst buffer. + * + * The parameter magicVariant will receive the magicVariant that was supplied when the frame was written, + * i.e. magicNumber - ZSTD_MAGIC_SKIPPABLE_START. This can be NULL if the caller is not interested + * in the magicVariant. + * + * Returns an error if destination buffer is not large enough, or if this is not a valid skippable frame. + * + * @return : number of bytes written or a ZSTD error. + */ +size_t ZSTD_readSkippableFrame(void* dst, size_t dstCapacity, + unsigned* magicVariant, /* optional, can be NULL */ + const void* src, size_t srcSize) +{ + RETURN_ERROR_IF(srcSize < ZSTD_SKIPPABLEHEADERSIZE, srcSize_wrong, ""); + + { U32 const magicNumber = MEM_readLE32(src); + size_t skippableFrameSize = readSkippableFrameSize(src, srcSize); + size_t skippableContentSize = skippableFrameSize - ZSTD_SKIPPABLEHEADERSIZE; + + /* check input validity */ + RETURN_ERROR_IF(!ZSTD_isSkippableFrame(src, srcSize), frameParameter_unsupported, ""); + RETURN_ERROR_IF(skippableFrameSize < ZSTD_SKIPPABLEHEADERSIZE || skippableFrameSize > srcSize, srcSize_wrong, ""); + RETURN_ERROR_IF(skippableContentSize > dstCapacity, dstSize_tooSmall, ""); + + /* deliver payload */ + if (skippableContentSize > 0 && dst != NULL) + ZSTD_memcpy(dst, (const BYTE *)src + ZSTD_SKIPPABLEHEADERSIZE, skippableContentSize); + if (magicVariant != NULL) + *magicVariant = magicNumber - ZSTD_MAGIC_SKIPPABLE_START; + return skippableContentSize; + } +} + +/** ZSTD_findDecompressedSize() : + * `srcSize` must be the exact length of some number of ZSTD compressed and/or + * skippable frames + * note: compatible with legacy mode + * @return : decompressed size of the frames contained */ +unsigned long long ZSTD_findDecompressedSize(const void* src, size_t srcSize) +{ + unsigned long long totalDstSize = 0; + + while (srcSize >= ZSTD_startingInputLength(ZSTD_f_zstd1)) { + U32 const magicNumber = MEM_readLE32(src); + + if ((magicNumber & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) { + size_t const skippableSize = readSkippableFrameSize(src, srcSize); + if (ZSTD_isError(skippableSize)) return ZSTD_CONTENTSIZE_ERROR; + assert(skippableSize <= srcSize); + + src = (const BYTE *)src + skippableSize; + srcSize -= skippableSize; + continue; + } + + { unsigned long long const fcs = ZSTD_getFrameContentSize(src, srcSize); + if (fcs >= ZSTD_CONTENTSIZE_ERROR) return fcs; + + if (totalDstSize + fcs < totalDstSize) + return ZSTD_CONTENTSIZE_ERROR; /* check for overflow */ + totalDstSize += fcs; + } + /* skip to next frame */ + { size_t const frameSrcSize = ZSTD_findFrameCompressedSize(src, srcSize); + if (ZSTD_isError(frameSrcSize)) return ZSTD_CONTENTSIZE_ERROR; + assert(frameSrcSize <= srcSize); + + src = (const BYTE *)src + frameSrcSize; + srcSize -= frameSrcSize; + } + } /* while (srcSize >= ZSTD_frameHeaderSize_prefix) */ + + if (srcSize) return ZSTD_CONTENTSIZE_ERROR; + + return totalDstSize; +} + +/** ZSTD_getDecompressedSize() : + * compatible with legacy mode + * @return : decompressed size if known, 0 otherwise + note : 0 can mean any of the following : + - frame content is empty + - decompressed size field is not present in frame header + - frame header unknown / not supported + - frame header not complete (`srcSize` too small) */ +unsigned long long ZSTD_getDecompressedSize(const void* src, size_t srcSize) +{ + unsigned long long const ret = ZSTD_getFrameContentSize(src, srcSize); + ZSTD_STATIC_ASSERT(ZSTD_CONTENTSIZE_ERROR < ZSTD_CONTENTSIZE_UNKNOWN); + return (ret >= ZSTD_CONTENTSIZE_ERROR) ? 0 : ret; +} + + +/** ZSTD_decodeFrameHeader() : + * `headerSize` must be the size provided by ZSTD_frameHeaderSize(). + * If multiple DDict references are enabled, also will choose the correct DDict to use. + * @return : 0 if success, or an error code, which can be tested using ZSTD_isError() */ +static size_t ZSTD_decodeFrameHeader(ZSTD_DCtx* dctx, const void* src, size_t headerSize) +{ + size_t const result = ZSTD_getFrameHeader_advanced(&(dctx->fParams), src, headerSize, dctx->format); + if (ZSTD_isError(result)) return result; /* invalid header */ + RETURN_ERROR_IF(result>0, srcSize_wrong, "headerSize too small"); + + /* Reference DDict requested by frame if dctx references multiple ddicts */ + if (dctx->refMultipleDDicts == ZSTD_rmd_refMultipleDDicts && dctx->ddictSet) { + ZSTD_DCtx_selectFrameDDict(dctx); + } + +#ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + /* Skip the dictID check in fuzzing mode, because it makes the search + * harder. + */ + RETURN_ERROR_IF(dctx->fParams.dictID && (dctx->dictID != dctx->fParams.dictID), + dictionary_wrong, ""); +#endif + dctx->validateChecksum = (dctx->fParams.checksumFlag && !dctx->forceIgnoreChecksum) ? 1 : 0; + if (dctx->validateChecksum) XXH64_reset(&dctx->xxhState, 0); + dctx->processedCSize += headerSize; + return 0; +} + +static ZSTD_frameSizeInfo ZSTD_errorFrameSizeInfo(size_t ret) +{ + ZSTD_frameSizeInfo frameSizeInfo; + frameSizeInfo.compressedSize = ret; + frameSizeInfo.decompressedBound = ZSTD_CONTENTSIZE_ERROR; + return frameSizeInfo; +} + +static ZSTD_frameSizeInfo ZSTD_findFrameSizeInfo(const void* src, size_t srcSize, ZSTD_format_e format) +{ + ZSTD_frameSizeInfo frameSizeInfo; + ZSTD_memset(&frameSizeInfo, 0, sizeof(ZSTD_frameSizeInfo)); + +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) + if (format == ZSTD_f_zstd1 && ZSTD_isLegacy(src, srcSize)) + return ZSTD_findFrameSizeInfoLegacy(src, srcSize); +#endif + + if (format == ZSTD_f_zstd1 && (srcSize >= ZSTD_SKIPPABLEHEADERSIZE) + && (MEM_readLE32(src) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) { + frameSizeInfo.compressedSize = readSkippableFrameSize(src, srcSize); + assert(ZSTD_isError(frameSizeInfo.compressedSize) || + frameSizeInfo.compressedSize <= srcSize); + return frameSizeInfo; + } else { + const BYTE* ip = (const BYTE*)src; + const BYTE* const ipstart = ip; + size_t remainingSize = srcSize; + size_t nbBlocks = 0; + ZSTD_frameHeader zfh; + + /* Extract Frame Header */ + { size_t const ret = ZSTD_getFrameHeader_advanced(&zfh, src, srcSize, format); + if (ZSTD_isError(ret)) + return ZSTD_errorFrameSizeInfo(ret); + if (ret > 0) + return ZSTD_errorFrameSizeInfo(ERROR(srcSize_wrong)); + } + + ip += zfh.headerSize; + remainingSize -= zfh.headerSize; + + /* Iterate over each block */ + while (1) { + blockProperties_t blockProperties; + size_t const cBlockSize = ZSTD_getcBlockSize(ip, remainingSize, &blockProperties); + if (ZSTD_isError(cBlockSize)) + return ZSTD_errorFrameSizeInfo(cBlockSize); + + if (ZSTD_blockHeaderSize + cBlockSize > remainingSize) + return ZSTD_errorFrameSizeInfo(ERROR(srcSize_wrong)); + + ip += ZSTD_blockHeaderSize + cBlockSize; + remainingSize -= ZSTD_blockHeaderSize + cBlockSize; + nbBlocks++; + + if (blockProperties.lastBlock) break; + } + + /* Final frame content checksum */ + if (zfh.checksumFlag) { + if (remainingSize < 4) + return ZSTD_errorFrameSizeInfo(ERROR(srcSize_wrong)); + ip += 4; + } + + frameSizeInfo.nbBlocks = nbBlocks; + frameSizeInfo.compressedSize = (size_t)(ip - ipstart); + frameSizeInfo.decompressedBound = (zfh.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN) + ? zfh.frameContentSize + : (unsigned long long)nbBlocks * zfh.blockSizeMax; + return frameSizeInfo; + } +} + +static size_t ZSTD_findFrameCompressedSize_advanced(const void *src, size_t srcSize, ZSTD_format_e format) { + ZSTD_frameSizeInfo const frameSizeInfo = ZSTD_findFrameSizeInfo(src, srcSize, format); + return frameSizeInfo.compressedSize; +} + +/** ZSTD_findFrameCompressedSize() : + * See docs in zstd.h + * Note: compatible with legacy mode */ +size_t ZSTD_findFrameCompressedSize(const void *src, size_t srcSize) +{ + return ZSTD_findFrameCompressedSize_advanced(src, srcSize, ZSTD_f_zstd1); +} + +/** ZSTD_decompressBound() : + * compatible with legacy mode + * `src` must point to the start of a ZSTD frame or a skippeable frame + * `srcSize` must be at least as large as the frame contained + * @return : the maximum decompressed size of the compressed source + */ +unsigned long long ZSTD_decompressBound(const void* src, size_t srcSize) +{ + unsigned long long bound = 0; + /* Iterate over each frame */ + while (srcSize > 0) { + ZSTD_frameSizeInfo const frameSizeInfo = ZSTD_findFrameSizeInfo(src, srcSize, ZSTD_f_zstd1); + size_t const compressedSize = frameSizeInfo.compressedSize; + unsigned long long const decompressedBound = frameSizeInfo.decompressedBound; + if (ZSTD_isError(compressedSize) || decompressedBound == ZSTD_CONTENTSIZE_ERROR) + return ZSTD_CONTENTSIZE_ERROR; + assert(srcSize >= compressedSize); + src = (const BYTE*)src + compressedSize; + srcSize -= compressedSize; + bound += decompressedBound; + } + return bound; +} + +size_t ZSTD_decompressionMargin(void const* src, size_t srcSize) +{ + size_t margin = 0; + unsigned maxBlockSize = 0; + + /* Iterate over each frame */ + while (srcSize > 0) { + ZSTD_frameSizeInfo const frameSizeInfo = ZSTD_findFrameSizeInfo(src, srcSize, ZSTD_f_zstd1); + size_t const compressedSize = frameSizeInfo.compressedSize; + unsigned long long const decompressedBound = frameSizeInfo.decompressedBound; + ZSTD_frameHeader zfh; + + FORWARD_IF_ERROR(ZSTD_getFrameHeader(&zfh, src, srcSize), ""); + if (ZSTD_isError(compressedSize) || decompressedBound == ZSTD_CONTENTSIZE_ERROR) + return ERROR(corruption_detected); + + if (zfh.frameType == ZSTD_frame) { + /* Add the frame header to our margin */ + margin += zfh.headerSize; + /* Add the checksum to our margin */ + margin += zfh.checksumFlag ? 4 : 0; + /* Add 3 bytes per block */ + margin += 3 * frameSizeInfo.nbBlocks; + + /* Compute the max block size */ + maxBlockSize = MAX(maxBlockSize, zfh.blockSizeMax); + } else { + assert(zfh.frameType == ZSTD_skippableFrame); + /* Add the entire skippable frame size to our margin. */ + margin += compressedSize; + } + + assert(srcSize >= compressedSize); + src = (const BYTE*)src + compressedSize; + srcSize -= compressedSize; + } + + /* Add the max block size back to the margin. */ + margin += maxBlockSize; + + return margin; +} + +/*-************************************************************* + * Frame decoding + ***************************************************************/ + +/** ZSTD_insertBlock() : + * insert `src` block into `dctx` history. Useful to track uncompressed blocks. */ +size_t ZSTD_insertBlock(ZSTD_DCtx* dctx, const void* blockStart, size_t blockSize) +{ + DEBUGLOG(5, "ZSTD_insertBlock: %u bytes", (unsigned)blockSize); + ZSTD_checkContinuity(dctx, blockStart, blockSize); + dctx->previousDstEnd = (const char*)blockStart + blockSize; + return blockSize; +} + + +static size_t ZSTD_copyRawBlock(void* dst, size_t dstCapacity, + const void* src, size_t srcSize) +{ + DEBUGLOG(5, "ZSTD_copyRawBlock"); + RETURN_ERROR_IF(srcSize > dstCapacity, dstSize_tooSmall, ""); + if (dst == NULL) { + if (srcSize == 0) return 0; + RETURN_ERROR(dstBuffer_null, ""); + } + ZSTD_memmove(dst, src, srcSize); + return srcSize; +} + +static size_t ZSTD_setRleBlock(void* dst, size_t dstCapacity, + BYTE b, + size_t regenSize) +{ + RETURN_ERROR_IF(regenSize > dstCapacity, dstSize_tooSmall, ""); + if (dst == NULL) { + if (regenSize == 0) return 0; + RETURN_ERROR(dstBuffer_null, ""); + } + ZSTD_memset(dst, b, regenSize); + return regenSize; +} + +static void ZSTD_DCtx_trace_end(ZSTD_DCtx const* dctx, U64 uncompressedSize, U64 compressedSize, unsigned streaming) +{ +#if ZSTD_TRACE + if (dctx->traceCtx && ZSTD_trace_decompress_end != NULL) { + ZSTD_Trace trace; + ZSTD_memset(&trace, 0, sizeof(trace)); + trace.version = ZSTD_VERSION_NUMBER; + trace.streaming = streaming; + if (dctx->ddict) { + trace.dictionaryID = ZSTD_getDictID_fromDDict(dctx->ddict); + trace.dictionarySize = ZSTD_DDict_dictSize(dctx->ddict); + trace.dictionaryIsCold = dctx->ddictIsCold; + } + trace.uncompressedSize = (size_t)uncompressedSize; + trace.compressedSize = (size_t)compressedSize; + trace.dctx = dctx; + ZSTD_trace_decompress_end(dctx->traceCtx, &trace); + } +#else + (void)dctx; + (void)uncompressedSize; + (void)compressedSize; + (void)streaming; +#endif +} + + +/*! ZSTD_decompressFrame() : + * @dctx must be properly initialized + * will update *srcPtr and *srcSizePtr, + * to make *srcPtr progress by one frame. */ +static size_t ZSTD_decompressFrame(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void** srcPtr, size_t *srcSizePtr) +{ + const BYTE* const istart = (const BYTE*)(*srcPtr); + const BYTE* ip = istart; + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = dstCapacity != 0 ? ostart + dstCapacity : ostart; + BYTE* op = ostart; + size_t remainingSrcSize = *srcSizePtr; + + DEBUGLOG(4, "ZSTD_decompressFrame (srcSize:%i)", (int)*srcSizePtr); + + /* check */ + RETURN_ERROR_IF( + remainingSrcSize < ZSTD_FRAMEHEADERSIZE_MIN(dctx->format)+ZSTD_blockHeaderSize, + srcSize_wrong, ""); + + /* Frame Header */ + { size_t const frameHeaderSize = ZSTD_frameHeaderSize_internal( + ip, ZSTD_FRAMEHEADERSIZE_PREFIX(dctx->format), dctx->format); + if (ZSTD_isError(frameHeaderSize)) return frameHeaderSize; + RETURN_ERROR_IF(remainingSrcSize < frameHeaderSize+ZSTD_blockHeaderSize, + srcSize_wrong, ""); + FORWARD_IF_ERROR( ZSTD_decodeFrameHeader(dctx, ip, frameHeaderSize) , ""); + ip += frameHeaderSize; remainingSrcSize -= frameHeaderSize; + } + + /* Shrink the blockSizeMax if enabled */ + if (dctx->maxBlockSizeParam != 0) + dctx->fParams.blockSizeMax = MIN(dctx->fParams.blockSizeMax, (unsigned)dctx->maxBlockSizeParam); + + /* Loop on each block */ + while (1) { + BYTE* oBlockEnd = oend; + size_t decodedSize; + blockProperties_t blockProperties; + size_t const cBlockSize = ZSTD_getcBlockSize(ip, remainingSrcSize, &blockProperties); + if (ZSTD_isError(cBlockSize)) return cBlockSize; + + ip += ZSTD_blockHeaderSize; + remainingSrcSize -= ZSTD_blockHeaderSize; + RETURN_ERROR_IF(cBlockSize > remainingSrcSize, srcSize_wrong, ""); + + if (ip >= op && ip < oBlockEnd) { + /* We are decompressing in-place. Limit the output pointer so that we + * don't overwrite the block that we are currently reading. This will + * fail decompression if the input & output pointers aren't spaced + * far enough apart. + * + * This is important to set, even when the pointers are far enough + * apart, because ZSTD_decompressBlock_internal() can decide to store + * literals in the output buffer, after the block it is decompressing. + * Since we don't want anything to overwrite our input, we have to tell + * ZSTD_decompressBlock_internal to never write past ip. + * + * See ZSTD_allocateLiteralsBuffer() for reference. + */ + oBlockEnd = op + (ip - op); + } + + switch(blockProperties.blockType) + { + case bt_compressed: + assert(dctx->isFrameDecompression == 1); + decodedSize = ZSTD_decompressBlock_internal(dctx, op, (size_t)(oBlockEnd-op), ip, cBlockSize, not_streaming); + break; + case bt_raw : + /* Use oend instead of oBlockEnd because this function is safe to overlap. It uses memmove. */ + decodedSize = ZSTD_copyRawBlock(op, (size_t)(oend-op), ip, cBlockSize); + break; + case bt_rle : + decodedSize = ZSTD_setRleBlock(op, (size_t)(oBlockEnd-op), *ip, blockProperties.origSize); + break; + case bt_reserved : + default: + RETURN_ERROR(corruption_detected, "invalid block type"); + } + FORWARD_IF_ERROR(decodedSize, "Block decompression failure"); + DEBUGLOG(5, "Decompressed block of dSize = %u", (unsigned)decodedSize); + if (dctx->validateChecksum) { + XXH64_update(&dctx->xxhState, op, decodedSize); + } + if (decodedSize) /* support dst = NULL,0 */ { + op += decodedSize; + } + assert(ip != NULL); + ip += cBlockSize; + remainingSrcSize -= cBlockSize; + if (blockProperties.lastBlock) break; + } + + if (dctx->fParams.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN) { + RETURN_ERROR_IF((U64)(op-ostart) != dctx->fParams.frameContentSize, + corruption_detected, ""); + } + if (dctx->fParams.checksumFlag) { /* Frame content checksum verification */ + RETURN_ERROR_IF(remainingSrcSize<4, checksum_wrong, ""); + if (!dctx->forceIgnoreChecksum) { + U32 const checkCalc = (U32)XXH64_digest(&dctx->xxhState); + U32 checkRead; + checkRead = MEM_readLE32(ip); + RETURN_ERROR_IF(checkRead != checkCalc, checksum_wrong, ""); + } + ip += 4; + remainingSrcSize -= 4; + } + ZSTD_DCtx_trace_end(dctx, (U64)(op-ostart), (U64)(ip-istart), /* streaming */ 0); + /* Allow caller to get size read */ + DEBUGLOG(4, "ZSTD_decompressFrame: decompressed frame of size %zi, consuming %zi bytes of input", op-ostart, ip - (const BYTE*)*srcPtr); + *srcPtr = ip; + *srcSizePtr = remainingSrcSize; + return (size_t)(op-ostart); +} + +static +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_decompressMultiFrame(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict, size_t dictSize, + const ZSTD_DDict* ddict) +{ + void* const dststart = dst; + int moreThan1Frame = 0; + + DEBUGLOG(5, "ZSTD_decompressMultiFrame"); + assert(dict==NULL || ddict==NULL); /* either dict or ddict set, not both */ + + if (ddict) { + dict = ZSTD_DDict_dictContent(ddict); + dictSize = ZSTD_DDict_dictSize(ddict); + } + + while (srcSize >= ZSTD_startingInputLength(dctx->format)) { + +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) + if (dctx->format == ZSTD_f_zstd1 && ZSTD_isLegacy(src, srcSize)) { + size_t decodedSize; + size_t const frameSize = ZSTD_findFrameCompressedSizeLegacy(src, srcSize); + if (ZSTD_isError(frameSize)) return frameSize; + RETURN_ERROR_IF(dctx->staticSize, memory_allocation, + "legacy support is not compatible with static dctx"); + + decodedSize = ZSTD_decompressLegacy(dst, dstCapacity, src, frameSize, dict, dictSize); + if (ZSTD_isError(decodedSize)) return decodedSize; + + { + unsigned long long const expectedSize = ZSTD_getFrameContentSize(src, srcSize); + RETURN_ERROR_IF(expectedSize == ZSTD_CONTENTSIZE_ERROR, corruption_detected, "Corrupted frame header!"); + if (expectedSize != ZSTD_CONTENTSIZE_UNKNOWN) { + RETURN_ERROR_IF(expectedSize != decodedSize, corruption_detected, + "Frame header size does not match decoded size!"); + } + } + + assert(decodedSize <= dstCapacity); + dst = (BYTE*)dst + decodedSize; + dstCapacity -= decodedSize; + + src = (const BYTE*)src + frameSize; + srcSize -= frameSize; + + continue; + } +#endif + + if (dctx->format == ZSTD_f_zstd1 && srcSize >= 4) { + U32 const magicNumber = MEM_readLE32(src); + DEBUGLOG(5, "reading magic number %08X", (unsigned)magicNumber); + if ((magicNumber & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) { + /* skippable frame detected : skip it */ + size_t const skippableSize = readSkippableFrameSize(src, srcSize); + FORWARD_IF_ERROR(skippableSize, "invalid skippable frame"); + assert(skippableSize <= srcSize); + + src = (const BYTE *)src + skippableSize; + srcSize -= skippableSize; + continue; /* check next frame */ + } } + + if (ddict) { + /* we were called from ZSTD_decompress_usingDDict */ + FORWARD_IF_ERROR(ZSTD_decompressBegin_usingDDict(dctx, ddict), ""); + } else { + /* this will initialize correctly with no dict if dict == NULL, so + * use this in all cases but ddict */ + FORWARD_IF_ERROR(ZSTD_decompressBegin_usingDict(dctx, dict, dictSize), ""); + } + ZSTD_checkContinuity(dctx, dst, dstCapacity); + + { const size_t res = ZSTD_decompressFrame(dctx, dst, dstCapacity, + &src, &srcSize); + RETURN_ERROR_IF( + (ZSTD_getErrorCode(res) == ZSTD_error_prefix_unknown) + && (moreThan1Frame==1), + srcSize_wrong, + "At least one frame successfully completed, " + "but following bytes are garbage: " + "it's more likely to be a srcSize error, " + "specifying more input bytes than size of frame(s). " + "Note: one could be unlucky, it might be a corruption error instead, " + "happening right at the place where we expect zstd magic bytes. " + "But this is _much_ less likely than a srcSize field error."); + if (ZSTD_isError(res)) return res; + assert(res <= dstCapacity); + if (res != 0) + dst = (BYTE*)dst + res; + dstCapacity -= res; + } + moreThan1Frame = 1; + } /* while (srcSize >= ZSTD_frameHeaderSize_prefix) */ + + RETURN_ERROR_IF(srcSize, srcSize_wrong, "input not entirely consumed"); + + return (size_t)((BYTE*)dst - (BYTE*)dststart); +} + +size_t ZSTD_decompress_usingDict(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict, size_t dictSize) +{ + return ZSTD_decompressMultiFrame(dctx, dst, dstCapacity, src, srcSize, dict, dictSize, NULL); +} + + +static ZSTD_DDict const* ZSTD_getDDict(ZSTD_DCtx* dctx) +{ + switch (dctx->dictUses) { + default: + assert(0 /* Impossible */); + ZSTD_FALLTHROUGH; + case ZSTD_dont_use: + ZSTD_clearDict(dctx); + return NULL; + case ZSTD_use_indefinitely: + return dctx->ddict; + case ZSTD_use_once: + dctx->dictUses = ZSTD_dont_use; + return dctx->ddict; + } +} + +size_t ZSTD_decompressDCtx(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize) +{ + return ZSTD_decompress_usingDDict(dctx, dst, dstCapacity, src, srcSize, ZSTD_getDDict(dctx)); +} + + +size_t ZSTD_decompress(void* dst, size_t dstCapacity, const void* src, size_t srcSize) +{ +#if defined(ZSTD_HEAPMODE) && (ZSTD_HEAPMODE>=1) + size_t regenSize; + ZSTD_DCtx* const dctx = ZSTD_createDCtx_internal(ZSTD_defaultCMem); + RETURN_ERROR_IF(dctx==NULL, memory_allocation, "NULL pointer!"); + regenSize = ZSTD_decompressDCtx(dctx, dst, dstCapacity, src, srcSize); + ZSTD_freeDCtx(dctx); + return regenSize; +#else /* stack mode */ + ZSTD_DCtx dctx; + ZSTD_initDCtx_internal(&dctx); + return ZSTD_decompressDCtx(&dctx, dst, dstCapacity, src, srcSize); +#endif +} + + +/*-************************************** +* Advanced Streaming Decompression API +* Bufferless and synchronous +****************************************/ +size_t ZSTD_nextSrcSizeToDecompress(ZSTD_DCtx* dctx) { return dctx->expected; } + +/** + * Similar to ZSTD_nextSrcSizeToDecompress(), but when a block input can be streamed, we + * allow taking a partial block as the input. Currently only raw uncompressed blocks can + * be streamed. + * + * For blocks that can be streamed, this allows us to reduce the latency until we produce + * output, and avoid copying the input. + * + * @param inputSize - The total amount of input that the caller currently has. + */ +static size_t ZSTD_nextSrcSizeToDecompressWithInputSize(ZSTD_DCtx* dctx, size_t inputSize) { + if (!(dctx->stage == ZSTDds_decompressBlock || dctx->stage == ZSTDds_decompressLastBlock)) + return dctx->expected; + if (dctx->bType != bt_raw) + return dctx->expected; + return BOUNDED(1, inputSize, dctx->expected); +} + +ZSTD_nextInputType_e ZSTD_nextInputType(ZSTD_DCtx* dctx) { + switch(dctx->stage) + { + default: /* should not happen */ + assert(0); + ZSTD_FALLTHROUGH; + case ZSTDds_getFrameHeaderSize: + ZSTD_FALLTHROUGH; + case ZSTDds_decodeFrameHeader: + return ZSTDnit_frameHeader; + case ZSTDds_decodeBlockHeader: + return ZSTDnit_blockHeader; + case ZSTDds_decompressBlock: + return ZSTDnit_block; + case ZSTDds_decompressLastBlock: + return ZSTDnit_lastBlock; + case ZSTDds_checkChecksum: + return ZSTDnit_checksum; + case ZSTDds_decodeSkippableHeader: + ZSTD_FALLTHROUGH; + case ZSTDds_skipFrame: + return ZSTDnit_skippableFrame; + } +} + +static int ZSTD_isSkipFrame(ZSTD_DCtx* dctx) { return dctx->stage == ZSTDds_skipFrame; } + +/** ZSTD_decompressContinue() : + * srcSize : must be the exact nb of bytes expected (see ZSTD_nextSrcSizeToDecompress()) + * @return : nb of bytes generated into `dst` (necessarily <= `dstCapacity) + * or an error code, which can be tested using ZSTD_isError() */ +size_t ZSTD_decompressContinue(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize) +{ + DEBUGLOG(5, "ZSTD_decompressContinue (srcSize:%u)", (unsigned)srcSize); + /* Sanity check */ + RETURN_ERROR_IF(srcSize != ZSTD_nextSrcSizeToDecompressWithInputSize(dctx, srcSize), srcSize_wrong, "not allowed"); + ZSTD_checkContinuity(dctx, dst, dstCapacity); + + dctx->processedCSize += srcSize; + + switch (dctx->stage) + { + case ZSTDds_getFrameHeaderSize : + assert(src != NULL); + if (dctx->format == ZSTD_f_zstd1) { /* allows header */ + assert(srcSize >= ZSTD_FRAMEIDSIZE); /* to read skippable magic number */ + if ((MEM_readLE32(src) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) { /* skippable frame */ + ZSTD_memcpy(dctx->headerBuffer, src, srcSize); + dctx->expected = ZSTD_SKIPPABLEHEADERSIZE - srcSize; /* remaining to load to get full skippable frame header */ + dctx->stage = ZSTDds_decodeSkippableHeader; + return 0; + } } + dctx->headerSize = ZSTD_frameHeaderSize_internal(src, srcSize, dctx->format); + if (ZSTD_isError(dctx->headerSize)) return dctx->headerSize; + ZSTD_memcpy(dctx->headerBuffer, src, srcSize); + dctx->expected = dctx->headerSize - srcSize; + dctx->stage = ZSTDds_decodeFrameHeader; + return 0; + + case ZSTDds_decodeFrameHeader: + assert(src != NULL); + ZSTD_memcpy(dctx->headerBuffer + (dctx->headerSize - srcSize), src, srcSize); + FORWARD_IF_ERROR(ZSTD_decodeFrameHeader(dctx, dctx->headerBuffer, dctx->headerSize), ""); + dctx->expected = ZSTD_blockHeaderSize; + dctx->stage = ZSTDds_decodeBlockHeader; + return 0; + + case ZSTDds_decodeBlockHeader: + { blockProperties_t bp; + size_t const cBlockSize = ZSTD_getcBlockSize(src, ZSTD_blockHeaderSize, &bp); + if (ZSTD_isError(cBlockSize)) return cBlockSize; + RETURN_ERROR_IF(cBlockSize > dctx->fParams.blockSizeMax, corruption_detected, "Block Size Exceeds Maximum"); + dctx->expected = cBlockSize; + dctx->bType = bp.blockType; + dctx->rleSize = bp.origSize; + if (cBlockSize) { + dctx->stage = bp.lastBlock ? ZSTDds_decompressLastBlock : ZSTDds_decompressBlock; + return 0; + } + /* empty block */ + if (bp.lastBlock) { + if (dctx->fParams.checksumFlag) { + dctx->expected = 4; + dctx->stage = ZSTDds_checkChecksum; + } else { + dctx->expected = 0; /* end of frame */ + dctx->stage = ZSTDds_getFrameHeaderSize; + } + } else { + dctx->expected = ZSTD_blockHeaderSize; /* jump to next header */ + dctx->stage = ZSTDds_decodeBlockHeader; + } + return 0; + } + + case ZSTDds_decompressLastBlock: + case ZSTDds_decompressBlock: + DEBUGLOG(5, "ZSTD_decompressContinue: case ZSTDds_decompressBlock"); + { size_t rSize; + switch(dctx->bType) + { + case bt_compressed: + DEBUGLOG(5, "ZSTD_decompressContinue: case bt_compressed"); + assert(dctx->isFrameDecompression == 1); + rSize = ZSTD_decompressBlock_internal(dctx, dst, dstCapacity, src, srcSize, is_streaming); + dctx->expected = 0; /* Streaming not supported */ + break; + case bt_raw : + assert(srcSize <= dctx->expected); + rSize = ZSTD_copyRawBlock(dst, dstCapacity, src, srcSize); + FORWARD_IF_ERROR(rSize, "ZSTD_copyRawBlock failed"); + assert(rSize == srcSize); + dctx->expected -= rSize; + break; + case bt_rle : + rSize = ZSTD_setRleBlock(dst, dstCapacity, *(const BYTE*)src, dctx->rleSize); + dctx->expected = 0; /* Streaming not supported */ + break; + case bt_reserved : /* should never happen */ + default: + RETURN_ERROR(corruption_detected, "invalid block type"); + } + FORWARD_IF_ERROR(rSize, ""); + RETURN_ERROR_IF(rSize > dctx->fParams.blockSizeMax, corruption_detected, "Decompressed Block Size Exceeds Maximum"); + DEBUGLOG(5, "ZSTD_decompressContinue: decoded size from block : %u", (unsigned)rSize); + dctx->decodedSize += rSize; + if (dctx->validateChecksum) XXH64_update(&dctx->xxhState, dst, rSize); + dctx->previousDstEnd = (char*)dst + rSize; + + /* Stay on the same stage until we are finished streaming the block. */ + if (dctx->expected > 0) { + return rSize; + } + + if (dctx->stage == ZSTDds_decompressLastBlock) { /* end of frame */ + DEBUGLOG(4, "ZSTD_decompressContinue: decoded size from frame : %u", (unsigned)dctx->decodedSize); + RETURN_ERROR_IF( + dctx->fParams.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN + && dctx->decodedSize != dctx->fParams.frameContentSize, + corruption_detected, ""); + if (dctx->fParams.checksumFlag) { /* another round for frame checksum */ + dctx->expected = 4; + dctx->stage = ZSTDds_checkChecksum; + } else { + ZSTD_DCtx_trace_end(dctx, dctx->decodedSize, dctx->processedCSize, /* streaming */ 1); + dctx->expected = 0; /* ends here */ + dctx->stage = ZSTDds_getFrameHeaderSize; + } + } else { + dctx->stage = ZSTDds_decodeBlockHeader; + dctx->expected = ZSTD_blockHeaderSize; + } + return rSize; + } + + case ZSTDds_checkChecksum: + assert(srcSize == 4); /* guaranteed by dctx->expected */ + { + if (dctx->validateChecksum) { + U32 const h32 = (U32)XXH64_digest(&dctx->xxhState); + U32 const check32 = MEM_readLE32(src); + DEBUGLOG(4, "ZSTD_decompressContinue: checksum : calculated %08X :: %08X read", (unsigned)h32, (unsigned)check32); + RETURN_ERROR_IF(check32 != h32, checksum_wrong, ""); + } + ZSTD_DCtx_trace_end(dctx, dctx->decodedSize, dctx->processedCSize, /* streaming */ 1); + dctx->expected = 0; + dctx->stage = ZSTDds_getFrameHeaderSize; + return 0; + } + + case ZSTDds_decodeSkippableHeader: + assert(src != NULL); + assert(srcSize <= ZSTD_SKIPPABLEHEADERSIZE); + assert(dctx->format != ZSTD_f_zstd1_magicless); + ZSTD_memcpy(dctx->headerBuffer + (ZSTD_SKIPPABLEHEADERSIZE - srcSize), src, srcSize); /* complete skippable header */ + dctx->expected = MEM_readLE32(dctx->headerBuffer + ZSTD_FRAMEIDSIZE); /* note : dctx->expected can grow seriously large, beyond local buffer size */ + dctx->stage = ZSTDds_skipFrame; + return 0; + + case ZSTDds_skipFrame: + dctx->expected = 0; + dctx->stage = ZSTDds_getFrameHeaderSize; + return 0; + + default: + assert(0); /* impossible */ + RETURN_ERROR(GENERIC, "impossible to reach"); /* some compilers require default to do something */ + } +} + + +static size_t ZSTD_refDictContent(ZSTD_DCtx* dctx, const void* dict, size_t dictSize) +{ + dctx->dictEnd = dctx->previousDstEnd; + dctx->virtualStart = (const char*)dict - ((const char*)(dctx->previousDstEnd) - (const char*)(dctx->prefixStart)); + dctx->prefixStart = dict; + dctx->previousDstEnd = (const char*)dict + dictSize; +#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION + dctx->dictContentBeginForFuzzing = dctx->prefixStart; + dctx->dictContentEndForFuzzing = dctx->previousDstEnd; +#endif + return 0; +} + +/*! ZSTD_loadDEntropy() : + * dict : must point at beginning of a valid zstd dictionary. + * @return : size of entropy tables read */ +size_t +ZSTD_loadDEntropy(ZSTD_entropyDTables_t* entropy, + const void* const dict, size_t const dictSize) +{ + const BYTE* dictPtr = (const BYTE*)dict; + const BYTE* const dictEnd = dictPtr + dictSize; + + RETURN_ERROR_IF(dictSize <= 8, dictionary_corrupted, "dict is too small"); + assert(MEM_readLE32(dict) == ZSTD_MAGIC_DICTIONARY); /* dict must be valid */ + dictPtr += 8; /* skip header = magic + dictID */ + + ZSTD_STATIC_ASSERT(offsetof(ZSTD_entropyDTables_t, OFTable) == offsetof(ZSTD_entropyDTables_t, LLTable) + sizeof(entropy->LLTable)); + ZSTD_STATIC_ASSERT(offsetof(ZSTD_entropyDTables_t, MLTable) == offsetof(ZSTD_entropyDTables_t, OFTable) + sizeof(entropy->OFTable)); + ZSTD_STATIC_ASSERT(sizeof(entropy->LLTable) + sizeof(entropy->OFTable) + sizeof(entropy->MLTable) >= HUF_DECOMPRESS_WORKSPACE_SIZE); + { void* const workspace = &entropy->LLTable; /* use fse tables as temporary workspace; implies fse tables are grouped together */ + size_t const workspaceSize = sizeof(entropy->LLTable) + sizeof(entropy->OFTable) + sizeof(entropy->MLTable); +#ifdef HUF_FORCE_DECOMPRESS_X1 + /* in minimal huffman, we always use X1 variants */ + size_t const hSize = HUF_readDTableX1_wksp(entropy->hufTable, + dictPtr, dictEnd - dictPtr, + workspace, workspaceSize, /* flags */ 0); +#else + size_t const hSize = HUF_readDTableX2_wksp(entropy->hufTable, + dictPtr, (size_t)(dictEnd - dictPtr), + workspace, workspaceSize, /* flags */ 0); +#endif + RETURN_ERROR_IF(HUF_isError(hSize), dictionary_corrupted, ""); + dictPtr += hSize; + } + + { short offcodeNCount[MaxOff+1]; + unsigned offcodeMaxValue = MaxOff, offcodeLog; + size_t const offcodeHeaderSize = FSE_readNCount(offcodeNCount, &offcodeMaxValue, &offcodeLog, dictPtr, (size_t)(dictEnd-dictPtr)); + RETURN_ERROR_IF(FSE_isError(offcodeHeaderSize), dictionary_corrupted, ""); + RETURN_ERROR_IF(offcodeMaxValue > MaxOff, dictionary_corrupted, ""); + RETURN_ERROR_IF(offcodeLog > OffFSELog, dictionary_corrupted, ""); + ZSTD_buildFSETable( entropy->OFTable, + offcodeNCount, offcodeMaxValue, + OF_base, OF_bits, + offcodeLog, + entropy->workspace, sizeof(entropy->workspace), + /* bmi2 */0); + dictPtr += offcodeHeaderSize; + } + + { short matchlengthNCount[MaxML+1]; + unsigned matchlengthMaxValue = MaxML, matchlengthLog; + size_t const matchlengthHeaderSize = FSE_readNCount(matchlengthNCount, &matchlengthMaxValue, &matchlengthLog, dictPtr, (size_t)(dictEnd-dictPtr)); + RETURN_ERROR_IF(FSE_isError(matchlengthHeaderSize), dictionary_corrupted, ""); + RETURN_ERROR_IF(matchlengthMaxValue > MaxML, dictionary_corrupted, ""); + RETURN_ERROR_IF(matchlengthLog > MLFSELog, dictionary_corrupted, ""); + ZSTD_buildFSETable( entropy->MLTable, + matchlengthNCount, matchlengthMaxValue, + ML_base, ML_bits, + matchlengthLog, + entropy->workspace, sizeof(entropy->workspace), + /* bmi2 */ 0); + dictPtr += matchlengthHeaderSize; + } + + { short litlengthNCount[MaxLL+1]; + unsigned litlengthMaxValue = MaxLL, litlengthLog; + size_t const litlengthHeaderSize = FSE_readNCount(litlengthNCount, &litlengthMaxValue, &litlengthLog, dictPtr, (size_t)(dictEnd-dictPtr)); + RETURN_ERROR_IF(FSE_isError(litlengthHeaderSize), dictionary_corrupted, ""); + RETURN_ERROR_IF(litlengthMaxValue > MaxLL, dictionary_corrupted, ""); + RETURN_ERROR_IF(litlengthLog > LLFSELog, dictionary_corrupted, ""); + ZSTD_buildFSETable( entropy->LLTable, + litlengthNCount, litlengthMaxValue, + LL_base, LL_bits, + litlengthLog, + entropy->workspace, sizeof(entropy->workspace), + /* bmi2 */ 0); + dictPtr += litlengthHeaderSize; + } + + RETURN_ERROR_IF(dictPtr+12 > dictEnd, dictionary_corrupted, ""); + { int i; + size_t const dictContentSize = (size_t)(dictEnd - (dictPtr+12)); + for (i=0; i<3; i++) { + U32 const rep = MEM_readLE32(dictPtr); dictPtr += 4; + RETURN_ERROR_IF(rep==0 || rep > dictContentSize, + dictionary_corrupted, ""); + entropy->rep[i] = rep; + } } + + return (size_t)(dictPtr - (const BYTE*)dict); +} + +static size_t ZSTD_decompress_insertDictionary(ZSTD_DCtx* dctx, const void* dict, size_t dictSize) +{ + if (dictSize < 8) return ZSTD_refDictContent(dctx, dict, dictSize); + { U32 const magic = MEM_readLE32(dict); + if (magic != ZSTD_MAGIC_DICTIONARY) { + return ZSTD_refDictContent(dctx, dict, dictSize); /* pure content mode */ + } } + dctx->dictID = MEM_readLE32((const char*)dict + ZSTD_FRAMEIDSIZE); + + /* load entropy tables */ + { size_t const eSize = ZSTD_loadDEntropy(&dctx->entropy, dict, dictSize); + RETURN_ERROR_IF(ZSTD_isError(eSize), dictionary_corrupted, ""); + dict = (const char*)dict + eSize; + dictSize -= eSize; + } + dctx->litEntropy = dctx->fseEntropy = 1; + + /* reference dictionary content */ + return ZSTD_refDictContent(dctx, dict, dictSize); +} + +size_t ZSTD_decompressBegin(ZSTD_DCtx* dctx) +{ + assert(dctx != NULL); +#if ZSTD_TRACE + dctx->traceCtx = (ZSTD_trace_decompress_begin != NULL) ? ZSTD_trace_decompress_begin(dctx) : 0; +#endif + dctx->expected = ZSTD_startingInputLength(dctx->format); /* dctx->format must be properly set */ + dctx->stage = ZSTDds_getFrameHeaderSize; + dctx->processedCSize = 0; + dctx->decodedSize = 0; + dctx->previousDstEnd = NULL; + dctx->prefixStart = NULL; + dctx->virtualStart = NULL; + dctx->dictEnd = NULL; + dctx->entropy.hufTable[0] = (HUF_DTable)((ZSTD_HUFFDTABLE_CAPACITY_LOG)*0x1000001); /* cover both little and big endian */ + dctx->litEntropy = dctx->fseEntropy = 0; + dctx->dictID = 0; + dctx->bType = bt_reserved; + dctx->isFrameDecompression = 1; + ZSTD_STATIC_ASSERT(sizeof(dctx->entropy.rep) == sizeof(repStartValue)); + ZSTD_memcpy(dctx->entropy.rep, repStartValue, sizeof(repStartValue)); /* initial repcodes */ + dctx->LLTptr = dctx->entropy.LLTable; + dctx->MLTptr = dctx->entropy.MLTable; + dctx->OFTptr = dctx->entropy.OFTable; + dctx->HUFptr = dctx->entropy.hufTable; + return 0; +} + +size_t ZSTD_decompressBegin_usingDict(ZSTD_DCtx* dctx, const void* dict, size_t dictSize) +{ + FORWARD_IF_ERROR( ZSTD_decompressBegin(dctx) , ""); + if (dict && dictSize) + RETURN_ERROR_IF( + ZSTD_isError(ZSTD_decompress_insertDictionary(dctx, dict, dictSize)), + dictionary_corrupted, ""); + return 0; +} + + +/* ====== ZSTD_DDict ====== */ + +size_t ZSTD_decompressBegin_usingDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict) +{ + DEBUGLOG(4, "ZSTD_decompressBegin_usingDDict"); + assert(dctx != NULL); + if (ddict) { + const char* const dictStart = (const char*)ZSTD_DDict_dictContent(ddict); + size_t const dictSize = ZSTD_DDict_dictSize(ddict); + const void* const dictEnd = dictStart + dictSize; + dctx->ddictIsCold = (dctx->dictEnd != dictEnd); + DEBUGLOG(4, "DDict is %s", + dctx->ddictIsCold ? "~cold~" : "hot!"); + } + FORWARD_IF_ERROR( ZSTD_decompressBegin(dctx) , ""); + if (ddict) { /* NULL ddict is equivalent to no dictionary */ + ZSTD_copyDDictParameters(dctx, ddict); + } + return 0; +} + +/*! ZSTD_getDictID_fromDict() : + * Provides the dictID stored within dictionary. + * if @return == 0, the dictionary is not conformant with Zstandard specification. + * It can still be loaded, but as a content-only dictionary. */ +unsigned ZSTD_getDictID_fromDict(const void* dict, size_t dictSize) +{ + if (dictSize < 8) return 0; + if (MEM_readLE32(dict) != ZSTD_MAGIC_DICTIONARY) return 0; + return MEM_readLE32((const char*)dict + ZSTD_FRAMEIDSIZE); +} + +/*! ZSTD_getDictID_fromFrame() : + * Provides the dictID required to decompress frame stored within `src`. + * If @return == 0, the dictID could not be decoded. + * This could for one of the following reasons : + * - The frame does not require a dictionary (most common case). + * - The frame was built with dictID intentionally removed. + * Needed dictionary is a hidden piece of information. + * Note : this use case also happens when using a non-conformant dictionary. + * - `srcSize` is too small, and as a result, frame header could not be decoded. + * Note : possible if `srcSize < ZSTD_FRAMEHEADERSIZE_MAX`. + * - This is not a Zstandard frame. + * When identifying the exact failure cause, it's possible to use + * ZSTD_getFrameHeader(), which will provide a more precise error code. */ +unsigned ZSTD_getDictID_fromFrame(const void* src, size_t srcSize) +{ + ZSTD_frameHeader zfp = { 0, 0, 0, ZSTD_frame, 0, 0, 0, 0, 0 }; + size_t const hError = ZSTD_getFrameHeader(&zfp, src, srcSize); + if (ZSTD_isError(hError)) return 0; + return zfp.dictID; +} + + +/*! ZSTD_decompress_usingDDict() : +* Decompression using a pre-digested Dictionary +* Use dictionary without significant overhead. */ +size_t ZSTD_decompress_usingDDict(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_DDict* ddict) +{ + /* pass content and size in case legacy frames are encountered */ + return ZSTD_decompressMultiFrame(dctx, dst, dstCapacity, src, srcSize, + NULL, 0, + ddict); +} + + +/*===================================== +* Streaming decompression +*====================================*/ + +ZSTD_DStream* ZSTD_createDStream(void) +{ + DEBUGLOG(3, "ZSTD_createDStream"); + return ZSTD_createDCtx_internal(ZSTD_defaultCMem); +} + +ZSTD_DStream* ZSTD_initStaticDStream(void *workspace, size_t workspaceSize) +{ + return ZSTD_initStaticDCtx(workspace, workspaceSize); +} + +ZSTD_DStream* ZSTD_createDStream_advanced(ZSTD_customMem customMem) +{ + return ZSTD_createDCtx_internal(customMem); +} + +size_t ZSTD_freeDStream(ZSTD_DStream* zds) +{ + return ZSTD_freeDCtx(zds); +} + + +/* *** Initialization *** */ + +size_t ZSTD_DStreamInSize(void) { return ZSTD_BLOCKSIZE_MAX + ZSTD_blockHeaderSize; } +size_t ZSTD_DStreamOutSize(void) { return ZSTD_BLOCKSIZE_MAX; } + +size_t ZSTD_DCtx_loadDictionary_advanced(ZSTD_DCtx* dctx, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType) +{ + RETURN_ERROR_IF(dctx->streamStage != zdss_init, stage_wrong, ""); + ZSTD_clearDict(dctx); + if (dict && dictSize != 0) { + dctx->ddictLocal = ZSTD_createDDict_advanced(dict, dictSize, dictLoadMethod, dictContentType, dctx->customMem); + RETURN_ERROR_IF(dctx->ddictLocal == NULL, memory_allocation, "NULL pointer!"); + dctx->ddict = dctx->ddictLocal; + dctx->dictUses = ZSTD_use_indefinitely; + } + return 0; +} + +size_t ZSTD_DCtx_loadDictionary_byReference(ZSTD_DCtx* dctx, const void* dict, size_t dictSize) +{ + return ZSTD_DCtx_loadDictionary_advanced(dctx, dict, dictSize, ZSTD_dlm_byRef, ZSTD_dct_auto); +} + +size_t ZSTD_DCtx_loadDictionary(ZSTD_DCtx* dctx, const void* dict, size_t dictSize) +{ + return ZSTD_DCtx_loadDictionary_advanced(dctx, dict, dictSize, ZSTD_dlm_byCopy, ZSTD_dct_auto); +} + +size_t ZSTD_DCtx_refPrefix_advanced(ZSTD_DCtx* dctx, const void* prefix, size_t prefixSize, ZSTD_dictContentType_e dictContentType) +{ + FORWARD_IF_ERROR(ZSTD_DCtx_loadDictionary_advanced(dctx, prefix, prefixSize, ZSTD_dlm_byRef, dictContentType), ""); + dctx->dictUses = ZSTD_use_once; + return 0; +} + +size_t ZSTD_DCtx_refPrefix(ZSTD_DCtx* dctx, const void* prefix, size_t prefixSize) +{ + return ZSTD_DCtx_refPrefix_advanced(dctx, prefix, prefixSize, ZSTD_dct_rawContent); +} + + +/* ZSTD_initDStream_usingDict() : + * return : expected size, aka ZSTD_startingInputLength(). + * this function cannot fail */ +size_t ZSTD_initDStream_usingDict(ZSTD_DStream* zds, const void* dict, size_t dictSize) +{ + DEBUGLOG(4, "ZSTD_initDStream_usingDict"); + FORWARD_IF_ERROR( ZSTD_DCtx_reset(zds, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_DCtx_loadDictionary(zds, dict, dictSize) , ""); + return ZSTD_startingInputLength(zds->format); +} + +/* note : this variant can't fail */ +size_t ZSTD_initDStream(ZSTD_DStream* zds) +{ + DEBUGLOG(4, "ZSTD_initDStream"); + FORWARD_IF_ERROR(ZSTD_DCtx_reset(zds, ZSTD_reset_session_only), ""); + FORWARD_IF_ERROR(ZSTD_DCtx_refDDict(zds, NULL), ""); + return ZSTD_startingInputLength(zds->format); +} + +/* ZSTD_initDStream_usingDDict() : + * ddict will just be referenced, and must outlive decompression session + * this function cannot fail */ +size_t ZSTD_initDStream_usingDDict(ZSTD_DStream* dctx, const ZSTD_DDict* ddict) +{ + DEBUGLOG(4, "ZSTD_initDStream_usingDDict"); + FORWARD_IF_ERROR( ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only) , ""); + FORWARD_IF_ERROR( ZSTD_DCtx_refDDict(dctx, ddict) , ""); + return ZSTD_startingInputLength(dctx->format); +} + +/* ZSTD_resetDStream() : + * return : expected size, aka ZSTD_startingInputLength(). + * this function cannot fail */ +size_t ZSTD_resetDStream(ZSTD_DStream* dctx) +{ + DEBUGLOG(4, "ZSTD_resetDStream"); + FORWARD_IF_ERROR(ZSTD_DCtx_reset(dctx, ZSTD_reset_session_only), ""); + return ZSTD_startingInputLength(dctx->format); +} + + +size_t ZSTD_DCtx_refDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict) +{ + RETURN_ERROR_IF(dctx->streamStage != zdss_init, stage_wrong, ""); + ZSTD_clearDict(dctx); + if (ddict) { + dctx->ddict = ddict; + dctx->dictUses = ZSTD_use_indefinitely; + if (dctx->refMultipleDDicts == ZSTD_rmd_refMultipleDDicts) { + if (dctx->ddictSet == NULL) { + dctx->ddictSet = ZSTD_createDDictHashSet(dctx->customMem); + if (!dctx->ddictSet) { + RETURN_ERROR(memory_allocation, "Failed to allocate memory for hash set!"); + } + } + assert(!dctx->staticSize); /* Impossible: ddictSet cannot have been allocated if static dctx */ + FORWARD_IF_ERROR(ZSTD_DDictHashSet_addDDict(dctx->ddictSet, ddict, dctx->customMem), ""); + } + } + return 0; +} + +/* ZSTD_DCtx_setMaxWindowSize() : + * note : no direct equivalence in ZSTD_DCtx_setParameter, + * since this version sets windowSize, and the other sets windowLog */ +size_t ZSTD_DCtx_setMaxWindowSize(ZSTD_DCtx* dctx, size_t maxWindowSize) +{ + ZSTD_bounds const bounds = ZSTD_dParam_getBounds(ZSTD_d_windowLogMax); + size_t const min = (size_t)1 << bounds.lowerBound; + size_t const max = (size_t)1 << bounds.upperBound; + RETURN_ERROR_IF(dctx->streamStage != zdss_init, stage_wrong, ""); + RETURN_ERROR_IF(maxWindowSize < min, parameter_outOfBound, ""); + RETURN_ERROR_IF(maxWindowSize > max, parameter_outOfBound, ""); + dctx->maxWindowSize = maxWindowSize; + return 0; +} + +size_t ZSTD_DCtx_setFormat(ZSTD_DCtx* dctx, ZSTD_format_e format) +{ + return ZSTD_DCtx_setParameter(dctx, ZSTD_d_format, (int)format); +} + +ZSTD_bounds ZSTD_dParam_getBounds(ZSTD_dParameter dParam) +{ + ZSTD_bounds bounds = { 0, 0, 0 }; + switch(dParam) { + case ZSTD_d_windowLogMax: + bounds.lowerBound = ZSTD_WINDOWLOG_ABSOLUTEMIN; + bounds.upperBound = ZSTD_WINDOWLOG_MAX; + return bounds; + case ZSTD_d_format: + bounds.lowerBound = (int)ZSTD_f_zstd1; + bounds.upperBound = (int)ZSTD_f_zstd1_magicless; + ZSTD_STATIC_ASSERT(ZSTD_f_zstd1 < ZSTD_f_zstd1_magicless); + return bounds; + case ZSTD_d_stableOutBuffer: + bounds.lowerBound = (int)ZSTD_bm_buffered; + bounds.upperBound = (int)ZSTD_bm_stable; + return bounds; + case ZSTD_d_forceIgnoreChecksum: + bounds.lowerBound = (int)ZSTD_d_validateChecksum; + bounds.upperBound = (int)ZSTD_d_ignoreChecksum; + return bounds; + case ZSTD_d_refMultipleDDicts: + bounds.lowerBound = (int)ZSTD_rmd_refSingleDDict; + bounds.upperBound = (int)ZSTD_rmd_refMultipleDDicts; + return bounds; + case ZSTD_d_disableHuffmanAssembly: + bounds.lowerBound = 0; + bounds.upperBound = 1; + return bounds; + case ZSTD_d_maxBlockSize: + bounds.lowerBound = ZSTD_BLOCKSIZE_MAX_MIN; + bounds.upperBound = ZSTD_BLOCKSIZE_MAX; + return bounds; + + default:; + } + bounds.error = ERROR(parameter_unsupported); + return bounds; +} + +/* ZSTD_dParam_withinBounds: + * @return 1 if value is within dParam bounds, + * 0 otherwise */ +static int ZSTD_dParam_withinBounds(ZSTD_dParameter dParam, int value) +{ + ZSTD_bounds const bounds = ZSTD_dParam_getBounds(dParam); + if (ZSTD_isError(bounds.error)) return 0; + if (value < bounds.lowerBound) return 0; + if (value > bounds.upperBound) return 0; + return 1; +} + +#define CHECK_DBOUNDS(p,v) { \ + RETURN_ERROR_IF(!ZSTD_dParam_withinBounds(p, v), parameter_outOfBound, ""); \ +} + +size_t ZSTD_DCtx_getParameter(ZSTD_DCtx* dctx, ZSTD_dParameter param, int* value) +{ + switch (param) { + case ZSTD_d_windowLogMax: + *value = (int)ZSTD_highbit32((U32)dctx->maxWindowSize); + return 0; + case ZSTD_d_format: + *value = (int)dctx->format; + return 0; + case ZSTD_d_stableOutBuffer: + *value = (int)dctx->outBufferMode; + return 0; + case ZSTD_d_forceIgnoreChecksum: + *value = (int)dctx->forceIgnoreChecksum; + return 0; + case ZSTD_d_refMultipleDDicts: + *value = (int)dctx->refMultipleDDicts; + return 0; + case ZSTD_d_disableHuffmanAssembly: + *value = (int)dctx->disableHufAsm; + return 0; + case ZSTD_d_maxBlockSize: + *value = dctx->maxBlockSizeParam; + return 0; + default:; + } + RETURN_ERROR(parameter_unsupported, ""); +} + +size_t ZSTD_DCtx_setParameter(ZSTD_DCtx* dctx, ZSTD_dParameter dParam, int value) +{ + RETURN_ERROR_IF(dctx->streamStage != zdss_init, stage_wrong, ""); + switch(dParam) { + case ZSTD_d_windowLogMax: + if (value == 0) value = ZSTD_WINDOWLOG_LIMIT_DEFAULT; + CHECK_DBOUNDS(ZSTD_d_windowLogMax, value); + dctx->maxWindowSize = ((size_t)1) << value; + return 0; + case ZSTD_d_format: + CHECK_DBOUNDS(ZSTD_d_format, value); + dctx->format = (ZSTD_format_e)value; + return 0; + case ZSTD_d_stableOutBuffer: + CHECK_DBOUNDS(ZSTD_d_stableOutBuffer, value); + dctx->outBufferMode = (ZSTD_bufferMode_e)value; + return 0; + case ZSTD_d_forceIgnoreChecksum: + CHECK_DBOUNDS(ZSTD_d_forceIgnoreChecksum, value); + dctx->forceIgnoreChecksum = (ZSTD_forceIgnoreChecksum_e)value; + return 0; + case ZSTD_d_refMultipleDDicts: + CHECK_DBOUNDS(ZSTD_d_refMultipleDDicts, value); + if (dctx->staticSize != 0) { + RETURN_ERROR(parameter_unsupported, "Static dctx does not support multiple DDicts!"); + } + dctx->refMultipleDDicts = (ZSTD_refMultipleDDicts_e)value; + return 0; + case ZSTD_d_disableHuffmanAssembly: + CHECK_DBOUNDS(ZSTD_d_disableHuffmanAssembly, value); + dctx->disableHufAsm = value != 0; + return 0; + case ZSTD_d_maxBlockSize: + if (value != 0) CHECK_DBOUNDS(ZSTD_d_maxBlockSize, value); + dctx->maxBlockSizeParam = value; + return 0; + default:; + } + RETURN_ERROR(parameter_unsupported, ""); +} + +size_t ZSTD_DCtx_reset(ZSTD_DCtx* dctx, ZSTD_ResetDirective reset) +{ + if ( (reset == ZSTD_reset_session_only) + || (reset == ZSTD_reset_session_and_parameters) ) { + dctx->streamStage = zdss_init; + dctx->noForwardProgress = 0; + dctx->isFrameDecompression = 1; + } + if ( (reset == ZSTD_reset_parameters) + || (reset == ZSTD_reset_session_and_parameters) ) { + RETURN_ERROR_IF(dctx->streamStage != zdss_init, stage_wrong, ""); + ZSTD_clearDict(dctx); + ZSTD_DCtx_resetParameters(dctx); + } + return 0; +} + + +size_t ZSTD_sizeof_DStream(const ZSTD_DStream* dctx) +{ + return ZSTD_sizeof_DCtx(dctx); +} + +static size_t ZSTD_decodingBufferSize_internal(unsigned long long windowSize, unsigned long long frameContentSize, size_t blockSizeMax) +{ + size_t const blockSize = MIN((size_t)MIN(windowSize, ZSTD_BLOCKSIZE_MAX), blockSizeMax); + /* We need blockSize + WILDCOPY_OVERLENGTH worth of buffer so that if a block + * ends at windowSize + WILDCOPY_OVERLENGTH + 1 bytes, we can start writing + * the block at the beginning of the output buffer, and maintain a full window. + * + * We need another blockSize worth of buffer so that we can store split + * literals at the end of the block without overwriting the extDict window. + */ + unsigned long long const neededRBSize = windowSize + (blockSize * 2) + (WILDCOPY_OVERLENGTH * 2); + unsigned long long const neededSize = MIN(frameContentSize, neededRBSize); + size_t const minRBSize = (size_t) neededSize; + RETURN_ERROR_IF((unsigned long long)minRBSize != neededSize, + frameParameter_windowTooLarge, ""); + return minRBSize; +} + +size_t ZSTD_decodingBufferSize_min(unsigned long long windowSize, unsigned long long frameContentSize) +{ + return ZSTD_decodingBufferSize_internal(windowSize, frameContentSize, ZSTD_BLOCKSIZE_MAX); +} + +size_t ZSTD_estimateDStreamSize(size_t windowSize) +{ + size_t const blockSize = MIN(windowSize, ZSTD_BLOCKSIZE_MAX); + size_t const inBuffSize = blockSize; /* no block can be larger */ + size_t const outBuffSize = ZSTD_decodingBufferSize_min(windowSize, ZSTD_CONTENTSIZE_UNKNOWN); + return ZSTD_estimateDCtxSize() + inBuffSize + outBuffSize; +} + +size_t ZSTD_estimateDStreamSize_fromFrame(const void* src, size_t srcSize) +{ + U32 const windowSizeMax = 1U << ZSTD_WINDOWLOG_MAX; /* note : should be user-selectable, but requires an additional parameter (or a dctx) */ + ZSTD_frameHeader zfh; + size_t const err = ZSTD_getFrameHeader(&zfh, src, srcSize); + if (ZSTD_isError(err)) return err; + RETURN_ERROR_IF(err>0, srcSize_wrong, ""); + RETURN_ERROR_IF(zfh.windowSize > windowSizeMax, + frameParameter_windowTooLarge, ""); + return ZSTD_estimateDStreamSize((size_t)zfh.windowSize); +} + + +/* ***** Decompression ***** */ + +static int ZSTD_DCtx_isOverflow(ZSTD_DStream* zds, size_t const neededInBuffSize, size_t const neededOutBuffSize) +{ + return (zds->inBuffSize + zds->outBuffSize) >= (neededInBuffSize + neededOutBuffSize) * ZSTD_WORKSPACETOOLARGE_FACTOR; +} + +static void ZSTD_DCtx_updateOversizedDuration(ZSTD_DStream* zds, size_t const neededInBuffSize, size_t const neededOutBuffSize) +{ + if (ZSTD_DCtx_isOverflow(zds, neededInBuffSize, neededOutBuffSize)) + zds->oversizedDuration++; + else + zds->oversizedDuration = 0; +} + +static int ZSTD_DCtx_isOversizedTooLong(ZSTD_DStream* zds) +{ + return zds->oversizedDuration >= ZSTD_WORKSPACETOOLARGE_MAXDURATION; +} + +/* Checks that the output buffer hasn't changed if ZSTD_obm_stable is used. */ +static size_t ZSTD_checkOutBuffer(ZSTD_DStream const* zds, ZSTD_outBuffer const* output) +{ + ZSTD_outBuffer const expect = zds->expectedOutBuffer; + /* No requirement when ZSTD_obm_stable is not enabled. */ + if (zds->outBufferMode != ZSTD_bm_stable) + return 0; + /* Any buffer is allowed in zdss_init, this must be the same for every other call until + * the context is reset. + */ + if (zds->streamStage == zdss_init) + return 0; + /* The buffer must match our expectation exactly. */ + if (expect.dst == output->dst && expect.pos == output->pos && expect.size == output->size) + return 0; + RETURN_ERROR(dstBuffer_wrong, "ZSTD_d_stableOutBuffer enabled but output differs!"); +} + +/* Calls ZSTD_decompressContinue() with the right parameters for ZSTD_decompressStream() + * and updates the stage and the output buffer state. This call is extracted so it can be + * used both when reading directly from the ZSTD_inBuffer, and in buffered input mode. + * NOTE: You must break after calling this function since the streamStage is modified. + */ +static size_t ZSTD_decompressContinueStream( + ZSTD_DStream* zds, char** op, char* oend, + void const* src, size_t srcSize) { + int const isSkipFrame = ZSTD_isSkipFrame(zds); + if (zds->outBufferMode == ZSTD_bm_buffered) { + size_t const dstSize = isSkipFrame ? 0 : zds->outBuffSize - zds->outStart; + size_t const decodedSize = ZSTD_decompressContinue(zds, + zds->outBuff + zds->outStart, dstSize, src, srcSize); + FORWARD_IF_ERROR(decodedSize, ""); + if (!decodedSize && !isSkipFrame) { + zds->streamStage = zdss_read; + } else { + zds->outEnd = zds->outStart + decodedSize; + zds->streamStage = zdss_flush; + } + } else { + /* Write directly into the output buffer */ + size_t const dstSize = isSkipFrame ? 0 : (size_t)(oend - *op); + size_t const decodedSize = ZSTD_decompressContinue(zds, *op, dstSize, src, srcSize); + FORWARD_IF_ERROR(decodedSize, ""); + *op += decodedSize; + /* Flushing is not needed. */ + zds->streamStage = zdss_read; + assert(*op <= oend); + assert(zds->outBufferMode == ZSTD_bm_stable); + } + return 0; +} + +size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inBuffer* input) +{ + const char* const src = (const char*)input->src; + const char* const istart = input->pos != 0 ? src + input->pos : src; + const char* const iend = input->size != 0 ? src + input->size : src; + const char* ip = istart; + char* const dst = (char*)output->dst; + char* const ostart = output->pos != 0 ? dst + output->pos : dst; + char* const oend = output->size != 0 ? dst + output->size : dst; + char* op = ostart; + U32 someMoreWork = 1; + + DEBUGLOG(5, "ZSTD_decompressStream"); + RETURN_ERROR_IF( + input->pos > input->size, + srcSize_wrong, + "forbidden. in: pos: %u vs size: %u", + (U32)input->pos, (U32)input->size); + RETURN_ERROR_IF( + output->pos > output->size, + dstSize_tooSmall, + "forbidden. out: pos: %u vs size: %u", + (U32)output->pos, (U32)output->size); + DEBUGLOG(5, "input size : %u", (U32)(input->size - input->pos)); + FORWARD_IF_ERROR(ZSTD_checkOutBuffer(zds, output), ""); + + while (someMoreWork) { + switch(zds->streamStage) + { + case zdss_init : + DEBUGLOG(5, "stage zdss_init => transparent reset "); + zds->streamStage = zdss_loadHeader; + zds->lhSize = zds->inPos = zds->outStart = zds->outEnd = 0; +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) + zds->legacyVersion = 0; +#endif + zds->hostageByte = 0; + zds->expectedOutBuffer = *output; + ZSTD_FALLTHROUGH; + + case zdss_loadHeader : + DEBUGLOG(5, "stage zdss_loadHeader (srcSize : %u)", (U32)(iend - ip)); +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) + if (zds->legacyVersion) { + RETURN_ERROR_IF(zds->staticSize, memory_allocation, + "legacy support is incompatible with static dctx"); + { size_t const hint = ZSTD_decompressLegacyStream(zds->legacyContext, zds->legacyVersion, output, input); + if (hint==0) zds->streamStage = zdss_init; + return hint; + } } +#endif + { size_t const hSize = ZSTD_getFrameHeader_advanced(&zds->fParams, zds->headerBuffer, zds->lhSize, zds->format); + if (zds->refMultipleDDicts && zds->ddictSet) { + ZSTD_DCtx_selectFrameDDict(zds); + } + if (ZSTD_isError(hSize)) { +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT>=1) + U32 const legacyVersion = ZSTD_isLegacy(istart, iend-istart); + if (legacyVersion) { + ZSTD_DDict const* const ddict = ZSTD_getDDict(zds); + const void* const dict = ddict ? ZSTD_DDict_dictContent(ddict) : NULL; + size_t const dictSize = ddict ? ZSTD_DDict_dictSize(ddict) : 0; + DEBUGLOG(5, "ZSTD_decompressStream: detected legacy version v0.%u", legacyVersion); + RETURN_ERROR_IF(zds->staticSize, memory_allocation, + "legacy support is incompatible with static dctx"); + FORWARD_IF_ERROR(ZSTD_initLegacyStream(&zds->legacyContext, + zds->previousLegacyVersion, legacyVersion, + dict, dictSize), ""); + zds->legacyVersion = zds->previousLegacyVersion = legacyVersion; + { size_t const hint = ZSTD_decompressLegacyStream(zds->legacyContext, legacyVersion, output, input); + if (hint==0) zds->streamStage = zdss_init; /* or stay in stage zdss_loadHeader */ + return hint; + } } +#endif + return hSize; /* error */ + } + if (hSize != 0) { /* need more input */ + size_t const toLoad = hSize - zds->lhSize; /* if hSize!=0, hSize > zds->lhSize */ + size_t const remainingInput = (size_t)(iend-ip); + assert(iend >= ip); + if (toLoad > remainingInput) { /* not enough input to load full header */ + if (remainingInput > 0) { + ZSTD_memcpy(zds->headerBuffer + zds->lhSize, ip, remainingInput); + zds->lhSize += remainingInput; + } + input->pos = input->size; + /* check first few bytes */ + FORWARD_IF_ERROR( + ZSTD_getFrameHeader_advanced(&zds->fParams, zds->headerBuffer, zds->lhSize, zds->format), + "First few bytes detected incorrect" ); + /* return hint input size */ + return (MAX((size_t)ZSTD_FRAMEHEADERSIZE_MIN(zds->format), hSize) - zds->lhSize) + ZSTD_blockHeaderSize; /* remaining header bytes + next block header */ + } + assert(ip != NULL); + ZSTD_memcpy(zds->headerBuffer + zds->lhSize, ip, toLoad); zds->lhSize = hSize; ip += toLoad; + break; + } } + + /* check for single-pass mode opportunity */ + if (zds->fParams.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN + && zds->fParams.frameType != ZSTD_skippableFrame + && (U64)(size_t)(oend-op) >= zds->fParams.frameContentSize) { + size_t const cSize = ZSTD_findFrameCompressedSize_advanced(istart, (size_t)(iend-istart), zds->format); + if (cSize <= (size_t)(iend-istart)) { + /* shortcut : using single-pass mode */ + size_t const decompressedSize = ZSTD_decompress_usingDDict(zds, op, (size_t)(oend-op), istart, cSize, ZSTD_getDDict(zds)); + if (ZSTD_isError(decompressedSize)) return decompressedSize; + DEBUGLOG(4, "shortcut to single-pass ZSTD_decompress_usingDDict()"); + assert(istart != NULL); + ip = istart + cSize; + op = op ? op + decompressedSize : op; /* can occur if frameContentSize = 0 (empty frame) */ + zds->expected = 0; + zds->streamStage = zdss_init; + someMoreWork = 0; + break; + } } + + /* Check output buffer is large enough for ZSTD_odm_stable. */ + if (zds->outBufferMode == ZSTD_bm_stable + && zds->fParams.frameType != ZSTD_skippableFrame + && zds->fParams.frameContentSize != ZSTD_CONTENTSIZE_UNKNOWN + && (U64)(size_t)(oend-op) < zds->fParams.frameContentSize) { + RETURN_ERROR(dstSize_tooSmall, "ZSTD_obm_stable passed but ZSTD_outBuffer is too small"); + } + + /* Consume header (see ZSTDds_decodeFrameHeader) */ + DEBUGLOG(4, "Consume header"); + FORWARD_IF_ERROR(ZSTD_decompressBegin_usingDDict(zds, ZSTD_getDDict(zds)), ""); + + if (zds->format == ZSTD_f_zstd1 + && (MEM_readLE32(zds->headerBuffer) & ZSTD_MAGIC_SKIPPABLE_MASK) == ZSTD_MAGIC_SKIPPABLE_START) { /* skippable frame */ + zds->expected = MEM_readLE32(zds->headerBuffer + ZSTD_FRAMEIDSIZE); + zds->stage = ZSTDds_skipFrame; + } else { + FORWARD_IF_ERROR(ZSTD_decodeFrameHeader(zds, zds->headerBuffer, zds->lhSize), ""); + zds->expected = ZSTD_blockHeaderSize; + zds->stage = ZSTDds_decodeBlockHeader; + } + + /* control buffer memory usage */ + DEBUGLOG(4, "Control max memory usage (%u KB <= max %u KB)", + (U32)(zds->fParams.windowSize >>10), + (U32)(zds->maxWindowSize >> 10) ); + zds->fParams.windowSize = MAX(zds->fParams.windowSize, 1U << ZSTD_WINDOWLOG_ABSOLUTEMIN); + RETURN_ERROR_IF(zds->fParams.windowSize > zds->maxWindowSize, + frameParameter_windowTooLarge, ""); + if (zds->maxBlockSizeParam != 0) + zds->fParams.blockSizeMax = MIN(zds->fParams.blockSizeMax, (unsigned)zds->maxBlockSizeParam); + + /* Adapt buffer sizes to frame header instructions */ + { size_t const neededInBuffSize = MAX(zds->fParams.blockSizeMax, 4 /* frame checksum */); + size_t const neededOutBuffSize = zds->outBufferMode == ZSTD_bm_buffered + ? ZSTD_decodingBufferSize_internal(zds->fParams.windowSize, zds->fParams.frameContentSize, zds->fParams.blockSizeMax) + : 0; + + ZSTD_DCtx_updateOversizedDuration(zds, neededInBuffSize, neededOutBuffSize); + + { int const tooSmall = (zds->inBuffSize < neededInBuffSize) || (zds->outBuffSize < neededOutBuffSize); + int const tooLarge = ZSTD_DCtx_isOversizedTooLong(zds); + + if (tooSmall || tooLarge) { + size_t const bufferSize = neededInBuffSize + neededOutBuffSize; + DEBUGLOG(4, "inBuff : from %u to %u", + (U32)zds->inBuffSize, (U32)neededInBuffSize); + DEBUGLOG(4, "outBuff : from %u to %u", + (U32)zds->outBuffSize, (U32)neededOutBuffSize); + if (zds->staticSize) { /* static DCtx */ + DEBUGLOG(4, "staticSize : %u", (U32)zds->staticSize); + assert(zds->staticSize >= sizeof(ZSTD_DCtx)); /* controlled at init */ + RETURN_ERROR_IF( + bufferSize > zds->staticSize - sizeof(ZSTD_DCtx), + memory_allocation, ""); + } else { + ZSTD_customFree(zds->inBuff, zds->customMem); + zds->inBuffSize = 0; + zds->outBuffSize = 0; + zds->inBuff = (char*)ZSTD_customMalloc(bufferSize, zds->customMem); + RETURN_ERROR_IF(zds->inBuff == NULL, memory_allocation, ""); + } + zds->inBuffSize = neededInBuffSize; + zds->outBuff = zds->inBuff + zds->inBuffSize; + zds->outBuffSize = neededOutBuffSize; + } } } + zds->streamStage = zdss_read; + ZSTD_FALLTHROUGH; + + case zdss_read: + DEBUGLOG(5, "stage zdss_read"); + { size_t const neededInSize = ZSTD_nextSrcSizeToDecompressWithInputSize(zds, (size_t)(iend - ip)); + DEBUGLOG(5, "neededInSize = %u", (U32)neededInSize); + if (neededInSize==0) { /* end of frame */ + zds->streamStage = zdss_init; + someMoreWork = 0; + break; + } + if ((size_t)(iend-ip) >= neededInSize) { /* decode directly from src */ + FORWARD_IF_ERROR(ZSTD_decompressContinueStream(zds, &op, oend, ip, neededInSize), ""); + assert(ip != NULL); + ip += neededInSize; + /* Function modifies the stage so we must break */ + break; + } } + if (ip==iend) { someMoreWork = 0; break; } /* no more input */ + zds->streamStage = zdss_load; + ZSTD_FALLTHROUGH; + + case zdss_load: + { size_t const neededInSize = ZSTD_nextSrcSizeToDecompress(zds); + size_t const toLoad = neededInSize - zds->inPos; + int const isSkipFrame = ZSTD_isSkipFrame(zds); + size_t loadedSize; + /* At this point we shouldn't be decompressing a block that we can stream. */ + assert(neededInSize == ZSTD_nextSrcSizeToDecompressWithInputSize(zds, (size_t)(iend - ip))); + if (isSkipFrame) { + loadedSize = MIN(toLoad, (size_t)(iend-ip)); + } else { + RETURN_ERROR_IF(toLoad > zds->inBuffSize - zds->inPos, + corruption_detected, + "should never happen"); + loadedSize = ZSTD_limitCopy(zds->inBuff + zds->inPos, toLoad, ip, (size_t)(iend-ip)); + } + if (loadedSize != 0) { + /* ip may be NULL */ + ip += loadedSize; + zds->inPos += loadedSize; + } + if (loadedSize < toLoad) { someMoreWork = 0; break; } /* not enough input, wait for more */ + + /* decode loaded input */ + zds->inPos = 0; /* input is consumed */ + FORWARD_IF_ERROR(ZSTD_decompressContinueStream(zds, &op, oend, zds->inBuff, neededInSize), ""); + /* Function modifies the stage so we must break */ + break; + } + case zdss_flush: + { + size_t const toFlushSize = zds->outEnd - zds->outStart; + size_t const flushedSize = ZSTD_limitCopy(op, (size_t)(oend-op), zds->outBuff + zds->outStart, toFlushSize); + + op = op ? op + flushedSize : op; + + zds->outStart += flushedSize; + if (flushedSize == toFlushSize) { /* flush completed */ + zds->streamStage = zdss_read; + if ( (zds->outBuffSize < zds->fParams.frameContentSize) + && (zds->outStart + zds->fParams.blockSizeMax > zds->outBuffSize) ) { + DEBUGLOG(5, "restart filling outBuff from beginning (left:%i, needed:%u)", + (int)(zds->outBuffSize - zds->outStart), + (U32)zds->fParams.blockSizeMax); + zds->outStart = zds->outEnd = 0; + } + break; + } } + /* cannot complete flush */ + someMoreWork = 0; + break; + + default: + assert(0); /* impossible */ + RETURN_ERROR(GENERIC, "impossible to reach"); /* some compilers require default to do something */ + } } + + /* result */ + input->pos = (size_t)(ip - (const char*)(input->src)); + output->pos = (size_t)(op - (char*)(output->dst)); + + /* Update the expected output buffer for ZSTD_obm_stable. */ + zds->expectedOutBuffer = *output; + + if ((ip==istart) && (op==ostart)) { /* no forward progress */ + zds->noForwardProgress ++; + if (zds->noForwardProgress >= ZSTD_NO_FORWARD_PROGRESS_MAX) { + RETURN_ERROR_IF(op==oend, noForwardProgress_destFull, ""); + RETURN_ERROR_IF(ip==iend, noForwardProgress_inputEmpty, ""); + assert(0); + } + } else { + zds->noForwardProgress = 0; + } + { size_t nextSrcSizeHint = ZSTD_nextSrcSizeToDecompress(zds); + if (!nextSrcSizeHint) { /* frame fully decoded */ + if (zds->outEnd == zds->outStart) { /* output fully flushed */ + if (zds->hostageByte) { + if (input->pos >= input->size) { + /* can't release hostage (not present) */ + zds->streamStage = zdss_read; + return 1; + } + input->pos++; /* release hostage */ + } /* zds->hostageByte */ + return 0; + } /* zds->outEnd == zds->outStart */ + if (!zds->hostageByte) { /* output not fully flushed; keep last byte as hostage; will be released when all output is flushed */ + input->pos--; /* note : pos > 0, otherwise, impossible to finish reading last block */ + zds->hostageByte=1; + } + return 1; + } /* nextSrcSizeHint==0 */ + nextSrcSizeHint += ZSTD_blockHeaderSize * (ZSTD_nextInputType(zds) == ZSTDnit_block); /* preload header of next block */ + assert(zds->inPos <= nextSrcSizeHint); + nextSrcSizeHint -= zds->inPos; /* part already loaded*/ + return nextSrcSizeHint; + } +} + +size_t ZSTD_decompressStream_simpleArgs ( + ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, size_t* dstPos, + const void* src, size_t srcSize, size_t* srcPos) +{ + ZSTD_outBuffer output; + ZSTD_inBuffer input; + output.dst = dst; + output.size = dstCapacity; + output.pos = *dstPos; + input.src = src; + input.size = srcSize; + input.pos = *srcPos; + { size_t const cErr = ZSTD_decompressStream(dctx, &output, &input); + *dstPos = output.pos; + *srcPos = input.pos; + return cErr; + } +} +/**** ended inlining decompress/zstd_decompress.c ****/ +/**** start inlining decompress/zstd_decompress_block.c ****/ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ + +/* zstd_decompress_block : + * this module takes care of decompressing _compressed_ block */ + +/*-******************************************************* +* Dependencies +*********************************************************/ +/**** skipping file: ../common/zstd_deps.h ****/ +/**** skipping file: ../common/compiler.h ****/ +/**** skipping file: ../common/cpu.h ****/ +/**** skipping file: ../common/mem.h ****/ +#define FSE_STATIC_LINKING_ONLY +/**** skipping file: ../common/fse.h ****/ +/**** skipping file: ../common/huf.h ****/ +/**** skipping file: ../common/zstd_internal.h ****/ +/**** skipping file: zstd_decompress_internal.h ****/ +/**** skipping file: zstd_ddict.h ****/ +/**** skipping file: zstd_decompress_block.h ****/ +/**** skipping file: ../common/bits.h ****/ + +/*_******************************************************* +* Macros +**********************************************************/ + +/* These two optional macros force the use one way or another of the two + * ZSTD_decompressSequences implementations. You can't force in both directions + * at the same time. + */ +#if defined(ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT) && \ + defined(ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG) +#error "Cannot force the use of the short and the long ZSTD_decompressSequences variants!" +#endif + + +/*_******************************************************* +* Memory operations +**********************************************************/ +static void ZSTD_copy4(void* dst, const void* src) { ZSTD_memcpy(dst, src, 4); } + + +/*-************************************************************* + * Block decoding + ***************************************************************/ + +static size_t ZSTD_blockSizeMax(ZSTD_DCtx const* dctx) +{ + size_t const blockSizeMax = dctx->isFrameDecompression ? dctx->fParams.blockSizeMax : ZSTD_BLOCKSIZE_MAX; + assert(blockSizeMax <= ZSTD_BLOCKSIZE_MAX); + return blockSizeMax; +} + +/*! ZSTD_getcBlockSize() : + * Provides the size of compressed block from block header `src` */ +size_t ZSTD_getcBlockSize(const void* src, size_t srcSize, + blockProperties_t* bpPtr) +{ + RETURN_ERROR_IF(srcSize < ZSTD_blockHeaderSize, srcSize_wrong, ""); + + { U32 const cBlockHeader = MEM_readLE24(src); + U32 const cSize = cBlockHeader >> 3; + bpPtr->lastBlock = cBlockHeader & 1; + bpPtr->blockType = (blockType_e)((cBlockHeader >> 1) & 3); + bpPtr->origSize = cSize; /* only useful for RLE */ + if (bpPtr->blockType == bt_rle) return 1; + RETURN_ERROR_IF(bpPtr->blockType == bt_reserved, corruption_detected, ""); + return cSize; + } +} + +/* Allocate buffer for literals, either overlapping current dst, or split between dst and litExtraBuffer, or stored entirely within litExtraBuffer */ +static void ZSTD_allocateLiteralsBuffer(ZSTD_DCtx* dctx, void* const dst, const size_t dstCapacity, const size_t litSize, + const streaming_operation streaming, const size_t expectedWriteSize, const unsigned splitImmediately) +{ + size_t const blockSizeMax = ZSTD_blockSizeMax(dctx); + assert(litSize <= blockSizeMax); + assert(dctx->isFrameDecompression || streaming == not_streaming); + assert(expectedWriteSize <= blockSizeMax); + if (streaming == not_streaming && dstCapacity > blockSizeMax + WILDCOPY_OVERLENGTH + litSize + WILDCOPY_OVERLENGTH) { + /* If we aren't streaming, we can just put the literals after the output + * of the current block. We don't need to worry about overwriting the + * extDict of our window, because it doesn't exist. + * So if we have space after the end of the block, just put it there. + */ + dctx->litBuffer = (BYTE*)dst + blockSizeMax + WILDCOPY_OVERLENGTH; + dctx->litBufferEnd = dctx->litBuffer + litSize; + dctx->litBufferLocation = ZSTD_in_dst; + } else if (litSize <= ZSTD_LITBUFFEREXTRASIZE) { + /* Literals fit entirely within the extra buffer, put them there to avoid + * having to split the literals. + */ + dctx->litBuffer = dctx->litExtraBuffer; + dctx->litBufferEnd = dctx->litBuffer + litSize; + dctx->litBufferLocation = ZSTD_not_in_dst; + } else { + assert(blockSizeMax > ZSTD_LITBUFFEREXTRASIZE); + /* Literals must be split between the output block and the extra lit + * buffer. We fill the extra lit buffer with the tail of the literals, + * and put the rest of the literals at the end of the block, with + * WILDCOPY_OVERLENGTH of buffer room to allow for overreads. + * This MUST not write more than our maxBlockSize beyond dst, because in + * streaming mode, that could overwrite part of our extDict window. + */ + if (splitImmediately) { + /* won't fit in litExtraBuffer, so it will be split between end of dst and extra buffer */ + dctx->litBuffer = (BYTE*)dst + expectedWriteSize - litSize + ZSTD_LITBUFFEREXTRASIZE - WILDCOPY_OVERLENGTH; + dctx->litBufferEnd = dctx->litBuffer + litSize - ZSTD_LITBUFFEREXTRASIZE; + } else { + /* initially this will be stored entirely in dst during huffman decoding, it will partially be shifted to litExtraBuffer after */ + dctx->litBuffer = (BYTE*)dst + expectedWriteSize - litSize; + dctx->litBufferEnd = (BYTE*)dst + expectedWriteSize; + } + dctx->litBufferLocation = ZSTD_split; + assert(dctx->litBufferEnd <= (BYTE*)dst + expectedWriteSize); + } +} + +/*! ZSTD_decodeLiteralsBlock() : + * Where it is possible to do so without being stomped by the output during decompression, the literals block will be stored + * in the dstBuffer. If there is room to do so, it will be stored in full in the excess dst space after where the current + * block will be output. Otherwise it will be stored at the end of the current dst blockspace, with a small portion being + * stored in dctx->litExtraBuffer to help keep it "ahead" of the current output write. + * + * @return : nb of bytes read from src (< srcSize ) + * note : symbol not declared but exposed for fullbench */ +static size_t ZSTD_decodeLiteralsBlock(ZSTD_DCtx* dctx, + const void* src, size_t srcSize, /* note : srcSize < BLOCKSIZE */ + void* dst, size_t dstCapacity, const streaming_operation streaming) +{ + DEBUGLOG(5, "ZSTD_decodeLiteralsBlock"); + RETURN_ERROR_IF(srcSize < MIN_CBLOCK_SIZE, corruption_detected, ""); + + { const BYTE* const istart = (const BYTE*) src; + symbolEncodingType_e const litEncType = (symbolEncodingType_e)(istart[0] & 3); + size_t const blockSizeMax = ZSTD_blockSizeMax(dctx); + + switch(litEncType) + { + case set_repeat: + DEBUGLOG(5, "set_repeat flag : re-using stats from previous compressed literals block"); + RETURN_ERROR_IF(dctx->litEntropy==0, dictionary_corrupted, ""); + ZSTD_FALLTHROUGH; + + case set_compressed: + RETURN_ERROR_IF(srcSize < 5, corruption_detected, "srcSize >= MIN_CBLOCK_SIZE == 2; here we need up to 5 for case 3"); + { size_t lhSize, litSize, litCSize; + U32 singleStream=0; + U32 const lhlCode = (istart[0] >> 2) & 3; + U32 const lhc = MEM_readLE32(istart); + size_t hufSuccess; + size_t expectedWriteSize = MIN(blockSizeMax, dstCapacity); + int const flags = 0 + | (ZSTD_DCtx_get_bmi2(dctx) ? HUF_flags_bmi2 : 0) + | (dctx->disableHufAsm ? HUF_flags_disableAsm : 0); + switch(lhlCode) + { + case 0: case 1: default: /* note : default is impossible, since lhlCode into [0..3] */ + /* 2 - 2 - 10 - 10 */ + singleStream = !lhlCode; + lhSize = 3; + litSize = (lhc >> 4) & 0x3FF; + litCSize = (lhc >> 14) & 0x3FF; + break; + case 2: + /* 2 - 2 - 14 - 14 */ + lhSize = 4; + litSize = (lhc >> 4) & 0x3FFF; + litCSize = lhc >> 18; + break; + case 3: + /* 2 - 2 - 18 - 18 */ + lhSize = 5; + litSize = (lhc >> 4) & 0x3FFFF; + litCSize = (lhc >> 22) + ((size_t)istart[4] << 10); + break; + } + RETURN_ERROR_IF(litSize > 0 && dst == NULL, dstSize_tooSmall, "NULL not handled"); + RETURN_ERROR_IF(litSize > blockSizeMax, corruption_detected, ""); + if (!singleStream) + RETURN_ERROR_IF(litSize < MIN_LITERALS_FOR_4_STREAMS, literals_headerWrong, + "Not enough literals (%zu) for the 4-streams mode (min %u)", + litSize, MIN_LITERALS_FOR_4_STREAMS); + RETURN_ERROR_IF(litCSize + lhSize > srcSize, corruption_detected, ""); + RETURN_ERROR_IF(expectedWriteSize < litSize , dstSize_tooSmall, ""); + ZSTD_allocateLiteralsBuffer(dctx, dst, dstCapacity, litSize, streaming, expectedWriteSize, 0); + + /* prefetch huffman table if cold */ + if (dctx->ddictIsCold && (litSize > 768 /* heuristic */)) { + PREFETCH_AREA(dctx->HUFptr, sizeof(dctx->entropy.hufTable)); + } + + if (litEncType==set_repeat) { + if (singleStream) { + hufSuccess = HUF_decompress1X_usingDTable( + dctx->litBuffer, litSize, istart+lhSize, litCSize, + dctx->HUFptr, flags); + } else { + assert(litSize >= MIN_LITERALS_FOR_4_STREAMS); + hufSuccess = HUF_decompress4X_usingDTable( + dctx->litBuffer, litSize, istart+lhSize, litCSize, + dctx->HUFptr, flags); + } + } else { + if (singleStream) { +#if defined(HUF_FORCE_DECOMPRESS_X2) + hufSuccess = HUF_decompress1X_DCtx_wksp( + dctx->entropy.hufTable, dctx->litBuffer, litSize, + istart+lhSize, litCSize, dctx->workspace, + sizeof(dctx->workspace), flags); +#else + hufSuccess = HUF_decompress1X1_DCtx_wksp( + dctx->entropy.hufTable, dctx->litBuffer, litSize, + istart+lhSize, litCSize, dctx->workspace, + sizeof(dctx->workspace), flags); +#endif + } else { + hufSuccess = HUF_decompress4X_hufOnly_wksp( + dctx->entropy.hufTable, dctx->litBuffer, litSize, + istart+lhSize, litCSize, dctx->workspace, + sizeof(dctx->workspace), flags); + } + } + if (dctx->litBufferLocation == ZSTD_split) + { + assert(litSize > ZSTD_LITBUFFEREXTRASIZE); + ZSTD_memcpy(dctx->litExtraBuffer, dctx->litBufferEnd - ZSTD_LITBUFFEREXTRASIZE, ZSTD_LITBUFFEREXTRASIZE); + ZSTD_memmove(dctx->litBuffer + ZSTD_LITBUFFEREXTRASIZE - WILDCOPY_OVERLENGTH, dctx->litBuffer, litSize - ZSTD_LITBUFFEREXTRASIZE); + dctx->litBuffer += ZSTD_LITBUFFEREXTRASIZE - WILDCOPY_OVERLENGTH; + dctx->litBufferEnd -= WILDCOPY_OVERLENGTH; + assert(dctx->litBufferEnd <= (BYTE*)dst + blockSizeMax); + } + + RETURN_ERROR_IF(HUF_isError(hufSuccess), corruption_detected, ""); + + dctx->litPtr = dctx->litBuffer; + dctx->litSize = litSize; + dctx->litEntropy = 1; + if (litEncType==set_compressed) dctx->HUFptr = dctx->entropy.hufTable; + return litCSize + lhSize; + } + + case set_basic: + { size_t litSize, lhSize; + U32 const lhlCode = ((istart[0]) >> 2) & 3; + size_t expectedWriteSize = MIN(blockSizeMax, dstCapacity); + switch(lhlCode) + { + case 0: case 2: default: /* note : default is impossible, since lhlCode into [0..3] */ + lhSize = 1; + litSize = istart[0] >> 3; + break; + case 1: + lhSize = 2; + litSize = MEM_readLE16(istart) >> 4; + break; + case 3: + lhSize = 3; + RETURN_ERROR_IF(srcSize<3, corruption_detected, "srcSize >= MIN_CBLOCK_SIZE == 2; here we need lhSize = 3"); + litSize = MEM_readLE24(istart) >> 4; + break; + } + + RETURN_ERROR_IF(litSize > 0 && dst == NULL, dstSize_tooSmall, "NULL not handled"); + RETURN_ERROR_IF(litSize > blockSizeMax, corruption_detected, ""); + RETURN_ERROR_IF(expectedWriteSize < litSize, dstSize_tooSmall, ""); + ZSTD_allocateLiteralsBuffer(dctx, dst, dstCapacity, litSize, streaming, expectedWriteSize, 1); + if (lhSize+litSize+WILDCOPY_OVERLENGTH > srcSize) { /* risk reading beyond src buffer with wildcopy */ + RETURN_ERROR_IF(litSize+lhSize > srcSize, corruption_detected, ""); + if (dctx->litBufferLocation == ZSTD_split) + { + ZSTD_memcpy(dctx->litBuffer, istart + lhSize, litSize - ZSTD_LITBUFFEREXTRASIZE); + ZSTD_memcpy(dctx->litExtraBuffer, istart + lhSize + litSize - ZSTD_LITBUFFEREXTRASIZE, ZSTD_LITBUFFEREXTRASIZE); + } + else + { + ZSTD_memcpy(dctx->litBuffer, istart + lhSize, litSize); + } + dctx->litPtr = dctx->litBuffer; + dctx->litSize = litSize; + return lhSize+litSize; + } + /* direct reference into compressed stream */ + dctx->litPtr = istart+lhSize; + dctx->litSize = litSize; + dctx->litBufferEnd = dctx->litPtr + litSize; + dctx->litBufferLocation = ZSTD_not_in_dst; + return lhSize+litSize; + } + + case set_rle: + { U32 const lhlCode = ((istart[0]) >> 2) & 3; + size_t litSize, lhSize; + size_t expectedWriteSize = MIN(blockSizeMax, dstCapacity); + switch(lhlCode) + { + case 0: case 2: default: /* note : default is impossible, since lhlCode into [0..3] */ + lhSize = 1; + litSize = istart[0] >> 3; + break; + case 1: + lhSize = 2; + RETURN_ERROR_IF(srcSize<3, corruption_detected, "srcSize >= MIN_CBLOCK_SIZE == 2; here we need lhSize+1 = 3"); + litSize = MEM_readLE16(istart) >> 4; + break; + case 3: + lhSize = 3; + RETURN_ERROR_IF(srcSize<4, corruption_detected, "srcSize >= MIN_CBLOCK_SIZE == 2; here we need lhSize+1 = 4"); + litSize = MEM_readLE24(istart) >> 4; + break; + } + RETURN_ERROR_IF(litSize > 0 && dst == NULL, dstSize_tooSmall, "NULL not handled"); + RETURN_ERROR_IF(litSize > blockSizeMax, corruption_detected, ""); + RETURN_ERROR_IF(expectedWriteSize < litSize, dstSize_tooSmall, ""); + ZSTD_allocateLiteralsBuffer(dctx, dst, dstCapacity, litSize, streaming, expectedWriteSize, 1); + if (dctx->litBufferLocation == ZSTD_split) + { + ZSTD_memset(dctx->litBuffer, istart[lhSize], litSize - ZSTD_LITBUFFEREXTRASIZE); + ZSTD_memset(dctx->litExtraBuffer, istart[lhSize], ZSTD_LITBUFFEREXTRASIZE); + } + else + { + ZSTD_memset(dctx->litBuffer, istart[lhSize], litSize); + } + dctx->litPtr = dctx->litBuffer; + dctx->litSize = litSize; + return lhSize+1; + } + default: + RETURN_ERROR(corruption_detected, "impossible"); + } + } +} + +/* Hidden declaration for fullbench */ +size_t ZSTD_decodeLiteralsBlock_wrapper(ZSTD_DCtx* dctx, + const void* src, size_t srcSize, + void* dst, size_t dstCapacity); +size_t ZSTD_decodeLiteralsBlock_wrapper(ZSTD_DCtx* dctx, + const void* src, size_t srcSize, + void* dst, size_t dstCapacity) +{ + dctx->isFrameDecompression = 0; + return ZSTD_decodeLiteralsBlock(dctx, src, srcSize, dst, dstCapacity, not_streaming); +} + +/* Default FSE distribution tables. + * These are pre-calculated FSE decoding tables using default distributions as defined in specification : + * https://github.com/facebook/zstd/blob/release/doc/zstd_compression_format.md#default-distributions + * They were generated programmatically with following method : + * - start from default distributions, present in /lib/common/zstd_internal.h + * - generate tables normally, using ZSTD_buildFSETable() + * - printout the content of tables + * - pretify output, report below, test with fuzzer to ensure it's correct */ + +/* Default FSE distribution table for Literal Lengths */ +static const ZSTD_seqSymbol LL_defaultDTable[(1<tableLog = 0; + DTableH->fastMode = 0; + + cell->nbBits = 0; + cell->nextState = 0; + assert(nbAddBits < 255); + cell->nbAdditionalBits = nbAddBits; + cell->baseValue = baseValue; +} + + +/* ZSTD_buildFSETable() : + * generate FSE decoding table for one symbol (ll, ml or off) + * cannot fail if input is valid => + * all inputs are presumed validated at this stage */ +FORCE_INLINE_TEMPLATE +void ZSTD_buildFSETable_body(ZSTD_seqSymbol* dt, + const short* normalizedCounter, unsigned maxSymbolValue, + const U32* baseValue, const U8* nbAdditionalBits, + unsigned tableLog, void* wksp, size_t wkspSize) +{ + ZSTD_seqSymbol* const tableDecode = dt+1; + U32 const maxSV1 = maxSymbolValue + 1; + U32 const tableSize = 1 << tableLog; + + U16* symbolNext = (U16*)wksp; + BYTE* spread = (BYTE*)(symbolNext + MaxSeq + 1); + U32 highThreshold = tableSize - 1; + + + /* Sanity Checks */ + assert(maxSymbolValue <= MaxSeq); + assert(tableLog <= MaxFSELog); + assert(wkspSize >= ZSTD_BUILD_FSE_TABLE_WKSP_SIZE); + (void)wkspSize; + /* Init, lay down lowprob symbols */ + { ZSTD_seqSymbol_header DTableH; + DTableH.tableLog = tableLog; + DTableH.fastMode = 1; + { S16 const largeLimit= (S16)(1 << (tableLog-1)); + U32 s; + for (s=0; s= largeLimit) DTableH.fastMode=0; + assert(normalizedCounter[s]>=0); + symbolNext[s] = (U16)normalizedCounter[s]; + } } } + ZSTD_memcpy(dt, &DTableH, sizeof(DTableH)); + } + + /* Spread symbols */ + assert(tableSize <= 512); + /* Specialized symbol spreading for the case when there are + * no low probability (-1 count) symbols. When compressing + * small blocks we avoid low probability symbols to hit this + * case, since header decoding speed matters more. + */ + if (highThreshold == tableSize - 1) { + size_t const tableMask = tableSize-1; + size_t const step = FSE_TABLESTEP(tableSize); + /* First lay down the symbols in order. + * We use a uint64_t to lay down 8 bytes at a time. This reduces branch + * misses since small blocks generally have small table logs, so nearly + * all symbols have counts <= 8. We ensure we have 8 bytes at the end of + * our buffer to handle the over-write. + */ + { + U64 const add = 0x0101010101010101ull; + size_t pos = 0; + U64 sv = 0; + U32 s; + for (s=0; s=0); + pos += (size_t)n; + } + } + /* Now we spread those positions across the table. + * The benefit of doing it in two stages is that we avoid the + * variable size inner loop, which caused lots of branch misses. + * Now we can run through all the positions without any branch misses. + * We unroll the loop twice, since that is what empirically worked best. + */ + { + size_t position = 0; + size_t s; + size_t const unroll = 2; + assert(tableSize % unroll == 0); /* FSE_MIN_TABLELOG is 5 */ + for (s = 0; s < (size_t)tableSize; s += unroll) { + size_t u; + for (u = 0; u < unroll; ++u) { + size_t const uPosition = (position + (u * step)) & tableMask; + tableDecode[uPosition].baseValue = spread[s + u]; + } + position = (position + (unroll * step)) & tableMask; + } + assert(position == 0); + } + } else { + U32 const tableMask = tableSize-1; + U32 const step = FSE_TABLESTEP(tableSize); + U32 s, position = 0; + for (s=0; s highThreshold)) position = (position + step) & tableMask; /* lowprob area */ + } } + assert(position == 0); /* position must reach all cells once, otherwise normalizedCounter is incorrect */ + } + + /* Build Decoding table */ + { + U32 u; + for (u=0; u max, corruption_detected, ""); + { U32 const symbol = *(const BYTE*)src; + U32 const baseline = baseValue[symbol]; + U8 const nbBits = nbAdditionalBits[symbol]; + ZSTD_buildSeqTable_rle(DTableSpace, baseline, nbBits); + } + *DTablePtr = DTableSpace; + return 1; + case set_basic : + *DTablePtr = defaultTable; + return 0; + case set_repeat: + RETURN_ERROR_IF(!flagRepeatTable, corruption_detected, ""); + /* prefetch FSE table if used */ + if (ddictIsCold && (nbSeq > 24 /* heuristic */)) { + const void* const pStart = *DTablePtr; + size_t const pSize = sizeof(ZSTD_seqSymbol) * (SEQSYMBOL_TABLE_SIZE(maxLog)); + PREFETCH_AREA(pStart, pSize); + } + return 0; + case set_compressed : + { unsigned tableLog; + S16 norm[MaxSeq+1]; + size_t const headerSize = FSE_readNCount(norm, &max, &tableLog, src, srcSize); + RETURN_ERROR_IF(FSE_isError(headerSize), corruption_detected, ""); + RETURN_ERROR_IF(tableLog > maxLog, corruption_detected, ""); + ZSTD_buildFSETable(DTableSpace, norm, max, baseValue, nbAdditionalBits, tableLog, wksp, wkspSize, bmi2); + *DTablePtr = DTableSpace; + return headerSize; + } + default : + assert(0); + RETURN_ERROR(GENERIC, "impossible"); + } +} + +size_t ZSTD_decodeSeqHeaders(ZSTD_DCtx* dctx, int* nbSeqPtr, + const void* src, size_t srcSize) +{ + const BYTE* const istart = (const BYTE*)src; + const BYTE* const iend = istart + srcSize; + const BYTE* ip = istart; + int nbSeq; + DEBUGLOG(5, "ZSTD_decodeSeqHeaders"); + + /* check */ + RETURN_ERROR_IF(srcSize < MIN_SEQUENCES_SIZE, srcSize_wrong, ""); + + /* SeqHead */ + nbSeq = *ip++; + if (nbSeq > 0x7F) { + if (nbSeq == 0xFF) { + RETURN_ERROR_IF(ip+2 > iend, srcSize_wrong, ""); + nbSeq = MEM_readLE16(ip) + LONGNBSEQ; + ip+=2; + } else { + RETURN_ERROR_IF(ip >= iend, srcSize_wrong, ""); + nbSeq = ((nbSeq-0x80)<<8) + *ip++; + } + } + *nbSeqPtr = nbSeq; + + if (nbSeq == 0) { + /* No sequence : section ends immediately */ + RETURN_ERROR_IF(ip != iend, corruption_detected, + "extraneous data present in the Sequences section"); + return (size_t)(ip - istart); + } + + /* FSE table descriptors */ + RETURN_ERROR_IF(ip+1 > iend, srcSize_wrong, ""); /* minimum possible size: 1 byte for symbol encoding types */ + RETURN_ERROR_IF(*ip & 3, corruption_detected, ""); /* The last field, Reserved, must be all-zeroes. */ + { symbolEncodingType_e const LLtype = (symbolEncodingType_e)(*ip >> 6); + symbolEncodingType_e const OFtype = (symbolEncodingType_e)((*ip >> 4) & 3); + symbolEncodingType_e const MLtype = (symbolEncodingType_e)((*ip >> 2) & 3); + ip++; + + /* Build DTables */ + { size_t const llhSize = ZSTD_buildSeqTable(dctx->entropy.LLTable, &dctx->LLTptr, + LLtype, MaxLL, LLFSELog, + ip, iend-ip, + LL_base, LL_bits, + LL_defaultDTable, dctx->fseEntropy, + dctx->ddictIsCold, nbSeq, + dctx->workspace, sizeof(dctx->workspace), + ZSTD_DCtx_get_bmi2(dctx)); + RETURN_ERROR_IF(ZSTD_isError(llhSize), corruption_detected, "ZSTD_buildSeqTable failed"); + ip += llhSize; + } + + { size_t const ofhSize = ZSTD_buildSeqTable(dctx->entropy.OFTable, &dctx->OFTptr, + OFtype, MaxOff, OffFSELog, + ip, iend-ip, + OF_base, OF_bits, + OF_defaultDTable, dctx->fseEntropy, + dctx->ddictIsCold, nbSeq, + dctx->workspace, sizeof(dctx->workspace), + ZSTD_DCtx_get_bmi2(dctx)); + RETURN_ERROR_IF(ZSTD_isError(ofhSize), corruption_detected, "ZSTD_buildSeqTable failed"); + ip += ofhSize; + } + + { size_t const mlhSize = ZSTD_buildSeqTable(dctx->entropy.MLTable, &dctx->MLTptr, + MLtype, MaxML, MLFSELog, + ip, iend-ip, + ML_base, ML_bits, + ML_defaultDTable, dctx->fseEntropy, + dctx->ddictIsCold, nbSeq, + dctx->workspace, sizeof(dctx->workspace), + ZSTD_DCtx_get_bmi2(dctx)); + RETURN_ERROR_IF(ZSTD_isError(mlhSize), corruption_detected, "ZSTD_buildSeqTable failed"); + ip += mlhSize; + } + } + + return ip-istart; +} + + +typedef struct { + size_t litLength; + size_t matchLength; + size_t offset; +} seq_t; + +typedef struct { + size_t state; + const ZSTD_seqSymbol* table; +} ZSTD_fseState; + +typedef struct { + BIT_DStream_t DStream; + ZSTD_fseState stateLL; + ZSTD_fseState stateOffb; + ZSTD_fseState stateML; + size_t prevOffset[ZSTD_REP_NUM]; +} seqState_t; + +/*! ZSTD_overlapCopy8() : + * Copies 8 bytes from ip to op and updates op and ip where ip <= op. + * If the offset is < 8 then the offset is spread to at least 8 bytes. + * + * Precondition: *ip <= *op + * Postcondition: *op - *op >= 8 + */ +HINT_INLINE void ZSTD_overlapCopy8(BYTE** op, BYTE const** ip, size_t offset) { + assert(*ip <= *op); + if (offset < 8) { + /* close range match, overlap */ + static const U32 dec32table[] = { 0, 1, 2, 1, 4, 4, 4, 4 }; /* added */ + static const int dec64table[] = { 8, 8, 8, 7, 8, 9,10,11 }; /* subtracted */ + int const sub2 = dec64table[offset]; + (*op)[0] = (*ip)[0]; + (*op)[1] = (*ip)[1]; + (*op)[2] = (*ip)[2]; + (*op)[3] = (*ip)[3]; + *ip += dec32table[offset]; + ZSTD_copy4(*op+4, *ip); + *ip -= sub2; + } else { + ZSTD_copy8(*op, *ip); + } + *ip += 8; + *op += 8; + assert(*op - *ip >= 8); +} + +/*! ZSTD_safecopy() : + * Specialized version of memcpy() that is allowed to READ up to WILDCOPY_OVERLENGTH past the input buffer + * and write up to 16 bytes past oend_w (op >= oend_w is allowed). + * This function is only called in the uncommon case where the sequence is near the end of the block. It + * should be fast for a single long sequence, but can be slow for several short sequences. + * + * @param ovtype controls the overlap detection + * - ZSTD_no_overlap: The source and destination are guaranteed to be at least WILDCOPY_VECLEN bytes apart. + * - ZSTD_overlap_src_before_dst: The src and dst may overlap and may be any distance apart. + * The src buffer must be before the dst buffer. + */ +static void ZSTD_safecopy(BYTE* op, const BYTE* const oend_w, BYTE const* ip, ptrdiff_t length, ZSTD_overlap_e ovtype) { + ptrdiff_t const diff = op - ip; + BYTE* const oend = op + length; + + assert((ovtype == ZSTD_no_overlap && (diff <= -8 || diff >= 8 || op >= oend_w)) || + (ovtype == ZSTD_overlap_src_before_dst && diff >= 0)); + + if (length < 8) { + /* Handle short lengths. */ + while (op < oend) *op++ = *ip++; + return; + } + if (ovtype == ZSTD_overlap_src_before_dst) { + /* Copy 8 bytes and ensure the offset >= 8 when there can be overlap. */ + assert(length >= 8); + ZSTD_overlapCopy8(&op, &ip, diff); + length -= 8; + assert(op - ip >= 8); + assert(op <= oend); + } + + if (oend <= oend_w) { + /* No risk of overwrite. */ + ZSTD_wildcopy(op, ip, length, ovtype); + return; + } + if (op <= oend_w) { + /* Wildcopy until we get close to the end. */ + assert(oend > oend_w); + ZSTD_wildcopy(op, ip, oend_w - op, ovtype); + ip += oend_w - op; + op += oend_w - op; + } + /* Handle the leftovers. */ + while (op < oend) *op++ = *ip++; +} + +/* ZSTD_safecopyDstBeforeSrc(): + * This version allows overlap with dst before src, or handles the non-overlap case with dst after src + * Kept separate from more common ZSTD_safecopy case to avoid performance impact to the safecopy common case */ +static void ZSTD_safecopyDstBeforeSrc(BYTE* op, const BYTE* ip, ptrdiff_t length) { + ptrdiff_t const diff = op - ip; + BYTE* const oend = op + length; + + if (length < 8 || diff > -8) { + /* Handle short lengths, close overlaps, and dst not before src. */ + while (op < oend) *op++ = *ip++; + return; + } + + if (op <= oend - WILDCOPY_OVERLENGTH && diff < -WILDCOPY_VECLEN) { + ZSTD_wildcopy(op, ip, oend - WILDCOPY_OVERLENGTH - op, ZSTD_no_overlap); + ip += oend - WILDCOPY_OVERLENGTH - op; + op += oend - WILDCOPY_OVERLENGTH - op; + } + + /* Handle the leftovers. */ + while (op < oend) *op++ = *ip++; +} + +/* ZSTD_execSequenceEnd(): + * This version handles cases that are near the end of the output buffer. It requires + * more careful checks to make sure there is no overflow. By separating out these hard + * and unlikely cases, we can speed up the common cases. + * + * NOTE: This function needs to be fast for a single long sequence, but doesn't need + * to be optimized for many small sequences, since those fall into ZSTD_execSequence(). + */ +FORCE_NOINLINE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_execSequenceEnd(BYTE* op, + BYTE* const oend, seq_t sequence, + const BYTE** litPtr, const BYTE* const litLimit, + const BYTE* const prefixStart, const BYTE* const virtualStart, const BYTE* const dictEnd) +{ + BYTE* const oLitEnd = op + sequence.litLength; + size_t const sequenceLength = sequence.litLength + sequence.matchLength; + const BYTE* const iLitEnd = *litPtr + sequence.litLength; + const BYTE* match = oLitEnd - sequence.offset; + BYTE* const oend_w = oend - WILDCOPY_OVERLENGTH; + + /* bounds checks : careful of address space overflow in 32-bit mode */ + RETURN_ERROR_IF(sequenceLength > (size_t)(oend - op), dstSize_tooSmall, "last match must fit within dstBuffer"); + RETURN_ERROR_IF(sequence.litLength > (size_t)(litLimit - *litPtr), corruption_detected, "try to read beyond literal buffer"); + assert(op < op + sequenceLength); + assert(oLitEnd < op + sequenceLength); + + /* copy literals */ + ZSTD_safecopy(op, oend_w, *litPtr, sequence.litLength, ZSTD_no_overlap); + op = oLitEnd; + *litPtr = iLitEnd; + + /* copy Match */ + if (sequence.offset > (size_t)(oLitEnd - prefixStart)) { + /* offset beyond prefix */ + RETURN_ERROR_IF(sequence.offset > (size_t)(oLitEnd - virtualStart), corruption_detected, ""); + match = dictEnd - (prefixStart - match); + if (match + sequence.matchLength <= dictEnd) { + ZSTD_memmove(oLitEnd, match, sequence.matchLength); + return sequenceLength; + } + /* span extDict & currentPrefixSegment */ + { size_t const length1 = dictEnd - match; + ZSTD_memmove(oLitEnd, match, length1); + op = oLitEnd + length1; + sequence.matchLength -= length1; + match = prefixStart; + } + } + ZSTD_safecopy(op, oend_w, match, sequence.matchLength, ZSTD_overlap_src_before_dst); + return sequenceLength; +} + +/* ZSTD_execSequenceEndSplitLitBuffer(): + * This version is intended to be used during instances where the litBuffer is still split. It is kept separate to avoid performance impact for the good case. + */ +FORCE_NOINLINE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_execSequenceEndSplitLitBuffer(BYTE* op, + BYTE* const oend, const BYTE* const oend_w, seq_t sequence, + const BYTE** litPtr, const BYTE* const litLimit, + const BYTE* const prefixStart, const BYTE* const virtualStart, const BYTE* const dictEnd) +{ + BYTE* const oLitEnd = op + sequence.litLength; + size_t const sequenceLength = sequence.litLength + sequence.matchLength; + const BYTE* const iLitEnd = *litPtr + sequence.litLength; + const BYTE* match = oLitEnd - sequence.offset; + + + /* bounds checks : careful of address space overflow in 32-bit mode */ + RETURN_ERROR_IF(sequenceLength > (size_t)(oend - op), dstSize_tooSmall, "last match must fit within dstBuffer"); + RETURN_ERROR_IF(sequence.litLength > (size_t)(litLimit - *litPtr), corruption_detected, "try to read beyond literal buffer"); + assert(op < op + sequenceLength); + assert(oLitEnd < op + sequenceLength); + + /* copy literals */ + RETURN_ERROR_IF(op > *litPtr && op < *litPtr + sequence.litLength, dstSize_tooSmall, "output should not catch up to and overwrite literal buffer"); + ZSTD_safecopyDstBeforeSrc(op, *litPtr, sequence.litLength); + op = oLitEnd; + *litPtr = iLitEnd; + + /* copy Match */ + if (sequence.offset > (size_t)(oLitEnd - prefixStart)) { + /* offset beyond prefix */ + RETURN_ERROR_IF(sequence.offset > (size_t)(oLitEnd - virtualStart), corruption_detected, ""); + match = dictEnd - (prefixStart - match); + if (match + sequence.matchLength <= dictEnd) { + ZSTD_memmove(oLitEnd, match, sequence.matchLength); + return sequenceLength; + } + /* span extDict & currentPrefixSegment */ + { size_t const length1 = dictEnd - match; + ZSTD_memmove(oLitEnd, match, length1); + op = oLitEnd + length1; + sequence.matchLength -= length1; + match = prefixStart; + } + } + ZSTD_safecopy(op, oend_w, match, sequence.matchLength, ZSTD_overlap_src_before_dst); + return sequenceLength; +} + +HINT_INLINE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_execSequence(BYTE* op, + BYTE* const oend, seq_t sequence, + const BYTE** litPtr, const BYTE* const litLimit, + const BYTE* const prefixStart, const BYTE* const virtualStart, const BYTE* const dictEnd) +{ + BYTE* const oLitEnd = op + sequence.litLength; + size_t const sequenceLength = sequence.litLength + sequence.matchLength; + BYTE* const oMatchEnd = op + sequenceLength; /* risk : address space overflow (32-bits) */ + BYTE* const oend_w = oend - WILDCOPY_OVERLENGTH; /* risk : address space underflow on oend=NULL */ + const BYTE* const iLitEnd = *litPtr + sequence.litLength; + const BYTE* match = oLitEnd - sequence.offset; + + assert(op != NULL /* Precondition */); + assert(oend_w < oend /* No underflow */); + +#if defined(__aarch64__) + /* prefetch sequence starting from match that will be used for copy later */ + PREFETCH_L1(match); +#endif + /* Handle edge cases in a slow path: + * - Read beyond end of literals + * - Match end is within WILDCOPY_OVERLIMIT of oend + * - 32-bit mode and the match length overflows + */ + if (UNLIKELY( + iLitEnd > litLimit || + oMatchEnd > oend_w || + (MEM_32bits() && (size_t)(oend - op) < sequenceLength + WILDCOPY_OVERLENGTH))) + return ZSTD_execSequenceEnd(op, oend, sequence, litPtr, litLimit, prefixStart, virtualStart, dictEnd); + + /* Assumptions (everything else goes into ZSTD_execSequenceEnd()) */ + assert(op <= oLitEnd /* No overflow */); + assert(oLitEnd < oMatchEnd /* Non-zero match & no overflow */); + assert(oMatchEnd <= oend /* No underflow */); + assert(iLitEnd <= litLimit /* Literal length is in bounds */); + assert(oLitEnd <= oend_w /* Can wildcopy literals */); + assert(oMatchEnd <= oend_w /* Can wildcopy matches */); + + /* Copy Literals: + * Split out litLength <= 16 since it is nearly always true. +1.6% on gcc-9. + * We likely don't need the full 32-byte wildcopy. + */ + assert(WILDCOPY_OVERLENGTH >= 16); + ZSTD_copy16(op, (*litPtr)); + if (UNLIKELY(sequence.litLength > 16)) { + ZSTD_wildcopy(op + 16, (*litPtr) + 16, sequence.litLength - 16, ZSTD_no_overlap); + } + op = oLitEnd; + *litPtr = iLitEnd; /* update for next sequence */ + + /* Copy Match */ + if (sequence.offset > (size_t)(oLitEnd - prefixStart)) { + /* offset beyond prefix -> go into extDict */ + RETURN_ERROR_IF(UNLIKELY(sequence.offset > (size_t)(oLitEnd - virtualStart)), corruption_detected, ""); + match = dictEnd + (match - prefixStart); + if (match + sequence.matchLength <= dictEnd) { + ZSTD_memmove(oLitEnd, match, sequence.matchLength); + return sequenceLength; + } + /* span extDict & currentPrefixSegment */ + { size_t const length1 = dictEnd - match; + ZSTD_memmove(oLitEnd, match, length1); + op = oLitEnd + length1; + sequence.matchLength -= length1; + match = prefixStart; + } + } + /* Match within prefix of 1 or more bytes */ + assert(op <= oMatchEnd); + assert(oMatchEnd <= oend_w); + assert(match >= prefixStart); + assert(sequence.matchLength >= 1); + + /* Nearly all offsets are >= WILDCOPY_VECLEN bytes, which means we can use wildcopy + * without overlap checking. + */ + if (LIKELY(sequence.offset >= WILDCOPY_VECLEN)) { + /* We bet on a full wildcopy for matches, since we expect matches to be + * longer than literals (in general). In silesia, ~10% of matches are longer + * than 16 bytes. + */ + ZSTD_wildcopy(op, match, (ptrdiff_t)sequence.matchLength, ZSTD_no_overlap); + return sequenceLength; + } + assert(sequence.offset < WILDCOPY_VECLEN); + + /* Copy 8 bytes and spread the offset to be >= 8. */ + ZSTD_overlapCopy8(&op, &match, sequence.offset); + + /* If the match length is > 8 bytes, then continue with the wildcopy. */ + if (sequence.matchLength > 8) { + assert(op < oMatchEnd); + ZSTD_wildcopy(op, match, (ptrdiff_t)sequence.matchLength - 8, ZSTD_overlap_src_before_dst); + } + return sequenceLength; +} + +HINT_INLINE +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +size_t ZSTD_execSequenceSplitLitBuffer(BYTE* op, + BYTE* const oend, const BYTE* const oend_w, seq_t sequence, + const BYTE** litPtr, const BYTE* const litLimit, + const BYTE* const prefixStart, const BYTE* const virtualStart, const BYTE* const dictEnd) +{ + BYTE* const oLitEnd = op + sequence.litLength; + size_t const sequenceLength = sequence.litLength + sequence.matchLength; + BYTE* const oMatchEnd = op + sequenceLength; /* risk : address space overflow (32-bits) */ + const BYTE* const iLitEnd = *litPtr + sequence.litLength; + const BYTE* match = oLitEnd - sequence.offset; + + assert(op != NULL /* Precondition */); + assert(oend_w < oend /* No underflow */); + /* Handle edge cases in a slow path: + * - Read beyond end of literals + * - Match end is within WILDCOPY_OVERLIMIT of oend + * - 32-bit mode and the match length overflows + */ + if (UNLIKELY( + iLitEnd > litLimit || + oMatchEnd > oend_w || + (MEM_32bits() && (size_t)(oend - op) < sequenceLength + WILDCOPY_OVERLENGTH))) + return ZSTD_execSequenceEndSplitLitBuffer(op, oend, oend_w, sequence, litPtr, litLimit, prefixStart, virtualStart, dictEnd); + + /* Assumptions (everything else goes into ZSTD_execSequenceEnd()) */ + assert(op <= oLitEnd /* No overflow */); + assert(oLitEnd < oMatchEnd /* Non-zero match & no overflow */); + assert(oMatchEnd <= oend /* No underflow */); + assert(iLitEnd <= litLimit /* Literal length is in bounds */); + assert(oLitEnd <= oend_w /* Can wildcopy literals */); + assert(oMatchEnd <= oend_w /* Can wildcopy matches */); + + /* Copy Literals: + * Split out litLength <= 16 since it is nearly always true. +1.6% on gcc-9. + * We likely don't need the full 32-byte wildcopy. + */ + assert(WILDCOPY_OVERLENGTH >= 16); + ZSTD_copy16(op, (*litPtr)); + if (UNLIKELY(sequence.litLength > 16)) { + ZSTD_wildcopy(op+16, (*litPtr)+16, sequence.litLength-16, ZSTD_no_overlap); + } + op = oLitEnd; + *litPtr = iLitEnd; /* update for next sequence */ + + /* Copy Match */ + if (sequence.offset > (size_t)(oLitEnd - prefixStart)) { + /* offset beyond prefix -> go into extDict */ + RETURN_ERROR_IF(UNLIKELY(sequence.offset > (size_t)(oLitEnd - virtualStart)), corruption_detected, ""); + match = dictEnd + (match - prefixStart); + if (match + sequence.matchLength <= dictEnd) { + ZSTD_memmove(oLitEnd, match, sequence.matchLength); + return sequenceLength; + } + /* span extDict & currentPrefixSegment */ + { size_t const length1 = dictEnd - match; + ZSTD_memmove(oLitEnd, match, length1); + op = oLitEnd + length1; + sequence.matchLength -= length1; + match = prefixStart; + } } + /* Match within prefix of 1 or more bytes */ + assert(op <= oMatchEnd); + assert(oMatchEnd <= oend_w); + assert(match >= prefixStart); + assert(sequence.matchLength >= 1); + + /* Nearly all offsets are >= WILDCOPY_VECLEN bytes, which means we can use wildcopy + * without overlap checking. + */ + if (LIKELY(sequence.offset >= WILDCOPY_VECLEN)) { + /* We bet on a full wildcopy for matches, since we expect matches to be + * longer than literals (in general). In silesia, ~10% of matches are longer + * than 16 bytes. + */ + ZSTD_wildcopy(op, match, (ptrdiff_t)sequence.matchLength, ZSTD_no_overlap); + return sequenceLength; + } + assert(sequence.offset < WILDCOPY_VECLEN); + + /* Copy 8 bytes and spread the offset to be >= 8. */ + ZSTD_overlapCopy8(&op, &match, sequence.offset); + + /* If the match length is > 8 bytes, then continue with the wildcopy. */ + if (sequence.matchLength > 8) { + assert(op < oMatchEnd); + ZSTD_wildcopy(op, match, (ptrdiff_t)sequence.matchLength-8, ZSTD_overlap_src_before_dst); + } + return sequenceLength; +} + + +static void +ZSTD_initFseState(ZSTD_fseState* DStatePtr, BIT_DStream_t* bitD, const ZSTD_seqSymbol* dt) +{ + const void* ptr = dt; + const ZSTD_seqSymbol_header* const DTableH = (const ZSTD_seqSymbol_header*)ptr; + DStatePtr->state = BIT_readBits(bitD, DTableH->tableLog); + DEBUGLOG(6, "ZSTD_initFseState : val=%u using %u bits", + (U32)DStatePtr->state, DTableH->tableLog); + BIT_reloadDStream(bitD); + DStatePtr->table = dt + 1; +} + +FORCE_INLINE_TEMPLATE void +ZSTD_updateFseStateWithDInfo(ZSTD_fseState* DStatePtr, BIT_DStream_t* bitD, U16 nextState, U32 nbBits) +{ + size_t const lowBits = BIT_readBits(bitD, nbBits); + DStatePtr->state = nextState + lowBits; +} + +/* We need to add at most (ZSTD_WINDOWLOG_MAX_32 - 1) bits to read the maximum + * offset bits. But we can only read at most STREAM_ACCUMULATOR_MIN_32 + * bits before reloading. This value is the maximum number of bytes we read + * after reloading when we are decoding long offsets. + */ +#define LONG_OFFSETS_MAX_EXTRA_BITS_32 \ + (ZSTD_WINDOWLOG_MAX_32 > STREAM_ACCUMULATOR_MIN_32 \ + ? ZSTD_WINDOWLOG_MAX_32 - STREAM_ACCUMULATOR_MIN_32 \ + : 0) + +typedef enum { ZSTD_lo_isRegularOffset, ZSTD_lo_isLongOffset=1 } ZSTD_longOffset_e; + +/** + * ZSTD_decodeSequence(): + * @p longOffsets : tells the decoder to reload more bit while decoding large offsets + * only used in 32-bit mode + * @return : Sequence (litL + matchL + offset) + */ +FORCE_INLINE_TEMPLATE seq_t +ZSTD_decodeSequence(seqState_t* seqState, const ZSTD_longOffset_e longOffsets, const int isLastSeq) +{ + seq_t seq; + /* + * ZSTD_seqSymbol is a 64 bits wide structure. + * It can be loaded in one operation + * and its fields extracted by simply shifting or bit-extracting on aarch64. + * GCC doesn't recognize this and generates more unnecessary ldr/ldrb/ldrh + * operations that cause performance drop. This can be avoided by using this + * ZSTD_memcpy hack. + */ +#if defined(__aarch64__) && (defined(__GNUC__) && !defined(__clang__)) + ZSTD_seqSymbol llDInfoS, mlDInfoS, ofDInfoS; + ZSTD_seqSymbol* const llDInfo = &llDInfoS; + ZSTD_seqSymbol* const mlDInfo = &mlDInfoS; + ZSTD_seqSymbol* const ofDInfo = &ofDInfoS; + ZSTD_memcpy(llDInfo, seqState->stateLL.table + seqState->stateLL.state, sizeof(ZSTD_seqSymbol)); + ZSTD_memcpy(mlDInfo, seqState->stateML.table + seqState->stateML.state, sizeof(ZSTD_seqSymbol)); + ZSTD_memcpy(ofDInfo, seqState->stateOffb.table + seqState->stateOffb.state, sizeof(ZSTD_seqSymbol)); +#else + const ZSTD_seqSymbol* const llDInfo = seqState->stateLL.table + seqState->stateLL.state; + const ZSTD_seqSymbol* const mlDInfo = seqState->stateML.table + seqState->stateML.state; + const ZSTD_seqSymbol* const ofDInfo = seqState->stateOffb.table + seqState->stateOffb.state; +#endif + seq.matchLength = mlDInfo->baseValue; + seq.litLength = llDInfo->baseValue; + { U32 const ofBase = ofDInfo->baseValue; + BYTE const llBits = llDInfo->nbAdditionalBits; + BYTE const mlBits = mlDInfo->nbAdditionalBits; + BYTE const ofBits = ofDInfo->nbAdditionalBits; + BYTE const totalBits = llBits+mlBits+ofBits; + + U16 const llNext = llDInfo->nextState; + U16 const mlNext = mlDInfo->nextState; + U16 const ofNext = ofDInfo->nextState; + U32 const llnbBits = llDInfo->nbBits; + U32 const mlnbBits = mlDInfo->nbBits; + U32 const ofnbBits = ofDInfo->nbBits; + + assert(llBits <= MaxLLBits); + assert(mlBits <= MaxMLBits); + assert(ofBits <= MaxOff); + /* + * As gcc has better branch and block analyzers, sometimes it is only + * valuable to mark likeliness for clang, it gives around 3-4% of + * performance. + */ + + /* sequence */ + { size_t offset; + if (ofBits > 1) { + ZSTD_STATIC_ASSERT(ZSTD_lo_isLongOffset == 1); + ZSTD_STATIC_ASSERT(LONG_OFFSETS_MAX_EXTRA_BITS_32 == 5); + ZSTD_STATIC_ASSERT(STREAM_ACCUMULATOR_MIN_32 > LONG_OFFSETS_MAX_EXTRA_BITS_32); + ZSTD_STATIC_ASSERT(STREAM_ACCUMULATOR_MIN_32 - LONG_OFFSETS_MAX_EXTRA_BITS_32 >= MaxMLBits); + if (MEM_32bits() && longOffsets && (ofBits >= STREAM_ACCUMULATOR_MIN_32)) { + /* Always read extra bits, this keeps the logic simple, + * avoids branches, and avoids accidentally reading 0 bits. + */ + U32 const extraBits = LONG_OFFSETS_MAX_EXTRA_BITS_32; + offset = ofBase + (BIT_readBitsFast(&seqState->DStream, ofBits - extraBits) << extraBits); + BIT_reloadDStream(&seqState->DStream); + offset += BIT_readBitsFast(&seqState->DStream, extraBits); + } else { + offset = ofBase + BIT_readBitsFast(&seqState->DStream, ofBits/*>0*/); /* <= (ZSTD_WINDOWLOG_MAX-1) bits */ + if (MEM_32bits()) BIT_reloadDStream(&seqState->DStream); + } + seqState->prevOffset[2] = seqState->prevOffset[1]; + seqState->prevOffset[1] = seqState->prevOffset[0]; + seqState->prevOffset[0] = offset; + } else { + U32 const ll0 = (llDInfo->baseValue == 0); + if (LIKELY((ofBits == 0))) { + offset = seqState->prevOffset[ll0]; + seqState->prevOffset[1] = seqState->prevOffset[!ll0]; + seqState->prevOffset[0] = offset; + } else { + offset = ofBase + ll0 + BIT_readBitsFast(&seqState->DStream, 1); + { size_t temp = (offset==3) ? seqState->prevOffset[0] - 1 : seqState->prevOffset[offset]; + temp -= !temp; /* 0 is not valid: input corrupted => force offset to -1 => corruption detected at execSequence */ + if (offset != 1) seqState->prevOffset[2] = seqState->prevOffset[1]; + seqState->prevOffset[1] = seqState->prevOffset[0]; + seqState->prevOffset[0] = offset = temp; + } } } + seq.offset = offset; + } + + if (mlBits > 0) + seq.matchLength += BIT_readBitsFast(&seqState->DStream, mlBits/*>0*/); + + if (MEM_32bits() && (mlBits+llBits >= STREAM_ACCUMULATOR_MIN_32-LONG_OFFSETS_MAX_EXTRA_BITS_32)) + BIT_reloadDStream(&seqState->DStream); + if (MEM_64bits() && UNLIKELY(totalBits >= STREAM_ACCUMULATOR_MIN_64-(LLFSELog+MLFSELog+OffFSELog))) + BIT_reloadDStream(&seqState->DStream); + /* Ensure there are enough bits to read the rest of data in 64-bit mode. */ + ZSTD_STATIC_ASSERT(16+LLFSELog+MLFSELog+OffFSELog < STREAM_ACCUMULATOR_MIN_64); + + if (llBits > 0) + seq.litLength += BIT_readBitsFast(&seqState->DStream, llBits/*>0*/); + + if (MEM_32bits()) + BIT_reloadDStream(&seqState->DStream); + + DEBUGLOG(6, "seq: litL=%u, matchL=%u, offset=%u", + (U32)seq.litLength, (U32)seq.matchLength, (U32)seq.offset); + + if (!isLastSeq) { + /* don't update FSE state for last Sequence */ + ZSTD_updateFseStateWithDInfo(&seqState->stateLL, &seqState->DStream, llNext, llnbBits); /* <= 9 bits */ + ZSTD_updateFseStateWithDInfo(&seqState->stateML, &seqState->DStream, mlNext, mlnbBits); /* <= 9 bits */ + if (MEM_32bits()) BIT_reloadDStream(&seqState->DStream); /* <= 18 bits */ + ZSTD_updateFseStateWithDInfo(&seqState->stateOffb, &seqState->DStream, ofNext, ofnbBits); /* <= 8 bits */ + BIT_reloadDStream(&seqState->DStream); + } + } + + return seq; +} + +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) +#if DEBUGLEVEL >= 1 +static int ZSTD_dictionaryIsActive(ZSTD_DCtx const* dctx, BYTE const* prefixStart, BYTE const* oLitEnd) +{ + size_t const windowSize = dctx->fParams.windowSize; + /* No dictionary used. */ + if (dctx->dictContentEndForFuzzing == NULL) return 0; + /* Dictionary is our prefix. */ + if (prefixStart == dctx->dictContentBeginForFuzzing) return 1; + /* Dictionary is not our ext-dict. */ + if (dctx->dictEnd != dctx->dictContentEndForFuzzing) return 0; + /* Dictionary is not within our window size. */ + if ((size_t)(oLitEnd - prefixStart) >= windowSize) return 0; + /* Dictionary is active. */ + return 1; +} +#endif + +static void ZSTD_assertValidSequence( + ZSTD_DCtx const* dctx, + BYTE const* op, BYTE const* oend, + seq_t const seq, + BYTE const* prefixStart, BYTE const* virtualStart) +{ +#if DEBUGLEVEL >= 1 + if (dctx->isFrameDecompression) { + size_t const windowSize = dctx->fParams.windowSize; + size_t const sequenceSize = seq.litLength + seq.matchLength; + BYTE const* const oLitEnd = op + seq.litLength; + DEBUGLOG(6, "Checking sequence: litL=%u matchL=%u offset=%u", + (U32)seq.litLength, (U32)seq.matchLength, (U32)seq.offset); + assert(op <= oend); + assert((size_t)(oend - op) >= sequenceSize); + assert(sequenceSize <= ZSTD_blockSizeMax(dctx)); + if (ZSTD_dictionaryIsActive(dctx, prefixStart, oLitEnd)) { + size_t const dictSize = (size_t)((char const*)dctx->dictContentEndForFuzzing - (char const*)dctx->dictContentBeginForFuzzing); + /* Offset must be within the dictionary. */ + assert(seq.offset <= (size_t)(oLitEnd - virtualStart)); + assert(seq.offset <= windowSize + dictSize); + } else { + /* Offset must be within our window. */ + assert(seq.offset <= windowSize); + } + } +#else + (void)dctx, (void)op, (void)oend, (void)seq, (void)prefixStart, (void)virtualStart; +#endif +} +#endif + +#ifndef ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG + + +FORCE_INLINE_TEMPLATE size_t +DONT_VECTORIZE +ZSTD_decompressSequences_bodySplitLitBuffer( ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + const BYTE* ip = (const BYTE*)seqStart; + const BYTE* const iend = ip + seqSize; + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = ZSTD_maybeNullPtrAdd(ostart, maxDstSize); + BYTE* op = ostart; + const BYTE* litPtr = dctx->litPtr; + const BYTE* litBufferEnd = dctx->litBufferEnd; + const BYTE* const prefixStart = (const BYTE*) (dctx->prefixStart); + const BYTE* const vBase = (const BYTE*) (dctx->virtualStart); + const BYTE* const dictEnd = (const BYTE*) (dctx->dictEnd); + DEBUGLOG(5, "ZSTD_decompressSequences_bodySplitLitBuffer (%i seqs)", nbSeq); + + /* Literals are split between internal buffer & output buffer */ + if (nbSeq) { + seqState_t seqState; + dctx->fseEntropy = 1; + { U32 i; for (i=0; ientropy.rep[i]; } + RETURN_ERROR_IF( + ERR_isError(BIT_initDStream(&seqState.DStream, ip, iend-ip)), + corruption_detected, ""); + ZSTD_initFseState(&seqState.stateLL, &seqState.DStream, dctx->LLTptr); + ZSTD_initFseState(&seqState.stateOffb, &seqState.DStream, dctx->OFTptr); + ZSTD_initFseState(&seqState.stateML, &seqState.DStream, dctx->MLTptr); + assert(dst != NULL); + + ZSTD_STATIC_ASSERT( + BIT_DStream_unfinished < BIT_DStream_completed && + BIT_DStream_endOfBuffer < BIT_DStream_completed && + BIT_DStream_completed < BIT_DStream_overflow); + + /* decompress without overrunning litPtr begins */ + { seq_t sequence = {0,0,0}; /* some static analyzer believe that @sequence is not initialized (it necessarily is, since for(;;) loop as at least one iteration) */ + /* Align the decompression loop to 32 + 16 bytes. + * + * zstd compiled with gcc-9 on an Intel i9-9900k shows 10% decompression + * speed swings based on the alignment of the decompression loop. This + * performance swing is caused by parts of the decompression loop falling + * out of the DSB. The entire decompression loop should fit in the DSB, + * when it can't we get much worse performance. You can measure if you've + * hit the good case or the bad case with this perf command for some + * compressed file test.zst: + * + * perf stat -e cycles -e instructions -e idq.all_dsb_cycles_any_uops \ + * -e idq.all_mite_cycles_any_uops -- ./zstd -tq test.zst + * + * If you see most cycles served out of the MITE you've hit the bad case. + * If you see most cycles served out of the DSB you've hit the good case. + * If it is pretty even then you may be in an okay case. + * + * This issue has been reproduced on the following CPUs: + * - Kabylake: Macbook Pro (15-inch, 2019) 2.4 GHz Intel Core i9 + * Use Instruments->Counters to get DSB/MITE cycles. + * I never got performance swings, but I was able to + * go from the good case of mostly DSB to half of the + * cycles served from MITE. + * - Coffeelake: Intel i9-9900k + * - Coffeelake: Intel i7-9700k + * + * I haven't been able to reproduce the instability or DSB misses on any + * of the following CPUS: + * - Haswell + * - Broadwell: Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GH + * - Skylake + * + * Alignment is done for each of the three major decompression loops: + * - ZSTD_decompressSequences_bodySplitLitBuffer - presplit section of the literal buffer + * - ZSTD_decompressSequences_bodySplitLitBuffer - postsplit section of the literal buffer + * - ZSTD_decompressSequences_body + * Alignment choices are made to minimize large swings on bad cases and influence on performance + * from changes external to this code, rather than to overoptimize on the current commit. + * + * If you are seeing performance stability this script can help test. + * It tests on 4 commits in zstd where I saw performance change. + * + * https://gist.github.com/terrelln/9889fc06a423fd5ca6e99351564473f4 + */ +#if defined(__GNUC__) && defined(__x86_64__) + __asm__(".p2align 6"); +# if __GNUC__ >= 7 + /* good for gcc-7, gcc-9, and gcc-11 */ + __asm__("nop"); + __asm__(".p2align 5"); + __asm__("nop"); + __asm__(".p2align 4"); +# if __GNUC__ == 8 || __GNUC__ == 10 + /* good for gcc-8 and gcc-10 */ + __asm__("nop"); + __asm__(".p2align 3"); +# endif +# endif +#endif + + /* Handle the initial state where litBuffer is currently split between dst and litExtraBuffer */ + for ( ; nbSeq; nbSeq--) { + sequence = ZSTD_decodeSequence(&seqState, isLongOffset, nbSeq==1); + if (litPtr + sequence.litLength > dctx->litBufferEnd) break; + { size_t const oneSeqSize = ZSTD_execSequenceSplitLitBuffer(op, oend, litPtr + sequence.litLength - WILDCOPY_OVERLENGTH, sequence, &litPtr, litBufferEnd, prefixStart, vBase, dictEnd); +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) + assert(!ZSTD_isError(oneSeqSize)); + ZSTD_assertValidSequence(dctx, op, oend, sequence, prefixStart, vBase); +#endif + if (UNLIKELY(ZSTD_isError(oneSeqSize))) + return oneSeqSize; + DEBUGLOG(6, "regenerated sequence size : %u", (U32)oneSeqSize); + op += oneSeqSize; + } } + DEBUGLOG(6, "reached: (litPtr + sequence.litLength > dctx->litBufferEnd)"); + + /* If there are more sequences, they will need to read literals from litExtraBuffer; copy over the remainder from dst and update litPtr and litEnd */ + if (nbSeq > 0) { + const size_t leftoverLit = dctx->litBufferEnd - litPtr; + DEBUGLOG(6, "There are %i sequences left, and %zu/%zu literals left in buffer", nbSeq, leftoverLit, sequence.litLength); + if (leftoverLit) { + RETURN_ERROR_IF(leftoverLit > (size_t)(oend - op), dstSize_tooSmall, "remaining lit must fit within dstBuffer"); + ZSTD_safecopyDstBeforeSrc(op, litPtr, leftoverLit); + sequence.litLength -= leftoverLit; + op += leftoverLit; + } + litPtr = dctx->litExtraBuffer; + litBufferEnd = dctx->litExtraBuffer + ZSTD_LITBUFFEREXTRASIZE; + dctx->litBufferLocation = ZSTD_not_in_dst; + { size_t const oneSeqSize = ZSTD_execSequence(op, oend, sequence, &litPtr, litBufferEnd, prefixStart, vBase, dictEnd); +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) + assert(!ZSTD_isError(oneSeqSize)); + ZSTD_assertValidSequence(dctx, op, oend, sequence, prefixStart, vBase); +#endif + if (UNLIKELY(ZSTD_isError(oneSeqSize))) + return oneSeqSize; + DEBUGLOG(6, "regenerated sequence size : %u", (U32)oneSeqSize); + op += oneSeqSize; + } + nbSeq--; + } + } + + if (nbSeq > 0) { + /* there is remaining lit from extra buffer */ + +#if defined(__GNUC__) && defined(__x86_64__) + __asm__(".p2align 6"); + __asm__("nop"); +# if __GNUC__ != 7 + /* worse for gcc-7 better for gcc-8, gcc-9, and gcc-10 and clang */ + __asm__(".p2align 4"); + __asm__("nop"); + __asm__(".p2align 3"); +# elif __GNUC__ >= 11 + __asm__(".p2align 3"); +# else + __asm__(".p2align 5"); + __asm__("nop"); + __asm__(".p2align 3"); +# endif +#endif + + for ( ; nbSeq ; nbSeq--) { + seq_t const sequence = ZSTD_decodeSequence(&seqState, isLongOffset, nbSeq==1); + size_t const oneSeqSize = ZSTD_execSequence(op, oend, sequence, &litPtr, litBufferEnd, prefixStart, vBase, dictEnd); +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) + assert(!ZSTD_isError(oneSeqSize)); + ZSTD_assertValidSequence(dctx, op, oend, sequence, prefixStart, vBase); +#endif + if (UNLIKELY(ZSTD_isError(oneSeqSize))) + return oneSeqSize; + DEBUGLOG(6, "regenerated sequence size : %u", (U32)oneSeqSize); + op += oneSeqSize; + } + } + + /* check if reached exact end */ + DEBUGLOG(5, "ZSTD_decompressSequences_bodySplitLitBuffer: after decode loop, remaining nbSeq : %i", nbSeq); + RETURN_ERROR_IF(nbSeq, corruption_detected, ""); + DEBUGLOG(5, "bitStream : start=%p, ptr=%p, bitsConsumed=%u", seqState.DStream.start, seqState.DStream.ptr, seqState.DStream.bitsConsumed); + RETURN_ERROR_IF(!BIT_endOfDStream(&seqState.DStream), corruption_detected, ""); + /* save reps for next block */ + { U32 i; for (i=0; ientropy.rep[i] = (U32)(seqState.prevOffset[i]); } + } + + /* last literal segment */ + if (dctx->litBufferLocation == ZSTD_split) { + /* split hasn't been reached yet, first get dst then copy litExtraBuffer */ + size_t const lastLLSize = (size_t)(litBufferEnd - litPtr); + DEBUGLOG(6, "copy last literals from segment : %u", (U32)lastLLSize); + RETURN_ERROR_IF(lastLLSize > (size_t)(oend - op), dstSize_tooSmall, ""); + if (op != NULL) { + ZSTD_memmove(op, litPtr, lastLLSize); + op += lastLLSize; + } + litPtr = dctx->litExtraBuffer; + litBufferEnd = dctx->litExtraBuffer + ZSTD_LITBUFFEREXTRASIZE; + dctx->litBufferLocation = ZSTD_not_in_dst; + } + /* copy last literals from internal buffer */ + { size_t const lastLLSize = (size_t)(litBufferEnd - litPtr); + DEBUGLOG(6, "copy last literals from internal buffer : %u", (U32)lastLLSize); + RETURN_ERROR_IF(lastLLSize > (size_t)(oend-op), dstSize_tooSmall, ""); + if (op != NULL) { + ZSTD_memcpy(op, litPtr, lastLLSize); + op += lastLLSize; + } } + + DEBUGLOG(6, "decoded block of size %u bytes", (U32)(op - ostart)); + return (size_t)(op - ostart); +} + +FORCE_INLINE_TEMPLATE size_t +DONT_VECTORIZE +ZSTD_decompressSequences_body(ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + const BYTE* ip = (const BYTE*)seqStart; + const BYTE* const iend = ip + seqSize; + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = dctx->litBufferLocation == ZSTD_not_in_dst ? ZSTD_maybeNullPtrAdd(ostart, maxDstSize) : dctx->litBuffer; + BYTE* op = ostart; + const BYTE* litPtr = dctx->litPtr; + const BYTE* const litEnd = litPtr + dctx->litSize; + const BYTE* const prefixStart = (const BYTE*)(dctx->prefixStart); + const BYTE* const vBase = (const BYTE*)(dctx->virtualStart); + const BYTE* const dictEnd = (const BYTE*)(dctx->dictEnd); + DEBUGLOG(5, "ZSTD_decompressSequences_body: nbSeq = %d", nbSeq); + + /* Regen sequences */ + if (nbSeq) { + seqState_t seqState; + dctx->fseEntropy = 1; + { U32 i; for (i = 0; i < ZSTD_REP_NUM; i++) seqState.prevOffset[i] = dctx->entropy.rep[i]; } + RETURN_ERROR_IF( + ERR_isError(BIT_initDStream(&seqState.DStream, ip, iend - ip)), + corruption_detected, ""); + ZSTD_initFseState(&seqState.stateLL, &seqState.DStream, dctx->LLTptr); + ZSTD_initFseState(&seqState.stateOffb, &seqState.DStream, dctx->OFTptr); + ZSTD_initFseState(&seqState.stateML, &seqState.DStream, dctx->MLTptr); + assert(dst != NULL); + +#if defined(__GNUC__) && defined(__x86_64__) + __asm__(".p2align 6"); + __asm__("nop"); +# if __GNUC__ >= 7 + __asm__(".p2align 5"); + __asm__("nop"); + __asm__(".p2align 3"); +# else + __asm__(".p2align 4"); + __asm__("nop"); + __asm__(".p2align 3"); +# endif +#endif + + for ( ; nbSeq ; nbSeq--) { + seq_t const sequence = ZSTD_decodeSequence(&seqState, isLongOffset, nbSeq==1); + size_t const oneSeqSize = ZSTD_execSequence(op, oend, sequence, &litPtr, litEnd, prefixStart, vBase, dictEnd); +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) + assert(!ZSTD_isError(oneSeqSize)); + ZSTD_assertValidSequence(dctx, op, oend, sequence, prefixStart, vBase); +#endif + if (UNLIKELY(ZSTD_isError(oneSeqSize))) + return oneSeqSize; + DEBUGLOG(6, "regenerated sequence size : %u", (U32)oneSeqSize); + op += oneSeqSize; + } + + /* check if reached exact end */ + assert(nbSeq == 0); + RETURN_ERROR_IF(!BIT_endOfDStream(&seqState.DStream), corruption_detected, ""); + /* save reps for next block */ + { U32 i; for (i=0; ientropy.rep[i] = (U32)(seqState.prevOffset[i]); } + } + + /* last literal segment */ + { size_t const lastLLSize = (size_t)(litEnd - litPtr); + DEBUGLOG(6, "copy last literals : %u", (U32)lastLLSize); + RETURN_ERROR_IF(lastLLSize > (size_t)(oend-op), dstSize_tooSmall, ""); + if (op != NULL) { + ZSTD_memcpy(op, litPtr, lastLLSize); + op += lastLLSize; + } } + + DEBUGLOG(6, "decoded block of size %u bytes", (U32)(op - ostart)); + return (size_t)(op - ostart); +} + +static size_t +ZSTD_decompressSequences_default(ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + return ZSTD_decompressSequences_body(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} + +static size_t +ZSTD_decompressSequencesSplitLitBuffer_default(ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + return ZSTD_decompressSequences_bodySplitLitBuffer(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} +#endif /* ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG */ + +#ifndef ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT + +FORCE_INLINE_TEMPLATE + +size_t ZSTD_prefetchMatch(size_t prefetchPos, seq_t const sequence, + const BYTE* const prefixStart, const BYTE* const dictEnd) +{ + prefetchPos += sequence.litLength; + { const BYTE* const matchBase = (sequence.offset > prefetchPos) ? dictEnd : prefixStart; + /* note : this operation can overflow when seq.offset is really too large, which can only happen when input is corrupted. + * No consequence though : memory address is only used for prefetching, not for dereferencing */ + const BYTE* const match = ZSTD_wrappedPtrSub(ZSTD_wrappedPtrAdd(matchBase, prefetchPos), sequence.offset); + PREFETCH_L1(match); PREFETCH_L1(match+CACHELINE_SIZE); /* note : it's safe to invoke PREFETCH() on any memory address, including invalid ones */ + } + return prefetchPos + sequence.matchLength; +} + +/* This decoding function employs prefetching + * to reduce latency impact of cache misses. + * It's generally employed when block contains a significant portion of long-distance matches + * or when coupled with a "cold" dictionary */ +FORCE_INLINE_TEMPLATE size_t +ZSTD_decompressSequencesLong_body( + ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + const BYTE* ip = (const BYTE*)seqStart; + const BYTE* const iend = ip + seqSize; + BYTE* const ostart = (BYTE*)dst; + BYTE* const oend = dctx->litBufferLocation == ZSTD_in_dst ? dctx->litBuffer : ZSTD_maybeNullPtrAdd(ostart, maxDstSize); + BYTE* op = ostart; + const BYTE* litPtr = dctx->litPtr; + const BYTE* litBufferEnd = dctx->litBufferEnd; + const BYTE* const prefixStart = (const BYTE*) (dctx->prefixStart); + const BYTE* const dictStart = (const BYTE*) (dctx->virtualStart); + const BYTE* const dictEnd = (const BYTE*) (dctx->dictEnd); + + /* Regen sequences */ + if (nbSeq) { +#define STORED_SEQS 8 +#define STORED_SEQS_MASK (STORED_SEQS-1) +#define ADVANCED_SEQS STORED_SEQS + seq_t sequences[STORED_SEQS]; + int const seqAdvance = MIN(nbSeq, ADVANCED_SEQS); + seqState_t seqState; + int seqNb; + size_t prefetchPos = (size_t)(op-prefixStart); /* track position relative to prefixStart */ + + dctx->fseEntropy = 1; + { int i; for (i=0; ientropy.rep[i]; } + assert(dst != NULL); + assert(iend >= ip); + RETURN_ERROR_IF( + ERR_isError(BIT_initDStream(&seqState.DStream, ip, iend-ip)), + corruption_detected, ""); + ZSTD_initFseState(&seqState.stateLL, &seqState.DStream, dctx->LLTptr); + ZSTD_initFseState(&seqState.stateOffb, &seqState.DStream, dctx->OFTptr); + ZSTD_initFseState(&seqState.stateML, &seqState.DStream, dctx->MLTptr); + + /* prepare in advance */ + for (seqNb=0; seqNblitBufferLocation == ZSTD_split && litPtr + sequences[(seqNb - ADVANCED_SEQS) & STORED_SEQS_MASK].litLength > dctx->litBufferEnd) { + /* lit buffer is reaching split point, empty out the first buffer and transition to litExtraBuffer */ + const size_t leftoverLit = dctx->litBufferEnd - litPtr; + if (leftoverLit) + { + RETURN_ERROR_IF(leftoverLit > (size_t)(oend - op), dstSize_tooSmall, "remaining lit must fit within dstBuffer"); + ZSTD_safecopyDstBeforeSrc(op, litPtr, leftoverLit); + sequences[(seqNb - ADVANCED_SEQS) & STORED_SEQS_MASK].litLength -= leftoverLit; + op += leftoverLit; + } + litPtr = dctx->litExtraBuffer; + litBufferEnd = dctx->litExtraBuffer + ZSTD_LITBUFFEREXTRASIZE; + dctx->litBufferLocation = ZSTD_not_in_dst; + { size_t const oneSeqSize = ZSTD_execSequence(op, oend, sequences[(seqNb - ADVANCED_SEQS) & STORED_SEQS_MASK], &litPtr, litBufferEnd, prefixStart, dictStart, dictEnd); +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) + assert(!ZSTD_isError(oneSeqSize)); + ZSTD_assertValidSequence(dctx, op, oend, sequences[(seqNb - ADVANCED_SEQS) & STORED_SEQS_MASK], prefixStart, dictStart); +#endif + if (ZSTD_isError(oneSeqSize)) return oneSeqSize; + + prefetchPos = ZSTD_prefetchMatch(prefetchPos, sequence, prefixStart, dictEnd); + sequences[seqNb & STORED_SEQS_MASK] = sequence; + op += oneSeqSize; + } } + else + { + /* lit buffer is either wholly contained in first or second split, or not split at all*/ + size_t const oneSeqSize = dctx->litBufferLocation == ZSTD_split ? + ZSTD_execSequenceSplitLitBuffer(op, oend, litPtr + sequences[(seqNb - ADVANCED_SEQS) & STORED_SEQS_MASK].litLength - WILDCOPY_OVERLENGTH, sequences[(seqNb - ADVANCED_SEQS) & STORED_SEQS_MASK], &litPtr, litBufferEnd, prefixStart, dictStart, dictEnd) : + ZSTD_execSequence(op, oend, sequences[(seqNb - ADVANCED_SEQS) & STORED_SEQS_MASK], &litPtr, litBufferEnd, prefixStart, dictStart, dictEnd); +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) + assert(!ZSTD_isError(oneSeqSize)); + ZSTD_assertValidSequence(dctx, op, oend, sequences[(seqNb - ADVANCED_SEQS) & STORED_SEQS_MASK], prefixStart, dictStart); +#endif + if (ZSTD_isError(oneSeqSize)) return oneSeqSize; + + prefetchPos = ZSTD_prefetchMatch(prefetchPos, sequence, prefixStart, dictEnd); + sequences[seqNb & STORED_SEQS_MASK] = sequence; + op += oneSeqSize; + } + } + RETURN_ERROR_IF(!BIT_endOfDStream(&seqState.DStream), corruption_detected, ""); + + /* finish queue */ + seqNb -= seqAdvance; + for ( ; seqNblitBufferLocation == ZSTD_split && litPtr + sequence->litLength > dctx->litBufferEnd) { + const size_t leftoverLit = dctx->litBufferEnd - litPtr; + if (leftoverLit) { + RETURN_ERROR_IF(leftoverLit > (size_t)(oend - op), dstSize_tooSmall, "remaining lit must fit within dstBuffer"); + ZSTD_safecopyDstBeforeSrc(op, litPtr, leftoverLit); + sequence->litLength -= leftoverLit; + op += leftoverLit; + } + litPtr = dctx->litExtraBuffer; + litBufferEnd = dctx->litExtraBuffer + ZSTD_LITBUFFEREXTRASIZE; + dctx->litBufferLocation = ZSTD_not_in_dst; + { size_t const oneSeqSize = ZSTD_execSequence(op, oend, *sequence, &litPtr, litBufferEnd, prefixStart, dictStart, dictEnd); +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) + assert(!ZSTD_isError(oneSeqSize)); + ZSTD_assertValidSequence(dctx, op, oend, sequences[seqNb&STORED_SEQS_MASK], prefixStart, dictStart); +#endif + if (ZSTD_isError(oneSeqSize)) return oneSeqSize; + op += oneSeqSize; + } + } + else + { + size_t const oneSeqSize = dctx->litBufferLocation == ZSTD_split ? + ZSTD_execSequenceSplitLitBuffer(op, oend, litPtr + sequence->litLength - WILDCOPY_OVERLENGTH, *sequence, &litPtr, litBufferEnd, prefixStart, dictStart, dictEnd) : + ZSTD_execSequence(op, oend, *sequence, &litPtr, litBufferEnd, prefixStart, dictStart, dictEnd); +#if defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) && defined(FUZZING_ASSERT_VALID_SEQUENCE) + assert(!ZSTD_isError(oneSeqSize)); + ZSTD_assertValidSequence(dctx, op, oend, sequences[seqNb&STORED_SEQS_MASK], prefixStart, dictStart); +#endif + if (ZSTD_isError(oneSeqSize)) return oneSeqSize; + op += oneSeqSize; + } + } + + /* save reps for next block */ + { U32 i; for (i=0; ientropy.rep[i] = (U32)(seqState.prevOffset[i]); } + } + + /* last literal segment */ + if (dctx->litBufferLocation == ZSTD_split) { /* first deplete literal buffer in dst, then copy litExtraBuffer */ + size_t const lastLLSize = litBufferEnd - litPtr; + RETURN_ERROR_IF(lastLLSize > (size_t)(oend - op), dstSize_tooSmall, ""); + if (op != NULL) { + ZSTD_memmove(op, litPtr, lastLLSize); + op += lastLLSize; + } + litPtr = dctx->litExtraBuffer; + litBufferEnd = dctx->litExtraBuffer + ZSTD_LITBUFFEREXTRASIZE; + } + { size_t const lastLLSize = litBufferEnd - litPtr; + RETURN_ERROR_IF(lastLLSize > (size_t)(oend-op), dstSize_tooSmall, ""); + if (op != NULL) { + ZSTD_memmove(op, litPtr, lastLLSize); + op += lastLLSize; + } + } + + return (size_t)(op - ostart); +} + +static size_t +ZSTD_decompressSequencesLong_default(ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + return ZSTD_decompressSequencesLong_body(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} +#endif /* ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT */ + + + +#if DYNAMIC_BMI2 + +#ifndef ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG +static BMI2_TARGET_ATTRIBUTE size_t +DONT_VECTORIZE +ZSTD_decompressSequences_bmi2(ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + return ZSTD_decompressSequences_body(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} +static BMI2_TARGET_ATTRIBUTE size_t +DONT_VECTORIZE +ZSTD_decompressSequencesSplitLitBuffer_bmi2(ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + return ZSTD_decompressSequences_bodySplitLitBuffer(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} +#endif /* ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG */ + +#ifndef ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT +static BMI2_TARGET_ATTRIBUTE size_t +ZSTD_decompressSequencesLong_bmi2(ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + return ZSTD_decompressSequencesLong_body(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} +#endif /* ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT */ + +#endif /* DYNAMIC_BMI2 */ + +typedef size_t (*ZSTD_decompressSequences_t)( + ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset); + +#ifndef ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG +static size_t +ZSTD_decompressSequences(ZSTD_DCtx* dctx, void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + DEBUGLOG(5, "ZSTD_decompressSequences"); +#if DYNAMIC_BMI2 + if (ZSTD_DCtx_get_bmi2(dctx)) { + return ZSTD_decompressSequences_bmi2(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); + } +#endif + return ZSTD_decompressSequences_default(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} +static size_t +ZSTD_decompressSequencesSplitLitBuffer(ZSTD_DCtx* dctx, void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + DEBUGLOG(5, "ZSTD_decompressSequencesSplitLitBuffer"); +#if DYNAMIC_BMI2 + if (ZSTD_DCtx_get_bmi2(dctx)) { + return ZSTD_decompressSequencesSplitLitBuffer_bmi2(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); + } +#endif + return ZSTD_decompressSequencesSplitLitBuffer_default(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} +#endif /* ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG */ + + +#ifndef ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT +/* ZSTD_decompressSequencesLong() : + * decompression function triggered when a minimum share of offsets is considered "long", + * aka out of cache. + * note : "long" definition seems overloaded here, sometimes meaning "wider than bitstream register", and sometimes meaning "farther than memory cache distance". + * This function will try to mitigate main memory latency through the use of prefetching */ +static size_t +ZSTD_decompressSequencesLong(ZSTD_DCtx* dctx, + void* dst, size_t maxDstSize, + const void* seqStart, size_t seqSize, int nbSeq, + const ZSTD_longOffset_e isLongOffset) +{ + DEBUGLOG(5, "ZSTD_decompressSequencesLong"); +#if DYNAMIC_BMI2 + if (ZSTD_DCtx_get_bmi2(dctx)) { + return ZSTD_decompressSequencesLong_bmi2(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); + } +#endif + return ZSTD_decompressSequencesLong_default(dctx, dst, maxDstSize, seqStart, seqSize, nbSeq, isLongOffset); +} +#endif /* ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT */ + + +/** + * @returns The total size of the history referenceable by zstd, including + * both the prefix and the extDict. At @p op any offset larger than this + * is invalid. + */ +static size_t ZSTD_totalHistorySize(BYTE* op, BYTE const* virtualStart) +{ + return (size_t)(op - virtualStart); +} + +typedef struct { + unsigned longOffsetShare; + unsigned maxNbAdditionalBits; +} ZSTD_OffsetInfo; + +/* ZSTD_getOffsetInfo() : + * condition : offTable must be valid + * @return : "share" of long offsets (arbitrarily defined as > (1<<23)) + * compared to maximum possible of (1< 22) info.longOffsetShare += 1; + } + + assert(tableLog <= OffFSELog); + info.longOffsetShare <<= (OffFSELog - tableLog); /* scale to OffFSELog */ + } + + return info; +} + +/** + * @returns The maximum offset we can decode in one read of our bitstream, without + * reloading more bits in the middle of the offset bits read. Any offsets larger + * than this must use the long offset decoder. + */ +static size_t ZSTD_maxShortOffset(void) +{ + if (MEM_64bits()) { + /* We can decode any offset without reloading bits. + * This might change if the max window size grows. + */ + ZSTD_STATIC_ASSERT(ZSTD_WINDOWLOG_MAX <= 31); + return (size_t)-1; + } else { + /* The maximum offBase is (1 << (STREAM_ACCUMULATOR_MIN + 1)) - 1. + * This offBase would require STREAM_ACCUMULATOR_MIN extra bits. + * Then we have to subtract ZSTD_REP_NUM to get the maximum possible offset. + */ + size_t const maxOffbase = ((size_t)1 << (STREAM_ACCUMULATOR_MIN + 1)) - 1; + size_t const maxOffset = maxOffbase - ZSTD_REP_NUM; + assert(ZSTD_highbit32((U32)maxOffbase) == STREAM_ACCUMULATOR_MIN); + return maxOffset; + } +} + +size_t +ZSTD_decompressBlock_internal(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, const streaming_operation streaming) +{ /* blockType == blockCompressed */ + const BYTE* ip = (const BYTE*)src; + DEBUGLOG(5, "ZSTD_decompressBlock_internal (cSize : %u)", (unsigned)srcSize); + + /* Note : the wording of the specification + * allows compressed block to be sized exactly ZSTD_blockSizeMax(dctx). + * This generally does not happen, as it makes little sense, + * since an uncompressed block would feature same size and have no decompression cost. + * Also, note that decoder from reference libzstd before < v1.5.4 + * would consider this edge case as an error. + * As a consequence, avoid generating compressed blocks of size ZSTD_blockSizeMax(dctx) + * for broader compatibility with the deployed ecosystem of zstd decoders */ + RETURN_ERROR_IF(srcSize > ZSTD_blockSizeMax(dctx), srcSize_wrong, ""); + + /* Decode literals section */ + { size_t const litCSize = ZSTD_decodeLiteralsBlock(dctx, src, srcSize, dst, dstCapacity, streaming); + DEBUGLOG(5, "ZSTD_decodeLiteralsBlock : cSize=%u, nbLiterals=%zu", (U32)litCSize, dctx->litSize); + if (ZSTD_isError(litCSize)) return litCSize; + ip += litCSize; + srcSize -= litCSize; + } + + /* Build Decoding Tables */ + { + /* Compute the maximum block size, which must also work when !frame and fParams are unset. + * Additionally, take the min with dstCapacity to ensure that the totalHistorySize fits in a size_t. + */ + size_t const blockSizeMax = MIN(dstCapacity, ZSTD_blockSizeMax(dctx)); + size_t const totalHistorySize = ZSTD_totalHistorySize(ZSTD_maybeNullPtrAdd((BYTE*)dst, blockSizeMax), (BYTE const*)dctx->virtualStart); + /* isLongOffset must be true if there are long offsets. + * Offsets are long if they are larger than ZSTD_maxShortOffset(). + * We don't expect that to be the case in 64-bit mode. + * + * We check here to see if our history is large enough to allow long offsets. + * If it isn't, then we can't possible have (valid) long offsets. If the offset + * is invalid, then it is okay to read it incorrectly. + * + * If isLongOffsets is true, then we will later check our decoding table to see + * if it is even possible to generate long offsets. + */ + ZSTD_longOffset_e isLongOffset = (ZSTD_longOffset_e)(MEM_32bits() && (totalHistorySize > ZSTD_maxShortOffset())); + /* These macros control at build-time which decompressor implementation + * we use. If neither is defined, we do some inspection and dispatch at + * runtime. + */ +#if !defined(ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT) && \ + !defined(ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG) + int usePrefetchDecoder = dctx->ddictIsCold; +#else + /* Set to 1 to avoid computing offset info if we don't need to. + * Otherwise this value is ignored. + */ + int usePrefetchDecoder = 1; +#endif + int nbSeq; + size_t const seqHSize = ZSTD_decodeSeqHeaders(dctx, &nbSeq, ip, srcSize); + if (ZSTD_isError(seqHSize)) return seqHSize; + ip += seqHSize; + srcSize -= seqHSize; + + RETURN_ERROR_IF((dst == NULL || dstCapacity == 0) && nbSeq > 0, dstSize_tooSmall, "NULL not handled"); + RETURN_ERROR_IF(MEM_64bits() && sizeof(size_t) == sizeof(void*) && (size_t)(-1) - (size_t)dst < (size_t)(1 << 20), dstSize_tooSmall, + "invalid dst"); + + /* If we could potentially have long offsets, or we might want to use the prefetch decoder, + * compute information about the share of long offsets, and the maximum nbAdditionalBits. + * NOTE: could probably use a larger nbSeq limit + */ + if (isLongOffset || (!usePrefetchDecoder && (totalHistorySize > (1u << 24)) && (nbSeq > 8))) { + ZSTD_OffsetInfo const info = ZSTD_getOffsetInfo(dctx->OFTptr, nbSeq); + if (isLongOffset && info.maxNbAdditionalBits <= STREAM_ACCUMULATOR_MIN) { + /* If isLongOffset, but the maximum number of additional bits that we see in our table is small + * enough, then we know it is impossible to have too long an offset in this block, so we can + * use the regular offset decoder. + */ + isLongOffset = ZSTD_lo_isRegularOffset; + } + if (!usePrefetchDecoder) { + U32 const minShare = MEM_64bits() ? 7 : 20; /* heuristic values, correspond to 2.73% and 7.81% */ + usePrefetchDecoder = (info.longOffsetShare >= minShare); + } + } + + dctx->ddictIsCold = 0; + +#if !defined(ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT) && \ + !defined(ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG) + if (usePrefetchDecoder) { +#else + (void)usePrefetchDecoder; + { +#endif +#ifndef ZSTD_FORCE_DECOMPRESS_SEQUENCES_SHORT + return ZSTD_decompressSequencesLong(dctx, dst, dstCapacity, ip, srcSize, nbSeq, isLongOffset); +#endif + } + +#ifndef ZSTD_FORCE_DECOMPRESS_SEQUENCES_LONG + /* else */ + if (dctx->litBufferLocation == ZSTD_split) + return ZSTD_decompressSequencesSplitLitBuffer(dctx, dst, dstCapacity, ip, srcSize, nbSeq, isLongOffset); + else + return ZSTD_decompressSequences(dctx, dst, dstCapacity, ip, srcSize, nbSeq, isLongOffset); +#endif + } +} + + +ZSTD_ALLOW_POINTER_OVERFLOW_ATTR +void ZSTD_checkContinuity(ZSTD_DCtx* dctx, const void* dst, size_t dstSize) +{ + if (dst != dctx->previousDstEnd && dstSize > 0) { /* not contiguous */ + dctx->dictEnd = dctx->previousDstEnd; + dctx->virtualStart = (const char*)dst - ((const char*)(dctx->previousDstEnd) - (const char*)(dctx->prefixStart)); + dctx->prefixStart = dst; + dctx->previousDstEnd = dst; + } +} + + +size_t ZSTD_decompressBlock_deprecated(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize) +{ + size_t dSize; + dctx->isFrameDecompression = 0; + ZSTD_checkContinuity(dctx, dst, dstCapacity); + dSize = ZSTD_decompressBlock_internal(dctx, dst, dstCapacity, src, srcSize, not_streaming); + FORWARD_IF_ERROR(dSize, ""); + dctx->previousDstEnd = (char*)dst + dSize; + return dSize; +} + + +/* NOTE: Must just wrap ZSTD_decompressBlock_deprecated() */ +size_t ZSTD_decompressBlock(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize) +{ + return ZSTD_decompressBlock_deprecated(dctx, dst, dstCapacity, src, srcSize); +} +/**** ended inlining decompress/zstd_decompress_block.c ****/ + diff --git a/ref/zstd/zstd.h b/ref/zstd/zstd.h new file mode 100644 index 00000000..5d1fef8a --- /dev/null +++ b/ref/zstd/zstd.h @@ -0,0 +1,3089 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * All rights reserved. + * + * This source code is licensed under both the BSD-style license (found in the + * LICENSE file in the root directory of this source tree) and the GPLv2 (found + * in the COPYING file in the root directory of this source tree). + * You may select, at your option, one of the above-listed licenses. + */ +#if defined (__cplusplus) +extern "C" { +#endif + +#ifndef ZSTD_H_235446 +#define ZSTD_H_235446 + +/* ====== Dependencies ======*/ +#include /* INT_MAX */ +#include /* size_t */ + + +/* ===== ZSTDLIB_API : control library symbols visibility ===== */ +#ifndef ZSTDLIB_VISIBLE + /* Backwards compatibility with old macro name */ +# ifdef ZSTDLIB_VISIBILITY +# define ZSTDLIB_VISIBLE ZSTDLIB_VISIBILITY +# elif defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__) +# define ZSTDLIB_VISIBLE __attribute__ ((visibility ("default"))) +# else +# define ZSTDLIB_VISIBLE +# endif +#endif + +#ifndef ZSTDLIB_HIDDEN +# if defined(__GNUC__) && (__GNUC__ >= 4) && !defined(__MINGW32__) +# define ZSTDLIB_HIDDEN __attribute__ ((visibility ("hidden"))) +# else +# define ZSTDLIB_HIDDEN +# endif +#endif + +#if defined(ZSTD_DLL_EXPORT) && (ZSTD_DLL_EXPORT==1) +# define ZSTDLIB_API __declspec(dllexport) ZSTDLIB_VISIBLE +#elif defined(ZSTD_DLL_IMPORT) && (ZSTD_DLL_IMPORT==1) +# define ZSTDLIB_API __declspec(dllimport) ZSTDLIB_VISIBLE /* It isn't required but allows to generate better code, saving a function pointer load from the IAT and an indirect jump.*/ +#else +# define ZSTDLIB_API ZSTDLIB_VISIBLE +#endif + +/* Deprecation warnings : + * Should these warnings be a problem, it is generally possible to disable them, + * typically with -Wno-deprecated-declarations for gcc or _CRT_SECURE_NO_WARNINGS in Visual. + * Otherwise, it's also possible to define ZSTD_DISABLE_DEPRECATE_WARNINGS. + */ +#ifdef ZSTD_DISABLE_DEPRECATE_WARNINGS +# define ZSTD_DEPRECATED(message) /* disable deprecation warnings */ +#else +# if defined (__cplusplus) && (__cplusplus >= 201402) /* C++14 or greater */ +# define ZSTD_DEPRECATED(message) [[deprecated(message)]] +# elif (defined(GNUC) && (GNUC > 4 || (GNUC == 4 && GNUC_MINOR >= 5))) || defined(__clang__) +# define ZSTD_DEPRECATED(message) __attribute__((deprecated(message))) +# elif defined(__GNUC__) && (__GNUC__ >= 3) +# define ZSTD_DEPRECATED(message) __attribute__((deprecated)) +# elif defined(_MSC_VER) +# define ZSTD_DEPRECATED(message) __declspec(deprecated(message)) +# else +# pragma message("WARNING: You need to implement ZSTD_DEPRECATED for this compiler") +# define ZSTD_DEPRECATED(message) +# endif +#endif /* ZSTD_DISABLE_DEPRECATE_WARNINGS */ + + +/******************************************************************************* + Introduction + + zstd, short for Zstandard, is a fast lossless compression algorithm, targeting + real-time compression scenarios at zlib-level and better compression ratios. + The zstd compression library provides in-memory compression and decompression + functions. + + The library supports regular compression levels from 1 up to ZSTD_maxCLevel(), + which is currently 22. Levels >= 20, labeled `--ultra`, should be used with + caution, as they require more memory. The library also offers negative + compression levels, which extend the range of speed vs. ratio preferences. + The lower the level, the faster the speed (at the cost of compression). + + Compression can be done in: + - a single step (described as Simple API) + - a single step, reusing a context (described as Explicit context) + - unbounded multiple steps (described as Streaming compression) + + The compression ratio achievable on small data can be highly improved using + a dictionary. Dictionary compression can be performed in: + - a single step (described as Simple dictionary API) + - a single step, reusing a dictionary (described as Bulk-processing + dictionary API) + + Advanced experimental functions can be accessed using + `#define ZSTD_STATIC_LINKING_ONLY` before including zstd.h. + + Advanced experimental APIs should never be used with a dynamically-linked + library. They are not "stable"; their definitions or signatures may change in + the future. Only static linking is allowed. +*******************************************************************************/ + +/*------ Version ------*/ +#define ZSTD_VERSION_MAJOR 1 +#define ZSTD_VERSION_MINOR 5 +#define ZSTD_VERSION_RELEASE 6 +#define ZSTD_VERSION_NUMBER (ZSTD_VERSION_MAJOR *100*100 + ZSTD_VERSION_MINOR *100 + ZSTD_VERSION_RELEASE) + +/*! ZSTD_versionNumber() : + * Return runtime library version, the value is (MAJOR*100*100 + MINOR*100 + RELEASE). */ +ZSTDLIB_API unsigned ZSTD_versionNumber(void); + +#define ZSTD_LIB_VERSION ZSTD_VERSION_MAJOR.ZSTD_VERSION_MINOR.ZSTD_VERSION_RELEASE +#define ZSTD_QUOTE(str) #str +#define ZSTD_EXPAND_AND_QUOTE(str) ZSTD_QUOTE(str) +#define ZSTD_VERSION_STRING ZSTD_EXPAND_AND_QUOTE(ZSTD_LIB_VERSION) + +/*! ZSTD_versionString() : + * Return runtime library version, like "1.4.5". Requires v1.3.0+. */ +ZSTDLIB_API const char* ZSTD_versionString(void); + +/* ************************************* + * Default constant + ***************************************/ +#ifndef ZSTD_CLEVEL_DEFAULT +# define ZSTD_CLEVEL_DEFAULT 3 +#endif + +/* ************************************* + * Constants + ***************************************/ + +/* All magic numbers are supposed read/written to/from files/memory using little-endian convention */ +#define ZSTD_MAGICNUMBER 0xFD2FB528 /* valid since v0.8.0 */ +#define ZSTD_MAGIC_DICTIONARY 0xEC30A437 /* valid since v0.7.0 */ +#define ZSTD_MAGIC_SKIPPABLE_START 0x184D2A50 /* all 16 values, from 0x184D2A50 to 0x184D2A5F, signal the beginning of a skippable frame */ +#define ZSTD_MAGIC_SKIPPABLE_MASK 0xFFFFFFF0 + +#define ZSTD_BLOCKSIZELOG_MAX 17 +#define ZSTD_BLOCKSIZE_MAX (1<= ZSTD_compressBound(srcSize)` guarantees that zstd will have + * enough space to successfully compress the data. + * @return : compressed size written into `dst` (<= `dstCapacity), + * or an error code if it fails (which can be tested using ZSTD_isError()). */ +ZSTDLIB_API size_t ZSTD_compress( void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + int compressionLevel); + +/*! ZSTD_decompress() : + * `compressedSize` : must be the _exact_ size of some number of compressed and/or skippable frames. + * `dstCapacity` is an upper bound of originalSize to regenerate. + * If user cannot imply a maximum upper bound, it's better to use streaming mode to decompress data. + * @return : the number of bytes decompressed into `dst` (<= `dstCapacity`), + * or an errorCode if it fails (which can be tested using ZSTD_isError()). */ +ZSTDLIB_API size_t ZSTD_decompress( void* dst, size_t dstCapacity, + const void* src, size_t compressedSize); + +/*! ZSTD_getFrameContentSize() : requires v1.3.0+ + * `src` should point to the start of a ZSTD encoded frame. + * `srcSize` must be at least as large as the frame header. + * hint : any size >= `ZSTD_frameHeaderSize_max` is large enough. + * @return : - decompressed size of `src` frame content, if known + * - ZSTD_CONTENTSIZE_UNKNOWN if the size cannot be determined + * - ZSTD_CONTENTSIZE_ERROR if an error occurred (e.g. invalid magic number, srcSize too small) + * note 1 : a 0 return value means the frame is valid but "empty". + * note 2 : decompressed size is an optional field, it may not be present, typically in streaming mode. + * When `return==ZSTD_CONTENTSIZE_UNKNOWN`, data to decompress could be any size. + * In which case, it's necessary to use streaming mode to decompress data. + * Optionally, application can rely on some implicit limit, + * as ZSTD_decompress() only needs an upper bound of decompressed size. + * (For example, data could be necessarily cut into blocks <= 16 KB). + * note 3 : decompressed size is always present when compression is completed using single-pass functions, + * such as ZSTD_compress(), ZSTD_compressCCtx() ZSTD_compress_usingDict() or ZSTD_compress_usingCDict(). + * note 4 : decompressed size can be very large (64-bits value), + * potentially larger than what local system can handle as a single memory segment. + * In which case, it's necessary to use streaming mode to decompress data. + * note 5 : If source is untrusted, decompressed size could be wrong or intentionally modified. + * Always ensure return value fits within application's authorized limits. + * Each application can set its own limits. + * note 6 : This function replaces ZSTD_getDecompressedSize() */ +#define ZSTD_CONTENTSIZE_UNKNOWN (0ULL - 1) +#define ZSTD_CONTENTSIZE_ERROR (0ULL - 2) +ZSTDLIB_API unsigned long long ZSTD_getFrameContentSize(const void *src, size_t srcSize); + +/*! ZSTD_getDecompressedSize() : + * NOTE: This function is now obsolete, in favor of ZSTD_getFrameContentSize(). + * Both functions work the same way, but ZSTD_getDecompressedSize() blends + * "empty", "unknown" and "error" results to the same return value (0), + * while ZSTD_getFrameContentSize() gives them separate return values. + * @return : decompressed size of `src` frame content _if known and not empty_, 0 otherwise. */ +ZSTD_DEPRECATED("Replaced by ZSTD_getFrameContentSize") +ZSTDLIB_API +unsigned long long ZSTD_getDecompressedSize(const void* src, size_t srcSize); + +/*! ZSTD_findFrameCompressedSize() : Requires v1.4.0+ + * `src` should point to the start of a ZSTD frame or skippable frame. + * `srcSize` must be >= first frame size + * @return : the compressed size of the first frame starting at `src`, + * suitable to pass as `srcSize` to `ZSTD_decompress` or similar, + * or an error code if input is invalid */ +ZSTDLIB_API size_t ZSTD_findFrameCompressedSize(const void* src, size_t srcSize); + + +/*====== Helper functions ======*/ +/* ZSTD_compressBound() : + * maximum compressed size in worst case single-pass scenario. + * When invoking `ZSTD_compress()` or any other one-pass compression function, + * it's recommended to provide @dstCapacity >= ZSTD_compressBound(srcSize) + * as it eliminates one potential failure scenario, + * aka not enough room in dst buffer to write the compressed frame. + * Note : ZSTD_compressBound() itself can fail, if @srcSize > ZSTD_MAX_INPUT_SIZE . + * In which case, ZSTD_compressBound() will return an error code + * which can be tested using ZSTD_isError(). + * + * ZSTD_COMPRESSBOUND() : + * same as ZSTD_compressBound(), but as a macro. + * It can be used to produce constants, which can be useful for static allocation, + * for example to size a static array on stack. + * Will produce constant value 0 if srcSize too large. + */ +#define ZSTD_MAX_INPUT_SIZE ((sizeof(size_t)==8) ? 0xFF00FF00FF00FF00ULL : 0xFF00FF00U) +#define ZSTD_COMPRESSBOUND(srcSize) (((size_t)(srcSize) >= ZSTD_MAX_INPUT_SIZE) ? 0 : (srcSize) + ((srcSize)>>8) + (((srcSize) < (128<<10)) ? (((128<<10) - (srcSize)) >> 11) /* margin, from 64 to 0 */ : 0)) /* this formula ensures that bound(A) + bound(B) <= bound(A+B) as long as A and B >= 128 KB */ +ZSTDLIB_API size_t ZSTD_compressBound(size_t srcSize); /*!< maximum compressed size in worst case single-pass scenario */ +/* ZSTD_isError() : + * Most ZSTD_* functions returning a size_t value can be tested for error, + * using ZSTD_isError(). + * @return 1 if error, 0 otherwise + */ +ZSTDLIB_API unsigned ZSTD_isError(size_t code); /*!< tells if a `size_t` function result is an error code */ +ZSTDLIB_API const char* ZSTD_getErrorName(size_t code); /*!< provides readable string from an error code */ +ZSTDLIB_API int ZSTD_minCLevel(void); /*!< minimum negative compression level allowed, requires v1.4.0+ */ +ZSTDLIB_API int ZSTD_maxCLevel(void); /*!< maximum compression level available */ +ZSTDLIB_API int ZSTD_defaultCLevel(void); /*!< default compression level, specified by ZSTD_CLEVEL_DEFAULT, requires v1.5.0+ */ + + +/*************************************** +* Explicit context +***************************************/ +/*= Compression context + * When compressing many times, + * it is recommended to allocate a context just once, + * and reuse it for each successive compression operation. + * This will make workload friendlier for system's memory. + * Note : re-using context is just a speed / resource optimization. + * It doesn't change the compression ratio, which remains identical. + * Note 2 : In multi-threaded environments, + * use one different context per thread for parallel execution. + */ +typedef struct ZSTD_CCtx_s ZSTD_CCtx; +ZSTDLIB_API ZSTD_CCtx* ZSTD_createCCtx(void); +ZSTDLIB_API size_t ZSTD_freeCCtx(ZSTD_CCtx* cctx); /* accept NULL pointer */ + +/*! ZSTD_compressCCtx() : + * Same as ZSTD_compress(), using an explicit ZSTD_CCtx. + * Important : in order to mirror `ZSTD_compress()` behavior, + * this function compresses at the requested compression level, + * __ignoring any other advanced parameter__ . + * If any advanced parameter was set using the advanced API, + * they will all be reset. Only `compressionLevel` remains. + */ +ZSTDLIB_API size_t ZSTD_compressCCtx(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + int compressionLevel); + +/*= Decompression context + * When decompressing many times, + * it is recommended to allocate a context only once, + * and reuse it for each successive compression operation. + * This will make workload friendlier for system's memory. + * Use one context per thread for parallel execution. */ +typedef struct ZSTD_DCtx_s ZSTD_DCtx; +ZSTDLIB_API ZSTD_DCtx* ZSTD_createDCtx(void); +ZSTDLIB_API size_t ZSTD_freeDCtx(ZSTD_DCtx* dctx); /* accept NULL pointer */ + +/*! ZSTD_decompressDCtx() : + * Same as ZSTD_decompress(), + * requires an allocated ZSTD_DCtx. + * Compatible with sticky parameters (see below). + */ +ZSTDLIB_API size_t ZSTD_decompressDCtx(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize); + + +/********************************************* +* Advanced compression API (Requires v1.4.0+) +**********************************************/ + +/* API design : + * Parameters are pushed one by one into an existing context, + * using ZSTD_CCtx_set*() functions. + * Pushed parameters are sticky : they are valid for next compressed frame, and any subsequent frame. + * "sticky" parameters are applicable to `ZSTD_compress2()` and `ZSTD_compressStream*()` ! + * __They do not apply to one-shot variants such as ZSTD_compressCCtx()__ . + * + * It's possible to reset all parameters to "default" using ZSTD_CCtx_reset(). + * + * This API supersedes all other "advanced" API entry points in the experimental section. + * In the future, we expect to remove API entry points from experimental which are redundant with this API. + */ + + +/* Compression strategies, listed from fastest to strongest */ +typedef enum { ZSTD_fast=1, + ZSTD_dfast=2, + ZSTD_greedy=3, + ZSTD_lazy=4, + ZSTD_lazy2=5, + ZSTD_btlazy2=6, + ZSTD_btopt=7, + ZSTD_btultra=8, + ZSTD_btultra2=9 + /* note : new strategies _might_ be added in the future. + Only the order (from fast to strong) is guaranteed */ +} ZSTD_strategy; + +typedef enum { + + /* compression parameters + * Note: When compressing with a ZSTD_CDict these parameters are superseded + * by the parameters used to construct the ZSTD_CDict. + * See ZSTD_CCtx_refCDict() for more info (superseded-by-cdict). */ + ZSTD_c_compressionLevel=100, /* Set compression parameters according to pre-defined cLevel table. + * Note that exact compression parameters are dynamically determined, + * depending on both compression level and srcSize (when known). + * Default level is ZSTD_CLEVEL_DEFAULT==3. + * Special: value 0 means default, which is controlled by ZSTD_CLEVEL_DEFAULT. + * Note 1 : it's possible to pass a negative compression level. + * Note 2 : setting a level does not automatically set all other compression parameters + * to default. Setting this will however eventually dynamically impact the compression + * parameters which have not been manually set. The manually set + * ones will 'stick'. */ + /* Advanced compression parameters : + * It's possible to pin down compression parameters to some specific values. + * In which case, these values are no longer dynamically selected by the compressor */ + ZSTD_c_windowLog=101, /* Maximum allowed back-reference distance, expressed as power of 2. + * This will set a memory budget for streaming decompression, + * with larger values requiring more memory + * and typically compressing more. + * Must be clamped between ZSTD_WINDOWLOG_MIN and ZSTD_WINDOWLOG_MAX. + * Special: value 0 means "use default windowLog". + * Note: Using a windowLog greater than ZSTD_WINDOWLOG_LIMIT_DEFAULT + * requires explicitly allowing such size at streaming decompression stage. */ + ZSTD_c_hashLog=102, /* Size of the initial probe table, as a power of 2. + * Resulting memory usage is (1 << (hashLog+2)). + * Must be clamped between ZSTD_HASHLOG_MIN and ZSTD_HASHLOG_MAX. + * Larger tables improve compression ratio of strategies <= dFast, + * and improve speed of strategies > dFast. + * Special: value 0 means "use default hashLog". */ + ZSTD_c_chainLog=103, /* Size of the multi-probe search table, as a power of 2. + * Resulting memory usage is (1 << (chainLog+2)). + * Must be clamped between ZSTD_CHAINLOG_MIN and ZSTD_CHAINLOG_MAX. + * Larger tables result in better and slower compression. + * This parameter is useless for "fast" strategy. + * It's still useful when using "dfast" strategy, + * in which case it defines a secondary probe table. + * Special: value 0 means "use default chainLog". */ + ZSTD_c_searchLog=104, /* Number of search attempts, as a power of 2. + * More attempts result in better and slower compression. + * This parameter is useless for "fast" and "dFast" strategies. + * Special: value 0 means "use default searchLog". */ + ZSTD_c_minMatch=105, /* Minimum size of searched matches. + * Note that Zstandard can still find matches of smaller size, + * it just tweaks its search algorithm to look for this size and larger. + * Larger values increase compression and decompression speed, but decrease ratio. + * Must be clamped between ZSTD_MINMATCH_MIN and ZSTD_MINMATCH_MAX. + * Note that currently, for all strategies < btopt, effective minimum is 4. + * , for all strategies > fast, effective maximum is 6. + * Special: value 0 means "use default minMatchLength". */ + ZSTD_c_targetLength=106, /* Impact of this field depends on strategy. + * For strategies btopt, btultra & btultra2: + * Length of Match considered "good enough" to stop search. + * Larger values make compression stronger, and slower. + * For strategy fast: + * Distance between match sampling. + * Larger values make compression faster, and weaker. + * Special: value 0 means "use default targetLength". */ + ZSTD_c_strategy=107, /* See ZSTD_strategy enum definition. + * The higher the value of selected strategy, the more complex it is, + * resulting in stronger and slower compression. + * Special: value 0 means "use default strategy". */ + + ZSTD_c_targetCBlockSize=130, /* v1.5.6+ + * Attempts to fit compressed block size into approximatively targetCBlockSize. + * Bound by ZSTD_TARGETCBLOCKSIZE_MIN and ZSTD_TARGETCBLOCKSIZE_MAX. + * Note that it's not a guarantee, just a convergence target (default:0). + * No target when targetCBlockSize == 0. + * This is helpful in low bandwidth streaming environments to improve end-to-end latency, + * when a client can make use of partial documents (a prominent example being Chrome). + * Note: this parameter is stable since v1.5.6. + * It was present as an experimental parameter in earlier versions, + * but it's not recommended using it with earlier library versions + * due to massive performance regressions. + */ + /* LDM mode parameters */ + ZSTD_c_enableLongDistanceMatching=160, /* Enable long distance matching. + * This parameter is designed to improve compression ratio + * for large inputs, by finding large matches at long distance. + * It increases memory usage and window size. + * Note: enabling this parameter increases default ZSTD_c_windowLog to 128 MB + * except when expressly set to a different value. + * Note: will be enabled by default if ZSTD_c_windowLog >= 128 MB and + * compression strategy >= ZSTD_btopt (== compression level 16+) */ + ZSTD_c_ldmHashLog=161, /* Size of the table for long distance matching, as a power of 2. + * Larger values increase memory usage and compression ratio, + * but decrease compression speed. + * Must be clamped between ZSTD_HASHLOG_MIN and ZSTD_HASHLOG_MAX + * default: windowlog - 7. + * Special: value 0 means "automatically determine hashlog". */ + ZSTD_c_ldmMinMatch=162, /* Minimum match size for long distance matcher. + * Larger/too small values usually decrease compression ratio. + * Must be clamped between ZSTD_LDM_MINMATCH_MIN and ZSTD_LDM_MINMATCH_MAX. + * Special: value 0 means "use default value" (default: 64). */ + ZSTD_c_ldmBucketSizeLog=163, /* Log size of each bucket in the LDM hash table for collision resolution. + * Larger values improve collision resolution but decrease compression speed. + * The maximum value is ZSTD_LDM_BUCKETSIZELOG_MAX. + * Special: value 0 means "use default value" (default: 3). */ + ZSTD_c_ldmHashRateLog=164, /* Frequency of inserting/looking up entries into the LDM hash table. + * Must be clamped between 0 and (ZSTD_WINDOWLOG_MAX - ZSTD_HASHLOG_MIN). + * Default is MAX(0, (windowLog - ldmHashLog)), optimizing hash table usage. + * Larger values improve compression speed. + * Deviating far from default value will likely result in a compression ratio decrease. + * Special: value 0 means "automatically determine hashRateLog". */ + + /* frame parameters */ + ZSTD_c_contentSizeFlag=200, /* Content size will be written into frame header _whenever known_ (default:1) + * Content size must be known at the beginning of compression. + * This is automatically the case when using ZSTD_compress2(), + * For streaming scenarios, content size must be provided with ZSTD_CCtx_setPledgedSrcSize() */ + ZSTD_c_checksumFlag=201, /* A 32-bits checksum of content is written at end of frame (default:0) */ + ZSTD_c_dictIDFlag=202, /* When applicable, dictionary's ID is written into frame header (default:1) */ + + /* multi-threading parameters */ + /* These parameters are only active if multi-threading is enabled (compiled with build macro ZSTD_MULTITHREAD). + * Otherwise, trying to set any other value than default (0) will be a no-op and return an error. + * In a situation where it's unknown if the linked library supports multi-threading or not, + * setting ZSTD_c_nbWorkers to any value >= 1 and consulting the return value provides a quick way to check this property. + */ + ZSTD_c_nbWorkers=400, /* Select how many threads will be spawned to compress in parallel. + * When nbWorkers >= 1, triggers asynchronous mode when invoking ZSTD_compressStream*() : + * ZSTD_compressStream*() consumes input and flush output if possible, but immediately gives back control to caller, + * while compression is performed in parallel, within worker thread(s). + * (note : a strong exception to this rule is when first invocation of ZSTD_compressStream2() sets ZSTD_e_end : + * in which case, ZSTD_compressStream2() delegates to ZSTD_compress2(), which is always a blocking call). + * More workers improve speed, but also increase memory usage. + * Default value is `0`, aka "single-threaded mode" : no worker is spawned, + * compression is performed inside Caller's thread, and all invocations are blocking */ + ZSTD_c_jobSize=401, /* Size of a compression job. This value is enforced only when nbWorkers >= 1. + * Each compression job is completed in parallel, so this value can indirectly impact the nb of active threads. + * 0 means default, which is dynamically determined based on compression parameters. + * Job size must be a minimum of overlap size, or ZSTDMT_JOBSIZE_MIN (= 512 KB), whichever is largest. + * The minimum size is automatically and transparently enforced. */ + ZSTD_c_overlapLog=402, /* Control the overlap size, as a fraction of window size. + * The overlap size is an amount of data reloaded from previous job at the beginning of a new job. + * It helps preserve compression ratio, while each job is compressed in parallel. + * This value is enforced only when nbWorkers >= 1. + * Larger values increase compression ratio, but decrease speed. + * Possible values range from 0 to 9 : + * - 0 means "default" : value will be determined by the library, depending on strategy + * - 1 means "no overlap" + * - 9 means "full overlap", using a full window size. + * Each intermediate rank increases/decreases load size by a factor 2 : + * 9: full window; 8: w/2; 7: w/4; 6: w/8; 5:w/16; 4: w/32; 3:w/64; 2:w/128; 1:no overlap; 0:default + * default value varies between 6 and 9, depending on strategy */ + + /* note : additional experimental parameters are also available + * within the experimental section of the API. + * At the time of this writing, they include : + * ZSTD_c_rsyncable + * ZSTD_c_format + * ZSTD_c_forceMaxWindow + * ZSTD_c_forceAttachDict + * ZSTD_c_literalCompressionMode + * ZSTD_c_srcSizeHint + * ZSTD_c_enableDedicatedDictSearch + * ZSTD_c_stableInBuffer + * ZSTD_c_stableOutBuffer + * ZSTD_c_blockDelimiters + * ZSTD_c_validateSequences + * ZSTD_c_useBlockSplitter + * ZSTD_c_useRowMatchFinder + * ZSTD_c_prefetchCDictTables + * ZSTD_c_enableSeqProducerFallback + * ZSTD_c_maxBlockSize + * Because they are not stable, it's necessary to define ZSTD_STATIC_LINKING_ONLY to access them. + * note : never ever use experimentalParam? names directly; + * also, the enums values themselves are unstable and can still change. + */ + ZSTD_c_experimentalParam1=500, + ZSTD_c_experimentalParam2=10, + ZSTD_c_experimentalParam3=1000, + ZSTD_c_experimentalParam4=1001, + ZSTD_c_experimentalParam5=1002, + /* was ZSTD_c_experimentalParam6=1003; is now ZSTD_c_targetCBlockSize */ + ZSTD_c_experimentalParam7=1004, + ZSTD_c_experimentalParam8=1005, + ZSTD_c_experimentalParam9=1006, + ZSTD_c_experimentalParam10=1007, + ZSTD_c_experimentalParam11=1008, + ZSTD_c_experimentalParam12=1009, + ZSTD_c_experimentalParam13=1010, + ZSTD_c_experimentalParam14=1011, + ZSTD_c_experimentalParam15=1012, + ZSTD_c_experimentalParam16=1013, + ZSTD_c_experimentalParam17=1014, + ZSTD_c_experimentalParam18=1015, + ZSTD_c_experimentalParam19=1016 +} ZSTD_cParameter; + +typedef struct { + size_t error; + int lowerBound; + int upperBound; +} ZSTD_bounds; + +/*! ZSTD_cParam_getBounds() : + * All parameters must belong to an interval with lower and upper bounds, + * otherwise they will either trigger an error or be automatically clamped. + * @return : a structure, ZSTD_bounds, which contains + * - an error status field, which must be tested using ZSTD_isError() + * - lower and upper bounds, both inclusive + */ +ZSTDLIB_API ZSTD_bounds ZSTD_cParam_getBounds(ZSTD_cParameter cParam); + +/*! ZSTD_CCtx_setParameter() : + * Set one compression parameter, selected by enum ZSTD_cParameter. + * All parameters have valid bounds. Bounds can be queried using ZSTD_cParam_getBounds(). + * Providing a value beyond bound will either clamp it, or trigger an error (depending on parameter). + * Setting a parameter is generally only possible during frame initialization (before starting compression). + * Exception : when using multi-threading mode (nbWorkers >= 1), + * the following parameters can be updated _during_ compression (within same frame): + * => compressionLevel, hashLog, chainLog, searchLog, minMatch, targetLength and strategy. + * new parameters will be active for next job only (after a flush()). + * @return : an error code (which can be tested using ZSTD_isError()). + */ +ZSTDLIB_API size_t ZSTD_CCtx_setParameter(ZSTD_CCtx* cctx, ZSTD_cParameter param, int value); + +/*! ZSTD_CCtx_setPledgedSrcSize() : + * Total input data size to be compressed as a single frame. + * Value will be written in frame header, unless if explicitly forbidden using ZSTD_c_contentSizeFlag. + * This value will also be controlled at end of frame, and trigger an error if not respected. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Note 1 : pledgedSrcSize==0 actually means zero, aka an empty frame. + * In order to mean "unknown content size", pass constant ZSTD_CONTENTSIZE_UNKNOWN. + * ZSTD_CONTENTSIZE_UNKNOWN is default value for any new frame. + * Note 2 : pledgedSrcSize is only valid once, for the next frame. + * It's discarded at the end of the frame, and replaced by ZSTD_CONTENTSIZE_UNKNOWN. + * Note 3 : Whenever all input data is provided and consumed in a single round, + * for example with ZSTD_compress2(), + * or invoking immediately ZSTD_compressStream2(,,,ZSTD_e_end), + * this value is automatically overridden by srcSize instead. + */ +ZSTDLIB_API size_t ZSTD_CCtx_setPledgedSrcSize(ZSTD_CCtx* cctx, unsigned long long pledgedSrcSize); + +typedef enum { + ZSTD_reset_session_only = 1, + ZSTD_reset_parameters = 2, + ZSTD_reset_session_and_parameters = 3 +} ZSTD_ResetDirective; + +/*! ZSTD_CCtx_reset() : + * There are 2 different things that can be reset, independently or jointly : + * - The session : will stop compressing current frame, and make CCtx ready to start a new one. + * Useful after an error, or to interrupt any ongoing compression. + * Any internal data not yet flushed is cancelled. + * Compression parameters and dictionary remain unchanged. + * They will be used to compress next frame. + * Resetting session never fails. + * - The parameters : changes all parameters back to "default". + * This also removes any reference to any dictionary or external sequence producer. + * Parameters can only be changed between 2 sessions (i.e. no compression is currently ongoing) + * otherwise the reset fails, and function returns an error value (which can be tested using ZSTD_isError()) + * - Both : similar to resetting the session, followed by resetting parameters. + */ +ZSTDLIB_API size_t ZSTD_CCtx_reset(ZSTD_CCtx* cctx, ZSTD_ResetDirective reset); + +/*! ZSTD_compress2() : + * Behave the same as ZSTD_compressCCtx(), but compression parameters are set using the advanced API. + * (note that this entry point doesn't even expose a compression level parameter). + * ZSTD_compress2() always starts a new frame. + * Should cctx hold data from a previously unfinished frame, everything about it is forgotten. + * - Compression parameters are pushed into CCtx before starting compression, using ZSTD_CCtx_set*() + * - The function is always blocking, returns when compression is completed. + * NOTE: Providing `dstCapacity >= ZSTD_compressBound(srcSize)` guarantees that zstd will have + * enough space to successfully compress the data, though it is possible it fails for other reasons. + * @return : compressed size written into `dst` (<= `dstCapacity), + * or an error code if it fails (which can be tested using ZSTD_isError()). + */ +ZSTDLIB_API size_t ZSTD_compress2( ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize); + + +/*********************************************** +* Advanced decompression API (Requires v1.4.0+) +************************************************/ + +/* The advanced API pushes parameters one by one into an existing DCtx context. + * Parameters are sticky, and remain valid for all following frames + * using the same DCtx context. + * It's possible to reset parameters to default values using ZSTD_DCtx_reset(). + * Note : This API is compatible with existing ZSTD_decompressDCtx() and ZSTD_decompressStream(). + * Therefore, no new decompression function is necessary. + */ + +typedef enum { + + ZSTD_d_windowLogMax=100, /* Select a size limit (in power of 2) beyond which + * the streaming API will refuse to allocate memory buffer + * in order to protect the host from unreasonable memory requirements. + * This parameter is only useful in streaming mode, since no internal buffer is allocated in single-pass mode. + * By default, a decompression context accepts window sizes <= (1 << ZSTD_WINDOWLOG_LIMIT_DEFAULT). + * Special: value 0 means "use default maximum windowLog". */ + + /* note : additional experimental parameters are also available + * within the experimental section of the API. + * At the time of this writing, they include : + * ZSTD_d_format + * ZSTD_d_stableOutBuffer + * ZSTD_d_forceIgnoreChecksum + * ZSTD_d_refMultipleDDicts + * ZSTD_d_disableHuffmanAssembly + * ZSTD_d_maxBlockSize + * Because they are not stable, it's necessary to define ZSTD_STATIC_LINKING_ONLY to access them. + * note : never ever use experimentalParam? names directly + */ + ZSTD_d_experimentalParam1=1000, + ZSTD_d_experimentalParam2=1001, + ZSTD_d_experimentalParam3=1002, + ZSTD_d_experimentalParam4=1003, + ZSTD_d_experimentalParam5=1004, + ZSTD_d_experimentalParam6=1005 + +} ZSTD_dParameter; + +/*! ZSTD_dParam_getBounds() : + * All parameters must belong to an interval with lower and upper bounds, + * otherwise they will either trigger an error or be automatically clamped. + * @return : a structure, ZSTD_bounds, which contains + * - an error status field, which must be tested using ZSTD_isError() + * - both lower and upper bounds, inclusive + */ +ZSTDLIB_API ZSTD_bounds ZSTD_dParam_getBounds(ZSTD_dParameter dParam); + +/*! ZSTD_DCtx_setParameter() : + * Set one compression parameter, selected by enum ZSTD_dParameter. + * All parameters have valid bounds. Bounds can be queried using ZSTD_dParam_getBounds(). + * Providing a value beyond bound will either clamp it, or trigger an error (depending on parameter). + * Setting a parameter is only possible during frame initialization (before starting decompression). + * @return : 0, or an error code (which can be tested using ZSTD_isError()). + */ +ZSTDLIB_API size_t ZSTD_DCtx_setParameter(ZSTD_DCtx* dctx, ZSTD_dParameter param, int value); + +/*! ZSTD_DCtx_reset() : + * Return a DCtx to clean state. + * Session and parameters can be reset jointly or separately. + * Parameters can only be reset when no active frame is being decompressed. + * @return : 0, or an error code, which can be tested with ZSTD_isError() + */ +ZSTDLIB_API size_t ZSTD_DCtx_reset(ZSTD_DCtx* dctx, ZSTD_ResetDirective reset); + + +/**************************** +* Streaming +****************************/ + +typedef struct ZSTD_inBuffer_s { + const void* src; /**< start of input buffer */ + size_t size; /**< size of input buffer */ + size_t pos; /**< position where reading stopped. Will be updated. Necessarily 0 <= pos <= size */ +} ZSTD_inBuffer; + +typedef struct ZSTD_outBuffer_s { + void* dst; /**< start of output buffer */ + size_t size; /**< size of output buffer */ + size_t pos; /**< position where writing stopped. Will be updated. Necessarily 0 <= pos <= size */ +} ZSTD_outBuffer; + + + +/*-*********************************************************************** +* Streaming compression - HowTo +* +* A ZSTD_CStream object is required to track streaming operation. +* Use ZSTD_createCStream() and ZSTD_freeCStream() to create/release resources. +* ZSTD_CStream objects can be reused multiple times on consecutive compression operations. +* It is recommended to reuse ZSTD_CStream since it will play nicer with system's memory, by re-using already allocated memory. +* +* For parallel execution, use one separate ZSTD_CStream per thread. +* +* note : since v1.3.0, ZSTD_CStream and ZSTD_CCtx are the same thing. +* +* Parameters are sticky : when starting a new compression on the same context, +* it will reuse the same sticky parameters as previous compression session. +* When in doubt, it's recommended to fully initialize the context before usage. +* Use ZSTD_CCtx_reset() to reset the context and ZSTD_CCtx_setParameter(), +* ZSTD_CCtx_setPledgedSrcSize(), or ZSTD_CCtx_loadDictionary() and friends to +* set more specific parameters, the pledged source size, or load a dictionary. +* +* Use ZSTD_compressStream2() with ZSTD_e_continue as many times as necessary to +* consume input stream. The function will automatically update both `pos` +* fields within `input` and `output`. +* Note that the function may not consume the entire input, for example, because +* the output buffer is already full, in which case `input.pos < input.size`. +* The caller must check if input has been entirely consumed. +* If not, the caller must make some room to receive more compressed data, +* and then present again remaining input data. +* note: ZSTD_e_continue is guaranteed to make some forward progress when called, +* but doesn't guarantee maximal forward progress. This is especially relevant +* when compressing with multiple threads. The call won't block if it can +* consume some input, but if it can't it will wait for some, but not all, +* output to be flushed. +* @return : provides a minimum amount of data remaining to be flushed from internal buffers +* or an error code, which can be tested using ZSTD_isError(). +* +* At any moment, it's possible to flush whatever data might remain stuck within internal buffer, +* using ZSTD_compressStream2() with ZSTD_e_flush. `output->pos` will be updated. +* Note that, if `output->size` is too small, a single invocation with ZSTD_e_flush might not be enough (return code > 0). +* In which case, make some room to receive more compressed data, and call again ZSTD_compressStream2() with ZSTD_e_flush. +* You must continue calling ZSTD_compressStream2() with ZSTD_e_flush until it returns 0, at which point you can change the +* operation. +* note: ZSTD_e_flush will flush as much output as possible, meaning when compressing with multiple threads, it will +* block until the flush is complete or the output buffer is full. +* @return : 0 if internal buffers are entirely flushed, +* >0 if some data still present within internal buffer (the value is minimal estimation of remaining size), +* or an error code, which can be tested using ZSTD_isError(). +* +* Calling ZSTD_compressStream2() with ZSTD_e_end instructs to finish a frame. +* It will perform a flush and write frame epilogue. +* The epilogue is required for decoders to consider a frame completed. +* flush operation is the same, and follows same rules as calling ZSTD_compressStream2() with ZSTD_e_flush. +* You must continue calling ZSTD_compressStream2() with ZSTD_e_end until it returns 0, at which point you are free to +* start a new frame. +* note: ZSTD_e_end will flush as much output as possible, meaning when compressing with multiple threads, it will +* block until the flush is complete or the output buffer is full. +* @return : 0 if frame fully completed and fully flushed, +* >0 if some data still present within internal buffer (the value is minimal estimation of remaining size), +* or an error code, which can be tested using ZSTD_isError(). +* +* *******************************************************************/ + +typedef ZSTD_CCtx ZSTD_CStream; /**< CCtx and CStream are now effectively same object (>= v1.3.0) */ + /* Continue to distinguish them for compatibility with older versions <= v1.2.0 */ +/*===== ZSTD_CStream management functions =====*/ +ZSTDLIB_API ZSTD_CStream* ZSTD_createCStream(void); +ZSTDLIB_API size_t ZSTD_freeCStream(ZSTD_CStream* zcs); /* accept NULL pointer */ + +/*===== Streaming compression functions =====*/ +typedef enum { + ZSTD_e_continue=0, /* collect more data, encoder decides when to output compressed result, for optimal compression ratio */ + ZSTD_e_flush=1, /* flush any data provided so far, + * it creates (at least) one new block, that can be decoded immediately on reception; + * frame will continue: any future data can still reference previously compressed data, improving compression. + * note : multithreaded compression will block to flush as much output as possible. */ + ZSTD_e_end=2 /* flush any remaining data _and_ close current frame. + * note that frame is only closed after compressed data is fully flushed (return value == 0). + * After that point, any additional data starts a new frame. + * note : each frame is independent (does not reference any content from previous frame). + : note : multithreaded compression will block to flush as much output as possible. */ +} ZSTD_EndDirective; + +/*! ZSTD_compressStream2() : Requires v1.4.0+ + * Behaves about the same as ZSTD_compressStream, with additional control on end directive. + * - Compression parameters are pushed into CCtx before starting compression, using ZSTD_CCtx_set*() + * - Compression parameters cannot be changed once compression is started (save a list of exceptions in multi-threading mode) + * - output->pos must be <= dstCapacity, input->pos must be <= srcSize + * - output->pos and input->pos will be updated. They are guaranteed to remain below their respective limit. + * - endOp must be a valid directive + * - When nbWorkers==0 (default), function is blocking : it completes its job before returning to caller. + * - When nbWorkers>=1, function is non-blocking : it copies a portion of input, distributes jobs to internal worker threads, flush to output whatever is available, + * and then immediately returns, just indicating that there is some data remaining to be flushed. + * The function nonetheless guarantees forward progress : it will return only after it reads or write at least 1+ byte. + * - Exception : if the first call requests a ZSTD_e_end directive and provides enough dstCapacity, the function delegates to ZSTD_compress2() which is always blocking. + * - @return provides a minimum amount of data remaining to be flushed from internal buffers + * or an error code, which can be tested using ZSTD_isError(). + * if @return != 0, flush is not fully completed, there is still some data left within internal buffers. + * This is useful for ZSTD_e_flush, since in this case more flushes are necessary to empty all buffers. + * For ZSTD_e_end, @return == 0 when internal buffers are fully flushed and frame is completed. + * - after a ZSTD_e_end directive, if internal buffer is not fully flushed (@return != 0), + * only ZSTD_e_end or ZSTD_e_flush operations are allowed. + * Before starting a new compression job, or changing compression parameters, + * it is required to fully flush internal buffers. + * - note: if an operation ends with an error, it may leave @cctx in an undefined state. + * Therefore, it's UB to invoke ZSTD_compressStream2() of ZSTD_compressStream() on such a state. + * In order to be re-employed after an error, a state must be reset, + * which can be done explicitly (ZSTD_CCtx_reset()), + * or is sometimes implied by methods starting a new compression job (ZSTD_initCStream(), ZSTD_compressCCtx()) + */ +ZSTDLIB_API size_t ZSTD_compressStream2( ZSTD_CCtx* cctx, + ZSTD_outBuffer* output, + ZSTD_inBuffer* input, + ZSTD_EndDirective endOp); + + +/* These buffer sizes are softly recommended. + * They are not required : ZSTD_compressStream*() happily accepts any buffer size, for both input and output. + * Respecting the recommended size just makes it a bit easier for ZSTD_compressStream*(), + * reducing the amount of memory shuffling and buffering, resulting in minor performance savings. + * + * However, note that these recommendations are from the perspective of a C caller program. + * If the streaming interface is invoked from some other language, + * especially managed ones such as Java or Go, through a foreign function interface such as jni or cgo, + * a major performance rule is to reduce crossing such interface to an absolute minimum. + * It's not rare that performance ends being spent more into the interface, rather than compression itself. + * In which cases, prefer using large buffers, as large as practical, + * for both input and output, to reduce the nb of roundtrips. + */ +ZSTDLIB_API size_t ZSTD_CStreamInSize(void); /**< recommended size for input buffer */ +ZSTDLIB_API size_t ZSTD_CStreamOutSize(void); /**< recommended size for output buffer. Guarantee to successfully flush at least one complete compressed block. */ + + +/* ***************************************************************************** + * This following is a legacy streaming API, available since v1.0+ . + * It can be replaced by ZSTD_CCtx_reset() and ZSTD_compressStream2(). + * It is redundant, but remains fully supported. + ******************************************************************************/ + +/*! + * Equivalent to: + * + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_refCDict(zcs, NULL); // clear the dictionary (if any) + * ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel); + * + * Note that ZSTD_initCStream() clears any previously set dictionary. Use the new API + * to compress with a dictionary. + */ +ZSTDLIB_API size_t ZSTD_initCStream(ZSTD_CStream* zcs, int compressionLevel); +/*! + * Alternative for ZSTD_compressStream2(zcs, output, input, ZSTD_e_continue). + * NOTE: The return value is different. ZSTD_compressStream() returns a hint for + * the next read size (if non-zero and not an error). ZSTD_compressStream2() + * returns the minimum nb of bytes left to flush (if non-zero and not an error). + */ +ZSTDLIB_API size_t ZSTD_compressStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output, ZSTD_inBuffer* input); +/*! Equivalent to ZSTD_compressStream2(zcs, output, &emptyInput, ZSTD_e_flush). */ +ZSTDLIB_API size_t ZSTD_flushStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output); +/*! Equivalent to ZSTD_compressStream2(zcs, output, &emptyInput, ZSTD_e_end). */ +ZSTDLIB_API size_t ZSTD_endStream(ZSTD_CStream* zcs, ZSTD_outBuffer* output); + + +/*-*************************************************************************** +* Streaming decompression - HowTo +* +* A ZSTD_DStream object is required to track streaming operations. +* Use ZSTD_createDStream() and ZSTD_freeDStream() to create/release resources. +* ZSTD_DStream objects can be reused multiple times. +* +* Use ZSTD_initDStream() to start a new decompression operation. +* @return : recommended first input size +* Alternatively, use advanced API to set specific properties. +* +* Use ZSTD_decompressStream() repetitively to consume your input. +* The function will update both `pos` fields. +* If `input.pos < input.size`, some input has not been consumed. +* It's up to the caller to present again remaining data. +* The function tries to flush all data decoded immediately, respecting output buffer size. +* If `output.pos < output.size`, decoder has flushed everything it could. +* But if `output.pos == output.size`, there might be some data left within internal buffers., +* In which case, call ZSTD_decompressStream() again to flush whatever remains in the buffer. +* Note : with no additional input provided, amount of data flushed is necessarily <= ZSTD_BLOCKSIZE_MAX. +* @return : 0 when a frame is completely decoded and fully flushed, +* or an error code, which can be tested using ZSTD_isError(), +* or any other value > 0, which means there is still some decoding or flushing to do to complete current frame : +* the return value is a suggested next input size (just a hint for better latency) +* that will never request more than the remaining frame size. +* *******************************************************************************/ + +typedef ZSTD_DCtx ZSTD_DStream; /**< DCtx and DStream are now effectively same object (>= v1.3.0) */ + /* For compatibility with versions <= v1.2.0, prefer differentiating them. */ +/*===== ZSTD_DStream management functions =====*/ +ZSTDLIB_API ZSTD_DStream* ZSTD_createDStream(void); +ZSTDLIB_API size_t ZSTD_freeDStream(ZSTD_DStream* zds); /* accept NULL pointer */ + +/*===== Streaming decompression functions =====*/ + +/*! ZSTD_initDStream() : + * Initialize/reset DStream state for new decompression operation. + * Call before new decompression operation using same DStream. + * + * Note : This function is redundant with the advanced API and equivalent to: + * ZSTD_DCtx_reset(zds, ZSTD_reset_session_only); + * ZSTD_DCtx_refDDict(zds, NULL); + */ +ZSTDLIB_API size_t ZSTD_initDStream(ZSTD_DStream* zds); + +/*! ZSTD_decompressStream() : + * Streaming decompression function. + * Call repetitively to consume full input updating it as necessary. + * Function will update both input and output `pos` fields exposing current state via these fields: + * - `input.pos < input.size`, some input remaining and caller should provide remaining input + * on the next call. + * - `output.pos < output.size`, decoder finished and flushed all remaining buffers. + * - `output.pos == output.size`, potentially uncflushed data present in the internal buffers, + * call ZSTD_decompressStream() again to flush remaining data to output. + * Note : with no additional input, amount of data flushed <= ZSTD_BLOCKSIZE_MAX. + * + * @return : 0 when a frame is completely decoded and fully flushed, + * or an error code, which can be tested using ZSTD_isError(), + * or any other value > 0, which means there is some decoding or flushing to do to complete current frame. + * + * Note: when an operation returns with an error code, the @zds state may be left in undefined state. + * It's UB to invoke `ZSTD_decompressStream()` on such a state. + * In order to re-use such a state, it must be first reset, + * which can be done explicitly (`ZSTD_DCtx_reset()`), + * or is implied for operations starting some new decompression job (`ZSTD_initDStream`, `ZSTD_decompressDCtx()`, `ZSTD_decompress_usingDict()`) + */ +ZSTDLIB_API size_t ZSTD_decompressStream(ZSTD_DStream* zds, ZSTD_outBuffer* output, ZSTD_inBuffer* input); + +ZSTDLIB_API size_t ZSTD_DStreamInSize(void); /*!< recommended size for input buffer */ +ZSTDLIB_API size_t ZSTD_DStreamOutSize(void); /*!< recommended size for output buffer. Guarantee to successfully flush at least one complete block in all circumstances. */ + + +/************************** +* Simple dictionary API +***************************/ +/*! ZSTD_compress_usingDict() : + * Compression at an explicit compression level using a Dictionary. + * A dictionary can be any arbitrary data segment (also called a prefix), + * or a buffer with specified information (see zdict.h). + * Note : This function loads the dictionary, resulting in significant startup delay. + * It's intended for a dictionary used only once. + * Note 2 : When `dict == NULL || dictSize < 8` no dictionary is used. */ +ZSTDLIB_API size_t ZSTD_compress_usingDict(ZSTD_CCtx* ctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize, + int compressionLevel); + +/*! ZSTD_decompress_usingDict() : + * Decompression using a known Dictionary. + * Dictionary must be identical to the one used during compression. + * Note : This function loads the dictionary, resulting in significant startup delay. + * It's intended for a dictionary used only once. + * Note : When `dict == NULL || dictSize < 8` no dictionary is used. */ +ZSTDLIB_API size_t ZSTD_decompress_usingDict(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize); + + +/*********************************** + * Bulk processing dictionary API + **********************************/ +typedef struct ZSTD_CDict_s ZSTD_CDict; + +/*! ZSTD_createCDict() : + * When compressing multiple messages or blocks using the same dictionary, + * it's recommended to digest the dictionary only once, since it's a costly operation. + * ZSTD_createCDict() will create a state from digesting a dictionary. + * The resulting state can be used for future compression operations with very limited startup cost. + * ZSTD_CDict can be created once and shared by multiple threads concurrently, since its usage is read-only. + * @dictBuffer can be released after ZSTD_CDict creation, because its content is copied within CDict. + * Note 1 : Consider experimental function `ZSTD_createCDict_byReference()` if you prefer to not duplicate @dictBuffer content. + * Note 2 : A ZSTD_CDict can be created from an empty @dictBuffer, + * in which case the only thing that it transports is the @compressionLevel. + * This can be useful in a pipeline featuring ZSTD_compress_usingCDict() exclusively, + * expecting a ZSTD_CDict parameter with any data, including those without a known dictionary. */ +ZSTDLIB_API ZSTD_CDict* ZSTD_createCDict(const void* dictBuffer, size_t dictSize, + int compressionLevel); + +/*! ZSTD_freeCDict() : + * Function frees memory allocated by ZSTD_createCDict(). + * If a NULL pointer is passed, no operation is performed. */ +ZSTDLIB_API size_t ZSTD_freeCDict(ZSTD_CDict* CDict); + +/*! ZSTD_compress_usingCDict() : + * Compression using a digested Dictionary. + * Recommended when same dictionary is used multiple times. + * Note : compression level is _decided at dictionary creation time_, + * and frame parameters are hardcoded (dictID=yes, contentSize=yes, checksum=no) */ +ZSTDLIB_API size_t ZSTD_compress_usingCDict(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_CDict* cdict); + + +typedef struct ZSTD_DDict_s ZSTD_DDict; + +/*! ZSTD_createDDict() : + * Create a digested dictionary, ready to start decompression operation without startup delay. + * dictBuffer can be released after DDict creation, as its content is copied inside DDict. */ +ZSTDLIB_API ZSTD_DDict* ZSTD_createDDict(const void* dictBuffer, size_t dictSize); + +/*! ZSTD_freeDDict() : + * Function frees memory allocated with ZSTD_createDDict() + * If a NULL pointer is passed, no operation is performed. */ +ZSTDLIB_API size_t ZSTD_freeDDict(ZSTD_DDict* ddict); + +/*! ZSTD_decompress_usingDDict() : + * Decompression using a digested Dictionary. + * Recommended when same dictionary is used multiple times. */ +ZSTDLIB_API size_t ZSTD_decompress_usingDDict(ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_DDict* ddict); + + +/******************************** + * Dictionary helper functions + *******************************/ + +/*! ZSTD_getDictID_fromDict() : Requires v1.4.0+ + * Provides the dictID stored within dictionary. + * if @return == 0, the dictionary is not conformant with Zstandard specification. + * It can still be loaded, but as a content-only dictionary. */ +ZSTDLIB_API unsigned ZSTD_getDictID_fromDict(const void* dict, size_t dictSize); + +/*! ZSTD_getDictID_fromCDict() : Requires v1.5.0+ + * Provides the dictID of the dictionary loaded into `cdict`. + * If @return == 0, the dictionary is not conformant to Zstandard specification, or empty. + * Non-conformant dictionaries can still be loaded, but as content-only dictionaries. */ +ZSTDLIB_API unsigned ZSTD_getDictID_fromCDict(const ZSTD_CDict* cdict); + +/*! ZSTD_getDictID_fromDDict() : Requires v1.4.0+ + * Provides the dictID of the dictionary loaded into `ddict`. + * If @return == 0, the dictionary is not conformant to Zstandard specification, or empty. + * Non-conformant dictionaries can still be loaded, but as content-only dictionaries. */ +ZSTDLIB_API unsigned ZSTD_getDictID_fromDDict(const ZSTD_DDict* ddict); + +/*! ZSTD_getDictID_fromFrame() : Requires v1.4.0+ + * Provides the dictID required to decompressed the frame stored within `src`. + * If @return == 0, the dictID could not be decoded. + * This could for one of the following reasons : + * - The frame does not require a dictionary to be decoded (most common case). + * - The frame was built with dictID intentionally removed. Whatever dictionary is necessary is a hidden piece of information. + * Note : this use case also happens when using a non-conformant dictionary. + * - `srcSize` is too small, and as a result, the frame header could not be decoded (only possible if `srcSize < ZSTD_FRAMEHEADERSIZE_MAX`). + * - This is not a Zstandard frame. + * When identifying the exact failure cause, it's possible to use ZSTD_getFrameHeader(), which will provide a more precise error code. */ +ZSTDLIB_API unsigned ZSTD_getDictID_fromFrame(const void* src, size_t srcSize); + + +/******************************************************************************* + * Advanced dictionary and prefix API (Requires v1.4.0+) + * + * This API allows dictionaries to be used with ZSTD_compress2(), + * ZSTD_compressStream2(), and ZSTD_decompressDCtx(). + * Dictionaries are sticky, they remain valid when same context is reused, + * they only reset when the context is reset + * with ZSTD_reset_parameters or ZSTD_reset_session_and_parameters. + * In contrast, Prefixes are single-use. + ******************************************************************************/ + + +/*! ZSTD_CCtx_loadDictionary() : Requires v1.4.0+ + * Create an internal CDict from `dict` buffer. + * Decompression will have to use same dictionary. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special: Loading a NULL (or 0-size) dictionary invalidates previous dictionary, + * meaning "return to no-dictionary mode". + * Note 1 : Dictionary is sticky, it will be used for all future compressed frames, + * until parameters are reset, a new dictionary is loaded, or the dictionary + * is explicitly invalidated by loading a NULL dictionary. + * Note 2 : Loading a dictionary involves building tables. + * It's also a CPU consuming operation, with non-negligible impact on latency. + * Tables are dependent on compression parameters, and for this reason, + * compression parameters can no longer be changed after loading a dictionary. + * Note 3 :`dict` content will be copied internally. + * Use experimental ZSTD_CCtx_loadDictionary_byReference() to reference content instead. + * In such a case, dictionary buffer must outlive its users. + * Note 4 : Use ZSTD_CCtx_loadDictionary_advanced() + * to precisely select how dictionary content must be interpreted. + * Note 5 : This method does not benefit from LDM (long distance mode). + * If you want to employ LDM on some large dictionary content, + * prefer employing ZSTD_CCtx_refPrefix() described below. + */ +ZSTDLIB_API size_t ZSTD_CCtx_loadDictionary(ZSTD_CCtx* cctx, const void* dict, size_t dictSize); + +/*! ZSTD_CCtx_refCDict() : Requires v1.4.0+ + * Reference a prepared dictionary, to be used for all future compressed frames. + * Note that compression parameters are enforced from within CDict, + * and supersede any compression parameter previously set within CCtx. + * The parameters ignored are labelled as "superseded-by-cdict" in the ZSTD_cParameter enum docs. + * The ignored parameters will be used again if the CCtx is returned to no-dictionary mode. + * The dictionary will remain valid for future compressed frames using same CCtx. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special : Referencing a NULL CDict means "return to no-dictionary mode". + * Note 1 : Currently, only one dictionary can be managed. + * Referencing a new dictionary effectively "discards" any previous one. + * Note 2 : CDict is just referenced, its lifetime must outlive its usage within CCtx. */ +ZSTDLIB_API size_t ZSTD_CCtx_refCDict(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict); + +/*! ZSTD_CCtx_refPrefix() : Requires v1.4.0+ + * Reference a prefix (single-usage dictionary) for next compressed frame. + * A prefix is **only used once**. Tables are discarded at end of frame (ZSTD_e_end). + * Decompression will need same prefix to properly regenerate data. + * Compressing with a prefix is similar in outcome as performing a diff and compressing it, + * but performs much faster, especially during decompression (compression speed is tunable with compression level). + * This method is compatible with LDM (long distance mode). + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special: Adding any prefix (including NULL) invalidates any previous prefix or dictionary + * Note 1 : Prefix buffer is referenced. It **must** outlive compression. + * Its content must remain unmodified during compression. + * Note 2 : If the intention is to diff some large src data blob with some prior version of itself, + * ensure that the window size is large enough to contain the entire source. + * See ZSTD_c_windowLog. + * Note 3 : Referencing a prefix involves building tables, which are dependent on compression parameters. + * It's a CPU consuming operation, with non-negligible impact on latency. + * If there is a need to use the same prefix multiple times, consider loadDictionary instead. + * Note 4 : By default, the prefix is interpreted as raw content (ZSTD_dct_rawContent). + * Use experimental ZSTD_CCtx_refPrefix_advanced() to alter dictionary interpretation. */ +ZSTDLIB_API size_t ZSTD_CCtx_refPrefix(ZSTD_CCtx* cctx, + const void* prefix, size_t prefixSize); + +/*! ZSTD_DCtx_loadDictionary() : Requires v1.4.0+ + * Create an internal DDict from dict buffer, to be used to decompress all future frames. + * The dictionary remains valid for all future frames, until explicitly invalidated, or + * a new dictionary is loaded. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special : Adding a NULL (or 0-size) dictionary invalidates any previous dictionary, + * meaning "return to no-dictionary mode". + * Note 1 : Loading a dictionary involves building tables, + * which has a non-negligible impact on CPU usage and latency. + * It's recommended to "load once, use many times", to amortize the cost + * Note 2 :`dict` content will be copied internally, so `dict` can be released after loading. + * Use ZSTD_DCtx_loadDictionary_byReference() to reference dictionary content instead. + * Note 3 : Use ZSTD_DCtx_loadDictionary_advanced() to take control of + * how dictionary content is loaded and interpreted. + */ +ZSTDLIB_API size_t ZSTD_DCtx_loadDictionary(ZSTD_DCtx* dctx, const void* dict, size_t dictSize); + +/*! ZSTD_DCtx_refDDict() : Requires v1.4.0+ + * Reference a prepared dictionary, to be used to decompress next frames. + * The dictionary remains active for decompression of future frames using same DCtx. + * + * If called with ZSTD_d_refMultipleDDicts enabled, repeated calls of this function + * will store the DDict references in a table, and the DDict used for decompression + * will be determined at decompression time, as per the dict ID in the frame. + * The memory for the table is allocated on the first call to refDDict, and can be + * freed with ZSTD_freeDCtx(). + * + * If called with ZSTD_d_refMultipleDDicts disabled (the default), only one dictionary + * will be managed, and referencing a dictionary effectively "discards" any previous one. + * + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Special: referencing a NULL DDict means "return to no-dictionary mode". + * Note 2 : DDict is just referenced, its lifetime must outlive its usage from DCtx. + */ +ZSTDLIB_API size_t ZSTD_DCtx_refDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict); + +/*! ZSTD_DCtx_refPrefix() : Requires v1.4.0+ + * Reference a prefix (single-usage dictionary) to decompress next frame. + * This is the reverse operation of ZSTD_CCtx_refPrefix(), + * and must use the same prefix as the one used during compression. + * Prefix is **only used once**. Reference is discarded at end of frame. + * End of frame is reached when ZSTD_decompressStream() returns 0. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + * Note 1 : Adding any prefix (including NULL) invalidates any previously set prefix or dictionary + * Note 2 : Prefix buffer is referenced. It **must** outlive decompression. + * Prefix buffer must remain unmodified up to the end of frame, + * reached when ZSTD_decompressStream() returns 0. + * Note 3 : By default, the prefix is treated as raw content (ZSTD_dct_rawContent). + * Use ZSTD_CCtx_refPrefix_advanced() to alter dictMode (Experimental section) + * Note 4 : Referencing a raw content prefix has almost no cpu nor memory cost. + * A full dictionary is more costly, as it requires building tables. + */ +ZSTDLIB_API size_t ZSTD_DCtx_refPrefix(ZSTD_DCtx* dctx, + const void* prefix, size_t prefixSize); + +/* === Memory management === */ + +/*! ZSTD_sizeof_*() : Requires v1.4.0+ + * These functions give the _current_ memory usage of selected object. + * Note that object memory usage can evolve (increase or decrease) over time. */ +ZSTDLIB_API size_t ZSTD_sizeof_CCtx(const ZSTD_CCtx* cctx); +ZSTDLIB_API size_t ZSTD_sizeof_DCtx(const ZSTD_DCtx* dctx); +ZSTDLIB_API size_t ZSTD_sizeof_CStream(const ZSTD_CStream* zcs); +ZSTDLIB_API size_t ZSTD_sizeof_DStream(const ZSTD_DStream* zds); +ZSTDLIB_API size_t ZSTD_sizeof_CDict(const ZSTD_CDict* cdict); +ZSTDLIB_API size_t ZSTD_sizeof_DDict(const ZSTD_DDict* ddict); + +#endif /* ZSTD_H_235446 */ + + +/* ************************************************************************************** + * ADVANCED AND EXPERIMENTAL FUNCTIONS + **************************************************************************************** + * The definitions in the following section are considered experimental. + * They are provided for advanced scenarios. + * They should never be used with a dynamic library, as prototypes may change in the future. + * Use them only in association with static linking. + * ***************************************************************************************/ + +#if defined(ZSTD_STATIC_LINKING_ONLY) && !defined(ZSTD_H_ZSTD_STATIC_LINKING_ONLY) +#define ZSTD_H_ZSTD_STATIC_LINKING_ONLY + +/* This can be overridden externally to hide static symbols. */ +#ifndef ZSTDLIB_STATIC_API +# if defined(ZSTD_DLL_EXPORT) && (ZSTD_DLL_EXPORT==1) +# define ZSTDLIB_STATIC_API __declspec(dllexport) ZSTDLIB_VISIBLE +# elif defined(ZSTD_DLL_IMPORT) && (ZSTD_DLL_IMPORT==1) +# define ZSTDLIB_STATIC_API __declspec(dllimport) ZSTDLIB_VISIBLE +# else +# define ZSTDLIB_STATIC_API ZSTDLIB_VISIBLE +# endif +#endif + +/**************************************************************************************** + * experimental API (static linking only) + **************************************************************************************** + * The following symbols and constants + * are not planned to join "stable API" status in the near future. + * They can still change in future versions. + * Some of them are planned to remain in the static_only section indefinitely. + * Some of them might be removed in the future (especially when redundant with existing stable functions) + * ***************************************************************************************/ + +#define ZSTD_FRAMEHEADERSIZE_PREFIX(format) ((format) == ZSTD_f_zstd1 ? 5 : 1) /* minimum input size required to query frame header size */ +#define ZSTD_FRAMEHEADERSIZE_MIN(format) ((format) == ZSTD_f_zstd1 ? 6 : 2) +#define ZSTD_FRAMEHEADERSIZE_MAX 18 /* can be useful for static allocation */ +#define ZSTD_SKIPPABLEHEADERSIZE 8 + +/* compression parameter bounds */ +#define ZSTD_WINDOWLOG_MAX_32 30 +#define ZSTD_WINDOWLOG_MAX_64 31 +#define ZSTD_WINDOWLOG_MAX ((int)(sizeof(size_t) == 4 ? ZSTD_WINDOWLOG_MAX_32 : ZSTD_WINDOWLOG_MAX_64)) +#define ZSTD_WINDOWLOG_MIN 10 +#define ZSTD_HASHLOG_MAX ((ZSTD_WINDOWLOG_MAX < 30) ? ZSTD_WINDOWLOG_MAX : 30) +#define ZSTD_HASHLOG_MIN 6 +#define ZSTD_CHAINLOG_MAX_32 29 +#define ZSTD_CHAINLOG_MAX_64 30 +#define ZSTD_CHAINLOG_MAX ((int)(sizeof(size_t) == 4 ? ZSTD_CHAINLOG_MAX_32 : ZSTD_CHAINLOG_MAX_64)) +#define ZSTD_CHAINLOG_MIN ZSTD_HASHLOG_MIN +#define ZSTD_SEARCHLOG_MAX (ZSTD_WINDOWLOG_MAX-1) +#define ZSTD_SEARCHLOG_MIN 1 +#define ZSTD_MINMATCH_MAX 7 /* only for ZSTD_fast, other strategies are limited to 6 */ +#define ZSTD_MINMATCH_MIN 3 /* only for ZSTD_btopt+, faster strategies are limited to 4 */ +#define ZSTD_TARGETLENGTH_MAX ZSTD_BLOCKSIZE_MAX +#define ZSTD_TARGETLENGTH_MIN 0 /* note : comparing this constant to an unsigned results in a tautological test */ +#define ZSTD_STRATEGY_MIN ZSTD_fast +#define ZSTD_STRATEGY_MAX ZSTD_btultra2 +#define ZSTD_BLOCKSIZE_MAX_MIN (1 << 10) /* The minimum valid max blocksize. Maximum blocksizes smaller than this make compressBound() inaccurate. */ + + +#define ZSTD_OVERLAPLOG_MIN 0 +#define ZSTD_OVERLAPLOG_MAX 9 + +#define ZSTD_WINDOWLOG_LIMIT_DEFAULT 27 /* by default, the streaming decoder will refuse any frame + * requiring larger than (1< 0: + * If litLength != 0: + * rep == 1 --> offset == repeat_offset_1 + * rep == 2 --> offset == repeat_offset_2 + * rep == 3 --> offset == repeat_offset_3 + * If litLength == 0: + * rep == 1 --> offset == repeat_offset_2 + * rep == 2 --> offset == repeat_offset_3 + * rep == 3 --> offset == repeat_offset_1 - 1 + * + * Note: This field is optional. ZSTD_generateSequences() will calculate the value of + * 'rep', but repeat offsets do not necessarily need to be calculated from an external + * sequence provider's perspective. For example, ZSTD_compressSequences() does not + * use this 'rep' field at all (as of now). + */ +} ZSTD_Sequence; + +typedef struct { + unsigned windowLog; /**< largest match distance : larger == more compression, more memory needed during decompression */ + unsigned chainLog; /**< fully searched segment : larger == more compression, slower, more memory (useless for fast) */ + unsigned hashLog; /**< dispatch table : larger == faster, more memory */ + unsigned searchLog; /**< nb of searches : larger == more compression, slower */ + unsigned minMatch; /**< match length searched : larger == faster decompression, sometimes less compression */ + unsigned targetLength; /**< acceptable match size for optimal parser (only) : larger == more compression, slower */ + ZSTD_strategy strategy; /**< see ZSTD_strategy definition above */ +} ZSTD_compressionParameters; + +typedef struct { + int contentSizeFlag; /**< 1: content size will be in frame header (when known) */ + int checksumFlag; /**< 1: generate a 32-bits checksum using XXH64 algorithm at end of frame, for error detection */ + int noDictIDFlag; /**< 1: no dictID will be saved into frame header (dictID is only useful for dictionary compression) */ +} ZSTD_frameParameters; + +typedef struct { + ZSTD_compressionParameters cParams; + ZSTD_frameParameters fParams; +} ZSTD_parameters; + +typedef enum { + ZSTD_dct_auto = 0, /* dictionary is "full" when starting with ZSTD_MAGIC_DICTIONARY, otherwise it is "rawContent" */ + ZSTD_dct_rawContent = 1, /* ensures dictionary is always loaded as rawContent, even if it starts with ZSTD_MAGIC_DICTIONARY */ + ZSTD_dct_fullDict = 2 /* refuses to load a dictionary if it does not respect Zstandard's specification, starting with ZSTD_MAGIC_DICTIONARY */ +} ZSTD_dictContentType_e; + +typedef enum { + ZSTD_dlm_byCopy = 0, /**< Copy dictionary content internally */ + ZSTD_dlm_byRef = 1 /**< Reference dictionary content -- the dictionary buffer must outlive its users. */ +} ZSTD_dictLoadMethod_e; + +typedef enum { + ZSTD_f_zstd1 = 0, /* zstd frame format, specified in zstd_compression_format.md (default) */ + ZSTD_f_zstd1_magicless = 1 /* Variant of zstd frame format, without initial 4-bytes magic number. + * Useful to save 4 bytes per generated frame. + * Decoder cannot recognise automatically this format, requiring this instruction. */ +} ZSTD_format_e; + +typedef enum { + /* Note: this enum controls ZSTD_d_forceIgnoreChecksum */ + ZSTD_d_validateChecksum = 0, + ZSTD_d_ignoreChecksum = 1 +} ZSTD_forceIgnoreChecksum_e; + +typedef enum { + /* Note: this enum controls ZSTD_d_refMultipleDDicts */ + ZSTD_rmd_refSingleDDict = 0, + ZSTD_rmd_refMultipleDDicts = 1 +} ZSTD_refMultipleDDicts_e; + +typedef enum { + /* Note: this enum and the behavior it controls are effectively internal + * implementation details of the compressor. They are expected to continue + * to evolve and should be considered only in the context of extremely + * advanced performance tuning. + * + * Zstd currently supports the use of a CDict in three ways: + * + * - The contents of the CDict can be copied into the working context. This + * means that the compression can search both the dictionary and input + * while operating on a single set of internal tables. This makes + * the compression faster per-byte of input. However, the initial copy of + * the CDict's tables incurs a fixed cost at the beginning of the + * compression. For small compressions (< 8 KB), that copy can dominate + * the cost of the compression. + * + * - The CDict's tables can be used in-place. In this model, compression is + * slower per input byte, because the compressor has to search two sets of + * tables. However, this model incurs no start-up cost (as long as the + * working context's tables can be reused). For small inputs, this can be + * faster than copying the CDict's tables. + * + * - The CDict's tables are not used at all, and instead we use the working + * context alone to reload the dictionary and use params based on the source + * size. See ZSTD_compress_insertDictionary() and ZSTD_compress_usingDict(). + * This method is effective when the dictionary sizes are very small relative + * to the input size, and the input size is fairly large to begin with. + * + * Zstd has a simple internal heuristic that selects which strategy to use + * at the beginning of a compression. However, if experimentation shows that + * Zstd is making poor choices, it is possible to override that choice with + * this enum. + */ + ZSTD_dictDefaultAttach = 0, /* Use the default heuristic. */ + ZSTD_dictForceAttach = 1, /* Never copy the dictionary. */ + ZSTD_dictForceCopy = 2, /* Always copy the dictionary. */ + ZSTD_dictForceLoad = 3 /* Always reload the dictionary */ +} ZSTD_dictAttachPref_e; + +typedef enum { + ZSTD_lcm_auto = 0, /**< Automatically determine the compression mode based on the compression level. + * Negative compression levels will be uncompressed, and positive compression + * levels will be compressed. */ + ZSTD_lcm_huffman = 1, /**< Always attempt Huffman compression. Uncompressed literals will still be + * emitted if Huffman compression is not profitable. */ + ZSTD_lcm_uncompressed = 2 /**< Always emit uncompressed literals. */ +} ZSTD_literalCompressionMode_e; + +typedef enum { + /* Note: This enum controls features which are conditionally beneficial. Zstd typically will make a final + * decision on whether or not to enable the feature (ZSTD_ps_auto), but setting the switch to ZSTD_ps_enable + * or ZSTD_ps_disable allow for a force enable/disable the feature. + */ + ZSTD_ps_auto = 0, /* Let the library automatically determine whether the feature shall be enabled */ + ZSTD_ps_enable = 1, /* Force-enable the feature */ + ZSTD_ps_disable = 2 /* Do not use the feature */ +} ZSTD_paramSwitch_e; + +/*************************************** +* Frame header and size functions +***************************************/ + +/*! ZSTD_findDecompressedSize() : + * `src` should point to the start of a series of ZSTD encoded and/or skippable frames + * `srcSize` must be the _exact_ size of this series + * (i.e. there should be a frame boundary at `src + srcSize`) + * @return : - decompressed size of all data in all successive frames + * - if the decompressed size cannot be determined: ZSTD_CONTENTSIZE_UNKNOWN + * - if an error occurred: ZSTD_CONTENTSIZE_ERROR + * + * note 1 : decompressed size is an optional field, that may not be present, especially in streaming mode. + * When `return==ZSTD_CONTENTSIZE_UNKNOWN`, data to decompress could be any size. + * In which case, it's necessary to use streaming mode to decompress data. + * note 2 : decompressed size is always present when compression is done with ZSTD_compress() + * note 3 : decompressed size can be very large (64-bits value), + * potentially larger than what local system can handle as a single memory segment. + * In which case, it's necessary to use streaming mode to decompress data. + * note 4 : If source is untrusted, decompressed size could be wrong or intentionally modified. + * Always ensure result fits within application's authorized limits. + * Each application can set its own limits. + * note 5 : ZSTD_findDecompressedSize handles multiple frames, and so it must traverse the input to + * read each contained frame header. This is fast as most of the data is skipped, + * however it does mean that all frame data must be present and valid. */ +ZSTDLIB_STATIC_API unsigned long long ZSTD_findDecompressedSize(const void* src, size_t srcSize); + +/*! ZSTD_decompressBound() : + * `src` should point to the start of a series of ZSTD encoded and/or skippable frames + * `srcSize` must be the _exact_ size of this series + * (i.e. there should be a frame boundary at `src + srcSize`) + * @return : - upper-bound for the decompressed size of all data in all successive frames + * - if an error occurred: ZSTD_CONTENTSIZE_ERROR + * + * note 1 : an error can occur if `src` contains an invalid or incorrectly formatted frame. + * note 2 : the upper-bound is exact when the decompressed size field is available in every ZSTD encoded frame of `src`. + * in this case, `ZSTD_findDecompressedSize` and `ZSTD_decompressBound` return the same value. + * note 3 : when the decompressed size field isn't available, the upper-bound for that frame is calculated by: + * upper-bound = # blocks * min(128 KB, Window_Size) + */ +ZSTDLIB_STATIC_API unsigned long long ZSTD_decompressBound(const void* src, size_t srcSize); + +/*! ZSTD_frameHeaderSize() : + * srcSize must be >= ZSTD_FRAMEHEADERSIZE_PREFIX. + * @return : size of the Frame Header, + * or an error code (if srcSize is too small) */ +ZSTDLIB_STATIC_API size_t ZSTD_frameHeaderSize(const void* src, size_t srcSize); + +typedef enum { ZSTD_frame, ZSTD_skippableFrame } ZSTD_frameType_e; +typedef struct { + unsigned long long frameContentSize; /* if == ZSTD_CONTENTSIZE_UNKNOWN, it means this field is not available. 0 means "empty" */ + unsigned long long windowSize; /* can be very large, up to <= frameContentSize */ + unsigned blockSizeMax; + ZSTD_frameType_e frameType; /* if == ZSTD_skippableFrame, frameContentSize is the size of skippable content */ + unsigned headerSize; + unsigned dictID; + unsigned checksumFlag; + unsigned _reserved1; + unsigned _reserved2; +} ZSTD_frameHeader; + +/*! ZSTD_getFrameHeader() : + * decode Frame Header, or requires larger `srcSize`. + * @return : 0, `zfhPtr` is correctly filled, + * >0, `srcSize` is too small, value is wanted `srcSize` amount, + * or an error code, which can be tested using ZSTD_isError() */ +ZSTDLIB_STATIC_API size_t ZSTD_getFrameHeader(ZSTD_frameHeader* zfhPtr, const void* src, size_t srcSize); /**< doesn't consume input */ +/*! ZSTD_getFrameHeader_advanced() : + * same as ZSTD_getFrameHeader(), + * with added capability to select a format (like ZSTD_f_zstd1_magicless) */ +ZSTDLIB_STATIC_API size_t ZSTD_getFrameHeader_advanced(ZSTD_frameHeader* zfhPtr, const void* src, size_t srcSize, ZSTD_format_e format); + +/*! ZSTD_decompressionMargin() : + * Zstd supports in-place decompression, where the input and output buffers overlap. + * In this case, the output buffer must be at least (Margin + Output_Size) bytes large, + * and the input buffer must be at the end of the output buffer. + * + * _______________________ Output Buffer ________________________ + * | | + * | ____ Input Buffer ____| + * | | | + * v v v + * |---------------------------------------|-----------|----------| + * ^ ^ ^ + * |___________________ Output_Size ___________________|_ Margin _| + * + * NOTE: See also ZSTD_DECOMPRESSION_MARGIN(). + * NOTE: This applies only to single-pass decompression through ZSTD_decompress() or + * ZSTD_decompressDCtx(). + * NOTE: This function supports multi-frame input. + * + * @param src The compressed frame(s) + * @param srcSize The size of the compressed frame(s) + * @returns The decompression margin or an error that can be checked with ZSTD_isError(). + */ +ZSTDLIB_STATIC_API size_t ZSTD_decompressionMargin(const void* src, size_t srcSize); + +/*! ZSTD_DECOMPRESS_MARGIN() : + * Similar to ZSTD_decompressionMargin(), but instead of computing the margin from + * the compressed frame, compute it from the original size and the blockSizeLog. + * See ZSTD_decompressionMargin() for details. + * + * WARNING: This macro does not support multi-frame input, the input must be a single + * zstd frame. If you need that support use the function, or implement it yourself. + * + * @param originalSize The original uncompressed size of the data. + * @param blockSize The block size == MIN(windowSize, ZSTD_BLOCKSIZE_MAX). + * Unless you explicitly set the windowLog smaller than + * ZSTD_BLOCKSIZELOG_MAX you can just use ZSTD_BLOCKSIZE_MAX. + */ +#define ZSTD_DECOMPRESSION_MARGIN(originalSize, blockSize) ((size_t)( \ + ZSTD_FRAMEHEADERSIZE_MAX /* Frame header */ + \ + 4 /* checksum */ + \ + ((originalSize) == 0 ? 0 : 3 * (((originalSize) + (blockSize) - 1) / blockSize)) /* 3 bytes per block */ + \ + (blockSize) /* One block of margin */ \ + )) + +typedef enum { + ZSTD_sf_noBlockDelimiters = 0, /* Representation of ZSTD_Sequence has no block delimiters, sequences only */ + ZSTD_sf_explicitBlockDelimiters = 1 /* Representation of ZSTD_Sequence contains explicit block delimiters */ +} ZSTD_sequenceFormat_e; + +/*! ZSTD_sequenceBound() : + * `srcSize` : size of the input buffer + * @return : upper-bound for the number of sequences that can be generated + * from a buffer of srcSize bytes + * + * note : returns number of sequences - to get bytes, multiply by sizeof(ZSTD_Sequence). + */ +ZSTDLIB_STATIC_API size_t ZSTD_sequenceBound(size_t srcSize); + +/*! ZSTD_generateSequences() : + * WARNING: This function is meant for debugging and informational purposes ONLY! + * Its implementation is flawed, and it will be deleted in a future version. + * It is not guaranteed to succeed, as there are several cases where it will give + * up and fail. You should NOT use this function in production code. + * + * This function is deprecated, and will be removed in a future version. + * + * Generate sequences using ZSTD_compress2(), given a source buffer. + * + * @param zc The compression context to be used for ZSTD_compress2(). Set any + * compression parameters you need on this context. + * @param outSeqs The output sequences buffer of size @p outSeqsSize + * @param outSeqsSize The size of the output sequences buffer. + * ZSTD_sequenceBound(srcSize) is an upper bound on the number + * of sequences that can be generated. + * @param src The source buffer to generate sequences from of size @p srcSize. + * @param srcSize The size of the source buffer. + * + * Each block will end with a dummy sequence + * with offset == 0, matchLength == 0, and litLength == length of last literals. + * litLength may be == 0, and if so, then the sequence of (of: 0 ml: 0 ll: 0) + * simply acts as a block delimiter. + * + * @returns The number of sequences generated, necessarily less than + * ZSTD_sequenceBound(srcSize), or an error code that can be checked + * with ZSTD_isError(). + */ +ZSTD_DEPRECATED("For debugging only, will be replaced by ZSTD_extractSequences()") +ZSTDLIB_STATIC_API size_t +ZSTD_generateSequences(ZSTD_CCtx* zc, + ZSTD_Sequence* outSeqs, size_t outSeqsSize, + const void* src, size_t srcSize); + +/*! ZSTD_mergeBlockDelimiters() : + * Given an array of ZSTD_Sequence, remove all sequences that represent block delimiters/last literals + * by merging them into the literals of the next sequence. + * + * As such, the final generated result has no explicit representation of block boundaries, + * and the final last literals segment is not represented in the sequences. + * + * The output of this function can be fed into ZSTD_compressSequences() with CCtx + * setting of ZSTD_c_blockDelimiters as ZSTD_sf_noBlockDelimiters + * @return : number of sequences left after merging + */ +ZSTDLIB_STATIC_API size_t ZSTD_mergeBlockDelimiters(ZSTD_Sequence* sequences, size_t seqsSize); + +/*! ZSTD_compressSequences() : + * Compress an array of ZSTD_Sequence, associated with @src buffer, into dst. + * @src contains the entire input (not just the literals). + * If @srcSize > sum(sequence.length), the remaining bytes are considered all literals + * If a dictionary is included, then the cctx should reference the dict. (see: ZSTD_CCtx_refCDict(), ZSTD_CCtx_loadDictionary(), etc.) + * The entire source is compressed into a single frame. + * + * The compression behavior changes based on cctx params. In particular: + * If ZSTD_c_blockDelimiters == ZSTD_sf_noBlockDelimiters, the array of ZSTD_Sequence is expected to contain + * no block delimiters (defined in ZSTD_Sequence). Block boundaries are roughly determined based on + * the block size derived from the cctx, and sequences may be split. This is the default setting. + * + * If ZSTD_c_blockDelimiters == ZSTD_sf_explicitBlockDelimiters, the array of ZSTD_Sequence is expected to contain + * block delimiters (defined in ZSTD_Sequence). Behavior is undefined if no block delimiters are provided. + * + * If ZSTD_c_validateSequences == 0, this function will blindly accept the sequences provided. Invalid sequences cause undefined + * behavior. If ZSTD_c_validateSequences == 1, then if sequence is invalid (see doc/zstd_compression_format.md for + * specifics regarding offset/matchlength requirements) then the function will bail out and return an error. + * + * In addition to the two adjustable experimental params, there are other important cctx params. + * - ZSTD_c_minMatch MUST be set as less than or equal to the smallest match generated by the match finder. It has a minimum value of ZSTD_MINMATCH_MIN. + * - ZSTD_c_compressionLevel accordingly adjusts the strength of the entropy coder, as it would in typical compression. + * - ZSTD_c_windowLog affects offset validation: this function will return an error at higher debug levels if a provided offset + * is larger than what the spec allows for a given window log and dictionary (if present). See: doc/zstd_compression_format.md + * + * Note: Repcodes are, as of now, always re-calculated within this function, so ZSTD_Sequence::rep is unused. + * Note 2: Once we integrate ability to ingest repcodes, the explicit block delims mode must respect those repcodes exactly, + * and cannot emit an RLE block that disagrees with the repcode history + * @return : final compressed size, or a ZSTD error code. + */ +ZSTDLIB_STATIC_API size_t +ZSTD_compressSequences( ZSTD_CCtx* cctx, void* dst, size_t dstSize, + const ZSTD_Sequence* inSeqs, size_t inSeqsSize, + const void* src, size_t srcSize); + + +/*! ZSTD_writeSkippableFrame() : + * Generates a zstd skippable frame containing data given by src, and writes it to dst buffer. + * + * Skippable frames begin with a 4-byte magic number. There are 16 possible choices of magic number, + * ranging from ZSTD_MAGIC_SKIPPABLE_START to ZSTD_MAGIC_SKIPPABLE_START+15. + * As such, the parameter magicVariant controls the exact skippable frame magic number variant used, so + * the magic number used will be ZSTD_MAGIC_SKIPPABLE_START + magicVariant. + * + * Returns an error if destination buffer is not large enough, if the source size is not representable + * with a 4-byte unsigned int, or if the parameter magicVariant is greater than 15 (and therefore invalid). + * + * @return : number of bytes written or a ZSTD error. + */ +ZSTDLIB_STATIC_API size_t ZSTD_writeSkippableFrame(void* dst, size_t dstCapacity, + const void* src, size_t srcSize, unsigned magicVariant); + +/*! ZSTD_readSkippableFrame() : + * Retrieves a zstd skippable frame containing data given by src, and writes it to dst buffer. + * + * The parameter magicVariant will receive the magicVariant that was supplied when the frame was written, + * i.e. magicNumber - ZSTD_MAGIC_SKIPPABLE_START. This can be NULL if the caller is not interested + * in the magicVariant. + * + * Returns an error if destination buffer is not large enough, or if the frame is not skippable. + * + * @return : number of bytes written or a ZSTD error. + */ +ZSTDLIB_API size_t ZSTD_readSkippableFrame(void* dst, size_t dstCapacity, unsigned* magicVariant, + const void* src, size_t srcSize); + +/*! ZSTD_isSkippableFrame() : + * Tells if the content of `buffer` starts with a valid Frame Identifier for a skippable frame. + */ +ZSTDLIB_API unsigned ZSTD_isSkippableFrame(const void* buffer, size_t size); + + + +/*************************************** +* Memory management +***************************************/ + +/*! ZSTD_estimate*() : + * These functions make it possible to estimate memory usage + * of a future {D,C}Ctx, before its creation. + * This is useful in combination with ZSTD_initStatic(), + * which makes it possible to employ a static buffer for ZSTD_CCtx* state. + * + * ZSTD_estimateCCtxSize() will provide a memory budget large enough + * to compress data of any size using one-shot compression ZSTD_compressCCtx() or ZSTD_compress2() + * associated with any compression level up to max specified one. + * The estimate will assume the input may be arbitrarily large, + * which is the worst case. + * + * Note that the size estimation is specific for one-shot compression, + * it is not valid for streaming (see ZSTD_estimateCStreamSize*()) + * nor other potential ways of using a ZSTD_CCtx* state. + * + * When srcSize can be bound by a known and rather "small" value, + * this knowledge can be used to provide a tighter budget estimation + * because the ZSTD_CCtx* state will need less memory for small inputs. + * This tighter estimation can be provided by employing more advanced functions + * ZSTD_estimateCCtxSize_usingCParams(), which can be used in tandem with ZSTD_getCParams(), + * and ZSTD_estimateCCtxSize_usingCCtxParams(), which can be used in tandem with ZSTD_CCtxParams_setParameter(). + * Both can be used to estimate memory using custom compression parameters and arbitrary srcSize limits. + * + * Note : only single-threaded compression is supported. + * ZSTD_estimateCCtxSize_usingCCtxParams() will return an error code if ZSTD_c_nbWorkers is >= 1. + */ +ZSTDLIB_STATIC_API size_t ZSTD_estimateCCtxSize(int maxCompressionLevel); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCCtxSize_usingCParams(ZSTD_compressionParameters cParams); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCCtxSize_usingCCtxParams(const ZSTD_CCtx_params* params); +ZSTDLIB_STATIC_API size_t ZSTD_estimateDCtxSize(void); + +/*! ZSTD_estimateCStreamSize() : + * ZSTD_estimateCStreamSize() will provide a memory budget large enough for streaming compression + * using any compression level up to the max specified one. + * It will also consider src size to be arbitrarily "large", which is a worst case scenario. + * If srcSize is known to always be small, ZSTD_estimateCStreamSize_usingCParams() can provide a tighter estimation. + * ZSTD_estimateCStreamSize_usingCParams() can be used in tandem with ZSTD_getCParams() to create cParams from compressionLevel. + * ZSTD_estimateCStreamSize_usingCCtxParams() can be used in tandem with ZSTD_CCtxParams_setParameter(). Only single-threaded compression is supported. This function will return an error code if ZSTD_c_nbWorkers is >= 1. + * Note : CStream size estimation is only correct for single-threaded compression. + * ZSTD_estimateCStreamSize_usingCCtxParams() will return an error code if ZSTD_c_nbWorkers is >= 1. + * Note 2 : ZSTD_estimateCStreamSize* functions are not compatible with the Block-Level Sequence Producer API at this time. + * Size estimates assume that no external sequence producer is registered. + * + * ZSTD_DStream memory budget depends on frame's window Size. + * This information can be passed manually, using ZSTD_estimateDStreamSize, + * or deducted from a valid frame Header, using ZSTD_estimateDStreamSize_fromFrame(); + * Any frame requesting a window size larger than max specified one will be rejected. + * Note : if streaming is init with function ZSTD_init?Stream_usingDict(), + * an internal ?Dict will be created, which additional size is not estimated here. + * In this case, get total size by adding ZSTD_estimate?DictSize + */ +ZSTDLIB_STATIC_API size_t ZSTD_estimateCStreamSize(int maxCompressionLevel); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCStreamSize_usingCParams(ZSTD_compressionParameters cParams); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCStreamSize_usingCCtxParams(const ZSTD_CCtx_params* params); +ZSTDLIB_STATIC_API size_t ZSTD_estimateDStreamSize(size_t maxWindowSize); +ZSTDLIB_STATIC_API size_t ZSTD_estimateDStreamSize_fromFrame(const void* src, size_t srcSize); + +/*! ZSTD_estimate?DictSize() : + * ZSTD_estimateCDictSize() will bet that src size is relatively "small", and content is copied, like ZSTD_createCDict(). + * ZSTD_estimateCDictSize_advanced() makes it possible to control compression parameters precisely, like ZSTD_createCDict_advanced(). + * Note : dictionaries created by reference (`ZSTD_dlm_byRef`) are logically smaller. + */ +ZSTDLIB_STATIC_API size_t ZSTD_estimateCDictSize(size_t dictSize, int compressionLevel); +ZSTDLIB_STATIC_API size_t ZSTD_estimateCDictSize_advanced(size_t dictSize, ZSTD_compressionParameters cParams, ZSTD_dictLoadMethod_e dictLoadMethod); +ZSTDLIB_STATIC_API size_t ZSTD_estimateDDictSize(size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod); + +/*! ZSTD_initStatic*() : + * Initialize an object using a pre-allocated fixed-size buffer. + * workspace: The memory area to emplace the object into. + * Provided pointer *must be 8-bytes aligned*. + * Buffer must outlive object. + * workspaceSize: Use ZSTD_estimate*Size() to determine + * how large workspace must be to support target scenario. + * @return : pointer to object (same address as workspace, just different type), + * or NULL if error (size too small, incorrect alignment, etc.) + * Note : zstd will never resize nor malloc() when using a static buffer. + * If the object requires more memory than available, + * zstd will just error out (typically ZSTD_error_memory_allocation). + * Note 2 : there is no corresponding "free" function. + * Since workspace is allocated externally, it must be freed externally too. + * Note 3 : cParams : use ZSTD_getCParams() to convert a compression level + * into its associated cParams. + * Limitation 1 : currently not compatible with internal dictionary creation, triggered by + * ZSTD_CCtx_loadDictionary(), ZSTD_initCStream_usingDict() or ZSTD_initDStream_usingDict(). + * Limitation 2 : static cctx currently not compatible with multi-threading. + * Limitation 3 : static dctx is incompatible with legacy support. + */ +ZSTDLIB_STATIC_API ZSTD_CCtx* ZSTD_initStaticCCtx(void* workspace, size_t workspaceSize); +ZSTDLIB_STATIC_API ZSTD_CStream* ZSTD_initStaticCStream(void* workspace, size_t workspaceSize); /**< same as ZSTD_initStaticCCtx() */ + +ZSTDLIB_STATIC_API ZSTD_DCtx* ZSTD_initStaticDCtx(void* workspace, size_t workspaceSize); +ZSTDLIB_STATIC_API ZSTD_DStream* ZSTD_initStaticDStream(void* workspace, size_t workspaceSize); /**< same as ZSTD_initStaticDCtx() */ + +ZSTDLIB_STATIC_API const ZSTD_CDict* ZSTD_initStaticCDict( + void* workspace, size_t workspaceSize, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_compressionParameters cParams); + +ZSTDLIB_STATIC_API const ZSTD_DDict* ZSTD_initStaticDDict( + void* workspace, size_t workspaceSize, + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType); + + +/*! Custom memory allocation : + * These prototypes make it possible to pass your own allocation/free functions. + * ZSTD_customMem is provided at creation time, using ZSTD_create*_advanced() variants listed below. + * All allocation/free operations will be completed using these custom variants instead of regular ones. + */ +typedef void* (*ZSTD_allocFunction) (void* opaque, size_t size); +typedef void (*ZSTD_freeFunction) (void* opaque, void* address); +typedef struct { ZSTD_allocFunction customAlloc; ZSTD_freeFunction customFree; void* opaque; } ZSTD_customMem; +static +#ifdef __GNUC__ +__attribute__((__unused__)) +#endif +ZSTD_customMem const ZSTD_defaultCMem = { NULL, NULL, NULL }; /**< this constant defers to stdlib's functions */ + +ZSTDLIB_STATIC_API ZSTD_CCtx* ZSTD_createCCtx_advanced(ZSTD_customMem customMem); +ZSTDLIB_STATIC_API ZSTD_CStream* ZSTD_createCStream_advanced(ZSTD_customMem customMem); +ZSTDLIB_STATIC_API ZSTD_DCtx* ZSTD_createDCtx_advanced(ZSTD_customMem customMem); +ZSTDLIB_STATIC_API ZSTD_DStream* ZSTD_createDStream_advanced(ZSTD_customMem customMem); + +ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_advanced(const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_compressionParameters cParams, + ZSTD_customMem customMem); + +/*! Thread pool : + * These prototypes make it possible to share a thread pool among multiple compression contexts. + * This can limit resources for applications with multiple threads where each one uses + * a threaded compression mode (via ZSTD_c_nbWorkers parameter). + * ZSTD_createThreadPool creates a new thread pool with a given number of threads. + * Note that the lifetime of such pool must exist while being used. + * ZSTD_CCtx_refThreadPool assigns a thread pool to a context (use NULL argument value + * to use an internal thread pool). + * ZSTD_freeThreadPool frees a thread pool, accepts NULL pointer. + */ +typedef struct POOL_ctx_s ZSTD_threadPool; +ZSTDLIB_STATIC_API ZSTD_threadPool* ZSTD_createThreadPool(size_t numThreads); +ZSTDLIB_STATIC_API void ZSTD_freeThreadPool (ZSTD_threadPool* pool); /* accept NULL pointer */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_refThreadPool(ZSTD_CCtx* cctx, ZSTD_threadPool* pool); + + +/* + * This API is temporary and is expected to change or disappear in the future! + */ +ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_advanced2( + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + const ZSTD_CCtx_params* cctxParams, + ZSTD_customMem customMem); + +ZSTDLIB_STATIC_API ZSTD_DDict* ZSTD_createDDict_advanced( + const void* dict, size_t dictSize, + ZSTD_dictLoadMethod_e dictLoadMethod, + ZSTD_dictContentType_e dictContentType, + ZSTD_customMem customMem); + + +/*************************************** +* Advanced compression functions +***************************************/ + +/*! ZSTD_createCDict_byReference() : + * Create a digested dictionary for compression + * Dictionary content is just referenced, not duplicated. + * As a consequence, `dictBuffer` **must** outlive CDict, + * and its content must remain unmodified throughout the lifetime of CDict. + * note: equivalent to ZSTD_createCDict_advanced(), with dictLoadMethod==ZSTD_dlm_byRef */ +ZSTDLIB_STATIC_API ZSTD_CDict* ZSTD_createCDict_byReference(const void* dictBuffer, size_t dictSize, int compressionLevel); + +/*! ZSTD_getCParams() : + * @return ZSTD_compressionParameters structure for a selected compression level and estimated srcSize. + * `estimatedSrcSize` value is optional, select 0 if not known */ +ZSTDLIB_STATIC_API ZSTD_compressionParameters ZSTD_getCParams(int compressionLevel, unsigned long long estimatedSrcSize, size_t dictSize); + +/*! ZSTD_getParams() : + * same as ZSTD_getCParams(), but @return a full `ZSTD_parameters` object instead of sub-component `ZSTD_compressionParameters`. + * All fields of `ZSTD_frameParameters` are set to default : contentSize=1, checksum=0, noDictID=0 */ +ZSTDLIB_STATIC_API ZSTD_parameters ZSTD_getParams(int compressionLevel, unsigned long long estimatedSrcSize, size_t dictSize); + +/*! ZSTD_checkCParams() : + * Ensure param values remain within authorized range. + * @return 0 on success, or an error code (can be checked with ZSTD_isError()) */ +ZSTDLIB_STATIC_API size_t ZSTD_checkCParams(ZSTD_compressionParameters params); + +/*! ZSTD_adjustCParams() : + * optimize params for a given `srcSize` and `dictSize`. + * `srcSize` can be unknown, in which case use ZSTD_CONTENTSIZE_UNKNOWN. + * `dictSize` must be `0` when there is no dictionary. + * cPar can be invalid : all parameters will be clamped within valid range in the @return struct. + * This function never fails (wide contract) */ +ZSTDLIB_STATIC_API ZSTD_compressionParameters ZSTD_adjustCParams(ZSTD_compressionParameters cPar, unsigned long long srcSize, size_t dictSize); + +/*! ZSTD_CCtx_setCParams() : + * Set all parameters provided within @p cparams into the working @p cctx. + * Note : if modifying parameters during compression (MT mode only), + * note that changes to the .windowLog parameter will be ignored. + * @return 0 on success, or an error code (can be checked with ZSTD_isError()). + * On failure, no parameters are updated. + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_setCParams(ZSTD_CCtx* cctx, ZSTD_compressionParameters cparams); + +/*! ZSTD_CCtx_setFParams() : + * Set all parameters provided within @p fparams into the working @p cctx. + * @return 0 on success, or an error code (can be checked with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_setFParams(ZSTD_CCtx* cctx, ZSTD_frameParameters fparams); + +/*! ZSTD_CCtx_setParams() : + * Set all parameters provided within @p params into the working @p cctx. + * @return 0 on success, or an error code (can be checked with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_setParams(ZSTD_CCtx* cctx, ZSTD_parameters params); + +/*! ZSTD_compress_advanced() : + * Note : this function is now DEPRECATED. + * It can be replaced by ZSTD_compress2(), in combination with ZSTD_CCtx_setParameter() and other parameter setters. + * This prototype will generate compilation warnings. */ +ZSTD_DEPRECATED("use ZSTD_compress2") +ZSTDLIB_STATIC_API +size_t ZSTD_compress_advanced(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const void* dict,size_t dictSize, + ZSTD_parameters params); + +/*! ZSTD_compress_usingCDict_advanced() : + * Note : this function is now DEPRECATED. + * It can be replaced by ZSTD_compress2(), in combination with ZSTD_CCtx_loadDictionary() and other parameter setters. + * This prototype will generate compilation warnings. */ +ZSTD_DEPRECATED("use ZSTD_compress2 with ZSTD_CCtx_loadDictionary") +ZSTDLIB_STATIC_API +size_t ZSTD_compress_usingCDict_advanced(ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + const ZSTD_CDict* cdict, + ZSTD_frameParameters fParams); + + +/*! ZSTD_CCtx_loadDictionary_byReference() : + * Same as ZSTD_CCtx_loadDictionary(), but dictionary content is referenced, instead of being copied into CCtx. + * It saves some memory, but also requires that `dict` outlives its usage within `cctx` */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_loadDictionary_byReference(ZSTD_CCtx* cctx, const void* dict, size_t dictSize); + +/*! ZSTD_CCtx_loadDictionary_advanced() : + * Same as ZSTD_CCtx_loadDictionary(), but gives finer control over + * how to load the dictionary (by copy ? by reference ?) + * and how to interpret it (automatic ? force raw mode ? full mode only ?) */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_loadDictionary_advanced(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod, ZSTD_dictContentType_e dictContentType); + +/*! ZSTD_CCtx_refPrefix_advanced() : + * Same as ZSTD_CCtx_refPrefix(), but gives finer control over + * how to interpret prefix content (automatic ? force raw mode (default) ? full mode only ?) */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_refPrefix_advanced(ZSTD_CCtx* cctx, const void* prefix, size_t prefixSize, ZSTD_dictContentType_e dictContentType); + +/* === experimental parameters === */ +/* these parameters can be used with ZSTD_setParameter() + * they are not guaranteed to remain supported in the future */ + + /* Enables rsyncable mode, + * which makes compressed files more rsync friendly + * by adding periodic synchronization points to the compressed data. + * The target average block size is ZSTD_c_jobSize / 2. + * It's possible to modify the job size to increase or decrease + * the granularity of the synchronization point. + * Once the jobSize is smaller than the window size, + * it will result in compression ratio degradation. + * NOTE 1: rsyncable mode only works when multithreading is enabled. + * NOTE 2: rsyncable performs poorly in combination with long range mode, + * since it will decrease the effectiveness of synchronization points, + * though mileage may vary. + * NOTE 3: Rsyncable mode limits maximum compression speed to ~400 MB/s. + * If the selected compression level is already running significantly slower, + * the overall speed won't be significantly impacted. + */ + #define ZSTD_c_rsyncable ZSTD_c_experimentalParam1 + +/* Select a compression format. + * The value must be of type ZSTD_format_e. + * See ZSTD_format_e enum definition for details */ +#define ZSTD_c_format ZSTD_c_experimentalParam2 + +/* Force back-reference distances to remain < windowSize, + * even when referencing into Dictionary content (default:0) */ +#define ZSTD_c_forceMaxWindow ZSTD_c_experimentalParam3 + +/* Controls whether the contents of a CDict + * are used in place, or copied into the working context. + * Accepts values from the ZSTD_dictAttachPref_e enum. + * See the comments on that enum for an explanation of the feature. */ +#define ZSTD_c_forceAttachDict ZSTD_c_experimentalParam4 + +/* Controlled with ZSTD_paramSwitch_e enum. + * Default is ZSTD_ps_auto. + * Set to ZSTD_ps_disable to never compress literals. + * Set to ZSTD_ps_enable to always compress literals. (Note: uncompressed literals + * may still be emitted if huffman is not beneficial to use.) + * + * By default, in ZSTD_ps_auto, the library will decide at runtime whether to use + * literals compression based on the compression parameters - specifically, + * negative compression levels do not use literal compression. + */ +#define ZSTD_c_literalCompressionMode ZSTD_c_experimentalParam5 + +/* User's best guess of source size. + * Hint is not valid when srcSizeHint == 0. + * There is no guarantee that hint is close to actual source size, + * but compression ratio may regress significantly if guess considerably underestimates */ +#define ZSTD_c_srcSizeHint ZSTD_c_experimentalParam7 + +/* Controls whether the new and experimental "dedicated dictionary search + * structure" can be used. This feature is still rough around the edges, be + * prepared for surprising behavior! + * + * How to use it: + * + * When using a CDict, whether to use this feature or not is controlled at + * CDict creation, and it must be set in a CCtxParams set passed into that + * construction (via ZSTD_createCDict_advanced2()). A compression will then + * use the feature or not based on how the CDict was constructed; the value of + * this param, set in the CCtx, will have no effect. + * + * However, when a dictionary buffer is passed into a CCtx, such as via + * ZSTD_CCtx_loadDictionary(), this param can be set on the CCtx to control + * whether the CDict that is created internally can use the feature or not. + * + * What it does: + * + * Normally, the internal data structures of the CDict are analogous to what + * would be stored in a CCtx after compressing the contents of a dictionary. + * To an approximation, a compression using a dictionary can then use those + * data structures to simply continue what is effectively a streaming + * compression where the simulated compression of the dictionary left off. + * Which is to say, the search structures in the CDict are normally the same + * format as in the CCtx. + * + * It is possible to do better, since the CDict is not like a CCtx: the search + * structures are written once during CDict creation, and then are only read + * after that, while the search structures in the CCtx are both read and + * written as the compression goes along. This means we can choose a search + * structure for the dictionary that is read-optimized. + * + * This feature enables the use of that different structure. + * + * Note that some of the members of the ZSTD_compressionParameters struct have + * different semantics and constraints in the dedicated search structure. It is + * highly recommended that you simply set a compression level in the CCtxParams + * you pass into the CDict creation call, and avoid messing with the cParams + * directly. + * + * Effects: + * + * This will only have any effect when the selected ZSTD_strategy + * implementation supports this feature. Currently, that's limited to + * ZSTD_greedy, ZSTD_lazy, and ZSTD_lazy2. + * + * Note that this means that the CDict tables can no longer be copied into the + * CCtx, so the dict attachment mode ZSTD_dictForceCopy will no longer be + * usable. The dictionary can only be attached or reloaded. + * + * In general, you should expect compression to be faster--sometimes very much + * so--and CDict creation to be slightly slower. Eventually, we will probably + * make this mode the default. + */ +#define ZSTD_c_enableDedicatedDictSearch ZSTD_c_experimentalParam8 + +/* ZSTD_c_stableInBuffer + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable. + * + * Tells the compressor that input data presented with ZSTD_inBuffer + * will ALWAYS be the same between calls. + * Technically, the @src pointer must never be changed, + * and the @pos field can only be updated by zstd. + * However, it's possible to increase the @size field, + * allowing scenarios where more data can be appended after compressions starts. + * These conditions are checked by the compressor, + * and compression will fail if they are not respected. + * Also, data in the ZSTD_inBuffer within the range [src, src + pos) + * MUST not be modified during compression or it will result in data corruption. + * + * When this flag is enabled zstd won't allocate an input window buffer, + * because the user guarantees it can reference the ZSTD_inBuffer until + * the frame is complete. But, it will still allocate an output buffer + * large enough to fit a block (see ZSTD_c_stableOutBuffer). This will also + * avoid the memcpy() from the input buffer to the input window buffer. + * + * NOTE: So long as the ZSTD_inBuffer always points to valid memory, using + * this flag is ALWAYS memory safe, and will never access out-of-bounds + * memory. However, compression WILL fail if conditions are not respected. + * + * WARNING: The data in the ZSTD_inBuffer in the range [src, src + pos) MUST + * not be modified during compression or it will result in data corruption. + * This is because zstd needs to reference data in the ZSTD_inBuffer to find + * matches. Normally zstd maintains its own window buffer for this purpose, + * but passing this flag tells zstd to rely on user provided buffer instead. + */ +#define ZSTD_c_stableInBuffer ZSTD_c_experimentalParam9 + +/* ZSTD_c_stableOutBuffer + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable. + * + * Tells he compressor that the ZSTD_outBuffer will not be resized between + * calls. Specifically: (out.size - out.pos) will never grow. This gives the + * compressor the freedom to say: If the compressed data doesn't fit in the + * output buffer then return ZSTD_error_dstSizeTooSmall. This allows us to + * always decompress directly into the output buffer, instead of decompressing + * into an internal buffer and copying to the output buffer. + * + * When this flag is enabled zstd won't allocate an output buffer, because + * it can write directly to the ZSTD_outBuffer. It will still allocate the + * input window buffer (see ZSTD_c_stableInBuffer). + * + * Zstd will check that (out.size - out.pos) never grows and return an error + * if it does. While not strictly necessary, this should prevent surprises. + */ +#define ZSTD_c_stableOutBuffer ZSTD_c_experimentalParam10 + +/* ZSTD_c_blockDelimiters + * Default is 0 == ZSTD_sf_noBlockDelimiters. + * + * For use with sequence compression API: ZSTD_compressSequences(). + * + * Designates whether or not the given array of ZSTD_Sequence contains block delimiters + * and last literals, which are defined as sequences with offset == 0 and matchLength == 0. + * See the definition of ZSTD_Sequence for more specifics. + */ +#define ZSTD_c_blockDelimiters ZSTD_c_experimentalParam11 + +/* ZSTD_c_validateSequences + * Default is 0 == disabled. Set to 1 to enable sequence validation. + * + * For use with sequence compression API: ZSTD_compressSequences(). + * Designates whether or not we validate sequences provided to ZSTD_compressSequences() + * during function execution. + * + * Without validation, providing a sequence that does not conform to the zstd spec will cause + * undefined behavior, and may produce a corrupted block. + * + * With validation enabled, if sequence is invalid (see doc/zstd_compression_format.md for + * specifics regarding offset/matchlength requirements) then the function will bail out and + * return an error. + * + */ +#define ZSTD_c_validateSequences ZSTD_c_experimentalParam12 + +/* ZSTD_c_useBlockSplitter + * Controlled with ZSTD_paramSwitch_e enum. + * Default is ZSTD_ps_auto. + * Set to ZSTD_ps_disable to never use block splitter. + * Set to ZSTD_ps_enable to always use block splitter. + * + * By default, in ZSTD_ps_auto, the library will decide at runtime whether to use + * block splitting based on the compression parameters. + */ +#define ZSTD_c_useBlockSplitter ZSTD_c_experimentalParam13 + +/* ZSTD_c_useRowMatchFinder + * Controlled with ZSTD_paramSwitch_e enum. + * Default is ZSTD_ps_auto. + * Set to ZSTD_ps_disable to never use row-based matchfinder. + * Set to ZSTD_ps_enable to force usage of row-based matchfinder. + * + * By default, in ZSTD_ps_auto, the library will decide at runtime whether to use + * the row-based matchfinder based on support for SIMD instructions and the window log. + * Note that this only pertains to compression strategies: greedy, lazy, and lazy2 + */ +#define ZSTD_c_useRowMatchFinder ZSTD_c_experimentalParam14 + +/* ZSTD_c_deterministicRefPrefix + * Default is 0 == disabled. Set to 1 to enable. + * + * Zstd produces different results for prefix compression when the prefix is + * directly adjacent to the data about to be compressed vs. when it isn't. + * This is because zstd detects that the two buffers are contiguous and it can + * use a more efficient match finding algorithm. However, this produces different + * results than when the two buffers are non-contiguous. This flag forces zstd + * to always load the prefix in non-contiguous mode, even if it happens to be + * adjacent to the data, to guarantee determinism. + * + * If you really care about determinism when using a dictionary or prefix, + * like when doing delta compression, you should select this option. It comes + * at a speed penalty of about ~2.5% if the dictionary and data happened to be + * contiguous, and is free if they weren't contiguous. We don't expect that + * intentionally making the dictionary and data contiguous will be worth the + * cost to memcpy() the data. + */ +#define ZSTD_c_deterministicRefPrefix ZSTD_c_experimentalParam15 + +/* ZSTD_c_prefetchCDictTables + * Controlled with ZSTD_paramSwitch_e enum. Default is ZSTD_ps_auto. + * + * In some situations, zstd uses CDict tables in-place rather than copying them + * into the working context. (See docs on ZSTD_dictAttachPref_e above for details). + * In such situations, compression speed is seriously impacted when CDict tables are + * "cold" (outside CPU cache). This parameter instructs zstd to prefetch CDict tables + * when they are used in-place. + * + * For sufficiently small inputs, the cost of the prefetch will outweigh the benefit. + * For sufficiently large inputs, zstd will by default memcpy() CDict tables + * into the working context, so there is no need to prefetch. This parameter is + * targeted at a middle range of input sizes, where a prefetch is cheap enough to be + * useful but memcpy() is too expensive. The exact range of input sizes where this + * makes sense is best determined by careful experimentation. + * + * Note: for this parameter, ZSTD_ps_auto is currently equivalent to ZSTD_ps_disable, + * but in the future zstd may conditionally enable this feature via an auto-detection + * heuristic for cold CDicts. + * Use ZSTD_ps_disable to opt out of prefetching under any circumstances. + */ +#define ZSTD_c_prefetchCDictTables ZSTD_c_experimentalParam16 + +/* ZSTD_c_enableSeqProducerFallback + * Allowed values are 0 (disable) and 1 (enable). The default setting is 0. + * + * Controls whether zstd will fall back to an internal sequence producer if an + * external sequence producer is registered and returns an error code. This fallback + * is block-by-block: the internal sequence producer will only be called for blocks + * where the external sequence producer returns an error code. Fallback parsing will + * follow any other cParam settings, such as compression level, the same as in a + * normal (fully-internal) compression operation. + * + * The user is strongly encouraged to read the full Block-Level Sequence Producer API + * documentation (below) before setting this parameter. */ +#define ZSTD_c_enableSeqProducerFallback ZSTD_c_experimentalParam17 + +/* ZSTD_c_maxBlockSize + * Allowed values are between 1KB and ZSTD_BLOCKSIZE_MAX (128KB). + * The default is ZSTD_BLOCKSIZE_MAX, and setting to 0 will set to the default. + * + * This parameter can be used to set an upper bound on the blocksize + * that overrides the default ZSTD_BLOCKSIZE_MAX. It cannot be used to set upper + * bounds greater than ZSTD_BLOCKSIZE_MAX or bounds lower than 1KB (will make + * compressBound() inaccurate). Only currently meant to be used for testing. + * + */ +#define ZSTD_c_maxBlockSize ZSTD_c_experimentalParam18 + +/* ZSTD_c_searchForExternalRepcodes + * This parameter affects how zstd parses external sequences, such as sequences + * provided through the compressSequences() API or from an external block-level + * sequence producer. + * + * If set to ZSTD_ps_enable, the library will check for repeated offsets in + * external sequences, even if those repcodes are not explicitly indicated in + * the "rep" field. Note that this is the only way to exploit repcode matches + * while using compressSequences() or an external sequence producer, since zstd + * currently ignores the "rep" field of external sequences. + * + * If set to ZSTD_ps_disable, the library will not exploit repeated offsets in + * external sequences, regardless of whether the "rep" field has been set. This + * reduces sequence compression overhead by about 25% while sacrificing some + * compression ratio. + * + * The default value is ZSTD_ps_auto, for which the library will enable/disable + * based on compression level. + * + * Note: for now, this param only has an effect if ZSTD_c_blockDelimiters is + * set to ZSTD_sf_explicitBlockDelimiters. That may change in the future. + */ +#define ZSTD_c_searchForExternalRepcodes ZSTD_c_experimentalParam19 + +/*! ZSTD_CCtx_getParameter() : + * Get the requested compression parameter value, selected by enum ZSTD_cParameter, + * and store it into int* value. + * @return : 0, or an error code (which can be tested with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_getParameter(const ZSTD_CCtx* cctx, ZSTD_cParameter param, int* value); + + +/*! ZSTD_CCtx_params : + * Quick howto : + * - ZSTD_createCCtxParams() : Create a ZSTD_CCtx_params structure + * - ZSTD_CCtxParams_setParameter() : Push parameters one by one into + * an existing ZSTD_CCtx_params structure. + * This is similar to + * ZSTD_CCtx_setParameter(). + * - ZSTD_CCtx_setParametersUsingCCtxParams() : Apply parameters to + * an existing CCtx. + * These parameters will be applied to + * all subsequent frames. + * - ZSTD_compressStream2() : Do compression using the CCtx. + * - ZSTD_freeCCtxParams() : Free the memory, accept NULL pointer. + * + * This can be used with ZSTD_estimateCCtxSize_advanced_usingCCtxParams() + * for static allocation of CCtx for single-threaded compression. + */ +ZSTDLIB_STATIC_API ZSTD_CCtx_params* ZSTD_createCCtxParams(void); +ZSTDLIB_STATIC_API size_t ZSTD_freeCCtxParams(ZSTD_CCtx_params* params); /* accept NULL pointer */ + +/*! ZSTD_CCtxParams_reset() : + * Reset params to default values. + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_reset(ZSTD_CCtx_params* params); + +/*! ZSTD_CCtxParams_init() : + * Initializes the compression parameters of cctxParams according to + * compression level. All other parameters are reset to their default values. + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_init(ZSTD_CCtx_params* cctxParams, int compressionLevel); + +/*! ZSTD_CCtxParams_init_advanced() : + * Initializes the compression and frame parameters of cctxParams according to + * params. All other parameters are reset to their default values. + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_init_advanced(ZSTD_CCtx_params* cctxParams, ZSTD_parameters params); + +/*! ZSTD_CCtxParams_setParameter() : Requires v1.4.0+ + * Similar to ZSTD_CCtx_setParameter. + * Set one compression parameter, selected by enum ZSTD_cParameter. + * Parameters must be applied to a ZSTD_CCtx using + * ZSTD_CCtx_setParametersUsingCCtxParams(). + * @result : a code representing success or failure (which can be tested with + * ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_setParameter(ZSTD_CCtx_params* params, ZSTD_cParameter param, int value); + +/*! ZSTD_CCtxParams_getParameter() : + * Similar to ZSTD_CCtx_getParameter. + * Get the requested value of one compression parameter, selected by enum ZSTD_cParameter. + * @result : 0, or an error code (which can be tested with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtxParams_getParameter(const ZSTD_CCtx_params* params, ZSTD_cParameter param, int* value); + +/*! ZSTD_CCtx_setParametersUsingCCtxParams() : + * Apply a set of ZSTD_CCtx_params to the compression context. + * This can be done even after compression is started, + * if nbWorkers==0, this will have no impact until a new compression is started. + * if nbWorkers>=1, new parameters will be picked up at next job, + * with a few restrictions (windowLog, pledgedSrcSize, nbWorkers, jobSize, and overlapLog are not updated). + */ +ZSTDLIB_STATIC_API size_t ZSTD_CCtx_setParametersUsingCCtxParams( + ZSTD_CCtx* cctx, const ZSTD_CCtx_params* params); + +/*! ZSTD_compressStream2_simpleArgs() : + * Same as ZSTD_compressStream2(), + * but using only integral types as arguments. + * This variant might be helpful for binders from dynamic languages + * which have troubles handling structures containing memory pointers. + */ +ZSTDLIB_STATIC_API size_t ZSTD_compressStream2_simpleArgs ( + ZSTD_CCtx* cctx, + void* dst, size_t dstCapacity, size_t* dstPos, + const void* src, size_t srcSize, size_t* srcPos, + ZSTD_EndDirective endOp); + + +/*************************************** +* Advanced decompression functions +***************************************/ + +/*! ZSTD_isFrame() : + * Tells if the content of `buffer` starts with a valid Frame Identifier. + * Note : Frame Identifier is 4 bytes. If `size < 4`, @return will always be 0. + * Note 2 : Legacy Frame Identifiers are considered valid only if Legacy Support is enabled. + * Note 3 : Skippable Frame Identifiers are considered valid. */ +ZSTDLIB_STATIC_API unsigned ZSTD_isFrame(const void* buffer, size_t size); + +/*! ZSTD_createDDict_byReference() : + * Create a digested dictionary, ready to start decompression operation without startup delay. + * Dictionary content is referenced, and therefore stays in dictBuffer. + * It is important that dictBuffer outlives DDict, + * it must remain read accessible throughout the lifetime of DDict */ +ZSTDLIB_STATIC_API ZSTD_DDict* ZSTD_createDDict_byReference(const void* dictBuffer, size_t dictSize); + +/*! ZSTD_DCtx_loadDictionary_byReference() : + * Same as ZSTD_DCtx_loadDictionary(), + * but references `dict` content instead of copying it into `dctx`. + * This saves memory if `dict` remains around., + * However, it's imperative that `dict` remains accessible (and unmodified) while being used, so it must outlive decompression. */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_loadDictionary_byReference(ZSTD_DCtx* dctx, const void* dict, size_t dictSize); + +/*! ZSTD_DCtx_loadDictionary_advanced() : + * Same as ZSTD_DCtx_loadDictionary(), + * but gives direct control over + * how to load the dictionary (by copy ? by reference ?) + * and how to interpret it (automatic ? force raw mode ? full mode only ?). */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_loadDictionary_advanced(ZSTD_DCtx* dctx, const void* dict, size_t dictSize, ZSTD_dictLoadMethod_e dictLoadMethod, ZSTD_dictContentType_e dictContentType); + +/*! ZSTD_DCtx_refPrefix_advanced() : + * Same as ZSTD_DCtx_refPrefix(), but gives finer control over + * how to interpret prefix content (automatic ? force raw mode (default) ? full mode only ?) */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_refPrefix_advanced(ZSTD_DCtx* dctx, const void* prefix, size_t prefixSize, ZSTD_dictContentType_e dictContentType); + +/*! ZSTD_DCtx_setMaxWindowSize() : + * Refuses allocating internal buffers for frames requiring a window size larger than provided limit. + * This protects a decoder context from reserving too much memory for itself (potential attack scenario). + * This parameter is only useful in streaming mode, since no internal buffer is allocated in single-pass mode. + * By default, a decompression context accepts all window sizes <= (1 << ZSTD_WINDOWLOG_LIMIT_DEFAULT) + * @return : 0, or an error code (which can be tested using ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_setMaxWindowSize(ZSTD_DCtx* dctx, size_t maxWindowSize); + +/*! ZSTD_DCtx_getParameter() : + * Get the requested decompression parameter value, selected by enum ZSTD_dParameter, + * and store it into int* value. + * @return : 0, or an error code (which can be tested with ZSTD_isError()). + */ +ZSTDLIB_STATIC_API size_t ZSTD_DCtx_getParameter(ZSTD_DCtx* dctx, ZSTD_dParameter param, int* value); + +/* ZSTD_d_format + * experimental parameter, + * allowing selection between ZSTD_format_e input compression formats + */ +#define ZSTD_d_format ZSTD_d_experimentalParam1 +/* ZSTD_d_stableOutBuffer + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable. + * + * Tells the decompressor that the ZSTD_outBuffer will ALWAYS be the same + * between calls, except for the modifications that zstd makes to pos (the + * caller must not modify pos). This is checked by the decompressor, and + * decompression will fail if it ever changes. Therefore the ZSTD_outBuffer + * MUST be large enough to fit the entire decompressed frame. This will be + * checked when the frame content size is known. The data in the ZSTD_outBuffer + * in the range [dst, dst + pos) MUST not be modified during decompression + * or you will get data corruption. + * + * When this flag is enabled zstd won't allocate an output buffer, because + * it can write directly to the ZSTD_outBuffer, but it will still allocate + * an input buffer large enough to fit any compressed block. This will also + * avoid the memcpy() from the internal output buffer to the ZSTD_outBuffer. + * If you need to avoid the input buffer allocation use the buffer-less + * streaming API. + * + * NOTE: So long as the ZSTD_outBuffer always points to valid memory, using + * this flag is ALWAYS memory safe, and will never access out-of-bounds + * memory. However, decompression WILL fail if you violate the preconditions. + * + * WARNING: The data in the ZSTD_outBuffer in the range [dst, dst + pos) MUST + * not be modified during decompression or you will get data corruption. This + * is because zstd needs to reference data in the ZSTD_outBuffer to regenerate + * matches. Normally zstd maintains its own buffer for this purpose, but passing + * this flag tells zstd to use the user provided buffer. + */ +#define ZSTD_d_stableOutBuffer ZSTD_d_experimentalParam2 + +/* ZSTD_d_forceIgnoreChecksum + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable + * + * Tells the decompressor to skip checksum validation during decompression, regardless + * of whether checksumming was specified during compression. This offers some + * slight performance benefits, and may be useful for debugging. + * Param has values of type ZSTD_forceIgnoreChecksum_e + */ +#define ZSTD_d_forceIgnoreChecksum ZSTD_d_experimentalParam3 + +/* ZSTD_d_refMultipleDDicts + * Experimental parameter. + * Default is 0 == disabled. Set to 1 to enable + * + * If enabled and dctx is allocated on the heap, then additional memory will be allocated + * to store references to multiple ZSTD_DDict. That is, multiple calls of ZSTD_refDDict() + * using a given ZSTD_DCtx, rather than overwriting the previous DDict reference, will instead + * store all references. At decompression time, the appropriate dictID is selected + * from the set of DDicts based on the dictID in the frame. + * + * Usage is simply calling ZSTD_refDDict() on multiple dict buffers. + * + * Param has values of byte ZSTD_refMultipleDDicts_e + * + * WARNING: Enabling this parameter and calling ZSTD_DCtx_refDDict(), will trigger memory + * allocation for the hash table. ZSTD_freeDCtx() also frees this memory. + * Memory is allocated as per ZSTD_DCtx::customMem. + * + * Although this function allocates memory for the table, the user is still responsible for + * memory management of the underlying ZSTD_DDict* themselves. + */ +#define ZSTD_d_refMultipleDDicts ZSTD_d_experimentalParam4 + +/* ZSTD_d_disableHuffmanAssembly + * Set to 1 to disable the Huffman assembly implementation. + * The default value is 0, which allows zstd to use the Huffman assembly + * implementation if available. + * + * This parameter can be used to disable Huffman assembly at runtime. + * If you want to disable it at compile time you can define the macro + * ZSTD_DISABLE_ASM. + */ +#define ZSTD_d_disableHuffmanAssembly ZSTD_d_experimentalParam5 + +/* ZSTD_d_maxBlockSize + * Allowed values are between 1KB and ZSTD_BLOCKSIZE_MAX (128KB). + * The default is ZSTD_BLOCKSIZE_MAX, and setting to 0 will set to the default. + * + * Forces the decompressor to reject blocks whose content size is + * larger than the configured maxBlockSize. When maxBlockSize is + * larger than the windowSize, the windowSize is used instead. + * This saves memory on the decoder when you know all blocks are small. + * + * This option is typically used in conjunction with ZSTD_c_maxBlockSize. + * + * WARNING: This causes the decoder to reject otherwise valid frames + * that have block sizes larger than the configured maxBlockSize. + */ +#define ZSTD_d_maxBlockSize ZSTD_d_experimentalParam6 + + +/*! ZSTD_DCtx_setFormat() : + * This function is REDUNDANT. Prefer ZSTD_DCtx_setParameter(). + * Instruct the decoder context about what kind of data to decode next. + * This instruction is mandatory to decode data without a fully-formed header, + * such ZSTD_f_zstd1_magicless for example. + * @return : 0, or an error code (which can be tested using ZSTD_isError()). */ +ZSTD_DEPRECATED("use ZSTD_DCtx_setParameter() instead") +ZSTDLIB_STATIC_API +size_t ZSTD_DCtx_setFormat(ZSTD_DCtx* dctx, ZSTD_format_e format); + +/*! ZSTD_decompressStream_simpleArgs() : + * Same as ZSTD_decompressStream(), + * but using only integral types as arguments. + * This can be helpful for binders from dynamic languages + * which have troubles handling structures containing memory pointers. + */ +ZSTDLIB_STATIC_API size_t ZSTD_decompressStream_simpleArgs ( + ZSTD_DCtx* dctx, + void* dst, size_t dstCapacity, size_t* dstPos, + const void* src, size_t srcSize, size_t* srcPos); + + +/******************************************************************** +* Advanced streaming functions +* Warning : most of these functions are now redundant with the Advanced API. +* Once Advanced API reaches "stable" status, +* redundant functions will be deprecated, and then at some point removed. +********************************************************************/ + +/*===== Advanced Streaming compression functions =====*/ + +/*! ZSTD_initCStream_srcSize() : + * This function is DEPRECATED, and equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_refCDict(zcs, NULL); // clear the dictionary (if any) + * ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel); + * ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize); + * + * pledgedSrcSize must be correct. If it is not known at init time, use + * ZSTD_CONTENTSIZE_UNKNOWN. Note that, for compatibility with older programs, + * "0" also disables frame content size field. It may be enabled in the future. + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_srcSize(ZSTD_CStream* zcs, + int compressionLevel, + unsigned long long pledgedSrcSize); + +/*! ZSTD_initCStream_usingDict() : + * This function is DEPRECATED, and is equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_setParameter(zcs, ZSTD_c_compressionLevel, compressionLevel); + * ZSTD_CCtx_loadDictionary(zcs, dict, dictSize); + * + * Creates of an internal CDict (incompatible with static CCtx), except if + * dict == NULL or dictSize < 8, in which case no dict is used. + * Note: dict is loaded with ZSTD_dct_auto (treated as a full zstd dictionary if + * it begins with ZSTD_MAGIC_DICTIONARY, else as raw content) and ZSTD_dlm_byCopy. + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_usingDict(ZSTD_CStream* zcs, + const void* dict, size_t dictSize, + int compressionLevel); + +/*! ZSTD_initCStream_advanced() : + * This function is DEPRECATED, and is equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_setParams(zcs, params); + * ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize); + * ZSTD_CCtx_loadDictionary(zcs, dict, dictSize); + * + * dict is loaded with ZSTD_dct_auto and ZSTD_dlm_byCopy. + * pledgedSrcSize must be correct. + * If srcSize is not known at init time, use value ZSTD_CONTENTSIZE_UNKNOWN. + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_advanced(ZSTD_CStream* zcs, + const void* dict, size_t dictSize, + ZSTD_parameters params, + unsigned long long pledgedSrcSize); + +/*! ZSTD_initCStream_usingCDict() : + * This function is DEPRECATED, and equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_refCDict(zcs, cdict); + * + * note : cdict will just be referenced, and must outlive compression session + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset and ZSTD_CCtx_refCDict, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_usingCDict(ZSTD_CStream* zcs, const ZSTD_CDict* cdict); + +/*! ZSTD_initCStream_usingCDict_advanced() : + * This function is DEPRECATED, and is equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_setFParams(zcs, fParams); + * ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize); + * ZSTD_CCtx_refCDict(zcs, cdict); + * + * same as ZSTD_initCStream_usingCDict(), with control over frame parameters. + * pledgedSrcSize must be correct. If srcSize is not known at init time, use + * value ZSTD_CONTENTSIZE_UNKNOWN. + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset and ZSTD_CCtx_refCDict, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_initCStream_usingCDict_advanced(ZSTD_CStream* zcs, + const ZSTD_CDict* cdict, + ZSTD_frameParameters fParams, + unsigned long long pledgedSrcSize); + +/*! ZSTD_resetCStream() : + * This function is DEPRECATED, and is equivalent to: + * ZSTD_CCtx_reset(zcs, ZSTD_reset_session_only); + * ZSTD_CCtx_setPledgedSrcSize(zcs, pledgedSrcSize); + * Note: ZSTD_resetCStream() interprets pledgedSrcSize == 0 as ZSTD_CONTENTSIZE_UNKNOWN, but + * ZSTD_CCtx_setPledgedSrcSize() does not do the same, so ZSTD_CONTENTSIZE_UNKNOWN must be + * explicitly specified. + * + * start a new frame, using same parameters from previous frame. + * This is typically useful to skip dictionary loading stage, since it will reuse it in-place. + * Note that zcs must be init at least once before using ZSTD_resetCStream(). + * If pledgedSrcSize is not known at reset time, use macro ZSTD_CONTENTSIZE_UNKNOWN. + * If pledgedSrcSize > 0, its value must be correct, as it will be written in header, and controlled at the end. + * For the time being, pledgedSrcSize==0 is interpreted as "srcSize unknown" for compatibility with older programs, + * but it will change to mean "empty" in future version, so use macro ZSTD_CONTENTSIZE_UNKNOWN instead. + * @return : 0, or an error code (which can be tested using ZSTD_isError()) + * This prototype will generate compilation warnings. + */ +ZSTD_DEPRECATED("use ZSTD_CCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API +size_t ZSTD_resetCStream(ZSTD_CStream* zcs, unsigned long long pledgedSrcSize); + + +typedef struct { + unsigned long long ingested; /* nb input bytes read and buffered */ + unsigned long long consumed; /* nb input bytes actually compressed */ + unsigned long long produced; /* nb of compressed bytes generated and buffered */ + unsigned long long flushed; /* nb of compressed bytes flushed : not provided; can be tracked from caller side */ + unsigned currentJobID; /* MT only : latest started job nb */ + unsigned nbActiveWorkers; /* MT only : nb of workers actively compressing at probe time */ +} ZSTD_frameProgression; + +/* ZSTD_getFrameProgression() : + * tells how much data has been ingested (read from input) + * consumed (input actually compressed) and produced (output) for current frame. + * Note : (ingested - consumed) is amount of input data buffered internally, not yet compressed. + * Aggregates progression inside active worker threads. + */ +ZSTDLIB_STATIC_API ZSTD_frameProgression ZSTD_getFrameProgression(const ZSTD_CCtx* cctx); + +/*! ZSTD_toFlushNow() : + * Tell how many bytes are ready to be flushed immediately. + * Useful for multithreading scenarios (nbWorkers >= 1). + * Probe the oldest active job, defined as oldest job not yet entirely flushed, + * and check its output buffer. + * @return : amount of data stored in oldest job and ready to be flushed immediately. + * if @return == 0, it means either : + * + there is no active job (could be checked with ZSTD_frameProgression()), or + * + oldest job is still actively compressing data, + * but everything it has produced has also been flushed so far, + * therefore flush speed is limited by production speed of oldest job + * irrespective of the speed of concurrent (and newer) jobs. + */ +ZSTDLIB_STATIC_API size_t ZSTD_toFlushNow(ZSTD_CCtx* cctx); + + +/*===== Advanced Streaming decompression functions =====*/ + +/*! + * This function is deprecated, and is equivalent to: + * + * ZSTD_DCtx_reset(zds, ZSTD_reset_session_only); + * ZSTD_DCtx_loadDictionary(zds, dict, dictSize); + * + * note: no dictionary will be used if dict == NULL or dictSize < 8 + */ +ZSTD_DEPRECATED("use ZSTD_DCtx_reset + ZSTD_DCtx_loadDictionary, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API size_t ZSTD_initDStream_usingDict(ZSTD_DStream* zds, const void* dict, size_t dictSize); + +/*! + * This function is deprecated, and is equivalent to: + * + * ZSTD_DCtx_reset(zds, ZSTD_reset_session_only); + * ZSTD_DCtx_refDDict(zds, ddict); + * + * note : ddict is referenced, it must outlive decompression session + */ +ZSTD_DEPRECATED("use ZSTD_DCtx_reset + ZSTD_DCtx_refDDict, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API size_t ZSTD_initDStream_usingDDict(ZSTD_DStream* zds, const ZSTD_DDict* ddict); + +/*! + * This function is deprecated, and is equivalent to: + * + * ZSTD_DCtx_reset(zds, ZSTD_reset_session_only); + * + * reuse decompression parameters from previous init; saves dictionary loading + */ +ZSTD_DEPRECATED("use ZSTD_DCtx_reset, see zstd.h for detailed instructions") +ZSTDLIB_STATIC_API size_t ZSTD_resetDStream(ZSTD_DStream* zds); + + +/* ********************* BLOCK-LEVEL SEQUENCE PRODUCER API ********************* + * + * *** OVERVIEW *** + * The Block-Level Sequence Producer API allows users to provide their own custom + * sequence producer which libzstd invokes to process each block. The produced list + * of sequences (literals and matches) is then post-processed by libzstd to produce + * valid compressed blocks. + * + * This block-level offload API is a more granular complement of the existing + * frame-level offload API compressSequences() (introduced in v1.5.1). It offers + * an easier migration story for applications already integrated with libzstd: the + * user application continues to invoke the same compression functions + * ZSTD_compress2() or ZSTD_compressStream2() as usual, and transparently benefits + * from the specific advantages of the external sequence producer. For example, + * the sequence producer could be tuned to take advantage of known characteristics + * of the input, to offer better speed / ratio, or could leverage hardware + * acceleration not available within libzstd itself. + * + * See contrib/externalSequenceProducer for an example program employing the + * Block-Level Sequence Producer API. + * + * *** USAGE *** + * The user is responsible for implementing a function of type + * ZSTD_sequenceProducer_F. For each block, zstd will pass the following + * arguments to the user-provided function: + * + * - sequenceProducerState: a pointer to a user-managed state for the sequence + * producer. + * + * - outSeqs, outSeqsCapacity: an output buffer for the sequence producer. + * outSeqsCapacity is guaranteed >= ZSTD_sequenceBound(srcSize). The memory + * backing outSeqs is managed by the CCtx. + * + * - src, srcSize: an input buffer for the sequence producer to parse. + * srcSize is guaranteed to be <= ZSTD_BLOCKSIZE_MAX. + * + * - dict, dictSize: a history buffer, which may be empty, which the sequence + * producer may reference as it parses the src buffer. Currently, zstd will + * always pass dictSize == 0 into external sequence producers, but this will + * change in the future. + * + * - compressionLevel: a signed integer representing the zstd compression level + * set by the user for the current operation. The sequence producer may choose + * to use this information to change its compression strategy and speed/ratio + * tradeoff. Note: the compression level does not reflect zstd parameters set + * through the advanced API. + * + * - windowSize: a size_t representing the maximum allowed offset for external + * sequences. Note that sequence offsets are sometimes allowed to exceed the + * windowSize if a dictionary is present, see doc/zstd_compression_format.md + * for details. + * + * The user-provided function shall return a size_t representing the number of + * sequences written to outSeqs. This return value will be treated as an error + * code if it is greater than outSeqsCapacity. The return value must be non-zero + * if srcSize is non-zero. The ZSTD_SEQUENCE_PRODUCER_ERROR macro is provided + * for convenience, but any value greater than outSeqsCapacity will be treated as + * an error code. + * + * If the user-provided function does not return an error code, the sequences + * written to outSeqs must be a valid parse of the src buffer. Data corruption may + * occur if the parse is not valid. A parse is defined to be valid if the + * following conditions hold: + * - The sum of matchLengths and literalLengths must equal srcSize. + * - All sequences in the parse, except for the final sequence, must have + * matchLength >= ZSTD_MINMATCH_MIN. The final sequence must have + * matchLength >= ZSTD_MINMATCH_MIN or matchLength == 0. + * - All offsets must respect the windowSize parameter as specified in + * doc/zstd_compression_format.md. + * - If the final sequence has matchLength == 0, it must also have offset == 0. + * + * zstd will only validate these conditions (and fail compression if they do not + * hold) if the ZSTD_c_validateSequences cParam is enabled. Note that sequence + * validation has a performance cost. + * + * If the user-provided function returns an error, zstd will either fall back + * to an internal sequence producer or fail the compression operation. The user can + * choose between the two behaviors by setting the ZSTD_c_enableSeqProducerFallback + * cParam. Fallback compression will follow any other cParam settings, such as + * compression level, the same as in a normal compression operation. + * + * The user shall instruct zstd to use a particular ZSTD_sequenceProducer_F + * function by calling + * ZSTD_registerSequenceProducer(cctx, + * sequenceProducerState, + * sequenceProducer) + * This setting will persist until the next parameter reset of the CCtx. + * + * The sequenceProducerState must be initialized by the user before calling + * ZSTD_registerSequenceProducer(). The user is responsible for destroying the + * sequenceProducerState. + * + * *** LIMITATIONS *** + * This API is compatible with all zstd compression APIs which respect advanced parameters. + * However, there are three limitations: + * + * First, the ZSTD_c_enableLongDistanceMatching cParam is not currently supported. + * COMPRESSION WILL FAIL if it is enabled and the user tries to compress with a block-level + * external sequence producer. + * - Note that ZSTD_c_enableLongDistanceMatching is auto-enabled by default in some + * cases (see its documentation for details). Users must explicitly set + * ZSTD_c_enableLongDistanceMatching to ZSTD_ps_disable in such cases if an external + * sequence producer is registered. + * - As of this writing, ZSTD_c_enableLongDistanceMatching is disabled by default + * whenever ZSTD_c_windowLog < 128MB, but that's subject to change. Users should + * check the docs on ZSTD_c_enableLongDistanceMatching whenever the Block-Level Sequence + * Producer API is used in conjunction with advanced settings (like ZSTD_c_windowLog). + * + * Second, history buffers are not currently supported. Concretely, zstd will always pass + * dictSize == 0 to the external sequence producer (for now). This has two implications: + * - Dictionaries are not currently supported. Compression will *not* fail if the user + * references a dictionary, but the dictionary won't have any effect. + * - Stream history is not currently supported. All advanced compression APIs, including + * streaming APIs, work with external sequence producers, but each block is treated as + * an independent chunk without history from previous blocks. + * + * Third, multi-threading within a single compression is not currently supported. In other words, + * COMPRESSION WILL FAIL if ZSTD_c_nbWorkers > 0 and an external sequence producer is registered. + * Multi-threading across compressions is fine: simply create one CCtx per thread. + * + * Long-term, we plan to overcome all three limitations. There is no technical blocker to + * overcoming them. It is purely a question of engineering effort. + */ + +#define ZSTD_SEQUENCE_PRODUCER_ERROR ((size_t)(-1)) + +typedef size_t (*ZSTD_sequenceProducer_F) ( + void* sequenceProducerState, + ZSTD_Sequence* outSeqs, size_t outSeqsCapacity, + const void* src, size_t srcSize, + const void* dict, size_t dictSize, + int compressionLevel, + size_t windowSize +); + +/*! ZSTD_registerSequenceProducer() : + * Instruct zstd to use a block-level external sequence producer function. + * + * The sequenceProducerState must be initialized by the caller, and the caller is + * responsible for managing its lifetime. This parameter is sticky across + * compressions. It will remain set until the user explicitly resets compression + * parameters. + * + * Sequence producer registration is considered to be an "advanced parameter", + * part of the "advanced API". This means it will only have an effect on compression + * APIs which respect advanced parameters, such as compress2() and compressStream2(). + * Older compression APIs such as compressCCtx(), which predate the introduction of + * "advanced parameters", will ignore any external sequence producer setting. + * + * The sequence producer can be "cleared" by registering a NULL function pointer. This + * removes all limitations described above in the "LIMITATIONS" section of the API docs. + * + * The user is strongly encouraged to read the full API documentation (above) before + * calling this function. */ +ZSTDLIB_STATIC_API void +ZSTD_registerSequenceProducer( + ZSTD_CCtx* cctx, + void* sequenceProducerState, + ZSTD_sequenceProducer_F sequenceProducer +); + +/*! ZSTD_CCtxParams_registerSequenceProducer() : + * Same as ZSTD_registerSequenceProducer(), but operates on ZSTD_CCtx_params. + * This is used for accurate size estimation with ZSTD_estimateCCtxSize_usingCCtxParams(), + * which is needed when creating a ZSTD_CCtx with ZSTD_initStaticCCtx(). + * + * If you are using the external sequence producer API in a scenario where ZSTD_initStaticCCtx() + * is required, then this function is for you. Otherwise, you probably don't need it. + * + * See tests/zstreamtest.c for example usage. */ +ZSTDLIB_STATIC_API void +ZSTD_CCtxParams_registerSequenceProducer( + ZSTD_CCtx_params* params, + void* sequenceProducerState, + ZSTD_sequenceProducer_F sequenceProducer +); + + +/********************************************************************* +* Buffer-less and synchronous inner streaming functions (DEPRECATED) +* +* This API is deprecated, and will be removed in a future version. +* It allows streaming (de)compression with user allocated buffers. +* However, it is hard to use, and not as well tested as the rest of +* our API. +* +* Please use the normal streaming API instead: ZSTD_compressStream2, +* and ZSTD_decompressStream. +* If there is functionality that you need, but it doesn't provide, +* please open an issue on our GitHub. +********************************************************************* */ + +/** + Buffer-less streaming compression (synchronous mode) + + A ZSTD_CCtx object is required to track streaming operations. + Use ZSTD_createCCtx() / ZSTD_freeCCtx() to manage resource. + ZSTD_CCtx object can be reused multiple times within successive compression operations. + + Start by initializing a context. + Use ZSTD_compressBegin(), or ZSTD_compressBegin_usingDict() for dictionary compression. + + Then, consume your input using ZSTD_compressContinue(). + There are some important considerations to keep in mind when using this advanced function : + - ZSTD_compressContinue() has no internal buffer. It uses externally provided buffers only. + - Interface is synchronous : input is consumed entirely and produces 1+ compressed blocks. + - Caller must ensure there is enough space in `dst` to store compressed data under worst case scenario. + Worst case evaluation is provided by ZSTD_compressBound(). + ZSTD_compressContinue() doesn't guarantee recover after a failed compression. + - ZSTD_compressContinue() presumes prior input ***is still accessible and unmodified*** (up to maximum distance size, see WindowLog). + It remembers all previous contiguous blocks, plus one separated memory segment (which can itself consists of multiple contiguous blocks) + - ZSTD_compressContinue() detects that prior input has been overwritten when `src` buffer overlaps. + In which case, it will "discard" the relevant memory section from its history. + + Finish a frame with ZSTD_compressEnd(), which will write the last block(s) and optional checksum. + It's possible to use srcSize==0, in which case, it will write a final empty block to end the frame. + Without last block mark, frames are considered unfinished (hence corrupted) by compliant decoders. + + `ZSTD_CCtx` object can be reused (ZSTD_compressBegin()) to compress again. +*/ + +/*===== Buffer-less streaming compression functions =====*/ +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressBegin(ZSTD_CCtx* cctx, int compressionLevel); +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressBegin_usingDict(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, int compressionLevel); +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressBegin_usingCDict(ZSTD_CCtx* cctx, const ZSTD_CDict* cdict); /**< note: fails if cdict==NULL */ + +ZSTD_DEPRECATED("This function will likely be removed in a future release. It is misleading and has very limited utility.") +ZSTDLIB_STATIC_API +size_t ZSTD_copyCCtx(ZSTD_CCtx* cctx, const ZSTD_CCtx* preparedCCtx, unsigned long long pledgedSrcSize); /**< note: if pledgedSrcSize is not known, use ZSTD_CONTENTSIZE_UNKNOWN */ + +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressContinue(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); +ZSTD_DEPRECATED("The buffer-less API is deprecated in favor of the normal streaming API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressEnd(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); + +/* The ZSTD_compressBegin_advanced() and ZSTD_compressBegin_usingCDict_advanced() are now DEPRECATED and will generate a compiler warning */ +ZSTD_DEPRECATED("use advanced API to access custom parameters") +ZSTDLIB_STATIC_API +size_t ZSTD_compressBegin_advanced(ZSTD_CCtx* cctx, const void* dict, size_t dictSize, ZSTD_parameters params, unsigned long long pledgedSrcSize); /**< pledgedSrcSize : If srcSize is not known at init time, use ZSTD_CONTENTSIZE_UNKNOWN */ +ZSTD_DEPRECATED("use advanced API to access custom parameters") +ZSTDLIB_STATIC_API +size_t ZSTD_compressBegin_usingCDict_advanced(ZSTD_CCtx* const cctx, const ZSTD_CDict* const cdict, ZSTD_frameParameters const fParams, unsigned long long const pledgedSrcSize); /* compression parameters are already set within cdict. pledgedSrcSize must be correct. If srcSize is not known, use macro ZSTD_CONTENTSIZE_UNKNOWN */ +/** + Buffer-less streaming decompression (synchronous mode) + + A ZSTD_DCtx object is required to track streaming operations. + Use ZSTD_createDCtx() / ZSTD_freeDCtx() to manage it. + A ZSTD_DCtx object can be reused multiple times. + + First typical operation is to retrieve frame parameters, using ZSTD_getFrameHeader(). + Frame header is extracted from the beginning of compressed frame, so providing only the frame's beginning is enough. + Data fragment must be large enough to ensure successful decoding. + `ZSTD_frameHeaderSize_max` bytes is guaranteed to always be large enough. + result : 0 : successful decoding, the `ZSTD_frameHeader` structure is correctly filled. + >0 : `srcSize` is too small, please provide at least result bytes on next attempt. + errorCode, which can be tested using ZSTD_isError(). + + It fills a ZSTD_frameHeader structure with important information to correctly decode the frame, + such as the dictionary ID, content size, or maximum back-reference distance (`windowSize`). + Note that these values could be wrong, either because of data corruption, or because a 3rd party deliberately spoofs false information. + As a consequence, check that values remain within valid application range. + For example, do not allocate memory blindly, check that `windowSize` is within expectation. + Each application can set its own limits, depending on local restrictions. + For extended interoperability, it is recommended to support `windowSize` of at least 8 MB. + + ZSTD_decompressContinue() needs previous data blocks during decompression, up to `windowSize` bytes. + ZSTD_decompressContinue() is very sensitive to contiguity, + if 2 blocks don't follow each other, make sure that either the compressor breaks contiguity at the same place, + or that previous contiguous segment is large enough to properly handle maximum back-reference distance. + There are multiple ways to guarantee this condition. + + The most memory efficient way is to use a round buffer of sufficient size. + Sufficient size is determined by invoking ZSTD_decodingBufferSize_min(), + which can return an error code if required value is too large for current system (in 32-bits mode). + In a round buffer methodology, ZSTD_decompressContinue() decompresses each block next to previous one, + up to the moment there is not enough room left in the buffer to guarantee decoding another full block, + which maximum size is provided in `ZSTD_frameHeader` structure, field `blockSizeMax`. + At which point, decoding can resume from the beginning of the buffer. + Note that already decoded data stored in the buffer should be flushed before being overwritten. + + There are alternatives possible, for example using two or more buffers of size `windowSize` each, though they consume more memory. + + Finally, if you control the compression process, you can also ignore all buffer size rules, + as long as the encoder and decoder progress in "lock-step", + aka use exactly the same buffer sizes, break contiguity at the same place, etc. + + Once buffers are setup, start decompression, with ZSTD_decompressBegin(). + If decompression requires a dictionary, use ZSTD_decompressBegin_usingDict() or ZSTD_decompressBegin_usingDDict(). + + Then use ZSTD_nextSrcSizeToDecompress() and ZSTD_decompressContinue() alternatively. + ZSTD_nextSrcSizeToDecompress() tells how many bytes to provide as 'srcSize' to ZSTD_decompressContinue(). + ZSTD_decompressContinue() requires this _exact_ amount of bytes, or it will fail. + + result of ZSTD_decompressContinue() is the number of bytes regenerated within 'dst' (necessarily <= dstCapacity). + It can be zero : it just means ZSTD_decompressContinue() has decoded some metadata item. + It can also be an error code, which can be tested with ZSTD_isError(). + + A frame is fully decoded when ZSTD_nextSrcSizeToDecompress() returns zero. + Context can then be reset to start a new decompression. + + Note : it's possible to know if next input to present is a header or a block, using ZSTD_nextInputType(). + This information is not required to properly decode a frame. + + == Special case : skippable frames == + + Skippable frames allow integration of user-defined data into a flow of concatenated frames. + Skippable frames will be ignored (skipped) by decompressor. + The format of skippable frames is as follows : + a) Skippable frame ID - 4 Bytes, Little endian format, any value from 0x184D2A50 to 0x184D2A5F + b) Frame Size - 4 Bytes, Little endian format, unsigned 32-bits + c) Frame Content - any content (User Data) of length equal to Frame Size + For skippable frames ZSTD_getFrameHeader() returns zfhPtr->frameType==ZSTD_skippableFrame. + For skippable frames ZSTD_decompressContinue() always returns 0 : it only skips the content. +*/ + +/*===== Buffer-less streaming decompression functions =====*/ + +ZSTDLIB_STATIC_API size_t ZSTD_decodingBufferSize_min(unsigned long long windowSize, unsigned long long frameContentSize); /**< when frame content size is not known, pass in frameContentSize == ZSTD_CONTENTSIZE_UNKNOWN */ + +ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin(ZSTD_DCtx* dctx); +ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin_usingDict(ZSTD_DCtx* dctx, const void* dict, size_t dictSize); +ZSTDLIB_STATIC_API size_t ZSTD_decompressBegin_usingDDict(ZSTD_DCtx* dctx, const ZSTD_DDict* ddict); + +ZSTDLIB_STATIC_API size_t ZSTD_nextSrcSizeToDecompress(ZSTD_DCtx* dctx); +ZSTDLIB_STATIC_API size_t ZSTD_decompressContinue(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); + +/* misc */ +ZSTD_DEPRECATED("This function will likely be removed in the next minor release. It is misleading and has very limited utility.") +ZSTDLIB_STATIC_API void ZSTD_copyDCtx(ZSTD_DCtx* dctx, const ZSTD_DCtx* preparedDCtx); +typedef enum { ZSTDnit_frameHeader, ZSTDnit_blockHeader, ZSTDnit_block, ZSTDnit_lastBlock, ZSTDnit_checksum, ZSTDnit_skippableFrame } ZSTD_nextInputType_e; +ZSTDLIB_STATIC_API ZSTD_nextInputType_e ZSTD_nextInputType(ZSTD_DCtx* dctx); + + + + +/* ========================================= */ +/** Block level API (DEPRECATED) */ +/* ========================================= */ + +/*! + + This API is deprecated in favor of the regular compression API. + You can get the frame header down to 2 bytes by setting: + - ZSTD_c_format = ZSTD_f_zstd1_magicless + - ZSTD_c_contentSizeFlag = 0 + - ZSTD_c_checksumFlag = 0 + - ZSTD_c_dictIDFlag = 0 + + This API is not as well tested as our normal API, so we recommend not using it. + We will be removing it in a future version. If the normal API doesn't provide + the functionality you need, please open a GitHub issue. + + Block functions produce and decode raw zstd blocks, without frame metadata. + Frame metadata cost is typically ~12 bytes, which can be non-negligible for very small blocks (< 100 bytes). + But users will have to take in charge needed metadata to regenerate data, such as compressed and content sizes. + + A few rules to respect : + - Compressing and decompressing require a context structure + + Use ZSTD_createCCtx() and ZSTD_createDCtx() + - It is necessary to init context before starting + + compression : any ZSTD_compressBegin*() variant, including with dictionary + + decompression : any ZSTD_decompressBegin*() variant, including with dictionary + - Block size is limited, it must be <= ZSTD_getBlockSize() <= ZSTD_BLOCKSIZE_MAX == 128 KB + + If input is larger than a block size, it's necessary to split input data into multiple blocks + + For inputs larger than a single block, consider using regular ZSTD_compress() instead. + Frame metadata is not that costly, and quickly becomes negligible as source size grows larger than a block. + - When a block is considered not compressible enough, ZSTD_compressBlock() result will be 0 (zero) ! + ===> In which case, nothing is produced into `dst` ! + + User __must__ test for such outcome and deal directly with uncompressed data + + A block cannot be declared incompressible if ZSTD_compressBlock() return value was != 0. + Doing so would mess up with statistics history, leading to potential data corruption. + + ZSTD_decompressBlock() _doesn't accept uncompressed data as input_ !! + + In case of multiple successive blocks, should some of them be uncompressed, + decoder must be informed of their existence in order to follow proper history. + Use ZSTD_insertBlock() for such a case. +*/ + +/*===== Raw zstd block functions =====*/ +ZSTD_DEPRECATED("The block API is deprecated in favor of the normal compression API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_getBlockSize (const ZSTD_CCtx* cctx); +ZSTD_DEPRECATED("The block API is deprecated in favor of the normal compression API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_compressBlock (ZSTD_CCtx* cctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); +ZSTD_DEPRECATED("The block API is deprecated in favor of the normal compression API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_decompressBlock(ZSTD_DCtx* dctx, void* dst, size_t dstCapacity, const void* src, size_t srcSize); +ZSTD_DEPRECATED("The block API is deprecated in favor of the normal compression API. See docs.") +ZSTDLIB_STATIC_API size_t ZSTD_insertBlock (ZSTD_DCtx* dctx, const void* blockStart, size_t blockSize); /**< insert uncompressed block into `dctx` history. Useful for multi-blocks decompression. */ + +#endif /* ZSTD_H_ZSTD_STATIC_LINKING_ONLY */ + +#if defined (__cplusplus) +} +#endif diff --git a/rendering/editoroverlay.h b/rendering/editoroverlay.h new file mode 100644 index 00000000..57442d43 --- /dev/null +++ b/rendering/editoroverlay.h @@ -0,0 +1,41 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +#include + +namespace gfx +{ + +// flat coloured geometry the editor shows in the 3d view, drawn with the depth test over the translucent scenery +struct editor_overlay +{ + struct batch + { + glm::vec4 color{1.0f}; + unsigned int type{0}; // GL primitive + std::vector points; // relative to the origin + }; + glm::dvec3 origin{0.0}; + std::vector batches; + unsigned int revision{0}; // changed with every new content + + void clear() + { + batches.clear(); + ++revision; + } +}; + +} // namespace gfx + +inline gfx::editor_overlay EditorOverlay; diff --git a/rendering/geometrybank.cpp b/rendering/geometrybank.cpp index fcd731dd..f2db8b8b 100644 --- a/rendering/geometrybank.cpp +++ b/rendering/geometrybank.cpp @@ -300,8 +300,8 @@ geometry_bank::replace( gfx::vertex_array &Vertices, userdata_array &Userdata, g // TBD: we could bail out with an error instead if such request occurs chunk.vertices.resize( Offset + Vertices.size(), gfx::basic_vertex() ); chunk.userdata.resize( Offset + Userdata.size(), gfx::vertex_userdata() ); - chunk.vertices.insert( std::end( chunk.vertices ), std::begin( Vertices ), std::end( Vertices ) ); - chunk.userdata.insert( std::end( chunk.userdata ), std::begin( Userdata ), std::end( Userdata ) ); + std::copy( std::begin( Vertices ), std::end( Vertices ), std::begin( chunk.vertices ) + Offset ); + std::copy( std::begin( Userdata ), std::end( Userdata ), std::begin( chunk.userdata ) + Offset ); } // template method implementation replace_( Geometry ); @@ -332,6 +332,13 @@ geometry_bank::draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream return draw_instanced_( Geometry, Units, InstanceCount, Streams ); } +// draws geometry stored in supplied sequence of chunks of the bank +std::size_t +geometry_bank::draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams ) { + // template method implementation + return draw_batch_( First, Last, Units, Streams ); +} + // frees subclass-specific resources associated with the bank, typically called when the bank wasn't in use for a period of time void geometry_bank::release() { @@ -436,6 +443,32 @@ geometrybank_manager::draw_instanced( gfx::geometry_handle const &Geometry, std: m_primitivecount += bankrecord.first->draw_instanced( Geometry, m_units, InstanceCount, Streams ); } +// draws geometry stored in supplied sequence of chunks +void +geometrybank_manager::draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, unsigned int const Streams ) { + + while( First != Last ) { + + if( *First == null_handle ) { + ++First; + continue; + } + // neighbouring chunks stored in the same bank go to the bank together + auto const *last { First + 1 }; + while( ( last != Last ) + && ( last->bank == First->bank ) ) { + ++last; + } + + auto &bankrecord = bank( *First ); + + bankrecord.second = m_garbagecollector.timestamp(); + m_primitivecount += bankrecord.first->draw_batch( First, last, m_units, Streams ); + + First = last; + } +} + // provides direct access to index data of specfied chunk gfx::index_array const & geometrybank_manager::indices( gfx::geometry_handle const &Geometry ) const { diff --git a/rendering/geometrybank.h b/rendering/geometrybank.h index 39735e00..20baf4ed 100644 --- a/rendering/geometrybank.h +++ b/rendering/geometrybank.h @@ -121,6 +121,8 @@ public: auto draw( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; // draws geometry stored in specified chunk N times via glDrawElementsInstanced* auto draw_instanced( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams = basic_streams ) -> std::size_t; + // draws geometry stored in supplied sequence of chunks of the bank, in their order. the bank is free to combine them into fewer draw calls + auto draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) -> std::size_t; // draws geometry stored in supplied list of chunks template auto draw( Iterator_ First, Iterator_ Last, gfx::stream_units const &Units, unsigned int const Streams = basic_streams ) ->std::size_t { @@ -187,6 +189,11 @@ private: std::size_t count { 0 }; for( std::size_t i = 0; i < InstanceCount; ++i ) { count += draw_( Geometry, Units, Streams ); } return count; } + // draw_batch() subclass details. Default implementation falls back to regular draw of each chunk. + virtual auto draw_batch_( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t { + std::size_t count { 0 }; + for( ; First != Last; ++First ) { count += draw_( *First, Units, Streams ); } + return count; } // resource release subclass details virtual void release_() = 0; }; @@ -218,6 +225,8 @@ public: // draws geometry stored in specified chunk InstanceCount times via GPU instancing. // The shader reads per-instance modelview matrices from instance_ubo[gl_InstanceID]. void draw_instanced( gfx::geometry_handle const &Geometry, std::size_t const InstanceCount, unsigned int const Streams = basic_streams ); + // draws geometry stored in supplied sequence of chunks, in their order. neighbouring chunks of the same bank are passed to it together + void draw_batch( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, unsigned int const Streams = basic_streams ); template void draw( Iterator_ First, Iterator_ Last, unsigned int const Streams = basic_streams ) { while( First != Last ) { diff --git a/rendering/opengl33geometrybank.cpp b/rendering/opengl33geometrybank.cpp index 506d22f1..4713ec69 100644 --- a/rendering/opengl33geometrybank.cpp +++ b/rendering/opengl33geometrybank.cpp @@ -259,6 +259,66 @@ opengl33_vaogeometrybank::draw_instanced_( gfx::geometry_handle const &Geometry, return prims * InstanceCount; } +// draw_batch() subclass details — neighbouring non-indexed chunks of the same type are sent with a single GL call. +// The chunks are drawn in the supplied order, same as with a draw_() call for each of them. +std::size_t +opengl33_vaogeometrybank::draw_batch_( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams ) +{ + std::size_t count { 0 }; + + if( !glMultiDrawArrays ) { + // GLES context doesn't provide the call + for( ; First != Last; ++First ) { count += draw_( *First, Units, Streams ); } + return count; + } + + setup_buffer(); + + std::array offsets; + std::array sizes; + GLsizei pending { 0 }; + unsigned int pendingtype { 0 }; + + auto const flush = [ & ]() { + if( pending == 0 ) { return; } + m_vao->bind(); + if( pending == 1 ) { ::glDrawArrays( pendingtype, offsets[ 0 ], sizes[ 0 ] ); } + else { ::glMultiDrawArrays( pendingtype, offsets.data(), sizes.data(), pending ); } + pending = 0; + }; + + for( ; First != Last; ++First ) { + + auto const &chunkrecord = m_chunkrecords.at( First->chunk - 1 ); + if( ( false == chunkrecord.is_good ) + || ( chunkrecord.index_count > 0 ) ) { + // chunks which wait for upload of their data and indexed chunks take the regular route + flush(); + count += draw_( *First, Units, Streams ); + continue; + } + + auto const type { gfx::geometry_bank::chunk( *First ).type }; + if( ( pending == static_cast( offsets.size() ) ) + || ( ( pending > 0 ) && ( type != pendingtype ) ) ) { + flush(); + } + pendingtype = type; + offsets[ pending ] = static_cast( chunkrecord.vertex_offset ); + sizes[ pending ] = static_cast( chunkrecord.vertex_count ); + ++pending; + + switch( type ) { + case GL_TRIANGLES: { count += chunkrecord.vertex_count / 3; break; } + case GL_TRIANGLE_STRIP: { count += ( chunkrecord.vertex_count > 2 ? chunkrecord.vertex_count - 2 : 0 ); break; } + default: { break; } + } + } + flush(); + + return count; +} + // release () subclass details void opengl33_vaogeometrybank::release_() { diff --git a/rendering/opengl33geometrybank.h b/rendering/opengl33geometrybank.h index f5cd7e37..79526f10 100644 --- a/rendering/opengl33geometrybank.h +++ b/rendering/opengl33geometrybank.h @@ -56,6 +56,9 @@ private: // draw_instanced() subclass details — issues glDrawElementsInstancedBaseVertex auto draw_instanced_( gfx::geometry_handle const &Geometry, gfx::stream_units const &Units, std::size_t const InstanceCount, unsigned int const Streams ) -> std::size_t override; + // draw_batch() subclass details — issues glMultiDrawArrays for runs of non-indexed chunks of the same type + auto + draw_batch_( gfx::geometry_handle const *First, gfx::geometry_handle const *Last, gfx::stream_units const &Units, unsigned int const Streams ) -> std::size_t override; // release() subclass details void release_() override; diff --git a/rendering/opengl33renderer.cpp b/rendering/opengl33renderer.cpp index 772363e5..d20454c3 100644 --- a/rendering/opengl33renderer.cpp +++ b/rendering/opengl33renderer.cpp @@ -21,6 +21,7 @@ http://mozilla.org/MPL/2.0/. #include "model/AnimModel.h" #include "rendering/opengl33geometrybank.h" #include "rendering/screenshot.h" +#include "rendering/editoroverlay.h" #include //#define EU07_DEBUG_OPENGL @@ -30,6 +31,28 @@ int constexpr EU07_REFLECTIONFIDELITYOFFSET { 250 }; // artificial increase of r namespace { +float +editor_track_view_range(float const Range, bool const Apply) +{ + if (false == Apply || false == Global.editor_tracks) + return Range; + float extra = 50000.0f; + if (Global.EditorOrtho) + extra = Global.EditorOrthoExtent * 3.0f; + return Range > extra ? Range : extra; +} + +bool +editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit) +{ + if (false == Apply) + return false; + double nearest = glm::length(Cell->m_area.center - Camera) - static_cast(Cell->m_area.radius); + if (nearest < 0.0) + nearest = 0.0; + return nearest > SceneryLimit; +} + // returns material assigned to the submodel, resolving replacable skins. null_handle if no skin set is bound material_handle submodel_material( TSubModel const *Submodel ) { @@ -593,6 +616,12 @@ bool opengl33_renderer::Render() } // generate new frame opengl_texture::reset_unit_cache(); + // textures of new heightmap terrain layers go to the arrays the terrain shader reads them from + m_terrainmaterials.update(); + if( EditorModeFlag ) { + // the editor moves and resizes model instances in place, which leaves the copies of their bounds out of date + ++m_instanceboundsversion; + } m_renderpass.draw_mode = rendermode::none; // force setup anew m_renderpass.draw_stats = debug_stats(); @@ -651,12 +680,16 @@ void opengl33_renderer::SwapBuffers() m_debugtimestext.clear(); m_debugtimestext = - "cpu frame total: " + to_string( Timer::subsystem.gfx_color.average() + Timer::subsystem.gfx_shadows.average() + Timer::subsystem.gfx_swap.average(), 2 ) + " ms\n" + "cpu frame total: " + to_string( Timer::subsystem.gfx_color.average() + Timer::subsystem.gfx_shadows.average() + ( Global.gfx_envmap_enabled ? Timer::subsystem.gfx_reflections.average() : 0.f ) + Timer::subsystem.gfx_swap.average(), 2 ) + " ms\n" + " color: " + to_string( Timer::subsystem.gfx_color.average(), 2 ) + " ms (" + std::to_string( m_cellqueue.size() ) + " sectors)\n"; if( Global.gfx_shadowmap_enabled ) { m_debugtimestext += " shadows: " + to_string( Timer::subsystem.gfx_shadows.average(), 2 ) + " ms\n"; } + if( Global.gfx_envmap_enabled ) { + m_debugtimestext += + " reflections: " + to_string( Timer::subsystem.gfx_reflections.average(), 2 ) + " ms\n"; + } m_debugtimestext += " swap: " + to_string( Timer::subsystem.gfx_swap.average(), 2 ) + " ms\n"; if( !Global.gfx_usegles ) { if (m_gllasttime) @@ -713,8 +746,8 @@ void opengl33_renderer::draw_debug_ui() ImGui::SliderFloat("in_cutoff", &conf.in_cutoff, 0.9f, 1.1f); ImGui::SliderFloat("out_cutoff", &conf.out_cutoff, 0.9f, 1.1f); - ImGui::SliderFloat("falloff_linear", &conf.falloff_linear, 0.0f, 1.0f, "%.3f", 2.0f); - ImGui::SliderFloat("falloff_quadratic", &conf.falloff_quadratic, 0.0f, 1.0f, "%.3f", 2.0f); + ImGui::SliderFloat("falloff_linear", &conf.falloff_linear, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("falloff_quadratic", &conf.falloff_quadratic, 0.0f, 1.0f, "%.3f", ImGuiSliderFlags_Logarithmic); ImGui::SliderFloat("ambient", &conf.ambient, 0.0f, 3.0f); ImGui::SliderFloat("intensity", &conf.intensity, 0.0f, 10.0f); @@ -841,8 +874,10 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode) { // potentially update environmental cube map m_renderpass.draw_stats = {}; + Timer::subsystem.gfx_reflections.start(); if (Render_reflections(vp)) m_renderpass = m_colorpass; // restore color pass settings + Timer::subsystem.gfx_reflections.stop(); setup_env_map(m_env_tex.get()); } @@ -950,6 +985,7 @@ void opengl33_renderer::Render_pass(viewport_config &vp, rendermode const Mode) glDebug("render translucent region"); setup_drawing(true); Render_Alpha(simulation::Region); + Render_editor_overlay(); // particles Render_particles(); @@ -1400,23 +1436,36 @@ bool opengl33_renderer::Render_reflections(viewport_config &vp) { if( Global.reflectiontune.update_interval == 0 ) { return false; } - auto const timestamp{ Timer::GetRenderTime() }; - if( ( timestamp - m_environmentupdatetime < Global.reflectiontune.update_interval ) - && ( glm::length2( m_renderpass.pass_camera.position() - m_environmentupdatelocation ) < sq(1000.0)) ) // length2 is better than length for comparing because it does not require sqrt function - { - // run update every 5+ mins of simulation time, or at least 1km from the last location - return false; + if( m_environmentupdateface >= 6 ) { + // no update in progress, check whether it's time for another one + auto const timestamp{ Timer::GetRenderTime() }; + if( ( timestamp - m_environmentupdatetime < Global.reflectiontune.update_interval ) + && ( glm::length2( m_renderpass.pass_camera.position() - m_environmentupdatelocation ) < sq(1000.0)) ) // length2 is better than length for comparing because it does not require sqrt function + { + // run update every 5+ mins of simulation time, or at least 1km from the last location + return false; + } + m_environmentupdatetime = timestamp; + m_environmentupdatelocation = m_renderpass.pass_camera.position(); + m_environmentupdateface = 0; } - m_environmentupdatetime = timestamp; - m_environmentupdatelocation = m_renderpass.pass_camera.position(); + // an update draws the scene six times, which is too much for a single frame. its cost is spread + // by drawing one face of the map per frame, all of them as seen from the place the update began at. + // the first update is done in one go, as there's nothing to show in the meantime + auto const lastface{ m_environmentready ? m_environmentupdateface : 5 }; glViewport(0, 0, gl::ENVMAP_SIZE, gl::ENVMAP_SIZE); - for (m_environmentcubetextureface = 0; m_environmentcubetextureface < 6; ++m_environmentcubetextureface) + for (; m_environmentupdateface <= lastface; ++m_environmentupdateface) { + m_environmentcubetextureface = m_environmentupdateface; m_env_fb->attach(*m_env_tex, m_environmentcubetextureface, GL_COLOR_ATTACHMENT0); if (m_env_fb->is_complete()) Render_pass(vp, rendermode::reflections); } - m_env_tex->generate_mipmaps(); + if (m_environmentupdateface >= 6) + { + m_env_tex->generate_mipmaps(); + m_environmentready = true; + } m_env_fb->detach(GL_COLOR_ATTACHMENT0); return true; @@ -1535,6 +1584,17 @@ glm::mat4 opengl33_renderer::ortho_frustumtest_projection(float l, float r, floa return glm::ortho(l, r, b, t, znear, zfar); } +// the main viewport of the editor in the orthographic view replaces the perspective projection +void opengl33_renderer::editor_ortho_projection(viewport_config const &Viewport, float const Zfar, glm::mat4 &Projection, glm::mat4 &Frustum) +{ + if (false == (EditorModeFlag && Global.EditorOrtho && Viewport.main)) + return; + auto const height{Global.EditorOrthoExtent}; + auto const width{height * Global.window_size.x / std::max(1.f, static_cast(Global.window_size.y))}; + Projection = ortho_projection(-width, width, -height, height, -Zfar, Zfar); + Frustum = ortho_frustumtest_projection(-width, width, -height, height, -Zfar, Zfar); +} + void opengl33_renderer::setup_pass(viewport_config &Viewport, renderpass_config &Config, rendermode const Mode, float const Znear, float const Zfar, bool const Ignoredebug) { @@ -1606,10 +1666,13 @@ void opengl33_renderer::setup_pass(viewport_config &Viewport, renderpass_config Config.draw_range = std::max( 2000.f, Config.draw_range ); // projection - auto const zfar = ( Zfar > 1.f ? Zfar : Config.draw_range * Zfar ); + auto zfar = ( Zfar > 1.f ? Zfar : Config.draw_range * Zfar ); auto const znear = ( Znear > 1.f ? Znear : Znear > 0.f ? Znear * zfar : 0.1f * Global.ZoomFactor); + if (EditorModeFlag && Viewport.main) + zfar = editor_track_view_range(zfar, true); camera.projection() = perspective_projection(Viewport.projection, znear, zfar, frustumtest_proj); + editor_ortho_projection(Viewport, zfar, camera.projection(), frustumtest_proj); break; } case rendermode::shadows: @@ -1689,6 +1752,8 @@ void opengl33_renderer::setup_pass(viewport_config &Viewport, renderpass_config // projection float znear = 0.1f * Global.ZoomFactor; float zfar = Config.draw_range * Global.fDistanceFactor; + if (EditorModeFlag && Viewport.main && Mode == rendermode::pickscenery) + zfar = editor_track_view_range(zfar, true); if (vr) { glm::mat4 transform = vr->get_pick_transform(); @@ -1709,12 +1774,15 @@ void opengl33_renderer::setup_pass(viewport_config &Viewport, renderpass_config } camera.projection() = perspective_projection(proj, znear, zfar, frustumtest_proj); + editor_ortho_projection(Viewport, zfar, camera.projection(), frustumtest_proj); break; } case rendermode::reflections: { // modelview - camera.position() = (((true == DebugCameraFlag) && (false == Ignoredebug)) ? Global.pDebugCamera.Pos : Global.pCamera.Pos); + // the faces of the map are drawn over a few frames. each is drawn from the place the camera was at + // when the update began, so they fit together along the edges when the camera moves in the meantime + camera.position() = m_environmentupdatelocation; glm::dvec3 const cubefacetargetvectors[6] = {{1.0, 0.0, 0.0}, {-1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, -1.0, 0.0}, {0.0, 0.0, 1.0}, {0.0, 0.0, -1.0}}; glm::dvec3 const cubefaceupvectors[6] = {{0.0, -1.0, 0.0}, {0.0, -1.0, 0.0}, {0.0, 0.0, 1.0}, {0.0, 0.0, -1.0}, {0.0, -1.0, 0.0}, {0.0, -1.0, 0.0}}; auto const cubefaceindex = m_environmentcubetextureface; @@ -2351,6 +2419,20 @@ IMaterial const *opengl33_renderer::Material(material_handle const Material) con return &m_materials.material(Material); } +material_handle opengl33_renderer::Terrain_Material(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement) +{ + auto const material{m_terrainmaterials.make(Reuse, Layers, Samples, Weights, Placement)}; + if (material != null_handle) + return material; + // the terrain shader can't be used, the chunk makes do with a plain material + return gfx_renderer::Terrain_Material(Reuse, Layers, Samples, Weights, Placement); +} + +void opengl33_renderer::Terrain_Release(material_handle const Material) +{ + m_terrainmaterials.release(Material); +} + opengl_material &opengl33_renderer::Material(material_handle const Material) { return m_materials.material(Material); @@ -2407,7 +2489,9 @@ void opengl33_renderer::Render(scene::basic_region *Region) glm::vec3 const cameraposition{m_renderpass.pass_camera.position()}; auto const camerax = static_cast(std::floor(cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); auto const cameraz = static_cast(std::floor(cameraposition.z / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2)); - int const segmentcount = 2 * static_cast(std::ceil(m_renderpass.draw_range * Global.fDistanceFactor / scene::EU07_SECTIONSIZE)); + float section_range = m_renderpass.draw_range * Global.fDistanceFactor; + section_range = editor_track_view_range(section_range, EditorModeFlag && (m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery)); + int const segmentcount = 2 * static_cast(std::ceil(section_range / scene::EU07_SECTIONSIZE)); int const originx = camerax - segmentcount / 2; int const originz = cameraz - segmentcount / 2; @@ -2603,6 +2687,9 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // cache initial iterator for the second sweep auto first{First}; + auto const scenery_limit{static_cast(Global.BaseDrawRange) * Global.fDistanceFactor + 250.0}; + bool const far_paths{EditorModeFlag && Global.editor_tracks && (m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery)}; + glm::dvec3 const camera{m_renderpass.pass_camera.position()}; // first pass draws elements which we know are located in section banks, to reduce vbo switching while (First != Last) { @@ -2610,6 +2697,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite auto *cell = First->second; // przeliczenia animacji torów w sektorze cell->RaAnimate(m_framestamp); + bool const tracks_only{editor_far_tracks_only(far_paths, cell, camera, scenery_limit)}; switch (m_renderpass.draw_mode) { @@ -2622,9 +2710,12 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // render // opaque non-instanced shapes - for (auto const &shape : cell->m_shapesopaque) + if (false == tracks_only) { - Render(shape, false); + for (auto const &shape : cell->m_shapesopaque) + { + Render(shape, false); + } } // tracks // TODO: update after path node refactoring @@ -2680,9 +2771,12 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite // render // opaque non-instanced shapes // non-interactive scenery elements get neutral colour - model_ubs.param[0] = colors::none; - for (auto const &shape : cell->m_shapesopaque) - Render(shape, false); + if (false == tracks_only) + { + model_ubs.param[0] = colors::none; + for (auto const &shape : cell->m_shapesopaque) + Render(shape, false); + } // tracks for (auto *path : cell->m_paths) { @@ -2713,6 +2807,8 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite case rendermode::color: case rendermode::shadows: { + if (editor_far_tracks_only(far_paths, cell, camera, scenery_limit)) + break; // TBD, TODO: refactor in to a method to reuse in branch below? // opaque parts of instanced models -- accumulate this cell's buckets // into the frame-level map; the actual Render_Instanced() calls are @@ -2745,10 +2841,7 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) ) / static_cast( Global.fDistanceFactor ) }; if( cellnearestdistancesquared <= sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) { - for( auto const &bucket : cell->m_instancebuckets_opaque ) { - auto &dest = m_frame_instance_buckets[ bucket.first ]; - dest.insert( dest.end(), bucket.second.begin(), bucket.second.end() ); - } + Queue_Instances( cell ); } } // remaining (non-instanceable) opaque instance nodes go through the per-node path @@ -2769,13 +2862,16 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite } case rendermode::reflections: { - if( Global.reflectiontune.fidelity >= 1 ) { + // a cell lying whole beyond the range the reflections draw model instances at is left out, as each + // of its instances would fail the range test done for it in Render_Instanced() and Render( TAnimModel * ). + // like the cull of the regular passes above, the test goes by the nearest point of the bounding sphere + // of the cell, which holds the locations of all its instances + auto const cellnearestdistance { std::max( 0.0, glm::length( cell->m_area.center - m_renderpass.pass_camera.position() ) - cell->m_area.radius ) }; + if( ( Global.reflectiontune.fidelity >= 1 ) + && ( cellnearestdistance <= Global.reflectiontune.range_instances ) ) { // opaque parts of instanced models -- accumulate into the // frame-level map; flushed once per unique model after the loop. - for( auto const &bucket : cell->m_instancebuckets_opaque ) { - auto &dest = m_frame_instance_buckets[ bucket.first ]; - dest.insert( dest.end(), bucket.second.begin(), bucket.second.end() ); - } + Queue_Instances( cell ); for( auto *instance : cell->m_instancesopaque ) { if( instance->m_instanceable ) { continue; } Render( instance ); @@ -2793,6 +2889,8 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite } case rendermode::pickscenery: { + if (editor_far_tracks_only(far_paths, cell, camera, scenery_limit)) + break; // opaque parts of instanced models // same procedure like with regular render, but each node receives custom colour used for picking. // picking always uses the per-instance path because each instance needs a unique pick colour; @@ -2830,7 +2928,13 @@ void opengl33_renderer::Render(cell_sequence::iterator First, cell_sequence::ite void opengl33_renderer::Draw_Geometry(std::vector::iterator begin, std::vector::iterator end) { - m_geometry.draw(begin, end); + if (begin == end) + { + return; + } + // the handles are handed over as a whole, so chunks kept in the same bank can be drawn with fewer calls + auto const *first{&(*begin)}; + m_geometry.draw_batch(first, first + std::distance(begin, end)); } void opengl33_renderer::Draw_Geometry(const gfx::geometrybank_handle &handle) @@ -3028,6 +3132,88 @@ void opengl33_renderer::Render(TAnimModel *Instance) } } +// adds the bucketed instances of a scene cell to the frame-level buckets, for Render_Instanced() to draw. +// Render_Instanced() culls each instance it's given. the tests it makes are repeated here on the copies of instance +// bounds kept by the buckets, with some slack for the single precision of the copies, and the instances which fail are +// left out: the instances themselves are fetched only when there's a chance for them to be drawn +void opengl33_renderer::Queue_Instances( scene::basic_cell const *Cell ) +{ + // the editor moves and resizes the instances in place, the copies can't be relied on while it's active + auto const usebounds { false == EditorModeFlag }; + auto const reflections { m_renderpass.draw_mode == rendermode::reflections }; + // same choices as in Render_Instanced(): shadows measure the distance from the real camera, reflections + // ignore zoom and distance factor, test the plain distance against their own range and add the fidelity offset + auto const &distancecamera { m_renderpass.draw_mode == rendermode::shadows ? m_renderpass.viewport_camera : m_renderpass.pass_camera }; + glm::dvec3 const cameraposition { distancecamera.position() }; + auto const distancescale { reflections ? 1.0 : 1.0 / ( static_cast( Global.ZoomFactor ) * static_cast( Global.ZoomFactor ) * static_cast( Global.fDistanceFactor ) ) }; + auto const distanceoffset { reflections ? sq( static_cast( EU07_REFLECTIONFIDELITYOFFSET ) ) : 0.0 }; + auto const slack { 1.001 }; + auto const distancelimit { slack * ( reflections ? sq( static_cast( Global.reflectiontune.range_instances ) ) : sq( static_cast( m_renderpass.draw_range ) + 250.0 ) ) }; + // up close the rounding of the copies is too large a part of the distance for the slack to cover it + auto const distancetested { sq( 64.0 ) }; + + for( auto const &bucket : Cell->m_instancebuckets_opaque ) { + auto const &instances { bucket.second.instances }; + if( false == usebounds ) { + auto &dest { m_frame_instance_buckets[ bucket.first ] }; + dest.insert( dest.end(), instances.begin(), instances.end() ); + continue; + } + if( ( bucket.second.boundsversion != m_instanceboundsversion ) + || ( bucket.second.bounds.size() != instances.size() ) ) { + Update_Instance_Bounds( bucket.first.pModel, bucket.second ); + } + auto const &bounds { bucket.second.bounds }; + std::vector *dest { nullptr }; // looked up when there's something to put in it + for( std::size_t idx = 0; idx < bounds.size(); ++idx ) { + auto const &instancebounds { bounds[ idx ] }; + auto const center { glm::dvec3{ instancebounds.center } }; + auto const distancesquared { glm::length2( center - cameraposition ) }; + if( distancesquared > distancetested ) { + auto const scaleddistancesquared { distancesquared * distancescale }; + if( scaleddistancesquared > distancelimit ) { continue; } + if( scaleddistancesquared + distanceoffset >= slack * instancebounds.rangesquaredmax ) { continue; } + } + if( false == m_renderpass.pass_camera.visible( scene::bounding_area{ center, instancebounds.radius } ) ) { continue; } + if( dest == nullptr ) { + dest = &m_frame_instance_buckets[ bucket.first ]; + } + dest->emplace_back( instances[ idx ] ); + } + } +} + +// makes the copies of the bounds of the instances held by a bucket of a scene cell +void opengl33_renderer::Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ) +{ + // a submodel is drawn only closer than its fSquareMaxDist, see Render( TSubModel * ). past the largest of these + // there's nothing of the model left to draw, which makes it another limit of the range of its instances + auto lodrangesquared { ( Model != nullptr && Model->Root != nullptr ) ? 0.f : std::numeric_limits::max() }; + m_instance_lodpending.clear(); + m_instance_lodpending.push_back( Model != nullptr ? Model->Root : nullptr ); + while( false == m_instance_lodpending.empty() ) { + auto const *submodel = m_instance_lodpending.back(); + m_instance_lodpending.pop_back(); + if( submodel == nullptr ) { continue; } + lodrangesquared = std::max( lodrangesquared, submodel->fSquareMaxDist ); + m_instance_lodpending.push_back( submodel->Child ); + m_instance_lodpending.push_back( submodel->Next ); + } + + Bucket.bounds.clear(); + Bucket.bounds.reserve( Bucket.instances.size() ); + for( auto const *instance : Bucket.instances ) { + scene::basic_cell::instance_bounds instancebounds; + if( instance != nullptr ) { + instancebounds.center = glm::vec3{ instance->m_area.center }; + instancebounds.radius = instance->m_area.radius; + instancebounds.rangesquaredmax = std::min( lodrangesquared, static_cast( std::min( instance->m_rangesquaredmax, static_cast( std::numeric_limits::max() ) ) ) ); + } + Bucket.bounds.emplace_back( instancebounds ); + } + Bucket.boundsversion = m_instanceboundsversion; +} + // True GPU-instanced render path for a group of TAnimModel instances sharing the same // TModel3d. The submodel tree is walked ONCE per batch; at every submodel that draws // geometry we issue a single glDrawElementsInstancedBaseVertex(N) covering all visible @@ -3213,17 +3399,17 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vectorupdate( reinterpret_cast( &identity ), 0, sizeof( identity ) ); - } - offset_idx += this_batch; ++m_renderpass.draw_stats.instanced_drawcalls; } } + // 3. Restore instance_modelview[0] to identity so subsequent + // non-instanced draws continue to compute identity * modelview. + // done once the last batch is out, as everything drawn up to that point is instanced + { + glm::mat4 const identity( 1.0f ); + instance_ubo->update( reinterpret_cast( &identity ), 0, sizeof( identity ) ); + } m_renderpass.draw_stats.instances += static_cast( m_instance_survivors.size() ); m_renderpass.draw_stats.models += static_cast( m_instance_survivors.size() ); @@ -3239,6 +3425,119 @@ void opengl33_renderer::Render_Instanced( TModel3d *Model, std::vector(Global.gfx_texture_releasedistance) * m_colorpass.draw_range}; + auto const camera{m_colorpass.pass_camera.position()}; + + m_textures.begin_release_scan(); + for (auto *vehicle : simulation::Vehicles.sequence()) + { + if (vehicle == nullptr) + { + continue; + } + bool const isnear{glm::length(vehicle->vPosition - camera) - vehicle->radius() <= range}; + auto const mark_model = [&](TModel3d *Model) { + if (Model == nullptr) + { + return; + } + for (auto const texture : Model_Textures(Model)) + { + m_textures.mark_as_needed(texture, isnear); + } + }; + mark_model(vehicle->mdModel); + mark_model(vehicle->mdLowPolyInt); + mark_model(vehicle->mdLoad); + for (auto *attachment : vehicle->mdAttachments) + { + mark_model(attachment); + } + for (auto const skin : vehicle->m_materialdata.replacable_skins) + { + if (skin <= 0) + { + continue; + } + auto const &material{m_materials.material(skin)}; + for (auto const texture : material.textures) + { + m_textures.mark_as_needed(texture, isnear); + } + for (auto const &variant : material.texture_variants) + { + for (auto const texture : variant) + { + m_textures.mark_as_needed(texture, isnear); + } + } + } + } + m_textures.release_unneeded(); +} + +// textures used by materials of the sub-models of specified model, replaceable skins excluded +std::vector const &opengl33_renderer::Model_Textures(TModel3d *Model) +{ + auto const lookup{m_modeltextures.find(Model)}; + if (lookup != m_modeltextures.end()) + { + return lookup->second; + } + auto &textures{m_modeltextures[Model]}; + std::vector submodels; + if (Model->GetSMRoot() != nullptr) + { + submodels.emplace_back(Model->GetSMRoot()); + } + while (false == submodels.empty()) + { + auto *submodel{submodels.back()}; + submodels.pop_back(); + if (submodel->NextGet() != nullptr) + { + submodels.emplace_back(submodel->NextGet()); + } + if (submodel->ChildGet() != nullptr) + { + submodels.emplace_back(submodel->ChildGet()); + } + // negative material is one of replaceable skins of the vehicle, these are handled with the vehicle + if (submodel->GetMaterial() <= 0) + { + continue; + } + auto const &material{m_materials.material(submodel->GetMaterial())}; + for (auto const texture : material.textures) + { + if (texture > 0) + { + textures.emplace_back(texture); + } + } + for (auto const &variant : material.texture_variants) + { + for (auto const texture : variant) + { + if (texture > 0) + { + textures.emplace_back(texture); + } + } + } + } + std::sort(std::begin(textures), std::end(textures)); + textures.erase(std::unique(std::begin(textures), std::end(textures)), std::end(textures)); + return textures; +} + void opengl33_renderer::Render_Sleepers( TTrack *Track ) { if( Track == nullptr ) { return; } @@ -3741,7 +4040,7 @@ void opengl33_renderer::Render(TSubModel *Submodel) { glDebug("Render TSubModel"); - if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist)) + if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist) && (false == m_previewing || Submodel->fVisible > 0.f)) { glm::mat4 future_stack = model_ubs.future; @@ -3960,6 +4259,31 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator } } + // geometry of neighbouring tracks which share the material is gathered and drawn together. + // only neighbours are combined, the tracks are still drawn in their order in the cell + material_handle batchmaterial{null_handle}; + auto const flush_batch = [&]() + { + if (false == m_pathbatch.empty()) + { + draw(std::begin(m_pathbatch), std::end(m_pathbatch)); + m_pathbatch.clear(); + } + batchmaterial = null_handle; + }; + // returns: true if the material starts a new batch, false if it continues the current one + auto const begin_batch = [&](material_handle const Material) + { + if (Material == batchmaterial) + { + return false; + } + flush_batch(); + Bind_Material(Material); + batchmaterial = Material; + return true; + }; + // TODO: render auto generated trackbeds together with regular trackbeds in pass 1, and all rails in pass 2 // first pass, material 1 for (auto first{First}; first != Last; ++first) @@ -3986,15 +4310,21 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator { if (track->eEnvironment != e_flat) { + // track with light level of its own is drawn on its own + flush_batch(); setup_environment_light(track->eEnvironment); - } - Bind_Material(track->m_material1); - draw(std::begin(track->Geometry1), std::end(track->Geometry1)); - if (track->eEnvironment != e_flat) - { + Bind_Material(track->m_material1); + draw(std::begin(track->Geometry1), std::end(track->Geometry1)); // restore default lighting setup_environment_light(); + break; } + if (false == begin_batch(track->m_material1)) + { + // the track is drawn with the call of its predecessor + --m_renderpass.draw_stats.drawcalls; + } + m_pathbatch.insert(std::end(m_pathbatch), std::begin(track->Geometry1), std::end(track->Geometry1)); break; } case rendermode::shadows: @@ -4002,10 +4332,14 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator if ((std::abs(track->fTexHeight1) < 0.35f) || (track->iCategoryFlag != 2)) { // shadows are only calculated for high enough roads, typically meaning track platforms + --m_renderpass.draw_stats.paths; + --m_renderpass.draw_stats.drawcalls; continue; } if( Material( track->m_material1 ).shadow_rank > Global.gfx_shadow_rank_cutoff ) { // skip if the shadow caster rank is too low for currently set threshold + --m_renderpass.draw_stats.paths; + --m_renderpass.draw_stats.drawcalls; continue; } Bind_Material_Shadow(track->m_material1); @@ -4020,6 +4354,7 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator } } } + flush_batch(); // second pass, material 2 for (auto first{First}; first != Last; ++first) { @@ -4041,15 +4376,16 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator { if (track->eEnvironment != e_flat) { + flush_batch(); setup_environment_light(track->eEnvironment); - } - Bind_Material(track->m_material2); - draw(std::begin(track->Geometry2), std::end(track->Geometry2)); - if (track->eEnvironment != e_flat) - { + Bind_Material(track->m_material2); + draw(std::begin(track->Geometry2), std::end(track->Geometry2)); // restore default lighting setup_environment_light(); + break; } + begin_batch(track->m_material2); + m_pathbatch.insert(std::end(m_pathbatch), std::begin(track->Geometry2), std::end(track->Geometry2)); break; } case rendermode::shadows: @@ -4075,6 +4411,7 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator } } } + flush_batch(); // third pass, material 3 for (auto first{First}; first != Last; ++first) @@ -4102,15 +4439,16 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator { if (track->eEnvironment != e_flat) { + flush_batch(); setup_environment_light(track->eEnvironment); - } - Bind_Material(track->SwitchExtension->m_material3); - draw(track->SwitchExtension->Geometry3); - if (track->eEnvironment != e_flat) - { + Bind_Material(track->SwitchExtension->m_material3); + draw(track->SwitchExtension->Geometry3); // restore default lighting setup_environment_light(); + break; } + begin_batch(track->SwitchExtension->m_material3); + m_pathbatch.emplace_back(track->SwitchExtension->Geometry3); break; } case rendermode::shadows: @@ -4136,6 +4474,7 @@ void opengl33_renderer::Render(scene::basic_cell::path_sequence::const_iterator } } } + flush_batch(); // fourth pass: per-track sleeper models (sleepermodel optional directive). // drawn after rails/trackbeds so depth pre-pass culling is favourable, and only in passes @@ -4492,6 +4831,55 @@ void opengl33_renderer::Render_Alpha(TTraction *Traction) glLineWidth(1.0f); } +void opengl33_renderer::Render_editor_overlay() +{ + if (false == EditorModeFlag || EditorOverlay.batches.empty()) + return; + glDebug("Render_editor_overlay"); + + if (m_editor_overlay_revision != EditorOverlay.revision) + { + m_editor_overlay_revision = EditorOverlay.revision; + if (m_editor_overlay_bank == null_handle) + m_editor_overlay_bank = Create_Bank(); + auto const &batches{EditorOverlay.batches}; + for (std::size_t i = 0; i < batches.size(); ++i) + { + gfx::vertex_array vertices; + vertices.reserve(batches[i].points.size()); + for (auto const &point : batches[i].points) + vertices.emplace_back(point, glm::vec3(0.f, 1.f, 0.f), glm::vec2()); + gfx::userdata_array userdata; + if (i >= m_editor_overlay_geometry.size()) + m_editor_overlay_geometry.emplace_back(); + auto &geometry{m_editor_overlay_geometry[i]}; + if (geometry != null_handle) + m_geometry.replace(vertices, userdata, geometry); + else if (false == vertices.empty()) + geometry = m_geometry.create_chunk(vertices, userdata, m_editor_overlay_bank, batches[i].type); + } + } + + ::glPushMatrix(); + auto const origin{EditorOverlay.origin - m_renderpass.pass_camera.position()}; + ::glTranslated(origin.x, origin.y, origin.z); + ::glDisable(GL_CULL_FACE); + ::glDepthMask(GL_FALSE); + m_line_shader->bind(); + auto const &batches{EditorOverlay.batches}; + for (std::size_t i = 0; i < batches.size() && i < m_editor_overlay_geometry.size(); ++i) + { + if (batches[i].points.empty() || m_editor_overlay_geometry[i] == null_handle) + continue; + model_ubs.param[0] = batches[i].color; + draw(m_editor_overlay_geometry[i]); + ++m_renderpass.draw_stats.drawcalls; + } + ::glDepthMask(GL_TRUE); + ::glEnable(GL_CULL_FACE); + ::glPopMatrix(); +} + void opengl33_renderer::Render_Alpha(scene::lines_node const &Lines) { glDebug("Render_Alpha scene::lines_node"); @@ -4668,7 +5056,7 @@ void opengl33_renderer::Render_Alpha_text(TSubModel *Submodel) void opengl33_renderer::Render_Alpha(TSubModel *Submodel) { // renderowanie przezroczystych przez DL - if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist)) + if ((Submodel->iVisible) && (TSubModel::fSquareDist >= Submodel->fSquareMinDist) && (TSubModel::fSquareDist < Submodel->fSquareMaxDist) && (false == m_previewing || Submodel->fVisible > 0.f)) { glm::mat4 future_stack = model_ubs.future; @@ -5099,6 +5487,7 @@ glm::dvec3 opengl33_renderer::Update_Mouse_Position() if (pointdepth != std::numeric_limits::max()) { + m_mousehit = (GLAD_GL_ARB_clip_control || GLAD_GL_EXT_clip_control) ? pointdepth > 0.0f : pointdepth < 1.0f; if (GLAD_GL_ARB_clip_control || GLAD_GL_EXT_clip_control) { if (pointdepth > 0.0f) m_worldmousecoordinates = glm::unProjectZO(glm::vec3(bufferpos, pointdepth), glm::mat4(glm::mat3(m_colorpass.pass_camera.modelview())), m_colorpass.pass_camera.projection(), @@ -5170,6 +5559,11 @@ void opengl33_renderer::Update(double const Deltatime) m_textures.update(); } + if ((Global.gfx_texture_releasedistance > 0.f) && (true == simulation::is_ready)) + { + Update_Texture_Release(); + } + if ((true == Global.ControlPicking) && (false == FreeFlyModeFlag)) Pick_Control_Callback([](const TSubModel *, const glm::vec2) {}); // temporary conditions for testing. eventually will be coupled with editor mode @@ -5213,6 +5607,414 @@ void opengl33_renderer::MakeScreenshot() screenshot_manager::make_screenshot(); } +// model previews + +namespace +{ +// direction from the previewed model to the camera. models are made facing -z (the front view of 3ds max), +// so the camera looks at their front, right side and top +glm::dvec3 const preview_viewdirection{-0.6123724, 0.5, -0.6123724}; +// vertical field of view of the preview camera, in degrees +float const preview_fieldofview{30.f}; +// brightness of the hdr background, past the white point of the tonemapping, so translucent parts blend with white +float const preview_background{16.f}; +} // namespace + +void opengl33_renderer::Preview_Bounds(TSubModel const *Submodel, glm::dmat4 const &Transform, glm::dvec3 &Min, glm::dvec3 &Max) const +{ + // same walk as the one of Render(TSubModel *) bar the range checks, which leave out parts only for the bounds to be smaller + for (auto const *submodel{Submodel}; submodel != nullptr; submodel = submodel->Next) + { + if ((submodel->iVisible == 0) || (submodel->fVisible <= 0.f)) + { + continue; + } + auto transform{Transform}; + if ((submodel->iFlags & 0xC000) && (submodel->fMatrix != nullptr)) + { + transform *= glm::dmat4{glm::make_mat4(submodel->fMatrix->readArray())}; + } + if ((submodel->eType < TP_ROTATOR) && (submodel->m_geometry.handle != null_handle)) + { + for (auto const &vertex : m_geometry.vertices(submodel->m_geometry.handle)) + { + auto const point{glm::dvec3{transform * glm::dvec4{glm::dvec3{vertex.position}, 1.0}}}; + Min = glm::min(Min, point); + Max = glm::max(Max, point); + } + } + Preview_Bounds(submodel->Child, transform, Min, Max); + } +} + +bool opengl33_renderer::Preview_Targets(int const Size) +{ + if (m_preview.size == Size) + { + return true; + } + m_preview = preview_targets(); + + m_preview.hdr_tex = std::make_unique(); + m_preview.hdr_tex->alloc_rendertarget(GL_RGBA16F, GL_RGBA, Size, Size, 1, 1, GL_CLAMP_TO_EDGE); + m_preview.hdr_rbds = std::make_unique(); + m_preview.hdr_rbds->alloc(GL_DEPTH32F_STENCIL8, Size, Size); + m_preview.hdr_fb = std::make_unique(); + m_preview.hdr_fb->attach(*m_preview.hdr_tex, GL_COLOR_ATTACHMENT0); + m_preview.hdr_fb->attach(*m_preview.hdr_rbds, GL_DEPTH_STENCIL_ATTACHMENT); + m_preview.hdr_fb->setup_drawing(1); + if (false == m_preview.hdr_fb->is_complete()) + { + ErrorLog("preview framebuffer setup failed"); + m_preview = preview_targets(); + return false; + } + // the tonemapping writes srgb values, either by itself or through the srgb target + m_preview.ldr_tex = std::make_unique(); + m_preview.ldr_tex->alloc_rendertarget((Global.gfx_shadergamma ? GL_RGBA8 : GL_SRGB8_ALPHA8), GL_RGBA, Size, Size, 1, 1, GL_CLAMP_TO_EDGE); + m_preview.ldr_fb = std::make_unique(); + m_preview.ldr_fb->attach(*m_preview.ldr_tex, GL_COLOR_ATTACHMENT0); + m_preview.ldr_fb->setup_drawing(1); + if (false == m_preview.ldr_fb->is_complete()) + { + ErrorLog("preview framebuffer setup failed"); + m_preview = preview_targets(); + return false; + } + gl::framebuffer::unbind(); + + m_preview.size = Size; + return true; +} + +void opengl33_renderer::Preview_Model(TAnimModel *Instance, float const Squaredistance, bool const Alpha) +{ + // the placement of Render(TAnimModel *), without its range and visibility tests + ::glPushMatrix(); + auto const position{Instance->location() - m_renderpass.pass_camera.position()}; + ::glTranslated(position.x, position.y, position.z); + auto const &angle{Instance->vAngle}; + if (angle.y != 0.0) + ::glRotated(angle.y, 0.f, 1.f, 0.f); + if (angle.x != 0.0) + ::glRotated(angle.x, 1.f, 0.f, 0.f); + if (angle.z != 0.0) + ::glRotated(angle.z, 0.f, 0.f, 1.f); + auto const &scale{Instance->Scale()}; + ::glScalef(scale.x, scale.y, scale.z); + if (Alpha) + Render_Alpha(Instance->pModel, Instance->Material(), Squaredistance); + else + Render(Instance->pModel, Instance->Material(), Squaredistance); + ::glPopMatrix(); +} + +void opengl33_renderer::Preview_Color(TAnimModel *Instance, float const Squaredistance, int const Size, glm::dvec3 const &Eye, glm::mat4 const &View, glm::mat4 const &Projection, bool const Shadows) +{ + m_renderpass.draw_mode = rendermode::color; + m_renderpass.pass_camera.position() = Eye; + m_renderpass.pass_camera.modelview() = View; + m_renderpass.pass_camera.projection() = Projection; + m_renderpass.viewport_camera.position() = Eye; + setup_matrices(); + + scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + scene_ubs.inv_view = glm::inverse(glm::mat4{glm::mat3{View}}); + scene_ubs.time = 0.f; + for (auto idx = 0; idx < m_shadowpass.size(); ++idx) + { + // everything is in the first cascade + scene_ubs.cascade_end[idx] = std::numeric_limits::max(); + } + scene_ubo->update(scene_ubs); + scene_ubo->bind_uniform(); + // the sun is the only light, and there's no fog + auto const &modelview{OpenGLMatrices.data(GL_MODELVIEW)}; + light_ubs.ambient = m_sunlight.ambient; + light_ubs.fog_color = glm::vec3{1.f}; + light_ubs.lights_count = 1; + light_ubs.lights[0].type = gl::light_element_ubs::DIR; + light_ubs.lights[0].dir = modelview * glm::vec4(m_sunlight.direction, 0.0f); + light_ubs.lights[0].color = m_sunlight.diffuse; + light_ubs.lights[0].ambient = 0.0f; + light_ubs.lights[0].intensity = 1.0f; + light_ubo->update(light_ubs); + model_ubs.fog_density = 0.f; + model_ubs.future = glm::mat4(); + model_ubs.emission = 0.f; + if (Shadows) + { + setup_shadow_bind_map(); + setup_shadow_color(colors::shadow); + } + else + { + setup_shadow_unbind_map(); + setup_shadow_color(colors::white); + } + setup_env_map(nullptr); + + m_preview.hdr_fb->bind(); + glViewport(0, 0, Size, Size); + glClearColor(preview_background, preview_background, preview_background, 1.f); + glClearStencil(0); + glStencilMask(0xFF); + m_preview.hdr_fb->clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); + if (!Global.gfx_usegles) + glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); + glEnable(GL_DEPTH_TEST); + // every fragment which made it past the alpha test marks the pixel as covered, translucent ones included + glEnable(GL_STENCIL_TEST); + glStencilFunc(GL_ALWAYS, 1, 0xFF); + glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); + + setup_drawing(false); + Preview_Model(Instance, Squaredistance, false); + setup_drawing(true); + Preview_Model(Instance, Squaredistance, true); + setup_drawing(false); + + glDisable(GL_STENCIL_TEST); + setup_shadow_unbind_map(); +} + +bool opengl33_renderer::Render_Preview(TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector &Image) +{ + if ((Instance == nullptr) || (Instance->pModel == nullptr) || (Size <= 0) || (Margin < 0) || (Margin * 2 >= Size)) + { + return false; + } + // the image is drawn at double size and scaled down, for antialiased edges which blend with the white background + GLint maxtexturesize{0}, maxrenderbuffersize{0}; + GLint maxviewport[2]{0, 0}; + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxtexturesize); + glGetIntegerv(GL_MAX_RENDERBUFFER_SIZE, &maxrenderbuffersize); + glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxviewport); + auto const maxsize{std::min({maxtexturesize, maxrenderbuffersize, maxviewport[0], maxviewport[1]})}; + if (Size > maxsize) + { + ErrorLog("preview size " + std::to_string(Size) + " exceeds the limit of the graphics driver, " + std::to_string(maxsize)); + return false; + } + auto const supersampling{(Size * 2 <= maxsize ? 2 : 1)}; + auto const targetsize{Size * supersampling}; + if (false == Preview_Targets(targetsize)) + { + return false; + } + + glfwMakeContextCurrent(m_window); + opengl_texture::reset_unit_cache(); + gfx::opengl33_vaogeometrybank::reset(); + m_previewing = true; + // lights and visibility of the submodels, for this instance + Instance->RaAnimate(m_framestamp); + Instance->RaPrepare(); + + // bounding sphere of the model, which the camera is placed to fit in its view + glm::dvec3 boundsmin{std::numeric_limits::max()}; + glm::dvec3 boundsmax{std::numeric_limits::lowest()}; + Preview_Bounds(Instance->pModel->Root, glm::translate(glm::dmat4{1.0}, Instance->location()) * glm::dmat4{Instance->rotation_scale()}, boundsmin, boundsmax); + if (boundsmin.x > boundsmax.x) + { + m_previewing = false; + return false; + } + auto const centre{(boundsmin + boundsmax) * 0.5}; + auto const radius{std::max(0.01, glm::length(boundsmax - boundsmin) * 0.5)}; + auto const fieldofview{glm::radians(preview_fieldofview)}; + auto const distance{radius / std::sin(fieldofview * 0.5)}; + auto const eye{centre + glm::normalize(preview_viewdirection) * distance}; + glm::mat4 const view{glm::lookAt(eye, centre, glm::dvec3{0.0, 1.0, 0.0})}; + // the sphere is given some slack for parts which aren't where their vertices are, like billboards turning to the camera + auto const znear{static_cast(std::max(distance - radius * 1.5, distance * 0.05))}; + auto const zfar{static_cast(distance + radius * 1.5)}; + auto const projection{perspective_projection_raw(fieldofview, 1.f, znear, zfar)}; + // level of detail as seen from the camera, like in Render(TAnimModel *) + auto const squaredistance{static_cast(glm::length2(Instance->location() - eye) / Global.fDistanceFactor)}; + + // key light from above the left shoulder of the viewer, so the visible sides of the model differ in tone + auto const forward{glm::normalize(centre - eye)}; + auto const right{glm::normalize(glm::cross(forward, glm::dvec3{0.0, 1.0, 0.0}))}; + auto const tosun{glm::normalize(-forward * 0.4 + glm::dvec3{0.0, 1.0, 0.0} - right * 0.9)}; + m_sunlight.direction = glm::vec3{-tosun}; + m_sunlight.diffuse = glm::vec4{1.0f, 0.9686275f, 0.9411765f, 1.f}; + m_sunlight.ambient = glm::vec4{0.65f, 0.66f, 0.68f, 1.f}; + m_sunlight.factor = 1.f; + + auto const shadows{Shadows && Global.gfx_shadowmap_enabled && (m_shadow_fb != nullptr)}; + if (shadows) + { + // one map covering the whole model, seen from the sun + m_renderpass.draw_mode = rendermode::shadows; + m_renderpass.pass_camera.position() = centre; + m_renderpass.pass_camera.modelview() = glm::mat4{glm::lookAt(centre, centre + glm::dvec3{m_sunlight.direction}, glm::dvec3{0.0, 1.0, 0.0})}; + auto const extent{static_cast(radius * 1.5)}; + m_renderpass.pass_camera.projection() = ortho_projection(-extent, extent, -extent, extent, -extent, extent); + m_renderpass.viewport_camera.position() = eye; + setup_matrices(); + scene_ubs.projection = OpenGLMatrices.data(GL_PROJECTION); + scene_ubo->update(scene_ubs); + + glViewport(0, 0, m_shadowbuffersize, m_shadowbuffersize); + m_shadow_fb->attach(*m_shadow_tex, GL_DEPTH_ATTACHMENT, 0); + m_shadow_fb->clear(GL_DEPTH_BUFFER_BIT); + glEnable(GL_DEPTH_TEST); + setup_drawing(false); + Preview_Model(Instance, squaredistance, false); + m_shadow_fb->unbind(); + for (auto &pass : m_shadowpass) + { + pass = m_renderpass; + } + } + + // the view is cropped to the part of the image the model covers: a look from a bit further first, to find it, + // then a closer one at the full size, as thin parts can show up only there, to refine the crop to a pixel. + // the crop maps the area to the image less the margins + std::vector coverage; + auto const fitsize{targetsize}; + auto const area{1.0 - 2.0 * Margin / Size}; // half of the extent of the area for the model, in normalized device coordinates + glm::dvec2 cropcentre{0.0}; + auto cropscale{0.75}; + auto tight{false}; // whether the crop was fitted to the model + auto const cropped = [&]() { + return glm::mat4{glm::scale(glm::dmat4{1.0}, glm::dvec3{cropscale, cropscale, 1.0}) * glm::translate(glm::dmat4{1.0}, glm::dvec3{-cropcentre, 0.0})} * projection; + }; + for (auto attempt = 0; attempt < 6; ++attempt) + { + auto const passsize{(tight ? fitsize : std::min(fitsize, 512))}; + Preview_Color(Instance, squaredistance, passsize, eye, view, cropped(), shadows); + coverage.resize(static_cast(passsize) * passsize); + m_preview.hdr_fb->bind(); + glReadPixels(0, 0, passsize, passsize, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, coverage.data()); + + glm::ivec2 pixelmin{passsize, passsize}, pixelmax{-1, -1}; + for (auto y = 0; y < passsize; ++y) + { + auto const *row{coverage.data() + static_cast(y) * passsize}; + for (auto x = 0; x < passsize; ++x) + { + if (row[x] != 0) + { + pixelmin = glm::min(pixelmin, glm::ivec2{x, y}); + pixelmax = glm::max(pixelmax, glm::ivec2{x, y}); + } + } + } + if (pixelmax.x < 0) + { + if (attempt == 0) + { + // nothing to see + m_previewing = false; + m_renderpass.draw_mode = rendermode::none; + gl::framebuffer::unbind(); + return false; + } + // the crop missed the model, start over with the plain view + cropcentre = glm::dvec2{0.0}; + cropscale = 0.5; + tight = false; + continue; + } + // covered area in normalized device coordinates of the uncropped view + auto const tondc = [&](glm::ivec2 const &Pixel) { return (glm::dvec2{-1.0} + 2.0 * glm::dvec2{Pixel} / static_cast(passsize)) / cropscale + cropcentre; }; + auto const ndcmin{tondc(pixelmin)}; + auto const ndcmax{tondc(pixelmax + 1)}; + cropcentre = (ndcmin + ndcmax) * 0.5; + // a model fitted to the area keeps clear of the image edges, unless the margin is too thin to tell + auto const clipped{(pixelmin.x == 0) || (pixelmin.y == 0) || (pixelmax.x == passsize - 1) || (pixelmax.y == passsize - 1)}; + if (clipped && ((false == tight) || (Margin * passsize / Size >= 2))) + { + // part of the model is out of the view, look again from further away + cropscale *= 0.5; + tight = false; + continue; + } + cropscale = 2.0 * area / std::max(ndcmax.x - ndcmin.x, ndcmax.y - ndcmin.y); + if (tight) + { + // measured through a fitted crop, which makes it accurate to a pixel of the fitting pass + break; + } + tight = true; + } + + // the image itself + Preview_Color(Instance, squaredistance, targetsize, eye, view, cropped(), shadows); + glViewport(0, 0, targetsize, targetsize); + if (!Global.gfx_usegles && !Global.gfx_shadergamma) + glEnable(GL_FRAMEBUFFER_SRGB); + m_pfx_tonemapping->apply(*m_preview.hdr_tex, m_preview.ldr_fb.get()); + if (!Global.gfx_usegles && !Global.gfx_shadergamma) + glDisable(GL_FRAMEBUFFER_SRGB); + opengl_texture::reset_unit_cache(); + // the post processing binds its own vertex array, which the geometry banks don't know about + gfx::opengl33_vaogeometrybank::reset(); + + auto const targetpixels{static_cast(targetsize) * targetsize}; + std::vector colors(targetpixels * 4); + m_preview.ldr_fb->bind(); + glReadPixels(0, 0, targetsize, targetsize, GL_RGBA, GL_UNSIGNED_BYTE, colors.data()); + coverage.resize(targetpixels); + m_preview.hdr_fb->bind(); + glReadPixels(0, 0, targetsize, targetsize, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, coverage.data()); + gl::framebuffer::unbind(); + glDepthMask(GL_TRUE); + + m_previewing = false; + m_renderpass.draw_mode = rendermode::none; + + // uncovered pixels are white, the samples are averaged in linear space and the rows flipped to go from the top + static auto const tolinear{[]() { + std::array table; + for (auto idx = 0; idx < 256; ++idx) + { + auto const value{idx / 255.f}; + table[idx] = (value <= 0.04045f ? value / 12.92f : std::pow((value + 0.055f) / 1.055f, 2.4f)); + } + return table; + }()}; + auto const tosrgb = [](float const Value) { + auto const value{std::clamp(Value, 0.f, 1.f)}; + auto const encoded{(value <= 0.0031308f ? value * 12.92f : 1.055f * std::pow(value, 1.f / 2.4f) - 0.055f)}; + return static_cast(std::lround(encoded * 255.f)); + }; + auto const samplecount{static_cast(supersampling * supersampling)}; + Image.resize(static_cast(Size) * Size * 3); + for (auto y = 0; y < Size; ++y) + { + auto *output{Image.data() + static_cast(y) * Size * 3}; + auto const sourcey{(Size - 1 - y) * supersampling}; + for (auto x = 0; x < Size; ++x) + { + glm::vec3 color{0.f}; + for (auto sy = 0; sy < supersampling; ++sy) + { + for (auto sx = 0; sx < supersampling; ++sx) + { + auto const sample{static_cast(sourcey + sy) * targetsize + x * supersampling + sx}; + if (coverage[sample] != 0) + { + auto const *rgba{colors.data() + sample * 4}; + color += glm::vec3{tolinear[rgba[0]], tolinear[rgba[1]], tolinear[rgba[2]]}; + } + else + { + color += glm::vec3{1.f}; + } + } + } + color /= samplecount; + output[x * 3 + 0] = tosrgb(color.r); + output[x * 3 + 1] = tosrgb(color.g); + output[x * 3 + 2] = tosrgb(color.b); + } + } + return true; +} + void opengl33_renderer::Update_Lights(light_array &Lights) { glDebug("Update_Lights"); @@ -5538,3 +6340,33 @@ void opengl33_renderer::opengl33_imgui_renderer::Render() ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); } +std::uint64_t opengl33_renderer::opengl33_imgui_renderer::Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) +{ + // the ui colours are made linear in its shaders, the image is too, by sampling it as srgb. the bindings the renderer + // keeps track of are put back as they were + GLint texture{0}, unpack{0}, alignment{4}; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture); + glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &unpack); + glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + GLuint image{0}; + glGenTextures(1, &image); + glBindTexture(GL_TEXTURE_2D, image); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8_ALPHA8, Width, Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, Rgba); + glBindTexture(GL_TEXTURE_2D, static_cast(texture)); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast(unpack)); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); + return image; +} + +void opengl33_renderer::opengl33_imgui_renderer::Release_Image(std::uint64_t const Image) +{ + auto const image{static_cast(Image)}; + glDeleteTextures(1, &image); +} + diff --git a/rendering/opengl33renderer.h b/rendering/opengl33renderer.h index 562670d9..64574420 100644 --- a/rendering/opengl33renderer.h +++ b/rendering/opengl33renderer.h @@ -29,6 +29,7 @@ http://mozilla.org/MPL/2.0/. #include "gl/glsl_common.h" #include "gl/pbo.h" #include "gl/query.h" +#include "rendering/opengl33terrain.h" // bare-bones render controller, in lack of anything better yet class opengl33_renderer : public gfx_renderer { @@ -81,6 +82,10 @@ class opengl33_renderer : public gfx_renderer { Bind_Material( material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr ) override; IMaterial const * Material( material_handle const Material ) const override; + material_handle + Terrain_Material( material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) override; + void + Terrain_Release( material_handle const Material ) override; // shader methods auto Fetch_Shader( std::string const &name ) -> std::shared_ptr override; // texture methods @@ -105,6 +110,8 @@ class opengl33_renderer : public gfx_renderer { Pick_Node() const override { return m_picksceneryitem; } glm::dvec3 Mouse_Position() const override { return m_worldmousecoordinates; } + bool + Mouse_Hit() const override { return m_mousehit; } glm::mat4 Camera_View_Matrix() const override { return glm::mat4( glm::mat3( m_colorpass.pass_camera.modelview() ) ); } glm::mat4 @@ -128,6 +135,7 @@ class opengl33_renderer : public gfx_renderer { std::string const & info_stats() const override; void MakeScreenshot() override; + bool Render_Preview( TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector &Image ) override; @@ -282,6 +290,11 @@ class opengl33_renderer : public gfx_renderer { // instanced_drawcall in draw_stats and contributes Instances.size() to the // instances counter. Instances are still individually frustum/distance culled. void Render_Instanced( TModel3d *Model, std::vector const &Instances ); + // adds the bucketed instances of a scene cell to the frame-level buckets drawn by Render_Instanced(), + // less the ones the copies of their bounds show to be out of sight in the current render pass + void Queue_Instances( scene::basic_cell const *Cell ); + // makes the copies of the bounds of the instances held by a bucket of a scene cell + void Update_Instance_Bounds( TModel3d const *Model, scene::basic_cell::instance_bucket const &Bucket ); bool Render(TDynamicObject *Dynamic); bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); bool Render(TModel3d *Model, material_data const *Material, float const Squaredistance); @@ -297,6 +310,10 @@ class opengl33_renderer : public gfx_renderer { bool Render_interior( bool const Alpha = false ); bool Render_lowpoly( TDynamicObject *Dynamic, float const Squaredistance, bool const Setup, bool const Alpha = false ); bool Render_coupler_adapter( TDynamicObject *Dynamic, float const Squaredistance, int const End, bool const Alpha = false ); + // releases gl textures used only by vehicles farther than gfx.textures.releasedistance times the draw range + void Update_Texture_Release(); + // textures used by materials of the sub-models of specified model, replaceable skins excluded + std::vector const &Model_Textures( TModel3d *Model ); void Render(TMemCell *Memcell); void Render_particles(); void Render_precipitation(); @@ -306,6 +323,7 @@ class opengl33_renderer : public gfx_renderer { void Render_Alpha(TAnimModel *Instance); void Render_Alpha(TTraction *Traction); void Render_Alpha(scene::lines_node const &Lines); + void Render_editor_overlay(); bool Render_Alpha(TDynamicObject *Dynamic); bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance, glm::dvec3 const &Position, glm::vec3 const &Angle); bool Render_Alpha(TModel3d *Model, material_data const *Material, float const Squaredistance); @@ -322,11 +340,22 @@ class opengl33_renderer : public gfx_renderer { void draw_debug_ui(); + // model previews + // extends provided bounds with the geometry of the submodel and its siblings and children visible in a preview + void Preview_Bounds( TSubModel const *Submodel, glm::dmat4 const &Transform, glm::dvec3 &Min, glm::dvec3 &Max ) const; + // prepares the offscreen targets of model previews for square images of specified size + bool Preview_Targets( int const Size ); + // draws the instance on its own with the current render pass settings + void Preview_Model( TAnimModel *Instance, float const Squaredistance, bool const Alpha ); + // draws the lit instance into the hdr preview target, marking in its stencil the pixels covered by the model + void Preview_Color( TAnimModel *Instance, float const Squaredistance, int const Size, glm::dvec3 const &Eye, glm::mat4 const &View, glm::mat4 const &Projection, bool const Shadows ); + // members GLFWwindow *m_window{nullptr}; // main window gfx::geometrybank_manager m_geometry; material_manager m_materials; texture_manager m_textures; + opengl33_terrain_materials m_terrainmaterials{m_materials, m_textures}; opengl33_light m_sunlight; opengllight_array m_lights; /* @@ -344,6 +373,8 @@ class opengl33_renderer : public gfx_renderer { glm::mat4 m_shadowtexturematrix; // conversion from camera-centric world space to light-centric clip space int m_environmentcubetextureface{0}; // helper, currently processed cube map face + int m_environmentupdateface{6}; // cube map face the environment map update in progress draws next; 6: no update in progress + bool m_environmentready{false}; // the environment map was drawn whole at least once double m_environmentupdatetime{0}; // time of the most recent environment map update glm::dvec3 m_environmentupdatelocation; // coordinates of most recent environment map update opengl33_skydome m_skydomerenderer; @@ -379,6 +410,13 @@ class opengl33_renderer : public gfx_renderer { // of once per cell -- collapsing many tiny instanced draws into a few large // batches. Reused every pass; the vectors are emptied (not freed) at the top of Render(scene::basic_region*). scene::basic_cell::instance_bucket_map m_frame_instance_buckets; + // textures used by vehicle models, for the release of far textures + std::unordered_map> m_modeltextures; + // geometry of neighbouring tracks of a cell which share the material, gathered to be drawn together. reused by each Render(path_sequence) + std::vector m_pathbatch; + // copies of instance bounds kept by the buckets of scene cells are good as long as they carry this number. + // it changes with each frame drawn with the scenery editor active, as the editor moves and resizes instances in place + unsigned int m_instanceboundsversion { 1 }; renderpass_config m_colorpass; // parametrs of most recent color pass std::array m_shadowpass; // parametrs of most recent shadowmap pass for each of csm stages std::vector m_pickcontrolsitems; @@ -387,6 +425,7 @@ class opengl33_renderer : public gfx_renderer { std::vector m_picksceneryitems; scene::basic_node *m_picksceneryitem{nullptr}; glm::vec3 m_worldmousecoordinates { 0.f }; + bool m_mousehit { false }; #ifdef EU07_USE_DEBUG_CAMERA renderpass_config m_worldcamera; // debug item #endif @@ -400,6 +439,7 @@ class opengl33_renderer : public gfx_renderer { glm::mat4 perspective_projection(const viewport_proj_config &c, float n, float f, glm::mat4 &frustum); glm::mat4 ortho_projection(float left, float right, float bottom, float top, float z_near, float z_far); glm::mat4 ortho_frustumtest_projection(float left, float right, float bottom, float top, float z_near, float z_far); + void editor_ortho_projection(viewport_config const &Viewport, float const Zfar, glm::mat4 &Projection, glm::mat4 &Frustum); std::vector> m_control_pick_requests; std::vector> m_node_pick_requests; @@ -449,6 +489,9 @@ class opengl33_renderer : public gfx_renderer { std::unordered_map> m_shaders; std::unique_ptr m_line_shader; + gfx::geometrybank_handle m_editor_overlay_bank; + std::vector m_editor_overlay_geometry; + unsigned int m_editor_overlay_revision{0}; std::unique_ptr m_freespot_shader; std::unique_ptr m_billboard_shader; std::unique_ptr m_celestial_shader; @@ -521,6 +564,19 @@ class opengl33_renderer : public gfx_renderer { std::unique_ptr vr; bool debug_ui_active = false; + // offscreen targets of model previews + struct preview_targets + { + int size { 0 }; + std::unique_ptr hdr_tex; + std::unique_ptr hdr_rbds; // depth and stencil, the stencil marks pixels covered by the model + std::unique_ptr hdr_fb; + std::unique_ptr ldr_tex; + std::unique_ptr ldr_fb; + } m_preview; + // set while a preview is drawn: submodels faded out entirely are skipped, instead of being drawn invisible over the stencil + bool m_previewing { false }; + static bool renderer_register; class opengl33_imgui_renderer : public imgui_renderer @@ -529,6 +585,8 @@ class opengl33_renderer : public gfx_renderer { virtual void Shutdown() override; virtual void BeginFrame() override; virtual void Render() override; + virtual std::uint64_t Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) override; + virtual void Release_Image(std::uint64_t const Image) override; } m_imgui_renderer; virtual imgui_renderer* GetImguiRenderer() override { diff --git a/rendering/opengl33terrain.cpp b/rendering/opengl33terrain.cpp new file mode 100644 index 00000000..e8ab570b --- /dev/null +++ b/rendering/opengl33terrain.cpp @@ -0,0 +1,440 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "rendering/opengl33terrain.h" + +#include "rendering/opengl33geometrybank.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" + +namespace +{ + +std::size_t constexpr max_layers{8}; +int constexpr initial_capacity{8}; +int constexpr copy_attempts{600}; // frames to wait for the textures of a layer to load before it's left with what it has +float constexpr default_repeat{8.f}; + +// array textures: diffuse, normal maps, specular/gloss maps +std::array constexpr array_sizes{1024, 512, 256}; + +void bind_for_upload(opengl_texture const &Texture) +{ + ::glActiveTexture(GL_TEXTURE0); + ::glBindTexture(Texture.target, Texture.id); +} + +void set_anisotropy(GLenum const Target) +{ + if (Global.AnisotropicFiltering >= 0 && (GLAD_GL_EXT_texture_filter_anisotropic || GLAD_GL_ARB_texture_filter_anisotropic)) + ::glTexParameterf(Target, GL_TEXTURE_MAX_ANISOTROPY_EXT, Global.AnisotropicFiltering); +} + +} // namespace + +bool opengl33_terrain_materials::init() +{ + if (m_state != 0) + return m_state > 0; + m_state = -1; + try + { + m_copy = std::make_unique(gl::shader("copy.frag")); + } + catch (gl::shader_exception const &e) + { + ErrorLog("heightmap terrain: can't use the copy shader: " + std::string(e.what())); + return false; + } + m_framebuffer = std::make_unique(); + + m_white = make_texture("internal_src:terrain/white", GL_TEXTURE_2D); + { + auto const &white{m_textures.texture(m_white)}; + std::uint8_t const texel[4]{255, 255, 255, 255}; + bind_for_upload(white); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texel); + } + char const *names[]{"internal_src:terrain/layers_diffuse", "internal_src:terrain/layers_normal", "internal_src:terrain/layers_specgloss"}; + for (std::size_t i = 0; i < m_arrays.size(); ++i) + { + auto &array{m_arrays[i]}; + array.handle = make_texture(names[i], GL_TEXTURE_2D_ARRAY); + array.size = array_sizes[i]; + array.srgb = (i == 0); + array.format = (array.srgb ? GL_SRGB8_ALPHA8 : GL_RGBA8); + make_storage(array, initial_capacity); + } + m_capacity = initial_capacity; + opengl_texture::reset_unit_cache(); + + // the shader is checked now, so a broken one makes the terrain fall back on plain materials instead of the failure indicator + try + { + GfxRenderer->Fetch_Shader("terrain"); + } + catch (gl::shader_exception const &e) + { + ErrorLog("heightmap terrain: can't use the terrain shader: " + std::string(e.what())); + return false; + } + m_state = 1; + return true; +} + +texture_handle opengl33_terrain_materials::make_texture(std::string const &Name, GLenum const Target) +{ + auto const handle{m_textures.create(Name, false, GL_RGBA)}; + auto &texture{m_textures.texture(handle)}; + if (texture.id == opengl_texture::invalid_id) + ::glGenTextures(1, &texture.id); + texture.target = Target; + // the content is supplied here, the texture manager isn't to load, release or replace it + texture.is_ready = true; + texture.is_static = true; + texture.is_rendertarget = true; + texture.is_texstub = false; + texture.data_state = resource_state::good; + return handle; +} + +void opengl33_terrain_materials::make_storage(texture_array &Array, int const Layers) +{ + auto &texture{m_textures.texture(Array.handle)}; + bind_for_upload(texture); + ::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + ::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + ::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT); + ::glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT); + set_anisotropy(GL_TEXTURE_2D_ARRAY); + ::glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, Array.format, Array.size, Array.size, Layers, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + texture.data_width = Array.size; + texture.data_height = Array.size; + texture.layers = Layers; + texture.data_format = Array.format; + texture.data_components = GL_RGBA; +} + +int opengl33_terrain_materials::layer_index(gfx::terrain_layer const &Layer) +{ + for (auto const &existing : m_layers) + if (existing.material == Layer.material && existing.size == Layer.size) + return existing.index; + + layer entry; + entry.material = Layer.material; + entry.size = Layer.size; + entry.index = static_cast(m_layers.size()); + auto const &material{m_materials.material(Layer.material)}; + entry.repeat = (Layer.size > 0.f ? Layer.size : (material.size.x > 0.f ? material.size.x : default_repeat)); + if (material.shader) + { + entry.normalmap = material.shader->texture_conf.count("normalmap") > 0; + entry.specgloss = material.shader->texture_conf.count("specgloss") > 0; + } + entry.reflection = (std::isfinite(material.params[1].z) ? material.params[1].z : 0.f); + m_layers.push_back(entry); + m_pending = true; + + if (static_cast(m_layers.size()) > m_capacity) + { + // more room: the arrays are made anew, and every layer copied again + auto const capacity{m_capacity * 2}; + for (auto &array : m_arrays) + { + auto &texture{m_textures.texture(array.handle)}; + ::glDeleteTextures(1, &texture.id); + ::glGenTextures(1, &texture.id); + make_storage(array, capacity); + } + m_capacity = capacity; + for (auto &existing : m_layers) + { + existing.copied = false; + existing.attempts = 0; + } + opengl_texture::reset_unit_cache(); + } + return entry.index; +} + +std::size_t opengl33_terrain_materials::acquire_slot(material_handle const Reuse) +{ + if (Reuse != null_handle) + { + auto const lookup{m_slotof.find(Reuse)}; + if (lookup != m_slotof.end()) + { + m_slots[lookup->second].used = true; + return lookup->second; + } + } + if (false == m_free.empty()) + { + auto const index{m_free.back()}; + m_free.pop_back(); + m_slots[index].used = true; + return index; + } + auto const index{m_slots.size()}; + auto const suffix{std::to_string(index)}; + slot entry; + entry.weights[0] = make_texture("internal_src:terrain/weights0_" + suffix, GL_TEXTURE_2D); + entry.weights[1] = make_texture("internal_src:terrain/weights1_" + suffix, GL_TEXTURE_2D); + entry.table = make_texture("internal_src:terrain/table_" + suffix, GL_TEXTURE_2D); + // the textures get their content before the material is made of them, which checks they're there + std::uint8_t const blank[4]{0, 0, 0, 0}; + for (auto const handle : entry.weights) + { + bind_for_upload(m_textures.texture(handle)); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, blank); + ::glGenerateMipmap(GL_TEXTURE_2D); + } + { + bind_for_upload(m_textures.texture(entry.table)); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + ::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + std::array const zeros{}; + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(max_layers), 2, 0, GL_RGBA, GL_FLOAT, zeros.data()); + } + opengl_texture::reset_unit_cache(); + auto const name = [&](texture_handle const Handle) { return "internal_src:" + m_textures.texture(Handle).name; }; + std::string const definition{ + "shader: terrain\n" + "texture1: " + name(m_white) + "\n" + "texture2: " + name(entry.weights[0]) + "\n" + "texture3: " + name(entry.weights[1]) + "\n" + "texture4: " + name(m_arrays[0].handle) + "\n" + "texture5: " + name(m_arrays[1].handle) + "\n" + "texture6: " + name(m_arrays[2].handle) + "\n" + "texture7: " + name(entry.table) + "\n"}; + entry.material = m_materials.create_from_text("internal_src:terrain/chunk_" + suffix, definition); + if (entry.material == null_handle || false == m_materials.material(entry.material).is_good) + { + ErrorLog("heightmap terrain: can't make the material of a chunk"); + m_state = -1; + return static_cast(-1); + } + entry.used = true; + m_slots.push_back(entry); + m_slotof.emplace(entry.material, index); + return index; +} + +material_handle opengl33_terrain_materials::make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement) +{ + if (Layers.empty() || false == init()) + return null_handle; + auto const count{std::min(Layers.size(), max_layers)}; + std::array indices{}; + for (std::size_t i = 0; i < count; ++i) + { + if (Layers[i].material == null_handle) + return null_handle; + indices[i] = layer_index(Layers[i]); + } + auto const slotindex{acquire_slot(Reuse)}; + if (slotindex == static_cast(-1)) + return null_handle; + auto &entry{m_slots[slotindex]}; + + // weights, four layers to a texture + auto const painted{Weights != nullptr && Samples > 0 && count > 1}; + auto const side{painted ? Samples + 1 : 1}; + auto const samples{static_cast(side) * side}; + std::vector texels(samples * 4); + for (std::size_t texture = 0; texture < 2; ++texture) + { + std::fill(texels.begin(), texels.end(), 0); + for (std::size_t channel = 0; channel < 4; ++channel) + { + auto const layer{texture * 4 + channel}; + if (layer >= count) + break; + if (false == painted) + { + if (layer == 0) + texels[channel] = 255; + continue; + } + auto const *source{Weights + layer * samples}; + for (std::size_t i = 0; i < samples; ++i) + texels[i * 4 + channel] = source[i]; + } + bind_for_upload(m_textures.texture(entry.weights[texture])); + ::glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, side, side, 0, GL_RGBA, GL_UNSIGNED_BYTE, texels.data()); + ::glGenerateMipmap(GL_TEXTURE_2D); + } + // what the layers are: index in the arrays, repeat, which maps they have, reflection + std::array table{}; + for (std::size_t i = 0; i < count; ++i) + { + auto const &layer{m_layers[indices[i]]}; + table[i * 4 + 0] = static_cast(layer.index); + table[i * 4 + 1] = 1.f / std::max(0.01f, layer.repeat); + table[i * 4 + 2] = layer.normalmap ? 1.f : 0.f; + table[i * 4 + 3] = layer.specgloss ? 1.f : 0.f; + table[(max_layers + i) * 4 + 0] = layer.reflection; + // the textures turned by the layer of the chunk (the same material can be turned differently in another layer) + auto const angle{glm::radians(static_cast(Layers[i].rotation))}; + table[(max_layers + i) * 4 + 1] = static_cast(std::cos(angle)); + table[(max_layers + i) * 4 + 2] = static_cast(std::sin(angle)); + } + bind_for_upload(m_textures.texture(entry.table)); + ::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, static_cast(max_layers), 2, 0, GL_RGBA, GL_FLOAT, table.data()); + opengl_texture::reset_unit_cache(); + + auto &material{m_materials.material(entry.material)}; + material.params[2] = glm::vec4(Placement.x, Placement.y, 1.f / std::max(1.f, Placement.z), static_cast(side - 1)); + return entry.material; +} + +void opengl33_terrain_materials::release(material_handle const Material) +{ + auto const lookup{m_slotof.find(Material)}; + if (lookup == m_slotof.end() || false == m_slots[lookup->second].used) + return; + m_slots[lookup->second].used = false; + m_free.push_back(lookup->second); +} + +bool opengl33_terrain_materials::copy_layer(layer &Layer) +{ + auto const &material{m_materials.material(Layer.material)}; + auto const slot = [&](char const *Name, int const Fallback) { + if (material.shader) + { + auto const lookup{material.shader->texture_conf.find(Name)}; + if (lookup != material.shader->texture_conf.end()) + return static_cast(lookup->second.id); + return -1; + } + return Fallback; + }; + std::array const slots{slot("diffuse", 0), Layer.normalmap ? slot("normalmap", 1) : -1, Layer.specgloss ? slot("specgloss", 2) : -1}; + // all maps of the layer have to be ready, so they go together + std::array sources{}; + for (std::size_t i = 0; i < slots.size(); ++i) + { + if (slots[i] < 0 || material.textures[slots[i]] == null_handle) + continue; + auto &texture{m_textures.mark_as_used(material.textures[slots[i]])}; + if (false == texture.is_ready) + { + // a texture which is on the card already may have dropped its data after the upload (create() would refuse it then), + // one released by the sweep or never read has to be read first + if (texture.data_state == resource_state::none) + { + texture.reload_on_use = false; + texture.load(); + } + if (texture.data_state == resource_state::failed) + continue; + if (false == texture.create()) + { + if (++Layer.attempts < copy_attempts) + return false; + WriteLog("heightmap terrain: texture \"" + texture.name + "\" isn't ready, the terrain goes without it"); + continue; // waited long enough, the layer goes without it + } + } + sources[i] = &texture; + } + // defaults for maps the layer doesn't have: grey, flat, without specular reflections + std::array, 3> const defaults{{{0.5f, 0.5f, 0.5f, 1.f}, {0.5f, 0.5f, 1.f, 1.f}, {0.f, 1.f, 0.f, 1.f}}}; + for (std::size_t i = 0; i < m_arrays.size(); ++i) + { + auto const &array{m_arrays[i]}; + auto &target{m_textures.texture(array.handle)}; + m_framebuffer->attach(target, GL_COLOR_ATTACHMENT0, Layer.index); + m_framebuffer->setup_drawing(1); + ::glViewport(0, 0, array.size, array.size); + if (sources[i] == nullptr) + { + ::glClearBufferfv(GL_COLOR, 0, defaults[i].data()); + continue; + } + if (false == Global.gfx_usegles) + { + if (array.srgb) + ::glEnable(GL_FRAMEBUFFER_SRGB); + else + ::glDisable(GL_FRAMEBUFFER_SRGB); + } + m_copy->apply(*sources[i], m_framebuffer.get()); + } + Layer.copied = true; + return true; +} + +void opengl33_terrain_materials::update() +{ + if (false == m_pending || m_state <= 0) + return; + + auto const blend{::glIsEnabled(GL_BLEND)}; + auto const scissor{::glIsEnabled(GL_SCISSOR_TEST)}; + auto const cull{::glIsEnabled(GL_CULL_FACE)}; + auto const depth{::glIsEnabled(GL_DEPTH_TEST)}; + auto const srgb{(false == Global.gfx_usegles) && ::glIsEnabled(GL_FRAMEBUFFER_SRGB)}; + ::glDisable(GL_BLEND); + ::glDisable(GL_SCISSOR_TEST); + ::glDisable(GL_CULL_FACE); + + bool copied{false}; + m_pending = false; + for (auto &layer : m_layers) + { + if (layer.copied) + continue; + if (copy_layer(layer)) + copied = true; + else + m_pending = true; + } + if (copied) + { + for (auto const &array : m_arrays) + { + bind_for_upload(m_textures.texture(array.handle)); + ::glGenerateMipmap(GL_TEXTURE_2D_ARRAY); + } + } + + // leave the state as the rest of the renderer knows it + gl::framebuffer::unbind(); + if (blend) + ::glEnable(GL_BLEND); + if (scissor) + ::glEnable(GL_SCISSOR_TEST); + if (cull) + ::glEnable(GL_CULL_FACE); + if (depth) + ::glEnable(GL_DEPTH_TEST); + else + ::glDisable(GL_DEPTH_TEST); + if (false == Global.gfx_usegles) + { + if (srgb) + ::glEnable(GL_FRAMEBUFFER_SRGB); + else + ::glDisable(GL_FRAMEBUFFER_SRGB); + } + ::glDepthMask(GL_TRUE); + opengl_texture::reset_unit_cache(); + gfx::opengl33_vaogeometrybank::reset(); +} diff --git a/rendering/opengl33terrain.h b/rendering/opengl33terrain.h new file mode 100644 index 00000000..60d12113 --- /dev/null +++ b/rendering/opengl33terrain.h @@ -0,0 +1,85 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "rendering/renderer.h" +#include "model/material.h" +#include "model/Texture.h" +#include "gl/framebuffer.h" +#include "gl/postfx.h" + +// materials of the heightmap terrain for the opengl 3.3 renderer (shader mat_terrain). +// the textures of the layer materials are copied into texture arrays shared by all chunks; each chunk gets a material +// of its own, with textures holding the weights of its layers and a table of what its layers are +class opengl33_terrain_materials +{ + public: + opengl33_terrain_materials(material_manager &Materials, texture_manager &Textures) : m_materials(Materials), m_textures(Textures) {} + + // see gfx_renderer::Terrain_Material. returns null_handle if the terrain shader can't be used + material_handle make(material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement); + void release(material_handle const Material); + // copies the textures of new layers into the arrays; to be called before the frame is drawn + void update(); + + private: + struct layer + { + material_handle material{null_handle}; + float size{0.f}; // as given by the chunk + float repeat{8.f}; // metres the textures are repeated at + int index{0}; // in the arrays + bool normalmap{false}; + bool specgloss{false}; + float reflection{0.f}; + bool copied{false}; + int attempts{0}; + }; + struct texture_array + { + texture_handle handle{null_handle}; + int size{0}; + GLint format{0}; + bool srgb{false}; + }; + struct slot + { + material_handle material{null_handle}; + std::array weights{null_handle, null_handle}; + texture_handle table{null_handle}; + bool used{false}; + }; + + bool init(); + texture_handle make_texture(std::string const &Name, GLenum const Target); + void make_storage(texture_array &Array, int const Layers); + int layer_index(gfx::terrain_layer const &Layer); + bool copy_layer(layer &Layer); + std::size_t acquire_slot(material_handle const Reuse); + + material_manager &m_materials; + texture_manager &m_textures; + int m_state{0}; // 0: not set up yet, 1: ready, -1: can't be used + texture_handle m_white{null_handle}; + std::array m_arrays; // diffuse, normal maps, specular/gloss + int m_capacity{0}; // layers the arrays have room for + std::vector m_layers; + bool m_pending{false}; + std::vector m_slots; + std::vector m_free; + std::unordered_map m_slotof; + std::unique_ptr m_copy; + std::unique_ptr m_framebuffer; +}; diff --git a/rendering/openglrenderer.cpp b/rendering/openglrenderer.cpp index 27c94da8..59152c64 100644 --- a/rendering/openglrenderer.cpp +++ b/rendering/openglrenderer.cpp @@ -33,6 +33,28 @@ int constexpr EU07_REFLECTIONFIDELITYOFFSET { 250 }; // artificial increase of r namespace { +float +editor_track_view_range(float const Range, bool const Apply) +{ + if( false == Apply || false == Global.editor_tracks ) + return Range; + float extra = 50000.0f; + if( Global.EditorOrtho ) + extra = Global.EditorOrthoExtent * 3.0f; + return Range > extra ? Range : extra; +} + +bool +editor_far_tracks_only(bool const Apply, scene::basic_cell const *Cell, glm::dvec3 const &Camera, double const SceneryLimit) +{ + if( false == Apply ) + return false; + double nearest = glm::length( Cell->m_area.center - Camera ) - static_cast( Cell->m_area.radius ); + if( nearest < 0.0 ) + nearest = 0.0; + return nearest > SceneryLimit; +} + // returns material assigned to the submodel, resolving replacable skins. null_handle if no skin set is bound material_handle submodel_material( TSubModel const *Submodel ) { @@ -1875,7 +1897,9 @@ opengl_renderer::Render( scene::basic_region *Region ) { glm::vec3 const cameraposition { m_renderpass.camera.position() }; auto const camerax = static_cast( std::floor( cameraposition.x / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2 ) ); auto const cameraz = static_cast( std::floor( cameraposition.z / scene::EU07_SECTIONSIZE + scene::EU07_REGIONSIDESECTIONCOUNT / 2 ) ); - int const segmentcount = 2 * static_cast( std::ceil( m_renderpass.draw_range * Global.fDistanceFactor / scene::EU07_SECTIONSIZE ) ); + float section_range = m_renderpass.draw_range * Global.fDistanceFactor; + section_range = editor_track_view_range( section_range, EditorModeFlag && ( m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery ) ); + int const segmentcount = 2 * static_cast( std::ceil( section_range / scene::EU07_SECTIONSIZE ) ); int const originx = camerax - segmentcount / 2; int const originz = cameraz - segmentcount / 2; @@ -2040,12 +2064,16 @@ opengl_renderer::Render( cell_sequence::iterator First, cell_sequence::iterator // cache initial iterator for the second sweep auto first { First }; + auto const scenery_limit{ static_cast( Global.BaseDrawRange ) * Global.fDistanceFactor + 250.0 }; + bool const far_paths{ EditorModeFlag && Global.editor_tracks && ( m_renderpass.draw_mode == rendermode::color || m_renderpass.draw_mode == rendermode::pickscenery ) }; + glm::dvec3 const camera{ m_renderpass.camera.position() }; // first pass draws elements which we know are located in section banks, to reduce vbo switching while( First != Last ) { auto *cell = First->second; // przeliczenia animacji torów w sektorze cell->RaAnimate( m_framestamp ); + bool const tracks_only{ editor_far_tracks_only( far_paths, cell, camera, scenery_limit ) }; switch( m_renderpass.draw_mode ) { case rendermode::color: { @@ -2056,7 +2084,9 @@ opengl_renderer::Render( cell_sequence::iterator First, cell_sequence::iterator // render // opaque non-instanced shapes - for( auto const &shape : cell->m_shapesopaque ) { Render( shape, false ); } + if( false == tracks_only ) { + for( auto const &shape : cell->m_shapesopaque ) { Render( shape, false ); } + } // tracks // TODO: update after path node refactoring Render( std::begin( cell->m_paths ), std::end( cell->m_paths ) ); @@ -2158,8 +2188,10 @@ opengl_renderer::Render( cell_sequence::iterator First, cell_sequence::iterator // render // opaque non-instanced shapes // non-interactive scenery elements get neutral colour - ::glColor3fv( glm::value_ptr( colors::none ) ); - for( auto const &shape : cell->m_shapesopaque ) { Render( shape, false ); } + if( false == tracks_only ) { + ::glColor3fv( glm::value_ptr( colors::none ) ); + for( auto const &shape : cell->m_shapesopaque ) { Render( shape, false ); } + } // tracks for( auto *path : cell->m_paths ) { ::glColor3fv( glm::value_ptr( pick_color( m_picksceneryitems.size() + 1 ) ) ); @@ -2188,6 +2220,8 @@ opengl_renderer::Render( cell_sequence::iterator First, cell_sequence::iterator case rendermode::color: case rendermode::shadows: case rendermode::cabshadows: { + if( editor_far_tracks_only( far_paths, cell, camera, scenery_limit ) ) + break; // TBD, TODO: refactor in to a method to reuse in branch below? // opaque parts of instanced models for( auto *instance : cell->m_instancesopaque ) { @@ -2230,6 +2264,8 @@ opengl_renderer::Render( cell_sequence::iterator First, cell_sequence::iterator break; } case rendermode::pickscenery: { + if( editor_far_tracks_only( far_paths, cell, camera, scenery_limit ) ) + break; // opaque parts of instanced models // same procedure like with regular render, but each node receives custom colour used for picking for( auto *instance : cell->m_instancesopaque ) { @@ -4565,3 +4601,29 @@ void opengl_renderer::opengl_imgui_renderer::Render() ImGui_ImplOpenGL2_RenderDrawData(ImGui::GetDrawData()); } +std::uint64_t opengl_renderer::opengl_imgui_renderer::Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) +{ + // the ui of this renderer works on srgb colours as they are. the binding the renderer keeps track of is put back as it was + GLint texture{0}, alignment{4}; + glGetIntegerv(GL_TEXTURE_BINDING_2D, &texture); + glGetIntegerv(GL_UNPACK_ALIGNMENT, &alignment); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + GLuint image{0}; + glGenTextures(1, &image); + glBindTexture(GL_TEXTURE_2D, image); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Width, Height, 0, GL_RGBA, GL_UNSIGNED_BYTE, Rgba); + glBindTexture(GL_TEXTURE_2D, static_cast(texture)); + glPixelStorei(GL_UNPACK_ALIGNMENT, alignment); + return image; +} + +void opengl_renderer::opengl_imgui_renderer::Release_Image(std::uint64_t const Image) +{ + auto const image{static_cast(Image)}; + glDeleteTextures(1, &image); +} + diff --git a/rendering/openglrenderer.h b/rendering/openglrenderer.h index 83dca483..78fffa40 100644 --- a/rendering/openglrenderer.h +++ b/rendering/openglrenderer.h @@ -385,6 +385,8 @@ private: virtual void Shutdown() override; virtual void BeginFrame() override; virtual void Render() override; + virtual std::uint64_t Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) override; + virtual void Release_Image(std::uint64_t const Image) override; } m_imgui_renderer; virtual imgui_renderer *GetImguiRenderer() override diff --git a/rendering/renderer.cpp b/rendering/renderer.cpp index 610473a3..d9d4b5df 100644 --- a/rendering/renderer.cpp +++ b/rendering/renderer.cpp @@ -39,5 +39,29 @@ gfx_renderer_factory *gfx_renderer_factory::get_instance() gfx_renderer_factory *gfx_renderer_factory::instance; +material_handle gfx_renderer::Terrain_Material( material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) +{ + // without blending the chunk gets the material which covers the most of it + if( Layers.empty() ) { + return null_handle; + } + std::size_t best { 0 }; + if( ( Weights != nullptr ) && ( Samples > 0 ) ) { + auto const samples { static_cast( Samples + 1 ) * ( Samples + 1 ) }; + std::uint64_t bestsum { 0 }; + for( std::size_t layer = 0; layer < Layers.size(); ++layer ) { + std::uint64_t sum { 0 }; + for( std::size_t i = 0; i < samples; ++i ) { + sum += Weights[ layer * samples + i ]; + } + if( sum > bestsum ) { + bestsum = sum; + best = layer; + } + } + } + return Layers[ best ].material; +} + //--------------------------------------------------------------------------- diff --git a/rendering/renderer.h b/rendering/renderer.h index f7b6d762..0b9eb60f 100644 --- a/rendering/renderer.h +++ b/rendering/renderer.h @@ -21,6 +21,17 @@ namespace gl class program; } +namespace gfx +{ +// material painted over the heightmap terrain, and the size (metres) its textures are repeated at; 0: the size given by the material, or 8 m +struct terrain_layer +{ + material_handle material{null_handle}; + float size{0.f}; + float rotation{0.f}; // degrees the textures are turned by +}; +} // namespace gfx + class gfx_renderer { public: @@ -58,6 +69,14 @@ public: virtual auto Fetch_Material( std::string const &Filename, bool const Loadnow = true ) -> material_handle = 0; virtual void Bind_Material( material_handle const Material, TSubModel const *sm = nullptr, lighting_data const *lighting = nullptr ) = 0; virtual auto Material( material_handle const Material ) const -> IMaterial const * = 0; + // heightmap terrain: material of a chunk blending the materials of up to 8 layers by their weights. + // Weights: (Samples + 1)^2 values per layer, planar, rows of growing z, the weights of a sample adding up to 255; null with a single layer. + // Placement: corner of the chunk (x, z) in the texture coordinates of its mesh, and the size of the chunk. + // Reuse: a material made by this call before, to be filled anew, or null_handle. + // returns: the material. backends which can't blend give back the material of the layer covering the most + virtual auto Terrain_Material( material_handle const Reuse, std::vector const &Layers, int const Samples, std::uint8_t const *Weights, glm::vec3 const &Placement ) -> material_handle; + // gives back a material made by Terrain_Material, once its chunk is gone + virtual void Terrain_Release( material_handle const Material ) {} // shader methods virtual auto Fetch_Shader( std::string const &name ) -> std::shared_ptr = 0; // texture methods @@ -73,6 +92,7 @@ public: virtual auto Pick_Node() const -> scene::basic_node const * = 0; virtual auto Mouse_Position() const -> glm::dvec3 = 0; + virtual auto Mouse_Hit() const -> bool { return true; } // editor helpers: matrices/position of the most recent color pass camera. // the view matrix is camera-relative (rotation only, camera at origin), matching the // camera-relative rendering used by the engine; build object matrices relative to Camera_Position(). @@ -92,6 +112,10 @@ public: // imgui renderer virtual class imgui_renderer *GetImguiRenderer() = 0; virtual void MakeScreenshot() = 0; + // draws the model of a scenery instance on its own into a square image of Size pixels on white background, the model + // seen in perspective from the front, side and above and filling the image less Margin pixels at each edge. + // Image receives rgb rows, top row first. returns false if the backend can't make previews or there's nothing to draw + virtual auto Render_Preview( TAnimModel *Instance, int const Size, int const Margin, bool const Shadows, std::vector &Image ) -> bool { return false; } }; class gfx_renderer_factory @@ -116,6 +140,13 @@ class imgui_renderer virtual void Shutdown() = 0; virtual void BeginFrame() = 0; virtual void Render() = 0; + // images of the user interface itself (e.g. the previews of the node bank), apart from the textures of the scene: rgba, rows + // from the top, in srgb. returns: id for ImGui::Image, 0 if the renderer can't make them + virtual std::uint64_t Create_Image(std::uint8_t const *Rgba, int const Width, int const Height) + { + return 0; + } + virtual void Release_Image(std::uint64_t const Image) {} }; extern std::unique_ptr GfxRenderer; diff --git a/scene/heightmapterrain.cpp b/scene/heightmapterrain.cpp new file mode 100644 index 00000000..a2bc6a2f --- /dev/null +++ b/scene/heightmapterrain.cpp @@ -0,0 +1,1224 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/heightmapterrain.h" + +#include "scene/sceneterrain.h" + +#include +#include +#include +#include +#include +#include + +#include +#include "zstd.h" + +namespace heightmap +{ + +namespace +{ + +std::uint32_t constexpr fourcc(char const A, char const B, char const C, char const D) +{ + return static_cast(static_cast(A)) | (static_cast(static_cast(B)) << 8) | + (static_cast(static_cast(C)) << 16) | (static_cast(static_cast(D)) << 24); +} + +std::uint32_t constexpr tag_chunk{fourcc('C', 'H', 'N', 'K')}; +std::uint32_t constexpr tag_heights{fourcc('H', 'G', 'H', 'T')}; +std::uint32_t constexpr tag_base{fourcc('H', 'B', 'A', 'S')}; +std::uint32_t constexpr tag_paint{fourcc('S', 'P', 'L', 'T')}; +std::uint32_t constexpr tag_adjust{fourcc('H', 'A', 'D', 'J')}; + +std::uint32_t constexpr pack_magic{fourcc('T', 'C', 'H', '1')}; +std::uint32_t constexpr pack_version{1}; +std::size_t constexpr header_size{64}; +std::size_t constexpr index_entry_size{16}; +std::size_t constexpr slice_entry_size{32}; +std::uint32_t constexpr codec_stored{0}; +std::uint32_t constexpr codec_zstd{1}; +int constexpr zstd_level{3}; // light and fast, the data is simple + +// the slices are small, a slice larger than this is taken for a damaged file +std::uint32_t constexpr max_slice_size{256u * 1024u * 1024u}; + +template void put(std::vector &Out, Type_ const Value) +{ + auto const offset{Out.size()}; + Out.resize(offset + sizeof(Type_)); + std::memcpy(Out.data() + offset, &Value, sizeof(Type_)); +} + +template void put_at(std::vector &Out, std::size_t const Offset, Type_ const Value) +{ + std::memcpy(Out.data() + Offset, &Value, sizeof(Type_)); +} + +// reads values from a block of data, without going past its end +class reader +{ + public: + reader(std::uint8_t const *Data, std::size_t const Size) : m_data(Data), m_size(Size) {} + template bool get(Type_ &Value) + { + if (m_offset + sizeof(Type_) > m_size) + return false; + std::memcpy(&Value, m_data + m_offset, sizeof(Type_)); + m_offset += sizeof(Type_); + return true; + } + bool get(void *Out, std::size_t const Size) + { + if (m_offset + Size > m_size) + return false; + std::memcpy(Out, m_data + m_offset, Size); + m_offset += Size; + return true; + } + std::size_t left() const { return m_size - m_offset; } + + private: + std::uint8_t const *m_data; + std::size_t m_size; + std::size_t m_offset{0}; +}; + +void encode_heights(std::vector const &Heights, std::vector &Out) +{ + if (Heights.empty()) + return; + auto const [low, high]{std::minmax_element(Heights.begin(), Heights.end())}; + auto const base{static_cast(std::floor(*low / height_step))}; + auto const top{static_cast(std::ceil(*high / height_step))}; + if (top - base > 0xffff || base < std::numeric_limits::min() || top > std::numeric_limits::max()) + { + // too high a range for the steps: kept as they are + put(Out, 1); + for (auto const height : Heights) + put(Out, height); + return; + } + put(Out, 0); + put(Out, static_cast(base)); + // differences of the neighbouring points are small and repeat a lot, which compresses well + std::uint16_t previous{0}; + for (auto const height : Heights) + { + auto const steps{static_cast(std::clamp(std::llround(height / height_step) - base, 0, 0xffff))}; + put(Out, static_cast(steps - previous)); + previous = steps; + } +} + +bool decode_heights(std::vector const &Data, std::size_t const Count, std::vector &Out) +{ + reader input{Data.data(), Data.size()}; + std::uint8_t encoding{0}; + if (false == input.get(encoding)) + return false; + Out.resize(Count); + if (encoding == 1) + { + if (input.left() != Count * sizeof(float)) + return false; + return input.get(Out.data(), Count * sizeof(float)); + } + if (encoding != 0) + return false; + std::int32_t base{0}; + if (false == input.get(base) || input.left() != Count * sizeof(std::uint16_t)) + return false; + std::uint16_t value{0}; + for (std::size_t i = 0; i < Count; ++i) + { + std::uint16_t delta{0}; + input.get(delta); + value = static_cast(value + delta); + Out[i] = static_cast((static_cast(base) + value) * height_step); + } + return true; +} + +std::uint32_t checksum(std::vector const &Data) +{ + return scene::terrain_file::checksum(reinterpret_cast(Data.data()), Data.size()); +} + +int floor_div(int const Value, int const Divisor) +{ + return (Value >= 0 ? Value / Divisor : -((-Value + Divisor - 1) / Divisor)); +} + +// the larger of two values, the corner between them rounded off over K +double soft_max(double const A, double const B, double const K) +{ + if (K <= 1e-6) + return std::max(A, B); + auto const h{std::max(K - std::abs(A - B), 0.0) / K}; + return std::max(A, B) + h * h * K * 0.25; +} + +} // namespace + +int cells_for(float const Spacing) +{ + return static_cast(std::lround(chunk_size / valid_spacing(Spacing))); +} + +float valid_spacing(float const Spacing) +{ + float best{default_spacing}; + float distance{std::numeric_limits::max()}; + for (auto const spacing : spacings) + if (std::abs(spacing - Spacing) < distance) + { + distance = std::abs(spacing - Spacing); + best = spacing; + } + return best; +} + +chunk_key chunk_at(double const X, double const Z) +{ + return {static_cast(std::floor(X / chunk_size)), static_cast(std::floor(Z / chunk_size))}; +} + +glm::dvec2 chunk_corner(chunk_key const &Key) +{ + return {Key.first * chunk_size, Key.second * chunk_size}; +} + +pack_key pack_of(chunk_key const &Key) +{ + return {floor_div(Key.first, pack_side), floor_div(Key.second, pack_side)}; +} + +glm::dvec2 pack_corner(pack_key const &Key) +{ + return {Key.first * pack_side * chunk_size, Key.second * pack_side * chunk_size}; +} + +std::string pack_file_name(pack_key const &Key) +{ + return std::to_string(Key.first) + "_" + std::to_string(Key.second) + ".tch"; +} + +// chunk_data + +std::size_t chunk_data::layer_count() const +{ + std::size_t count{0}; + while (count < layers.size() && layers[count] != no_layer) + ++count; + return count; +} + +bool chunk_data::contains(double const X, double const Z) const +{ + auto const low{corner()}; + return X >= low.x && X <= low.x + chunk_size && Z >= low.y && Z <= low.y + chunk_size; +} + +double chunk_data::height_at(double const X, double const Z) const +{ + return height_in(heights, X, Z); +} + +double chunk_data::height_in(std::vector const &Heights, double const X, double const Z) const +{ + if (cells <= 0 || Heights.size() != static_cast(side()) * side()) + return 0.0; + auto const low{corner()}; + auto const fx{(X - low.x) / spacing()}; + auto const fz{(Z - low.y) / spacing()}; + auto const ix{std::clamp(static_cast(std::floor(fx)), 0, cells - 1)}; + auto const iz{std::clamp(static_cast(std::floor(fz)), 0, cells - 1)}; + auto const tx{std::clamp(fx - ix, 0.0, 1.0)}; + auto const tz{std::clamp(fz - iz, 0.0, 1.0)}; + auto const at = [&](int const X_, int const Z_) { return static_cast(Heights[static_cast(Z_) * side() + X_]); }; + auto const h00{at(ix, iz)}, h10{at(ix + 1, iz)}, h01{at(ix, iz + 1)}, h11{at(ix + 1, iz + 1)}; + // the quads are cut along the diagonal from (x0, z1) to (x1, z0), as the mesh is made + if (tx + tz <= 1.0) + return h00 + tx * (h10 - h00) + tz * (h01 - h00); + return h11 + (1.0 - tx) * (h01 - h11) + (1.0 - tz) * (h10 - h11); +} + +std::shared_ptr chunk_data::make_flat(chunk_key const &Key, float const Spacing, float const Height) +{ + auto chunk{std::make_shared()}; + chunk->key = Key; + chunk->cells = cells_for(Spacing); + chunk->heights.assign(static_cast(chunk->side()) * chunk->side(), Height); + chunk->layers[0] = 0; + return chunk; +} + +std::shared_ptr chunk_data::resampled(float const Spacing, int const Paint) const +{ + auto chunk{std::make_shared(*this)}; + chunk->cells = cells_for(Spacing); + if (chunk->cells != cells) + { + auto const low{corner()}; + auto const step{chunk->spacing()}; + // a coarser grid would otherwise pick single points of the finer one: each point inside the chunk takes the average of + // the ground around it (a tent of the size of its cell). the points of the edges are the ground itself, as they're + // shared with the neighbours, which keep theirs + auto const reach{chunk->cells < cells ? step * 0.5 : 0.0}; + auto const taps{reach > 0.0 ? std::max(1, static_cast(std::ceil(reach / spacing()))) : 0}; + auto const resample = [&](std::vector const &Source, std::vector &Target) { + Target.resize(static_cast(chunk->side()) * chunk->side()); + for (int iz = 0; iz <= chunk->cells; ++iz) + for (int ix = 0; ix <= chunk->cells; ++ix) + { + auto const x{low.x + ix * step}, z{low.y + iz * step}; + auto value{height_in(Source, x, z)}; + if (taps > 0 && ix > 0 && iz > 0 && ix < chunk->cells && iz < chunk->cells) + { + double sum{0.0}, weights{0.0}; + for (int dz = -taps; dz <= taps; ++dz) + for (int dx = -taps; dx <= taps; ++dx) + { + auto const weight{static_cast((taps + 1 - std::abs(dx)) * (taps + 1 - std::abs(dz)))}; + sum += weight * height_in(Source, x + dx * reach / taps, z + dz * reach / taps); + weights += weight; + } + value = sum / weights; + } + Target[static_cast(iz) * chunk->side() + ix] = static_cast(value); + } + }; + resample(heights, chunk->heights); + if (false == base.empty()) + resample(base, chunk->base); + if (false == adjust.empty()) + resample(adjust, chunk->adjust); + } + if (Paint > 0 && paint > 0 && Paint != paint) + { + // the weights of the samples between the old ones blended from them, and brought back to 255 a sample + auto const count{layer_count()}; + auto const oldside{static_cast(paint + 1)}; + auto const newside{static_cast(Paint + 1)}; + chunk->paint = Paint; + chunk->weights.assign(newside * newside * count, 0); + std::vector values(count); + for (std::size_t iz = 0; iz < newside; ++iz) + for (std::size_t ix = 0; ix < newside; ++ix) + { + auto const fx{static_cast(ix) * paint / Paint}, fz{static_cast(iz) * paint / Paint}; + auto const x0{std::min(static_cast(fx), oldside - 2)}, z0{std::min(static_cast(fz), oldside - 2)}; + auto const tx{fx - x0}, tz{fz - z0}; + double total{0.0}; + for (std::size_t slot = 0; slot < count; ++slot) + { + auto const at = [&](std::size_t const X, std::size_t const Z) { return static_cast(weights[slot * oldside * oldside + Z * oldside + X]); }; + values[slot] = (at(x0, z0) * (1.0 - tx) + at(x0 + 1, z0) * tx) * (1.0 - tz) + (at(x0, z0 + 1) * (1.0 - tx) + at(x0 + 1, z0 + 1) * tx) * tz; + total += values[slot]; + } + // rounded down, what's left of 255 goes to the heaviest + int sum{0}; + std::size_t heaviest{0}; + for (std::size_t slot = 0; slot < count; ++slot) + { + auto const weight{total > 0.0 ? static_cast(values[slot] * 255.0 / total) : (slot == 0 ? 255 : 0)}; + chunk->weights[slot * newside * newside + iz * newside + ix] = static_cast(weight); + sum += weight; + if (values[slot] > values[heaviest]) + heaviest = slot; + } + chunk->weights[heaviest * newside * newside + iz * newside + ix] += static_cast(255 - sum); + } + } + return chunk; +} + +std::uint8_t chunk_data::weight(std::size_t const Slot, int const Ix, int const Iz) const +{ + if (paint <= 0) + return Slot == 0 ? 255 : 0; + auto const samples{static_cast(paint + 1) * (paint + 1)}; + auto const index{Slot * samples + static_cast(Iz) * (paint + 1) + Ix}; + return index < weights.size() ? weights[index] : 0; +} + +int chunk_data::slot_of(std::uint16_t const Layer) const +{ + for (std::size_t slot = 0; slot < layers.size(); ++slot) + if (layers[slot] == Layer) + return static_cast(slot); + return -1; +} + +std::size_t chunk_data::dominant_slot() const +{ + auto const count{layer_count()}; + if (paint <= 0 || count < 2) + return 0; + auto const samples{static_cast(paint + 1) * (paint + 1)}; + std::size_t best{0}; + std::uint64_t bestsum{0}; + for (std::size_t slot = 0; slot < count; ++slot) + { + std::uint64_t sum{0}; + for (std::size_t i = 0; i < samples && slot * samples + i < weights.size(); ++i) + sum += weights[slot * samples + i]; + if (sum > bestsum) + { + bestsum = sum; + best = slot; + } + } + return best; +} + +void chunk_data::ensure_paint(int const Samples) +{ + if (paint > 0) + return; + if (layers[0] == no_layer) + layers[0] = 0; + paint = std::max(1, Samples); + auto const samples{static_cast(paint + 1) * (paint + 1)}; + weights.assign(samples * layer_count(), 0); + std::fill(weights.begin(), weights.begin() + samples, 255); +} + +void chunk_data::compact_layers() +{ + if (paint <= 0) + return; + auto const samples{static_cast(paint + 1) * (paint + 1)}; + auto const count{layer_count()}; + std::vector kept; + std::array keptlayers; + keptlayers.fill(no_layer); + std::size_t used{0}; + for (std::size_t slot = 0; slot < count; ++slot) + { + auto const begin{weights.begin() + slot * samples}; + if (std::all_of(begin, begin + samples, [](std::uint8_t const Weight) { return Weight == 0; })) + continue; + kept.insert(kept.end(), begin, begin + samples); + keptlayers[used++] = layers[slot]; + } + if (used == 0) + { + // nothing painted at all; shouldn't happen, but the chunk can't be left without a material + keptlayers[0] = layers[0] == no_layer ? 0 : layers[0]; + used = 1; + kept.clear(); + } + layers = keptlayers; + if (used == 1) + { + paint = 0; + weights.clear(); + return; + } + weights = std::move(kept); +} + +// modifiers + +std::pair modifier::bounds() const +{ + glm::dvec2 low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + double widest{0.0}; + for (auto const &point : points) + { + low = glm::min(low, point.position); + high = glm::max(high, point.position); + widest = std::max(widest, point.half_width); + } + if (points.empty()) + return {glm::dvec2{0.0}, glm::dvec2{0.0}}; + auto const margin{widest + reach}; + return {low - glm::dvec2{margin}, high + glm::dvec2{margin}}; +} + +namespace +{ +std::uint64_t grid_key(int const X, int const Z) +{ + return (static_cast(static_cast(X)) << 32) | static_cast(Z); +} +} // namespace + +modifier_shape::modifier_shape(modifier const &Modifier) : m_modifier(Modifier) +{ + for (auto const &point : Modifier.points) + m_widest = std::max(m_widest, point.half_width); + auto const bounds{Modifier.bounds()}; + m_min = bounds.first; + m_max = bounds.second; + auto const &points{Modifier.points}; + for (std::size_t i = 0; i + 1 < points.size(); ++i) + { + auto const &a{points[i].position}; + auto const &b{points[i + 1].position}; + auto const x0{static_cast(std::floor(std::min(a.x, b.x) / m_cell))}, x1{static_cast(std::floor(std::max(a.x, b.x) / m_cell))}; + auto const z0{static_cast(std::floor(std::min(a.y, b.y) / m_cell))}, z1{static_cast(std::floor(std::max(a.y, b.y) / m_cell))}; + for (auto x = x0; x <= x1; ++x) + for (auto z = z0; z <= z1; ++z) + m_grid.emplace_back(grid_key(x, z), static_cast(i)); + } + std::sort(m_grid.begin(), m_grid.end()); +} + +bool modifier_shape::reaches(glm::dvec2 const &Min, glm::dvec2 const &Max) const +{ + return false == m_grid.empty() && Max.x >= m_min.x && Min.x <= m_max.x && Max.y >= m_min.y && Min.y <= m_max.y; +} + +bool modifier_shape::apply(double const X, double const Z, float &Height) const +{ + if (m_grid.empty() || X < m_min.x || X > m_max.x || Z < m_min.y || Z > m_max.y) + return false; + auto const &points{m_modifier.points}; + auto const reach{m_widest + m_modifier.reach}; + auto distance{reach}; + double formation{0.0}, halfwidth{0.0}; + bool found{false}; + auto const x0{static_cast(std::floor((X - reach) / m_cell))}, x1{static_cast(std::floor((X + reach) / m_cell))}; + auto const z0{static_cast(std::floor((Z - reach) / m_cell))}, z1{static_cast(std::floor((Z + reach) / m_cell))}; + glm::dvec2 const point{X, Z}; + for (auto x = x0; x <= x1; ++x) + for (auto z = z0; z <= z1; ++z) + { + auto const key{grid_key(x, z)}; + auto it{std::lower_bound(m_grid.begin(), m_grid.end(), std::make_pair(key, std::uint32_t{0}))}; + for (; it != m_grid.end() && it->first == key; ++it) + { + auto const i{it->second}; + auto const &a{points[i].position}; + auto const &b{points[i + 1].position}; + auto const run{b - a}; + auto const length{glm::dot(run, run)}; + auto const t{length > 1e-12 ? std::clamp(glm::dot(point - a, run) / length, 0.0, 1.0) : 0.0}; + auto const offset{glm::length(point - (a + run * t))}; + if (offset < distance) + { + distance = offset; + formation = points[i].formation + (points[i + 1].formation - points[i].formation) * t; + halfwidth = points[i].half_width + (points[i + 1].half_width - points[i].half_width) * t; + found = true; + } + } + } + if (false == found) + return false; + // the formation under the axis, slopes from its edges down or up to the ground, the bends rounded off + auto const slope{std::max(0.1, m_modifier.slope)}; + auto const rounding{std::max(0.0, m_modifier.rounding)}; + auto const ground{static_cast(Height)}; + auto const beyond{soft_max(0.0, distance - halfwidth, rounding)}; + auto const bend{rounding / slope}; + auto const target{ground > formation ? -soft_max(-ground, -(formation + beyond / slope), bend) : soft_max(ground, formation - beyond / slope, bend)}; + if (std::abs(target - ground) < 0.001) + return false; + Height = static_cast(target); + return true; +} + +// manifest + +bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Error) +{ + try + { + auto const document{YAML::LoadFile(Path)}; + manifest result; + result.layers.clear(); + if (auto const node{document["version"]}) + result.version = node.as(); + if (auto const node{document["compress"]}) + result.compress = node.as(); + if (auto const node{document["spacing"]}) + result.spacing = valid_spacing(node.as()); + if (auto const node{document["water_material"]}) + result.water_material = node.as(); + if (auto const node{document["layers"]}; node && node.IsSequence()) + for (auto const &layer : node) + { + // a material name, or a map with the material, the size its textures repeat at and the angle they're turned by + if (layer.IsMap()) + { + layer_def entry; + if (auto const material{layer["material"]}) + entry.material = material.as(); + if (auto const size{layer["size"]}) + entry.size = std::max(0.f, size.as()); + if (auto const rotation{layer["rotation"]}) + entry.rotation = rotation.as(); + if (false == entry.material.empty()) + result.layers.push_back(entry); + } + else + result.layers.push_back({layer.as(), 0.f}); + } + if (result.layers.empty()) + result.layers.push_back({"grass", 0.f}); + if (auto const node{document["water"]}; node && node.IsSequence()) + for (auto const &entry : node) + { + water_body water; + if (auto const name{entry["name"]}) + water.name = name.as(); + if (auto const level{entry["level"]}) + water.level = level.as(); + if (auto const material{entry["material"]}) + water.material = material.as(); + if (auto const size{entry["size"]}) + water.size = std::max(0.f, size.as()); + if (auto const outline{entry["outline"]}; outline && outline.IsSequence()) + for (auto const &point : outline) + if (point.IsSequence() && point.size() >= 2) + water.outline.emplace_back(point[0].as(), point[1].as()); + if (water.outline.size() >= 3) + result.water.push_back(std::move(water)); + } + if (auto const node{document["modifiers"]}; node && node.IsSequence()) + for (auto const &entry : node) + { + modifier item; + if (auto const name{entry["name"]}) + item.name = name.as(); + if (auto const slope{entry["slope"]}) + item.slope = slope.as(); + if (auto const reach{entry["reach"]}) + item.reach = reach.as(); + if (auto const rounding{entry["rounding"]}) + item.rounding = rounding.as(); + if (auto const points{entry["points"]}; points && points.IsSequence()) + for (auto const &point : points) + if (point.IsSequence() && point.size() >= 4) + item.points.push_back({{point[0].as(), point[1].as()}, point[2].as(), point[3].as()}); + if (item.points.size() >= 2) + result.modifiers.push_back(std::move(item)); + } + Manifest = std::move(result); + return true; + } + catch (YAML::Exception const &exception) + { + if (Error != nullptr) + *Error = exception.what(); + return false; + } +} + +bool save_manifest(std::string const &Path, manifest const &Manifest, std::string *Error) +{ + YAML::Emitter out; + out.SetDoublePrecision(12); + out.SetFloatPrecision(7); + out << YAML::Comment("MaSzyna heightmap terrain; the chunks are in the .tch files next to this one"); + out << YAML::BeginMap; + out << YAML::Key << "version" << YAML::Value << Manifest.version; + out << YAML::Key << "compress" << YAML::Value << Manifest.compress; + out << YAML::Key << "spacing" << YAML::Value << Manifest.spacing; + out << YAML::Key << "water_material" << YAML::Value << Manifest.water_material; + out << YAML::Key << "layers" << YAML::Value << YAML::BeginSeq; + for (auto const &layer : Manifest.layers) + { + if (layer.size > 0.f || layer.rotation != 0.f) + { + out << YAML::Flow << YAML::BeginMap << YAML::Key << "material" << YAML::Value << layer.material << YAML::Key << "size" << YAML::Value << layer.size; + if (layer.rotation != 0.f) + out << YAML::Key << "rotation" << YAML::Value << layer.rotation; + out << YAML::EndMap; + } + else + out << layer.material; + } + out << YAML::EndSeq; + if (false == Manifest.water.empty()) + { + out << YAML::Key << "water" << YAML::Value << YAML::BeginSeq; + for (auto const &water : Manifest.water) + { + out << YAML::BeginMap; + out << YAML::Key << "name" << YAML::Value << water.name; + out << YAML::Key << "level" << YAML::Value << water.level; + if (false == water.material.empty()) + out << YAML::Key << "material" << YAML::Value << water.material; + if (water.size > 0.f) + out << YAML::Key << "size" << YAML::Value << water.size; + out << YAML::Key << "outline" << YAML::Value << YAML::BeginSeq; + for (auto const &point : water.outline) + out << YAML::Flow << YAML::BeginSeq << point.x << point.y << YAML::EndSeq; + out << YAML::EndSeq; + out << YAML::EndMap; + } + out << YAML::EndSeq; + } + if (false == Manifest.modifiers.empty()) + { + out << YAML::Key << "modifiers" << YAML::Value << YAML::BeginSeq; + for (auto const &item : Manifest.modifiers) + { + out << YAML::BeginMap; + out << YAML::Key << "name" << YAML::Value << item.name; + out << YAML::Key << "slope" << YAML::Value << item.slope; + out << YAML::Key << "reach" << YAML::Value << item.reach; + out << YAML::Key << "rounding" << YAML::Value << item.rounding; + out << YAML::Key << "points" << YAML::Value << YAML::BeginSeq; + for (auto const &point : item.points) + out << YAML::Flow << YAML::BeginSeq << point.position.x << point.position.y << point.formation << point.half_width << YAML::EndSeq; + out << YAML::EndSeq; + out << YAML::EndMap; + } + out << YAML::EndSeq; + } + out << YAML::EndMap; + if (false == out.good()) + { + if (Error != nullptr) + *Error = out.GetLastError(); + return false; + } + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path(Path).parent_path(), ec); + std::ofstream file{Path, std::ios::binary | std::ios::trunc}; + file << out.c_str() << '\n'; + if (false == file.good()) + { + if (Error != nullptr) + *Error = "can't write " + Path; + return false; + } + return true; +} + +// pack files + +bool pack_file::open(std::string const &Path, std::string *Error) +{ + auto const fail = [&](std::string const &Message) { + if (Error != nullptr) + *Error = Path + ": " + Message; + return false; + }; + m_path = Path; + for (auto &slices : m_slices) + slices.clear(); + std::ifstream file{Path, std::ios::binary}; + if (false == file.good()) + return fail("can't open the file"); + std::vector header(header_size); + file.read(reinterpret_cast(header.data()), header.size()); + if (file.gcount() != static_cast(header.size())) + return fail("the file is too short"); + reader input{header.data(), header.size()}; + std::uint32_t magic{0}, version{0}, side{0}; + std::int32_t px{0}, pz{0}; + float size{0.f}; + std::uint64_t indexoffset{0}; + input.get(magic); + input.get(version); + input.get(px); + input.get(pz); + input.get(side); + input.get(size); + input.get(indexoffset); + if (magic != pack_magic) + return fail("not a terrain chunks file"); + if (version != pack_version) + return fail("unsupported version " + std::to_string(version)); + if (side != pack_side || std::abs(size - chunk_size) > 1e-3) + return fail("unsupported chunk layout"); + m_key = {px, pz}; + + std::vector index(pack_side * pack_side * index_entry_size); + file.seekg(static_cast(indexoffset)); + file.read(reinterpret_cast(index.data()), index.size()); + if (file.gcount() != static_cast(index.size())) + return fail("damaged chunk index"); + reader entries{index.data(), index.size()}; + for (auto &slices : m_slices) + { + std::uint64_t tableoffset{0}; + std::uint32_t count{0}, reserved{0}; + entries.get(tableoffset); + entries.get(count); + entries.get(reserved); + if (tableoffset == 0 || count == 0) + continue; + if (count > 64) + return fail("damaged chunk index"); + std::vector table(count * slice_entry_size); + file.seekg(static_cast(tableoffset)); + file.read(reinterpret_cast(table.data()), table.size()); + if (file.gcount() != static_cast(table.size())) + return fail("damaged slice table"); + reader tableinput{table.data(), table.size()}; + slices.resize(count); + for (auto &slice : slices) + { + std::uint32_t reserved2{0}; + tableinput.get(slice.tag); + tableinput.get(slice.codec); + tableinput.get(slice.offset); + tableinput.get(slice.stored); + tableinput.get(slice.size); + tableinput.get(slice.crc); + tableinput.get(reserved2); + if (slice.stored > max_slice_size || slice.size > max_slice_size) + return fail("damaged slice table"); + } + } + return true; +} + +bool pack_file::has(chunk_key const &Key) const +{ + if (pack_of(Key) != m_key) + return false; + auto const x{Key.first - m_key.first * pack_side}; + auto const z{Key.second - m_key.second * pack_side}; + return false == m_slices[static_cast(z) * pack_side + x].empty(); +} + +std::vector pack_file::chunks() const +{ + std::vector result; + for (int z = 0; z < pack_side; ++z) + for (int x = 0; x < pack_side; ++x) + if (false == m_slices[static_cast(z) * pack_side + x].empty()) + result.emplace_back(m_key.first * pack_side + x, m_key.second * pack_side + z); + return result; +} + +std::shared_ptr pack_file::read(chunk_key const &Key, std::string *Error) const +{ + auto const fail = [&](std::string const &Message) -> std::shared_ptr { + if (Error != nullptr) + *Error = m_path + ": chunk " + std::to_string(Key.first) + "," + std::to_string(Key.second) + ": " + Message; + return nullptr; + }; + if (false == has(Key)) + return fail("not in the file"); + auto const &slices{m_slices[static_cast(Key.second - m_key.second * pack_side) * pack_side + (Key.first - m_key.first * pack_side)]}; + std::ifstream file{m_path, std::ios::binary}; + if (false == file.good()) + return fail("can't open the file"); + + auto const load = [&](slice const &Slice, std::vector &Out) { + std::vector stored(Slice.stored); + file.seekg(static_cast(Slice.offset)); + file.read(reinterpret_cast(stored.data()), stored.size()); + if (file.gcount() != static_cast(stored.size())) + return false; + if (Slice.codec == codec_stored) + Out = std::move(stored); + else if (Slice.codec == codec_zstd) + { + Out.resize(Slice.size); + auto const result{ZSTD_decompress(Out.data(), Out.size(), stored.data(), stored.size())}; + if (ZSTD_isError(result) || result != Slice.size) + return false; + } + else + return false; + return Out.size() == Slice.size && checksum(Out) == Slice.crc; + }; + + auto chunk{std::make_shared()}; + chunk->key = Key; + slice const *heights{nullptr}, *base{nullptr}, *adjust{nullptr}, *paint{nullptr}; + bool header{false}; + for (auto const &slice : slices) + { + if (slice.tag == tag_chunk) + { + std::vector data; + if (false == load(slice, data)) + return fail("damaged chunk header"); + reader input{data.data(), data.size()}; + std::uint16_t cells{0}, samples{0}; + input.get(cells); + input.get(samples); + for (auto &layer : chunk->layers) + input.get(layer); + chunk->cells = cells; + chunk->paint = samples; + header = true; + } + else if (slice.tag == tag_heights) + heights = &slice; + else if (slice.tag == tag_base) + base = &slice; + else if (slice.tag == tag_adjust) + adjust = &slice; + else if (slice.tag == tag_paint) + paint = &slice; + // slices of unknown kinds are left for newer versions of the simulator + } + if (false == header || heights == nullptr || chunk->cells <= 0 || chunk->cells > 4096) + return fail("incomplete chunk"); + if (chunk->layers[0] == no_layer) + chunk->layers[0] = 0; + auto const count{static_cast(chunk->side()) * chunk->side()}; + std::vector data; + if (false == load(*heights, data) || false == decode_heights(data, count, chunk->heights)) + return fail("damaged heights"); + if (base != nullptr && (false == load(*base, data) || false == decode_heights(data, count, chunk->base))) + return fail("damaged base heights"); + // touch-ups go with the base heights only + if (adjust != nullptr && false == chunk->base.empty() && (false == load(*adjust, data) || false == decode_heights(data, count, chunk->adjust))) + return fail("damaged touch-ups"); + if (chunk->paint > 0) + { + auto const expected{static_cast(chunk->paint + 1) * (chunk->paint + 1) * chunk->layer_count()}; + if (paint == nullptr || false == load(*paint, chunk->weights) || chunk->weights.size() != expected) + return fail("damaged paint"); + } + return chunk; +} + +bool write_pack(std::string const &Path, pack_key const &Key, std::vector const &Chunks, bool const Compress, std::string *Error) +{ + struct slice_record + { + std::uint32_t tag; + std::uint32_t codec; + std::uint64_t offset; + std::uint32_t stored; + std::uint32_t size; + std::uint32_t crc; + }; + std::array, pack_side * pack_side> tables; + std::vector payload; // slice data, placed after the header + + auto const add = [&](std::vector &Table, std::uint32_t const Tag, std::vector const &Data) { + slice_record record{Tag, codec_stored, header_size + payload.size(), static_cast(Data.size()), static_cast(Data.size()), checksum(Data)}; + if (Compress && Data.size() > 64) + { + std::vector packed(ZSTD_compressBound(Data.size())); + auto const result{ZSTD_compress(packed.data(), packed.size(), Data.data(), Data.size(), zstd_level)}; + if (false == ZSTD_isError(result) && result < Data.size()) + { + packed.resize(result); + record.codec = codec_zstd; + record.stored = static_cast(packed.size()); + payload.insert(payload.end(), packed.begin(), packed.end()); + Table.push_back(record); + return; + } + } + payload.insert(payload.end(), Data.begin(), Data.end()); + Table.push_back(record); + }; + + for (auto const &chunk : Chunks) + { + if (chunk == nullptr || false == chunk->valid() || pack_of(chunk->key) != Key) + continue; + auto &table{tables[static_cast(chunk->key.second - Key.second * pack_side) * pack_side + (chunk->key.first - Key.first * pack_side)]}; + table.clear(); + std::vector data; + put(data, static_cast(chunk->cells)); + put(data, static_cast(chunk->layer_count() > 1 ? chunk->paint : 0)); + for (auto const layer : chunk->layers) + put(data, layer); + add(table, tag_chunk, data); + data.clear(); + encode_heights(chunk->heights, data); + add(table, tag_heights, data); + if (chunk->base.size() == chunk->heights.size()) + { + data.clear(); + encode_heights(chunk->base, data); + add(table, tag_base, data); + if (chunk->adjust.size() == chunk->heights.size()) + { + data.clear(); + encode_heights(chunk->adjust, data); + add(table, tag_adjust, data); + } + } + if (chunk->paint > 0 && chunk->layer_count() > 1) + add(table, tag_paint, chunk->weights); + } + + std::vector file; + file.reserve(header_size + payload.size() + 4096); + put(file, pack_magic); + put(file, pack_version); + put(file, Key.first); + put(file, Key.second); + put(file, pack_side); + put(file, static_cast(chunk_size)); + auto const indexfield{file.size()}; + put(file, 0); + file.resize(header_size, 0); + file.insert(file.end(), payload.begin(), payload.end()); + // slice tables + std::array tableoffsets{}; + for (std::size_t i = 0; i < tables.size(); ++i) + { + if (tables[i].empty()) + continue; + tableoffsets[i] = file.size(); + for (auto const &record : tables[i]) + { + put(file, record.tag); + put(file, record.codec); + put(file, record.offset); + put(file, record.stored); + put(file, record.size); + put(file, record.crc); + put(file, 0); + } + } + // chunk index + put_at(file, indexfield, file.size()); + for (std::size_t i = 0; i < tables.size(); ++i) + { + put(file, tableoffsets[i]); + put(file, static_cast(tables[i].size())); + put(file, 0); + } + + // written next to the old file first, so a failed write doesn't take the old content with it + std::error_code ec; + std::filesystem::create_directories(std::filesystem::path(Path).parent_path(), ec); + auto const temporary{Path + ".tmp"}; + { + std::ofstream out{temporary, std::ios::binary | std::ios::trunc}; + out.write(reinterpret_cast(file.data()), file.size()); + if (false == out.good()) + { + if (Error != nullptr) + *Error = "can't write " + temporary; + return false; + } + } + std::filesystem::rename(temporary, Path, ec); + if (ec) + { + // some systems won't replace an existing file with rename + std::filesystem::remove(Path, ec); + std::filesystem::rename(temporary, Path, ec); + if (ec) + { + if (Error != nullptr) + *Error = "can't replace " + Path + ": " + ec.message(); + return false; + } + } + return true; +} + +std::vector triangulate(std::vector const &Outline) +{ + std::vector result; + auto const count{Outline.size()}; + if (count < 3) + return result; + // ear clipping, the outline turned counterclockwise first + double area{0.0}; + for (std::size_t i = 0; i < count; ++i) + { + auto const &a{Outline[i]}; + auto const &b{Outline[(i + 1) % count]}; + area += a.x * b.y - b.x * a.y; + } + std::vector ring(count); + for (std::size_t i = 0; i < count; ++i) + ring[i] = static_cast(area >= 0.0 ? i : count - 1 - i); + auto const cross = [](glm::dvec2 const &O, glm::dvec2 const &A, glm::dvec2 const &B) { return (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x); }; + auto const inside = [&](glm::dvec2 const &P, glm::dvec2 const &A, glm::dvec2 const &B, glm::dvec2 const &C) { + return cross(A, B, P) >= 0.0 && cross(B, C, P) >= 0.0 && cross(C, A, P) >= 0.0; + }; + std::size_t guard{0}; + while (ring.size() > 3 && guard < count * count) + { + ++guard; + bool clipped{false}; + for (std::size_t i = 0; i < ring.size(); ++i) + { + auto const prev{ring[(i + ring.size() - 1) % ring.size()]}; + auto const curr{ring[i]}; + auto const next{ring[(i + 1) % ring.size()]}; + auto const &a{Outline[prev]}; + auto const &b{Outline[curr]}; + auto const &c{Outline[next]}; + if (cross(a, b, c) <= 1e-12) + continue; // reflex or degenerate corner + bool ear{true}; + for (auto const other : ring) + { + if (other == prev || other == curr || other == next) + continue; + if (inside(Outline[other], a, b, c)) + { + ear = false; + break; + } + } + if (false == ear) + continue; + result.insert(result.end(), {prev, curr, next}); + ring.erase(ring.begin() + i); + clipped = true; + break; + } + if (false == clipped) + break; // a self-crossing outline; what's left goes as a fan + } + for (std::size_t i = 1; i + 1 < ring.size(); ++i) + result.insert(result.end(), {ring[0], ring[i], ring[i + 1]}); + return result; +} + +// meshes + +int max_stride(int const Cells) +{ + int stride{1}; + while (stride * 4 <= Cells) + stride *= 2; + return stride; +} + +namespace +{ +int segments(int const Cells, int const Stride) +{ + return (Cells + Stride - 1) / Stride; +} +} // namespace + +std::size_t mesh_vertex_count(int const Cells, int const Stride) +{ + auto const n{static_cast(segments(Cells, Stride))}; + return (n + 1) * (n + 1) + 4 * (n + 1); +} + +void build_indices(int const Cells, int const Stride, gfx::index_array &Out) +{ + auto const n{static_cast(segments(Cells, Stride))}; + auto const row{n + 1}; + auto const grid{row * row}; + Out.clear(); + Out.reserve(static_cast(n) * n * 6 + static_cast(n) * 24); + auto const at = [&](gfx::basic_index const I, gfx::basic_index const J) { return J * row + I; }; + for (gfx::basic_index j = 0; j < n; ++j) + for (gfx::basic_index i = 0; i < n; ++i) + { + auto const v00{at(i, j)}, v10{at(i + 1, j)}, v01{at(i, j + 1)}, v11{at(i + 1, j + 1)}; + Out.insert(Out.end(), {v00, v01, v10, v11, v10, v01}); + } + // skirts: south (low z) and east face the way the edge is walked turns them, north and west are turned around + auto const skirt = [&](gfx::basic_index const Skirt, auto const &Edge, bool const Flip) { + for (gfx::basic_index k = 0; k < n; ++k) + { + auto const a{Edge(k)}, b{Edge(k + 1)}, la{Skirt + k}, lb{Skirt + k + 1}; + if (Flip) + Out.insert(Out.end(), {la, b, a, la, lb, b}); + else + Out.insert(Out.end(), {a, b, la, b, lb, la}); + } + }; + skirt(grid, [&](gfx::basic_index const K) { return at(K, 0); }, false); + skirt(grid + row, [&](gfx::basic_index const K) { return at(K, n); }, true); + skirt(grid + 2 * row, [&](gfx::basic_index const K) { return at(0, K); }, true); + skirt(grid + 3 * row, [&](gfx::basic_index const K) { return at(n, K); }, false); +} + +void build_mesh(chunk_data const &Chunk, int const Stride, glm::dvec3 const &Origin, height_source const &Outside, mesh &Out) +{ + auto const cells{Chunk.cells}; + auto const stride{std::clamp(Stride, 1, max_stride(cells))}; + auto const n{segments(cells, stride)}; + auto const spacing{Chunk.spacing()}; + auto const low{Chunk.corner()}; + auto const texturebase{pack_corner(pack_of(Chunk.key))}; + auto const coordinate = [&](int const I) { return std::min(I * stride, cells); }; + + // heights of the grid points, the neighbouring chunks asked about the points past the edges + auto const sample = [&](int const Ix, int const Iz) -> double { + if (Ix >= 0 && Ix <= cells && Iz >= 0 && Iz <= cells) + return Chunk.height(Ix, Iz); + double height; + if (Outside && Outside(low.x + Ix * spacing, low.y + Iz * spacing, height)) + return height; + return Chunk.height(std::clamp(Ix, 0, cells), std::clamp(Iz, 0, cells)); + }; + // normals from the differences over the stride, so the distant, coarse grid is lit as it's drawn + auto const normal = [&](int const Ix, int const Iz) { + auto const dx{(sample(Ix + stride, Iz) - sample(Ix - stride, Iz)) / (2.0 * stride * spacing)}; + auto const dz{(sample(Ix, Iz + stride) - sample(Ix, Iz - stride)) / (2.0 * stride * spacing)}; + return glm::normalize(glm::vec3(static_cast(-dx), 1.f, static_cast(-dz))); + }; + + // the skirt has to reach below the edge of a neighbour drawn with a coarser grid + double deviation{0.0}; + { + auto const coarse{std::min(stride * 4, cells)}; + auto const edge = [&](auto const &Height) { + for (int start = 0; start < cells; start += coarse) + { + auto const end{std::min(start + coarse, cells)}; + auto const a{Height(start)}, b{Height(end)}; + for (int k = start + 1; k < end; ++k) + deviation = std::max(deviation, std::abs(Height(k) - (a + (b - a) * (k - start) / double(end - start)))); + } + }; + edge([&](int const K) { return static_cast(Chunk.height(K, 0)); }); + edge([&](int const K) { return static_cast(Chunk.height(K, cells)); }); + edge([&](int const K) { return static_cast(Chunk.height(0, K)); }); + edge([&](int const K) { return static_cast(Chunk.height(cells, K)); }); + } + auto const skirtdepth{static_cast(0.5 + 2.0 * deviation + 0.05 * stride * spacing)}; + + Out.vertices.clear(); + Out.vertices.reserve(mesh_vertex_count(cells, stride)); + glm::dvec3 lowest{std::numeric_limits::max()}, highest{-std::numeric_limits::max()}; + auto const vertex = [&](int const Ix, int const Iz, float const Drop) { + gfx::basic_vertex v; + glm::dvec3 const world{low.x + Ix * spacing, Chunk.height(Ix, Iz), low.y + Iz * spacing}; + lowest = glm::min(lowest, world); + highest = glm::max(highest, world); + v.position = glm::vec3(world - Origin); + v.position.y -= Drop; + v.normal = normal(Ix, Iz); + // texture coordinates in metres from the corner of the pack: small enough for floats, and seamless for any + // texture repeating a whole number of times over the pack. v runs along -z, the way the tangent space expects it + v.texture = glm::vec2(static_cast(world.x - texturebase.x), static_cast(texturebase.y - world.z)); + auto const tangent{glm::vec3(1.f, 0.f, 0.f) - v.normal * v.normal.x}; + v.tangent = glm::vec4(glm::length(tangent) > 1e-6f ? glm::normalize(tangent) : glm::vec3(1.f, 0.f, 0.f), 1.f); + Out.vertices.push_back(v); + }; + for (int j = 0; j <= n; ++j) + for (int i = 0; i <= n; ++i) + vertex(coordinate(i), coordinate(j), 0.f); + for (int k = 0; k <= n; ++k) + vertex(coordinate(k), 0, skirtdepth); + for (int k = 0; k <= n; ++k) + vertex(coordinate(k), cells, skirtdepth); + for (int k = 0; k <= n; ++k) + vertex(0, coordinate(k), skirtdepth); + for (int k = 0; k <= n; ++k) + vertex(cells, coordinate(k), skirtdepth); + + build_indices(cells, stride, Out.indices); + Out.centre = (lowest + highest) * 0.5; + Out.radius = static_cast(glm::length(highest - lowest) * 0.5 + skirtdepth); + Out.low = lowest - glm::dvec3{0.0, skirtdepth, 0.0}; + Out.high = highest; +} + +} // namespace heightmap diff --git a/scene/heightmapterrain.h b/scene/heightmapterrain.h new file mode 100644 index 00000000..c578e78f --- /dev/null +++ b/scene/heightmapterrain.h @@ -0,0 +1,250 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rendering/geometrybank.h" + +// heightmap terrain: the ground of a scenery as a grid of square chunks of heights, kept in the folder +// terrain// of the simulator: a description of the terrain (terrain.yaml) and pack files (.tch, terrain chunks) +// holding 16 x 16 chunks each. the chunks are loaded around the camera as they're needed. +// +// a chunk has its own point spacing (0.5, 1, 2 or 5 m), heights of its grid points including the points of its edges +// (shared with the neighbouring chunk), and weights of the materials painted over it, up to 8 of them per chunk. +namespace heightmap +{ + +double constexpr chunk_size{250.0}; // metres, side of a chunk; whole cells of every allowed spacing, and of the scene +int constexpr pack_side{16}; // chunks along the side of a pack file +double constexpr height_step{0.005}; // metres, quantum of the stored heights +std::size_t constexpr max_chunk_layers{8}; // materials painted over a single chunk +std::uint16_t constexpr no_layer{0xffff}; +int constexpr default_paint_samples{250}; // paint samples along the side of a chunk, less one: a sample per metre + +// point spacings a chunk can have, finest first +std::array constexpr spacings{0.5f, 1.f, 2.f, 5.f}; +float constexpr default_spacing{2.f}; +// cells along the side of a chunk with specified point spacing +int cells_for(float const Spacing); +// the allowed spacing closest to specified one +float valid_spacing(float const Spacing); + +using chunk_key = std::pair; +using pack_key = std::pair; + +// chunk covering specified point +chunk_key chunk_at(double const X, double const Z); +// corner of the chunk with the lowest coordinates +glm::dvec2 chunk_corner(chunk_key const &Key); +// pack holding specified chunk +pack_key pack_of(chunk_key const &Key); +// corner of the pack with the lowest coordinates; texture coordinates of the terrain start over at it +glm::dvec2 pack_corner(pack_key const &Key); +// name of the file of specified pack +std::string pack_file_name(pack_key const &Key); + +struct chunk_data +{ + chunk_key key{0, 0}; + int cells{0}; // quads along a side; (cells + 1)^2 grid points + std::vector heights; // world y of the grid points, rows of growing z, from the lowest x + std::vector base; // the heights before the modifiers were applied; empty where no modifier reaches the chunk + // touch-ups made by hand over what the modifiers give, added to it: heights = modifiers(base) + adjust. + // empty when there are none, and always when base is empty + std::vector adjust; + int paint{0}; // paint samples along a side, less one. 0: the chunk is covered with its first layer alone + std::array layers; // indices of the materials in the terrain palette, no_layer for unused + std::vector weights; // planar, (paint + 1)^2 per used layer, rows like the heights; weights of a sample add up to 255 + + chunk_data() { layers.fill(no_layer); } + + int side() const { return cells + 1; } + double spacing() const { return chunk_size / cells; } + glm::dvec2 corner() const { return chunk_corner(key); } + bool valid() const { return cells > 0 && heights.size() == static_cast(side()) * side(); } + // number of layers in use, the used ones come first + std::size_t layer_count() const; + float height(int const Ix, int const Iz) const { return heights[static_cast(Iz) * side() + Ix]; } + // height of the surface at specified world point, the way the full grid is triangulated; points outside are clamped to the chunk + double height_at(double const X, double const Z) const; + // as above, from provided heights of the grid points of the chunk + double height_in(std::vector const &Heights, double const X, double const Z) const; + bool contains(double const X, double const Z) const; + // a flat chunk of specified spacing at specified height, covered with the first material of the palette + static std::shared_ptr make_flat(chunk_key const &Key, float const Spacing, float const Height); + // the same chunk with the grid of another spacing, heights and paint taken over. a coarser grid takes the average + // of the ground around each of its points (the finer one only its own heights). Paint: paint samples along a side + // less one the chunk gets, 0 keeps the ones it has + std::shared_ptr resampled(float const Spacing, int const Paint = 0) const; + // weight of specified layer slot at paint sample (Ix, Iz) + std::uint8_t weight(std::size_t const Slot, int const Ix, int const Iz) const; + // layer slot of specified palette index, or -1 + int slot_of(std::uint16_t const Layer) const; + // layer slot of the material covering the most of the chunk + std::size_t dominant_slot() const; + // gives the chunk paint samples (with its first layer as the only one painted) if it has none yet + void ensure_paint(int const Samples = default_paint_samples); + // drops the layers left without weight anywhere, and the paint samples altogether when a single layer is left + void compact_layers(); +}; + +using chunk_ptr = std::shared_ptr; + +// body of water: a level surface within an outline +struct water_body +{ + std::string name; + double level{0.0}; + std::string material; // empty: the default water material + float size{0.f}; // metres the texture repeats at; 0: the size given by the material, or 8 m + std::vector outline; // x, z +}; + +// modifier of the terrain: a corridor along a line (axis of a track or a road) with a formation of specified +// height and width, and slopes from its edges to the ground +struct corridor_point +{ + glm::dvec2 position{0.0}; // x, z + double formation{0.0}; // height of the formation + double half_width{3.5}; // from the axis to the edge of the formation +}; + +struct modifier +{ + std::string name; + double slope{1.5}; // horizontal run of the slopes per metre of height + double reach{60.0}; // from the edge of the formation, where the slopes end even if they don't get to the ground + double rounding{1.5}; // over which the slopes bend into the formation and into the ground + std::vector points; + + // rectangle (x, z) the modifier can change the ground in + std::pair bounds() const; +}; + +// modifier prepared for many questions +class modifier_shape +{ + public: + explicit modifier_shape(modifier const &Modifier); + // leads Height at specified point the way the modifier does; returns: true if the height changed + bool apply(double const X, double const Z, float &Height) const; + bool reaches(glm::dvec2 const &Min, glm::dvec2 const &Max) const; + + private: + modifier const &m_modifier; + double m_widest{0.0}; + glm::dvec2 m_min{0.0}, m_max{0.0}; + double m_cell{16.0}; + std::vector> m_grid; // (cell key, segment), sorted +}; + +// material of the palette, and the size (metres) its textures repeat at; 0: the size given by the material, or 8 m. +// the same material can be in the palette more than once, e.g. turned by another angle +struct layer_def +{ + std::string material; + float size{0.f}; + float rotation{0.f}; // degrees the textures are turned by, counterclockwise seen from above +}; + +// description of the terrain, terrain.yaml +struct manifest +{ + int version{1}; + std::vector layers{{"grass", 0.f}}; // the palette of materials, the first covers new chunks + std::string water_material{"water"}; // of the bodies of water which don't name their own + bool compress{true}; // pack files store their data compressed with zstd + float spacing{default_spacing}; // point spacing of new chunks + std::vector water; + std::vector modifiers; +}; + +bool load_manifest(std::string const &Path, manifest &Manifest, std::string *Error = nullptr); +bool save_manifest(std::string const &Path, manifest const &Manifest, std::string *Error = nullptr); + +// pack file (.tch), little endian: +// +// header, 64 bytes: 'TCH1', uint32 version, int32 pack x, int32 pack z, uint32 chunks along a side, float chunk size, +// uint64 offset of the chunk index, reserved +// chunk index: per chunk (rows of growing z): uint64 offset of its slice table (0: no chunk), uint32 slice count, uint32 reserved +// slice tables: per slice: uint32 tag, uint32 codec (0: stored, 1: zstd), uint64 offset, uint32 stored size, uint32 size, +// uint32 crc-32 of the data, uint32 reserved +// slice data +// +// slices of a chunk: +// 'CHNK' uint16 cells, uint16 paint samples less one, uint16 layers[8] +// 'HGHT' heights: uint8 encoding (0: int32 base and uint16 steps of height_step above it, delta coded in row order; +// 1: float32), then the values +// 'HBAS' heights before the modifiers, as 'HGHT' +// 'HADJ' touch-ups over the modifiers, as 'HGHT' (only with 'HBAS'; older versions skip it and keep the heights) +// 'SPLT' paint weights: uint8 per sample per used layer, planar +class pack_file +{ + public: + // reads the header and the index of specified file. returns: true on success + bool open(std::string const &Path, std::string *Error = nullptr); + bool has(chunk_key const &Key) const; + // chunks the file holds + std::vector chunks() const; + // reads specified chunk. returns: the chunk, or null on failure + std::shared_ptr read(chunk_key const &Key, std::string *Error = nullptr) const; + pack_key key() const { return m_key; } + + private: + struct slice + { + std::uint32_t tag{0}; + std::uint32_t codec{0}; + std::uint64_t offset{0}; + std::uint32_t stored{0}; + std::uint32_t size{0}; + std::uint32_t crc{0}; + }; + std::string m_path; + pack_key m_key{0, 0}; + std::array, pack_side * pack_side> m_slices; +}; + +// writes specified chunks (all of the same pack) to the pack file. returns: true on success +bool write_pack(std::string const &Path, pack_key const &Key, std::vector const &Chunks, bool const Compress, std::string *Error = nullptr); + +// triangles (indices of the points, three to a triangle) covering a simple polygon in the XZ plane +std::vector triangulate(std::vector const &Outline); + +// samples the height of the ground outside of a chunk; returns false where it doesn't know +using height_source = std::function; + +// geometry of a chunk at specified level of detail: grid points taken every Stride points of the grid, with a skirt +// along the edges hiding the gaps to the neighbours drawn with another grid. positions relative to Origin +struct mesh +{ + gfx::vertex_array vertices; + gfx::index_array indices; + float radius{0.f}; // from the centre of the vertices + glm::dvec3 centre{0.0}; + glm::dvec3 low{0.0}, high{0.0}; // world bounds of the grid, the skirts included +}; +// strides the grid of a chunk with specified cells can be drawn with +int max_stride(int const Cells); +void build_mesh(chunk_data const &Chunk, int const Stride, glm::dvec3 const &Origin, height_source const &Outside, mesh &Out); +// indices of the mesh of a chunk with specified cells and stride; the same for every chunk of these +void build_indices(int const Cells, int const Stride, gfx::index_array &Out); +// vertices the mesh of a chunk with specified cells and stride has +std::size_t mesh_vertex_count(int const Cells, int const Stride); + +} // namespace heightmap diff --git a/scene/scene.cpp b/scene/scene.cpp index 8ebff031..7b0e2994 100644 --- a/scene/scene.cpp +++ b/scene/scene.cpp @@ -19,6 +19,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Timer.h" #include "utilities/Logs.h" #include "scene/sn_utils.h" +#include "scene/sceneterrain.h" #include "rendering/renderer.h" #include "widgets/map_objects.h" @@ -378,7 +379,9 @@ basic_cell::insert( TAnimModel *Instance ) { if( mat != nullptr ) { for( int i = 0; i < 5; ++i ) { key.skins[i] = mat->replacable_skins[i]; } } - m_instancebuckets_opaque[ key ].emplace_back( Instance ); + auto &bucket { m_instancebuckets_opaque[ key ] }; + bucket.instances.emplace_back( Instance ); + bucket.bounds.clear(); } } // re-calculate cell bounding area, in case model extends outside the cell's boundaries @@ -416,6 +419,37 @@ basic_cell::insert( TMemCell *Memorycell ) { // NOTE: memory cells are virtual 'points' hence they don't ever expand cell range } +void +basic_cell::erase( TTrack *Path ) { + + m_paths.erase( + std::remove( std::begin( m_paths ), std::end( m_paths ), Path ), + std::end( m_paths ) ); + if( tTrackAnim == Path ) { + tTrackAnim = Path->SwitchExtension->pNextAnim; + Path->SwitchExtension->pNextAnim = nullptr; + } + else { + for( auto *track = tTrackAnim; track != nullptr; track = track->SwitchExtension->pNextAnim ) { + if( track->SwitchExtension->pNextAnim == Path ) { + track->SwitchExtension->pNextAnim = Path->SwitchExtension->pNextAnim; + Path->SwitchExtension->pNextAnim = nullptr; + break; + } + } + } + Path->RaOwnerSet( nullptr ); + // TODO: update cell bounding area +} + +void +basic_cell::unregister_end( TTrack *Path ) { + + m_directories.paths.erase( + std::remove( std::begin( m_directories.paths ), std::end( m_directories.paths ), Path ), + std::end( m_directories.paths ) ); +} + // removes provided model instance from the cell void basic_cell::erase( TAnimModel *Instance ) { @@ -446,10 +480,12 @@ basic_cell::erase( TAnimModel *Instance ) { // NOTE: searched by pointer rather than by key: the instance may have lost its instanceable // status or changed its skins since it was inserted, which would leave a dangling pointer behind for( auto bucket = m_instancebuckets_opaque.begin(); bucket != m_instancebuckets_opaque.end(); ) { - bucket->second.erase( - std::remove( std::begin( bucket->second ), std::end( bucket->second ), Instance ), - std::end( bucket->second ) ); - if( bucket->second.empty() ) { + auto &instances { bucket->second.instances }; + instances.erase( + std::remove( std::begin( instances ), std::end( instances ), Instance ), + std::end( instances ) ); + bucket->second.bounds.clear(); + if( instances.empty() ) { bucket = m_instancebuckets_opaque.erase( bucket ); } else { @@ -472,6 +508,30 @@ basic_cell::erase( TMemCell *Memorycell ) { std::end( m_memorycells ) ); } +void +basic_cell::erase( TTraction *Traction ) { + + m_traction.erase( + std::remove( std::begin( m_traction ), std::end( m_traction ), Traction ), + std::end( m_traction ) ); +} + +void +basic_cell::unregister_end( TTraction *Traction ) { + + m_directories.traction.erase( + std::remove( std::begin( m_directories.traction ), std::end( m_directories.traction ), Traction ), + std::end( m_directories.traction ) ); +} + +void +basic_cell::erase( TEventLauncher *Launcher ) { + + m_eventlaunchers.erase( + std::remove( std::begin( m_eventlaunchers ), std::end( m_eventlaunchers ), Launcher ), + std::end( m_eventlaunchers ) ); +} + // registers provided path in the lookup directory of the cell void basic_cell::register_end( TTrack *Path ) { @@ -952,6 +1012,12 @@ basic_section::center( glm::dvec3 Center ) { void basic_section::create_geometry() { + if( false == m_terrain.empty() ) { + // this is the first thing done with a section about to be drawn or modified, which makes it + // the moment for the geometry kept in binary terrain files to arrive + load_terrain(); + } + if( true == m_geometrycreated ) { return; } else { // mark it done for future checks @@ -971,6 +1037,18 @@ basic_section::create_geometry() { } } +// loads geometry of the section kept in binary terrain files, if any is waiting +void +basic_section::load_terrain() { + + // the list is emptied first, as loading can lead back here + auto const blocks { std::move( m_terrain ) }; + m_terrain.clear(); + for( auto const &block : blocks ) { + terrain_file::load( *this, block ); + } +} + void basic_section::create_map_geometry(const gfx::geometrybank_handle handle) { std::vector lines; @@ -1722,6 +1800,16 @@ void basic_region::create_map_geometry() } } +// loads all geometry kept in binary terrain files which wasn't needed so far +void basic_region::load_terrain() +{ + for( auto *section : m_sections ) { + if( section != nullptr ) { + section->load_terrain(); + } + } +} + void basic_region::update_poi_geometry() { std::vector vertices; diff --git a/scene/scene.h b/scene/scene.h index 9f5201f8..086e2e83 100644 --- a/scene/scene.h +++ b/scene/scene.h @@ -39,8 +39,14 @@ struct scratch_data { struct binary_data { - bool terrain{ false }; - bool terrain_included{false}; + bool terrain{ false }; // static geometry comes from binary terrain, its text definitions are skipped + bool terrain_included{false}; // binary terrain comes from the files named by terrain directives instead of the default one + bool terrain_default{ false }; // the scenario has usable binary terrain file of its own + std::vector terrain_files; // files named by terrain directives; each one is loaded once + std::size_t geometry_imported{ 0 }; // pieces of static geometry read from the text definitions so far + std::size_t geometry_skipped{ 0 }; // pieces of static geometry left out so far, as provided by binary terrain + std::vector terrain_textfiles; // terrain files (.txtf with binary .btf version) named by terrain directives so far + std::vector terrain_binaryfiles; // binary versions of these, to be put to use once the scenery is initialized } binary; struct location_data { @@ -63,7 +69,9 @@ struct scratch_data { std::vector vehicles; std::vector couplings; TDynamicObject * driver { nullptr }; + TTrack * path { nullptr }; // given directly, for a trainset placed in the editor on a path which may have no name bool is_open { false }; + bool reversed { false }; std::unordered_map assignment; } trainset; @@ -74,6 +82,16 @@ struct scratch_data { bool time_initialized { false }; }; +struct terrain_source; + +// piece of a binary terrain file holding geometry of a single section, waiting to be loaded +struct terrain_block { + + std::shared_ptr source; // the file and data shared by its pieces + std::uint64_t offset { 0 }; // location of the piece in the file + std::uint32_t size { 0 }; +}; + // basic element of rudimentary partitioning scheme for the section. fixed size, no further subdivision // TBD, TODO: replace with quadtree scheme? class basic_cell { @@ -145,12 +163,24 @@ public: // registers provided traction piece in the lookup directory of the cell void register_end( TTraction *Traction ); + void + erase( TTrack *Path ); + void + unregister_end( TTrack *Path ); // removes provided model instance from the cell void erase( TAnimModel *Instance ); // removes provided memory cell from the cell void erase( TMemCell *Memorycell ); + // removes provided traction piece from the cell + void + erase( TTraction *Traction ); + void + unregister_end( TTraction *Traction ); + // removes provided event launcher from the cell + void + erase( TEventLauncher *Launcher ); // find a vehicle located nearest to specified point, within specified radius. reurns: located vehicle and distance std::tuple find( glm::dvec3 const &Point, float const Radius, bool const Onlycontrolled, bool const Findbycoupler ) const; @@ -209,6 +239,21 @@ public: } }; using instance_bucket_map = std::unordered_map< instance_bucket_key, std::vector, instance_bucket_key_hash >; + // what it takes to tell that an instance is out of sight, copied from the instance. + // a cell can hold thousands of instances, each of them large and placed wherever the memory was free, and a render + // pass rejects most of them. the renderer makes the copies and goes by them, to fetch only the instances it may draw + struct instance_bounds { + glm::vec3 center { 0.f }; // location of the instance, in the precision the frustum test takes it + float radius { 0.f }; + float rangesquaredmax { 0.f }; // past it the instance isn't drawn: by its own range, or the lod stages of its model + }; + struct instance_bucket { + std::vector instances; + // caches owned by the renderer; emptied whenever the content of the bucket changes + mutable std::vector bounds; // copies for the instances, in the same order + mutable unsigned int boundsversion { 0 }; + }; + using instance_bucketdata_map = std::unordered_map< instance_bucket_key, instance_bucket, instance_bucket_key_hash >; using sound_sequence = std::vector; using eventlauncher_sequence = std::vector; using memorycell_sequence = std::vector; @@ -229,7 +274,7 @@ public: // batched instance buckets keyed by shared TModel3d*; populated alongside // m_instancesopaque for nodes whose TAnimModel::m_instanceable == true. // The renderer uses these to amortise per-model state setup across many instances. - instance_bucket_map m_instancebuckets_opaque; + instance_bucketdata_map m_instancebuckets_opaque; traction_sequence m_traction; sound_sequence m_sounds; eventlauncher_sequence m_eventlaunchers; @@ -308,6 +353,10 @@ public: void register_node( Type_ *Node, glm::dvec3 const &Point ) { cell( Point ).register_end( Node ); } + template + void + unregister_node( Type_ *Node, glm::dvec3 const &Point ) { + cell( Point ).unregister_end( Node ); } // find a vehicle located nearest to specified point, within specified radius. reurns: located vehicle and distance std::tuple find( glm::dvec3 const &Point, float const Radius, bool const Onlycontrolled, bool const Findbycoupler ); @@ -326,6 +375,9 @@ public: // generates renderable version of held non-instanced geometry void create_geometry(); + // loads geometry of the section kept in binary terrain files, if any is waiting + void + load_terrain(); void create_map_geometry(const gfx::geometrybank_handle handle); void @@ -354,6 +406,7 @@ public: // content cell_array m_cells; // partitioning scheme shapenode_sequence m_shapes; // large pieces of opaque geometry and (legacy) terrain + std::vector m_terrain; // geometry in binary terrain files, loaded when the section comes into use // TODO: implement dedicated, higher fidelity, fixed resolution terrain mesh item // gfx renderer data gfx::geometrybank_handle m_geometrybank; @@ -435,6 +488,13 @@ public: auto const location{ Node->location() }; if( point_inside( location ) ) { section( location ).erase( Node ); } } + template + void + erase_and_unregister( Type_ *Node ) { + for( auto const &point : Node->endpoints() ) { + if( point_inside( point ) ) { + section( point ).unregister_node( Node, point ); } } + erase( Node ); } // find a vehicle located nearest to specified point, within specified radius. reurns: located vehicle and distance std::tuple find_vehicle( glm::dvec3 const &Point, float const Radius, bool const Onlycontrolled, bool const Findbycoupler ); @@ -454,6 +514,9 @@ public: create_map_geometry(); void update_poi_geometry(); + // loads all geometry kept in binary terrain files which wasn't needed so far + void + load_terrain(); basic_section* get_section(size_t section) { return m_sections[section]; } gfx::geometrybank_handle diff --git a/scene/scenelayers.cpp b/scene/scenelayers.cpp new file mode 100644 index 00000000..b735e344 --- /dev/null +++ b/scene/scenelayers.cpp @@ -0,0 +1,1131 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/scenelayers.h" + +#include "simulation/simulation.h" +#include "model/AnimModel.h" +#include "world/Track.h" +#include "world/Traction.h" +#include "utilities/Globals.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include + +namespace scene +{ + +node_layers Layers; + +// converts world space location to the one which produces it when loaded with this context in effect +glm::dvec3 layer_context::to_local(glm::dvec3 Location) const +{ + // reverse of the transformation done by the scenery loader: rotation around the vertical axis, then scale, then offset + Location -= offset; + Location /= glm::dvec3{scale}; + if (rotation != glm::vec3{0.f}) + { + Location = glm::rotateY(Location, -glm::radians(static_cast(rotation.y))); + } + return Location; +} + +glm::dvec3 layer_context::to_world(glm::dvec3 Location) const +{ + if (rotation != glm::vec3{0.f}) + { + Location = glm::rotateY(Location, glm::radians(static_cast(rotation.y))); + } + Location *= glm::dvec3{scale}; + Location += offset; + return Location; +} + +bool layer_context::matches(layer_context const &Other) const +{ + return glm::all(glm::epsilonEqual(offset, Other.offset, 1e-4)) && glm::all(glm::epsilonEqual(rotation, Other.rotation, 1e-4f)) && glm::all(glm::epsilonEqual(scale, Other.scale, 1e-5f)); +} + +std::size_t basic_layer::item_count() const +{ + return std::accumulate(std::begin(items), std::end(items), std::size_t{0}); +} + +// indicates start of content of the scenario file. returns: handle to its layer +layer_handle node_layers::open(std::string const &Name) +{ + // the same file can be included more than once, all its content goes to a single layer then + auto const lookup{std::find_if(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })}; + auto layerhandle{static_cast(std::distance(std::begin(m_layers), lookup) + 1)}; + if (lookup == std::end(m_layers)) + { + if (m_layers.size() < static_cast(std::numeric_limits::max())) + { + m_layers.emplace_back(); + m_layers.back().name = Name; + m_layers.back().parent = handle(); + m_layers.back().late = m_initialized; + stat(layerhandle); + scan(layerhandle); + } + else + { + // out of handles, content of the file is attributed to the parent layer + layerhandle = handle(); + } + } + if (valid(layerhandle) && layerhandle != handle()) + { + layer(layerhandle).context_begin = m_context; + } + // NOTE: the stack receives an entry even if the layer wasn't created, to keep it in sync with close() calls + m_stack.emplace_back(layerhandle); + + return layerhandle; +} + +// indicates start of content of specified scenery file, included by specified directive. returns: handle to the layer of the file +layer_handle node_layers::open(std::string const &Name, include_site Site) +{ + Site.parent = handle(); + auto const layerhandle{open(Name)}; + if (valid(layerhandle) && layerhandle != Site.parent) + { + layer(layerhandle).sites.emplace_back(Site); + } + return layerhandle; +} + +// indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty +layer_handle node_layers::close() +{ + if (false == m_stack.empty()) + { + auto const closing{m_stack.back()}; + m_stack.pop_back(); + if (valid(closing) && closing != handle()) + { + layer(closing).context_end = m_context; + } + } + return handle(); +} + +void node_layers::clear() +{ + m_layers.clear(); + m_stack.clear(); + m_active = null_handle; + m_context = layer_context(); + m_sources.clear(); + m_erased.clear(); + m_renamed.clear(); + m_shapes.clear(); + m_shapeserased = false; + m_templates.clear(); + m_instances.clear(); + m_instance = 0; + m_initialized = false; + m_terraindirective = false; + m_marked.clear(); + m_marking.clear(); +} + +// indicates the scenario initialization (FirstInit) is being performed +void node_layers::initialization(source_span const &Span, bool const Infile) +{ + if (m_initialized) + { + return; + } + m_initialized = true; + if (m_stack.empty() || false == valid(m_stack.back())) + { + return; + } + // the layer file being loaded holds the directive, or includes the template which does + auto &holder{layer(m_stack.back())}; + if (Infile) + { + // NOTE: scenario without the directive is initialized at its end, with nothing in the file to point at + std::string text(Span.valid() ? static_cast(Span.end - Span.begin) : 0, '\0'); + std::ifstream input{path(m_stack.back()), std::ios_base::binary}; + if (text.empty() || false == input.is_open() || false == input.seekg(Span.begin).good() || false == input.read(text.data(), static_cast(text.size())).good() || + ToLower(text) != "firstinit") + { + return; + } + holder.init = Span; + } + else if (tracked(m_instance) && instance(m_instance).layer == m_stack.back()) + { + holder.init = instance(m_instance).span; + } + else + { + return; + } + holder.context_init = m_context; + // for the files it was included through the initialization takes place at their include directives + for (auto level{m_stack.size() - 1}; level > 0; --level) + { + auto const child{m_stack[level]}; + auto const parent{m_stack[level - 1]}; + if (false == valid(parent) || parent == child || layer(child).sites.empty() || layer(child).sites.back().parent != parent || layer(child).sites.back().fixed || + false == layer(child).sites.back().span.valid()) + { + break; + } + layer(parent).init = layer(child).sites.back().span; + layer(parent).context_init = layer(child).context_begin; + } +} + +// true if specified layer can receive content added in the editor +bool node_layers::accepts(layer_handle Layer, std::string *Reason) const +{ + Layer = resolve(Layer); + if (false == valid(Layer) || layer(Layer).removed) + { + return false; + } + if (false == writable(Layer, Reason)) + { + return false; + } + if (layer(Layer).late) + { + if (Reason != nullptr) + { + *Reason = "the file is loaded after the scenario initialization (FirstInit), which only vehicles should follow"; + } + return false; + } + return true; +} + +// follows layer merges. returns: handle of the layer which currently holds content of specified layer +layer_handle node_layers::resolve(layer_handle Layer) const +{ + // NOTE: merge() rules out cycles, the counter is only a safeguard + for (auto hops = 0; hops < 64 && valid(Layer) && layer(Layer).merged != null_handle; ++hops) + { + Layer = layer(Layer).merged; + } + return Layer; +} + +int node_layers::depth(layer_handle Layer) const +{ + auto depth{0}; + Layer = resolve(Layer); + while (valid(Layer) && layer(Layer).parent != null_handle && depth < 64) + { + Layer = resolve(layer(Layer).parent); + ++depth; + } + return depth; +} + +std::vector node_layers::tree() const +{ + std::vector order; + order.reserve(m_layers.size()); + // NOTE: the layers are few, plain scans do + std::function const add = [&](layer_handle const Parent) { + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + auto const candidate{static_cast(idx + 1)}; + if (listed(candidate) && resolve(layer(candidate).parent) == Parent && order.size() < m_layers.size()) + { + order.emplace_back(candidate); + add(candidate); + } + } + }; + add(null_handle); + return order; +} + +void node_layers::count(layer_handle Layer, layer_item const Item, int const Change) +{ + Layer = resolve(Layer); + if (false == valid(Layer)) + { + return; + } + auto &itemcount{layer(Layer).items[static_cast(Item)]}; + if (Change >= 0) + { + itemcount += static_cast(Change); + } + else + { + itemcount -= std::min(itemcount, static_cast(-Change)); + } +} + +void node_layers::active(layer_handle Layer) +{ + Layer = resolve(Layer); + if (false == accepts(Layer)) + { + return; + } + // new items land in the active layer, they'd be out of reach right away if it was hidden or locked + visible(Layer, true); + locked(Layer, false); + m_active = Layer; +} + +// shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it +void node_layers::visible(layer_handle Layer, bool const Visible) +{ + Layer = resolve(Layer); + if (false == valid(Layer) || layer(Layer).visible == Visible) + { + return; + } + layer(Layer).visible = Visible; + + auto const update_node = [this, Layer, Visible](basic_node *Node) { + if (Node == nullptr || resolve(Node->layer()) != Layer) + { + return; + } + if (Visible) + { + // nodes of a removed include stay out of sight, and so do the ones the editor replaced with its own + if (Node->m_layerhidden && (false == tracked(Node->m_instance) || (false == instance(Node->m_instance).removed && false == stale(Node)))) + { + Node->visible(true); + Node->m_layerhidden = false; + } + } + else if (Node->visible()) + { + // nodes which are invisible on their own aren't marked, so they stay that way when the layer is shown again + Node->visible(false); + Node->m_layerhidden = true; + } + }; + // only these node types are drawn by the renderers basing on their visibility flag + for (auto *instance : simulation::Instances.sequence()) + { + update_node(instance); + } + for (auto *path : simulation::Paths.sequence()) + { + update_node(path); + } + for (auto *traction : simulation::Traction.sequence()) + { + update_node(traction); + } +} + +void node_layers::locked(layer_handle Layer, bool const Locked) +{ + Layer = resolve(Layer); + if (valid(Layer)) + { + layer(Layer).locked = Locked; + } +} + +// true if the file of specified layer can be rewritten with changed nodes. optionally explains why it can't +bool node_layers::writable(layer_handle Layer, std::string *Reason) const +{ + Layer = resolve(Layer); + if (false == valid(Layer)) + { + return false; + } + auto const &target{layer(Layer)}; + char const *reason{nullptr}; + if (target.binary) + { + reason = "the file is loaded from binary terrain cache"; + } + else if (target.sites.size() > 1) + { + // one definition yields several nodes, there's no telling which of them should be written back + reason = "the file is included more than once"; + } + else if (false == target.sites.empty() && target.sites.front().parameters) + { + reason = "the file is included with parameters"; + } + if (reason != nullptr && Reason != nullptr) + { + *Reason = reason; + } + return reason == nullptr; +} + +// true if nodes of specified layer can be selected and modified in the editor +bool node_layers::editable(layer_handle Layer) const +{ + Layer = resolve(Layer); + // nodes without a layer (regular load, or created by the simulation) aren't restricted + return false == valid(Layer) || (layer(Layer).visible && false == layer(Layer).locked && false == layer(Layer).removed && writable(Layer)); +} + +// true if specified node can be selected and modified in the editor. optionally explains why it can't +bool node_layers::editable(basic_node const *Node, std::string *Reason) const +{ + if (Node == nullptr) + { + return false; + } + auto const nodelayer{resolve(Node->layer())}; + if (false == valid(nodelayer)) + { + return true; + } + std::string reason; + if (Node->from_template()) + { + reason = "it's a part of an include template (.inc)"; + } + else if (false == writable(nodelayer, &reason)) + { + // reason was filled by the call + } + else if (layer(nodelayer).locked) + { + reason = "its layer is locked"; + } + else if (false == layer(nodelayer).visible || layer(nodelayer).removed) + { + reason = "its layer is hidden"; + } + if (false == reason.empty() && Reason != nullptr) + { + *Reason = reason; + } + return reason.empty(); +} + +// moves specified node to specified layer +void node_layers::move(basic_node *Node, layer_handle Layer) +{ + Layer = resolve(Layer); + if (Node == nullptr || false == valid(Layer) || resolve(Node->layer()) == Layer) + { + return; + } + // NOTE: the only nodes the editor creates and moves between layers are model instances + count(Node->layer(), layer_item::model, -1); + Node->layer(Layer); + count(Layer, layer_item::model, 1); +} + +std::vector node_layers::hidden() const +{ + std::vector layers; + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + if (false == m_layers[idx].visible) + { + layers.emplace_back(static_cast(idx + 1)); + } + } + return layers; +} + +void node_layers::show_all() +{ + for (auto const hiddenlayer : hidden()) + { + // layers dropped from the scenery stay out of sight + if (false == layer(hiddenlayer).removed) + { + visible(hiddenlayer, true); + } + } +} + +// stores origin of a node defined directly in a layer file, to allow rewriting its definition on save +void node_layers::track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles, glm::vec3 const &Scale) +{ + if (Node == nullptr || Node->from_template() || false == valid(Node->layer()) || false == Span.valid()) + { + return; + } + m_sources[Node] = {Node->layer(), Span, m_context, Node->location(), Angles, Scale}; +} + +// indicates specified node was removed from the scene +void node_layers::forget(basic_node const *Node) +{ + auto const lookup{m_sources.find(Node)}; + if (lookup == m_sources.end()) + { + return; + } + m_erased.emplace_back(lookup->second.layer, lookup->second.span); + m_sources.erase(lookup); + m_renamed.erase(Node); +} + +// indicates specified node was given another name in the editor +void node_layers::renamed(basic_node const *Node) +{ + // NOTE: a node which isn't defined by a scenery file yet is written whole on save, along with its name + if (m_sources.find(Node) != m_sources.end()) + { + m_renamed.emplace(Node); + } +} + +void node_layers::shape(material_handle const Material, source_span const &Span) +{ + auto const current{handle()}; + if (false == valid(current) || false == Span.valid() || m_instance != 0) + { + return; + } + m_shapes.push_back({current, Span, Material, false}); +} + +std::pair node_layers::erase_shapes(std::set const &Materials, std::set const *Layers) +{ + std::pair result{0, 0}; + for (auto &shape : m_shapes) + { + if (shape.erased || Materials.count(shape.material) == 0 || (Layers != nullptr && Layers->count(shape.layer) == 0)) + { + continue; + } + if (false == writable(shape.layer)) + { + ++result.second; + continue; + } + shape.erased = true; + m_erased.emplace_back(shape.layer, shape.span); + m_shapeserased = true; + ++result.first; + } + return result; +} + +bool node_layers::is_descendant(layer_handle Layer, layer_handle const Ancestor) const +{ + // NOTE: content of a merged layer, includes of other files among it, belongs to the layer it was merged into + for (auto hops = 0; hops < 64 && valid(Layer); ++hops) + { + Layer = resolve(layer(Layer).parent); + if (Layer == Ancestor) + { + return true; + } + } + return false; +} + +std::string node_layers::path(layer_handle const Layer) const +{ + return Global.asCurrentSceneryPath + layer(Layer).name; +} + +// stores current state of the layer file, to detect later whether it was changed behind our back +void node_layers::stat(layer_handle const Layer) +{ + std::error_code error; + auto const filepath{std::filesystem::path(path(Layer))}; + auto const filesize{std::filesystem::file_size(filepath, error)}; + layer(Layer).filesize = error ? 0 : filesize; + auto const filetime{std::filesystem::last_write_time(filepath, error)}; + layer(Layer).filetime = error ? std::filesystem::file_time_type{} : filetime; +} + +// creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason +layer_handle node_layers::create(std::string Name, layer_handle Parent, std::string &Reason) +{ + Parent = resolve(Parent); + if (false == valid(Parent) || layer(Parent).removed || layer(Parent).binary) + { + Reason = "the parent layer can't include other files"; + return null_handle; + } + if (layer(Parent).late) + { + Reason = "the parent layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow"; + return null_handle; + } + // file names in the scenery are processed in lower case, with forward slashes + Name = ToLower(Name); + replace_slashes(Name); + Name.erase(0, Name.find_first_not_of(" \t")); + Name.erase(Name.find_last_not_of(" \t") + 1); + if (Name.empty()) + { + Reason = "the layer needs a file name"; + return null_handle; + } + if (Name.find_first_of(" \t\";:*?<>|") != std::string::npos || Name.front() == '/' || Name.back() == '/' || contains(Name, "..")) + { + Reason = "the file name has to be a plain path inside the scenery directory, without spaces"; + return null_handle; + } + if (std::filesystem::path(Name).extension().empty()) + { + Name += ".scm"; + } + if (Name.ends_with(".inc")) + { + Reason = ".inc files are include templates, not layers"; + return null_handle; + } + if (m_layers.size() >= static_cast(std::numeric_limits::max())) + { + Reason = "too many layers"; + return null_handle; + } + auto const taken{std::any_of(std::begin(m_layers), std::end(m_layers), [&](basic_layer const &Layer) { return Layer.name == Name && false == Layer.dead; })}; + if (taken || FileExists(Global.asCurrentSceneryPath + Name)) + { + Reason = "the file \"" + Name + "\" already exists"; + return null_handle; + } + + // the include directive goes after the last file included by the parent ahead of the scenario initialization, + // so the scenery files stay ahead of what follows them in a scenario, like the vehicles + auto anchor{null_handle}; + std::streamoff anchorend{-1}; + auto const &init{layer(Parent).init}; + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + auto const &sibling{m_layers[idx]}; + if (sibling.dead || sibling.created || sibling.sites.size() != 1) + { + continue; + } + auto const &site{sibling.sites.front()}; + if (site.parent == Parent && false == site.fixed && site.span.end > anchorend && (false == init.valid() || site.span.end <= init.begin)) + { + anchor = static_cast(idx + 1); + anchorend = site.span.end; + } + } + + m_layers.emplace_back(); + auto const layerhandle{static_cast(m_layers.size())}; + auto &created{m_layers.back()}; + created.name = Name; + created.parent = Parent; + created.created = true; + created.editormade = true; + created.anchor = anchor; + // placement the new file is going to be loaded with + created.context_begin = valid(anchor) ? layer(anchor).context_end : layer(Parent).context_insert(); + created.context_end = created.context_begin; + + active(layerhandle); + + return layerhandle; +} + +bool node_layers::can_remove(layer_handle Layer, std::string &Reason) const +{ + if (false == listed(Layer) || layer(Layer).removed) + { + Reason = "no such layer"; + return false; + } + auto const &target{layer(Layer)}; + if (target.created) + { + return true; + } + if (target.sites.empty()) + { + Reason = "the scenario file can't be removed"; + return false; + } + if (target.binary) + { + Reason = "the file is loaded from binary terrain cache"; + return false; + } + if (target.sites.size() > 1) + { + Reason = "the file is included more than once"; + return false; + } + if (target.sites.front().fixed) + { + Reason = "the file is included by an include template (.inc)"; + return false; + } + return true; +} + +// marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk +bool node_layers::remove(layer_handle Layer) +{ + std::string reason; + if (false == can_remove(Layer, reason)) + { + return false; + } + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + auto const candidate{static_cast(idx + 1)}; + // layers merged elsewhere keep going, their content is no longer a part of the removed file + if (false == listed(candidate) || (candidate != Layer && false == is_descendant(candidate, Layer))) + { + continue; + } + visible(candidate, false); + layer(candidate).removed = true; + if (m_active == candidate) + { + m_active = null_handle; + } + } + if (m_active == null_handle) + { + // fall back on the scenario file, which is always there + active(1); + } + return true; +} + +// takes back remove() which wasn't saved yet +void node_layers::restore(layer_handle Layer) +{ + if (false == valid(Layer) || false == layer(Layer).removed || layer(Layer).dead) + { + return; + } + // a layer can't be brought back without the file which includes it + auto const parent{resolve(layer(Layer).parent)}; + if (valid(parent) && layer(parent).removed) + { + restore(parent); + } + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + auto const candidate{static_cast(idx + 1)}; + if (false == listed(candidate) || (candidate != Layer && false == is_descendant(candidate, Layer))) + { + continue; + } + layer(candidate).removed = false; + visible(candidate, true); + } +} + +bool node_layers::can_merge(layer_handle Source, layer_handle Target, std::string &Reason) const +{ + if (false == listed(Source) || false == listed(Target) || layer(Source).removed || layer(Target).removed) + { + Reason = "no such layer"; + return false; + } + if (Source == Target) + { + Reason = "pick another layer to merge into"; + return false; + } + auto const &source{layer(Source)}; + auto const &target{layer(Target)}; + if (false == writable(Target, &Reason)) + { + return false; + } + if (is_descendant(Target, Source)) + { + // the merged text would include the file it was put in + Reason = "the target layer is included by the merged one"; + return false; + } + if (target.late != source.late && (source.created || source.sites.empty() || resolve(source.sites.front().parent) != Target)) + { + // the content would change sides of the scenario initialization + Reason = (target.late ? "the target layer is loaded after the scenario initialization (FirstInit), which only vehicles should follow" : + "the merged layer is loaded after the scenario initialization (FirstInit), its content has to stay there"); + return false; + } + if (false == source.context_insert().matches(target.context_insert())) + { + // directives of includes placed in the editor are prepared for the place their layer file receives them at + auto const unsaved{std::any_of(std::begin(m_instances), std::end(m_instances), [&](include_instance const &Instance) { + return false == Instance.dead && false == Instance.removed && false == Instance.span.valid() && resolve(Instance.layer) == Source; + })}; + if (unsaved) + { + Reason = "the merged layer has includes which weren't saved yet, save the scenery first"; + return false; + } + } + if (source.created) + { + // nothing but the nodes made in the editor to move + return true; + } + if (source.sites.empty()) + { + Reason = "the scenario file can't be merged into another layer"; + return false; + } + if (false == writable(Source, &Reason)) + { + return false; + } + if (source.sites.front().fixed) + { + Reason = "the file is included by an include template (.inc)"; + return false; + } + if (resolve(source.sites.front().parent) != Target) + { + // the text is moved to another file. its statements have to keep their meaning there, + // and can't leave anything behind for what used to follow it + if (false == source.context_begin.matches(target.context_insert()) || false == source.context_end.matches(source.context_begin)) + { + Reason = "the layers are loaded with different origin, rotation or scale"; + return false; + } + } + return true; +} + +// moves content of the source layer to the target layer +bool node_layers::merge(layer_handle Source, layer_handle Target) +{ + std::string reason; + if (false == can_merge(Source, Target, reason)) + { + return false; + } + // start with the same visibility on both sides, as it's kept per node + visible(Source, true); + visible(Target, true); + for (std::size_t idx = 0; idx < layer(Source).items.size(); ++idx) + { + layer(Target).items[idx] += layer(Source).items[idx]; + } + layer(Source).merged = Target; + if (m_active == Source) + { + active(Target); + } + return true; +} + +// true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes +bool node_layers::pending() const +{ + return false == m_marking.empty() || + std::any_of(std::begin(m_layers), std::end(m_layers), [](basic_layer const &Layer) { return false == Layer.dead && (Layer.created || Layer.removed || Layer.merged != null_handle); }); +} + +namespace +{ +// marks of the lines the editor keeps track of +std::array const editormarks{"//$p", "//$b", "//$c"}; +} // namespace + +void node_layers::scan(layer_handle const Layer) +{ + m_marked.erase(Layer); + std::ifstream file{path(Layer), std::ios_base::binary}; + if (false == file.is_open()) + { + return; + } + std::string content{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + if (content.find("//$") == std::string::npos) + { + return; + } + std::size_t begin{0}; + while (begin < content.size()) + { + auto end{content.find('\n', begin)}; + end = (end == std::string::npos ? content.size() : end + 1); + auto const first{content.find_first_not_of(" \t", begin)}; + if (first < end) + { + for (auto const *mark : editormarks) + { + auto const length{std::char_traits::length(mark)}; + if (content.compare(first, length, mark) != 0 || (first + length < end && content[first + length] != ' ' && content[first + length] != '\t' && content[first + length] != '\r' && content[first + length] != '\n')) + { + continue; + } + auto text{content.substr(first + length, end - first - length)}; + while (false == text.empty() && (text.back() == '\n' || text.back() == '\r')) + { + text.pop_back(); + } + text.erase(0, text.find_first_not_of(" \t")); + m_marked[Layer].push_back({mark, {static_cast(begin), static_cast(end)}, std::move(text)}); + break; + } + } + begin = end; + } +} + +std::vector node_layers::marked(layer_handle const Layer, std::string const &Mark) const +{ + std::vector result; + if (auto const pending{m_marking.find({Layer, Mark})}; pending != m_marking.end()) + { + return pending->second; + } + if (auto const lookup{m_marked.find(Layer)}; lookup != m_marked.end()) + { + for (auto const &line : lookup->second) + { + if (line.mark == Mark) + { + result.push_back(line.text); + } + } + } + return result; +} + +std::vector node_layers::marked_layers(std::string const &Mark) const +{ + std::vector result; + for (auto const &entry : m_marked) + { + if (listed(entry.first) && std::any_of(entry.second.begin(), entry.second.end(), [&](marked_line const &Line) { return Line.mark == Mark; })) + { + result.push_back(entry.first); + } + } + for (auto const &entry : m_marking) + { + if (entry.first.second == Mark && std::find(result.begin(), result.end(), entry.first.first) == result.end()) + { + result.push_back(entry.first.first); + } + } + return result; +} + +void node_layers::mark(layer_handle const Layer, std::string const &Mark, std::vector Lines) +{ + if (valid(Layer)) + { + m_marking[{Layer, Mark}] = std::move(Lines); + } +} + +// indicates start of content of a template, included by directive at specified location of the layer file being loaded +void node_layers::instance_begin(std::string const &File, source_span const &Span) +{ + if (m_instances.size() + 1 >= untracked_instance) + { + m_instance = untracked_instance; + return; + } + m_instances.emplace_back(); + auto &included{m_instances.back()}; + included.layer = handle(); + included.file = &(*m_templates.emplace(File).first); + included.span = Span; + included.context = m_context; + m_instance = static_cast(m_instances.size()); +} + +// true if specified include can be removed in the editor. optionally explains why it can't +bool node_layers::removable(instance_handle Instance, std::string *Reason) const +{ + std::string reason; + if (false == tracked(Instance) || instance(Instance).dead) + { + reason = "it's a part of an include template (.inc)"; + } + else + { + auto const includelayer{resolve(instance(Instance).layer)}; + if (false == valid(includelayer) || layer(includelayer).removed) + { + reason = "its layer is no longer a part of the scenery"; + } + else if (false == writable(includelayer, &reason)) + { + // reason was filled by the call + } + else if (layer(includelayer).locked) + { + reason = "its layer is locked"; + } + } + if (false == reason.empty() && Reason != nullptr) + { + *Reason = reason; + } + return reason.empty(); +} + +// drops specified include from the scenery, or brings it back +void node_layers::removed(instance_handle Instance, bool Removed) +{ + if (false == tracked(Instance) || m_instances[Instance - 1].dead || m_instances[Instance - 1].removed == Removed) + { + return; + } + m_instances[Instance - 1].removed = Removed; + + auto const includelayer{resolve(m_instances[Instance - 1].layer)}; + auto const layervisible{false == valid(includelayer) || layer(includelayer).visible}; + auto const update_node = [=, this](basic_node *Node, layer_item const Item) { + if (Node == nullptr || Node->m_instance != Instance || (Item == layer_item::model && stale(Node))) + { + return; + } + count(Node->layer(), Item, Removed ? -1 : 1); + if (Removed) + { + if (Node->visible()) + { + Node->visible(false); + Node->m_layerhidden = true; + } + } + else if (Node->m_layerhidden && layervisible) + { + Node->visible(true); + Node->m_layerhidden = false; + } + }; + for (auto *modelinstance : simulation::Instances.sequence()) + { + update_node(modelinstance, layer_item::model); + } + for (auto *path : simulation::Paths.sequence()) + { + update_node(path, layer_item::track); + } + for (auto *traction : simulation::Traction.sequence()) + { + update_node(traction, layer_item::traction); + } +} + +// registers include directive made in the editor, to be written to the file of specified layer on save +instance_handle node_layers::place(layer_handle Layer, std::string const &File, std::string const &Directive) +{ + Layer = resolve(Layer); + if (false == accepts(Layer) || m_instances.size() + 1 >= untracked_instance) + { + return 0; + } + m_instances.emplace_back(); + auto &included{m_instances.back()}; + included.layer = Layer; + included.file = &(*m_templates.emplace(File).first); + included.directive = Directive; + included.context = layer(Layer).context_insert(); + return static_cast(m_instances.size()); +} + +// text of the directive of specified include, as changed in the editor or as it stands in the scenery file +std::string node_layers::directive(instance_handle const Instance) const +{ + if (false == tracked(Instance)) + { + return {}; + } + auto const &included{instance(Instance)}; + if (false == included.directive.empty() || false == included.span.valid() || false == valid(included.layer)) + { + return included.directive; + } + std::string text(static_cast(included.span.end - included.span.begin), '\0'); + std::ifstream input{path(included.layer), std::ios_base::binary}; + if (false == input.is_open() || false == input.seekg(included.span.begin).good() || false == input.read(text.data(), static_cast(text.size())).good()) + { + return {}; + } + return text; +} + +// replaces the directive of specified include, the scenery file receives it on save +bool node_layers::modify(instance_handle const Instance, std::string const &Directive) +{ + if (false == tracked(Instance) || m_instances[Instance - 1].dead || Directive.empty()) + { + return false; + } + m_instances[Instance - 1].directive = Directive; + return true; +} + +// takes the models specified include was loaded with out of the scene, once the editor shows its own in their place +void node_layers::rebuilt(instance_handle const Instance) +{ + if (false == tracked(Instance) || m_instances[Instance - 1].rebuilt) + { + return; + } + auto const removed{m_instances[Instance - 1].removed}; + for (auto *model : simulation::Instances.sequence()) + { + if (model == nullptr || model->m_instance != Instance || model->m_preview) + { + continue; + } + // NOTE: the models themselves are left alone, events of the scenery can refer to them + simulation::Region->erase(model); + if (false == removed) + { + count(model->layer(), layer_item::model, -1); + } + if (model->visible()) + { + model->visible(false); + model->m_layerhidden = true; + } + } + m_instances[Instance - 1].rebuilt = true; +} + +} // namespace scene + +//--------------------------------------------------------------------------- + +std::string scene::node_layers::check_sources() const +{ + std::map files; + std::size_t good{0}, bad{0}; + std::string report; + for (auto const &[node, source] : m_sources) + { + auto &text{files[source.layer]}; + if (text.empty()) + { + std::ifstream file(path(source.layer), std::ios::binary); + text.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); + } + auto const ok{source.span.valid() && static_cast(source.span.end) <= text.size()}; + std::string piece{ok ? text.substr(source.span.begin, source.span.end - source.span.begin) : std::string{}}; + auto const model{dynamic_cast(node) != nullptr}; + auto const fine{ok && piece.rfind("node", 0) == 0 && (false == model || (piece.size() >= 8 && piece.compare(piece.size() - 8, 8, "endmodel") == 0))}; + if (fine) + ++good; + else if (++bad <= 5) + report += " [" + node->name() + " " + std::to_string(source.span.begin) + "-" + std::to_string(source.span.end) + ": " + piece.substr(0, 60) + " ... " + (piece.size() > 30 ? piece.substr(piece.size() - 30) : std::string{}) + "]"; // NOSONAR + } + return std::to_string(good) + " good, " + std::to_string(bad) + " bad" + report; +} diff --git a/scene/scenelayers.h b/scene/scenelayers.h new file mode 100644 index 00000000..daf3c587 --- /dev/null +++ b/scene/scenelayers.h @@ -0,0 +1,395 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include "scene/scenenode.h" + +#include +#include +#include +#include + +namespace scene +{ + +// kinds of scenery items a layer keeps count of +enum class layer_item +{ + model, + track, + traction, + powersource, + memcell, + launcher, + event, + vehicle, + sound, + shape, + lines, + count_ +}; + +// placement applied to scenery statements by the origin, rotate and scale directives in effect +struct layer_context +{ + glm::dvec3 offset{0.0}; + glm::vec3 rotation{0.f}; + glm::vec3 scale{1.f}; + + // converts world space location to the one which produces it when loaded with this context in effect + glm::dvec3 to_local(glm::dvec3 Location) const; + // reverse of to_local() + glm::dvec3 to_world(glm::dvec3 Location) const; + bool matches(layer_context const &Other) const; +}; + +// location of a piece of text in a scenery file, as byte offsets +struct source_span +{ + std::streamoff begin{-1}; + std::streamoff end{-1}; + + bool valid() const + { + return begin >= 0 && end > begin; + } +}; + +// include directive bringing a scenery file into another one +struct include_site +{ + layer_handle parent{null_handle}; // layer of the file holding the directive + source_span span; // location of the whole directive in that file + bool parameters{false}; // the file is included with parameters + bool fixed{false}; // the directive can't be rewritten: it's a part of an *.inc template, or its location is unknown +}; + +// scenery layer: content of a single scenery file (.scn, .scm, .ctr...) other than an .inc node template +struct basic_layer +{ + std::string name; // file name, relative to the scenery directory + layer_handle parent{null_handle}; // layer of the file this one was included from + bool visible{true}; + bool locked{false}; // items of locked layer can't be selected in the editor + bool binary{false}; // the file wasn't parsed, its content came from binary terrain file + std::array(layer_item::count_)> items{}; // amount of items of each kind + // source data, used by the scenery save + std::vector sites; // include directives bringing the file in; empty for the scenario file + layer_context context_begin; // placement in effect at the start of the file... + layer_context context_end; // ...and right after its end + // location in the file of what performs the scenario initialization, if the file takes a part in it: the FirstInit + // directive, or the directive including the file which holds it. only vehicles are meant to follow the + // initialization, so what the editor adds to the file goes ahead of this point rather than at the end + source_span init; + layer_context context_init; // placement in effect at that point + bool late{false}; // the file is loaded after the scenario initialization + std::uintmax_t filesize{0}; // state of the file when it was loaded or saved the last time + std::filesystem::file_time_type filetime{}; + // pending changes, applied by the scenery save + bool created{false}; // layer was created in the editor, its file doesn't exist yet + bool editormade{false}; // the file was made by the editor during this session, there's no original content to keep a backup of + layer_handle anchor{null_handle}; // for created layer: sibling its include directive goes after + bool removed{false}; // layer is to be dropped from the scenery + layer_handle merged{null_handle}; // layer which took over the content of this one + bool dead{false}; // removal or merge of the layer was saved, it's no longer a part of the scenery + + std::size_t item_count() const; + // placement in effect at the point of the file which receives the content added by the editor + layer_context const &context_insert() const + { + return init.valid() ? context_init : context_end; + } +}; + +// include directive bringing an *.inc template into a scenery layer file +struct include_instance +{ + layer_handle layer{null_handle}; // layer of the file holding the directive + std::string const *file{nullptr}; // name of the included template + source_span span; // location of the whole directive in the file; invalid for directive which wasn't saved yet + std::string directive; // text of the directive, for include placed or changed in the editor and not saved yet + layer_context context; // placement in effect at the directive + bool removed{false}; // the include is to be dropped from the scenery + bool dead{false}; // removal of the include was saved + bool rebuilt{false}; // models the include was loaded with gave way to the ones made by the editor +}; + +// marks nodes defined by a template which was included in a way the editor doesn't keep track of +instance_handle const untracked_instance{std::numeric_limits::max()}; + +// origin of a node defined in a scenery layer file, with the placement it was loaded with +struct node_source +{ + layer_handle layer{null_handle}; // layer of the file holding the node definition + source_span span; // location of the definition in that file + layer_context context; // placement in effect at the definition + glm::dvec3 location{0.0}; + glm::vec3 angles{0.f}; + glm::vec3 scale{1.f}; +}; + +// definition of a shape (triangles) in a scenery layer file +struct shape_source +{ + layer_handle layer{null_handle}; + source_span span; + material_handle material{null_handle}; + bool erased{false}; // the definition goes from the file on save +}; + +struct save_result +{ + bool success{false}; + std::vector files; // names of saved files + std::vector backups; // copies of the files made before they lost their ground + std::string message; // summary, or cause of the failure +}; + +// holds scenery layers, established during load of scenery opened for editing (-edit) +// NOTE: regular load doesn't create any layers, all nodes are left with null_handle +class node_layers +{ + public: + // methods + // indicates start of content of the scenario file. returns: handle to its layer + layer_handle open(std::string const &Name); + // indicates start of content of specified scenery file, included by specified directive. returns: handle to the layer of the file + layer_handle open(std::string const &Name, include_site Site); + // indicates end of content of the current scenery file. returns: handle to the parent layer, or null_handle if the stack is empty + layer_handle close(); + // returns layer of the scenery file being loaded, or null_handle if there's none + layer_handle handle() const + { + return m_stack.empty() ? null_handle : m_stack.back(); + } + // removes all layers; layer handles stored in the nodes aren't touched + void clear(); + bool empty() const + { + return m_layers.empty(); + } + std::size_t size() const + { + return m_layers.size(); + } + bool valid(layer_handle const Layer) const + { + return Layer != null_handle && Layer <= m_layers.size(); + } + // grants direct access to specified layer. NOTE: handle has to be valid + basic_layer &layer(layer_handle const Layer) + { + return m_layers[Layer - 1]; + } + basic_layer const &layer(layer_handle const Layer) const + { + return m_layers[Layer - 1]; + } + // follows layer merges. returns: handle of the layer which currently holds content of specified layer + layer_handle resolve(layer_handle Layer) const; + // true if the layer is a part of the scenery and should be presented to the user + bool listed(layer_handle const Layer) const + { + return valid(Layer) && false == layer(Layer).dead && layer(Layer).merged == null_handle; + } + // number of layers between specified layer and the scenario file + int depth(layer_handle Layer) const; + // handles of the layers to present to the user, each followed by the layers it includes + std::vector tree() const; + // adjusts item count of specified layer + void count(layer_handle Layer, layer_item const Item, int const Change = 1); + // layer receiving the items created in the editor, or null_handle if there's none + layer_handle active() const + { + return m_active; + } + // NOTE: active layer is made visible and unlocked + void active(layer_handle Layer); + // shows or hides nodes of specified layer. visibility defined in the scenery is preserved, hidden layer only overrides it + void visible(layer_handle Layer, bool const Visible); + void locked(layer_handle Layer, bool const Locked); + // true if the file of specified layer can be rewritten with changed nodes. optionally explains why it can't + bool writable(layer_handle Layer, std::string *Reason = nullptr) const; + // true if nodes of specified layer can be selected and modified in the editor + bool editable(layer_handle Layer) const; + // true if specified node can be selected and modified in the editor. optionally explains why it can't + bool editable(basic_node const *Node, std::string *Reason = nullptr) const; + // moves specified node to specified layer + void move(basic_node *Node, layer_handle Layer); + // handles of layers which are currently hidden + std::vector hidden() const; + // restores visibility of all layers + void show_all(); + + // scenery load bookkeeping + // registers *.inc template used by the scenery + void template_used(std::string const &Name) + { + m_templates.emplace(Name); + } + std::set const &templates() const + { + return m_templates; + } + // indicates start of content of a template, included by directive at specified location of the layer file being loaded + void instance_begin(std::string const &File, source_span const &Span); + // indicates end of content of the template + void instance_end() + { + m_instance = 0; + } + // include whose template is being loaded, 0 if there's none + instance_handle instance() const + { + return m_instance; + } + // updates placement in effect for the statements which follow + void context(layer_context const &Context) + { + m_context = Context; + } + // stores origin of a node defined directly in a layer file, to allow rewriting its definition on save + void track(basic_node const *Node, source_span const &Span, glm::vec3 const &Angles = glm::vec3{0.f}, glm::vec3 const &Scale = glm::vec3{1.f}); + // indicates specified node was removed from the scene + void forget(basic_node const *Node); + // indicates specified node was given another name in the editor, for its definition to get it on save + void renamed(basic_node const *Node); + // stores location of a shape defined directly in the layer file being loaded + void shape(material_handle const Material, source_span const &Span); + std::vector const &shapes() const + { + return m_shapes; + } + // drops the definitions of the shapes of specified materials from the writable layer files on save; only from specified layers, if given. + // returns: amount of the shapes to drop, and of the ones which stay as their files can't be rewritten + std::pair erase_shapes(std::set const &Materials, std::set const *Layers = nullptr); + // indicates the scenario initialization (FirstInit) is being performed. Span: location of the directive. + // Infile: the directive comes straight from the layer file being loaded + void initialization(source_span const &Span, bool Infile); + // true if specified layer can receive content added in the editor. optionally explains why it can't + bool accepts(layer_handle Layer, std::string *Reason = nullptr) const; + // notes the scenery already holds the directive setting up terrain made in the editor + void terrain_directive(bool const Present) + { + m_terraindirective = Present; + } + bool terrain_directive() const + { + return m_terraindirective; + } + + // editor data kept in the scenery files as comment lines starting with a mark, e.g. "//$p ...", which the simulation skips + // text of the lines with specified mark in specified layer, with the mark taken off + std::vector marked(layer_handle Layer, std::string const &Mark) const; + // layers whose files hold lines with specified mark + std::vector marked_layers(std::string const &Mark) const; + // lines to take the place of the ones with specified mark in the layer file, on save. the mark is added to each + void mark(layer_handle Layer, std::string const &Mark, std::vector Lines); + + // layer management. changes are applied to the scenery files by save() + // creates empty layer, included from specified one. returns: handle to the new layer, or null_handle with the explanation in Reason + layer_handle create(std::string Name, layer_handle Parent, std::string &Reason); + bool can_remove(layer_handle Layer, std::string &Reason) const; + // marks specified layer and the layers it includes as dropped from the scenery. their files are left on the disk + bool remove(layer_handle Layer); + // takes back remove() which wasn't saved yet + void restore(layer_handle Layer); + bool can_merge(layer_handle Source, layer_handle Target, std::string &Reason) const; + // moves content of the source layer to the target layer + bool merge(layer_handle Source, layer_handle Target); + // true if there are layer changes awaiting save. NOTE: doesn't account for modified nodes + bool pending() const; + + // includes of *.inc templates. the editor handles each as a whole: it can be placed and removed, but not taken apart + // true if specified handle stands for an include the editor keeps track of + bool tracked(instance_handle const Instance) const + { + return Instance != 0 && Instance <= m_instances.size(); + } + // handles of the includes run from 1 up to this number + std::size_t instance_count() const + { + return m_instances.size(); + } + // grants access to specified include. NOTE: the include has to be tracked + include_instance const &instance(instance_handle const Instance) const + { + return m_instances[Instance - 1]; + } + // true if specified include can be removed in the editor. optionally explains why it can't + bool removable(instance_handle Instance, std::string *Reason = nullptr) const; + // drops specified include from the scenery, or brings it back. its nodes are hidden right away, the directive + // is erased from the layer file on save; what else the template defines stays in the scene until the next load + void removed(instance_handle Instance, bool Removed); + // registers include directive made in the editor, to be written to the file of specified layer on save. + // the directive has to be prepared for the placement in effect at the end of that file. returns: handle to the include + instance_handle place(layer_handle Layer, std::string const &File, std::string const &Directive); + // text of the directive of specified include, as changed in the editor or as it stands in the scenery file. + // returns: empty text if the directive can't be read + std::string directive(instance_handle Instance) const; + // replaces the directive of specified include, the scenery file receives it on save. NOTE: the nodes aren't touched + bool modify(instance_handle Instance, std::string const &Directive); + // takes the models specified include was loaded with out of the scene, once the editor shows its own in their place + void rebuilt(instance_handle Instance); + // true if specified node was replaced by the editor with a node showing the current state of its include + bool stale(basic_node const *Node) const + { + return tracked(Node->m_instance) && instance(Node->m_instance).rebuilt && false == Node->m_preview; + } + // true if specified layer is included, directly or not, by the other one + bool is_descendant(layer_handle Layer, layer_handle const Ancestor) const; + + // writes changes made in the editor to the scenery files: changed, created and deleted model instances and + // memory cells, and layer changes. the files are left intact unless the whole operation can be performed. + // Rootstatements: additional statements to place at the end of the scenario file + save_result save(std::vector const &Rootstatements = {}, std::vector const &Trailingstatements = {}); + std::string check_sources() const; + + private: + // types + struct file_patch; + struct composition; + struct save_state; + struct marked_line + { + std::string mark; + source_span span; // whole line, its end included + std::string text; + }; + // methods + std::string path(layer_handle const Layer) const; + void stat(layer_handle const Layer); + // finds the marked lines in the layer file + void scan(layer_handle const Layer); + bool compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const; + // members + std::vector m_layers; // layer handle is index in the vector + 1 + std::vector m_stack; // helper, layers of scenery files being loaded + layer_handle m_active{null_handle}; + layer_context m_context; // placement in effect at the current point of scenery load + std::unordered_map m_sources; // origins of nodes which can be rewritten on save + std::vector> m_erased; // definitions of nodes deleted since the load or the last save + std::unordered_set m_renamed; // nodes given another name since the load or the last save + std::vector m_shapes; + bool m_shapeserased{false}; // the binary terrain file holds shapes which the save drops + std::set m_templates; // *.inc templates used by the scenery + std::vector m_instances; // includes of the templates; instance handle is index in the vector + 1 + instance_handle m_instance{0}; // helper, include whose template is being loaded + bool m_initialized{false}; // helper, the scenario initialization was performed already + bool m_terraindirective{false}; + std::map> m_marked; + std::map, std::vector> m_marking; // changes awaiting save +}; + +extern node_layers Layers; + +} // namespace scene + +//--------------------------------------------------------------------------- diff --git a/scene/scenelayerssave.cpp b/scene/scenelayerssave.cpp new file mode 100644 index 00000000..a07e2a3f --- /dev/null +++ b/scene/scenelayerssave.cpp @@ -0,0 +1,1907 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/scenelayers.h" +#include "world/Track.h" +#include "world/Road.h" +#include "world/Sweep.h" +#include "world/RoadPoint.h" + +#include "simulation/simulation.h" +#include "model/AnimModel.h" +#include "scene/scenenodegroups.h" +#include "world/MemCell.h" +#include "world/Traction.h" +#include "world/EvLaunch.h" +#include "world/Event.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/utilities.h" + +#include +#include +#include + +// scenery save for sceneries opened for editing. +// the scenery files are patched rather than generated: text of a file is carried over as it is, except for the +// definitions of nodes changed in the editor and the include directives affected by layer changes. this keeps +// everything the editor doesn't understand or didn't touch, comments and formatting included. + +namespace scene +{ + +namespace +{ + +struct text_token +{ + std::size_t begin; + std::size_t end; + std::string text; // lower case +}; + +struct text_change +{ + std::size_t begin; + std::size_t end; + std::string text; +}; + +bool is_separator(char const Char) +{ + return Char == ' ' || Char == '\t' || Char == '\n' || Char == '\r' || Char == ';'; +} + +// splits scenery text into tokens the same way the scenery parser does: comments are skipped, quoted text is kept together +std::vector tokenize(std::string const &Text) +{ + std::vector tokens; + auto const size{Text.size()}; + std::size_t idx{0}; + auto const starts = [&](char const *Mark) { return Text.compare(idx, 2, Mark) == 0; }; + while (idx < size) + { + if (is_separator(Text[idx])) + { + ++idx; + continue; + } + if (starts("//")) + { + idx = Text.find('\n', idx); + idx = (idx == std::string::npos ? size : idx + 1); + continue; + } + if (starts("/*")) + { + idx = Text.find("*/", idx + 2); + idx = (idx == std::string::npos ? size : idx + 2); + continue; + } + auto const begin{idx}; + while (idx < size && false == is_separator(Text[idx]) && false == starts("//") && false == starts("/*")) + { + if (Text[idx] == '\"') + { + // quoted part runs up to the closing quote, whatever is inside + ++idx; + while (idx < size && Text[idx] != '\"') + { + idx += (Text[idx] == '\\' ? 2 : 1); + } + } + ++idx; + } + idx = std::min(idx, size); + tokens.push_back({begin, idx, ToLower(Text.substr(begin, idx - begin))}); + } + return tokens; +} + +std::string number(double const Value) +{ + std::ostringstream converter; + converter.imbue(std::locale::classic()); + converter << std::fixed << std::setprecision(3) << Value; + auto text{converter.str()}; + return text == "-0.000" ? "0.000" : text; +} + +std::string numbers(glm::dvec3 const &Values) +{ + return number(Values.x) + ' ' + number(Values.y) + ' ' + number(Values.z); +} + +void apply(std::string &Text, std::vector Changes) +{ + // from the end, so the locations of pending changes stay valid + std::sort(std::begin(Changes), std::end(Changes), [](text_change const &Left, text_change const &Right) { return Left.begin > Right.begin; }); + for (auto const &change : Changes) + { + Text.replace(change.begin, change.end - change.begin, change.text); + } +} + +// placement to write into definition of a model instance. empty parts are left as they are +struct model_placement +{ + std::optional location; // in the coordinates used by the definition + std::optional angles; // complete rotation of the instance + glm::vec3 rotation{0.f}; // rotation the loader adds to the angle specified in the definition + std::optional scale; // scale factor to put in the definition + std::optional name; // name of the node +}; + +bool about_equal(float const Left, float const Right) +{ + return std::abs(Left - Right) < 1e-3f; +} + +// definition layout: node model [options] endmodel +bool patch_model(std::string &Text, model_placement const &Placement, std::string &Error) +{ + auto const tokens{tokenize(Text)}; + if (tokens.size() < 12 || tokens[0].text != "node" || tokens[4].text != "model" || tokens.back().text != "endmodel") + { + Error = "unexpected layout of model definition"; + return false; + } + std::vector changes; + auto const replace = [&](std::size_t const Index, std::string const &Value) { changes.push_back({tokens[Index].begin, tokens[Index].end, Value}); }; + // optional blocks of three values, located past the texture + auto const last{tokens.size() - 1}; + auto const find_block = [&](char const *Keyword) { + for (std::size_t idx = 11; idx + 3 < last; ++idx) + { + if (tokens[idx].text == Keyword) + { + return idx; + } + } + return std::size_t{0}; + }; + std::string additions; // blocks to insert ahead of the closing keyword + + if (Placement.name) + { + replace(3, *Placement.name); + } + if (Placement.location) + { + replace(5, number(Placement.location->x)); + replace(6, number(Placement.location->y)); + replace(7, number(Placement.location->z)); + } + if (Placement.angles) + { + auto const &angles{*Placement.angles}; + auto const block{find_block("angles")}; + if (block != 0) + { + // the block specifies complete rotation of the instance, regardless of anything else + replace(block + 1, number(angles.x)); + replace(block + 2, number(angles.y)); + replace(block + 3, number(angles.z)); + } + else if (about_equal(angles.x, Placement.rotation.x) && about_equal(angles.z, Placement.rotation.z)) + { + // rotation around the vertical axis alone fits in the basic angle parameter + replace(8, number(angles.y - Placement.rotation.y)); + } + else + { + additions += "angles " + numbers(angles) + ' '; + } + } + if (Placement.scale) + { + auto const &scale{*Placement.scale}; + auto const unit{about_equal(scale.x, 1.f) && about_equal(scale.y, 1.f) && about_equal(scale.z, 1.f)}; + auto const block{find_block("scale")}; + if (block != 0) + { + if (unit) + { + // drop the block along with the whitespace separating it from the next token + changes.push_back({tokens[block].begin, tokens[block + 4].begin, ""}); + } + else + { + replace(block + 1, number(scale.x)); + replace(block + 2, number(scale.y)); + replace(block + 3, number(scale.z)); + } + } + else if (false == unit) + { + additions += "scale " + numbers(scale) + ' '; + } + } + if (false == additions.empty()) + { + changes.push_back({tokens[last].begin, tokens[last].begin, additions}); + } + apply(Text, changes); + return true; +} + +// definition layout: node memcell endmemcell +bool patch_memcell(std::string &Text, std::optional const &Location, std::string const &Track, std::string &Error) +{ + auto const tokens{tokenize(Text)}; + if (tokens.size() < 13 || tokens[0].text != "node" || tokens[4].text != "memcell" || tokens.back().text != "endmemcell") + { + Error = "unexpected layout of memory cell definition"; + return false; + } + std::vector changes; + if (Location) + changes.insert(changes.end(), {{tokens[5].begin, tokens[5].end, number(Location->x)}, {tokens[6].begin, tokens[6].end, number(Location->y)}, {tokens[7].begin, tokens[7].end, number(Location->z)}}); + auto const &track{tokens[tokens.size() - 2]}; + if (false == Track.empty() && track.text != Track) + changes.push_back({track.begin, track.end, Track}); + apply(Text, changes); + return true; +} + +// definition layout: node traction +// [parallel ] endtraction +bool patch_traction(std::string &Text, TTraction const &Traction, glm::dvec3 const &Offset, std::string &Error) +{ + auto const tokens{tokenize(Text)}; + if (tokens.size() < 30 || tokens[0].text != "node" || tokens[4].text != "traction" || tokens.back().text != "endtraction") + { + Error = "unexpected layout of traction definition"; + return false; + } + std::vector changes; + std::size_t index{12}; + for (auto const &point : {Traction.pPoint1, Traction.pPoint2, Traction.pPoint3, Traction.pPoint4}) + { + auto const local{point - Offset}; + for (auto const value : {local.x, local.y, local.z}) + { + changes.push_back({tokens[index].begin, tokens[index].end, number(value)}); + ++index; + } + } + auto const &p1{Traction.pPoint1}; + auto const &p2{Traction.pPoint2}; + auto const &p3{Traction.pPoint3}; + auto const &p4{Traction.pPoint4}; + changes.push_back({tokens[24].begin, tokens[24].end, number((p3.y - p1.y + p4.y - p2.y) * 0.5 - Traction.fHeightDifference)}); + if (Traction.iNumSections > 0) + { + // the load truncates length / segment length, the margin keeps the rounding of the number from losing a section + changes.push_back({tokens[25].begin, tokens[25].end, number(glm::length(p1 - p2) / (Traction.iNumSections + 0.001))}); + } + apply(Text, changes); + return true; +} + +// definition layout: node eventlauncher ... end +bool patch_launcher(std::string &Text, glm::dvec3 const &Location, std::string &Error) +{ + auto const tokens{tokenize(Text)}; + if (tokens.size() < 9 || tokens[0].text != "node" || tokens[4].text != "eventlauncher") + { + Error = "unexpected layout of event launcher definition"; + return false; + } + apply(Text, {{tokens[5].begin, tokens[5].end, number(Location.x)}, {tokens[6].begin, tokens[6].end, number(Location.y)}, {tokens[7].begin, tokens[7].end, number(Location.z)}}); + return true; +} + +std::string detect_eol(std::string const &Text, std::string const &Fallback) +{ + auto const linebreak{Text.find('\n')}; + if (linebreak == std::string::npos) + { + return Fallback; + } + return (linebreak > 0 && Text[linebreak - 1] == '\r') ? "\r\n" : "\n"; +} + +void ensure_newline(std::string &Text, std::string const &Eol) +{ + if (false == Text.empty() && Text.back() != '\n') + { + Text += Eol; + } +} + +bool read_file(std::string const &Path, std::string &Content) +{ + std::ifstream input{Path, std::ios_base::binary}; + if (false == input.is_open()) + { + return false; + } + Content.assign(std::istreambuf_iterator(input), std::istreambuf_iterator()); + return true; +} + +} // namespace + +// changes to make in the text of a single scenery file +struct node_layers::file_patch +{ + struct edit + { + std::streamoff begin{0}; // replaced range of the original text. empty range inserts, negative location stands for the end of the text + std::streamoff end{0}; + std::string text; // replacement + std::size_t lead{0}; // size of the part of replacement preceding the definition it carries + std::size_t length{0}; // size of that definition + basic_node const *node{nullptr}; // node defined by the replacement + layer_handle site{null_handle}; // layer included by the directive in the replacement + instance_handle instance{0}; // include of a template made by the directive in the replacement + layer_handle inlined{null_handle}; // layer whose content replaces the range + }; + std::vector edits; +}; + +// text of a scenery file after the changes, with the new locations of what it defines +struct node_layers::composition +{ + struct node_place + { + basic_node const *node; + source_span span; + bool rewritten; + }; + struct site_place + { + layer_handle layer; + source_span span; + }; + struct instance_place + { + instance_handle instance; + source_span span; + }; + std::string text; + std::vector nodes; + std::vector sites; + std::vector instances; + source_span init; // location of what performs the scenario initialization, see basic_layer::init + + // adds another piece of text at the end + void append(composition const &Other) + { + insert(static_cast(text.size()), Other); + } + // adds another piece of text at specified location + void insert(std::streamoff const Location, composition const &Other) + { + auto const size{static_cast(Other.text.size())}; + auto const move = [=](source_span &Span) { + if (Span.begin >= Location) + { + Span.begin += size; + Span.end += size; + } + }; + for (auto &place : nodes) + { + move(place.span); + } + for (auto &place : sites) + { + move(place.span); + } + for (auto &place : instances) + { + move(place.span); + } + if (init.valid()) + { + move(init); + } + else if (Other.init.valid()) + { + init = {Other.init.begin + Location, Other.init.end + Location}; + } + text.insert(static_cast(Location), Other.text); + for (auto place : Other.nodes) + { + place.span.begin += Location; + place.span.end += Location; + nodes.emplace_back(place); + } + for (auto place : Other.sites) + { + place.span.begin += Location; + place.span.end += Location; + sites.emplace_back(place); + } + for (auto place : Other.instances) + { + place.span.begin += Location; + place.span.end += Location; + instances.emplace_back(place); + } + } +}; + +struct node_layers::save_state +{ + std::map content; // original text of the files + std::map patches; + std::map> nodes; // unchanged nodes defined in each file + std::map> includes; // unchanged include directives in each file, by included layer + std::map> instances; // unchanged includes of templates in each file + std::string eol{"\r\n"}; + std::string error; +}; + +// produces text of specified file with the changes applied +bool node_layers::compose(save_state &State, layer_handle const Layer, composition &Output, int const Depth) const +{ + if (Depth > 32) + { + State.error = "layers include each other in a loop"; + return false; + } + static std::string const empty; + auto const contentlookup{State.content.find(Layer)}; + auto const &base{contentlookup != State.content.end() ? contentlookup->second : empty}; + auto const basesize{static_cast(base.size())}; + auto const eol{detect_eol(base, State.eol)}; + + std::vector edits; + if (auto const lookup{State.patches.find(Layer)}; lookup != State.patches.end()) + { + edits = lookup->second.edits; + } + // what's added to the file goes ahead of the scenario initialization, if the file takes a part in it + auto const &init{layer(Layer).init}; + auto const hasinit{init.valid() && init.end <= basesize}; + auto addition{basesize}; + if (hasinit) + { + addition = init.begin; + // on a line of its own, where the initialization has one + auto linebegin{addition}; + while (linebegin > 0 && (base[linebegin - 1] == ' ' || base[linebegin - 1] == '\t')) + { + --linebegin; + } + if (linebegin == 0 || base[linebegin - 1] == '\n') + { + addition = linebegin; + } + } + auto initedited{false}; // the directive standing for the initialization is replaced or removed + for (auto &edit : edits) + { + if (edit.begin < 0) + { + edit.begin = edit.end = addition; + } + initedited |= hasinit && edit.begin == init.begin && edit.end == init.end; + } + std::stable_sort(std::begin(edits), std::end(edits), [](file_patch::edit const &Left, file_patch::edit const &Right) { return Left.begin < Right.begin; }); + std::streamoff cursor{0}; + for (auto const &edit : edits) + { + if (edit.begin < cursor || edit.end < edit.begin || edit.end > basesize) + { + State.error = "conflicting changes in file \"" + layer(Layer).name + "\""; + return false; + } + cursor = edit.end; + } + + // locations in the original text past which everything is shifted by given amount + std::vector> shifts; + cursor = 0; + for (std::size_t idx = 0; idx < edits.size(); ++idx) + { + auto const &edit{edits[idx]}; + auto begin{edit.begin}; + auto end{edit.end}; + if (edit.text.empty() && edit.inlined == null_handle && end > begin) + { + // a definition is removed. if it had a line for itself take the whole line out, instead of leaving it blank + auto const limit{idx + 1 < edits.size() ? edits[idx + 1].begin : basesize}; + auto linebegin{begin}; + while (linebegin > cursor && (base[linebegin - 1] == ' ' || base[linebegin - 1] == '\t')) + { + --linebegin; + } + auto lineend{end}; + while (lineend < limit && (base[lineend] == ' ' || base[lineend] == '\t')) + { + ++lineend; + } + auto const startsline{linebegin == 0 || base[linebegin - 1] == '\n'}; + auto endsline{lineend == basesize}; + if (lineend + 1 < limit && base[lineend] == '\r' && base[lineend + 1] == '\n') + { + lineend += 2; + endsline = true; + } + else if (lineend < limit && base[lineend] == '\n') + { + lineend += 1; + endsline = true; + } + if (startsline && endsline) + { + begin = linebegin; + end = lineend; + } + } + Output.text.append(base, static_cast(cursor), static_cast(begin - cursor)); + if (edit.inlined != null_handle) + { + // content of the included file takes place of the include directive + composition included; + if (false == compose(State, edit.inlined, included, Depth + 1)) + { + return false; + } + if (end < basesize && (base[end] == '\r' || base[end] == '\n')) + { + // the line break which followed the directive ends the included text now, a second one would leave a blank line + if (false == included.text.empty() && included.text.back() == '\n') + { + included.text.pop_back(); + if (false == included.text.empty() && included.text.back() == '\r') + { + included.text.pop_back(); + } + } + } + else + { + ensure_newline(included.text, eol); + } + Output.append(included); + } + else + { + // the line break leading the replacement isn't needed if the text ahead happens to end with one + auto const lead{(Output.text.empty() || Output.text.back() == '\n') ? std::size_t{0} : edit.lead}; + auto const offset{static_cast(Output.text.size() + lead)}; + auto const span{source_span{offset, offset + static_cast(edit.length)}}; + if (edit.node != nullptr) + { + Output.nodes.push_back({edit.node, span, true}); + } + if (edit.site != null_handle) + { + Output.sites.push_back({edit.site, span}); + } + if (edit.instance != 0) + { + Output.instances.push_back({edit.instance, span}); + } + Output.text.append(edit.text, edit.lead - lead, std::string::npos); + } + cursor = end; + shifts.emplace_back(end, static_cast(Output.text.size()) - end); + } + Output.text.append(base, static_cast(cursor), std::string::npos); + + // what the changes didn't touch only moves along with the text around it + auto const shift = [&](source_span Span) { + auto const lookup{std::upper_bound(std::begin(shifts), std::end(shifts), Span.begin, + [](std::streamoff const Location, std::pair const &Shift) { return Location < Shift.first; })}; + auto const offset{lookup == std::begin(shifts) ? 0 : std::prev(lookup)->second}; + Span.begin += offset; + Span.end += offset; + return Span; + }; + if (auto const lookup{State.nodes.find(Layer)}; lookup != State.nodes.end()) + { + for (auto const *node : lookup->second) + { + Output.nodes.push_back({node, shift(m_sources.at(node).span), false}); + } + } + if (auto const lookup{State.includes.find(Layer)}; lookup != State.includes.end()) + { + for (auto const included : lookup->second) + { + Output.sites.push_back({included, shift(layer(included).sites.front().span)}); + } + } + if (auto const lookup{State.instances.find(Layer)}; lookup != State.instances.end()) + { + for (auto const included : lookup->second) + { + Output.instances.push_back({included, shift(instance(included).span)}); + } + } + // NOTE: with the file holding the initialization inlined, its own mark was carried over along with its text + if (hasinit && false == initedited && false == Output.init.valid()) + { + Output.init = shift(init); + } + return true; +} + +// writes changes made in the editor to the scenery files +save_result node_layers::save(std::vector const &Rootstatements, std::vector const &Trailingstatements) +{ + save_result result; + if (m_layers.empty()) + { + result.message = "The scenery wasn't opened for editing, there are no layers to save."; + return result; + } + auto const root{layer_handle{1}}; + save_state state; + auto const fail = [&](std::string const &Message) { + result.message = "Scenery not saved: " + Message; + ErrorLog(result.message); + return result; + }; + // loads text of specified layer file, if it wasn't loaded yet + auto const load = [&](layer_handle const Layer) { + if (layer(Layer).created || state.content.count(Layer) != 0) + { + return true; + } + std::error_code error; + auto const filepath{std::filesystem::path(path(Layer))}; + auto const filesize{std::filesystem::file_size(filepath, error)}; + auto const filetime{error ? std::filesystem::file_time_type{} : std::filesystem::last_write_time(filepath, error)}; + std::string content; + if (error || false == read_file(path(Layer), content)) + { + state.error = "can't read file \"" + layer(Layer).name + "\""; + return false; + } + if (filesize != layer(Layer).filesize || filetime != layer(Layer).filetime || content.size() != filesize) + { + // locations of the definitions are no longer valid + state.error = "file \"" + layer(Layer).name + "\" was changed outside of the editor since the scenery was loaded"; + return false; + } + state.content.emplace(Layer, std::move(content)); + return true; + }; + // a layer is saved if it's still a part of the scenery, as a file of its own + auto const is_output = [&](layer_handle const Layer) { return listed(Layer) && false == layer(Layer).removed && false == layer(Layer).binary; }; + + if (false == load(root)) + { + return fail(state.error); + } + state.eol = detect_eol(state.content[root], "\r\n"); + + // changed and deleted nodes + std::set rewritten; + std::map>> created; // new definitions, by target layer + auto const process = [&](basic_node const *Node, TAnimModel const *Model) { + auto const nodelayer{resolve(Node->layer())}; + if (false == is_output(nodelayer)) + { + return true; + } + if (Model != nullptr && Groups.relocating(Node)) + { + // written anew along with its group, below + return true; + } + auto const location{Node->location()}; + auto const angles{Model != nullptr ? Model->Angles() : glm::vec3{0.f}}; + auto const scale{Model != nullptr ? Model->Scale() : glm::vec3{1.f}}; + auto const lookup{m_sources.find(Node)}; + std::string error; + if (lookup != m_sources.end()) + { + // defined in a scenery file. rewrite the placement in its definition if it's no longer the one it was loaded with + auto const &source{lookup->second}; + if (false == writable(source.layer)) + { + // the editor doesn't let these be changed, as one definition stands for more than one node + return true; + } + auto const moved{glm::distance(location, source.location) > 1e-4}; + auto const rotated{false == glm::all(glm::epsilonEqual(angles, source.angles, 1e-3f))}; + auto const scaled{false == glm::all(glm::epsilonEqual(scale, source.scale, 1e-4f))}; + auto const renamed{Model != nullptr && m_renamed.find(Node) != m_renamed.end()}; + auto const *cell{Model == nullptr ? static_cast(Node) : nullptr}; + auto const track{cell != nullptr && cell->Track != nullptr ? cell->Track->name() : std::string{}}; + if (false == (moved || rotated || scaled || renamed || (false == track.empty() && Node->dirty()))) // NOSONAR + { + return true; + } + if (false == load(source.layer)) + { + return false; + } + auto const &content{state.content[source.layer]}; + if (source.span.end > static_cast(content.size())) + { + state.error = "definition of \"" + Node->name() + "\" is out of bounds of file \"" + layer(source.layer).name + "\""; // NOSONAR + return false; + } + auto text{content.substr(static_cast(source.span.begin), static_cast(source.span.end - source.span.begin))}; + auto patched{false}; + if (Model != nullptr) + { + model_placement placement; + placement.rotation = source.context.rotation; + if (moved) + { + placement.location = source.context.to_local(location); + } + if (rotated) + { + placement.angles = angles; + } + if (scaled) + { + placement.scale = scale / source.context.scale; + } + if (renamed) + { + placement.name = (Node->name().empty() ? "none" : Node->name()); + } + patched = patch_model(text, placement, error); + } + else + { + patched = patch_memcell(text, moved ? std::optional{source.context.to_local(location)} : std::nullopt, track, error); + } + if (false == patched) + { + state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\""; // NOSONAR + return false; + } + if (text.size() == static_cast(source.span.end - source.span.begin) && content.compare(static_cast(source.span.begin), text.size(), text) == 0) + { + return true; + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + edit.length = text.size(); + edit.text = std::move(text); + edit.node = Node; + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(Node); + } + else if (Model != nullptr && false == Node->from_template()) + { + // created in the editor. the definition is added to the layer file, with the placement in effect at the place it goes to + std::string text; + Node->export_as_text(text); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + auto const &context{layer(nodelayer).context_insert()}; + if (false == context.matches(layer_context())) + { + model_placement placement; + placement.rotation = context.rotation; + placement.location = context.to_local(location); + placement.angles = angles; + placement.scale = scale / context.scale; + if (false == patch_model(text, placement, error)) + { + state.error = error + " of \"" + Node->name() + "\""; + return false; + } + } + created[nodelayer].emplace_back(Node, std::move(text)); + } + return true; + }; + for (auto const *instance : simulation::Instances.sequence()) + { + if (instance != nullptr && false == process(instance, instance)) + { + return fail(state.error); + } + } + for (auto const *memorycell : simulation::Memory.sequence()) + { + if (memorycell != nullptr && false == process(memorycell, nullptr)) + { + return fail(state.error); + } + } + // models grouped or ungrouped in the editor: their definitions are taken out of where they are and written anew, a group + // as one block between group and endgroup in the layer of its first model, a model of a dissolved group on its own + std::set relocated; + auto const relocate = [&](TAnimModel const *Model, layer_handle const Target, std::vector> &Output) { + if (auto const lookup{m_sources.find(Model)}; lookup != m_sources.end()) + { + auto const &source{lookup->second}; + if (false == writable(source.layer)) + { + // one definition stands for more than one node, it stays where it is + return true; + } + if (false == load(source.layer)) + { + return false; + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(Model); + } + std::string text; + Model->export_as_text(text); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + auto const &context{layer(Target).context_insert()}; + if (false == context.matches(layer_context())) + { + model_placement placement; + placement.rotation = context.rotation; + placement.location = context.to_local(Model->location()); + placement.angles = Model->Angles(); + placement.scale = Model->Scale() / context.scale; + std::string error; + if (false == patch_model(text, placement, error)) + { + state.error = error + " of \"" + Model->name() + "\""; + return false; + } + } + Output.emplace_back(Model, std::move(text)); + relocated.emplace(Model); + return true; + }; + for (auto *node : Groups.loose()) + { + auto const *model{dynamic_cast(node)}; + if (model == nullptr || model->from_template() || false == is_output(resolve(node->layer()))) + { + continue; + } + if (false == relocate(model, resolve(node->layer()), created[resolve(node->layer())])) + { + return fail(state.error); + } + } + for (auto const group : Groups.rewritten()) + { + auto const &nodes{Groups.group(group).nodes}; + if (nodes.empty() || false == is_output(resolve(nodes.front()->layer()))) + { + continue; + } + auto const target{resolve(nodes.front()->layer())}; + auto &output{created[target]}; + // NOTE: lines with no node are written as they are, without a place in the bookkeeping + output.emplace_back(nullptr, "group"); + for (auto const *node : nodes) + { + auto const *model{dynamic_cast(node)}; + if (model == nullptr || model->from_template()) + { + continue; + } + if (false == relocate(model, target, output)) + { + return fail(state.error); + } + } + output.emplace_back(nullptr, "endgroup"); + } + auto const push_edit = [&](layer_handle const Layer, source_span const &Span, std::string Text, basic_node const *Node) { + file_patch::edit edit; + edit.begin = Span.begin; + edit.end = Span.end; + edit.length = Text.size(); + edit.text = std::move(Text); + edit.node = Node; + state.patches[Layer].edits.emplace_back(std::move(edit)); + }; + // changes which can't be written, as their definition stands for more than one node + std::vector unwritten; + // traction and event launchers are rewritten when the editor moved them along with the track they belong to + std::vector patchednodes; + auto const patch_node = [&](basic_node *Node, auto const &Patch) { + auto const lookup{m_sources.find(Node)}; + if (lookup == m_sources.end() || false == Node->dirty()) + { + return true; + } + auto const &source{lookup->second}; + if (false == is_output(resolve(source.layer))) + { + return true; + } + if (false == writable(source.layer)) + { + unwritten.push_back(Node->name()); + return true; + } + if (false == load(source.layer)) + { + return false; + } + auto const &content{state.content[source.layer]}; + if (source.span.end > static_cast(content.size())) + { + state.error = "definition of \"" + Node->name() + "\" is out of bounds of file \"" + layer(source.layer).name + "\""; + return false; + } + auto text{content.substr(static_cast(source.span.begin), static_cast(source.span.end - source.span.begin))}; + std::string error; + if (false == Patch(text, source, error)) + { + state.error = error + " of \"" + Node->name() + "\" in file \"" + layer(source.layer).name + "\""; + return false; + } + push_edit(source.layer, source.span, std::move(text), Node); + rewritten.emplace(Node); + patchednodes.push_back(Node); + return true; + }; + for (auto *traction : simulation::Traction.sequence()) + { + if (traction != nullptr && + false == patch_node(traction, [&](std::string &Text, node_source const &Source, std::string &Error) { return patch_traction(Text, *traction, Source.context.offset, Error); })) + { + return fail(state.error); + } + } + for (auto *launcher : simulation::Events.launchers()) + { + if (launcher != nullptr && + false == patch_node(launcher, [&](std::string &Text, node_source const &Source, std::string &Error) { return patch_launcher(Text, Source.context.to_local(launcher->location()), Error); })) + { + return fail(state.error); + } + } + std::vector savedpaths; + std::vector droppedpaths; + auto const path_text = [](TTrack &Path, glm::dvec3 const &Offset) { + auto const paths{Path.m_paths}; + for (auto &path : Path.m_paths) + { + path.points[segment_data::point::start] -= Offset; + path.points[segment_data::point::end] -= Offset; + } + std::string text; + Path.export_as_text(text); + Path.m_paths = paths; + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *path : simulation::Paths.sequence()) + { + if (path == nullptr || (path->iCategoryFlag & 0x80) != 0 || path->m_road != nullptr || path->from_template()) + { + continue; + } + auto const nodelayer{resolve(path->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(path)}; + if (lookup == m_sources.end()) + { + if (path->m_editorremoved || false == path->dirty()) + { + continue; + } + // a file included more than once would get a copy of the new path at every inclusion + auto const target{writable(nodelayer) ? nodelayer : root}; + created[target].emplace_back(path, path_text(*path, layer(target).context_insert().offset)); + savedpaths.push_back(path); + continue; + } + auto const &source{lookup->second}; + if (false == path->m_editorremoved && false == path->dirty()) + { + continue; + } + if (false == writable(source.layer)) + { + unwritten.push_back(path->name().empty() ? std::string{"(noname)"} : path->name()); + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + if (path->m_editorremoved) + { + push_edit(source.layer, source.span, {}, nullptr); + droppedpaths.push_back(path); + } + else + { + push_edit(source.layer, source.span, path_text(*path, source.context.offset), path); + savedpaths.push_back(path); + } + rewritten.emplace(path); + } + // roads. the lanes aren't saved, they're generated from the definition of the road + std::vector savedroads; + std::vector droppedroads; + auto const road_text = [](road_node &Road, glm::dvec3 const &Offset) { + auto const definition{Road.definition()}; + auto local{definition}; + local.axis.points[segment_data::point::start] -= Offset; + local.axis.points[segment_data::point::end] -= Offset; + Road.define(local); + std::string text; + Road.export_as_text(text); + Road.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->from_template()) + { + continue; + } + auto const nodelayer{resolve(road->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(road)}; + if (lookup == m_sources.end()) + { + if (road->m_editorremoved || false == road->dirty()) + { + continue; + } + created[nodelayer].emplace_back(road, road_text(*road, layer(nodelayer).context_insert().offset)); + savedroads.push_back(road); + continue; + } + auto const &source{lookup->second}; + if ((false == road->m_editorremoved && false == road->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (road->m_editorremoved) + { + droppedroads.push_back(road); + } + else + { + edit.text = road_text(*road, source.context.offset); + edit.length = edit.text.size(); + edit.node = road; + savedroads.push_back(road); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(road); + } + // models laid along curves, likewise + std::vector savedsweeps; + std::vector droppedsweeps; + auto const sweep_text = [](sweep_node &Sweep, glm::dvec3 const &Offset) { + auto const definition{Sweep.definition()}; + auto local{definition}; + for (auto &piece : local.pieces) + { + piece.points[segment_data::point::start] -= Offset; + piece.points[segment_data::point::end] -= Offset; + } + Sweep.define(local); + std::string text; + Sweep.export_as_text(text); + Sweep.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *sweep : simulation::Sweeps.sequence()) + { + if (sweep == nullptr || sweep->from_template()) + { + continue; + } + auto const nodelayer{resolve(sweep->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(sweep)}; + if (lookup == m_sources.end()) + { + if (sweep->m_editorremoved || false == sweep->dirty()) + { + continue; + } + created[nodelayer].emplace_back(sweep, sweep_text(*sweep, layer(nodelayer).context_insert().offset)); + savedsweeps.push_back(sweep); + continue; + } + auto const &source{lookup->second}; + if ((false == sweep->m_editorremoved && false == sweep->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (sweep->m_editorremoved) + { + droppedsweeps.push_back(sweep); + } + else + { + edit.text = sweep_text(*sweep, source.context.offset); + edit.length = edit.text.size(); + edit.node = sweep; + savedsweeps.push_back(sweep); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(sweep); + } + // road junctions, likewise + std::vector savedjunctions; + std::vector droppedjunctions; + auto const junction_text = [](junction_node &Junction, glm::dvec3 const &Offset) { + auto const definition{Junction.definition()}; + auto local{definition}; + local.centre -= Offset; + for (auto &arm : local.arms) + { + arm.position -= Offset; + } + Junction.define(local); + std::string text; + Junction.export_as_text(text); + Junction.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *junction : simulation::Junctions.sequence()) + { + if (junction == nullptr || junction->from_template()) + { + continue; + } + auto const nodelayer{resolve(junction->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(junction)}; + if (lookup == m_sources.end()) + { + if (junction->m_editorremoved || false == junction->dirty()) + { + continue; + } + created[nodelayer].emplace_back(junction, junction_text(*junction, layer(nodelayer).context_insert().offset)); + savedjunctions.push_back(junction); + continue; + } + auto const &source{lookup->second}; + if ((false == junction->m_editorremoved && false == junction->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (junction->m_editorremoved) + { + droppedjunctions.push_back(junction); + } + else + { + edit.text = junction_text(*junction, source.context.offset); + edit.length = edit.text.size(); + edit.node = junction; + savedjunctions.push_back(junction); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(junction); + } + // level crossings and traffic points of the roads + std::vector savedroadpoints; + std::vector droppedroadpoints; + auto const roadpoint_text = [](roadpoint_node &Point, glm::dvec3 const &Offset) { + auto const definition{Point.definition()}; + auto local{definition}; + local.position -= Offset; + Point.define(local); + std::string text; + Point.export_as_text(text); + Point.define(definition); + text.erase(text.find_last_not_of(" \t\r\n") + 1); + return text; + }; + for (auto *point : simulation::Roadpoints.sequence()) + { + if (point == nullptr || point->from_template()) + { + continue; + } + auto const nodelayer{resolve(point->layer())}; + if (false == is_output(nodelayer)) + { + continue; + } + auto const lookup{m_sources.find(point)}; + if (lookup == m_sources.end()) + { + if (point->m_editorremoved || false == point->dirty()) + { + continue; + } + created[nodelayer].emplace_back(point, roadpoint_text(*point, layer(nodelayer).context_insert().offset)); + savedroadpoints.push_back(point); + continue; + } + auto const &source{lookup->second}; + if ((false == point->m_editorremoved && false == point->dirty()) || false == writable(source.layer)) + { + continue; + } + if (false == load(source.layer)) + { + return fail(state.error); + } + file_patch::edit edit; + edit.begin = source.span.begin; + edit.end = source.span.end; + if (point->m_editorremoved) + { + droppedroadpoints.push_back(point); + } + else + { + edit.text = roadpoint_text(*point, source.context.offset); + edit.length = edit.text.size(); + edit.node = point; + savedroadpoints.push_back(point); + } + state.patches[source.layer].edits.emplace_back(std::move(edit)); + rewritten.emplace(point); + } + for (auto const &erased : m_erased) + { + file_patch::edit edit; + edit.begin = erased.second.begin; + edit.end = erased.second.end; + state.patches[erased.first].edits.emplace_back(std::move(edit)); + } + + // editor data in the comment lines: the old lines go, the new ones are added to the file like new definitions + std::map> markedtext; + for (auto const &marking : m_marking) + { + auto const markedlayer{marking.first.first}; + auto const &mark{marking.first.second}; + if (auto const lookup{m_marked.find(markedlayer)}; lookup != m_marked.end()) + { + for (auto const &line : lookup->second) + { + if (line.mark == mark) + { + push_edit(markedlayer, line.span, {}, nullptr); + } + } + } + auto const target{resolve(markedlayer)}; + if (marking.second.empty() || false == is_output(target)) + { + continue; + } + for (auto const &text : marking.second) + { + markedtext[target].push_back(mark + ' ' + text); + } + } + + // includes of templates + std::map> placed; // directives made in the editor, by target layer + for (std::size_t idx = 0; idx < m_instances.size(); ++idx) + { + auto const &included{m_instances[idx]}; + if (included.dead) + { + continue; + } + if (included.span.valid()) + { + if (included.removed) + { + file_patch::edit edit; + edit.begin = included.span.begin; + edit.end = included.span.end; + state.patches[included.layer].edits.emplace_back(std::move(edit)); + } + else if (false == included.directive.empty()) + { + // changed in the editor, the new directive takes the place of the one in the file + file_patch::edit edit; + edit.begin = included.span.begin; + edit.end = included.span.end; + edit.length = included.directive.size(); + edit.text = included.directive; + edit.instance = static_cast(idx + 1); + state.patches[included.layer].edits.emplace_back(std::move(edit)); + } + } + else if (false == included.removed && false == included.directive.empty()) + { + auto const target{resolve(included.layer)}; + if (false == is_output(target)) + { + continue; + } + if (false == included.context.matches(layer(target).context_insert())) + { + // shouldn't happen, merge of layers is refused when it'd lead to this + return fail("include \"" + *included.file + "\" was placed for different origin, rotation or scale than its layer file receives it with"); + } + placed[target].emplace_back(static_cast(idx + 1)); + } + } + + // layer changes + std::map> appended; // merged layers placed at the end of their target + std::set absorbed; // merged layers which cease to exist as files + std::set detached; // layers whose include directives are replaced + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + auto const candidate{static_cast(idx + 1)}; + auto const &source{layer(candidate)}; + if (source.dead) + { + continue; + } + auto const hassite{false == source.created && source.sites.size() == 1 && false == source.sites.front().fixed && source.sites.front().span.valid()}; + if (source.merged != null_handle) + { + auto const target{resolve(candidate)}; + absorbed.emplace(candidate); + if (hassite) + { + file_patch::edit edit; + edit.begin = source.sites.front().span.begin; + edit.end = source.sites.front().span.end; + if (resolve(source.sites.front().parent) == target) + { + edit.inlined = candidate; + } + else + { + appended[target].emplace_back(candidate); + } + state.patches[source.sites.front().parent].edits.emplace_back(std::move(edit)); + detached.emplace(candidate); + } + else + { + appended[target].emplace_back(candidate); + } + } + else if (source.removed) + { + if (hassite) + { + file_patch::edit edit; + edit.begin = source.sites.front().span.begin; + edit.end = source.sites.front().span.end; + state.patches[source.sites.front().parent].edits.emplace_back(std::move(edit)); + detached.emplace(candidate); + } + } + else if (source.created) + { + // include directive for the new file, on a line of its own + auto const parent{source.parent}; + auto const directive{"include " + source.name + " end"}; + file_patch::edit edit; + edit.site = candidate; + edit.length = directive.size(); + if (valid(source.anchor) && layer(source.anchor).sites.size() == 1 && layer(source.anchor).sites.front().parent == parent && layer(source.anchor).sites.front().span.valid()) + { + edit.begin = edit.end = layer(source.anchor).sites.front().span.end; + edit.text = state.eol + directive; + edit.lead = state.eol.size(); + } + else + { + // NOTE: negative location stands for the place the file receives additions at + edit.begin = edit.end = -1; + if (false == layer(parent).created) + { + if (false == load(parent)) + { + return fail(state.error); + } + // dropped by compose() if the text ahead ends with a line break already + edit.text = state.eol; + edit.lead = state.eol.size(); + } + edit.text += directive + state.eol; + } + state.patches[parent].edits.emplace_back(std::move(edit)); + } + } + + // files to write + std::vector outputs; + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + auto const candidate{static_cast(idx + 1)}; + if (false == is_output(candidate)) + { + continue; + } + if (layer(candidate).created || state.patches.count(candidate) != 0 || appended.count(candidate) != 0 || created.count(candidate) != 0 || placed.count(candidate) != 0 || + markedtext.count(candidate) != 0 || (candidate == root && (false == Rootstatements.empty() || false == Trailingstatements.empty()))) + { + outputs.emplace_back(candidate); + } + } + // text of the merged layers travels with the file they end up in, but changes made to it are still theirs + std::vector composed{outputs}; + composed.insert(std::end(composed), std::begin(absorbed), std::end(absorbed)); + for (auto const candidate : composed) + { + if (false == load(candidate)) + { + return fail(state.error); + } + } + // what is defined in these files and isn't rewritten moves along with the text + for (auto const &source : m_sources) + { + if (state.content.count(source.second.layer) != 0 && rewritten.count(source.first) == 0) + { + state.nodes[source.second.layer].emplace_back(source.first); + } + } + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + auto const candidate{static_cast(idx + 1)}; + auto const &included{layer(candidate)}; + if (included.dead || included.created || detached.count(candidate) != 0 || included.sites.size() != 1 || included.sites.front().fixed || false == included.sites.front().span.valid()) + { + continue; + } + state.includes[included.sites.front().parent].emplace_back(candidate); + } + for (std::size_t idx = 0; idx < m_instances.size(); ++idx) + { + auto const &included{m_instances[idx]}; + if (false == included.dead && false == included.removed && included.directive.empty() && included.span.valid() && state.content.count(included.layer) != 0) + { + state.instances[included.layer].emplace_back(static_cast(idx + 1)); + } + } + + std::map texts; + for (auto const output : outputs) + { + auto &text{texts[output]}; + auto const eol{detect_eol(state.content.count(output) != 0 ? state.content[output] : std::string{}, state.eol)}; + if (layer(output).created) + { + // NOTE: the parser treats an empty file as a missing one + text.text = "// scenery layer created in the editor" + eol; + } + composition body; + if (false == compose(state, output, body, 0)) + { + return fail(state.error); + } + text.append(body); + // what the editor adds to the file + composition added; + if (auto const lookup{appended.find(output)}; lookup != appended.end()) + { + for (auto const merged : lookup->second) + { + composition mergedtext; + if (false == compose(state, merged, mergedtext, 0)) + { + return fail(state.error); + } + ensure_newline(added.text, eol); + added.append(mergedtext); + } + } + if (auto const lookup{created.find(output)}; lookup != created.end()) + { + for (auto const &definition : lookup->second) + { + ensure_newline(added.text, eol); + auto const offset{static_cast(added.text.size())}; + if (definition.first != nullptr) + { + added.nodes.push_back({definition.first, {offset, offset + static_cast(definition.second.size())}, true}); + } + added.text += definition.second + eol; + } + } + if (auto const lookup{placed.find(output)}; lookup != placed.end()) + { + for (auto const included : lookup->second) + { + auto const &directive{instance(included).directive}; + ensure_newline(added.text, eol); + auto const offset{static_cast(added.text.size())}; + added.instances.push_back({included, {offset, offset + static_cast(directive.size())}}); + added.text += directive + eol; + } + } + if (auto const lookup{markedtext.find(output)}; lookup != markedtext.end()) + { + for (auto const &line : lookup->second) + { + ensure_newline(added.text, eol); + added.text += line + eol; + } + } + if (output == root) + { + for (auto const &statement : Rootstatements) + { + ensure_newline(added.text, eol); + added.text += statement + eol; + } + } + if (output == root && false == Trailingstatements.empty()) + { + composition trailing; + for (auto const &statement : Trailingstatements) + trailing.text += statement + eol; + ensure_newline(text.text, eol); + text.append(trailing); + } + if (added.text.empty()) + { + continue; + } + if (false == text.init.valid()) + { + ensure_newline(text.text, eol); + text.append(added); + continue; + } + // the scenario initialization is meant to be followed by nothing but the vehicles, so the additions go ahead + // of it. they take whole lines, with the initialization left at the start of a line the way it was + ensure_newline(added.text, eol); + auto location{text.init.begin}; + while (location > 0 && (text.text[location - 1] == ' ' || text.text[location - 1] == '\t')) + { + --location; + } + if (location == 0 || text.text[location - 1] == '\n') + { + text.insert(location, added); + } + else + { + composition separated; + separated.text = eol; + separated.append(added); + text.insert(text.init.begin, separated); + } + } + + // with everything worked out it's safe to touch the files. new content goes to temporary files first, + // so a failure half way through doesn't leave the scenery with only a part of the changes + std::error_code error; + std::vector written; + for (auto const output : outputs) + { + auto const lookup{state.content.find(output)}; + if (lookup != state.content.end() && lookup->second == texts[output].text) + { + continue; + } + auto const filepath{std::filesystem::path(path(output))}; + if (filepath.has_parent_path()) + { + std::filesystem::create_directories(filepath.parent_path(), error); + } + std::ofstream file{path(output) + ".tmp", std::ios_base::binary | std::ios_base::trunc}; + file << texts[output].text; + file.close(); + if (file.fail()) + { + for (auto const leftover : written) + { + std::filesystem::remove(path(leftover) + ".tmp", error); + } + std::filesystem::remove(path(output) + ".tmp", error); + return fail("can't write file \"" + layer(output).name + "\""); + } + written.emplace_back(output); + } + // a file which loses its ground to the terrain of the editor keeps a copy of itself each time, whatever was saved before. + // without the copy nothing is replaced + { + auto const now{std::time(nullptr)}; + char stamp[32]; + std::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", std::localtime(&now)); + for (auto const output : written) + { + auto const filepath{path(output)}; + bool const losesground{std::any_of(std::begin(m_shapes), std::end(m_shapes), [&](shape_source const &Shape) { return Shape.erased && Shape.layer == output; })}; + if (false == losesground || false == std::filesystem::exists(filepath, error)) + { + continue; + } + auto const backup{filepath + ".bak-" + stamp + "-przed-terenem-edytora"}; + std::error_code copyerror; + if (false == std::filesystem::copy_file(filepath, backup, std::filesystem::copy_options::overwrite_existing, copyerror)) + { + for (auto const leftover : written) + { + std::filesystem::remove(path(leftover) + ".tmp", error); + } + return fail("can't make a backup of file \"" + layer(output).name + "\": " + copyerror.message()); + } + WriteLog("Scenery save: the triangles of the ground go from \"" + layer(output).name + "\", its previous content is kept in \"" + backup + "\""); + result.backups.emplace_back(backup); + } + } + std::vector replaced; + for (auto const output : written) + { + auto const filepath{path(output)}; + // the first save of a file leaves its original content next to it + if (false == layer(output).editormade && std::filesystem::exists(filepath, error) && false == std::filesystem::exists(filepath + ".bak", error)) + { + std::filesystem::copy_file(filepath, filepath + ".bak", error); + } + std::filesystem::rename(filepath + ".tmp", filepath, error); + if (error) + { + // e.g. the file is held open by another program. take back what was done so far: the scenery files + // have to stay in step with each other, and with what we know about them + auto const cause{error.message()}; + for (auto const restored : replaced) + { + auto const lookup{state.content.find(restored)}; + if (lookup != state.content.end()) + { + std::ofstream{path(restored), std::ios_base::binary | std::ios_base::trunc} << lookup->second; + stat(restored); + } + else + { + std::filesystem::remove(path(restored), error); + } + } + for (auto const leftover : written) + { + std::filesystem::remove(path(leftover) + ".tmp", error); + } + result.files.clear(); + return fail("can't replace file \"" + layer(output).name + "\": " + cause); + } + replaced.emplace_back(output); + result.files.emplace_back(layer(output).name); + } + for (auto const &file : result.files) + { + WriteLog("Scenery save: file \"" + file + "\" updated"); + } + + // the scenery files now match the scene, update the bookkeeping to reflect it + Groups.saved(); + auto removedterrain{false}; + for (auto const output : outputs) + { + for (auto const &place : texts[output].nodes) + { + auto &source{m_sources[place.node]}; + auto const isnew{source.layer == null_handle}; + source.layer = output; + source.span = place.span; + // a definition written anew elsewhere is in the placement of the place it went to, as a new one is + if (isnew || relocated.count(place.node) != 0) + { + source.context = layer(output).context_insert(); + } + if (isnew || place.rewritten) + { + auto const *model{dynamic_cast(place.node)}; + source.location = place.node->location(); + source.angles = (model != nullptr ? model->Angles() : glm::vec3{0.f}); + source.scale = (model != nullptr ? model->Scale() : glm::vec3{1.f}); + } + } + for (auto const &place : texts[output].instances) + { + auto &included{m_instances[place.instance - 1]}; + included.layer = output; + included.span = place.span; + included.directive.clear(); + } + for (auto const &place : texts[output].sites) + { + auto &included{layer(place.layer)}; + if (included.sites.empty()) + { + // directive of a layer created in the editor + included.sites.emplace_back(); + } + included.sites.front().parent = output; + included.sites.front().span = place.span; + included.parent = output; + } + // the mark of the scenario initialization moved along with the text around it, or came with the text of a merged layer + auto &saved{layer(output)}; + if (false == saved.init.valid() && texts[output].init.valid()) + { + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + if (m_layers[idx].init.valid() && m_layers[idx].merged != null_handle && resolve(static_cast(idx + 1)) == output) + { + saved.context_init = m_layers[idx].context_init; + } + } + } + saved.init = texts[output].init; + } + for (std::size_t idx = 0; idx < m_layers.size(); ++idx) + { + auto const candidate{static_cast(idx + 1)}; + auto &saved{layer(candidate)}; + if (saved.dead) + { + continue; + } + if (saved.merged != null_handle || saved.removed) + { + removedterrain |= saved.removed && (saved.items[static_cast(layer_item::shape)] + saved.items[static_cast(layer_item::lines)] > 0); + saved.dead = true; + saved.sites.clear(); + } + saved.created = false; + saved.anchor = null_handle; + } + for (auto const output : written) + { + stat(output); + scan(output); + } + for (auto const &marking : m_marking) + { + if (resolve(marking.first.first) != marking.first.first) + { + // the lines of the merged layer are now in the file of its target + m_marked.erase(marking.first.first); + } + } + m_marking.clear(); + for (auto &included : m_instances) + { + // removed includes are gone from the files now + included.dead = included.dead || included.removed; + } + m_erased.clear(); + m_renamed.clear(); + // the shapes of the rewritten files are somewhere else in them now + m_shapes.erase(std::remove_if(std::begin(m_shapes), std::end(m_shapes), [&](shape_source const &Shape) { return std::find(std::begin(composed), std::end(composed), Shape.layer) != std::end(composed); }), std::end(m_shapes)); + for (auto *path : savedpaths) + { + path->m_dirty = false; + } + for (auto *node : patchednodes) + { + node->m_dirty = false; + } + for (auto const *path : droppedpaths) + { + m_sources.erase(path); + } + for (auto *road : savedroads) + { + road->m_dirty = false; + } + for (auto const *road : droppedroads) + { + m_sources.erase(road); + } + for (auto *sweep : savedsweeps) + { + sweep->m_dirty = false; + } + for (auto const *sweep : droppedsweeps) + { + m_sources.erase(sweep); + } + for (auto *junction : savedjunctions) + { + junction->m_dirty = false; + } + for (auto const *junction : droppedjunctions) + { + m_sources.erase(junction); + } + for (auto *point : savedroadpoints) + { + point->m_dirty = false; + } + for (auto const *point : droppedroadpoints) + { + m_sources.erase(point); + } + removedterrain |= m_shapeserased; + m_shapeserased = false; + if (removedterrain) + { + // binary terrain file still holds geometry of the dropped layer, have it rebuilt on the next load + auto terrainfile{layer(root).name}; + while (false == terrainfile.empty() && terrainfile.front() == '$') + { + terrainfile.erase(0, 1); + } + erase_extension(terrainfile); + if (std::filesystem::remove(Global.asCurrentSceneryPath + terrainfile + ".sbt", error)) + { + WriteLog("Scenery save: binary terrain file removed, it'll be generated again on the next load"); + } + } + + result.success = true; + if (result.files.empty()) + { + result.message = "No changes to save."; + } + else + { + result.message = "Saved: "; + for (auto const &file : result.files) + { + result.message += file + (&file != &result.files.back() ? ", " : ""); + } + for (auto const &backup : result.backups) + { + result.message += ". Backup before the ground was taken out: " + std::filesystem::path(backup).filename().string(); + } + } + if (false == unwritten.empty()) + { + std::string names; + for (std::size_t i = 0; i < std::min(unwritten.size(), 5); ++i) + { + names += (i > 0 ? ", " : "") + unwritten[i]; + } + if (unwritten.size() > 5) + { + names += ", ..."; + } + auto const warning{std::to_string(unwritten.size()) + " changed nodes not saved, their files are included with parameters or more than once: " + names}; + WriteLog("Scenery save: " + warning, logtype::generic); + result.message = result.files.empty() ? warning : result.message + ". " + warning; + } + return result; +} + +} // namespace scene + +//--------------------------------------------------------------------------- diff --git a/scene/scenemodelentries.cpp b/scene/scenemodelentries.cpp new file mode 100644 index 00000000..d11255e3 --- /dev/null +++ b/scene/scenemodelentries.cpp @@ -0,0 +1,401 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/scenemodelentries.h" + +#include "utilities/parser.h" + +#include +#include +#include +#include +#include +#include + +namespace scene +{ + +namespace +{ + +enum char_kind : unsigned char +{ + char_regular, + char_separator, // one of the separators the scenery parser splits the text on + char_special // can make the parser treat the token in a way of its own: a quote, or a part of a comment mark +}; + +constexpr std::array make_char_kinds() +{ + std::array kinds{}; + kinds['\n'] = char_separator; + kinds['\r'] = char_separator; + kinds['\t'] = char_separator; + kinds[' '] = char_separator; + kinds[';'] = char_separator; + kinds['"'] = char_special; + kinds['/'] = char_special; + return kinds; +} + +constexpr std::array char_kinds{make_char_kinds()}; + +char to_lower(char const Character) +{ + // only the ascii letters, like the parser does it + return (Character >= 'A' && Character <= 'Z') ? static_cast(Character - 'A' + 'a') : Character; +} + +// compares a token with a lower case keyword the way a token converted by the parser would compare +bool equals(std::string_view const Token, std::string_view const Keyword) +{ + if (Token.size() != Keyword.size()) + { + return false; + } + for (std::size_t idx = 0; idx < Token.size(); ++idx) + { + if (to_lower(Token[idx]) != Keyword[idx]) + { + return false; + } + } + return true; +} + +// reads the definitions held by one piece of a text +class entry_reader +{ + public: + explicit entry_reader(std::string_view const Text) : m_text(Text) {} + // reads definitions from specified place in the text, until the end of one reaches Limit or something else turns up. + // NOTE: line breaks are counted from the place the reading starts at + void read(std::size_t Begin, std::size_t Limit, bool Keywordread); + // members + model_entry_block block; + std::size_t cursor{0}; // end of the last definition read, or the place the reading started at + std::size_t linebreaks{0}; // line breaks ahead of the cursor + bool complete{false}; // the reading got as far as the limit + + private: + // moves past the separators and the line comments. returns: false at a block comment + bool skip(std::size_t &Position, std::size_t &Linebreaks) const; + // takes the token at specified place, which has to be past the separators. returns: false if the parser wouldn't take + // the token the way it's written: there's a quote in it, or a comment starts in its middle + bool token(std::size_t &Position, std::string_view &Token) const; + // takes the next token. returns: false if there isn't one which can be used as it's written + bool next(std::size_t &Position, std::size_t &Linebreaks, std::string_view &Token) const + { + return skip(Position, Linebreaks) && token(Position, Token) && false == Token.empty(); + } + // takes the next token as a number. returns: false unless the parser would convert the token without a stream + template bool number(std::size_t &Position, std::size_t &Linebreaks, Type_ &Value) const + { + std::string_view text; + return next(Position, Linebreaks, text) && cParser::parseNumber(text, Value); + } + // takes a complete definition. returns: false if what's there isn't one, or isn't one the reader covers + bool entry(std::size_t &Position, std::size_t &Linebreaks, bool Keywordread, model_entry &Entry); + std::uint32_t appearance(std::string_view Model, std::string_view Texture); + // members + std::string_view m_text; + std::unordered_map m_appearances; // by the part of the text which names the model and the texture + std::string_view m_recentkey; + std::uint32_t m_recentappearance{0}; +}; + +bool entry_reader::skip(std::size_t &Position, std::size_t &Linebreaks) const +{ + auto const *const data{m_text.data()}; + auto const size{m_text.size()}; + while (Position < size) + { + auto const character{data[Position]}; + if (char_kinds[static_cast(character)] == char_separator) + { + if (character == '\n') + { + ++Linebreaks; + } + ++Position; + continue; + } + if (character == '/' && Position + 1 < size) + { + if (data[Position + 1] == '/') + { + // the comment takes the rest of the line, along with the line break + auto const end{m_text.find('\n', Position + 2)}; + if (end == std::string_view::npos) + { + Position = size; + } + else + { + Position = end + 1; + ++Linebreaks; + } + continue; + } + if (data[Position + 1] == '*') + { + return false; + } + } + break; + } + return true; +} + +bool entry_reader::token(std::size_t &Position, std::string_view &Token) const +{ + auto const *const data{m_text.data()}; + auto const size{m_text.size()}; + auto const begin{Position}; + while (Position < size) + { + auto const kind{char_kinds[static_cast(data[Position])]}; + if (kind == char_separator) + { + break; + } + if (kind == char_special) + { + if (data[Position] == '"') + { + return false; + } + if (Position + 1 < size && (data[Position + 1] == '/' || data[Position + 1] == '*')) + { + return false; + } + } + ++Position; + } + Token = m_text.substr(begin, Position - begin); + return true; +} + +bool entry_reader::entry(std::size_t &Position, std::size_t &Linebreaks, bool const Keywordread, model_entry &Entry) +{ + std::string_view word; + if (false == Keywordread) + { + if (false == next(Position, Linebreaks, word) || false == equals(word, "node")) + { + return false; + } + Entry.begin = Position - word.size(); + // the parser takes a token along with the separator which ends it, and the loader notes the line right after + Entry.line = Linebreaks + ((Position < m_text.size() && m_text[Position] == '\n') ? 1 : 0); + } + // NOTE: negative minimal range marks the model as terrain, which is processed in a way of its own + if (false == number(Position, Linebreaks, Entry.range_max) || false == number(Position, Linebreaks, Entry.range_min) || Entry.range_min < 0.0) + { + return false; + } + // NOTE: the parser takes include for a directive wherever it shows up + if (false == next(Position, Linebreaks, Entry.name) || equals(Entry.name, "include")) + { + return false; + } + if (false == next(Position, Linebreaks, word) || false == equals(word, "model")) + { + return false; + } + if (false == number(Position, Linebreaks, Entry.location.x) || false == number(Position, Linebreaks, Entry.location.y) || false == number(Position, Linebreaks, Entry.location.z) || + false == number(Position, Linebreaks, Entry.angle)) + { + return false; + } + std::string_view model; + std::string_view texture; + if (false == next(Position, Linebreaks, model) || equals(model, "include") || false == next(Position, Linebreaks, texture) || equals(texture, "include")) + { + return false; + } + while (true) + { + if (false == next(Position, Linebreaks, word)) + { + return false; + } + if (equals(word, "endmodel")) + { + break; + } + if (equals(word, "angles")) + { + // NOTE: a block which shows up again is left to the parser, along with the question of what it means + if (Entry.has_angles || false == number(Position, Linebreaks, Entry.angles.x) || false == number(Position, Linebreaks, Entry.angles.y) || false == number(Position, Linebreaks, Entry.angles.z)) + { + return false; + } + Entry.has_angles = true; + } + else if (equals(word, "scale")) + { + if (Entry.has_scale || false == number(Position, Linebreaks, Entry.scale.x) || false == number(Position, Linebreaks, Entry.scale.y) || false == number(Position, Linebreaks, Entry.scale.z)) + { + return false; + } + Entry.has_scale = true; + } + else if (equals(word, "notransition")) + { + Entry.notransition = true; + } + else + { + return false; + } + } + Entry.end = Position; + Entry.appearance = appearance(model, texture); + return true; +} + +std::uint32_t entry_reader::appearance(std::string_view const Model, std::string_view const Texture) +{ + // the two tokens along with whatever is between them. the same pair written in another way gets an entry of its own + std::string_view const key{Model.data(), static_cast((Texture.data() + Texture.size()) - Model.data())}; + if (key == m_recentkey) + { + return m_recentappearance; + } + auto lookup{m_appearances.find(key)}; + if (lookup == m_appearances.end()) + { + model_appearance appearance; + appearance.model.assign(Model); + std::transform(std::begin(appearance.model), std::end(appearance.model), std::begin(appearance.model), [](char const Character) { return Character == '\\' ? '/' : to_lower(Character); }); + appearance.texture.assign(Texture); + std::replace(std::begin(appearance.texture), std::end(appearance.texture), '\\', '/'); + block.appearances.emplace_back(std::move(appearance)); + lookup = m_appearances.emplace(key, static_cast(block.appearances.size() - 1)).first; + } + m_recentkey = key; + m_recentappearance = lookup->second; + return lookup->second; +} + +void entry_reader::read(std::size_t const Begin, std::size_t const Limit, bool Keywordread) +{ + cursor = Begin; + linebreaks = 0; + while (cursor < Limit) + { + auto position{cursor}; + auto breaks{linebreaks}; + model_entry definition; + if (false == entry(position, breaks, Keywordread, definition)) + { + break; + } + Keywordread = false; + block.entries.emplace_back(definition); + cursor = position; + linebreaks = breaks; + } + complete = (cursor >= Limit); +} + +} // namespace + +model_entry_run read_model_entries(std::string_view const Text, bool const Keywordread, model_reader_setup const &Setup) +{ + model_entry_run run; + + entry_reader lead{Text}; + lead.read(0, std::min(Text.size(), Setup.lead), Keywordread); + if (lead.block.entries.empty()) + { + return run; + } + auto cursor{lead.cursor}; + auto linebreaks{lead.linebreaks}; + run.blocks.emplace_back(std::move(lead.block)); + + if (lead.complete && cursor < Text.size()) + { + // the run goes on: the next part of the text is divided between the threads. a thread needs a place to start at, + // which is found by looking for the keyword ending a definition. whether the place really is an end of a definition + // is known only when the thread reading the part ahead of it gets there, so the pieces are put together as long as + // each one ended right where the next one began. if that's not the case, e.g. the place found was inside a comment, + // the pieces past it are dropped and their text is read again by the next call, starting from a place which is certain + auto const limit{cursor + std::min(Text.size() - cursor, std::max(Setup.window, 1))}; + auto const threads{std::clamp(Setup.threads != 0 ? Setup.threads : std::thread::hardware_concurrency(), 1, 16)}; + auto const count{std::clamp((limit - cursor) / std::max(Setup.share, 1), 1, threads)}; + std::string_view const endmark{"endmodel"}; + std::vector bounds{cursor}; + for (std::size_t idx = 1; idx < count; ++idx) + { + auto const mark{Text.find(endmark, cursor + (limit - cursor) / count * idx)}; + if (mark == std::string_view::npos || mark + endmark.size() >= limit) + { + break; + } + if (mark + endmark.size() > bounds.back()) + { + bounds.emplace_back(mark + endmark.size()); + } + } + bounds.emplace_back(limit); + + std::vector readers(bounds.size() - 1, entry_reader{Text}); + { + std::vector> tasks; + for (std::size_t idx = 1; idx < readers.size(); ++idx) + { + auto const work{[&readers, &bounds, idx]() { readers[idx].read(bounds[idx], bounds[idx + 1], false); }}; + try + { + tasks.emplace_back(std::async(std::launch::async, work)); + } + catch (std::system_error const &) + { + // no thread to be had, the piece is read right here + work(); + } + } + readers.front().read(bounds[0], bounds[1], false); + for (auto &task : tasks) + { + task.get(); + } + } + for (std::size_t idx = 0; idx < readers.size(); ++idx) + { + auto &reader{readers[idx]}; + for (auto &definition : reader.block.entries) + { + definition.line += linebreaks; + } + cursor = reader.cursor; + linebreaks += reader.linebreaks; + if (false == reader.block.entries.empty()) + { + run.blocks.emplace_back(std::move(reader.block)); + } + if (reader.cursor != bounds[idx + 1]) + { + break; + } + } + } + run.length = cursor; + run.linebreaks = linebreaks; + + return run; +} + +} // namespace scene + +//--------------------------------------------------------------------------- diff --git a/scene/scenemodelentries.h b/scene/scenemodelentries.h new file mode 100644 index 00000000..98b171ee --- /dev/null +++ b/scene/scenemodelentries.h @@ -0,0 +1,81 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace scene +{ + +// model file and replacable skin of a model instance, in the form TAnimModel::Load() gives the tokens which name them +struct model_appearance +{ + std::string model; // lower case, forward slashes + std::string texture; // forward slashes +}; + +// definition of a model instance read from scenery text: +// node model [angles ] [scale ] [notransition] endmodel +struct model_entry +{ + double range_max{0.0}; + double range_min{0.0}; + std::string_view name; // the way the text spells it. NOTE: refers to the text + glm::dvec3 location{0.0}; + float angle{0.f}; // rotation around the vertical axis + glm::vec3 angles{0.f}; // content of the angles block, if there's one + glm::vec3 scale{1.f}; // content of the scale block, if there's one + bool has_angles{false}; + bool has_scale{false}; + bool notransition{false}; + std::uint32_t appearance{0}; // model and texture of the instance, index into the appearances of the block holding the entry + std::size_t begin{0}; // location of the definition in the text: from the start of its node keyword... + std::size_t end{0}; // ...to the end of its closing keyword + std::size_t line{0}; // line breaks in the text ahead of the definition, counted the way the parser counts them for a node +}; + +// definitions read from one piece of the text +struct model_entry_block +{ + std::vector entries; + std::vector appearances; +}; + +// run of definitions a text starts with +struct model_entry_run +{ + std::vector blocks; // in the order of the text; no block is empty + std::size_t length{0}; // amount of the text taken by the definitions + std::size_t linebreaks{0}; // number of line breaks in that part of the text +}; + +// how the text is divided between the threads +struct model_reader_setup +{ + std::size_t lead{64 * 1024}; // amount of the text read before the threads get involved; most runs of definitions are short + std::size_t window{8 * 1024 * 1024}; // most of the text a single call goes through past the lead + std::size_t share{256 * 1024}; // least amount of the text worth a thread of its own + unsigned int threads{0}; // most threads put to work; 0: as many as the hardware runs at a time +}; + +// reads the definitions of model instances provided text starts with. the reading ends where the text holds something else +// than a definition the reader knows inside out: another kind of node or directive, a block comment, a definition of +// terrain, one with lights or anything else not listed above, with quoted text, an include or a comment glued to a token. +// that part is left for the scenery parser, so the outcome is the same as if the parser went through all of the text. +// Keywordread: the node keyword of the first definition was already taken off the text; begin and line of that entry aren't set +model_entry_run read_model_entries(std::string_view Text, bool Keywordread, model_reader_setup const &Setup = model_reader_setup()); + +} // namespace scene + +//--------------------------------------------------------------------------- diff --git a/scene/scenenode.cpp b/scene/scenenode.cpp index 1ba822e1..301b6ea1 100644 --- a/scene/scenenode.cpp +++ b/scene/scenenode.cpp @@ -309,7 +309,8 @@ shape_node::import( cParser &Input, scene::node_data const &Nodedata ) { } token = Input.getToken(); - } while( token != "endtri" ); + } while( token != "endtri" + && false == token.empty() ); // data which ends without closing the node would otherwise keep the loop going forever return *this; } @@ -428,7 +429,9 @@ bool shape_node::merge( shape_node &Shape ) { if( m_data.material != Shape.m_data.material - || m_data.lighting != Shape.m_data.lighting ) { + || m_data.lighting != Shape.m_data.lighting + || m_data.terrainfile != Shape.m_data.terrainfile ) { + // (shapes of different terrain files are kept apart, so the editor can tell which file the geometry comes from) // can't merge nodes with different appearance return false; } @@ -717,7 +720,9 @@ memory_node::deserialize( cParser &Input, node_data const &Nodedata ) { basic_node::basic_node( scene::node_data const &Nodedata ) : - m_name( Nodedata.name ) + m_name( Nodedata.name ), + m_layer( Nodedata.layer ), + m_instance( Nodedata.instance ) { uuid = UID::random(); node_type = Nodedata.type; diff --git a/scene/scenenode.h b/scene/scenenode.h index ca727c2c..48dad5b6 100644 --- a/scene/scenenode.h +++ b/scene/scenenode.h @@ -70,6 +70,8 @@ struct node_data { double range_max { std::numeric_limits::max() }; std::string name; std::string type; + layer_handle layer { null_handle }; // scenery layer the node is defined in, if any + instance_handle instance { 0 }; // include of an *.inc template which defines the node, if it isn't defined directly by a scenery layer file }; // holds unique piece of geometry, covered with single material @@ -77,6 +79,7 @@ class shape_node { friend class basic_region; // region might want to modify node content when it's being inserted + friend class terrain_file; // binary terrain files hold complete content of the nodes public: // types @@ -96,6 +99,8 @@ public: gfx::geometry_handle geometry { 0, 0 }; // relative origin-centered chunk of geometry held by gfx renderer std::vector vertices; // world space source data of the geometry gfx::userdata_array userdata; + // terrain file the shape comes from, see scene::terrain_file::reference_of(); 0: none, or not known (only the editor asks) + std::uint16_t terrainfile { 0 }; // methods: // sends content of the struct to provided stream void @@ -143,6 +148,15 @@ public: // replaces the renderable geometry handle (used by the editor when it re-uploads terrain geometry) void geometry( gfx::geometry_handle const &Handle ); + // set lighting (used by generated geometry painted with plain colour) + void + lighting( lighting_data const &Lighting ); + // sets whether the shape is drawn with the translucent geometry (used by generated geometry) + void + translucent( bool const Translucent ); + // notes the terrain file the shape comes from + void + source_file( std::uint16_t const Reference ) { m_data.terrainfile = Reference; } // data access shapenode_data const & data() const; @@ -174,6 +188,17 @@ void shape_node::geometry( gfx::geometry_handle const &Handle ) { m_data.geometry = Handle; } +// set lighting +inline +void +shape_node::lighting( lighting_data const &Lighting ) { + m_data.lighting = Lighting; +} +inline +void +shape_node::translucent( bool const Translucent ) { + m_data.translucent = Translucent; +} // data access inline shape_node::shapenode_data const & @@ -359,6 +384,13 @@ public: group( scene::group_handle Group ); scene::group_handle group() const; + void + layer( scene::layer_handle Layer ); + scene::layer_handle + layer() const; + // true if the node is defined by an *.inc template + bool + from_template() const { return m_instance != 0; } void mark_dirty() { m_dirty = true; } bool @@ -375,6 +407,12 @@ public: bool m_visible { true }; // visibility flag std::string m_name; bool m_dirty { false }; + bool m_layerhidden { false }; // the node was visible until the editor hid its scenery layer, or the include which defines it + bool m_preview { false }; // the node was made by the editor to show an include it placed or changed, and is replaced when the include changes + scene::layer_handle m_layer { null_handle }; // scenery layer this node belongs to, if any + // include of an *.inc template which defines the node; 0 if the node is defined directly by a scenery layer file. + // such nodes can't be rewritten one by one on scenery save. set only for scenery opened for editing + scene::instance_handle m_instance { 0 }; UID uuid; private: @@ -443,6 +481,18 @@ basic_node::group() const { return m_group; } +inline +void +basic_node::layer( scene::layer_handle Layer ) { + m_layer = Layer; +} + +inline +scene::layer_handle +basic_node::layer() const { + return m_layer; +} + } // scene //--------------------------------------------------------------------------- diff --git a/scene/scenenodegroups.cpp b/scene/scenenodegroups.cpp index 20ac524b..78128780 100644 --- a/scene/scenenodegroups.cpp +++ b/scene/scenenodegroups.cpp @@ -22,9 +22,20 @@ node_groups Groups; // requests creation of a new node group. returns: handle to the group scene::group_handle -node_groups::create() { +node_groups::create( bool const Separate ) { - m_activegroup.push( create_handle() ); + if( false == Separate ) { + m_activegroup.push( create_handle() ); + return handle(); + } + // a handle no other group has, see make() + auto group { std::max( 2, static_cast( m_groupmap.size() + 1 ) ) }; + while( m_groupmap.find( group ) != m_groupmap.end() ) { + ++group; + } + // NOTE: registered before the nodes come, so that a group nested in this one doesn't get the same handle + m_groupmap[ group ]; + m_activegroup.push( group ); return handle(); } @@ -107,6 +118,11 @@ node_groups::update_map() for (auto const &pair : m_groupmap) { auto const &group = pair.second; + // named models of the group, collected once the group turns out to hold a semaphore. + // nested groups are flattened, so a group can hold millions of nodes, most of them nameless scenery; + // going through all of them for each semaphore took minutes + std::vector namedmodels; + bool namedmodelscollected = false; for (basic_node *node : group.nodes) { std::string postfix { "_sem_mem" }; @@ -128,10 +144,17 @@ node_groups::update_map() } } - for (basic_node *node : group.nodes) - if (auto *model = dynamic_cast(node)) - if (model->name().starts_with(sem_name)) - sem_info->models.push_back(model); + if (false == namedmodelscollected) { + for (basic_node *groupnode : group.nodes) + if (false == groupnode->name().empty()) + if (auto *model = dynamic_cast(groupnode)) + namedmodels.push_back(model); + namedmodelscollected = true; + } + + for (auto *model : namedmodels) + if (model->name().starts_with(sem_name)) + sem_info->models.push_back(model); } if (Global.map_manualswitchcontrol) { @@ -239,12 +262,16 @@ void node_groups::insert( scene::group_handle const Group, scene::basic_node *Node ) { // TBD, TODO: automatically unregister the node from its current group? + auto const previousgroup { Node->group() }; Node->group( Group ); if( Group == null_handle ) { return; } auto &nodesequence { m_groupmap[ Group ].nodes }; - if( std::find( std::begin( nodesequence ), std::end( nodesequence ), Node ) == std::end( nodesequence ) ) { + // a node which wasn't a member of the group can't be on its list, which saves the search through the list. + // it matters as a group can be large, the root group of a scenery holds nearly all its nodes + if( previousgroup != Group + || std::find( std::begin( nodesequence ), std::end( nodesequence ), Node ) == std::end( nodesequence ) ) { // don't add the same node twice nodesequence.emplace_back( Node ); } @@ -255,12 +282,15 @@ void node_groups::insert( scene::group_handle const Group, basic_event *Event ) { // TBD, TODO: automatically unregister the event from its current group? + auto const previousgroup { Event->group() }; Event->group( Group ); if( Group == null_handle ) { return; } auto &eventsequence { m_groupmap[ Group ].events }; - if( std::find( std::begin( eventsequence ), std::end( eventsequence ), Event ) == std::end( eventsequence ) ) { + // same as with the nodes, the list is searched only for an event which already was a member of the group + if( previousgroup != Group + || std::find( std::begin( eventsequence ), std::end( eventsequence ), Event ) == std::end( eventsequence ) ) { // don't add the same node twice eventsequence.emplace_back( Event ); } @@ -315,6 +345,87 @@ node_groups::erase( group_map::const_iterator Group ) { m_groupmap.erase( Group ); } +group_handle +node_groups::make( std::vector const &Nodes ) { + + // a handle no other group has, past the first one, which the editor treats as no group at all + auto handle { std::max( 2, static_cast( m_groupmap.size() + 1 ) ) }; + while( m_groupmap.find( handle ) != m_groupmap.end() ) { + ++handle; + } + for( auto *node : Nodes ) { + leave( node ); + m_loose.erase( node ); + insert( handle, node ); + } + m_rewrite.insert( handle ); + return handle; +} + +void +node_groups::dissolve( scene::group_handle const Group ) { + + auto const lookup { m_groupmap.find( Group ) }; + if( lookup == m_groupmap.end() ) { return; } + for( auto *node : lookup->second.nodes ) { + m_loose.insert( node ); + } + m_rewrite.erase( Group ); + erase( lookup ); +} + +void +node_groups::remove( scene::basic_node *Node ) { + + leave( Node ); + m_loose.erase( Node ); +} + +void +node_groups::leave( scene::basic_node *Node ) { + + auto const group { Node->group() }; + Node->group( null_handle ); + auto const lookup { m_groupmap.find( group ) }; + if( lookup == m_groupmap.end() ) { return; } + auto &nodes { lookup->second.nodes }; + nodes.erase( std::remove( std::begin( nodes ), std::end( nodes ), Node ), std::end( nodes ) ); + if( nodes.empty() && lookup->second.events.empty() ) { + // a group made in the editor goes with its last node + m_rewrite.erase( group ); + m_groupmap.erase( lookup ); + } +} + +bool +node_groups::relocating( scene::basic_node const *Node ) const { + + return ( m_loose.count( Node ) != 0 ) || ( m_rewrite.count( Node->group() ) != 0 ); +} + +std::vector +node_groups::rewritten() const { + + return { std::begin( m_rewrite ), std::end( m_rewrite ) }; +} + +std::vector +node_groups::loose() const { + + std::vector nodes; + for( auto const *node : m_loose ) { + nodes.emplace_back( const_cast( node ) ); + } + return nodes; +} + +void +node_groups::saved() { + + m_rewrite.clear(); + m_loose.clear(); +} + // creates handle for a new group group_handle node_groups::create_handle() { diff --git a/scene/scenenodegroups.h b/scene/scenenodegroups.h index 350fa3d3..fb7ea95f 100644 --- a/scene/scenenodegroups.h +++ b/scene/scenenodegroups.h @@ -29,8 +29,9 @@ public: node_groups() = default; // methods // requests creation of a new node group. returns: handle to the group + // nested groups are flattened into the outer one, unless Separate: then the group stands by itself (group ... endgroup) group_handle - create(); + create( bool const Separate = false ); // indicates creation of current group ended. returns: handle to the parent group or null_handle if group stack is empty group_handle close(); @@ -50,6 +51,26 @@ public: scene::basic_group & group( scene::group_handle const Group ) { return m_groupmap[ Group ]; } + // groups made in the scenery editor. the nodes leave the groups they were in. returns: handle to the new group + group_handle + make( std::vector const &Nodes ); + // the nodes of the group are left on their own + void + dissolve( scene::group_handle const Group ); + // the node, about to be deleted, leaves its group + void + remove( scene::basic_node *Node ); + // groups made or dissolved in the editor are written anew when the scenery is saved: the definitions of their nodes are + // taken out of where they are, a group's written as one block between group and endgroup, a dissolved one's nodes each on its own + bool + relocating( scene::basic_node const *Node ) const; + std::vector + rewritten() const; + std::vector + loose() const; + // the scenery files match the groups now + void + saved(); // sends basic content of the class in legacy (text) format to provided stream void export_as_text( std::ostream &Output, bool const Dirty ) const; @@ -66,9 +87,14 @@ private: create_handle(); bool assign_cross_switch(map::track_switch&sw, std::string &sw_name, const std::string &id, size_t idx); + // the node leaves the group it's in + void + leave( scene::basic_node *Node ); // members group_map m_groupmap; // map of established node groups std::stack m_activegroup; // helper, group to be assigned to newly created nodes + std::set m_rewrite; // groups made in the editor, to be written as blocks on save + std::set m_loose; // nodes of groups dissolved in the editor, to be written on their own on save }; extern node_groups Groups; diff --git a/scene/sceneterrain.cpp b/scene/sceneterrain.cpp new file mode 100644 index 00000000..d0453f2b --- /dev/null +++ b/scene/sceneterrain.cpp @@ -0,0 +1,1167 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "scene/sceneterrain.h" + +#include "scene/scene.h" +#include "rendering/renderer.h" +#include "rendering/nullrenderer.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +/* +binary terrain file layout, all values little endian: + + uint32 file id ('EU07') + uint32 format id ('BTF1') + uint64 length of the source text file + uint32 checksum (crc-32) of the source text file + uint32 section size, uint32 cell size, uint32 number of sections along the side of the region + uint32 material count + uint32 section count + uint64 vertex count + material names, null-terminated + section directory, per section: + uint32 section index, uint64 offset of the section data in the file, uint32 size of the section data, float32 bounding radius of the section + section data, per section: + uint32 block id ('BTFS') + uint32 section index + uint32 shape count + per shape: + uint32 material index + uint32 index of the section cell the shape is located in + float64 squared minimum and maximum visibility range + float32 x12 diffuse, ambient and specular colours + float64 x3 centre, float32 bounding radius + uint32 vertex count + per vertex: float32 x3 position relative to the centre of the section, float32 x3 normal, float32 x2 texture coordinates + +the shapes are the ones the scenery loader would put in the section cells for the same text. where the loader would merge them +further into section-wide pieces depends on the materials, which are known only at run time, so that part is left for loading. +*/ + +namespace scene +{ + +// defined along with the scenery loader code which filters out legacy switch trackbeds +extern std::vector switchtrackbedtextures; + +// data shared by the pieces of a binary terrain file put to use +struct terrain_source +{ + struct material + { + material_handle handle{null_handle}; + bool translucent{false}; + bool clamps{false}; // texture coordinates are kept off the edges of the texture + bool clampt{false}; + bool skip{false}; // geometry using the material isn't loaded + }; + std::string file; + std::vector materials; + std::uint16_t reference{0}; // see terrain_file::reference_of() +}; + +namespace +{ + +std::uint32_t const EU07_FILEHEADER{MAKE_ID4('E', 'U', '0', '7')}; +std::uint32_t const EU07_FILEVERSION_TERRAIN{MAKE_ID4('B', 'T', 'F', '2')}; +std::uint32_t const EU07_TERRAINBLOCK{MAKE_ID4('B', 'T', 'F', 'S')}; +std::size_t const EU07_TERRAINVERTEXSIZE{8 * sizeof(float)}; + +struct file_header +{ + std::uint64_t sourcesize{0}; + std::uint32_t sourcechecksum{0}; + std::uint32_t materialcount{0}; + std::uint32_t sectioncount{0}; + std::uint64_t vertexcount{0}; + std::uint64_t extraoffset{0}; + std::uint32_t extrasize{0}; +}; + +// adds value to the data in the byte order of the file +template void put(std::string &Output, Type_ Value) +{ + char bytes[sizeof(Type_)]; + std::memcpy(bytes, &Value, sizeof(Type_)); + if (std::endian::native != std::endian::little) + { + std::reverse(std::begin(bytes), std::end(bytes)); + } + Output.append(bytes, sizeof(Type_)); +} + +// extracts values in the byte order of the file from a block of data, keeping track of its bounds +class data_reader +{ + public: + data_reader(char const *Data, std::size_t const Size) : m_data(Data), m_size(Size) {} + template Type_ get() + { + Type_ value{}; + if (false == has(sizeof(Type_))) + { + m_good = false; + m_position = m_size; + return value; + } + char bytes[sizeof(Type_)]; + std::memcpy(bytes, m_data + m_position, sizeof(Type_)); + if (std::endian::native != std::endian::little) + { + std::reverse(std::begin(bytes), std::end(bytes)); + } + std::memcpy(&value, bytes, sizeof(Type_)); + m_position += sizeof(Type_); + return value; + } + std::string get_string() + { + std::string text; + while (has(1) && m_data[m_position] != 0) + { + text += m_data[m_position++]; + } + if (false == has(1)) + { + m_good = false; + return text; + } + ++m_position; // terminator + return text; + } + // extracts a series of values at once. NOTE: the caller is expected to check the data is there + void get(float *Values, std::size_t const Count) + { + if (false == has(Count * sizeof(float))) + { + m_good = false; + m_position = m_size; + return; + } + if (std::endian::native == std::endian::little) + { + std::memcpy(Values, m_data + m_position, Count * sizeof(float)); + m_position += Count * sizeof(float); + } + else + { + for (std::size_t index{0}; index < Count; ++index) + { + Values[index] = get(); + } + } + } + void skip(std::size_t const Size) + { + if (false == has(Size)) + { + m_good = false; + m_position = m_size; + return; + } + m_position += Size; + } + bool has(std::size_t const Size) const + { + return Size <= m_size - m_position; + } + bool good() const + { + return m_good; + } + + private: + char const *m_data; + std::size_t m_size; + std::size_t m_position{0}; + bool m_good{true}; +}; + +// loads content of specified file into provided string. returns: true on success, false otherwise +bool read_file(std::string const &Filename, std::string &Data) +{ + std::ifstream input{Filename, std::ios::binary}; + if (input.fail()) + { + return false; + } + auto const size{input.rdbuf()->pubseekoff(0, std::ios_base::end)}; + input.rdbuf()->pubseekoff(0, std::ios_base::beg); + if (size < 0 || static_cast(size) >= Data.max_size()) + { + return false; + } + Data.resize(static_cast(size)); + input.read(Data.data(), static_cast(Data.size())); + return static_cast(std::max(0, input.gcount())) == Data.size(); +} + +// loads the fixed part of binary terrain file header. returns: true if the file can be used, false otherwise +bool read_header(std::istream &Input, file_header &Header) +{ + char bytes[60]; + Input.read(bytes, sizeof(bytes)); + if (Input.gcount() != static_cast(sizeof(bytes))) + { + return false; + } + data_reader data{bytes, sizeof(bytes)}; + if (data.get() != EU07_FILEHEADER || data.get() != EU07_FILEVERSION_TERRAIN) + { + // wrong file type + return false; + } + Header.sourcesize = data.get(); + Header.sourcechecksum = data.get(); + // the content is arranged for specific layout of the scene, and can't be used with any other + if (data.get() != static_cast(EU07_SECTIONSIZE) || data.get() != static_cast(EU07_CELLSIZE) || + data.get() != static_cast(EU07_REGIONSIDESECTIONCOUNT)) + { + return false; + } + Header.materialcount = data.get(); + Header.sectioncount = data.get(); + Header.vertexcount = data.get(); + Header.extraoffset = data.get(); + Header.extrasize = data.get(); + return data.good(); +} + +bool read_header(std::string const &Filename, file_header &Header) +{ + std::ifstream input{Filename, std::ios::binary}; + return (false == input.fail()) && read_header(input, Header); +} + +// stand-in for the renderer used during conversion of text terrain. the conversion doesn't need +// anything of the materials but a way to tell them apart, so it goes by their names alone +class conversion_renderer : public null_renderer +{ + public: + material_handle Fetch_Material(std::string const &Filename, bool const Loadnow = true) override + { + // the names are brought to the form the material manager would look them up by + auto name{Filename}; + if (name.find("make:") != 0 && name.find("internal_src:") != 0) + { + if (contains(name, '|')) + { + name.erase(name.find('|')); + } + erase_extension(name); + replace_slashes(name); + erase_leading_slashes(name); + } + auto const lookup{m_lookup.find(name)}; + if (lookup != m_lookup.end()) + { + return lookup->second; + } + m_materials.emplace_back(std::make_unique()); + m_materials.back()->name = name; + m_names.emplace_back(name); + m_lookup.emplace(name, static_cast(m_materials.size())); + return static_cast(m_materials.size()); + } + IMaterial const *Material(material_handle const Material) const override + { + return (Material > 0 && static_cast(Material) <= m_materials.size() ? m_materials[Material - 1].get() : IMaterial::null_material()); + } + // names of the materials, in the order of their handles, starting with handle 1 + std::vector const &names() const + { + return m_names; + } + + private: + std::vector> m_materials; + std::vector m_names; + std::unordered_map m_lookup; +}; + +// puts the stand-in renderer in place of the real one for as long as the object lives. +// the conversion is done by the code which loads scenery geometry from text, and that code asks the global renderer for the materials +class conversion_scope +{ + public: + conversion_scope() : m_trackbeds(Global.CreateSwitchTrackbeds) + { + auto renderer{std::make_unique()}; + m_materials = renderer.get(); + m_renderer = std::move(renderer); + std::swap(GfxRenderer, m_renderer); + // the binary file holds everything the text does. geometry of legacy switch trackbeds is filtered out on load, as that depends on the settings + Global.CreateSwitchTrackbeds = false; + } + ~conversion_scope() + { + std::swap(GfxRenderer, m_renderer); + Global.CreateSwitchTrackbeds = m_trackbeds; + } + conversion_scope(conversion_scope const &) = delete; + conversion_scope &operator=(conversion_scope const &) = delete; + std::vector const &materials() const + { + return m_materials->names(); + } + + private: + std::unique_ptr m_renderer; + conversion_renderer *m_materials{nullptr}; + bool m_trackbeds; +}; + +// collects what the loader needs to know about the material with specified name +terrain_source::material fetch_material(std::string const &Name) +{ + terrain_source::material material; + material.handle = GfxRenderer->Fetch_Material(Name); + // the appearance is determined from the assigned diffuse texture, the same way it's done for the geometry loaded from text + auto const texturehandle = material.handle != null_handle ? GfxRenderer->Material(material.handle)->GetTexture(0) : null_handle; + auto const &texture = texturehandle ? GfxRenderer->Texture(texturehandle) : *ITexture::null_texture(); + material.clamps = texturehandle ? contains(texture.get_traits(), 's') : false; + material.clampt = texturehandle ? contains(texture.get_traits(), 't') : false; + material.translucent = (texturehandle != null_handle) && contains(texture.get_name(), '@') && (true == texture.get_has_alpha()); + if (Global.CreateSwitchTrackbeds) + { + // geometry with blacklisted texture, part of old switch trackbed + auto const materialname{GfxRenderer->Material(material.handle)->GetName()}; + material.skip = + std::any_of(std::begin(switchtrackbedtextures), std::end(switchtrackbedtextures), [&](std::string const &Texture) { return contains(materialname, Texture); }); + } + return material; +} + +} // namespace + +// calculates checksum (crc-32) of provided data +std::uint32_t terrain_file::checksum(char const *Data, std::size_t Size) +{ + // lookup for 8 bytes of input at a time + static auto const tables{[]() { + std::array, 8> lookup{}; + for (std::uint32_t index{0}; index < 256; ++index) + { + auto value{index}; + for (auto bit{0}; bit < 8; ++bit) + { + value = (value & 1 ? 0xEDB88320u ^ (value >> 1) : value >> 1); + } + lookup[0][index] = value; + } + for (std::uint32_t index{0}; index < 256; ++index) + { + for (std::size_t table{1}; table < lookup.size(); ++table) + { + lookup[table][index] = (lookup[table - 1][index] >> 8) ^ lookup[0][lookup[table - 1][index] & 0xFF]; + } + } + return lookup; + }()}; + + auto const *data{reinterpret_cast(Data)}; + std::uint32_t crc{0xFFFFFFFFu}; + while (Size >= 8) + { + auto const low{crc ^ (static_cast(data[0]) | (static_cast(data[1]) << 8) | (static_cast(data[2]) << 16) | (static_cast(data[3]) << 24))}; + crc = tables[7][low & 0xFF] ^ tables[6][(low >> 8) & 0xFF] ^ tables[5][(low >> 16) & 0xFF] ^ tables[4][low >> 24] ^ tables[3][data[4]] ^ tables[2][data[5]] ^ tables[1][data[6]] ^ tables[0][data[7]]; + data += 8; + Size -= 8; + } + while (Size-- > 0) + { + crc = tables[0][(crc ^ *data++) & 0xFF] ^ (crc >> 8); + } + return crc ^ 0xFFFFFFFFu; +} + +// converts provided content of a text terrain file to binary format and stores it in specified file. returns: true on success, false otherwise +bool terrain_file::write(std::string const &Text, std::string const &Binaryfile, std::string &Message) +{ + conversion_scope scope; + auto region{std::make_unique()}; + scratch_data scratchpad{}; + std::string extra; + { + std::size_t begin{0}; + while (begin < Text.size()) + { + auto end{Text.find('\n', begin)}; + end = (end == std::string::npos ? Text.size() : end + 1); + auto const first{Text.find_first_not_of(" \t", begin)}; + if (first < end && Text.compare(first, 3, "//$") == 0) + { + auto line{Text.substr(first, end - first)}; + while (false == line.empty() && (line.back() == '\n' || line.back() == '\r')) + line.pop_back(); + extra += line + "\n"; + } + begin = end; + } + } + + { + cParser input{Text, cParser::buffer_TEXT}; + // content of the files the text would include isn't covered by its checksum, so there can't be any + input.expandIncludes = false; + auto token{input.getToken()}; + while (false == token.empty()) + { + if (token == "node") + { + auto const begin{static_cast(input.TokenBegin())}; + node_data nodedata; + input.getTokens(4); + input >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type; + if (nodedata.type == "sweep") + { + auto rest{input.getToken()}; + while (false == rest.empty() && rest != "endsweep") + rest = input.getToken(); + auto const end{static_cast(input.TokenEnd())}; + auto const offset{scratchpad.location.offset.empty() ? glm::dvec3{0.0} : scratchpad.location.offset.top()}; + auto const placed{offset != glm::dvec3{0.0}}; + if (placed) + extra += "origin " + std::to_string(offset.x) + ' ' + std::to_string(offset.y) + ' ' + std::to_string(offset.z) + "\n"; + extra += Text.substr(begin, end - begin) + "\n"; + if (placed) + extra += "endorigin\n"; + token = input.getToken(); + continue; + } + if (nodedata.type != "triangles" && nodedata.type != "triangle_strip" && nodedata.type != "triangle_fan") + { + Message = "node of type \"" + nodedata.type + "\" (line " + std::to_string(input.Line()) + ") can't be a part of binary terrain"; + return false; + } + auto shape{shape_node().import(input, nodedata)}; + // the shapes are all sent to the section cells, which groups them the way it'd be done for translucent geometry. + // whether they can be merged into section-wide pieces instead is decided when the file is loaded, see the layout notes + shape.m_data.translucent = true; + region->insert(shape, scratchpad, true); + } + else if (token == "origin") + { + glm::dvec3 offset; + input.getTokens(3); + input >> offset.x >> offset.y >> offset.z; + scratchpad.location.offset.emplace(offset + (scratchpad.location.offset.empty() ? glm::dvec3() : scratchpad.location.offset.top())); + } + else if (token == "endorigin") + { + if (false == scratchpad.location.offset.empty()) + { + scratchpad.location.offset.pop(); + } + } + else if (token == "rotate") + { + input.getTokens(3); + input >> scratchpad.location.rotation.x >> scratchpad.location.rotation.y >> scratchpad.location.rotation.z; + } + else + { + Message = "\"" + token + "\" (line " + std::to_string(input.Line()) + ") can't be a part of binary terrain"; + return false; + } + token = input.getToken(); + } + } + + // sections holding any geometry + std::vector sections; + for (std::size_t index{0}; index < region->m_sections.size(); ++index) + { + auto const *section{region->m_sections[index]}; + if (section == nullptr) + { + continue; + } + if (std::any_of(std::begin(section->m_cells), std::end(section->m_cells), [](basic_cell const &Cell) { return false == Cell.m_shapestranslucent.empty(); })) + { + sections.emplace_back(static_cast(index)); + } + } + + auto const temporaryfile{Binaryfile + ".tmp"}; + std::uint64_t shapecount{0}; + std::uint64_t vertexcount{0}; + { + std::ofstream output{temporaryfile, std::ios::binary | std::ios::trunc}; + if (output.fail()) + { + Message = "can't write to file \"" + temporaryfile + "\""; + return false; + } + + std::string data; + put(data, EU07_FILEHEADER); + put(data, EU07_FILEVERSION_TERRAIN); + put(data, static_cast(Text.size())); + put(data, terrain_file::checksum(Text.data(), Text.size())); + put(data, static_cast(EU07_SECTIONSIZE)); + put(data, static_cast(EU07_CELLSIZE)); + put(data, static_cast(EU07_REGIONSIDESECTIONCOUNT)); + put(data, static_cast(scope.materials().size())); + put(data, static_cast(sections.size())); + auto const vertexcountposition{data.size()}; + put(data, vertexcount); // filled in when the count is known + auto const extraposition{data.size()}; + put(data, std::uint64_t{0}); + put(data, std::uint32_t{0}); + for (auto const &material : scope.materials()) + { + data.append(material.c_str(), material.size() + 1); + } + auto const directoryposition{data.size()}; + // the directory is written once the section data is in place and its location known + std::size_t const directoryentrysize{sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)}; + data.append(sections.size() * directoryentrysize, '\0'); + output.write(data.data(), static_cast(data.size())); + std::uint64_t offset{data.size()}; + + std::string directory; + for (auto const sectionindex : sections) + { + auto *section{region->m_sections[sectionindex]}; + data.clear(); + put(data, EU07_TERRAINBLOCK); + put(data, sectionindex); + std::uint32_t sectionshapecount{0}; + for (auto const &cell : section->m_cells) + { + sectionshapecount += static_cast(cell.m_shapestranslucent.size()); + } + put(data, sectionshapecount); + for (std::size_t cellindex{0}; cellindex < section->m_cells.size(); ++cellindex) + { + for (auto &shape : section->m_cells[cellindex].m_shapestranslucent) + { + auto const radius{shape.radius()}; + auto const &shapedata{shape.data()}; + put(data, static_cast(shapedata.material - 1)); + put(data, static_cast(cellindex)); + put(data, shapedata.rangesquared_min); + put(data, shapedata.rangesquared_max); + for (auto const &colour : {shapedata.lighting.diffuse, shapedata.lighting.ambient, shapedata.lighting.specular}) + { + put(data, colour.r); + put(data, colour.g); + put(data, colour.b); + put(data, colour.a); + } + put(data, shapedata.area.center.x); + put(data, shapedata.area.center.y); + put(data, shapedata.area.center.z); + put(data, radius); + put(data, static_cast(shapedata.vertices.size())); + for (auto const &vertex : shapedata.vertices) + { + glm::vec3 const position{vertex.position - section->m_area.center}; + put(data, position.x); + put(data, position.y); + put(data, position.z); + put(data, vertex.normal.x); + put(data, vertex.normal.y); + put(data, vertex.normal.z); + put(data, vertex.texture.x); + put(data, vertex.texture.y); + } + ++shapecount; + vertexcount += shapedata.vertices.size(); + } + } + if (data.size() > std::numeric_limits::max()) + { + Message = "too much geometry in a single section of the scene"; + output.close(); + std::error_code error; + std::filesystem::remove(temporaryfile, error); + return false; + } + output.write(data.data(), static_cast(data.size())); + put(directory, sectionindex); + put(directory, offset); + put(directory, static_cast(data.size())); + put(directory, section->m_area.radius); + offset += data.size(); + } + output.write(extra.data(), static_cast(extra.size())); + data.clear(); + put(data, static_cast(offset)); + put(data, static_cast(extra.size())); + output.seekp(static_cast(extraposition)); + output.write(data.data(), static_cast(data.size())); + // fill in the parts left for later + output.seekp(static_cast(directoryposition)); + output.write(directory.data(), static_cast(directory.size())); + data.clear(); + put(data, vertexcount); + output.seekp(static_cast(vertexcountposition)); + output.write(data.data(), static_cast(data.size())); + output.flush(); + if (output.fail()) + { + Message = "can't write to file \"" + temporaryfile + "\""; + output.close(); + std::error_code error; + std::filesystem::remove(temporaryfile, error); + return false; + } + } + // the finished file replaces the previous version in one go, so an interrupted conversion doesn't leave a broken one behind + std::error_code error; + std::filesystem::rename(temporaryfile, Binaryfile, error); + if (error) + { + Message = "can't write to file \"" + Binaryfile + "\""; + std::filesystem::remove(temporaryfile, error); + return false; + } + + Message = std::to_string(sections.size()) + " section(s), " + std::to_string(shapecount) + " shape(s), " + std::to_string(vertexcount / 3) + " triangle(s)"; + return true; +} + +// converts specified text terrain file to binary format. returns: true on success, false otherwise +bool terrain_file::convert(std::string const &Textfile, std::string const &Binaryfile, std::string *Message) +{ + std::string message; + std::string text; + auto result{false}; + if (read_file(Textfile, text)) + { + result = write(text, Binaryfile, message); + } + else + { + message = "can't read the file"; + } + if (Message != nullptr) + { + *Message = message; + } + return result; +} + +// makes sure binary version of specified text terrain file is in place and matches the text, as far as the settings ask for it +terrain_file::state terrain_file::prepare(std::string const &Textfile, std::string const &Binaryfile) +{ + file_header header; + auto const binarypresent{read_header(Binaryfile, header)}; + + std::error_code error; + auto const textsize{std::filesystem::file_size(Textfile, error)}; + if (error) + { + // with no text to check the binary file against it's taken as it is + return (binarypresent ? state::binary : state::missing); + } + + // how far the check goes is up to the settings (file.binary.terrain.checksum): none at all, length of the text, or its length and checksum + if (binarypresent && Global.file_binary_terrain_checksum <= 0) + { + return state::binary; + } + std::string text; + auto textloaded{false}; + if (binarypresent && (header.sourcesize == textsize)) + { + if (Global.file_binary_terrain_checksum == 1) + { + // NOTE: this takes a text changed without a change of its length for the one the binary file was made from + return state::binary; + } + // the length is the same; the checksum tells whether the content is, too + textloaded = read_file(Textfile, text); + if (textloaded && (text.size() == header.sourcesize) && (checksum(text.data(), text.size()) == header.sourcechecksum)) + { + return state::binary; + } + } + if (false == textloaded && false == read_file(Textfile, text)) + { + ErrorLog("Bad file: can't read terrain file \"" + Textfile + "\""); + // NOTE: binary file which doesn't match the text isn't used, the scenery is expected to show what its files say + return state::missing; + } + + WriteLog("Terrain file \"" + Textfile + "\": " + (binarypresent ? "binary version doesn't match the text" : "no usable binary version found") + ", converting..."); + std::string message; + if (write(text, Binaryfile, message)) + { + WriteLog("Terrain file \"" + Textfile + "\": conversion done, " + message); + return state::binary; + } + ErrorLog("Bad file: terrain file \"" + Textfile + "\" wasn't converted to binary format, " + message + ". The file will be loaded as text"); + if (binarypresent) + { + // the binary file left behind is of no use, and would be taken for the real thing if the text went missing + std::filesystem::remove(Binaryfile, error); + } + return state::text; +} + +std::string terrain_file::extra(std::string const &Binaryfile) +{ + std::ifstream input{Binaryfile, std::ios::binary}; + file_header header; + if (input.fail() || false == read_header(input, header) || header.extrasize == 0) + { + return {}; + } + std::string text(header.extrasize, '\0'); + input.seekg(static_cast(header.extraoffset)); + input.read(text.data(), static_cast(text.size())); + if (static_cast(std::max(0, input.gcount())) != text.size()) + { + ErrorLog("Bad file: \"" + Binaryfile + "\" is damaged, the editor data it holds can't be read"); + return {}; + } + return text; +} + +std::vector &terrain_file::references() +{ + static std::vector references; + return references; +} + +std::uint16_t terrain_file::reference_of(std::string const &File) +{ + auto file{File}; + replace_slashes(file); + auto const &list{references()}; + for (std::size_t index{0}; index < list.size() && index < std::numeric_limits::max(); ++index) + { + auto const base{Global.asCurrentSceneryPath + list[index].file}; + if (file.ends_with(base + ".txtf") || file.ends_with(base + ".btf")) + { + return static_cast(index + 1); + } + } + return 0; +} + +namespace +{ + +// world position of a vertex of a shape placed with specified rotation and offset, the way basic_region::insert() does it +glm::dvec3 place_vertex(glm::dvec3 Position, glm::vec3 const &Rotation, glm::dvec3 const &Offset) +{ + if (Rotation != glm::vec3(0.f)) + { + auto const rotation{glm::radians(glm::dvec3{Rotation})}; + Position = glm::rotateZ(Position, rotation.z); + Position = glm::rotateX(Position, rotation.x); + Position = glm::rotateY(Position, rotation.y); + } + return Position + Offset; +} + +bool read_text_triangles(std::string const &Text, terrain_reference const &Reference, terrain_file::triangle_visitor const &Visit, std::string &Message) +{ + conversion_scope scope; + std::stack offsets; + offsets.push(Reference.offset); + auto rotation{Reference.rotation}; + std::size_t count{0}; + cParser input{Text, cParser::buffer_TEXT}; + input.expandIncludes = false; + auto token{input.getToken()}; + while (false == token.empty()) + { + if (token == "node") + { + node_data nodedata; + input.getTokens(4); + input >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type; + if (nodedata.type == "triangles" || nodedata.type == "triangle_strip" || nodedata.type == "triangle_fan") + { + auto const shape{shape_node().import(input, nodedata)}; + auto const &data{shape.data()}; + auto const *material{GfxRenderer->Material(data.material)}; + auto const name{material != nullptr ? material->GetName() : std::string{}}; + for (std::size_t index{0}; index + 2 < data.vertices.size(); index += 3) + { + Visit(name, {place_vertex(data.vertices[index].position, rotation, offsets.top()), place_vertex(data.vertices[index + 1].position, rotation, offsets.top()), + place_vertex(data.vertices[index + 2].position, rotation, offsets.top())}); + ++count; + } + } + else + { + // anything else the file holds (the editor keeps sweeps there) is of no interest here + auto const end{"end" + nodedata.type}; + auto rest{input.getToken()}; + while (false == rest.empty() && rest != end) + rest = input.getToken(); + } + } + else if (token == "origin") + { + glm::dvec3 offset; + input.getTokens(3); + input >> offset.x >> offset.y >> offset.z; + offsets.push(offset + offsets.top()); + } + else if (token == "endorigin") + { + if (offsets.size() > 1) + { + offsets.pop(); + } + } + else if (token == "rotate") + { + input.getTokens(3); + input >> rotation.x >> rotation.y >> rotation.z; + } + token = input.getToken(); + } + Message = std::to_string(count) + " triangle(s)"; + return true; +} + +bool read_binary_triangles(std::string const &Binaryfile, terrain_file::triangle_visitor const &Visit, std::string &Message) +{ + std::string content; + if (false == read_file(Binaryfile, content)) + { + Message = "can't read the file"; + return false; + } + std::istringstream stream{content}; + file_header header; + if (false == read_header(stream, header)) + { + Message = "the file is of either unrecognized type or version"; + return false; + } + std::vector materials; + for (std::uint32_t index{0}; index < header.materialcount; ++index) + { + std::string name; + if (false == static_cast(std::getline(stream, name, '\0'))) + { + Message = "the file is damaged"; + return false; + } + materials.emplace_back(std::move(name)); + } + auto const directorystart{static_cast(stream.tellg())}; + std::size_t const entrysize{sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)}; + if (directorystart > content.size() || content.size() - directorystart < header.sectioncount * entrysize) + { + Message = "the file is damaged"; + return false; + } + data_reader directory{content.data() + directorystart, header.sectioncount * entrysize}; + std::size_t count{0}; + for (std::uint32_t entry{0}; entry < header.sectioncount; ++entry) + { + auto const sectionindex{directory.get()}; + auto const offset{directory.get()}; + auto const size{directory.get()}; + directory.get(); + if (false == directory.good() || offset > content.size() || content.size() - offset < size) + { + Message = "the file is damaged"; + return false; + } + // the vertices are relative to the centre of their section, see attach() + auto const column{static_cast(sectionindex % EU07_REGIONSIDESECTIONCOUNT)}; + auto const row{static_cast(sectionindex / EU07_REGIONSIDESECTIONCOUNT)}; + auto const centeroffset{-(EU07_REGIONSIDESECTIONCOUNT / 2 * EU07_SECTIONSIZE) + EU07_SECTIONSIZE / 2}; + glm::dvec3 const centre{static_cast(centeroffset + column * EU07_SECTIONSIZE), 0.0, static_cast(centeroffset + row * EU07_SECTIONSIZE)}; + data_reader reader{content.data() + offset, size}; + if (reader.get() != EU07_TERRAINBLOCK) + { + Message = "the file is damaged"; + return false; + } + reader.get(); // section index + auto shapecount{reader.get()}; + while (shapecount-- > 0 && reader.good()) + { + auto const materialindex{reader.get()}; + // cell, ranges, colours, centre and radius + reader.skip(sizeof(std::uint32_t) + 2 * sizeof(double) + 12 * sizeof(float) + 3 * sizeof(double) + sizeof(float)); + auto const vertexcount{reader.get()}; + if (false == reader.good() || false == reader.has(static_cast(vertexcount) * EU07_TERRAINVERTEXSIZE) || materialindex >= materials.size()) + { + Message = "the file is damaged"; + return false; + } + std::array triangle; + for (std::uint32_t vertex{0}; vertex < vertexcount; ++vertex) + { + float values[8]; + reader.get(values, 8); + triangle[vertex % 3] = centre + glm::dvec3{values[0], values[1], values[2]}; + if (vertex % 3 == 2) + { + Visit(materials[materialindex], triangle); + ++count; + } + } + } + if (false == reader.good()) + { + Message = "the file is damaged"; + return false; + } + } + Message = std::to_string(count) + " triangle(s)"; + return true; +} + +} // namespace + +bool terrain_file::read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message, bool *Text) +{ + auto const textfile{Global.asCurrentSceneryPath + Reference.file + ".txtf"}; + auto const binaryfile{Global.asCurrentSceneryPath + Reference.file + ".btf"}; + std::string message; + auto result{false}; + auto const fromtext{FileExists(textfile)}; + if (fromtext) + { + std::string text; + result = read_file(textfile, text) && read_text_triangles(text, Reference, Visit, message); + if (message.empty()) + message = "can't read the file"; + } + else if (FileExists(binaryfile)) + { + // the binary file holds the geometry where the text alone puts it; it stands in for files placed without offset or rotation only + result = read_binary_triangles(binaryfile, Visit, message); + } + else + { + message = "the file is missing"; + } + if (Message != nullptr) + *Message = message; + if (Text != nullptr) + *Text = fromtext; + return result; +} + +// makes content of specified binary terrain file a part of provided region +bool terrain_file::attach(std::string const &Binaryfile, basic_region &Region, bool Deferred) +{ + std::ifstream input{Binaryfile, std::ios::binary}; + file_header header; + if (input.fail() || false == read_header(input, header)) + { + ErrorLog("Bad file: \"" + Binaryfile + "\" is of either unrecognized type or version"); + return false; + } + auto source{std::make_shared()}; + source->file = Binaryfile; + source->reference = reference_of(Binaryfile); + source->materials.reserve(header.materialcount); + for (std::uint32_t index{0}; index < header.materialcount; ++index) + { + std::string name; + if (false == static_cast(std::getline(input, name, '\0'))) + { + break; + } + source->materials.emplace_back(fetch_material(name)); + } + + std::string data(header.sectioncount * (sizeof(std::uint32_t) + sizeof(std::uint64_t) + sizeof(std::uint32_t) + sizeof(float)), '\0'); + input.read(data.data(), static_cast(data.size())); + data_reader reader{data.data(), static_cast(std::max(0, input.gcount()))}; + + std::uint32_t sectioncount{0}; + for (std::uint32_t index{0}; index < header.sectioncount && reader.good(); ++index) + { + auto const sectionindex{reader.get()}; + terrain_block block; + block.source = source; + block.offset = reader.get(); + block.size = reader.get(); + auto const radius{reader.get()}; + if (false == reader.good() || sectionindex >= Region.m_sections.size()) + { + break; + } + // location of the section follows from its index, see basic_region::section() + auto const column{static_cast(sectionindex % EU07_REGIONSIDESECTIONCOUNT)}; + auto const row{static_cast(sectionindex / EU07_REGIONSIDESECTIONCOUNT)}; + auto const centeroffset{-(EU07_REGIONSIDESECTIONCOUNT / 2 * EU07_SECTIONSIZE) + EU07_SECTIONSIZE / 2}; + auto §ion{Region.section(glm::dvec3{static_cast(centeroffset + column * EU07_SECTIONSIZE), 0.0, static_cast(centeroffset + row * EU07_SECTIONSIZE)})}; + // the section has to be seen as reaching as far as its geometry does, before that geometry is loaded + section.m_area.radius = std::max(section.m_area.radius, radius); + section.m_terrain.emplace_back(block); + if (false == Deferred) + { + section.load_terrain(); + } + ++sectioncount; + } + if (false == reader.good() || sectioncount != header.sectioncount || source->materials.size() != header.materialcount) + { + ErrorLog("Bad file: \"" + Binaryfile + "\" is damaged, part of its content may be missing"); + } + WriteLog("Terrain file \"" + Binaryfile + "\": " + std::to_string(sectioncount) + " section(s), " + std::to_string(header.vertexcount / 3) + " triangle(s)" + + (Deferred ? ", loaded when needed" : "")); + return true; +} + +// loads specified piece of a binary terrain file into provided section +void terrain_file::load(basic_section &Section, terrain_block const &Block) +{ + if (Block.source == nullptr) + { + return; + } + auto const &source{*Block.source}; + + std::string data(Block.size, '\0'); + { + std::ifstream input{source.file, std::ios::binary}; + input.seekg(static_cast(Block.offset)); + input.read(data.data(), static_cast(data.size())); + if (input.fail() || static_cast(std::max(0, input.gcount())) != data.size()) + { + ErrorLog("Bad file: failed to load terrain section from \"" + source.file + "\""); + return; + } + } + data_reader reader{data.data(), data.size()}; + if (reader.get() != EU07_TERRAINBLOCK) + { + // the file isn't what it was when its directory was read + ErrorLog("Bad file: failed to load terrain section from \"" + source.file + "\", the file was changed"); + return; + } + reader.get(); // section index + auto shapecount{reader.get()}; + + // large pieces of opaque geometry are held by the section itself, merged by their appearance. see basic_section::insert() + std::vector sectionshapes; + auto const place = [&](shape_node &Shape, std::vector &Shapes, glm::dvec3 const &Origin) { + Shape.origin(Origin); + if (true == Section.m_geometrycreated) + { + // the section was put to use already, it won't prepare the geometry on its own + Shape.create_geometry(Section.m_geometrybank); + } + Shapes.emplace_back(std::move(Shape)); + }; + + while (shapecount-- > 0 && reader.good()) + { + auto const materialindex{reader.get()}; + auto const cellindex{reader.get()}; + shape_node shape; + auto &shapedata{shape.m_data}; + shapedata.rangesquared_min = reader.get(); + shapedata.rangesquared_max = reader.get(); + for (auto *colour : {&shapedata.lighting.diffuse, &shapedata.lighting.ambient, &shapedata.lighting.specular}) + { + colour->r = reader.get(); + colour->g = reader.get(); + colour->b = reader.get(); + colour->a = reader.get(); + } + shapedata.area.center.x = reader.get(); + shapedata.area.center.y = reader.get(); + shapedata.area.center.z = reader.get(); + shapedata.area.radius = reader.get(); + auto const vertexcount{reader.get()}; + if (false == reader.good() || false == reader.has(static_cast(vertexcount) * EU07_TERRAINVERTEXSIZE) || materialindex >= source.materials.size() || + cellindex >= Section.m_cells.size()) + { + ErrorLog("Bad file: terrain section data in \"" + source.file + "\" is damaged"); + break; + } + auto const &material{source.materials[materialindex]}; + if (true == material.skip) + { + reader.skip(static_cast(vertexcount) * EU07_TERRAINVERTEXSIZE); + continue; + } + shapedata.material = material.handle; + shapedata.translucent = material.translucent; + shapedata.terrainfile = source.reference; + shapedata.vertices.resize(vertexcount); + for (auto &vertex : shapedata.vertices) + { + float values[8]; + reader.get(values, 8); + vertex.position = Section.m_area.center + glm::dvec3{values[0], values[1], values[2]}; + vertex.normal = glm::vec3{values[3], values[4], values[5]}; + vertex.texture = glm::vec2{values[6], values[7]}; + // clamp texture coordinates if texture wrapping is off + // NOTE: for the text this is done before a large triangle gets divided along the section borders, here it comes after. + // the coordinates of the vertices added by the division can thus differ, by the clamp margin for a texture mapped the way clamping expects + if (true == material.clamps) + { + vertex.texture.s = std::clamp(vertex.texture.s, 0.001f, 0.999f); + } + if (true == material.clampt) + { + vertex.texture.t = std::clamp(vertex.texture.t, 0.001f, 0.999f); + } + } + + if (true == shapedata.translucent || shapedata.rangesquared_max <= 90000.0 || shapedata.rangesquared_min > 0.0) + { + // small, translucent or not always visible shapes are placed in the sub-cells + auto &cell{Section.m_cells[cellindex]}; + cell.m_active = true; + cell.m_area.radius = std::max(cell.m_area.radius, static_cast(glm::length(cell.m_area.center - shapedata.area.center) + shapedata.area.radius)); + place(shape, (shapedata.translucent ? cell.m_shapestranslucent : cell.m_shapesopaque), cell.m_area.center); + } + else + { + auto merged{false}; + for (auto &target : sectionshapes) + { + if (true == target.merge(shape)) + { + merged = true; + break; + } + } + if (false == merged) + { + sectionshapes.emplace_back(std::move(shape)); + } + } + } + for (auto &shape : sectionshapes) + { + place(shape, Section.m_shapes, Section.m_area.center); + } +} + +} // namespace scene + +//--------------------------------------------------------------------------- diff --git a/scene/sceneterrain.h b/scene/sceneterrain.h new file mode 100644 index 00000000..7291ed92 --- /dev/null +++ b/scene/sceneterrain.h @@ -0,0 +1,79 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace scene +{ + +class basic_region; +class basic_section; +struct terrain_block; + +// binary terrain file (.btf): static geometry of a text terrain file (.txtf), arranged by the sections +// of the scene so each can be loaded on its own, when it comes into use. +// the binary file records length and checksum of the text it was made from, and is made anew when these don't match +// terrain file named by a terrain directive of the scenery, with the placement it was loaded with +struct terrain_reference +{ + std::string file; // name relative to the scenery folder, without extension + glm::dvec3 offset{0.0}; + glm::vec3 rotation{0.f}; +}; + +class terrain_file +{ + public: + // types + // receives a triangle of a terrain file: name of its material, the vertices in the world + using triangle_visitor = std::function const &Triangle)>; + // what a scenery is left with for a terrain file it refers to + enum class state + { + binary, // binary file is in place and up to date + text, // binary file can't be had, the text file has to be loaded the way any other scenery file is + missing // there's nothing to load + }; + // methods + // calculates checksum (crc-32) of provided data + static std::uint32_t checksum(char const *Data, std::size_t Size); + // converts specified text terrain file to binary format. returns: true on success, false otherwise. + // description of the result or of the failure is stored in Message, if provided + static bool convert(std::string const &Textfile, std::string const &Binaryfile, std::string *Message = nullptr); + // makes sure binary version of specified text terrain file is in place and matches the text, as far as the settings ask for it + static state prepare(std::string const &Textfile, std::string const &Binaryfile); + // makes content of specified binary terrain file a part of provided region. the geometry is loaded right away, + // or left for the sections of the region to load when they're about to be used. returns: true on success, false otherwise + static bool attach(std::string const &Binaryfile, basic_region &Region, bool Deferred); + // loads specified piece of a binary terrain file into provided section + static void load(basic_section &Section, terrain_block const &Block); + static std::string extra(std::string const &Binaryfile); + // terrain files used by the scenery being loaded or shown, in the order the directives named them + static std::vector &references(); + // number (counted from 1) of the reference whose text or binary file is the specified file, 0 if there's none + static std::uint16_t reference_of(std::string const &File); + // goes through the triangles of the terrain file of specified reference, read from the text file if it's there, + // otherwise from the binary one. returns: true on success, Message describing what was read or the failure + static bool read_triangles(terrain_reference const &Reference, triangle_visitor const &Visit, std::string *Message = nullptr, bool *Text = nullptr); + + private: + // converts provided content of a text terrain file to binary format and stores it in specified file. returns: true on success, false otherwise + static bool write(std::string const &Text, std::string const &Binaryfile, std::string &Message); +}; + +} // namespace scene + +//--------------------------------------------------------------------------- diff --git a/shaders/mat_terrain.frag b/shaders/mat_terrain.frag new file mode 100644 index 00000000..77519e1c --- /dev/null +++ b/shaders/mat_terrain.frag @@ -0,0 +1,139 @@ +in vec3 f_normal; +in vec2 f_coord; +in vec4 f_pos; +in mat3 f_tbn; + +in vec4 f_clip_pos; +in vec4 f_clip_future_pos; + +#include + +layout(location = 0) out vec4 out_color; +#if MOTIONBLUR_ENABLED +layout(location = 1) out vec4 out_motion; +#endif + +// heightmap terrain: up to 8 materials (layers) painted over a chunk, blended by their weights. +// the textures of all layers of the terrain are kept in texture arrays; the table of the chunk tells which layers it has: +// row 0, per layer: index in the arrays, 1 / metres the textures repeat at, has normal map, has specular/gloss map +// row 1, per layer: reflection, cosine and sine of the angle the textures are turned by +// f_coord holds metres from the corner of the pack of chunks, along x and -z + +#param (color, 0, 0, 4, diffuse) +#param (diffuse, 1, 0, 1, diffuse) +#param (specular, 1, 1, 1, specular) +#param (reflection, 1, 2, 1, one) +#param (glossiness, 1, 3, 1, glossiness) +// corner of the chunk (x, z) in texture coordinates, 1 / size of the chunk, paint samples along a side less one +#param (placement, 2, 0, 4, zero) + +#texture (opaque, 0, RGBA) +uniform sampler2D opaque; +#texture (weights0, 1, RGBA) +uniform sampler2D weights0; +#texture (weights1, 2, RGBA) +uniform sampler2D weights1; +#texture (layerdiffuse, 3, sRGB_A) +uniform highp sampler2DArray layerdiffuse; +#texture (layernormal, 4, RGBA) +uniform highp sampler2DArray layernormal; +#texture (layerspecgloss, 5, RGBA) +uniform highp sampler2DArray layerspecgloss; +#texture (layertable, 6, RGBA) +uniform highp sampler2D layertable; + +#define NORMALMAP +#include +#include +#include + +void main() +{ + // place within the chunk, 0..1 along x and z, and the paint samples around it + vec2 local = clamp(vec2(f_coord.x - param[2].x, -f_coord.y - param[2].y) * param[2].z, 0.0, 1.0); + float samples = param[2].w; + vec2 paintcoord = (local * samples + 0.5) / (samples + 1.0); + vec4 w0 = texture(weights0, paintcoord); + vec4 w1 = texture(weights1, paintcoord); + float weights[8] = float[8](w0.r, w0.g, w0.b, w0.a, w1.r, w1.g, w1.b, w1.a); + float painted = dot(w0, vec4(1.0)) + dot(w1, vec4(1.0)); + + // the layers are sampled only where they're painted, so the gradients are taken here, where every pixel gets them + vec2 coorddx = dFdx(f_coord); + vec2 coorddy = dFdy(f_coord); + + vec3 albedo = vec3(0.0); + vec3 normal = vec3(0.0); + float reflectivity = 0.0; + float specularity = 0.0; + float gloss = 0.0; + float metal = 0.0; + float total = 0.0; + for (int i = 0; i < 8; ++i) + { + // a chunk without weights shows its first layer + float weight = (painted < 0.004 ? (i == 0 ? 1.0 : 0.0) : weights[i]); + if (weight < 0.004) + continue; + vec4 info = texelFetch(layertable, ivec2(i, 0), 0); + vec4 extra = texelFetch(layertable, ivec2(i, 1), 0); + // turned counterclockwise seen from above; f_coord runs along x and -z, so that's clockwise in it + vec2 turn = (extra.y == 0.0 && extra.z == 0.0) ? vec2(1.0, 0.0) : extra.yz; + mat2 rotation = mat2(turn.x, -turn.y, turn.y, turn.x); + vec3 coord = vec3(rotation * f_coord * info.y, info.x); + vec2 dx = rotation * coorddx * info.y; + vec2 dy = rotation * coorddy * info.y; + albedo += textureGrad(layerdiffuse, coord, dx, dy).rgb * weight; + vec3 layernormal_ts = vec3(0.0, 0.0, 1.0); + float layerreflection = extra.x; + if (info.z > 0.5) + { + vec4 normalmap = textureGrad(layernormal, coord, dx, dy); + layernormal_ts.xy = normalmap.rg * 2.0 - 1.0; + layernormal_ts.z = sqrt(1.0 - clamp(dot(layernormal_ts.xy, layernormal_ts.xy), 0.0, 1.0)); + // the normal map leans along the turned texture; turned back to the axes of the terrain + layernormal_ts.xy = layernormal_ts.xy * rotation; + layerreflection *= normalmap.a; + } + normal += layernormal_ts * weight; + if (info.w > 0.5) + { + vec4 specgloss = textureGrad(layerspecgloss, coord, dx, dy); + specularity += specgloss.r * weight; + gloss += specgloss.g * weight; + metal += specgloss.b * weight; + } + else + { + gloss += weight; + } + reflectivity += layerreflection * weight; + total += weight; + } + total = max(total, 0.004); + albedo /= total; + specularity /= total; + metal /= total; + reflectivity = param[1].z * reflectivity / total; + glossiness = (gloss / total) * abs(param[1].w); + metalic = metal; + + vec3 fragnormal = normalize(f_tbn * normalize(normal)); + vec3 fragcolor = ambient; + fragcolor = apply_lights(fragcolor, fragnormal, albedo, reflectivity, specularity, shadow_tone); + + vec4 color = vec4(apply_fog(fragcolor), alpha_mult); +#if POSTFX_ENABLED + out_color = color; +#else + out_color = tonemap(color); +#endif +#if MOTIONBLUR_ENABLED + { + vec2 a = (f_clip_future_pos.xy / f_clip_future_pos.w) * 0.5 + 0.5; + vec2 b = (f_clip_pos.xy / f_clip_pos.w) * 0.5 + 0.5; + + out_motion = vec4(a - b, 0.0f, 0.0f); + } +#endif +} diff --git a/simulation/simulation.cpp b/simulation/simulation.cpp index 36edc4c7..70f9e307 100644 --- a/simulation/simulation.cpp +++ b/simulation/simulation.cpp @@ -16,6 +16,9 @@ http://mozilla.org/MPL/2.0/. #include "world/Event.h" #include "world/MemCell.h" #include "world/Track.h" +#include "world/Road.h" +#include "world/Sweep.h" +#include "world/RoadPoint.h" #include "world/Traction.h" #include "world/TractionPower.h" #include "audio/sound.h" @@ -24,6 +27,7 @@ http://mozilla.org/MPL/2.0/. #include "rendering/lightarray.h" #include "rendering/particles.h" #include "scene/scene.h" +#include "scene/scenelayers.h" #include "vehicle/Train.h" #include "application/application.h" #include "utilities/Logs.h" @@ -35,6 +39,10 @@ state_manager State; event_manager Events; memory_table Memory; path_table Paths; +road_table Roads; +sweep_table Sweeps; +junction_table Junctions; +roadpoint_table Roadpoints; traction_table Traction; powergridsource_table Powergrid; instance_table Instances; @@ -113,6 +121,9 @@ state_manager::update( double const Deltatime, int Iterationcount ) { TAnimModel::AnimUpdate( totaltime ); // wykonanie zakolejkowanych animacji simulation::Powergrid.update( totaltime ); + // road junctions, level crossings and the points road vehicles appear at; ahead of the vehicles, so the drivers act on what these decide + simulation::Junctions.update( totaltime ); + simulation::Roadpoints.update( totaltime ); simulation::Vehicles.update( Deltatime, Iterationcount ); } @@ -492,15 +503,79 @@ TAnimModel * state_manager::create_model(const std::string &src, const std::stri return m_serializer.create_model(src, name, position); } +std::vector state_manager::insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed) { + return m_serializer.insert_trainset(Name, Path, Offset, Vehicles, Reversed); +} + TEventLauncher * state_manager::create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position) { return m_serializer.create_eventlauncher(src, name, position); } +std::pair state_manager::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { + return m_serializer.preview_include(Directive, Context, Layer, Instance); +} + +std::pair state_manager::rebuild_include(scene::instance_handle const Instance, std::vector &Retired) { + if (false == scene::Layers.tracked(Instance)) { + return { 0, 0 }; + } + auto const &included { scene::Layers.instance(Instance) }; + // the models the include was loaded with don't match it anymore. + // NOTE: done ahead of retiring the models made by the editor, which are told from the loaded ones by their preview flag + scene::Layers.rebuilt(Instance); + for (auto *model : Instances.sequence()) { + if (model == nullptr || model->m_instance != Instance || false == model->m_preview) { + continue; + } + if (false == included.removed) { + scene::Layers.count(model->layer(), scene::layer_item::model, -1); + } + Region->erase(model); + scene::Hierarchy.erase(model->uuid.to_string()); + model->visible(false); + model->m_preview = false; + Retired.emplace_back(model); + } + if (included.dead) { + return { 0, 0 }; + } + auto const layer { scene::Layers.resolve(included.layer) }; + auto const result { m_serializer.preview_include(scene::Layers.directive(Instance), included.context, layer, Instance) }; + // what's shown for an include which is out of sight at the moment goes out of sight as well + auto const layerhidden { scene::Layers.valid(layer) && false == scene::Layers.layer(layer).visible }; + if (included.removed || layerhidden) { + for (auto *model : Instances.sequence()) { + if (model == nullptr || model->m_instance != Instance || false == model->m_preview) { + continue; + } + if (included.removed) { + scene::Layers.count(model->layer(), scene::layer_item::model, -1); + } + if (model->visible()) { + model->visible(false); + model->m_layerhidden = true; + } + } + } + return result; +} + void state_manager::delete_model(TAnimModel *model) { + scene::Layers.count(model->layer(), scene::layer_item::model, -1); + scene::Layers.forget(model); Region->erase(model); Instances.purge(model); } +bool state_manager::rename_model(TAnimModel *model, std::string const &name) { + if (model == nullptr || false == Instances.rename(model, name)) { + return false; + } + model->m_name = (name == "none" ? std::string{} : name); + scene::Layers.renamed(model); + return true; +} + void state_manager::delete_eventlauncher(TEventLauncher *launcher) { launcher->dRadius = 0.0f; // disable it } diff --git a/simulation/simulation.h b/simulation/simulation.h index 09ea4d42..0e96da68 100644 --- a/simulation/simulation.h +++ b/simulation/simulation.h @@ -40,12 +40,26 @@ public: // create model from node string TAnimModel * create_model(const std::string &src, const std::string &name, const glm::dvec3 &position); + // places the vehicles, given as their node statements, as a trainset on the path. returns: the vehicles made + std::vector + insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed = false); // create eventlauncher from node string TEventLauncher * create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position); + // creates model instances defined by an include directive placed in the scenery editor + std::pair + preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance); + // replaces models shown for specified include with the ones defined by its current directive. Retired: receives + // the replaced models made by the editor. they're out of the scene already, but the renderer can still hold on + // to them for a few frames; pass them to Instances.purge() afterwards + std::pair + rebuild_include(scene::instance_handle Instance, std::vector &Retired); // delete TAnimModel instance void delete_model(TAnimModel *model); + // gives TAnimModel instance another name; empty text leaves it without one. returns: false if another instance uses the name + bool + rename_model(TAnimModel *model, std::string const &name); // delete TEventLauncher instance void delete_eventlauncher(TEventLauncher *launcher); @@ -74,6 +88,10 @@ extern state_manager State; extern event_manager Events; extern memory_table Memory; extern path_table Paths; +extern road_table Roads; +extern sweep_table Sweeps; +extern junction_table Junctions; +extern roadpoint_table Roadpoints; extern traction_table Traction; extern powergridsource_table Powergrid; extern instance_table Instances; diff --git a/simulation/simulationstateserializer.cpp b/simulation/simulationstateserializer.cpp index 99a733d3..1ff771d1 100644 --- a/simulation/simulationstateserializer.cpp +++ b/simulation/simulationstateserializer.cpp @@ -16,6 +16,9 @@ http://mozilla.org/MPL/2.0/. #include "simulation/simulationsounds.h" #include "simulation/simulationenvironment.h" #include "scene/scenenodegroups.h" +#include "scene/scenelayers.h" +#include "scene/sceneterrain.h" +#include "scene/scenemodelentries.h" #include "rendering/particles.h" #include "world/Event.h" #include "world/MemCell.h" @@ -24,14 +27,28 @@ http://mozilla.org/MPL/2.0/. #include "model/AnimModel.h" #include "rendering/lightarray.h" #include "world/TractionPower.h" +#include "world/Road.h" +#include "world/Sweep.h" +#include "world/RoadPoint.h" #include "application/application.h" #include "rendering/renderer.h" #include "utilities/Logs.h" #include "utilities/utilities.h" #include "editor/editorTerrainStreamer.hpp" + namespace simulation { +namespace { + +double vehicle_length( std::string const &Folder, std::string const &Type ) { + + TMoverParameters probe( 0.0, Type, Type, 0 ); + return probe.LoadFIZ( paths::dynamic + Folder + "/" ) ? probe.Dim.L : 0.0; +} + +} // namespace + std::shared_ptr state_serializer::deserialize_begin( std::string const &Scenariofile ) { @@ -40,6 +57,7 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { // drop any streamed editor terrain from a previously loaded scenery before the old region (and // its sections, which those chunks referenced) is destroyed below EditorTerrain.reset(); + scene::terrain_file::references().clear(); // TODO: move initialization to separate routine so we can reuse it SafeDelete( Region ); @@ -59,7 +77,9 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { // compilation to binary file isn't supported for rainsted-created overrides // NOTE: we postpone actual loading of the scene until we process time, season and weather data state->scratchpad.binary.terrain = Region->is_scene( Scenariofile ) ; + state->scratchpad.binary.terrain_default = state->scratchpad.binary.terrain; } + Global.file_binary_terrain_skipped = 0; if (false != state->scratchpad.binary.terrain) { @@ -73,6 +93,14 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { } scene::Groups.create(); + scene::Layers.clear(); + if( true == Global.editor_session ) { + // scenery opened for editing: keep track of which scenery file defines each node. + // the scenario file itself is the root layer, and the default target for nodes created in the editor + state->input.sceneryLayers = true; + scene::Layers.active( scene::Layers.open( Scenariofile ) ); + } + if( false == state->input.ok() ) throw invalid_scenery_exception(); @@ -107,8 +135,10 @@ state_serializer::deserialize_begin( std::string const &Scenariofile ) { { "time", &state_serializer::deserialize_time }, { "trainset", &state_serializer::deserialize_trainset }, { "terrain", &state_serializer::deserialize_terrain }, + { "heightmap_terrain", &state_serializer::deserialize_heightmapterrain }, { "editorterrain", &state_serializer::deserialize_editorterrain }, - { "endtrainset", &state_serializer::deserialize_endtrainset } }; + { "endtrainset", &state_serializer::deserialize_endtrainset }, + { "reversed", &state_serializer::deserialize_reversed } }; for( auto &function : functionlist ) { state->functionmap.emplace( function.first, std::bind( function.second, this, std::ref( state->input ), std::ref( state->scratchpad ) ) ); @@ -155,18 +185,29 @@ state_serializer::deserialize_continue(std::shared_ptr state } scene::Groups.close(); + scene::Layers.close(); scene::Groups.update_map(); Region->create_map_geometry(); if( true == Global.file_binary_terrain && false == state->scratchpad.binary.terrain + && true == state->scratchpad.binary.terrain_textfiles.empty() && state->scenariofile != "$.scn" ) { // if we didn't find usable binary version of the scenario files, create them now for future use - // as long as the scenario file wasn't rainsted-created base file override + // as long as the scenario file wasn't rainsted-created base file override. + // NOTE: this is done only for the sceneries which don't refer to terrain files (.txtf, .btf). these get binary + // versions of their terrain files instead, and a file holding all geometry of the scenery would only get in their way Region->serialize( state->scenariofile ); } + // geometry of the roads is generated on each load instead of being kept in the binary terrain file, + // so it's inserted in the region only after that file had its chance to be written + simulation::Roads.create_geometry( Scratchpad ); + simulation::Junctions.create_geometry( Scratchpad ); + simulation::Roadpoints.create_geometry(); + simulation::Sweeps.create_geometry( Scratchpad ); + return false; } @@ -345,6 +386,7 @@ state_serializer::deserialize_event( cParser &Input, scene::scratch_data &Scratc if( true == simulation::Events.insert( event ) ) { scene::Groups.insert( scene::Groups.handle(), event ); + scene::Layers.count( scene::Layers.handle(), scene::layer_item::event ); } else { delete event; @@ -368,6 +410,9 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc if( true == Scratchpad.initialized ) { return; } + // scenery opened for editing: what the editor adds to the scenery files is kept ahead of the initialization + scene::Layers.initialization( { Input.TokenBegin(), Input.TokenEnd() }, Input.InLayerFile() ); + if( true == Scratchpad.binary.terrain ) { // at this stage it should be safe to import terrain from the binary scene file // TBD: postpone loading furter and only load required blocks during the simulation? @@ -375,10 +420,32 @@ state_serializer::deserialize_firstinit( cParser &Input, scene::scratch_data &Sc { Region->deserialize(Scratchpad.name); } + else + { + // files named by terrain directives are loaded here rather than on the spot, as by this point + // it's known whether all of them can be used. the list holds each file once + for (auto const &terrainfile : Scratchpad.binary.terrain_files) + { + Region->deserialize(terrainfile); + } + } } + // binary versions of terrain files only announce what they hold at this point, the sections of the scene load it when they need it. + // scenery opened for editing gets it all at once, the editor works with complete geometry + for( auto const &terrainfile : Scratchpad.binary.terrain_binaryfiles ) { + scene::terrain_file::attach( terrainfile, *Region, false == Global.editor_session ); + } + Scratchpad.binary.terrain_binaryfiles.clear(); simulation::Paths.InitTracks(); + // the roads tie up their own lanes where they gain or lose some, the junctions tie the lanes of the roads together, + // what's left loose after that gets closed by the roads + simulation::Roads.InitLanes(); + simulation::Junctions.InitJunctions(); + simulation::Roads.InitRoads(); + // crossings and traffic points go by the lanes, which are complete at this point + simulation::Roadpoints.InitRoadpoints(); simulation::Traction.InitTraction(); simulation::Events.InitEvents(); simulation::Events.InitLaunchers(); @@ -406,7 +473,8 @@ void state_serializer::init_time() { void state_serializer::deserialize_group( cParser &Input, scene::scratch_data &Scratchpad ) { - scene::Groups.create(); + // a group of the scenery stands by itself, rather than being a part of the file it's in (e.g. made in the editor) + scene::Groups.create( true ); } void @@ -426,8 +494,16 @@ void state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratchpad ) { auto const inputline = Input.Line(); // cache in case we need to report error + auto const sourcebegin = Input.TokenBegin(); // location of the node definition, for scenery opened for editing scene::node_data nodedata; + nodedata.layer = scene::Layers.handle(); + if( ( nodedata.layer != null_handle ) && ( false == Input.InLayerFile() ) ) { + // defined by a template, or by something else which isn't a scenery layer file + nodedata.instance = ( scene::Layers.instance() != 0 ? scene::Layers.instance() : scene::untracked_instance ); + } + // a run of plain model instances is taken straight from the text, instead of token by token + if( true == deserialize_models( Input, Scratchpad, nodedata, inputline, sourcebegin ) ) { return; } // common data and node type indicator Input.getTokens( 4 ); Input @@ -477,6 +553,83 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } scene::Groups.insert( scene::Groups.handle(), path ); simulation::Region->insert_and_register( path ); + scene::Layers.track( path, { sourcebegin, Input.TokenEnd() } ); + } + else if( nodedata.type == "road" ) { + + auto *road { new road_node( nodedata ) }; + road->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + if( false == simulation::Roads.insert( road ) ) { + ErrorLog( "Bad scenario: duplicate road name \"" + road->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), road ); + // the lanes are regular paths, registered right away so they get joined with their neighbours along with the tracks + road->create_lanes(); + scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } ); + } + else if( nodedata.type == "sweep" ) { + + auto *sweep { new sweep_node( nodedata ) }; + sweep->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + if( false == simulation::Sweeps.insert( sweep ) ) { + ErrorLog( "Bad scenario: duplicate sweep name \"" + sweep->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), sweep ); + scene::Layers.track( sweep, { sourcebegin, Input.TokenEnd() } ); + } + else if( nodedata.type == "junction" ) { + + auto *junction { new junction_node( nodedata ) }; + junction->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + road_node::state stretch; + if( junction->as_road( stretch ) ) { + // a junction of two roads is where a road changes its lanes, which used to take a junction and is done by a road now. + // it's loaded as the road it would be, and marked as changed so it's written as one when the scenery is saved + delete junction; + nodedata.type = "road"; + auto *road { new road_node( nodedata ) }; + road->define( stretch ); + if( false == simulation::Roads.insert( road ) ) { + ErrorLog( "Bad scenario: duplicate road name \"" + road->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), road ); + road->create_lanes(); + road->mark_dirty(); + scene::Layers.track( road, { sourcebegin, Input.TokenEnd() } ); + } + else { + if( false == simulation::Junctions.insert( junction ) ) { + ErrorLog( "Bad scenario: duplicate junction name \"" + junction->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), junction ); + scene::Layers.track( junction, { sourcebegin, Input.TokenEnd() } ); + } + } + else if( roadpoint_node::is_keyword( nodedata.type ) ) { + // level crossing, or a point where road vehicles appear or are taken away + auto *point { new roadpoint_node( nodedata ) }; + point->import( + Input, + ( Scratchpad.location.offset.empty() ? + glm::dvec3 { 0.0 } : + glm::dvec3 { Scratchpad.location.offset.top() } ) ); + if( false == simulation::Roadpoints.insert( point ) ) { + ErrorLog( "Bad scenario: duplicate road point name \"" + point->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), point ); + scene::Layers.track( point, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "traction" ) { @@ -489,6 +642,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } scene::Groups.insert( scene::Groups.handle(), traction ); simulation::Region->insert_and_register( traction ); + scene::Layers.track( traction, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "tractionpowersource" ) { @@ -510,6 +664,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch // 3d terrain if( false == Scratchpad.binary.terrain ) { // if we're loading data from text .scn file convert and import + ++Scratchpad.binary.geometry_imported; auto *instance = deserialize_model( Input, Scratchpad, nodedata ); // model import can potentially fail if( instance == nullptr ) { return; } @@ -536,6 +691,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } else { // if binary terrain file was present, we already have this data + ++Scratchpad.binary.geometry_skipped; skip_until( Input, "endmodel" ); } } @@ -545,24 +701,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch // model import can potentially fail if( instance == nullptr ) { return; } - if( instance->Model() != nullptr ) { - for( auto const &smokesource : instance->Model()->smoke_sources() ) { - Particles.insert( - smokesource.first, - instance, - smokesource.second ); - } - } - - if( false == simulation::Instances.insert( instance ) ) { - ErrorLog( "Bad scenario: duplicate 3d model instance name \"" + instance->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); - } - scene::Groups.insert( scene::Groups.handle(), instance ); - simulation::Region->insert( instance ); - scene::basic_node *hierarchy_node = instance; - if (hierarchy_node) - { scene::Hierarchy[hierarchy_node->uuid.to_string()] = hierarchy_node; - } + insert_model( instance, Input, inputline, { sourcebegin, Input.TokenEnd() } ); } } else if( nodedata.type == "triangles" @@ -578,17 +717,39 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch && Input.Name().starts_with("scenery/zwr") && Input.Name().ends_with(".inc") ) }; + material_handle material { null_handle }; if( false == skip ) { + ++Scratchpad.binary.geometry_imported; + auto shape { scene::shape_node().import( Input, nodedata ) }; + material = shape.data().material; + if( true == Global.editor_session ) { + shape.source_file( scene::terrain_file::reference_of( Input.Name() ) ); + } simulation::Region->insert( - scene::shape_node().import( - Input, nodedata ), + std::move( shape ), Scratchpad, true ); } else { + if( true == Scratchpad.binary.terrain ) { + ++Scratchpad.binary.geometry_skipped; + } + if( nodedata.layer != null_handle && Input.InLayerFile() ) { + // the editor wants to know the material even of the shapes which come from the binary terrain + auto token { Input.getToken() }; + if( token == "material" ) { + skip_until( Input, "endmaterial" ); + token = Input.getToken(); + } + replace_slashes( token ); + material = GfxRenderer->Fetch_Material( token ); + } skip_until( Input, "endtri" ); } + if( nodedata.layer != null_handle && Input.InLayerFile() ) { + scene::Layers.shape( material, { sourcebegin, Input.TokenEnd() } ); + } } else if( nodedata.type == "lines" || nodedata.type == "line_strip" @@ -596,6 +757,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch if( false == Scratchpad.binary.terrain ) { + ++Scratchpad.binary.geometry_imported; simulation::Region->insert( scene::lines_node().import( Input, nodedata ), @@ -603,6 +765,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } else { // all lines were already loaded from the binary version of the file + ++Scratchpad.binary.geometry_skipped; skip_until( Input, "endline" ); } } @@ -614,6 +777,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch } scene::Groups.insert( scene::Groups.handle(), memorycell ); simulation::Region->insert( memorycell ); + scene::Layers.track( memorycell, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "eventlauncher" ) { @@ -633,6 +797,7 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch simulation::Region->insert( eventlauncher ); } } + scene::Layers.track( eventlauncher, { sourcebegin, Input.TokenEnd() } ); } else if( nodedata.type == "sound" ) { @@ -643,8 +808,151 @@ state_serializer::deserialize_node( cParser &Input, scene::scratch_data &Scratch simulation::Region->insert( sound ); } + if( nodedata.layer != null_handle ) { + // scenery opened for editing: keep count of what the scenery file contains + // NOTE: node types which failed to load bail out earlier and aren't counted + static std::unordered_map const itemtypes { + { "dynamic", scene::layer_item::vehicle }, + { "track", scene::layer_item::track }, + { "road", scene::layer_item::track }, + { "junction", scene::layer_item::track }, + { "crossing", scene::layer_item::track }, + { "spawn", scene::layer_item::track }, + { "despawn", scene::layer_item::track }, + { "crosswalk", scene::layer_item::track }, + { "traction", scene::layer_item::traction }, + { "tractionpowersource", scene::layer_item::powersource }, + { "model", scene::layer_item::model }, + { "triangles", scene::layer_item::shape }, + { "triangle_strip", scene::layer_item::shape }, + { "triangle_fan", scene::layer_item::shape }, + { "lines", scene::layer_item::lines }, + { "line_strip", scene::layer_item::lines }, + { "line_loop", scene::layer_item::lines }, + { "memcell", scene::layer_item::memcell }, + { "eventlauncher", scene::layer_item::launcher }, + { "sound", scene::layer_item::sound } }; + auto const lookup { itemtypes.find( nodedata.type ) }; + if( lookup != itemtypes.end() ) { + scene::Layers.count( + nodedata.layer, + // models with negative minimum range are 3d terrain, converted to shapes + ( lookup->second == scene::layer_item::model && nodedata.range_min < 0.0 ) ? + scene::layer_item::shape : + lookup->second ); + } + } } +// makes model instance defined by a scenery file a part of the simulation +void +state_serializer::insert_model( TAnimModel *Instance, cParser const &Input, std::size_t const Line, scene::source_span const &Span ) { + + if( Instance->Model() != nullptr ) { + for( auto const &smokesource : Instance->Model()->smoke_sources() ) { + Particles.insert( + smokesource.first, + Instance, + smokesource.second ); + } + } + + if( false == simulation::Instances.insert( Instance ) ) { + ErrorLog( "Bad scenario: duplicate 3d model instance name \"" + Instance->name() + "\" defined in file \"" + Input.Name() + "\" (line " + std::to_string( Line ) + ")" ); + } + scene::Groups.insert( scene::Groups.handle(), Instance ); + simulation::Region->insert( Instance ); + scene::Layers.track( Instance, Span, Instance->Angles(), Instance->Scale() ); + // the lookup by uuid serves the scenery editor. it's filled only for a scenery opened for editing, as with a lot + // of instances it takes more time than everything else done for an instance put together + if( true == Global.editor_session ) { + scene::Hierarchy[ Instance->uuid.to_string() ] = Instance; + } +} + +// loads a run of model instances straight from the text of the scenery file, starting with the instance whose node keyword +// was just read. the text is taken apart by a reader made for these definitions, with a few threads sharing the work; the +// instances are then created here one by one, in the order of the text. returns: true if any instances were loaded, false +// if the definition at hand has to go through the parser +bool +state_serializer::deserialize_models( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data &Nodedata, std::size_t const Line, std::streamoff const Sourcebegin ) { + + auto const text { Input.remainingText() }; + if( true == text.empty() ) { return false; } + + auto const run { scene::read_model_entries( text, true ) }; + if( true == run.blocks.empty() ) { return false; } + // NOTE: the parser moves on but the text stays where it is, the definitions refer to it + Input.skipText( run.length, run.linebreaks, std::string_view { "endmodel" }.size() ); + auto const textbegin { Input.TokenEnd() - static_cast( run.length ) }; + + Nodedata.type = "model"; + auto loaded { 0 }; + for( auto const &block : run.blocks ) { + // instances of the same model with the same skin are set up after the first one of them + std::vector twins( block.appearances.size(), nullptr ); + for( auto const &entry : block.entries ) { + + Nodedata.range_max = entry.range_max; + Nodedata.range_min = entry.range_min; + // the parser supplies the names in lower case, which goes only for the ascii letters + Nodedata.name.assign( entry.name ); + for( auto &character : Nodedata.name ) { + if( character >= 'A' && character <= 'Z' ) { + character = static_cast( character - 'A' + 'a' ); + } + } + if( Nodedata.name == "none" ) { Nodedata.name.clear(); } + + // what follows is what deserialize_model() does + auto *instance = new TAnimModel( Nodedata ); + instance->Angles( Scratchpad.location.rotation + glm::vec3 { 0.f, entry.angle, 0.f } ); + if( false == Scratchpad.location.scale.empty() ) { + instance->Scale( Scratchpad.location.scale.top() ); + } + auto const &appearance { block.appearances[ entry.appearance ] }; + auto &twin { twins[ entry.appearance ] }; + instance->Load( + appearance.model, appearance.texture, twin, + ( entry.has_angles ? &entry.angles : nullptr ), + ( entry.has_scale ? &entry.scale : nullptr ), + false == entry.notransition ); + if( twin == nullptr ) { twin = instance; } + instance->location( transform( entry.location, Scratchpad ) ); + + // location of the first definition is known from its node keyword, which went through the parser + auto const first { loaded == 0 }; + insert_model( + instance, Input, + ( first ? Line : Line + entry.line ), + { ( first ? Sourcebegin : textbegin + static_cast( entry.begin ) ), textbegin + static_cast( entry.end ) } ); + ++loaded; + } + } + if( Nodedata.layer != null_handle ) { + // scenery opened for editing: keep count of what the scenery file contains + scene::Layers.count( Nodedata.layer, scene::layer_item::model, loaded ); + } + + return true; +} + +namespace { + +// passes the placement in effect to the layer bookkeeping of scenery opened for editing +void +sync_layer_context( scene::scratch_data const &Scratchpad ) { + + if( true == scene::Layers.empty() ) { return; } + + scene::Layers.context( { + ( Scratchpad.location.offset.empty() ? glm::dvec3{ 0.0 } : Scratchpad.location.offset.top() ), + Scratchpad.location.rotation, + ( Scratchpad.location.scale.empty() ? glm::vec3{ 1.f } : Scratchpad.location.scale.top() ) } ); +} + +} // namespace + void state_serializer::deserialize_origin( cParser &Input, scene::scratch_data &Scratchpad ) { @@ -660,6 +968,7 @@ state_serializer::deserialize_origin( cParser &Input, scene::scratch_data &Scrat Scratchpad.location.offset.empty() ? glm::dvec3() : Scratchpad.location.offset.top() ) ); + sync_layer_context( Scratchpad ); } void @@ -671,6 +980,7 @@ state_serializer::deserialize_endorigin( cParser &Input, scene::scratch_data &Sc else { ErrorLog( "Bad origin: endorigin instruction with empty origin stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" ); } + sync_layer_context( Scratchpad ); } void @@ -694,6 +1004,7 @@ state_serializer::deserialize_scale( cParser &Input, scene::scratch_data &Scratc // scales compose component-wise, mirroring how origin offsets compose additively. glm::vec3 const parent = Scratchpad.location.scale.empty() ? glm::vec3(1.0f) : Scratchpad.location.scale.top(); Scratchpad.location.scale.emplace( factor * parent ); + sync_layer_context( Scratchpad ); } void @@ -705,6 +1016,7 @@ state_serializer::deserialize_endscale( cParser &Input, scene::scratch_data &Scr else { ErrorLog( "Bad scale: endscale instruction with empty scale stack in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" ); } + sync_layer_context( Scratchpad ); } void @@ -715,6 +1027,7 @@ state_serializer::deserialize_rotate( cParser &Input, scene::scratch_data &Scrat >> Scratchpad.location.rotation.x >> Scratchpad.location.rotation.y >> Scratchpad.location.rotation.z; + sync_layer_context( Scratchpad ); } void @@ -786,50 +1099,248 @@ state_serializer::deserialize_trainset( cParser &Input, scene::scratch_data &Scr >> Scratchpad.trainset.velocity; } +namespace { + +// processes binary terrain file named by a terrain directive. +// a binary terrain file holds all static geometry of the scenery it was written for, and while one is in use +// the text definitions of that geometry are skipped, wherever they are. the two can't be mixed without some +// of the geometry showing up twice or not at all. thus the named files are used only if nothing was read from +// the text yet and every one of them is usable, each is loaded once, and if that can't be done the scenery +// falls back on the default binary file of the scenario, or on the text. +void +include_binary_terrain( std::string File, scene::scratch_data &Scratchpad ) { + + auto &binary { Scratchpad.binary }; + + replace_slashes( File ); + if( false == binary.terrain_textfiles.empty() ) { + // the scenery keeps its terrain in files of the current format, which don't mix with a file holding all of its geometry + WriteLog( "Included SBT file: " + File + " ignored, the scenery uses terrain files" ); + return; + } + if( std::find( binary.terrain_files.begin(), binary.terrain_files.end(), File ) != binary.terrain_files.end() ) { + WriteLog( "Included SBT file: " + File + " is already in use, ignored" ); + return; + } + + if( false == simulation::Region->is_scene( File ) ) { + // the file is missing, or of a type or version we can't read + if( true == binary.terrain_files.empty() ) { + // nothing changes: the default file if there's one, or the text otherwise, remains the source of the geometry + WriteLog( "Included SBT file: " + File + " can't be used, ignored" ); + } + else if( true == Scratchpad.initialized ) { + // the other named files are loaded already, it's too late to give up on them + ErrorLog( "Bad scenario: included SBT file \"" + File + "\" can't be used, the geometry it should provide will be missing" ); + } + else if( true == binary.terrain_default ) { + // the default file was made out of the complete text of this scenario, so it can stand in for the incomplete set + binary.terrain_files.clear(); + binary.terrain_included = false; + WriteLog( "Included SBT file: " + File + " can't be used, falling back on the default SBT" ); + } + else if( ( binary.geometry_skipped == 0 ) && ( Global.file_binary_terrain_skipped == 0 ) ) { + // none of the text definitions was skipped so far, so the whole scenery can still be read from the text + binary.terrain_files.clear(); + binary.terrain_included = false; + binary.terrain = false; + Global.file_binary_terrain_state = false; + WriteLog( "Included SBT file: " + File + " can't be used, the scenery will be loaded from the text files" ); + } + else { + ErrorLog( "Bad scenario: included SBT file \"" + File + "\" can't be used, the geometry it should provide will be missing" ); + } + return; + } + + if( binary.geometry_imported > 0 ) { + // part of the geometry was read from the text already, and the binary file would bring it in for the second time + WriteLog( "Included SBT file: " + File + " ignored, the scenery geometry is already being loaded from the text files" ); + return; + } + if( ( true == Scratchpad.initialized ) && ( true == binary.terrain ) && ( false == binary.terrain_included ) ) { + // likewise if the default file was loaded by now + WriteLog( "Included SBT file: " + File + " ignored, the scenery geometry is already loaded from the default SBT" ); + return; + } + + binary.terrain_files.emplace_back( File ); + binary.terrain_included = true; + binary.terrain = true; + Global.file_binary_terrain_state = true; + Scratchpad.terrain_name = File; + WriteLog( "Included SBT file: " + File ); + if( true == Scratchpad.initialized ) { + // past the initialization there's nothing to wait for + simulation::Region->deserialize( File ); + } +} + +// processes terrain file named by a terrain directive: static geometry kept as text (.txtf), with binary version (.btf) +// made out of it and loaded in its place. unlike the legacy binary terrain above, the binary file stands in +// only for the text file of the same name, and has no bearing on how the rest of the scenery is loaded. +// a file which is to be read as text is added to Includes, for the caller to pass to the parser +void +include_terrain_file( std::string File, std::string &Includes, scene::scratch_data &Scratchpad ) { + + auto &binary { Scratchpad.binary }; + + replace_slashes( File ); + erase_extension( File ); + if( std::find( binary.terrain_textfiles.begin(), binary.terrain_textfiles.end(), File ) != binary.terrain_textfiles.end() ) { + WriteLog( "Terrain file: " + File + " is already in use, ignored" ); + return; + } + binary.terrain_textfiles.emplace_back( File ); + // the editor converts the triangles of the terrain files to heightmap terrain, it has to know them and where they're placed + scene::terrain_file::references().push_back( { + File, + ( Scratchpad.location.offset.empty() ? glm::dvec3( 0.0 ) : Scratchpad.location.offset.top() ), + Scratchpad.location.rotation } ); + + auto const textfile { Global.asCurrentSceneryPath + File + ".txtf" }; + auto const binaryfile { Global.asCurrentSceneryPath + File + ".btf" }; + auto const textpresent { FileExists( textfile ) }; + // binary file holds the geometry where the text alone puts it, it can't stand in for a file placed with an offset or rotation + auto const relocated { + ( false == Scratchpad.location.offset.empty() && Scratchpad.location.offset.top() != glm::dvec3( 0.0 ) ) + || ( Scratchpad.location.rotation != glm::vec3( 0.f ) ) }; + + auto state { scene::terrain_file::state::text }; + if( true == relocated ) { + if( false == textpresent ) { + ErrorLog( "Bad scenario: terrain file \"" + File + "\" placed with an offset or rotation can be loaded only from the text, which is missing" ); + return; + } + } + else if( ( false == textpresent ) + || ( ( false == Global.editor_session ) && ( true == Global.file_binary_terrain ) ) ) { + // NOTE: scenery opened for editing works on its text files, the text is left as is also if binary terrain is turned off. + // a binary file which is all there is gets loaded regardless + state = scene::terrain_file::prepare( textfile, binaryfile ); + } + + switch( state ) { + case scene::terrain_file::state::binary: { + if( true == binary.terrain ) { + // legacy binary terrain file is in use, expected to hold all static geometry of the scenery, this terrain likely included + if( ( false == Scratchpad.initialized ) + && ( binary.geometry_skipped == 0 ) + && ( Global.file_binary_terrain_skipped == 0 ) ) { + // nothing was left out on account of that file yet, so the scenery can still do without it + binary.terrain = false; + binary.terrain_included = false; + binary.terrain_default = false; + binary.terrain_files.clear(); + Global.file_binary_terrain_state = false; + WriteLog( "Terrain file: " + File + " in use, SBT of the scenery is ignored" ); + } + else { + ErrorLog( "Bad scenario: terrain file \"" + File + "\" ignored, the scenery geometry already comes from an SBT file. Remove the SBT file to have the terrain file used" ); + return; + } + } + if( true == Scratchpad.initialized ) { + scene::terrain_file::attach( binaryfile, *simulation::Region, false == Global.editor_session ); + } + else { + binary.terrain_binaryfiles.emplace_back( binaryfile ); + } + Includes += scene::terrain_file::extra( binaryfile ) + ' '; + break; + } + case scene::terrain_file::state::text: { + // read the way any other scenery file is + WriteLog( "Terrain file: " + File + " loaded as text" ); + Includes += "include \"" + textfile + "\" end "; + break; + } + default: { + ErrorLog( "Bad scenario: terrain file \"" + File + "\" not found" ); + break; + } + } +} + +} // namespace + +void +state_serializer::deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad ) { + + if( false == Scratchpad.trainset.is_open + || false == Scratchpad.trainset.vehicles.empty() ) { + ErrorLog( "Bad trainset: \"reversed\" has to follow the trainset header, ahead of its vehicles, in file \"" + Input.Name() + "\" (line " + std::to_string( Input.Line() - 1 ) + ")" ); + return; + } + Scratchpad.trainset.reversed = true; +} + void state_serializer::deserialize_terrain(cParser &Input, scene::scratch_data &Scratchpad) { - std::string line; - Input.getTokens(1); - Input >> line; - if (Global.file_binary_terrain && line.ends_with(".sbt")) - { - Scratchpad.binary.terrain = Region->is_scene(line); - Global.file_binary_terrain_state = true; - Scratchpad.binary.terrain_included = true; - Scratchpad.terrain_name = line; - WriteLog("Included SBT file: " + line); - Region->deserialize(Scratchpad.terrain_name); + // the directive names a terrain file, or a number of them + // NOTE: the directive is read to its end first, as processing of a terrain file can leave content for the parser to go through next + std::vector files; + std::string token; + while (false == (token = Input.getToken()).empty() && token != "endterrain") + { + files.emplace_back(token); + } - } - - skip_until(Input, "endterrain"); + std::string includes; // terrain files to be read as text, in the order the directive names them + for (auto const &file : files) + { + if (file.ends_with(".txtf") || file.ends_with(".btf")) + { + include_terrain_file(file, includes, Scratchpad); + } + else if (Global.file_binary_terrain && file.ends_with(".sbt")) + { + include_binary_terrain(file, Scratchpad); + } + } + if (false == includes.empty()) + { + Input.injectString(includes); + } +} +void +state_serializer::deserialize_heightmapterrain(cParser &Input, scene::scratch_data &Scratchpad) +{ + // heightmap terrain, kept in terrain// of the simulator. format: + // heightmap_terrain [name] endheightmap_terrain + // without the name the terrain is named after the scenery file. the chunks are loaded around the camera in every mode + std::string name; + while( true ) { + auto const token { Input.getToken( false ) }; + if( token.empty() || token == "endheightmap_terrain" ) { + break; + } + if( name.empty() ) { + name = token; + } + } + if( name.empty() ) { + name = Global.SceneryFile; + auto const slash { name.find_last_of( "/\\" ) }; + if( slash != std::string::npos ) { + name.erase( 0, slash + 1 ); + } + erase_extension( name ); + } + scene::Layers.terrain_directive(true); + if( false == name.empty() ) { + EditorTerrain.open( name ); + } } void state_serializer::deserialize_editorterrain(cParser &Input, scene::scratch_data &Scratchpad) { - // editor-authored streaming terrain. format: - // editorterrain endeditorterrain - // the global streamer loads its 16-bit chunk files around the camera in every mode - std::string folder; - int cells = 32; - float cellsize = 2.0f; - int radius = 4; - Input.getTokens(4); - Input >> folder >> cells >> cellsize >> radius; + // the chunk files of the first editor terrain were replaced with the heightmap terrain before they were used in sceneries skip_until(Input, "endeditorterrain"); - - if (!folder.empty() && cells > 0 && cellsize > 0.0f) - { - EditorTerrain.directory(folder); - EditorTerrain.configure(cells, cellsize, radius < 0 ? 0 : radius, 0.0f, std::string()); - EditorTerrain.active(true); - WriteLog("Editor terrain stream enabled: " + folder + " (cells " + std::to_string(cells) - + ", cellsize " + std::to_string(cellsize) + ", radius " + std::to_string(radius) + ")", - logtype::generic); - } + WriteLog("Bad scenario: obsolete \"editorterrain\" directive ignored, the editor terrain is \"heightmap_terrain\" now", logtype::generic); } void @@ -858,9 +1369,16 @@ state_serializer::deserialize_endtrainset( cParser &Input, scene::scratch_data & } if( vehicleindex > 0 ) { // from second vehicle on couple it with the previous one - Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext( - vehicle, - Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + if( Scratchpad.trainset.reversed ) { + vehicle->AttachNext( + Scratchpad.trainset.vehicles[ vehicleindex - 1 ], + Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + } + else { + Scratchpad.trainset.vehicles[ vehicleindex - 1 ]->AttachNext( + vehicle, + Scratchpad.trainset.couplings[ vehicleindex - 1 ] ); + } } ++vehicleindex; } @@ -1040,7 +1558,7 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra loadtype = ""; } - auto *path = simulation::Paths.find( pathname ); + auto *path = Scratchpad.trainset.path != nullptr ? Scratchpad.trainset.path : simulation::Paths.find( pathname ); if( path == nullptr ) { ErrorLog( "Bad scenario: vehicle \"" + Nodedata.name + "\" placed on nonexistent path \"" + pathname + "\" in file \"" + Input.Name() + "\" (line " + std::to_string( inputline ) + ")" ); @@ -1048,7 +1566,9 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra return nullptr; } - if( true == Scratchpad.trainset.vehicles.empty() // jeśli pierwszy pojazd, + auto const reversedset { Scratchpad.trainset.is_open && Scratchpad.trainset.reversed }; + if( false == reversedset + && true == Scratchpad.trainset.vehicles.empty() // jeśli pierwszy pojazd, && false == path->m_events0.empty() // tor ma Event0 && std::abs(velocity) <= 1.f // a skład stoi && Scratchpad.trainset.offset >= 0.0 // ale może nie sięgać na owy tor @@ -1058,23 +1578,34 @@ state_serializer::deserialize_dynamic( cParser &Input, scene::scratch_data &Scra } auto *vehicle = new TDynamicObject(); - + + auto const gap { offset == -1.0 ? 0.0 : offset }; + auto const turned { ( offset == -1.0 ) != reversedset }; + auto const expected { reversedset ? vehicle_length( datafolder, mmdfile ) : 0.0 }; auto const length = vehicle->Init( Nodedata.name, datafolder, skinfile, mmdfile, path, - offset == -1.0 ? Scratchpad.trainset.offset : Scratchpad.trainset.offset - offset, + reversedset ? Scratchpad.trainset.offset + gap + expected : Scratchpad.trainset.offset - gap, drivertype, velocity, Scratchpad.trainset.name, loadcount, loadtype, - offset == -1.0, + turned, params ); + if( length != 0.0 && reversedset ) { + if( std::abs( length - expected ) > 0.01 ) { + vehicle->place_on_track( path, Scratchpad.trainset.offset + gap + length, turned ); + } + Scratchpad.trainset.offset += length; + } if( length != 0.0 ) { // zero oznacza błąd // przesunięcie dla kolejnego, minus bo idziemy w stronę punktu 1 - Scratchpad.trainset.offset -= length; + if( false == reversedset ) { + Scratchpad.trainset.offset -= length; + } // automatically establish permanent connections for couplers which specify them in their definitions if( coupling != 0 && vehicle->MoverParameters->Couplers[(offset == -1.0 ? end::front : end::rear)].AllowedFlag & coupling::permanent ) { @@ -1196,14 +1727,9 @@ state_serializer::export_as_text(std::string const &Scenariofile) const { scmfile << "// sounds\n"; Region->export_as_text( scmfile ); - // editor-authored streaming terrain: emit a directive pointing at its 16-bit chunk folder so the - // scenery streams it on load (in every mode) + // heightmap terrain: the directive which opens its folder, so the scenery streams it on load (in every mode) if( EditorTerrain.active() ) { - scmfile << "// editor terrain\neditorterrain " - << EditorTerrain.directory() << ' ' - << EditorTerrain.cells() << ' ' - << EditorTerrain.cellsize() << ' ' - << EditorTerrain.radius() << " endeditorterrain\n"; + scmfile << "// heightmap terrain\nheightmap_terrain " << EditorTerrain.name() << " endheightmap_terrain\n"; } scmfile << "// modified objects\ninclude " << filename << "_export_dirty.scm\n"; @@ -1253,14 +1779,9 @@ state_serializer::export_as_text(std::string const &Scenariofile) const { scmfile << "// sounds\n"; Region->export_as_text( scmfile ); - // editor-authored streaming terrain: emit a directive pointing at its 16-bit chunk folder so the - // scenery streams it on load (in every mode) + // heightmap terrain: the directive which opens its folder, so the scenery streams it on load (in every mode) if( EditorTerrain.active() ) { - scmfile << "// editor terrain\neditorterrain " - << EditorTerrain.directory() << ' ' - << EditorTerrain.cells() << ' ' - << EditorTerrain.cellsize() << ' ' - << EditorTerrain.radius() << " endeditorterrain\n"; + scmfile << "// heightmap terrain\nheightmap_terrain " << EditorTerrain.name() << " endheightmap_terrain\n"; } scmfile << "// modified objects\ninclude " << filename << "_export_dirty.scm\n"; @@ -1290,10 +1811,39 @@ state_serializer::export_nodes_to_stream(std::ostream &scmfile, bool Dirty) cons // tracks scmfile << "// paths\n"; for( auto const *path : Paths.sequence() ) { + if( path == nullptr || path->m_editorremoved ) { + continue; + } + if( path->m_road != nullptr ) { + // lanes are generated anew from their road + continue; + } if( path->dirty() == Dirty && path->group() == null_handle ) { path->export_as_text( scmfile ); } } + // roads + scmfile << "// roads\n"; + for( auto const *road : Roads.sequence() ) { + if( road != nullptr && false == road->m_editorremoved && road->dirty() == Dirty && road->group() == null_handle ) { + road->export_as_text( scmfile ); + } + } + for( auto const *junction : Junctions.sequence() ) { + if( junction != nullptr && false == junction->m_editorremoved && junction->dirty() == Dirty && junction->group() == null_handle ) { + junction->export_as_text( scmfile ); + } + } + for( auto const *point : Roadpoints.sequence() ) { + if( point != nullptr && false == point->m_editorremoved && point->dirty() == Dirty && point->group() == null_handle ) { + point->export_as_text( scmfile ); + } + } + for( auto const *sweep : Sweeps.sequence() ) { + if( sweep != nullptr && false == sweep->m_editorremoved && sweep->dirty() == Dirty && sweep->group() == null_handle ) { + sweep->export_as_text( scmfile ); + } + } // traction scmfile << "// traction\n"; for( auto const *traction : Traction.sequence() ) { @@ -1328,6 +1878,7 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st parser.getTokens(2); // name, type nodedata.name = name; nodedata.type = "model"; + nodedata.layer = scene::Layers.active(); // null_handle unless the scenery was opened for editing scene::scratch_data scratch; @@ -1340,10 +1891,100 @@ TAnimModel *state_serializer::create_model(const std::string &src, const std::st cloned->location(position); simulation::Instances.insert(cloned); simulation::Region->insert(cloned); + scene::Layers.count(cloned->layer(), scene::layer_item::model); return cloned; } +std::vector state_serializer::insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed) { + scene::scratch_data scratch; + scratch.trainset.is_open = true; + scratch.trainset.name = Name; + scratch.trainset.track = Path != nullptr ? Path->name() : std::string{}; + scratch.trainset.path = Path; + scratch.trainset.offset = static_cast(Offset); + scratch.trainset.reversed = Reversed; + cParser parser(Vehicles, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction); + auto token { parser.getToken() }; + while (false == token.empty()) { + if (token == "node") { deserialize_node(parser, scratch); } + token = parser.getToken(); + } + auto const vehicles { scratch.trainset.vehicles }; + if (false == vehicles.empty()) { deserialize_endtrainset(parser, scratch); } + return vehicles; +} + +std::pair state_serializer::preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance) { + // statements which take more than a single token, with the tokens ending them + static std::unordered_map const nodeends { + { "dynamic", "enddynamic" }, { "track", "endtrack" }, { "road", "endroad" }, { "junction", "endjunction" }, { "crossing", "endcrossing" }, { "spawn", "endspawn" }, { "despawn", "enddespawn" }, + { "crosswalk", "endcrosswalk" }, { "traction", "endtraction" }, { "tractionpowersource", "end" }, { "model", "endmodel" }, + { "triangles", "endtri" }, { "triangle_strip", "endtri" }, { "triangle_fan", "endtri" }, { "lines", "endline" }, { "line_strip", "endline" }, { "line_loop", "endline" }, + { "memcell", "endmemcell" }, { "eventlauncher", "end" }, { "sound", "endsound" } }; + static std::unordered_map const statementends { + { "event", "endevent" }, { "trainset", "endtrainset" }, { "isolated", "endisolated" }, { "area", "endarea" }, { "assignment", "endassignment" }, + { "atmo", "endatmo" }, { "camera", "endcamera" }, { "config", "endconfig" }, { "description", "enddescription" }, { "light", "endlight" }, + { "sky", "endsky" }, { "test", "endtest" }, { "time", "endtime" }, { "terrain", "endterrain" }, { "heightmap_terrain", "endheightmap_terrain" }, { "editorterrain", "endeditorterrain" } }; + + cParser parser(Directive, cParser::buffer_TEXT, Global.asCurrentSceneryPath, Global.bLoadTraction); + // the template is processed with the placement its directive is going to be loaded with + scene::scratch_data scratch; + scratch.location.offset.emplace(Context.offset); + scratch.location.rotation = Context.rotation; + scratch.location.scale.emplace(Context.scale); + + auto created { 0 }; + auto skipped { 0 }; + auto token { parser.getToken() }; + while (false == token.empty()) { + if (token == "origin") { deserialize_origin(parser, scratch); } + else if (token == "endorigin") { deserialize_endorigin(parser, scratch); } + else if (token == "scale") { deserialize_scale(parser, scratch); } + else if (token == "endscale") { deserialize_endscale(parser, scratch); } + else if (token == "rotate") { deserialize_rotate(parser, scratch); } + else if (token == "node") { + scene::node_data nodedata; + parser.getTokens(4); + parser >> nodedata.range_max >> nodedata.range_min >> nodedata.name >> nodedata.type; + if (nodedata.name == "none") { nodedata.name.clear(); } + nodedata.layer = Layer; + nodedata.instance = Instance; + auto *instance { (nodedata.type == "model" && nodedata.range_min >= 0.0) ? deserialize_model(parser, scratch, nodedata) : nullptr }; + if (instance != nullptr) { + // NOTE: unlike for a model loaded with the scenery, smoke sources of the model aren't set up. + // they'd be left with a dangling owner when the model is replaced after a change of the include + instance->m_preview = true; + simulation::Instances.insert(instance); + simulation::Region->insert(instance); + scene::Hierarchy[instance->uuid.to_string()] = instance; + scene::Layers.count(Layer, scene::layer_item::model); + ++created; + } + else if (nodedata.type != "model" || nodedata.range_min < 0.0) { + // NOTE: a model which failed to load is consumed up to its end already + auto const lookup { nodeends.find(nodedata.type) }; + if (lookup != nodeends.end()) { skip_until(parser, lookup->second); } + ++skipped; + } + } + else if (token == "lua") { + parser.getTokens(1, false); + ++skipped; + } + else { + auto const lookup { statementends.find(token) }; + if (lookup != statementends.end()) { + skip_until(parser, lookup->second); + ++skipped; + } + // anything else is a single token with no parameters, or something the loader wouldn't recognize either + } + token = parser.getToken(); + } + return { created, skipped }; +} + TEventLauncher *state_serializer::create_eventlauncher(const std::string &src, const std::string &name, const glm::dvec3 &position) { cParser parser(src); parser.getTokens(); // "node" diff --git a/simulation/simulationstateserializer.h b/simulation/simulationstateserializer.h index a5d6f2dc..897749e7 100644 --- a/simulation/simulationstateserializer.h +++ b/simulation/simulationstateserializer.h @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/parser.h" #include "scene/scene.h" +#include "scene/scenelayers.h" namespace simulation { @@ -43,8 +44,14 @@ public: export_as_text( std::string const &Scenariofile ) const; // create new model from node stirng TAnimModel * create_model(std::string const &src, std::string const &name, const glm::dvec3 &position); + // places the vehicles, given as their node statements, as a trainset on the path, while the simulation runs. returns: the vehicles made + std::vector insert_trainset(std::string const &Name, TTrack *Path, double const Offset, std::string const &Vehicles, bool const Reversed = false); // create new eventlauncher from node stirng TEventLauncher * create_eventlauncher(std::string const &src, std::string const &name, const glm::dvec3 &position); + // creates model instances defined by an include directive placed or changed in the scenery editor, to show the outcome. + // everything else the included template defines is left out, and takes effect when the saved scenery is loaded. + // Context: placement in effect at the location of the directive. returns: number of created models and of skipped statements + std::pair preview_include(std::string const &Directive, scene::layer_context const &Context, scene::layer_handle Layer, scene::instance_handle Instance); private: // methods @@ -73,14 +80,20 @@ private: void deserialize_time( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_trainset( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_terrain( cParser &Input, scene::scratch_data &Scratchpad ); + void deserialize_heightmapterrain( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_editorterrain( cParser &Input, scene::scratch_data &Scratchpad ); void deserialize_endtrainset( cParser &Input, scene::scratch_data &Scratchpad ); + void deserialize_reversed( cParser &Input, scene::scratch_data &Scratchpad ); TTrack * deserialize_path( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TTraction * deserialize_traction( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TTractionPowerSource * deserialize_tractionpowersource( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TMemCell * deserialize_memorycell( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TEventLauncher * deserialize_eventlauncher( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); TAnimModel * deserialize_model( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); + // loads a run of model instances straight from the text. returns: true if any were loaded, false if the definition at hand is left to the parser + bool deserialize_models( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data &Nodedata, std::size_t Line, std::streamoff Sourcebegin ); + // makes model instance defined by a scenery file a part of the simulation + void insert_model( TAnimModel *Instance, cParser const &Input, std::size_t Line, scene::source_span const &Span ); TDynamicObject * deserialize_dynamic( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); sound_source * deserialize_sound( cParser &Input, scene::scratch_data &Scratchpad, scene::node_data const &Nodedata ); void init_time(); diff --git a/tools/editor/signal_headers.py b/tools/editor/signal_headers.py new file mode 100644 index 00000000..000195ea --- /dev/null +++ b/tools/editor/signal_headers.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +"""Writes //$e descriptions into the signal templates of the scenery directory, so the editor can place them. + +usage: signal_headers.py [--write] +without --write it only reports what it would write +""" + +import os +import re +import sys +import tarfile +import time + +FAMILIES = [ + (re.compile(r'^ss[dow]?\d'), 'main'), + (re.compile(r'^sk(\d|-un)'), 'main'), + (re.compile(r'^sbl\d'), 'block'), + (re.compile(r'^(ts\d|tk\d|top|tsosp)'), 'warning'), + (re.compile(r'^(ps\d|sp3)'), 'repeater'), + (re.compile(r'^(ms\d|zs2nb)'), 'shunt'), + (re.compile(r'^(zs1c|zs2cb|tzm|z1)'), 'stop'), + (re.compile(r'^sz(i|k|y|yn|-ksztalt\d?)\.inc$'), 'substitute'), +] + +COLOURS = [('zielon', 'z'), ('czerwon', 'c'), ('pomara', 'p'), ('żółt', 'p'), ('biał', 'b'), ('mleczn', 'b'), ('niebiesk', 'n'), ('ślep', 'x'), ('brak', 'x')] + +LINEMARK = '//$e' + + +def tokens(text): + text = re.sub(r'//[^\n]*', ' ', text) + text = re.sub(r'/\*.*?\*/', ' ', text, flags=re.S) + return re.split(r'[\s;]+', text.lower()) + + +def statements(words, keyword): + out = [] + idx = 0 + while idx < len(words): + if words[idx] == keyword: + if keyword == 'event': + stop = idx + while stop < len(words) and words[stop] != 'endevent': + stop += 1 + else: + stop = idx + 10 + out.append(words[idx:stop]) + idx += 1 + return out + + +def examine(path, file): + raw = open(path, 'rb').read() # NOSONAR + try: + text = raw.decode('utf-8') + encoding = 'utf-8' + except UnicodeDecodeError: + text = raw.decode('cp1250') + encoding = 'cp1250' + lines = [line for line in text.splitlines() if not line.startswith(LINEMARK)] + body = '\n'.join(lines) + words = tokens(body) + count = max([int(m) for m in re.findall(r'\(p(\d+)\)', body)] or [0]) + + if 'origin' not in words or words[words.index('origin') + 1:words.index('origin') + 4] != ['(p2)', '(p3)', '(p4)']: + return None, 'no origin (p2) (p3) (p4)' + if 'rotate' not in words or words[words.index('rotate') + 1:words.index('rotate') + 4] != ['0', '(p5)', '0']: + return None, 'no rotate 0 (p5) 0' + if count < 5: + return None, 'fewer than 5 parameters' + + models = [n for n in statements(words, 'node') if len(n) > 9 and n[4] == 'model'] + head = next((n for n in models if n[3].startswith('(p1)')), None) + if head is None: + return None, 'no model named after (p1)' + model = head[9] + + cells = {} + for n in statements(words, 'node'): + if len(n) > 8 and n[4] == 'memcell': + cells[n[3]] = n[8] + read = '' + for e in statements(words, 'event'): + velocity = (len(e) > 4 and e[2] == 'getvalues' and cells.get(e[4]) in ('setvelocity', 'shuntvelocity')) or (len(e) > 8 and e[2] == 'putvalues' and e[8] in ('setvelocity', 'shuntvelocity')) + if velocity and e[1].startswith('(p1)'): + if not read or e[1].endswith('_sem_info'): + read = e[1] + + kind = next((k for pattern, k in FAMILIES if pattern.search(file)), None) + if kind is None: + return None, 'not a signal family' + if kind == 'repeater' and cells: + return None, 'repeater with a memory cell' + if kind in ('main', 'block', 'shunt', 'stop', 'substitute') and not read and cells: + return None, 'memory cell which no event reads for the train' + if kind in ('warning', 'repeater'): + read = '' + + models_used = [n[9] for n in models] + lamps = '' + lean = '' + m = re.match(r'sem/(?:[a-z]+)_(\d)_([lpyk])_([a-z]+)[_.]', model) + if m: + lamps = m.group(3) + lean = {'l': 'left', 'y': 'right', 'p': '', 'k': ''}[m.group(2)] + else: + m = re.match(r'sem/karzelki/k(?:[a-z]*?\d|tm|to|s)([zcpbnx]+)[.+]', model) or re.match(r'sem/glowice/(?:s|to)\d(?:i|y[lp])([zcpbnx]+)_dd\.', model) + if m: + lamps = m.group(1) + if not lamps: + for line in lines: + if line.lower().lstrip('/ ').startswith('komory:'): + for word in line.split(':', 1)[1].split(','): + word = word.strip().lower() + lamps += next((c for prefix, c in COLOURS if word.startswith(prefix)), '') + break + + if 'karzelki/' in model or re.search(r'k(w\d+)?(\+.*)?\.inc$', file) or 'pod_betonowa' in body: + mount = 'dwarf' + elif '_drb_' in model and not any(('slupy/' in m or 'post' in m or 'drabinki/' in m) for m in models_used): + mount = 'gantry' + else: + mount = 'mast' + + description = '' + for line in lines: + stripped = line.strip() + if stripped.startswith('//'): + stripped = stripped.lstrip('/').strip() + if stripped.lower().startswith('typ:'): + stripped = stripped[4:].strip() + if stripped: + description = stripped + break + elif stripped: + break + + said = description.lower() + if 'w lewo' in said: + lean = 'left' + elif 'w prawo' in said: + lean = 'right' + elif 'prost' in said: + lean = '' + + params = [ + "{id: 1, role: name, label: Name, default: A}", + "{id: 2, role: pos.x}", + "{id: 3, role: pos.y}", + "{id: 4, role: pos.z}", + "{id: 5, role: rot.y}", + ] + if count >= 6: + params.append("{id: 6, role: texture, label: Plate, default: 0_nothing}" if 'tabl/(p6)' in body.lower() else "{id: 6, role: text, label: Symbol, default: none}") + for id in range(7, count + 1): + label = 'Linked signal' if id == 7 else 'Parameter %d' % id + params.append("{id: %d, role: text, label: %s, default: none}" % (id, label)) + + signal = ['kind: ' + kind, 'mount: ' + mount] + if lamps: + signal.append('lamps: ' + lamps) + if lean: + signal.append('lean: ' + lean) + if read: + signal.append('read: "' + read + '"') + header = [ + 'version: 1', + 'name: ' + file[:-4], + 'category: signal', + ] + if description: + header.append('description: "' + description.replace('\\', '\\\\').replace('"', '\\"') + '"') + header.append('signal: {' + ', '.join(signal) + '}') + header.append('params:') + header += [' - ' + p for p in params] + return {'raw': raw, 'text': text, 'encoding': encoding, 'header': header, 'kind': kind, 'model': model, 'read': read, 'lamps': lamps, 'mount': mount, 'lean': lean, 'count': count}, '' + + +def rewrite(info): + text = info['text'] + eol = '\r\n' if '\r\n' in text else '\n' + bom = text.startswith('') + if bom: + text = text[1:] + kept = [line for line in text.split(eol) if not line.startswith(LINEMARK)] + block = [LINEMARK + ' ' + line for line in info['header']] + out = eol.join(block + kept) + return (('' if bom else '') + out).encode(info['encoding']) + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + return 1 + root = os.path.join(sys.argv[1], 'scenery') + write = '--write' in sys.argv + files = sorted(f for f in os.listdir(root) if f.endswith('.inc')) + files += ['pkp/semafory/' + f for f in sorted(os.listdir(os.path.join(root, 'pkp', 'semafory'))) if f.endswith('.inc')] + done = [] + skipped = [] + for file in files: + name = os.path.basename(file).lower() + if not any(pattern.search(name) for pattern, _ in FAMILIES): + continue + info, reason = examine(os.path.join(root, file), name) + if info is None: + skipped.append((file, reason)) + continue + done.append((file, info)) + + for file, info in done: + print('%-34s %-10s %-6s %-6s %-6s p%d %-22s %s' % (file, info['kind'], info['mount'], info['lean'] or '-', info['lamps'] or '-', info['count'], info['read'] or '-', info['model'])) + for file, reason in skipped: + print('SKIP %-34s %s' % (file, reason)) + kinds = {} + for _, info in done: + kinds[info['kind']] = kinds.get(info['kind'], 0) + 1 + print('described: %d %s, skipped: %d' % (len(done), kinds, len(skipped))) + + if write: + backup = os.path.join(sys.argv[1], 'editor_backup') + os.makedirs(backup, exist_ok=True) # NOSONAR + archive = os.path.join(backup, time.strftime('signals_%Y%m%d_%H%M%S.tar')) + with tarfile.open(archive, 'w') as tar: # NOSONAR + for file, _ in done: + tar.add(os.path.join(root, file), arcname='scenery/' + file) # NOSONAR + for file, info in done: + content = rewrite(info) + if content != info['raw']: + with open(os.path.join(root, file), 'wb') as out: # NOSONAR + out.write(content) + print('written, originals in', archive) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/utilities/Classes.h b/utilities/Classes.h index f79cb851..6a5ca167 100644 --- a/utilities/Classes.h +++ b/utilities/Classes.h @@ -43,6 +43,14 @@ class vehicle_table; class train_table; struct light_array; class particle_manager; +class road_node; // kawałek drogi, z którego powstają pasy ruchu +class road_table; +class sweep_node; // model ułożony wzdłuż krzywej: wygięty albo powtarzany +class sweep_table; +class junction_node; +class junction_table; +class roadpoint_node; // przejazd kolejowy albo miejsce, w którym pojazdy drogowe się pojawiają lub znikają +class roadpoint_table; struct dictionary_source; class trainset_desc; class scenery_desc; @@ -56,6 +64,8 @@ namespace scene { struct node_data; class basic_node; using group_handle = std::size_t; +using layer_handle = std::uint16_t; +using instance_handle = std::uint32_t; } namespace Mtable diff --git a/utilities/Globals.cpp b/utilities/Globals.cpp index 47b04d18..b4f2e14b 100644 --- a/utilities/Globals.cpp +++ b/utilities/Globals.cpp @@ -187,6 +187,12 @@ bool global_settings::ConfigParseAudio(cParser& Parser, const std::string& token return true; } + if (token == "sound.asyncload") + { + ParseOne(Parser, AudioAsyncLoad); + return true; + } + if (token == "sound.volume.vehicle") { ParseOneClamped(Parser, VehicleVolume, 0.f, 1.f); @@ -630,6 +636,15 @@ bool global_settings::ConfigParseSimulation(cParser& Parser, const std::string& return true; } + if (token == "file.binary.terrain.checksum") + { + // off, simple or full; anything else leaves the setting as it was + std::string mode; + ParseOne(Parser, mode, 1, true); + file_binary_terrain_checksum = (mode == "off" ? 0 : mode == "simple" ? 1 : mode == "full" ? 2 : file_binary_terrain_checksum); + return true; + } + if (token == "inactivepause") { ParseOne(Parser, bInactivePause); @@ -769,6 +784,12 @@ bool global_settings::ConfigParseUI(cParser& Parser, const std::string& token) return true; } + if (token == "ui.viewports") + { + ParseOne(Parser, ui_viewports, 1); + return true; + } + if (token == "gui.defaultwindows") { ParseOne(Parser, gui_defaultwindows, 1); @@ -1338,6 +1359,18 @@ global_settings::ConfigParse_gfx( cParser &Parser, std::string_view const Token Parser.getTokens(); Parser >> ResourceMove; } + else if (Token == "gfx.textures.streaming") + { + Parser.getTokens(); + Parser >> gfx_texture_streaming; + } + else if (Token == "gfx.textures.releasedistance") + { + // multiplier of the draw range, 0 turns the release off. textures within the draw range are never released + Parser.getTokens(); + Parser >> gfx_texture_releasedistance; + gfx_texture_releasedistance = (gfx_texture_releasedistance > 0.f ? std::max(gfx_texture_releasedistance, 1.f) : 0.f); + } else if (Token == "gfx.reflections.framerate") { auto const updatespersecond{std::abs(Parser.getToken())}; @@ -1521,6 +1554,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "sound.volume.vehicle", VehicleVolume ); export_as_text( Output, "sound.volume.positional", EnvironmentPositionalVolume ); export_as_text( Output, "sound.volume.ambient", EnvironmentAmbientVolume ); + export_as_text( Output, "sound.asyncload", AudioAsyncLoad ); export_as_text( Output, "physicslog", WriteLogFlag ); export_as_text( Output, "fullphysics", FullPhysics ); export_as_text( Output, "debuglog", iWriteLogEnabled ); @@ -1575,6 +1609,8 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "createswitchtrackbeds", CreateSwitchTrackbeds ); export_as_text( Output, "gfx.resource.sweep", ResourceSweep ); export_as_text( Output, "gfx.resource.move", ResourceMove ); + export_as_text( Output, "gfx.textures.streaming", gfx_texture_streaming ); + export_as_text( Output, "gfx.textures.releasedistance", gfx_texture_releasedistance ); export_as_text( Output, "gfx.reflections.framerate", 1.0 / reflectiontune.update_interval ); export_as_text( Output, "gfx.reflections.fidelity", reflectiontune.fidelity ); export_as_text( Output, "timespeed", fTimeSpeed ); @@ -1582,6 +1618,7 @@ global_settings::export_as_text( std::ostream &Output ) const { export_as_text( Output, "latitude", fLatitudeDeg ); export_as_text( Output, "convertmodels", iConvertModels ); export_as_text( Output, "file.binary.terrain", file_binary_terrain ); + export_as_text( Output, "file.binary.terrain.checksum", std::string( file_binary_terrain_checksum == 0 ? "off" : file_binary_terrain_checksum == 1 ? "simple" : "full" ) ); export_as_text( Output, "inactivepause", bInactivePause ); export_as_text( Output, "slowmotion", iSlowMotionMask ); export_as_text( Output, "hideconsole", bHideConsole ); @@ -1639,6 +1676,7 @@ global_settings::export_as_text( std::ostream &Output ) const { << UITextColor.g * 255 << " " << UITextColor.b * 255 << "\n"; export_as_text( Output, "ui.bg.opacity", UIBgOpacity ); + export_as_text( Output, "ui.viewports", ui_viewports ); export_as_text( Output, "input.gamepad", InputGamepad ); #ifdef WITH_UART if( uart_conf.enable ) { diff --git a/utilities/Globals.h b/utilities/Globals.h index d0f70701..8f5748c3 100644 --- a/utilities/Globals.h +++ b/utilities/Globals.h @@ -84,10 +84,15 @@ struct global_settings { std::string szDefaultExt{ szTexturesDDS }; std::string SceneryFile; std::string local_start_vehicle{ "EU07-424" }; + std::string editor_enter_vehicle; // vehicle placed in the editor, to be entered when the driving goes on + bool editor_session{ false }; // scenery was opened for editing (-edit): loader keeps track of scenery layers and hands over to the editor + bool editor_tracks{ false }; // F3 track workspace: draw and pick tracks without the scenery draw-range cap int iConvertModels{ 0 }; // tworzenie plików binarnych int iConvertIndexRange{ 1000 }; // range of duplicate vertex scan bool file_binary_terrain{ true }; // enable binary terrain (de)serialization + int file_binary_terrain_checksum{ 1 }; // check of a binary terrain file (.btf) against its text: 0 = none, 1 = length of the text, 2 = length and checksum bool file_binary_terrain_state{true}; + int file_binary_terrain_skipped{0}; // number of terrain includes left out by the parser during the current scenery load // logs bool priorityLoadText3D{false}; // ladowanie T3D priorytetowo int iWriteLogEnabled{ 3 }; // maska bitowa: 1-zapis do pliku, 2-okienko, 4-nazwy torów @@ -167,6 +172,8 @@ struct global_settings { bool bUseVBO{ true }; // czy jest VBO w karcie graficznej (czy użyć) float AnisotropicFiltering{ 8.f }; // requested level of anisotropic filtering. TODO: move it to renderer object float FieldOfView{ 45.f }; // vertical field of view for the camera. TODO: move it to the renderer + bool EditorOrtho{ false }; + float EditorOrthoExtent{ 150.f }; GLint iMaxTextureSize{ 4096 }; // maksymalny rozmiar tekstury GLint iMaxCabTextureSize{ 4096 }; // largest allowed texture in vehicle cab int iMultisampling{ 2 }; // tryb antyaliasingu: 0=brak,1=2px,2=4px,3=8px,4=16px @@ -176,6 +183,8 @@ struct global_settings { float SmokeFidelity{ 1.f }; // determines amount of generated smoke particles bool ResourceSweep{ true }; // gfx resource garbage collection bool ResourceMove{ false }; // gfx resources are moved between cpu and gpu side instead of sending a copy + bool gfx_texture_streaming{ false }; // data of dds textures is read by worker threads, the rest of the loading doesn't wait for it + float gfx_texture_releasedistance{ 0.f }; // gl textures of vehicles farther than this many draw ranges are released. 0 = off bool compress_tex{ true }; // all textures are compressed on gpu side std::string asSky{ "1" }; float fFpsAverage{ 0.f }; // oczekiwana wartosć FPS @@ -191,6 +200,7 @@ struct global_settings { float EnvironmentAmbientVolume{ 1.0f }; int audio_max_sources = 30; std::string AudioRenderer; + bool AudioAsyncLoad{ false }; // sound files are decoded by worker threads // input float fMouseXScale{ 1.5f }; float fMouseYScale{ 0.2f }; @@ -297,6 +307,7 @@ struct global_settings { float ui_fontsize = 13.0f; float ui_scale = 1.0f; + bool ui_viewports = true; // panels of the ui can be dragged out of the simulator window, into windows of their own float map_highlight_distance = 3000.0f; diff --git a/utilities/Names.h b/utilities/Names.h index 0533c696..52d438f6 100644 --- a/utilities/Names.h +++ b/utilities/Names.h @@ -11,6 +11,7 @@ http://mozilla.org/MPL/2.0/. #include #include +#include template class basic_table { @@ -31,7 +32,7 @@ public: } auto const itemhandle { m_items.size() - 1 }; // add item name to the map - auto mapping = m_itemmap.emplace( itemname, itemhandle ); + auto mapping = m_itemmap.emplace( std::move( itemname ), itemhandle ); if( true == mapping.second ) { return true; } @@ -42,6 +43,33 @@ public: { return insert(Item, Item->name()); } + // changes the name specified item is found by; empty text or "none" leaves the item without one. + // NOTE: the name held by the item itself is for the caller to change afterwards. + // returns: false if the name is in use by another item or the item isn't in the collection, true otherwise + bool + rename( Type_ const *Item, std::string const &Name ) { + auto const named { false == Name.empty() && Name != "none" }; + if( named ) { + auto const taken { m_itemmap.find( Name ) }; + if( taken != m_itemmap.end() + && m_items[ taken->second ] != nullptr + && m_items[ taken->second ] != Item ) { + return false; } } + // the current name leads to the item unless the item has none, or shares it with an item added later + auto itemhandle { m_items.size() }; + auto const current { m_itemmap.find( Item->name() ) }; + if( current != m_itemmap.end() + && m_items[ current->second ] == Item ) { + itemhandle = current->second; + m_itemmap.erase( current ); } + else { + auto const lookup { std::find( m_items.begin(), m_items.end(), Item ) }; + if( lookup == m_items.end() ) { + return false; } + itemhandle = static_cast( std::distance( m_items.begin(), lookup ) ); } + if( named ) { + m_itemmap[ Name ] = itemhandle; } + return true; } void purge (std::string const &Name) { auto lookup = m_itemmap.find( Name ); diff --git a/utilities/parser.cpp b/utilities/parser.cpp index 77f91ee3..d22cb4d2 100644 --- a/utilities/parser.cpp +++ b/utilities/parser.cpp @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include "utilities/Logs.h" #include "scene/scenenodegroups.h" +#include "scene/scenelayers.h" /* MaSzyna EU07 locomotive simulator parser @@ -24,24 +25,14 @@ http://mozilla.org/MPL/2.0/. namespace { -inline std::array makeBreakTable(const char *brk) -{ - std::array arr{}; - for (unsigned char c : std::string_view(brk ? brk : "")) - { - arr[c] = true; - } - return arr; -} - inline char toLowerChar(char c) { // Only fold ASCII letters. Bytes >= 0x80 belong to multibyte UTF-8 // sequences and must be passed through untouched, otherwise a non-"C" // global locale could remap them and corrupt UTF-8 encoded text. - const unsigned char uc = static_cast(c); - if (uc < 0x80) - return static_cast(std::tolower(uc)); + // The remaining ASCII characters have no lower case form, and don't need the call. + if (c >= 'A' && c <= 'Z') + return static_cast(std::tolower(static_cast(c))); return c; } @@ -69,7 +60,28 @@ cParser::cParser(std::string const &Stream, buffertype const Type, std::string P case buffer_FILE: { Path.append(Stream); - mStream = std::make_shared(Path, std::ios_base::binary); + // the file is read in one go, tokens are then extracted straight from the memory + std::ifstream file(Path, std::ios_base::binary); + if (file.fail()) + { + mFailBit = true; + } + else + { + auto const size{file.rdbuf()->pubseekoff(0, std::ios_base::end)}; + file.rdbuf()->pubseekoff(0, std::ios_base::beg); + if (size > 0 && static_cast(size) < mBuffer.max_size()) + { + mBuffer.resize(static_cast(size)); + file.read(mBuffer.data(), static_cast(mBuffer.size())); + mBuffer.resize(static_cast(std::max(0, file.gcount()))); + } + else if (size != 0) + { + // not a file which can be read, e.g. a directory, which some systems allow to open + mFailBit = true; + } + } // content of *.inc files is potentially grouped together if (Stream.size() >= 4 && ToLower(Stream.substr(Stream.size() - 4)) == ".inc") { @@ -80,7 +92,7 @@ cParser::cParser(std::string const &Stream, buffertype const Type, std::string P } case buffer_TEXT: { - mStream = std::make_shared(Stream); + mBuffer = Stream; break; } default: @@ -89,18 +101,14 @@ cParser::cParser(std::string const &Stream, buffertype const Type, std::string P } } // calculate stream size - if (mStream) + if (true == mFailBit) { - if (true == mStream->fail()) - { - ErrorLog("Failed to open file \"" + Path + "\""); - } - else - { - mSize = mStream->rdbuf()->pubseekoff(0, std::ios_base::end); - mStream->rdbuf()->pubseekoff(0, std::ios_base::beg); - mLine = 1; - } + ErrorLog("Failed to open file \"" + Path + "\""); + } + else + { + mSize = static_cast(mBuffer.size()); + mLine = 1; } // set parameter set if one was provided if (false == Parameters.empty()) @@ -118,6 +126,15 @@ cParser::~cParser() // wrap up the node group holding content of processed file scene::Groups.close(); } + if (true == mLayerFile) + { + // content of the processed file ended, following nodes belong to the parent layer again + scene::Layers.close(); + } + if (true == mInstanceFile) + { + scene::Layers.instance_end(); + } } template <> glm::vec3 cParser::getToken(bool const ToLower, char const *Break) @@ -137,7 +154,7 @@ template <> cParser &cParser::operator>>(std::string &Right) return *this; } - Right = this->tokens.front(); + Right = std::move(this->tokens.front()); this->tokens.pop_front(); return *this; @@ -157,6 +174,25 @@ template <> cParser &cParser::operator>>(bool &Right) return *this; } +template <> std::string cParser::getToken(bool const ToLower, const char *Break) +{ + // does what the generic version would, minus the trip of the token through the queue + if (true == m_autoclear) + { + tokens.clear(); + } + std::string token; + if (false == tokens.empty()) + { + *this >> token; + } + else + { + readToken(token, ToLower, Break); + } + return token; +} + template <> bool cParser::getToken(bool const ToLower, const char *Break) { @@ -224,30 +260,147 @@ bool cParser::getTokens(unsigned int Count, bool ToLower, const char *Break) return true; } -std::string cParser::readTokenFromStream(bool ToLower, const char *Break) +// mimics std::istream::peek() != EOF of the stream the parser used to read from +bool cParser::hasChar() { - std::string token; - token.reserve(64); + if (mEofBit || mFailBit) + { + // an attempt to read from a stream which isn't in good state marks it as failed + mFailBit = true; + return false; + } + if (static_cast(mPosition) >= mBuffer.size()) + { + mEofBit = true; + return false; + } + return true; +} - const auto breakTable = makeBreakTable(Break); +// mimics std::istream::get(char&). NOTE: doesn't advance the read position, the caller does +bool cParser::getChar(char &Char) +{ + if (mEofBit || mFailBit) + { + mFailBit = true; + return false; + } + if (static_cast(mPosition) >= mBuffer.size()) + { + // failed extraction at the end of data sets both flags + mEofBit = true; + mFailBit = true; + return false; + } + Char = mBuffer[static_cast(mPosition)]; + return true; +} + +// prepares character lookup for specified set of token separators +void cParser::updateCharClasses(char const *Break) +{ + if (Break == nullptr) + { + Break = ""; + } + if (mCharClassesValid && std::strcmp(mCharClassesBreak.c_str(), Break) == 0) + { + // the set of separators rarely changes from one token to another + return; + } + mCharClassesBreak = Break; + mCharClasses.fill(0); + for (unsigned char const c : mCharClassesBreak) + { + mCharClasses[c] |= char_break; + } + // characters which need more than being added to the token: + // line ends are counted, + mCharClasses[static_cast('\n')] |= char_special; + // quotes glue the text they enclose into a single token, + mCharClasses[static_cast('\"')] |= char_special; + // and a comment can start only once the last character of its opening mark arrives + for (auto const &comment : mComments) + { + for (auto character{0}; character < 256; ++character) + { + // NOTE: the mark is compared with the token, which can hold the character converted to lower case + if (comment.first.empty() || comment.first.back() == static_cast(character) || comment.first.back() == toLowerChar(static_cast(character))) + { + mCharClasses[character] |= char_special; + } + } + } + mCharClassesValid = true; +} + +void cParser::readTokenFromStream(std::string &token, bool ToLower, const char *Break) +{ + token.clear(); + + updateCharClasses(Break); char c = 0; + bool separated = false; // the token was ended by a separator, which isn't a part of it - while (token.empty() && mStream->peek() != EOF) { - while (mStream->peek() != EOF) { // idk why but with mStream->get(c) not all cars are loaded - c = static_cast(mStream->get()); + while (token.empty() && hasChar()) { + while (hasChar()) { + auto const *const data{mBuffer.data()}; + auto const size{mBuffer.size()}; + auto const begin{static_cast(mPosition)}; + if (mCharClasses[static_cast(data[begin])] == 0) + { + // run of regular characters, which only need to be added to the token + auto end{begin + 1}; + while (end < size && mCharClasses[static_cast(data[end])] == 0) + { + ++end; + } + if (token.empty()) + { + mTokenBegin = mPosition; + } + auto const converted{token.size()}; + token.append(data + begin, end - begin); + if (ToLower) + { + for (auto index{converted}; index < token.size(); ++index) + { + token[index] = toLowerChar(token[index]); + } + } + mPosition = static_cast(end); + continue; + } + + c = data[begin]; + ++mPosition; if (c == '\n') { ++mLine; } const unsigned char uc = static_cast(c); - if (breakTable[uc]) { + if (mCharClasses[uc] & char_break) { // separator ends token (or continues skipping if token empty) - if (!token.empty()) + if (!token.empty()) { + separated = true; break; + } + // the rest of a run of separators is skipped right away + auto next{begin + 1}; + while (next < size && (mCharClasses[static_cast(data[next])] & char_break)) { + if (data[next] == '\n') { + ++mLine; + } + ++next; + } + mPosition = static_cast(next); continue; } + if (token.empty()) { + mTokenBegin = mPosition - 1; + } if (ToLower) c = toLowerChar(c); token.push_back(c); @@ -259,8 +412,8 @@ std::string cParser::readTokenFromStream(bool ToLower, const char *Break) } } } - - return token; + // NOTE: comment glued to the end of a token is counted as a part of it + mTokenEnd = separated ? mPosition - 1 : mPosition; } void cParser::stripFirstTokenBOM(std::string& token, bool ToLower, const char* Break) { @@ -269,6 +422,7 @@ void cParser::stripFirstTokenBOM(std::string& token, bool ToLower, const char* B if (startsWithBOM(token)) { token.erase(0, 3); + mTokenBegin += 3; } // if first "token" was standalone BOM, read the next real token (avoid recursion) @@ -331,10 +485,22 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri return; } + // layers are named by their files relative to the scenery folder; the terrain files (.txtf) come from includes made up by + // the loader, relative to the folder of the simulator + auto layername{mPath + includefile}; + layername = (layername.starts_with(Global.asCurrentSceneryPath) ? layername.substr(Global.asCurrentSceneryPath.size()) : includefile); + const bool isTerrain = contains(includefile, "_ter.scm"); if (isTerrain && true == Global.file_binary_terrain_state) { WriteLog("SBT found, ignoring: " + includefile); readParameters(srcParser); // preserve original side-effect: still consume parameters + ++Global.file_binary_terrain_skipped; + if (sceneryLayers) + { + // the file is still a part of the scenery, even though its content comes from the binary terrain file + scene::Layers.layer(scene::Layers.open(layername, include_site())).binary = true; + scene::Layers.close(); + } return; } @@ -354,11 +520,51 @@ void cParser::startIncludeFromParser(cParser& srcParser, bool ToLower, std::stri if (mIncludeParser->mSize <= 0) { ErrorLog("Bad include: can't open file \"" + includefile + "\""); } + + if (sceneryLayers) + { + mIncludeParser->sceneryLayers = true; + // content of an included scenery file forms a layer of its own. *.inc files are node templates + // reused all over the scenery, so their content stays in the layer of the file which includes them + if (mIncludeParser->mIncFile) + { + scene::Layers.template_used(includefile); + // include of a template made directly by a layer file is handled by the editor as a whole. templates + // included by that template are a part of it + auto const site{include_site()}; + if (false == site.fixed && scene::Layers.instance() == 0 && mIncludeParser->mSize > 0) + { + scene::Layers.instance_begin(includefile, site.span); + mIncludeParser->mInstanceFile = true; + } + } + else if (mIncludeParser->mSize > 0) + { + auto site{include_site()}; + site.parameters = false == mIncludeParser->parameters.empty(); + scene::Layers.open(layername, site); + mIncludeParser->mLayerFile = true; + } + } +} + +// describes the include directive being processed, for the scenery layer bookkeeping +scene::include_site cParser::include_site() const +{ + scene::include_site site; + // the directive can be rewritten on scenery save only if we know where it is, and it sits in a layer file + site.fixed = mIncludeBegin < 0 || mFile.empty() || mIncFile; + if (false == site.fixed) + { + site.span = {mIncludeBegin, mTokenEnd}; + } + return site; } bool cParser::handleIncludeIfPresent(std::string& token, bool ToLower, const char* Break) { // token-mode include: token == "include" if (expandIncludes && token == "include") { + mIncludeBegin = mTokenBegin; std::string includefile; if (allowRandomIncludes) includefile = deserialize_random_set(*this); @@ -374,6 +580,7 @@ bool cParser::handleIncludeIfPresent(std::string& token, bool ToLower, const cha // line-mode HACK: Break == "\n\r" and line begins with "include" if (std::strcmp(Break, "\n\r") == 0 && token.compare(0, 7, "include") == 0) { + mIncludeBegin = -1; // the directive is parsed out of a line of text, its exact location isn't known cParser includeparser(token.substr(7)); std::string includefile; if (allowRandomIncludes) @@ -398,19 +605,30 @@ void cParser::readToken(std::string &out, bool ToLower, const char *Break) if (out.empty()) { mIncludeParser = nullptr; - out = readTokenFromStream(ToLower, Break); + readTokenFromStream(out, ToLower, Break); } } else { - out = readTokenFromStream(ToLower, Break); + readTokenFromStream(out, ToLower, Break); } - stripFirstTokenBOM(out, ToLower, Break); + // NOTE: the checks repeat what the called methods test first, to skip the calls for vast majority of the tokens + if (mFirstToken) + { + stripFirstTokenBOM(out, ToLower, Break); + } - substituteParameters(out, ToLower); + if (false == parameters.empty()) + { + substituteParameters(out, ToLower); + } - handleIncludeIfPresent(out, ToLower, Break); + if (out.size() >= 7 && out[0] == 'i') + { + // nothing else can be an include directive + handleIncludeIfPresent(out, ToLower, Break); + } } std::vector cParser::readParameters(cParser &Input) @@ -432,8 +650,9 @@ std::string cParser::readQuotes(char const Quote) std::string token; char c{0}; bool escaped = false; - while (mStream->get(c)) + while (getChar(c)) { // get all chars until the quote mark + ++mPosition; if (escaped) { escaped = false; @@ -459,24 +678,23 @@ std::string cParser::readQuotes(char const Quote) void cParser::skipComment(std::string const &Endmark) { // pobieranie znaków aż do znalezienia znacznika końca - std::string input; char c{0}; - auto const endmarksize = Endmark.size(); - while (mStream->get(c)) + if (false == getChar(c)) { - if (c == '\n') - { - // update line counter - ++mLine; - } - input += c; - if (input == Endmark) // szukanie znacznika końca - break; - if (input.size() >= endmarksize) - { - // keep the read text short, to avoid pointless string re-allocations on longer comments - input = input.substr(1); - } + return; + } + auto const begin{static_cast(mPosition)}; + // szukanie znacznika końca + // NOTE: with nothing to look for the comment runs until the end of data + auto const found{Endmark.empty() ? std::string::npos : mBuffer.find(Endmark, begin)}; + auto const end{found != std::string::npos ? found + Endmark.size() : mBuffer.size()}; + // update line counter + mLine += static_cast(std::count(mBuffer.data() + begin, mBuffer.data() + end, '\n')); + mPosition = static_cast(end); + if (found == std::string::npos) + { + // the data ended before the comment did; record the attempt to read past the end + getChar(c); } return; } @@ -523,12 +741,13 @@ void cParser::injectString(const std::string &str) { mIncludeParser = std::make_shared(str, buffer_TEXT, "", LoadTraction, std::vector(), allowRandomIncludes); mIncludeParser->autoclear(m_autoclear); + mIncludeParser->sceneryLayers = sceneryLayers; } } int cParser::getProgress() const { - return static_cast(mStream->rdbuf()->pubseekoff(0, std::ios_base::cur) * 100 / mSize); + return (mSize > 0 ? static_cast(mPosition * 100 / mSize) : 100); } int cParser::getFullProgress() const @@ -566,6 +785,8 @@ void cParser::addCommentStyle(std::string const &Commentstart, std::string const { mComments.insert(commentmap::value_type(Commentstart, Commentend)); + // the character lookup depends on the comment marks + mCharClassesValid = false; } // returns name of currently open file, or empty string for text type stream @@ -600,3 +821,59 @@ int cParser::LineMain() const { return mIncludeParser ? -1 : mLine; } + +std::streamoff cParser::TokenBegin() const +{ + return mIncludeParser ? mIncludeParser->TokenBegin() : mTokenBegin; +} + +std::streamoff cParser::TokenEnd() const +{ + return mIncludeParser ? mIncludeParser->TokenEnd() : mTokenEnd; +} + +bool cParser::InLayerFile() const +{ + return mIncludeParser ? mIncludeParser->InLayerFile() : (false == mFile.empty() && false == mIncFile); +} + +std::string_view cParser::remainingText() const +{ + if (false == tokens.empty()) + { + return {}; + } + // each parser on the way gets to change the tokens of the file being read + auto const *source{this}; + while (true) + { + if (false == source->parameters.empty() || false == source->skipComments || source->mComments.size() != 2) + { + return {}; + } + if (source->mIncludeParser == nullptr) + { + break; + } + source = source->mIncludeParser.get(); + } + // NOTE: the first token of a file is left to the regular code, which deals with the byte order mark + if (source->mFirstToken || source->mEofBit || source->mFailBit || static_cast(source->mPosition) >= source->mBuffer.size()) + { + return {}; + } + return std::string_view{source->mBuffer}.substr(static_cast(source->mPosition)); +} + +void cParser::skipText(std::size_t const Count, std::size_t const Lines, std::size_t const Tokenlength) +{ + if (mIncludeParser) + { + mIncludeParser->skipText(Count, Lines, Tokenlength); + return; + } + mPosition = static_cast(std::min(static_cast(mPosition) + Count, mBuffer.size())); + mLine += Lines; + mTokenEnd = mPosition; + mTokenBegin = mPosition - static_cast(std::min(Tokenlength, static_cast(mPosition))); +} diff --git a/utilities/parser.h b/utilities/parser.h index 131efd8b..19fc34f1 100644 --- a/utilities/parser.h +++ b/utilities/parser.h @@ -10,10 +10,19 @@ http://mozilla.org/MPL/2.0/. #pragma once #include +#include #include #include #include #include +#include +#include +#include + +namespace scene +{ +struct include_site; +} ///////////////////////////////////////////////////////////////////////////////////////////////////// // cParser -- generic class for parsing text data, either from file or provided string @@ -56,11 +65,11 @@ class cParser //: public std::stringstream inline bool eof() { - return mStream->eof(); }; + return mEofBit; }; inline bool ok() { - return !mStream->fail(); }; + return !mFailBit; }; cParser & autoclear( bool const Autoclear ); inline @@ -69,7 +78,7 @@ class cParser //: public std::stringstream return m_autoclear; } bool getTokens( unsigned int Count = 1, bool ToLower = true, char const *Break = "\n\r\t ;" ); - std::string readTokenFromStream(bool ToLower, const char *Break); + void readTokenFromStream(std::string &token, bool ToLower, const char *Break); void stripFirstTokenBOM(std::string &token, bool ToLower, const char *Break); void substituteParameters(std::string &token, bool ToLower); void skipIncludeBlock(); @@ -94,12 +103,58 @@ class cParser //: public std::stringstream std::size_t Line() const; // returns number of currently processed line in main file, -1 if inside include int LineMain() const; + // location of the most recently read token in its source file, as byte offsets + std::streamoff TokenBegin() const; + std::streamoff TokenEnd() const; + // true if the most recently read token came straight from a scenery file which isn't an *.inc node template + bool InLayerFile() const; + // text which follows the most recently read token, for the readers which take entries of a known kind straight from + // the text instead of token by token. empty if the tokens can't be taken that way: some wait to be picked up, there + // are parameters of an include to put in them, or comment marks other than the standard ones are in use + std::string_view remainingText() const; + // moves past specified amount of the text returned by remainingText(): Lines is the number of line breaks in it, + // Tokenlength the length of the token it ends with + void skipText(std::size_t Count, std::size_t Lines, std::size_t Tokenlength); + // converts text holding a plain number without involving a stream. returns: true on success, false if the text needs regular treatment + template + static bool parseNumber( std::string_view Token, Type_ &Output ); bool expandIncludes = true; bool allowRandomIncludes = false; bool skipComments = true; + bool sceneryLayers = false; // included scenery files are registered as scenery layers (scenery opened for editing) private: + // types: + // retrieved tokens waiting for pick up. a vector with moving head instead of a deque, + // as the latter can allocate memory for each stored item + struct token_queue { + std::vector items; + std::size_t head { 0 }; + + bool empty() const { + return head == items.size(); } + std::size_t size() const { + return items.size() - head; } + std::string & front() { + return items[ head ]; } + std::string const & front() const { + return items[ head ]; } + void emplace_back( std::string &&Token ) { + items.emplace_back( std::move( Token ) ); } + void pop_front() { + ++head; + if( head == items.size() ) { + clear(); } + else if( head >= 64 && head * 2 >= items.size() ) { + // keep the queue from growing without end if it never gets fully emptied + items.erase( items.begin(), items.begin() + static_cast( head ) ); + head = 0; } } + void clear() { + items.clear(); + head = 0; } + }; void startIncludeFromParser(cParser &srcParser, bool ToLower, std::string includefile); + scene::include_site include_site() const; bool handleIncludeIfPresent(std::string &token, bool ToLower, const char *Break); // methods: void readToken(std::string& out, bool ToLower = true, const char *Break = "\n\r\t ;"); @@ -109,15 +164,34 @@ class cParser //: public std::stringstream bool findQuotes( std::string &String ); bool trimComments( std::string &String ); std::size_t count(); + // input access. these mimic peek() and get() of the stream the parser used to read from, + // including their effect on the state reported by eof() and ok() + bool hasChar(); + bool getChar( char &Char ); + void updateCharClasses( char const *Break ); // members: bool m_autoclear { true }; // unretrieved tokens are discarded when another read command is issued (legacy behaviour) bool LoadTraction { true }; // load traction? - std::shared_ptr mStream; // relevant kind of buffer is attached on creation. + std::string mBuffer; // content of the open file or the provided text, loaded on creation. + bool mEofBit { false }; // input state, maintained the way a stream would do it + bool mFailBit { false }; + enum charclass : unsigned char { + char_break = 1, // separates tokens + char_special = 2 }; // can't be simply added to the token: line end, quote, or potential end of a comment mark + std::array mCharClasses {}; // lookup built for the most recently used set of token separators + std::string mCharClassesBreak; // set of separators the lookup was built for + bool mCharClassesValid { false }; std::string mFile; // name of the open file, if any std::string mPath; // path to open stream, for relative path lookups. std::streamoff mSize { 0 }; // size of open stream, for progress report. std::size_t mLine { 0 }; // currently processed line bool mIncFile { false }; // the parser is processing an *.inc file + bool mLayerFile { false }; // the parser is processing a file registered as scenery layer + bool mInstanceFile { false }; // the parser is processing a template whose include is registered with the scenery layers + std::streamoff mPosition { 0 }; // amount of bytes read from the stream so far + std::streamoff mTokenBegin { 0 }; // location of the most recently read token + std::streamoff mTokenEnd { 0 }; + std::streamoff mIncludeBegin { -1 }; // location of the include directive being processed, -1 if unknown bool mFirstToken { true }; // processing first token in the current file; helper used when checking for utf bom typedef std::map commentmap; commentmap mComments { @@ -125,7 +199,7 @@ class cParser //: public std::stringstream commentmap::value_type( "//", "\n" ) }; std::shared_ptr mIncludeParser; // child class to handle include directives. std::vector parameters; // parameter list for included file. - std::deque tokens; + token_queue tokens; }; @@ -134,14 +208,53 @@ glm::vec3 cParser::getToken( bool const ToLower, const char *Break ); +template +bool +cParser::parseNumber( std::string_view const Token, Type_ &Output ) { + + using type = std::remove_cv_t; + if constexpr( +#ifdef __cpp_lib_to_chars + std::is_floating_point_v || +#endif + ( std::is_integral_v + && false == std::is_same_v + && false == std::is_same_v + && false == std::is_same_v + && false == std::is_same_v + && false == std::is_same_v + && false == std::is_same_v + && false == std::is_same_v + && false == std::is_same_v ) ) { + + auto const *first { Token.data() }; + auto const *last { first + Token.size() }; + // only text which is a number from start to end is converted here. anything else, including forms + // the stream extraction doesn't recognize (inf, nan), is left for the stream to make sense of + auto const *digits { ( first != last && *first == '-' ) ? first + 1 : first }; + if( digits == last ) { return false; } + if( ( *digits < '0' || *digits > '9' ) && *digits != '.' ) { return false; } + type value {}; + auto const result { std::from_chars( first, last, value ) }; + if( result.ec != std::errc() || result.ptr != last ) { return false; } + Output = value; + return true; + } + else { + return false; + } +} + template cParser& cParser::operator>>( Type_ &Right ) { if( true == this->tokens.empty() ) { return *this; } - std::stringstream converter( this->tokens.front() ); - converter >> Right; + if( false == parseNumber( this->tokens.front(), Right ) ) { + std::stringstream converter( this->tokens.front() ); + converter >> Right; + } this->tokens.pop_front(); return *this; @@ -158,3 +271,7 @@ cParser::operator>>( bool &Right ); template<> bool cParser::getToken( bool const ToLower, const char *Break ); + +template<> +std::string +cParser::getToken( bool const ToLower, const char *Break ); diff --git a/utilities/translation.cpp b/utilities/translation.cpp index 0885ab7c..b1db1227 100644 --- a/utilities/translation.cpp +++ b/utilities/translation.cpp @@ -30,6 +30,11 @@ void locale::init() while (parse_translation(stream)); + // texts of the scenery editor, kept apart from the translation of the simulator + std::fstream editor("lang/editor_" + Global.asLang + ".po", std::ios_base::in | std::ios_base::binary); + if (editor.is_open()) + while (parse_translation(editor)); + WriteLog("translation: " + std::to_string(lang_mapping.size()) + " strings loaded"); } diff --git a/utilities/uuid.hpp b/utilities/uuid.hpp index 5eeb89f4..f68ad49b 100644 --- a/utilities/uuid.hpp +++ b/utilities/uuid.hpp @@ -28,23 +28,17 @@ public: } std::string to_string() const { - std::ostringstream os; - os << std::hex << std::setfill('0'); - auto put = [&](int i){ - os << std::setw(2) << static_cast(bytes[i]); - }; - // format 8-4-4-4-12 - for (int i = 0; i < 4; ++i) put(i); - for (int i = 4; i < 6; ++i) put(i); - os << '-'; - for (int i = 6; i < 8; ++i) put(i); - os << '-'; - for (int i = 8; i < 10; ++i) put(i); - os << '-'; - for (int i = 10; i < 12; ++i) put(i); - os << '-'; - for (int i = 12; i < 16; ++i) put(i); - return os.str(); + // NOTE: done by hand as the text is made for each model instance of a scenery, and a string stream took a few microseconds each + static constexpr char digits[] = "0123456789abcdef"; + std::string text; + text.reserve(36); + // format 12-4-4-4-8, the one node names in scenery files were written with + for (int i = 0; i < 16; ++i) { + if (i == 6 || i == 8 || i == 10 || i == 12) text += '-'; + text += digits[bytes[i] >> 4]; + text += digits[bytes[i] & 0x0F]; + } + return text; } static UID from_string(const std::string& str) { diff --git a/vehicle/Driver.cpp b/vehicle/Driver.cpp index 4437950b..75bbb29e 100644 --- a/vehicle/Driver.cpp +++ b/vehicle/Driver.cpp @@ -154,6 +154,11 @@ const double HeavyPassengetTrainAcceleration = 0.20; const double CargoTrainAcceleration = 0.25; const double HeavyCargoTrainAcceleration = 0.10; const double PrepareTime = 2.0; //[s] przebłyski świadomości przy odpalaniu +const double FSOverchargePipePress = 5.2; +const double FSOverchargeTime = 2.0; +const double FSOverchargeCRP = 0.05; +const double FSMaxTime = 10.0; +const double DepartBlockedFSDelay = 10.0; bool WriteLogFlag = false; double const deltalog = 0.05; // przyrost czasu @@ -648,7 +653,7 @@ void TController::TableTraceRoute(double fDistance, TDynamicObject *pVehicle) routewanted = 1 + std::floor( Random( static_cast( pTrack->RouteCount() ) - 0.001 ) ); } */ - auto const routewanted { 1 + std::floor( Random( static_cast( pTrack->RouteCount() ) - 0.001 ) ) }; + auto const routewanted { pTrack->RouteDraw() }; sSpeedTable[iLast].iFlags |= (pTrack->CrossSegment(fLastDir < 0 ? tLast->iPrevDirection : tLast->iNextDirection, /* @@ -1410,7 +1415,7 @@ TController::TableUpdateEvent( double &Velocity, TCommandType &Command, TSpeedPo } // jeśli jest mienięty poprzedni semafor a wcześniej // byl nowy to go dorzucamy do zmiennej, żeby cały czas widział najbliższy - if( SemNextIndex == npos ) { + if( SemNextIndex == npos ) { // NOSONAR SemNextIndex = Pointindex; if( Global.iWriteLogEnabled & 8 ) { WriteLog( "Speed table update for " + OwnerName() + ", next semaphor is " + sSpeedTable[ SemNextIndex ].GetName() ); @@ -6009,6 +6014,17 @@ TController::determine_consist_state() { && fReady < 0.8 ) { // delikatniejszy warunek, obejmuje wszystkie wagony Ready = true; //żeby uznać za odhamowany } + auto const departblocked { + AIControllFlag + && mvOccupied->Vel < EU07_AI_NOMOVEMENT + && VelDesired > 0.0 + && ( false == Ready || fReady > 0.4 ) }; + if( false == departblocked ) { + DepartBlockedStart = -1.0; + } + else if( DepartBlockedStart < 0.0 ) { + DepartBlockedStart = ElapsedTime; + } // second pass, for diesel engines verify the (live) engines are fully started // TODO: cache presence of diesel engines in the consist, to skip this test if there isn't any p = pVehicles[ end::front ]; // pojazd na czole składu @@ -8014,8 +8030,9 @@ void TController::control_main_pipe() { || mvOccupied->BrakeHandle == TBrakeHandle::MHZ_K8P || mvOccupied->BrakeHandle == TBrakeHandle::M394 ) { - if( (iDrivigFlags & moveOerlikons || true == IsCargoTrain) && BrakeCtrlPosition == gbh_RP && AbsAccS < 0.03 && AccDesired > -0.03 && VelDesired - mvOccupied->Vel > 2.0 && fReady > 0.35 && - mvOccupied->EqvtPipePress < 4.5 // don't charge with a risk of overcharging the main pipe + auto const departblocked { is_departure_blocked( DepartBlockedFSDelay ) }; + if( (iDrivigFlags & moveOerlikons || true == IsCargoTrain || departblocked) && BrakeCtrlPosition == gbh_RP && AbsAccS < 0.03 && AccDesired > -0.03 && VelDesired - mvOccupied->Vel > 2.0 && fReady > 0.35 && + ( mvOccupied->EqvtPipePress < 4.5 || departblocked ) // don't charge with a risk of overcharging the main pipe && mvOccupied->Compressor >= 7.5 // don't charge without sufficient pressure in the tank && BrakeChargingCooldown >= 0.0 // don't charge while cooldown is active && (ActualProximityDist > 100.0 // don't charge if we're about to be braking soon @@ -8026,14 +8043,66 @@ void TController::control_main_pipe() { BrakeChargingCooldown = -1 * std::clamp(iVehicles * 3, 30, 90); } + update_brake_charging(); + if( mvOccupied->Compressor < 5.0 || ( BrakeCtrlPosition < gbh_RP - && mvOccupied->EqvtPipePress > (fReady < 0.25 ? 5.1 : 5.2) ) ) { + && ( mvOccupied->EqvtPipePress > (fReady < 0.25 ? 5.1 : 5.2) || is_brake_charging_done() ) ) ) { cue_action( driver_hint::trainbrakerelease ); } } } +void TController::update_brake_charging() { + + if( std::abs( BrakeCtrlPosition - gbh_FS ) >= 0.5 ) { + BrakeChargingStart = -1.0; + BrakeChargingPipeOverchargeStart.clear(); + return; + } + if( BrakeChargingStart < 0.0 ) { + BrakeChargingStart = ElapsedTime; + } + for( auto const *vehicle { pVehicles[ end::front ] }; vehicle != nullptr; vehicle = vehicle->Next() ) { + auto const isovercharged { + vehicle->MoverParameters != mvOccupied + && vehicle->MoverParameters->PipePress > FSOverchargePipePress }; + if( isovercharged ) { + BrakeChargingPipeOverchargeStart.try_emplace( vehicle, ElapsedTime ); + } + else { + BrakeChargingPipeOverchargeStart.erase( vehicle ); + } + } +} + +bool TController::is_departure_blocked( double const Delay ) const { + + return DepartBlockedStart >= 0.0 + && ElapsedTime - DepartBlockedStart > Delay; +} + +double TController::consist_max_crp() const { + + auto result { 0.0 }; + for( auto const *vehicle { pVehicles[ end::front ] }; vehicle != nullptr; vehicle = vehicle->Next() ) { + result = std::max( result, vehicle->MoverParameters->Hamulec->GetCRP() ); + } + return result; +} + +bool TController::is_brake_charging_done() const { + + if( BrakeChargingStart < 0.0 ) { + return false; + } + return ElapsedTime - BrakeChargingStart > FSMaxTime + || consist_max_crp() > mvOccupied->HighPipePress + FSOverchargeCRP + || std::ranges::any_of( + BrakeChargingPipeOverchargeStart, + [this]( auto const &Vehicle ) { return ElapsedTime - Vehicle.second > FSOverchargeTime; } ); +} + void TController::check_route_ahead( double const Range ) { diff --git a/vehicle/Driver.h b/vehicle/Driver.h index 644b8122..da14b1b3 100644 --- a/vehicle/Driver.h +++ b/vehicle/Driver.h @@ -348,6 +348,10 @@ private: void control_tractive_and_braking_force(); void control_releaser(); void control_main_pipe(); + void update_brake_charging(); + bool is_brake_charging_done() const; + double consist_max_crp() const; + bool is_departure_blocked( double const Delay ) const; void control_relays(); void control_motor_connectors(); void control_tractive_force(); @@ -450,6 +454,9 @@ private: double ReactionTime = 0.0; // czas reakcji Ra: czego i na co? świadomości AI double fBrakeTime = 0.0; // wpisana wartość jest zmniejszana do 0, gdy ujemna należy zmienić nastawę hamulca double BrakeChargingCooldown{}; // prevents the ai from trying to charge the train brake too frequently + double BrakeChargingStart = -1.0; + std::unordered_map BrakeChargingPipeOverchargeStart; + double DepartBlockedStart = -1.0; TBrakeSystem BrakeSystem = TBrakeSystem::Individual; //type of main brake bool ForcePNBrake = false; //is it necessary to use PN brake instead of EP brake int DynamicBrakeTest = 0; //is it necessary to make brake test while driving diff --git a/vehicle/DynObj.cpp b/vehicle/DynObj.cpp index 6af23f07..32ba307f 100644 --- a/vehicle/DynObj.cpp +++ b/vehicle/DynObj.cpp @@ -1422,7 +1422,7 @@ TDynamicObject * TDynamicObject::ABuScanNearestObject(glm::vec3 pos, TTrack *Tra ActDist += CurrDist; if (ScanDir > 0) // do przodu { - if (Track->iNextDirection) + if (Track->iNextDirection) // NOSONAR { Track = Track->CurrentNext(); ScanDir = -ScanDir; @@ -1432,7 +1432,7 @@ TDynamicObject * TDynamicObject::ABuScanNearestObject(glm::vec3 pos, TTrack *Tra } else // do tyłu { - if (Track->iPrevDirection) + if (Track->iPrevDirection) // NOSONAR Track = Track->CurrentPrev(); else { @@ -1917,11 +1917,11 @@ void TDynamicObject::place_on_track(TTrack *Track, double fDist, bool Reversed) switch (iNumAxles) { // Ra: pojazdy wstawiane są na tor początkowy, a potem przesuwane case 2: // ustawianie osi na torze - Axle0.Init(Track, this, iDirection ? 1 : -1); Axle0.Reset(); + Axle0.Init(Track, this, iDirection ? 1 : -1); Axle0.Move((iDirection ? fDist : -fDist) + fAxleDistHalf, false); - Axle1.Init(Track, this, iDirection ? 1 : -1); Axle1.Reset(); + Axle1.Init(Track, this, iDirection ? 1 : -1); Axle1.Move((iDirection ? fDist : -fDist) - fAxleDistHalf, false); // false, żeby nie generować eventów break; } @@ -2275,7 +2275,8 @@ TDynamicObject::Init(std::string Name, // nazwa pojazdu, np. "EU07-424" if (MoverParameters->CategoryFlag & 2) // jeśli samochód { // ustawianie samochodow na poboczu albo na środku drogi - if( Track->fTrackWidth < 3.5 ) // jeśli droga wąska + if( Track->fTrackWidth < 3.5 // jeśli droga wąska + || Track->m_road != nullptr ) // a lane of a road node is one-way, the vehicle keeps to its middle MoverParameters->OffsetTrackH = 0.0; // to stawiamy na środku, niezależnie od stanu // ruchu else if( driveractive ) {// od 3.5m do 8.0m jedzie po środku pasa, dla diff --git a/widgets/map.cpp b/widgets/map.cpp index 25beb794..575fbf21 100644 --- a/widgets/map.cpp +++ b/widgets/map.cpp @@ -308,13 +308,16 @@ void ui::map_panel::render_contents() ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)); - ImGui::ImageButton((ImTextureID)(intptr_t)(m_tex->id), + // imgui 1.89+ takes the frame padding of the image button from the style + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + ImGui::ImageButton("##map", + (ImTextureID)(intptr_t)(m_tex->id), surface_size_im, ImVec2(0, surface_size.y / fb_size), ImVec2(surface_size.x / fb_size, 0), - 0, ImVec4(0.0f, 0.0f, 0.0f, 0.0f), ImVec4(1.0f, 1.0f, 1.0f, Global.UIBgOpacity)); + ImGui::PopStyleVar(); ImGui::PopStyleColor(3); if (ImGui::IsItemHovered()) diff --git a/widgets/vehicleparams.cpp b/widgets/vehicleparams.cpp index 51ad7a07..14ca9923 100644 --- a/widgets/vehicleparams.cpp +++ b/widgets/vehicleparams.cpp @@ -60,7 +60,7 @@ void ui::vehicleparams_panel::draw_mini(const TMoverParameters &mover) if (ImGui::BeginChild("mini", ImVec2(x, y))) { - ImGui::Image(reinterpret_cast(tex.get_id()), ImVec2(x, y), ImVec2(0, 1), ImVec2(1, 0)); + ImGui::Image((ImTextureID)(intptr_t)(tex.get_id()), ImVec2(x, y), ImVec2(0, 1), ImVec2(1, 0)); if (mover.Pantographs[end::rear].is_active) draw_infobutton("╨╨╨", ImVec2(126, 10)); @@ -129,7 +129,7 @@ void ui::vehicleparams_panel::render_contents() ImVec2 size = ImGui::GetContentRegionAvail(); - ImGui::Image(reinterpret_cast(entry.rt->shared_tex), size, ImVec2(uv0.x, uv0.y), ImVec2(uv1.x, uv1.y)); + ImGui::Image((ImTextureID)(intptr_t)(entry.rt->shared_tex), size, ImVec2(uv0.x, uv0.y), ImVec2(uv1.x, uv1.y)); } ImGui::End(); } diff --git a/world/Event.cpp b/world/Event.cpp index 4e042b29..fa752078 100644 --- a/world/Event.cpp +++ b/world/Event.cpp @@ -1243,6 +1243,26 @@ multi_event::init() { } } +std::vector multi_event::children() const { + std::vector result; + for (auto const &childevent : m_children) { + if (std::get(childevent) != nullptr) { + result.push_back(std::get(childevent)); + } + } + return result; +} + +std::vector basic_event::target_nodes() const { + std::vector result; + for (auto const &target : m_targets) { + if (std::get(target) != nullptr) { + result.push_back(std::get(target)); + } + } + return result; +} + std::vector multi_event::dump_children_names() const { std::vector result; for (auto const &childevent : m_children) { diff --git a/world/Event.h b/world/Event.h index 52ee3468..69d66e3c 100644 --- a/world/Event.h +++ b/world/Event.h @@ -47,6 +47,7 @@ public: // destructor virtual ~basic_event(); // methods + std::vector target_nodes() const; // restores event data from provided stream virtual void @@ -334,6 +335,7 @@ public: void init() override; std::vector dump_children_names() const; + std::vector children() const; private: // types @@ -699,6 +701,17 @@ public: // returns all eventlaunchers in radius ignoring height std::vector find_eventlaunchers(glm::vec2 center, float radius) const; + std::deque const & + sequence() const { + return m_events; } + // all event launchers, global ones included + std::vector + launchers() const { + auto const &input { m_inputdrivenlaunchers.sequence() }; + auto const &radio { m_radiodrivenlaunchers.sequence() }; + std::vector result( input.begin(), input.end() ); + result.insert( result.end(), radio.begin(), radio.end() ); + return result; } private: // types diff --git a/world/Road.cpp b/world/Road.cpp new file mode 100644 index 00000000..78c008ea --- /dev/null +++ b/world/Road.cpp @@ -0,0 +1,4294 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "world/Road.h" + +#include "world/RoadOrder.h" +#include "world/Track.h" +#include "vehicle/DynObj.h" +#include "simulation/simulation.h" +#include "scene/scene.h" +#include "rendering/renderer.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" + +namespace +{ + +// dimensions of the markings, in metres +// NOTE: these give a layout resembling an ordinary polish road, they weren't taken from the regulations +constexpr double line_width{0.12}; +constexpr double line_spacing{0.12}; // gap between two lines painted side by side +constexpr double line_lift{0.02}; // the markings are raised over the surface, so the two don't fight for depth +constexpr double edge_inset{0.15}; // distance between the edge of the surface and the line running along it +constexpr double dash_period{12.0}; // a third of it is painted, the rest is the gap +constexpr double dash_step{2.0}; // longest straight piece of a painted line +// speed and reach of the paths made to turn the vehicles back at a loose end of the road +constexpr float turn_velocity{10.f}; +constexpr double turn_reach{6.0}; +// length of the paths left of the lanes at the ends of a piece which gains or loses lanes +constexpr double lane_lead{2.0}; + +glm::dvec3 const up{0.0, 1.0, 0.0}; + +// placement of a cross-section of the road +struct axis_frame +{ + glm::dvec3 position; + glm::dvec3 tangent; // direction of the axis + glm::dvec3 left; // direction across the road, tilted by the roll + double roll{0.0}; +}; + +// the axis of a road piece; a cubic bezier curve, the same thing the paths of the tracks are made of +class road_axis +{ + + public: + explicit road_axis(segment_data const &Path) + { + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + m_curved = (control1 != glm::dvec3{0.0}) || (control2 != glm::dvec3{0.0}); + m_points[0] = start; + m_points[3] = end; + if (m_curved) + { + m_points[1] = start + control1; + m_points[2] = end + control2; + } + else + { + // a straight gets its control points in thirds of the length, the way the tracks do it + m_points[1] = start + (end - start) / 3.0; + m_points[2] = end - (end - start) / 3.0; + } + m_rolls = {glm::radians(static_cast(Path.rolls[0])), glm::radians(static_cast(Path.rolls[1]))}; + m_radius = std::abs(Path.radius); + // distances along the curve for evenly spaced values of its parameter, to convert the former into the latter + m_stations.resize(samples + 1); + m_stations[0] = 0.0; + auto previous{m_points[0]}; + for (int idx = 1; idx <= samples; ++idx) + { + auto const current{point(static_cast(idx) / samples)}; + m_stations[idx] = m_stations[idx - 1] + glm::distance(previous, current); + previous = current; + } + } + bool curved() const + { + return m_curved; + } + double length() const + { + return m_stations.back(); + } + // control vector of specified end of the curve, relative to that end + glm::dvec3 control(int const End) const + { + return End == 0 ? m_points[1] - m_points[0] : m_points[2] - m_points[3]; + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * m_points[0] + 3.0 * u * u * T * m_points[1] + 3.0 * u * T * T * m_points[2] + T * T * T * m_points[3]; + } + // curvature of the axis seen from above, positive when it bends to the left + double curvature(double const T) const + { + auto const first{derivative(T)}; + auto const second{6.0 * (1.0 - T) * (m_points[2] - 2.0 * m_points[1] + m_points[0]) + 6.0 * T * (m_points[3] - 2.0 * m_points[2] + m_points[1])}; + auto const speedsquared{first.x * first.x + first.z * first.z}; + if (speedsquared < 1e-9) + { + return 0.0; + } + return (second.x * first.z - second.z * first.x) / (speedsquared * std::sqrt(speedsquared)); + } + // cross-section at specified value of the curve parameter + axis_frame frame(double const T) const + { + axis_frame frame; + frame.position = point(T); + auto direction{derivative(T)}; + if (glm::length2(direction) < 1e-9) + { + // a control point placed right at the end of the curve + direction = derivative(std::clamp(T, 0.01, 0.99)); + } + if (glm::length2(direction) < 1e-9) + { + direction = m_points[3] - m_points[0]; + } + if (glm::length2(direction) < 1e-9) + { + direction = glm::dvec3{0.0, 0.0, 1.0}; + } + frame.tangent = glm::normalize(direction); + // with the y axis up and the vehicle facing along z its left side is at positive x + glm::dvec3 across{frame.tangent.z, 0.0, -frame.tangent.x}; + across = (glm::length2(across) < 1e-9 ? glm::dvec3{1.0, 0.0, 0.0} : glm::normalize(across)); + // positive roll lowers the right side of the road, same as it does for the tracks + frame.roll = glm::mix(m_rolls[0], m_rolls[1], T); + frame.left = across * std::cos(frame.roll) + up * std::sin(frame.roll); + return frame; + } + // cross-section at specified distance from the start of the axis + axis_frame at(double const Station) const + { + return frame(parameter(Station)); + } + // control vector of specified end of a part of the curve, relative to that end + glm::dvec3 control(int const End, double const From, double const To) const + { + if (From <= 0.0 && To >= 1.0) + { + return control(End); + } + // a part of a cubic curve is a cubic curve, with the tangents scaled to the part of the parameter range it takes + return End == 0 ? derivative(From) * ((To - From) / 3.0) : derivative(To) * (-(To - From) / 3.0); + } + // value of the curve parameter at specified distance from the start of the axis + double parameter(double const Station) const + { + if (Station <= 0.0) + { + return 0.0; + } + if (Station >= length()) + { + return 1.0; + } + // first of the sampled distances past the requested one; it's never the very first sample + auto const idx{std::distance(m_stations.begin(), std::upper_bound(m_stations.begin(), m_stations.end(), Station))}; + auto const span{m_stations[idx] - m_stations[idx - 1]}; + auto const fraction{span > 0.0 ? (Station - m_stations[idx - 1]) / span : 0.0}; + return (static_cast(idx - 1) + fraction) / samples; + } + // part of the length of the axis which lies before specified value of the curve parameter + double fraction(double const T) const + { + if (T <= 0.0 || length() <= 0.0) + { + return 0.0; + } + if (T >= 1.0) + { + return 1.0; + } + auto const position{T * samples}; + auto const idx{std::min(samples - 1, static_cast(position))}; + return glm::mix(m_stations[idx], m_stations[idx + 1], position - idx) / length(); + } + // length of pieces the road is cut into along the axis; follows the rules the tracks use + double step() const + { + if (false == m_curved) + { + return 10.0; + } + auto const fidelity{std::max(1.0, static_cast(Global.SplineFidelity))}; + return std::clamp(m_radius != 0.0 ? m_radius * 0.02 / fidelity : length() * 0.1, 2.0 / fidelity, 10.0 / fidelity); + } + + private: + static constexpr int samples{64}; + + glm::dvec3 derivative(double const T) const + { + auto const u{1.0 - T}; + return 3.0 * u * u * (m_points[1] - m_points[0]) + 6.0 * u * T * (m_points[2] - m_points[1]) + 3.0 * T * T * (m_points[3] - m_points[2]); + } + + std::array m_points; + std::array m_rolls{}; + double m_radius{0.0}; + bool m_curved{false}; + std::vector m_stations; +}; + +// lines of a road a corner of a cross-section can be measured from. on a piece which gains or loses lanes the edges +// and the line the directions meet at move across the axis along the way, each on its own +enum profile_anchor : int +{ + anchor_axis = 0, + anchor_left = 1, // left edge of the lanes + anchor_right = 2, // right edge of the lanes + anchor_middle = 3 // the line the two directions meet at +}; + +// corner of the cross-section of something laid along the road +struct profile_point +{ + double offset; // distance from the axis, positive to the left. this part of it follows the taper of the road + double height; // above the surface + float texture; // texture coordinate across the road + double shift{0.0}; // further distance from the axis, which the taper leaves alone + double spread{0.0}; // how much of the gap between the two directions of the road it's moved by: 0.5 for their left edge, -0.5 for the right one + double grow{0.0}; // what the shift changes by along the axis, evenly + double sink{0.0}; // what the height changes by along the axis, evenly + int anchor{anchor_axis}; // line of the road the corner is measured from, one of profile_anchor + double tile{0.0}; // if set the image is laid across the road by where the corner is: its coordinate grows by one for each that many metres from the axis +}; + +// how far the taper of a road has got at specified part of its length. it starts and ends level, so the pieces of a road +// meet without a bend in their edges, and it's what a lane running from one width to another comes to on a straight +double taper_progress(double const Fraction) +{ + auto const t{std::clamp(Fraction, 0.0, 1.0)}; + return t * t * (3.0 - 2.0 * t); +} + +// stretches provided profile along a part of the axis. the profile points go left to right, each two neighbours make a band of triangles. +// Road: the road it's done for, which says how the offsets of the profile change along the axis +void loft(std::vector &Output, road_axis const &Axis, std::vector const &Profile, double const From, double const To, double const Step, double const Texturelength, + road_node::state const &Road) +{ + auto const &Taper{Road.taper}; + auto const &median{Road.median.width}; + auto const divided{Road.divided()}; + if (Profile.size() < 2 || To <= From || Step <= 0.0) + { + return; + } + auto const count{std::max(1, static_cast(std::ceil((To - From) / Step - 0.001)))}; + auto const pointcount{Profile.size()}; + // each band is flat across the road, and follows its bends along it + auto const normal = [](glm::dvec3 const &Tangent, glm::dvec3 const &Across) { + auto const result{glm::cross(Tangent, Across)}; + return glm::vec3{glm::length2(result) < 1e-12 ? up : glm::normalize(result)}; + }; + std::vector previous(pointcount), current(pointcount); + std::vector previoustexture(pointcount), currenttexture(pointcount); + auto const anchored{std::any_of(Profile.begin(), Profile.end(), [](profile_point const &Point) { return Point.anchor != anchor_axis; })}; + axis_frame previousframe; + double previousstation{From}; + + for (int section = 0; section <= count; ++section) + { + auto const station{From + (To - From) * section / count}; + auto const frame{Axis.at(station)}; + auto const fraction{Axis.length() > 0.0 ? station / Axis.length() : 0.0}; + auto const scale{Taper[0] == Taper[1] ? static_cast(Taper[0]) : glm::mix(static_cast(Taper[0]), static_cast(Taper[1]), taper_progress(fraction))}; + auto const gap{false == divided ? 0.0 : glm::mix(static_cast(median[0]), static_cast(median[1]), taper_progress(fraction))}; + // where the lines the corners are measured from are at this section + std::array lead{0.0, 0.0, 0.0, 0.0}; + if (anchored) + { + auto const half{0.5 * Road.breadth(fraction)}; + lead = {0.0, half, -half, Road.middle(fraction)}; + } + for (std::size_t idx = 0; idx < pointcount; ++idx) + { + auto const &point{Profile[idx]}; + auto const across{lead[point.anchor] + (point.offset * scale + point.shift + point.spread * gap + point.grow * fraction)}; + current[idx] = frame.position + frame.left * across + up * (point.height + point.sink * fraction); + currenttexture[idx] = (point.tile > 0.0 ? static_cast(point.texture + across / point.tile) : point.texture); + } + if (section > 0) + { + auto const texturestart{static_cast(previousstation / Texturelength)}; + auto const textureend{static_cast(station / Texturelength)}; + for (std::size_t idx = 0; idx + 1 < pointcount; ++idx) + { + auto const normalstart{normal(previousframe.tangent, previous[idx] - previous[idx + 1])}; + auto const normalend{normal(frame.tangent, current[idx] - current[idx + 1])}; + world_vertex const leftstart{previous[idx], normalstart, {previoustexture[idx], texturestart}}; + world_vertex const rightstart{previous[idx + 1], normalstart, {previoustexture[idx + 1], texturestart}}; + world_vertex const leftend{current[idx], normalend, {currenttexture[idx], textureend}}; + world_vertex const rightend{current[idx + 1], normalend, {currenttexture[idx + 1], textureend}}; + Output.push_back(leftstart); + Output.push_back(rightstart); + Output.push_back(leftend); + + Output.push_back(rightstart); + Output.push_back(rightend); + Output.push_back(leftend); + } + } + previous.swap(current); + previoustexture.swap(currenttexture); + previousframe = frame; + previousstation = station; + } +} + +// shape of a line running beside a part of the axis of a road, from one distance from the axis to another, positive being to the left. +// From, To: values of the curve parameter of the axis. the line is laid out the way the axis goes +segment_data offset_path(segment_data const &Axis, road_axis const &Centre, double const From, double const To, double const Fromoffset, double const Tooffset) +{ + auto const begin{Centre.frame(From)}; + auto const end{Centre.frame(To)}; + + segment_data path; + path.points[segment_data::point::start] = begin.position + begin.left * Fromoffset; + path.points[segment_data::point::end] = end.position + end.left * Tooffset; + if (Centre.curved() || Fromoffset != Tooffset) + { + // a line running beside a bend is shorter or longer than the bend itself, and so are its control vectors. + // the control vectors go along the axis at both ends, which is what makes the lane meet the next piece without a bend + path.points[segment_data::point::control1] = Centre.control(0, From, To) * std::max(0.05, 1.0 - Fromoffset * std::cos(begin.roll) * Centre.curvature(From)); + path.points[segment_data::point::control2] = Centre.control(1, From, To) * std::max(0.05, 1.0 - Tooffset * std::cos(end.roll) * Centre.curvature(To)); + // if the roll changes along the road its sides rise or fall relative to the axis + auto const climb{(Tooffset * std::sin(end.roll) - Fromoffset * std::sin(begin.roll)) / 3.0}; + path.points[segment_data::point::control1].y += climb; + path.points[segment_data::point::control2].y -= climb; + } + path.rolls = {From <= 0.0 ? Axis.rolls[0] : glm::mix(Axis.rolls[0], Axis.rolls[1], static_cast(From)), + To >= 1.0 ? Axis.rolls[1] : glm::mix(Axis.rolls[0], Axis.rolls[1], static_cast(To))}; + if (Axis.radius != 0.f) + { + // the lanes on the inner side of the bend have it tighter + auto const turn{Centre.curvature(0.5 * (From + To)) >= 0.0 ? 1.0 : -1.0}; + path.radius = static_cast(std::max(1.0, std::abs(Axis.radius) - turn * 0.5 * (Fromoffset + Tooffset))); + } + return path; +} + +// turns a line laid out along the axis of a road into the path of a lane. Against: the lane goes against the axis +segment_data lane_shape(segment_data Path, bool const Against) +{ + if (Against) + { + std::swap(Path.points[segment_data::point::start], Path.points[segment_data::point::end]); + std::swap(Path.points[segment_data::point::control1], Path.points[segment_data::point::control2]); + Path.rolls = {-Path.rolls[1], -Path.rolls[0]}; + } + if (Global.bRollFix) + { + // with this setting on a rolled path gets raised when it's set up, by an amount fit for a railway track. + // the surface of the road stays where it is, so the lane is lowered beforehand to make up for it + Path.points[segment_data::point::start].y -= std::abs(std::sin(glm::radians(static_cast(Path.rolls[0])))) * 0.75; + Path.points[segment_data::point::end].y -= std::abs(std::sin(glm::radians(static_cast(Path.rolls[1])))) * 0.75; + } + return Path; +} + +// corner of a cross-section which changes along the road +struct section_corner +{ + double offset; // distance from the axis, positive to the left + double height; + float texture; +}; + +// stretches a profile whose corners move from one cross-section to the next along the axis. +// Shape: fills in the corners, left to right, for specified distance from the start of the axis; it has to give the same number of them each time +template +void sweep(std::vector &Output, road_axis const &Axis, std::vector const &Stations, double const Texturelength, Shape_ Shape) +{ + auto const normal = [](glm::dvec3 const &Tangent, glm::dvec3 const &Across) { + auto const result{glm::cross(Tangent, Across)}; + return glm::vec3{glm::length2(result) < 1e-12 ? up : glm::normalize(result)}; + }; + std::vector corners, previouscorners; + std::vector previous, current; + axis_frame previousframe; + double previousstation{0.0}; + for (std::size_t section = 0; section < Stations.size(); ++section) + { + auto const station{Stations[section]}; + auto const frame{Axis.at(station)}; + corners.clear(); + Shape(station, corners); + current.resize(corners.size()); + for (std::size_t idx = 0; idx < corners.size(); ++idx) + { + current[idx] = frame.position + frame.left * corners[idx].offset + up * corners[idx].height; + } + if (section > 0 && previous.size() == current.size()) + { + auto const texturestart{static_cast(previousstation / Texturelength)}; + auto const textureend{static_cast(station / Texturelength)}; + for (std::size_t idx = 0; idx + 1 < current.size(); ++idx) + { + auto const normalstart{normal(previousframe.tangent, previous[idx] - previous[idx + 1])}; + auto const normalend{normal(frame.tangent, current[idx] - current[idx + 1])}; + world_vertex const leftstart{previous[idx], normalstart, {previouscorners[idx].texture, texturestart}}; + world_vertex const rightstart{previous[idx + 1], normalstart, {previouscorners[idx + 1].texture, texturestart}}; + world_vertex const leftend{current[idx], normalend, {corners[idx].texture, textureend}}; + world_vertex const rightend{current[idx + 1], normalend, {corners[idx + 1].texture, textureend}}; + Output.push_back(leftstart); + Output.push_back(rightstart); + Output.push_back(leftend); + + Output.push_back(rightstart); + Output.push_back(rightend); + Output.push_back(leftend); + } + } + previous.swap(current); + previouscorners = corners; + previousframe = frame; + previousstation = station; + } +} + +// closes the end of something raised over the surface with a flat face. +// Corners: outline of the face; Forward: the face looks the way the axis goes rather than back along it +void cap(std::vector &Output, axis_frame const &Frame, std::vector const &Corners, bool const Forward, double const Tile) +{ + if (Corners.size() < 3) + { + return; + } + auto const facing{Forward ? Frame.tangent : -Frame.tangent}; + auto const point = [&Frame](section_corner const &Corner) { return Frame.position + Frame.left * Corner.offset + up * Corner.height; }; + for (std::size_t idx = 1; idx + 1 < Corners.size(); ++idx) + { + std::array order{0, idx, idx + 1}; + if (glm::dot(glm::cross(point(Corners[idx]) - point(Corners[0]), point(Corners[idx + 1]) - point(Corners[0])), facing) < 0.0) + { + std::swap(order[1], order[2]); + } + for (auto const corner : order) + { + Output.push_back({point(Corners[corner]), glm::vec3{facing}, {static_cast(Corners[corner].offset / Tile), static_cast(Corners[corner].height / Tile)}}); + } + } +} + +// proportions of the image specified material is painted with +float texture_ratio(std::string const &Material) +{ + auto const material{GfxRenderer->Fetch_Material(Material)}; + if (material == null_handle) + { + return 1.f; + } + auto const texture{GfxRenderer->Material(material)->GetTexture(0)}; + if (texture == null_handle) + { + return 1.f; + } + auto const &image{GfxRenderer->Texture(texture)}; + return image.get_height() > 0 ? static_cast(image.get_width()) / static_cast(image.get_height()) : 1.f; +} + +} // namespace + +// brings the content to a usable form: values within their limits, an entry for each lane and for each pair of neighbours +void road_node::state::normalize() +{ + forward = std::clamp(forward, 0, 8); + backward = std::clamp(backward, 0, 8); + for (int direction = 0; direction < 2; ++direction) + { + auto &count{direction == 0 ? forward : backward}; + for (auto &end : missing) + { + end[direction] = std::clamp(end[direction], 0, count); + } + // lanes the piece has at neither of its ends aren't there at all + auto const both{std::min(missing[0][direction], missing[1][direction])}; + count -= both; + missing[0][direction] -= both; + missing[1][direction] -= both; + } + if (forward + backward == 0) + { + forward = 1; + } + for (auto &end : missing) + { + // there has to be a lane at each end + if (forward - end[0] + backward - end[1] < 1) + { + (forward > 0 ? end[0] : end[1]) = (forward > 0 ? forward : backward) - 1; + } + } + lanewidth = std::max(1.f, lanewidth); + weight = std::clamp(weight, 0.f, 10000.f); + for (auto &width : median.width) + { + width = std::clamp(width, 0.f, 100.f); + } + if (median.material.empty()) + { + median.material = "none"; + } + for (auto &round : median.round) + { + round = std::clamp(round, 0.f, 100.f); + } + kerbwidth = std::clamp(kerbwidth, 0.05f, 2.f); + if (kerbmaterial.empty()) + { + kerbmaterial = "none"; + } + if (bankmaterial.empty()) + { + bankmaterial = "none"; + } + for (auto &bank : banks) + { + for (auto &width : bank.width) + { + width = std::clamp(width, 0.f, 100.f); + } + for (auto &drop : bank.drop) + { + drop = std::clamp(drop, -100.f, 100.f); + } + } + for (auto &end : taper) + { + end = std::clamp(end, 0.25f, 4.f); + } + if (texturelength < 0.01f) + { + texturelength = 4.f; + } + if (surface.empty()) + { + surface = "none"; + } + // the paths of the lanes are loaded from text, so what goes into it has to be something they understand + static std::array const environments{"flat", "mountains", "mountain", "canyon", "tunnel", "bridge", "bank"}; + if (std::find(environments.begin(), environments.end(), environment) == environments.end()) + { + environment = "flat"; + } + auto const count{static_cast(forward + backward)}; + if (lanes.size() != count) + { + lane_data fresh; + fresh.width = lanewidth; + fresh.velocity = velocity; + lanes.assign(count, fresh); + } + for (auto &lane : lanes) + { + lane.width = std::max(1.f, lane.width); + } + if (changes.size() != count - 1) + { + changes.resize(count - 1); + for (std::size_t boundary = 0; boundary < changes.size(); ++boundary) + { + changes[boundary] = default_change(boundary); + } + } + for (auto &change : changes) + { + change = std::clamp(change, static_cast(change_none), static_cast(change_both)); + } +} + +// identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet +std::string road_node::state::lane_id(std::size_t const Lane) const +{ + auto const against{static_cast(backward)}; + return Lane < against ? "b" + std::to_string(against - Lane) : "f" + std::to_string(Lane - against + 1); +} + +// number of a lane in the left to right order, -1 if there's no lane with such identifier +int road_node::state::lane_index(std::string const &Id) const +{ + if (Id.size() < 2 || Id.size() > 3 || (Id[0] != 'f' && Id[0] != 'b')) + { + return -1; + } + int number{0}; + for (std::size_t idx = 1; idx < Id.size(); ++idx) + { + if (Id[idx] < '0' || Id[idx] > '9') + { + return -1; + } + number = number * 10 + (Id[idx] - '0'); + } + if (number < 1 || number > (Id[0] == 'f' ? forward : backward)) + { + return -1; + } + return Id[0] == 'f' ? backward + number - 1 : backward - number; +} + +// true if the piece has other lanes at its end than at its start +bool road_node::state::changing() const +{ + return missing[0][0] != 0 || missing[0][1] != 0 || missing[1][0] != 0 || missing[1][1] != 0; +} + +// sets the lanes the piece has at the start and at the end of its axis +void road_node::state::lanes_between(int const Startforward, int const Startbackward, int const Endforward, int const Endbackward) +{ + auto const along{std::clamp(std::max(Startforward, Endforward), 0, 8)}; + auto const against{std::clamp(std::max(Startbackward, Endbackward), 0, 8)}; + if (along != forward || against != backward) + { + // a different set of lanes starts with the common width and speed limit, and the default rules for changing lanes + forward = along; + backward = against; + lanes.clear(); + changes.clear(); + } + missing[0] = {forward - std::max(0, Startforward), backward - std::max(0, Startbackward)}; + missing[1] = {forward - std::max(0, Endforward), backward - std::max(0, Endbackward)}; + normalize(); +} + +// number of lanes at an end of the axis: of the ones going along it, or of the ones going against it +int road_node::state::lanes_at(int const End, bool const Along) const +{ + return Along ? forward - missing[End][0] : backward - missing[End][1]; +} + +// true if the piece has the lane at specified end of the axis +bool road_node::state::lane_at(std::size_t const Lane, int const End) const +{ + // the lanes of each direction are counted outwards from where the directions meet, and it's the outermost ones a piece is short of + auto const against{static_cast(backward)}; + return Lane < against ? Lane >= static_cast(missing[End][1]) : Lane - against < static_cast(forward - missing[End][0]); +} + +// the same road with the lanes it has at specified end of the axis all the way +road_node::state road_node::state::section(int const End) const +{ + auto result{*this}; + result.lanes.clear(); + result.changes.clear(); + for (std::size_t lane = 0; lane < lanes.size(); ++lane) + { + if (false == lane_at(lane, End)) + { + continue; + } + // the lanes a piece has at an end lie side by side, so the one before this one is its neighbour there as well + if (false == result.lanes.empty() && lane - 1 < changes.size()) + { + result.changes.emplace_back(changes[lane - 1]); + } + result.lanes.emplace_back(lanes[lane]); + } + result.forward = lanes_at(End, true); + result.backward = lanes_at(End, false); + result.missing = {}; + return result; +} + +// permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise +int road_node::state::default_change(std::size_t const Boundary) const +{ + auto const opposite{backward > 0 && forward > 0 && Boundary + 1 == static_cast(backward)}; + if (false == opposite) + { + return change_both; + } + // overtaking across the oncoming lane is left for the simple two lane roads + return (forward == 1 && backward == 1) ? change_both : change_none; +} + +// distance of the middle of a lane from the axis, positive to the left when facing along the axis +double road_node::state::lane_offset(std::size_t const Lane) const +{ + auto offset{0.5 * width()}; + for (std::size_t idx = 0; idx < Lane && idx < lanes.size(); ++idx) + { + offset -= lanes[idx].width; + } + return Lane < lanes.size() ? offset - 0.5 * lanes[Lane].width : offset; +} + +// the same at an end of the axis, where the road is centred on the axis with the lanes it has there +double road_node::state::lane_offset(std::size_t const Lane, int const End) const +{ + auto const against{static_cast(backward)}; + auto lane{Lane}; + if (Lane < lanes.size() && false == lane_at(Lane, End)) + { + // a lane the road is short of there comes out of the outermost lane its direction has at that end, or goes into it. + // with no such lane it starts, or ends, at the line the directions meet at + if (lanes_at(End, Lane >= against) < 1) + { + return divide(End); + } + lane = (Lane < against ? static_cast(missing[End][1]) : against + static_cast(lanes_at(End, true) - 1)); + } + auto offset{0.5 * width(End)}; + for (std::size_t idx = 0; idx < lane && idx < lanes.size(); ++idx) + { + if (lane_at(idx, End)) + { + offset -= lanes[idx].width; + } + } + return lane < lanes.size() ? offset - 0.5 * lanes[lane].width : offset; +} + +// distance of the middle of a lane from the axis at specified part of the length of the axis, with the taper and what's between the directions counted in +double road_node::state::lane_position(std::size_t const Lane, double const Fraction) const +{ + auto const side{Lane < static_cast(backward) ? 0.5 : -0.5}; + auto const beginoffset{lane_offset(Lane, 0) * taper[0] + side * median_width(0.0)}; + auto const endoffset{lane_offset(Lane, 1) * taper[1] + side * median_width(1.0)}; + return glm::mix(beginoffset, endoffset, taper_progress(Fraction)); +} + +// combined width of the lanes, before the taper +double road_node::state::width() const +{ + double total{0.0}; + for (auto const &lane : lanes) + { + total += lane.width; + } + return total; +} + +// the same for the lanes the piece has at an end of the axis +double road_node::state::width(int const End) const +{ + double total{0.0}; + for (std::size_t lane = 0; lane < lanes.size(); ++lane) + { + if (lane_at(lane, End)) + { + total += lanes[lane].width; + } + } + return total; +} + +// combined width of the lanes at specified part of the length of the axis, with the taper counted in +double road_node::state::breadth(double const Fraction) const +{ + return glm::mix(width(0) * taper[0], width(1) * taper[1], taper_progress(Fraction)); +} + +// what the widths of the lanes are multiplied by at specified value of the curve parameter +double road_node::state::scale(double const T) const +{ + return glm::mix(static_cast(taper[0]), static_cast(taper[1]), taper_progress(T)); +} + +// true if the two directions of the road are kept apart anywhere along it +bool road_node::state::divided() const +{ + return forward > 0 && backward > 0 && (median.width[0] > 0.f || median.width[1] > 0.f); +} + +// how far apart the two directions are at specified value of the curve parameter +double road_node::state::median_width(double const T) const +{ + return divided() ? glm::mix(static_cast(median.width[0]), static_cast(median.width[1]), taper_progress(T)) : 0.0; +} + +// distance from the axis of the line the two directions meet at, on a road which doesn't keep them apart +double road_node::state::divide() const +{ + auto offset{0.5 * width()}; + for (std::size_t idx = 0; idx < static_cast(backward) && idx < lanes.size(); ++idx) + { + offset -= lanes[idx].width; + } + return offset; +} + +// the same for the lanes the piece has at an end of the axis +double road_node::state::divide(int const End) const +{ + auto offset{0.5 * width(End)}; + for (std::size_t idx = 0; idx < static_cast(backward) && idx < lanes.size(); ++idx) + { + if (lane_at(idx, End)) + { + offset -= lanes[idx].width; + } + } + return offset; +} + +// the same at specified part of the length of the axis, with the taper counted in +double road_node::state::middle(double const Fraction) const +{ + return glm::mix(divide(0) * taper[0], divide(1) * taper[1], taper_progress(Fraction)); +} + +// width of the whole roadway at specified value of the curve parameter +double road_node::state::span(double const T) const +{ + return (changing() ? breadth(T) : width() * scale(T)) + median_width(T); +} + +// shape of the middle of a lane, laid out in the direction of travel +segment_data road_node::state::lane_path(std::size_t const Lane) const +{ + road_axis const centre{axis}; + // on a road which gets wider or narrower the lane moves away from the axis or towards it along the way, + // and so it does where the two directions are led apart, or where the road gains or loses lanes + auto const side{Lane < static_cast(backward) ? 0.5 : -0.5}; + auto const beginoffset{lane_offset(Lane, 0) * taper[0] + side * median_width(0.0)}; + auto const endoffset{lane_offset(Lane, 1) * taper[1] + side * median_width(1.0)}; + return lane_shape(offset_path(axis, centre, 0.0, 1.0, beginoffset, endoffset), Lane < static_cast(backward)); +} + +// shape of the way from the middle of a lane to the middle of another over a part of the axis, laid out in the direction of travel +segment_data road_node::state::lane_path(std::size_t const Lane, std::size_t const Tolane, double const From, double const To) const +{ + road_axis const centre{axis}; + auto const against{Lane < static_cast(backward)}; + // the traffic going against the axis gets to the part by its far end + auto const fromoffset{lane_position(against ? Tolane : Lane, centre.fraction(From))}; + auto const tooffset{lane_position(against ? Lane : Tolane, centre.fraction(To))}; + return lane_shape(offset_path(axis, centre, From, To, fromoffset, tooffset), against); +} + +// part of the axis over which a piece which gains or loses lanes leads the lanes it has at one end to the lanes it has at the other +std::array road_node::state::change_range() const +{ + road_axis const centre{axis}; + auto const lead{std::min(lane_lead, 0.2 * centre.length())}; + return {centre.parameter(lead), centre.parameter(centre.length() - lead)}; +} + +// length of the axis +double road_node::state::length() const +{ + return road_axis{axis}.length(); +} + +// position on the axis and direction of the axis, for specified value of the curve parameter +glm::dvec3 road_node::state::point(double const T) const +{ + return road_axis{axis}.point(T); +} + +glm::dvec3 road_node::state::tangent(double const T) const +{ + return road_axis{axis}.frame(T).tangent; +} + +road_node::road_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {} + +// restores content of the node from provided input stream; reads up to and including the closing 'endroad' +void road_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + auto const label{m_name.empty() ? std::string{"unnamed road"} : "road \"" + m_name + "\""}; + + m_state = state{}; + m_state.axis.deserialize(Input, Offset); + + // properties of the lanes can precede the statement which sets how many of them there are, so they wait until everything is read + struct lane_property + { + std::string lane; + std::string property; + float value; + }; + struct change_property + { + std::string first; + std::string second; + std::string mode; + }; + std::vector laneproperties; + std::vector changeproperties; + // lanes at the end of the axis, if these are other than the ones at its start + std::array endlanes{-1, -1}; + + auto token{Input.getToken()}; + while (false == token.empty() && token != "endroad") + { + if (token == "lanes") + { + // lanes + Input.getTokens(2); + Input >> m_state.forward >> m_state.backward; + } + else if (token == "lanesto") + { + // lanesto + Input.getTokens(2); + Input >> endlanes[0] >> endlanes[1]; + } + else if (token == "width") + { + Input.getTokens(); + Input >> m_state.lanewidth; + } + else if (token == "velocity") + { + Input.getTokens(); + Input >> m_state.velocity; + } + else if (token == "taper") + { + // taper + Input.getTokens(2); + Input >> m_state.taper[0] >> m_state.taper[1]; + } + else if (token == "lane") + { + // lane + lane_property property; + property.lane = Input.getToken(); + property.property = Input.getToken(); + property.value = 0.f; + Input.getTokens(); + Input >> property.value; + laneproperties.emplace_back(property); + } + else if (token == "change") + { + // change + change_property property; + property.first = Input.getToken(); + property.second = Input.getToken(); + property.mode = Input.getToken(); + changeproperties.emplace_back(property); + } + else if (token == "surface") + { + m_state.surface = Input.getToken(); + replace_slashes(m_state.surface); + } + else if (token == "texlength") + { + Input.getTokens(); + Input >> m_state.texturelength; + } + else if (token == "side") + { + // side + auto const where{Input.getToken()}; + auto const type{Input.getToken()}; + side_data side; + Input.getTokens(); + Input >> side.width; + side.material = Input.getToken(); + replace_slashes(side.material); + side.type = (type == "shoulder" ? side_type::shoulder : type == "sidewalk" ? side_type::sidewalk : side_type::none); + if (type != "shoulder" && type != "sidewalk" && type != "none") + { + ErrorLog("Bad road: unknown kind of side \"" + type + "\" defined for " + label); + } + if (where == "left" || where == "both") + { + m_state.sides[0] = side; + } + if (where == "right" || where == "both") + { + m_state.sides[1] = side; + } + if (where != "left" && where != "right" && where != "both") + { + ErrorLog("Bad road: unknown side \"" + where + "\" defined for " + label); + } + } + else if (token == "kerb") + { + Input.getTokens(); + Input >> m_state.kerbheight; + } + else if (token == "slope") + { + // slope + Input.getTokens(2); + Input >> m_state.slope.x >> m_state.slope.y; + } + else if (token == "markings") + { + auto const colour{Input.getToken()}; + m_state.markings = (colour == "white" ? marking_colour::white : colour == "orange" ? marking_colour::orange : marking_colour::none); + if (colour != "white" && colour != "orange" && colour != "none") + { + ErrorLog("Bad road: unknown colour of markings \"" + colour + "\" defined for " + label); + } + } + else if (token == "friction") + { + Input.getTokens(); + Input >> m_state.friction; + } + else if (token == "environment") + { + m_state.environment = Input.getToken(); + } + else if (token == "weight") + { + Input.getTokens(); + Input >> m_state.weight; + } + else if (token == "median") + { + // median + auto const type{Input.getToken()}; + m_state.median.type = (type == "gap" ? median_type::gap : type == "island" ? median_type::island : median_type::painted); + if (type != "gap" && type != "island" && type != "painted") + { + ErrorLog("Bad road: unknown kind of median \"" + type + "\" defined for " + label); + } + Input.getTokens(2); + Input >> m_state.median.width[0] >> m_state.median.width[1]; + m_state.median.material = Input.getToken(); + replace_slashes(m_state.median.material); + } + else if (token == "medianround") + { + // medianround + Input.getTokens(2); + Input >> m_state.median.round[0] >> m_state.median.round[1]; + } + else if (token == "kerbs") + { + // kerbs + auto const where{Input.getToken()}; + m_state.kerbs = {where == "left" || where == "both", where == "right" || where == "both"}; + if (where != "left" && where != "right" && where != "both") + { + ErrorLog("Bad road: unknown side \"" + where + "\" defined for the kerbs of " + label); + } + Input.getTokens(); + Input >> m_state.kerbwidth; + m_state.kerbmaterial = Input.getToken(); + replace_slashes(m_state.kerbmaterial); + } + else if (token == "bank") + { + // bank + auto const where{Input.getToken()}; + bank_data bank; + bank.set = true; + Input.getTokens(4); + Input >> bank.width[0] >> bank.drop[0] >> bank.width[1] >> bank.drop[1]; + if (where == "left" || where == "both") + { + m_state.banks[0] = bank; + } + if (where == "right" || where == "both") + { + m_state.banks[1] = bank; + } + if (where != "left" && where != "right" && where != "both") + { + ErrorLog("Bad road: unknown side \"" + where + "\" defined for a bank of " + label); + } + } + else if (token == "bankmaterial") + { + m_state.bankmaterial = Input.getToken(); + replace_slashes(m_state.bankmaterial); + } + else if (token == "roundabout") + { + m_state.roundabout = true; + } + else + { + ErrorLog("Bad road: unknown property: \"" + token + "\" defined for " + label); + } + token = Input.getToken(); + } + + if (m_state.forward + m_state.backward <= 0) + { + ErrorLog("Bad road: " + label + " has no lanes, a single one is used instead"); + } + auto const environment{m_state.environment}; + if (endlanes[0] >= 0 || endlanes[1] >= 0) + { + if (std::max(0, endlanes[0]) + std::max(0, endlanes[1]) <= 0) + { + ErrorLog("Bad road: " + label + " has no lanes at its end, a single one is used instead"); + } + m_state.lanes_between(m_state.forward, m_state.backward, endlanes[0], endlanes[1]); + // the traffic going along the axis gets on the piece at its start, the one going against it at its end + int const gains[]{m_state.lanes_at(1, true) - m_state.lanes_at(0, true), m_state.lanes_at(0, false) - m_state.lanes_at(1, false)}; + int const leaving[]{m_state.lanes_at(1, true), m_state.lanes_at(0, false)}; + int const arriving[]{m_state.lanes_at(0, true), m_state.lanes_at(1, false)}; + for (int direction = 0; direction < 2; ++direction) + { + if (arriving[direction] > 0 && gains[direction] > 2) + { + ErrorLog("Bad road: " + label + " gains more than two lanes at once, the ones past the second can't be got on"); + } + if (arriving[direction] > 0 && leaving[direction] == 0) + { + ErrorLog("Bad road: " + label + " has lanes which lead nowhere, as their direction ends within it"); + } + } + } + m_state.normalize(); + if (environment != m_state.environment) + { + ErrorLog("Bad road: unknown environment \"" + environment + "\" defined for " + label); + } + + for (auto const &property : laneproperties) + { + auto const lane{m_state.lane_index(property.lane)}; + if (lane < 0) + { + ErrorLog("Bad road: " + label + " has no lane \"" + property.lane + "\""); + continue; + } + if (property.property == "width") + { + m_state.lanes[lane].width = std::max(1.f, property.value); + } + else if (property.property == "velocity") + { + m_state.lanes[lane].velocity = property.value; + } + else + { + ErrorLog("Bad road: unknown lane property: \"" + property.property + "\" defined for " + label); + } + } + for (auto const &property : changeproperties) + { + auto const first{m_state.lane_index(property.first)}; + auto const second{m_state.lane_index(property.second)}; + if (first < 0 || second < 0 || std::abs(first - second) != 1) + { + ErrorLog("Bad road: lanes \"" + property.first + "\" and \"" + property.second + "\" aren't neighbours on " + label); + continue; + } + // the permissions are stored for the pair taken left to right + auto const firstonleft{first < second}; + auto &change{m_state.changes[std::min(first, second)]}; + if (property.mode == "both") + { + change = change_both; + } + else if (property.mode == "none") + { + change = change_none; + } + else if (property.mode == "ab") + { + change = (firstonleft ? change_toright : change_toleft); + } + else if (property.mode == "ba") + { + change = (firstonleft ? change_toleft : change_toright); + } + else + { + ErrorLog("Bad road: unknown lane change mode: \"" + property.mode + "\" defined for " + label); + } + } + + location(m_state.point(0.5)); +} + +// replaces what the node knows about the road +void road_node::define(state const &State) +{ + m_state = State; + m_state.normalize(); + location(m_state.point(0.5)); + m_area.radius = -1.f; +} + +// creates paths of the lanes and hands them over to the simulation +void road_node::create_lanes() +{ + auto const count{m_state.lanes.size()}; + m_tracks.clear(); + for (auto &tracks : m_lanetracks) + { + tracks.assign(count, nullptr); + } + auto const range{m_state.change_range()}; + for (std::size_t lane = 0; lane < count; ++lane) + { + auto const along{lane >= static_cast(m_state.backward)}; + auto const name{m_name.empty() ? std::string{} : m_name + ":" + m_state.lane_id(lane)}; + auto const &data{m_state.lanes[lane]}; + if (m_state.missing[0][along ? 0 : 1] == 0 && m_state.missing[1][along ? 0 : 1] == 0) + { + // the direction keeps its lanes all the way, and so each of them is a single path + auto *track{create_track(name, m_state.lane_path(lane), data.width, data.velocity)}; + m_lanetracks[0][lane] = track; + m_lanetracks[1][lane] = track; + m_tracks.emplace_back(track); + continue; + } + // the direction gains or loses lanes along the way. a lane gets a short path at each end the piece has it at, which is what + // the neighbours and the junctions join; what leads from the ones at one end to the ones at the other is made by create_links() + for (int end = 0; end < 2; ++end) + { + if (false == m_state.lane_at(lane, end)) + { + continue; + } + // the traffic going along the axis gets on the piece at its start + auto const entry{(end == 0) == along}; + auto *track{create_track(name.empty() || entry ? name : name + ":out", m_state.lane_path(lane, lane, end == 0 ? 0.0 : range[1], end == 0 ? range[0] : 1.0), data.width, data.velocity)}; + m_lanetracks[end][lane] = track; + m_tracks.emplace_back(track); + } + } +} + +// makes the paths which lead the lanes the piece has at one end to the lanes it has at the other, where these differ +void road_node::create_links() +{ + if (false == m_links.empty() || false == m_state.changing() || m_lanetracks[0].size() != m_state.lanes.size()) + { + return; + } + auto const range{m_state.change_range()}; + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + auto const write_point = [&text](glm::dvec3 const &Point) { text << Point.x << ' ' << Point.y << ' ' << Point.z << ' '; }; + for (int direction = 0; direction < 2; ++direction) + { + if (m_state.missing[0][direction] == 0 && m_state.missing[1][direction] == 0) + { + continue; + } + auto const along{direction == 0}; + // ends of the axis the traffic of the direction gets on the piece and leaves it by + auto const entry{along ? 0 : 1}; + auto const exit{1 - entry}; + auto const arriving{m_state.lanes_at(entry, along)}; + auto const leaving{m_state.lanes_at(exit, along)}; + // place of a lane of the direction, counted from where the directions meet, in the left to right order + auto const index = [&](int const Number) { return static_cast(along ? m_state.backward + Number - 1 : m_state.backward - Number); }; + for (int number = 1; number <= arriving && leaving > 0; ++number) + { + auto const lane{index(number)}; + auto *head{m_lanetracks[entry][lane]}; + if (head == nullptr) + { + continue; + } + // a lane leads to itself; one the road loses leads to the outermost lane left, and the outermost lane of a road + // which gains lanes leads to these as well. a path has up to three ways out, so that's two lanes gained at most + std::vector targets{index(std::min(number, leaving))}; + for (int extra = arriving + 1; number == arriving && extra <= leaving && targets.size() < 3; ++extra) + { + targets.emplace_back(index(extra)); + } + std::vector tails; + std::vector ways; + for (auto const target : targets) + { + if (m_lanetracks[exit][target] != nullptr) + { + tails.emplace_back(m_lanetracks[exit][target]); + ways.emplace_back(m_state.lane_path(lane, target, range[0], range[1])); + } + } + if (tails.empty()) + { + continue; + } + m_ways.insert(m_ways.end(), ways.begin(), ways.end()); + auto const name{m_name.empty() ? std::string{} : m_name + ":" + m_state.lane_id(lane) + ":way"}; + auto const &data{m_state.lanes[lane]}; + if (tails.size() == 1) + { + auto *link{create_track(name, ways[0], data.width, data.velocity)}; + // tied by hand: lanes the road loses end where the one they lead to carries on, which is more than joining path ends can express + head->ConnectNextPrev(link, 0); + link->trNext = tails[0]; + link->iNextDirection = 0; + if (tails[0]->trPrev == nullptr) + { + tails[0]->trPrev = link; + tails[0]->iPrevDirection = 1; + } + m_links.emplace_back(link); + continue; + } + // a fork is a crossroads, which is what the drivers know how to pick a way through. the ways leave the lane + // the way it goes, and each joins its lane the way that one goes + // NOTE: a crossroads isn't set up the way a rolled path is, so its points are given as they are and it's level across + auto const raise = [](glm::dvec3 Point, float const Roll) { + if (Global.bRollFix) + { + Point.y += std::abs(std::sin(glm::radians(static_cast(Roll)))) * 0.75; + } + return Point; + }; + auto const start{raise(ways[0].points[segment_data::point::start], ways[0].rolls[0])}; + auto const startcontrol{ways[0].points[segment_data::point::control1]}; + std::vector ends; + std::vector endcontrols; + for (auto const &way : ways) + { + ends.emplace_back(raise(way.points[segment_data::point::end], way.rolls[1])); + endcontrols.emplace_back(way.points[segment_data::point::control2]); + } + text.str(""); + text << "cross " << glm::distance(start, ends[0]) << ' ' << data.width << ' ' << m_state.friction << ' ' << m_state.sounddistance << ' ' << m_state.quality << " 0 " << m_state.environment + << " unvis "; + // first path: from the lane to itself + write_point(start); + text << "0 "; + write_point(startcontrol); + write_point(endcontrols[0]); + write_point(ends[0]); + text << "0 0 "; + if (ways.size() == 2) + { + // second path of a crossroads of three roads starts where the first one does + write_point(start); + text << "0 "; + write_point(startcontrol); + write_point(endcontrols[1]); + write_point(ends[1]); + text << "0 0 "; + } + else + { + // second path of a crossroads of four roads runs between its third and fourth point + write_point(ends[1]); + text << "0 "; + write_point(endcontrols[1]); + write_point(endcontrols[2]); + write_point(ends[2]); + text << "0 0 "; + } + if (data.velocity > 0.f) + { + text << "velocity " << data.velocity << ' '; + } + text << "endtrack"; + auto *link{create_track(name, text.str())}; + if (link->SwitchExtension != nullptr) + { + auto &extension{*link->SwitchExtension}; + // the points of the crossroads are stored as: first, second, fourth, third + extension.pPrevs[0] = head; + extension.iPrevDirection[0] = 1; + extension.pNexts[0] = tails[0]; + extension.iNextDirection[0] = 0; + extension.pPrevs[1] = (tails.size() == 3 ? tails[2] : tails[1]); + extension.iPrevDirection[1] = 0; + extension.pNexts[1] = (tails.size() == 3 ? tails[1] : nullptr); + extension.iNextDirection[1] = 0; + link->Switch(0); + head->trNext = link; + head->iNextDirection = 0; + // the drivers take each of the lanes as readily as any other + link->m_routeweights = {100.f, 100.f, tails.size() == 3 ? 100.f : 0.f}; + // the lanes get to know the path leading to them, for whatever goes backwards and for those who look who's coming + int const ends2[]{1, 2}; + int const ends3[]{1, 3, 2}; + for (std::size_t idx = 0; idx < tails.size(); ++idx) + { + if (tails[idx]->trPrev == nullptr) + { + tails[idx]->trPrev = link; + tails[idx]->iPrevDirection = (tails.size() == 3 ? ends3[idx] : ends2[idx]); + } + } + } + m_links.emplace_back(link); + } + } +} + +// links lanes of opposite directions at an end of the road which leads nowhere, so the vehicles can turn back +void road_node::close_ends() +{ + // NOTE: without the link a vehicle reaching the end would be sent back on the lane it came by, against its direction + if (m_lanetracks[0].size() != m_state.lanes.size()) + { + return; + } + road_axis const axis{m_state.axis}; + for (int end = 0; end < 2; ++end) + { + // the lanes going along the axis leave the road at its end, the ones going against it leave at its start + auto const outwards{end == 1 ? axis.frame(1.0).tangent : -axis.frame(0.0).tangent}; + for (int pair = 1; pair <= std::min(m_state.lanes_at(end, true), m_state.lanes_at(end, false)); ++pair) + { + auto const forward{static_cast(m_state.backward + pair - 1)}; + auto const backward{static_cast(m_state.backward - pair)}; + auto *exit{m_lanetracks[end][end == 1 ? forward : backward]}; + auto *entry{m_lanetracks[end][end == 1 ? backward : forward]}; + if (exit == nullptr || entry == nullptr || exit->trNext != nullptr || entry->trPrev != nullptr) + { + continue; + } + segment_data path; + path.points[segment_data::point::start] = exit->CurrentSegment()->FastGetPoint_1(); + path.points[segment_data::point::end] = entry->CurrentSegment()->FastGetPoint_0(); + auto const span{glm::distance(path.points[segment_data::point::start], path.points[segment_data::point::end])}; + path.points[segment_data::point::control1] = outwards * std::max(turn_reach, 1.5 * span); + path.points[segment_data::point::control2] = outwards * std::max(turn_reach, 1.5 * span); + path.radius = static_cast(std::max(2.0, 0.5 * span)); + auto *turn{create_track(m_name.empty() ? std::string{} : m_name + ":" + (end == 1 ? "endturn" : "startturn") + std::to_string(pair), path, m_state.lanes[forward].width, turn_velocity)}; + exit->ConnectNextPrev(turn, 0); + turn->ConnectNextPrev(entry, 0); + m_turns.emplace_back(turn); + } + } +} + +// gives up the paths of the lanes +std::vector road_node::release_lanes() +{ + std::vector released; + released.swap(m_tracks); + released.insert(released.end(), m_links.begin(), m_links.end()); + m_links.clear(); + m_ways.clear(); + for (auto &tracks : m_lanetracks) + { + tracks.clear(); + } + released.erase(std::remove(released.begin(), released.end(), nullptr), released.end()); + return released; +} + +// gives up the paths made to close the loose ends +std::vector road_node::release_turns() +{ + std::vector released; + released.swap(m_turns); + return released; +} + +// true if there's a vehicle on any path of the road +bool road_node::occupied() const +{ + for (auto const *tracks : {&m_tracks, &m_links, &m_turns}) + { + for (auto const *track : *tracks) + { + if (track != nullptr && false == track->Dynamics.empty()) + { + return true; + } + } + } + return false; +} + +// generates geometry of the surface, the sides and the markings +std::vector road_node::create_shapes() const +{ + // triangles are gathered separately for each look, so the lot can be drawn with as few shapes as possible + struct batch + { + std::string material; + lighting_data lighting; + std::vector vertices; + }; + std::vector batches; + auto const vertices = [&batches](std::string const &Material, lighting_data const &Lighting) -> std::vector & { + for (auto &entry : batches) + { + if (entry.material == Material && entry.lighting == Lighting) + { + return entry.vertices; + } + } + batches.emplace_back(); + batches.back().material = Material; + batches.back().lighting = Lighting; + return batches.back().vertices; + }; + + auto const &road{m_state}; + road_axis const axis{road.axis}; + auto const length{axis.length()}; + auto const step{axis.step()}; + auto const halfwidth{0.5 * road.width()}; + double const texturelength{road.texturelength}; + // where the two directions are kept apart there's a roadway for each, moved away from the axis by half of what's between them + auto const divided{road.divided()}; + auto const divide{road.divide()}; + auto const widest{static_cast(std::max(road.median.width[0], road.median.width[1]))}; + auto const height{static_cast(road.kerbheight)}; + auto const usable = [](std::string const &Material) { return false == Material.empty() && Material != "none"; }; + // on a piece which gains or loses lanes the edges and the line the directions meet at move across the axis along the way, + // so what's laid along them is measured from them. elsewhere they keep their distance from the axis, and it's measured from the axis + auto const changing{road.changing()}; + // distance of one of these lines from the axis, on a road as wide as its lanes say + auto const distance = [&](int const Line) { return Line == anchor_left ? halfwidth : Line == anchor_right ? -halfwidth : Line == anchor_middle ? divide : 0.0; }; + // where the line the directions meet at is, for specified distance from the start of the axis + auto const meeting = [&](double const Station) { + return changing ? road.middle(Station / length) : divide * glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(Station / length)); + }; + + // surface. the image is centered on the axis, and repeated if the road is wider + if (road.surface != "none") + { + auto const tile{static_cast(texture_ratio(road.surface)) * texturelength}; + auto const corner = [&](int const Line, double const Spread) { + if (changing) + { + return profile_point{0.0, 0.0, 0.5f, 0.0, Spread, 0.0, 0.0, Line, tile}; + } + auto const offset{distance(Line)}; + return profile_point{offset, 0.0, static_cast(0.5 + (offset + Spread * widest) / tile), 0.0, Spread}; + }; + auto &surface{vertices(road.surface, lighting_data{})}; + if (false == divided) + { + loft(surface, axis, {corner(anchor_left, 0.0), corner(anchor_right, 0.0)}, 0.0, length, step, texturelength, road); + } + else + { + loft(surface, axis, {corner(anchor_left, 0.5), corner(anchor_middle, 0.5)}, 0.0, length, step, texturelength, road); + if (road.median.type != median_type::gap) + { + // the surface carries on between the roadways; an island stands on it, so there's something under its rounded ends + loft(surface, axis, {corner(anchor_middle, 0.5), corner(anchor_middle, -0.5)}, 0.0, length, step, texturelength, road); + } + loft(surface, axis, {corner(anchor_middle, -0.5), corner(anchor_right, -0.5)}, 0.0, length, step, texturelength, road); + } + } + + // an island between the roadways: a kerb facing each of them, and what's between the kerbs + if (divided && road.median.type == median_type::island && length > 0.0) + { + auto const kerbed{usable(road.kerbmaterial)}; + auto const covered{usable(road.median.material)}; + // the ends of the island can be rounded; it gets narrower there along a quarter of an ellipse + std::array const nose{std::min(static_cast(road.median.round[0]), 0.5 * length), std::min(static_cast(road.median.round[1]), 0.5 * length)}; + int const nosesteps{8}; + double const quarter{1.5707963267948966}; + std::vector stations; + for (int idx = 0; idx < nosesteps && nose[0] > 0.0; ++idx) + { + stations.emplace_back(nose[0] * (1.0 - std::cos(quarter * idx / nosesteps))); + } + auto const body{length - nose[0] - nose[1]}; + auto const bodysteps{std::max(1, static_cast(std::ceil(body / step - 0.001)))}; + for (int idx = 0; idx <= bodysteps; ++idx) + { + stations.emplace_back(nose[0] + body * idx / bodysteps); + } + for (int idx = 1; idx <= nosesteps && nose[1] > 0.0; ++idx) + { + stations.emplace_back(length - nose[1] * (1.0 - std::sin(quarter * idx / nosesteps))); + } + // middle of the island and half of its width, for specified distance from the start of the axis + auto const &middle{meeting}; + auto const half = [&](double const Station) { + auto result{0.5 * glm::mix(static_cast(road.median.width[0]), static_cast(road.median.width[1]), taper_progress(Station / length))}; + if (Station < nose[0]) + { + auto const along{(nose[0] - Station) / nose[0]}; + result *= std::sqrt(std::max(0.0, 1.0 - along * along)); + } + else if (Station > length - nose[1]) + { + auto const along{(Station - (length - nose[1])) / nose[1]}; + result *= std::sqrt(std::max(0.0, 1.0 - along * along)); + } + return result; + }; + if (kerbed) + { + auto const tile{static_cast(texture_ratio(road.kerbmaterial)) * texturelength}; + auto const top{static_cast(height / tile)}; + auto const inner{static_cast((height + road.kerbwidth) / tile)}; + auto &kerbs{vertices(road.kerbmaterial, lighting_data{})}; + sweep(kerbs, axis, stations, texturelength, [&](double const Station, std::vector &Corners) { + auto const edge{middle(Station) + half(Station)}; + Corners = {{edge, 0.0, 0.f}, {edge, height, top}, {edge - std::min(static_cast(road.kerbwidth), half(Station)), height, inner}}; + }); + sweep(kerbs, axis, stations, texturelength, [&](double const Station, std::vector &Corners) { + auto const edge{middle(Station) - half(Station)}; + Corners = {{edge + std::min(static_cast(road.kerbwidth), half(Station)), height, inner}, {edge, height, top}, {edge, 0.0, 0.f}}; + }); + } + if (covered) + { + auto const tile{static_cast(texture_ratio(road.median.material)) * texturelength}; + sweep(vertices(road.median.material, lighting_data{}), axis, stations, texturelength, [&](double const Station, std::vector &Corners) { + auto const centre{middle(Station)}; + auto const reach{half(Station)}; + auto const inset{kerbed ? std::min(static_cast(road.kerbwidth), reach) : 0.0}; + Corners.clear(); + if (false == kerbed) + { + // without kerbs of their own material the sides of the island are covered with what its top is + Corners.push_back({centre + reach, 0.0, 0.f}); + } + Corners.push_back({centre + reach - inset, height, static_cast(height / tile)}); + Corners.push_back({centre - reach + inset, height, static_cast((height + 2.0 * reach) / tile)}); + if (false == kerbed) + { + Corners.push_back({centre - reach, 0.0, static_cast((2.0 * height + 2.0 * reach) / tile)}); + } + }); + } + // an end which isn't rounded gets a face, so the island isn't hollow where the next piece has none + if (kerbed || covered) + { + auto const &material{covered ? road.median.material : road.kerbmaterial}; + auto const tile{static_cast(texture_ratio(material)) * texturelength}; + for (int end = 0; end < 2; ++end) + { + auto const station{end == 0 ? 0.0 : length}; + auto const reach{half(station)}; + if (nose[end] > 0.0 || reach < 0.025) + { + continue; + } + auto const centre{middle(station)}; + std::vector const face{{centre + reach, 0.0, 0.f}, {centre + reach, height, 0.f}, {centre - reach, height, 0.f}, {centre - reach, 0.0, 0.f}}; + cap(vertices(material, lighting_data{}), axis.at(station), face, end == 1, tile); + } + } + } + + // sides: a kerb, then a shoulder or a sidewalk, then a bank leading to the ground; each of them optional + for (int side = 0; side < 2; ++side) + { + auto const &data{road.sides[side]}; + auto const direction{side == 0 ? 1.0 : -1.0}; + auto const sided{data.type != side_type::none && data.width > 0.f && usable(data.material)}; + auto const kerbed{road.kerbs[side] && usable(road.kerbmaterial)}; + // the profiles are given from the edge of the surface outwards, as distances from that edge + auto const emit = [&](std::string const &Material, std::vector Profile) { + for (auto &point : Profile) + { + point.offset = (changing ? 0.0 : direction * halfwidth); + point.anchor = (changing ? (side == 0 ? anchor_left : anchor_right) : anchor_axis); + point.spread = (divided ? direction * 0.5 : 0.0); + point.shift *= direction; + point.grow *= direction; + } + if (side == 0) + { + // the loft takes the corners left to right + std::reverse(Profile.begin(), Profile.end()); + } + loft(vertices(Material, lighting_data{}), axis, Profile, 0.0, length, step, texturelength, road); + }; + // faces closing what's raised over the surface at both ends of the piece, so it isn't hollow where the next piece has none. + // From, To: distances from the edge of the surface + auto const close = [&](std::string const &Material, double const From, double const To) { + auto const tile{static_cast(texture_ratio(Material)) * texturelength}; + for (int end = 0; end < 2; ++end) + { + auto const edge{direction * (0.5 * road.width(end) * road.taper[end] + (divided ? 0.5 * road.median.width[end] : 0.0))}; + cap(vertices(Material, lighting_data{}), axis.at(end == 0 ? 0.0 : length), + {{edge + direction * From, 0.0, 0.f}, {edge + direction * From, height, 0.f}, {edge + direction * To, height, 0.f}, {edge + direction * To, 0.0, 0.f}}, end == 1, tile); + } + }; + double reach{0.0}; // how far from the edge of the surface the next thing starts + double level{0.0}; // and how high + if (kerbed) + { + auto const tile{static_cast(texture_ratio(road.kerbmaterial)) * texturelength}; + double const width{road.kerbwidth}; + close(road.kerbmaterial, 0.0, width); + std::vector kerb{{0.0, 0.0, 0.f, 0.0}, {0.0, height, static_cast(height / tile), 0.0}, {0.0, height, static_cast((height + width) / tile), width}}; + if (false == sided || data.type != side_type::sidewalk) + { + // with nothing raised behind it the kerb has a back + kerb.push_back({0.0, 0.0, static_cast((2.0 * height + width) / tile), width}); + } + emit(road.kerbmaterial, kerb); + reach = width; + } + if (sided && data.type == side_type::shoulder) + { + // the image is laid out like the one of the legacy roads: the edge of the surface on its right, the bank on its left half + std::vector profile{{0.0, 0.0, 1.f, reach}, {0.0, 0.0, 0.5f, reach + data.width}}; + reach += data.width; + auto const &bank{road.banks[side]}; + glm::dvec2 const start{bank.set ? bank.width[0] : road.slope.x, bank.set ? bank.drop[0] : road.slope.y}; + glm::dvec2 const end{bank.set ? bank.width[1] : road.slope.x, bank.set ? bank.drop[1] : road.slope.y}; + if (start.x > 0.0 || end.x > 0.0) + { + profile.push_back({0.0, -start.y, 0.f, reach + start.x, 0.0, end.x - start.x, start.y - end.y}); + } + emit(data.material, profile); + } + else if (sided) + { + // the image starts at the foot of the kerb, and is laid out in its own proportions + auto const tile{static_cast(texture_ratio(data.material)) * texturelength}; + std::vector profile; + if (false == kerbed) + { + profile.push_back({0.0, 0.0, 0.f, 0.0}); + } + profile.push_back({0.0, height, static_cast(height / tile), reach}); + profile.push_back({0.0, height, static_cast((height + data.width) / tile), reach + data.width}); + emit(data.material, profile); + close(data.material, reach, reach + data.width); + reach += data.width; + level = height; + } + // a bank next to something other than a shoulder is covered with a material of its own + auto const &bank{road.banks[side]}; + if ((false == sided || data.type != side_type::shoulder) && bank.set && usable(road.bankmaterial) && (bank.width[0] > 0.f || bank.width[1] > 0.f)) + { + auto const tile{static_cast(texture_ratio(road.bankmaterial)) * texturelength}; + profile_point foot{0.0, level - bank.drop[0], static_cast(std::max(bank.width[0], bank.width[1]) / tile), reach + bank.width[0]}; + foot.grow = static_cast(bank.width[1]) - bank.width[0]; + foot.sink = static_cast(bank.drop[0]) - bank.drop[1]; + emit(road.bankmaterial, {{0.0, level, 0.f, reach}, foot}); + } + } + + // markings. these are painted with plain colour instead of an image + if (road.markings != marking_colour::none) + { + lighting_data paint; + paint.diffuse = (road.markings == marking_colour::white ? glm::vec4{0.92f, 0.92f, 0.92f, 1.f} : glm::vec4{0.95f, 0.5f, 0.08f, 1.f}); + paint.ambient = paint.diffuse; + auto &lines{vertices("colored", paint)}; + + // Line, Offset: where the line runs on a road as wide as its lanes say, as a distance from one of the lines of the road, + // Shift: how far from there it's kept whatever the width, Spread: which of the roadways it goes with, where the road has two, + // From, To: part of the piece it's painted on + auto const solid = [&](int const Line, double const Offset, double const Shift, double const Spread, double const From, double const To) { + std::vector const line{{Offset, line_lift, 0.f, Shift + 0.5 * line_width, Spread, 0.0, 0.0, Line}, + {Offset, line_lift, 1.f, Shift - 0.5 * line_width, Spread, 0.0, 0.0, Line}}; + loft(lines, axis, line, From, To, std::min(step, dash_step), texturelength, road); + }; + // the dashes are fitted so the gaps at both ends of the road are a half of the regular one, which makes them match the next piece + auto const dashed = [&](int const Line, double const Offset, double const Shift, double const Spread, double const From, double const To) { + auto const count{std::max(1, static_cast(std::lround(length / dash_period)))}; + auto const period{length / count}; + std::vector const line{{Offset, line_lift, 0.f, Shift + 0.5 * line_width, Spread, 0.0, 0.0, Line}, + {Offset, line_lift, 1.f, Shift - 0.5 * line_width, Spread, 0.0, 0.0, Line}}; + for (int dash = 0; dash < count; ++dash) + { + auto const from{(dash + 1.0 / 3.0) * period}; + if (from < From || from + period / 3.0 > To) + { + continue; + } + loft(lines, axis, line, from, from + period / 3.0, dash_step, texturelength, road); + } + }; + // part of the piece the line between two lanes is painted on. a lane the road gains or loses along the piece + // gets its line where there's room for a half of it + auto const stretch = [&](std::size_t const Boundary) { + auto const against{static_cast(road.backward)}; + std::pair result{0.0, length}; + if (false == changing || Boundary + 1 == against) + { + return result; + } + // the one of the two which is further from where the directions meet + auto const outer{Boundary + 1 < against ? Boundary : Boundary + 1}; + if (road.lane_at(outer, 0) && road.lane_at(outer, 1)) + { + return result; + } + // what the direction needs to be as wide as for the line, on a road as wide as its lanes say + auto needed{0.5 * road.lanes[outer].width}; + for (auto lane{outer < against ? outer + 1 : against}; lane < (outer < against ? against : outer); ++lane) + { + needed += road.lanes[lane].width; + } + int const samples{64}; + auto const room = [&](int const Sample) { + auto const fraction{static_cast(Sample) / samples}; + auto const half{0.5 * road.breadth(fraction)}; + auto const middle{road.middle(fraction)}; + auto const scale{glm::mix(static_cast(road.taper[0]), static_cast(road.taper[1]), taper_progress(fraction))}; + return (outer < against ? half - middle : half + middle) >= needed * scale; + }; + auto first{0}; + while (first <= samples && false == room(first)) + { + ++first; + } + auto last{samples}; + while (last >= first && false == room(last)) + { + --last; + } + result = {length * first / samples, length * last / samples}; + return result; + }; + + // edges of the surface + auto const inset{edge_inset + 0.5 * line_width}; + if (0.5 * std::min(road.width(0) * road.taper[0], road.width(1) * road.taper[1]) - inset > 0.5) + { + solid(changing ? anchor_left : anchor_axis, changing ? 0.0 : halfwidth, -inset, divided ? 0.5 : 0.0, 0.0, length); + solid(changing ? anchor_right : anchor_axis, changing ? 0.0 : -halfwidth, inset, divided ? -0.5 : 0.0, 0.0, length); + } + // lines between the lanes. a lane can be left across a dashed line, or across a pair of lines if the dashed one is on its side + auto offset{halfwidth}; + for (std::size_t boundary = 0; boundary < road.changes.size() && boundary < road.lanes.size(); ++boundary) + { + offset -= road.lanes[boundary].width; + auto const opposite{road.backward > 0 && boundary + 1 == static_cast(road.backward)}; + auto const pair{0.5 * (line_width + line_spacing)}; + // the lanes keep their widths, and so the lines between them keep their distance from where the directions meet + auto const from{changing ? anchor_middle : anchor_axis}; + auto const place{changing ? offset - divide : offset}; + auto const range{stretch(boundary)}; + if (changing && range.second - range.first < 1.0) + { + continue; + } + if (opposite && divided) + { + // each of the two roadways gets its edge line + solid(from, place, inset, 0.5, range.first, range.second); + solid(from, place, -inset, -0.5, range.first, range.second); + continue; + } + auto const spread{false == divided ? 0.0 : boundary + 1 < static_cast(road.backward) ? 0.5 : -0.5}; + switch (road.changes[boundary]) + { + case change_both: + { + dashed(from, place, 0.0, spread, range.first, range.second); + break; + } + case change_toright: + { + dashed(from, place, pair, spread, range.first, range.second); + solid(from, place, -pair, spread, range.first, range.second); + break; + } + case change_toleft: + { + solid(from, place, pair, spread, range.first, range.second); + dashed(from, place, -pair, spread, range.first, range.second); + break; + } + default: + { + if (opposite) + { + // traffic going opposite ways is kept apart with a double line + solid(from, place, pair, spread, range.first, range.second); + solid(from, place, -pair, spread, range.first, range.second); + } + else + { + solid(from, place, 0.0, spread, range.first, range.second); + } + break; + } + } + } + if (divided && road.median.type == median_type::painted && length > 0.0) + { + // what's between the roadways is closed to the traffic: slanted stripes are led from one edge line to the other + double const period{3.0}; + double const stripe{0.5}; + glm::vec3 const upwards{0.f, 1.f, 0.f}; + auto const gap_at = [&](double const Station) { return glm::mix(static_cast(road.median.width[0]), static_cast(road.median.width[1]), taper_progress(Station / length)); }; + // point of the edge of one of the roadways, a bit into what's between them. Side: 1 for the left one, -1 for the right one + auto const edge = [&](double const Station, double const Side) { + auto const station{std::clamp(Station, 0.0, length)}; + auto const frame{axis.at(station)}; + return frame.position + frame.left * (meeting(station) + Side * std::max(0.0, 0.5 * gap_at(station) - 2.0 * inset)) + up * line_lift; + }; + // the stripes are spread evenly over the piece, and none of them reaches past its ends + auto const count{std::max(1, static_cast(std::lround(length / period)))}; + auto const spacing{length / count}; + for (int index = 0; index < count; ++index) + { + auto const centre{(index + 0.5) * spacing}; + auto const gap{gap_at(centre)}; + if (gap < 1.0) + { + continue; + } + // a stripe gets as far along the road as it gets across it, as long as that leaves room for the next one + auto const slant{std::clamp(gap, 0.0, std::max(0.0, spacing - stripe - 0.25))}; + auto const station{centre - 0.5 * (slant + stripe)}; + auto const leftstart{edge(station, 1.0)}; + auto const leftend{edge(station + stripe, 1.0)}; + auto const rightstart{edge(station + slant, -1.0)}; + auto const rightend{edge(station + slant + stripe, -1.0)}; + lines.push_back({leftstart, upwards, {0.f, 0.f}}); + lines.push_back({rightstart, upwards, {1.f, 0.f}}); + lines.push_back({leftend, upwards, {0.f, 1.f}}); + lines.push_back({rightstart, upwards, {1.f, 0.f}}); + lines.push_back({rightend, upwards, {1.f, 1.f}}); + lines.push_back({leftend, upwards, {0.f, 1.f}}); + } + } + } + + std::vector shapes; + for (auto &entry : batches) + { + if (entry.vertices.empty()) + { + continue; + } + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material(entry.material), std::move(entry.vertices), glm::dvec3{0.0}); + shape.lighting(entry.lighting); + shapes.emplace_back(std::move(shape)); + } + return shapes; +} + +namespace +{ + +// banks holding geometry put in the scene by the owned shapes +std::unordered_set ownedbanks; + +double const junction_reach{0.25}; // an end of a road this close to an arm is attached to it +double const junction_fit{0.05}; // a lane has to end this close to where the junction expects it +int const corner_steps{6}; // pieces a rounded corner is made of +double const stopline_width{0.5}; +double const stopline_inset{0.2}; // distance of a stop line from the end of its road +double const gate_period{0.2}; // seconds between looks at the traffic around a junction +double const gate_reach{120.0}; // how far back along the lanes leading to a junction the vehicles are looked for +double const gate_gap{6.0}; // a vehicle this many seconds from a junction starts to matter to the others, and is told about them +double const gate_zone{15.0}; // and so does one this close, however slow it goes +double const gate_commit{8.0}; // a vehicle let through this close to the junction is on its way, the others wait for it +double const gate_patience{6.0}; // seconds a vehicle let through can stand still before the others stop waiting for it +double const way_clearance{2.2}; // ways through a junction which pass this close to each other can't be taken at the same time + +double const crosswalk_inset{1.0}; // distance of a pedestrian crossing from the end of its road, which leaves room for a stop line + +// corner of the cross-section of what lines the edge of a junction +struct edge_point +{ + double offset; // distance from the edge of the surface + double height; + float texture; + double grow{0.0}; // what the offset changes by along the edge, evenly + double sink{0.0}; // what the height changes by along the edge, evenly +}; + +// something which lines the edge of a junction: a kerb, a shoulder with its bank, a sidewalk, a bank +struct edge_lining +{ + std::string material; + std::vector profile; +}; + +// what the edge of a junction is lined with, from the edge outwards. Bank: slope leading to the ground from that edge +std::vector edge_linings(junction_node::state const &State, road_node::bank_data const &Bank) +{ + std::vector linings; + auto const usable = [](std::string const &Material) { return false == Material.empty() && Material != "none"; }; + auto const &side{State.side}; + auto const sided{side.type != road_node::side_type::none && side.width > 0.f && usable(side.material)}; + auto const kerbed{State.kerbs && usable(State.kerbmaterial)}; + double const height{State.kerbheight}; + double reach{0.0}; // how far from the edge of the surface the next thing starts + double level{0.0}; // and how high + if (kerbed) + { + auto const tile{static_cast(texture_ratio(State.kerbmaterial)) * State.texturelength}; + double const width{State.kerbwidth}; + edge_lining kerb; + kerb.material = State.kerbmaterial; + kerb.profile = {{0.0, 0.0, 0.f}, {0.0, height, static_cast(height / tile)}, {width, height, static_cast((height + width) / tile)}}; + if (false == sided || side.type != road_node::side_type::sidewalk) + { + // with nothing raised behind it the kerb has a back + kerb.profile.push_back({width, 0.0, static_cast((2.0 * height + width) / tile)}); + } + linings.emplace_back(kerb); + reach = width; + } + if (sided && side.type == road_node::side_type::shoulder) + { + edge_lining shoulder; + shoulder.material = side.material; + shoulder.profile = {{reach, 0.0, 1.f}, {reach + side.width, 0.0, 0.5f}}; + reach += side.width; + glm::dvec2 const start{Bank.set ? Bank.width[0] : State.slope.x, Bank.set ? Bank.drop[0] : State.slope.y}; + glm::dvec2 const end{Bank.set ? Bank.width[1] : State.slope.x, Bank.set ? Bank.drop[1] : State.slope.y}; + if (start.x > 0.0 || end.x > 0.0) + { + shoulder.profile.push_back({reach + start.x, -start.y, 0.f, end.x - start.x, start.y - end.y}); + } + linings.emplace_back(shoulder); + } + else if (sided) + { + auto const tile{static_cast(texture_ratio(side.material)) * State.texturelength}; + edge_lining sidewalk; + sidewalk.material = side.material; + if (false == kerbed) + { + sidewalk.profile.push_back({0.0, 0.0, 0.f}); + } + sidewalk.profile.push_back({reach, height, static_cast(height / tile)}); + sidewalk.profile.push_back({reach + side.width, height, static_cast((height + side.width) / tile)}); + linings.emplace_back(sidewalk); + reach += side.width; + level = height; + } + // a bank next to something other than a shoulder is covered with a material of its own + if ((false == sided || side.type != road_node::side_type::shoulder) && Bank.set && usable(State.bankmaterial) && (Bank.width[0] > 0.f || Bank.width[1] > 0.f)) + { + auto const tile{static_cast(texture_ratio(State.bankmaterial)) * State.texturelength}; + edge_lining bank; + bank.material = State.bankmaterial; + bank.profile = {{reach, level, 0.f}, {reach + Bank.width[0], level - Bank.drop[0], static_cast(std::max(Bank.width[0], Bank.width[1]) / tile)}}; + bank.profile.back().grow = static_cast(Bank.width[1]) - Bank.width[0]; + bank.profile.back().sink = static_cast(Bank.drop[0]) - Bank.drop[1]; + linings.emplace_back(bank); + } + return linings; +} + +// direction on the ground as a vector in space +glm::dvec3 flat(glm::dvec2 const &Direction) +{ + return glm::dvec3{Direction.x, 0.0, Direction.y}; +} + +// direction to the left of the provided one, on the ground +glm::dvec2 left_of(glm::dvec2 const &Direction) +{ + return glm::dvec2{Direction.y, -Direction.x}; +} + +// angle of the turn from one direction to another, positive to the left +double turn_angle(glm::dvec2 const &From, glm::dvec2 const &To) +{ + return std::atan2(glm::dot(To, left_of(From)), glm::dot(To, From)); +} + +// slope a lane has at one of its ends, in the direction of travel +double lane_grade(TTrack const &Lane, bool const Atend) +{ + if (Lane.m_paths.empty()) + { + return 0.0; + } + auto const &path{Lane.m_paths.front()}; + auto const &control{path.points[Atend ? segment_data::point::control2 : segment_data::point::control1]}; + auto const direction{control != glm::dvec3{0.0} ? (Atend ? -control : control) : path.points[segment_data::point::end] - path.points[segment_data::point::start]}; + auto const run{glm::length(glm::dvec2{direction.x, direction.z})}; + return run > 1e-6 ? direction.y / run : 0.0; +} + +// what a driver calls the way of a fork which leads to specified lane: the number of the path of the crossroads it's on, +// negative if it's taken against the path. 0 if the fork doesn't lead to the lane +int fork_code(TTrack const &Fork, TTrack const *Lane) +{ + if (Fork.SwitchExtension == nullptr || Lane == nullptr) + { + return 0; + } + auto const &extension{*Fork.SwitchExtension}; + if (extension.pNexts[0] == Lane) + { + return 1; + } + if (extension.pNexts[1] != nullptr) + { + // three ways out + return extension.pNexts[1] == Lane ? 5 : extension.pPrevs[1] == Lane ? -4 : 0; + } + return extension.pPrevs[1] == Lane ? -2 : 0; +} + +} // namespace + +// puts provided shapes in the section of the scene holding specified point +void owned_shapes::show(std::vector Shapes, glm::dvec3 const &Location) +{ + if (m_section != nullptr || Shapes.empty() || simulation::Region == nullptr || false == simulation::Region->point_inside(Location)) + { + return; + } + auto §ion{simulation::Region->section(Location)}; + // the rest of the section has to be built by now, or it'd count our shapes as waiting for it too + section.create_geometry(); + if (m_bank.bank == 0 && m_bank.chunk == 0) + { + m_bank = GfxRenderer->Create_Bank(); + ownedbanks.emplace(m_bank.bank); + } + for (auto &shape : Shapes) + { + // shapes held by a section are drawn relative to its centre + shape.origin(section.m_area.center); + auto const centre{shape.data().area.center}; + auto const radius{shape.radius()}; + shape.create_geometry(m_bank); + m_geometry.emplace_back(shape.data().geometry); + section.m_shapes.emplace_back(std::move(shape)); + // the section may need to reach further to keep the shape from being culled at its edges + section.m_area.radius = std::max(section.m_area.radius, static_cast(glm::length(section.m_area.center - centre) + radius)); + } + m_section = §ion; +} + +// takes the shapes back +void owned_shapes::hide() +{ + if (m_section == nullptr) + { + return; + } + // our shapes are told from the others by their geometry. the renderer reclaims the chunks once they're no longer drawn + auto &shapes{m_section->m_shapes}; + shapes.erase(std::remove_if(shapes.begin(), shapes.end(), + [this](scene::shape_node const &Shape) { + auto const handle{Shape.data().geometry}; + return std::any_of(m_geometry.begin(), m_geometry.end(), [&handle](gfx::geometry_handle const &Own) { return Own.bank == handle.bank && Own.chunk == handle.chunk; }); + }), + shapes.end()); + m_geometry.clear(); + m_section = nullptr; +} + +// true if specified geometry was put in the scene by any object of this class +bool owned_shapes::owns(gfx::geometry_handle const &Geometry) +{ + return Geometry.bank != 0 && ownedbanks.count(Geometry.bank) != 0; +} + +// puts geometry of the road in the scene as shapes of its own, which can be taken back +void road_node::show() +{ + if (m_merged || m_shapes.shown()) + { + return; + } + m_shapes.show(create_shapes(), location()); +} + +// takes back geometry put in the scene by show() +void road_node::hide() +{ + m_shapes.hide(); +} + +// brings the content to a usable form +void junction_node::state::normalize() +{ + for (auto &arm : arms) + { + arm.incoming = std::clamp(arm.incoming, 0, 8); + arm.outgoing = std::clamp(arm.outgoing, 0, 8); + arm.width = std::max(1.f, arm.width); + arm.median = (arm.incoming > 0 && arm.outgoing > 0 ? std::clamp(arm.median, 0.f, 100.f) : 0.f); + arm.crosswalk = std::clamp(arm.crosswalk, 0.f, 20.f); + for (int end = 0; end < 2; ++end) + { + arm.bank.width[end] = std::clamp(arm.bank.width[end], 0.f, 100.f); + arm.bank.drop[end] = std::clamp(arm.bank.drop[end], -100.f, 100.f); + } + arm.direction = (glm::length(arm.direction) > 1e-6 ? glm::normalize(arm.direction) : glm::dvec2{0.0, 1.0}); + if (arm.turns.size() > static_cast(arm.incoming)) + { + arm.turns.resize(static_cast(arm.incoming)); + } + for (auto &turns : arm.turns) + { + turns &= (turn_left | turn_straight | turn_right); + } + } + side.width = std::max(0.f, side.width); + kerbheight = std::max(0.f, kerbheight); + kerbwidth = std::clamp(kerbwidth, 0.05f, 2.f); + if (kerbmaterial.empty()) + { + kerbmaterial = "none"; + } + if (bankmaterial.empty()) + { + bankmaterial = "none"; + } + if (medianmaterial.empty()) + { + medianmaterial = "none"; + } + slope = glm::max(slope, glm::vec2{0.f}); + if (texturelength < 0.01f) + { + texturelength = 4.f; + } + if (surface.empty()) + { + surface = "none"; + } + static std::array const environments{"flat", "mountains", "mountain", "canyon", "tunnel", "bridge", "bank"}; + if (std::find(environments.begin(), environments.end(), environment) == environments.end()) + { + environment = "flat"; + } +} + +// width of the road at specified arm +double junction_node::state::arm_width(std::size_t const Arm) const +{ + return Arm < arms.size() ? static_cast(arms[Arm].incoming + arms[Arm].outgoing) * arms[Arm].width + arms[Arm].median : 0.0; +} + +// where a lane of an arm meets the junction; the lanes of each kind are counted from the middle of the road, starting with 1 +glm::dvec3 junction_node::state::lane_point(std::size_t const Arm, bool const Incoming, int const Lane) const +{ + auto const &arm{arms[Arm]}; + // seen from the junction the traffic coming in is on the left side of the road, and the one going out on the right + auto const divide{0.5 * arm_width(Arm) - static_cast(arm.incoming) * arm.width}; + // what keeps the two directions apart lies past the lanes coming in + auto const offset{Incoming ? divide + (Lane - 0.5) * arm.width : divide - arm.median - (Lane - 0.5) * arm.width}; + return arm.position + flat(left_of(arm.direction)) * offset; +} + +// arms in the order they're met going around the junction +std::vector junction_node::state::arm_order() const +{ + std::vector order(arms.size()); + for (std::size_t idx = 0; idx < order.size(); ++idx) + { + order[idx] = idx; + } + // the order goes from the z axis towards the x axis, which makes triangles spanned from the centre face up + std::sort(order.begin(), order.end(), [this](std::size_t const Left, std::size_t const Right) { return std::atan2(arms[Left].direction.x, arms[Left].direction.y) < std::atan2(arms[Right].direction.x, arms[Right].direction.y); }); + return order; +} + +// edge of the surface: ends of the arms joined with rounded corners, going around the junction +std::vector junction_node::state::outline(std::vector> *Corners) const +{ + std::vector points; + auto const order{arm_order()}; + for (std::size_t idx = 0; idx < order.size(); ++idx) + { + auto const &arm{arms[order[idx]]}; + auto const &next{arms[order[(idx + 1) % order.size()]]}; + auto const halfwidth{0.5 * arm_width(order[idx])}; + auto const nexthalfwidth{0.5 * arm_width(order[(idx + 1) % order.size()])}; + // the end of the arm, then the corner leading to the next one + auto const right{arm.position - flat(left_of(arm.direction)) * halfwidth}; + auto const left{arm.position + flat(left_of(arm.direction)) * halfwidth}; + auto const nextright{next.position - flat(left_of(next.direction)) * nexthalfwidth}; + points.emplace_back(right); + points.emplace_back(left); + // the corner is bent towards the point where the edges of both roads would meet, if there's a reasonable one + auto control{0.5 * (left + nextright)}; + auto const cross{arm.direction.x * next.direction.y - arm.direction.y * next.direction.x}; + if (std::abs(cross) > 0.05) + { + // left - arm.direction * t == nextright - next.direction * s + glm::dvec2 const delta{nextright.x - left.x, nextright.z - left.z}; + auto const t{-(delta.x * next.direction.y - delta.y * next.direction.x) / cross}; + auto const s{-(delta.x * arm.direction.y - delta.y * arm.direction.x) / cross}; + if (t > 0.0 && s > 0.0 && t < 100.0 && s < 100.0) + { + control = left - flat(arm.direction) * t; + control.y = 0.5 * (left.y + nextright.y); + } + } + std::vector corner{left}; + for (int step = 1; step < corner_steps; ++step) + { + auto const t{static_cast(step) / corner_steps}; + auto const point{(1.0 - t) * (1.0 - t) * left + 2.0 * (1.0 - t) * t * control + t * t * nextright}; + points.emplace_back(point); + corner.emplace_back(point); + } + corner.emplace_back(nextright); + if (Corners != nullptr) + { + Corners->emplace_back(std::move(corner)); + } + } + return points; +} + +junction_node::junction_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {} + +// restores content of the node from provided input stream; reads up to and including the closing 'endjunction' +void junction_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + auto const label{m_name.empty() ? std::string{"unnamed junction"} : "junction \"" + m_name + "\""}; + + m_state = state{}; + Input.getTokens(3); + Input >> m_state.centre.x >> m_state.centre.y >> m_state.centre.z; + m_state.centre += Offset; + std::vector stoplines; + // what's said about the arms may come ahead of them, so it waits until all is read + std::vector> priorities; + std::vector> medians; + std::vector> crosswalks; + std::vector> banks; + struct turn_property + { + int arm; + int lane; + std::string ways; + }; + std::vector turns; + + auto token{Input.getToken()}; + while (false == token.empty() && token != "endjunction") + { + if (token == "arm") + { + // arm + arm_data arm; + Input.getTokens(3); + Input >> arm.position.x >> arm.position.y >> arm.position.z; + arm.position += Offset; + Input.getTokens(2); + Input >> arm.direction.x >> arm.direction.y; + Input.getTokens(3); + Input >> arm.incoming >> arm.outgoing >> arm.width; + m_state.arms.emplace_back(arm); + } + else if (token == "surface") + { + m_state.surface = Input.getToken(); + replace_slashes(m_state.surface); + } + else if (token == "texlength") + { + Input.getTokens(); + Input >> m_state.texturelength; + } + else if (token == "markings") + { + auto const colour{Input.getToken()}; + m_state.markings = (colour == "white" ? road_node::marking_colour::white : colour == "orange" ? road_node::marking_colour::orange : road_node::marking_colour::none); + } + else if (token == "side") + { + // side + auto const type{Input.getToken()}; + Input.getTokens(); + Input >> m_state.side.width; + m_state.side.material = Input.getToken(); + replace_slashes(m_state.side.material); + m_state.side.type = (type == "shoulder" ? road_node::side_type::shoulder : type == "sidewalk" ? road_node::side_type::sidewalk : road_node::side_type::none); + if (type != "shoulder" && type != "sidewalk" && type != "none") + { + ErrorLog("Bad junction: unknown kind of side \"" + type + "\" defined for " + label); + } + } + else if (token == "kerb") + { + Input.getTokens(); + Input >> m_state.kerbheight; + } + else if (token == "slope") + { + // slope + Input.getTokens(2); + Input >> m_state.slope.x >> m_state.slope.y; + } + else if (token == "kerbs") + { + // kerbs + m_state.kerbs = true; + Input.getTokens(); + Input >> m_state.kerbwidth; + m_state.kerbmaterial = Input.getToken(); + replace_slashes(m_state.kerbmaterial); + } + else if (token == "stopline") + { + // stopline ; the arms it's for may come later, so the numbers wait + int number{0}; + Input.getTokens(); + Input >> number; + stoplines.emplace_back(number); + } + else if (token == "priority") + { + // priority + int number{0}; + Input.getTokens(); + Input >> number; + priorities.emplace_back(number, Input.getToken()); + } + else if (token == "crosswalk") + { + // crosswalk + int number{0}; + float length{0.f}; + Input.getTokens(2); + Input >> number >> length; + crosswalks.emplace_back(number, length); + } + else if (token == "cornerbank") + { + // cornerbank + int number{0}; + road_node::bank_data bank; + bank.set = true; + Input.getTokens(5); + Input >> number >> bank.width[0] >> bank.drop[0] >> bank.width[1] >> bank.drop[1]; + banks.emplace_back(number, bank); + } + else if (token == "bankmaterial") + { + m_state.bankmaterial = Input.getToken(); + replace_slashes(m_state.bankmaterial); + } + else if (token == "median") + { + // median + auto const type{Input.getToken()}; + m_state.median = (type == "gap" ? road_node::median_type::gap : type == "island" ? road_node::median_type::island : road_node::median_type::painted); + m_state.medianmaterial = Input.getToken(); + replace_slashes(m_state.medianmaterial); + } + else if (token == "armmedian") + { + // armmedian + int number{0}; + float width{0.f}; + Input.getTokens(2); + Input >> number >> width; + medians.emplace_back(number, width); + } + else if (token == "turns") + { + // turns + turn_property property; + property.arm = 0; + property.lane = 0; + Input.getTokens(2); + Input >> property.arm >> property.lane; + property.ways = Input.getToken(); + turns.emplace_back(property); + } + else if (token == "velocity") + { + Input.getTokens(); + Input >> m_state.velocity; + } + else if (token == "friction") + { + Input.getTokens(); + Input >> m_state.friction; + } + else if (token == "environment") + { + m_state.environment = Input.getToken(); + } + else + { + ErrorLog("Bad junction: unknown property: \"" + token + "\" defined for " + label); + } + token = Input.getToken(); + } + for (auto const number : stoplines) + { + if (number < 1 || number > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(number) + " to put a stop line at"); + continue; + } + m_state.arms[number - 1].stopline = true; + } + for (auto const &entry : medians) + { + if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(entry.first) + " to set the median for"); + continue; + } + m_state.arms[entry.first - 1].median = entry.second; + } + for (auto const &entry : crosswalks) + { + if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(entry.first) + " to paint a pedestrian crossing at"); + continue; + } + m_state.arms[entry.first - 1].crosswalk = entry.second; + } + for (auto const &entry : banks) + { + if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(entry.first) + " to set the bank for"); + continue; + } + m_state.arms[entry.first - 1].bank = entry.second; + } + for (auto const &entry : priorities) + { + if (entry.first < 1 || entry.first > static_cast(m_state.arms.size())) + { + ErrorLog("Bad junction: " + label + " has no arm " + std::to_string(entry.first) + " to set the right of way for"); + continue; + } + auto &priority{m_state.arms[entry.first - 1].priority}; + priority = (entry.second == "main" ? right_of_way::priority : entry.second == "yield" ? right_of_way::yield : entry.second == "stop" ? right_of_way::stop : right_of_way::none); + if (entry.second != "main" && entry.second != "yield" && entry.second != "stop" && entry.second != "none") + { + ErrorLog("Bad junction: unknown right of way \"" + entry.second + "\" defined for " + label); + } + } + for (auto const &entry : turns) + { + if (entry.arm < 1 || entry.arm > static_cast(m_state.arms.size()) || entry.lane < 1 || entry.lane > m_state.arms[entry.arm - 1].incoming) + { + ErrorLog("Bad junction: " + label + " has no lane " + std::to_string(entry.lane) + " leading in at arm " + std::to_string(entry.arm)); + continue; + } + auto &lanes{m_state.arms[entry.arm - 1].turns}; + lanes.resize(std::max(lanes.size(), static_cast(entry.lane)), 0); + auto const has = [&entry](char const Way) { return entry.ways.find(Way) != std::string::npos; }; + lanes[entry.lane - 1] = (has('l') ? turn_left : 0) | (has('s') ? turn_straight : 0) | (has('r') ? turn_right : 0); + } + if (m_state.arms.size() < 2 || m_state.arms.size() > 4) + { + ErrorLog("Bad junction: " + label + " has " + std::to_string(m_state.arms.size()) + " arms, it takes 2 to 4"); + if (m_state.arms.size() > 4) + { + m_state.arms.resize(4); + } + } + m_state.normalize(); + location(m_state.centre); +} + +void junction_node::define(state const &State) +{ + m_state = State; + m_state.normalize(); + location(m_state.centre); + m_area.radius = -1.f; +} + +// a junction of two roads is a stretch where a road changes its lanes, which a road can do by itself +bool junction_node::as_road(road_node::state &Road) const +{ + auto const &arms{m_state.arms}; + if (arms.size() != 2) + { + return false; + } + auto const &from{arms[0]}; + auto const &to{arms[1]}; + auto const span{glm::distance(from.position, to.position)}; + if (span < 0.1) + { + return false; + } + road_node::state road; + // the axis is the way from the first road to the second, which leaves and joins them the way they go + road.axis.points[segment_data::point::start] = from.position; + road.axis.points[segment_data::point::control1] = glm::dvec3{0.0} - flat(from.direction) * (0.39 * span); + road.axis.points[segment_data::point::control2] = glm::dvec3{0.0} - flat(to.direction) * (0.39 * span); + road.axis.points[segment_data::point::end] = to.position; + // at the first road the lanes leading into the junction go the way the axis does, at the second one it's the lanes leading out of it + road.lanewidth = from.width; + road.velocity = m_state.velocity; + road.lanes_between(from.incoming, from.outgoing, to.outgoing, to.incoming); + road.taper = {1.f, from.width > 0.f ? to.width / from.width : 1.f}; + road.median.type = m_state.median; + road.median.material = m_state.medianmaterial; + road.median.width = {from.median, to.median}; + road.surface = m_state.surface; + road.texturelength = m_state.texturelength; + road.markings = m_state.markings; + road.sides = {m_state.side, m_state.side}; + road.kerbs = {m_state.kerbs, m_state.kerbs}; + road.kerbwidth = m_state.kerbwidth; + road.kerbmaterial = m_state.kerbmaterial; + road.kerbheight = m_state.kerbheight; + road.slope = m_state.slope; + // the bank of the corner which starts at the first road runs along the right edge. the one which starts at the second road + // runs along the left edge, the other way + road.banks[1] = from.bank; + road.banks[0] = to.bank; + std::swap(road.banks[0].width[0], road.banks[0].width[1]); + std::swap(road.banks[0].drop[0], road.banks[0].drop[1]); + road.bankmaterial = m_state.bankmaterial; + road.friction = m_state.friction; + road.sounddistance = m_state.sounddistance; + road.quality = m_state.quality; + road.environment = m_state.environment; + road.normalize(); + Road = road; + return true; +} + +// makes paths leading through the junction, between the lanes of the roads attached to it at the moment +void junction_node::create_links() +{ + m_movements.clear(); + auto const &arms{m_state.arms}; + // lanes of the roads attached to the arms; a road is attached if one of its ends is where the arm is + struct arm_lanes + { + std::vector incoming; + std::vector outgoing; + float weight{100.f}; // how willing the drivers are to leave by the road + }; + std::vector lanes(arms.size()); + for (std::size_t arm = 0; arm < arms.size(); ++arm) + { + lanes[arm].incoming.assign(static_cast(arms[arm].incoming), nullptr); + lanes[arm].outgoing.assign(static_cast(arms[arm].outgoing), nullptr); + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + { + continue; + } + auto const &layout{road->definition()}; + for (auto const atend : {false, true}) + { + if (glm::distance(layout.axis.points[atend ? segment_data::point::end : segment_data::point::start], arms[arm].position) > junction_reach) + { + continue; + } + lanes[arm].weight = layout.weight; + // the lanes heading for this end of the road lead into the junction, the others lead out of it + for (int lane = 1; lane <= arms[arm].incoming; ++lane) + { + // the road may have other lanes at its other end; it's the ones it has here which count + auto const index{layout.lane_index((atend ? "f" : "b") + std::to_string(lane))}; + auto *track{index >= 0 ? road->lane_track(static_cast(index), atend ? 1 : 0) : nullptr}; + if (track != nullptr && glm::distance(glm::dvec3{track->CurrentSegment()->FastGetPoint_1()}, m_state.lane_point(arm, true, lane)) < junction_fit) + { + lanes[arm].incoming[lane - 1] = track; + } + } + for (int lane = 1; lane <= arms[arm].outgoing; ++lane) + { + auto const index{layout.lane_index((atend ? "b" : "f") + std::to_string(lane))}; + auto *track{index >= 0 ? road->lane_track(static_cast(index), atend ? 1 : 0) : nullptr}; + if (track != nullptr && glm::distance(glm::dvec3{track->CurrentSegment()->FastGetPoint_0()}, m_state.lane_point(arm, false, lane)) < junction_fit) + { + lanes[arm].outgoing[lane - 1] = track; + } + } + } + } + } + + enum class way + { + left, + straight, + right + }; + struct exit_data + { + std::size_t arm; + double turn; + way kind; + TTrack *track; + }; + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + auto const write_point = [&text](glm::dvec3 const &Point) { text << Point.x << ' ' << Point.y << ' ' << Point.z << ' '; }; + + for (std::size_t arm = 0; arm < arms.size(); ++arm) + { + auto const heading{-arms[arm].direction}; + // the other arms with a road to leave by, from the leftmost to the rightmost + std::vector exits; + for (std::size_t other = 0; other < arms.size(); ++other) + { + if (other == arm || std::all_of(lanes[other].outgoing.begin(), lanes[other].outgoing.end(), [](TTrack const *Track) { return Track == nullptr; })) + { + continue; + } + exits.push_back({other, turn_angle(heading, arms[other].direction), way::straight, nullptr}); + } + std::sort(exits.begin(), exits.end(), [](exit_data const &Left, exit_data const &Right) { return Left.turn > Right.turn; }); + if (exits.size() == 3) + { + exits[0].kind = way::left; + exits[2].kind = way::right; + } + else + { + auto const sideways{glm::radians(50.0)}; + for (auto &exit : exits) + { + exit.kind = (exit.turn > sideways ? way::left : exit.turn < -sideways ? way::right : way::straight); + } + if (exits.size() == 2 && exits[0].kind == exits[1].kind) + { + // two ways of the same kind are told apart by which one is more to the left + if (exits[0].kind == way::right) + { + exits[0].kind = way::straight; + } + else if (exits[0].kind == way::left) + { + exits[1].kind = way::straight; + } + else + { + exits[0].kind = way::left; + exits[1].kind = way::right; + } + } + } + auto const lanecount{static_cast(lanes[arm].incoming.size())}; + for (int lane = 1; lane <= lanecount; ++lane) + { + auto *entry{lanes[arm].incoming[lane - 1]}; + if (entry == nullptr) + { + continue; + } + // with more lanes than one the inner lane takes the left turns, the outer one the right turns, and all go straight; + // unless the scenery says which ways the lane can be left by + auto const turns{static_cast(lane - 1) < arms[arm].turns.size() ? arms[arm].turns[lane - 1] : 0}; + std::vector ways; + for (auto const &exit : exits) + { + auto const allowed{turns != 0 ? (turns & (exit.kind == way::left ? turn_left : exit.kind == way::right ? turn_right : turn_straight)) != 0 : + lanecount == 1 || exit.kind == way::straight || (exit.kind == way::left && lane == 1) || (exit.kind == way::right && lane == lanecount)}; + if (allowed) + { + ways.emplace_back(exit); + } + } + if (ways.empty()) + { + ways = exits; + } + for (auto &exit : ways) + { + // the lane of the same number on the road to leave by, or the nearest one there is + auto const &candidates{lanes[exit.arm].outgoing}; + for (int shift = 0; shift < static_cast(candidates.size()) && exit.track == nullptr; ++shift) + { + for (auto const index : {lane - 1 - shift, lane - 1 + shift}) + { + if (index >= 0 && index < static_cast(candidates.size()) && candidates[index] != nullptr) + { + exit.track = candidates[index]; + break; + } + } + } + if (exit.track == nullptr) + { + exit.track = candidates.back(); + } + } + ways.erase(std::remove_if(ways.begin(), ways.end(), [](exit_data const &Exit) { return Exit.track == nullptr; }), ways.end()); + if (ways.empty()) + { + continue; + } + if (ways.size() == 3) + { + // the legacy crossroads has the way straight ahead at its second point, the left one at the third and the right one at the fourth + std::swap(ways[0], ways[1]); + } + if (exits.size() == 1 && lane == lanecount) + { + // where the only road to leave by has more lanes than the one coming in, the road gets wider: the lanes it gains + // are entered from the outermost lane. a path has up to three ways out, so that's two lanes gained at most + auto const &candidates{lanes[exits[0].arm].outgoing}; + for (auto extra = static_cast(lanecount); extra < candidates.size() && ways.size() < 3; ++extra) + { + if (candidates[extra] != nullptr && candidates[extra] != ways.front().track) + { + auto way{ways.front()}; + way.track = candidates[extra]; + ways.emplace_back(way); + } + } + } + + // shape of the ways: each leaves the lane coming in the way that lane goes, and joins the lane going out the way that one goes + glm::dvec3 const start{entry->CurrentSegment()->FastGetPoint_1()}; + std::vector ends; + std::vector endcontrols; + double reach{0.0}; + for (auto const &exit : ways) + { + glm::dvec3 const end{exit.track->CurrentSegment()->FastGetPoint_0()}; + auto const span{glm::distance(start, end)}; + ends.emplace_back(end); + // the slopes of the lanes are carried on, so a junction on a hill doesn't make a step + endcontrols.emplace_back(-(flat(arms[exit.arm].direction) + up * lane_grade(*exit.track, false)) * (0.39 * span)); + reach += 0.39 * span / ways.size(); + } + auto const startcontrol{(flat(heading) + up * lane_grade(*entry, true)) * reach}; + gate_data gate; + gate.arm = arm; + gate.lane = lane; + gate.entry = entry; + gate.line = start; + // what a driver calls each way, once they picked it: the number of the path of the crossroads it's on, negative if it's taken against the path + int const codes2[]{1, -2}; + int const codes3[]{1, 5, -4}; + for (std::size_t idx = 0; idx < ways.size(); ++idx) + { + segment_data movement; + movement.points[segment_data::point::start] = start; + movement.points[segment_data::point::control1] = startcontrol; + movement.points[segment_data::point::control2] = endcontrols[idx]; + movement.points[segment_data::point::end] = ends[idx]; + m_movements.emplace_back(movement); + way_data route; + route.exit = ways[idx].arm; + route.kind = (ways[idx].kind == way::left ? turn_left : ways[idx].kind == way::right ? turn_right : turn_straight); + route.code = (ways.size() == 3 ? codes3[idx] : ways.size() == 2 ? codes2[idx] : 0); + route.track = ways[idx].track; + route.path = movement; + gate.ways.emplace_back(route); + } + + text.str(""); + text << (ways.size() == 1 ? "road " : "cross ") << glm::distance(start, ends[0]) << ' ' << arms[arm].width << ' ' << m_state.friction << ' ' << m_state.sounddistance << ' ' << m_state.quality << " 0 " << m_state.environment << " unvis "; + // first path: from the lane coming in to the first way out + write_point(start); + text << "0 "; + write_point(startcontrol); + write_point(endcontrols[0]); + write_point(ends[0]); + text << "0 0 "; + if (ways.size() == 2) + { + // second path of a crossroads of three roads starts where the first one does + write_point(start); + text << "0 "; + write_point(startcontrol); + write_point(endcontrols[1]); + write_point(ends[1]); + text << "0 0 "; + } + else if (ways.size() == 3) + { + // second path of a crossroads of four roads runs between its third and fourth point + write_point(ends[1]); + text << "0 "; + write_point(endcontrols[1]); + write_point(endcontrols[2]); + write_point(ends[2]); + text << "0 0 "; + } + if (m_state.velocity > 0.f) + { + text << "velocity " << m_state.velocity << ' '; + } + text << "endtrack"; + auto *link{create_track(text.str(), m_links.size())}; + + // the path is tied to the lanes by hand: the lanes going out are shared by the paths from all the other arms, + // which is more than the automatic joining of path ends can express + if (ways.size() == 1) + { + entry->ConnectNextPrev(link, 0); + link->trNext = ways[0].track; + link->iNextDirection = 0; + if (ways[0].track->trPrev == nullptr) + { + ways[0].track->trPrev = link; + ways[0].track->iPrevDirection = 1; + } + } + else if (link->SwitchExtension != nullptr) + { + auto &extension{*link->SwitchExtension}; + // the points of the crossroads are stored as: first, second, fourth, third + extension.pPrevs[0] = entry; + extension.iPrevDirection[0] = 1; + extension.pNexts[0] = ways[0].track; + extension.iNextDirection[0] = 0; + extension.pPrevs[1] = (ways.size() == 3 ? ways[2].track : ways[1].track); + extension.iPrevDirection[1] = 0; + extension.pNexts[1] = (ways.size() == 3 ? ways[1].track : nullptr); + extension.iNextDirection[1] = 0; + link->Switch(0); + entry->trNext = link; + entry->iNextDirection = 0; + // odds of each way being picked go by how willing the drivers are to take the road it leads to. + // the routes of a crossroads are numbered: left, right, straight ahead + if (ways.size() == 3) + { + link->m_routeweights = {lanes[ways[1].arm].weight, lanes[ways[2].arm].weight, lanes[ways[0].arm].weight}; + } + else + { + link->m_routeweights = {lanes[ways[0].arm].weight, lanes[ways[1].arm].weight, 0.f}; + } + // the lanes going out get to know one of the paths leading to them, for whatever goes backwards + int const ends2[]{1, 2}; + int const ends3[]{1, 3, 2}; + for (std::size_t idx = 0; idx < ways.size(); ++idx) + { + if (ways[idx].track->trPrev == nullptr) + { + ways[idx].track->trPrev = link; + ways[idx].track->iPrevDirection = (ways.size() == 3 ? ends3[idx] : ends2[idx]); + } + } + } + m_links.emplace_back(link); + gate.link = link; + m_gates.emplace_back(std::move(gate)); + } + } + create_gates(); +} + +// sets up what the traffic of each lane leading in is told, once the ways through are made +void junction_node::create_gates() +{ + if (m_state.arms.size() < 3) + { + // where a road only changes its lanes nobody gets in the way of anybody + m_gates.clear(); + return; + } + auto const point = [](segment_data const &Path, double const T) { + auto const &p0{Path.points[segment_data::point::start]}; + auto const &p3{Path.points[segment_data::point::end]}; + auto const p1{p0 + Path.points[segment_data::point::control1]}; + auto const p2{p3 + Path.points[segment_data::point::control2]}; + auto const u{1.0 - T}; + auto const result{u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3}; + return glm::dvec2{result.x, result.z}; + }; + int const samples{10}; + for (std::size_t index = 0; index < m_gates.size(); ++index) + { + auto &gate{m_gates[index]}; + // lanes the vehicles heading for the junction are on: the one leading in, and the same lane of the pieces before it + gate.approach.emplace_back(gate.entry); + gate.wishes.emplace_back(0); + auto reach{glm::distance(glm::dvec3{gate.entry->CurrentSegment()->FastGetPoint_0()}, glm::dvec3{gate.entry->CurrentSegment()->FastGetPoint_1()})}; + for (auto *lane{gate.entry}; reach < gate_reach && gate.approach.size() < 16;) + { + auto *previous{lane->trPrev}; + // lanes a road loses join the one which is left: the vehicles on the paths leading them here are coming this way too + if (auto const *owner{dynamic_cast(lane->m_road)}; owner != nullptr) + { + for (auto *link : owner->links()) + { + if (link != previous && link->trNext == lane && link->m_paths.size() == 1) + { + gate.approach.emplace_back(link); + gate.wishes.emplace_back(0); + } + } + } + auto wish{0}; + if (previous != nullptr && previous->m_paths.size() > 1 && dynamic_cast(previous->m_road) != nullptr) + { + // a path on which a road forks into more lanes: of the vehicles on it the ones which picked the way leading here count + wish = fork_code(*previous, lane); + if (wish == 0) + { + break; + } + } + else if (previous == nullptr || previous->trNext != lane || dynamic_cast(previous->m_road) == nullptr || previous->m_paths.size() != 1) + { + break; + } + gate.approach.emplace_back(previous); + gate.wishes.emplace_back(wish); + reach += glm::distance(glm::dvec3{previous->CurrentSegment()->FastGetPoint_0()}, glm::dvec3{previous->CurrentSegment()->FastGetPoint_1()}); + lane = previous; + } + // the order, found by the drivers a bit ahead of where the lane ends + if (index >= m_orders.size()) + { + m_orders.emplace_back(new road_order()); + m_orders.back()->m_name = m_name + ":gate" + std::to_string(m_orders.size()); + } + gate.order = m_orders[index]; + gate.order->m_velocity = -1.0; + gate.order->m_location = gate.line; + gate.entry->m_events2.emplace_back(gate.order->m_name, gate.order); + // ways which get in the way of each other: the ones from other roads which cross it, or lead to the same lane + for (std::size_t way = 0; way < gate.ways.size(); ++way) + { + for (std::size_t other = 0; other < m_gates.size(); ++other) + { + if (m_gates[other].arm == gate.arm) + { + continue; + } + for (std::size_t otherway = 0; otherway < m_gates[other].ways.size(); ++otherway) + { + auto const &path{gate.ways[way].path}; + auto const &otherpath{m_gates[other].ways[otherway].path}; + auto nearest{std::numeric_limits::max()}; + for (int first = 0; first <= samples; ++first) + { + for (int second = 0; second <= samples; ++second) + { + nearest = std::min(nearest, glm::distance(point(path, static_cast(first) / samples), point(otherpath, static_cast(second) / samples))); + } + } + if (nearest < way_clearance) + { + gate.ways[way].conflicts.emplace_back(other, otherway); + } + } + } + } + } + m_scan = 0.0; + m_quiet = false; +} + +// gives up the paths leading through the junction +std::vector junction_node::release_links() +{ + // the lanes leading in are let go of. a driver who has one of the orders in sight keeps it; it's not to hold anyone anymore + for (auto const &gate : m_gates) + { + if (gate.entry == nullptr || gate.order == nullptr) + { + continue; + } + auto &events{gate.entry->m_events2}; + auto const *order{static_cast(gate.order)}; + events.erase(std::remove_if(events.begin(), events.end(), [order](std::pair const &Entry) { return Entry.second == order; }), events.end()); + } + for (auto *order : m_orders) + { + order->m_velocity = -1.0; + } + m_gates.clear(); + std::vector released; + released.swap(m_links); + m_movements.clear(); + return released; +} + +// tells the vehicles heading for the junction whether they can drive on +void junction_node::update(double const Deltatime) +{ + if (m_gates.empty()) + { + return; + } + m_scan -= Deltatime; + if (m_scan > 0.0) + { + return; + } + auto const elapsed{gate_period - m_scan}; + m_scan = gate_period; + + // ways a vehicle is going to take, or is on: the one its driver picked, or any of them if the driver didn't pick yet + auto const ways_of = [](gate_data const &Gate, TDynamicObject *Vehicle) { + auto const all{(1u << Gate.ways.size()) - 1u}; + if (Gate.ways.size() < 2) + { + return all; + } + auto const code{Vehicle->RouteWish(Gate.link)}; + for (std::size_t way = 0; way < Gate.ways.size(); ++way) + { + if (Gate.ways[way].code == code) + { + return 1u << way; + } + } + return all; + }; + // who's first in line on each lane leading in, and who's on the ways through + auto traffic{false}; + for (auto &gate : m_gates) + { + TDynamicObject *first{nullptr}; + auto nearest{std::numeric_limits::max()}; + for (std::size_t idx = 0; idx < gate.approach.size(); ++idx) + { + auto *lane{gate.approach[idx]}; + auto const wish{idx < gate.wishes.size() ? gate.wishes[idx] : 0}; + for (auto *vehicle : lane->Dynamics) + { + if (vehicle == nullptr) + { + continue; + } + if (wish != 0) + { + // on a fork: the ones which picked another lane aren't coming this way + auto const picked{vehicle->RouteWish(lane)}; + if (picked != 0 && picked != wish) + { + continue; + } + } + auto const position{vehicle->GetPosition()}; + auto const distance{glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{gate.line.x, gate.line.z})}; + if (distance < nearest) + { + nearest = distance; + first = vehicle; + } + } + } + if (first != gate.first) + { + gate.first = first; + gate.committed = false; + gate.stopped = false; + gate.waited = 0.0; + gate.idle = 0.0; + } + gate.inside = 0; + for (auto *vehicle : gate.link->Dynamics) + { + if (vehicle != nullptr) + { + gate.inside |= ways_of(gate, vehicle); + } + } + if (first != nullptr) + { + gate.distance = nearest; + gate.speed = std::abs(first->MoverParameters->V); + gate.halflength = 0.5 * first->MoverParameters->Dim.L; + gate.taken = ways_of(gate, first); + gate.active = gate.distance < std::max(gate_zone, gate.speed * gate_gap) + gate.halflength; + } + else + { + gate.taken = 0; + gate.active = false; + gate.blocked = false; + } + traffic = traffic || first != nullptr || gate.inside != 0; + } + if (false == traffic) + { + if (false == m_quiet) + { + for (auto &gate : m_gates) + { + gate.order->m_velocity = -1.0; + } + m_quiet = true; + } + return; + } + m_quiet = false; + + auto const &arms{m_state.arms}; + auto const rank = [&arms](gate_data const &Gate) { return arms[Gate.arm].priority == right_of_way::priority ? 2 : arms[Gate.arm].priority == right_of_way::none ? 1 : 0; }; + // true if any of the ways marked for one lane gets in the way of any marked for another + auto const clash = [this](std::size_t const Gate, unsigned const Ways, std::size_t const Other, unsigned const Otherways) { + auto const &gate{m_gates[Gate]}; + for (std::size_t way = 0; way < gate.ways.size(); ++way) + { + if ((Ways & (1u << way)) == 0) + { + continue; + } + for (auto const &conflict : gate.ways[way].conflicts) + { + if (conflict.first == Other && (Otherways & (1u << conflict.second)) != 0) + { + return true; + } + } + } + return false; + }; + auto const turns_left = [](gate_data const &Gate) { + if (Gate.taken == 0) + { + return false; + } + for (std::size_t way = 0; way < Gate.ways.size(); ++way) + { + if ((Gate.taken & (1u << way)) != 0 && Gate.ways[way].kind != turn_left) + { + return false; + } + } + return true; + }; + // true if the first vehicle of one lane goes ahead of the first vehicle of another + auto const precedes = [&](gate_data const &Rival, gate_data const &Gate) { + if (rank(Rival) != rank(Gate)) + { + return rank(Rival) > rank(Gate); + } + // neither has the right of way over the other: the one coming from the right goes first, + // and so does the one coming from the opposite side, if its way is turned across + auto const bearing{turn_angle(-arms[Gate.arm].direction, arms[Rival.arm].direction)}; + if (bearing < -glm::radians(20.0) && bearing > -glm::radians(160.0)) + { + return true; + } + return std::abs(bearing) <= glm::radians(20.0) && turns_left(Gate) && false == turns_left(Rival); + }; + // the lanes are gone through from the ones with the right of way, and among these from the one waiting the longest + std::vector order(m_gates.size()); + for (std::size_t index = 0; index < order.size(); ++index) + { + order[index] = index; + } + std::stable_sort(order.begin(), order.end(), [&](std::size_t const Left, std::size_t const Right) { + return rank(m_gates[Left]) != rank(m_gates[Right]) ? rank(m_gates[Left]) > rank(m_gates[Right]) : m_gates[Left].waited > m_gates[Right].waited; + }); + for (auto const index : order) + { + auto &gate{m_gates[index]}; + if (gate.first == nullptr || false == gate.active) + { + // nobody to tell anything, or too far to be bothered + gate.blocked = false; + gate.committed = false; + gate.order->m_velocity = -1.0; + continue; + } + auto blocked{false}; + for (std::size_t other = 0; other < m_gates.size() && false == blocked; ++other) + { + auto const &rival{m_gates[other]}; + if (rival.arm == gate.arm) + { + continue; + } + // the ones driving through the junction, and the ones which were let in + if (rival.inside != 0 && clash(index, gate.taken, other, rival.inside)) + { + blocked = true; + } + if (rival.first == nullptr || false == rival.active || false == clash(index, gate.taken, other, rival.taken)) + { + continue; + } + if (rival.committed) + { + blocked = true; + } + // the ones to give way to. one which stands waiting for somebody else isn't waited for: + // it'll be told to wait for this one in turn, once this one is let in + else if (precedes(rival, gate) && false == (rival.blocked && rival.speed < 0.5)) + { + blocked = true; + } + } + if (arms[gate.arm].priority == right_of_way::stop && false == gate.stopped) + { + // the stop has to be made, whatever the traffic + if (gate.speed < 0.3 && gate.distance < gate.halflength + 6.0) + { + gate.stopped = true; + } + else + { + blocked = true; + } + } + if (blocked) + { + gate.committed = false; + if (gate.speed < 0.5) + { + gate.waited += elapsed; + } + } + else if (gate.committed) + { + // a vehicle which was let in and doesn't go, for whatever reason, isn't waited for forever + gate.idle = (gate.speed < 0.5 ? gate.idle + elapsed : 0.0); + if (gate.idle > gate_patience) + { + gate.committed = false; + gate.idle = 0.0; + } + } + else if (gate.distance < gate.halflength + gate_commit) + { + gate.committed = true; + gate.idle = 0.0; + } + gate.blocked = blocked; + gate.order->m_velocity = (blocked ? 0.0 : -1.0); + } +} + +// true if there's a vehicle on any path of the junction +bool junction_node::occupied() const +{ + return std::any_of(m_links.begin(), m_links.end(), [](TTrack const *Track) { return Track != nullptr && false == Track->Dynamics.empty(); }); +} + +// geometry of a junction of two roads: a stretch of road, on which the lanes of one are led to the lanes of the other +std::vector junction_node::create_transition_shapes() const +{ + std::vector shapes; + auto const &arms{m_state.arms}; + auto const &from{arms[0]}; + auto const &to{arms[1]}; + // the way from the first road to the second, which leaves and joins them the way they go + auto const p0{from.position}; + auto const p3{to.position}; + auto const span{glm::distance(p0, p3)}; + if (span < 0.1) + { + return shapes; + } + auto const p1{p0 - flat(from.direction) * (0.39 * span)}; + auto const p2{p3 - flat(to.direction) * (0.39 * span)}; + // edges of the two roadways as distances from that way, positive to the left of it. at the first road the lanes + // leading into the junction go the way the stretch does, at the second one it's the lanes leading out of it + struct edges + { + double right; + double forward; // edge of the roadway going along the way, on the side of the other one + double backward; // edge of the roadway going against it, on the side of the other one + double left; + double lanewidth; + }; + auto const width0{m_state.arm_width(0)}; + auto const width1{m_state.arm_width(1)}; + edges const start{-0.5 * width0, -0.5 * width0 + from.incoming * from.width, 0.5 * width0 - from.outgoing * from.width, 0.5 * width0, from.width}; + edges const end{-0.5 * width1, -0.5 * width1 + to.outgoing * to.width, 0.5 * width1 - to.incoming * to.width, 0.5 * width1, to.width}; + struct section + { + glm::dvec3 position; + glm::dvec3 left; + glm::dvec3 tangent; + double station; + double fraction; + edges offsets; + }; + auto const at = [&](double const T) { + section result; + auto const u{1.0 - T}; + result.position = u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + auto const direction{3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2)}; + glm::dvec2 const heading{direction.x, direction.z}; + result.tangent = (glm::length2(direction) > 1e-12 ? glm::normalize(direction) : flat(to.direction)); + result.left = flat(left_of(glm::length(heading) > 1e-9 ? glm::normalize(heading) : to.direction)); + // the edges get from where they are at one road to where they are at the other the way the edges of a road which changes its width do + auto const blend{taper_progress(T)}; + result.offsets = {glm::mix(start.right, end.right, blend), glm::mix(start.forward, end.forward, blend), glm::mix(start.backward, end.backward, blend), glm::mix(start.left, end.left, blend), + glm::mix(start.lanewidth, end.lanewidth, blend)}; + result.station = 0.0; + result.fraction = T; + return result; + }; + auto const steps{std::clamp(static_cast(span / 2.0), 8, 32)}; + std::vector
sections; + for (int idx = 0; idx <= steps; ++idx) + { + sections.emplace_back(at(static_cast(idx) / steps)); + if (idx > 0) + { + sections[idx].station = sections[idx - 1].station + glm::distance(sections[idx - 1].position, sections[idx].position); + } + } + auto const length{sections.back().station}; + + // triangles are gathered separately for each look + struct batch + { + std::string material; + lighting_data lighting; + std::vector vertices; + }; + std::vector batches; + auto const vertices = [&batches](std::string const &Material, lighting_data const &Lighting) -> std::vector & { + for (auto &entry : batches) + { + if (entry.material == Material && entry.lighting == Lighting) + { + return entry.vertices; + } + } + batches.emplace_back(); + batches.back().material = Material; + batches.back().lighting = Lighting; + return batches.back().vertices; + }; + auto const usable = [](std::string const &Material) { return false == Material.empty() && Material != "none"; }; + auto const place = [](section const &Section, section_corner const &Corner) { return Section.position + Section.left * Corner.offset + up * Corner.height; }; + // a band of triangles between two lines running along the stretch, from a section to another. + // Left, Right: give the distance from the way, the height and the texture coordinate of each line at a section + auto const strip = [&](std::vector &Output, auto const &Left, auto const &Right, std::size_t const First, std::size_t const Last) { + for (auto idx{First}; idx < Last && idx + 1 < sections.size(); ++idx) + { + auto const &here{sections[idx]}; + auto const &there{sections[idx + 1]}; + section_corner const left0{Left(here)}; + section_corner const right0{Right(here)}; + section_corner const left1{Left(there)}; + section_corner const right1{Right(there)}; + auto const leftstart{place(here, left0)}; + auto const rightstart{place(here, right0)}; + auto const leftend{place(there, left1)}; + auto const rightend{place(there, right1)}; + auto const facing{glm::cross(here.tangent, leftstart - rightstart)}; + glm::vec3 const normal{glm::length2(facing) < 1e-12 ? up : glm::normalize(facing)}; + auto const texturestart{static_cast(here.station / m_state.texturelength)}; + auto const textureend{static_cast(there.station / m_state.texturelength)}; + Output.push_back({leftstart, normal, {left0.texture, texturestart}}); + Output.push_back({rightstart, normal, {right0.texture, texturestart}}); + Output.push_back({leftend, normal, {left1.texture, textureend}}); + Output.push_back({rightstart, normal, {right0.texture, texturestart}}); + Output.push_back({rightend, normal, {right1.texture, textureend}}); + Output.push_back({leftend, normal, {left1.texture, textureend}}); + } + }; + auto const whole{sections.size() - 1}; + // the two directions are apart at either road + auto const apart{from.median > 0.f || to.median > 0.f}; + auto const forth{from.incoming > 0 || to.outgoing > 0}; + auto const back{from.outgoing > 0 || to.incoming > 0}; + double const height{m_state.kerbheight}; + + // surface. the image is centered on the way, like it is on a road + if (m_state.surface != "none") + { + auto const tile{static_cast(texture_ratio(m_state.surface)) * m_state.texturelength}; + auto const along = [tile](double edges::*Edge) { + return [tile, Edge](section const &Section) { return section_corner{Section.offsets.*Edge, 0.0, static_cast(0.5 + Section.offsets.*Edge / tile)}; }; + }; + auto &surface{vertices(m_state.surface, lighting_data{})}; + if (apart && m_state.median == road_node::median_type::gap) + { + // a roadway for each direction, with the ground showing between them + strip(surface, along(&edges::forward), along(&edges::right), 0, whole); + strip(surface, along(&edges::left), along(&edges::backward), 0, whole); + } + else + { + strip(surface, along(&edges::left), along(&edges::right), 0, whole); + } + } + // an island where the two directions part + if (apart && m_state.median == road_node::median_type::island && (usable(m_state.medianmaterial) || (m_state.kerbs && usable(m_state.kerbmaterial)))) + { + auto const &material{usable(m_state.medianmaterial) ? m_state.medianmaterial : m_state.kerbmaterial}; + auto const tile{static_cast(texture_ratio(material)) * m_state.texturelength}; + auto const widest{std::max(from.median, to.median)}; + auto &island{vertices(material, lighting_data{})}; + auto const corner = [](double edges::*Edge, double const Height, float const Texture) { + return [Edge, Height, Texture](section const &Section) { return section_corner{Section.offsets.*Edge, Height, Texture}; }; + }; + auto const top{static_cast(height / tile)}; + auto const across{static_cast((height + widest) / tile)}; + strip(island, corner(&edges::backward, 0.0, 0.f), corner(&edges::backward, height, top), 0, whole); + strip(island, corner(&edges::backward, height, top), corner(&edges::forward, height, across), 0, whole); + strip(island, corner(&edges::forward, height, across), corner(&edges::forward, 0.0, static_cast((2.0 * height + widest) / tile)), 0, whole); + } + // what lines the edges: the same the corners of a junction of more roads get. the right edge is the corner which starts + // at the first road, the left one is the corner which starts at the second road, so it's gone along backwards + for (int side = 0; side < 2; ++side) + { + auto const sign{side == 0 ? -1.0 : 1.0}; + for (auto const &lining : edge_linings(m_state, arms[side].bank)) + { + auto &output{vertices(lining.material, lighting_data{})}; + for (std::size_t band = 0; band + 1 < lining.profile.size(); ++band) + { + auto const corner = [side, sign](edge_point const &Point) { + return [sign, &Point, side](section const &Section) { + auto const fraction{side == 0 ? Section.fraction : 1.0 - Section.fraction}; + auto const edge{side == 0 ? Section.offsets.right : Section.offsets.left}; + return section_corner{edge + sign * (Point.offset + Point.grow * fraction), Point.height + Point.sink * fraction, Point.texture}; + }; + }; + auto const inner{corner(lining.profile[band])}; + auto const outer{corner(lining.profile[band + 1])}; + if (side == 0) + { + strip(output, inner, outer, 0, whole); + } + else + { + strip(output, outer, inner, 0, whole); + } + } + } + } + + // markings + if (m_state.markings != road_node::marking_colour::none) + { + lighting_data paint; + paint.diffuse = (m_state.markings == road_node::marking_colour::white ? glm::vec4{0.92f, 0.92f, 0.92f, 1.f} : glm::vec4{0.95f, 0.5f, 0.08f, 1.f}); + paint.ambient = paint.diffuse; + auto &lines{vertices("colored", paint)}; + // a line running at some distance from one of the edges + auto const line = [&](double edges::*Edge, double const Shift, double const Lanes, std::size_t const First, std::size_t const Last) { + auto const side = [Edge, Shift, Lanes](double const Half, float const Texture) { + return [Edge, Shift, Lanes, Half, Texture](section const &Section) { + return section_corner{Section.offsets.*Edge + Shift + Lanes * Section.offsets.lanewidth + Half, line_lift, Texture}; + }; + }; + strip(lines, side(0.5 * line_width, 0.f), side(-0.5 * line_width, 1.f), First, Last); + }; + auto const inset{edge_inset + 0.5 * line_width}; + line(&edges::right, inset, 0.0, 0, whole); + line(&edges::left, -inset, 0.0, 0, whole); + if (forth && back) + { + if (apart) + { + // each of the two roadways gets its edge line + line(&edges::forward, -inset, 0.0, 0, whole); + line(&edges::backward, inset, 0.0, 0, whole); + } + else + { + // traffic going opposite ways is kept apart with a double line + auto const pair{0.5 * (line_width + line_spacing)}; + line(&edges::forward, pair, 0.0, 0, whole); + line(&edges::forward, -pair, 0.0, 0, whole); + } + } + // lines between the lanes of each direction, dashed. the lanes are counted from the middle of the road, so the ones + // both roads have run all the way; a line of a lane only one of them has starts where the roadway got wide enough for the lane + for (int direction = 0; direction < 2; ++direction) + { + auto const lanes{direction == 0 ? std::max(from.incoming, to.outgoing) : std::max(from.outgoing, to.incoming)}; + for (int lane = 1; lane < lanes; ++lane) + { + for (std::size_t idx = 0; idx < whole; ++idx) + { + if (std::fmod(sections[idx].station, dash_period) >= dash_period / 3.0) + { + continue; + } + auto const room = [&](section const &Section) { + auto const &offsets{Section.offsets}; + return (direction == 0 ? offsets.forward - offsets.right : offsets.left - offsets.backward) >= (lane + 0.5) * offsets.lanewidth; + }; + if (room(sections[idx]) && room(sections[idx + 1])) + { + line(direction == 0 ? &edges::forward : &edges::backward, 0.0, direction == 0 ? -lane : lane, idx, idx + 1); + } + } + } + } + if (apart && m_state.median == road_node::median_type::painted && length > 0.0) + { + // what's between the roadways is closed to the traffic: slanted stripes are led from one edge line to the other + double const period{3.0}; + double const stripe{0.5}; + glm::vec3 const upwards{0.f, 1.f, 0.f}; + // point of the edge of one of the roadways, a bit into what's between them + auto const edge = [&](double const Station, bool const Left) { + auto const where{at(std::clamp(Station / length, 0.0, 1.0))}; + auto const gap{where.offsets.backward - where.offsets.forward}; + auto const margin{std::min(2.0 * inset, 0.5 * gap)}; + return where.position + where.left * (Left ? where.offsets.backward - margin : where.offsets.forward + margin) + up * line_lift; + }; + auto const count{std::max(1, static_cast(std::lround(length / period)))}; + auto const spacing{length / count}; + for (int index = 0; index < count; ++index) + { + auto const centre{(index + 0.5) * spacing}; + auto const middle{at(std::clamp(centre / length, 0.0, 1.0))}; + auto const gap{middle.offsets.backward - middle.offsets.forward}; + if (gap < 1.0) + { + continue; + } + auto const slant{std::clamp(gap, 0.0, std::max(0.0, spacing - stripe - 0.25))}; + auto const station{centre - 0.5 * (slant + stripe)}; + auto const leftstart{edge(station, true)}; + auto const leftend{edge(station + stripe, true)}; + auto const rightstart{edge(station + slant, false)}; + auto const rightend{edge(station + slant + stripe, false)}; + lines.push_back({leftstart, upwards, {0.f, 0.f}}); + lines.push_back({rightstart, upwards, {1.f, 0.f}}); + lines.push_back({leftend, upwards, {0.f, 1.f}}); + lines.push_back({rightstart, upwards, {1.f, 0.f}}); + lines.push_back({rightend, upwards, {1.f, 1.f}}); + lines.push_back({leftend, upwards, {0.f, 1.f}}); + } + } + } + + for (auto &entry : batches) + { + if (entry.vertices.empty()) + { + continue; + } + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material(entry.material), std::move(entry.vertices), glm::dvec3{0.0}); + shape.lighting(entry.lighting); + shapes.emplace_back(std::move(shape)); + } + return shapes; +} + +// generates geometry of the surface +std::vector junction_node::create_shapes() const +{ + if (m_state.arms.size() == 2) + { + // where a road changes its lanes it's drawn as a stretch of that road + return create_transition_shapes(); + } + std::vector shapes; + std::vector> corners; + auto const points{m_state.outline(&corners)}; + if (points.size() < 3) + { + return shapes; + } + auto const ¢re{m_state.centre}; + if (m_state.surface != "none") + { + // a fan of triangles spanned from the centre, with the image laid out flat over the ground + auto const tile{static_cast(texture_ratio(m_state.surface)) * m_state.texturelength}; + auto const vertex = [&](glm::dvec3 const &Point, glm::vec3 const &Normal) { + return world_vertex{Point, Normal, {static_cast(0.5 + (Point.x - centre.x) / tile), static_cast(0.5 + (Point.z - centre.z) / m_state.texturelength)}}; + }; + std::vector vertices; + for (std::size_t idx = 0; idx < points.size(); ++idx) + { + auto const ¤t{points[idx]}; + auto const &next{points[(idx + 1) % points.size()]}; + auto normal{glm::cross(current - centre, next - centre)}; + if (glm::length2(normal) < 1e-12) + { + continue; + } + glm::vec3 const facing{glm::normalize(normal)}; + vertices.emplace_back(vertex(centre, facing)); + vertices.emplace_back(vertex(current, facing)); + vertices.emplace_back(vertex(next, facing)); + } + if (false == vertices.empty()) + { + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material(m_state.surface), std::move(vertices), glm::dvec3{0.0}); + shapes.emplace_back(std::move(shape)); + } + } + // the corners are lined the way the edges of a road are, so the kerbs, the shoulders, the sidewalks and the banks of the roads carry on around the junction + { + auto const order{m_state.arm_order()}; + // triangles are gathered for each material, in the order the materials come up + std::vector>> lined; + auto const batch = [&lined](std::string const &Material) -> std::vector & { + for (auto &entry : lined) + { + if (entry.first == Material) + { + return entry.second; + } + } + lined.emplace_back(Material, std::vector{}); + return lined.back().second; + }; + for (std::size_t idx = 0; idx < corners.size() && idx < order.size(); ++idx) + { + auto const &corner{corners[idx]}; + if (corner.size() < 2) + { + continue; + } + // the way out of the surface at each point of the corner. at its ends it's the way across the roads there, + // so what lines the corner meets what the roads have along their edges + std::vector outwards(corner.size()); + outwards.front() = flat(left_of(m_state.arms[order[idx]].direction)); + outwards.back() = -flat(left_of(m_state.arms[order[(idx + 1) % order.size()]].direction)); + for (std::size_t point = 1; point + 1 < corner.size(); ++point) + { + // going around the junction the surface is on the left, and the way out of it on the right + glm::dvec2 const along{corner[point + 1].x - corner[point - 1].x, corner[point + 1].z - corner[point - 1].z}; + outwards[point] = (glm::length(along) > 1e-9 ? -flat(left_of(glm::normalize(along))) : outwards[point - 1]); + } + double total{0.0}; + for (std::size_t point = 0; point + 1 < corner.size(); ++point) + { + total += glm::distance(corner[point], corner[point + 1]); + } + // the bank of the corner is the one set for the road the corner starts at + for (auto const &lining : edge_linings(m_state, m_state.arms[order[idx]].bank)) + { + auto &vertices{batch(lining.material)}; + auto const &profile{lining.profile}; + double station{0.0}; + for (std::size_t point = 0; point + 1 < corner.size(); ++point) + { + auto const length{glm::distance(corner[point], corner[point + 1])}; + auto const tangent{corner[point + 1] - corner[point]}; + auto const texturestart{static_cast(station / m_state.texturelength)}; + auto const textureend{static_cast((station + length) / m_state.texturelength)}; + auto const fractionstart{total > 0.0 ? station / total : 0.0}; + auto const fractionend{total > 0.0 ? (station + length) / total : 0.0}; + for (std::size_t band = 0; band + 1 < profile.size(); ++band) + { + auto const &inner{profile[band]}; + auto const &outer{profile[band + 1]}; + auto const leftstart{corner[point] + outwards[point] * (inner.offset + inner.grow * fractionstart) + up * (inner.height + inner.sink * fractionstart)}; + auto const rightstart{corner[point] + outwards[point] * (outer.offset + outer.grow * fractionstart) + up * (outer.height + outer.sink * fractionstart)}; + auto const leftend{corner[point + 1] + outwards[point + 1] * (inner.offset + inner.grow * fractionend) + up * (inner.height + inner.sink * fractionend)}; + auto const rightend{corner[point + 1] + outwards[point + 1] * (outer.offset + outer.grow * fractionend) + up * (outer.height + outer.sink * fractionend)}; + auto facing{glm::cross(tangent, leftstart - rightstart)}; + glm::vec3 const normal{glm::length2(facing) < 1e-12 ? up : glm::normalize(facing)}; + vertices.push_back({leftstart, normal, {inner.texture, texturestart}}); + vertices.push_back({rightstart, normal, {outer.texture, texturestart}}); + vertices.push_back({leftend, normal, {inner.texture, textureend}}); + vertices.push_back({rightstart, normal, {outer.texture, texturestart}}); + vertices.push_back({rightend, normal, {outer.texture, textureend}}); + vertices.push_back({leftend, normal, {inner.texture, textureend}}); + } + station += length; + } + } + } + for (auto &entry : lined) + { + if (entry.second.empty()) + { + continue; + } + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material(entry.first), std::move(entry.second), glm::dvec3{0.0}); + shapes.emplace_back(std::move(shape)); + } + } + if (m_state.markings != road_node::marking_colour::none) + { + // edge lines carried around the corners + std::vector vertices; + glm::dvec3 const lift{0.0, line_lift, 0.0}; + glm::vec3 const upwards{0.f, 1.f, 0.f}; + auto const &arms{m_state.arms}; + for (std::size_t arm = 0; arm < arms.size(); ++arm) + { + if (false == arms[arm].stopline || arms[arm].incoming < 1) + { + continue; + } + // a stop line: across the lanes leading into the junction, right where the road ends + auto const across{flat(left_of(arms[arm].direction))}; + auto const inwards{-flat(arms[arm].direction)}; + auto const halfwidth{0.5 * m_state.arm_width(arm)}; + auto const from{arms[arm].position + across * (halfwidth - static_cast(arms[arm].incoming) * arms[arm].width + 0.5 * line_width) + lift}; + auto const to{arms[arm].position + across * (halfwidth - edge_inset - line_width) + lift}; + auto const leftstart{from + inwards * (stopline_inset + stopline_width)}; + auto const rightstart{from + inwards * stopline_inset}; + auto const leftend{to + inwards * (stopline_inset + stopline_width)}; + auto const rightend{to + inwards * stopline_inset}; + vertices.push_back({leftstart, upwards, {0.f, 0.f}}); + vertices.push_back({rightstart, upwards, {1.f, 0.f}}); + vertices.push_back({leftend, upwards, {0.f, 1.f}}); + vertices.push_back({rightstart, upwards, {1.f, 0.f}}); + vertices.push_back({rightend, upwards, {1.f, 1.f}}); + vertices.push_back({leftend, upwards, {0.f, 1.f}}); + } + for (std::size_t arm = 0; arm < arms.size(); ++arm) + { + if (arms[arm].crosswalk <= 0.f) + { + continue; + } + // a pedestrian crossing: stripes laid the way the road goes, side by side across it, past the place for a stop line + auto const across{flat(left_of(arms[arm].direction))}; + auto const inwards{-flat(arms[arm].direction)}; + auto const reach{0.5 * m_state.arm_width(arm) - edge_inset}; + auto const stripes{std::max(1, static_cast(std::floor((2.0 * reach + stopline_width) / (2.0 * stopline_width))))}; + auto const first{-0.5 * (stripes * 2 - 1) * stopline_width}; + for (int stripe = 0; stripe < stripes; ++stripe) + { + auto const offset{first + stripe * 2.0 * stopline_width}; + auto const from{arms[arm].position + across * offset + lift}; + auto const to{arms[arm].position + across * (offset + stopline_width) + lift}; + auto const leftstart{from + inwards * (crosswalk_inset + arms[arm].crosswalk)}; + auto const rightstart{from + inwards * crosswalk_inset}; + auto const leftend{to + inwards * (crosswalk_inset + arms[arm].crosswalk)}; + auto const rightend{to + inwards * crosswalk_inset}; + vertices.push_back({leftstart, upwards, {0.f, 0.f}}); + vertices.push_back({rightstart, upwards, {1.f, 0.f}}); + vertices.push_back({leftend, upwards, {0.f, 1.f}}); + vertices.push_back({rightstart, upwards, {1.f, 0.f}}); + vertices.push_back({rightend, upwards, {1.f, 1.f}}); + vertices.push_back({leftend, upwards, {0.f, 1.f}}); + } + } + if (arms.size() == 2 && arms[0].incoming > 0 && arms[0].outgoing > 0 && arms[1].incoming > 0 && arms[1].outgoing > 0) + { + // a junction of two roads is where a road changes its lanes. the double line keeping the directions apart is led through it + auto const divide = [&](std::size_t const Arm) { return arms[Arm].position + flat(left_of(arms[Arm].direction)) * (0.5 * m_state.arm_width(Arm) - static_cast(arms[Arm].incoming) * arms[Arm].width); }; + auto const p0{divide(0)}; + auto const p3{divide(1)}; + auto const reach{0.39 * glm::distance(p0, p3)}; + auto const p1{p0 - flat(arms[0].direction) * reach}; + auto const p2{p3 - flat(arms[1].direction) * reach}; + auto const pair{0.5 * (line_width + line_spacing)}; + int const steps{12}; + glm::dvec3 previous{p0}; + glm::dvec3 previousleft{flat(left_of(-arms[0].direction))}; + for (int step = 1; step <= steps; ++step) + { + auto const t{static_cast(step) / steps}; + auto const u{1.0 - t}; + auto const current{u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3}; + auto const direction{3.0 * u * u * (p1 - p0) + 6.0 * u * t * (p2 - p1) + 3.0 * t * t * (p3 - p2)}; + glm::dvec2 const heading{direction.x, direction.z}; + auto const left{glm::length(heading) > 1e-9 ? flat(left_of(glm::normalize(heading))) : previousleft}; + for (auto const side : {-1.0, 1.0}) + { + auto const leftstart{previous + previousleft * (side * pair + 0.5 * line_width) + lift}; + auto const rightstart{previous + previousleft * (side * pair - 0.5 * line_width) + lift}; + auto const leftend{current + left * (side * pair + 0.5 * line_width) + lift}; + auto const rightend{current + left * (side * pair - 0.5 * line_width) + lift}; + vertices.push_back({leftstart, upwards, {0.f, 0.f}}); + vertices.push_back({rightstart, upwards, {1.f, 0.f}}); + vertices.push_back({leftend, upwards, {0.f, 1.f}}); + vertices.push_back({rightstart, upwards, {1.f, 0.f}}); + vertices.push_back({rightend, upwards, {1.f, 1.f}}); + vertices.push_back({leftend, upwards, {0.f, 1.f}}); + } + previous = current; + previousleft = left; + } + } + for (auto const &corner : corners) + { + for (std::size_t idx = 0; idx + 1 < corner.size(); ++idx) + { + // the line keeps its distance from the edge, on the side of the centre + auto const inwards = [&](glm::dvec3 const &Point) { + auto direction{centre - Point}; + direction.y = 0.0; + return glm::length2(direction) > 1e-12 ? glm::normalize(direction) : glm::dvec3{0.0}; + }; + auto const outer0{corner[idx] + inwards(corner[idx]) * edge_inset + lift}; + auto const inner0{corner[idx] + inwards(corner[idx]) * (edge_inset + line_width) + lift}; + auto const outer1{corner[idx + 1] + inwards(corner[idx + 1]) * edge_inset + lift}; + auto const inner1{corner[idx + 1] + inwards(corner[idx + 1]) * (edge_inset + line_width) + lift}; + // the corners are listed going around the junction the way which makes these face up + vertices.push_back({inner0, upwards, {0.f, 0.f}}); + vertices.push_back({outer0, upwards, {1.f, 0.f}}); + vertices.push_back({outer1, upwards, {1.f, 1.f}}); + vertices.push_back({inner0, upwards, {0.f, 0.f}}); + vertices.push_back({outer1, upwards, {1.f, 1.f}}); + vertices.push_back({inner1, upwards, {0.f, 1.f}}); + } + } + if (false == vertices.empty()) + { + lighting_data paint; + paint.diffuse = (m_state.markings == road_node::marking_colour::white ? glm::vec4{0.92f, 0.92f, 0.92f, 1.f} : glm::vec4{0.95f, 0.5f, 0.08f, 1.f}); + paint.ambient = paint.diffuse; + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material("colored"), std::move(vertices), glm::dvec3{0.0}); + shape.lighting(paint); + shapes.emplace_back(std::move(shape)); + } + } + return shapes; +} + +void junction_node::show() +{ + if (m_merged || m_shapes.shown()) + { + return; + } + m_shapes.show(create_shapes(), location()); +} + +void junction_node::hide() +{ + m_shapes.hide(); +} + +// creates a path through the junction from provided definition and registers it with the simulation +TTrack *junction_node::create_track(std::string const &Definition, std::size_t const Index) +{ + cParser parser(Definition, cParser::buffer_TEXT); + scene::node_data nodedata; + nodedata.range_max = -1.0; + nodedata.name = (m_name.empty() ? std::string{} : m_name + ":way" + std::to_string(Index + 1)); + nodedata.type = "track"; + auto *track{new TTrack(nodedata)}; + track->Load(&parser, glm::dvec3{0.0}); + track->m_road = this; + // NOTE: the table points the name at the newest path, which is what a junction made anew in the editor needs + simulation::Paths.insert(track); + simulation::Region->insert_and_register(track); + return track; +} + +float junction_node::radius_() +{ + float radius{0.f}; + for (std::size_t arm = 0; arm < m_state.arms.size(); ++arm) + { + radius = std::max(radius, static_cast(glm::distance(m_state.arms[arm].position, m_state.centre) + 0.5 * m_state.arm_width(arm))); + } + return radius; +} + +void junction_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +void junction_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +// export() subclass details, sends basic content of the class in legacy (text) format to provided stream +void junction_node::export_as_text_(std::ostream &Output) const +{ + Output << "junction "; + auto const precision{Output.precision(std::numeric_limits::digits10)}; + Output << m_state.centre.x << ' ' << m_state.centre.y << ' ' << m_state.centre.z << ' '; + for (auto const &arm : m_state.arms) + { + Output << "arm " << arm.position.x << ' ' << arm.position.y << ' ' << arm.position.z << ' ' << arm.direction.x << ' ' << arm.direction.y << ' ' << arm.incoming << ' ' << arm.outgoing << ' ' << arm.width << ' '; + } + Output.precision(precision); + Output << "surface " << m_state.surface << ' ' << "texlength " << m_state.texturelength << ' ' << "markings " + << (m_state.markings == road_node::marking_colour::white ? "white" : m_state.markings == road_node::marking_colour::orange ? "orange" : "none") << ' '; + if (m_state.side.type != road_node::side_type::none) + { + Output << "side " << (m_state.side.type == road_node::side_type::shoulder ? "shoulder" : "sidewalk") << ' ' << m_state.side.width << ' ' << (m_state.side.material.empty() ? "none" : m_state.side.material) << ' ' << "kerb " << m_state.kerbheight << ' ' + << "slope " << m_state.slope.x << ' ' << m_state.slope.y << ' '; + } + if (m_state.kerbs) + { + Output << "kerbs " << m_state.kerbwidth << ' ' << m_state.kerbmaterial << ' '; + if (m_state.side.type == road_node::side_type::none) + { + Output << "kerb " << m_state.kerbheight << ' '; + } + } + for (std::size_t arm = 0; arm < m_state.arms.size(); ++arm) + { + if (m_state.arms[arm].stopline) + { + Output << "stopline " << arm + 1 << ' '; + } + if (m_state.arms[arm].median > 0.f) + { + Output << "armmedian " << arm + 1 << ' ' << m_state.arms[arm].median << ' '; + } + if (m_state.arms[arm].crosswalk > 0.f) + { + Output << "crosswalk " << arm + 1 << ' ' << m_state.arms[arm].crosswalk << ' '; + } + auto const &bank{m_state.arms[arm].bank}; + if (bank.set) + { + Output << "cornerbank " << arm + 1 << ' ' << bank.width[0] << ' ' << bank.drop[0] << ' ' << bank.width[1] << ' ' << bank.drop[1] << ' '; + } + auto const priority{m_state.arms[arm].priority}; + if (priority != right_of_way::none) + { + Output << "priority " << arm + 1 << ' ' << (priority == right_of_way::priority ? "main" : priority == right_of_way::yield ? "yield" : "stop") << ' '; + } + for (std::size_t lane = 0; lane < m_state.arms[arm].turns.size(); ++lane) + { + auto const turns{m_state.arms[arm].turns[lane]}; + if (turns != 0) + { + Output << "turns " << arm + 1 << ' ' << lane + 1 << ' '; + Output << ((turns & turn_left) != 0 ? "l" : "") << ((turns & turn_straight) != 0 ? "s" : "") << ((turns & turn_right) != 0 ? "r" : "") << ' '; + } + } + } + if (m_state.bankmaterial != "none") + { + Output << "bankmaterial " << m_state.bankmaterial << ' '; + } + if (m_state.arms.size() == 2 && (m_state.arms[0].median > 0.f || m_state.arms[1].median > 0.f)) + { + Output << "median " << (m_state.median == road_node::median_type::gap ? "gap" : m_state.median == road_node::median_type::island ? "island" : "painted") << ' '; + Output << m_state.medianmaterial << ' '; + } + Output << "velocity " << m_state.velocity << ' ' << "friction " << m_state.friction << ' ' << "environment " << m_state.environment << ' '; + Output << "endjunction" + << "\n"; +} + +// creates a path for the vehicles and registers it with the simulation +TTrack *road_node::create_track(std::string const &Name, segment_data const &Path, float const Width, float const Velocity) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + auto const &end{Path.points[segment_data::point::end]}; + // the path is put together the way the scenery defines an invisible road, to have it set up by the code which loads these + std::ostringstream text; + text.precision(std::numeric_limits::digits10); + text << "road " << glm::distance(start, end) << ' ' << Width << ' ' << m_state.friction << ' ' << m_state.sounddistance << ' ' << m_state.quality << " 0 " << m_state.environment << " unvis " << start.x << ' ' << start.y << ' ' << start.z << ' ' << Path.rolls[0] << ' ' + << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' << end.x << ' ' << end.y << ' ' << end.z << ' ' << Path.rolls[1] << ' ' << Path.radius << ' '; + if (Velocity > 0.f) + { + text << "velocity " << Velocity << ' '; + } + text << "endtrack"; + + return create_track(Name, text.str()); +} + +// creates a path for the vehicles from its definition and registers it with the simulation +TTrack *road_node::create_track(std::string const &Name, std::string const &Definition) +{ + cParser parser(Definition, cParser::buffer_TEXT); + scene::node_data nodedata; + nodedata.range_max = -1.0; + nodedata.name = Name; + nodedata.type = "track"; + auto *track{new TTrack(nodedata)}; + track->Load(&parser, glm::dvec3{0.0}); + track->m_road = this; + if (false == simulation::Paths.insert(track)) + { + // NOTE: the table points the name at the newest path, which is what a road made anew in the editor needs + if (false == m_editorremoved && false == dirty()) + { + ErrorLog("Bad scenario: duplicate track name \"" + track->name() + "\" generated for a road"); + } + } + simulation::Region->insert_and_register(track); + return track; +} + +// radius() subclass details, calculates node's bounding radius +float road_node::radius_() +{ + auto reach{static_cast(m_state.slope.x)}; + for (auto const &bank : m_state.banks) + { + if (bank.set) + { + reach = std::max(reach, static_cast(std::max(bank.width[0], bank.width[1]))); + } + } + return static_cast(0.5 * m_state.length() + 0.5 * std::max(m_state.span(0.0), m_state.span(1.0)) + m_state.kerbwidth + std::max(m_state.sides[0].width, m_state.sides[1].width) + reach); +} + +// serialize() subclass details, sends content of the subclass to provided stream +void road_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +// deserialize() subclass details, restores content of the subclass from provided stream +void road_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +// export() subclass details, sends basic content of the class in legacy (text) format to provided stream +void road_node::export_as_text_(std::ostream &Output) const +{ + auto const &road{m_state}; + Output << "road "; + // axis + auto const precision{Output.precision(std::numeric_limits::digits10)}; + Output << road.axis.points[segment_data::point::start].x << ' ' << road.axis.points[segment_data::point::start].y << ' ' << road.axis.points[segment_data::point::start].z << ' ' << road.axis.rolls[0] << ' ' + << road.axis.points[segment_data::point::control1].x << ' ' << road.axis.points[segment_data::point::control1].y << ' ' << road.axis.points[segment_data::point::control1].z << ' ' + << road.axis.points[segment_data::point::control2].x << ' ' << road.axis.points[segment_data::point::control2].y << ' ' << road.axis.points[segment_data::point::control2].z << ' ' + << road.axis.points[segment_data::point::end].x << ' ' << road.axis.points[segment_data::point::end].y << ' ' << road.axis.points[segment_data::point::end].z << ' ' << road.axis.rolls[1] << ' ' + << road.axis.radius << ' '; + Output.precision(precision); + // lanes + Output << "lanes " << road.lanes_at(0, true) << ' ' << road.lanes_at(0, false) << ' '; + if (road.changing()) + { + Output << "lanesto " << road.lanes_at(1, true) << ' ' << road.lanes_at(1, false) << ' '; + } + Output << "width " << road.lanewidth << ' '; + if (road.taper[0] != 1.f || road.taper[1] != 1.f) + { + Output << "taper " << road.taper[0] << ' ' << road.taper[1] << ' '; + } + if (road.velocity > 0.f) + { + Output << "velocity " << road.velocity << ' '; + } + for (std::size_t lane = 0; lane < road.lanes.size(); ++lane) + { + if (road.lanes[lane].width != road.lanewidth) + { + Output << "lane " << road.lane_id(lane) << " width " << road.lanes[lane].width << ' '; + } + if (road.lanes[lane].velocity != road.velocity) + { + Output << "lane " << road.lane_id(lane) << " velocity " << road.lanes[lane].velocity << ' '; + } + } + for (std::size_t boundary = 0; boundary < road.changes.size(); ++boundary) + { + if (road.changes[boundary] == road.default_change(boundary)) + { + continue; + } + Output << "change " << road.lane_id(boundary) << ' ' << road.lane_id(boundary + 1) << ' ' << (road.changes[boundary] == change_both ? "both" : road.changes[boundary] == change_toright ? "ab" : road.changes[boundary] == change_toleft ? "ba" : "none") << ' '; + } + // appearance + Output << "surface " << road.surface << ' ' << "texlength " << road.texturelength << ' '; + char const *sidenames[]{"left", "right"}; + for (int side = 0; side < 2; ++side) + { + auto const &data{road.sides[side]}; + if (data.type == side_type::none) + { + continue; + } + Output << "side " << sidenames[side] << ' ' << (data.type == side_type::shoulder ? "shoulder" : "sidewalk") << ' ' << data.width << ' ' << (data.material.empty() ? "none" : data.material) << ' '; + } + Output << "kerb " << road.kerbheight << ' ' << "slope " << road.slope.x << ' ' << road.slope.y << ' ' << "markings " << (road.markings == marking_colour::white ? "white" : road.markings == marking_colour::orange ? "orange" : "none") << ' '; + // properties passed on to the lanes + Output << "friction " << road.friction << ' ' << "environment " << road.environment << ' '; + if (road.weight != 100.f) + { + Output << "weight " << road.weight << ' '; + } + if (road.median.width[0] > 0.f || road.median.width[1] > 0.f) + { + Output << "median " << (road.median.type == median_type::gap ? "gap" : road.median.type == median_type::island ? "island" : "painted") << ' '; + Output << road.median.width[0] << ' ' << road.median.width[1] << ' ' << road.median.material << ' '; + if (road.median.round[0] > 0.f || road.median.round[1] > 0.f) + { + Output << "medianround " << road.median.round[0] << ' ' << road.median.round[1] << ' '; + } + } + if (road.kerbs[0] || road.kerbs[1]) + { + Output << "kerbs " << (road.kerbs[0] && road.kerbs[1] ? "both" : road.kerbs[0] ? "left" : "right") << ' ' << road.kerbwidth << ' ' << road.kerbmaterial << ' '; + } + for (int side = 0; side < 2; ++side) + { + auto const &bank{road.banks[side]}; + if (bank.set) + { + Output << "bank " << sidenames[side] << ' ' << bank.width[0] << ' ' << bank.drop[0] << ' ' << bank.width[1] << ' ' << bank.drop[1] << ' '; + } + } + if (road.bankmaterial != "none") + { + Output << "bankmaterial " << road.bankmaterial << ' '; + } + if (road.roundabout) + { + Output << "roundabout "; + } + // footer + Output << "endroad" + << "\n"; +} + +// legacy style initialization, to be performed when the tracks are already joined +void road_table::InitLanes() +{ + // the pieces which gain or lose lanes lead the ones they have at one end to the ones they have at the other + for (auto *road : m_items) + { + if (road != nullptr) + { + road->create_links(); + } + } +} + +// legacy style initialization, to be performed when the junctions are done +void road_table::InitRoads() +{ + for (auto *road : m_items) + { + if (road != nullptr) + { + road->close_ends(); + } + } +} + +// generates geometry of the roads and puts it in the scene +void road_table::create_geometry(scene::scratch_data &Scratchpad) +{ + if (simulation::Region == nullptr) + { + return; + } + for (auto *road : m_items) + { + if (road == nullptr) + { + continue; + } + if (Global.editor_session && false == Global.NvRenderer) + { + // scenery opened for editing: the road keeps its geometry apart, so it can be changed later. + // NOTE: the experimental renderer draws only what the scenery had when it was loaded + road->show(); + continue; + } + // otherwise the geometry joins the rest of the scenery, which lets it be drawn along with it at no extra cost + for (auto &shape : road->create_shapes()) + { + // it's already where it belongs, so the placement set by the scenery isn't to be applied to it + simulation::Region->insert(shape, Scratchpad, false); + } + road->merged(true); + } +} + +// legacy style initialization, to be performed when the tracks are joined, ahead of the roads +void junction_table::InitJunctions() +{ + for (auto *junction : m_items) + { + if (junction != nullptr) + { + junction->create_links(); + } + } +} + +// to be called with each step of the simulation +void junction_table::update(double const Deltatime) +{ + for (auto *junction : m_items) + { + if (junction != nullptr && false == junction->m_editorremoved) + { + junction->update(Deltatime); + } + } +} + +// generates geometry of the junctions and puts it in the scene +void junction_table::create_geometry(scene::scratch_data &Scratchpad) +{ + if (simulation::Region == nullptr) + { + return; + } + for (auto *junction : m_items) + { + if (junction == nullptr) + { + continue; + } + if (Global.editor_session && false == Global.NvRenderer) + { + junction->show(); + continue; + } + for (auto &shape : junction->create_shapes()) + { + simulation::Region->insert(shape, Scratchpad, false); + } + junction->merged(true); + } +} + +//--------------------------------------------------------------------------- diff --git a/world/Road.h b/world/Road.h new file mode 100644 index 00000000..930f3595 --- /dev/null +++ b/world/Road.h @@ -0,0 +1,542 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +#include + +#include "utilities/Classes.h" +#include "utilities/Names.h" +#include "scene/scenenode.h" +#include "world/Segment.h" + +namespace scene +{ +struct scratch_data; +class basic_section; +} +class road_order; + +// geometry a node puts in the scene as shapes of its own, apart from the shapes of the scenery, so it can take it back +class owned_shapes +{ + public: + // puts provided shapes in the section of the scene holding specified point + void show(std::vector Shapes, glm::dvec3 const &Location); + // takes the shapes back + void hide(); + bool shown() const + { + return m_section != nullptr; + } + // true if specified geometry was put in the scene by any object of this class + static bool owns(gfx::geometry_handle const &Geometry); + + private: + gfx::geometrybank_handle m_bank{0, 0}; + std::vector m_geometry; + scene::basic_section *m_section{nullptr}; +}; + +// a piece of a road for wheeled traffic: an axis, and a cross-section which stays the same along it, save for getting +// evenly wider or narrower if it's told to. +// the node itself is neither driven on nor drawn. it produces the lanes, which are ordinary invisible one-way tracks, +// and the surface, the sides and the markings, which are ordinary shapes. this way the vehicles, the ai and the renderers +// deal with a road without knowing about it. +// scenery entry: +// node road [ ]... endroad +// properties, past the ones of the lanes and the look: median , +// medianround , +// kerbs , bank , +// bankmaterial , weight , roundabout, +// lanesto : lanes the piece has at the end of its axis, where these aren't the ones it starts with +class road_node : public scene::basic_node +{ + + public: + // types + enum class side_type + { + none, + shoulder, // level with the surface, closed with a bank + sidewalk // raised on a kerb + }; + enum class marking_colour + { + none, + white, + orange + }; + // which of two neighbouring lanes can be left across the line they share; the lanes are taken left to right, facing along the axis + enum lane_change : int + { + change_none = 0, + change_toright = 1, // from the left lane to the right one + change_toleft = 2, // from the right lane to the left one + change_both = 3 + }; + struct lane_data + { + float width{3.5f}; + float velocity{-1.f}; // speed limit, none if negative + }; + struct side_data + { + side_type type{side_type::none}; + float width{0.f}; + std::string material; + }; + // what keeps the two directions of a road apart, where they're led away from each other + enum class median_type + { + gap, // nothing: the ground shows between two roadways + painted, // the surface carries on, marked as closed to the traffic + island // raised on a kerb + }; + struct median_data + { + median_type type{median_type::painted}; + std::array width{0.f, 0.f}; // at the start and at the end of the axis + std::string material{"none"}; // what an island is covered with + std::array round{0.f, 0.f}; // length of the rounded end of an island, at the start and at the end of the axis. 0: the island is cut straight + }; + // slope leading from the edge of the road down, or up, to the ground + struct bank_data + { + bool set{false}; // false: a shoulder gets the bank the road gives to all its shoulders, anything else gets none + std::array width{0.f, 0.f}; // at the start and at the end of the axis + std::array drop{0.f, 0.f}; // how far below the edge of the road it ends; negative if it rises + + bool operator==(bank_data const &Other) const + { + return set == Other.set && width == Other.width && drop == Other.drop; + } + }; + // everything the scenery says about the road. the lanes go left to right when facing along the axis: + // the ones going against it, outermost first, then the ones going along it + struct state + { + segment_data axis; + int forward{1}; // number of lanes going along the axis + int backward{1}; // number of lanes going against the axis + float lanewidth{3.5f}; // what a lane gets unless it's told otherwise + float velocity{-1.f}; + std::vector lanes; + std::vector changes; // permissions to change the lane, one for each pair of neighbouring lanes + std::array taper{1.f, 1.f}; // what the widths of the lanes are multiplied by at the start and at the end of the axis + std::string surface{"asphaltdark1"}; + float texturelength{4.f}; + std::array sides; // left, right + float kerbheight{0.12f}; + glm::vec2 slope{1.f, 0.4f}; // width and drop of the bank which closes a shoulder + marking_colour markings{marking_colour::white}; + float friction{0.85f}; + float sounddistance{25.f}; + int quality{15}; + std::string environment{"flat"}; + float weight{100.f}; // how willing the drivers are to take the road when leaving a junction, next to the other roads they can take there + median_data median; + std::array kerbs{false, false}; // left, right: a kerb runs along the edge of the surface, as high as the kerb of a sidewalk + float kerbwidth{0.15f}; + std::string kerbmaterial{"none"}; + std::array banks; // left, right + std::string bankmaterial{"none"}; // what a bank is covered with, other than one of a shoulder, which goes with the shoulder + bool roundabout{false}; // the piece is a part of a roundabout: traffic on it goes ahead of the traffic joining it + // lanes the piece is short of at an end of its axis: [end][0: the ones going along the axis, 1: the ones going against it]. + // these are the outermost lanes of the direction, which the road gains or loses along the piece; + // forward and backward say how many lanes there are where the piece has the most of them + std::array, 2> missing{}; + + // brings the content to a usable form: values within their limits, an entry for each lane and for each pair of neighbours + void normalize(); + // true if the piece has other lanes at its end than at its start + bool changing() const; + // sets the lanes the piece has at the start and at the end of its axis + void lanes_between(int const Startforward, int const Startbackward, int const Endforward, int const Endbackward); + // number of lanes at an end of the axis (0: the start, 1: the end): of the ones going along it, or of the ones going against it + int lanes_at(int const End, bool const Along) const; + // true if the piece has the lane at specified end of the axis + bool lane_at(std::size_t const Lane, int const End) const; + // the same road with the lanes it has at specified end of the axis all the way + state section(int const End) const; + // identifier of a lane: f1..fn go along the axis and b1..bn against it, both counted outwards from where the directions meet + std::string lane_id(std::size_t const Lane) const; + // number of a lane in the left to right order, -1 if there's no lane with such identifier + int lane_index(std::string const &Id) const; + // permission to change the lane a pair of neighbouring lanes gets if the scenery doesn't say otherwise + int default_change(std::size_t const Boundary) const; + // distance of the middle of a lane from the axis, positive to the left when facing along the axis + double lane_offset(std::size_t const Lane) const; + // the same at an end of the axis, where the road is centred on the axis with the lanes it has there. + // a lane the road is short of at that end is where the lane it comes out of, or goes into, is + double lane_offset(std::size_t const Lane, int const End) const; + // distance of the middle of a lane from the axis at specified part of the length of the axis, with the taper and what's between the directions counted in + double lane_position(std::size_t const Lane, double const Fraction) const; + // combined width of the lanes, before the taper + double width() const; + // the same for the lanes the piece has at an end of the axis + double width(int const End) const; + // combined width of the lanes at specified part of the length of the axis, with the taper counted in + double breadth(double const Fraction) const; + // what the widths of the lanes are multiplied by at specified value of the curve parameter + double scale(double const T) const; + // true if the two directions of the road are kept apart anywhere along it + bool divided() const; + // how far apart the two directions are at specified value of the curve parameter + double median_width(double const T) const; + // distance from the axis of the line the two directions meet at, on a road which doesn't keep them apart + double divide() const; + // the same for the lanes the piece has at an end of the axis + double divide(int const End) const; + // the same at specified part of the length of the axis, with the taper counted in + double middle(double const Fraction) const; + // width of the whole roadway at specified value of the curve parameter: the lanes and what's between the directions + double span(double const T) const; + // shape of the middle of a lane, laid out in the direction of travel + segment_data lane_path(std::size_t const Lane) const; + // shape of the way from the middle of a lane to the middle of another over a part of the axis, laid out in the direction of travel. + // From, To: values of the curve parameter of the axis, in the order of the axis + segment_data lane_path(std::size_t const Lane, std::size_t const Tolane, double const From, double const To) const; + // part of the axis, as values of its curve parameter, over which a piece which gains or loses lanes leads the lanes it has + // at one end to the lanes it has at the other. before it and past it there's a short path for each lane the piece has at that end + std::array change_range() const; + // length of the axis + double length() const; + // position on the axis and direction of the axis, for specified value of the curve parameter + glm::dvec3 point(double const T) const; + glm::dvec3 tangent(double const T) const; + }; + // constructors + explicit road_node(scene::node_data const &Nodedata); + // methods + // restores content of the node from provided input stream; reads up to and including the closing 'endroad' + void import(cParser &Input, glm::dvec3 const &Offset); + state const &definition() const + { + return m_state; + } + // replaces what the node knows about the road. the lanes and the geometry stay the way they were made, + // it's up to the caller to take them down beforehand and make them again + void define(state const &State); + // creates paths of the lanes and hands them over to the simulation + void create_lanes(); + // makes the paths which lead the lanes the piece has at one end to the lanes it has at the other, where these differ. + // to be called once the lanes are joined with the lanes of the neighbours + void create_links(); + // links lanes of opposite directions at an end of the road which leads nowhere, so the vehicles can turn back + void close_ends(); + // gives up the paths of the lanes, or the ones made to close the loose ends. the paths belong to the path table, + // the caller is expected to take them out of use + std::vector release_lanes(); + std::vector release_turns(); + // paths of the lanes, and the ones made to close the loose ends. the former go in the order of the lanes; + // on a piece which gains or loses lanes these are the short paths left of each such lane at the ends of the piece + std::vector const &tracks() const + { + return m_tracks; + } + // path of a lane at an end of the axis (0: the start, 1: the end); nullptr if the piece doesn't have the lane there + TTrack *lane_track(std::size_t const Lane, int const End) const + { + return Lane < m_lanetracks[End].size() ? m_lanetracks[End][Lane] : nullptr; + } + // paths leading from the lanes at one end to the lanes at the other, on a piece which gains or loses lanes + std::vector const &links() const + { + return m_links; + } + // shapes of the ways these make, for display + std::vector const &ways() const + { + return m_ways; + } + std::vector const &turns() const + { + return m_turns; + } + // true if there's a vehicle on any path of the road + bool occupied() const; + // generates geometry of the surface, the sides and the markings + std::vector create_shapes() const; + // puts geometry of the road in the scene as shapes of its own, which can be taken back + void show(); + // takes back geometry put in the scene by show() + void hide(); + // marks geometry of the road as mixed with other shapes of the scenery; such road can't be changed anymore + void merged(bool const Merged) + { + m_merged = Merged; + } + bool merged() const + { + return m_merged; + } + // members + bool m_editorremoved{false}; // removed in the editor; kept around so the removal can be undone + + private: + // methods + // radius() subclass details, calculates node's bounding radius + float radius_() override; + // serialize() subclass details, sends content of the subclass to provided stream + void serialize_(std::ostream &Output) const override; + // deserialize() subclass details, restores content of the subclass from provided stream + void deserialize_(std::istream &Input) override; + // export() subclass details, sends basic content of the class in legacy (text) format to provided stream + void export_as_text_(std::ostream &Output) const override; + // creates a path for the vehicles and registers it with the simulation + TTrack *create_track(std::string const &Name, segment_data const &Path, float const Width, float const Velocity); + // the same for a path put together by the caller, the way the scenery defines one + TTrack *create_track(std::string const &Name, std::string const &Definition); + // members + state m_state; + std::vector m_tracks; + std::array, 2> m_lanetracks; // path of each lane at the start and at the end of the axis + std::vector m_links; + std::vector m_ways; + std::vector m_turns; + bool m_merged{false}; + owned_shapes m_shapes; // geometry put in the scene by show() +}; + +// a place where roads meet. each arm of the junction is where a road ends: a point in the middle of that end, the way +// the road leaves, and the lanes it has there. the junction produces the surface between the arms, and paths leading +// the vehicles from each lane coming in to the lanes going out of the other arms. a lane with more than one way out +// gets a legacy crossroads path, which is what the vehicles and their drivers already know how to pick a way through. +// scenery entry: +// node junction arm ... [ ]... endjunction +// properties: surface , texlength , markings , side , kerb , +// slope , stopline , velocity , friction , environment , +// priority , turns , +// armmedian , kerbs , +// cornerbank (at that road, then at the next one), bankmaterial , +// crosswalk , median +// a junction of two roads is drawn as a stretch of road, on which the lanes of one are led to the lanes of the other. +// that's what a road which changed its lanes used to take; such a junction found in a scenery is loaded as a piece of road instead, see as_road() +// the arms don't have to be level with each other: the surface is spanned between their ends and the centre. +// the junction also tells the vehicles when to wait: the ones which have to give way wait for the ones with the right of way, +// and where neither has it, for the ones coming from their right, or from the opposite side when turning left across their way +class junction_node : public scene::basic_node +{ + + public: + // types + // what the vehicles coming by a road are to do about the ones coming by the other roads + enum class right_of_way + { + none, // the rule of the right hand + priority, // the others give way + yield, // gives way + stop // stops, then gives way + }; + // ways a lane can be left by + enum turn_flags : int + { + turn_left = 1, + turn_straight = 2, + turn_right = 4 + }; + struct arm_data + { + glm::dvec3 position{0.0}; // middle of the end of the road + glm::dvec2 direction{0.0, 1.0}; // the way out of the junction, seen from above (x, z) + int incoming{1}; // lanes leading into the junction + int outgoing{1}; // lanes leading out of it + float width{3.5f}; // width of a lane + bool stopline{false}; // a line is painted across the lanes leading into the junction + float median{0.f}; // how far apart the two directions of the road are there + float crosswalk{0.f}; // length of the pedestrian crossing painted across the road where it meets the junction; 0: none + // slope leading to the ground from the corner which starts at the left edge of this road, seen from the junction, and goes on + // to the next road. it starts at this road and ends at the next one + road_node::bank_data bank; + right_of_way priority{right_of_way::none}; + // ways each lane leading into the junction can be left by, a sum of turn_flags; the lanes are counted from the middle of the road. + // a lane with no entry, or with 0, gets what the junction works out: the inner lane takes the left turns, the outer one the right turns + std::vector turns; + }; + // a way through the junction from a lane leading into it + struct way_data + { + std::size_t exit{0}; // arm it leaves by + int kind{turn_straight}; // one of turn_flags + int code{0}; // what a driver calls it, once they picked it + TTrack *track{nullptr}; // lane it leads to + segment_data path; + std::vector> conflicts; // ways it can't be taken along with: lane leading in, and a way from that lane + }; + // a lane leading into the junction, with what its traffic is told + struct gate_data + { + std::size_t arm{0}; + int lane{1}; + TTrack *entry{nullptr}; // the lane + TTrack *link{nullptr}; // path, or crossroads of paths, leading on from it + std::vector ways; + std::vector approach; // lanes the vehicles heading for the junction are looked for on, nearest first + // for each of these: the way a vehicle on it has to have picked to be heading for the junction; 0 if there's no choice there. + // it's set for a path on which a road forks into more lanes + std::vector wishes; + glm::dvec3 line{0.0}; // where the lane ends + road_order *order{nullptr}; + // what's going on at the moment + TDynamicObject *first{nullptr}; // vehicle nearest to the junction + double distance{0.0}; // of that vehicle from the end of the lane + double speed{0.0}; + double halflength{0.0}; + unsigned taken{0}; // ways that vehicle may take, a bit for each + unsigned inside{0}; // ways with a vehicle on them + bool active{false}; // the vehicle is near enough to matter + bool blocked{false}; // it's told to wait + bool committed{false}; // it was let through, the others wait for it + bool stopped{false}; // it made its stop at the stop sign + double waited{0.0}; // how long it's been standing there + double idle{0.0}; // how long it's been standing there since it was let through + }; + struct state + { + glm::dvec3 centre{0.0}; + std::vector arms; + std::string surface{"asphaltdark1"}; + float texturelength{4.f}; + road_node::marking_colour markings{road_node::marking_colour::white}; + road_node::side_data side; // what the corners between the roads are lined with, the way the sides of a road are + bool kerbs{false}; // a kerb runs around the corners, the way it runs along the edges of a road + float kerbwidth{0.15f}; + std::string kerbmaterial{"none"}; + float kerbheight{0.12f}; + glm::vec2 slope{1.f, 0.4f}; // width and drop of the bank which closes a shoulder + std::string bankmaterial{"none"}; // what a bank of a corner is covered with, other than one of a shoulder + // a junction of two roads, where a road changes its lanes: what's between the two directions where they're apart + road_node::median_type median{road_node::median_type::painted}; + std::string medianmaterial{"none"}; // what an island is covered with + float velocity{30.f}; // speed limit on the way through + float friction{0.85f}; + float sounddistance{25.f}; + int quality{15}; + std::string environment{"flat"}; + + // brings the content to a usable form + void normalize(); + // width of the road at specified arm + double arm_width(std::size_t const Arm) const; + // where a lane of an arm meets the junction; the lanes of each kind are counted from the middle of the road, starting with 1 + glm::dvec3 lane_point(std::size_t const Arm, bool const Incoming, int const Lane) const; + // arms in the order they're met going around the junction + std::vector arm_order() const; + // edge of the surface: ends of the arms joined with rounded corners, going around the junction. + // Corners, if provided, receives the points of each corner apart + std::vector outline(std::vector> *Corners = nullptr) const; + }; + // constructors + explicit junction_node(scene::node_data const &Nodedata); + // methods + // restores content of the node from provided input stream; reads up to and including the closing 'endjunction' + void import(cParser &Input, glm::dvec3 const &Offset); + state const &definition() const + { + return m_state; + } + void define(state const &State); + // a junction of two roads is a stretch where a road changes its lanes, which a road can do by itself. + // Road: receives the piece of road to take the place of the junction. returns: false if the junction isn't of that kind + bool as_road(road_node::state &Road) const; + // makes paths leading through the junction, between the lanes of the roads attached to it at the moment + void create_links(); + // gives up the paths leading through the junction; the caller is expected to take them out of use + std::vector release_links(); + std::vector const &links() const + { + return m_links; + } + // shapes of the ways through the junction, for display + std::vector const &movements() const + { + return m_movements; + } + // true if there's a vehicle on any path of the junction + bool occupied() const; + // tells the vehicles heading for the junction whether they can drive on. to be called with each step of the simulation + void update(double const Deltatime); + // lanes leading into the junction, with what their traffic is told at the moment + std::vector const &gates() const + { + return m_gates; + } + // generates geometry of the surface + std::vector create_shapes() const; + // puts geometry of the junction in the scene as shapes of its own, or takes it back + void show(); + void hide(); + void merged(bool const Merged) + { + m_merged = Merged; + } + bool merged() const + { + return m_merged; + } + // members + bool m_editorremoved{false}; + + private: + // methods + float radius_() override; + void serialize_(std::ostream &Output) const override; + void deserialize_(std::istream &Input) override; + void export_as_text_(std::ostream &Output) const override; + // creates a path through the junction from provided definition and registers it with the simulation + TTrack *create_track(std::string const &Definition, std::size_t const Index); + // geometry of a junction of two roads + std::vector create_transition_shapes() const; + // sets up what the traffic of each lane leading in is told, once the ways through are made + void create_gates(); + // members + state m_state; + std::vector m_links; + std::vector m_movements; + std::vector m_gates; + std::vector m_orders; // orders for the drivers, used again each time the gates are made. never freed, as a driver can hold on to one + double m_scan{0.0}; // time left to the next look at the traffic + bool m_quiet{false}; // there was no traffic the last time + bool m_merged{false}; + owned_shapes m_shapes; +}; + +// collection of roads present in the scene +class road_table : public basic_table +{ + + public: + // legacy style initialization, to be performed when the tracks are already joined: ties up the lanes of the pieces + // which gain or lose them. the junctions go by these, so it comes ahead of their initialization + void InitLanes(); + // the same, to be performed once the junctions are done: closes the ends of the roads which lead nowhere + void InitRoads(); + // generates geometry of the roads and puts it in the scene + void create_geometry(scene::scratch_data &Scratchpad); +}; + +// collection of road junctions present in the scene +class junction_table : public basic_table +{ + + public: + // legacy style initialization, to be performed when the tracks are joined, ahead of the roads + void InitJunctions(); + // to be called with each step of the simulation + void update(double const Deltatime); + // generates geometry of the junctions and puts it in the scene + void create_geometry(scene::scratch_data &Scratchpad); +}; + +//--------------------------------------------------------------------------- diff --git a/world/RoadOrder.h b/world/RoadOrder.h new file mode 100644 index 00000000..79bf2d43 --- /dev/null +++ b/world/RoadOrder.h @@ -0,0 +1,60 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include + +#include + +#include "world/Event.h" + +// an order for the drivers of road vehicles heading for a place they may have to wait at: a closed level crossing, +// a junction they have to give way at. to them it's what a signal is to the driver of a train. it doesn't go to +// the event manager and never runs: the drivers find it on the lane they scan, and ask it what speed it allows +class road_order : public basic_event +{ + + public: + road_order() + { + m_passive = true; + } + void init() override {} + std::string input_text() const override + { + return "SetVelocity"; + } + TCommandType input_command() const override + { + return TCommandType::cm_SetVelocity; + } + double input_value(int const Index) const override + { + return m_velocity; + } + glm::dvec3 input_location() const override + { + return m_location; + } + // members + double m_velocity{-1.0}; // 0: stop, -1: no limit + glm::dvec3 m_location{0.0}; + + private: + std::string type() const override + { + return "roadorder"; + } + void deserialize_(cParser &Input, scene::scratch_data &Scratchpad) override {} + void run_() override {} + void export_as_text_(std::ostream &Output) const override {} +}; + +//--------------------------------------------------------------------------- diff --git a/world/RoadPoint.cpp b/world/RoadPoint.cpp new file mode 100644 index 00000000..bc4cea25 --- /dev/null +++ b/world/RoadPoint.cpp @@ -0,0 +1,1266 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "world/RoadPoint.h" + +#include "world/Track.h" +#include "world/Event.h" +#include "world/RoadOrder.h" +#include "vehicle/DynObj.h" +#include "vehicle/Driver.h" +#include "vehicle/Train.h" +#include "model/Model3d.h" +#include "simulation/simulation.h" +#include "rendering/particles.h" +#include "rendering/renderer.h" +#include "scene/scene.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "utilities/utilities.h" + +namespace +{ + +constexpr double stopline_width{0.5}; +constexpr double stopline_inset{0.15}; // gap between the ends of a stop line and the edges of its lane +constexpr double line_lift{0.02}; // the line is raised over the surface, so the two don't fight for depth +constexpr double scan_period{0.2}; // seconds between looks at the rails of a crossing +constexpr double hold_time{4.0}; // seconds a crossing stays closed after the last train was seen +constexpr double near_zone{30.0}; // a rail vehicle this close to the stop lines keeps the crossing closed whichever way it goes +constexpr double clear_zone{20.0}; // a vehicle isn't put on the road with another one this close +constexpr double retry_period{1.0}; // seconds to the next try, when a vehicle can't be put on the road +constexpr double front_room{2.5}; // half the length a vehicle is presumed to have before it's made +constexpr double queue_reach{250.0}; // how far back from a closed crossing the vehicles standing on the lanes are taken to wait for it +constexpr double stuck_time{15.0}; // seconds a vehicle of a spawn point can stand still before it's taken off the road +constexpr double stuck_speed{0.1}; // m/s, slower than this is standing still +constexpr double reverse_speed{0.5}; // m/s, a vehicle going backwards faster than this is taken off the road +constexpr double stripe_width{0.5}; // of a pedestrian crossing; the gaps are as wide + +// a path as a curve: points along it, and distances along it +class path_curve +{ + + public: + explicit path_curve(segment_data const &Path) + { + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + auto const &control1{Path.points[segment_data::point::control1]}; + auto const &control2{Path.points[segment_data::point::control2]}; + m_points[0] = start; + m_points[3] = end; + if (control1 == glm::dvec3{0.0} && control2 == glm::dvec3{0.0}) + { + m_points[1] = start + (end - start) / 3.0; + m_points[2] = end - (end - start) / 3.0; + } + else + { + m_points[1] = start + control1; + m_points[2] = end + control2; + } + m_stations[0] = 0.0; + auto previous{m_points[0]}; + for (int idx = 1; idx <= samples; ++idx) + { + auto const current{point(static_cast(idx) / samples)}; + m_stations[idx] = m_stations[idx - 1] + glm::distance(previous, current); + previous = current; + } + } + double length() const + { + return m_stations[samples]; + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * m_points[0] + 3.0 * u * u * T * m_points[1] + 3.0 * u * T * T * m_points[2] + T * T * T * m_points[3]; + } + glm::dvec3 direction(double const T) const + { + auto const u{1.0 - T}; + auto const result{3.0 * u * u * (m_points[1] - m_points[0]) + 6.0 * u * T * (m_points[2] - m_points[1]) + 3.0 * T * T * (m_points[3] - m_points[2])}; + return glm::length2(result) > 1e-12 ? glm::normalize(result) : glm::dvec3{0.0, 0.0, 1.0}; + } + // value of the curve parameter for specified distance from the start, and the other way around + double parameter(double const Station) const + { + if (Station <= 0.0) + { + return 0.0; + } + if (Station >= length()) + { + return 1.0; + } + auto const idx{std::distance(m_stations.begin(), std::upper_bound(m_stations.begin(), m_stations.end(), Station))}; + auto const span{m_stations[idx] - m_stations[idx - 1]}; + return (static_cast(idx - 1) + (span > 0.0 ? (Station - m_stations[idx - 1]) / span : 0.0)) / samples; + } + double station(double const T) const + { + auto const position{std::clamp(T, 0.0, 1.0) * samples}; + auto const idx{std::min(samples - 1, static_cast(position))}; + return glm::mix(m_stations[idx], m_stations[idx + 1], position - idx); + } + // value of the curve parameter for the point nearest to specified one, seen from above; Distance receives how far it is + double nearest(glm::dvec3 const &Point, double &Distance) const + { + auto const distance = [&](double const T) { + auto const position{point(T)}; + return glm::distance(glm::dvec2{position.x, position.z}, glm::dvec2{Point.x, Point.z}); + }; + double best{0.0}; + for (int idx = 1; idx <= samples; ++idx) + { + if (distance(static_cast(idx) / samples) < distance(best)) + { + best = static_cast(idx) / samples; + } + } + auto step{1.0 / samples}; + for (int pass = 0; pass < 16; ++pass) + { + step *= 0.5; + if (best - step >= 0.0 && distance(best - step) < distance(best)) + { + best -= step; + } + else if (best + step <= 1.0 && distance(best + step) < distance(best)) + { + best += step; + } + } + Distance = distance(best); + return best; + } + + private: + static constexpr int samples{32}; + std::array m_points; + std::array m_stations{}; +}; + +// true if the path can't have a point within specified distance from specified one; a cheap way to tell most of the scenery apart +bool out_of_reach(segment_data const &Path, glm::dvec3 const &Point, double const Reach) +{ + auto const &start{Path.points[segment_data::point::start]}; + auto const &end{Path.points[segment_data::point::end]}; + // the curve lies within its control points, which are no further from the start than this + auto const span{glm::length(Path.points[segment_data::point::control1]) + glm::distance(start, end) + glm::length(Path.points[segment_data::point::control2])}; + return glm::distance(glm::dvec2{start.x, start.z}, glm::dvec2{Point.x, Point.z}) > span + Reach; +} + +// an order for the drivers heading for a closed crossing +using stop_event = road_order; + +// the road passing under specified point: the one whose axis is nearest. T: receives the value of the curve parameter of its axis there +road_node const *road_under(glm::dvec3 const &Point, double &T) +{ + road_node const *result{nullptr}; + auto best{std::numeric_limits::max()}; + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved || out_of_reach(road->definition().axis, Point, 50.0)) + { + continue; + } + auto const &layout{road->definition()}; + path_curve const axis{layout.axis}; + double distance{0.0}; + auto const t{axis.nearest(Point, distance)}; + if (distance > 0.5 * layout.span(t) + 1.0 || std::abs(axis.point(t).y - Point.y) > 3.0 || distance >= best) + { + continue; + } + best = distance; + result = road; + T = t; + } + return result; +} + +// lane of a road with the point nearest to specified one, within specified distance from it. Station: receives the distance of that point from the start of the lane +TTrack *nearest_lane(glm::dvec3 const &Point, double const Reach, double &Station) +{ + TTrack *result{nullptr}; + auto best{Reach}; + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + { + continue; + } + for (auto *lane : road->tracks()) + { + if (lane == nullptr || lane->m_paths.empty() || out_of_reach(lane->m_paths.front(), Point, Reach)) + { + continue; + } + path_curve const curve{lane->m_paths.front()}; + double distance{0.0}; + auto const t{curve.nearest(Point, distance)}; + if (distance >= best || std::abs(curve.point(t).y - Point.y) > 3.0) + { + continue; + } + best = distance; + result = lane; + Station = curve.station(t); + } + } + return result; +} + +double planar_distance(glm::dvec3 const &Left, glm::dvec3 const &Right) +{ + return glm::distance(glm::dvec2{Left.x, Left.z}, glm::dvec2{Right.x, Right.z}); +} + +} // namespace + +// brings the content to a usable form +void roadpoint_node::state::normalize() +{ + clearance = std::clamp(clearance, 1.f, 100.f); + warning = std::clamp(warning, 50.f, 5000.f); + interval = std::clamp(interval, 1.f, 3600.f); + variation = std::clamp(variation, 0.f, 1.f); + velocity = std::clamp(velocity, 5.f, 200.f); + count = std::clamp(count, 1, 64); + radius = std::clamp(radius, 0.5f, 50.f); + length = std::clamp(length, 1.f, 20.f); +} + +roadpoint_node::roadpoint_node(scene::node_data const &Nodedata) : basic_node(Nodedata) +{ + m_state.kind = (Nodedata.type == "spawn" ? kind_type::spawn : Nodedata.type == "despawn" ? kind_type::despawn : Nodedata.type == "crosswalk" ? kind_type::crosswalk : kind_type::crossing); +} + +// scenery keyword for specified kind of point +std::string roadpoint_node::keyword(kind_type const Kind) +{ + return Kind == kind_type::spawn ? "spawn" : Kind == kind_type::despawn ? "despawn" : Kind == kind_type::crosswalk ? "crosswalk" : "crossing"; +} + +bool roadpoint_node::is_keyword(std::string const &Type) +{ + return Type == "crossing" || Type == "spawn" || Type == "despawn" || Type == "crosswalk"; +} + +// reads vehicles out of a text +std::vector roadpoint_node::parse_vehicles(std::string const &Text) +{ + std::vector result; + auto const add = [&result](std::vector const &Tokens, std::size_t const First, std::string const &Load, std::string const &Loadtype) { + vehicle_data vehicle; + vehicle.folder = Tokens[First]; + vehicle.skin = Tokens[First + 1]; + vehicle.type = Tokens[First + 2]; + replace_slashes(vehicle.folder); + replace_slashes(vehicle.skin); + replace_slashes(vehicle.type); + vehicle.load = std::max(0, std::atoi(Load.c_str())); + if (vehicle.load > 0) + { + vehicle.loadtype = Loadtype; + } + result.emplace_back(vehicle); + }; + std::vector tokens; + { + std::istringstream stream{Text}; + std::string token; + while (stream >> token) + { + tokens.emplace_back(token); + } + } + if (std::find(tokens.begin(), tokens.end(), "dynamic") != tokens.end()) + { + // node dynamic [] enddynamic + for (std::size_t idx = 0; idx < tokens.size(); ++idx) + { + if (tokens[idx] != "dynamic" || idx + 3 >= tokens.size()) + { + continue; + } + auto last{idx + 4}; + while (last < tokens.size() && tokens[last] != "enddynamic") + { + ++last; + } + // past the vehicle itself come the path, the offset, the driver and the velocity, then the load + auto const load{idx + 8}; + add(tokens, idx + 1, load < last ? tokens[load] : std::string{}, load + 1 < last ? tokens[load + 1] : std::string{}); + idx = last; + } + return result; + } + std::istringstream lines{Text}; + std::string line; + while (std::getline(lines, line)) + { + std::istringstream stream{line}; + std::vector parts; + std::string part; + while (stream >> part) + { + parts.emplace_back(part); + } + if (parts.size() >= 3) + { + add(parts, 0, parts.size() > 3 ? parts[3] : std::string{}, parts.size() > 4 ? parts[4] : std::string{}); + } + } + return result; +} + +// restores content of the node from provided input stream; reads up to and including the closing statement +void roadpoint_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + auto const type{keyword(m_state.kind)}; + auto const label{type + " \"" + m_name + "\""}; + auto const kind{m_state.kind}; + m_state = state{}; + m_state.kind = kind; + Input.getTokens(3); + Input >> m_state.position.x >> m_state.position.y >> m_state.position.z; + m_state.position += Offset; + int copies{0}; // of each vehicle, the way the first sceneries with spawn points gave the size of the set + auto counted{false}; + + auto token{Input.getToken()}; + while (false == token.empty() && token != "end" + type) + { + if (token == "clearance") + { + Input.getTokens(); + Input >> m_state.clearance; + } + else if (token == "warning") + { + Input.getTokens(); + Input >> m_state.warning; + } + else if (token == "stoplines") + { + m_state.stoplines = (Input.getToken() != "no"); + } + else if (token == "interval") + { + Input.getTokens(); + Input >> m_state.interval; + } + else if (token == "variation") + { + Input.getTokens(); + Input >> m_state.variation; + } + else if (token == "velocity") + { + Input.getTokens(); + Input >> m_state.velocity; + } + else if (token == "count") + { + Input.getTokens(); + Input >> m_state.count; + counted = true; + } + else if (token == "copies") + { + Input.getTokens(); + Input >> copies; + } + else if (token == "length") + { + Input.getTokens(); + Input >> m_state.length; + } + else if (token == "radius") + { + Input.getTokens(); + Input >> m_state.radius; + } + else if (token == "node") + { + // a vehicle, given the way vehicles are put in a scenery: + // node dynamic [] enddynamic + std::string discard; + std::string nodetype; + Input.getTokens(4); + Input >> discard >> discard >> discard >> nodetype; + vehicle_data vehicle; + if (nodetype == "dynamic") + { + vehicle.folder = Input.getToken(); + vehicle.skin = Input.getToken(); + vehicle.type = Input.getToken(); + replace_slashes(vehicle.folder); + replace_slashes(vehicle.skin); + replace_slashes(vehicle.type); + } + else + { + ErrorLog("Bad road point: " + label + " lists a \"" + nodetype + "\" where it takes vehicles only"); + } + // where the vehicle is put and how it's driven is up to the point, what's left to take is its load + std::vector rest; + auto part{Input.getToken()}; + while (false == part.empty() && part != "enddynamic" && part != "end" + type) + { + rest.emplace_back(part); + part = Input.getToken(); + } + if (rest.size() > 4) + { + vehicle.load = std::atoi(rest[4].c_str()); + } + if (rest.size() > 5 && vehicle.load > 0) + { + vehicle.loadtype = rest[5]; + } + if (false == vehicle.type.empty()) + { + m_state.vehicles.emplace_back(vehicle); + } + if (part != "enddynamic") + { + ErrorLog("Bad road point: a vehicle of " + label + " isn't closed with \"enddynamic\""); + if (part == "end" + type) + { + break; + } + } + } + else + { + ErrorLog("Bad road point: unknown property: \"" + token + "\" defined for " + label); + } + token = Input.getToken(); + } + if (false == counted && copies > 0) + { + m_state.count = copies * static_cast(std::max(1, m_state.vehicles.size())); + } + m_state.normalize(); + location(m_state.position); +} + +// replaces what the node knows about itself +void roadpoint_node::define(state const &State) +{ + auto const vehicles{m_state.vehicles}; + auto const count{m_state.count}; + m_state = State; + m_state.normalize(); + if (m_prepared && (m_state.vehicles != vehicles || m_state.count != count)) + { + // vehicles can't be unmade, there's too much holding on to them. the old ones are left alone: + // those which wait stay where they are, those on the road drive on until something takes them away + m_pool.clear(); + m_prepared = false; + } + location(m_state.position); + m_area.radius = -1.f; +} + +// ties the point to the lanes and the rails as they are at the moment +void roadpoint_node::bind() +{ + unbind(); + m_bound = true; + if (m_editorremoved) + { + return; + } + switch (m_state.kind) + { + case kind_type::crossing: + { + bind_crossing(); + break; + } + case kind_type::spawn: + { + // the vehicles are put on the lane the point is on + double station{0.0}; + auto *lane{nearest_lane(m_state.position, 3.0, station)}; + if (lane != nullptr) + { + m_lanes.emplace_back(lane); + // at the very start of a lane which leads from nowhere the vehicle would hang over its end + m_offset = (lane->trPrev == nullptr ? std::max(station, 2.0 * front_room) : station); + } + break; + } + case kind_type::crosswalk: + { + // the stripes go across the road the point is on + double t{0.0}; + auto const *road{road_under(m_state.position, t)}; + if (road != nullptr) + { + auto const &layout{road->definition()}; + path_curve const axis{layout.axis}; + m_onroad = true; + m_along = axis.direction(t); + m_halfwidth = 0.5 * layout.span(t); + // the point itself may be off the axis, the stripes aren't + auto const centre{axis.point(t)}; + m_state.position = centre; + } + break; + } + case kind_type::despawn: + { + // the vehicles are taken from every lane which passes close enough + for (auto const *road : simulation::Roads.sequence()) + { + if (road == nullptr || road->m_editorremoved) + { + continue; + } + for (auto *lane : road->tracks()) + { + if (lane == nullptr || lane->m_paths.empty() || out_of_reach(lane->m_paths.front(), m_state.position, m_state.radius)) + { + continue; + } + path_curve const curve{lane->m_paths.front()}; + double distance{0.0}; + auto const t{curve.nearest(m_state.position, distance)}; + if (distance <= m_state.radius && std::abs(curve.point(t).y - m_state.position.y) <= 3.0) + { + m_lanes.emplace_back(lane); + } + } + // and from the paths which lead the lanes of a piece which gains or loses them from one of its ends to the other + for (auto *link : road->links()) + { + if (link == nullptr) + { + continue; + } + for (auto const &way : link->m_paths) + { + if (out_of_reach(way, m_state.position, m_state.radius)) + { + continue; + } + path_curve const curve{way}; + double distance{0.0}; + auto const t{curve.nearest(m_state.position, distance)}; + if (distance <= m_state.radius && std::abs(curve.point(t).y - m_state.position.y) <= 3.0) + { + m_lanes.emplace_back(link); + break; + } + } + } + } + break; + } + } +} + +// finds the rails of a crossing, and the places the vehicles of each lane are to stop at +void roadpoint_node::bind_crossing() +{ + // the road the crossing is on: the one whose axis passes nearest + double where{0.0}; + auto const *crossed{road_under(m_state.position, where)}; + if (crossed != nullptr) + { + auto const &layout{crossed->definition()}; + if (layout.changing()) + { + // NOTE: on such a piece a lane is a string of paths, and may fork; the vehicles are stopped ahead of all that + ErrorLog("Bad road point: crossing \"" + m_name + "\" lies on a piece of road which gains or loses lanes, the vehicles are stopped before that piece"); + } + for (std::size_t idx = 0; idx < layout.lanes.size(); ++idx) + { + // the path the traffic of the lane gets on the piece by; on a piece which keeps its lanes it's the lane itself + auto *lane{crossed->lane_track(idx, idx >= static_cast(layout.backward) ? 0 : 1)}; + if (lane == nullptr || lane->m_paths.empty()) + { + continue; + } + // the lanes are laid out the way the traffic goes, so the place to stop at is back along the lane from the crossing + double distance{0.0}; + path_curve curve{lane->m_paths.front()}; + auto station{curve.station(curve.nearest(m_state.position, distance))}; + double remaining{m_state.clearance}; + for (int guard = 0; guard < 16; ++guard) + { + if (station >= remaining) + { + station -= remaining; + break; + } + // it's on the piece before this one, if there's any: the same lane of the road, not a way through a junction + auto *previous{lane->trPrev}; + auto const *owner{previous != nullptr ? dynamic_cast(previous->m_road) : nullptr}; + if (owner == nullptr || previous->m_paths.empty() || previous->trNext != lane || std::find(owner->tracks().begin(), owner->tracks().end(), previous) == owner->tracks().end()) + { + station = 0.0; + break; + } + remaining -= station; + lane = previous; + curve = path_curve{lane->m_paths.front()}; + station = curve.length(); + } + auto const t{curve.parameter(station)}; + stop_data stop; + stop.track = lane; + stop.position = curve.point(t); + stop.direction = curve.direction(t); + stop.width = (idx < crossed->definition().lanes.size() ? crossed->definition().lanes[idx].width : crossed->definition().lanewidth); + m_stops.emplace_back(stop); + } + } + // orders for the drivers. the ones made before are used again, as a driver may still hold on to one + while (m_events.size() < m_stops.size()) + { + auto *event{new stop_event()}; + event->m_name = m_name + ":stop" + std::to_string(m_events.size() + 1); + m_events.emplace_back(event); + } + for (std::size_t idx = 0; idx < m_stops.size(); ++idx) + { + auto *event{static_cast(m_events[idx])}; + event->m_location = m_stops[idx].position; + // lanes are driven from their first point to the second, which is the way events of the second kind are for + m_stops[idx].track->m_events2.emplace_back(event->m_name, event); + } + + // rails: the tracks passing between the stop lines, and what leads to them within the warning distance + std::vector> reached; + auto const known = [&reached](TTrack const *Track) { return std::any_of(reached.begin(), reached.end(), [Track](std::pair const &Entry) { return Entry.first == Track; }); }; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0) + { + continue; + } + for (auto const &path : track->m_paths) + { + if (out_of_reach(path, m_state.position, m_state.clearance)) + { + continue; + } + path_curve const curve{path}; + double distance{0.0}; + auto const t{curve.nearest(m_state.position, distance)}; + if (distance <= m_state.clearance && std::abs(curve.point(t).y - m_state.position.y) <= 3.0) + { + reached.emplace_back(track, 0.0); + break; + } + } + } + for (std::size_t idx = 0; idx < reached.size(); ++idx) + { + auto *track{reached[idx].first}; + double length{0.0}; + for (auto const &path : track->m_paths) + { + length = std::max(length, path_curve{path}.length()); + } + auto const travelled{reached[idx].second + length}; + if (travelled > m_state.warning) + { + continue; + } + std::vector neighbours{track->trPrev, track->trNext}; + if (track->SwitchExtension != nullptr) + { + // both ways of a switch count, as the train can come by either + neighbours.insert(neighbours.end(), {track->SwitchExtension->pPrevs[0], track->SwitchExtension->pPrevs[1], track->SwitchExtension->pNexts[0], track->SwitchExtension->pNexts[1]}); + } + for (auto *neighbour : neighbours) + { + if (neighbour != nullptr && (neighbour->iCategoryFlag & 1) != 0 && false == known(neighbour)) + { + reached.emplace_back(neighbour, travelled); + } + } + } + for (auto const &entry : reached) + { + m_rails.emplace_back(entry.first); + } + // lanes the vehicles wait on while the crossing is closed: the ones with the stops, and what leads to them + for (auto const &stop : m_stops) + { + auto *lane{stop.track}; + double reach{0.0}; + for (int guard = 0; lane != nullptr && guard < 64 && reach < queue_reach; ++guard) + { + if (std::find(m_queue.begin(), m_queue.end(), lane) == m_queue.end()) + { + m_queue.emplace_back(lane); + } + if (false == lane->m_paths.empty()) + { + reach += glm::distance(lane->m_paths.front().points[segment_data::point::start], lane->m_paths.front().points[segment_data::point::end]); + } + lane = lane->trPrev; + } + } + close(m_closed); +} + +// lets go of the lanes and the rails +void roadpoint_node::unbind() +{ + if (false == m_bound) + { + return; + } + m_bound = false; + for (std::size_t idx = 0; idx < m_stops.size() && idx < m_events.size(); ++idx) + { + auto &events{m_stops[idx].track->m_events2}; + auto const *event{m_events[idx]}; + events.erase(std::remove_if(events.begin(), events.end(), [event](std::pair const &Entry) { return Entry.second == event; }), events.end()); + } + // a driver who has one of the orders in sight keeps it; it's not to hold anyone anymore + for (auto *event : m_events) + { + static_cast(event)->m_velocity = -1.0; + } + m_stops.clear(); + m_rails.clear(); + m_queue.clear(); + m_lanes.clear(); + m_seen.clear(); + m_onroad = false; +} + +// makes the vehicles of a spawn point +void roadpoint_node::prepare() +{ + if (m_prepared || m_state.kind != kind_type::spawn || m_editorremoved) + { + return; + } + if (false == m_bound) + { + bind(); + } + if (m_lanes.empty()) + { + ErrorLog("Bad road point: spawn \"" + m_name + "\" isn't on a lane of a road, there's nowhere to put its vehicles"); + return; + } + m_prepared = true; + // the first vehicle shows up after a part of the interval, so points with the same interval don't all start at once + m_timer = LocalRandom(0.0, m_state.interval); + // vehicles are known by their names, so a point without one makes something up for them + static int nameless{0}; + auto const basename{m_name.empty() ? "roadspawn" + std::to_string(++nameless) : m_name}; + // the listed vehicles are gone through as many times as it takes, from a random one on, so the set gets each kind in turn + auto const kinds{m_state.vehicles.size()}; + auto const start{kinds > 0 ? std::min(kinds - 1, static_cast(LocalRandom(0.0, static_cast(kinds)))) : 0}; + for (int index = 0; index < m_state.count && kinds > 0; ++index) + { + auto const &definition{m_state.vehicles[(start + static_cast(index)) % kinds]}; + // a vehicle like any other of the scenery, made where the point is and driven at the speed of the point... + auto *vehicle{new TDynamicObject()}; + auto const length{vehicle->Init(basename + ":" + std::to_string(++m_made), definition.folder, definition.skin, definition.type, m_lanes.front(), m_offset + front_room, "headdriver", + m_state.velocity, "", static_cast(definition.load), definition.loadtype, false, "")}; + if (length == 0.0) + { + ErrorLog("Bad road point: vehicle \"" + definition.folder + "/" + definition.type + "\" of spawn \"" + m_name + "\" couldn't be made"); + if (vehicle->MyTrack != nullptr) + { + vehicle->MyTrack->RemoveDynamicObject(vehicle); + } + delete vehicle; + continue; + } + if (vehicle->mdModel != nullptr) + { + for (auto const &smokesource : vehicle->mdModel->smoke_sources()) + { + simulation::Particles.insert(smokesource.first, vehicle, smokesource.second); + } + } + if (false == simulation::Vehicles.insert(vehicle)) + { + ErrorLog("Bad road point: vehicle name \"" + vehicle->name() + "\" made for spawn \"" + m_name + "\" is taken already"); + } + pooled_vehicle entry; + entry.vehicle = vehicle; + entry.velocity = vehicle->MoverParameters->V; + m_pool.emplace_back(entry); + // ...which is taken off the road right away, the way the vehicles leaving the scenery are, to wait for its turn + if (vehicle->MyTrack != nullptr) + { + vehicle->MyTrack->RemoveDynamicObject(vehicle); + vehicle->MyTrack = nullptr; + } + vehicle->bEnabled = false; + } + if (m_pool.empty()) + { + ErrorLog("Bad road point: spawn \"" + m_name + "\" has no vehicles to put on the road"); + } +} + +// true if specified vehicle stands in the line of vehicles held by the crossing +bool roadpoint_node::holds(TDynamicObject const &Vehicle) const +{ + return m_state.kind == kind_type::crossing && m_closed && false == m_editorremoved && std::find(m_queue.begin(), m_queue.end(), Vehicle.MyTrack) != m_queue.end(); +} + +std::size_t roadpoint_node::waiting() const +{ + return static_cast(std::count_if(m_pool.begin(), m_pool.end(), [](pooled_vehicle const &Entry) { return false == Entry.vehicle->bEnabled && Entry.vehicle->MyTrack == nullptr; })); +} + +// to be called with each step of the simulation +void roadpoint_node::update(double const Deltatime) +{ + if (m_editorremoved) + { + return; + } + if (false == m_bound) + { + bind(); + } + switch (m_state.kind) + { + case kind_type::crossing: + { + update_crossing(Deltatime); + break; + } + case kind_type::spawn: + { + update_spawn(Deltatime); + break; + } + case kind_type::despawn: + { + update_despawn(); + break; + } + case kind_type::crosswalk: + { + // paint only + break; + } + } +} + +// closes the crossing while there's a rail vehicle coming, or standing right at it +void roadpoint_node::update_crossing(double const Deltatime) +{ + m_scan -= Deltatime; + m_hold -= Deltatime; + if (m_scan <= 0.0) + { + m_scan = scan_period; + std::map seen; + for (auto const *track : m_rails) + { + for (auto const *vehicle : track->Dynamics) + { + if (vehicle == nullptr) + { + continue; + } + auto const distance{planar_distance(vehicle->GetPosition(), m_state.position)}; + seen.emplace(vehicle, distance); + auto const before{m_seen.find(vehicle)}; + // a vehicle getting closer is on its way here; one which stands still or goes away isn't, unless it's at the crossing itself + if (distance < m_state.clearance + near_zone || (before != m_seen.end() && distance < before->second - 0.01)) + { + m_hold = hold_time; + } + } + } + m_seen.swap(seen); + } + if ((m_hold > 0.0) != m_closed) + { + close(m_hold > 0.0); + } +} + +// tells the drivers whether they can go +void roadpoint_node::close(bool const Closed) +{ + m_closed = Closed; + for (std::size_t idx = 0; idx < m_stops.size() && idx < m_events.size(); ++idx) + { + static_cast(m_events[idx])->m_velocity = (Closed ? 0.0 : -1.0); + } +} + +// puts a waiting vehicle on the road when its time comes +void roadpoint_node::update_spawn(double const Deltatime) +{ + if (false == m_prepared) + { + // the point was made in the editor, or the scenery was loaded for editing + prepare(); + } + // vehicles of the point which got stuck are taken off the road, to wait for their next turn: the ones standing still + // for too long, unless it's a closed level crossing they wait at, and the ones which started to go backwards + for (auto &entry : m_pool) + { + auto *vehicle{entry.vehicle}; + if (false == vehicle->bEnabled || vehicle->MyTrack == nullptr || (simulation::Train != nullptr && simulation::Train->Dynamic() == vehicle)) + { + // not on the road, or taken over by the user + entry.standing = 0.0; + continue; + } + auto const speed{vehicle->MoverParameters->V}; + // forward is the way it went when it was put on the road + auto const backwards{speed * entry.velocity < 0.0 && std::abs(speed) > reverse_speed}; + if (std::abs(speed) > stuck_speed || simulation::Roadpoints.holds(*vehicle)) + { + entry.standing = 0.0; + } + else + { + entry.standing += Deltatime; + } + if (backwards || entry.standing > stuck_time) + { + WriteLog("Road traffic: vehicle \"" + vehicle->name() + "\" taken off the road, " + (backwards ? "it was going backwards" : "it stood still for too long")); + entry.standing = 0.0; + vehicle->bEnabled = false; + TDynamicObject::bDynamicRemove = true; + } + } + if (m_pool.empty() || m_lanes.empty()) + { + return; + } + m_timer -= Deltatime; + if (m_timer > 0.0) + { + return; + } + // not with something in the way + auto const *lane{m_lanes.front()}; + for (auto const *track : {lane, static_cast(lane->trPrev), static_cast(lane->trNext)}) + { + if (track == nullptr) + { + continue; + } + for (auto const *other : track->Dynamics) + { + if (other != nullptr && planar_distance(other->GetPosition(), m_state.position) < clear_zone) + { + m_timer = retry_period; + return; + } + } + } + // one of the vehicles which wait, picked at random + std::vector ready; + for (auto const &entry : m_pool) + { + if (false == entry.vehicle->bEnabled && entry.vehicle->MyTrack == nullptr) + { + ready.emplace_back(&entry); + } + } + if (ready.empty()) + { + m_timer = retry_period; + return; + } + auto const &entry{*ready[std::min(ready.size() - 1, static_cast(LocalRandom(0.0, static_cast(ready.size()))))]}; + auto *vehicle{entry.vehicle}; + // it comes back the way it was made: whole, at speed, with the driver told to drive on + vehicle->bEnabled = true; + vehicle->MoverParameters->DamageFlag = 0; + vehicle->MoverParameters->EngDmgFlag = 0; + vehicle->MoverParameters->V = entry.velocity; + vehicle->place_on_track(m_lanes.front(), m_offset + 0.5 * vehicle->MoverParameters->Dim.L, false); + if (vehicle->Mechanik != nullptr) + { + // the same order a vehicle put in the scenery gets; it has the driver forget the road they knew + vehicle->Mechanik->PutCommand("Timetable:", m_state.velocity, 0, nullptr); + } + m_timer = m_state.interval * (1.0 + m_state.variation * LocalRandom(-1.0, 1.0)); +} + +// takes away the road vehicles which got to the point +void roadpoint_node::update_despawn() +{ + for (auto const *lane : m_lanes) + { + for (auto *vehicle : lane->Dynamics) + { + if (vehicle == nullptr || false == vehicle->bEnabled || vehicle->MoverParameters->CategoryFlag != 2) + { + continue; + } + if (simulation::Train != nullptr && simulation::Train->Dynamic() == vehicle) + { + // not the one the user sits in + continue; + } + if (planar_distance(vehicle->GetPosition(), m_state.position) > m_state.radius + 0.5 * vehicle->MoverParameters->Dim.L) + { + continue; + } + // the way vehicles reaching the end of the scenery go: marked here, taken off the road once all vehicles made their move + vehicle->bEnabled = false; + TDynamicObject::bDynamicRemove = true; + } + } +} + +// generates geometry of the stop lines +std::vector roadpoint_node::create_shapes() const +{ + std::vector shapes; + std::vector vertices; + glm::dvec3 const lift{0.0, line_lift, 0.0}; + glm::vec3 const upwards{0.f, 1.f, 0.f}; + if (m_state.kind == kind_type::crosswalk && m_onroad) + { + // stripes laid along the road, side by side from one edge of it to the other + glm::dvec3 across{m_along.z, 0.0, -m_along.x}; + if (glm::length2(across) > 1e-12) + { + across = glm::normalize(across); + auto const reach{m_halfwidth - stopline_inset}; + auto const stripes{std::max(1, static_cast(std::floor((2.0 * reach + stripe_width) / (2.0 * stripe_width))))}; + // the stripes are spread evenly over the width, with one at each edge + auto const first{-0.5 * (stripes * 2 - 1) * stripe_width}; + auto const back{-m_along * static_cast(m_state.length)}; + for (int stripe = 0; stripe < stripes; ++stripe) + { + auto const offset{first + stripe * 2.0 * stripe_width}; + auto const right{m_state.position + across * offset + m_along * (0.5 * m_state.length) + lift}; + auto const left{right + across * stripe_width}; + vertices.push_back({right + back, upwards, {0.f, 0.f}}); + vertices.push_back({right, upwards, {1.f, 0.f}}); + vertices.push_back({left + back, upwards, {0.f, 1.f}}); + vertices.push_back({right, upwards, {1.f, 0.f}}); + vertices.push_back({left, upwards, {1.f, 1.f}}); + vertices.push_back({left + back, upwards, {0.f, 1.f}}); + } + } + } + for (auto const &stop : m_stops) + { + if (m_state.kind != kind_type::crossing || false == m_state.stoplines) + { + break; + } + // a line across the lane, ending where the vehicles are to stop + glm::dvec3 across{stop.direction.z, 0.0, -stop.direction.x}; + if (glm::length2(across) < 1e-12) + { + continue; + } + across = glm::normalize(across); + glm::dvec3 back{-stop.direction.x, 0.0, -stop.direction.z}; + back = glm::normalize(back) * stopline_width; + auto const reach{std::max(0.25, 0.5 * stop.width - stopline_inset)}; + auto const right{stop.position - across * reach + lift}; + auto const left{stop.position + across * reach + lift}; + vertices.push_back({right + back, upwards, {0.f, 0.f}}); + vertices.push_back({right, upwards, {1.f, 0.f}}); + vertices.push_back({left + back, upwards, {0.f, 1.f}}); + vertices.push_back({right, upwards, {1.f, 0.f}}); + vertices.push_back({left, upwards, {1.f, 1.f}}); + vertices.push_back({left + back, upwards, {0.f, 1.f}}); + } + if (false == vertices.empty()) + { + lighting_data paint; + paint.diffuse = glm::vec4{0.92f, 0.92f, 0.92f, 1.f}; + paint.ambient = paint.diffuse; + scene::shape_node shape; + shape.make_terrain(GfxRenderer->Fetch_Material("colored"), std::move(vertices), glm::dvec3{0.0}); + shape.lighting(paint); + shapes.emplace_back(std::move(shape)); + } + return shapes; +} + +// puts geometry of the point in the scene as shapes of its own, which can be taken back +void roadpoint_node::show() +{ + if (m_shapes.shown() || m_editorremoved || simulation::Region == nullptr || Global.NvRenderer) + { + // NOTE: the experimental renderer draws only what the scenery had when it was loaded + return; + } + m_shapes.show(create_shapes(), m_state.position); +} + +void roadpoint_node::hide() +{ + m_shapes.hide(); +} + +// radius() subclass details, calculates node's bounding radius +float roadpoint_node::radius_() +{ + return m_state.kind == kind_type::crossing ? m_state.clearance + 5.f : m_state.kind == kind_type::crosswalk ? static_cast(m_halfwidth) + m_state.length : m_state.radius; +} + +// serialize() subclass details, sends content of the subclass to provided stream +void roadpoint_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +// deserialize() subclass details, restores content of the subclass from provided stream +void roadpoint_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +// export() subclass details, sends basic content of the class in legacy (text) format to provided stream +void roadpoint_node::export_as_text_(std::ostream &Output) const +{ + auto const type{keyword(m_state.kind)}; + Output << type << ' '; + auto const precision{Output.precision(std::numeric_limits::digits10)}; + Output << m_state.position.x << ' ' << m_state.position.y << ' ' << m_state.position.z << ' '; + Output.precision(precision); + switch (m_state.kind) + { + case kind_type::crossing: + { + Output << "clearance " << m_state.clearance << ' ' << "warning " << m_state.warning << ' ' << "stoplines " << (m_state.stoplines ? "yes" : "no") << ' '; + break; + } + case kind_type::spawn: + { + Output << "interval " << m_state.interval << ' ' << "variation " << m_state.variation << ' ' << "velocity " << m_state.velocity << ' ' << "count " << m_state.count << ' '; + for (auto const &vehicle : m_state.vehicles) + { + // the way vehicles are put in a scenery. the path and the offset are there to keep that form, the point doesn't use them + Output << "node -1 0 none dynamic " << vehicle.folder << ' ' << vehicle.skin << ' ' << vehicle.type << " none 0 headdriver " << m_state.velocity << ' ' << vehicle.load << ' '; + if (vehicle.load > 0 && false == vehicle.loadtype.empty()) + { + Output << vehicle.loadtype << ' '; + } + Output << "enddynamic "; + } + break; + } + case kind_type::despawn: + { + Output << "radius " << m_state.radius << ' '; + break; + } + case kind_type::crosswalk: + { + Output << "length " << m_state.length << ' '; + break; + } + } + Output << "end" << type << "\n"; +} + +// legacy style initialization, to be performed when the lanes of the roads are tied together +void roadpoint_table::InitRoadpoints() +{ + for (auto *point : m_items) + { + if (point == nullptr) + { + continue; + } + point->bind(); + if (false == Global.editor_session) + { + // the vehicles are made now, so they don't hold the simulation up later. a scenery opened for editing + // does without them until the simulation is run + point->prepare(); + } + } +} + +// lets go of the lanes and the rails, ahead of a change of the roads +void roadpoint_table::unbind() +{ + for (auto *point : m_items) + { + if (point != nullptr) + { + point->unbind(); + } + } +} + +// ties all points anew, after the roads were changed +void roadpoint_table::bind() +{ + for (auto *point : m_items) + { + if (point == nullptr) + { + continue; + } + // the stop lines go where the lanes are now + point->hide(); + point->bind(); + if (m_geometry) + { + point->show(); + } + } +} + +// puts geometry of the points in the scene +void roadpoint_table::create_geometry() +{ + m_geometry = true; + for (auto *point : m_items) + { + if (point != nullptr) + { + point->show(); + } + } +} + +// true if specified vehicle stands in the line of vehicles held by a closed level crossing +bool roadpoint_table::holds(TDynamicObject const &Vehicle) const +{ + return std::any_of(m_items.begin(), m_items.end(), [&Vehicle](roadpoint_node const *Point) { return Point != nullptr && Point->holds(Vehicle); }); +} + +// to be called with each step of the simulation +void roadpoint_table::update(double const Deltatime) +{ + for (auto *point : m_items) + { + if (point != nullptr) + { + point->update(Deltatime); + } + } +} + +//--------------------------------------------------------------------------- diff --git a/world/RoadPoint.h b/world/RoadPoint.h new file mode 100644 index 00000000..4b390ba6 --- /dev/null +++ b/world/RoadPoint.h @@ -0,0 +1,223 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include +#include + +#include + +#include "utilities/Classes.h" +#include "utilities/Names.h" +#include "scene/scenenode.h" +#include "world/Road.h" + +class TDynamicObject; +class basic_event; + +// a place on a road where something happens to the traffic, or is painted for it: +// - a level crossing: vehicles are stopped ahead of the rails for as long as a train is near, +// - a point where vehicles appear, drawn from a set made ready while the scenery is loaded, +// - a point where vehicles are taken away. the ones which came from a point of the previous kind go back to its set, +// - a pedestrian crossing: stripes painted across the road. +// scenery entries: +// node crossing [clearance ] [warning ] [stoplines ] endcrossing +// node spawn [interval ] [variation <0..1>] [velocity ] [count ] +// [node dynamic [] enddynamic]... endspawn +// node despawn [radius ] enddespawn +// node crosswalk [length ] endcrosswalk +// the vehicles of a spawn point are given the way vehicles are put in a scenery; where they're put and how they're driven +// is up to the point, so the path, the offset, the driver and the velocity of these entries don't matter. the listed vehicles +// are what the point draws from to have vehicles of its own on the roads; one which gets stuck is taken off the road +class roadpoint_node : public scene::basic_node +{ + + public: + // types + enum class kind_type + { + crossing, + spawn, + despawn, + crosswalk + }; + struct vehicle_data + { + std::string folder; // where the vehicle is defined, in the folder of the vehicles + std::string skin; + std::string type; // name of its definition files + int load{0}; + std::string loadtype; + + bool operator==(vehicle_data const &Other) const + { + return folder == Other.folder && skin == Other.skin && type == Other.type && load == Other.load && loadtype == Other.loadtype; + } + }; + struct state + { + kind_type kind{kind_type::crossing}; + glm::dvec3 position{0.0}; + // level crossing + float clearance{6.f}; // distance along the road from the point to the places the vehicles stop at + float warning{800.f}; // how far along the rails a train coming closes the crossing + bool stoplines{true}; // lines are painted where the vehicles stop + // point where vehicles appear + float interval{20.f}; // seconds from a vehicle to the next + float variation{0.5f}; // part of the interval which is drawn at random + float velocity{50.f}; // speed the vehicles appear with, and are told to keep, km/h + int count{6}; // how many vehicles of the point can be on the roads at a time. that many are made ready, drawn from the listed ones + std::vector vehicles; + // point where vehicles are taken away + float radius{2.f}; // vehicles on lanes this close to the point are taken + // pedestrian crossing + float length{4.f}; // how much of the road the stripes take along it + + // brings the content to a usable form + void normalize(); + }; + // place the vehicles of a lane stop at while a crossing is closed + struct stop_data + { + TTrack *track{nullptr}; // the lane it's on + glm::dvec3 position{0.0}; + glm::dvec3 direction{0.0, 0.0, 1.0}; // the way the lane goes there + float width{3.5f}; // of the lane + }; + // constructors + explicit roadpoint_node(scene::node_data const &Nodedata); + // methods + // restores content of the node from provided input stream; reads up to and including the closing statement + void import(cParser &Input, glm::dvec3 const &Offset); + state const &definition() const + { + return m_state; + } + // replaces what the node knows about itself. what it's tied to stays, it's up to the caller to bind it again. + // a spawn point given another set of vehicles makes them anew; the ones it had stay on the road until they're taken away + void define(state const &State); + // scenery keyword for specified kind of point, and the statement closing its entry + static std::string keyword(kind_type const Kind); + static bool is_keyword(std::string const &Type); + // reads vehicles out of a text. each is given the way vehicles are put in a scenery, or, if there's no such entry + // in the text, as " [ ]" on a line of its own + static std::vector parse_vehicles(std::string const &Text); + // ties the point to the lanes and the rails as they are at the moment, or lets go of them + void bind(); + void unbind(); + // makes the vehicles of a spawn point, once. it loads their models, which takes a while + void prepare(); + // to be called with each step of the simulation + void update(double const Deltatime); + // state for display + bool closed() const + { + return m_closed; + } + std::vector const &stops() const + { + return m_stops; + } + std::vector const &lanes() const + { + return m_lanes; + } + std::size_t rails() const + { + return m_rails.size(); + } + // vehicles made for a spawn point, and how many of them wait for their turn + std::size_t vehicles() const + { + return m_pool.size(); + } + std::size_t waiting() const; + // true if specified vehicle stands in the line of vehicles held by the crossing, which is closed + bool holds(TDynamicObject const &Vehicle) const; + // generates geometry of the stop lines + std::vector create_shapes() const; + // puts geometry of the point in the scene, or takes it back + void show(); + void hide(); + bool shown() const + { + return m_shapes.shown(); + } + // members + bool m_editorremoved{false}; // removed in the editor; kept around so the removal can be undone + + private: + // types + struct pooled_vehicle + { + TDynamicObject *vehicle{nullptr}; + double velocity{0.0}; // what its speed was when it was made, to be given back each time it's put on the road + double standing{0.0}; // how long it's been standing still on the road + }; + // methods + float radius_() override; + void serialize_(std::ostream &Output) const override; + void deserialize_(std::istream &Input) override; + void export_as_text_(std::ostream &Output) const override; + void bind_crossing(); + void update_crossing(double const Deltatime); + void update_spawn(double const Deltatime); + void update_despawn(); + void close(bool const Closed); + // members + state m_state; + bool m_bound{false}; + // level crossing + std::vector m_stops; + std::vector m_events; // orders for the drivers, one for each stop. these are never freed, as a driver can hold on to one + std::vector m_rails; // rails a train closes the crossing from + std::vector m_queue; // lanes the vehicles held by the crossing wait on + std::map m_seen; // rail vehicles near, with how far each was the last time + bool m_closed{false}; + double m_hold{0.0}; // time left for the crossing to stay closed after the last train was seen + double m_scan{0.0}; // time left to the next look at the rails + // spawn and despawn + std::vector m_lanes; // spawn: the lane the vehicles are put on. despawn: lanes the vehicles are taken from + double m_offset{0.0}; // spawn: distance of the point from the start of that lane + std::vector m_pool; + bool m_prepared{false}; + int m_made{0}; // number of vehicles made so far, for their names + double m_timer{0.0}; // time left to the next vehicle + // pedestrian crossing: the road at the point + bool m_onroad{false}; + glm::dvec3 m_along{0.0, 0.0, 1.0}; // the way the road goes + double m_halfwidth{0.0}; // of the road + owned_shapes m_shapes; +}; + +// collection of the road points present in the scene +class roadpoint_table : public basic_table +{ + + public: + // legacy style initialization, to be performed when the lanes of the roads are tied together + void InitRoadpoints(); + // lets go of the lanes and the rails, ahead of a change of the roads, and ties all points anew when it's done + void unbind(); + void bind(); + // puts geometry of the points in the scene + void create_geometry(); + // to be called with each step of the simulation + void update(double const Deltatime); + // true if specified vehicle stands in the line of vehicles held by a closed level crossing + bool holds(TDynamicObject const &Vehicle) const; + + private: + bool m_geometry{false}; // the scene is ready to take geometry of the points +}; + +//--------------------------------------------------------------------------- diff --git a/world/Sweep.cpp b/world/Sweep.cpp new file mode 100644 index 00000000..f036015d --- /dev/null +++ b/world/Sweep.cpp @@ -0,0 +1,1371 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#include "stdafx.h" +#include "world/Sweep.h" +#include "editor/editorGauge.hpp" + +#include "model/MdlMngr.h" +#include "model/Model3d.h" +#include "rendering/renderer.h" +#include "scene/scene.h" +#include "simulation/simulation.h" +#include "utilities/Globals.h" +#include "utilities/Logs.h" +#include "utilities/parser.h" +#include "vehicle/DynObj.h" +#include "world/Track.h" + +#include +#include + +#include +#include +#include +#include +#include + +namespace +{ + +double const sample_spacing{0.5}; // m along the curve between the samples +std::size_t const vertex_limit{4000000}; // copies beyond this many vertices are left out +double const slice_width{1.0}; // m of the model, the faces of a bent copy are cut into +double const edge_band{0.3}; // m from the side facing the curve, where the top of the edge is looked for +double const outer_reach{30.0}; +double const overhang_band{0.15}; +double const overhang_depth{0.05}; + +struct cubic +{ + glm::dvec3 p0, p1, p2, p3; + + explicit cubic(segment_data const &Piece) + { + p0 = Piece.points[segment_data::point::start]; + p3 = Piece.points[segment_data::point::end]; + auto const &control1{Piece.points[segment_data::point::control1]}; + auto const &control2{Piece.points[segment_data::point::control2]}; + if (control1 == glm::dvec3{} && control2 == glm::dvec3{}) + { + p1 = glm::mix(p0, p3, 1.0 / 3.0); + p2 = glm::mix(p0, p3, 2.0 / 3.0); + } + else + { + p1 = p0 + control1; + p2 = p3 + control2; + } + } + glm::dvec3 point(double const T) const + { + auto const u{1.0 - T}; + return u * u * u * p0 + 3.0 * u * u * T * p1 + 3.0 * u * T * T * p2 + T * T * T * p3; + } + glm::dvec3 first(double const T) const + { + auto const u{1.0 - T}; + return 3.0 * u * u * (p1 - p0) + 6.0 * u * T * (p2 - p1) + 3.0 * T * T * (p3 - p2); + } +}; + +double plan_distance(glm::dvec3 const &A, glm::dvec3 const &B) +{ + return glm::length(glm::dvec2{B.x - A.x, B.z - A.z}); +} + +// the gauge changes from 26 m ahead of a curve or of a cant ramp (PKP PLK ST-T2), so the tracks beyond the ends are looked at this far +double const lead_reach{26.0}; + +segment_data turned_piece(segment_data const &Path) +{ + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; +} + +bool same_points(segment_data const &A, segment_data const &B) +{ + for (int i = 0; i < 4; ++i) + if (glm::distance(A.points[i], B.points[i]) > 1e-3) + return false; + return true; +} + +glm::dvec3 plan_heading(segment_data const &Piece, double const T) +{ + auto const direction{cubic{Piece}.first(T)}; + glm::dvec3 const plan{direction.x, 0.0, direction.z}; + return glm::length(plan) > 1e-9 ? glm::normalize(plan) : glm::dvec3{0.0, 0.0, 1.0}; +} + +// paths going on from the end of the last of the pieces, each turned to lead away from it, up to the lead reach +std::vector paths_beyond(segment_data Last, std::vector const &Own) +{ + std::vector result; + double length{0.0}; + while (length < lead_reach && result.size() < 16) + { + auto const at{Last.points[segment_data::point::end]}; + if (simulation::Region == nullptr || false == simulation::Region->point_inside(at)) + break; + auto const &paths{simulation::Region->section(at).cell(at).m_directories.paths}; + std::optional next; + for (auto *track : paths) + { + if (track == nullptr || track->m_editorremoved || (track->iCategoryFlag & 1) == 0) + continue; + for (auto const &path : track->m_paths) + for (auto const forward : {true, false}) + { + auto const candidate{forward ? path : turned_piece(path)}; + if (glm::distance(candidate.points[segment_data::point::start], at) > 0.01 || glm::dot(plan_heading(Last, 1.0), plan_heading(candidate, 0.0)) < 0.9) + continue; + auto const known = [&](segment_data const &Piece) { return same_points(Piece, candidate) || same_points(turned_piece(Piece), candidate); }; + if (std::any_of(Own.begin(), Own.end(), known) || std::any_of(result.begin(), result.end(), known)) + continue; + auto const straight{glm::dot(plan_heading(candidate, 0.0), plan_heading(candidate, 1.0))}; + if (next.has_value() && straight <= glm::dot(plan_heading(*next, 0.0), plan_heading(*next, 1.0))) + continue; + next = candidate; + } + } + if (false == next.has_value()) + break; + cubic const curve{*next}; + for (int k = 1; k <= 8; ++k) + length += plan_distance(curve.point((k - 1) / 8.0), curve.point(k / 8.0)); + result.push_back(*next); + Last = *next; + } + return result; +} + +// triangles of the model which share the look, in the space of the model +struct model_part +{ + material_handle material{null_handle}; + lighting_data lighting; + bool translucent{false}; + std::vector vertices; +}; + +// without the parts lit at night (the glow, the pool of light on the ground), which the instances show only with the lights on +void gather_parts(TSubModel *Submodel, glm::dmat4 const &Transform, material_data const &Skins, std::array const &Lit, std::vector &Parts) +{ + for (auto *submodel = Submodel; submodel != nullptr; submodel = submodel->Next) + { + if (std::find(Lit.begin(), Lit.end(), submodel) != Lit.end()) + continue; + auto transform{Transform}; + if ((submodel->iFlags & 0xC000) != 0 && submodel->GetMatrix() != nullptr) + { + transform = Transform * glm::dmat4(glm::make_mat4(submodel->GetMatrix()->readArray())); + } + // the nearest level of detail only, the others would be drawn over it + if (submodel->eType == GL_TRIANGLES && submodel->fSquareMinDist <= 0.f && (submodel->m_geometry.handle.bank != 0 || submodel->m_geometry.handle.chunk != 0)) + { + auto material{submodel->m_material}; + if (material < 0) + { + material = Skins.replacable_skins[std::clamp(-material, 1, 4)]; + } + model_part part; + part.material = material; + part.lighting.ambient = submodel->f4Ambient; + part.lighting.diffuse = submodel->f4Diffuse; + part.lighting.specular = submodel->f4Specular; + part.translucent = material != null_handle && GfxRenderer->Material(material)->get_or_guess_opacity() == 0.0f; + auto const normals{glm::transpose(glm::inverse(glm::dmat3(transform)))}; + auto const &vertices{GfxRenderer->Vertices(submodel->m_geometry.handle)}; + auto const &indices{GfxRenderer->Indices(submodel->m_geometry.handle)}; + auto const take = [&](gfx::basic_vertex const &Vertex) { + auto vertex{Vertex.to_world()}; + vertex.position = glm::dvec3(transform * glm::dvec4(vertex.position, 1.0)); + auto const normal{normals * glm::dvec3(vertex.normal)}; + vertex.normal = glm::length(normal) > 1e-9 ? glm::vec3(glm::normalize(normal)) : vertex.normal; + part.vertices.push_back(vertex); + }; + if (false == indices.empty()) + { + for (std::size_t i = 0; i + 2 < indices.size(); i += 3) + { + take(vertices[indices[i]]); + take(vertices[indices[i + 1]]); + take(vertices[indices[i + 2]]); + } + } + else + { + for (std::size_t i = 0; i + 2 < vertices.size(); i += 3) + { + take(vertices[i]); + take(vertices[i + 1]); + take(vertices[i + 2]); + } + } + if (false == part.vertices.empty()) + { + Parts.push_back(std::move(part)); + } + } + if (submodel->Child != nullptr) + { + gather_parts(submodel->Child, transform, Skins, Lit, Parts); + } + } +} + +world_vertex blend(world_vertex const &A, world_vertex const &B, double const T) +{ + world_vertex result; + result.position = glm::mix(A.position, B.position, T); + auto const normal{glm::mix(A.normal, B.normal, static_cast(T))}; + result.normal = glm::length(normal) > 1e-6f ? glm::normalize(normal) : A.normal; + result.texture = glm::mix(A.texture, B.texture, static_cast(T)); + return result; +} + +// the part of the polygon on one side of the plane Along = Cut: Side 1 keeps what's beyond it, -1 what's before it +std::vector clip(std::vector const &Polygon, std::function const &Along, double const Cut, double const Side) +{ + std::vector result; + for (std::size_t i = 0; i < Polygon.size(); ++i) + { + auto const ¤t{Polygon[i]}; + auto const &next{Polygon[(i + 1) % Polygon.size()]}; + auto const a{Side * (Along(current.position) - Cut)}; + auto const b{Side * (Along(next.position) - Cut)}; + if (a >= 0.0) + result.push_back(current); + if ((a >= 0.0) != (b >= 0.0)) + result.push_back(blend(current, next, a / (a - b))); + } + return result; +} + +// triangles cut across the axis of the model into slices, so a long face bends with the curve instead of spanning it straight +std::vector slices(std::vector const &Vertices, std::function const &Along, double const Low, double const Width) +{ + std::vector result; + result.reserve(Vertices.size()); + for (std::size_t i = 0; i + 2 < Vertices.size(); i += 3) + { + auto const a0{Along(Vertices[i].position)}; + auto const a1{Along(Vertices[i + 1].position)}; + auto const a2{Along(Vertices[i + 2].position)}; + auto const first{static_cast(std::floor((std::min({a0, a1, a2}) - Low) / Width))}; + auto const last{static_cast(std::ceil((std::max({a0, a1, a2}) - Low) / Width)) - 1}; + if (last <= first) + { + result.insert(result.end(), Vertices.begin() + i, Vertices.begin() + i + 3); + continue; + } + std::vector const triangle{Vertices[i], Vertices[i + 1], Vertices[i + 2]}; + for (int slice = first; slice <= last; ++slice) + { + auto const polygon{clip(clip(triangle, Along, Low + slice * Width, 1.0), Along, Low + (slice + 1) * Width, -1.0)}; + for (std::size_t k = 1; k + 1 < polygon.size(); ++k) + { + result.push_back(polygon[0]); + result.push_back(polygon[k]); + result.push_back(polygon[k + 1]); + } + } + } + return result; +} + +struct laid_model +{ + std::vector parts; + glm::dvec3 origin{0.0}; + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{-std::numeric_limits::max()}; + bool bent{false}; +}; + +struct layout +{ + std::vector models; + glm::dvec3 low{std::numeric_limits::max()}; + glm::dvec3 high{-std::numeric_limits::max()}; + glm::dvec3 shift{0.0}; + bool edge_far{false}; +}; + +bool lay_out(sweep_node const &Sweep, layout &Layout) +{ + auto const &definition{Sweep.definition()}; + auto const scaling{glm::scale(glm::dmat4(1.0), definition.scale)}; + for (auto const &entry : Sweep.items()) + { + laid_model model; + auto const transform{scaling * entry.transform}; + gather_parts(entry.model->Root, transform, entry.skins, entry.model->instance_parts().lights_on, model.parts); + model.origin = glm::dvec3(transform[3]); + if (false == model.parts.empty()) + Layout.models.push_back(std::move(model)); + } + if (Layout.models.empty()) + return false; + auto &low{Layout.low}; + auto &high{Layout.high}; + for (auto &model : Layout.models) + { + for (auto const &part : model.parts) + for (auto const &vertex : part.vertices) + { + auto const local{Sweep.axes(vertex.position)}; + model.low = glm::min(model.low, local); + model.high = glm::max(model.high, local); + } + low = glm::min(low, model.low); + high = glm::max(high, model.high); + } + for (auto &model : Layout.models) + model.bent = definition.bend && (Layout.models.size() == 1 || model.high.x - model.low.x >= 0.5 * (high.x - low.x)); + if (std::any_of(Layout.models.begin(), Layout.models.end(), [](laid_model const &Model) { return Model.bent; })) + { + low = glm::dvec3{std::numeric_limits::max()}; + high = glm::dvec3{-std::numeric_limits::max()}; + for (auto const &model : Layout.models) + if (model.bent) + { + low = glm::min(low, model.low); + high = glm::max(high, model.high); + } + } + auto const nearside{definition.lateral >= 0.0 ? low.y : high.y}; + auto const farside{definition.lateral >= 0.0 ? high.y : low.y}; + double edgetop{-std::numeric_limits::max()}; + double nearbottom{std::numeric_limits::max()}, farbottom{std::numeric_limits::max()}; + for (auto const &model : Layout.models) + if (model.bent || false == definition.bend) + for (auto const &part : model.parts) + for (auto const &vertex : part.vertices) + { + auto const local{Sweep.axes(vertex.position)}; + if (std::abs(local.y - nearside) <= edge_band) + edgetop = std::max(edgetop, local.z); + if (std::abs(local.y - nearside) <= overhang_band) + nearbottom = std::min(nearbottom, local.z); + if (std::abs(local.y - farside) <= overhang_band) + farbottom = std::min(farbottom, local.z); + } + if (edgetop == -std::numeric_limits::max()) + edgetop = high.z; + Layout.edge_far = farbottom > nearbottom + overhang_depth; + auto const point{Sweep.axes(definition.scale * definition.point)}; + double const sides[]{0.0, -nearside, -farside, -0.5 * (low.y + high.y), -point.y}; + double const heights[]{0.0, -low.z, -high.z, -edgetop, -point.z}; + Layout.shift = glm::dvec3{0.0, sides[std::clamp(definition.side_anchor, 0, 4)], heights[std::clamp(definition.height_anchor, 0, 4)]}; + return true; +} + +} // namespace + +// height of the head of the rail on the side of the offset over the points of the track, less the height of the rail; 0 for no side +double sweep_node::rail_head(double const Roll, double const Lateral) +{ + auto const roll{glm::radians(Roll)}; + auto const raise{Global.bRollFix ? 0.75 * std::abs(std::sin(roll)) : 0.0}; + if (Lateral == 0.0) + return raise; + return raise - (Lateral > 0.0 ? 1.0 : -1.0) * 0.75 * std::sin(roll); +} + +bool sweep_node::state::operator==(state const &Other) const +{ + if (model != Other.model || skin != Other.skin || bend != Other.bend || step != Other.step || from != Other.from || to != Other.to || lateral != Other.lateral || height != Other.height || along_x != Other.along_x || + flip != Other.flip || mirror != Other.mirror || tilt != Other.tilt || face != Other.face || widen != Other.widen || platform != Other.platform || parameters != Other.parameters || side_anchor != Other.side_anchor || height_anchor != Other.height_anchor || point != Other.point || scale != Other.scale || pieces.size() != Other.pieces.size()) + { + return false; + } + for (std::size_t i = 0; i < pieces.size(); ++i) + { + if (pieces[i].points != Other.pieces[i].points || pieces[i].rolls != Other.pieces[i].rolls) + { + return false; + } + } + return true; +} + +sweep_node::sweep_node(scene::node_data const &Nodedata) : basic_node(Nodedata) {} + +void sweep_node::import(cParser &Input, glm::dvec3 const &Offset) +{ + m_state = state{}; + m_state.model = Input.getToken(); + m_state.skin = Input.getToken(); + auto token{Input.getToken()}; + while (false == token.empty() && token != "endsweep") + { + if (token == "mode") + { + m_state.bend = Input.getToken() != "repeat"; + } + else if (token == "step") + { + Input.getTokens(); + Input >> m_state.step; + } + else if (token == "range") + { + Input.getTokens(2); + Input >> m_state.from >> m_state.to; + } + else if (token == "offset") + { + Input.getTokens(2); + Input >> m_state.lateral >> m_state.height; + } + else if (token == "axis") + { + m_state.along_x = Input.getToken() == "x"; + } + else if (token == "flip") + { + m_state.flip = true; + } + else if (token == "mirror") + { + m_state.mirror = true; + } + else if (token == "tilt") + { + m_state.tilt = true; + } + else if (token == "widen") + { + m_state.widen = true; + } + else if (token == "platform") + { + m_state.platform = true; + } + else if (token == "noface") + { + m_state.face = false; + } + else if (token == "parameters") + { + int count{0}; + Input.getTokens(); + Input >> count; + m_state.parameters.clear(); + for (int i = 0; i < count; ++i) + m_state.parameters.push_back(Input.getToken(false)); + } + else if (token == "anchor") + { + auto const side{Input.getToken()}; + auto const height{Input.getToken()}; + m_state.side_anchor = side == "near" ? 1 : side == "far" ? 2 : side == "centre" ? 3 : side == "point" ? 4 : 0; + m_state.height_anchor = height == "bottom" ? 1 : height == "top" ? 2 : height == "edge" ? 3 : height == "point" ? 4 : 0; + } + else if (token == "point") + { + Input.getTokens(3); + Input >> m_state.point.x >> m_state.point.y >> m_state.point.z; + } + else if (token == "scale") + { + Input.getTokens(3); + Input >> m_state.scale.x >> m_state.scale.y >> m_state.scale.z; + } + else if (token == "piece") + { + segment_data piece; + piece.deserialize(Input, Offset); + m_state.pieces.push_back(piece); + } + else + { + ErrorLog("Bad sweep: unknown property \"" + token + "\" of \"" + m_name + "\" in file \"" + Input.Name() + "\""); + } + token = Input.getToken(); + } + rebuild_samples(); +} + +void sweep_node::define(state const &State) +{ + auto const shown{m_shapes.shown()}; + if (shown) + { + hide(); + } + m_state = State; + rebuild_samples(); + if (shown) + { + show(); + } +} + +void sweep_node::sample_piece(segment_data const &Piece, std::vector &Samples, double &Station) +{ + cubic const curve{Piece}; + auto const count{std::clamp(static_cast(plan_distance(curve.p0, curve.p3) / sample_spacing), 4, 4000)}; + for (int k = Samples.empty() ? 0 : 1; k <= count; ++k) + { + auto const t{static_cast(k) / count}; + auto const position{curve.point(t)}; + if (false == Samples.empty()) + { + Station += plan_distance(Samples.back().position, position); + } + auto tangent{curve.first(t)}; + tangent = glm::length(tangent) > 1e-9 ? glm::normalize(tangent) : glm::dvec3{0.0, 0.0, 1.0}; + Samples.push_back({Station, position, tangent, Piece.rolls[0] + (Piece.rolls[1] - Piece.rolls[0]) * t, {}, {}}); + } +} + +// the structure gauge widens ahead of the curves and of the cant, the same way the gauge check of the editor has it +void sweep_node::gauge_along(std::vector &Samples) +{ + if (Samples.size() < 3) + return; + std::vector chainage, curvature, cant; + for (std::size_t i = 0; i < Samples.size(); ++i) + { + auto const &before{Samples[i == 0 ? 0 : i - 1]}; + auto const &after{Samples[std::min(i + 1, Samples.size() - 1)]}; + glm::dvec2 const a{before.tangent.x, before.tangent.z}; + glm::dvec2 const b{after.tangent.x, after.tangent.z}; + auto const span{after.station - before.station}; + auto const turn{glm::length(a) > 1e-9 && glm::length(b) > 1e-9 ? std::atan2(a.x * b.y - a.y * b.x, glm::dot(a, b)) : 0.0}; + chainage.push_back(Samples[i].station); + curvature.push_back(span > 1e-9 ? turn / span : 0.0); + cant.push_back(1.5 * std::abs(std::sin(glm::radians(Samples[i].roll)))); + } + auto const sections{gauge::sections(gauge::kind::unified, chainage, curvature, cant)}; + for (std::size_t i = 0; i < Samples.size() && i < sections.size(); ++i) + { + Samples[i].widening = sections[i].lower; + Samples[i].cant = sections[i].cant; + } +} + +void sweep_node::beside_switches(std::vector &Samples) +{ + if (Samples.size() < 3) + return; + auto const side{m_state.lateral >= 0.0 ? 1 : 0}; + auto const sign{m_state.lateral >= 0.0 ? 1.0 : -1.0}; + auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; + auto const total = [&](sample const &Sample) { return Sample.widening[side] + std::max(0.0, Sample.cant[side]) * edgeheight / 1.5; }; + auto const plan = [](glm::dvec3 const &Vector) { + glm::dvec3 const flat{Vector.x, 0.0, Vector.z}; + return glm::length(flat) > 1e-9 ? glm::normalize(flat) : glm::dvec3{0.0, 0.0, 1.0}; + }; + glm::dvec3 low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + for (auto const &sample : Samples) + { + low = glm::min(low, sample.position); + high = glm::max(high, sample.position); + } + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved || track->eType != tt_Switch || track->m_paths.size() < 2) + continue; + auto const points{track->m_paths.front().points[segment_data::point::start]}; + if (points.x < low.x - 1.0 || points.x > high.x + 1.0 || points.z < low.z - 1.0 || points.z > high.z + 1.0) + continue; + std::size_t at{0}; + auto best{std::numeric_limits::max()}; + for (std::size_t i = 0; i < Samples.size(); ++i) + if (auto const distance{plan_distance(Samples[i].position, points)}; distance < best) + { + best = distance; + at = i; + } + if (best > sample_spacing) + continue; + auto const &closest_sample{Samples[at]}; + auto const along{plan(closest_sample.tangent)}; + glm::dvec3 const toward{points.x - closest_sample.position.x, 0.0, points.z - closest_sample.position.z}; + if (std::abs(glm::dot(toward, glm::dvec3{-along.z, 0.0, along.x})) > 0.05) + continue; + auto const s0{closest_sample.station + glm::dot(toward, along)}; + for (auto const &path : track->m_paths) + { + auto const heading{glm::dot(plan_heading(path, 0.0), along)}; + if (std::abs(heading) < 0.9) + continue; + auto const beyond{paths_beyond(path, {path})}; + std::vector chain; + double station{0.0}; + if (heading > 0.0) + { + for (auto const &sample : Samples) + if (sample.station >= s0 - outer_reach && sample.station <= s0 + 1e-3) + { + station = sample.station; + chain.push_back(sample); + } + sample_piece(path, chain, station); + for (auto const &piece : beyond) + sample_piece(piece, chain, station); + } + else + { + for (auto piece = beyond.rbegin(); piece != beyond.rend(); ++piece) + sample_piece(turned_piece(*piece), chain, station); + sample_piece(turned_piece(path), chain, station); + auto const shift{station - s0}; + for (auto const &sample : Samples) + if (sample.station > s0 + 1e-3 && sample.station <= s0 + outer_reach) + { + chain.push_back(sample); + chain.back().station += shift; + } + } + gauge_along(chain); + for (auto &here : Samples) + { + if (plan_distance(here.position, points) > outer_reach + 2.0 * lead_reach + 100.0) + continue; + auto const forward{plan(here.tangent)}; + sample const *nearest{nullptr}; + auto closest{std::numeric_limits::max()}; + for (auto const &point : chain) + { + auto const gap{std::abs(glm::dot(glm::dvec3{point.position.x - here.position.x, 0.0, point.position.z - here.position.z}, forward))}; + if (gap < closest) + { + closest = gap; + nearest = &point; + } + } + if (nearest == nullptr || closest > 1.5 * sample_spacing) + continue; + auto const offset{sign * glm::dot(glm::dvec3{nearest->position.x - here.position.x, 0.0, nearest->position.z - here.position.z}, glm::dvec3{-forward.z, 0.0, forward.x})}; + if (offset > 0.05) + continue; + auto const needed{offset + total(*nearest)}; + if (needed > total(here)) + here.widening[side] = needed - std::max(0.0, here.cant[side]) * edgeheight / 1.5; + } + } + } +} + +bool sweep_node::bridge() +{ + auto const turned = [](segment_data const &Path) { + segment_data result{Path}; + result.points[segment_data::point::start] = Path.points[segment_data::point::end]; + result.points[segment_data::point::end] = Path.points[segment_data::point::start]; + result.points[segment_data::point::control1] = Path.points[segment_data::point::control2]; + result.points[segment_data::point::control2] = Path.points[segment_data::point::control1]; + result.rolls = {-Path.rolls[1], -Path.rolls[0]}; + return result; + }; + auto const same = [](segment_data const &A, segment_data const &B) { + for (int i = 0; i < 4; ++i) + if (glm::distance(A.points[i], B.points[i]) > 1e-3) + return false; + return true; + }; + auto const length = [](segment_data const &Piece) { + std::vector samples; + double station{0.0}; + sample_piece(Piece, samples, station); + return station; + }; + auto const heading = [](segment_data const &Piece, double const T) { + auto const direction{cubic{Piece}.first(T)}; + glm::dvec3 const plan{direction.x, 0.0, direction.z}; + return glm::length(plan) > 1e-9 ? glm::normalize(plan) : glm::dvec3{0.0, 0.0, 1.0}; + }; + auto state{m_state}; + auto changed{false}; + double station{0.0}; + for (std::size_t i = 0; i + 1 < state.pieces.size(); ++i) + { + station += length(state.pieces[i]); + auto const target{state.pieces[i + 1].points[segment_data::point::start]}; + auto at{state.pieces[i].points[segment_data::point::end]}; + if (glm::distance(at, target) < 0.01) + continue; + auto const chord{plan_distance(at, target)}; + std::vector fill; + auto previous{state.pieces[i]}; + auto reached{false}; + for (int step = 0; step < 20 && false == reached; ++step) + { + std::optional best; + auto bestdistance{std::numeric_limits::max()}; + for (auto *track : simulation::Paths.sequence()) + { + if (track == nullptr || track->m_editorremoved) + continue; + for (auto const &path : track->m_paths) + for (auto const forward : {true, false}) + { + auto const candidate{forward ? path : turned(path)}; + if (glm::distance(candidate.points[segment_data::point::start], at) > 0.01 || same(candidate, previous) || same(turned(candidate), previous)) + continue; + if (glm::dot(heading(previous, 1.0), heading(candidate, 0.0)) < 0.5) + continue; + auto const distance{glm::distance(candidate.points[segment_data::point::end], target)}; + if (distance < bestdistance) + { + bestdistance = distance; + best = candidate; + } + } + } + if (false == best.has_value()) + break; + fill.push_back(*best); + previous = *best; + at = best->points[segment_data::point::end]; + reached = glm::distance(at, target) < 0.01; + } + if (false == reached) + continue; + double added{0.0}; + for (auto const &piece : fill) + added += length(piece); + auto const shift{added - chord}; + if (state.from >= station) + state.from += shift; + if (state.to >= station) + state.to += shift; + state.pieces.insert(state.pieces.begin() + i + 1, fill.begin(), fill.end()); + station += added; + i += fill.size(); + changed = true; + } + if (changed) + { + WriteLog("Sweep \"" + m_name + "\": the gaps between its pieces filled with the paths which join them"); + define(state); + } + return changed; +} + +void sweep_node::refresh() +{ + auto const current{m_state}; + define(current); +} + +void sweep_node::rebuild_samples() +{ + m_boundsvalid = false; + m_samples.clear(); + std::vector before, after; + if ((m_state.widen || m_state.platform) && false == m_state.pieces.empty()) + { + before = paths_beyond(turned_piece(m_state.pieces.front()), m_state.pieces); + after = paths_beyond(m_state.pieces.back(), m_state.pieces); + } + double station{0.0}; + for (auto piece = before.rbegin(); piece != before.rend(); ++piece) + sample_piece(turned_piece(*piece), m_samples, station); + auto const head{station}; + auto const first{m_samples.empty() ? std::size_t{0} : m_samples.size() - 1}; + for (auto const &piece : m_state.pieces) + sample_piece(piece, m_samples, station); + auto const last{m_samples.size()}; + for (auto const &piece : after) + sample_piece(piece, m_samples, station); + gauge_along(m_samples); + if (m_state.widen || m_state.platform) + beside_switches(m_samples); + // the paths beyond the ends only lead the gauge in, the copies stay on the pieces + m_samples.erase(m_samples.begin() + last, m_samples.end()); + m_samples.erase(m_samples.begin(), m_samples.begin() + first); + for (auto &sample : m_samples) + sample.station -= head; + m_distances.clear(); + glm::dvec3 previous{0.0}; + for (auto const &sample : m_samples) + { + frame at; + at.widening = sample.widening; + at.cant = sample.cant; + glm::dvec3 left{-sample.tangent.z, 0.0, sample.tangent.x}; + left = glm::length(left) > 1e-9 ? glm::normalize(left) : glm::dvec3{-1.0, 0.0, 0.0}; + auto const point{sample.position + left * (m_state.lateral + setback(at))}; + m_distances.push_back(m_distances.empty() ? 0.0 : m_distances.back() + std::max(1e-6, plan_distance(previous, point))); + previous = point; + } + if (false == m_samples.empty()) + { + location(frame_at(0.5 * length()).position); + } + m_area.radius = -1.f; +} + +double sweep_node::length() const +{ + return m_samples.empty() ? 0.0 : m_samples.back().station; +} + +double sweep_node::start() const +{ + return m_state.from; +} + +double sweep_node::end() const +{ + return m_state.to < 0.0 ? length() : std::max(m_state.to, start()); +} + +double sweep_node::distance_at(double const Station) const +{ + if (m_samples.size() < 2) + return Station; + if (Station <= m_samples.front().station) + return m_distances.front() + Station - m_samples.front().station; + if (Station >= m_samples.back().station) + return m_distances.back() + Station - m_samples.back().station; + auto const after{static_cast(std::upper_bound(m_samples.begin(), m_samples.end(), Station, [](double const Value, sample const &Sample) { return Value < Sample.station; }) - m_samples.begin())}; + auto const before{after - 1}; + auto const span{m_samples[after].station - m_samples[before].station}; + auto const f{span > 1e-9 ? (Station - m_samples[before].station) / span : 0.0}; + return m_distances[before] + (m_distances[after] - m_distances[before]) * f; +} + +double sweep_node::station_at(double const Distance) const +{ + if (m_samples.size() < 2) + return Distance; + if (Distance <= m_distances.front()) + return m_samples.front().station + Distance - m_distances.front(); + if (Distance >= m_distances.back()) + return m_samples.back().station + Distance - m_distances.back(); + auto const after{static_cast(std::upper_bound(m_distances.begin(), m_distances.end(), Distance) - m_distances.begin())}; + auto const before{after - 1}; + auto const span{m_distances[after] - m_distances[before]}; + auto const f{span > 1e-12 ? (Distance - m_distances[before]) / span : 0.0}; + return m_samples[before].station + (m_samples[after].station - m_samples[before].station) * f; +} + +double sweep_node::setback(frame const &At) const +{ + if (false == m_state.widen && false == m_state.platform) + return 0.0; + auto const side{m_state.lateral >= 0.0 ? 1 : 0}; + auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; + auto const widening{At.widening[side] + std::max(0.0, At.cant[side]) * edgeheight / 1.5}; + return m_state.lateral >= 0.0 ? widening : -widening; +} + +double sweep_node::rise(frame const &At) const +{ + if (m_state.tilt) + return rail_head(At.roll, 0.0); + if (false == m_state.platform) + return rail_head(At.roll, m_state.lateral); + // PKP PLK ST-T2 table 3: H = Hi + D/2 -+ D (XB + dbS) / 1500 over the lower rail, with the cant D of the gauge, which + // changes from 20 m (inner side) and 26 m (outer side) ahead of the cant ramp, as the platform has to. taken as the + // turn of the edge with the plane of the rail heads, as the table has it only to the first order, and on the outer + // side, where the edge stays at XB + dbS instead of XB + dbS - dbD, the plane is higher by dbD D / 1500 there + auto const side{m_state.lateral >= 0.0 ? 1 : 0}; + auto const cant{At.cant[side]}; + auto const lower{rail_head(At.roll, 0.0) - 0.75 * std::abs(std::sin(glm::radians(At.roll)))}; + auto const edgeheight{std::max(0.0, m_state.height - gauge::rail_height)}; + auto const slope{std::clamp(cant / 1.5, -0.5, 0.5)}; + auto const level{std::sqrt(1.0 - slope * slope)}; + auto const across{std::abs(m_state.lateral) + std::abs(setback(At))}; + return lower + 0.5 * std::abs(cant) - (across - edgeheight * slope) * slope / level + edgeheight * (level - 1.0); +} + +glm::dvec3 sweep_node::axes(glm::dvec3 const &Vector) const +{ + auto const &definition{m_state}; + auto const needsflip{definition.along_x ? definition.lateral < 0.0 : definition.lateral > 0.0}; + auto const flip{definition.flip != (definition.face && needsflip)}; + glm::dvec3 result{definition.along_x ? Vector.x : Vector.z, definition.along_x ? Vector.z : -Vector.x, Vector.y}; + if (flip) + { + result.x = -result.x; + result.y = -result.y; + } + if (definition.mirror) + { + result.y = -result.y; + } + return result; +} + +bool sweep_node::bounds(glm::dvec3 &Low, glm::dvec3 &High, glm::dvec3 &Shift) const +{ + if (false == m_boundsvalid) + { + layout laid; + m_boundsfound = lay_out(*this, laid); + m_low = laid.low; + m_high = laid.high; + m_shift = laid.shift; + m_edgefar = laid.edge_far; + m_boundsvalid = true; + } + Low = m_low; + High = m_high; + Shift = m_shift; + return m_boundsfound; +} + +bool sweep_node::edge_far() const +{ + glm::dvec3 low, high, shift; + return bounds(low, high, shift) && m_edgefar; +} + +sweep_node::frame sweep_node::frame_at(double const Station) const +{ + frame result; + if (m_samples.empty()) + { + return result; + } + auto const found{std::upper_bound(m_samples.begin(), m_samples.end(), Station, [](double const Value, sample const &Sample) { return Value < Sample.station; })}; + auto const after{found == m_samples.end() ? std::prev(found) : found}; + auto const before{after == m_samples.begin() ? after : std::prev(after)}; + auto const span{after->station - before->station}; + auto const f{span > 1e-9 ? std::clamp((Station - before->station) / span, 0.0, 1.0) : 0.0}; + result.position = glm::mix(before->position, after->position, f); + // beyond the ends the curve goes on straight + if (Station < m_samples.front().station) + { + result.position = m_samples.front().position + m_samples.front().tangent * (Station - m_samples.front().station); + } + else if (Station > m_samples.back().station) + { + result.position = m_samples.back().position + m_samples.back().tangent * (Station - m_samples.back().station); + } + auto forward{glm::mix(before->tangent, after->tangent, f)}; + forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0}; + glm::dvec3 left{-forward.z, 0.0, forward.x}; + left = glm::length(left) > 1e-9 ? glm::normalize(left) : glm::dvec3{-1.0, 0.0, 0.0}; + glm::dvec3 up{glm::normalize(glm::cross(left, forward))}; + result.roll = before->roll + (after->roll - before->roll) * f; + for (int side = 0; side < 2; ++side) + { + result.widening[side] = before->widening[side] + (after->widening[side] - before->widening[side]) * f; + result.cant[side] = before->cant[side] + (after->cant[side] - before->cant[side]) * f; + } + if (m_state.tilt) + { + auto const roll{glm::radians(result.roll)}; + auto const turnedleft{left * std::cos(roll) - up * std::sin(roll)}; + up = up * std::cos(roll) + left * std::sin(roll); + left = turnedleft; + } + result.forward = forward; + result.left = left; + result.up = up; + return result; +} + +// models a template is made of, with their placement in the space of the template +std::vector sweep_node::items() const +{ + std::vector result; + auto const &definition{m_state}; + auto const ends = [](std::string const &Name, std::string const &Extension) { return Name.size() >= Extension.size() && Name.compare(Name.size() - Extension.size(), Extension.size(), Extension) == 0; }; + auto name{definition.model}; + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char const Character) { return static_cast(std::tolower(Character)); }); + if (false == ends(name, ".inc")) + { + item single; + single.model = TModelsManager::GetModel(definition.model, false, false); + if (false == definition.skin.empty() && definition.skin != "none") + single.skins.assign(definition.skin); + if (single.model != nullptr) + result.push_back(single); + return result; + } + cParser parser(definition.model, cParser::buffer_FILE, Global.asCurrentSceneryPath, Global.bLoadTraction, definition.parameters); + std::vector offsets{glm::dvec3{0.0}}; + glm::dvec3 rotation{0.0}; + auto token{parser.getToken()}; + while (false == token.empty()) + { + if (token == "origin") + { + glm::dvec3 offset; + parser.getTokens(3); + parser >> offset.x >> offset.y >> offset.z; + offsets.push_back(offsets.back() + offset); + } + else if (token == "endorigin") + { + if (offsets.size() > 1) + offsets.pop_back(); + } + else if (token == "rotate") + { + parser.getTokens(3); + parser >> rotation.x >> rotation.y >> rotation.z; + } + else if (token == "node") + { + double rangemax, rangemin; + std::string nodename, type; + parser.getTokens(4); + parser >> rangemax >> rangemin >> nodename >> type; + if (type == "model") + { + glm::dvec3 location; + double yaw; + parser.getTokens(4); + parser >> location.x >> location.y >> location.z >> yaw; + auto const model{parser.getToken()}; + auto const skin{parser.getToken()}; + auto rest{parser.getToken()}; + while (false == rest.empty() && rest != "endmodel") + rest = parser.getToken(); + item entry; + entry.model = TModelsManager::GetModel(model, false, false); + if (skin != "none" && false == skin.empty() && skin.front() != '*') + entry.skins.assign(skin); + // placed the way the scenery places a model in an origin turned by a rotate statement + auto const place{glm::rotateY(location, glm::radians(rotation.y)) + offsets.back()}; + glm::dmat4 transform{glm::translate(glm::dmat4(1.0), place)}; + transform = glm::rotate(transform, glm::radians(rotation.y + yaw), glm::dvec3{0.0, 1.0, 0.0}); + transform = glm::rotate(transform, glm::radians(rotation.x), glm::dvec3{1.0, 0.0, 0.0}); + transform = glm::rotate(transform, glm::radians(rotation.z), glm::dvec3{0.0, 0.0, 1.0}); + entry.transform = transform; + if (entry.model != nullptr && rangemin >= 0.0) + result.push_back(entry); + } + else + { + // whatever else the template defines isn't laid along the curve + auto rest{parser.getToken()}; + while (false == rest.empty() && rest != "end" + type && rest != "end" && rest != "endtri" && rest != "endline") + rest = parser.getToken(); + } + } + token = parser.getToken(); + } + return result; +} + +double sweep_node::project(glm::dvec3 const &Point, double &Lateral) const +{ + double best{std::numeric_limits::max()}; + double station{0.0}; + Lateral = 0.0; + glm::dvec2 const point{Point.x, Point.z}; + for (std::size_t i = 1; i < m_samples.size(); ++i) + { + glm::dvec2 const a{m_samples[i - 1].position.x, m_samples[i - 1].position.z}; + glm::dvec2 const b{m_samples[i].position.x, m_samples[i].position.z}; + auto const ab{b - a}; + auto const length2{glm::dot(ab, ab)}; + if (length2 < 1e-12) + continue; + auto const t{std::clamp(glm::dot(point - a, ab) / length2, 0.0, 1.0)}; + auto const distance{glm::length(point - (a + ab * t))}; + if (distance < best) + { + best = distance; + station = m_samples[i - 1].station + (m_samples[i].station - m_samples[i - 1].station) * t; + auto const side{ab.x * (point - a).y - ab.y * (point - a).x}; + Lateral = side >= 0.0 ? distance : -distance; + } + } + return station; +} + +double sweep_node::model_length() const +{ + auto const pieces{items()}; + auto const scaling{glm::scale(glm::dmat4(1.0), m_state.scale)}; + double low{std::numeric_limits::max()}, high{-std::numeric_limits::max()}; + for (auto const &entry : pieces) + { + std::vector parts; + gather_parts(entry.model->Root, scaling * entry.transform, entry.skins, entry.model->instance_parts().lights_on, parts); + for (auto const &part : parts) + for (auto const &vertex : part.vertices) + { + auto const along{m_state.along_x ? vertex.position.x : vertex.position.z}; + low = std::min(low, along); + high = std::max(high, along); + } + } + return high > low ? high - low : 0.0; +} + +std::vector sweep_node::create_shapes() const +{ + std::vector shapes; + auto const &definition{m_state}; + if (m_samples.size() < 2) + return shapes; + layout laid; + if (false == lay_out(*this, laid)) + return shapes; + auto &pieces{laid.models}; + auto const low{laid.low}; + auto const high{laid.high}; + auto const anchor_shift{laid.shift}; + auto const own{high.x - low.x}; + auto const first{distance_at(start())}; + auto const stretch{distance_at(end()) - first}; + if (own < 1e-3 || stretch < 1e-3) + return shapes; + if (definition.bend) + { + auto const along = [&](glm::dvec3 const &Position) { return axes(Position).x; }; + for (auto &model : pieces) + if (model.bent) + for (auto &part : model.parts) + part.vertices = slices(part.vertices, along, low.x, slice_width); + } + auto const copy_length{definition.step > 0.01 ? definition.step : own}; + // copies end to end fill the stretch exactly, each one squeezed or stretched a bit; repeated ones go at even steps + auto const copies{definition.bend ? std::max(1, static_cast(std::round(stretch / copy_length))) : static_cast(std::floor(stretch / copy_length + 1e-6)) + 1}; + auto const spacing{definition.bend ? stretch / copies : copy_length}; + auto const scale{definition.bend ? spacing / own : 1.0}; + std::size_t percopy{0}; + for (auto const &model : pieces) + for (auto const &part : model.parts) + percopy += part.vertices.size(); + auto const limit{std::max(1, vertex_limit / std::max(1, percopy))}; + if (static_cast(copies) > limit) + ErrorLog("Bad sweep: \"" + m_name + "\" would make " + std::to_string(copies) + " copies of \"" + definition.model + "\", only " + std::to_string(limit) + " are made"); + auto const count{std::min(copies, limit)}; + struct batch + { + material_handle material; + lighting_data lighting; + bool translucent; + std::vector vertices; + }; + std::vector batches; + auto const target = [&](model_part const &Part) -> std::vector & { + for (auto &entry : batches) + if (entry.material == Part.material && entry.lighting == Part.lighting && entry.translucent == Part.translucent) + return entry.vertices; + batches.push_back({Part.material, Part.lighting, Part.translucent, {}}); + return batches.back().vertices; + }; + // heights are given from the head of the rail on the side of the copies. the cant turns the track about its inner rail when the + // simulation raises it for the cant, about the axis otherwise; a copy which leans with the cant goes up with the axis + auto const cone = [&](frame const &At) { return rise(At); }; + auto const upright = [](frame At) { + glm::dvec3 forward{At.forward.x, 0.0, At.forward.z}; + forward = glm::length(forward) > 1e-9 ? glm::normalize(forward) : glm::dvec3{0.0, 0.0, 1.0}; + At.forward = forward; + At.left = glm::dvec3{-forward.z, 0.0, forward.x}; + At.up = glm::dvec3{0.0, 1.0, 0.0}; + return At; + }; + glm::dvec3 const offset{0.0, definition.height, 0.0}; + for (std::size_t copy = 0; copy < count; ++copy) + { + auto const base{first + spacing * static_cast(copy)}; + for (auto const &model : pieces) + { + // a model which doesn't bend stands upright at its point: the place of its origin along the curve + auto const anchor{axes(model.origin) + anchor_shift}; + auto const stand{upright(frame_at(station_at(definition.bend ? base + (anchor.x - low.x) * scale : base)))}; + for (auto const &part : model.parts) + { + auto &vertices{target(part)}; + auto const first{vertices.size()}; + for (auto const &vertex : part.vertices) + { + auto const local{axes(vertex.position) + anchor_shift}; + auto const direction{axes(glm::dvec3(vertex.normal))}; + world_vertex placed{vertex}; + if (model.bent) + { + auto const at{frame_at(station_at(base + (local.x - low.x) * scale))}; + placed.position = at.position + at.left * (local.y + definition.lateral + setback(at)) + at.up * local.z + offset + glm::dvec3{0.0, cone(at), 0.0}; + placed.normal = glm::vec3(glm::normalize(at.forward * direction.x + at.left * direction.y + at.up * direction.z)); + } + else + { + auto const along{definition.bend ? local.x - anchor.x : local.x}; + placed.position = stand.position + stand.forward * along + stand.left * (local.y + definition.lateral + setback(stand)) + stand.up * local.z + offset + glm::dvec3{0.0, cone(stand), 0.0}; + placed.normal = glm::vec3(glm::normalize(stand.forward * direction.x + stand.left * direction.y + stand.up * direction.z)); + } + vertices.push_back(placed); + } + if (definition.mirror) + { + // a mirror image turns the triangles inside out + for (auto i = first; i + 2 < vertices.size(); i += 3) + std::swap(vertices[i + 1], vertices[i + 2]); + } + } + } + } + for (auto &entry : batches) + { + if (entry.vertices.empty()) + continue; + scene::shape_node shape; + shape.make_terrain(entry.material, std::move(entry.vertices), glm::dvec3{0.0}); + shape.lighting(entry.lighting); + shape.translucent(entry.translucent); + shapes.emplace_back(std::move(shape)); + } + return shapes; +} + +void sweep_node::show() +{ + if (m_merged || m_shapes.shown() || m_editorremoved) + { + return; + } + m_shapes.show(create_shapes(), location()); +} + +void sweep_node::hide() +{ + m_shapes.hide(); +} + +float sweep_node::radius_() +{ + return static_cast(0.5 * length() + std::abs(m_state.lateral) + 25.0); +} + +void sweep_node::serialize_(std::ostream &Output) const +{ + // TODO: implement +} + +void sweep_node::deserialize_(std::istream &Input) +{ + // TODO: implement +} + +void sweep_node::export_as_text_(std::ostream &Output) const +{ + auto const &sweep{m_state}; + Output << "sweep " << sweep.model << ' ' << (sweep.skin.empty() ? std::string{"none"} : sweep.skin) << ' '; + Output << "mode " << (sweep.bend ? "bend" : "repeat") << ' '; + if (sweep.step > 0.0) + { + Output << "step " << sweep.step << ' '; + } + if (sweep.from != 0.0 || sweep.to >= 0.0) + { + Output << "range " << sweep.from << ' ' << sweep.to << ' '; + } + if (sweep.lateral != 0.0 || sweep.height != 0.0) + { + Output << "offset " << sweep.lateral << ' ' << sweep.height << ' '; + } + if (sweep.along_x) + { + Output << "axis x "; + } + if (sweep.flip) + { + Output << "flip "; + } + if (sweep.mirror) + { + Output << "mirror "; + } + if (sweep.tilt) + { + Output << "tilt "; + } + if (false == sweep.face) + { + Output << "noface "; + } + if (sweep.widen) + { + Output << "widen "; + } + if (sweep.platform) + { + Output << "platform "; + } + if (false == sweep.parameters.empty()) + { + Output << "parameters " << sweep.parameters.size() << ' '; + for (auto const ¶meter : sweep.parameters) + Output << (parameter.empty() ? std::string{"none"} : parameter) << ' '; + } + if (sweep.side_anchor != 0 || sweep.height_anchor != 0) + { + char const *sides[] = {"origin", "near", "far", "centre", "point"}; + char const *heights[] = {"origin", "bottom", "top", "edge", "point"}; + Output << "anchor " << sides[std::clamp(sweep.side_anchor, 0, 4)] << ' ' << heights[std::clamp(sweep.height_anchor, 0, 4)] << ' '; + } + if (sweep.side_anchor == 4 || sweep.height_anchor == 4) + { + Output << "point " << sweep.point.x << ' ' << sweep.point.y << ' ' << sweep.point.z << ' '; + } + if (sweep.scale != glm::dvec3{1.0}) + { + Output << "scale " << sweep.scale.x << ' ' << sweep.scale.y << ' ' << sweep.scale.z << ' '; + } + auto const precision{Output.precision(std::numeric_limits::digits10)}; + for (auto const &piece : sweep.pieces) + { + auto const &start{piece.points[segment_data::point::start]}; + auto const &control1{piece.points[segment_data::point::control1]}; + auto const &control2{piece.points[segment_data::point::control2]}; + auto const &end{piece.points[segment_data::point::end]}; + Output << "\npiece " << start.x << ' ' << start.y << ' ' << start.z << ' ' << piece.rolls[0] << ' ' << control1.x << ' ' << control1.y << ' ' << control1.z << ' ' << control2.x << ' ' << control2.y << ' ' << control2.z << ' ' + << end.x << ' ' << end.y << ' ' << end.z << ' ' << piece.rolls[1] << ' ' << piece.radius; + } + Output.precision(precision); + Output << "\nendsweep\n"; +} + +void sweep_table::create_geometry(scene::scratch_data &Scratchpad) +{ + if (simulation::Region == nullptr) + { + return; + } + for (auto *sweep : m_items) + { + if (sweep == nullptr) + { + continue; + } + if (sweep->bridge()) + { + if (Global.editor_session) + sweep->mark_dirty(); + } + else if (sweep->definition().widen || sweep->definition().platform) + { + sweep->refresh(); + } + if (Global.editor_session && false == Global.NvRenderer) + { + // scenery opened for editing: the copies are kept apart, so they can be changed later + sweep->show(); + continue; + } + for (auto &shape : sweep->create_shapes()) + { + simulation::Region->insert(shape, Scratchpad, false); + } + sweep->merged(true); + } +} diff --git a/world/Sweep.h b/world/Sweep.h new file mode 100644 index 00000000..c1a59581 --- /dev/null +++ b/world/Sweep.h @@ -0,0 +1,183 @@ +/* +This Source Code Form is subject to the +terms of the Mozilla Public License, v. +2.0. If a copy of the MPL was not +distributed with this file, You can +obtain one at +http://mozilla.org/MPL/2.0/. +*/ + +#pragma once + +#include +#include +#include + +#include + +#include "utilities/Classes.h" +#include "utilities/Names.h" +#include "scene/scenenode.h" +#include "world/Road.h" +#include "world/Segment.h" +#include "vehicle/DynObj.h" + +namespace scene +{ +struct scratch_data; +} + +// a model laid along a curve: copies of it bent to follow the curve end to end, or set along it at even steps. +// the node isn't drawn itself, it makes ordinary shapes of the copies, so the renderers deal with it as with the rest +// of the scenery. animations, lights and other submodels which aren't plain triangles are left out of the copies. +// scenery entry: +// node sweep [ ]... piece ... endsweep +// properties: mode , step , +// range (m along the curve, to below 0: up to its end), offset , +// anchor (what of the model the offset puts there: sideways its origin, its side +// facing the curve, the other side or its middle; up its origin, its bottom or its top), +// axis (axis of the model laid along the curve), flip (the model goes the other way), mirror (its sides swapped), +// widen (the offset grows by the widening of the structure gauge in the curves, the way the platforms are set back), +// platform (as widen, and the copies stand on the plane of the rail heads carried on to the edge: lowered on the inner +// side of a canted curve, raised on the outer one), +// noface (the model isn't turned to have its -x side, or -z for one laid along x, facing the curve), +// parameters ... (of a template: the model can be an *.inc file, its long models are bent, the others stand +// upright at their places along the curve), +// scale (of the model, along its own axes), point (a point of the model, in its own space, taken by the anchor point), +// anchor sides: origin, near, far, centre, point; heights: origin, bottom, top, edge (the top of the side facing the curve), point, +// tilt (bent copies lean with the cant of the curve; otherwise they stay upright, as the platforms and the walls by a track do, +// at the height the plane of the cant has at their sideways offset) +class sweep_node : public scene::basic_node +{ + + public: + struct state + { + std::string model; + std::string skin{"none"}; + bool bend{true}; + double step{0.0}; + double from{0.0}; + double to{-1.0}; + double lateral{0.0}; + double height{0.0}; + bool along_x{false}; + bool flip{false}; + bool mirror{false}; + bool tilt{false}; + bool face{true}; // the model is turned so its -x side (-z for a model laid along x) faces the curve + bool widen{false}; // moved away from the curve by the widening of the structure gauge in the curves + bool platform{false}; // set back as widen does, and its height taken from the plane of the rail heads at its edge + std::vector parameters; // of a template (*.inc) laid along the curve + int side_anchor{0}; // 0: origin, 1: the side facing the curve, 2: the far side, 3: the middle, 4: the point + int height_anchor{0}; // 0: origin, 1: bottom, 2: top, 3: top of the side facing the curve, 4: the point + glm::dvec3 point{0.0}; + glm::dvec3 scale{1.0}; + std::vector pieces; + + bool operator==(state const &Other) const; + }; + // point of the curve, with the directions the copies are laid out by + struct frame + { + glm::dvec3 position{0.0}; + glm::dvec3 forward{0.0, 0.0, 1.0}; + glm::dvec3 left{-1.0, 0.0, 0.0}; + glm::dvec3 up{0.0, 1.0, 0.0}; + double roll{0.0}; // degrees, of the curve there + std::array widening{}; // of the structure gauge, below the platforms: right and left side, metres + std::array cant{}; // metres, positive on the inner side of the curve + }; + + explicit sweep_node(scene::node_data const &Nodedata); + + void import(cParser &Input, glm::dvec3 const &Offset); + void define(state const &State); + state const &definition() const + { + return m_state; + } + // length of the curve, in the plan + double length() const; + frame frame_at(double const Station) const; + // station of the point of the curve nearest to the point in the plan, and how far to the right of the curve it is + double project(glm::dvec3 const &Point, double &Lateral) const; + // stretch of the curve the copies take + double start() const; + double end() const; + // length of a copy along the curve, as the model has it + double model_length() const; + bool bounds(glm::dvec3 &Low, glm::dvec3 &High, glm::dvec3 &Shift) const; + bool edge_far() const; + glm::dvec3 axes(glm::dvec3 const &Vector) const; + double distance_at(double const Station) const; + double station_at(double const Distance) const; + double setback(frame const &At) const; + // height the copies stand at over the points of the curve, less the height of the rail: the head of the rail on their side, + // for a platform the plane of the rail heads carried on to its edge, lower on the inner side of a canted curve + double rise(frame const &At) const; + std::vector create_shapes() const; + // a model of what's laid along the curve, placed in its space + struct item + { + TModel3d *model{nullptr}; + material_data skins; + glm::dmat4 transform{1.0}; + }; + std::vector items() const; + // height of the head of the rail on the side of the offset over the points of the track, less the height of the rail + static double rail_head(double const Roll, double const Lateral); + // puts the copies in the scene as shapes of their own, which can be taken back + void show(); + void hide(); + void refresh(); + bool bridge(); + void merged(bool const Merged) + { + m_merged = Merged; + } + bool merged() const + { + return m_merged; + } + + bool m_editorremoved{false}; + + private: + void rebuild_samples(); + float radius_() override; + void serialize_(std::ostream &Output) const override; + void deserialize_(std::istream &Input) override; + void export_as_text_(std::ostream &Output) const override; + + struct sample + { + double station; + glm::dvec3 position; + glm::dvec3 tangent; + double roll; // degrees + std::array widening; + std::array cant; + }; + static void sample_piece(segment_data const &Piece, std::vector &Samples, double &Station); + static void gauge_along(std::vector &Samples); + void beside_switches(std::vector &Samples); + state m_state; + std::vector m_samples; + std::vector m_distances; + mutable bool m_boundsvalid{false}; + mutable bool m_boundsfound{false}; + mutable bool m_edgefar{false}; + mutable glm::dvec3 m_low{0.0}, m_high{0.0}, m_shift{0.0}; + bool m_merged{false}; + owned_shapes m_shapes; +}; + +// collection of the models laid along curves present in the scene +class sweep_table : public basic_table +{ + + public: + // generates geometry of the copies and puts it in the scene + void create_geometry(scene::scratch_data &Scratchpad); +}; diff --git a/world/Track.cpp b/world/Track.cpp index e9c9d87d..f1a29798 100644 --- a/world/Track.cpp +++ b/world/Track.cpp @@ -382,8 +382,7 @@ void TTrack::ConnectNextNext(TTrack *pTrack, int typ) void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) { // pobranie obiektu trajektorii ruchu - glm::dvec3 pt, vec, p1, p2, cp1, cp2, p3, p4, cp3, cp4; // dodatkowe punkty potrzebne do skrzyżowań - double a1, a2, r1, r2, r3, r4; + double a1, a2; std::string str; size_t i; //,state; //Ra: teraz już nie ma początkowego stanu zwrotnicy we wpisie std::string token; @@ -501,8 +500,6 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) else if (Global.iWriteLogEnabled & 4) WriteLog("unvis"); Init(); // ustawia SwitchExtension - double segsize = 5.0; // długość odcinka segmentowania - // path data // all subtypes contain at least one path m_paths.emplace_back(); @@ -521,6 +518,194 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } } + init_segments( true ); + + // optional attributes + parser->getTokens(); + *parser >> token; + str = token; + while (str != "endtrack") + { + if (str == "event0") + { + parser->getTokens(); + *parser >> token; + m_events0.emplace_back( token, nullptr ); + } + else if (str == "event1") + { + parser->getTokens(); + *parser >> token; + m_events1.emplace_back( token, nullptr ); + } + else if (str == "event2") + { + parser->getTokens(); + *parser >> token; + m_events2.emplace_back( token, nullptr ); + } + else if (str == "eventall0") + { + parser->getTokens(); + *parser >> token; + m_events0all.emplace_back( token, nullptr ); + } + else if (str == "eventall1") + { + parser->getTokens(); + *parser >> token; + m_events1all.emplace_back( token, nullptr ); + } + else if (str == "eventall2") + { + parser->getTokens(); + *parser >> token; + m_events2all.emplace_back( token, nullptr ); + } + else if (str == "velocity") + { + parser->getTokens(); + *parser >> fVelocity; //*0.28; McZapkie-010602 + if (SwitchExtension) // jeśli tor ruchomy + if (std::abs(fVelocity) >= 1.0) //żeby zero nie ograniczało dożywotnio + // zapamiętanie głównego ograniczenia; a np. -40 ogranicza tylko na bok + SwitchExtension->fVelocity = static_cast(fVelocity); + } + else if (str == "isolated") + { // obwód izolowany, do którego tor należy + parser->getTokens(); + *parser >> token; + Isolated.push_back(TIsolated::Find(token)); + } + else if (str == "angle1") + { // kąt ścięcia końca od strony 1 + // NOTE: not used/implemented + parser->getTokens(); + *parser >> a1; + //Segment->AngleSet(0, a1); + } + else if (str == "angle2") + { // kąt ścięcia końca od strony 2 + // NOTE: not used/implemented + parser->getTokens(); + *parser >> a2; + //Segment->AngleSet(1, a2); + } + else if (str == "fouling1") + { // wskazanie modelu ukresu w kierunku 1 + // NOTE: not used/implemented + parser->getTokens(); + *parser >> token; + // nFouling[0]= + } + else if (str == "fouling2") + { // wskazanie modelu ukresu w kierunku 2 + // NOTE: not used/implemented + parser->getTokens(); + *parser >> token; + // nFouling[1]= + } + else if (str == "overhead") + { // informacja o stanie sieci: 0-jazda bezprądowa, >0-z opuszczonym i ograniczeniem prędkości + parser->getTokens(); + *parser >> fOverhead; + if (fOverhead > 0.0) + iAction |= 0x40; // flaga opuszczenia pantografu (tor uwzględniany w skanowaniu jako + // ograniczenie dla pantografujących) + } + else if( str == "vradius" ) { + // y-axis track radius + // NOTE: not used/implemented + parser->getTokens(); + *parser >> fVerticalRadius; + } + else if( str == "trackbed" ) { + // switch trackbed texture + auto const trackbedtexture { parser->getToken() }; + if( eType == tt_Switch ) { + SwitchExtension->m_material3 = GfxRenderer->Fetch_Material( trackbedtexture ); + } + } + else if( str == "railprofile" ) { + // rail profile + auto const railprofile { parser->getToken() }; + if( iCategoryFlag == 1 ) { + m_profile1 = fetch_track_rail_profile( railprofile ); + } + } + else if( str == "friction" ) { + // memory cell holding friction value modifiers + m_friction.first = parser->getToken(); + } + else if( str == "sleepermodel" ) { + // sleepermodel + // - frequency: meters between consecutive sleeper instances (must be > 0) + // - model: path to the .e3d sleeper model + // - skin: replacable skin path, or "none" for the model's defaults + // - offset: local-space offset applied per-instance (x=left/right, y=forward/back, z=up/down) + // - ballastZ: vertical shift applied to the auto-generated trackbed (ballast). negative pushes ballast down. + float frequency { 0.f }; + float offsetx { 0.f }, offsety { 0.f }, offsetz { 0.f }; + float ballastz { 0.f }; + parser->getTokens( 1, false ); *parser >> frequency; + auto modelpath { parser->getToken( false ) }; + auto skinpath { parser->getToken( false ) }; + parser->getTokens( 3, false ); *parser >> offsetx >> offsety >> offsetz; + parser->getTokens( 1, false ); *parser >> ballastz; + + if( frequency <= 0.01f ) { + ErrorLog( "Bad track: invalid sleepermodel frequency (" + std::to_string( frequency ) + ") for track \"" + m_name + "\"" ); + } + else { + replace_slashes( modelpath ); + m_sleeper_enabled = true; + m_sleeper_frequency = frequency; + m_sleeper_model_name = modelpath; + m_sleeper_skin_name = skinpath; + m_sleeper_offset = glm::vec3( offsetx, offsety, offsetz ); + m_sleeper_ballast_z = ballastz; + // model and skin are resolved (and instance transforms baked) in build_sleeper_transforms, + // called after segment initialisation so the path geometry is final. + } + } + else + ErrorLog("Bad track: unknown property: \"" + str + "\" defined for track \"" + m_name + "\""); + parser->getTokens(); + *parser >> token; + str = token; + } + // alternatywny zapis nazwy odcinka izolowanego - po znaku "@" w nazwie toru + if ((i = m_name.find("@")) != std::string::npos && i < m_name.length()) + { + Isolated.push_back(TIsolated::Find(m_name.substr(i + 1, m_name.length()))); + m_name = m_name.substr(0, i - 1); // usunięcie z nazwy + } + + update_location(); + // sleeper transforms are baked later in create_geometry(), once the owning cell has + // assigned this track its m_origin (otherwise the local-space matrices would be relative + // to a stale origin and the renderer would draw sleepers in the wrong place). +} + +void TTrack::update_location() { + + location( ( + CurrentSegment()->FastGetPoint_0() + + CurrentSegment()->FastGetPoint( 0.5 ) + + CurrentSegment()->FastGetPoint_1() ) + / 3.0 ); +} + +void TTrack::init_segments( bool const Initial ) { + + glm::dvec3 p1, p2, cp1, cp2, p3, p4, cp3, cp4; // dodatkowe punkty potrzebne do skrzyżowań + double r1, r2, r3, r4; + double segsize = 5.0; // długość odcinka segmentowania + + fTexHeight1 -= fTexHeightOffset; + fTexHeightOffset = 0.f; + iTrapezoid = 0; + switch (eType) { // Ra: łuki segmentowane co 5m albo 314-kątem foremnym case tt_Table: { @@ -773,7 +958,9 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) } } - Switch(0); // na stałe w położeniu 0 - nie ma początkowego stanu zwrotnicy we wpisie + if( Initial ) { + Switch(0); + } // na stałe w położeniu 0 - nie ma początkowego stanu zwrotnicy we wpisie if( eType == tt_Switch ) // Ra: zamienić później na iloczyn wektorowy @@ -796,177 +983,6 @@ void TTrack::Load(cParser *parser, glm::dvec3 const &pOrigin) break; } } - - // optional attributes - parser->getTokens(); - *parser >> token; - str = token; - while (str != "endtrack") - { - if (str == "event0") - { - parser->getTokens(); - *parser >> token; - m_events0.emplace_back( token, nullptr ); - } - else if (str == "event1") - { - parser->getTokens(); - *parser >> token; - m_events1.emplace_back( token, nullptr ); - } - else if (str == "event2") - { - parser->getTokens(); - *parser >> token; - m_events2.emplace_back( token, nullptr ); - } - else if (str == "eventall0") - { - parser->getTokens(); - *parser >> token; - m_events0all.emplace_back( token, nullptr ); - } - else if (str == "eventall1") - { - parser->getTokens(); - *parser >> token; - m_events1all.emplace_back( token, nullptr ); - } - else if (str == "eventall2") - { - parser->getTokens(); - *parser >> token; - m_events2all.emplace_back( token, nullptr ); - } - else if (str == "velocity") - { - parser->getTokens(); - *parser >> fVelocity; //*0.28; McZapkie-010602 - if (SwitchExtension) // jeśli tor ruchomy - if (std::abs(fVelocity) >= 1.0) //żeby zero nie ograniczało dożywotnio - // zapamiętanie głównego ograniczenia; a np. -40 ogranicza tylko na bok - SwitchExtension->fVelocity = static_cast(fVelocity); - } - else if (str == "isolated") - { // obwód izolowany, do którego tor należy - parser->getTokens(); - *parser >> token; - Isolated.push_back(TIsolated::Find(token)); - } - else if (str == "angle1") - { // kąt ścięcia końca od strony 1 - // NOTE: not used/implemented - parser->getTokens(); - *parser >> a1; - //Segment->AngleSet(0, a1); - } - else if (str == "angle2") - { // kąt ścięcia końca od strony 2 - // NOTE: not used/implemented - parser->getTokens(); - *parser >> a2; - //Segment->AngleSet(1, a2); - } - else if (str == "fouling1") - { // wskazanie modelu ukresu w kierunku 1 - // NOTE: not used/implemented - parser->getTokens(); - *parser >> token; - // nFouling[0]= - } - else if (str == "fouling2") - { // wskazanie modelu ukresu w kierunku 2 - // NOTE: not used/implemented - parser->getTokens(); - *parser >> token; - // nFouling[1]= - } - else if (str == "overhead") - { // informacja o stanie sieci: 0-jazda bezprądowa, >0-z opuszczonym i ograniczeniem prędkości - parser->getTokens(); - *parser >> fOverhead; - if (fOverhead > 0.0) - iAction |= 0x40; // flaga opuszczenia pantografu (tor uwzględniany w skanowaniu jako - // ograniczenie dla pantografujących) - } - else if( str == "vradius" ) { - // y-axis track radius - // NOTE: not used/implemented - parser->getTokens(); - *parser >> fVerticalRadius; - } - else if( str == "trackbed" ) { - // switch trackbed texture - auto const trackbedtexture { parser->getToken() }; - if( eType == tt_Switch ) { - SwitchExtension->m_material3 = GfxRenderer->Fetch_Material( trackbedtexture ); - } - } - else if( str == "railprofile" ) { - // rail profile - auto const railprofile { parser->getToken() }; - if( iCategoryFlag == 1 ) { - m_profile1 = fetch_track_rail_profile( railprofile ); - } - } - else if( str == "friction" ) { - // memory cell holding friction value modifiers - m_friction.first = parser->getToken(); - } - else if( str == "sleepermodel" ) { - // sleepermodel - // - frequency: meters between consecutive sleeper instances (must be > 0) - // - model: path to the .e3d sleeper model - // - skin: replacable skin path, or "none" for the model's defaults - // - offset: local-space offset applied per-instance (x=left/right, y=forward/back, z=up/down) - // - ballastZ: vertical shift applied to the auto-generated trackbed (ballast). negative pushes ballast down. - float frequency { 0.f }; - float offsetx { 0.f }, offsety { 0.f }, offsetz { 0.f }; - float ballastz { 0.f }; - parser->getTokens( 1, false ); *parser >> frequency; - auto modelpath { parser->getToken( false ) }; - auto skinpath { parser->getToken( false ) }; - parser->getTokens( 3, false ); *parser >> offsetx >> offsety >> offsetz; - parser->getTokens( 1, false ); *parser >> ballastz; - - if( frequency <= 0.01f ) { - ErrorLog( "Bad track: invalid sleepermodel frequency (" + std::to_string( frequency ) + ") for track \"" + m_name + "\"" ); - } - else { - replace_slashes( modelpath ); - m_sleeper_enabled = true; - m_sleeper_frequency = frequency; - m_sleeper_model_name = modelpath; - m_sleeper_skin_name = skinpath; - m_sleeper_offset = glm::vec3( offsetx, offsety, offsetz ); - m_sleeper_ballast_z = ballastz; - // model and skin are resolved (and instance transforms baked) in build_sleeper_transforms, - // called after segment initialisation so the path geometry is final. - } - } - else - ErrorLog("Bad track: unknown property: \"" + str + "\" defined for track \"" + m_name + "\""); - parser->getTokens(); - *parser >> token; - str = token; - } - // alternatywny zapis nazwy odcinka izolowanego - po znaku "@" w nazwie toru - if ((i = m_name.find("@")) != std::string::npos && i < m_name.length()) - { - Isolated.push_back(TIsolated::Find(m_name.substr(i + 1, m_name.length()))); - m_name = m_name.substr(0, i - 1); // usunięcie z nazwy - } - - // calculate path location - location( ( - CurrentSegment()->FastGetPoint_0() - + CurrentSegment()->FastGetPoint( 0.5 ) - + CurrentSegment()->FastGetPoint_1() ) - / 3.0 ); - // sleeper transforms are baked later in create_geometry(), once the owning cell has - // assigned this track its m_origin (otherwise the local-space matrices would be relative - // to a stale origin and the renderer would draw sleepers in the wrong place). } bool TTrack::AssignEvents() { @@ -1333,6 +1349,20 @@ glm::vec3 TTrack::get_nearest_point(const glm::dvec3 &point) const } // wypełnianie tablic VBO +void TTrack::rebuild_geometry( gfx::geometrybank_handle const &Bank ) { + + Geometry1.clear(); + Geometry2.clear(); + if( SwitchExtension ) { + SwitchExtension->Geometry3 = gfx::geometry_handle{}; + } + m_sleeper_local_transforms.clear(); + + if( Bank == null_handle ) { return; } + + create_geometry( Bank ); +} + void TTrack::create_geometry( gfx::geometrybank_handle const &Bank ) { gfx::userdata_array empty_userdata; // bake per-instance sleeper transforms now that the owning cell has assigned m_origin. @@ -1912,6 +1942,29 @@ bool TTrack::SwitchForced(int i, TDynamicObject *o) return true; }; +// picks one of the routes through a crossroads for a driver heading for it +int TTrack::RouteDraw() const { + + auto const count { std::min( RouteCount(), static_cast( m_routeweights.size() ) ) }; + auto total { 0.0 }; + for( int route = 0; route < count; ++route ) { + total += std::max( 0.f, m_routeweights[ route ] ); + } + if( total <= 0.0 ) { + // even odds, the way it's always been for the crossroads of the scenery + return 1 + static_cast( std::floor( Random( static_cast( RouteCount() ) - 0.001 ) ) ); + } + auto draw { Random( total ) }; + auto last { 1 }; + for( int route = 0; route < count; ++route ) { + if( m_routeweights[ route ] <= 0.f ) { continue; } + last = route + 1; + draw -= m_routeweights[ route ]; + if( draw < 0.0 ) { break; } + } + return last; +} + int TTrack::CrossSegment(int from, int into) { // ustawienie wskaźnika na segement w pożądanym kierunku (into) od strony (from) // zwraca kod segmentu, z kierunkiem jazdy jako znakiem ± @@ -2319,6 +2372,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { << fTexSlope << ' '; } // path data + auto const precision { Output.precision( std::numeric_limits::digits10 ) }; for( auto const &path : m_paths ) { Output << path.points[ segment_data::point::start ].x << ' ' @@ -2341,6 +2395,7 @@ TTrack::export_as_text_( std::ostream &Output ) const { << path.radius << ' '; } + Output.precision( precision ); // optional attributes std::vector< std::pair< std::string, event_sequence const * > > const eventsequences { { "event0", &m_events0 }, { "eventall0", &m_events0all }, @@ -2379,13 +2434,17 @@ TTrack::export_as_text_( std::ostream &Output ) const { } if( eType == tt_Switch && SwitchExtension->m_material3 != null_handle) { - auto texturefile { GfxRenderer->Material( m_material2 )->GetName() }; + auto texturefile { GfxRenderer->Material( SwitchExtension->m_material3 )->GetName() }; if( texturefile.find( paths::textures ) == 0 ) { // don't include 'textures/' in the path texturefile.erase( 0, std::string{ paths::textures }.size() ); } Output << "trackbed " << texturefile << ' '; } + if( iCategoryFlag == 1 + && false == m_profile1.first.empty() ) { + Output << "railprofile " << m_profile1.first << ' '; + } if( false == m_friction.first.empty() ) { Output << "friction " << m_friction.first << ' '; } diff --git a/world/Track.h b/world/Track.h index 9bf38e0a..72a6b7ff 100644 --- a/world/Track.h +++ b/world/Track.h @@ -12,6 +12,7 @@ http://mozilla.org/MPL/2.0/. #include #include #include +#include #include #include @@ -154,6 +155,7 @@ class TTrack : public scene::basic_node friend opengl33_renderer; // NOTE: temporary arrangement friend itemproperties_panel; + friend class editor_track; public: std::vector Isolated; // obwód izolowany obsługujący zajęcia/zwolnienia grupy torów @@ -184,6 +186,9 @@ public: geometryhandle_sequence Geometry2; // geometry chunks textured with texture 2 std::vector m_paths; // source data for owned paths + bool m_editorremoved { false }; + scene::basic_node *m_road { nullptr }; // road or road junction which generated this path. such path is one-way, and isn't saved nor edited on its own + std::array m_routeweights { 0.f, 0.f, 0.f }; // odds of each route through a crossroads being picked by a driver, by the number of the route. all zero: even odds int iterate_stamp = 0; // sleepermodel optional parameter ------------------------------------------------- @@ -277,7 +282,11 @@ public: int RouteCount() const { return SwitchExtension != nullptr ? SwitchExtension->iRoads - 1 : 1; } + // picks one of the routes through a crossroads for a driver heading for it. the routes are numbered from 1 + int RouteDraw() const; void Load(cParser *parser, glm::dvec3 const &pOrigin); + void init_segments( bool const Initial ); + void update_location(); bool AssignEvents(); bool AssignForcedEvents(basic_event *NewEventPlus, basic_event *NewEventMinus); void QueueEvents( event_sequence const &Events, TDynamicObject const *Owner ); @@ -300,6 +309,7 @@ public: double ActiveLength(); void create_geometry( gfx::geometrybank_handle const &Bank ); // wypełnianie VBO + void rebuild_geometry( gfx::geometrybank_handle const &Bank ); void create_map_geometry(std::vector &Bank, const gfx::geometrybank_handle Extra); void get_map_active_paths(map_colored_paths &handles); void get_map_paths_for_state(map_colored_paths &handles, int state); diff --git a/world/TrkFoll.cpp b/world/TrkFoll.cpp index b1884896..c437553d 100644 --- a/world/TrkFoll.cpp +++ b/world/TrkFoll.cpp @@ -51,6 +51,9 @@ TTrack * TTrackFollower::SetCurrentTrack(TTrack *pTrack, int end) { case tt_Switch: // jeśli zwrotnica, to przekładamy ją, aby uzyskać dobry segment { + if( pCurrentTrack == nullptr ) { + break; + } int i = end ? pCurrentTrack->iNextDirection : pCurrentTrack->iPrevDirection; if (i > 0) // jeżeli wjazd z ostrza pTrack->SwitchForced(i >> 1, Owner); // to przełożenie zwrotnicy - rozprucie!